Skip to content

Commit 8c3d951

Browse files
authored
Add Swift module support to ZipBuilder (#5040)
1 parent 9ecb963 commit 8c3d951

File tree

1 file changed

+91
-23
lines changed

1 file changed

+91
-23
lines changed

ZipBuilder/Sources/ZipBuilder/FrameworkBuilder.swift

Lines changed: 91 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,12 @@ struct FrameworkBuilder {
221221
"ARCHS=\(cleanArch)",
222222
"VALID_ARCHS=\(cleanArch)",
223223
"ONLY_ACTIVE_ARCH=NO",
224-
"BUILD_LIBRARIES_FOR_DISTRIBUTION=YES",
224+
// BUILD_LIBRARY_FOR_DISTRIBUTION=YES is necessary for Swift libraries.
225+
// See https://forums.developer.apple.com/thread/125646.
226+
// Unlike the comment there, the option here is sufficient to cause .swiftinterface
227+
// files to be generated in the .swiftmodule directory. The .swiftinterface files
228+
// are required for xcodebuild to successfully generate an xcframework.
229+
"BUILD_LIBRARY_FOR_DISTRIBUTION=YES",
225230
"SUPPORTS_MACCATALYST=\(isMacCatalystString)",
226231
"BUILD_DIR=\(buildDir.path)",
227232
"-sdk", platform.rawValue,
@@ -267,7 +272,7 @@ struct FrameworkBuilder {
267272
var actualFramework: String
268273
do {
269274
let files = try FileManager.default.contentsOfDirectory(at: frameworkPath,
270-
includingPropertiesForKeys: nil).compactMap { $0.absoluteString }
275+
includingPropertiesForKeys: nil).compactMap { $0.path }
271276
let frameworkDir = files.filter { $0.contains(".framework") }
272277
actualFramework = URL(fileURLWithPath: frameworkDir[0]).lastPathComponent
273278
} catch {
@@ -427,21 +432,21 @@ struct FrameworkBuilder {
427432

428433
// Find CocoaPods generated umbrella header.
429434
var umbrellaHeader = ""
430-
if framework == "gRPC-Core" {
435+
if framework == "gRPC-Core" || framework == "TensorFlowLiteObjC" {
431436
// TODO: Proper handling of podspec-specified module.modulemap files with customized umbrella
432437
// headers. This is good enough for Firebase since it doesn't need these modules.
433438
umbrellaHeader = "\(framework)-umbrella.h"
434439
} else {
435440
var umbrellaHeaderURL: URL
436441
do {
437442
let files = try fileManager.contentsOfDirectory(at: headersDir,
438-
includingPropertiesForKeys: nil).compactMap { $0.absoluteString }
439-
let umbrellas = files.filter { $0.contains("umbrella.h") }
443+
includingPropertiesForKeys: nil).compactMap { $0.path }
444+
let umbrellas = files.filter { $0.hasSuffix("umbrella.h") }
440445
if umbrellas.count != 1 {
441446
fatalError("Did not find exactly one umbrella header in \(headersDir).")
442447
}
443448
guard let firstUmbrella = umbrellas.first,
444-
let foundHeader = URL(string: firstUmbrella) else { /* error */
449+
let foundHeader = URL(string: firstUmbrella) else {
445450
fatalError("Failed to get umbrella header in \(headersDir).")
446451
}
447452
umbrellaHeaderURL = foundHeader
@@ -527,10 +532,6 @@ struct FrameworkBuilder {
527532
thinArchives: [Architecture: URL],
528533
destination: URL,
529534
moduleMapContents: String) {
530-
// Build the fat archive using the `lipo` command. We need the full archive path and the list of
531-
// thin paths (as Strings, not URLs).
532-
let thinPaths = thinArchives.map { $0.value.path }
533-
534535
// Store all fat archives in a temporary directory that includes all architectures included as
535536
// the parent folder.
536537
let fatArchivesDir: URL = {
@@ -552,8 +553,10 @@ struct FrameworkBuilder {
552553
fatalError("Could not create directories needed to build \(framework): \(error)")
553554
}
554555

556+
// Build the fat archive using the `lipo` command. We need the full archive path.
555557
let fatArchive = fatArchivesDir.appendingPathComponent(framework)
556-
let result = syncExec(command: "/usr/bin/lipo", args: ["-create", "-output", fatArchive.path] + thinPaths)
558+
let result = syncExec(command: "/usr/bin/lipo", args: ["-create", "-output", fatArchive.path] +
559+
thinArchives.map { $0.value.path })
557560
switch result {
558561
case let .error(code, output):
559562
fatalError("""
@@ -571,19 +574,84 @@ struct FrameworkBuilder {
571574
} catch {
572575
fatalError("Could not copy \(framework) to destination: \(error)")
573576
}
574-
// Copy the module map to the destination.
575-
let moduleDir = destination.appendingPathComponent("Modules")
576-
do {
577-
try FileManager.default.createDirectory(at: moduleDir, withIntermediateDirectories: true)
578-
} catch {
579-
fatalError("Could not create Modules directory for framework: \(framework). \(error)")
577+
578+
// CocoaPods does not put dependent frameworks and libraries into the module maps it generates.
579+
// Instead it use build options to specify them. For the zip build, we need the module maps to
580+
// include the dependent frameworks and libraries. Therefore we reconstruct them by parsing
581+
// the CocoaPods config files and add them here.
582+
// Currently we only to the construction for Objective C since Swift Module directories require
583+
// several other files. See https://github.com/firebase/firebase-ios-sdk/pull/5040.
584+
// Therefore, for Swift we do a simple copy of the Modules files from an Xcode build.
585+
// This is sufficient for the testing done so far, but more testing is required to determine
586+
// if dependent libraries and frameworks also may need to be added to the Swift module maps in
587+
// some cases.
588+
let builtSwiftModules = makeSwiftModuleMap(thinArchives: thinArchives, destination: destination)
589+
if !builtSwiftModules {
590+
// Copy the module map to the destination.
591+
let moduleDir = destination.appendingPathComponent("Modules")
592+
do {
593+
try FileManager.default.createDirectory(at: moduleDir, withIntermediateDirectories: true)
594+
} catch {
595+
fatalError("Could not create Modules directory for framework: \(framework). \(error)")
596+
}
597+
let modulemap = moduleDir.appendingPathComponent("module.modulemap")
598+
do {
599+
try moduleMapContents.write(to: modulemap, atomically: true, encoding: .utf8)
600+
} catch {
601+
fatalError("Could not write modulemap to disk for \(framework): \(error)")
602+
}
580603
}
581-
let modulemap = moduleDir.appendingPathComponent("module.modulemap")
582-
do {
583-
try moduleMapContents.write(to: modulemap, atomically: true, encoding: .utf8)
584-
} catch {
585-
fatalError("Could not write modulemap to disk for \(framework): \(error)")
604+
}
605+
606+
private func makeSwiftModuleMap(thinArchives: [Architecture: URL], destination: URL) -> Bool {
607+
let fileManager = FileManager.default
608+
for archive in thinArchives {
609+
let frameworkDir = archive.value.deletingLastPathComponent()
610+
// Get the Modules directory. The Catalyst one is a symbolic link.
611+
let moduleDir = frameworkDir.appendingPathComponent("Modules").resolvingSymlinksInPath()
612+
do {
613+
let files = try fileManager.contentsOfDirectory(at: moduleDir,
614+
includingPropertiesForKeys: nil).compactMap { $0.path }
615+
let swiftModules = files.filter { $0.hasSuffix(".swiftmodule") }
616+
if swiftModules.isEmpty {
617+
return false
618+
}
619+
guard let first = swiftModules.first,
620+
let swiftModule = URL(string: first) else {
621+
fatalError("Failed to get swiftmodule in \(moduleDir).")
622+
}
623+
let destModuleDir = destination.appendingPathComponent("Modules")
624+
if !fileManager.directoryExists(at: destModuleDir) {
625+
do {
626+
try fileManager.copyItem(at: moduleDir, to: destModuleDir)
627+
} catch {
628+
fatalError("Could not copy Modules from \(moduleDir) to " + "\(destModuleDir): \(error)")
629+
}
630+
} else {
631+
// If the Modules directory is already there, only copy in the architecture specific files
632+
// from the *.swiftmodule subdirectory.
633+
do {
634+
let files = try fileManager.contentsOfDirectory(at: swiftModule,
635+
includingPropertiesForKeys: nil).compactMap { $0.path }
636+
let destSwiftModuleDir = destModuleDir.appendingPathComponent(swiftModule.lastPathComponent)
637+
for file in files {
638+
let fileURL = URL(fileURLWithPath: file)
639+
do {
640+
try fileManager.copyItem(at: fileURL, to:
641+
destSwiftModuleDir.appendingPathComponent(fileURL.lastPathComponent))
642+
} catch {
643+
fatalError("Could not copy Swift module file from \(fileURL) to " + "\(destSwiftModuleDir): \(error)")
644+
}
645+
}
646+
} catch {
647+
fatalError("Failed to get Modules directory contents - \(moduleDir): \(error.localizedDescription)")
648+
}
649+
}
650+
} catch {
651+
fatalError("Error while enumerating files \(moduleDir): \(error.localizedDescription)")
652+
}
586653
}
654+
return true
587655
}
588656

589657
/// Packages an XCFramework based on an almost complete framework folder (missing the binary but includes everything else needed)
@@ -619,7 +687,7 @@ struct FrameworkBuilder {
619687
for platform in Architecture.TargetPlatform.allCases {
620688
// Get all the slices that belong to the specific platform in order to lipo them together.
621689
let slices = thinArchives.filter { $0.key.platform == platform }
622-
if slices.count == 0 {
690+
if slices.isEmpty {
623691
continue
624692
}
625693
let platformDir = platformFrameworksDir.appendingPathComponent(platform.rawValue)

0 commit comments

Comments
 (0)