ZipBuilder.swift 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932
  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 FirebaseManifest
  17. import Foundation
  18. /// Misc. constants used in the build tool.
  19. enum Constants {
  20. /// Constants related to the Xcode project template.
  21. enum ProjectPath {
  22. // Required for building.
  23. static let infoPlist = "Info.plist"
  24. static let projectFile = "FrameworkMaker.xcodeproj"
  25. /// All required files for building the Zip file.
  26. static let requiredFilesForBuilding: [String] = [projectFile, infoPlist]
  27. // Required for distribution.
  28. static let readmeName = "README.md"
  29. static let metadataName = "METADATA.md"
  30. // Required from the Firebase pod.
  31. static let firebaseHeader = "Firebase.h"
  32. static let modulemap = "module.modulemap"
  33. /// The dummy Firebase library for Carthage distribution.
  34. static let dummyFirebaseLib = "dummy_Firebase_lib"
  35. }
  36. /// The text added to the README for a product if it contains Resources. The empty line at the end
  37. /// is intentional.
  38. static let resourcesRequiredText = """
  39. You'll also need to add the resources in the Resources
  40. directory into your target's main bundle.
  41. """
  42. }
  43. /// A zip file builder. The zip file can be built with the `buildAndAssembleReleaseDir()` function.
  44. struct ZipBuilder {
  45. /// Artifacts from building and assembling the release directory.
  46. struct ReleaseArtifacts {
  47. /// The Firebase version.
  48. let firebaseVersion: String
  49. /// The directory that contains the properly assembled release artifacts.
  50. let zipDir: URL
  51. /// The directory that contains the properly assembled release artifacts for Carthage if
  52. /// building it.
  53. let carthageDir: URL?
  54. }
  55. /// Relevant paths in the filesystem to build the release directory.
  56. struct FilesystemPaths {
  57. // MARK: - Required Paths
  58. /// The root of the `firebase-ios-sdk` git repo.
  59. let repoDir: URL
  60. /// The path to the directory containing the blank xcodeproj and Info.plist for building source
  61. /// based frameworks. Generated based on the `repoDir`.
  62. var templateDir: URL {
  63. return type(of: self).templateDir(fromRepoDir: repoDir)
  64. }
  65. // MARK: - Optional Paths
  66. /// The root directory for build artifacts. If `nil`, a temporary directory will be used.
  67. let buildRoot: URL?
  68. /// The output directory for any artifacts generated during the build. If `nil`, a temporary
  69. /// directory will be used.
  70. let outputDir: URL?
  71. /// The path to where local podspecs are stored.
  72. let localPodspecPath: URL?
  73. /// The path to a directory to move all build logs to. If `nil`, a temporary directory will be
  74. /// used.
  75. var logsOutputDir: URL?
  76. /// Creates the struct containing all properties needed for a build.
  77. /// - Parameter repoDir: The root of the `firebase-ios-sdk` git repo.
  78. /// - Parameter buildRoot: The root directory for build artifacts. If `nil`, a temporary
  79. /// directory will be used.
  80. /// - Parameter outputDir: The output directory for any artifacts generated. If `nil`, a
  81. /// temporary directory will be used.
  82. /// - Parameter localPodspecPath: A path to where local podspecs are stored.
  83. /// - Parameter logsOutputDir: The output directory for any logs. If `nil`, a temporary
  84. /// directory will be used.
  85. init(repoDir: URL,
  86. buildRoot: URL?,
  87. outputDir: URL?,
  88. localPodspecPath: URL?,
  89. logsOutputDir: URL?) {
  90. self.repoDir = repoDir
  91. self.buildRoot = buildRoot
  92. self.outputDir = outputDir
  93. self.localPodspecPath = localPodspecPath
  94. self.logsOutputDir = logsOutputDir
  95. }
  96. /// Returns the expected template directory given the repo directory provided.
  97. static func templateDir(fromRepoDir repoDir: URL) -> URL {
  98. return repoDir.appendingPathComponents(["ReleaseTooling", "Template"])
  99. }
  100. }
  101. /// Paths needed throughout the process of packaging the Zip file.
  102. public let paths: FilesystemPaths
  103. /// The platforms to target for the builds.
  104. public let platforms: [Platform]
  105. /// Specifies if the builder is building dynamic frameworks instead of static frameworks.
  106. private let dynamicFrameworks: Bool
  107. /// Custom CocoaPods spec repos to be used. If not provided, the tool will only use the CocoaPods
  108. /// master repo.
  109. private let customSpecRepos: [URL]?
  110. /// Creates a ZipBuilder struct to build and assemble zip files and Carthage builds.
  111. ///
  112. /// - Parameters:
  113. /// - paths: Paths that are needed throughout the process of packaging the Zip file.
  114. /// - platforms: The platforms to target for the builds.
  115. /// - dynamicFrameworks: Specifies if dynamic frameworks should be built, otherwise static
  116. /// frameworks are built.
  117. /// - customSpecRepo: A custom spec repo to be used for fetching CocoaPods from.
  118. init(paths: FilesystemPaths,
  119. platforms: [Platform],
  120. dynamicFrameworks: Bool,
  121. customSpecRepos: [URL]? = nil) {
  122. self.paths = paths
  123. self.platforms = platforms
  124. self.customSpecRepos = customSpecRepos
  125. self.dynamicFrameworks = dynamicFrameworks
  126. }
  127. /// Builds and assembles the contents for the zip build.
  128. ///
  129. /// - Parameter podsToInstall: All pods to install.
  130. /// - Parameter includeCarthage: Build Carthage distribution as well.
  131. /// - Parameter includeDependencies: Include dependencies of requested pod in distribution.
  132. /// - Returns: Arrays of pod install info and the frameworks installed.
  133. func buildAndAssembleZip(podsToInstall: [CocoaPodUtils.VersionedPod],
  134. includeCarthage: Bool,
  135. includeDependencies: Bool) ->
  136. ([String: CocoaPodUtils.PodInfo], [String: [URL]], URL?) {
  137. // Remove CocoaPods cache so the build gets updates after a version is rebuilt during the
  138. // release process. Always do this, since it can be the source of subtle failures on rebuilds.
  139. CocoaPodUtils.cleanPodCache()
  140. // We need to install all the pods in order to get every single framework that we'll need
  141. // for the zip file. We can't install each one individually since some pods depend on different
  142. // subspecs from the same pod (ex: GoogleUtilities, GoogleToolboxForMac, etc). All of the code
  143. // wouldn't be included so we need to install all of the subspecs to catch the superset of all
  144. // required frameworks, then use that as the source of frameworks to pull from when including
  145. // the folders in each product directory.
  146. let linkage: CocoaPodUtils.LinkageType = dynamicFrameworks ? .dynamic : .standardStatic
  147. var groupedFrameworks: [String: [URL]] = [:]
  148. var carthageGoogleUtilitiesFrameworks: [URL] = []
  149. var podsBuilt: [String: CocoaPodUtils.PodInfo] = [:]
  150. var xcframeworks: [String: [URL]] = [:]
  151. var resources: [String: URL] = [:]
  152. for platform in platforms {
  153. let projectDir = FileManager.default.temporaryDirectory(withName: "project-" + platform.name)
  154. CocoaPodUtils.podInstallPrepare(inProjectDir: projectDir, templateDir: paths.templateDir)
  155. let platformPods = podsToInstall.filter { $0.platforms.contains(platform.name) }
  156. CocoaPodUtils.installPods(platformPods,
  157. inDir: projectDir,
  158. platform: platform,
  159. customSpecRepos: customSpecRepos,
  160. localPodspecPath: paths.localPodspecPath,
  161. linkage: linkage)
  162. // Find out what pods were installed with the above commands.
  163. let installedPods = CocoaPodUtils.installedPodsInfo(inProjectDir: projectDir,
  164. localPodspecPath: paths.localPodspecPath)
  165. // If module maps are needed for static frameworks, build them here to be available to copy
  166. // into the generated frameworks.
  167. if !dynamicFrameworks {
  168. ModuleMapBuilder(customSpecRepos: customSpecRepos,
  169. selectedPods: installedPods,
  170. platform: platform,
  171. paths: paths).build()
  172. }
  173. let podsToBuild = includeDependencies ? installedPods : installedPods.filter {
  174. platformPods.map { $0.name.components(separatedBy: "/").first }.contains($0.key)
  175. }
  176. // Build in a sorted order to make the build deterministic and to avoid exposing random
  177. // build order bugs.
  178. // Also AppCheck must be built after other pods so that its restricted architecture
  179. // selection does not restrict any of its dependencies.
  180. var sortedPods = podsToBuild.keys.sorted()
  181. sortedPods.removeAll(where: { value in
  182. value == "FirebaseAppCheck"
  183. })
  184. sortedPods.append("FirebaseAppCheck")
  185. for podName in sortedPods {
  186. guard let podInfo = podsToBuild[podName] else {
  187. continue
  188. }
  189. if podName == "Firebase" {
  190. // Don't build the Firebase pod.
  191. } else if podInfo.isSourcePod {
  192. let builder = FrameworkBuilder(projectDir: projectDir,
  193. targetPlatforms: platform.platformTargets,
  194. dynamicFrameworks: dynamicFrameworks)
  195. let (frameworks, resourceContents) =
  196. builder.compileFrameworkAndResources(withName: podName,
  197. logsOutputDir: paths.logsOutputDir,
  198. setCarthage: false,
  199. podInfo: podInfo)
  200. groupedFrameworks[podName] = (groupedFrameworks[podName] ?? []) + frameworks
  201. if includeCarthage, podName == "GoogleUtilities" {
  202. let (cdFrameworks, _) = builder.compileFrameworkAndResources(withName: podName,
  203. logsOutputDir: paths
  204. .logsOutputDir,
  205. setCarthage: true,
  206. podInfo: podInfo)
  207. carthageGoogleUtilitiesFrameworks += cdFrameworks
  208. }
  209. let fileManager = FileManager.default
  210. if let resourceContents,
  211. let contents = try? fileManager.contentsOfDirectory(at: resourceContents,
  212. includingPropertiesForKeys: nil),
  213. !contents.isEmpty {
  214. resources[podName] = resourceContents
  215. }
  216. } else if podsBuilt[podName] == nil {
  217. // Binary pods need to be collected once, since the platforms should already be merged.
  218. let binaryFrameworks = collectBinaryFrameworks(fromPod: podName, podInfo: podInfo)
  219. xcframeworks[podName] = binaryFrameworks
  220. }
  221. // Union all pods built across platforms.
  222. // Be conservative and favor iOS if it exists - and workaround
  223. // bug where Firebase.h doesn't get installed for tvOS and macOS.
  224. // Fixed in #7284.
  225. if podsBuilt[podName] == nil {
  226. podsBuilt[podName] = podInfo
  227. }
  228. }
  229. }
  230. // Now consolidate the built frameworks for all platforms into a single xcframework.
  231. let xcframeworksDir = FileManager.default.temporaryDirectory(withName: "xcframeworks")
  232. do {
  233. try FileManager.default.createDirectory(at: xcframeworksDir,
  234. withIntermediateDirectories: false)
  235. } catch {
  236. fatalError("Could not create XCFrameworks directory: \(error)")
  237. }
  238. for groupedFramework in groupedFrameworks {
  239. let name = groupedFramework.key
  240. let xcframework = FrameworkBuilder.makeXCFramework(withName: name,
  241. frameworks: postProcessFrameworks(groupedFramework
  242. .value),
  243. xcframeworksDir: xcframeworksDir,
  244. resourceContents: resources[name])
  245. xcframeworks[name] = [xcframework]
  246. }
  247. for (framework, paths) in xcframeworks {
  248. print("Frameworks for pod: \(framework) were compiled at \(paths)")
  249. }
  250. guard includeCarthage else {
  251. // No Carthage build necessary, return now.
  252. return (podsBuilt, xcframeworks, nil)
  253. }
  254. let xcframeworksCarthageDir = FileManager.default.temporaryDirectory(withName: "xcf-carthage")
  255. do {
  256. try FileManager.default.createDirectory(at: xcframeworksCarthageDir,
  257. withIntermediateDirectories: false)
  258. } catch {
  259. fatalError("Could not create XCFrameworks Carthage directory: \(error)")
  260. }
  261. let carthageGoogleUtilitiesXcframework = FrameworkBuilder.makeXCFramework(
  262. withName: "GoogleUtilities",
  263. frameworks: postProcessFrameworks(carthageGoogleUtilitiesFrameworks),
  264. xcframeworksDir: xcframeworksCarthageDir,
  265. resourceContents: nil
  266. )
  267. return (podsBuilt, xcframeworks, carthageGoogleUtilitiesXcframework)
  268. }
  269. func postProcessFrameworks(_ frameworks: [URL]) -> [URL] {
  270. for framework in frameworks {
  271. // CocoaPods creates a `_CodeSignature` directory. Delete it.
  272. // Note that the build only produces a `_CodeSignature` directory for
  273. // macOS and macCatalyst (`Versions/A/`), but we try to delete it for
  274. // other platforms just in case it were to appear.
  275. for path in ["", "Versions/A/"] {
  276. let codeSignatureDir = framework
  277. .appendingPathComponent(path)
  278. .appendingPathComponent("_CodeSignature")
  279. .resolvingSymlinksInPath()
  280. try? FileManager.default.removeItem(at: codeSignatureDir)
  281. }
  282. // Delete `gRPCCertificates-Cpp.bundle` since it is not needed (#9184).
  283. // Depending on the platform, it may be at the root of the framework or
  284. // in a symlinked `Resources` directory (for macOS, macCatalyst). Attempt
  285. // to delete at either patch for each framework.
  286. for path in ["", "Resources"] {
  287. let grpcCertsBundle = framework
  288. .appendingPathComponent(path)
  289. .appendingPathComponent("gRPCCertificates-Cpp.bundle")
  290. .resolvingSymlinksInPath()
  291. try? FileManager.default.removeItem(at: grpcCertsBundle)
  292. }
  293. // The macOS slice's `PrivateHeaders` directory may have a
  294. // `PrivateHeaders` file in it that symbolically links to nowhere. Delete
  295. // it here to avoid putting it in the zip or crashing the Carthage hash
  296. // generation. Because this will throw an error for cases where the file
  297. // does not exist, the error is ignored.
  298. let privateHeadersDir = framework.appendingPathComponent("PrivateHeaders")
  299. if !FileManager.default.directoryExists(at: privateHeadersDir.resolvingSymlinksInPath()) {
  300. try? FileManager.default.removeItem(at: privateHeadersDir)
  301. }
  302. // The `Headers` and `PrivateHeaders` directories may contain a symlink
  303. // of the same name. Delete it here to avoid putting it in the zip or
  304. // crashing the Carthage hash generation.
  305. for path in ["Headers", "PrivateHeaders"] {
  306. let headersDir = framework.appendingPathComponent(path).resolvingSymlinksInPath()
  307. try? FileManager.default.removeItem(at: headersDir.appendingPathComponent(path))
  308. }
  309. }
  310. return frameworks
  311. }
  312. /// Try to build and package the contents of the Zip file. This will throw an error as soon as it
  313. /// encounters an error, or will quit due to a fatal error with the appropriate log.
  314. ///
  315. /// - Parameter templateDir: The template project for pod install.
  316. /// - Throws: One of many errors that could have happened during the build phase.
  317. func buildAndAssembleFirebaseRelease(templateDir: URL) throws -> ReleaseArtifacts {
  318. let manifest = FirebaseManifest.shared
  319. var podsToInstall = manifest.pods.map {
  320. CocoaPodUtils.VersionedPod(name: $0.name,
  321. version: manifest.versionString($0),
  322. platforms: $0.platforms)
  323. }
  324. guard !podsToInstall.isEmpty else {
  325. fatalError("Failed to find versions for Firebase release")
  326. }
  327. // We don't release Google-Mobile-Ads-SDK and GoogleSignIn, but we include their latest
  328. // version for convenience in the Zip and Carthage builds.
  329. podsToInstall.append(CocoaPodUtils.VersionedPod(name: "Google-Mobile-Ads-SDK",
  330. version: nil,
  331. platforms: ["ios"]))
  332. podsToInstall.append(CocoaPodUtils.VersionedPod(name: "GoogleSignIn",
  333. version: nil,
  334. platforms: ["ios"]))
  335. print("Final expected versions for the Zip file: \(podsToInstall)")
  336. let (installedPods, frameworks, carthageGoogleUtilitiesXcframeworkFirebase) =
  337. buildAndAssembleZip(podsToInstall: podsToInstall,
  338. includeCarthage: true,
  339. // Always include dependencies for Firebase zips.
  340. includeDependencies: true)
  341. // We need the Firebase pod to get the version for Carthage and to copy the `Firebase.h` and
  342. // `module.modulemap` file from it.
  343. guard let firebasePod = installedPods["Firebase"] else {
  344. fatalError("Could not get the Firebase pod from list of installed pods. All pods " +
  345. "installed: \(installedPods)")
  346. }
  347. guard let carthageGoogleUtilitiesXcframework = carthageGoogleUtilitiesXcframeworkFirebase else {
  348. fatalError("GoogleUtilitiesXcframework is missing")
  349. }
  350. let zipDir = try assembleDistributions(withPackageKind: "Firebase",
  351. podsToInstall: podsToInstall,
  352. installedPods: installedPods,
  353. frameworksToAssemble: frameworks,
  354. firebasePod: firebasePod)
  355. // Replace Core Diagnostics
  356. var carthageFrameworks = frameworks
  357. carthageFrameworks["GoogleUtilities"] = [carthageGoogleUtilitiesXcframework]
  358. let carthageDir = try assembleDistributions(withPackageKind: "CarthageFirebase",
  359. podsToInstall: podsToInstall,
  360. installedPods: installedPods,
  361. frameworksToAssemble: carthageFrameworks,
  362. firebasePod: firebasePod)
  363. return ReleaseArtifacts(firebaseVersion: firebasePod.version,
  364. zipDir: zipDir, carthageDir: carthageDir)
  365. }
  366. // MARK: - Private
  367. /// Assemble the folder structure of the Zip file. In order to get the frameworks
  368. /// required, we will `pod install` only those subspecs and then fetch the information for all
  369. /// the frameworks that were installed, copying the frameworks from our list of compiled
  370. /// frameworks. The whole process is:
  371. /// 1. Copy any required files (headers, modulemap, etc) over beforehand to fail fast if anything
  372. /// is misconfigured.
  373. /// 2. Get the frameworks required for Analytics, copy them to the Analytics folder.
  374. /// 3. Go through the rest of the subspecs (excluding those included in Analytics) and copy them
  375. /// to a folder with the name of the subspec.
  376. /// 4. Assemble the `README` file based off the template and copy it to the directory.
  377. /// 5. Return the URL of the folder containing the contents of the Zip file.
  378. ///
  379. /// - Returns: Return the URL of the folder containing the contents of the Zip or Carthage
  380. /// distribution.
  381. /// - Throws: One of many errors that could have happened during the build phase.
  382. private func assembleDistributions(withPackageKind packageKind: String,
  383. podsToInstall: [CocoaPodUtils.VersionedPod],
  384. installedPods: [String: CocoaPodUtils.PodInfo],
  385. frameworksToAssemble: [String: [URL]],
  386. firebasePod: CocoaPodUtils.PodInfo) throws -> URL {
  387. // Create the directory that will hold all the contents of the Zip file.
  388. let fileManager = FileManager.default
  389. let zipDir = fileManager.temporaryDirectory(withName: packageKind)
  390. do {
  391. if fileManager.directoryExists(at: zipDir) {
  392. try fileManager.removeItem(at: zipDir)
  393. }
  394. try fileManager.createDirectory(at: zipDir,
  395. withIntermediateDirectories: true,
  396. attributes: nil)
  397. }
  398. // Copy all required files from the Firebase pod. This will cause a fatalError if anything
  399. // fails.
  400. copyFirebasePodFiles(fromDir: firebasePod.installedLocation, to: zipDir)
  401. // Start with installing Analytics, since we'll need to exclude those frameworks from the rest
  402. // of the folders.
  403. let analyticsFrameworks: [String]
  404. let analyticsDir: URL
  405. do {
  406. // This returns the Analytics directory and a list of framework names that Analytics requires.
  407. /// Example: ["FirebaseInstallations, "GoogleAppMeasurement", "nanopb", <...>]
  408. let (dir, frameworks) = try installAndCopyFrameworks(forPod: "FirebaseAnalytics",
  409. inFolder: "FirebaseAnalytics",
  410. withInstalledPods: installedPods,
  411. rootZipDir: zipDir,
  412. builtFrameworks: frameworksToAssemble)
  413. analyticsFrameworks = frameworks
  414. analyticsDir = dir
  415. } catch {
  416. fatalError("Could not copy frameworks from Analytics into the zip file: \(error)")
  417. }
  418. // Start the README dependencies string with the frameworks built in Analytics.
  419. var metadataDeps = dependencyString(for: "FirebaseAnalytics",
  420. in: analyticsDir,
  421. frameworks: analyticsFrameworks)
  422. // Loop through all the other subspecs that aren't Core and Analytics and write them to their
  423. // final destination, including resources.
  424. let analyticsPods = analyticsFrameworks.map {
  425. $0.replacingOccurrences(of: ".framework", with: "")
  426. }
  427. let manifest = FirebaseManifest.shared
  428. let firebaseZipPods = manifest.pods.filter { $0.zip }.map { $0.name }
  429. // Skip Analytics and the pods bundled with it.
  430. let remainingPods = installedPods.filter {
  431. $0.key == "Google-Mobile-Ads-SDK" ||
  432. $0.key == "GoogleSignIn" ||
  433. (firebaseZipPods.contains($0.key) &&
  434. $0.key != "FirebaseAnalytics" &&
  435. $0.key != "Firebase" &&
  436. podsToInstall.map { $0.name }.contains($0.key))
  437. }.sorted { $0.key < $1.key }
  438. for pod in remainingPods {
  439. let folder = pod.key == "GoogleSignInSwiftSupport" ? "GoogleSignIn" :
  440. pod.key.replacingOccurrences(of: "Swift", with: "")
  441. do {
  442. if frameworksToAssemble[pod.key] == nil {
  443. // Continue if the pod wasn't built.
  444. continue
  445. }
  446. let (productDir, podFrameworks) =
  447. try installAndCopyFrameworks(forPod: pod.key,
  448. inFolder: folder,
  449. withInstalledPods: installedPods,
  450. rootZipDir: zipDir,
  451. builtFrameworks: frameworksToAssemble,
  452. frameworksToIgnore: analyticsPods)
  453. // Update the README.
  454. metadataDeps += dependencyString(for: folder, in: productDir, frameworks: podFrameworks)
  455. } catch {
  456. fatalError("Could not copy frameworks from \(pod) into the zip file: \(error)")
  457. }
  458. do {
  459. // Update Resources: For the zip distribution, they get pulled from the xcframework to the
  460. // top-level product directory. For the Carthage distribution, they propagate to each
  461. // individual framework.
  462. // TODO: Investigate changing the zip distro to also have Resources in the .frameworks to
  463. // enable different platform Resources.
  464. let productPath = zipDir.appendingPathComponent(folder)
  465. let contents = try fileManager.contentsOfDirectory(atPath: productPath.path)
  466. for fileOrFolder in contents {
  467. let xcPath = productPath.appendingPathComponent(fileOrFolder)
  468. let xcResourceDir = xcPath.appendingPathComponent("Resources")
  469. // Ignore anything that not an xcframework with Resources
  470. guard fileManager.isDirectory(at: xcPath),
  471. xcPath.lastPathComponent.hasSuffix("xcframework"),
  472. fileManager.directoryExists(at: xcResourceDir) else { continue }
  473. if packageKind == "Firebase" {
  474. // Move all the bundles in the frameworks out to a common "Resources" directory to
  475. // match the existing Zip structure.
  476. let resourcesDir = productPath.appendingPathComponent("Resources")
  477. try fileManager.moveItem(at: xcResourceDir, to: resourcesDir)
  478. } else {
  479. let xcContents = try fileManager.contentsOfDirectory(atPath: xcPath.path)
  480. for fileOrFolder in xcContents {
  481. let platformPath = xcPath.appendingPathComponent(fileOrFolder)
  482. guard fileManager.isDirectory(at: platformPath) else { continue }
  483. let platformContents = try fileManager.contentsOfDirectory(atPath: platformPath.path)
  484. for fileOrFolder in platformContents {
  485. let frameworkPath = platformPath.appendingPathComponent(fileOrFolder)
  486. // Ignore anything that not a framework.
  487. guard fileManager.isDirectory(at: frameworkPath),
  488. frameworkPath.lastPathComponent.hasSuffix("framework") else { continue }
  489. let resourcesDir = frameworkPath.appendingPathComponent("Resources")
  490. .resolvingSymlinksInPath()
  491. // On macOS platforms, this directory will already be there, so
  492. // ignore error that it already exists.
  493. try? fileManager.createDirectory(
  494. at: resourcesDir,
  495. withIntermediateDirectories: true
  496. )
  497. let xcResourceDirContents = try fileManager.contentsOfDirectory(
  498. at: xcResourceDir,
  499. includingPropertiesForKeys: nil
  500. )
  501. for file in xcResourceDirContents {
  502. try fileManager.copyItem(
  503. at: file,
  504. to: resourcesDir.appendingPathComponent(file.lastPathComponent)
  505. )
  506. }
  507. }
  508. }
  509. try fileManager.removeItem(at: xcResourceDir)
  510. }
  511. }
  512. } catch {
  513. fatalError("Could not setup Resources for \(pod.key) for \(packageKind) \(error)")
  514. }
  515. // Special case for Crashlytics:
  516. // Copy additional tools to avoid users from downloading another artifact to upload symbols.
  517. let crashlyticsPodName = "FirebaseCrashlytics"
  518. if pod.key == crashlyticsPodName {
  519. for file in ["upload-symbols", "run"] {
  520. let source = pod.value.installedLocation.appendingPathComponent(file)
  521. let target = zipDir.appendingPathComponent(crashlyticsPodName)
  522. .appendingPathComponent(file)
  523. do {
  524. try fileManager.copyItem(at: source, to: target)
  525. } catch {
  526. fatalError("Error copying Crashlytics tools from \(source) to \(target): \(error)")
  527. }
  528. }
  529. }
  530. }
  531. // Assemble the `METADATA.md` file by injecting the template file with
  532. // this zip version's dependency graph and dependency versioning table.
  533. let metadataPath = paths.templateDir.appendingPathComponent(Constants.ProjectPath.metadataName)
  534. let metadataTemplate: String
  535. do {
  536. metadataTemplate = try String(contentsOf: metadataPath)
  537. } catch {
  538. fatalError("Could not get contents of the METADATA template: \(error)")
  539. }
  540. let versionsText = versionsString(for: installedPods)
  541. let metadataText = metadataTemplate
  542. .replacingOccurrences(of: "__INTEGRATION__", with: metadataDeps)
  543. .replacingOccurrences(of: "__VERSIONS__", with: versionsText)
  544. do {
  545. try metadataText.write(to: zipDir.appendingPathComponent(Constants.ProjectPath.metadataName),
  546. atomically: true,
  547. encoding: .utf8)
  548. } catch {
  549. fatalError("Could not write METADATA to Zip directory: \(error)")
  550. }
  551. // Assemble the `README.md`.
  552. let readmePath = paths.templateDir.appendingPathComponent(Constants.ProjectPath.readmeName)
  553. try fileManager.copyItem(at: readmePath, to: zipDir.appendingPathComponent("README.md"))
  554. print("Contents of the packaged release were assembled at: \(zipDir)")
  555. return zipDir
  556. }
  557. /// Copies all frameworks from the `InstalledPod` (pulling from the `frameworkLocations`) and copy
  558. /// them to the destination directory.
  559. ///
  560. /// - Parameters:
  561. /// - installedPods: Names of all the pods installed, which will be used as a
  562. /// list to find out what frameworks to copy to the destination.
  563. /// - dir: Destination directory for all the frameworks.
  564. /// - frameworkLocations: A dictionary containing the pod name as the key and a location to
  565. /// the compiled frameworks.
  566. /// - ignoreFrameworks: A list of Pod
  567. /// - Returns: The filenames of the frameworks that were copied.
  568. /// - Throws: Various FileManager errors in case the copying fails, or an error if the framework
  569. /// doesn't exist in `frameworkLocations`.
  570. @discardableResult
  571. func copyFrameworks(fromPods installedPods: [String],
  572. toDirectory dir: URL,
  573. frameworkLocations: [String: [URL]],
  574. frameworksToIgnore: [String] = []) throws -> [String] {
  575. let fileManager = FileManager.default
  576. if !fileManager.directoryExists(at: dir) {
  577. try fileManager.createDirectory(at: dir, withIntermediateDirectories: false, attributes: nil)
  578. }
  579. // Keep track of the names of the frameworks copied over.
  580. var copiedFrameworkNames: [String] = []
  581. // Loop through each installedPod item and get the name so we can fetch the framework and copy
  582. // it to the destination directory.
  583. for podName in installedPods {
  584. // Skip the Firebase pod and specifically ignored frameworks.
  585. guard podName != "Firebase" else {
  586. continue
  587. }
  588. guard let xcframeworks = frameworkLocations[podName] else {
  589. let reason = "Unable to find frameworks for \(podName) in cache of frameworks built to " +
  590. "include in the Zip file for that framework's folder."
  591. let error = NSError(domain: "com.firebase.zipbuilder",
  592. code: 1,
  593. userInfo: [NSLocalizedDescriptionKey: reason])
  594. throw error
  595. }
  596. // Copy each of the frameworks over, unless it's explicitly ignored.
  597. for xcframework in xcframeworks {
  598. let xcframeworkName = xcframework.lastPathComponent
  599. let name = (xcframeworkName as NSString).deletingPathExtension
  600. if frameworksToIgnore.contains(name) {
  601. continue
  602. }
  603. let destination = dir.appendingPathComponent(xcframeworkName)
  604. try fileManager.copyItem(at: xcframework, to: destination)
  605. copiedFrameworkNames
  606. .append(xcframeworkName.replacingOccurrences(of: ".xcframework", with: ""))
  607. }
  608. }
  609. return copiedFrameworkNames
  610. }
  611. /// Copies required files from the Firebase pod (`Firebase.h`, `module.modulemap`, and `NOTICES`)
  612. /// into
  613. /// the given `zipDir`. Will cause a fatalError if anything fails since the zip file can't exist
  614. /// without these files.
  615. private func copyFirebasePodFiles(fromDir firebasePodDir: URL, to zipDir: URL) {
  616. let firebasePodFiles = ["NOTICES", "Sources/" + Constants.ProjectPath.firebaseHeader,
  617. "Sources/" + Constants.ProjectPath.modulemap]
  618. let firebaseFiles = firebasePodDir.appendingPathComponent("CoreOnly")
  619. let firebaseFilesToCopy = firebasePodFiles.map {
  620. firebaseFiles.appendingPathComponent($0)
  621. }
  622. // Copy each Firebase file.
  623. for file in firebaseFilesToCopy {
  624. // Each file should be copied to the destination project directory with the same name.
  625. let destination = zipDir.appendingPathComponent(file.lastPathComponent)
  626. do {
  627. if !FileManager.default.fileExists(atPath: destination.path) {
  628. print("Copying final distribution file \(file) to \(destination)...")
  629. try FileManager.default.copyItem(at: file, to: destination)
  630. }
  631. } catch {
  632. fatalError("Could not copy final distribution files to temporary directory before " +
  633. "building. Failed while attempting to copy \(file) to \(destination). \(error)")
  634. }
  635. }
  636. }
  637. /// Creates the String required for this pod to be added to the README. Creates a header and
  638. /// lists each framework in alphabetical order with the appropriate indentation, as well as a
  639. /// message about resources if they exist.
  640. ///
  641. /// - Parameters:
  642. /// - subspec: The subspec that requires documentation.
  643. /// - frameworks: All the frameworks required by the subspec.
  644. /// - includesResources: A flag to include or exclude the text for adding Resources.
  645. /// - Returns: A string with a header for the subspec name, and a list of frameworks required to
  646. /// integrate for the product to work. Formatted and ready for insertion into the
  647. /// README.
  648. private func dependencyString(for podName: String, in dir: URL, frameworks: [String]) -> String {
  649. var result = readmeHeader(podName: podName)
  650. for framework in frameworks.sorted() {
  651. // The .xcframework suffix has been stripped. The .framework suffix has not been.
  652. if framework.hasSuffix(".framework") {
  653. result += "- \(framework)\n"
  654. } else {
  655. result += "- \(framework).xcframework\n"
  656. }
  657. }
  658. result += "\n" // Necessary for Resource message to print properly in markdown.
  659. // Check if there is a Resources directory, and if so, add the disclaimer to the dependency
  660. // string. At this point, resources will be at the root of XCFrameworks.
  661. do {
  662. let fileManager = FileManager.default
  663. let resourceDirs = try fileManager.contentsOfDirectory(
  664. at: dir,
  665. includingPropertiesForKeys: [.isDirectoryKey]
  666. ).flatMap {
  667. try fileManager.contentsOfDirectory(
  668. at: $0,
  669. includingPropertiesForKeys: [.isDirectoryKey]
  670. )
  671. }.filter {
  672. $0.lastPathComponent == "Resources"
  673. }
  674. if !resourceDirs.isEmpty {
  675. result += Constants.resourcesRequiredText
  676. result += "\n" // Separate from next pod in listing for text version.
  677. }
  678. } catch {
  679. fatalError("""
  680. Tried to find Resources directory for \(podName) in order to build the README, but an error
  681. occurred: \(error).
  682. """)
  683. }
  684. return result
  685. }
  686. /// Describes the dependency on other frameworks for the README file.
  687. func readmeHeader(podName: String) -> String {
  688. var header = "## \(podName)"
  689. if !(podName == "FirebaseAnalytics" || podName == "GoogleSignIn") {
  690. header += " (~> FirebaseAnalytics)"
  691. }
  692. header += "\n"
  693. return header
  694. }
  695. /// Installs a subspec and attempts to copy all the frameworks required for it from
  696. /// `buildFramework` and puts them into a new directory in the `rootZipDir` matching the
  697. /// subspec's name.
  698. ///
  699. /// - Parameters:
  700. /// - subspec: The subspec to install and get the dependencies list.
  701. /// - projectDir: Root of the project containing the Podfile.
  702. /// - rootZipDir: The root directory to be turned into the Zip file.
  703. /// - builtFrameworks: All frameworks that have been built, with the framework name as the key
  704. /// and the framework's location as the value.
  705. /// - podsToIgnore: Pods to avoid copying, if any.
  706. /// - Throws: Throws various errors from copying frameworks.
  707. /// - Returns: The product directory containing all frameworks and the names of the frameworks
  708. /// that were copied for this subspec.
  709. @discardableResult
  710. func installAndCopyFrameworks(forPod podName: String,
  711. inFolder folder: String,
  712. withInstalledPods installedPods: [String: CocoaPodUtils.PodInfo],
  713. rootZipDir: URL,
  714. builtFrameworks: [String: [URL]],
  715. frameworksToIgnore: [String] = []) throws
  716. -> (productDir: URL, frameworks: [String]) {
  717. let podsToCopy = [podName] +
  718. CocoaPodUtils.transitiveMasterPodDependencies(for: podName, in: installedPods)
  719. // Remove any duplicates from the `podsToCopy` array. The easiest way to do this is to wrap it
  720. // in a set then back to an array.
  721. let dedupedPods = Array(Set(podsToCopy))
  722. // Copy the frameworks into the proper product directory.
  723. let productDir = rootZipDir.appendingPathComponent(folder)
  724. let namedFrameworks = try copyFrameworks(fromPods: dedupedPods,
  725. toDirectory: productDir,
  726. frameworkLocations: builtFrameworks,
  727. frameworksToIgnore: frameworksToIgnore)
  728. let copiedFrameworks = namedFrameworks.filter {
  729. // Skip frameworks that aren't contained in the "frameworksToIgnore" array and the Firebase
  730. // pod.
  731. !(frameworksToIgnore.contains($0) || $0 == "Firebase")
  732. }
  733. return (productDir, copiedFrameworks)
  734. }
  735. /// Creates the String that displays all the versions of each pod, in alphabetical order.
  736. ///
  737. /// - Parameter pods: All pods that were installed, with their versions.
  738. /// - Returns: A String to be added to the README.
  739. private func versionsString(for pods: [String: CocoaPodUtils.PodInfo]) -> String {
  740. // Get the longest name in order to generate padding with spaces so it looks nicer.
  741. let maxLength: Int = {
  742. guard let pod = pods.keys.max(by: { $0.count < $1.count }) else {
  743. // The longest pod as of this writing is 29 characters, if for whatever reason this fails
  744. // just assume 30 characters long.
  745. return 30
  746. }
  747. // Return room for a space afterwards.
  748. return pod.count + 1
  749. }()
  750. let header: String = {
  751. // Center the CocoaPods title within the spaces given. If there's an odd number of spaces, add
  752. // the extra space after the CocoaPods title.
  753. let cocoaPods = "CocoaPod"
  754. let spacesToPad = maxLength - cocoaPods.count
  755. let halfPadding = String(repeating: " ", count: spacesToPad / 2)
  756. // Start with the spaces padding, then add the CocoaPods title.
  757. var result = halfPadding + cocoaPods + halfPadding
  758. if spacesToPad % 2 != 0 {
  759. // Add an extra space since the padding isn't even
  760. result += " "
  761. }
  762. // Add the versioning text and return.
  763. result += "| Version\n"
  764. // Add a line underneath each.
  765. result += String(repeating: "-", count: maxLength) + "|" + String(repeating: "-", count: 9)
  766. result += "\n"
  767. return result
  768. }()
  769. // Sort the pods by name for a cleaner display.
  770. let sortedPods = pods.sorted { $0.key < $1.key }
  771. // Get the name and version of each pod, padding it along the way.
  772. var podVersions = ""
  773. for pod in sortedPods {
  774. // Insert the name and enough spaces to reach the end of the column.
  775. let podName = pod.key
  776. podVersions += podName + String(repeating: " ", count: maxLength - podName.count)
  777. // Add a pipe and the version.
  778. podVersions += "| " + pod.value.version + "\n"
  779. }
  780. return header + podVersions
  781. }
  782. // MARK: - Framework Generation
  783. /// Collects the .framework and .xcframeworks files from the binary pods. This will go through
  784. /// the contents of the directory and copy the .frameworks to a temporary directory. Returns a
  785. /// dictionary with the framework name for the key and all information for frameworks to install
  786. /// EXCLUDING resources, as they are handled later (if not included in the .framework file
  787. /// already).
  788. private func collectBinaryFrameworks(fromPod podName: String,
  789. podInfo: CocoaPodUtils.PodInfo) -> [URL] {
  790. // Verify the Pods folder exists and we can get the contents of it.
  791. let fileManager = FileManager.default
  792. // Create the temporary directory we'll be storing the build/assembled frameworks in, and remove
  793. // the Resources directory if it already exists.
  794. let binaryZipDir = fileManager.temporaryDirectory(withName: "binary_zip")
  795. do {
  796. try fileManager.createDirectory(at: binaryZipDir,
  797. withIntermediateDirectories: true,
  798. attributes: nil)
  799. } catch {
  800. fatalError("Cannot create temporary directory to store binary frameworks: \(error)")
  801. }
  802. var frameworks: [URL] = []
  803. // TODO: packageAllResources is disabled for binary frameworks since it's not needed for Firebase
  804. // and it does not yet support xcframeworks.
  805. // Package all resources into the frameworks since that's how Carthage needs it packaged.
  806. // do {
  807. // // TODO: Figure out if we need to exclude bundles here or not.
  808. // try ResourcesManager.packageAllResources(containedIn: podInfo.installedLocation)
  809. // } catch {
  810. // fatalError("Tried to package resources for \(podName) but it failed: \(error)")
  811. // }
  812. // Copy each of the frameworks to a known temporary directory and store the location.
  813. for framework in podInfo.binaryFrameworks {
  814. // Copy it to the temporary directory and save it to our list of frameworks.
  815. let zipLocation = binaryZipDir.appendingPathComponent(framework.lastPathComponent)
  816. // Remove the framework if it exists since it could be out of date.
  817. fileManager.removeIfExists(at: zipLocation)
  818. do {
  819. try fileManager.copyItem(at: framework, to: zipLocation)
  820. } catch {
  821. fatalError("Cannot copy framework at \(framework) while " +
  822. "attempting to generate frameworks. \(error)")
  823. }
  824. frameworks.append(zipLocation)
  825. }
  826. return frameworks
  827. }
  828. }