| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- /*
- * Copyright 2019 Google
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- import Foundation
- import Utils
- import FirebaseManifest
- /// CocoaPod related utility functions. The enum type is used as a namespace here instead of having
- /// root functions, and no cases should be added to it.
- enum CocoaPodUtils {
- /// The linkage type to specify for CocoaPods installation.
- enum LinkageType {
- /// Forced static libraries. Uses `use_modular_headers!` in the Podfile. Required for module map
- /// generation
- case forcedStatic
- /// Dynamic frameworks. Uses `use_frameworks!` in the Podfile.
- case dynamic
- /// Static frameworks. Uses `use_frameworks! :linkage => :static` in the Podfile. Enum case is
- /// prefixed with `standard` to avoid the `static` keyword.
- case standardStatic
- }
- // MARK: - Public API
- // Codable is required because Decodable does not make CodingKeys available.
- struct VersionedPod: Codable, CustomDebugStringConvertible {
- /// Public name of the pod.
- let name: String
- /// The version of the requested pod.
- let version: String?
- /// Platforms supported
- let platforms: Set<String>
- init(name: String,
- version: String?,
- platforms: Set<String> = ["ios", "macos", "tvos"]) {
- self.name = name
- self.version = version
- self.platforms = platforms
- }
- init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: CodingKeys.self)
- name = try container.decode(String.self, forKey: .name)
- if let platforms = try container.decodeIfPresent(Set<String>.self, forKey: .platforms) {
- self.platforms = platforms
- } else {
- platforms = ["ios", "macos", "tvos"]
- }
- if let version = try container.decodeIfPresent(String.self, forKey: .version) {
- self.version = version
- } else {
- version = nil
- }
- }
- /// The debug description as required by `CustomDebugStringConvertible`.
- var debugDescription: String {
- var desc = name
- if let version = version {
- desc.append(" v\(version)")
- }
- return desc
- }
- }
- /// Information associated with an installed pod.
- /// This is a class so that moduleMapContents can be updated via reference.
- class PodInfo {
- /// The version of the generated pod.
- let version: String
- /// The pod dependencies.
- let dependencies: [String]
- /// The location of the pod on disk.
- let installedLocation: URL
- /// Source pod flag.
- let isSourcePod: Bool
- /// Binary frameworks in this pod.
- let binaryFrameworks: [URL]
- /// Subspecs installed for this pod.
- let subspecs: Set<String>
- /// The contents of the module map for all frameworks associated with the pod.
- var moduleMapContents: ModuleMapBuilder.ModuleMapContents?
- init(version: String,
- dependencies: [String],
- installedLocation: URL,
- subspecs: Set<String>,
- localPodspecPath: URL?) {
- self.version = version
- self.dependencies = dependencies
- self.installedLocation = installedLocation
- self.subspecs = subspecs
- // Get all the frameworks contained in this directory.
- var binaryFrameworks: [URL] = []
- if installedLocation != localPodspecPath {
- do {
- binaryFrameworks = try FileManager.default.recursivelySearch(for: .frameworks,
- in: installedLocation)
- } catch {
- fatalError("Cannot search for .framework files in Pods directory " +
- "\(installedLocation): \(error)")
- }
- }
- self.binaryFrameworks = binaryFrameworks
- isSourcePod = binaryFrameworks == []
- }
- }
- /// Executes the `pod cache clean --all` command to remove any cached CocoaPods.
- static func cleanPodCache() {
- let result = Shell.executeCommandFromScript("pod cache clean --all", outputToConsole: false)
- switch result {
- case let .error(code, _):
- fatalError("Could not clean the pod cache, the command exited with \(code). Try running the" +
- "command in Terminal to see what's wrong.")
- case .success:
- // No need to do anything else, continue on.
- print("Successfully cleaned pod cache.")
- return
- }
- }
- /// Gets metadata from installed Pods. Reads the `Podfile.lock` file and parses it.
- static func installedPodsInfo(inProjectDir projectDir: URL,
- localPodspecPath: URL?) -> [String: PodInfo] {
- // Read from the Podfile.lock to get the installed versions and names.
- let podfileLock: String
- do {
- podfileLock = try String(contentsOf: projectDir.appendingPathComponent("Podfile.lock"))
- } catch {
- fatalError("Could not read contents of `Podfile.lock` to get installed Pod info in " +
- "\(projectDir): \(error)")
- }
- // Get the pods in the format of [PodInfo].
- return loadPodInfoFromPodfileLock(contents: podfileLock,
- inProjectDir: projectDir,
- localPodspecPath: localPodspecPath)
- }
- /// Install an array of pods in a specific directory, returning a dictionary of PodInfo for each pod
- /// that was installed.
- /// - Parameters:
- /// - pods: List of VersionedPods to install
- /// - directory: Destination directory for the pods.
- /// - platform: Install for one platform at a time.
- /// - customSpecRepos: Additional spec repos to check for installation.
- /// - linkage: Specifies the linkage type. When `forcedStatic` is used, for the module map
- /// construction, we want pod names not module names in the generated OTHER_LD_FLAGS
- /// options.
- /// - Returns: A dictionary of PodInfo's keyed by the pod name.
- @discardableResult
- static func installPods(_ pods: [VersionedPod],
- inDir directory: URL,
- platform: Platform,
- customSpecRepos: [URL]?,
- localPodspecPath: URL?,
- linkage: LinkageType) -> [String: PodInfo] {
- let fileManager = FileManager.default
- // Ensure the directory exists, otherwise we can't install all subspecs.
- guard fileManager.directoryExists(at: directory) else {
- fatalError("Attempted to install subpecs (\(pods)) in a directory that doesn't exist: " +
- "\(directory)")
- }
- // Ensure there are actual podspecs to install.
- guard !pods.isEmpty else {
- fatalError("Attempted to install an empty array of subspecs")
- }
- // Attempt to write the Podfile to disk.
- do {
- try writePodfile(for: pods,
- toDirectory: directory,
- customSpecRepos: customSpecRepos,
- platform: platform,
- localPodspecPath: localPodspecPath,
- linkage: linkage)
- } catch let FileManager.FileError.directoryNotFound(path) {
- fatalError("Failed to write Podfile with pods \(pods) at path \(path)")
- } catch let FileManager.FileError.writeToFileFailed(path, error) {
- fatalError("Failed to write Podfile for all pods at path: \(path), error: \(error)")
- } catch {
- fatalError("Unspecified error writing Podfile for all pods to disk: \(error)")
- }
- // Run pod install on the directory that contains the Podfile and blank Xcode project.
- checkCocoaPodsVersion(directory: directory)
- let result = Shell.executeCommandFromScript("pod install", workingDir: directory)
- switch result {
- case let .error(code, output):
- fatalError("""
- `pod install` failed with exit code \(code) while trying to install pods:
- \(pods)
- Output from `pod install`:
- \(output)
- """)
- case let .success(output):
- // Print the output to the console and return the information for all installed pods.
- print(output)
- return installedPodsInfo(inProjectDir: directory, localPodspecPath: localPodspecPath)
- }
- }
- /// Load installed Pods from the contents of a `Podfile.lock` file.
- ///
- /// - Parameter contents: The contents of a `Podfile.lock` file.
- /// - Returns: A dictionary of PodInfo structs keyed by the pod name.
- static func loadPodInfoFromPodfileLock(contents: String,
- inProjectDir projectDir: URL,
- localPodspecPath: URL?) -> [String: PodInfo] {
- // This pattern matches a pod name with its version (two to three components)
- // Examples:
- // - FirebaseUI/Google (4.1.1):
- // - GoogleSignIn (4.0.2):
- // Force unwrap the regular expression since we know it will work, it's a constant being passed
- // in. If any changes are made, be sure to run this script to ensure it works.
- let depRegex: NSRegularExpression = try! NSRegularExpression(pattern: " - (.+).*",
- options: [])
- let quotes = CharacterSet(charactersIn: "\"")
- var pods: [String: String] = [:]
- var deps: [String: Set<String>] = [:]
- var currentPod: String?
- for line in contents.components(separatedBy: .newlines) {
- if line.starts(with: "DEPENDENCIES:") {
- break
- }
- if let (pod, version) = detectVersion(fromLine: line) {
- currentPod = pod.trimmingCharacters(in: quotes)
- pods[currentPod!] = version
- } else if let currentPod = currentPod {
- let matches = depRegex
- .matches(in: line, range: NSRange(location: 0, length: line.utf8.count))
- // Match something like - GTMSessionFetcher/Full (= 1.3.0)
- if let match = matches.first {
- let depLine = (line as NSString).substring(with: match.range(at: 0)) as String
- // Split spaces and subspecs.
- let dep = depLine.components(separatedBy: [" "])[2].trimmingCharacters(in: quotes)
- if dep != currentPod {
- deps[currentPod, default: Set()].insert(dep)
- }
- }
- }
- }
- // Organize the subspecs
- var versions: [String: String] = [:]
- var subspecs: [String: Set<String>] = [:]
- for (podName, version) in pods {
- let subspecArray = podName.components(separatedBy: "/")
- if subspecArray.count == 1 || subspecArray[0] == "abseil" {
- // Special case for abseil since it has two layers and no external deps.
- versions[subspecArray[0]] = version
- } else if subspecArray.count > 2 {
- fatalError("Multi-layered subspecs are not supported - \(podName)")
- } else {
- if let previousVersion = versions[podName], version != previousVersion {
- fatalError("Different installed versions for \(podName)." +
- "\(version) versus \(previousVersion)")
- } else {
- let basePodName = subspecArray[0]
- versions[basePodName] = version
- subspecs[basePodName, default: Set()].insert(subspecArray[1])
- deps[basePodName] = deps[basePodName, default: Set()].union(deps[podName] ?? Set())
- }
- }
- }
- // Generate an InstalledPod for each Pod found.
- let podsDir = projectDir.appendingPathComponent("Pods")
- var installedPods: [String: PodInfo] = [:]
- for (podName, version) in versions {
- var podDir = podsDir.appendingPathComponent(podName)
- // Make sure that pod got installed if it's not coming from a local podspec.
- if !FileManager.default.directoryExists(at: podDir) {
- guard let repoDir = localPodspecPath else {
- fatalError("Directory for \(podName) doesn't exist at \(podDir) - failed while getting " +
- "information for installed Pods.")
- }
- podDir = repoDir
- }
- let dependencies = [String](deps[podName] ?? [])
- let podInfo = PodInfo(version: version,
- dependencies: dependencies,
- installedLocation: podDir,
- subspecs: subspecs[podName] ?? Set(),
- localPodspecPath: localPodspecPath)
- installedPods[podName] = podInfo
- }
- return installedPods
- }
- static func updateRepos() {
- let result = Shell.executeCommandFromScript("pod repo update")
- switch result {
- case let .error(_, output):
- fatalError("Command `pod repo update` failed: \(output)")
- case .success:
- return
- }
- }
- static func podInstallPrepare(inProjectDir projectDir: URL, templateDir: URL) {
- do {
- // Create the directory and all intermediate directories.
- try FileManager.default.createDirectory(at: projectDir, withIntermediateDirectories: true)
- } catch {
- // Use `do/catch` instead of `guard let tempDir = try?` so we can print the error thrown.
- fatalError("Cannot create temporary directory at beginning of script: \(error)")
- }
- // Copy the Xcode project needed in order to be able to install Pods there.
- let templateFiles = Constants.ProjectPath.requiredFilesForBuilding.map {
- templateDir.appendingPathComponent($0)
- }
- for file in templateFiles {
- // Each file should be copied to the temporary project directory with the same name.
- let destination = projectDir.appendingPathComponent(file.lastPathComponent)
- do {
- if !FileManager.default.fileExists(atPath: destination.path) {
- print("Copying template file \(file) to \(destination)...")
- try FileManager.default.copyItem(at: file, to: destination)
- }
- } catch {
- fatalError("Could not copy template project to temporary directory in order to install " +
- "pods. Failed while attempting to copy \(file) to \(destination). \(error)")
- }
- }
- }
- /// Get all transitive pod dependencies for a pod.
- /// - Returns: An array of Strings of pod names.
- static func transitivePodDependencies(for podName: String,
- in installedPods: [String: PodInfo]) -> [String] {
- var newDeps = Set([podName])
- var returnDeps = Set<String>()
- repeat {
- var foundDeps = Set<String>()
- for dep in newDeps {
- let childDeps = installedPods[dep]?.dependencies ?? []
- foundDeps.formUnion(Set(childDeps))
- }
- newDeps = foundDeps.subtracting(returnDeps)
- returnDeps.formUnion(newDeps)
- } while newDeps.count > 0
- return Array(returnDeps)
- }
- /// Get all transitive pod dependencies for a pod with subspecs merged.
- /// - Returns: An array of Strings of pod names.
- static func transitiveMasterPodDependencies(for podName: String,
- in installedPods: [String: PodInfo]) -> [String] {
- return Array(Set(transitivePodDependencies(for: podName, in: installedPods).map {
- $0.components(separatedBy: "/")[0]
- }))
- }
- /// Get all transitive pod dependencies for a pod.
- /// - Returns: An array of dependencies with versions for a given pod.
- static func transitiveVersionedPodDependencies(for podName: String,
- in installedPods: [String: PodInfo])
- -> [VersionedPod] {
- return transitivePodDependencies(for: podName, in: installedPods).map {
- var podVersion: String?
- if let version = installedPods[$0]?.version {
- podVersion = version
- } else {
- // See if there's a version on the base pod.
- let basePod = String($0.split(separator: "/")[0])
- podVersion = installedPods[basePod]?.version
- }
- return CocoaPodUtils.VersionedPod(name: $0, version: podVersion)
- }
- }
- // MARK: - Private Helpers
- // Tests the input to see if it matches a CocoaPod framework and its version.
- // Returns the framework and version or nil if match failed.
- // Used to process entries from Podfile.lock
- /// Tests the input and sees if it matches a CocoaPod framework and its version. This is used to
- /// process entries from Podfile.lock.
- ///
- /// - Parameters:
- /// - input: A line entry from Podfile.lock.
- /// - Returns: A tuple of the framework and version, if it can be parsed.
- private static func detectVersion(fromLine input: String)
- -> (framework: String, version: String)? {
- // Get the components of the line to parse them individually. Ignore any whitespace only Strings.
- let components = input.components(separatedBy: " ").filter { !$0.isEmpty }
- // Expect three components: the `-`, the pod name, and the version in parens. This will filter out
- // dependencies that have version requirements like `(~> 3.2.1)` in it.
- guard components.count == 3 else { return nil }
- // The first component is simple, just the `-`.
- guard components.first == "-" else { return nil }
- // The second component is a pod/framework name, which we want to return eventually. Remove any
- // extraneous quotes.
- let framework = components[1].trimmingCharacters(in: CharacterSet(charactersIn: "\""))
- // The third component is the version in parentheses, potentially with a `:` at the end. Let's
- // just strip the unused characters (including quotes) and return the version. We don't
- // necesarily have to match against semver since it's a non trivial regex and we don't actually
- // care, `Podfile.lock` has a standard format that we know will be valid. Also strip out any
- // extra quotes.
- let version = components[2].trimmingCharacters(in: CharacterSet(charactersIn: "():\""))
- return (framework, version)
- }
- /// Create the contents of a Podfile for an array of subspecs. This assumes the array of subspecs
- /// is not empty.
- private static func generatePodfile(for pods: [VersionedPod],
- customSpecsRepos: [URL]?,
- platform: Platform,
- localPodspecPath: URL?,
- linkage: LinkageType) -> String {
- // Start assembling the Podfile.
- var podfile = ""
- // If custom Specs repos were passed in, prefix the Podfile with the custom repos followed by
- // the CocoaPods master Specs repo.
- if let customSpecsRepos = customSpecsRepos {
- let reposText = customSpecsRepos.map { "source '\($0)'" }
- podfile += """
- \(reposText.joined(separator: "\n"))
- source 'https://cdn.cocoapods.org/'
- """ // Explicit newline above to ensure it's included in the String.
- }
- switch linkage {
- case .forcedStatic:
- podfile += " use_modular_headers!\n"
- case .dynamic:
- podfile += " use_frameworks!\n"
- case .standardStatic:
- podfile += " use_frameworks! :linkage => :static\n"
- }
- // Include the platform and its minimum version.
- podfile += """
- platform :\(platform.name), '\(platform.minimumVersion)'
- target 'FrameworkMaker' do\n
- """
- var versionsSpecified = false
- let firebaseVersion = FirebaseManifest.shared.version
- let versionChunks = firebaseVersion.split(separator: ".")
- let minorVersion = "\(versionChunks[0]).\(versionChunks[1]).0"
- // Loop through the subspecs passed in and use the actual Pod name.
- for pod in pods {
- let podspec = String(pod.name.split(separator: "/")[0] + ".podspec")
- // Check if we want to use a local version of the podspec.
- if let localURL = localPodspecPath,
- FileManager.default.fileExists(atPath: localURL.appendingPathComponent(podspec).path) {
- podfile += " pod '\(pod.name)', :path => '\(localURL.path)'"
- } else if let podVersion = pod.version {
- // To support Firebase patch versions in the Firebase zip distribution, allow patch updates
- // for all pods except Firebase and FirebaseCore. The Firebase Swift pods are not yet in the
- // zip distribution.
- var podfileVersion = podVersion
- if pod.name.starts(with: "Firebase"),
- !pod.name.hasSuffix("Swift"),
- pod.name != "Firebase",
- pod.name != "FirebaseCore" {
- podfileVersion = podfileVersion.replacingOccurrences(
- of: firebaseVersion,
- with: minorVersion
- )
- podfileVersion = "~> \(podfileVersion)"
- }
- podfile += " pod '\(pod.name)', '\(podfileVersion)'"
- } else if pod.name.starts(with: "Firebase"),
- let localURL = localPodspecPath,
- FileManager.default
- .fileExists(atPath: localURL.appendingPathComponent("Firebase.podspec").path) {
- // Let Firebase.podspec force the right version for unspecified closed Firebase pods.
- let podString = pod.name.replacingOccurrences(of: "Firebase", with: "")
- podfile += " pod 'Firebase/\(podString)', :path => '\(localURL.path)'"
- } else {
- podfile += " pod '\(pod.name)'"
- }
- if pod.version != nil {
- // Don't add Google pods if versions were specified or we're doing a secondary install
- // to create module maps.
- versionsSpecified = true
- }
- podfile += "\n"
- }
- // If we're using local pods, explicitly add FirebaseInstallations,
- // and any Google* podspecs if they exist and there are no explicit versions in the Podfile.
- // Note there are versions for local podspecs if we're doing the secondary install for module
- // map building.
- if !versionsSpecified, let localURL = localPodspecPath {
- let podspecs = try! FileManager.default.contentsOfDirectory(atPath: localURL.path)
- for podspec in podspecs {
- if podspec == "FirebaseInstallations.podspec" ||
- podspec == "FirebaseCoreDiagnostics.podspec" ||
- podspec == "FirebaseCore.podspec" ||
- podspec == "FirebaseCoreExtension.podspec" ||
- podspec == "FirebaseCoreInternal.podspec" ||
- podspec == "FirebaseAppCheck.podspec" ||
- podspec == "FirebaseAuth.podspec" ||
- podspec == "FirebaseMessaging.podspec" ||
- podspec == "FirebaseRemoteConfig.podspec" ||
- podspec == "FirebaseABTesting.podspec" {
- let podName = podspec.replacingOccurrences(of: ".podspec", with: "")
- podfile += " pod '\(podName)', :path => '\(localURL.path)/\(podspec)'\n"
- }
- }
- }
- podfile += "end"
- return podfile
- }
- /// Write a podfile that contains all the pods passed in to the directory passed in with a name
- /// "Podfile".
- private static func writePodfile(for pods: [VersionedPod],
- toDirectory directory: URL,
- customSpecRepos: [URL]?,
- platform: Platform,
- localPodspecPath: URL?,
- linkage: LinkageType) throws {
- guard FileManager.default.directoryExists(at: directory) else {
- // Throw an error so the caller can provide a better error message.
- throw FileManager.FileError.directoryNotFound(path: directory.path)
- }
- // Generate the full path of the Podfile and attempt to write it to disk.
- let path = directory.appendingPathComponent("Podfile")
- let podfile = generatePodfile(for: pods,
- customSpecsRepos: customSpecRepos,
- platform: platform,
- localPodspecPath: localPodspecPath,
- linkage: linkage)
- do {
- try podfile.write(toFile: path.path, atomically: true, encoding: .utf8)
- } catch {
- throw FileManager.FileError.writeToFileFailed(file: path.path, error: error)
- }
- }
- private static var checkedCocoaPodsVersion = false
- /// At least 1.9.0 is required for `use_frameworks! :linkage => :static`
- /// - Parameters:
- /// - directory: Destination directory for the pods.
- private static func checkCocoaPodsVersion(directory: URL) {
- if checkedCocoaPodsVersion {
- return
- }
- checkedCocoaPodsVersion = true
- let podVersion = Shell.executeCommandFromScript("pod --version", workingDir: directory)
- switch podVersion {
- case let .error(code, output):
- fatalError("""
- `pod --version` failed with exit code \(code)
- Output from `pod --version`:
- \(output)
- """)
- case let .success(output):
- let version = output.components(separatedBy: ".")
- guard version.count >= 2 else {
- fatalError("Failed to parse CocoaPods version: \(version)")
- }
- let major = Int(version[0])
- guard let minor = Int(version[1]) else {
- fatalError("Failed to parse minor version from \(version)")
- }
- if major == 1, minor < 9 {
- fatalError("CocoaPods version must be at least 1.9.0. Using \(output)")
- }
- }
- }
- }
|