| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681 |
- /*
- * 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
- /// A structure to build a .framework in a given project directory.
- struct FrameworkBuilder {
- /// Platforms to be included in the built frameworks.
- private let targetPlatforms: [TargetPlatform]
- /// The directory containing the Xcode project and Pods folder.
- private let projectDir: URL
- /// Flag for building dynamic frameworks instead of static frameworks.
- private let dynamicFrameworks: Bool
- /// The Pods directory for building the framework.
- private var podsDir: URL {
- return projectDir.appendingPathComponent("Pods", isDirectory: true)
- }
- /// Default initializer.
- init(projectDir: URL, targetPlatforms: [TargetPlatform], dynamicFrameworks: Bool) {
- self.projectDir = projectDir
- self.targetPlatforms = targetPlatforms
- self.dynamicFrameworks = dynamicFrameworks
- }
- // MARK: - Public Functions
- /// Compiles the specified framework in a temporary directory and writes the build logs to file.
- /// This will compile all architectures for a single platform at a time.
- ///
- /// - Parameter framework: The name of the framework to be built.
- /// - Parameter logsOutputDir: The path to the directory to place build logs.
- /// - Parameter setCarthage: Set Carthage diagnostics flag in build.
- /// - Parameter moduleMapContents: Module map contents for all frameworks in this pod.
- /// - Returns: A path to the newly compiled frameworks, and Resources.
- func compileFrameworkAndResources(withName framework: String,
- logsOutputDir: URL? = nil,
- setCarthage: Bool,
- podInfo: CocoaPodUtils.PodInfo) -> ([URL], URL?) {
- let fileManager = FileManager.default
- let logsDir = logsOutputDir ?? fileManager.temporaryDirectory(withName: "build_logs")
- do {
- // Create our logs directory if it doesn't exist.
- if !fileManager.directoryExists(at: logsDir) {
- try fileManager.createDirectory(at: logsDir, withIntermediateDirectories: true)
- }
- } catch {
- fatalError("Failure creating temporary directory while building \(framework): \(error)")
- }
- if dynamicFrameworks {
- return (buildDynamicFrameworks(withName: framework, logsDir: logsDir),
- nil)
- } else {
- return buildStaticFrameworks(
- withName: framework,
- logsDir: logsDir,
- setCarthage: setCarthage,
- podInfo: podInfo
- )
- }
- }
- // MARK: - Private Helpers
- /// This runs a command and immediately returns a Shell result.
- /// NOTE: This exists in conjunction with the `Shell.execute...` due to issues with different
- /// `.bash_profile` environment variables. This should be consolidated in the future.
- private static func syncExec(command: String,
- args: [String] = [],
- captureOutput: Bool = false) -> Shell
- .Result {
- let task = Process()
- task.launchPath = command
- task.arguments = args
- // If we want to output to the console, create a readabilityHandler and save each line along the
- // way. Otherwise, we can just read the pipe at the end. By disabling outputToConsole, some
- // commands (such as any xcodebuild) can run much, much faster.
- var output = ""
- if captureOutput {
- let pipe = Pipe()
- task.standardOutput = pipe
- let outHandle = pipe.fileHandleForReading
- outHandle.readabilityHandler = { pipe in
- // This will be run any time data is sent to the pipe. We want to print it and store it for
- // later. Ignore any non-valid Strings.
- guard let line = String(data: pipe.availableData, encoding: .utf8) else {
- print("Could not get data from pipe for command \(command): \(pipe.availableData)")
- return
- }
- if !line.isEmpty {
- output += line
- }
- }
- // Also set the termination handler on the task in order to stop the readabilityHandler from
- // parsing any more data from the task.
- task.terminationHandler = { t in
- guard let stdOut = t.standardOutput as? Pipe else { return }
- stdOut.fileHandleForReading.readabilityHandler = nil
- }
- } else {
- // No capturing output, just mark it as complete.
- output = "The task completed"
- }
- task.launch()
- task.waitUntilExit()
- // Normally we'd use a pipe to retrieve the output, but for whatever reason it slows things down
- // tremendously for xcodebuild.
- guard task.terminationStatus == 0 else {
- return .error(code: task.terminationStatus, output: output)
- }
- return .success(output: output)
- }
- /// Build all thin slices for an open source pod.
- /// - Parameter framework: The name of the framework to be built.
- /// - Parameter logsDir: The path to the directory to place build logs.
- /// - Parameter setCarthage: Set Carthage flag in GoogleUtilities for metrics.
- /// - Returns: A dictionary of URLs to the built thin libraries keyed by platform.
- private func buildFrameworksForAllPlatforms(withName framework: String,
- logsDir: URL,
- setCarthage: Bool) -> [TargetPlatform: URL] {
- // Build every architecture and save the locations in an array to be assembled.
- var slicedFrameworks = [TargetPlatform: URL]()
- for targetPlatform in targetPlatforms {
- let buildDir = projectDir.appendingPathComponent(targetPlatform.buildName)
- let sliced = buildSlicedFramework(withName: framework,
- targetPlatform: targetPlatform,
- buildDir: buildDir,
- logRoot: logsDir,
- setCarthage: setCarthage)
- slicedFrameworks[targetPlatform] = sliced
- }
- return slicedFrameworks
- }
- /// Uses `xcodebuild` to build a framework for a specific target platform.
- ///
- /// - Parameters:
- /// - framework: Name of the framework being built.
- /// - targetPlatform: The target platform to target for the build.
- /// - buildDir: Location where the project should be built.
- /// - logRoot: Root directory where all logs should be written.
- /// - setCarthage: Set Carthage flag in GoogleUtilities for metrics.
- /// - Returns: A URL to the framework that was built.
- private func buildSlicedFramework(withName framework: String,
- targetPlatform: TargetPlatform,
- buildDir: URL,
- logRoot: URL,
- setCarthage: Bool = false) -> URL {
- let isMacCatalyst = targetPlatform == .catalyst
- let isMacCatalystString = isMacCatalyst ? "YES" : "NO"
- let workspacePath = projectDir.appendingPathComponent("FrameworkMaker.xcworkspace").path
- let distributionFlag = setCarthage ? "-DFIREBASE_BUILD_CARTHAGE" :
- "-DFIREBASE_BUILD_ZIP_FILE"
- let cFlags = "OTHER_CFLAGS=$(value) \(distributionFlag)"
- var archs = targetPlatform.archs.map { $0.rawValue }.joined(separator: " ")
- // The 32 bit archs do not build for iOS 11.
- if framework == "FirebaseAppCheck" || framework.hasSuffix("Swift") {
- if targetPlatform == .iOSDevice {
- archs = "arm64"
- } else if targetPlatform == .iOSSimulator {
- archs = "x86_64 arm64"
- }
- }
- var args = ["build",
- "-configuration", "release",
- "-workspace", workspacePath,
- "-scheme", framework,
- "GCC_GENERATE_DEBUGGING_SYMBOLS=NO",
- "ARCHS=\(archs)",
- "VALID_ARCHS=\(archs)",
- "ONLY_ACTIVE_ARCH=NO",
- // BUILD_LIBRARY_FOR_DISTRIBUTION=YES is necessary for Swift libraries.
- // See https://forums.developer.apple.com/thread/125646.
- // Unlike the comment there, the option here is sufficient to cause .swiftinterface
- // files to be generated in the .swiftmodule directory. The .swiftinterface files
- // are required for xcodebuild to successfully generate an xcframework.
- "BUILD_LIBRARY_FOR_DISTRIBUTION=YES",
- // Remove the -fembed-bitcode-marker compiling flag.
- "ENABLE_BITCODE=NO",
- "SUPPORTS_MACCATALYST=\(isMacCatalystString)",
- "BUILD_DIR=\(buildDir.path)",
- "-sdk", targetPlatform.sdkName,
- cFlags]
- // Code signing isn't needed for libraries. Disabling signing is required for
- // Catalyst libs with resources. See
- // https://github.com/CocoaPods/CocoaPods/issues/8891#issuecomment-573301570
- if isMacCatalyst {
- args.append("CODE_SIGN_IDENTITY=-")
- }
- print("""
- Compiling \(framework) for \(targetPlatform.buildName) (\(archs)) with command:
- /usr/bin/xcodebuild \(args.joined(separator: " "))
- """)
- // Regardless if it succeeds or not, we want to write the log to file in case we need to inspect
- // things further.
- let logFileName = "\(framework)-\(targetPlatform.buildName).txt"
- let logFile = logRoot.appendingPathComponent(logFileName)
- let result = FrameworkBuilder.syncExec(command: "/usr/bin/xcodebuild",
- args: args,
- captureOutput: true)
- switch result {
- case let .error(code, output):
- // Write output to disk and print the location of it. Force unwrapping here since it's going
- // to crash anyways, and at this point the root log directory exists, we know it's UTF8, so it
- // should pass every time. Revisit if that's not the case.
- try! output.write(to: logFile, atomically: true, encoding: .utf8)
- fatalError("Error building \(framework) for \(targetPlatform.buildName). Code: \(code). See " +
- "the build log at \(logFile)")
- case let .success(output):
- // Try to write the output to the log file but if it fails it's not a huge deal since it was
- // a successful build.
- try? output.write(to: logFile, atomically: true, encoding: .utf8)
- print("""
- Successfully built \(framework) for \(targetPlatform.buildName). Build log is at \(logFile).
- """)
- // Use the Xcode-generated path to return the path to the compiled library.
- // The framework name may be different from the pod name if the module is reset in the
- // podspec - like Release-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework.
- print("buildDir: \(buildDir)")
- let frameworkPath = buildDir.appendingPathComponents([targetPlatform.buildDirName, framework])
- var actualFramework: String
- do {
- let files = try FileManager.default.contentsOfDirectory(at: frameworkPath,
- includingPropertiesForKeys: nil)
- .compactMap { $0.path }
- let frameworkDir = files.filter { $0.contains(".framework") }
- actualFramework = URL(fileURLWithPath: frameworkDir[0]).lastPathComponent
- } catch {
- fatalError("Error while enumerating files \(frameworkPath): \(error.localizedDescription)")
- }
- let libPath = frameworkPath.appendingPathComponent(actualFramework)
- print("buildSliced returns \(libPath)")
- return libPath
- }
- }
- // TODO: Automatically get the right name.
- /// The module name is different from the pod name when the module_name
- /// specifier is used in the podspec.
- ///
- /// - Parameter framework: The name of the pod to be built.
- /// - Returns: The corresponding framework/module name.
- private static func frameworkBuildName(_ framework: String) -> String {
- switch framework {
- case "abseil":
- return "absl"
- case "BoringSSL-GRPC":
- return "openssl_grpc"
- case "gRPC-Core":
- return "grpc"
- case "gRPC-C++":
- return "grpcpp"
- case "leveldb-library":
- return "leveldb"
- case "PromisesObjC":
- return "FBLPromises"
- case "PromisesSwift":
- return "Promises"
- case "Protobuf":
- return "protobuf"
- default:
- return framework
- }
- }
- /// Compiles the specified framework in a temporary directory and writes the build logs to file.
- /// This will compile all architectures and use the -create-xcframework command to create a modern
- /// "fat" framework.
- ///
- /// - Parameter framework: The name of the framework to be built.
- /// - Parameter logsDir: The path to the directory to place build logs.
- /// - Returns: A path to the newly compiled frameworks (with any included Resources embedded).
- private func buildDynamicFrameworks(withName framework: String,
- logsDir: URL) -> [URL] {
- // xcframework doesn't lipo things together but accepts fat frameworks for one target.
- // We group architectures here to deal with this fact.
- return targetPlatforms.map { targetPlatform in
- buildSlicedFramework(
- withName: framework,
- targetPlatform: targetPlatform,
- buildDir: projectDir.appendingPathComponent(targetPlatform.buildName),
- logRoot: logsDir
- )
- }
- }
- /// Compiles the specified framework in a temporary directory and writes the build logs to file.
- /// This will compile all architectures and use the -create-xcframework command to create a modern
- /// "fat" framework.
- ///
- /// - Parameter framework: The name of the framework to be built.
- /// - Parameter logsDir: The path to the directory to place build logs.
- /// - Parameter moduleMapContents: Module map contents for all frameworks in this pod.
- /// - Returns: A path to the newly compiled framework, and the Resource URL.
- private func buildStaticFrameworks(withName framework: String,
- logsDir: URL,
- setCarthage: Bool,
- podInfo: CocoaPodUtils.PodInfo) -> ([URL], URL) {
- // Build every architecture and save the locations in an array to be assembled.
- let slicedFrameworks = buildFrameworksForAllPlatforms(withName: framework, logsDir: logsDir,
- setCarthage: setCarthage)
- // Create the framework directory in the filesystem for the thin archives to go.
- let fileManager = FileManager.default
- let frameworkName = FrameworkBuilder.frameworkBuildName(framework)
- guard let anyPlatform = targetPlatforms.first,
- let archivePath = slicedFrameworks[anyPlatform] else {
- fatalError("Could not get a path to an archive to fetch headers in \(frameworkName).")
- }
- // Find CocoaPods generated umbrella header.
- var umbrellaHeader = ""
- // TODO(ncooke3): Evaluate if `TensorFlowLiteObjC` is needed?
- if framework == "gRPC-Core" || framework == "TensorFlowLiteObjC" {
- // TODO: Proper handling of podspec-specified module.modulemap files with customized umbrella
- // headers. This is good enough for Firebase since it doesn't need these modules.
- // TODO(ncooke3): Is this needed for gRPC-Core?
- umbrellaHeader = "\(framework)-umbrella.h"
- } else {
- var umbrellaHeaderURL: URL
- // Get the framework Headers directory. On macOS, it's a symbolic link.
- let headersDir = archivePath.appendingPathComponent("Headers").resolvingSymlinksInPath()
- do {
- let files = try fileManager.contentsOfDirectory(at: headersDir,
- includingPropertiesForKeys: nil)
- .compactMap { $0.path }
- let umbrellas = files.filter { $0.hasSuffix("umbrella.h") }
- if umbrellas.count != 1 {
- fatalError("Did not find exactly one umbrella header in \(headersDir).")
- }
- guard let firstUmbrella = umbrellas.first else {
- fatalError("Failed to get umbrella header in \(headersDir).")
- }
- umbrellaHeaderURL = URL(fileURLWithPath: firstUmbrella)
- } catch {
- fatalError("Error while enumerating files \(headersDir): \(error.localizedDescription)")
- }
- umbrellaHeader = umbrellaHeaderURL.lastPathComponent
- }
- // TODO: copy PrivateHeaders directory as well if it exists. SDWebImage is an example pod.
- // Move all the Resources into .bundle directories in the destination Resources dir. The
- // Resources live are contained within the folder structure:
- // `projectDir/arch/Release-platform/FrameworkName`.
- // The Resources are stored at the top-level of the .framework or .xcframework directory.
- // For Firebase distributions, they are propagated one level higher in the final distribution.
- let resourceContents = projectDir.appendingPathComponents([anyPlatform.buildName,
- anyPlatform.buildDirName,
- framework])
- guard let moduleMapContentsTemplate = podInfo.moduleMapContents else {
- fatalError("Module map contents missing for framework \(frameworkName)")
- }
- let moduleMapContents = moduleMapContentsTemplate.get(umbrellaHeader: umbrellaHeader)
- let frameworks = groupFrameworks(withName: frameworkName,
- isCarthage: setCarthage,
- slicedFrameworks: slicedFrameworks,
- moduleMapContents: moduleMapContents)
- // Remove the temporary thin archives.
- for slicedFramework in slicedFrameworks.values {
- do {
- try fileManager.removeItem(at: slicedFramework)
- } catch {
- // Just log a warning instead of failing, since this doesn't actually affect the build
- // itself. This should only be shown to help users clean up their disk afterwards.
- print("""
- WARNING: Failed to remove temporary sliced framework at \(slicedFramework.path). This should
- be removed from your system to save disk space. \(error). You should be able to remove the
- archive from Terminal with:
- rm \(slicedFramework.path)
- """)
- }
- }
- return (frameworks, resourceContents)
- }
- /// Parses CocoaPods config files or uses the passed in `moduleMapContents` to write the
- /// appropriate `moduleMap` to the `destination`.
- /// Returns true to fail if building for Carthage and there are Swift modules.
- @discardableResult
- private func packageModuleMaps(inFrameworks frameworks: [URL],
- frameworkName: String,
- moduleMapContents: String,
- destination: URL,
- buildingCarthage: Bool = false) -> Bool {
- // CocoaPods does not put dependent frameworks and libraries into the module maps it generates.
- // Instead it use build options to specify them. For the zip build, we need the module maps to
- // include the dependent frameworks and libraries. Therefore we reconstruct them by parsing
- // the CocoaPods config files and add them here.
- // In the case of a mixed language framework, not only are the Swift module
- // files copied, but a `module.modulemap` is created by combining the given
- // module map contents and a synthesized submodule that modularizes the
- // generated Swift header.
- if makeSwiftModuleMap(thinFrameworks: frameworks,
- frameworkName: frameworkName,
- destination: destination,
- moduleMapContents: moduleMapContents,
- buildingCarthage: buildingCarthage) {
- return buildingCarthage
- }
- let modulemapURL = destination
- .appendingPathComponent("Modules")
- .appendingPathComponent("module.modulemap")
- .resolvingSymlinksInPath()
- do {
- try moduleMapContents.write(to: modulemapURL, atomically: true, encoding: .utf8)
- } catch {
- let frameworkName: String = frameworks.first?.lastPathComponent ?? "<UNKNOWN"
- fatalError("Could not write modulemap to disk for \(frameworkName): \(error)")
- }
- return false
- }
- /// URLs pointing to the frameworks containing architecture specific code.
- /// Returns true if there are Swift modules.
- private func makeSwiftModuleMap(thinFrameworks: [URL],
- frameworkName: String,
- destination: URL,
- moduleMapContents: String,
- buildingCarthage: Bool = false) -> Bool {
- let fileManager = FileManager.default
- for thinFramework in thinFrameworks {
- // Get the Modules directory. The Catalyst one is a symbolic link.
- let moduleDir = thinFramework.appendingPathComponent("Modules").resolvingSymlinksInPath()
- do {
- let files = try fileManager.contentsOfDirectory(at: moduleDir,
- includingPropertiesForKeys: nil)
- .compactMap { $0.path }
- let swiftModules = files.filter { $0.hasSuffix(".swiftmodule") }
- if swiftModules.isEmpty {
- return false
- } else if buildingCarthage {
- return true
- }
- do {
- // If this point is reached, the framework contains a Swift module,
- // so it's built from either Swift sources or Swift & C Family
- // Language sources. Frameworks built from only Swift sources will
- // contain only two headers: the CocoaPods-generated umbrella header
- // and the Swift-generated Swift header. If the framework's `Headers`
- // directory contains more than two resources, then it is assumed
- // that the framework was built from mixed language sources because
- // those additional headers are public headers for the C Family
- // Language sources.
- let headersDir = destination.appendingPathComponent("Headers").resolvingSymlinksInPath()
- let headers = try fileManager.contentsOfDirectory(
- at: headersDir,
- includingPropertiesForKeys: nil
- )
- if headers.count > 2 {
- // It is assumed that the framework will always contain a
- // `module.modulemap` (either CocoaPods generates it or a custom
- // one was set in the podspec corresponding to the framework being
- // processed) within the framework's `Modules` directory. The main
- // module declaration within this `module.modulemap` should be
- // replaced with the given module map contents that was computed to
- // include frameworks and libraries that the framework slice
- // depends on.
- let newModuleMapContents = moduleMapContents + """
- module \(frameworkName).Swift {
- header "\(frameworkName)-Swift.h"
- requires objc
- }
- """
- let modulemapURL = destination.appendingPathComponents(["Modules", "module.modulemap"])
- .resolvingSymlinksInPath()
- try newModuleMapContents.write(to: modulemapURL, atomically: true, encoding: .utf8)
- }
- } catch {
- fatalError(
- "Error while synthesizing a mixed language framework's module map: \(error.localizedDescription)"
- )
- }
- } catch {
- fatalError("Error while enumerating files \(moduleDir): \(error.localizedDescription)")
- }
- }
- return true
- }
- /// Groups slices for each platform into a minimal set of frameworks.
- /// - Parameter withName: The framework name.
- /// - Parameter isCarthage: Name the temp directory differently for Carthage.
- /// - Parameter slicedFrameworks: All the frameworks sliced by platform.
- /// - Parameter moduleMapContents: Module map contents for all frameworks in this pod.
- private func groupFrameworks(withName framework: String,
- isCarthage: Bool,
- slicedFrameworks: [TargetPlatform: URL],
- moduleMapContents: String) -> ([URL]) {
- let fileManager = FileManager.default
- let platformFrameworksDir = fileManager.temporaryDirectory(
- withName: isCarthage ? "carthage_frameworks" : "platform_frameworks"
- )
- if !fileManager.directoryExists(at: platformFrameworksDir) {
- do {
- try fileManager.createDirectory(at: platformFrameworksDir,
- withIntermediateDirectories: true)
- } catch {
- fatalError("Could not create a temp directory to store all thin frameworks: \(error)")
- }
- }
- return slicedFrameworks.map { platform, frameworkPath in
- // Create the following structure in the platform frameworks directory:
- // - platform_frameworks
- // └── $(PLATFORM)
- // └── $(FRAMEWORK).framework
- let platformFrameworkDir = platformFrameworksDir
- .appendingPathComponent(platform.buildName)
- .appendingPathComponent(framework + ".framework")
- do {
- // Create `platform_frameworks/$(PLATFORM)` subdirectory.
- try fileManager.createDirectory(
- at: platformFrameworkDir.deletingLastPathComponent(),
- withIntermediateDirectories: true
- )
- // Copy the built framework to the `platform_frameworks/$(PLATFORM)/$(FRAMEWORK).framework`.
- try fileManager.copyItem(at: frameworkPath, to: platformFrameworkDir)
- } catch {
- fatalError("Could not copy directory for architecture slices on \(platform) for " +
- "\(framework): \(error)")
- }
- // The minimum OS version is set to 100.0 to work around b/327020913.
- // TODO(ncooke3): Revert this logic once b/327020913 is fixed.
- // TODO(ncooke3): Does this need to happen on macOS?
- do {
- let frameworkInfoPlistURL = platformFrameworkDir
- .appendingPathComponent(
- platform == .catalyst || platform == .macOS ? "Resources" : ""
- )
- .resolvingSymlinksInPath()
- .appendingPathComponent("Info.plist")
- var plistDictionary = try PropertyListSerialization.propertyList(
- from: Data(contentsOf: frameworkInfoPlistURL), format: nil
- ) as! [AnyHashable: Any]
- plistDictionary["MinimumOSVersion"] = "100.0"
- let updatedPlistData = try PropertyListSerialization.data(
- fromPropertyList: plistDictionary,
- format: .xml,
- options: 0
- )
- try updatedPlistData.write(to: frameworkInfoPlistURL)
- } catch {
- fatalError(
- "Could not modify framework-level plist for b/327020913 in framework directory \(framework): \(error)"
- )
- }
- // Move privacy manifest containing resource bundles into the framework.
- let resourceDir = platformFrameworkDir
- .appendingPathComponent(
- platform == .catalyst || platform == .macOS ? "Resources" : ""
- )
- .resolvingSymlinksInPath()
- // Move resource bundles into the platform framework.
- do {
- try fileManager.contentsOfDirectory(
- at: frameworkPath.deletingLastPathComponent(),
- includingPropertiesForKeys: nil
- )
- .filter { $0.pathExtension == "bundle" }
- // Bundles are moved rather than copied to prevent them from being
- // packaged in a `Resources` directory at the root of the xcframework.
- .forEach {
- try fileManager.moveItem(
- at: $0,
- to: resourceDir.appendingPathComponent($0.lastPathComponent)
- )
- }
- } catch {
- fatalError(
- "Could not move resources for framework \(frameworkPath), platform \(platform). Error: \(error)"
- )
- }
- // Use the appropriate moduleMaps
- packageModuleMaps(inFrameworks: [frameworkPath],
- frameworkName: framework,
- moduleMapContents: moduleMapContents,
- destination: platformFrameworkDir)
- return platformFrameworkDir
- }
- }
- /// Package the built frameworks into an XCFramework.
- /// - Parameter withName: The framework name.
- /// - Parameter frameworks: The grouped frameworks.
- /// - Parameter xcframeworksDir: Location at which to build the xcframework.
- /// - Parameter resourceContents: Location of the resources for this xcframework.
- static func makeXCFramework(withName name: String,
- frameworks: [URL],
- xcframeworksDir: URL,
- resourceContents: URL?) -> URL {
- let xcframework = xcframeworksDir
- .appendingPathComponent(frameworkBuildName(name) + ".xcframework")
- // The arguments for the frameworks need to be separated.
- let frameworkArgs = frameworks.flatMap { frameworkPath in
- do {
- // Xcode 15.0-15.2: Return the canonical path to work around issue
- // https://forums.swift.org/t/67439
- let frameworkCanonicalPath = try frameworkPath.resourceValues(forKeys: [.canonicalPathKey])
- .canonicalPath!
- return ["-framework", frameworkCanonicalPath]
- } catch {
- fatalError("Failed to get canonical path for \(frameworkPath): \(error)")
- }
- }
- let outputArgs = ["-output", xcframework.path]
- let args = ["-create-xcframework"] + frameworkArgs + outputArgs
- print("""
- Building \(xcframework) with command:
- /usr/bin/xcodebuild \(args.joined(separator: " "))
- """)
- let result = syncExec(command: "/usr/bin/xcodebuild", args: args, captureOutput: true)
- switch result {
- case let .error(code, output):
- fatalError("Could not build xcframework for \(name) exit code \(code): \(output)")
- case .success:
- print("XCFramework for \(name) built successfully at \(xcframework).")
- }
- // xcframework resources are packaged at top of xcframework.
- if let resourceContents = resourceContents {
- let resourceDir = xcframework.appendingPathComponent("Resources")
- do {
- try ResourcesManager.moveAllBundles(inDirectory: resourceContents, to: resourceDir)
- } catch {
- fatalError("Could not move bundles into Resources directory while building \(name): " +
- "\(error)")
- }
- }
- return xcframework
- }
- }
|