Created
October 30, 2020 19:58
-
-
Save Lavmint/4c7ab43f57d5fa6eb432799678dfc768 to your computer and use it in GitHub Desktop.
Attempt to generate xcframeworks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// File.swift | |
// | |
// | |
// Created by Alexey Averkin on 28.10.2020. | |
// | |
import Foundation | |
func main() { | |
throwable { | |
try xcframework( | |
url: "https://github.com/realm/realm-cocoa.git", | |
version: "10.1.1", | |
for: [.iOS], | |
to: "XCFrameworks") | |
} | |
} | |
main() | |
@discardableResult | |
func shell(_ command: String) -> String { | |
let task = Process() | |
let pipe = Pipe() | |
task.standardOutput = pipe | |
task.arguments = ["-c", command] | |
task.launchPath = "/bin/bash" | |
task.launch() | |
let data = pipe.fileHandleForReading.readDataToEndOfFile() | |
let output = String(data: data, encoding: .utf8)! | |
return output | |
} | |
enum EyebrowError: Error { | |
case urlInvalid(String) | |
case gitCloneFailed(String) | |
case xcprojectGenerationFailed | |
case xcprojectUnrecognizedName | |
case xcarchiveBuildFailed | |
case xcframeworkCreateFailed | |
case sdkFrameworkNotFound | |
} | |
enum Platform { | |
case iOS, macOS, tvOS, watchOS, driverOS | |
var sdks: [SDK] { | |
switch self { | |
case .iOS: | |
return [.iphoneos, .iphonesimulator] | |
case .macOS: | |
return [.macosx] | |
case .tvOS: | |
return [.appletvos, .appletvsimulator] | |
case .driverOS: | |
return [.watchos, .watchsimulator] | |
case .watchOS: | |
return [.driverkit] | |
} | |
} | |
} | |
enum SDK: String { | |
case iphoneos, iphonesimulator | |
case macosx | |
case appletvos, appletvsimulator | |
case watchos, watchsimulator | |
case driverkit = "driverkit.macosx" | |
} | |
func throwable(_ block: () throws -> Void) { | |
do { | |
try block() | |
} catch { | |
print(error) | |
} | |
} | |
@discardableResult | |
func xcframework(url: String, version: String, for platforms: [Platform], to path: String) throws -> URL { | |
let projectDirectoryURL = try clone(url: url, version: version) | |
let projectName = try dumpProjectName(in: projectDirectoryURL) | |
let projectURL = try generateXCProject(name: projectName, in: projectDirectoryURL) | |
let xcframeworkURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true) | |
.appendingPathComponent(path) | |
.appendingPathComponent(projectName) | |
.appendingPathExtension("xcframework") | |
guard !FileManager.default.fileExists(atPath: xcframeworkURL.path) else { | |
print("[\(projectName)] using xcframework: \(xcframeworkURL.path)") | |
return xcframeworkURL | |
} | |
let sdks = platforms.map({ $0.sdks }).reduce([], +) | |
var frameworkArgs: [String] = [] | |
for sdk in sdks { | |
let url = try archive(name: projectName, projectURL: projectURL, for: sdk) | |
let frameworkPath = try find(frameworkName: projectName, in: url) | |
frameworkArgs.append("-framework \(frameworkPath)") | |
} | |
let sdksString = sdks.map({ $0.rawValue }).joined(separator: ", ") | |
print("[\(projectName)] building xcframework for \(sdksString) ...") | |
let out = shell("xcodebuild -quiet -create-xcframework \(frameworkArgs) -output \(xcframeworkURL.path)") | |
print(out) | |
guard FileManager.default.fileExists(atPath: xcframeworkURL.path) else { | |
throw EyebrowError.xcframeworkCreateFailed | |
} | |
return xcframeworkURL | |
} | |
func find(frameworkName: String, in root: URL) throws -> String { | |
guard let path = shell("find \(root.path) -name \(frameworkName).framework").split(separator: "\n").first else { | |
throw EyebrowError.sdkFrameworkNotFound | |
} | |
return String(path) | |
} | |
func archive(name: String, projectURL: URL, for sdk: SDK) throws -> URL { | |
let archiveName = "\(name)-\(sdk.rawValue).xcarchive" | |
let archiveURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true) | |
.appendingPathComponent("Eyebrow") | |
.appendingPathComponent(archiveName) | |
guard !FileManager.default.fileExists(atPath: archiveURL.path) else { | |
print("[\(name)] using archive \(sdk.rawValue)") | |
return archiveURL | |
} | |
print("[\(name)] building archive for \(sdk.rawValue)...") | |
let out = shell("xcodebuild archive -quiet -project \(projectURL.path) -scheme \(name) -sdk \(sdk.rawValue) -parallelizeTargets -archivePath \(archiveURL.path) -configuration Release SKIP_INSTALL=NO BUILD_LIBRARIES_FOR_DISTRIBUTION=YES") | |
print(out) | |
guard FileManager.default.fileExists(atPath: archiveURL.path) else { | |
throw EyebrowError.xcarchiveBuildFailed | |
} | |
return archiveURL | |
} | |
func generateXCProject(name: String, in projectDirectoryURL: URL) throws -> URL { | |
let currentDir = FileManager.default.currentDirectoryPath | |
FileManager.default.changeCurrentDirectoryPath(projectDirectoryURL.path) | |
defer { | |
FileManager.default.changeCurrentDirectoryPath(currentDir) | |
} | |
let projectURL = URL(fileURLWithPath: projectDirectoryURL.path, isDirectory: true) | |
.appendingPathComponent(name) | |
.appendingPathExtension("xcodeproj") | |
guard !FileManager.default.fileExists(atPath: projectURL.path) else { | |
print("[\(name)] using xcodeproj: \(projectURL.path)") | |
return projectURL | |
} | |
print("[\(name)] generating xcodeproj...") | |
let out = shell("swift package generate-xcodeproj") | |
print(out) | |
guard FileManager.default.fileExists(atPath: projectURL.path) else { | |
throw EyebrowError.xcprojectGenerationFailed | |
} | |
return projectURL | |
} | |
func dumpProjectName(in projectDirectoryURL: URL) throws -> String { | |
let currentDir = FileManager.default.currentDirectoryPath | |
FileManager.default.changeCurrentDirectoryPath(projectDirectoryURL.path) | |
defer { | |
FileManager.default.changeCurrentDirectoryPath(currentDir) | |
} | |
guard let data = shell("swift package dump-package").data(using: .utf8) else { | |
throw EyebrowError.xcprojectUnrecognizedName | |
} | |
let json: Any | |
do { | |
json = try JSONSerialization.jsonObject(with: data, options: []) | |
} catch { | |
throw EyebrowError.xcprojectUnrecognizedName | |
} | |
guard let dict = json as? [String: Any], let name = dict["name"], let projectName = name as? String else { | |
throw EyebrowError.xcprojectUnrecognizedName | |
} | |
print("[\(name)] project name is: \(name)") | |
return projectName | |
} | |
func clone(url: String, version: String) throws -> URL { | |
guard let projectName = URL(string: url)?.deletingPathExtension().lastPathComponent else { | |
throw EyebrowError.urlInvalid(url) | |
} | |
let projectURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true) | |
.appendingPathComponent(projectName) | |
guard !FileManager.default.fileExists(atPath: projectURL.path) else { | |
print("using project dir at: \(projectURL.path)") | |
return projectURL | |
} | |
print("cloning \(url) on tag: \(version)...") | |
let out = shell("git clone --quiet --depth 1 --branch v\(version) \(url)") | |
print(out) | |
guard FileManager.default.fileExists(atPath: projectURL.path) else { | |
throw EyebrowError.gitCloneFailed(url) | |
} | |
return projectURL | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment