CocoaPodUtils.swift 25 KB

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