FrameworkBuilder.swift 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  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 logsDir = logsOutputDir ?? fileManager.temporaryDirectory(withName: "build_logs")
  51. do {
  52. // Create our logs directory if it doesn't exist.
  53. if !fileManager.directoryExists(at: logsDir) {
  54. try fileManager.createDirectory(at: logsDir, withIntermediateDirectories: true)
  55. }
  56. } catch {
  57. fatalError("Failure creating temporary directory while building \(framework): \(error)")
  58. }
  59. if dynamicFrameworks {
  60. return (buildDynamicFrameworks(withName: framework, logsDir: logsDir),
  61. nil)
  62. } else {
  63. return buildStaticFrameworks(
  64. withName: framework,
  65. logsDir: logsDir,
  66. setCarthage: setCarthage,
  67. podInfo: podInfo
  68. )
  69. }
  70. }
  71. // MARK: - Private Helpers
  72. /// This runs a command and immediately returns a Shell result.
  73. /// NOTE: This exists in conjunction with the `Shell.execute...` due to issues with different
  74. /// `.bash_profile` environment variables. This should be consolidated in the future.
  75. private static func syncExec(command: String,
  76. args: [String] = [],
  77. captureOutput: Bool = false) -> Shell
  78. .Result {
  79. let task = Process()
  80. task.launchPath = command
  81. task.arguments = args
  82. // If we want to output to the console, create a readabilityHandler and save each line along the
  83. // way. Otherwise, we can just read the pipe at the end. By disabling outputToConsole, some
  84. // commands (such as any xcodebuild) can run much, much faster.
  85. var output = ""
  86. if captureOutput {
  87. let pipe = Pipe()
  88. task.standardOutput = pipe
  89. let outHandle = pipe.fileHandleForReading
  90. outHandle.readabilityHandler = { pipe in
  91. // This will be run any time data is sent to the pipe. We want to print it and store it for
  92. // later. Ignore any non-valid Strings.
  93. guard let line = String(data: pipe.availableData, encoding: .utf8) else {
  94. print("Could not get data from pipe for command \(command): \(pipe.availableData)")
  95. return
  96. }
  97. if !line.isEmpty {
  98. output += line
  99. }
  100. }
  101. // Also set the termination handler on the task in order to stop the readabilityHandler from
  102. // parsing any more data from the task.
  103. task.terminationHandler = { t in
  104. guard let stdOut = t.standardOutput as? Pipe else { return }
  105. stdOut.fileHandleForReading.readabilityHandler = nil
  106. }
  107. } else {
  108. // No capturing output, just mark it as complete.
  109. output = "The task completed"
  110. }
  111. task.launch()
  112. task.waitUntilExit()
  113. // Normally we'd use a pipe to retrieve the output, but for whatever reason it slows things down
  114. // tremendously for xcodebuild.
  115. guard task.terminationStatus == 0 else {
  116. return .error(code: task.terminationStatus, output: output)
  117. }
  118. return .success(output: output)
  119. }
  120. /// Build all thin slices for an open source pod.
  121. /// - Parameter framework: The name of the framework to be built.
  122. /// - Parameter logsDir: The path to the directory to place build logs.
  123. /// - Parameter setCarthage: Set Carthage flag in GoogleUtilities for metrics.
  124. /// - Returns: A dictionary of URLs to the built thin libraries keyed by platform.
  125. private func buildFrameworksForAllPlatforms(withName framework: String,
  126. logsDir: URL,
  127. setCarthage: Bool) -> [TargetPlatform: URL] {
  128. // Build every architecture and save the locations in an array to be assembled.
  129. var slicedFrameworks = [TargetPlatform: URL]()
  130. for targetPlatform in targetPlatforms {
  131. let buildDir = projectDir.appendingPathComponent(targetPlatform.buildName)
  132. let sliced = buildSlicedFramework(withName: framework,
  133. targetPlatform: targetPlatform,
  134. buildDir: buildDir,
  135. logRoot: logsDir,
  136. setCarthage: setCarthage)
  137. slicedFrameworks[targetPlatform] = sliced
  138. }
  139. return slicedFrameworks
  140. }
  141. /// Uses `xcodebuild` to build a framework for a specific target platform.
  142. ///
  143. /// - Parameters:
  144. /// - framework: Name of the framework being built.
  145. /// - targetPlatform: The target platform to target for the build.
  146. /// - buildDir: Location where the project should be built.
  147. /// - logRoot: Root directory where all logs should be written.
  148. /// - setCarthage: Set Carthage flag in GoogleUtilities for metrics.
  149. /// - Returns: A URL to the framework that was built.
  150. private func buildSlicedFramework(withName framework: String,
  151. targetPlatform: TargetPlatform,
  152. buildDir: URL,
  153. logRoot: URL,
  154. setCarthage: Bool = false) -> URL {
  155. let isMacCatalyst = targetPlatform == .catalyst
  156. let isMacCatalystString = isMacCatalyst ? "YES" : "NO"
  157. let workspacePath = projectDir.appendingPathComponent("FrameworkMaker.xcworkspace").path
  158. let distributionFlag = setCarthage ? "-DFIREBASE_BUILD_CARTHAGE" :
  159. "-DFIREBASE_BUILD_ZIP_FILE"
  160. let cFlags = "OTHER_CFLAGS=$(value) \(distributionFlag)"
  161. var archs = targetPlatform.archs.map { $0.rawValue }.joined(separator: " ")
  162. // The 32 bit archs do not build for iOS 11.
  163. if framework == "FirebaseAppCheck" || framework.hasSuffix("Swift") {
  164. if targetPlatform == .iOSDevice {
  165. archs = "arm64"
  166. } else if targetPlatform == .iOSSimulator {
  167. archs = "x86_64 arm64"
  168. }
  169. }
  170. var args = ["build",
  171. "-configuration", "release",
  172. "-workspace", workspacePath,
  173. "-scheme", framework,
  174. "GCC_GENERATE_DEBUGGING_SYMBOLS=NO",
  175. "ARCHS=\(archs)",
  176. "VALID_ARCHS=\(archs)",
  177. "ONLY_ACTIVE_ARCH=NO",
  178. // BUILD_LIBRARY_FOR_DISTRIBUTION=YES is necessary for Swift libraries.
  179. // See https://forums.developer.apple.com/thread/125646.
  180. // Unlike the comment there, the option here is sufficient to cause .swiftinterface
  181. // files to be generated in the .swiftmodule directory. The .swiftinterface files
  182. // are required for xcodebuild to successfully generate an xcframework.
  183. "BUILD_LIBRARY_FOR_DISTRIBUTION=YES",
  184. // Remove the -fembed-bitcode-marker compiling flag.
  185. "ENABLE_BITCODE=NO",
  186. "SUPPORTS_MACCATALYST=\(isMacCatalystString)",
  187. "BUILD_DIR=\(buildDir.path)",
  188. "-sdk", targetPlatform.sdkName,
  189. cFlags]
  190. // Code signing isn't needed for libraries. Disabling signing is required for
  191. // Catalyst libs with resources. See
  192. // https://github.com/CocoaPods/CocoaPods/issues/8891#issuecomment-573301570
  193. if isMacCatalyst {
  194. args.append("CODE_SIGN_IDENTITY=-")
  195. }
  196. print("""
  197. Compiling \(framework) for \(targetPlatform.buildName) (\(archs)) with command:
  198. /usr/bin/xcodebuild \(args.joined(separator: " "))
  199. """)
  200. // Regardless if it succeeds or not, we want to write the log to file in case we need to inspect
  201. // things further.
  202. let logFileName = "\(framework)-\(targetPlatform.buildName).txt"
  203. let logFile = logRoot.appendingPathComponent(logFileName)
  204. let result = FrameworkBuilder.syncExec(command: "/usr/bin/xcodebuild",
  205. args: args,
  206. captureOutput: true)
  207. switch result {
  208. case let .error(code, output):
  209. // Write output to disk and print the location of it. Force unwrapping here since it's going
  210. // to crash anyways, and at this point the root log directory exists, we know it's UTF8, so it
  211. // should pass every time. Revisit if that's not the case.
  212. try! output.write(to: logFile, atomically: true, encoding: .utf8)
  213. fatalError("Error building \(framework) for \(targetPlatform.buildName). Code: \(code). See " +
  214. "the build log at \(logFile)")
  215. case let .success(output):
  216. // Try to write the output to the log file but if it fails it's not a huge deal since it was
  217. // a successful build.
  218. try? output.write(to: logFile, atomically: true, encoding: .utf8)
  219. print("""
  220. Successfully built \(framework) for \(targetPlatform.buildName). Build log is at \(logFile).
  221. """)
  222. // Use the Xcode-generated path to return the path to the compiled library.
  223. // The framework name may be different from the pod name if the module is reset in the
  224. // podspec - like Release-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework.
  225. print("buildDir: \(buildDir)")
  226. let frameworkPath = buildDir.appendingPathComponents([targetPlatform.buildDirName, framework])
  227. var actualFramework: String
  228. do {
  229. let files = try FileManager.default.contentsOfDirectory(at: frameworkPath,
  230. includingPropertiesForKeys: nil)
  231. .compactMap { $0.path }
  232. let frameworkDir = files.filter { $0.contains(".framework") }
  233. actualFramework = URL(fileURLWithPath: frameworkDir[0]).lastPathComponent
  234. } catch {
  235. fatalError("Error while enumerating files \(frameworkPath): \(error.localizedDescription)")
  236. }
  237. let libPath = frameworkPath.appendingPathComponent(actualFramework)
  238. print("buildSliced returns \(libPath)")
  239. return libPath
  240. }
  241. }
  242. // TODO: Automatically get the right name.
  243. /// The module name is different from the pod name when the module_name
  244. /// specifier is used in the podspec.
  245. ///
  246. /// - Parameter framework: The name of the pod to be built.
  247. /// - Returns: The corresponding framework/module name.
  248. private static func frameworkBuildName(_ framework: String) -> String {
  249. switch framework {
  250. case "abseil":
  251. return "absl"
  252. case "BoringSSL-GRPC":
  253. return "openssl_grpc"
  254. case "gRPC-Core":
  255. return "grpc"
  256. case "gRPC-C++":
  257. return "grpcpp"
  258. case "leveldb-library":
  259. return "leveldb"
  260. case "PromisesObjC":
  261. return "FBLPromises"
  262. case "PromisesSwift":
  263. return "Promises"
  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) -> [URL] {
  279. // xcframework doesn't lipo things together but accepts fat frameworks for one target.
  280. // We group architectures here to deal with this fact.
  281. return targetPlatforms.map { targetPlatform in
  282. buildSlicedFramework(
  283. withName: framework,
  284. targetPlatform: targetPlatform,
  285. buildDir: projectDir.appendingPathComponent(targetPlatform.buildName),
  286. logRoot: logsDir
  287. )
  288. }
  289. }
  290. /// Compiles the specified framework in a temporary directory and writes the build logs to file.
  291. /// This will compile all architectures and use the -create-xcframework command to create a modern
  292. /// "fat" framework.
  293. ///
  294. /// - Parameter framework: The name of the framework to be built.
  295. /// - Parameter logsDir: The path to the directory to place build logs.
  296. /// - Parameter moduleMapContents: Module map contents for all frameworks in this pod.
  297. /// - Returns: A path to the newly compiled framework, and the Resource URL.
  298. private func buildStaticFrameworks(withName framework: String,
  299. logsDir: URL,
  300. setCarthage: Bool,
  301. podInfo: CocoaPodUtils.PodInfo) -> ([URL], URL) {
  302. // Build every architecture and save the locations in an array to be assembled.
  303. let slicedFrameworks = buildFrameworksForAllPlatforms(withName: framework, logsDir: logsDir,
  304. setCarthage: setCarthage)
  305. // Create the framework directory in the filesystem for the thin archives to go.
  306. let fileManager = FileManager.default
  307. let frameworkName = FrameworkBuilder.frameworkBuildName(framework)
  308. guard let anyPlatform = targetPlatforms.first,
  309. let archivePath = slicedFrameworks[anyPlatform] else {
  310. fatalError("Could not get a path to an archive to fetch headers in \(frameworkName).")
  311. }
  312. // Find CocoaPods generated umbrella header.
  313. var umbrellaHeader = ""
  314. // TODO(ncooke3): Evaluate if `TensorFlowLiteObjC` is needed?
  315. if framework == "gRPC-Core" || framework == "TensorFlowLiteObjC" {
  316. // TODO: Proper handling of podspec-specified module.modulemap files with customized umbrella
  317. // headers. This is good enough for Firebase since it doesn't need these modules.
  318. // TODO(ncooke3): Is this needed for gRPC-Core?
  319. umbrellaHeader = "\(framework)-umbrella.h"
  320. } else {
  321. var umbrellaHeaderURL: URL
  322. // Get the framework Headers directory. On macOS, it's a symbolic link.
  323. let headersDir = archivePath.appendingPathComponent("Headers").resolvingSymlinksInPath()
  324. do {
  325. let files = try fileManager.contentsOfDirectory(at: headersDir,
  326. includingPropertiesForKeys: nil)
  327. .compactMap { $0.path }
  328. let umbrellas = files.filter { $0.hasSuffix("umbrella.h") }
  329. if umbrellas.count != 1 {
  330. fatalError("Did not find exactly one umbrella header in \(headersDir).")
  331. }
  332. guard let firstUmbrella = umbrellas.first else {
  333. fatalError("Failed to get umbrella header in \(headersDir).")
  334. }
  335. umbrellaHeaderURL = URL(fileURLWithPath: firstUmbrella)
  336. } catch {
  337. fatalError("Error while enumerating files \(headersDir): \(error.localizedDescription)")
  338. }
  339. umbrellaHeader = umbrellaHeaderURL.lastPathComponent
  340. }
  341. // TODO: copy PrivateHeaders directory as well if it exists. SDWebImage is an example pod.
  342. // Move all the Resources into .bundle directories in the destination Resources dir. The
  343. // Resources live are contained within the folder structure:
  344. // `projectDir/arch/Release-platform/FrameworkName`.
  345. // The Resources are stored at the top-level of the .framework or .xcframework directory.
  346. // For Firebase distributions, they are propagated one level higher in the final distribution.
  347. let resourceContents = projectDir.appendingPathComponents([anyPlatform.buildName,
  348. anyPlatform.buildDirName,
  349. framework])
  350. guard let moduleMapContentsTemplate = podInfo.moduleMapContents else {
  351. fatalError("Module map contents missing for framework \(frameworkName)")
  352. }
  353. let moduleMapContents = moduleMapContentsTemplate.get(umbrellaHeader: umbrellaHeader)
  354. let frameworks = groupFrameworks(withName: frameworkName,
  355. isCarthage: setCarthage,
  356. slicedFrameworks: slicedFrameworks,
  357. moduleMapContents: moduleMapContents)
  358. // Remove the temporary thin archives.
  359. for slicedFramework in slicedFrameworks.values {
  360. do {
  361. try fileManager.removeItem(at: slicedFramework)
  362. } catch {
  363. // Just log a warning instead of failing, since this doesn't actually affect the build
  364. // itself. This should only be shown to help users clean up their disk afterwards.
  365. print("""
  366. WARNING: Failed to remove temporary sliced framework at \(slicedFramework.path). This should
  367. be removed from your system to save disk space. \(error). You should be able to remove the
  368. archive from Terminal with:
  369. rm \(slicedFramework.path)
  370. """)
  371. }
  372. }
  373. return (frameworks, resourceContents)
  374. }
  375. /// Parses CocoaPods config files or uses the passed in `moduleMapContents` to write the
  376. /// appropriate `moduleMap` to the `destination`.
  377. /// Returns true to fail if building for Carthage and there are Swift modules.
  378. @discardableResult
  379. private func packageModuleMaps(inFrameworks frameworks: [URL],
  380. frameworkName: String,
  381. moduleMapContents: String,
  382. destination: URL,
  383. buildingCarthage: Bool = false) -> Bool {
  384. // CocoaPods does not put dependent frameworks and libraries into the module maps it generates.
  385. // Instead it use build options to specify them. For the zip build, we need the module maps to
  386. // include the dependent frameworks and libraries. Therefore we reconstruct them by parsing
  387. // the CocoaPods config files and add them here.
  388. // In the case of a mixed language framework, not only are the Swift module
  389. // files copied, but a `module.modulemap` is created by combining the given
  390. // module map contents and a synthesized submodule that modularizes the
  391. // generated Swift header.
  392. if makeSwiftModuleMap(thinFrameworks: frameworks,
  393. frameworkName: frameworkName,
  394. destination: destination,
  395. moduleMapContents: moduleMapContents,
  396. buildingCarthage: buildingCarthage) {
  397. return buildingCarthage
  398. }
  399. let modulemapURL = destination
  400. .appendingPathComponent("Modules")
  401. .appendingPathComponent("module.modulemap")
  402. .resolvingSymlinksInPath()
  403. do {
  404. try moduleMapContents.write(to: modulemapURL, atomically: true, encoding: .utf8)
  405. } catch {
  406. let frameworkName: String = frameworks.first?.lastPathComponent ?? "<UNKNOWN"
  407. fatalError("Could not write modulemap to disk for \(frameworkName): \(error)")
  408. }
  409. return false
  410. }
  411. /// URLs pointing to the frameworks containing architecture specific code.
  412. /// Returns true if there are Swift modules.
  413. private func makeSwiftModuleMap(thinFrameworks: [URL],
  414. frameworkName: String,
  415. destination: URL,
  416. moduleMapContents: String,
  417. buildingCarthage: Bool = false) -> Bool {
  418. let fileManager = FileManager.default
  419. for thinFramework in thinFrameworks {
  420. // Get the Modules directory. The Catalyst one is a symbolic link.
  421. let moduleDir = thinFramework.appendingPathComponent("Modules").resolvingSymlinksInPath()
  422. do {
  423. let files = try fileManager.contentsOfDirectory(at: moduleDir,
  424. includingPropertiesForKeys: nil)
  425. .compactMap { $0.path }
  426. let swiftModules = files.filter { $0.hasSuffix(".swiftmodule") }
  427. if swiftModules.isEmpty {
  428. return false
  429. } else if buildingCarthage {
  430. return true
  431. }
  432. do {
  433. // If this point is reached, the framework contains a Swift module,
  434. // so it's built from either Swift sources or Swift & C Family
  435. // Language sources. Frameworks built from only Swift sources will
  436. // contain only two headers: the CocoaPods-generated umbrella header
  437. // and the Swift-generated Swift header. If the framework's `Headers`
  438. // directory contains more than two resources, then it is assumed
  439. // that the framework was built from mixed language sources because
  440. // those additional headers are public headers for the C Family
  441. // Language sources.
  442. let headersDir = destination.appendingPathComponent("Headers").resolvingSymlinksInPath()
  443. let headers = try fileManager.contentsOfDirectory(
  444. at: headersDir,
  445. includingPropertiesForKeys: nil
  446. )
  447. if headers.count > 2 {
  448. // It is assumed that the framework will always contain a
  449. // `module.modulemap` (either CocoaPods generates it or a custom
  450. // one was set in the podspec corresponding to the framework being
  451. // processed) within the framework's `Modules` directory. The main
  452. // module declaration within this `module.modulemap` should be
  453. // replaced with the given module map contents that was computed to
  454. // include frameworks and libraries that the framework slice
  455. // depends on.
  456. let newModuleMapContents = moduleMapContents + """
  457. module \(frameworkName).Swift {
  458. header "\(frameworkName)-Swift.h"
  459. requires objc
  460. }
  461. """
  462. let modulemapURL = destination.appendingPathComponents(["Modules", "module.modulemap"])
  463. .resolvingSymlinksInPath()
  464. try newModuleMapContents.write(to: modulemapURL, atomically: true, encoding: .utf8)
  465. }
  466. } catch {
  467. fatalError(
  468. "Error while synthesizing a mixed language framework's module map: \(error.localizedDescription)"
  469. )
  470. }
  471. } catch {
  472. fatalError("Error while enumerating files \(moduleDir): \(error.localizedDescription)")
  473. }
  474. }
  475. return true
  476. }
  477. /// Groups slices for each platform into a minimal set of frameworks.
  478. /// - Parameter withName: The framework name.
  479. /// - Parameter isCarthage: Name the temp directory differently for Carthage.
  480. /// - Parameter slicedFrameworks: All the frameworks sliced by platform.
  481. /// - Parameter moduleMapContents: Module map contents for all frameworks in this pod.
  482. private func groupFrameworks(withName framework: String,
  483. isCarthage: Bool,
  484. slicedFrameworks: [TargetPlatform: URL],
  485. moduleMapContents: String) -> ([URL]) {
  486. let fileManager = FileManager.default
  487. let platformFrameworksDir = fileManager.temporaryDirectory(
  488. withName: isCarthage ? "carthage_frameworks" : "platform_frameworks"
  489. )
  490. if !fileManager.directoryExists(at: platformFrameworksDir) {
  491. do {
  492. try fileManager.createDirectory(at: platformFrameworksDir,
  493. withIntermediateDirectories: true)
  494. } catch {
  495. fatalError("Could not create a temp directory to store all thin frameworks: \(error)")
  496. }
  497. }
  498. return slicedFrameworks.map { platform, frameworkPath in
  499. // Create the following structure in the platform frameworks directory:
  500. // - platform_frameworks
  501. // └── $(PLATFORM)
  502. // └── $(FRAMEWORK).framework
  503. let platformFrameworkDir = platformFrameworksDir
  504. .appendingPathComponent(platform.buildName)
  505. .appendingPathComponent(framework + ".framework")
  506. do {
  507. // Create `platform_frameworks/$(PLATFORM)` subdirectory.
  508. try fileManager.createDirectory(
  509. at: platformFrameworkDir.deletingLastPathComponent(),
  510. withIntermediateDirectories: true
  511. )
  512. // Copy the built framework to the `platform_frameworks/$(PLATFORM)/$(FRAMEWORK).framework`.
  513. try fileManager.copyItem(at: frameworkPath, to: platformFrameworkDir)
  514. } catch {
  515. fatalError("Could not copy directory for architecture slices on \(platform) for " +
  516. "\(framework): \(error)")
  517. }
  518. // The minimum OS version is set to 100.0 to work around b/327020913.
  519. // TODO(ncooke3): Revert this logic once b/327020913 is fixed.
  520. // TODO(ncooke3): Does this need to happen on macOS?
  521. do {
  522. let frameworkInfoPlistURL = platformFrameworkDir
  523. .appendingPathComponent(
  524. platform == .catalyst || platform == .macOS ? "Resources" : ""
  525. )
  526. .resolvingSymlinksInPath()
  527. .appendingPathComponent("Info.plist")
  528. var plistDictionary = try PropertyListSerialization.propertyList(
  529. from: Data(contentsOf: frameworkInfoPlistURL), format: nil
  530. ) as! [AnyHashable: Any]
  531. plistDictionary["MinimumOSVersion"] = "100.0"
  532. let updatedPlistData = try PropertyListSerialization.data(
  533. fromPropertyList: plistDictionary,
  534. format: .xml,
  535. options: 0
  536. )
  537. try updatedPlistData.write(to: frameworkInfoPlistURL)
  538. } catch {
  539. fatalError(
  540. "Could not modify framework-level plist for b/327020913 in framework directory \(framework): \(error)"
  541. )
  542. }
  543. // Move privacy manifest containing resource bundles into the framework.
  544. let resourceDir = platformFrameworkDir
  545. .appendingPathComponent(
  546. platform == .catalyst || platform == .macOS ? "Resources" : ""
  547. )
  548. .resolvingSymlinksInPath()
  549. // Move resource bundles into the platform framework.
  550. do {
  551. try fileManager.contentsOfDirectory(
  552. at: frameworkPath.deletingLastPathComponent(),
  553. includingPropertiesForKeys: nil
  554. )
  555. .filter { $0.pathExtension == "bundle" }
  556. // Bundles are moved rather than copied to prevent them from being
  557. // packaged in a `Resources` directory at the root of the xcframework.
  558. .forEach {
  559. try fileManager.moveItem(
  560. at: $0,
  561. to: resourceDir.appendingPathComponent($0.lastPathComponent)
  562. )
  563. }
  564. } catch {
  565. fatalError(
  566. "Could not move resources for framework \(frameworkPath), platform \(platform). Error: \(error)"
  567. )
  568. }
  569. // Use the appropriate moduleMaps
  570. packageModuleMaps(inFrameworks: [frameworkPath],
  571. frameworkName: framework,
  572. moduleMapContents: moduleMapContents,
  573. destination: platformFrameworkDir)
  574. return platformFrameworkDir
  575. }
  576. }
  577. /// Package the built frameworks into an XCFramework.
  578. /// - Parameter withName: The framework name.
  579. /// - Parameter frameworks: The grouped frameworks.
  580. /// - Parameter xcframeworksDir: Location at which to build the xcframework.
  581. /// - Parameter resourceContents: Location of the resources for this xcframework.
  582. static func makeXCFramework(withName name: String,
  583. frameworks: [URL],
  584. xcframeworksDir: URL,
  585. resourceContents: URL?) -> URL {
  586. let xcframework = xcframeworksDir
  587. .appendingPathComponent(frameworkBuildName(name) + ".xcframework")
  588. // The arguments for the frameworks need to be separated.
  589. let frameworkArgs = frameworks.flatMap { frameworkPath in
  590. do {
  591. // Xcode 15.0-15.2: Return the canonical path to work around issue
  592. // https://forums.swift.org/t/67439
  593. let frameworkCanonicalPath = try frameworkPath.resourceValues(forKeys: [.canonicalPathKey])
  594. .canonicalPath!
  595. return ["-framework", frameworkCanonicalPath]
  596. } catch {
  597. fatalError("Failed to get canonical path for \(frameworkPath): \(error)")
  598. }
  599. }
  600. let outputArgs = ["-output", xcframework.path]
  601. let args = ["-create-xcframework"] + frameworkArgs + outputArgs
  602. print("""
  603. Building \(xcframework) with command:
  604. /usr/bin/xcodebuild \(args.joined(separator: " "))
  605. """)
  606. let result = syncExec(command: "/usr/bin/xcodebuild", args: args, captureOutput: true)
  607. switch result {
  608. case let .error(code, output):
  609. fatalError("Could not build xcframework for \(name) exit code \(code): \(output)")
  610. case .success:
  611. print("XCFramework for \(name) built successfully at \(xcframework).")
  612. }
  613. // xcframework resources are packaged at top of xcframework.
  614. if let resourceContents = resourceContents {
  615. let resourceDir = xcframework.appendingPathComponent("Resources")
  616. do {
  617. try ResourcesManager.moveAllBundles(inDirectory: resourceContents, to: resourceDir)
  618. } catch {
  619. fatalError("Could not move bundles into Resources directory while building \(name): " +
  620. "\(error)")
  621. }
  622. }
  623. return xcframework
  624. }
  625. }