Skip to content

Commit e3f5dd8

Browse files
authored
Add option to generate Carthage build. (#3517)
* Add option to generate Carthage build. Still a few things to do here, but the structure is there. * Revert unused changes. * Further Carthage work. * Hashing the contents of the directory for naming. * Update README for Carthage. * Change hash calculation to SHA256 for Carthage naming. * Undo accidental change * Generate Firebase pod for Carthage * Parsing JSON files. * Include RC number, new output naming. * Further iterations on getting Carthage working. * Fix long line. * Undo version removal. * Proper parsing of JSON manifest. * Fix JSON output, properly copy to outputDir. * Replace CoreDiagnostics framework with newly built one. * Clear output directory if it doesn't exist. * Fix the JSON formatting. * Added newline before closing JSON.
1 parent dea4aa2 commit e3f5dd8

File tree

9 files changed

+570
-27
lines changed

9 files changed

+570
-27
lines changed

ZipBuilder/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ swift run ZipBuilder -templateDir $(pwd)/Template -updatePodRepo false \
6262
-customSpecRepos sso://cpdc-internal/firebase
6363
```
6464

65+
## Carthage
66+
67+
Carthage binaries can also be built at the same time as the zip file by passing in `-carthagePath
68+
<path_to_json_files>` as a command line argument. This directory should contain JSON files describing versions
69+
and download locations for each product. This will result in a folder called "carthage" at the root where the zip
70+
directory exists containing all the zip files and JSON files necessary for distribution.
71+
6572
## Debugging
6673

6774
You can generate an Xcode project for the tool by running `swift package generate-xcodeproj` in this directory.
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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+
}

ZipBuilder/Sources/ZipBuilder/FileManager+Utils.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ public extension FileManager {
2222

2323
/// Describes a type of file to be searched for.
2424
enum SearchFileType {
25+
/// All files, not including folders.
26+
case allFiles
27+
2528
/// All folders with a `.bundle` extension.
2629
case bundles
2730

@@ -141,6 +144,11 @@ public extension FileManager {
141144
var matches: [URL] = []
142145
while let fileURL = dirEnumerator.nextObject() as? URL {
143146
switch type {
147+
case .allFiles:
148+
// Skip directories, include everything else.
149+
guard !isDirectory(at: fileURL) else { continue }
150+
151+
matches.append(fileURL)
144152
case let .directories(name):
145153
// Skip any non-directories.
146154
guard directoryExists(at: fileURL) else { continue }

0 commit comments

Comments
 (0)