CocoaPodUtils.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  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 FirebaseManifest
  17. import Foundation
  18. import Utils
  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", "watchos"]) {
  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", "watchos"]
  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. // The `dep` may be a subspec, so get root spec name to lookup it's
  334. // dependencies in the `installedPods` dictionary.
  335. let rootDep = dep.components(separatedBy: "/")[0]
  336. let childDeps = installedPods[rootDep]?.dependencies ?? []
  337. foundDeps.formUnion(Set(childDeps))
  338. }
  339. newDeps = foundDeps.subtracting(returnDeps)
  340. returnDeps.formUnion(newDeps)
  341. } while newDeps.count > 0
  342. return Array(returnDeps)
  343. }
  344. /// Get all transitive pod dependencies for a pod with subspecs merged.
  345. /// - Returns: An array of Strings of pod names.
  346. static func transitiveMasterPodDependencies(for podName: String,
  347. in installedPods: [String: PodInfo]) -> [String] {
  348. return Array(Set(transitivePodDependencies(for: podName, in: installedPods).map {
  349. $0.components(separatedBy: "/")[0]
  350. }))
  351. }
  352. /// Get all transitive pod dependencies for a pod.
  353. /// - Returns: An array of dependencies with versions for a given pod.
  354. static func transitiveVersionedPodDependencies(for podName: String,
  355. in installedPods: [String: PodInfo])
  356. -> [VersionedPod] {
  357. return transitivePodDependencies(for: podName, in: installedPods).map {
  358. var podVersion: String?
  359. if let version = installedPods[$0]?.version {
  360. podVersion = version
  361. } else {
  362. // See if there's a version on the base pod.
  363. let basePod = String($0.split(separator: "/")[0])
  364. podVersion = installedPods[basePod]?.version
  365. }
  366. return CocoaPodUtils.VersionedPod(name: $0, version: podVersion)
  367. }
  368. }
  369. // MARK: - Private Helpers
  370. // Tests the input to see if it matches a CocoaPod framework and its version.
  371. // Returns the framework and version or nil if match failed.
  372. // Used to process entries from Podfile.lock
  373. /// Tests the input and sees if it matches a CocoaPod framework and its version. This is used to
  374. /// process entries from Podfile.lock.
  375. ///
  376. /// - Parameters:
  377. /// - input: A line entry from Podfile.lock.
  378. /// - Returns: A tuple of the framework and version, if it can be parsed.
  379. private static func detectVersion(fromLine input: String)
  380. -> (framework: String, version: String)? {
  381. // Get the components of the line to parse them individually. Ignore any whitespace only
  382. // Strings.
  383. let components = input.components(separatedBy: " ").filter { !$0.isEmpty }
  384. // Expect three components: the `-`, the pod name, and the version in parens. This will filter
  385. // out
  386. // dependencies that have version requirements like `(~> 3.2.1)` in it.
  387. guard components.count == 3 else { return nil }
  388. // The first component is simple, just the `-`.
  389. guard components.first == "-" else { return nil }
  390. // The second component is a pod/framework name, which we want to return eventually. Remove any
  391. // extraneous quotes.
  392. let framework = components[1].trimmingCharacters(in: CharacterSet(charactersIn: "\""))
  393. // The third component is the version in parentheses, potentially with a `:` at the end. Let's
  394. // just strip the unused characters (including quotes) and return the version. We don't
  395. // necessarily have to match against semver since it's a non trivial regex and we don't actually
  396. // care, `Podfile.lock` has a standard format that we know will be valid. Also strip out any
  397. // extra quotes.
  398. let version = components[2].trimmingCharacters(in: CharacterSet(charactersIn: "():\""))
  399. return (framework, version)
  400. }
  401. /// Create the contents of a Podfile for an array of subspecs. This assumes the array of subspecs
  402. /// is not empty.
  403. private static func generatePodfile(for pods: [VersionedPod],
  404. customSpecsRepos: [URL]?,
  405. platform: Platform,
  406. localPodspecPath: URL?,
  407. linkage: LinkageType) -> String {
  408. // Start assembling the Podfile.
  409. var podfile = ""
  410. // If custom Specs repos were passed in, prefix the Podfile with the custom repos followed by
  411. // the CocoaPods master Specs repo.
  412. if let customSpecsRepos = customSpecsRepos {
  413. let reposText = customSpecsRepos.map { "source '\($0)'" }
  414. podfile += """
  415. \(reposText.joined(separator: "\n"))
  416. source 'https://cdn.cocoapods.org/'
  417. """ // Explicit newline above to ensure it's included in the String.
  418. }
  419. switch linkage {
  420. case .forcedStatic:
  421. podfile += " use_modular_headers!\n"
  422. case .dynamic:
  423. podfile += " use_frameworks!\n"
  424. case .standardStatic:
  425. podfile += " use_frameworks! :linkage => :static\n"
  426. }
  427. // Include the platform and its minimum version.
  428. podfile += """
  429. platform :\(platform.name), '\(platform.minimumVersion)'
  430. target 'FrameworkMaker' do\n
  431. """
  432. var versionsSpecified = false
  433. let firebaseVersion = FirebaseManifest.shared.version
  434. let versionChunks = firebaseVersion.split(separator: ".")
  435. let minorVersion = "\(versionChunks[0]).\(versionChunks[1]).0"
  436. // Loop through the subspecs passed in and use the actual Pod name.
  437. for pod in pods {
  438. let rootPod = String(pod.name.split(separator: "/").first!)
  439. // Check if we want to use a local version of the podspec.
  440. if let localURL = localPodspecPath,
  441. let pathURL = localPodspecPath?.appendingPathComponent("\(rootPod).podspec").path,
  442. FileManager.default.fileExists(atPath: pathURL),
  443. isSourcePodspec(pathURL) {
  444. podfile += " pod '\(pod.name)', :path => '\(localURL.path)'"
  445. } else if let podVersion = pod.version {
  446. // To support Firebase patch versions in the Firebase zip distribution, allow patch updates
  447. // for all pods except Firebase and FirebaseCore.
  448. var podfileVersion = podVersion
  449. if pod.name.starts(with: "Firebase"),
  450. !pod.name.hasSuffix("Swift"),
  451. pod.name != "Firebase",
  452. pod.name != "FirebaseCore" {
  453. podfileVersion = podfileVersion.replacingOccurrences(
  454. of: firebaseVersion,
  455. with: minorVersion
  456. )
  457. podfileVersion = "~> \(podfileVersion)"
  458. }
  459. podfile += " pod '\(pod.name)', '\(podfileVersion)'"
  460. } else if pod.name.starts(with: "Firebase"),
  461. let localURL = localPodspecPath,
  462. FileManager.default
  463. .fileExists(atPath: localURL.appendingPathComponent("Firebase.podspec").path) {
  464. // Let Firebase.podspec force the right version for unspecified closed Firebase pods.
  465. let podString = pod.name.replacingOccurrences(of: "Firebase", with: "")
  466. podfile += " pod 'Firebase/\(podString)', :path => '\(localURL.path)'"
  467. } else {
  468. podfile += " pod '\(pod.name)'"
  469. }
  470. if pod.version != nil {
  471. // Don't add Google pods if versions were specified or we're doing a secondary install
  472. // to create module maps.
  473. versionsSpecified = true
  474. }
  475. podfile += "\n"
  476. }
  477. // If we're using local pods, explicitly add FirebaseInstallations,
  478. // and any Google* podspecs if they exist and there are no explicit versions in the Podfile.
  479. // Note there are versions for local podspecs if we're doing the secondary install for module
  480. // map building.
  481. if !versionsSpecified, let localURL = localPodspecPath {
  482. let podspecs = try! FileManager.default.contentsOfDirectory(atPath: localURL.path)
  483. for podspec in podspecs {
  484. if podspec == "FirebaseInstallations.podspec" ||
  485. podspec == "FirebaseCore.podspec" ||
  486. podspec == "FirebaseCoreExtension.podspec" ||
  487. podspec == "FirebaseCoreInternal.podspec" ||
  488. podspec == "FirebaseAppCheck.podspec" ||
  489. podspec == "FirebaseAuth.podspec" ||
  490. podspec == "FirebaseMessaging.podspec" ||
  491. podspec == "FirebaseRemoteConfig.podspec" ||
  492. podspec == "FirebaseABTesting.podspec" {
  493. let podName = podspec.replacingOccurrences(of: ".podspec", with: "")
  494. podfile += " pod '\(podName)', :path => '\(localURL.path)/\(podspec)'\n"
  495. }
  496. }
  497. }
  498. podfile += "end"
  499. return podfile
  500. }
  501. private static func isSourcePodspec(_ podspecPath: String) -> Bool {
  502. do {
  503. let contents = try String(contentsOfFile: podspecPath, encoding: .utf8)
  504. // The presence of ".vendored_frameworks" in a podspec indicates a binary pod.
  505. return contents.range(of: ".vendored_frameworks") == nil
  506. } catch {
  507. fatalError("Could not read \(podspecPath): \(error)")
  508. }
  509. }
  510. /// Write a podfile that contains all the pods passed in to the directory passed in with a name
  511. /// "Podfile".
  512. private static func writePodfile(for pods: [VersionedPod],
  513. toDirectory directory: URL,
  514. customSpecRepos: [URL]?,
  515. platform: Platform,
  516. localPodspecPath: URL?,
  517. linkage: LinkageType) throws {
  518. guard FileManager.default.directoryExists(at: directory) else {
  519. // Throw an error so the caller can provide a better error message.
  520. throw FileManager.FileError.directoryNotFound(path: directory.path)
  521. }
  522. // Generate the full path of the Podfile and attempt to write it to disk.
  523. let path = directory.appendingPathComponent("Podfile")
  524. let podfile = generatePodfile(for: pods,
  525. customSpecsRepos: customSpecRepos,
  526. platform: platform,
  527. localPodspecPath: localPodspecPath,
  528. linkage: linkage)
  529. do {
  530. try podfile.write(toFile: path.path, atomically: true, encoding: .utf8)
  531. } catch {
  532. throw FileManager.FileError.writeToFileFailed(file: path.path, error: error)
  533. }
  534. }
  535. private static var checkedCocoaPodsVersion = false
  536. /// At least 1.9.0 is required for `use_frameworks! :linkage => :static`
  537. /// - Parameters:
  538. /// - directory: Destination directory for the pods.
  539. private static func checkCocoaPodsVersion(directory: URL) {
  540. if checkedCocoaPodsVersion {
  541. return
  542. }
  543. checkedCocoaPodsVersion = true
  544. let podVersion = Shell.executeCommandFromScript("pod --version", workingDir: directory)
  545. switch podVersion {
  546. case let .error(code, output):
  547. fatalError("""
  548. `pod --version` failed with exit code \(code)
  549. Output from `pod --version`:
  550. \(output)
  551. """)
  552. case let .success(output):
  553. let version = output.components(separatedBy: ".")
  554. guard version.count >= 2 else {
  555. fatalError("Failed to parse CocoaPods version: \(version)")
  556. }
  557. let major = Int(version[0])
  558. guard let minor = Int(version[1]) else {
  559. fatalError("Failed to parse minor version from \(version)")
  560. }
  561. if major == 1, minor < 9 {
  562. fatalError("CocoaPods version must be at least 1.9.0. Using \(output)")
  563. }
  564. }
  565. }
  566. }