Skip to content

Commit 84d62bf

Browse files
Build Swift SDKs for a Linux host if the --host parameter matches a Linux OS (*-unknown-linux-gnu) (#167)
This fixes #114 and provides the following: - If the `--host` param or auto-detected `hostTriple.os` is `.linux`, the amazonlinux2 Swift toolchain is downloaded and included in the Swift SDK. Examples: ``` swift run swift-sdk-generator make-linux-sdk --swift-version 6.0.3-RELEASE --host x86_64-unknown-linux-gnu --target aarch64-unknown-linux-gnu swift run swift-sdk-generator make-linux-sdk --swift-version 5.10.1-RELEASE --host aarch64-unknown-linux-gnu --target x86_64-unknown-linux-gnu ``` - I chose `amazonlinux2` for the host toolchain as it seems to be compatible with *every* Swift-supported Linux distribution with glibc versions and included libraries. - Updated the README.md to show that Ubuntu, RHEL, Amazon Linux 2, and Debian are officially supported as hosts and somewhat supported as targets... :) For testing all of this, I updated the EndToEndTests: - If the`SWIFT_SDK_GENERATOR_TEST_LINUX_SWIFT_SDKS` env variable is defined, Swift SDKs are built for a Linux host with the arch matching the machine on which the tests are running. So on an M-series Mac, the triple `aarch64-unknown-linux-gnu` is used. Or on an Intel Mac or PC, then `x86_64-unknown-linux-gnu` is used as the `--host` triple. - When Linux Swift SDKs are being tested, then a matrix of Swift docker containers are run against each test case to ensure the SDK works on every Linux distribution: ``` [SwiftSDKGeneratorTests] Creating test project /tmp/FDFD488C-089B-4213-B927-0DE0F9BD6B3A/swift-sdk-generator-test [SwiftSDKGeneratorTests] Updating minimum swift-tools-version in test project... [SwiftSDKGeneratorTests] Building test project in 6.0-focal container [SwiftSDKGeneratorTests] Test project built successfully [SwiftSDKGeneratorTests] Building test project in 6.0-focal container with static-swift-stdlib [SwiftSDKGeneratorTests] Test project built successfully [SwiftSDKGeneratorTests] Building test project in 6.0-jammy container [SwiftSDKGeneratorTests] Test project built successfully [SwiftSDKGeneratorTests] Building test project in 6.0-jammy container with static-swift-stdlib [SwiftSDKGeneratorTests] Test project built successfully [SwiftSDKGeneratorTests] Building test project in 6.0-noble container [SwiftSDKGeneratorTests] Test project built successfully [SwiftSDKGeneratorTests] Building test project in 6.0-noble container with static-swift-stdlib [SwiftSDKGeneratorTests] Test project built successfully [SwiftSDKGeneratorTests] Building test project in 6.0-fedora39 container [SwiftSDKGeneratorTests] Test project built successfully [SwiftSDKGeneratorTests] Building test project in 6.0-fedora39 container with static-swift-stdlib [SwiftSDKGeneratorTests] Test project built successfully [SwiftSDKGeneratorTests] Building test project in 6.0-rhel-ubi9 container [SwiftSDKGeneratorTests] Test project built successfully [SwiftSDKGeneratorTests] Building test project in 6.0-rhel-ubi9 container with static-swift-stdlib [SwiftSDKGeneratorTests] Test project built successfully [SwiftSDKGeneratorTests] Building test project in 6.0-amazonlinux2 container [SwiftSDKGeneratorTests] Test project built successfully [SwiftSDKGeneratorTests] Building test project in 6.0-amazonlinux2 container with static-swift-stdlib [SwiftSDKGeneratorTests] Test project built successfully [SwiftSDKGeneratorTests] Building test project in 6.0-bookworm container [SwiftSDKGeneratorTests] Test project built successfully [SwiftSDKGeneratorTests] Building test project in 6.0-bookworm container with static-swift-stdlib [SwiftSDKGeneratorTests] Test project built successfully ```
1 parent f63f812 commit 84d62bf

File tree

8 files changed

+235
-69
lines changed

8 files changed

+235
-69
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,16 @@ Linux distributions officially supported by the Swift project.
3737
| -: | :- | :- |
3838
| macOS (arm64) | ✅ macOS 13.0+ ||
3939
| macOS (x86_64) | ✅ macOS 13.0+[^1] ||
40-
| Ubuntu | ⚠️ (WIP) | ✅ 20.04 / 22.04 |
41-
| RHEL | ⚠️ (WIP) | ✅ UBI 9 |
42-
40+
| Ubuntu | ✅ 20.04+ | ✅ 20.04 / 22.04 |
41+
| RHEL | ✅ Fedora 39[^2], UBI 9 | ✅ UBI 9 |
42+
| Amazon Linux 2 | ✅ Supported | ✅ Supported[^3] |
43+
| Debian 12 | ✅ Supported[^2] | ✅ Supported[^2][^3] |
4344

4445
[^1]: Since LLVM project doesn't provide pre-built binaries of `lld` for macOS on x86_64, it will be automatically built
4546
from sources by the generator, which will increase its run by at least 15 minutes on recent hardware. You will also
4647
need CMake and Ninja preinstalled (e.g. via `brew install cmake ninja`).
48+
[^2]: These distributions are only supported by Swift 5.10.1 and later as both host and target platforms.
49+
[^3]: These versions are technically supported but require custom commands and a Docker container to build the Swift SDK, as the generator will not download dependencies for these distributions automatically. See [issue #138](https://github.com/swiftlang/swift-sdk-generator/issues/138).
4750

4851
## How to use it
4952

Sources/SwiftSDKGenerator/Artifacts/DownloadableArtifacts.swift

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,31 @@ struct DownloadableArtifacts: Sendable {
5151
self.versions = versions
5252
self.paths = paths
5353

54-
self.hostSwift = .init(
55-
remoteURL: versions.swiftDownloadURL(
56-
subdirectory: "xcode",
57-
platform: "osx",
58-
fileExtension: "pkg"
59-
),
60-
localPath: paths.artifactsCachePath
61-
.appending("host_swift_\(versions.swiftVersion)_\(hostTriple.triple).pkg"),
62-
isPrebuilt: true
63-
)
54+
if hostTriple.os == .linux {
55+
// Amazon Linux 2 is chosen for its best compatibility with all Swift-supported Linux hosts
56+
let linuxArchSuffix = hostTriple.arch == .aarch64 ? "-\(Triple.Arch.aarch64.linuxConventionName)" : ""
57+
self.hostSwift = .init(
58+
remoteURL: versions.swiftDownloadURL(
59+
subdirectory: "amazonlinux2\(linuxArchSuffix)",
60+
platform: "amazonlinux2\(linuxArchSuffix)",
61+
fileExtension: "tar.gz"
62+
),
63+
localPath: paths.artifactsCachePath
64+
.appending("host_swift_\(versions.swiftVersion)_\(hostTriple.triple).tar.gz"),
65+
isPrebuilt: true
66+
)
67+
} else {
68+
self.hostSwift = .init(
69+
remoteURL: versions.swiftDownloadURL(
70+
subdirectory: "xcode",
71+
platform: "osx",
72+
fileExtension: "pkg"
73+
),
74+
localPath: paths.artifactsCachePath
75+
.appending("host_swift_\(versions.swiftVersion)_\(hostTriple.triple).pkg"),
76+
isPrebuilt: true
77+
)
78+
}
6479

6580
self.hostLLVM = .init(
6681
remoteURL: URL(

Sources/SwiftSDKGenerator/Generator/SwiftSDKGenerator+Fixup.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,13 @@ extension SwiftSDKGenerator {
4949
}
5050

5151
func symlinkClangHeaders() throws {
52-
try self.createSymlink(
53-
at: self.pathsConfiguration.toolchainDirPath.appending("usr/lib/swift_static/clang"),
54-
pointingTo: "../swift/clang"
55-
)
52+
let swiftStaticClangPath = self.pathsConfiguration.toolchainDirPath.appending("usr/lib/swift_static/clang")
53+
if !doesFileExist(at: swiftStaticClangPath) {
54+
logGenerationStep("Symlinking clang headers...")
55+
try self.createSymlink(
56+
at: self.pathsConfiguration.toolchainDirPath.appending("usr/lib/swift_static/clang"),
57+
pointingTo: "../swift/clang"
58+
)
59+
}
5660
}
5761
}

Sources/SwiftSDKGenerator/Generator/SwiftSDKGenerator+Unpack.swift

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Helpers
1414

1515
import struct SystemPackage.FilePath
1616

17-
let unusedDarwinPlatforms = [
17+
let unusedTargetPlatforms = [
1818
"appletvos",
1919
"appletvsimulator",
2020
"embedded",
@@ -34,11 +34,13 @@ let unusedHostBinaries = [
3434
"swift-format",
3535
"swift-package",
3636
"swift-package-collection",
37+
"lldb*",
3738
]
3839

3940
let unusedHostLibraries = [
4041
"sourcekitd.framework",
4142
"libsourcekitdInProc.so",
43+
"liblldb.so*",
4244
]
4345

4446
extension SwiftSDKGenerator {
@@ -49,23 +51,32 @@ extension SwiftSDKGenerator {
4951
try self.createDirectoryIfNeeded(at: pathsConfiguration.toolchainDirPath)
5052

5153
let excludes =
52-
unusedDarwinPlatforms.map { "--exclude usr/lib/swift/\($0)" } +
53-
unusedDarwinPlatforms.map { "--exclude usr/lib/swift_static/\($0)" } +
54+
unusedTargetPlatforms.map { "--exclude usr/lib/swift/\($0)" } +
55+
unusedTargetPlatforms.map { "--exclude usr/lib/swift_static/\($0)" } +
5456
unusedHostBinaries.map { "--exclude usr/bin/\($0)" } +
5557
unusedHostLibraries.map { "--exclude usr/lib/\($0)" }
5658

57-
try await Shell.run(
58-
#"""
59-
tar -x --to-stdout -f \#(hostSwiftPackagePath) \*.pkg/Payload |
60-
tar -C "\#(pathsConfiguration.toolchainDirPath)" -x \#(excludes.joined(separator: " ")) --include usr
61-
"""#,
62-
shouldLogCommands: isVerbose
63-
)
59+
if hostSwiftPackagePath.string.contains("tar.gz") {
60+
try await Shell.run(
61+
#"""
62+
tar -xzf \#(hostSwiftPackagePath) -C "\#(pathsConfiguration.toolchainDirPath)" -x \#(excludes.joined(separator: " ")) --strip-components=1
63+
"""#,
64+
shouldLogCommands: isVerbose
65+
)
66+
} else {
67+
try await Shell.run(
68+
#"""
69+
tar -x --to-stdout -f \#(hostSwiftPackagePath) \*.pkg/Payload |
70+
tar -C "\#(pathsConfiguration.toolchainDirPath)" -x \#(excludes.joined(separator: " ")) --include usr
71+
"""#,
72+
shouldLogCommands: isVerbose
73+
)
74+
}
6475
}
6576

6677
func removeToolchainComponents(
6778
_ packagePath: FilePath,
68-
platforms: [String] = unusedDarwinPlatforms,
79+
platforms: [String] = unusedTargetPlatforms,
6980
libraries: [String] = unusedHostLibraries,
7081
binaries: [String] = unusedHostBinaries
7182
) async throws {

Sources/SwiftSDKGenerator/SwiftSDKRecipes/LinuxRecipe.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ public struct LinuxRecipe: SwiftSDKRecipe {
164164

165165
func itemsToDownload(from artifacts: DownloadableArtifacts) -> [DownloadableArtifacts.Item] {
166166
var items: [DownloadableArtifacts.Item] = []
167-
if self.hostSwiftSource != .preinstalled && !self.versionsConfiguration.swiftVersion.hasPrefix("6.0") {
167+
if self.hostSwiftSource != .preinstalled
168+
&& self.mainHostTriple.os != .linux
169+
&& !self.versionsConfiguration.swiftVersion.hasPrefix("6.0") {
168170
items.append(artifacts.hostLLVM)
169171
}
170172

@@ -279,12 +281,12 @@ public struct LinuxRecipe: SwiftSDKRecipe {
279281
try await generator.fixAbsoluteSymlinks(sdkDirPath: sdkDirPath)
280282

281283
if self.hostSwiftSource != .preinstalled {
282-
if !self.versionsConfiguration.swiftVersion.hasPrefix("6.0") {
284+
if self.mainHostTriple.os != .linux && !self.versionsConfiguration.swiftVersion.hasPrefix("6.0") {
283285
try await generator.prepareLLDLinker(engine, llvmArtifact: downloadableArtifacts.hostLLVM)
284286
}
285287

286288
if self.versionsConfiguration.swiftVersion.hasPrefix("5.9") ||
287-
self.versionsConfiguration.swiftVersion .hasPrefix("5.10") {
289+
self.versionsConfiguration.swiftVersion.hasPrefix("5.10") {
288290
try await generator.symlinkClangHeaders()
289291
}
290292

Sources/SwiftSDKGenerator/SwiftSDKRecipes/WebAssemblyRecipe.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public struct WebAssemblyRecipe: SwiftSDKRecipe {
115115
}()
116116
try await generator.removeToolchainComponents(
117117
pathsConfiguration.toolchainDirPath,
118-
platforms: unusedDarwinPlatforms + ["embedded"],
118+
platforms: unusedTargetPlatforms,
119119
libraries: unusedHostLibraries + liblldbNames,
120120
binaries: unusedHostBinaries + ["lldb", "lldb-argdumper", "lldb-server"]
121121
)

Tests/SwiftSDKGeneratorTests/EndToEndTests.swift

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,17 @@ struct SDKConfiguration {
168168
return res
169169
}
170170

171+
var hostArch: String? {
172+
let triple = try? SwiftSDKGenerator.getCurrentTriple(isVerbose: false)
173+
return triple?.arch?.rawValue
174+
}
175+
171176
var sdkGeneratorArguments: String {
172177
return [
173178
"--sdk-name \(bundleName)",
174179
withDocker ? "--with-docker" : nil,
175180
"--swift-version \(swiftVersion)-RELEASE",
181+
testLinuxSwiftSDKs ? "--host \(hostArch!)-unknown-linux-gnu" : nil,
176182
"--target \(architecture)-unknown-linux-gnu",
177183
"--linux-distribution-name \(linuxDistributionName)"
178184
].compactMap{ $0 }.joined(separator: " ")
@@ -187,31 +193,81 @@ func skipSlow() throws {
187193
)
188194
}
189195

196+
var testLinuxSwiftSDKs: Bool {
197+
ProcessInfo.processInfo.environment.keys.contains("SWIFT_SDK_GENERATOR_TEST_LINUX_SWIFT_SDKS")
198+
}
199+
190200
func buildTestcase(_ logger: Logger, testcase: String, bundleName: String, tempDir: URL) async throws {
191201
let testPackageURL = tempDir.appendingPathComponent("swift-sdk-generator-test")
192202
let testPackageDir = FilePath(testPackageURL.path)
193203
try FileManager.default.createDirectory(atPath: testPackageDir.string, withIntermediateDirectories: true)
194204

195-
logger.info("Creating test project")
205+
logger.info("Creating test project \(testPackageDir)")
196206
try await Shell.run("swift package --package-path \(testPackageDir) init --type executable")
197207
let main_swift = testPackageURL.appendingPathComponent("Sources/main.swift")
198208
try testcase.write(to: main_swift, atomically: true, encoding: .utf8)
199209

200-
logger.info("Building test project")
201-
var buildOutput = try await Shell.readStdout(
202-
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName)"
203-
)
204-
XCTAssertTrue(buildOutput.contains("Build complete!"))
205-
logger.info("Test project built successfully")
206-
207-
try await Shell.run("rm -rf \(testPackageDir.appending(".build"))")
210+
// This is a workaround for if Swift 6.0 is used as the host toolchain to run the generator.
211+
// We manually set the swift-tools-version to 5.9 to support building our test cases.
212+
logger.info("Updating minimum swift-tools-version in test project...")
213+
let package_swift = testPackageURL.appendingPathComponent("Package.swift")
214+
let text = try String(contentsOf: package_swift, encoding: .utf8)
215+
var lines = text.components(separatedBy: .newlines)
216+
if lines.count > 0 {
217+
lines[0] = "// swift-tools-version: 5.9"
218+
let result = lines.joined(separator: "\r\n")
219+
try result.write(to: package_swift, atomically: true, encoding: .utf8)
220+
}
208221

209-
logger.info("Building test project with static-swift-stdlib")
210-
buildOutput = try await Shell.readStdout(
211-
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName) --static-swift-stdlib"
212-
)
213-
XCTAssertTrue(buildOutput.contains("Build complete!"))
214-
logger.info("Test project built successfully")
222+
var buildOutput = ""
223+
224+
// If we are testing Linux Swift SDKs, we will run the test cases on a matrix of Docker containers
225+
// that contains each Swift-supported Linux distribution. This way we can validate that each
226+
// distribution is capable of building using the Linux Swift SDK.
227+
if testLinuxSwiftSDKs {
228+
let swiftContainerVersions = ["focal", "jammy", "noble", "fedora39", "rhel-ubi9", "amazonlinux2", "bookworm"]
229+
for containerVersion in swiftContainerVersions {
230+
logger.info("Building test project in 6.0-\(containerVersion) container")
231+
buildOutput = try await Shell.readStdout(
232+
"""
233+
docker run --rm -v \(testPackageDir):/src \
234+
-v $HOME/.swiftpm/swift-sdks:/root/.swiftpm/swift-sdks \
235+
--workdir /src swift:6.0-\(containerVersion) \
236+
/bin/bash -c "swift build --scratch-path /root/.build --experimental-swift-sdk \(bundleName)"
237+
"""
238+
)
239+
XCTAssertTrue(buildOutput.contains("Build complete!"))
240+
logger.info("Test project built successfully")
241+
242+
logger.info("Building test project in 6.0-\(containerVersion) container with static-swift-stdlib")
243+
buildOutput = try await Shell.readStdout(
244+
"""
245+
docker run --rm -v \(testPackageDir):/src \
246+
-v $HOME/.swiftpm/swift-sdks:/root/.swiftpm/swift-sdks \
247+
--workdir /src swift:6.0-\(containerVersion) \
248+
/bin/bash -c "swift build --scratch-path /root/.build --experimental-swift-sdk \(bundleName) --static-swift-stdlib"
249+
"""
250+
)
251+
XCTAssertTrue(buildOutput.contains("Build complete!"))
252+
logger.info("Test project built successfully")
253+
}
254+
} else {
255+
logger.info("Building test project")
256+
buildOutput = try await Shell.readStdout(
257+
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName)"
258+
)
259+
XCTAssertTrue(buildOutput.contains("Build complete!"))
260+
logger.info("Test project built successfully")
261+
262+
try await Shell.run("rm -rf \(testPackageDir.appending(".build"))")
263+
264+
logger.info("Building test project with static-swift-stdlib")
265+
buildOutput = try await Shell.readStdout(
266+
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName) --static-swift-stdlib"
267+
)
268+
XCTAssertTrue(buildOutput.contains("Build complete!"))
269+
logger.info("Test project built successfully")
270+
}
215271
}
216272

217273
func buildTestcases(config: SDKConfiguration) async throws {
@@ -241,6 +297,10 @@ func buildTestcases(config: SDKConfiguration) async throws {
241297
try await buildTestcase(logger, testcase: testcase, bundleName: bundleName, tempDir: tempDir)
242298
}
243299
}
300+
301+
// Cleanup
302+
logger.info("Removing SDK to cleanup...")
303+
try await Shell.run("swift experimental-sdk remove \(bundleName)")
244304
}
245305

246306
final class Swift59_UbuntuEndToEndTests: XCTestCase {

0 commit comments

Comments
 (0)