Skip to content

Commit 88c74d7

Browse files
granluoBrian Chen
authored and
Brian Chen
committed
Add a tool to update podspec repo (#6333)
This tool is to automate the process of creating a podspec repo.
1 parent 70c0629 commit 88c74d7

File tree

2 files changed

+336
-0
lines changed

2 files changed

+336
-0
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// swift-tools-version:5.0
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
/*
4+
* Copyright 2020 Google LLC
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import PackageDescription
20+
21+
let package = Package(
22+
name: "PodSpecBuilder",
23+
products: [
24+
.executable(name: "SpecRepoBuilder", targets: ["SpecRepoBuilder"]),
25+
],
26+
dependencies: [
27+
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.3.0")),
28+
],
29+
targets: [
30+
.target(
31+
name: "SpecRepoBuilder",
32+
dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")]
33+
),
34+
]
35+
)
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
#!/usr/bin/swift
2+
3+
/*
4+
* Copyright 2020 Google LLC
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import ArgumentParser
20+
import Foundation
21+
22+
let _DEPENDENCY_LABEL_IN_SPEC = "dependency"
23+
let _SKIP_LINES_WITH_WORDS = ["unit_tests", "test_spec"]
24+
let _DEPENDENCY_LINE_SEPARATORS = [" ", ",", "/"] as CharacterSet
25+
let _POD_SOURCES = [
26+
"https://${BOT_TOKEN}@github.com/FirebasePrivate/SpecsTesting",
27+
"https://cdn.cocoapods.org/",
28+
]
29+
let _FLAGS = ["--skip-tests", "--allow-warnings"]
30+
let _FIREBASE_FLAGS = _FLAGS + ["--skip-import-validation", "--use-json"]
31+
let _FIREBASEFIRESTORE_FLAGS = _FLAGS + []
32+
let _EXCLUSIVE_PODS: [String] = ["GoogleAppMeasurement", "FirebaseAnalytics"]
33+
34+
class SpecFiles {
35+
private var specFilesDict: [String: URL]
36+
var depInstallOrder: [String]
37+
init(_ specDict: [String: URL]) {
38+
specFilesDict = specDict
39+
depInstallOrder = []
40+
}
41+
42+
func removeValue(forKey key: String) {
43+
specFilesDict.removeValue(forKey: key)
44+
}
45+
46+
func get(_ key: String) -> URL! {
47+
return specFilesDict[key]
48+
}
49+
50+
func contains(_ key: String) -> Bool {
51+
return specFilesDict[key] != nil
52+
}
53+
54+
func isEmpty() -> Bool {
55+
return specFilesDict.isEmpty
56+
}
57+
}
58+
59+
struct Shell {
60+
static let shared = Shell()
61+
@discardableResult
62+
func run(_ command: String, displayCommand: Bool = true,
63+
displayFailureResult: Bool = true) -> Int32 {
64+
let task = Process()
65+
let pipe = Pipe()
66+
task.standardOutput = pipe
67+
task.launchPath = "/bin/bash"
68+
task.arguments = ["-c", command]
69+
task.launch()
70+
if displayCommand {
71+
print("[SpecRepoBuilder] Command:\(command)\n")
72+
}
73+
task.waitUntilExit()
74+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
75+
let log = String(data: data, encoding: .utf8)!
76+
if displayFailureResult, task.terminationStatus != 0 {
77+
print("-----Exit code: \(task.terminationStatus)")
78+
print("-----Log:\n \(log)")
79+
}
80+
return task.terminationStatus
81+
}
82+
}
83+
84+
enum SpecRepoBuilderError: Error {
85+
case circularDependencies(pods: Set<String>)
86+
case failedToPush(pods: [String])
87+
}
88+
89+
struct FirebasePodUpdater: ParsableCommand {
90+
@Option(help: "The root of the firebase-ios-sdk checked out git repo.")
91+
var sdk_repo: String = FileManager().currentDirectoryPath
92+
@Option(help: "A list of podspec sources in Podfiles.")
93+
var pod_sources: [String] = _POD_SOURCES
94+
@Option(help: "Podspecs that will not be pushed to repo.")
95+
var exclude_pods: [String] = _EXCLUSIVE_PODS
96+
@Option(help: "Github Account Name.")
97+
var github_account: String = "FirebasePrivate"
98+
@Option(help: "Github Repo Name.")
99+
var sdk_repo_name: String = "SpecsTesting"
100+
@Option(help: "Local Podspec Repo Name.")
101+
var local_spec_repo_name: String
102+
@Flag(help: "Raise error while circular dependency detected.")
103+
var raise_circular_dep_error: Bool = false
104+
func generateOrderOfInstallation(pods: [String], podSpecDict: SpecFiles,
105+
parentDeps: inout Set<String>) {
106+
if podSpecDict.isEmpty() {
107+
return
108+
}
109+
110+
for pod in pods {
111+
if !podSpecDict.contains(pod) {
112+
continue
113+
}
114+
let deps = getTargetedDeps(of: pod, from: podSpecDict)
115+
if parentDeps.contains(pod) {
116+
print("Circular dependency is detected in \(pod) and \(parentDeps)")
117+
if raise_circular_dep_error {
118+
Self
119+
.exit(withError: SpecRepoBuilderError
120+
.circularDependencies(pods: parentDeps))
121+
}
122+
continue
123+
}
124+
parentDeps.insert(pod)
125+
generateOrderOfInstallation(
126+
pods: deps,
127+
podSpecDict: podSpecDict,
128+
parentDeps: &parentDeps
129+
)
130+
print("\(pod) depends on \(deps).")
131+
podSpecDict.depInstallOrder.append(pod)
132+
parentDeps.remove(pod)
133+
podSpecDict.removeValue(forKey: pod)
134+
}
135+
}
136+
137+
func searchDeps(of pod: String, from podSpecFilesObj: SpecFiles) -> [String] {
138+
var deps: [String] = []
139+
var fileContents = ""
140+
guard let podSpecURL = podSpecFilesObj.get(pod) else {
141+
return deps
142+
}
143+
do {
144+
fileContents = try String(contentsOfFile: podSpecURL.path, encoding: .utf8)
145+
} catch {
146+
fatalError("Could not read \(pod) podspec from \(podSpecURL.path).")
147+
}
148+
for line in fileContents.components(separatedBy: .newlines) {
149+
if line.contains(_DEPENDENCY_LABEL_IN_SPEC) {
150+
if _SKIP_LINES_WITH_WORDS.contains(where: line.contains) {
151+
continue
152+
}
153+
let newLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
154+
let tokens = newLine.components(separatedBy: _DEPENDENCY_LINE_SEPARATORS)
155+
if let depPrefix = tokens.first {
156+
if depPrefix.hasSuffix(_DEPENDENCY_LABEL_IN_SPEC) {
157+
let podNameRaw = String(tokens[1]).replacingOccurrences(of: "'", with: "")
158+
if podNameRaw != pod { deps.append(podNameRaw) }
159+
}
160+
}
161+
}
162+
}
163+
return deps
164+
}
165+
166+
func filterTargetDeps(_ deps: [String], with targets: SpecFiles) -> [String] {
167+
var targetedDeps: [String] = []
168+
for dep in deps {
169+
if targets.contains(dep) {
170+
targetedDeps.append(dep)
171+
}
172+
}
173+
return targetedDeps
174+
}
175+
176+
func getTargetedDeps(of pod: String, from podSpecDict: SpecFiles) -> [String] {
177+
let deps = searchDeps(of: pod, from: podSpecDict)
178+
return filterTargetDeps(deps, with: podSpecDict)
179+
}
180+
181+
func push_podspec(_ pod: String, from sdk_repo: String, sources: [String],
182+
flags: [String], shell_cmd: Shell = Shell.shared) -> Int32 {
183+
let pod_path = sdk_repo + "/" + pod + ".podspec"
184+
let sources_arg = sources.joined(separator: ",")
185+
let flags_arg = flags.joined(separator: " ")
186+
187+
let outcome =
188+
shell_cmd
189+
.run(
190+
"pod repo push \(local_spec_repo_name) \(pod_path) --sources=\(sources_arg) \(flags_arg)"
191+
)
192+
shell_cmd.run("pod repo update")
193+
194+
return outcome
195+
}
196+
197+
func erase_remote_repo(repo_path: String, from github_account: String, _ sdk_repo_name: String,
198+
shell_cmd: Shell = Shell.shared) {
199+
shell_cmd
200+
.run(
201+
"git clone --quiet https://${BOT_TOKEN}@github.com/\(github_account)/\(sdk_repo_name).git"
202+
)
203+
let fileManager = FileManager.default
204+
do {
205+
let dirs = try fileManager.contentsOfDirectory(atPath: "\(repo_path)/\(sdk_repo_name)")
206+
for dir in dirs {
207+
if !_EXCLUSIVE_PODS.contains(dir), dir != ".git" {
208+
shell_cmd.run("cd \(sdk_repo_name); git rm -r \(dir)")
209+
}
210+
}
211+
shell_cmd.run("cd \(sdk_repo_name); git commit -m 'Empty repo'; git push")
212+
} catch {
213+
print("Error while enumerating files \(repo_path): \(error.localizedDescription)")
214+
}
215+
do {
216+
try fileManager.removeItem(at: URL(fileURLWithPath: "\(repo_path)/\(sdk_repo_name)"))
217+
} catch {
218+
print("Error occurred while removing \(repo_path)/\(sdk_repo_name): \(error)")
219+
}
220+
}
221+
222+
mutating func run() throws {
223+
let fileManager = FileManager.default
224+
let cur_dir = FileManager().currentDirectoryPath
225+
var podSpecFiles: [String: URL] = [:]
226+
227+
let documentsURL = URL(fileURLWithPath: sdk_repo)
228+
do {
229+
let fileURLs = try fileManager.contentsOfDirectory(
230+
at: documentsURL,
231+
includingPropertiesForKeys: nil
232+
)
233+
let podspecURLs = fileURLs.filter { $0.pathExtension == "podspec" }
234+
for podspecURL in podspecURLs {
235+
let podName = podspecURL.deletingPathExtension().lastPathComponent
236+
if !_EXCLUSIVE_PODS.contains(podName) {
237+
podSpecFiles[podName] = podspecURL
238+
}
239+
}
240+
} catch {
241+
print(
242+
"Error while enumerating files \(documentsURL.path): \(error.localizedDescription)"
243+
)
244+
}
245+
246+
var tmpSet: Set<String> = []
247+
print("Detect podspecs: \(podSpecFiles.keys)")
248+
let specFile = SpecFiles(podSpecFiles)
249+
generateOrderOfInstallation(
250+
pods: Array(podSpecFiles.keys),
251+
podSpecDict: specFile,
252+
parentDeps: &tmpSet
253+
)
254+
print(specFile.depInstallOrder.joined(separator: "\n"))
255+
256+
do {
257+
if fileManager.fileExists(atPath: "\(cur_dir)/\(sdk_repo_name)") {
258+
print("remove \(sdk_repo_name) dir.")
259+
try fileManager.removeItem(at: URL(fileURLWithPath: "\(cur_dir)/\(sdk_repo_name)"))
260+
}
261+
erase_remote_repo(repo_path: "\(cur_dir)", from: github_account, sdk_repo_name)
262+
263+
} catch {
264+
print("error occurred. \(error)")
265+
}
266+
267+
var exitCode: Int32 = 0
268+
var failedPods: [String] = []
269+
for pod in specFile.depInstallOrder {
270+
var podExitCode: Int32 = 0
271+
print("----------\(pod)-----------")
272+
switch pod {
273+
case "Firebase":
274+
podExitCode = push_podspec(
275+
pod,
276+
from: sdk_repo,
277+
sources: pod_sources,
278+
flags: _FIREBASE_FLAGS
279+
)
280+
case "FirebaseFirestore":
281+
podExitCode = push_podspec(
282+
pod,
283+
from: sdk_repo,
284+
sources: pod_sources,
285+
flags: _FIREBASEFIRESTORE_FLAGS
286+
)
287+
default:
288+
podExitCode = push_podspec(pod, from: sdk_repo, sources: pod_sources, flags: _FLAGS)
289+
}
290+
if podExitCode != 0 {
291+
exitCode = 1
292+
failedPods.append(pod)
293+
}
294+
}
295+
if exitCode != 0 {
296+
Self.exit(withError: SpecRepoBuilderError.failedToPush(pods: failedPods))
297+
}
298+
}
299+
}
300+
301+
FirebasePodUpdater.main()

0 commit comments

Comments
 (0)