InitializeSource.swift 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. /*
  2. * Copyright 2021 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 Foundation
  17. import FirebaseManifest
  18. import Utils
  19. private enum Constants {}
  20. extension Constants {
  21. static let localSpecRepoName = "specstesting"
  22. static let specRepo = "https://github.com/firebase/SpecsTesting"
  23. static let sdkRepo = "https://github.com/firebase/firebase-ios-sdk"
  24. static let testingTagPrefix = "testing-"
  25. static let cocoapodsDir =
  26. "\(ProcessInfo.processInfo.environment["HOME"]!)/.cocoapods/repos/\(localSpecRepoName)"
  27. static let versionFetchPatterns = [
  28. "json": "\"version\"[[:space:]]*:[[:space:]]*\"(.*)\"",
  29. "podspec": "\\.version[[:space:]]*=[[:space:]]*\'([^~=><].*)\'",
  30. ]
  31. }
  32. struct InitializeSpecTesting {
  33. enum VersionFetchError: Error {
  34. case noMatchesCaught
  35. case multipleMatches
  36. case noSubgroupCaught
  37. }
  38. static func setupRepo(sdkRepoURL: URL) {
  39. let manifest = FirebaseManifest.shared
  40. addSpecRepo(repoURL: Constants.specRepo)
  41. addTestingTag(path: sdkRepoURL, manifest: manifest)
  42. updatePodspecs(path: sdkRepoURL, manifest: manifest)
  43. copyPodspecs(from: sdkRepoURL, manifest: manifest)
  44. }
  45. // The SpecsTesting repo will be added to `${HOME}/.cocoapods/`, and all
  46. // podspecs under this dir will be the source of the specs testing.
  47. private static func addSpecRepo(repoURL: String,
  48. podRepoName: String = Constants.localSpecRepoName) {
  49. let result = Shell.executeCommandFromScript("pod repo remove \(podRepoName)")
  50. switch result {
  51. case let .error(code, output):
  52. print("\(podRepoName) was not properly removed. \(podRepoName) probably" +
  53. "does not exist in local.\n \(output)")
  54. case let .success(output):
  55. print("\(podRepoName) was removed.")
  56. }
  57. Shell.executeCommand("pod repo add \(podRepoName) \(repoURL)")
  58. }
  59. // Add a testing tag to the head of the branch.
  60. private static func addTestingTag(path sdkRepoPath: URL, manifest: FirebaseManifest.Manifest) {
  61. let testingTag = Constants.testingTagPrefix + manifest.version
  62. // Add or update the testing tag to the local sdk repo.
  63. Shell.executeCommand("git tag -af \(testingTag) -m 'spectesting'", workingDir: sdkRepoPath)
  64. }
  65. // Update the podspec source.
  66. private static func updatePodspecs(path: URL, manifest: FirebaseManifest.Manifest) {
  67. for pod in manifest.pods {
  68. let version = manifest.versionString(pod)
  69. if !pod.isClosedSource {
  70. // Replace git and tag in the source of a podspec.
  71. // Before:
  72. // s.source = {
  73. // :git => 'https://github.com/firebase/firebase-ios-sdk.git',
  74. // :tag => 'CocoaPods-' + s.version.to_s
  75. // }
  76. // After `sed`:
  77. // s.source = {
  78. // :git => '\(path.path)',
  79. // :tag => 'testing-\(manifest.version)',
  80. // }
  81. Shell.executeCommand(
  82. "sed -i.bak -e \"s|\\(.*\\:git =>[[:space:]]*\\).*|\\1'\(path.path)',| ; " +
  83. "s|\\(.*\\:tag =>[[:space:]]*\\).*|\\1'\(Constants.testingTagPrefix + manifest.version)',|\" \(pod.name).podspec",
  84. workingDir: path
  85. )
  86. }
  87. }
  88. }
  89. // Copy updated specs to the `${HOME}/.cocoapods/` dir.
  90. private static func copyPodspecs(from specsDir: URL, manifest: FirebaseManifest.Manifest) {
  91. let path = specsDir.appendingPathComponent("*.{podspec,podspec.json}").path
  92. let paths = Shell.executeCommandFromScript("ls \(path)", outputToConsole: false)
  93. var candidateSpecs: [String]?
  94. switch paths {
  95. case let .error(code, output):
  96. print("specs are not properly read, \(output)")
  97. case let .success(output):
  98. candidateSpecs = output.trimmingCharacters(in: .whitespacesAndNewlines)
  99. .components(separatedBy: "\n")
  100. }
  101. guard let specs = candidateSpecs else {
  102. print("There are no files ending with `podspec` or `podspec.json` detected.")
  103. return
  104. }
  105. for spec in specs {
  106. let specInfo = fetchPodVersion(from: URL(fileURLWithPath: spec))
  107. // Create directories `${HOME}/.cocoapods/${Pod}/${version}`
  108. let podDirURL = createPodDirctory(
  109. specRepoPath: Constants.cocoapodsDir,
  110. podName: specInfo.name,
  111. version: specInfo.version
  112. )
  113. // Copy updated podspecs to directories `${HOME}/.cocoapods/${Pod}/${version}`
  114. Shell.executeCommand("cp -rf \(spec) \(podDirURL)")
  115. }
  116. }
  117. private static func fetchPodVersion(from path: URL) -> (name: String, version: String) {
  118. var contents: String = ""
  119. var podName: String = ""
  120. var version: String = ""
  121. do {
  122. contents = try String(contentsOfFile: path.path, encoding: .utf8)
  123. } catch {
  124. fatalError("Could not read the podspec. \(error)")
  125. }
  126. // Closed source podspecs, e.g. GoogleAppMeasurement.podspec.json.
  127. if path.pathExtension == "json" {
  128. // Remove both extenstions of `podspec` and `json`.
  129. podName = path.deletingPathExtension().deletingPathExtension().lastPathComponent
  130. } else if path.pathExtension == "podspec" {
  131. podName = path.deletingPathExtension().lastPathComponent
  132. }
  133. guard let versionPattern = Constants.versionFetchPatterns[path.pathExtension] else {
  134. fatalError("Regex pattern for \(path.pathExtension) is not found.")
  135. }
  136. do {
  137. version = try matchVersion(from: contents, withPattern: versionPattern)
  138. } catch VersionFetchError.noMatchesCaught {
  139. fatalError(
  140. "Podspec from '\(path.path)' cannot find a version with the following regex\n\(versionPattern)"
  141. )
  142. } catch VersionFetchError.noSubgroupCaught {
  143. fatalError(
  144. "A subgroup of version from Podspec, '\(path.path)', is not caught from the pattern\n\(versionPattern)"
  145. )
  146. } catch VersionFetchError.multipleMatches {
  147. print("found multiple version matches from \(path.path).")
  148. fatalError(
  149. "There should have only one version matching the regex pattern, please update the pattern\n\(versionPattern)"
  150. )
  151. } catch {
  152. fatalError("Version is not caught properly. \(error)")
  153. }
  154. return (podName, version)
  155. }
  156. private static func matchVersion(from content: String,
  157. withPattern regex: String) throws -> String {
  158. let versionMatches = try content.match(regex: regex)
  159. if versionMatches.isEmpty {
  160. throw VersionFetchError.noMatchesCaught
  161. }
  162. // One subgroup in the regex should be for the version
  163. else if versionMatches[0].count < 2 {
  164. throw VersionFetchError.noSubgroupCaught
  165. }
  166. // There are more than one string matching the regex. There should be only
  167. // one version matching the regex.
  168. else if versionMatches.count > 1 {
  169. print(versionMatches)
  170. throw VersionFetchError.multipleMatches
  171. }
  172. return versionMatches[0][1]
  173. }
  174. private static func createPodDirctory(specRepoPath: String, podName: String,
  175. version: String) -> URL {
  176. guard let specRepoURL = URL(string: specRepoPath) else {
  177. fatalError("\(specRepoPath) does not exist.")
  178. }
  179. let podDirPath = specRepoURL.appendingPathComponent(podName).appendingPathComponent(version)
  180. if !FileManager.default.fileExists(atPath: podDirPath.absoluteString) {
  181. do {
  182. print("create path: \(podDirPath.absoluteString)")
  183. try FileManager.default.createDirectory(atPath: podDirPath.absoluteString,
  184. withIntermediateDirectories: true,
  185. attributes: nil)
  186. } catch {
  187. print(error.localizedDescription)
  188. }
  189. }
  190. return podDirPath
  191. }
  192. }
  193. extension String: Error {
  194. /// Returns an array of matching groups, which contains matched string and
  195. /// subgroups.
  196. ///
  197. /// - Parameters:
  198. /// - regex: A string of regex.
  199. /// - Returns: An array of array containing each match and its subgroups.
  200. func match(regex: String) throws -> [[String]] {
  201. do {
  202. let regex = try NSRegularExpression(pattern: regex, options: [])
  203. let nsString = self as NSString
  204. let results = regex.matches(
  205. in: self,
  206. options: [],
  207. range: NSMakeRange(0, nsString.length)
  208. )
  209. return results.map { result in
  210. (0 ..< result.numberOfRanges).map {
  211. nsString.substring(with: result.range(at: $0))
  212. }
  213. }
  214. } catch {
  215. fatalError("regex is invalid\n\(error.localizedDescription)")
  216. }
  217. }
  218. }