CarthageUtils.swift 14 KB

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