CarthageUtils.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  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 CommonCrypto
  17. import Foundation
  18. import Utils
  19. struct CarthageBuildOptions {
  20. /// Location of directory containing all JSON Carthage manifests.
  21. let jsonDir: URL
  22. /// Version checking flag.
  23. let isVersionCheckEnabled: Bool
  24. }
  25. /// Carthage related utility functions. The enum type is used as a namespace here instead of having
  26. /// root functions, and no cases should be added to it.
  27. enum CarthageUtils {}
  28. extension CarthageUtils {
  29. /// Package all required files for a Carthage release.
  30. ///
  31. /// - Parameters:
  32. /// - templateDir: The template project directory, contains the dummy Firebase library.
  33. /// - carthageJSONDir: Location of directory containing all JSON Carthage manifests.
  34. /// - artifacts: Release Artifacts from build.
  35. /// - options: Carthage specific options for the build.
  36. /// - Returns: The path to the root of the Carthage installation.
  37. static func packageCarthageRelease(templateDir: URL,
  38. artifacts: ZipBuilder.ReleaseArtifacts,
  39. options: CarthageBuildOptions) -> URL? {
  40. guard let zipLocation = artifacts.carthageDir else { return nil }
  41. do {
  42. print("Creating Carthage release...")
  43. let carthagePath =
  44. zipLocation.deletingLastPathComponent().appendingPathComponent("carthage_build")
  45. // Create a copy of the release directory since we'll be modifying it.
  46. let fileManager = FileManager.default
  47. fileManager.removeIfExists(at: carthagePath)
  48. try fileManager.copyItem(at: zipLocation, to: carthagePath)
  49. // Package the Carthage distribution with the current directory structure.
  50. let carthageDir = zipLocation.deletingLastPathComponent().appendingPathComponent("carthage")
  51. fileManager.removeIfExists(at: carthageDir)
  52. let output = carthageDir.appendingPathComponents([artifacts.firebaseVersion, "latest"])
  53. try fileManager.createDirectory(at: output, withIntermediateDirectories: true)
  54. generateCarthageRelease(fromPackagedDir: carthagePath,
  55. templateDir: templateDir,
  56. jsonDir: options.jsonDir,
  57. artifacts: artifacts,
  58. outputDir: output,
  59. versionCheckEnabled: options.isVersionCheckEnabled)
  60. // Remove the duplicated Carthage build directory.
  61. fileManager.removeIfExists(at: carthagePath)
  62. print("Done creating Carthage release! Files written to \(output)")
  63. // Save the directory for later copying.
  64. return carthageDir
  65. } catch {
  66. fatalError("Could not copy output directory for Carthage build: \(error)")
  67. }
  68. }
  69. /// Generates all required files for a Carthage release.
  70. ///
  71. /// - Parameters:
  72. /// - packagedDir: The packaged directory assembled for the Carthage distribution.
  73. /// - templateDir: The template project directory, contains the dummy Firebase library.
  74. /// - jsonDir: Location of directory containing all JSON Carthage manifests.
  75. /// - firebaseVersion: The version of the Firebase pod.
  76. /// - coreDiagnosticsPath: The path to the Core Diagnostics framework built for Carthage.
  77. /// - outputDir: The directory where all artifacts should be created.
  78. private static func generateCarthageRelease(fromPackagedDir packagedDir: URL,
  79. templateDir: URL,
  80. jsonDir: URL,
  81. artifacts: ZipBuilder.ReleaseArtifacts,
  82. outputDir: URL,
  83. versionCheckEnabled: Bool) {
  84. let directories: [String]
  85. do {
  86. directories = try FileManager.default.contentsOfDirectory(atPath: packagedDir.path)
  87. } catch {
  88. fatalError("Could not get contents of Firebase directory to package Carthage build. \(error)")
  89. }
  90. let firebaseVersion = artifacts.firebaseVersion
  91. // Loop through each directory available and package it as a separate Zip file.
  92. for product in directories {
  93. let fullPath = packagedDir.appendingPathComponent(product)
  94. guard FileManager.default.isDirectory(at: fullPath) else { continue }
  95. // Abort Carthage generation if there are any xcframeworks.
  96. do {
  97. let files = try FileManager.default.contentsOfDirectory(at: fullPath,
  98. includingPropertiesForKeys: nil)
  99. let xcfFiles = files.filter { $0.pathExtension == "xcframework" }
  100. if xcfFiles.count > 0 {
  101. print("Skipping Carthage generation for \(product) since it includes xcframeworks.")
  102. continue
  103. }
  104. } catch {
  105. fatalError("Failed to get contents of \(fullPath).")
  106. }
  107. // Parse the JSON file, ensure that we're not trying to overwrite a release.
  108. var jsonManifest = parseJSONFile(fromDir: jsonDir, product: product)
  109. if versionCheckEnabled {
  110. guard jsonManifest[firebaseVersion] == nil else {
  111. print("Carthage release for \(product) \(firebaseVersion) already exists - skipping.")
  112. continue
  113. }
  114. }
  115. // Analytics includes all the Core frameworks and Firebase module, do extra work to package
  116. // it.
  117. if product == "FirebaseAnalytics" {
  118. createFirebaseFramework(version: firebaseVersion,
  119. inDir: fullPath,
  120. rootDir: packagedDir,
  121. templateDir: templateDir)
  122. // Copy the NOTICES file from FirebaseCore.
  123. let noticesName = "NOTICES"
  124. let coreNotices = fullPath.appendingPathComponents(["FirebaseCore.framework", noticesName])
  125. let noticesPath = packagedDir.appendingPathComponent(noticesName)
  126. do {
  127. try FileManager.default.copyItem(at: noticesPath, to: coreNotices)
  128. } catch {
  129. fatalError("Could not copy \(noticesName) to FirebaseCore for Carthage build. \(error)")
  130. }
  131. }
  132. // Hash the contents of the directory to get a unique name for Carthage.
  133. let hash: String
  134. do {
  135. // Only use the first 16 characters, that's what we did before.
  136. let fullHash = try HashCalculator.sha256Contents(ofDir: fullPath)
  137. hash = String(fullHash.prefix(16))
  138. } catch {
  139. fatalError("Could not hash contents of \(product) for Carthage build. \(error)")
  140. }
  141. // Generate the zip name to write to the manifest as well as the actual zip file.
  142. let zipName = "\(product)-\(hash).zip"
  143. let productZip = outputDir.appendingPathComponent(zipName)
  144. let zipped = Zip.zipContents(ofDir: fullPath, name: zipName)
  145. do {
  146. try FileManager.default.moveItem(at: zipped, to: productZip)
  147. } catch {
  148. fatalError("Could not move packaged zip file for \(product) during Carthage build. " +
  149. "\(error)")
  150. }
  151. // Force unwrapping because this can't fail at this point.
  152. let url =
  153. URL(string: "https://dl.google.com/dl/firebase/ios/carthage/\(firebaseVersion)/\(zipName)")!
  154. jsonManifest[firebaseVersion] = url
  155. // Write the updated manifest.
  156. let manifestPath = outputDir.appendingPathComponent(getJSONFileName(product: product))
  157. // Unfortunate workaround: There's a strange issue when serializing to JSON on macOS: URLs
  158. // will have the `/` escaped leading to an odd JSON output. Instead, let's output the
  159. // dictionary to a String and write that to disk. When Xcode 11 can be used, use a JSON
  160. // encoder with the `.withoutEscapingSlashes` option on `outputFormatting` like this:
  161. // do {
  162. // let encoder = JSONEncoder()
  163. // encoder.outputFormatting = [.sortedKeys, .prettyPrinted, .withoutEscapingSlashes]
  164. // let encodedManifest = try encoder.encode(jsonManifest)
  165. // catch { /* handle error */ }
  166. // Sort the manifest based on the key, $0 and $1 are the parameters and 0 is the first item in
  167. // the tuple (key).
  168. let sortedManifest = jsonManifest.sorted { $0.0 < $1.0 }
  169. // Generate the JSON format and combine all the lines afterwards.
  170. let manifestLines = sortedManifest.map { (version, url) -> String in
  171. // Two spaces at the beginning of the String are intentional.
  172. " \"\(version)\": \"\(url.absoluteString)\""
  173. }
  174. // Join all the lines with a comma and newline to make things easier to read.
  175. let contents = "{\n" + manifestLines.joined(separator: ",\n") + "\n}\n"
  176. guard let encodedManifest = contents.data(using: .utf8) else {
  177. fatalError("Could not encode Carthage JSON manifest for \(product) - UTF8 encoding failed.")
  178. }
  179. do {
  180. try encodedManifest.write(to: manifestPath)
  181. print("Successfully written Carthage JSON manifest for \(product).")
  182. } catch {
  183. fatalError("Could not write new Carthage JSON manifest to disk for \(product). \(error)")
  184. }
  185. }
  186. }
  187. /// Creates a fake Firebase.framework to use the module for `import Firebase` compatibility.
  188. ///
  189. /// - Parameters:
  190. /// - version: Firebase version.
  191. /// - destination: The destination directory for the Firebase framework.
  192. /// - rootDir: The root directory that contains other required files (like the Firebase header).
  193. /// - templateDir: The template directory containing the dummy Firebase library.
  194. private static func createFirebaseFramework(version: String,
  195. inDir destination: URL,
  196. rootDir: URL,
  197. templateDir: URL) {
  198. // Local FileManager for better readability.
  199. let fm = FileManager.default
  200. let frameworkDir = destination.appendingPathComponent("Firebase.framework")
  201. let headersDir = frameworkDir.appendingPathComponent("Headers")
  202. let modulesDir = frameworkDir.appendingPathComponent("Modules")
  203. // Create all the required directories.
  204. do {
  205. try fm.createDirectory(at: headersDir, withIntermediateDirectories: true)
  206. try fm.createDirectory(at: modulesDir, withIntermediateDirectories: true)
  207. } catch {
  208. fatalError("Could not create directories for Firebase framework in Carthage. \(error)")
  209. }
  210. // Copy the Firebase header and modulemap that was created in the Zip file.
  211. let header = rootDir.appendingPathComponent(Constants.ProjectPath.firebaseHeader)
  212. do {
  213. try fm.copyItem(at: header, to: headersDir.appendingPathComponent(header.lastPathComponent))
  214. // Generate the new modulemap since it differs from the Zip modulemap.
  215. let carthageModulemap = """
  216. framework module Firebase {
  217. header "Firebase.h"
  218. export *
  219. }
  220. """
  221. let modulemapPath = modulesDir.appendingPathComponent("module.modulemap")
  222. try carthageModulemap.write(to: modulemapPath, atomically: true, encoding: .utf8)
  223. } catch {
  224. fatalError("Couldn't write required files for Firebase framework in Carthage. \(error)")
  225. }
  226. // Copy the dummy Firebase library.
  227. let dummyLib = templateDir.appendingPathComponent(Constants.ProjectPath.dummyFirebaseLib)
  228. do {
  229. try fm.copyItem(at: dummyLib, to: frameworkDir.appendingPathComponent("Firebase"))
  230. } catch {
  231. fatalError("Couldn't copy dummy library for Firebase framework in Carthage. \(error)")
  232. }
  233. // Write the Info.plist.
  234. generatePlistContents(forName: "Firebase", withVersion: version, to: frameworkDir)
  235. }
  236. static func generatePlistContents(forName name: String,
  237. withVersion version: String,
  238. to location: URL) {
  239. let ver = version.components(separatedBy: "-")[0] // remove any version suffix.
  240. let plist: [String: String] = ["CFBundleIdentifier": "com.firebase.Firebase-\(name)",
  241. "CFBundleInfoDictionaryVersion": "6.0",
  242. "CFBundlePackageType": "FMWK",
  243. "CFBundleVersion": ver,
  244. "DTSDKName": "iphonesimulator11.2",
  245. "CFBundleExecutable": name,
  246. "CFBundleName": name]
  247. // Generate the data for an XML based plist.
  248. let encoder = PropertyListEncoder()
  249. encoder.outputFormat = .xml
  250. do {
  251. let data = try encoder.encode(plist)
  252. try data.write(to: location.appendingPathComponent("Info.plist"))
  253. } catch {
  254. fatalError("Failed to create Info.plist for \(name) during Carthage build: \(error)")
  255. }
  256. }
  257. /// Parses the JSON manifest for the particular product.
  258. ///
  259. /// - Parameters:
  260. /// - dir: The directory containing all JSON manifests.
  261. /// - product: The name of the Firebase product.
  262. /// - Returns: A dictionary with versions as keys and URLs as values.
  263. private static func parseJSONFile(fromDir dir: URL, product: String) -> [String: URL] {
  264. // Parse the JSON manifest.
  265. let jsonFileName = getJSONFileName(product: product)
  266. let jsonFile = dir.appendingPathComponent(jsonFileName)
  267. guard FileManager.default.fileExists(atPath: jsonFile.path) else {
  268. fatalError("Could not find JSON manifest for \(product) during Carthage build. " +
  269. "Location: \(jsonFile)")
  270. }
  271. let jsonData: Data
  272. do {
  273. jsonData = try Data(contentsOf: jsonFile)
  274. } catch {
  275. fatalError("Could not read JSON manifest for \(product) during Carthage build. " +
  276. "Location: \(jsonFile). \(error)")
  277. }
  278. // Get a dictionary out of the file.
  279. let decoder = JSONDecoder()
  280. do {
  281. let productReleases = try decoder.decode([String: URL].self, from: jsonData)
  282. return productReleases
  283. } catch {
  284. fatalError("Could not parse JSON manifest for \(product) during Carthage build. " +
  285. "Location: \(jsonFile). \(error)")
  286. }
  287. }
  288. /// Get the JSON filename for a product
  289. /// Consider using just the product name post Firebase 7. The conditions are to handle Firebase 6 compatibility.
  290. ///
  291. /// - Parameters:
  292. /// - product: The name of the Firebase product.
  293. /// - Returns: JSON file name for a product.
  294. private static func getJSONFileName(product: String) -> String {
  295. var jsonFileName: String
  296. if product == "GoogleSignIn" {
  297. jsonFileName = "FirebaseGoogleSignIn"
  298. } else if product == "Google-Mobile-Ads-SDK" {
  299. jsonFileName = "FirebaseAdMob"
  300. } else {
  301. jsonFileName = product
  302. }
  303. return jsonFileName + "Binary.json"
  304. }
  305. }