FrameworkBuilder.swift 40 KB

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