InitializeSource.swift 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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. enum 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(_, output):
  52. print("\(podRepoName) was not properly removed. \(podRepoName) probably" +
  53. "does not exist in local.\n \(output)")
  54. case .success:
  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. // Pods could have different versions, like `8.11.0` and `8.11.0-beta`.
  62. // These versions should be part of tags, so a warning from `pod spec lint`
  63. // could be avoided.
  64. // ```
  65. // The version should be included in the Git tag.
  66. // ```
  67. // The tag should include `s.version`, e.g.
  68. // If "s.version = '8.11.0-beta'", the tag should include 8.11.0-beta to
  69. // avoid triggering the warning.
  70. for pod in manifest.pods {
  71. let testingTag = Constants.testingTagPrefix + manifest.versionString(pod)
  72. // Add or update the testing tag to the local sdk repo.
  73. Shell.executeCommand("git tag -af \(testingTag) -m 'spectesting'", workingDir: sdkRepoPath)
  74. }
  75. }
  76. // Update the podspec source.
  77. private static func updatePodspecs(path: URL, manifest: FirebaseManifest.Manifest) {
  78. for pod in manifest.pods {
  79. let version = manifest.versionString(pod)
  80. if !pod.isClosedSource {
  81. // Replace git and tag in the source of a podspec.
  82. // Before:
  83. // s.source = {
  84. // :git => 'https://github.com/firebase/firebase-ios-sdk.git',
  85. // :tag => 'CocoaPods-' + s.version.to_s
  86. // }
  87. // After `sed`:
  88. // s.source = {
  89. // :git => '\(path.path)',
  90. // :tag => 'testing-\(version)',
  91. // }
  92. Shell.executeCommand(
  93. "sed -i.bak -e \"s|\\(.*\\:git =>[[:space:]]*\\).*|\\1'\(path.path)',| ; " +
  94. "s|\\(.*\\:tag =>[[:space:]]*\\).*|\\1'\(Constants.testingTagPrefix + version)',|\" \(pod.name).podspec",
  95. workingDir: path
  96. )
  97. }
  98. }
  99. }
  100. // Copy updated specs to the `${HOME}/.cocoapods/` dir.
  101. private static func copyPodspecs(from specsDir: URL, manifest: FirebaseManifest.Manifest) {
  102. let path = specsDir.appendingPathComponent("*.podspec").path
  103. let paths = Shell.executeCommandFromScript("ls \(path)", outputToConsole: false)
  104. var candidateSpecs: [String]?
  105. switch paths {
  106. case let .error(_, output):
  107. print("specs are not properly read, \(output)")
  108. case let .success(output):
  109. candidateSpecs = output.trimmingCharacters(in: .whitespacesAndNewlines)
  110. .components(separatedBy: "\n")
  111. }
  112. guard let specs = candidateSpecs else {
  113. print("There are no files ending with `podspec` detected.")
  114. return
  115. }
  116. for spec in specs {
  117. let specInfo = fetchPodVersion(from: URL(fileURLWithPath: spec))
  118. // Create directories `${HOME}/.cocoapods/${Pod}/${version}`
  119. let podDirURL = createPodDirectory(
  120. specRepoPath: Constants.cocoapodsDir,
  121. podName: specInfo.name,
  122. version: specInfo.version
  123. )
  124. // Copy updated podspecs to directories `${HOME}/.cocoapods/${Pod}/${version}`
  125. Shell.executeCommand("cp -rf \(spec) \(podDirURL)")
  126. }
  127. }
  128. private static func fetchPodVersion(from path: URL) -> (name: String, version: String) {
  129. var contents = ""
  130. var podName = ""
  131. var version = ""
  132. do {
  133. contents = try String(contentsOfFile: path.path, encoding: .utf8)
  134. } catch {
  135. fatalError("Could not read the podspec. \(error)")
  136. }
  137. // Closed source podspecs, e.g. `GoogleAppMeasurement.podspec`.
  138. if path.pathExtension == "json" {
  139. // Remove both extensions of `podspec` and `json`.
  140. podName = path.deletingPathExtension().deletingPathExtension().lastPathComponent
  141. } else if path.pathExtension == "podspec" {
  142. podName = path.deletingPathExtension().lastPathComponent
  143. }
  144. guard let versionPattern = Constants.versionFetchPatterns[path.pathExtension] else {
  145. fatalError("Regex pattern for \(path.pathExtension) is not found.")
  146. }
  147. do {
  148. version = try matchVersion(from: contents, withPattern: versionPattern)
  149. } catch VersionFetchError.noMatchesCaught {
  150. fatalError(
  151. "Podspec from '\(path.path)' cannot find a version with the following regex\n\(versionPattern)"
  152. )
  153. } catch VersionFetchError.noSubgroupCaught {
  154. fatalError(
  155. "A subgroup of version from Podspec, '\(path.path)', is not caught from the pattern\n\(versionPattern)"
  156. )
  157. } catch VersionFetchError.multipleMatches {
  158. print("found multiple version matches from \(path.path).")
  159. fatalError(
  160. "There should have only one version matching the regex pattern, please update the pattern\n\(versionPattern)"
  161. )
  162. } catch {
  163. fatalError("Version is not caught properly. \(error)")
  164. }
  165. return (podName, version)
  166. }
  167. private static func matchVersion(from content: String,
  168. withPattern regex: String) throws -> String {
  169. let versionMatches = try content.match(regex: regex)
  170. if versionMatches.isEmpty {
  171. throw VersionFetchError.noMatchesCaught
  172. }
  173. // One subgroup in the regex should be for the version
  174. else if versionMatches[0].count < 2 {
  175. throw VersionFetchError.noSubgroupCaught
  176. }
  177. // There are more than one string matching the regex. There should be only
  178. // one version matching the regex.
  179. else if versionMatches.count > 1 {
  180. print(versionMatches)
  181. throw VersionFetchError.multipleMatches
  182. }
  183. return versionMatches[0][1]
  184. }
  185. private static func createPodDirectory(specRepoPath: String, podName: String,
  186. version: String) -> URL {
  187. guard let specRepoURL = URL(string: specRepoPath) else {
  188. fatalError("\(specRepoPath) does not exist.")
  189. }
  190. let podDirPath = specRepoURL.appendingPathComponent(podName).appendingPathComponent(version)
  191. if !FileManager.default.fileExists(atPath: podDirPath.absoluteString) {
  192. do {
  193. print("create path: \(podDirPath.absoluteString)")
  194. try FileManager.default.createDirectory(atPath: podDirPath.absoluteString,
  195. withIntermediateDirectories: true,
  196. attributes: nil)
  197. } catch {
  198. print(error.localizedDescription)
  199. }
  200. }
  201. return podDirPath
  202. }
  203. }
  204. extension String: Error {
  205. /// Returns an array of matching groups, which contains matched string and
  206. /// subgroups.
  207. ///
  208. /// - Parameters:
  209. /// - regex: A string of regex.
  210. /// - Returns: An array of array containing each match and its subgroups.
  211. func match(regex: String) throws -> [[String]] {
  212. do {
  213. let regex = try NSRegularExpression(pattern: regex, options: [])
  214. let nsString = self as NSString
  215. let results = regex.matches(
  216. in: self,
  217. options: [],
  218. range: NSMakeRange(0, nsString.length)
  219. )
  220. return results.map { result in
  221. (0 ..< result.numberOfRanges).map {
  222. nsString.substring(with: result.range(at: $0))
  223. }
  224. }
  225. } catch {
  226. fatalError("regex is invalid\n\(error.localizedDescription)")
  227. }
  228. }
  229. }