main.swift 12 KB

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