main.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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 ArgumentParser
  17. import Foundation
  18. // Enables parsing of URLs as command line arguments.
  19. extension URL: ExpressibleByArgument {
  20. public init?(argument: String) {
  21. self.init(string: argument)
  22. }
  23. }
  24. // Enables parsing of platforms as a command line argument.
  25. extension Platform: ExpressibleByArgument {
  26. public init?(argument: String) {
  27. // Look for a match in SDK name.
  28. for platform in Platform.allCases {
  29. if argument == platform.name {
  30. self = platform
  31. return
  32. }
  33. }
  34. return nil
  35. }
  36. }
  37. struct ZipBuilderTool: ParsableCommand {
  38. // MARK: - Boolean Flags
  39. /// Enables or disables building arm64 slices for Apple silicon (simulator, etc).
  40. @Flag(default: true,
  41. inversion: .prefixedEnableDisable,
  42. help: ArgumentHelp("Enables or disables building arm64 slices for Apple silicon Macs."))
  43. var appleSiliconSupport: Bool
  44. /// Enables or disables building dependencies of pods.
  45. @Flag(default: true,
  46. inversion: .prefixedEnableDisable,
  47. help: ArgumentHelp("Whether or not to build dependencies of requested pods."))
  48. var buildDependencies: Bool
  49. /// Flag to enable or disable Carthage version checks. Skipping the check can speed up dev
  50. /// iterations.
  51. @Flag(default: true,
  52. // Allows `--enable-carthage-version-check` and `--disable-carthage-version-check`.
  53. inversion: FlagInversion.prefixedEnableDisable,
  54. help: ArgumentHelp("A flag for enabling or disabling versions checks for Carthage builds."))
  55. var carthageVersionCheck: Bool
  56. /// A flag that indicates to build dynamic library frameworks. The default is false and static
  57. /// linkage.
  58. @Flag(default: false,
  59. inversion: .prefixedNo,
  60. help: ArgumentHelp("A flag specifying to build dynamic library frameworks."))
  61. var dynamic: Bool
  62. @Flag(default: false,
  63. inversion: .prefixedNo,
  64. help: ArgumentHelp("A flag to indicate keeping (not deleting) the build artifacts."))
  65. var keepBuildArtifacts: Bool
  66. /// Flag to skip building the Catalyst slices.
  67. @Flag(default: true,
  68. inversion: .prefixedNo,
  69. help: ArgumentHelp("A flag to indicate skip building the Catalyst slice."))
  70. var includeCatalyst: Bool
  71. /// Flag to run `pod repo update` and `pod cache clean --all`.
  72. @Flag(default: true,
  73. inversion: .prefixedNo,
  74. help: ArgumentHelp("""
  75. A flag to run `pod repo update` and `pod cache clean -all` before building the "zip file".
  76. """))
  77. var updatePodRepo: Bool
  78. // MARK: - CocoaPods Arguments
  79. /// Custom CocoaPods spec repos to be used.
  80. @Option(parsing: .upToNextOption,
  81. help: ArgumentHelp("""
  82. A list of private CocoaPod Spec repos. If not provided, the tool will only use the \
  83. CocoaPods master repo.
  84. """))
  85. var customSpecRepos: [URL]
  86. // MARK: - Platform Arguments
  87. /// The minimum iOS Version to build for.
  88. @Option(default: "11.0", help: ArgumentHelp("The minimum supported iOS version."))
  89. var minimumIOSVersion: String
  90. /// The minimum macOS Version to build for.
  91. @Option(default: "10.13", help: ArgumentHelp("The minimum supported macOS version."))
  92. var minimumMacOSVersion: String
  93. /// The minimum tvOS Version to build for.
  94. @Option(default: "11.0", help: ArgumentHelp("The minimum supported tvOS version."))
  95. var minimumTVOSVersion: String
  96. /// The list of platforms to build for.
  97. @Option(parsing: .upToNextOption,
  98. help: ArgumentHelp("""
  99. The list of platforms to build for. The default list is \
  100. \(Platform.allCases.map { $0.name }).
  101. """))
  102. var platforms: [Platform]
  103. // MARK: - Specify Pods
  104. @Option(parsing: .upToNextOption,
  105. help: ArgumentHelp("List of pods to build."))
  106. var pods: [String]
  107. @Option(help: ArgumentHelp("""
  108. The path to a JSON file of the pods (with optional version) to package into a zip.
  109. """),
  110. transform: { str in
  111. // Get pods, with optional version, from the JSON file specified
  112. let url = URL(fileURLWithPath: str)
  113. let jsonData = try Data(contentsOf: url)
  114. return try JSONDecoder().decode([CocoaPodUtils.VersionedPod].self, from: jsonData)
  115. })
  116. var zipPods: [CocoaPodUtils.VersionedPod]?
  117. // MARK: - Filesystem Paths
  118. /// Path to override podspec search with local podspec.
  119. @Option(help: ArgumentHelp("Path to override podspec search with local podspec."),
  120. transform: URL.init(fileURLWithPath:))
  121. var localPodspecPath: URL?
  122. /// The path to the directory containing the blank xcodeproj and Info.plist for building source
  123. /// based frameworks.
  124. @Option(help: ArgumentHelp("""
  125. The root directory for build artifacts. If `nil`, a temporary directory will be used.
  126. """),
  127. transform: URL.init(fileURLWithPath:))
  128. var buildRoot: URL?
  129. /// The directory to copy the built Zip file to. If this is not set, the path to the Zip file will
  130. /// be logged to the console.
  131. @Option(help: ArgumentHelp("""
  132. The directory to copy the built Zip file to. If this is not set, the path to the Zip \
  133. file will be logged to the console.
  134. """),
  135. transform: URL.init(fileURLWithPath:))
  136. var outputDir: URL?
  137. // MARK: - Validation
  138. mutating func validate() throws {
  139. // Validate the output directory if provided.
  140. if let outputDir = outputDir, !FileManager.default.directoryExists(at: outputDir) {
  141. throw ValidationError("`output-dir` passed in does not exist. Value: \(outputDir)")
  142. }
  143. // Validate the buildRoot directory if provided.
  144. if let buildRoot = buildRoot, !FileManager.default.directoryExists(at: buildRoot) {
  145. throw ValidationError("`build-root` passed in does not exist. Value: \(buildRoot)")
  146. }
  147. if let localPodspecPath = localPodspecPath,
  148. !FileManager.default.directoryExists(at: localPodspecPath) {
  149. throw ValidationError("""
  150. `local-podspec-path` pass in does not exist. Value: \(localPodspecPath)
  151. """)
  152. }
  153. // Validate that Firebase builds are including dependencies.
  154. if !buildDependencies, zipPods == nil, pods.count == 0 {
  155. throw ValidationError("""
  156. The `enable-build-dependencies` option cannot be false unless a list of pods is \
  157. specified with the `zip-pods` or the `pods` option.
  158. """)
  159. }
  160. }
  161. // MARK: - Running the tool
  162. func run() throws {
  163. // Keep timing for how long it takes to build the zip file for information purposes.
  164. let buildStart = Date()
  165. var cocoaPodsUpdateMessage = ""
  166. // Do a `pod update` if requested.
  167. if updatePodRepo {
  168. CocoaPodUtils.updateRepos()
  169. cocoaPodsUpdateMessage =
  170. "CocoaPods took \(-buildStart.timeIntervalSinceNow) seconds to update."
  171. }
  172. // Register the build root if it was passed in.
  173. if let buildRoot = buildRoot {
  174. FileManager.registerBuildRoot(buildRoot: buildRoot.standardizedFileURL)
  175. }
  176. // Get the repoDir by deleting four path components from this file to the repo root.
  177. let repoDir = URL(fileURLWithPath: #file)
  178. .deletingLastPathComponent().deletingLastPathComponent()
  179. .deletingLastPathComponent().deletingLastPathComponent()
  180. // Validate the repoDir exists, as well as the templateDir.
  181. guard FileManager.default.directoryExists(at: repoDir) else {
  182. fatalError("Failed to find the repo root at \(repoDir).")
  183. }
  184. // Validate the templateDir exists.
  185. let templateDir = ZipBuilder.FilesystemPaths.templateDir(fromRepoDir: repoDir)
  186. guard FileManager.default.directoryExists(at: templateDir) else {
  187. fatalError("Missing template inside of the repo. \(templateDir) does not exist.")
  188. }
  189. // Update iOS target platforms if `--include-catalyst` was specified.
  190. if !includeCatalyst {
  191. SkipCatalyst.set()
  192. }
  193. // 32 bit iOS slices should only be built if the minimum iOS version is less than 11.
  194. guard let minVersion = Float(minimumIOSVersion) else {
  195. fatalError("Invalid minimum iOS version: \(minimumIOSVersion)")
  196. }
  197. if minVersion < 11.0 {
  198. Included32BitIOS.set()
  199. }
  200. let paths = ZipBuilder.FilesystemPaths(repoDir: repoDir,
  201. buildRoot: buildRoot,
  202. outputDir: outputDir,
  203. localPodspecPath: localPodspecPath,
  204. logsOutputDir: outputDir?
  205. .appendingPathComponent("build_logs"))
  206. // Populate the platforms list if it's empty. This isn't a great spot, but the argument parser
  207. // can't specify a default for arrays.
  208. let platformsToBuild = !platforms.isEmpty ? platforms : Platform.allCases
  209. let builder = ZipBuilder(paths: paths,
  210. platforms: platformsToBuild,
  211. dynamicFrameworks: dynamic,
  212. customSpecRepos: customSpecRepos)
  213. if let outputDir = outputDir {
  214. do {
  215. // Clear out the output directory if it exists.
  216. FileManager.default.removeIfExists(at: outputDir)
  217. try FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true)
  218. }
  219. }
  220. var podsToBuild = zipPods
  221. if pods.count > 0 {
  222. guard podsToBuild == nil else {
  223. fatalError("Only one of `--zipPods` or `--pods` can be specified.")
  224. }
  225. podsToBuild = pods.map { CocoaPodUtils.VersionedPod(name: $0, version: nil) }
  226. }
  227. if let podsToBuild = podsToBuild {
  228. // Set the platform minimum versions.
  229. PlatformMinimum.initialize(ios: minimumIOSVersion,
  230. macos: minimumMacOSVersion,
  231. tvos: minimumTVOSVersion)
  232. let (installedPods, frameworks, _) =
  233. builder.buildAndAssembleZip(podsToInstall: podsToBuild,
  234. includeCarthage: false,
  235. includeDependencies: buildDependencies)
  236. let staging = FileManager.default.temporaryDirectory(withName: "Binaries")
  237. try builder.copyFrameworks(fromPods: Array(installedPods.keys), toDirectory: staging,
  238. frameworkLocations: frameworks)
  239. let zipped = Zip.zipContents(ofDir: staging, name: "Frameworks.zip")
  240. print(zipped.absoluteString)
  241. if let outputDir = outputDir {
  242. let outputFile = outputDir.appendingPathComponent("Frameworks.zip")
  243. try FileManager.default.copyItem(at: zipped, to: outputFile)
  244. print("Success! Zip file can be found at \(outputFile.path)")
  245. } else {
  246. // Move zip to parent directory so it doesn't get removed with other artifacts.
  247. let parentLocation =
  248. zipped.deletingLastPathComponent().deletingLastPathComponent()
  249. .appendingPathComponent(zipped.lastPathComponent)
  250. // Clear out the output file if it exists.
  251. FileManager.default.removeIfExists(at: parentLocation)
  252. do {
  253. try FileManager.default.moveItem(at: zipped, to: parentLocation)
  254. } catch {
  255. fatalError("Could not move Zip file to output directory: \(error)")
  256. }
  257. print("Success! Zip file can be found at \(parentLocation.path)")
  258. }
  259. } else {
  260. // Do a Firebase Zip Release package build.
  261. // For the Firebase zip distribution, we disable version checking at install time by
  262. // setting a high version to install. The minimum versions are controlled by each individual
  263. // pod's podspec options.
  264. PlatformMinimum.useRecentVersions()
  265. let jsonDir = paths.repoDir.appendingPathComponents(["ReleaseTooling", "CarthageJSON"])
  266. let carthageOptions = CarthageBuildOptions(jsonDir: jsonDir,
  267. isVersionCheckEnabled: carthageVersionCheck)
  268. FirebaseBuilder(zipBuilder: builder).build(templateDir: paths.templateDir,
  269. carthageBuildOptions: carthageOptions)
  270. }
  271. if !keepBuildArtifacts {
  272. let tempDir = FileManager.default.temporaryDirectory(withName: "placeholder")
  273. FileManager.default.removeIfExists(at: tempDir.deletingLastPathComponent())
  274. }
  275. // Get the time since the start of the build to get the full time.
  276. let secondsSinceStart = -Int(buildStart.timeIntervalSinceNow)
  277. print("""
  278. Time profile:
  279. It took \(secondsSinceStart) seconds (~\(secondsSinceStart / 60)m) to build the zip file.
  280. \(cocoaPodsUpdateMessage)
  281. """)
  282. }
  283. }
  284. ZipBuilderTool.main()