ModuleMapBuilder.swift 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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. /// A struct to build a .modulemap in a given framework directory.
  18. ///
  19. struct ModuleMapBuilder {
  20. /// Information associated with a framework
  21. private class FrameworkInfo {
  22. let isSourcePod: Bool
  23. let versionedPod: CocoaPodUtils.VersionedPod
  24. let subspecs: Set<String>
  25. var transitiveFrameworks: Set<String>?
  26. var transitiveLibraries: Set<String>?
  27. init(isSourcePod: Bool, versionedPod: CocoaPodUtils.VersionedPod, subspecs: Set<String>) {
  28. self.isSourcePod = isSourcePod
  29. self.versionedPod = versionedPod
  30. self.subspecs = subspecs
  31. }
  32. }
  33. struct ModuleMapContents {
  34. /// Placeholder to fill in umbrella header.
  35. static let umbrellaPlaceholder = "UMBRELLA_PLACEHOLDER"
  36. let contents: String
  37. init(module: String, frameworks: Set<String>, libraries: Set<String>) {
  38. var content = """
  39. framework module \(module) {
  40. umbrella header "\(ModuleMapBuilder.ModuleMapContents.umbrellaPlaceholder)"
  41. export *
  42. module * { export * }
  43. """
  44. for framework in frameworks.sorted() {
  45. content += " link framework " + framework + "\n"
  46. }
  47. for library in libraries.sorted() {
  48. content += " link " + library + "\n"
  49. }
  50. // The empty line at the end is intentional, do not remove it.
  51. content += "}\n"
  52. contents = content
  53. }
  54. func get(umbrellaHeader: String) -> String {
  55. return contents.replacingOccurrences(of:
  56. ModuleMapBuilder.ModuleMapContents.umbrellaPlaceholder, with: umbrellaHeader)
  57. }
  58. }
  59. /// The directory containing the Xcode project and Pods folder.
  60. private let projectDir: URL
  61. /// Custom CocoaPods spec repos to be used. If not provided, the tool will only use the CocoaPods
  62. /// master repo.
  63. private let customSpecRepos: [URL]?
  64. /// Dictionary of all installed pods. Update moduleMapContents here.
  65. private var allPods: [String: CocoaPodUtils.PodInfo]
  66. /// Dictionary of installed pods required for this module.
  67. private var installedPods: [String: FrameworkInfo]
  68. /// The platform for this build.
  69. private let platform: Platform
  70. /// The path containing local podspec URLs, if specified.
  71. private let localPodspecPath: URL?
  72. /// Default initializer.
  73. init(customSpecRepos: [URL]?,
  74. selectedPods: [String: CocoaPodUtils.PodInfo],
  75. platform: Platform,
  76. paths: ZipBuilder.FilesystemPaths) {
  77. projectDir = FileManager.default.temporaryDirectory(withName: "module")
  78. CocoaPodUtils.podInstallPrepare(inProjectDir: projectDir, templateDir: paths.templateDir)
  79. self.customSpecRepos = customSpecRepos
  80. allPods = selectedPods
  81. var installedPods: [String: FrameworkInfo] = [:]
  82. for pod in selectedPods {
  83. let versionedPod = CocoaPodUtils.VersionedPod(name: pod.key, version: pod.value.version)
  84. installedPods[pod.key] = FrameworkInfo(isSourcePod: pod.value.isSourcePod,
  85. versionedPod: versionedPod,
  86. subspecs: pod.value.subspecs)
  87. }
  88. self.installedPods = installedPods
  89. self.platform = platform
  90. localPodspecPath = paths.localPodspecPath
  91. }
  92. // MARK: - Public Functions
  93. /// Build the module map files for the source frameworks.
  94. ///
  95. func build() {
  96. for framework in installedPods.values
  97. where framework.isSourcePod &&
  98. framework.versionedPod.name != "Firebase" &&
  99. framework.transitiveFrameworks == nil {
  100. generate(framework: framework)
  101. }
  102. }
  103. // MARK: - Internal Functions
  104. /// Build a module map for a single framework. A CocoaPod install is run to extract the required
  105. /// frameworks
  106. /// and libraries from the generated xcconfig. All previously installed dependent pods are put
  107. /// into the Podfile
  108. /// to make sure we install the right version and from the right location.
  109. private func generate(framework: FrameworkInfo) {
  110. let podName = framework.versionedPod.name
  111. let deps = CocoaPodUtils.transitiveVersionedPodDependencies(for: podName, in: allPods).filter {
  112. // Don't include Interop pods in the module map calculation since they shouldn't add anything
  113. // and it uses the platform-independent version of the dependency list, which causes a crash
  114. // for the iOS-only RecaptchaInterop pod when the subsequent code tries to `pod install` it
  115. // for macOS. All this module code should go away when we switch to building dynamic
  116. // frameworks.
  117. !$0.name.hasSuffix("Interop")
  118. }
  119. CocoaPodUtils.installPods(allSubspecList(framework: framework) + deps,
  120. inDir: projectDir,
  121. platform: platform,
  122. customSpecRepos: customSpecRepos,
  123. localPodspecPath: localPodspecPath,
  124. linkage: .forcedStatic)
  125. let xcconfigFile = projectDir.appendingPathComponents(["Pods", "Target Support Files",
  126. "Pods-FrameworkMaker",
  127. "Pods-FrameworkMaker.release.xcconfig"])
  128. allPods[podName]?
  129. .moduleMapContents = makeModuleMap(forFramework: framework, withXcconfigFile: xcconfigFile)
  130. }
  131. /// Convert a list of versioned pods to a list of versioned pods specified with all needed
  132. /// subspecs.
  133. private func allSubspecList(framework: FrameworkInfo) -> [CocoaPodUtils.VersionedPod] {
  134. let name = framework.versionedPod.name
  135. let version = framework.versionedPod.version
  136. guard framework.subspecs.count > 0 else {
  137. return [CocoaPodUtils.VersionedPod(name: name, version: version)]
  138. }
  139. return framework.subspecs.map { subspec in
  140. CocoaPodUtils.VersionedPod(name: "\(name)/\(subspec)", version: version)
  141. }
  142. }
  143. // Extract the framework and library dependencies for a framework from
  144. // the xcconfig file from an app generated from a Podfile only with that
  145. // CocoaPod and removing the frameworks and libraries from its transitive dependencies.
  146. private func getModuleDependencies(withXcconfigFile xcconfigFile: URL) ->
  147. (frameworks: [String], libraries: [String]) {
  148. do {
  149. let text = try String(contentsOf: xcconfigFile)
  150. let lines = text.components(separatedBy: .newlines)
  151. for line in lines {
  152. if line.hasPrefix("OTHER_LDFLAGS =") {
  153. var dependencyFrameworks: [String] = []
  154. var dependencyLibraries: [String] = []
  155. let tokens = line.components(separatedBy: " ")
  156. var addNext = false
  157. for token in tokens {
  158. if addNext {
  159. dependencyFrameworks.append(token)
  160. addNext = false
  161. } else if token == "-framework" {
  162. addNext = true
  163. } else if token.hasPrefix("-l") {
  164. let index = token.index(token.startIndex, offsetBy: 2)
  165. dependencyLibraries.append(String(token[index...]))
  166. }
  167. }
  168. return (dependencyFrameworks, dependencyLibraries)
  169. }
  170. }
  171. } catch {
  172. fatalError("Failed to open \(xcconfigFile): \(error)")
  173. }
  174. return ([], [])
  175. }
  176. private func makeModuleMap(forFramework framework: FrameworkInfo,
  177. withXcconfigFile xcconfigFile: URL) -> ModuleMapContents {
  178. let name = framework.versionedPod.name
  179. let dependencies = getModuleDependencies(withXcconfigFile: xcconfigFile)
  180. let frameworkDeps = Set(dependencies.frameworks)
  181. let libraryDeps = Set(dependencies.libraries)
  182. var transitiveFrameworkDeps = frameworkDeps
  183. var transitiveLibraryDeps = libraryDeps
  184. var myFrameworkDeps = frameworkDeps
  185. var myLibraryDeps = libraryDeps
  186. // Iterate through the deps looking for pods. At time of authoring, TensorFlowLiteObjC is the
  187. // only pod in frameworkDeps instead of libraryDeps.
  188. for library in libraryDeps.union(frameworkDeps) {
  189. let lib = library.replacingOccurrences(of: "\"", with: "")
  190. if let dependencyPod = installedPods[lib] {
  191. myLibraryDeps.remove(library)
  192. myFrameworkDeps.remove(library)
  193. if lib == name {
  194. continue
  195. }
  196. var podFrameworkDeps = dependencyPod.transitiveFrameworks
  197. var podLibraryDeps = dependencyPod.transitiveLibraries
  198. if podFrameworkDeps == nil {
  199. generate(framework: dependencyPod)
  200. podFrameworkDeps = dependencyPod.transitiveFrameworks
  201. podLibraryDeps = dependencyPod.transitiveLibraries
  202. }
  203. if let fdeps = podFrameworkDeps {
  204. transitiveFrameworkDeps.formUnion(fdeps)
  205. }
  206. if let ldeps = podLibraryDeps {
  207. transitiveLibraryDeps.formUnion(ldeps)
  208. }
  209. }
  210. }
  211. installedPods[name]?.transitiveFrameworks = transitiveFrameworkDeps
  212. installedPods[name]?.transitiveLibraries = transitiveLibraryDeps
  213. return ModuleMapContents(module: name, frameworks: myFrameworkDeps, libraries: myLibraryDeps)
  214. }
  215. }