CarthageUtils.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  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. factorProtobuf(inPackagedDir: packagedDir)
  85. let directories: [String]
  86. do {
  87. directories = try FileManager.default.contentsOfDirectory(atPath: packagedDir.path)
  88. } catch {
  89. fatalError("Could not get contents of Firebase directory to package Carthage build. \(error)")
  90. }
  91. let firebaseVersion = artifacts.firebaseVersion
  92. // Loop through each directory available and package it as a separate Zip file.
  93. for product in directories {
  94. let fullPath = packagedDir.appendingPathComponent(product)
  95. guard FileManager.default.isDirectory(at: fullPath) else { continue }
  96. // Abort Carthage generation if there are any xcframeworks.
  97. do {
  98. let files = try FileManager.default.contentsOfDirectory(at: fullPath,
  99. includingPropertiesForKeys: nil)
  100. let xcfFiles = files.filter { $0.pathExtension == "xcframework" }
  101. if xcfFiles.count > 0 {
  102. print("Skipping Carthage generation for \(product) since it includes xcframeworks.")
  103. continue
  104. }
  105. } catch {
  106. fatalError("Failed to get contents of \(fullPath).")
  107. }
  108. // Parse the JSON file, ensure that we're not trying to overwrite a release.
  109. var jsonManifest = parseJSONFile(fromDir: jsonDir, product: product)
  110. if versionCheckEnabled {
  111. guard jsonManifest[firebaseVersion] == nil else {
  112. print("Carthage release for \(product) \(firebaseVersion) already exists - skipping.")
  113. continue
  114. }
  115. }
  116. // Analytics includes all the Core frameworks and Firebase module, do extra work to package
  117. // it.
  118. if product == "FirebaseAnalytics" {
  119. createFirebaseFramework(version: firebaseVersion,
  120. inDir: fullPath,
  121. rootDir: packagedDir,
  122. templateDir: templateDir)
  123. // Copy the NOTICES file from FirebaseCore.
  124. let noticesName = "NOTICES"
  125. let coreNotices = fullPath.appendingPathComponents(["FirebaseCore.framework", noticesName])
  126. let noticesPath = packagedDir.appendingPathComponent(noticesName)
  127. do {
  128. try FileManager.default.copyItem(at: noticesPath, to: coreNotices)
  129. } catch {
  130. fatalError("Could not copy \(noticesName) to FirebaseCore for Carthage build. \(error)")
  131. }
  132. }
  133. // Hash the contents of the directory to get a unique name for Carthage.
  134. let hash: String
  135. do {
  136. // Only use the first 16 characters, that's what we did before.
  137. let fullHash = try HashCalculator.sha256Contents(ofDir: fullPath)
  138. hash = String(fullHash.prefix(16))
  139. } catch {
  140. fatalError("Could not hash contents of \(product) for Carthage build. \(error)")
  141. }
  142. // Generate the zip name to write to the manifest as well as the actual zip file.
  143. let zipName = "\(product)-\(hash).zip"
  144. let productZip = outputDir.appendingPathComponent(zipName)
  145. let zipped = Zip.zipContents(ofDir: fullPath, name: zipName)
  146. do {
  147. try FileManager.default.moveItem(at: zipped, to: productZip)
  148. } catch {
  149. fatalError("Could not move packaged zip file for \(product) during Carthage build. " +
  150. "\(error)")
  151. }
  152. // Force unwrapping because this can't fail at this point.
  153. let url =
  154. URL(string: "https://dl.google.com/dl/firebase/ios/carthage/\(firebaseVersion)/\(zipName)")!
  155. jsonManifest[firebaseVersion] = url
  156. // Write the updated manifest.
  157. let manifestPath = outputDir.appendingPathComponent(getJSONFileName(product: product))
  158. // Unfortunate workaround: There's a strange issue when serializing to JSON on macOS: URLs
  159. // will have the `/` escaped leading to an odd JSON output. Instead, let's output the
  160. // dictionary to a String and write that to disk. When Xcode 11 can be used, use a JSON
  161. // encoder with the `.withoutEscapingSlashes` option on `outputFormatting` like this:
  162. // do {
  163. // let encoder = JSONEncoder()
  164. // encoder.outputFormatting = [.sortedKeys, .prettyPrinted, .withoutEscapingSlashes]
  165. // let encodedManifest = try encoder.encode(jsonManifest)
  166. // catch { /* handle error */ }
  167. // Sort the manifest based on the key, $0 and $1 are the parameters and 0 is the first item in
  168. // the tuple (key).
  169. let sortedManifest = jsonManifest.sorted { $0.0 < $1.0 }
  170. // Generate the JSON format and combine all the lines afterwards.
  171. let manifestLines = sortedManifest.map { (version, url) -> String in
  172. // Two spaces at the beginning of the String are intentional.
  173. " \"\(version)\": \"\(url.absoluteString)\""
  174. }
  175. // Join all the lines with a comma and newline to make things easier to read.
  176. let contents = "{\n" + manifestLines.joined(separator: ",\n") + "\n}\n"
  177. guard let encodedManifest = contents.data(using: .utf8) else {
  178. fatalError("Could not encode Carthage JSON manifest for \(product) - UTF8 encoding failed.")
  179. }
  180. do {
  181. try encodedManifest.write(to: manifestPath)
  182. print("Successfully written Carthage JSON manifest for \(product).")
  183. } catch {
  184. fatalError("Could not write new Carthage JSON manifest to disk for \(product). \(error)")
  185. }
  186. }
  187. }
  188. /// Factor Protobuf into a separate Carthage distribution to avoid Carthage install issues
  189. /// trying to install the same framework from multiple bundles(#5276).
  190. ///
  191. /// - Parameters:
  192. /// - packagedDir: The packaged directory assembled for Carthage and Zip distribution.
  193. private static func factorProtobuf(inPackagedDir packagedDir: URL) {
  194. let directories: [String]
  195. let protobufDir = packagedDir.appendingPathComponent("FirebaseProtobuf")
  196. do {
  197. directories = try FileManager.default.contentsOfDirectory(atPath: packagedDir.path)
  198. } catch {
  199. fatalError("Could not get contents of Firebase directory to package Carthage build. \(error)")
  200. }
  201. let fileManager = FileManager.default
  202. var didMove = false
  203. // Loop through each directory to see if it includes Protobuf.framework.
  204. for package in directories {
  205. let fullPath = packagedDir.appendingPathComponent(package)
  206. .appendingPathComponent("Protobuf.framework")
  207. if fileManager.fileExists(atPath: fullPath.path) {
  208. if didMove == false {
  209. didMove = true
  210. do {
  211. try fileManager.createDirectory(at: protobufDir, withIntermediateDirectories: true)
  212. try fileManager
  213. .moveItem(at: fullPath, to: protobufDir.appendingPathComponent("Protobuf.framework"))
  214. } catch {
  215. fatalError("Failed to create Carthage protobuf directory at \(protobufDir) \(error)")
  216. }
  217. } else {
  218. fileManager.removeIfExists(at: fullPath)
  219. }
  220. }
  221. }
  222. }
  223. /// Creates a fake Firebase.framework to use the module for `import Firebase` compatibility.
  224. ///
  225. /// - Parameters:
  226. /// - version: Firebase version.
  227. /// - destination: The destination directory for the Firebase framework.
  228. /// - rootDir: The root directory that contains other required files (like the Firebase header).
  229. /// - templateDir: The template directory containing the dummy Firebase library.
  230. private static func createFirebaseFramework(version: String,
  231. inDir destination: URL,
  232. rootDir: URL,
  233. templateDir: URL) {
  234. // Local FileManager for better readability.
  235. let fm = FileManager.default
  236. let frameworkDir = destination.appendingPathComponent("Firebase.framework")
  237. let headersDir = frameworkDir.appendingPathComponent("Headers")
  238. let modulesDir = frameworkDir.appendingPathComponent("Modules")
  239. // Create all the required directories.
  240. do {
  241. try fm.createDirectory(at: headersDir, withIntermediateDirectories: true)
  242. try fm.createDirectory(at: modulesDir, withIntermediateDirectories: true)
  243. } catch {
  244. fatalError("Could not create directories for Firebase framework in Carthage. \(error)")
  245. }
  246. // Copy the Firebase header and modulemap that was created in the Zip file.
  247. let header = rootDir.appendingPathComponent(Constants.ProjectPath.firebaseHeader)
  248. do {
  249. try fm.copyItem(at: header, to: headersDir.appendingPathComponent(header.lastPathComponent))
  250. // Generate the new modulemap since it differs from the Zip modulemap.
  251. let carthageModulemap = """
  252. framework module Firebase {
  253. header "Firebase.h"
  254. export *
  255. }
  256. """
  257. let modulemapPath = modulesDir.appendingPathComponent("module.modulemap")
  258. try carthageModulemap.write(to: modulemapPath, atomically: true, encoding: .utf8)
  259. } catch {
  260. fatalError("Couldn't write required files for Firebase framework in Carthage. \(error)")
  261. }
  262. // Copy the dummy Firebase library.
  263. let dummyLib = templateDir.appendingPathComponent(Constants.ProjectPath.dummyFirebaseLib)
  264. do {
  265. try fm.copyItem(at: dummyLib, to: frameworkDir.appendingPathComponent("Firebase"))
  266. } catch {
  267. fatalError("Couldn't copy dummy library for Firebase framework in Carthage. \(error)")
  268. }
  269. // Write the Info.plist.
  270. generatePlistContents(forName: "Firebase", withVersion: version, to: frameworkDir)
  271. }
  272. static func generatePlistContents(forName name: String,
  273. withVersion version: String,
  274. to location: URL) {
  275. let ver = version.components(separatedBy: "-")[0] // remove any version suffix.
  276. let plist: [String: String] = ["CFBundleIdentifier": "com.firebase.Firebase-\(name)",
  277. "CFBundleInfoDictionaryVersion": "6.0",
  278. "CFBundlePackageType": "FMWK",
  279. "CFBundleVersion": ver,
  280. "DTSDKName": "iphonesimulator11.2",
  281. "CFBundleExecutable": name,
  282. "CFBundleName": name]
  283. // Generate the data for an XML based plist.
  284. let encoder = PropertyListEncoder()
  285. encoder.outputFormat = .xml
  286. do {
  287. let data = try encoder.encode(plist)
  288. try data.write(to: location.appendingPathComponent("Info.plist"))
  289. } catch {
  290. fatalError("Failed to create Info.plist for \(name) during Carthage build: \(error)")
  291. }
  292. }
  293. /// Parses the JSON manifest for the particular product.
  294. ///
  295. /// - Parameters:
  296. /// - dir: The directory containing all JSON manifests.
  297. /// - product: The name of the Firebase product.
  298. /// - Returns: A dictionary with versions as keys and URLs as values.
  299. private static func parseJSONFile(fromDir dir: URL, product: String) -> [String: URL] {
  300. // Parse the JSON manifest.
  301. let jsonFileName = getJSONFileName(product: product)
  302. let jsonFile = dir.appendingPathComponent(jsonFileName)
  303. guard FileManager.default.fileExists(atPath: jsonFile.path) else {
  304. fatalError("Could not find JSON manifest for \(product) during Carthage build. " +
  305. "Location: \(jsonFile)")
  306. }
  307. let jsonData: Data
  308. do {
  309. jsonData = try Data(contentsOf: jsonFile)
  310. } catch {
  311. fatalError("Could not read JSON manifest for \(product) during Carthage build. " +
  312. "Location: \(jsonFile). \(error)")
  313. }
  314. // Get a dictionary out of the file.
  315. let decoder = JSONDecoder()
  316. do {
  317. let productReleases = try decoder.decode([String: URL].self, from: jsonData)
  318. return productReleases
  319. } catch {
  320. fatalError("Could not parse JSON manifest for \(product) during Carthage build. " +
  321. "Location: \(jsonFile). \(error)")
  322. }
  323. }
  324. /// Get the JSON filename for a product
  325. /// Consider using just the product name post Firebase 7. The conditions are to handle Firebase 6 compatibility.
  326. ///
  327. /// - Parameters:
  328. /// - product: The name of the Firebase product.
  329. /// - Returns: JSON file name for a product.
  330. private static func getJSONFileName(product: String) -> String {
  331. var jsonFileName: String
  332. if product == "GoogleSignIn" {
  333. jsonFileName = "FirebaseGoogleSignIn"
  334. } else if product == "Google-Mobile-Ads-SDK" {
  335. jsonFileName = "FirebaseAdMob"
  336. } else {
  337. jsonFileName = product
  338. }
  339. return jsonFileName + "Binary.json"
  340. }
  341. }