main.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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: "12.0", help: ArgumentHelp("The minimum supported iOS version."))
  89. var minimumIOSVersion: String
  90. /// The minimum macOS Version to build for.
  91. @Option(default: "10.15", help: ArgumentHelp("The minimum supported macOS version."))
  92. var minimumMacOSVersion: String
  93. /// The minimum tvOS Version to build for.
  94. @Option(default: "13.0", help: ArgumentHelp("The minimum supported tvOS version."))
  95. var minimumTVOSVersion: String
  96. /// The minimum watchOS Version to build for.
  97. @Option(default: "7.0", help: ArgumentHelp("The minimum supported watchOS version."))
  98. var minimumWatchOSVersion: String
  99. /// The list of platforms to build for.
  100. @Option(parsing: .upToNextOption,
  101. help: ArgumentHelp("""
  102. The list of platforms to build for. The default list is \
  103. \(Platform.allCases.map { $0.name }).
  104. """))
  105. var platforms: [Platform]
  106. // MARK: - Specify Pods
  107. @Option(parsing: .upToNextOption,
  108. help: ArgumentHelp("List of pods to build."))
  109. var pods: [String]
  110. @Option(help: ArgumentHelp("""
  111. The path to a JSON file of the pods (with optional version) to package into a zip.
  112. """),
  113. transform: { str in
  114. // Get pods, with optional version, from the JSON file specified
  115. let url = URL(fileURLWithPath: str)
  116. let jsonData = try Data(contentsOf: url)
  117. return try JSONDecoder().decode([CocoaPodUtils.VersionedPod].self, from: jsonData)
  118. })
  119. var zipPods: [CocoaPodUtils.VersionedPod]?
  120. // MARK: - Filesystem Paths
  121. /// Path to override podspec search with local podspec.
  122. @Option(help: ArgumentHelp("Path to override podspec search with local podspec."),
  123. transform: URL.init(fileURLWithPath:))
  124. var localPodspecPath: URL?
  125. /// The path to the directory containing the blank xcodeproj and Info.plist for building source
  126. /// based frameworks.
  127. @Option(help: ArgumentHelp("""
  128. The root directory for build artifacts. If `nil`, a temporary directory will be used.
  129. """),
  130. transform: URL.init(fileURLWithPath:))
  131. var buildRoot: URL?
  132. /// The directory to copy the built Zip file to. If this is not set, the path to the Zip file will
  133. /// be logged to the console.
  134. @Option(help: ArgumentHelp("""
  135. The directory to copy the built Zip file to. If this is not set, the path to the Zip \
  136. file will be logged to the console.
  137. """),
  138. transform: URL.init(fileURLWithPath:))
  139. var outputDir: URL?
  140. // MARK: - Validation
  141. mutating func validate() throws {
  142. // Validate the output directory if provided.
  143. if let outputDir = outputDir, !FileManager.default.directoryExists(at: outputDir) {
  144. throw ValidationError("`output-dir` passed in does not exist. Value: \(outputDir)")
  145. }
  146. // Validate the buildRoot directory if provided.
  147. if let buildRoot = buildRoot, !FileManager.default.directoryExists(at: buildRoot) {
  148. throw ValidationError("`build-root` passed in does not exist. Value: \(buildRoot)")
  149. }
  150. if let localPodspecPath = localPodspecPath,
  151. !FileManager.default.directoryExists(at: localPodspecPath) {
  152. throw ValidationError("""
  153. `local-podspec-path` pass in does not exist. Value: \(localPodspecPath)
  154. """)
  155. }
  156. // Validate that Firebase builds are including dependencies.
  157. if !buildDependencies, zipPods == nil, pods.count == 0 {
  158. throw ValidationError("""
  159. The `enable-build-dependencies` option cannot be false unless a list of pods is \
  160. specified with the `zip-pods` or the `pods` option.
  161. """)
  162. }
  163. }
  164. // MARK: - Running the tool
  165. func run() throws {
  166. // Keep timing for how long it takes to build the zip file for information purposes.
  167. let buildStart = Date()
  168. var cocoaPodsUpdateMessage = ""
  169. // Do a `pod update` if requested.
  170. if updatePodRepo {
  171. CocoaPodUtils.updateRepos()
  172. cocoaPodsUpdateMessage =
  173. "CocoaPods took \(-buildStart.timeIntervalSinceNow) seconds to update."
  174. }
  175. // Register the build root if it was passed in.
  176. if let buildRoot = buildRoot {
  177. FileManager.registerBuildRoot(buildRoot: buildRoot.standardizedFileURL)
  178. }
  179. // Get the repoDir by deleting four path components from this file to the repo root.
  180. let repoDir = URL(fileURLWithPath: #file)
  181. .deletingLastPathComponent().deletingLastPathComponent()
  182. .deletingLastPathComponent().deletingLastPathComponent()
  183. // Validate the repoDir exists, as well as the templateDir.
  184. guard FileManager.default.directoryExists(at: repoDir) else {
  185. fatalError("Failed to find the repo root at \(repoDir).")
  186. }
  187. // Validate the templateDir exists.
  188. let templateDir = ZipBuilder.FilesystemPaths.templateDir(fromRepoDir: repoDir)
  189. guard FileManager.default.directoryExists(at: templateDir) else {
  190. fatalError("Missing template inside of the repo. \(templateDir) does not exist.")
  191. }
  192. // Update iOS target platforms if `--include-catalyst` was specified.
  193. if !includeCatalyst {
  194. SkipCatalyst.set()
  195. }
  196. // 32 bit iOS slices should only be built if the minimum iOS version is less than 11.
  197. guard let minVersion = Float(minimumIOSVersion) else {
  198. fatalError("Invalid minimum iOS version: \(minimumIOSVersion)")
  199. }
  200. if minVersion < 11.0 {
  201. Included32BitIOS.set()
  202. }
  203. let paths = ZipBuilder.FilesystemPaths(repoDir: repoDir,
  204. buildRoot: buildRoot,
  205. outputDir: outputDir,
  206. localPodspecPath: localPodspecPath,
  207. logsOutputDir: outputDir?
  208. .appendingPathComponent("build_logs"))
  209. // Populate the platforms list if it's empty. This isn't a great spot, but the argument parser
  210. // can't specify a default for arrays.
  211. let platformsToBuild = !platforms.isEmpty ? platforms : Platform.allCases
  212. let builder = ZipBuilder(paths: paths,
  213. platforms: platformsToBuild,
  214. dynamicFrameworks: dynamic,
  215. customSpecRepos: customSpecRepos)
  216. if let outputDir = outputDir {
  217. do {
  218. // Clear out the output directory if it exists.
  219. FileManager.default.removeIfExists(at: outputDir)
  220. try FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true)
  221. }
  222. }
  223. var podsToBuild = zipPods
  224. if pods.count > 0 {
  225. guard podsToBuild == nil else {
  226. fatalError("Only one of `--zipPods` or `--pods` can be specified.")
  227. }
  228. podsToBuild = pods.map { CocoaPodUtils.VersionedPod(name: $0, version: nil) }
  229. }
  230. if let podsToBuild = podsToBuild {
  231. // Set the platform minimum versions.
  232. PlatformMinimum.initialize(ios: minimumIOSVersion,
  233. macos: minimumMacOSVersion,
  234. tvos: minimumTVOSVersion,
  235. watchos: minimumWatchOSVersion)
  236. let (installedPods, frameworks, _) =
  237. builder.buildAndAssembleZip(podsToInstall: podsToBuild,
  238. includeCarthage: false,
  239. includeDependencies: buildDependencies)
  240. let staging = FileManager.default.temporaryDirectory(withName: "Binaries")
  241. try builder.copyFrameworks(fromPods: Array(installedPods.keys), toDirectory: staging,
  242. frameworkLocations: frameworks)
  243. let zipped = Zip.zipContents(ofDir: staging, name: "Frameworks.zip")
  244. print(zipped.absoluteString)
  245. if let outputDir = outputDir {
  246. let outputFile = outputDir.appendingPathComponent("Frameworks.zip")
  247. try FileManager.default.copyItem(at: zipped, to: outputFile)
  248. print("Success! Zip file can be found at \(outputFile.path)")
  249. } else {
  250. // Move zip to parent directory so it doesn't get removed with other artifacts.
  251. let parentLocation =
  252. zipped.deletingLastPathComponent().deletingLastPathComponent()
  253. .appendingPathComponent(zipped.lastPathComponent)
  254. // Clear out the output file if it exists.
  255. FileManager.default.removeIfExists(at: parentLocation)
  256. do {
  257. try FileManager.default.moveItem(at: zipped, to: parentLocation)
  258. } catch {
  259. fatalError("Could not move Zip file to output directory: \(error)")
  260. }
  261. print("Success! Zip file can be found at \(parentLocation.path)")
  262. }
  263. } else {
  264. // Do a Firebase Zip Release package build.
  265. // For the Firebase zip distribution, we disable version checking at install time by
  266. // setting a high version to install. The minimum versions are controlled by each individual
  267. // pod's podspec options.
  268. PlatformMinimum.useRecentVersions()
  269. let jsonDir = paths.repoDir.appendingPathComponents(["ReleaseTooling", "CarthageJSON"])
  270. let carthageOptions = CarthageBuildOptions(jsonDir: jsonDir,
  271. isVersionCheckEnabled: carthageVersionCheck)
  272. FirebaseBuilder(zipBuilder: builder).build(templateDir: paths.templateDir,
  273. carthageBuildOptions: carthageOptions)
  274. }
  275. if !keepBuildArtifacts {
  276. let tempDir = FileManager.default.temporaryDirectory(withName: "placeholder")
  277. FileManager.default.removeIfExists(at: tempDir.deletingLastPathComponent())
  278. }
  279. // Get the time since the start of the build to get the full time.
  280. let secondsSinceStart = -Int(buildStart.timeIntervalSinceNow)
  281. print("""
  282. Time profile:
  283. It took \(secondsSinceStart) seconds (~\(secondsSinceStart / 60)m) to build the zip file.
  284. \(cocoaPodsUpdateMessage)
  285. """)
  286. }
  287. }
  288. ZipBuilderTool.main()