| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- /*
- * 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/Firebase/SpecsTesting",
- "https://github.com/firebase/SpecsStaging.git",
- "https://cdn.cocoapods.org/",
- ]
- }
- // 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)
- // Error occurs when a direotyr path cannot be determined.
- case pathNotFound(_ path: String)
- }
- struct SpecRepoBuilder: ParsableCommand {
- @Option(help: "The root of the firebase-ios-sdk checked out git repo.")
- var sdkRepo: String = FileManager().currentDirectoryPath
- @Option(parsing: .upToNextOption, help: "A list of podspec sources in Podfiles.")
- var podSources: [String] = Constants.podSources
- @Option(parsing: .upToNextOption, help: "Podspecs that will not be pushed to repo.")
- var excludePods: [String] = []
- @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
- @Option(parsing: .upToNextOption, help: "Push selected podspecs.")
- var includePods: [String] = []
- @Flag(help: "Keep or erase a repo before push.")
- var keepRepo: Bool = false
- @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")
- print("Outcome is \(outcome)")
- 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 sdk_repo_path = "\(repoPath)/\(sdkRepoName)"
- print("The repo path is \(sdk_repo_path)")
- guard let repo_url = URL(string: sdk_repo_path) else {
- print("Error: cannot find \(sdk_repo_path).")
- Self
- .exit(withError: SpecRepoBuilderError
- .pathNotFound(sdk_repo_path))
- }
- // Skip hidden files, e.g. /.git
- let dirs = try fileManager.contentsOfDirectory(
- at: repo_url,
- includingPropertiesForKeys: nil,
- options: [.skipsHiddenFiles]
- )
- print("Found following unhidden dirs: \(dirs)")
- for dir in dirs {
- guard let isDir = (try dir.resourceValues(forKeys: [.isDirectoryKey])).isDirectory else {
- print("Error: cannot determine if \(dir.path) is a directory or not.")
- Self
- .exit(withError: SpecRepoBuilderError
- .pathNotFound(dir.path))
- }
- if isDir {
- print("Removing \(dir.path)")
- shell.run("cd \(sdkRepoName); git rm -r \(dir.path)")
- }
- }
- 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 excludePods.contains(podName) {
- continue
- } else if includePods.isEmpty || includePods.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"))
- if !keepRepo {
- 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)
- print("Failed pod - \(pod)")
- }
- }
- if exitCode != 0 {
- Self.exit(withError: SpecRepoBuilderError.failedToPush(pods: failedPods))
- }
- }
- }
- SpecRepoBuilder.main()
|