FrameworkBuilder.swift 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. /*
  2. * Copyright 2019 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import Foundation
  17. import Utils
  18. /// A structure to build a .framework in a given project directory.
  19. struct FrameworkBuilder {
  20. /// Platforms to be included in the built frameworks.
  21. private let targetPlatforms: [TargetPlatform]
  22. /// The directory containing the Xcode project and Pods folder.
  23. private let projectDir: URL
  24. /// Flag for building dynamic frameworks instead of static frameworks.
  25. private let dynamicFrameworks: Bool
  26. /// The Pods directory for building the framework.
  27. private var podsDir: URL {
  28. return projectDir.appendingPathComponent("Pods", isDirectory: true)
  29. }
  30. /// Default initializer.
  31. init(projectDir: URL, targetPlatforms: [TargetPlatform], dynamicFrameworks: Bool) {
  32. self.projectDir = projectDir
  33. self.targetPlatforms = targetPlatforms
  34. self.dynamicFrameworks = dynamicFrameworks
  35. }
  36. // MARK: - Public Functions
  37. /// Compiles the specified framework in a temporary directory and writes the build logs to file.
  38. /// This will compile all architectures for a single platform at a time.
  39. ///
  40. /// - Parameter framework: The name of the framework to be built.
  41. /// - Parameter logsOutputDir: The path to the directory to place build logs.
  42. /// - Parameter setCarthage: Set Carthage diagnostics flag in build.
  43. /// - Parameter moduleMapContents: Module map contents for all frameworks in this pod.
  44. /// - Returns: A path to the newly compiled frameworks, and Resources.
  45. func compileFrameworkAndResources(withName framework: String,
  46. logsOutputDir: URL? = nil,
  47. setCarthage: Bool,
  48. podInfo: CocoaPodUtils.PodInfo) -> ([URL], URL?) {
  49. let fileManager = FileManager.default
  50. let outputDir = fileManager.temporaryDirectory(withName: "frameworks_being_built")
  51. let logsDir = logsOutputDir ?? fileManager.temporaryDirectory(withName: "build_logs")
  52. do {
  53. // Remove the compiled frameworks directory, this isn't the cache we're using.
  54. if fileManager.directoryExists(at: outputDir) {
  55. try fileManager.removeItem(at: outputDir)
  56. }
  57. try fileManager.createDirectory(at: outputDir, withIntermediateDirectories: true)
  58. // Create our logs directory if it doesn't exist.
  59. if !fileManager.directoryExists(at: logsDir) {
  60. try fileManager.createDirectory(at: logsDir, withIntermediateDirectories: true)
  61. }
  62. } catch {
  63. fatalError("Failure creating temporary directory while building \(framework): \(error)")
  64. }
  65. if dynamicFrameworks {
  66. return (buildDynamicFrameworks(withName: framework, logsDir: logsDir, outputDir: outputDir),
  67. nil)
  68. } else {
  69. return buildStaticFrameworks(
  70. withName: framework,
  71. logsDir: logsDir,
  72. outputDir: outputDir,
  73. setCarthage: setCarthage,
  74. podInfo: podInfo
  75. )
  76. }
  77. }
  78. // MARK: - Private Helpers
  79. /// This runs a command and immediately returns a Shell result.
  80. /// NOTE: This exists in conjunction with the `Shell.execute...` due to issues with different
  81. /// `.bash_profile` environment variables. This should be consolidated in the future.
  82. private static func syncExec(command: String,
  83. args: [String] = [],
  84. captureOutput: Bool = false) -> Shell
  85. .Result {
  86. let task = Process()
  87. task.launchPath = command
  88. task.arguments = args
  89. // If we want to output to the console, create a readabilityHandler and save each line along the
  90. // way. Otherwise, we can just read the pipe at the end. By disabling outputToConsole, some
  91. // commands (such as any xcodebuild) can run much, much faster.
  92. var output = ""
  93. if captureOutput {
  94. let pipe = Pipe()
  95. task.standardOutput = pipe
  96. let outHandle = pipe.fileHandleForReading
  97. outHandle.readabilityHandler = { pipe in
  98. // This will be run any time data is sent to the pipe. We want to print it and store it for
  99. // later. Ignore any non-valid Strings.
  100. guard let line = String(data: pipe.availableData, encoding: .utf8) else {
  101. print("Could not get data from pipe for command \(command): \(pipe.availableData)")
  102. return
  103. }
  104. if !line.isEmpty {
  105. output += line
  106. }
  107. }
  108. // Also set the termination handler on the task in order to stop the readabilityHandler from
  109. // parsing any more data from the task.
  110. task.terminationHandler = { t in
  111. guard let stdOut = t.standardOutput as? Pipe else { return }
  112. stdOut.fileHandleForReading.readabilityHandler = nil
  113. }
  114. } else {
  115. // No capturing output, just mark it as complete.
  116. output = "The task completed"
  117. }
  118. task.launch()
  119. task.waitUntilExit()
  120. // Normally we'd use a pipe to retrieve the output, but for whatever reason it slows things down
  121. // tremendously for xcodebuild.
  122. guard task.terminationStatus == 0 else {
  123. return .error(code: task.terminationStatus, output: output)
  124. }
  125. return .success(output: output)
  126. }
  127. /// Build all thin slices for an open source pod.
  128. /// - Parameter framework: The name of the framework to be built.
  129. /// - Parameter logsDir: The path to the directory to place build logs.
  130. /// - Parameter setCarthage: Set Carthage flag in CoreDiagnostics for metrics.
  131. /// - Returns: A dictionary of URLs to the built thin libraries keyed by platform.
  132. private func buildFrameworksForAllPlatforms(withName framework: String,
  133. logsDir: URL,
  134. setCarthage: Bool) -> [TargetPlatform: URL] {
  135. // Build every architecture and save the locations in an array to be assembled.
  136. var slicedFrameworks = [TargetPlatform: URL]()
  137. for targetPlatform in targetPlatforms {
  138. let buildDir = projectDir.appendingPathComponent(targetPlatform.buildName)
  139. let sliced = buildSlicedFramework(withName: framework,
  140. targetPlatform: targetPlatform,
  141. buildDir: buildDir,
  142. logRoot: logsDir,
  143. setCarthage: setCarthage)
  144. slicedFrameworks[targetPlatform] = sliced
  145. }
  146. return slicedFrameworks
  147. }
  148. /// Uses `xcodebuild` to build a framework for a specific target platform.
  149. ///
  150. /// - Parameters:
  151. /// - framework: Name of the framework being built.
  152. /// - targetPlatform: The target platform to target for the build.
  153. /// - buildDir: Location where the project should be built.
  154. /// - logRoot: Root directory where all logs should be written.
  155. /// - setCarthage: Set Carthage flag in CoreDiagnostics for metrics.
  156. /// - Returns: A URL to the framework that was built.
  157. private func buildSlicedFramework(withName framework: String,
  158. targetPlatform: TargetPlatform,
  159. buildDir: URL,
  160. logRoot: URL,
  161. setCarthage: Bool = false) -> URL {
  162. let isMacCatalyst = targetPlatform == .catalyst
  163. let isMacCatalystString = isMacCatalyst ? "YES" : "NO"
  164. let workspacePath = projectDir.appendingPathComponent("FrameworkMaker.xcworkspace").path
  165. let distributionFlag = setCarthage ? "-DFIREBASE_BUILD_CARTHAGE" :
  166. "-DFIREBASE_BUILD_ZIP_FILE"
  167. let cFlags = "OTHER_CFLAGS=$(value) \(distributionFlag)"
  168. var archs = targetPlatform.archs.map { $0.rawValue }.joined(separator: " ")
  169. // The 32 bit archs do not build for iOS 11.
  170. if framework == "FirebaseAppCheck" || framework.hasSuffix("Swift") {
  171. if targetPlatform == .iOSDevice {
  172. archs = "arm64"
  173. } else if targetPlatform == .iOSSimulator {
  174. archs = "x86_64 arm64"
  175. }
  176. }
  177. var args = ["build",
  178. "-configuration", "release",
  179. "-workspace", workspacePath,
  180. "-scheme", framework,
  181. "GCC_GENERATE_DEBUGGING_SYMBOLS=NO",
  182. "ARCHS=\(archs)",
  183. "VALID_ARCHS=\(archs)",
  184. "ONLY_ACTIVE_ARCH=NO",
  185. // BUILD_LIBRARY_FOR_DISTRIBUTION=YES is necessary for Swift libraries.
  186. // See https://forums.developer.apple.com/thread/125646.
  187. // Unlike the comment there, the option here is sufficient to cause .swiftinterface
  188. // files to be generated in the .swiftmodule directory. The .swiftinterface files
  189. // are required for xcodebuild to successfully generate an xcframework.
  190. "BUILD_LIBRARY_FOR_DISTRIBUTION=YES",
  191. "SUPPORTS_MACCATALYST=\(isMacCatalystString)",
  192. "BUILD_DIR=\(buildDir.path)",
  193. "-sdk", targetPlatform.sdkName,
  194. cFlags]
  195. // Add bitcode option for platforms that need it.
  196. if targetPlatform.shouldEnableBitcode {
  197. args.append("BITCODE_GENERATION_MODE=bitcode")
  198. }
  199. // Code signing isn't needed for libraries. Disabling signing is required for
  200. // Catalyst libs with resources. See
  201. // https://github.com/CocoaPods/CocoaPods/issues/8891#issuecomment-573301570
  202. if isMacCatalyst {
  203. args.append("CODE_SIGN_IDENTITY=-")
  204. }
  205. print("""
  206. Compiling \(framework) for \(targetPlatform.buildName) (\(archs)) with command:
  207. /usr/bin/xcodebuild \(args.joined(separator: " "))
  208. """)
  209. // Regardless if it succeeds or not, we want to write the log to file in case we need to inspect
  210. // things further.
  211. let logFileName = "\(framework)-\(targetPlatform.buildName).txt"
  212. let logFile = logRoot.appendingPathComponent(logFileName)
  213. let result = FrameworkBuilder.syncExec(command: "/usr/bin/xcodebuild",
  214. args: args,
  215. captureOutput: true)
  216. switch result {
  217. case let .error(code, output):
  218. // Write output to disk and print the location of it. Force unwrapping here since it's going
  219. // to crash anyways, and at this point the root log directory exists, we know it's UTF8, so it
  220. // should pass every time. Revisit if that's not the case.
  221. try! output.write(to: logFile, atomically: true, encoding: .utf8)
  222. fatalError("Error building \(framework) for \(targetPlatform.buildName). Code: \(code). See " +
  223. "the build log at \(logFile)")
  224. case let .success(output):
  225. // Try to write the output to the log file but if it fails it's not a huge deal since it was
  226. // a successful build.
  227. try? output.write(to: logFile, atomically: true, encoding: .utf8)
  228. print("""
  229. Successfully built \(framework) for \(targetPlatform.buildName). Build log is at \(logFile).
  230. """)
  231. // Use the Xcode-generated path to return the path to the compiled library.
  232. // The framework name may be different from the pod name if the module is reset in the
  233. // podspec - like Release-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework.
  234. print("buildDir: \(buildDir)")
  235. let frameworkPath = buildDir.appendingPathComponents([targetPlatform.buildDirName, framework])
  236. var actualFramework: String
  237. do {
  238. let files = try FileManager.default.contentsOfDirectory(at: frameworkPath,
  239. includingPropertiesForKeys: nil)
  240. .compactMap { $0.path }
  241. let frameworkDir = files.filter { $0.contains(".framework") }
  242. actualFramework = URL(fileURLWithPath: frameworkDir[0]).lastPathComponent
  243. } catch {
  244. fatalError("Error while enumerating files \(frameworkPath): \(error.localizedDescription)")
  245. }
  246. let libPath = frameworkPath.appendingPathComponent(actualFramework)
  247. print("buildSliced returns \(libPath)")
  248. return libPath
  249. }
  250. }
  251. // TODO: Automatically get the right name.
  252. /// The dynamic framework name is different from the pod name when the module_name
  253. /// specifier is used in the podspec.
  254. ///
  255. /// - Parameter framework: The name of the framework to be built.
  256. /// - Returns: The corresponding dynamic framework name.
  257. private func frameworkBuildName(_ framework: String) -> String {
  258. if !dynamicFrameworks {
  259. return framework
  260. }
  261. switch framework {
  262. case "PromisesObjC":
  263. return "FBLPromises"
  264. case "Protobuf":
  265. return "protobuf"
  266. default:
  267. return framework
  268. }
  269. }
  270. /// Compiles the specified framework in a temporary directory and writes the build logs to file.
  271. /// This will compile all architectures and use the -create-xcframework command to create a modern
  272. /// "fat" framework.
  273. ///
  274. /// - Parameter framework: The name of the framework to be built.
  275. /// - Parameter logsDir: The path to the directory to place build logs.
  276. /// - Returns: A path to the newly compiled frameworks (with any included Resources embedded).
  277. private func buildDynamicFrameworks(withName framework: String,
  278. logsDir: URL,
  279. outputDir: URL) -> [URL] {
  280. // xcframework doesn't lipo things together but accepts fat frameworks for one target.
  281. // We group architectures here to deal with this fact.
  282. var thinFrameworks = [URL]()
  283. for targetPlatform in TargetPlatform.allCases {
  284. let buildDir = projectDir.appendingPathComponent(targetPlatform.buildName)
  285. let slicedFramework = buildSlicedFramework(withName: framework,
  286. targetPlatform: targetPlatform,
  287. buildDir: buildDir,
  288. logRoot: logsDir)
  289. thinFrameworks.append(slicedFramework)
  290. }
  291. return thinFrameworks
  292. }
  293. /// Compiles the specified framework in a temporary directory and writes the build logs to file.
  294. /// This will compile all architectures and use the -create-xcframework command to create a modern
  295. /// "fat" framework.
  296. ///
  297. /// - Parameter framework: The name of the framework to be built.
  298. /// - Parameter logsDir: The path to the directory to place build logs.
  299. /// - Parameter moduleMapContents: Module map contents for all frameworks in this pod.
  300. /// - Returns: A path to the newly compiled framework, and the Resource URL.
  301. private func buildStaticFrameworks(withName framework: String,
  302. logsDir: URL,
  303. outputDir: URL,
  304. setCarthage: Bool,
  305. podInfo: CocoaPodUtils.PodInfo) -> ([URL], URL) {
  306. // Build every architecture and save the locations in an array to be assembled.
  307. let slicedFrameworks = buildFrameworksForAllPlatforms(withName: framework, logsDir: logsDir,
  308. setCarthage: setCarthage)
  309. // Create the framework directory in the filesystem for the thin archives to go.
  310. let fileManager = FileManager.default
  311. let frameworkDir = outputDir.appendingPathComponent("\(framework).framework")
  312. do {
  313. try fileManager.createDirectory(at: frameworkDir, withIntermediateDirectories: true)
  314. } catch {
  315. fatalError("Could not create framework directory while building framework \(framework). " +
  316. "\(error)")
  317. }
  318. // Find the location of the public headers, any platform will do.
  319. guard let anyPlatform = targetPlatforms.first,
  320. let archivePath = slicedFrameworks[anyPlatform] else {
  321. fatalError("Could not get a path to an archive to fetch headers in \(framework).")
  322. }
  323. // Get the framework Headers directory. On macOS, it's a symbolic link.
  324. let headersDir = archivePath.appendingPathComponent("Headers").resolvingSymlinksInPath()
  325. // The macOS Headers directory can have a Headers file in it symbolically linked to nowhere.
  326. // Delete it here to avoid putting it in the zip or crashing the Carthage hash generation.
  327. // For example,in the 8.0.0 zip distribution see
  328. // Firebase/FirebaseAnalytics/PromisesObjC.xcframework/macos-arm64_x86_64/PromisesObjc
  329. // .framework/Headers/Headers
  330. do {
  331. try fileManager.removeItem(at: headersDir.appendingPathComponent("Headers"))
  332. } catch {
  333. // Ignore
  334. }
  335. // Find CocoaPods generated umbrella header.
  336. var umbrellaHeader = ""
  337. if framework == "gRPC-Core" || framework == "TensorFlowLiteObjC" {
  338. // TODO: Proper handling of podspec-specified module.modulemap files with customized umbrella
  339. // headers. This is good enough for Firebase since it doesn't need these modules.
  340. umbrellaHeader = "\(framework)-umbrella.h"
  341. } else {
  342. var umbrellaHeaderURL: URL
  343. do {
  344. let files = try fileManager.contentsOfDirectory(at: headersDir,
  345. includingPropertiesForKeys: nil)
  346. .compactMap { $0.path }
  347. let umbrellas = files.filter { $0.hasSuffix("umbrella.h") }
  348. if umbrellas.count != 1 {
  349. fatalError("Did not find exactly one umbrella header in \(headersDir).")
  350. }
  351. guard let firstUmbrella = umbrellas.first else {
  352. fatalError("Failed to get umbrella header in \(headersDir).")
  353. }
  354. umbrellaHeaderURL = URL(fileURLWithPath: firstUmbrella)
  355. } catch {
  356. fatalError("Error while enumerating files \(headersDir): \(error.localizedDescription)")
  357. }
  358. umbrellaHeader = umbrellaHeaderURL.lastPathComponent
  359. }
  360. // Copy the Headers over.
  361. let headersDestination = frameworkDir.appendingPathComponent("Headers")
  362. do {
  363. try fileManager.copyItem(at: headersDir, to: headersDestination)
  364. } catch {
  365. fatalError("Could not copy headers from \(headersDir) to Headers directory in " +
  366. "\(headersDestination): \(error)")
  367. }
  368. // Add an Info.plist. Required by Carthage and SPM binary xcframeworks.
  369. CarthageUtils.generatePlistContents(forName: framework,
  370. withVersion: podInfo.version,
  371. to: frameworkDir)
  372. // TODO: copy PrivateHeaders directory as well if it exists. SDWebImage is an example pod.
  373. // Move all the Resources into .bundle directories in the destination Resources dir. The
  374. // Resources live are contained within the folder structure:
  375. // `projectDir/arch/Release-platform/FrameworkName`.
  376. // The Resources are stored at the top-level of the .framework or .xcframework directory.
  377. // For Firebase distributions, they are propagated one level higher in the final distribution.
  378. let resourceContents = projectDir.appendingPathComponents([anyPlatform.buildName,
  379. anyPlatform.buildDirName,
  380. framework])
  381. guard let moduleMapContentsTemplate = podInfo.moduleMapContents else {
  382. fatalError("Module map contents missing for framework \(framework)")
  383. }
  384. let moduleMapContents = moduleMapContentsTemplate.get(umbrellaHeader: umbrellaHeader)
  385. let frameworks = groupFrameworks(withName: framework,
  386. isCarthage: setCarthage,
  387. fromFolder: frameworkDir,
  388. slicedFrameworks: slicedFrameworks,
  389. moduleMapContents: moduleMapContents)
  390. // Remove the temporary thin archives.
  391. for slicedFramework in slicedFrameworks.values {
  392. do {
  393. try fileManager.removeItem(at: slicedFramework)
  394. } catch {
  395. // Just log a warning instead of failing, since this doesn't actually affect the build
  396. // itself. This should only be shown to help users clean up their disk afterwards.
  397. print("""
  398. WARNING: Failed to remove temporary sliced framework at \(slicedFramework.path). This should
  399. be removed from your system to save disk space. \(error). You should be able to remove the
  400. archive from Terminal with:
  401. rm \(slicedFramework.path)
  402. """)
  403. }
  404. }
  405. return (frameworks, resourceContents)
  406. }
  407. /// Parses CocoaPods config files or uses the passed in `moduleMapContents` to write the
  408. /// appropriate `moduleMap` to the `destination`.
  409. /// Returns true to fail if building for Carthage and there are Swift modules.
  410. @discardableResult
  411. private func packageModuleMaps(inFrameworks frameworks: [URL],
  412. moduleMapContents: String,
  413. destination: URL,
  414. buildingCarthage: Bool = false) -> Bool {
  415. // CocoaPods does not put dependent frameworks and libraries into the module maps it generates.
  416. // Instead it use build options to specify them. For the zip build, we need the module maps to
  417. // include the dependent frameworks and libraries. Therefore we reconstruct them by parsing
  418. // the CocoaPods config files and add them here.
  419. // Currently we only do the construction for Objective C since Swift Module directories require
  420. // several other files. See https://github.com/firebase/firebase-ios-sdk/pull/5040.
  421. // Therefore, for Swift we do a simple copy of the Modules files from an Xcode build.
  422. // This is sufficient for the testing done so far, but more testing is required to determine
  423. // if dependent libraries and frameworks also may need to be added to the Swift module maps in
  424. // some cases.
  425. if makeSwiftModuleMap(thinFrameworks: frameworks,
  426. destination: destination,
  427. buildingCarthage: buildingCarthage) {
  428. return buildingCarthage
  429. }
  430. // Copy the module map to the destination.
  431. let moduleDir = destination.appendingPathComponent("Modules")
  432. do {
  433. try FileManager.default.createDirectory(at: moduleDir, withIntermediateDirectories: true)
  434. } catch {
  435. let frameworkName: String = frameworks.first?.lastPathComponent ?? "<UNKNOWN"
  436. fatalError("Could not create Modules directory for framework: \(frameworkName). \(error)")
  437. }
  438. let modulemap = moduleDir.appendingPathComponent("module.modulemap")
  439. do {
  440. try moduleMapContents.write(to: modulemap, atomically: true, encoding: .utf8)
  441. } catch {
  442. let frameworkName: String = frameworks.first?.lastPathComponent ?? "<UNKNOWN"
  443. fatalError("Could not write modulemap to disk for \(frameworkName): \(error)")
  444. }
  445. return false
  446. }
  447. /// URLs pointing to the frameworks containing architecture specific code.
  448. /// Returns true if there are Swift modules.
  449. private func makeSwiftModuleMap(thinFrameworks: [URL],
  450. destination: URL,
  451. buildingCarthage: Bool = false) -> Bool {
  452. let fileManager = FileManager.default
  453. for thinFramework in thinFrameworks {
  454. // Get the Modules directory. The Catalyst one is a symbolic link.
  455. let moduleDir = thinFramework.appendingPathComponent("Modules").resolvingSymlinksInPath()
  456. do {
  457. let files = try fileManager.contentsOfDirectory(at: moduleDir,
  458. includingPropertiesForKeys: nil)
  459. .compactMap { $0.path }
  460. let swiftModules = files.filter { $0.hasSuffix(".swiftmodule") }
  461. if swiftModules.isEmpty {
  462. return false
  463. } else if buildingCarthage {
  464. return true
  465. }
  466. guard let first = swiftModules.first,
  467. let swiftModule = URL(string: first) else {
  468. fatalError("Failed to get swiftmodule in \(moduleDir).")
  469. }
  470. let destModuleDir = destination.appendingPathComponent("Modules")
  471. if !fileManager.directoryExists(at: destModuleDir) {
  472. do {
  473. try fileManager.copyItem(at: moduleDir, to: destModuleDir)
  474. } catch {
  475. fatalError("Could not copy Modules from \(moduleDir) to " +
  476. "\(destModuleDir): \(error)")
  477. }
  478. } else {
  479. // If the Modules directory is already there, only copy in the architecture specific files
  480. // from the *.swiftmodule subdirectory.
  481. do {
  482. let files = try fileManager.contentsOfDirectory(at: swiftModule,
  483. includingPropertiesForKeys: nil)
  484. .compactMap { $0.path }
  485. let destSwiftModuleDir = destModuleDir
  486. .appendingPathComponent(swiftModule.lastPathComponent)
  487. for file in files {
  488. let fileURL = URL(fileURLWithPath: file)
  489. let projectDir = swiftModule.appendingPathComponent("Project")
  490. if fileURL.lastPathComponent == "Project",
  491. fileManager.directoryExists(at: projectDir) {
  492. // The Project directory (introduced with Xcode 11.4) already exists, only copy in
  493. // new contents.
  494. let projectFiles = try fileManager.contentsOfDirectory(at: projectDir,
  495. includingPropertiesForKeys: nil)
  496. .compactMap { $0.path }
  497. let destProjectDir = destSwiftModuleDir.appendingPathComponent("Project")
  498. for projectFile in projectFiles {
  499. let projectFileURL = URL(fileURLWithPath: projectFile)
  500. do {
  501. try fileManager.copyItem(at: projectFileURL, to:
  502. destProjectDir.appendingPathComponent(projectFileURL.lastPathComponent))
  503. } catch {
  504. fatalError("Could not copy Project file from \(projectFileURL) to " +
  505. "\(destProjectDir): \(error)")
  506. }
  507. }
  508. } else {
  509. do {
  510. try fileManager.copyItem(at: fileURL, to:
  511. destSwiftModuleDir
  512. .appendingPathComponent(fileURL.lastPathComponent))
  513. } catch {
  514. fatalError("Could not copy Swift module file from \(fileURL) to " +
  515. "\(destSwiftModuleDir): \(error)")
  516. }
  517. }
  518. }
  519. } catch {
  520. fatalError("Failed to get Modules directory contents - \(moduleDir):" +
  521. "\(error.localizedDescription)")
  522. }
  523. }
  524. } catch {
  525. fatalError("Error while enumerating files \(moduleDir): \(error.localizedDescription)")
  526. }
  527. }
  528. return true
  529. }
  530. /// Groups slices for each platform into a minimal set of frameworks.
  531. /// - Parameter withName: The framework name.
  532. /// - Parameter isCarthage: Name the temp directory differently for Carthage.
  533. /// - Parameter fromFolder: The almost complete framework folder. Includes Headers, Info.plist,
  534. /// and Resources.
  535. /// - Parameter slicedFrameworks: All the frameworks sliced by platform.
  536. /// - Parameter moduleMapContents: Module map contents for all frameworks in this pod.
  537. private func groupFrameworks(withName framework: String,
  538. isCarthage: Bool,
  539. fromFolder: URL,
  540. slicedFrameworks: [TargetPlatform: URL],
  541. moduleMapContents: String) -> ([URL]) {
  542. let fileManager = FileManager.default
  543. // Create a `.framework` for each of the thinArchives using the `fromFolder` as the base.
  544. let platformFrameworksDir = fileManager.temporaryDirectory(
  545. withName: isCarthage ? "carthage_frameworks" : "platform_frameworks"
  546. )
  547. if !fileManager.directoryExists(at: platformFrameworksDir) {
  548. do {
  549. try fileManager.createDirectory(at: platformFrameworksDir,
  550. withIntermediateDirectories: true)
  551. } catch {
  552. fatalError("Could not create a temp directory to store all thin frameworks: \(error)")
  553. }
  554. }
  555. // Group the thin frameworks into three groups: device, simulator, and Catalyst (all represented
  556. // by the `TargetPlatform` enum. The slices need to be packaged that way with lipo before
  557. // creating a .framework that works for similar grouped architectures. If built separately,
  558. // `-create-xcframework` will return an error and fail:
  559. // `Both ios-arm64 and ios-armv7 represent two equivalent library definitions`
  560. var frameworksBuilt: [URL] = []
  561. for (platform, frameworkPath) in slicedFrameworks {
  562. let platformDir = platformFrameworksDir.appendingPathComponent(platform.buildName)
  563. do {
  564. try fileManager.createDirectory(at: platformDir, withIntermediateDirectories: true)
  565. } catch {
  566. fatalError("Could not create directory for architecture slices on \(platform) for " +
  567. "\(framework): \(error)")
  568. }
  569. // Package a normal .framework given the `fromFolder` and the binary from `slicedFrameworks`.
  570. let destination = platformDir.appendingPathComponent(fromFolder.lastPathComponent)
  571. do {
  572. try fileManager.copyItem(at: fromFolder, to: destination)
  573. } catch {
  574. fatalError("Could not create framework directory needed to build \(framework): \(error)")
  575. }
  576. // Copy the binary to the right location.
  577. let binaryName = frameworkPath.lastPathComponent.replacingOccurrences(of: ".framework",
  578. with: "")
  579. let fatBinary = frameworkPath.appendingPathComponent(binaryName).resolvingSymlinksInPath()
  580. let fatBinaryDestination = destination.appendingPathComponent(framework)
  581. do {
  582. try fileManager.copyItem(at: fatBinary, to: fatBinaryDestination)
  583. } catch {
  584. fatalError("Could not copy fat binary to framework directory for \(framework): \(error)")
  585. }
  586. // Use the appropriate moduleMaps
  587. packageModuleMaps(inFrameworks: [frameworkPath],
  588. moduleMapContents: moduleMapContents,
  589. destination: destination)
  590. frameworksBuilt.append(destination)
  591. }
  592. return frameworksBuilt
  593. }
  594. /// Package the built frameworks into an XCFramework.
  595. /// - Parameter withName: The framework name.
  596. /// - Parameter frameworks: The grouped frameworks.
  597. /// - Parameter xcframeworksDir: Location at which to build the xcframework.
  598. /// - Parameter resourceContents: Location of the resources for this xcframework.
  599. static func makeXCFramework(withName name: String,
  600. frameworks: [URL],
  601. xcframeworksDir: URL,
  602. resourceContents: URL?) -> URL {
  603. let xcframework = xcframeworksDir.appendingPathComponent(name + ".xcframework")
  604. // The arguments for the frameworks need to be separated.
  605. var frameworkArgs: [String] = []
  606. for frameworkBuilt in frameworks {
  607. frameworkArgs.append("-framework")
  608. frameworkArgs.append(frameworkBuilt.path)
  609. }
  610. let outputArgs = ["-output", xcframework.path]
  611. let args = ["-create-xcframework"] + frameworkArgs + outputArgs
  612. print("""
  613. Building \(xcframework) with command:
  614. /usr/bin/xcodebuild \(args.joined(separator: " "))
  615. """)
  616. let result = syncExec(command: "/usr/bin/xcodebuild", args: args, captureOutput: true)
  617. switch result {
  618. case let .error(code, output):
  619. fatalError("Could not build xcframework for \(name) exit code \(code): \(output)")
  620. case .success:
  621. print("XCFramework for \(name) built successfully at \(xcframework).")
  622. }
  623. // xcframework resources are packaged at top of xcframework.
  624. if let resourceContents = resourceContents {
  625. let resourceDir = xcframework.appendingPathComponent("Resources")
  626. do {
  627. try ResourcesManager.moveAllBundles(inDirectory: resourceContents, to: resourceDir)
  628. } catch {
  629. fatalError("Could not move bundles into Resources directory while building \(name): " +
  630. "\(error)")
  631. }
  632. }
  633. return xcframework
  634. }
  635. }