Skip to content

Commit ac333d1

Browse files
authored
Use the ZipBuilder as a template to make a Firebase Pod updater (#4064)
1 parent 0a04968 commit ac333d1

File tree

5 files changed

+282
-1
lines changed

5 files changed

+282
-1
lines changed

ZipBuilder/Package.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@ let package = Package(
2323
name: "ZipBuilder",
2424
products: [
2525
.executable(name: "ReleasePackager", targets: ["ZipBuilder"]),
26+
.executable(name: "UpdateFirebasePod", targets: ["UpdateFirebasePod"]),
2627
],
2728
dependencies: [
2829
.package(url: "https://github.com/apple/swift-protobuf.git", .exact("1.2.0")),
2930
],
3031
targets: [
32+
.target(
33+
name: "UpdateFirebasePod",
34+
dependencies: ["ManifestReader"]
35+
),
3136
.target(
3237
name: "ZipBuilder",
3338
dependencies: ["ManifestReader"]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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 Foundation
18+
19+
import ManifestReader
20+
21+
/// Updates the Firebase Pod with a release's version set.
22+
struct FirebasePod {
23+
24+
/// Relevant paths in the filesystem to build the release directory.
25+
struct FilesystemPaths {
26+
// MARK: - Required Paths
27+
28+
/// A file URL to a textproto with the contents of a `FirebasePod_Release` object. Used to verify
29+
/// expected version numbers.
30+
let currentReleasePath: URL
31+
32+
/// A file path to the path of the checked out git repo.
33+
let gitRootPath: String
34+
}
35+
36+
/// Paths needed throughout the process of packaging the Zip file.
37+
private let paths: FilesystemPaths
38+
39+
/// Default initializer. If allPodsPath and currentReleasePath are provided, it will also verify
40+
/// that the
41+
///
42+
/// - Parameters:
43+
/// - paths: Paths that are needed throughout the process of packaging the Zip file.
44+
init(paths: FilesystemPaths) {
45+
self.paths = paths
46+
}
47+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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 Foundation
18+
19+
/// Describes an object that can check if a file eists in the filesystem. Used to allow for better
20+
/// testing with FileManager.
21+
protocol FileChecker {
22+
/// Returns a Boolean value that indicates whether a file or directory exists at a specified path.
23+
/// This matches the `FileManager` API.
24+
func fileExists(atPath: String) -> Bool
25+
}
26+
27+
// Make FileManager a FileChecker. This is empty since FileManager already provides this
28+
// functionality (natively and through our extensions).
29+
extension FileManager: FileChecker {}
30+
31+
// TODO: Evaluate if we should switch to Swift Package Manager's internal `Utility` module that
32+
// contains `ArgumentParser`. No immediate need, but provides some nice benefits.
33+
/// LaunchArgs reads from UserDefaults to assemble all launch arguments coming from the command line
34+
/// or the Xcode scheme. UserDefaults contains all launch arguments that are in the format of:
35+
/// `-myKey myValue`.
36+
struct LaunchArgs {
37+
/// Keys associated with the launch args. See `Usage` for descriptions of each flag.
38+
private enum Key: String, CaseIterable {
39+
case gitRoot
40+
case releasingPods
41+
42+
/// Usage description for the key.
43+
var usage: String {
44+
switch self {
45+
case .gitRoot:
46+
return "The root of the firebase-ios-sdk checked out git repo."
47+
case .releasingPods:
48+
return "The file path to a textproto file containing all the releasing Pods, of type."
49+
}
50+
}
51+
}
52+
53+
/// A file URL to a textproto with the contents of a `FirebasePod_Release` object. Used to verify
54+
/// expected version numbers.
55+
let currentReleasePath: URL
56+
57+
/// A file URL to the checked out gitRepo to update
58+
let gitRootPath: String
59+
60+
/// The shared instance for processing launch args using default arguments.
61+
static let shared: LaunchArgs = LaunchArgs()
62+
63+
/// Initializes with values pulled from the instance of UserDefaults passed in.
64+
///
65+
/// - Parameters:
66+
/// - defaults: User defaults containing launch arguments. Defaults to `standard`.
67+
/// - fileChecker: An object that can check if a file exists or not. Defaults to
68+
/// `FileManager.default`.
69+
init(userDefaults defaults: UserDefaults = UserDefaults.standard,
70+
fileChecker: FileChecker = FileManager.default) {
71+
72+
// Parse the current releases key.
73+
guard let currentRelease = defaults.string(forKey: Key.releasingPods.rawValue) else {
74+
LaunchArgs.exitWithUsageAndLog("Missing required key: `\(Key.releasingPods)` for the file " +
75+
"containing the list of releasing pods and versions.")
76+
}
77+
let url = URL(fileURLWithPath: currentRelease)
78+
guard fileChecker.fileExists(atPath: url.path) else {
79+
LaunchArgs.exitWithUsageAndLog("Could not parse \(Key.releasingPods) key: value passed " +
80+
"in is not a file URL or the file does not exist. Value: \(currentRelease)." +
81+
" Do you need to run prodaccess?")
82+
}
83+
currentReleasePath = url.standardizedFileURL
84+
85+
// Parse the gitRoot key.
86+
guard let gitRoot = defaults.string(forKey: Key.gitRoot.rawValue) else {
87+
LaunchArgs.exitWithUsageAndLog("Missing required key: `\(Key.gitRoot)` for the path " +
88+
"of the checked out git repo.")
89+
}
90+
91+
let gitUrl = URL(fileURLWithPath: gitRoot)
92+
guard fileChecker.fileExists(atPath: gitUrl.path) else {
93+
LaunchArgs.exitWithUsageAndLog("Could not parse \(Key.gitRoot) key: value passed " +
94+
"in is not a file URL or the file does not exist. Value: \(gitRoot)")
95+
}
96+
gitRootPath = gitRoot
97+
}
98+
99+
/// Prints an error that occurred, the proper usage String, and quits the application.
100+
private static func exitWithUsageAndLog(_ errorText: String) -> Never {
101+
print(errorText)
102+
103+
// Loop over all the possible keys and print their description.
104+
print("Usage: `swift run FirebasePod [ARGS]` where args are:")
105+
for option in Key.allCases {
106+
print("""
107+
-\(option.rawValue) <VALUE>
108+
\(option.usage)
109+
""")
110+
}
111+
fatalError("Invalid arguments. See output above for specific error and usage instructions.")
112+
}
113+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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 Foundation
18+
19+
import ManifestReader
20+
21+
// Get the launch arguments, parsed by user defaults.
22+
let args = LaunchArgs.shared
23+
24+
// Keep timing for how long it takes to change the Firebase pod versions.
25+
let buildStart = Date()
26+
var cocoaPodsUpdateMessage: String = ""
27+
28+
var paths = FirebasePod.FilesystemPaths(currentReleasePath: args.currentReleasePath,
29+
gitRootPath: args.gitRootPath)
30+
31+
/// Assembles the expected versions based on the release manifest passed in.
32+
/// Returns an array with the pod name as the key and version as the value,
33+
private func getExpectedVersions() -> [String: String] {
34+
// Merge the versions from the current release and the known public versions.
35+
var releasingVersions: [String: String] = [:]
36+
37+
// Override any of the expected versions with the current release manifest, if it exists.
38+
let currentRelease = ManifestReader.loadCurrentRelease(fromTextproto: paths.currentReleasePath)
39+
print("Overriding the following Pod versions, taken from the current release manifest:")
40+
for pod in currentRelease.sdk {
41+
releasingVersions[pod.sdkName] = pod.sdkVersion
42+
print("\(pod.sdkName): \(pod.sdkVersion)")
43+
}
44+
45+
if !releasingVersions.isEmpty {
46+
print("Updating Firebase Pod in git installation at \(paths.gitRootPath)) " +
47+
"with the following versions: \(releasingVersions)")
48+
}
49+
50+
return releasingVersions
51+
}
52+
53+
private func updateFirebasePod(newVersions: [String: String]) {
54+
let podspecFile = paths.gitRootPath + "/Firebase.podspec"
55+
var contents = ""
56+
do {
57+
contents = try String(contentsOfFile: podspecFile, encoding: .utf8)
58+
} catch {
59+
fatalError("Could not read Firebase podspec. \(error)")
60+
}
61+
for (pod, version) in newVersions {
62+
if pod == "Firebase" {
63+
// Replace version in string like s.version = '6.9.0'
64+
guard let range = contents.range(of: "s.version") else {
65+
fatalError("Could not find version of Firebase pod in podspec at \(podspecFile)")
66+
}
67+
var versionStartIndex = contents.index(range.upperBound, offsetBy: 1)
68+
while contents[versionStartIndex] != "'" {
69+
versionStartIndex = contents.index(versionStartIndex, offsetBy: 1)
70+
}
71+
var versionEndIndex = contents.index(versionStartIndex, offsetBy: 1)
72+
while contents[versionEndIndex] != "'" {
73+
versionEndIndex = contents.index(versionEndIndex, offsetBy: 1)
74+
}
75+
contents.removeSubrange(versionStartIndex...versionEndIndex)
76+
contents.insert(contentsOf:"'" + version + "'", at:versionStartIndex)
77+
} else {
78+
// Replace version in string like ss.dependency 'FirebaseCore', '6.3.0'
79+
guard let range = contents.range(of: pod) else {
80+
// This pod is not a top-level Firebase pod dependency.
81+
continue
82+
}
83+
var versionStartIndex = contents.index(range.upperBound, offsetBy: 2)
84+
while !contents[versionStartIndex].isWholeNumber {
85+
versionStartIndex = contents.index(versionStartIndex, offsetBy: 1)
86+
}
87+
var versionEndIndex = contents.index(versionStartIndex, offsetBy: 1)
88+
while contents[versionEndIndex] != "'" {
89+
versionEndIndex = contents.index(versionEndIndex, offsetBy: 1)
90+
}
91+
contents.removeSubrange(versionStartIndex...versionEndIndex)
92+
contents.insert(contentsOf:version + "'", at:versionStartIndex)
93+
}
94+
}
95+
do {
96+
try contents.write(toFile: podspecFile, atomically: false, encoding: String.Encoding.utf8)
97+
}
98+
catch {
99+
fatalError("Failed to write \(podspecFile). \(error)")
100+
}
101+
}
102+
103+
do {
104+
let newVersions = getExpectedVersions()
105+
updateFirebasePod(newVersions: newVersions)
106+
print("Updating Firebase pod for version \(String(describing: newVersions["Firebase"]!))")
107+
108+
// Get the time since the tool start.
109+
let secondsSinceStart = -Int(buildStart.timeIntervalSinceNow)
110+
print("""
111+
Time profile:
112+
It took \(secondsSinceStart) seconds (~\(secondsSinceStart / 60)m) to update the Firebase pod.
113+
\(cocoaPodsUpdateMessage)
114+
""")
115+
}
116+

ZipBuilder/Sources/ZipBuilder/LaunchArgs.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ struct LaunchArgs {
221221
carthageDir = nil
222222
}
223223

224-
// Parse the Carthage directory key.
224+
// Parse the Build Root key.
225225
if let buildRoot = defaults.string(forKey: Key.buildRoot.rawValue) {
226226
let url = URL(fileURLWithPath: buildRoot)
227227
guard fileChecker.directoryExists(at: url) else {

0 commit comments

Comments
 (0)