CocoaPodUtils.swift 25 KB

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