CocoaPodUtils.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. /*
  2. * Copyright 2019 Google
  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 Utils
  18. /// CocoaPod related utility functions. The enum type is used as a namespace here instead of having
  19. /// root functions, and no cases should be added to it.
  20. enum CocoaPodUtils {
  21. /// The linkage type to specify for CocoaPods installation.
  22. enum LinkageType {
  23. /// Forced static libraries. Uses `use_modular_headers!` in the Podfile. Required for module map
  24. /// generation
  25. case forcedStatic
  26. /// Dynamic frameworks. Uses `use_frameworks!` in the Podfile.
  27. case dynamic
  28. /// Static frameworks. Uses `use_frameworks! :linkage => :static` in the Podfile. Enum case is
  29. /// prefixed with `standard` to avoid the `static` keyword.
  30. case standardStatic
  31. }
  32. // MARK: - Public API
  33. // Codable is required because Decodable does not make CodingKeys available.
  34. struct VersionedPod: Codable, CustomDebugStringConvertible {
  35. /// Public name of the pod.
  36. let name: String
  37. /// The version of the requested pod.
  38. let version: String?
  39. /// Platforms supported
  40. let platforms: Set<String>
  41. init(name: String,
  42. version: String?,
  43. platforms: Set<String> = ["ios", "macos", "tvos"]) {
  44. self.name = name
  45. self.version = version
  46. self.platforms = platforms
  47. }
  48. init(from decoder: Decoder) throws {
  49. let container = try decoder.container(keyedBy: CodingKeys.self)
  50. name = try container.decode(String.self, forKey: .name)
  51. if let platforms = try container.decodeIfPresent(Set<String>.self, forKey: .platforms) {
  52. self.platforms = platforms
  53. } else {
  54. platforms = ["ios", "macos", "tvos"]
  55. }
  56. if let version = try container.decodeIfPresent(String.self, forKey: .version) {
  57. self.version = version
  58. } else {
  59. version = nil
  60. }
  61. }
  62. /// The debug description as required by `CustomDebugStringConvertible`.
  63. var debugDescription: String {
  64. var desc = name
  65. if let version = version {
  66. desc.append(" v\(version)")
  67. }
  68. return desc
  69. }
  70. }
  71. /// Information associated with an installed pod.
  72. /// This is a class so that moduleMapContents can be updated via reference.
  73. class PodInfo {
  74. /// The version of the generated pod.
  75. let version: String
  76. /// The pod dependencies.
  77. let dependencies: [String]
  78. /// The location of the pod on disk.
  79. let installedLocation: URL
  80. /// Source pod flag.
  81. let isSourcePod: Bool
  82. /// Binary frameworks in this pod.
  83. let binaryFrameworks: [URL]
  84. /// Subspecs installed for this pod.
  85. let subspecs: Set<String>
  86. /// The contents of the module map for all frameworks associated with the pod.
  87. var moduleMapContents: ModuleMapBuilder.ModuleMapContents?
  88. init(version: String,
  89. dependencies: [String],
  90. installedLocation: URL,
  91. subspecs: Set<String>,
  92. localPodspecPath: URL?) {
  93. self.version = version
  94. self.dependencies = dependencies
  95. self.installedLocation = installedLocation
  96. self.subspecs = subspecs
  97. // Get all the frameworks contained in this directory.
  98. var binaryFrameworks: [URL] = []
  99. if installedLocation != localPodspecPath {
  100. do {
  101. binaryFrameworks = try FileManager.default.recursivelySearch(for: .frameworks,
  102. in: installedLocation)
  103. } catch {
  104. fatalError("Cannot search for .framework files in Pods directory " +
  105. "\(installedLocation): \(error)")
  106. }
  107. }
  108. self.binaryFrameworks = binaryFrameworks
  109. isSourcePod = binaryFrameworks == []
  110. }
  111. }
  112. /// Executes the `pod cache clean --all` command to remove any cached CocoaPods.
  113. static func cleanPodCache() {
  114. let result = Shell.executeCommandFromScript("pod cache clean --all", outputToConsole: false)
  115. switch result {
  116. case let .error(code, _):
  117. fatalError("Could not clean the pod cache, the command exited with \(code). Try running the" +
  118. "command in Terminal to see what's wrong.")
  119. case .success:
  120. // No need to do anything else, continue on.
  121. print("Successfully cleaned pod cache.")
  122. return
  123. }
  124. }
  125. /// Gets metadata from installed Pods. Reads the `Podfile.lock` file and parses it.
  126. static func installedPodsInfo(inProjectDir projectDir: URL,
  127. localPodspecPath: URL?) -> [String: PodInfo] {
  128. // Read from the Podfile.lock to get the installed versions and names.
  129. let podfileLock: String
  130. do {
  131. podfileLock = try String(contentsOf: projectDir.appendingPathComponent("Podfile.lock"))
  132. } catch {
  133. fatalError("Could not read contents of `Podfile.lock` to get installed Pod info in " +
  134. "\(projectDir): \(error)")
  135. }
  136. // Get the pods in the format of [PodInfo].
  137. return loadPodInfoFromPodfileLock(contents: podfileLock,
  138. inProjectDir: projectDir,
  139. localPodspecPath: localPodspecPath)
  140. }
  141. /// Install an array of pods in a specific directory, returning a dictionary of PodInfo for each pod
  142. /// that was installed.
  143. /// - Parameters:
  144. /// - pods: List of VersionedPods to install
  145. /// - directory: Destination directory for the pods.
  146. /// - platform: Install for one platform at a time.
  147. /// - customSpecRepos: Additional spec repos to check for installation.
  148. /// - linkage: Specifies the linkage type. When `forcedStatic` is used, for the module map
  149. /// construction, we want pod names not module names in the generated OTHER_LD_FLAGS
  150. /// options.
  151. /// - Returns: A dictionary of PodInfo's keyed by the pod name.
  152. @discardableResult
  153. static func installPods(_ pods: [VersionedPod],
  154. inDir directory: URL,
  155. platform: Platform,
  156. customSpecRepos: [URL]?,
  157. localPodspecPath: URL?,
  158. linkage: LinkageType) -> [String: PodInfo] {
  159. let fileManager = FileManager.default
  160. // Ensure the directory exists, otherwise we can't install all subspecs.
  161. guard fileManager.directoryExists(at: directory) else {
  162. fatalError("Attempted to install subpecs (\(pods)) in a directory that doesn't exist: " +
  163. "\(directory)")
  164. }
  165. // Ensure there are actual podspecs to install.
  166. guard !pods.isEmpty else {
  167. fatalError("Attempted to install an empty array of subspecs")
  168. }
  169. // Attempt to write the Podfile to disk.
  170. do {
  171. try writePodfile(for: pods,
  172. toDirectory: directory,
  173. customSpecRepos: customSpecRepos,
  174. platform: platform,
  175. localPodspecPath: localPodspecPath,
  176. linkage: linkage)
  177. } catch let FileManager.FileError.directoryNotFound(path) {
  178. fatalError("Failed to write Podfile with pods \(pods) at path \(path)")
  179. } catch let FileManager.FileError.writeToFileFailed(path, error) {
  180. fatalError("Failed to write Podfile for all pods at path: \(path), error: \(error)")
  181. } catch {
  182. fatalError("Unspecified error writing Podfile for all pods to disk: \(error)")
  183. }
  184. // Run pod install on the directory that contains the Podfile and blank Xcode project.
  185. checkCocoaPodsVersion(directory: directory)
  186. let result = Shell.executeCommandFromScript("pod install", workingDir: directory)
  187. switch result {
  188. case let .error(code, output):
  189. fatalError("""
  190. `pod install` failed with exit code \(code) while trying to install pods:
  191. \(pods)
  192. Output from `pod install`:
  193. \(output)
  194. """)
  195. case let .success(output):
  196. // Print the output to the console and return the information for all installed pods.
  197. print(output)
  198. return installedPodsInfo(inProjectDir: directory, localPodspecPath: localPodspecPath)
  199. }
  200. }
  201. /// Load installed Pods from the contents of a `Podfile.lock` file.
  202. ///
  203. /// - Parameter contents: The contents of a `Podfile.lock` file.
  204. /// - Returns: A dictionary of PodInfo structs keyed by the pod name.
  205. static func loadPodInfoFromPodfileLock(contents: String,
  206. inProjectDir projectDir: URL,
  207. localPodspecPath: URL?) -> [String: PodInfo] {
  208. // This pattern matches a pod name with its version (two to three components)
  209. // Examples:
  210. // - FirebaseUI/Google (4.1.1):
  211. // - GoogleSignIn (4.0.2):
  212. // Force unwrap the regular expression since we know it will work, it's a constant being passed
  213. // in. If any changes are made, be sure to run this script to ensure it works.
  214. let depRegex: NSRegularExpression = try! NSRegularExpression(pattern: " - (.+).*",
  215. options: [])
  216. let quotes = CharacterSet(charactersIn: "\"")
  217. var pods: [String: String] = [:]
  218. var deps: [String: Set<String>] = [:]
  219. var currentPod: String?
  220. for line in contents.components(separatedBy: .newlines) {
  221. if line.starts(with: "DEPENDENCIES:") {
  222. break
  223. }
  224. if let (pod, version) = detectVersion(fromLine: line) {
  225. currentPod = pod.trimmingCharacters(in: quotes)
  226. pods[currentPod!] = version
  227. } else if let currentPod = currentPod {
  228. let matches = depRegex
  229. .matches(in: line, range: NSRange(location: 0, length: line.utf8.count))
  230. // Match something like - GTMSessionFetcher/Full (= 1.3.0)
  231. if let match = matches.first {
  232. let depLine = (line as NSString).substring(with: match.range(at: 0)) as String
  233. // Split spaces and subspecs.
  234. let dep = depLine.components(separatedBy: [" "])[2].trimmingCharacters(in: quotes)
  235. if dep != currentPod {
  236. deps[currentPod, default: Set()].insert(dep)
  237. }
  238. }
  239. }
  240. }
  241. // Organize the subspecs
  242. var versions: [String: String] = [:]
  243. var subspecs: [String: Set<String>] = [:]
  244. for (podName, version) in pods {
  245. let subspecArray = podName.components(separatedBy: "/")
  246. if subspecArray.count == 1 || subspecArray[0] == "abseil" {
  247. // Special case for abseil since it has two layers and no external deps.
  248. versions[subspecArray[0]] = version
  249. } else if subspecArray.count > 2 {
  250. fatalError("Multi-layered subspecs are not supported - \(podName)")
  251. } else {
  252. if let previousVersion = versions[podName], version != previousVersion {
  253. fatalError("Different installed versions for \(podName)." +
  254. "\(version) versus \(previousVersion)")
  255. } else {
  256. let basePodName = subspecArray[0]
  257. versions[basePodName] = version
  258. subspecs[basePodName, default: Set()].insert(subspecArray[1])
  259. deps[basePodName] = deps[basePodName, default: Set()].union(deps[podName] ?? Set())
  260. }
  261. }
  262. }
  263. // Generate an InstalledPod for each Pod found.
  264. let podsDir = projectDir.appendingPathComponent("Pods")
  265. var installedPods: [String: PodInfo] = [:]
  266. for (podName, version) in versions {
  267. var podDir = podsDir.appendingPathComponent(podName)
  268. // Make sure that pod got installed if it's not coming from a local podspec.
  269. if !FileManager.default.directoryExists(at: podDir) {
  270. guard let repoDir = localPodspecPath else {
  271. fatalError("Directory for \(podName) doesn't exist at \(podDir) - failed while getting " +
  272. "information for installed Pods.")
  273. }
  274. podDir = repoDir
  275. }
  276. let dependencies = [String](deps[podName] ?? [])
  277. let podInfo = PodInfo(version: version,
  278. dependencies: dependencies,
  279. installedLocation: podDir,
  280. subspecs: subspecs[podName] ?? Set(),
  281. localPodspecPath: localPodspecPath)
  282. installedPods[podName] = podInfo
  283. }
  284. return installedPods
  285. }
  286. static func updateRepos() {
  287. let result = Shell.executeCommandFromScript("pod repo update")
  288. switch result {
  289. case let .error(_, output):
  290. fatalError("Command `pod repo update` failed: \(output)")
  291. case .success:
  292. return
  293. }
  294. }
  295. static func podInstallPrepare(inProjectDir projectDir: URL, templateDir: URL) {
  296. do {
  297. // Create the directory and all intermediate directories.
  298. try FileManager.default.createDirectory(at: projectDir, withIntermediateDirectories: true)
  299. } catch {
  300. // Use `do/catch` instead of `guard let tempDir = try?` so we can print the error thrown.
  301. fatalError("Cannot create temporary directory at beginning of script: \(error)")
  302. }
  303. // Copy the Xcode project needed in order to be able to install Pods there.
  304. let templateFiles = Constants.ProjectPath.requiredFilesForBuilding.map {
  305. templateDir.appendingPathComponent($0)
  306. }
  307. for file in templateFiles {
  308. // Each file should be copied to the temporary project directory with the same name.
  309. let destination = projectDir.appendingPathComponent(file.lastPathComponent)
  310. do {
  311. if !FileManager.default.fileExists(atPath: destination.path) {
  312. print("Copying template file \(file) to \(destination)...")
  313. try FileManager.default.copyItem(at: file, to: destination)
  314. }
  315. } catch {
  316. fatalError("Could not copy template project to temporary directory in order to install " +
  317. "pods. Failed while attempting to copy \(file) to \(destination). \(error)")
  318. }
  319. }
  320. }
  321. /// Get all transitive pod dependencies for a pod.
  322. /// - Returns: An array of Strings of pod names.
  323. static func transitivePodDependencies(for podName: String,
  324. in installedPods: [String: PodInfo]) -> [String] {
  325. var newDeps = Set([podName])
  326. var returnDeps = Set<String>()
  327. repeat {
  328. var foundDeps = Set<String>()
  329. for dep in newDeps {
  330. let childDeps = installedPods[dep]?.dependencies ?? []
  331. foundDeps.formUnion(Set(childDeps))
  332. }
  333. newDeps = foundDeps.subtracting(returnDeps)
  334. returnDeps.formUnion(newDeps)
  335. } while newDeps.count > 0
  336. return Array(returnDeps)
  337. }
  338. /// Get all transitive pod dependencies for a pod with subspecs merged.
  339. /// - Returns: An array of Strings of pod names.
  340. static func transitiveMasterPodDependencies(for podName: String,
  341. in installedPods: [String: PodInfo]) -> [String] {
  342. return Array(Set(transitivePodDependencies(for: podName, in: installedPods).map {
  343. $0.components(separatedBy: "/")[0]
  344. }))
  345. }
  346. /// Get all transitive pod dependencies for a pod.
  347. /// - Returns: An array of dependencies with versions for a given pod.
  348. static func transitiveVersionedPodDependencies(for podName: String,
  349. in installedPods: [String: PodInfo])
  350. -> [VersionedPod] {
  351. return transitivePodDependencies(for: podName, in: installedPods).map {
  352. var podVersion: String?
  353. if let version = installedPods[$0]?.version {
  354. podVersion = version
  355. } else {
  356. // See if there's a version on the base pod.
  357. let basePod = String($0.split(separator: "/")[0])
  358. podVersion = installedPods[basePod]?.version
  359. }
  360. return CocoaPodUtils.VersionedPod(name: $0, version: podVersion)
  361. }
  362. }
  363. // MARK: - Private Helpers
  364. // Tests the input to see if it matches a CocoaPod framework and its version.
  365. // Returns the framework and version or nil if match failed.
  366. // Used to process entries from Podfile.lock
  367. /// Tests the input and sees if it matches a CocoaPod framework and its version. This is used to
  368. /// process entries from Podfile.lock.
  369. ///
  370. /// - Parameters:
  371. /// - input: A line entry from Podfile.lock.
  372. /// - Returns: A tuple of the framework and version, if it can be parsed.
  373. private static func detectVersion(fromLine input: String)
  374. -> (framework: String, version: String)? {
  375. // Get the components of the line to parse them individually. Ignore any whitespace only Strings.
  376. let components = input.components(separatedBy: " ").filter { !$0.isEmpty }
  377. // Expect three components: the `-`, the pod name, and the version in parens. This will filter out
  378. // dependencies that have version requirements like `(~> 3.2.1)` in it.
  379. guard components.count == 3 else { return nil }
  380. // The first component is simple, just the `-`.
  381. guard components.first == "-" else { return nil }
  382. // The second component is a pod/framework name, which we want to return eventually. Remove any
  383. // extraneous quotes.
  384. let framework = components[1].trimmingCharacters(in: CharacterSet(charactersIn: "\""))
  385. // The third component is the version in parentheses, potentially with a `:` at the end. Let's
  386. // just strip the unused characters (including quotes) and return the version. We don't
  387. // necesarily have to match against semver since it's a non trivial regex and we don't actually
  388. // care, `Podfile.lock` has a standard format that we know will be valid. Also strip out any
  389. // extra quotes.
  390. let version = components[2].trimmingCharacters(in: CharacterSet(charactersIn: "():\""))
  391. return (framework, version)
  392. }
  393. /// Create the contents of a Podfile for an array of subspecs. This assumes the array of subspecs
  394. /// is not empty.
  395. private static func generatePodfile(for pods: [VersionedPod],
  396. customSpecsRepos: [URL]?,
  397. platform: Platform,
  398. localPodspecPath: URL?,
  399. linkage: LinkageType) -> String {
  400. // Start assembling the Podfile.
  401. var podfile: String = ""
  402. // If custom Specs repos were passed in, prefix the Podfile with the custom repos followed by
  403. // the CocoaPods master Specs repo.
  404. if let customSpecsRepos = customSpecsRepos {
  405. let reposText = customSpecsRepos.map { "source '\($0)'" }
  406. podfile += """
  407. \(reposText.joined(separator: "\n"))
  408. source 'https://cdn.cocoapods.org/'
  409. """ // Explicit newline above to ensure it's included in the String.
  410. }
  411. switch linkage {
  412. case .forcedStatic:
  413. podfile += " use_modular_headers!\n"
  414. case .dynamic:
  415. podfile += " use_frameworks!\n"
  416. case .standardStatic:
  417. podfile += " use_frameworks! :linkage => :static\n"
  418. }
  419. // Include the platform and its minimum version.
  420. podfile += """
  421. platform :\(platform.name), '\(platform.minimumVersion)'
  422. target 'FrameworkMaker' do\n
  423. """
  424. var versionsSpecified = false
  425. // Loop through the subspecs passed in and use the actual Pod name.
  426. for pod in pods {
  427. let podspec = String(pod.name.split(separator: "/")[0] + ".podspec")
  428. // Check if we want to use a local version of the podspec.
  429. if let localURL = localPodspecPath,
  430. FileManager.default.fileExists(atPath: localURL.appendingPathComponent(podspec).path) {
  431. podfile += " pod '\(pod.name)', :path => '\(localURL.path)'"
  432. } else if let podVersion = pod.version {
  433. podfile += " pod '\(pod.name)', '\(podVersion)'"
  434. } else if pod.name.starts(with: "Firebase"),
  435. let localURL = localPodspecPath,
  436. FileManager.default
  437. .fileExists(atPath: localURL.appendingPathComponent("Firebase.podspec").path) {
  438. // Let Firebase.podspec force the right version for unspecified closed Firebase pods.
  439. let podString = pod.name.replacingOccurrences(of: "Firebase", with: "")
  440. podfile += " pod 'Firebase/\(podString)', :path => '\(localURL.path)'"
  441. } else {
  442. podfile += " pod '\(pod.name)'"
  443. }
  444. if pod.version != nil {
  445. // Don't add Google pods if versions were specified or we're doing a secondary install
  446. // to create module maps.
  447. versionsSpecified = true
  448. }
  449. podfile += "\n"
  450. }
  451. // If we're using local pods, explicitly add FirebaseInstanceID, FirebaseInstallations,
  452. // and any Google* podspecs if they exist and there are no explicit versions in the Podfile.
  453. // Note there are versions for local podspecs if we're doing the secondary install for module
  454. // map building.
  455. if !versionsSpecified, let localURL = localPodspecPath {
  456. let podspecs = try! FileManager.default.contentsOfDirectory(atPath: localURL.path)
  457. for podspec in podspecs {
  458. if podspec == "FirebaseInstanceID.podspec" ||
  459. podspec == "FirebaseInstallations.podspec" ||
  460. podspec == "FirebaseCoreDiagnostics.podspec" ||
  461. podspec == "FirebaseCore.podspec" ||
  462. podspec == "FirebaseRemoteConfig.podspec" ||
  463. podspec == "FirebaseABTesting.podspec" {
  464. let podName = podspec.replacingOccurrences(of: ".podspec", with: "")
  465. podfile += " pod '\(podName)', :path => '\(localURL.path)/\(podspec)'\n"
  466. }
  467. }
  468. }
  469. podfile += "end"
  470. return podfile
  471. }
  472. /// Write a podfile that contains all the pods passed in to the directory passed in with a name
  473. /// "Podfile".
  474. private static func writePodfile(for pods: [VersionedPod],
  475. toDirectory directory: URL,
  476. customSpecRepos: [URL]?,
  477. platform: Platform,
  478. localPodspecPath: URL?,
  479. linkage: LinkageType) throws {
  480. guard FileManager.default.directoryExists(at: directory) else {
  481. // Throw an error so the caller can provide a better error message.
  482. throw FileManager.FileError.directoryNotFound(path: directory.path)
  483. }
  484. // Generate the full path of the Podfile and attempt to write it to disk.
  485. let path = directory.appendingPathComponent("Podfile")
  486. let podfile = generatePodfile(for: pods,
  487. customSpecsRepos: customSpecRepos,
  488. platform: platform,
  489. localPodspecPath: localPodspecPath,
  490. linkage: linkage)
  491. do {
  492. try podfile.write(toFile: path.path, atomically: true, encoding: .utf8)
  493. } catch {
  494. throw FileManager.FileError.writeToFileFailed(file: path.path, error: error)
  495. }
  496. }
  497. private static var checkedCocoaPodsVersion = false
  498. /// At least 1.9.0 is required for `use_frameworks! :linkage => :static`
  499. /// - Parameters:
  500. /// - directory: Destination directory for the pods.
  501. private static func checkCocoaPodsVersion(directory: URL) {
  502. if checkedCocoaPodsVersion {
  503. return
  504. }
  505. checkedCocoaPodsVersion = true
  506. let podVersion = Shell.executeCommandFromScript("pod --version", workingDir: directory)
  507. switch podVersion {
  508. case let .error(code, output):
  509. fatalError("""
  510. `pod --version` failed with exit code \(code)
  511. Output from `pod --version`:
  512. \(output)
  513. """)
  514. case let .success(output):
  515. let version = output.components(separatedBy: ".")
  516. guard version.count >= 2 else {
  517. fatalError("Failed to parse CocoaPods version: \(version)")
  518. }
  519. let major = Int(version[0])
  520. guard let minor = Int(version[1]) else {
  521. fatalError("Failed to parse minor version from \(version)")
  522. }
  523. if major == 1, minor < 9 {
  524. fatalError("CocoaPods version must be at least 1.9.0. Using \(output)")
  525. }
  526. }
  527. }
  528. }