ZipBuilder.swift 35 KB

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