/* * 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 /// A struct to build a .modulemap in a given framework directory. /// struct ModuleMapBuilder { /// Information associated with a framework private class FrameworkInfo { let isSourcePod: Bool let versionedPod: CocoaPodUtils.VersionedPod let subspecs: Set var transitiveFrameworks: Set? var transitiveLibraries: Set? init(isSourcePod: Bool, versionedPod: CocoaPodUtils.VersionedPod, subspecs: Set) { self.isSourcePod = isSourcePod self.versionedPod = versionedPod self.subspecs = subspecs } } struct ModuleMapContents { /// Placeholder to fill in umbrella header. static let umbrellaPlaceholder = "UMBRELLA_PLACEHOLDER" let contents: String init(module: String, frameworks: Set, libraries: Set) { var content = """ framework module \(module) { umbrella header "\(ModuleMapBuilder.ModuleMapContents.umbrellaPlaceholder)" export * module * { export * } """ for framework in frameworks.sorted() { content += " link framework " + framework + "\n" } for library in libraries.sorted() { content += " link " + library + "\n" } // The empty line at the end is intentional, do not remove it. content += "}\n" contents = content } func get(umbrellaHeader: String) -> String { return contents.replacingOccurrences(of: ModuleMapBuilder.ModuleMapContents.umbrellaPlaceholder, with: umbrellaHeader) } } /// The directory containing the Xcode project and Pods folder. private let projectDir: URL /// Custom CocoaPods spec repos to be used. If not provided, the tool will only use the CocoaPods /// master repo. private let customSpecRepos: [URL]? /// Dictionary of all installed pods. Update moduleMapContents here. private var allPods: [String: CocoaPodUtils.PodInfo] /// Dictionary of installed pods required for this module. private var installedPods: [String: FrameworkInfo] /// The platform for this build. private let platform: Platform /// The path containing local podspec URLs, if specified. private let localPodspecPath: URL? /// Default initializer. init(customSpecRepos: [URL]?, selectedPods: [String: CocoaPodUtils.PodInfo], platform: Platform, paths: ZipBuilder.FilesystemPaths) { projectDir = FileManager.default.temporaryDirectory(withName: "module") CocoaPodUtils.podInstallPrepare(inProjectDir: projectDir, templateDir: paths.templateDir) self.customSpecRepos = customSpecRepos allPods = selectedPods var installedPods: [String: FrameworkInfo] = [:] for pod in selectedPods { let versionedPod = CocoaPodUtils.VersionedPod(name: pod.key, version: pod.value.version) installedPods[pod.key] = FrameworkInfo(isSourcePod: pod.value.isSourcePod, versionedPod: versionedPod, subspecs: pod.value.subspecs) } self.installedPods = installedPods self.platform = platform localPodspecPath = paths.localPodspecPath } // MARK: - Public Functions /// Build the module map files for the source frameworks. /// func build() { for framework in installedPods.values where framework.isSourcePod && framework.versionedPod.name != "Firebase" && framework.transitiveFrameworks == nil { generate(framework: framework) } } // MARK: - Internal Functions /// Build a module map for a single framework. A CocoaPod install is run to extract the required /// frameworks /// and libraries from the generated xcconfig. All previously installed dependent pods are put /// into the Podfile /// to make sure we install the right version and from the right location. private func generate(framework: FrameworkInfo) { let podName = framework.versionedPod.name let deps = CocoaPodUtils.transitiveVersionedPodDependencies(for: podName, in: allPods).filter { // Don't include Interop pods in the module map calculation since they shouldn't add anything // and it uses the platform-independent version of the dependency list, which causes a crash // for the iOS-only RecaptchaInterop pod when the subsequent code tries to `pod install` it // for macOS. All this module code should go away when we switch to building dynamic // frameworks. !$0.name.hasSuffix("Interop") } CocoaPodUtils.installPods(allSubspecList(framework: framework) + deps, inDir: projectDir, platform: platform, customSpecRepos: customSpecRepos, localPodspecPath: localPodspecPath, linkage: .forcedStatic) let xcconfigFile = projectDir.appendingPathComponents(["Pods", "Target Support Files", "Pods-FrameworkMaker", "Pods-FrameworkMaker.release.xcconfig"]) allPods[podName]? .moduleMapContents = makeModuleMap(forFramework: framework, withXcconfigFile: xcconfigFile) } /// Convert a list of versioned pods to a list of versioned pods specified with all needed /// subspecs. private func allSubspecList(framework: FrameworkInfo) -> [CocoaPodUtils.VersionedPod] { let name = framework.versionedPod.name let version = framework.versionedPod.version guard framework.subspecs.count > 0 else { return [CocoaPodUtils.VersionedPod(name: name, version: version)] } return framework.subspecs.map { subspec in CocoaPodUtils.VersionedPod(name: "\(name)/\(subspec)", version: version) } } // Extract the framework and library dependencies for a framework from // the xcconfig file from an app generated from a Podfile only with that // CocoaPod and removing the frameworks and libraries from its transitive dependencies. private func getModuleDependencies(withXcconfigFile xcconfigFile: URL) -> (frameworks: [String], libraries: [String]) { do { let text = try String(contentsOf: xcconfigFile) let lines = text.components(separatedBy: .newlines) for line in lines { if line.hasPrefix("OTHER_LDFLAGS =") { var dependencyFrameworks: [String] = [] var dependencyLibraries: [String] = [] let tokens = line.components(separatedBy: " ") var addNext = false for token in tokens { if addNext { dependencyFrameworks.append(token) addNext = false } else if token == "-framework" { addNext = true } else if token.hasPrefix("-l") { let index = token.index(token.startIndex, offsetBy: 2) dependencyLibraries.append(String(token[index...])) } } return (dependencyFrameworks, dependencyLibraries) } } } catch { fatalError("Failed to open \(xcconfigFile): \(error)") } return ([], []) } private func makeModuleMap(forFramework framework: FrameworkInfo, withXcconfigFile xcconfigFile: URL) -> ModuleMapContents { let name = framework.versionedPod.name let dependencies = getModuleDependencies(withXcconfigFile: xcconfigFile) let frameworkDeps = Set(dependencies.frameworks) let libraryDeps = Set(dependencies.libraries) var transitiveFrameworkDeps = frameworkDeps var transitiveLibraryDeps = libraryDeps var myFrameworkDeps = frameworkDeps var myLibraryDeps = libraryDeps // Iterate through the deps looking for pods. At time of authoring, TensorFlowLiteObjC is the // only pod in frameworkDeps instead of libraryDeps. for library in libraryDeps.union(frameworkDeps) { let lib = library.replacingOccurrences(of: "\"", with: "") if let dependencyPod = installedPods[lib] { myLibraryDeps.remove(library) myFrameworkDeps.remove(library) if lib == name { continue } var podFrameworkDeps = dependencyPod.transitiveFrameworks var podLibraryDeps = dependencyPod.transitiveLibraries if podFrameworkDeps == nil { generate(framework: dependencyPod) podFrameworkDeps = dependencyPod.transitiveFrameworks podLibraryDeps = dependencyPod.transitiveLibraries } if let fdeps = podFrameworkDeps { transitiveFrameworkDeps.formUnion(fdeps) } if let ldeps = podLibraryDeps { transitiveLibraryDeps.formUnion(ldeps) } } } installedPods[name]?.transitiveFrameworks = transitiveFrameworkDeps installedPods[name]?.transitiveLibraries = transitiveLibraryDeps return ModuleMapContents(module: name, frameworks: myFrameworkDeps, libraries: myLibraryDeps) } }