Skip to content

Instantly share code, notes, and snippets.

@Lavmint
Created October 30, 2020 19:58
Show Gist options
  • Save Lavmint/4c7ab43f57d5fa6eb432799678dfc768 to your computer and use it in GitHub Desktop.
Save Lavmint/4c7ab43f57d5fa6eb432799678dfc768 to your computer and use it in GitHub Desktop.
Attempt to generate xcframeworks
//
// 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