Skip to content
Draft
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
122 changes: 103 additions & 19 deletions Sources/XToolSupport/SDKBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@ struct SDKBuilder {
withIntermediateDirectories: true
)

// TODO: parallelize these two steps
// TODO: parallelize these three steps
// we need to synchronize progress reporting though

try await installMacros(in: output)

try await installToolset(in: output)

let dev = try await installDeveloper(in: output)
Expand Down Expand Up @@ -219,7 +221,49 @@ struct SDKBuilder {
try await tarExit
}

// swiftlint:disable:next cyclomatic_complexity
private func installMacros(in output: URL) async throws {
let oamVersion = "1.1.0"

@Dependency(\.httpClient) var httpClient
let url = URL(string: """
https://github.com/xtool-org/OpenAppleMacros/releases/download/\
v\(oamVersion)/OpenAppleMacrosServer-\(arch.rawValue)
""")!

let (response, body) = try await httpClient.send(HTTPRequest(url: url))
guard response.status == 200, let body else { throw Console.Error("Could not fetch toolset") }

let length: Int64? = switch body.length {
case .known(let known): known
case .unknown: nil
}

let outputPath = output.appendingPathComponent("OpenAppleMacrosServer").path
do {
let handle = try FileDescriptor.open(
FilePath(outputPath),
.writeOnly,
options: .create,
permissions: [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute]
)
defer { try? handle.close() }

var written: Int64 = 0
for try await chunk in body {
try handle.writeAll(chunk)
written += Int64(chunk.count)

if let length {
let progress = Int(Double(written) / Double(length) * 100)
print("\r[Downloading OpenAppleMacros] \(progress)%", terminator: "")
fflush(stdoutSafe)
}
}
}
print()
}

// swiftlint:disable:next cyclomatic_complexity function_body_length
private func installDeveloper(in output: URL) async throws -> URL {
let dev = output.appendingPathComponent("Developer")

Expand Down Expand Up @@ -299,42 +343,82 @@ struct SDKBuilder {

print("[Finalizing SDKs]")

/*
XCTest and Testing.framework are located in *.platform/Developer/{Library/Frameworks,usr/lib} rather than inside
the SDK. These search paths are explicitly included when building tests, which presumably ensures that normal
applications don't accidentally link against them in production.
for platform in ["iPhoneOS", "MacOSX", "iPhoneSimulator"] {
let platformDir = dev.appendingPathComponent("Platforms/\(platform).platform")

SwiftPM makes no such affordances outside of macOS, so we add the usr/lib path as include/library search paths
in the SDK config, and symlink the frameworks into the SDKs (since there's no frameworkSearchPaths option).
While this drops a safeguard it's better than not having the testing libs at all.
*/
/*
XCTest and Testing.framework are located in *.platform/Developer/{Library/Frameworks,usr/lib} rather than inside
the SDK. These search paths are explicitly included when building tests, which presumably ensures that normal
applications don't accidentally link against them in production.

for platform in ["iPhoneOS", "MacOSX", "iPhoneSimulator"] {
let lib = "../../../../../Library"
let dest = dev.appendingPathComponent("""
Platforms/\(platform).platform/Developer/SDKs/\(platform).sdk\
/System/Library/Frameworks
SwiftPM makes no such affordances outside of macOS, so we add the usr/lib path as include/library search paths
in the SDK config, and symlink the frameworks into the SDKs (since there's no frameworkSearchPaths option).
While this drops a safeguard it's better than not having the testing libs at all.
*/

let fwk = platformDir.appendingPathComponent("""
Developer/SDKs/\(platform).sdk/System/Library/Frameworks
""").path

let lib = "../../../../../Library"

try FileManager.default.createSymbolicLink(
atPath: "\(dest)/Testing.framework",
atPath: "\(fwk)/Testing.framework",
withDestinationPath: "\(lib)/Frameworks/Testing.framework"
)

try FileManager.default.createSymbolicLink(
atPath: "\(dest)/XCTest.framework",
atPath: "\(fwk)/XCTest.framework",
withDestinationPath: "\(lib)/Frameworks/XCTest.framework"
)

try FileManager.default.createSymbolicLink(
atPath: "\(dest)/XCUIAutomation.framework",
atPath: "\(fwk)/XCUIAutomation.framework",
withDestinationPath: "\(lib)/Frameworks/XCUIAutomation.framework"
)

try FileManager.default.createSymbolicLink(
atPath: "\(dest)/XCTestCore.framework",
atPath: "\(fwk)/XCTestCore.framework",
withDestinationPath: "\(lib)/PrivateFrameworks/XCTestCore.framework"
)

/*
Apply patches to load OpenAppleMacros as the swift-plugin-server
*/

let bin = platformDir.appendingPathComponent("Developer/usr/bin")
let pluginServer = bin.appendingPathComponent("swift-plugin-server")
try? FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
try? FileManager.default.removeItem(at: pluginServer)
try FileManager.default.createSymbolicLink(
atPath: pluginServer.path,
withDestinationPath: "../../../../../../OpenAppleMacrosServer"
)

let hostDir = platformDir.appendingPathComponent("Developer/usr/lib/swift/host")
let pluginsDir = hostDir.appendingPathComponent("plugins")

if platform == "iPhoneSimulator" {
// The iPhoneSimulator platform doesn't contain plugins; Xcode seems to be hardcoded
// to look at the ones from iPhoneOS.platform. We can symlink the iPhoneOS ones too.
try? FileManager.default.createDirectory(at: hostDir, withIntermediateDirectories: true)
try? FileManager.default.removeItem(at: pluginsDir)
try FileManager.default.createSymbolicLink(
atPath: pluginsDir.path,
withDestinationPath: "../../../../../../iPhoneOS.platform/Developer/usr/lib/swift/host/plugins"
)
} else {
let plugins = try FileManager.default.contentsOfDirectory(at: pluginsDir, includingPropertiesForKeys: nil)
// remove the lib*.dylib plugins and create lib*.so files, which is what swiftc expects
// on Linux hosts. the files are stubs (empty) because we just need them to convince
// swiftc that yes we can handle said modules. the actual logic lives in the OpenAppleMacros
// server.
for plugin in plugins {
try FileManager.default.removeItem(at: plugin)
let name = plugin.deletingPathExtension().lastPathComponent
try Data().write(to: pluginsDir.appendingPathComponent("\(name).so"))
}
}
}

return dev
Expand Down