Files
firebase-ios-sdk/ReleaseTooling/Sources/ZipBuilder/CarthageUtils.swift
T

385 lines
16 KiB
Swift

/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import CommonCrypto
import Foundation
import Utils
struct CarthageBuildOptions {
/// Location of directory containing all JSON Carthage manifests.
let jsonDir: URL
/// Version checking flag.
let isVersionCheckEnabled: Bool
}
/// Carthage related utility functions. The enum type is used as a namespace here instead of having
/// root functions, and no cases should be added to it.
enum CarthageUtils {}
extension CarthageUtils {
/// Package all required files for a Carthage release.
///
/// - Parameters:
/// - templateDir: The template project directory, contains the dummy Firebase library.
/// - carthageJSONDir: Location of directory containing all JSON Carthage manifests.
/// - artifacts: Release Artifacts from build.
/// - options: Carthage specific options for the build.
/// - Returns: The path to the root of the Carthage installation.
static func packageCarthageRelease(templateDir: URL,
artifacts: ZipBuilder.ReleaseArtifacts,
options: CarthageBuildOptions) -> URL? {
guard let zipLocation = artifacts.carthageDir else { return nil }
do {
print("Creating Carthage release...")
let carthagePath =
zipLocation.deletingLastPathComponent().appendingPathComponent("carthage_build")
// Create a copy of the release directory since we'll be modifying it.
let fileManager = FileManager.default
fileManager.removeIfExists(at: carthagePath)
try fileManager.copyItem(at: zipLocation, to: carthagePath)
// Package the Carthage distribution with the current directory structure.
let carthageDir = zipLocation.deletingLastPathComponent().appendingPathComponent("carthage")
fileManager.removeIfExists(at: carthageDir)
let output = carthageDir.appendingPathComponents([artifacts.firebaseVersion, "latest"])
try fileManager.createDirectory(at: output, withIntermediateDirectories: true)
generateCarthageRelease(fromPackagedDir: carthagePath,
templateDir: templateDir,
jsonDir: options.jsonDir,
artifacts: artifacts,
outputDir: output,
versionCheckEnabled: options.isVersionCheckEnabled)
// Remove the duplicated Carthage build directory.
fileManager.removeIfExists(at: carthagePath)
print("Done creating Carthage release! Files written to \(output)")
// Save the directory for later copying.
return carthageDir
} catch {
fatalError("Could not copy output directory for Carthage build: \(error)")
}
}
/// Generates all required files for a Carthage release.
///
/// - Parameters:
/// - packagedDir: The packaged directory assembled for the Carthage distribution.
/// - templateDir: The template project directory, contains the dummy Firebase library.
/// - jsonDir: Location of directory containing all JSON Carthage manifests.
/// - firebaseVersion: The version of the Firebase pod.
/// - coreDiagnosticsPath: The path to the Core Diagnostics framework built for Carthage.
/// - outputDir: The directory where all artifacts should be created.
private static func generateCarthageRelease(fromPackagedDir packagedDir: URL,
templateDir: URL,
jsonDir: URL,
artifacts: ZipBuilder.ReleaseArtifacts,
outputDir: URL,
versionCheckEnabled: Bool) {
factorProtobuf(inPackagedDir: packagedDir)
let directories: [String]
do {
directories = try FileManager.default.contentsOfDirectory(atPath: packagedDir.path)
} catch {
fatalError("Could not get contents of Firebase directory to package Carthage build. \(error)")
}
let firebaseVersion = artifacts.firebaseVersion
// Loop through each directory available and package it as a separate Zip file.
for product in directories {
let fullPath = packagedDir.appendingPathComponent(product)
guard FileManager.default.isDirectory(at: fullPath) else { continue }
// Abort Carthage generation if there are any xcframeworks.
do {
let files = try FileManager.default.contentsOfDirectory(at: fullPath,
includingPropertiesForKeys: nil)
let xcfFiles = files.filter { $0.pathExtension == "xcframework" }
if xcfFiles.count > 0 {
print("Skipping Carthage generation for \(product) since it includes xcframeworks.")
continue
}
} catch {
fatalError("Failed to get contents of \(fullPath).")
}
// Parse the JSON file, ensure that we're not trying to overwrite a release.
var jsonManifest = parseJSONFile(fromDir: jsonDir, product: product)
if versionCheckEnabled {
guard jsonManifest[firebaseVersion] == nil else {
print("Carthage release for \(product) \(firebaseVersion) already exists - skipping.")
continue
}
}
// Analytics includes all the Core frameworks and Firebase module, do extra work to package
// it.
if product == "FirebaseAnalytics" {
createFirebaseFramework(version: firebaseVersion,
inDir: fullPath,
rootDir: packagedDir,
templateDir: templateDir)
// Copy the NOTICES file from FirebaseCore.
let noticesName = "NOTICES"
let coreNotices = fullPath.appendingPathComponents(["FirebaseCore.framework", noticesName])
let noticesPath = packagedDir.appendingPathComponent(noticesName)
do {
try FileManager.default.copyItem(at: noticesPath, to: coreNotices)
} catch {
fatalError("Could not copy \(noticesName) to FirebaseCore for Carthage build. \(error)")
}
}
// Hash the contents of the directory to get a unique name for Carthage.
let hash: String
do {
// Only use the first 16 characters, that's what we did before.
let fullHash = try HashCalculator.sha256Contents(ofDir: fullPath)
hash = String(fullHash.prefix(16))
} catch {
fatalError("Could not hash contents of \(product) for Carthage build. \(error)")
}
// Generate the zip name to write to the manifest as well as the actual zip file.
let zipName = "\(product)-\(hash).zip"
let productZip = outputDir.appendingPathComponent(zipName)
let zipped = Zip.zipContents(ofDir: fullPath, name: zipName)
do {
try FileManager.default.moveItem(at: zipped, to: productZip)
} catch {
fatalError("Could not move packaged zip file for \(product) during Carthage build. " +
"\(error)")
}
// Force unwrapping because this can't fail at this point.
let url =
URL(string: "https://dl.google.com/dl/firebase/ios/carthage/\(firebaseVersion)/\(zipName)")!
jsonManifest[firebaseVersion] = url
// Write the updated manifest.
let manifestPath = outputDir.appendingPathComponent(getJSONFileName(product: product))
// Unfortunate workaround: There's a strange issue when serializing to JSON on macOS: URLs
// will have the `/` escaped leading to an odd JSON output. Instead, let's output the
// dictionary to a String and write that to disk. When Xcode 11 can be used, use a JSON
// encoder with the `.withoutEscapingSlashes` option on `outputFormatting` like this:
// do {
// let encoder = JSONEncoder()
// encoder.outputFormatting = [.sortedKeys, .prettyPrinted, .withoutEscapingSlashes]
// let encodedManifest = try encoder.encode(jsonManifest)
// catch { /* handle error */ }
// Sort the manifest based on the key, $0 and $1 are the parameters and 0 is the first item in
// the tuple (key).
let sortedManifest = jsonManifest.sorted { $0.0 < $1.0 }
// Generate the JSON format and combine all the lines afterwards.
let manifestLines = sortedManifest.map { (version, url) -> String in
// Two spaces at the beginning of the String are intentional.
" \"\(version)\": \"\(url.absoluteString)\""
}
// Join all the lines with a comma and newline to make things easier to read.
let contents = "{\n" + manifestLines.joined(separator: ",\n") + "\n}\n"
guard let encodedManifest = contents.data(using: .utf8) else {
fatalError("Could not encode Carthage JSON manifest for \(product) - UTF8 encoding failed.")
}
do {
try encodedManifest.write(to: manifestPath)
print("Successfully written Carthage JSON manifest for \(product).")
} catch {
fatalError("Could not write new Carthage JSON manifest to disk for \(product). \(error)")
}
}
}
/// Factor Protobuf into a separate Carthage distribution to avoid Carthage install issues
/// trying to install the same framework from multiple bundles(#5276).
///
/// - Parameters:
/// - packagedDir: The packaged directory assembled for Carthage and Zip distribution.
private static func factorProtobuf(inPackagedDir packagedDir: URL) {
let directories: [String]
let protobufDir = packagedDir.appendingPathComponent("FirebaseProtobuf")
do {
directories = try FileManager.default.contentsOfDirectory(atPath: packagedDir.path)
} catch {
fatalError("Could not get contents of Firebase directory to package Carthage build. \(error)")
}
let fileManager = FileManager.default
var didMove = false
// Loop through each directory to see if it includes Protobuf.framework.
for package in directories {
let fullPath = packagedDir.appendingPathComponent(package)
.appendingPathComponent("Protobuf.framework")
if fileManager.fileExists(atPath: fullPath.path) {
if didMove == false {
didMove = true
do {
try fileManager.createDirectory(at: protobufDir, withIntermediateDirectories: true)
try fileManager
.moveItem(at: fullPath, to: protobufDir.appendingPathComponent("Protobuf.framework"))
} catch {
fatalError("Failed to create Carthage protobuf directory at \(protobufDir) \(error)")
}
} else {
fileManager.removeIfExists(at: fullPath)
}
}
}
}
/// Creates a fake Firebase.framework to use the module for `import Firebase` compatibility.
///
/// - Parameters:
/// - version: Firebase version.
/// - destination: The destination directory for the Firebase framework.
/// - rootDir: The root directory that contains other required files (like the Firebase header).
/// - templateDir: The template directory containing the dummy Firebase library.
private static func createFirebaseFramework(version: String,
inDir destination: URL,
rootDir: URL,
templateDir: URL) {
// Local FileManager for better readability.
let fm = FileManager.default
let frameworkDir = destination.appendingPathComponent("Firebase.framework")
let headersDir = frameworkDir.appendingPathComponent("Headers")
let modulesDir = frameworkDir.appendingPathComponent("Modules")
// Create all the required directories.
do {
try fm.createDirectory(at: headersDir, withIntermediateDirectories: true)
try fm.createDirectory(at: modulesDir, withIntermediateDirectories: true)
} catch {
fatalError("Could not create directories for Firebase framework in Carthage. \(error)")
}
// Copy the Firebase header and modulemap that was created in the Zip file.
let header = rootDir.appendingPathComponent(Constants.ProjectPath.firebaseHeader)
do {
try fm.copyItem(at: header, to: headersDir.appendingPathComponent(header.lastPathComponent))
// Generate the new modulemap since it differs from the Zip modulemap.
let carthageModulemap = """
framework module Firebase {
header "Firebase.h"
export *
}
"""
let modulemapPath = modulesDir.appendingPathComponent("module.modulemap")
try carthageModulemap.write(to: modulemapPath, atomically: true, encoding: .utf8)
} catch {
fatalError("Couldn't write required files for Firebase framework in Carthage. \(error)")
}
// Copy the dummy Firebase library.
let dummyLib = templateDir.appendingPathComponent(Constants.ProjectPath.dummyFirebaseLib)
do {
try fm.copyItem(at: dummyLib, to: frameworkDir.appendingPathComponent("Firebase"))
} catch {
fatalError("Couldn't copy dummy library for Firebase framework in Carthage. \(error)")
}
// Write the Info.plist.
generatePlistContents(forName: "Firebase", withVersion: version, to: frameworkDir)
}
static func generatePlistContents(forName name: String,
withVersion version: String,
to location: URL) {
let ver = version.components(separatedBy: "-")[0] // remove any version suffix.
let plist: [String: String] = ["CFBundleIdentifier": "com.firebase.Firebase-\(name)",
"CFBundleInfoDictionaryVersion": "6.0",
"CFBundlePackageType": "FMWK",
"CFBundleVersion": ver,
"DTSDKName": "iphonesimulator11.2",
"CFBundleExecutable": name,
"CFBundleName": name]
// Generate the data for an XML based plist.
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
do {
let data = try encoder.encode(plist)
try data.write(to: location.appendingPathComponent("Info.plist"))
} catch {
fatalError("Failed to create Info.plist for \(name) during Carthage build: \(error)")
}
}
/// Parses the JSON manifest for the particular product.
///
/// - Parameters:
/// - dir: The directory containing all JSON manifests.
/// - product: The name of the Firebase product.
/// - Returns: A dictionary with versions as keys and URLs as values.
private static func parseJSONFile(fromDir dir: URL, product: String) -> [String: URL] {
// Parse the JSON manifest.
let jsonFileName = getJSONFileName(product: product)
let jsonFile = dir.appendingPathComponent(jsonFileName)
guard FileManager.default.fileExists(atPath: jsonFile.path) else {
fatalError("Could not find JSON manifest for \(product) during Carthage build. " +
"Location: \(jsonFile)")
}
let jsonData: Data
do {
jsonData = try Data(contentsOf: jsonFile)
} catch {
fatalError("Could not read JSON manifest for \(product) during Carthage build. " +
"Location: \(jsonFile). \(error)")
}
// Get a dictionary out of the file.
let decoder = JSONDecoder()
do {
let productReleases = try decoder.decode([String: URL].self, from: jsonData)
return productReleases
} catch {
fatalError("Could not parse JSON manifest for \(product) during Carthage build. " +
"Location: \(jsonFile). \(error)")
}
}
/// Get the JSON filename for a product
/// Consider using just the product name post Firebase 7. The conditions are to handle Firebase 6 compatibility.
///
/// - Parameters:
/// - product: The name of the Firebase product.
/// - Returns: JSON file name for a product.
private static func getJSONFileName(product: String) -> String {
var jsonFileName: String
if product == "GoogleSignIn" {
jsonFileName = "FirebaseGoogleSignIn"
} else if product == "Google-Mobile-Ads-SDK" {
jsonFileName = "FirebaseAdMob"
} else {
jsonFileName = product
}
return jsonFileName + "Binary.json"
}
}