Skip to content

Move to xcframework bundles #4737

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ZipBuilder/Sources/ZipBuilder/CocoaPodUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ enum CocoaPodUtils {

// Include the minimum iOS version.
podfile += """
use_frameworks!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use_frameworks! was purposely not there to build static library frameworks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems there's a way for xcframework containers to take either, but I didn't get it to work, since that container then expects just a static binar and not a fake framework with header constructs. With dyld3 being there on iOS 13 that shouldn't be a huge impact, and we can delete quite a bit of the script logic.

Folks with CocoaPods can still choose either.

platform :ios, '\(LaunchArgs.shared.minimumIOSVersion)'
target 'FrameworkMaker' do\n
"""
Expand Down
145 changes: 70 additions & 75 deletions ZipBuilder/Sources/ZipBuilder/FrameworkBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ enum Architecture: String, CaseIterable {
enum TargetPlatform: String {
case device = "iphoneos"
case simulator = "iphonesimulator"
case catalyst = "macosx"

/// Extra C flags that should be included as part of the build process for each target platform.
func otherCFlags() -> [String] {
Expand All @@ -32,21 +33,24 @@ enum Architecture: String, CaseIterable {
case .simulator:
// No extra arguments are required for simulator builds.
return []
case .catalyst:
return []
}
}
}

case arm64
case arm64e
case armv7
case i386
case x86_64
case x86_64h // x86_64h, Haswell, used for Mac Catalyst

/// The platform associated with the architecture.
var platform: TargetPlatform {
switch self {
case .arm64, .arm64e, .armv7: return .device
case .armv7, .arm64: return .device
case .i386, .x86_64: return .simulator
case .x86_64h: return .catalyst
}
}
}
Expand Down Expand Up @@ -96,7 +100,8 @@ struct FrameworkBuilder {
}

// Build the full cached framework path.
let cachedFrameworkDir = cachedFrameworkRoot.appendingPathComponent("\(podName).framework")
let realFramework = realFrameworkName(podName)
let cachedFrameworkDir = cachedFrameworkRoot.appendingPathComponent("\(realFramework).framework")
let frameworkDir = compileFrameworkAndResources(withName: podName)
do {
// Remove the previously cached framework if it exists, otherwise the `moveItem` call will
Expand Down Expand Up @@ -181,20 +186,31 @@ struct FrameworkBuilder {
/// - logRoot: Root directory where all logs should be written.
/// - Returns: A URL to the thin library that was built.
private func buildThin(framework: String,
arch: Architecture,
archs: [Architecture],
buildDir: URL,
logRoot: URL) -> URL {

let arch = archs[0]
let isMacCatalyst = arch == Architecture.x86_64h
let isMacCatalystString = isMacCatalyst ? "YES" : "NO"
let platform = arch.platform
let platformFolder = isMacCatalyst ? "maccatalyst" : platform.rawValue
let workspacePath = projectDir.appendingPathComponent("FrameworkMaker.xcworkspace").path
let distributionFlag = carthageBuild ? "-DFIREBASE_BUILD_CARTHAGE" : "-DFIREBASE_BUILD_ZIP_FILE"
let platformSpecificFlags = platform.otherCFlags().joined(separator: " ")
let cFlags = "OTHER_CFLAGS=$(value) \(distributionFlag) \(platformSpecificFlags)"
let cleanArch = isMacCatalyst ? Architecture.x86_64.rawValue : archs.map{$0.rawValue}.joined(separator: " ")

let args = ["build",
"-configuration", "release",
"-workspace", workspacePath,
"-scheme", framework,
"GCC_GENERATE_DEBUGGING_SYMBOLS=No",
"ARCHS=\(arch.rawValue)",
"ARCHS=\(cleanArch)",
"VALID_ARCHS=\(cleanArch)",
"ONLY_ACTIVE_ARCH=NO",
"BUILD_LIBRARIES_FOR_DISTRIBUTION=YES",
"SUPPORTS_MACCATALYST=\(isMacCatalystString)",
"BUILD_DIR=\(buildDir.path)",
"-sdk", platform.rawValue,
cFlags]
Expand Down Expand Up @@ -227,15 +243,30 @@ struct FrameworkBuilder {
""")

// Use the Xcode-generated path to return the path to the compiled library.
let libPath = buildDir.appendingPathComponents(["Release-\(platform.rawValue)",
let realFramework = realFrameworkName(framework)
let libPath = buildDir.appendingPathComponents(["Release-\(platformFolder)",
framework,
"lib\(framework).a"])
"\(realFramework).framework"])

print("buildThin returns \(libPath)")
return libPath
}
}

// Cries in Google. Why is this not the same?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely because of CocoaPods module_name overrides like https://github.com/protocolbuffers/protobuf/blob/master/Protobuf.podspec#L15

private func realFrameworkName(_ framework: String) -> String {
switch framework {
case "PromisesObjC":
return "FBLPromises"
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 lipo command to create a "fat" archive.
/// 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 logsOutputDir: The path to the directory to place build logs.
Expand Down Expand Up @@ -264,88 +295,52 @@ struct FrameworkBuilder {
// Build every architecture and save the locations in an array to be assembled.
// TODO: Pass in supported architectures here, for those open source SDKs that don't support
// individual architectures.
//
// xcframework doesn't lipo things together but accepts fat frameworks for one target.
// We group architectures here to deal with this fact.
var archs = LaunchArgs.shared.archs
var groupedArchs: [[Architecture]] = []

for pair in [[Architecture.armv7, .arm64], [Architecture.i386, .x86_64]] {
if archs.contains(pair[0]) && archs.contains(pair[1]) {
groupedArchs.append(pair)
archs = archs.filter() { !pair.contains($0) }
}
}
// Add remaining ungrouped
for arch in archs {
groupedArchs.append([arch])
}

var thinArchives = [URL]()
for arch in LaunchArgs.shared.archs {
let buildDir = projectDir.appendingPathComponent(arch.rawValue)
for archs in groupedArchs {
let buildDir = projectDir.appendingPathComponent(archs[0].rawValue)
let thinArchive = buildThin(framework: framework,
arch: arch,
archs: archs,
buildDir: buildDir,
logRoot: logsDir)
thinArchives.append(thinArchive)
}

// Create the framework directory in the filesystem for the thin archives to go.
let frameworkDir = outputDir.appendingPathComponent("\(framework).framework")
do {
try fileManager.createDirectory(at: frameworkDir, withIntermediateDirectories: true)
} catch {
fatalError("Could not create framework directory while building framework \(framework). " +
"\(error)")
let frameworkDir = outputDir.appendingPathComponent("\(framework).xcframework")

let inputArgs = thinArchives.flatMap { url -> [String] in
return ["-framework", url.path]
}

// Build the fat archive using the `lipo` command. We need the full archive path and the list of
// thin paths (as Strings, not URLs).
let thinPaths = thinArchives.map { $0.path }
let fatArchive = frameworkDir.appendingPathComponent(framework)
let result = syncExec(command: "/usr/bin/lipo", args: ["-create", "-output", fatArchive.path] + thinPaths)
print("About to create xcframework for \(frameworkDir.path) with \(inputArgs)")

// xcframework doesn't support legacy architectures: armv7, i386.
// It will throw a "Both ios-arm64 and ios-armv7 represent two equivalent library definitions" error.
let result = syncExec(command: "/usr/bin/xcodebuild", args: ["-create-xcframework", "-output", frameworkDir.path] + inputArgs)
switch result {
case let .error(code, output):
fatalError("""
lipo command exited with \(code) when trying to build \(framework). Output:
xcodebuild -create-xcframework command exited with \(code) when trying to build \(framework). Output:
\(output)
""")
case .success:
print("lipo command for \(framework) succeeded.")
}

// Remove the temporary thin archives.
for thinArchive in thinArchives {
do {
try fileManager.removeItem(at: thinArchive)
} 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 thin archive at \(thinArchive.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 \(thinArchive.path)
""")
}
}

// Verify Firebase headers include an explicit umbrella header for Firebase.h.
let headersDir = podsDir.appendingPathComponents(["Headers", "Public", framework])
if framework.hasPrefix("Firebase"), framework != "FirebaseCoreDiagnostics" {
let frameworkHeader = headersDir.appendingPathComponent("\(framework).h")
guard fileManager.fileExists(atPath: frameworkHeader.path) else {
fatalError("Missing explicit umbrella header for \(framework).")
}
}

// Copy the Headers over. Pass in the prefix to remove in order to generate the relative paths
// for some frameworks that have nested folders in their public headers.
let headersDestination = frameworkDir.appendingPathComponent("Headers")
do {
try recursivelyCopyHeaders(from: headersDir, to: headersDestination)
} catch {
fatalError("Could not copy headers from \(headersDir) to Headers directory in " +
"\(headersDestination): \(error)")
}

// 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`
let arch = Architecture.arm64
let contentsDir = projectDir.appendingPathComponents([arch.rawValue,
"Release-\(arch.platform.rawValue)",
framework])
let resourceDir = frameworkDir.appendingPathComponent("Resources")
do {
try ResourcesManager.moveAllBundles(inDirectory: contentsDir, to: resourceDir)
} catch {
fatalError("Could not move bundles into Resources directory while building \(framework): " +
"\(error)")
print("xcodebuild -create-xcframework command for \(framework) succeeded.")
}

return frameworkDir
Expand Down
2 changes: 1 addition & 1 deletion ZipBuilder/Sources/ZipBuilder/Zip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ struct Zip {
}

// Run the `zip` command. This could be replaced with a proper Zip library in the future.
let command = "zip -q -r -dg \(zip.lastPathComponent) \(directory.lastPathComponent)"
let command = "zip --symlinks -q -r -dg \(zip.lastPathComponent) \(directory.lastPathComponent)"
let result = Shell.executeCommandFromScript(command, workingDir: parentDir)
switch result {
case .success:
Expand Down
7 changes: 4 additions & 3 deletions ZipBuilder/Sources/ZipBuilder/ZipBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ struct ZipBuilder {
// copied in each product's directory.
let frameworks = generateFrameworks(fromPods: installedPods, inProjectDir: projectDir)

ModuleMapBuilder(frameworks: frameworks, customSpecRepos: customSpecRepos, allPods: installedPods).build()
//ModuleMapBuilder(frameworks: frameworks, customSpecRepos: customSpecRepos, allPods: installedPods).build()

for (framework, paths) in frameworks {
print("Frameworks for pod: \(framework) were compiled at \(paths)")
Expand Down Expand Up @@ -305,8 +305,8 @@ struct ZipBuilder {
if pod.key == crashlyticsPodName {
for file in ["upload-symbols", "run"] {
let source = pod.value.installedLocation.appendingPathComponent(file)
let target = zipDir.appendingPathComponent(crashlyticsPodName).appendingPathComponent(file)

let target = zipDir.appendingPathComponent(crashlyticsPodName).appendingPathComponent(file)
do {
try FileManager.default.copyItem(at: source, to: target)
} catch {
Expand Down Expand Up @@ -420,7 +420,8 @@ struct ZipBuilder {
continue
}

let destination = dir.appendingPathComponent(frameworkName)
let xcFrameworkName = frameworkName.replacingOccurrences(of: ".framework", with: ".xcframework")
let destination = dir.appendingPathComponent(xcFrameworkName)
try fileManager.copyItem(at: framework, to: destination)
copiedFrameworkNames.append(frameworkName.replacingOccurrences(of: ".framework", with: ""))
}
Expand Down
10 changes: 10 additions & 0 deletions ZipBuilder/Template/FrameworkMaker.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
13 changes: 13 additions & 0 deletions ZipBuilder/Template/FrameworkMaker.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXFileReference section */
05A46BD71CC9B2BE007BDB33 /* FrameworkMaker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FrameworkMaker.app; sourceTree = BUILT_PRODUCTS_DIR; };
784BA73523D9E621003FCA76 /* FrameworkMaker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FrameworkMaker.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -24,6 +25,7 @@
05A46BCE1CC9B2BE007BDB33 = {
isa = PBXGroup;
children = (
784BA73523D9E621003FCA76 /* FrameworkMaker.entitlements */,
05A46BD81CC9B2BE007BDB33 /* Products */,
);
sourceTree = "<group>";
Expand Down Expand Up @@ -75,6 +77,7 @@
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
English,
en,
Base,
);
Expand Down Expand Up @@ -150,6 +153,7 @@
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
VALID_ARCHS = "arm64 arm64e armv7 armv7s x86_64 x86_64h i386";
};
name = Debug;
};
Expand Down Expand Up @@ -188,28 +192,37 @@
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;
VALID_ARCHS = "arm64 arm64e armv7 armv7s x86_64 x86_64h i386";
};
name = Release;
};
05A46BEF1CC9B2BE007BDB33 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = FrameworkMaker.entitlements;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
INFOPLIST_FILE = Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = google.FrameworkMaker;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
05A46BF01CC9B2BE007BDB33 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = FrameworkMaker.entitlements;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
INFOPLIST_FILE = Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = google.FrameworkMaker;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
Expand Down