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