Files
firebase-ios-sdk/scripts/create_spec_repo/Sources/SpecRepoBuilder/main.swift
T
Gran 9f8d395637 Add release candidate prerelease workflow. (#6487)
This also update cocoapods source to cocoapods repo since cdn is failed to connect.
2020-09-22 19:46:23 -07:00

355 lines
12 KiB
Swift

/*
* Copyright 2020 Google LLC
*
* 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 ArgumentParser
import Foundation
private enum Constants {}
extension Constants {
static let specDependencyLabel = "dependency"
static let skipLinesWithWords = ["unit_tests", "test_spec"]
static let dependencyLineSeparators = CharacterSet(charactersIn: " ,/")
static let podSources = [
"https://${BOT_TOKEN}@github.com/FirebasePrivate/SpecsTesting",
"https://github.com/CocoaPods/Specs",
]
static let exclusivePods: [String] = ["GoogleAppMeasurement", "FirebaseAnalytics"]
}
// flags for 'pod push'
extension Constants {
static let flags = ["--skip-tests", "--allow-warnings"]
static let umbrellaPodFlags = Constants.flags + ["--skip-import-validation", "--use-json"]
}
// SpecFiles is a wraper of dict mapping from required pods to their path. This
// will also contain a sequence of installing podspecs.
class SpecFiles {
private var specFilesDict: [String: URL]
var depInstallOrder: [String]
var specSource: String
init(_ specDict: [String: URL], from specSourcePath: String) {
specFilesDict = specDict
depInstallOrder = []
specSource = specSourcePath
}
func removePod(_ key: String) {
specFilesDict.removeValue(forKey: key)
}
subscript(key: String) -> URL? {
return specFilesDict[key]
}
func contains(_ key: String) -> Bool {
return specFilesDict[key] != nil
}
func isEmpty() -> Bool {
return specFilesDict.isEmpty
}
}
struct Shell {
static let shared = Shell()
@discardableResult
func run(_ command: String, displayCommand: Bool = true,
displayFailureResult: Bool = true) -> Int32 {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.launchPath = "/bin/bash"
task.arguments = ["-c", command]
task.launch()
if displayCommand {
print("[SpecRepoBuilder] Command:\(command)\n")
}
task.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let log = String(data: data, encoding: .utf8)!
if displayFailureResult, task.terminationStatus != 0 {
print("-----Exit code: \(task.terminationStatus)")
print("-----Log:\n \(log)")
}
return task.terminationStatus
}
}
// Error types
enum SpecRepoBuilderError: Error {
// Error occurs when circular dependencies are detected and deps will be
// displayed.
case circularDependencies(pods: Set<String>)
// Error occurs when there exist specs that failed to push to a spec repo. All
// specs failed to push should be displayed.
case failedToPush(pods: [String])
// Error occurs when a podspec is not found in the repo.
case podspecNotFound(_ podspec: String, from: String)
}
struct SpecRepoBuilder: ParsableCommand {
@Option(help: "The root of the firebase-ios-sdk checked out git repo.")
var sdkRepo: String = FileManager().currentDirectoryPath
@Option(help: "A list of podspec sources in Podfiles.")
var podSources: [String] = Constants.podSources
@Option(help: "Podspecs that will not be pushed to repo.")
var excludePods: [String] = Constants.exclusivePods
@Option(help: "Github Account Name.")
var githubAccount: String = "FirebasePrivate"
@Option(help: "Github Repo Name.")
var sdkRepoName: String = "SpecsTesting"
@Option(help: "Local Podspec Repo Name.")
var localSpecRepoName: String
@Flag(help: "Raise error while circular dependency detected.")
var raiseCircularDepError: Bool = false
// This will track down dependencies of pods and keep the sequence of
// dependency installation in specFiles.depInstallOrder.
func generateOrderOfInstallation(pods: [String], specFiles: SpecFiles,
parentDeps: inout Set<String>) {
// pods are dependencies will be tracked down.
// specFiles includes required pods and their URLs.
// parentDeps will record the path of tracking down dependencies to avoid
// duplications and circular dependencies.
// Stop tracking down when the parent pod does not have any required deps.
if pods.isEmpty {
return
}
for pod in pods {
guard specFiles.contains(pod) else { continue }
let deps = getTargetedDeps(of: pod, from: specFiles)
// parentDeps will have all dependencies the current pod supports. If the
// current pod were in the parent dependencies, that means it was tracked
// before and it is circular dependency.
if parentDeps.contains(pod) {
print("Circular dependency is detected in \(pod) and \(parentDeps)")
if raiseCircularDepError {
Self
.exit(withError: SpecRepoBuilderError
.circularDependencies(pods: parentDeps))
}
continue
}
// Record the pod as a parent and use depth-first-search to track down
// dependencies of this pod.
parentDeps.insert(pod)
generateOrderOfInstallation(
pods: deps,
specFiles: specFiles,
parentDeps: &parentDeps
)
// When pod does not have required dep or its required deps are recorded,
// the pod itself will be recorded into the depInstallOrder.
if !specFiles.depInstallOrder.contains(pod) {
print("\(pod) depends on \(deps).")
specFiles.depInstallOrder.append(pod)
}
// When track back from a lower level, parentDep should track back by
// removing one pod.
parentDeps.remove(pod)
}
}
// Scan a podspec file and find and return all dependencies in this podspec.
func searchDeps(ofPod podName: String, from podSpecFiles: SpecFiles) -> [String] {
var deps: [String] = []
var fileContents = ""
guard let podSpecURL = podSpecFiles[podName] else {
Self
.exit(withError: SpecRepoBuilderError
.podspecNotFound(podName, from: podSpecFiles.specSource))
}
do {
fileContents = try String(contentsOfFile: podSpecURL.path, encoding: .utf8)
} catch {
fatalError("Could not read \(podName) podspec from \(podSpecURL.path).")
}
// Get all the lines containing `dependency` but don't contain words we
// want to ignore.
let depLines: [String] = fileContents
.components(separatedBy: .newlines)
.filter { $0.contains("dependency") }
// Skip lines with words in Constants.skipLinesWithWords
.filter { !Constants.skipLinesWithWords.contains(where: $0.contains)
}
for line in depLines {
let newLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
// This is to avoid pushing umbrellapods like Firebase/Core.
let tokens = newLine.components(separatedBy: Constants.dependencyLineSeparators)
if let depPrefix = tokens.first {
if depPrefix.hasSuffix(Constants.specDependencyLabel) {
// e.g. In Firebase.podspec, Firebase/Core will not be considered a
// dependency.
// "ss.dependency 'Firebase/Core'" will be splited in
// ["ss.dependency", "'Firebase", "Core'"]
let podNameRaw = String(tokens[1]).replacingOccurrences(of: "'", with: "")
// In the example above, deps here will not include Firebase since
// it is the same as the pod name.
if podNameRaw != podName { deps.append(podNameRaw) }
}
}
}
return deps
}
// Filter and get a list of required dependencies found in the repo.
func filterTargetDeps(_ deps: [String], with targets: SpecFiles) -> [String] {
var targetedDeps: [String] = []
for dep in deps {
// Only get unique and required dep in the output list.
if targets.contains(dep), !targetedDeps.contains(dep) {
targetedDeps.append(dep)
}
}
return targetedDeps
}
func getTargetedDeps(of pod: String, from specFiles: SpecFiles) -> [String] {
let deps = searchDeps(ofPod: pod, from: specFiles)
return filterTargetDeps(deps, with: specFiles)
}
func pushPodspec(forPod pod: String, sdkRepo: String, sources: [String],
flags: [String], shell: Shell = Shell.shared) -> Int32 {
let podPath = sdkRepo + "/" + pod + ".podspec"
let sourcesArg = sources.joined(separator: ",")
let flagsArg = flags.joined(separator: " ")
let outcome =
shell
.run(
"pod repo push \(localSpecRepoName) \(podPath) --sources=\(sourcesArg) \(flagsArg)"
)
shell.run("pod repo update")
return outcome
}
// This will commit and push to erase the entire remote spec repo.
func eraseRemoteRepo(repoPath: String, from githubAccount: String, _ sdkRepoName: String,
shell: Shell = Shell.shared) {
shell
.run(
"git clone --quiet https://${BOT_TOKEN}@github.com/\(githubAccount)/\(sdkRepoName).git"
)
let fileManager = FileManager.default
do {
let dirs = try fileManager.contentsOfDirectory(atPath: "\(repoPath)/\(sdkRepoName)")
for dir in dirs {
if dir != ".git" {
shell.run("cd \(sdkRepoName); git rm -r \(dir)")
}
}
shell.run("cd \(sdkRepoName); git commit -m 'Empty repo'; git push")
} catch {
print("Error while enumerating files \(repoPath): \(error.localizedDescription)")
}
do {
try fileManager.removeItem(at: URL(fileURLWithPath: "\(repoPath)/\(sdkRepoName)"))
} catch {
print("Error occurred while removing \(repoPath)/\(sdkRepoName): \(error)")
}
}
mutating func run() throws {
let fileManager = FileManager.default
let curDir = FileManager().currentDirectoryPath
var podSpecFiles: [String: URL] = [:]
let documentsURL = URL(fileURLWithPath: sdkRepo)
do {
let fileURLs = try fileManager.contentsOfDirectory(
at: documentsURL,
includingPropertiesForKeys: nil
)
let podspecURLs = fileURLs.filter { $0.pathExtension == "podspec" }
for podspecURL in podspecURLs {
let podName = podspecURL.deletingPathExtension().lastPathComponent
if !Constants.exclusivePods.contains(podName) {
podSpecFiles[podName] = podspecURL
}
}
} catch {
print(
"Error while enumerating files \(documentsURL.path): \(error.localizedDescription)"
)
}
// This set is used to keep parent dependencies and help detect circular
// dependencies.
var tmpSet: Set<String> = []
print("Detect podspecs: \(podSpecFiles.keys)")
let specFileDict = SpecFiles(podSpecFiles, from: sdkRepo)
generateOrderOfInstallation(
pods: Array(podSpecFiles.keys),
specFiles: specFileDict,
parentDeps: &tmpSet
)
print("Podspec push order:\n", specFileDict.depInstallOrder.joined(separator: "->\t"))
do {
if fileManager.fileExists(atPath: "\(curDir)/\(sdkRepoName)") {
print("remove \(sdkRepoName) dir.")
try fileManager.removeItem(at: URL(fileURLWithPath: "\(curDir)/\(sdkRepoName)"))
}
eraseRemoteRepo(repoPath: "\(curDir)", from: githubAccount, sdkRepoName)
} catch {
print("error occurred. \(error)")
}
var exitCode: Int32 = 0
var failedPods: [String] = []
for pod in specFileDict.depInstallOrder {
var podExitCode: Int32 = 0
print("----------\(pod)-----------")
switch pod {
case "Firebase":
podExitCode = pushPodspec(
forPod: pod,
sdkRepo: sdkRepo,
sources: podSources,
flags: Constants.umbrellaPodFlags
)
default:
podExitCode = pushPodspec(
forPod: pod,
sdkRepo: sdkRepo,
sources: podSources,
flags: Constants.flags
)
}
if podExitCode != 0 {
exitCode = 1
failedPods.append(pod)
}
}
if exitCode != 0 {
Self.exit(withError: SpecRepoBuilderError.failedToPush(pods: failedPods))
}
}
}
SpecRepoBuilder.main()