Skip to content

Add support for generating Swift SDKs with multiple host triples #232

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
44 changes: 29 additions & 15 deletions Sources/GeneratorCLI/GeneratorCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,13 @@ extension GeneratorCLI {
var targetSwiftPackagePath: String? = nil

@Option(
name: .customLong("host"),
help: """
The host triple of the bundle. Defaults to a triple of the machine this generator is \
running on if unspecified.
The host triples of the bundle. Defaults to a triple or triples of the machine this generator is \
running on if unspecified. Multiple host triples are only supported for macOS hosts.
"""
)
var host: Triple? = nil
var hosts: [Triple] = []

@Option(
help:
Expand Down Expand Up @@ -174,19 +175,26 @@ extension GeneratorCLI {
#endif
}

func deriveHostTriple() throws -> Triple {
if let host {
return host
func deriveHostTriples() throws -> [Triple] {
if !hosts.isEmpty {
return hosts
}
let current = try SwiftSDKGenerator.getCurrentTriple(isVerbose: self.verbose)
if let arch = hostArch {
let target = Triple(arch: arch, vendor: current.vendor!, os: current.os!)
appLogger.warning(
"deprecated: Please use `--host \(target.triple)` instead of `--host-arch \(arch)`"
)
return target
return [target]
}
// macOS toolchains are built as universal binaries
if current.isMacOSX {
return [
Triple("arm64-apple-macos"),
Triple("x86_64-apple-macos"),
]
}
return current
return [current]
}
}

Expand Down Expand Up @@ -230,7 +238,7 @@ extension GeneratorCLI {
)
var distributionVersion: String?

func deriveTargetTriple(hostTriple: Triple) -> Triple {
func deriveTargetTriple(hostTriples: [Triple]) -> Triple {
if let target = generatorOptions.target {
return target
}
Expand All @@ -241,7 +249,13 @@ extension GeneratorCLI {
)
return target
}
return Triple(arch: hostTriple.arch!, vendor: nil, os: .linux, environment: .gnu)
let arch: Triple.Arch
if hostTriples.count == 1, let hostTriple = hostTriples.first {
arch = hostTriple.arch!
} else {
arch = try! SwiftSDKGenerator.getCurrentTriple(isVerbose: false).arch!
}
return Triple(arch: arch, vendor: nil, os: .linux, environment: .gnu)
}

func run() async throws {
Expand All @@ -265,12 +279,12 @@ extension GeneratorCLI {
name: distributionName,
version: distributionVersion
)
let hostTriple = try self.generatorOptions.deriveHostTriple()
let targetTriple = self.deriveTargetTriple(hostTriple: hostTriple)
let hostTriples = try self.generatorOptions.deriveHostTriples()
let targetTriple = self.deriveTargetTriple(hostTriples: hostTriples)

let recipe = try LinuxRecipe(
targetTriple: targetTriple,
hostTriple: hostTriple,
hostTriples: hostTriples,
linuxDistribution: linuxDistribution,
swiftVersion: generatorOptions.swiftVersion,
swiftBranch: self.generatorOptions.swiftBranch,
Expand Down Expand Up @@ -333,8 +347,8 @@ extension GeneratorCLI {
}
let recipe = try WebAssemblyRecipe(
hostSwiftPackage: generatorOptions.hostSwiftPackagePath.map {
let hostTriple = try self.generatorOptions.deriveHostTriple()
return WebAssemblyRecipe.HostToolchainPackage(path: FilePath($0), triple: hostTriple)
let hostTriples = try self.generatorOptions.deriveHostTriples()
return WebAssemblyRecipe.HostToolchainPackage(path: FilePath($0), triples: hostTriples)
},
targetSwiftPackagePath: FilePath(targetSwiftPackagePath),
wasiSysroot: FilePath(self.wasiSysroot),
Expand Down
43 changes: 31 additions & 12 deletions Sources/SwiftSDKGenerator/Artifacts/DownloadableArtifacts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,30 @@ struct DownloadableArtifacts: Sendable {
private let paths: PathsConfiguration

init(
hostTriple: Triple,
hostTriples: [Triple],
targetTriple: Triple,
_ versions: VersionsConfiguration,
_ paths: PathsConfiguration
) throws {
self.versions = versions
self.paths = paths

if hostTriple.os == .linux {
guard let hostTriple = hostTriples.first else {
throw StringError("no host triples")
}

if hostTriples.allSatisfy({ $0.os == .macosx }) {
self.hostSwift = .init(
remoteURL: versions.swiftDownloadURL(
subdirectory: "xcode",
platform: "osx",
fileExtension: "pkg"
),
localPath: paths.artifactsCachePath
.appending("host_swift_\(versions.swiftVersion)_apple-macos.pkg"),
isPrebuilt: true
)
} else if hostTriple.os == .linux {
// Amazon Linux 2 is chosen for its best compatibility with all Swift-supported Linux hosts
let hostArchSuffix =
hostTriple.arch == .aarch64 ? "-\(Triple.Arch.aarch64.linuxConventionName)" : ""
Expand All @@ -67,16 +82,13 @@ struct DownloadableArtifacts: Sendable {
isPrebuilt: true
)
} else {
self.hostSwift = .init(
remoteURL: versions.swiftDownloadURL(
subdirectory: "xcode",
platform: "osx",
fileExtension: "pkg"
),
localPath: paths.artifactsCachePath
.appending("host_swift_\(versions.swiftVersion)_\(hostTriple.triple).pkg"),
isPrebuilt: true
)
if hostTriples.count > 1 {
throw StringError(
"unsupported host triples \(hostTriples.map { "\($0)" }.joined(separator: ", ")) (only macOS supports multiple host triples)"
)
} else {
throw StringError("unsupported host triple \(hostTriple)")
}
}

self.hostLLVM = .init(
Expand Down Expand Up @@ -121,3 +133,10 @@ struct DownloadableArtifacts: Sendable {
)
}
}

struct StringError: Error, CustomStringConvertible {
Copy link
Contributor

@MaxDesiatov MaxDesiatov Aug 13, 2025

Choose a reason for hiding this comment

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

I don't see any value in stringly typed APIs in Swift, especially for errors. These are harder to test against, and go against the grain of the static typing approach that Swift takes. IMO a nested enum Error: Swift.Error within DownloadableArtifacts with explicit cases for each error path would work, and would make it easier to adopt typed throws in the future.

let description: String
init(_ description: String) {
self.description = description
}
}
18 changes: 9 additions & 9 deletions Sources/SwiftSDKGenerator/SwiftSDKRecipes/LinuxRecipe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ package struct LinuxRecipe: SwiftSDKRecipe {
}

let mainTargetTriple: Triple
let mainHostTriple: Triple
let mainHostTriples: [Triple]
let linuxDistribution: LinuxDistribution
let targetSwiftSource: TargetSwiftSource
let hostSwiftSource: HostSwiftSource
Expand All @@ -46,7 +46,7 @@ package struct LinuxRecipe: SwiftSDKRecipe {

package init(
targetTriple: Triple,
hostTriple: Triple,
hostTriples: [Triple],
linuxDistribution: LinuxDistribution,
swiftVersion: String,
swiftBranch: String?,
Expand Down Expand Up @@ -89,7 +89,7 @@ package struct LinuxRecipe: SwiftSDKRecipe {

self.init(
mainTargetTriple: targetTriple,
mainHostTriple: hostTriple,
mainHostTriples: hostTriples,
linuxDistribution: linuxDistribution,
targetSwiftSource: targetSwiftSource,
hostSwiftSource: hostSwiftSource,
Expand All @@ -100,15 +100,15 @@ package struct LinuxRecipe: SwiftSDKRecipe {

package init(
mainTargetTriple: Triple,
mainHostTriple: Triple,
mainHostTriples: [Triple],
linuxDistribution: LinuxDistribution,
targetSwiftSource: TargetSwiftSource,
hostSwiftSource: HostSwiftSource,
versionsConfiguration: VersionsConfiguration,
logger: Logger
) {
self.mainTargetTriple = mainTargetTriple
self.mainHostTriple = mainHostTriple
self.mainHostTriples = mainHostTriples
self.linuxDistribution = linuxDistribution
self.targetSwiftSource = targetSwiftSource
self.hostSwiftSource = hostSwiftSource
Expand Down Expand Up @@ -180,7 +180,7 @@ package struct LinuxRecipe: SwiftSDKRecipe {
func itemsToDownload(from artifacts: DownloadableArtifacts) -> [DownloadableArtifacts.Item] {
var items: [DownloadableArtifacts.Item] = []
if self.hostSwiftSource != .preinstalled
&& self.mainHostTriple.os != .linux
&& !self.mainHostTriples.contains(where: { $0.os == .linux })
&& !self.versionsConfiguration.swiftVersion.hasPrefix("6.")
{
items.append(artifacts.hostLLVM)
Expand Down Expand Up @@ -218,7 +218,7 @@ package struct LinuxRecipe: SwiftSDKRecipe {
return nil
}

return [self.mainHostTriple]
return self.mainHostTriples
}

package func makeSwiftSDK(
Expand All @@ -240,7 +240,7 @@ package struct LinuxRecipe: SwiftSDKRecipe {
try await generator.createDirectoryIfNeeded(at: sdkDirPath)

var downloadableArtifacts = try DownloadableArtifacts(
hostTriple: mainHostTriple,
hostTriples: mainHostTriples,
targetTriple: generator.targetTriple,
self.versionsConfiguration,
generator.pathsConfiguration
Expand Down Expand Up @@ -335,7 +335,7 @@ package struct LinuxRecipe: SwiftSDKRecipe {
}

if self.hostSwiftSource != .preinstalled {
if self.mainHostTriple.os != .linux
if !self.mainHostTriples.contains(where: { $0.os == .linux })
&& !self.versionsConfiguration.swiftVersion.hasPrefix("6.")
{
try await generator.prepareLLDLinker(engine, llvmArtifact: downloadableArtifacts.hostLLVM)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ package struct WebAssemblyRecipe: SwiftSDKRecipe {

package struct HostToolchainPackage: Sendable {
let path: FilePath
let triple: Triple
let triples: [Triple]

package init(path: FilePath, triple: Triple) {
package init(path: FilePath, triples: [Triple]) {
self.path = path
self.triple = triple
self.triples = triples
}
}

Expand Down Expand Up @@ -145,7 +145,7 @@ package struct WebAssemblyRecipe: SwiftSDKRecipe {
logger.info("Copying Swift binaries for the host triple...")
var hostTriples: [Triple]? = nil
if let hostSwiftPackage {
hostTriples = [hostSwiftPackage.triple]
hostTriples = hostSwiftPackage.triples
try await generator.rsync(
from: hostSwiftPackage.path.appending("usr"),
to: pathsConfiguration.toolchainDirPath
Expand Down
4 changes: 2 additions & 2 deletions Tests/SwiftSDKGeneratorTests/ArchitectureMappingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ final class ArchitectureMappingTests: XCTestCase {
) async throws {
let recipe = try LinuxRecipe(
targetTriple: targetTriple,
hostTriple: hostTriple,
hostTriples: [hostTriple],
linuxDistribution: .ubuntu(.jammy),
swiftVersion: "5.8-RELEASE",
swiftBranch: nil,
Expand Down Expand Up @@ -79,7 +79,7 @@ final class ArchitectureMappingTests: XCTestCase {

// Verify download URLs
let artifacts = try await DownloadableArtifacts(
hostTriple: hostTriple,
hostTriples: [hostTriple],
targetTriple: sdk.targetTriple,
recipe.versionsConfiguration,
sdk.pathsConfiguration
Expand Down
Loading