|
| 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