main.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. /*
  2. * Copyright 2020 Google LLC
  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. import ArgumentParser
  17. import Foundation
  18. private enum Constants {}
  19. extension Constants {
  20. static let specDependencyLabel = "dependency"
  21. static let skipLinesWithWords = ["unit_tests", "test_spec"]
  22. static let dependencyLineSeparators = CharacterSet(charactersIn: " ,/")
  23. static let podSources = [
  24. "https://${BOT_TOKEN}@github.com/Firebase/SpecsTesting",
  25. "https://github.com/firebase/SpecsStaging.git",
  26. "https://cdn.cocoapods.org/",
  27. ]
  28. }
  29. // flags for 'pod push'
  30. extension Constants {
  31. static let flags = ["--skip-tests", "--allow-warnings"]
  32. static let umbrellaPodFlags = Constants.flags + ["--skip-import-validation", "--use-json"]
  33. }
  34. // SpecFiles is a wraper of dict mapping from required pods to their path. This
  35. // will also contain a sequence of installing podspecs.
  36. class SpecFiles {
  37. private var specFilesDict: [String: URL]
  38. var depInstallOrder: [String]
  39. var specSource: String
  40. init(_ specDict: [String: URL], from specSourcePath: String) {
  41. specFilesDict = specDict
  42. depInstallOrder = []
  43. specSource = specSourcePath
  44. }
  45. func removePod(_ key: String) {
  46. specFilesDict.removeValue(forKey: key)
  47. }
  48. subscript(key: String) -> URL? {
  49. return specFilesDict[key]
  50. }
  51. func contains(_ key: String) -> Bool {
  52. return specFilesDict[key] != nil
  53. }
  54. func isEmpty() -> Bool {
  55. return specFilesDict.isEmpty
  56. }
  57. }
  58. struct Shell {
  59. static let shared = Shell()
  60. @discardableResult
  61. func run(_ command: String, displayCommand: Bool = true,
  62. displayFailureResult: Bool = true) -> Int32 {
  63. let task = Process()
  64. let pipe = Pipe()
  65. task.standardOutput = pipe
  66. task.launchPath = "/bin/bash"
  67. task.arguments = ["-c", command]
  68. task.launch()
  69. if displayCommand {
  70. print("[SpecRepoBuilder] Command:\(command)\n")
  71. }
  72. task.waitUntilExit()
  73. let data = pipe.fileHandleForReading.readDataToEndOfFile()
  74. let log = String(data: data, encoding: .utf8)!
  75. if displayFailureResult, task.terminationStatus != 0 {
  76. print("-----Exit code: \(task.terminationStatus)")
  77. print("-----Log:\n \(log)")
  78. }
  79. return task.terminationStatus
  80. }
  81. }
  82. // Error types
  83. enum SpecRepoBuilderError: Error {
  84. // Error occurs when circular dependencies are detected and deps will be
  85. // displayed.
  86. case circularDependencies(pods: Set<String>)
  87. // Error occurs when there exist specs that failed to push to a spec repo. All
  88. // specs failed to push should be displayed.
  89. case failedToPush(pods: [String])
  90. // Error occurs when a podspec is not found in the repo.
  91. case podspecNotFound(_ podspec: String, from: String)
  92. // Error occurs when a direotyr path cannot be determined.
  93. case pathNotFound(_ path: String)
  94. }
  95. struct SpecRepoBuilder: ParsableCommand {
  96. @Option(help: "The root of the firebase-ios-sdk checked out git repo.")
  97. var sdkRepo: String = FileManager().currentDirectoryPath
  98. @Option(parsing: .upToNextOption, help: "A list of podspec sources in Podfiles.")
  99. var podSources: [String] = Constants.podSources
  100. @Option(parsing: .upToNextOption, help: "Podspecs that will not be pushed to repo.")
  101. var excludePods: [String] = []
  102. @Option(help: "GitHub Account Name.")
  103. var githubAccount: String = "FirebasePrivate"
  104. @Option(help: "GitHub Repo Name.")
  105. var sdkRepoName: String = "SpecsTesting"
  106. @Option(help: "Local Podspec Repo Name.")
  107. var localSpecRepoName: String
  108. @Option(parsing: .upToNextOption, help: "Push selected podspecs.")
  109. var includePods: [String] = []
  110. @Flag(help: "Keep or erase a repo before push.")
  111. var keepRepo: Bool = false
  112. @Flag(help: "Raise error while circular dependency detected.")
  113. var raiseCircularDepError: Bool = false
  114. // This will track down dependencies of pods and keep the sequence of
  115. // dependency installation in specFiles.depInstallOrder.
  116. func generateOrderOfInstallation(pods: [String], specFiles: SpecFiles,
  117. parentDeps: inout Set<String>) {
  118. // pods are dependencies will be tracked down.
  119. // specFiles includes required pods and their URLs.
  120. // parentDeps will record the path of tracking down dependencies to avoid
  121. // duplications and circular dependencies.
  122. // Stop tracking down when the parent pod does not have any required deps.
  123. if pods.isEmpty {
  124. return
  125. }
  126. for pod in pods {
  127. guard specFiles.contains(pod) else { continue }
  128. let deps = getTargetedDeps(of: pod, from: specFiles)
  129. // parentDeps will have all dependencies the current pod supports. If the
  130. // current pod were in the parent dependencies, that means it was tracked
  131. // before and it is circular dependency.
  132. if parentDeps.contains(pod) {
  133. print("Circular dependency is detected in \(pod) and \(parentDeps)")
  134. if raiseCircularDepError {
  135. Self
  136. .exit(withError: SpecRepoBuilderError
  137. .circularDependencies(pods: parentDeps))
  138. }
  139. continue
  140. }
  141. // Record the pod as a parent and use depth-first-search to track down
  142. // dependencies of this pod.
  143. parentDeps.insert(pod)
  144. generateOrderOfInstallation(
  145. pods: deps,
  146. specFiles: specFiles,
  147. parentDeps: &parentDeps
  148. )
  149. // When pod does not have required dep or its required deps are recorded,
  150. // the pod itself will be recorded into the depInstallOrder.
  151. if !specFiles.depInstallOrder.contains(pod) {
  152. print("\(pod) depends on \(deps).")
  153. specFiles.depInstallOrder.append(pod)
  154. }
  155. // When track back from a lower level, parentDep should track back by
  156. // removing one pod.
  157. parentDeps.remove(pod)
  158. }
  159. }
  160. // Scan a podspec file and find and return all dependencies in this podspec.
  161. func searchDeps(ofPod podName: String, from podSpecFiles: SpecFiles) -> [String] {
  162. var deps: [String] = []
  163. var fileContents = ""
  164. guard let podSpecURL = podSpecFiles[podName] else {
  165. Self
  166. .exit(withError: SpecRepoBuilderError
  167. .podspecNotFound(podName, from: podSpecFiles.specSource))
  168. }
  169. do {
  170. fileContents = try String(contentsOfFile: podSpecURL.path, encoding: .utf8)
  171. } catch {
  172. fatalError("Could not read \(podName) podspec from \(podSpecURL.path).")
  173. }
  174. // Get all the lines containing `dependency` but don't contain words we
  175. // want to ignore.
  176. let depLines: [String] = fileContents
  177. .components(separatedBy: .newlines)
  178. .filter { $0.contains("dependency") }
  179. // Skip lines with words in Constants.skipLinesWithWords
  180. .filter { !Constants.skipLinesWithWords.contains(where: $0.contains)
  181. }
  182. for line in depLines {
  183. let newLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
  184. // This is to avoid pushing umbrellapods like Firebase/Core.
  185. let tokens = newLine.components(separatedBy: Constants.dependencyLineSeparators)
  186. if let depPrefix = tokens.first {
  187. if depPrefix.hasSuffix(Constants.specDependencyLabel) {
  188. // e.g. In Firebase.podspec, Firebase/Core will not be considered a
  189. // dependency.
  190. // "ss.dependency 'Firebase/Core'" will be splited in
  191. // ["ss.dependency", "'Firebase", "Core'"]
  192. let podNameRaw = String(tokens[1]).replacingOccurrences(of: "'", with: "")
  193. // In the example above, deps here will not include Firebase since
  194. // it is the same as the pod name.
  195. if podNameRaw != podName { deps.append(podNameRaw) }
  196. }
  197. }
  198. }
  199. return deps
  200. }
  201. // Filter and get a list of required dependencies found in the repo.
  202. func filterTargetDeps(_ deps: [String], with targets: SpecFiles) -> [String] {
  203. var targetedDeps: [String] = []
  204. for dep in deps {
  205. // Only get unique and required dep in the output list.
  206. if targets.contains(dep), !targetedDeps.contains(dep) {
  207. targetedDeps.append(dep)
  208. }
  209. }
  210. return targetedDeps
  211. }
  212. func getTargetedDeps(of pod: String, from specFiles: SpecFiles) -> [String] {
  213. let deps = searchDeps(ofPod: pod, from: specFiles)
  214. return filterTargetDeps(deps, with: specFiles)
  215. }
  216. func pushPodspec(forPod pod: String, sdkRepo: String, sources: [String],
  217. flags: [String], shell: Shell = Shell.shared) -> Int32 {
  218. let podPath = sdkRepo + "/" + pod + ".podspec"
  219. let sourcesArg = sources.joined(separator: ",")
  220. let flagsArg = flags.joined(separator: " ")
  221. let outcome =
  222. shell
  223. .run(
  224. "pod repo push \(localSpecRepoName) \(podPath) --sources=\(sourcesArg) \(flagsArg)"
  225. )
  226. shell.run("pod repo update")
  227. print("Outcome is \(outcome)")
  228. return outcome
  229. }
  230. // This will commit and push to erase the entire remote spec repo.
  231. func eraseRemoteRepo(repoPath: String, from githubAccount: String, _ sdkRepoName: String,
  232. shell: Shell = Shell.shared) {
  233. shell
  234. .run(
  235. "git clone --quiet https://${BOT_TOKEN}@github.com/\(githubAccount)/\(sdkRepoName).git"
  236. )
  237. let fileManager = FileManager.default
  238. do {
  239. let sdk_repo_path = "\(repoPath)/\(sdkRepoName)"
  240. print("The repo path is \(sdk_repo_path)")
  241. guard let repo_url = URL(string: sdk_repo_path) else {
  242. print("Error: cannot find \(sdk_repo_path).")
  243. Self
  244. .exit(withError: SpecRepoBuilderError
  245. .pathNotFound(sdk_repo_path))
  246. }
  247. // Skip hidden files, e.g. /.git
  248. let dirs = try fileManager.contentsOfDirectory(
  249. at: repo_url,
  250. includingPropertiesForKeys: nil,
  251. options: [.skipsHiddenFiles]
  252. )
  253. print("Found following unhidden dirs: \(dirs)")
  254. for dir in dirs {
  255. guard let isDir = (try dir.resourceValues(forKeys: [.isDirectoryKey])).isDirectory else {
  256. print("Error: cannot determine if \(dir.path) is a directory or not.")
  257. Self
  258. .exit(withError: SpecRepoBuilderError
  259. .pathNotFound(dir.path))
  260. }
  261. if isDir {
  262. print("Removing \(dir.path)")
  263. shell.run("cd \(sdkRepoName); git rm -r \(dir.path)")
  264. }
  265. }
  266. shell.run("cd \(sdkRepoName); git commit -m 'Empty repo'; git push")
  267. } catch {
  268. print("Error while enumerating files \(repoPath): \(error.localizedDescription)")
  269. }
  270. do {
  271. try fileManager.removeItem(at: URL(fileURLWithPath: "\(repoPath)/\(sdkRepoName)"))
  272. } catch {
  273. print("Error occurred while removing \(repoPath)/\(sdkRepoName): \(error)")
  274. }
  275. }
  276. mutating func run() throws {
  277. let fileManager = FileManager.default
  278. let curDir = FileManager().currentDirectoryPath
  279. var podSpecFiles: [String: URL] = [:]
  280. let documentsURL = URL(fileURLWithPath: sdkRepo)
  281. do {
  282. let fileURLs = try fileManager.contentsOfDirectory(
  283. at: documentsURL,
  284. includingPropertiesForKeys: nil
  285. )
  286. let podspecURLs = fileURLs.filter { $0.pathExtension == "podspec" }
  287. for podspecURL in podspecURLs {
  288. let podName = podspecURL.deletingPathExtension().lastPathComponent
  289. if excludePods.contains(podName) {
  290. continue
  291. } else if includePods.isEmpty || includePods.contains(podName) {
  292. podSpecFiles[podName] = podspecURL
  293. }
  294. }
  295. } catch {
  296. print(
  297. "Error while enumerating files \(documentsURL.path): \(error.localizedDescription)"
  298. )
  299. }
  300. // This set is used to keep parent dependencies and help detect circular
  301. // dependencies.
  302. var tmpSet: Set<String> = []
  303. print("Detect podspecs: \(podSpecFiles.keys)")
  304. let specFileDict = SpecFiles(podSpecFiles, from: sdkRepo)
  305. generateOrderOfInstallation(
  306. pods: Array(podSpecFiles.keys),
  307. specFiles: specFileDict,
  308. parentDeps: &tmpSet
  309. )
  310. print("Podspec push order:\n", specFileDict.depInstallOrder.joined(separator: "->\t"))
  311. if !keepRepo {
  312. do {
  313. if fileManager.fileExists(atPath: "\(curDir)/\(sdkRepoName)") {
  314. print("remove \(sdkRepoName) dir.")
  315. try fileManager.removeItem(at: URL(fileURLWithPath: "\(curDir)/\(sdkRepoName)"))
  316. }
  317. eraseRemoteRepo(repoPath: "\(curDir)", from: githubAccount, sdkRepoName)
  318. } catch {
  319. print("error occurred. \(error)")
  320. }
  321. }
  322. var exitCode: Int32 = 0
  323. var failedPods: [String] = []
  324. for pod in specFileDict.depInstallOrder {
  325. var podExitCode: Int32 = 0
  326. print("----------\(pod)-----------")
  327. switch pod {
  328. case "Firebase":
  329. podExitCode = pushPodspec(
  330. forPod: pod,
  331. sdkRepo: sdkRepo,
  332. sources: podSources,
  333. flags: Constants.umbrellaPodFlags
  334. )
  335. default:
  336. podExitCode = pushPodspec(
  337. forPod: pod,
  338. sdkRepo: sdkRepo,
  339. sources: podSources,
  340. flags: Constants.flags
  341. )
  342. }
  343. if podExitCode != 0 {
  344. exitCode = 1
  345. failedPods.append(pod)
  346. print("Failed pod - \(pod)")
  347. }
  348. }
  349. if exitCode != 0 {
  350. Self.exit(withError: SpecRepoBuilderError.failedToPush(pods: failedPods))
  351. }
  352. }
  353. }
  354. SpecRepoBuilder.main()