Skip to content
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

Build Swift SDKs for a Linux host if the --host parameter matches a Linux OS (*-unknown-linux-gnu) #167

Merged
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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,16 @@ Linux distributions officially supported by the Swift project.
| -: | :- | :- |
| macOS (arm64) | ✅ macOS 13.0+ | ❌ |
| macOS (x86_64) | ✅ macOS 13.0+[^1] | ❌ |
| Ubuntu | ⚠️ (WIP) | ✅ 20.04 / 22.04 |
| RHEL | ⚠️ (WIP) | ✅ UBI 9 |

| Ubuntu | ✅ 20.04+ | ✅ 20.04 / 22.04 |
| RHEL | ✅ Fedora 39[^2], UBI 9 | ✅ UBI 9 |
| Amazon Linux 2 | ✅ Supported | ✅ Supported[^3] |
| Debian 12 | ✅ Supported[^2] | ✅ Supported[^2][^3] |

[^1]: Since LLVM project doesn't provide pre-built binaries of `lld` for macOS on x86_64, it will be automatically built
from sources by the generator, which will increase its run by at least 15 minutes on recent hardware. You will also
need CMake and Ninja preinstalled (e.g. via `brew install cmake ninja`).
[^2]: These distributions are only supported by Swift 5.10.1 and later as both host and target platforms.
[^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).

## How to use it

Expand Down
35 changes: 25 additions & 10 deletions Sources/SwiftSDKGenerator/Artifacts/DownloadableArtifacts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,31 @@ struct DownloadableArtifacts: Sendable {
self.versions = versions
self.paths = paths

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 hostTriple.os == .linux {
// Amazon Linux 2 is chosen for its best compatibility with all Swift-supported Linux hosts
let linuxArchSuffix = hostTriple.arch == .aarch64 ? "-\(Triple.Arch.aarch64.linuxConventionName)" : ""
self.hostSwift = .init(
remoteURL: versions.swiftDownloadURL(
subdirectory: "amazonlinux2\(linuxArchSuffix)",
platform: "amazonlinux2\(linuxArchSuffix)",
fileExtension: "tar.gz"
),
localPath: paths.artifactsCachePath
.appending("host_swift_\(versions.swiftVersion)_\(hostTriple.triple).tar.gz"),
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
)
}

self.hostLLVM = .init(
remoteURL: URL(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,13 @@ extension SwiftSDKGenerator {
}

func symlinkClangHeaders() throws {
try self.createSymlink(
at: self.pathsConfiguration.toolchainDirPath.appending("usr/lib/swift_static/clang"),
pointingTo: "../swift/clang"
)
let swiftStaticClangPath = self.pathsConfiguration.toolchainDirPath.appending("usr/lib/swift_static/clang")
if !doesFileExist(at: swiftStaticClangPath) {
logGenerationStep("Symlinking clang headers...")
try self.createSymlink(
at: self.pathsConfiguration.toolchainDirPath.appending("usr/lib/swift_static/clang"),
pointingTo: "../swift/clang"
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Helpers

import struct SystemPackage.FilePath

let unusedDarwinPlatforms = [
let unusedTargetPlatforms = [
"appletvos",
"appletvsimulator",
"embedded",
Expand All @@ -34,11 +34,13 @@ let unusedHostBinaries = [
"swift-format",
"swift-package",
"swift-package-collection",
"lldb*",
]

let unusedHostLibraries = [
"sourcekitd.framework",
"libsourcekitdInProc.so",
"liblldb.so*",
]

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

let excludes =
unusedDarwinPlatforms.map { "--exclude usr/lib/swift/\($0)" } +
unusedDarwinPlatforms.map { "--exclude usr/lib/swift_static/\($0)" } +
unusedTargetPlatforms.map { "--exclude usr/lib/swift/\($0)" } +
unusedTargetPlatforms.map { "--exclude usr/lib/swift_static/\($0)" } +
unusedHostBinaries.map { "--exclude usr/bin/\($0)" } +
unusedHostLibraries.map { "--exclude usr/lib/\($0)" }

try await Shell.run(
#"""
tar -x --to-stdout -f \#(hostSwiftPackagePath) \*.pkg/Payload |
tar -C "\#(pathsConfiguration.toolchainDirPath)" -x \#(excludes.joined(separator: " ")) --include usr
"""#,
shouldLogCommands: isVerbose
)
if hostSwiftPackagePath.string.contains("tar.gz") {
try await Shell.run(
#"""
tar -xzf \#(hostSwiftPackagePath) -C "\#(pathsConfiguration.toolchainDirPath)" -x \#(excludes.joined(separator: " ")) --strip-components=1
"""#,
shouldLogCommands: isVerbose
)
} else {
try await Shell.run(
#"""
tar -x --to-stdout -f \#(hostSwiftPackagePath) \*.pkg/Payload |
tar -C "\#(pathsConfiguration.toolchainDirPath)" -x \#(excludes.joined(separator: " ")) --include usr
"""#,
shouldLogCommands: isVerbose
)
}
}

func removeToolchainComponents(
_ packagePath: FilePath,
platforms: [String] = unusedDarwinPlatforms,
platforms: [String] = unusedTargetPlatforms,
libraries: [String] = unusedHostLibraries,
binaries: [String] = unusedHostBinaries
) async throws {
Expand Down
8 changes: 5 additions & 3 deletions Sources/SwiftSDKGenerator/SwiftSDKRecipes/LinuxRecipe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@ public struct LinuxRecipe: SwiftSDKRecipe {

func itemsToDownload(from artifacts: DownloadableArtifacts) -> [DownloadableArtifacts.Item] {
var items: [DownloadableArtifacts.Item] = []
if self.hostSwiftSource != .preinstalled && !self.versionsConfiguration.swiftVersion.hasPrefix("6.0") {
if self.hostSwiftSource != .preinstalled
&& self.mainHostTriple.os != .linux
&& !self.versionsConfiguration.swiftVersion.hasPrefix("6.0") {
items.append(artifacts.hostLLVM)
}

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

if self.hostSwiftSource != .preinstalled {
if !self.versionsConfiguration.swiftVersion.hasPrefix("6.0") {
if self.mainHostTriple.os != .linux && !self.versionsConfiguration.swiftVersion.hasPrefix("6.0") {
try await generator.prepareLLDLinker(engine, llvmArtifact: downloadableArtifacts.hostLLVM)
}

if self.versionsConfiguration.swiftVersion.hasPrefix("5.9") ||
self.versionsConfiguration.swiftVersion .hasPrefix("5.10") {
self.versionsConfiguration.swiftVersion.hasPrefix("5.10") {
try await generator.symlinkClangHeaders()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public struct WebAssemblyRecipe: SwiftSDKRecipe {
}()
try await generator.removeToolchainComponents(
pathsConfiguration.toolchainDirPath,
platforms: unusedDarwinPlatforms + ["embedded"],
platforms: unusedTargetPlatforms,
libraries: unusedHostLibraries + liblldbNames,
binaries: unusedHostBinaries + ["lldb", "lldb-argdumper", "lldb-server"]
)
Expand Down
90 changes: 75 additions & 15 deletions Tests/SwiftSDKGeneratorTests/EndToEndTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,17 @@ struct SDKConfiguration {
return res
}

var hostArch: String? {
let triple = try? SwiftSDKGenerator.getCurrentTriple(isVerbose: false)
return triple?.arch?.rawValue
}

var sdkGeneratorArguments: String {
return [
"--sdk-name \(bundleName)",
withDocker ? "--with-docker" : nil,
"--swift-version \(swiftVersion)-RELEASE",
testLinuxSwiftSDKs ? "--host \(hostArch!)-unknown-linux-gnu" : nil,
"--target \(architecture)-unknown-linux-gnu",
"--linux-distribution-name \(linuxDistributionName)"
].compactMap{ $0 }.joined(separator: " ")
Expand All @@ -187,31 +193,81 @@ func skipSlow() throws {
)
}

var testLinuxSwiftSDKs: Bool {
ProcessInfo.processInfo.environment.keys.contains("SWIFT_SDK_GENERATOR_TEST_LINUX_SWIFT_SDKS")
}

func buildTestcase(_ logger: Logger, testcase: String, bundleName: String, tempDir: URL) async throws {
let testPackageURL = tempDir.appendingPathComponent("swift-sdk-generator-test")
let testPackageDir = FilePath(testPackageURL.path)
try FileManager.default.createDirectory(atPath: testPackageDir.string, withIntermediateDirectories: true)

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

logger.info("Building test project")
var buildOutput = try await Shell.readStdout(
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName)"
)
XCTAssertTrue(buildOutput.contains("Build complete!"))
logger.info("Test project built successfully")

try await Shell.run("rm -rf \(testPackageDir.appending(".build"))")
// This is a workaround for if Swift 6.0 is used as the host toolchain to run the generator.
// We manually set the swift-tools-version to 5.9 to support building our test cases.
logger.info("Updating minimum swift-tools-version in test project...")
let package_swift = testPackageURL.appendingPathComponent("Package.swift")
let text = try String(contentsOf: package_swift, encoding: .utf8)
var lines = text.components(separatedBy: .newlines)
if lines.count > 0 {
lines[0] = "// swift-tools-version: 5.9"
let result = lines.joined(separator: "\r\n")
try result.write(to: package_swift, atomically: true, encoding: .utf8)
}

logger.info("Building test project with static-swift-stdlib")
buildOutput = try await Shell.readStdout(
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName) --static-swift-stdlib"
)
XCTAssertTrue(buildOutput.contains("Build complete!"))
logger.info("Test project built successfully")
var buildOutput = ""

// If we are testing Linux Swift SDKs, we will run the test cases on a matrix of Docker containers
// that contains each Swift-supported Linux distribution. This way we can validate that each
// distribution is capable of building using the Linux Swift SDK.
if testLinuxSwiftSDKs {
let swiftContainerVersions = ["focal", "jammy", "noble", "fedora39", "rhel-ubi9", "amazonlinux2", "bookworm"]
Copy link
Contributor

@euanh euanh Jan 9, 2025

Choose a reason for hiding this comment

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

Fedora 39 is already EOL but it looks like it's the most recent Fedora-based Swift container image available. 😞

swiftlang/swift-docker#434

Copy link
Contributor Author

@xtremekforever xtremekforever Jan 9, 2025

Choose a reason for hiding this comment

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

Yeah we could always remove it from the list. But, it has to use what's currently available...

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I think we should keep it for now so we have some Fedora coverage. ubi9 is similar but there may be differences which affect us.

for containerVersion in swiftContainerVersions {
logger.info("Building test project in 6.0-\(containerVersion) container")
buildOutput = try await Shell.readStdout(
"""
docker run --rm -v \(testPackageDir):/src \
-v $HOME/.swiftpm/swift-sdks:/root/.swiftpm/swift-sdks \
--workdir /src swift:6.0-\(containerVersion) \
/bin/bash -c "swift build --scratch-path /root/.build --experimental-swift-sdk \(bundleName)"
"""
)
XCTAssertTrue(buildOutput.contains("Build complete!"))
logger.info("Test project built successfully")

logger.info("Building test project in 6.0-\(containerVersion) container with static-swift-stdlib")
buildOutput = try await Shell.readStdout(
"""
docker run --rm -v \(testPackageDir):/src \
-v $HOME/.swiftpm/swift-sdks:/root/.swiftpm/swift-sdks \
--workdir /src swift:6.0-\(containerVersion) \
/bin/bash -c "swift build --scratch-path /root/.build --experimental-swift-sdk \(bundleName) --static-swift-stdlib"
"""
)
XCTAssertTrue(buildOutput.contains("Build complete!"))
logger.info("Test project built successfully")
}
} else {
logger.info("Building test project")
buildOutput = try await Shell.readStdout(
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName)"
)
XCTAssertTrue(buildOutput.contains("Build complete!"))
logger.info("Test project built successfully")

try await Shell.run("rm -rf \(testPackageDir.appending(".build"))")

logger.info("Building test project with static-swift-stdlib")
buildOutput = try await Shell.readStdout(
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName) --static-swift-stdlib"
)
XCTAssertTrue(buildOutput.contains("Build complete!"))
logger.info("Test project built successfully")
}
}

func buildTestcases(config: SDKConfiguration) async throws {
Expand Down Expand Up @@ -241,6 +297,10 @@ func buildTestcases(config: SDKConfiguration) async throws {
try await buildTestcase(logger, testcase: testcase, bundleName: bundleName, tempDir: tempDir)
}
}

// Cleanup
logger.info("Removing SDK to cleanup...")
try await Shell.run("swift experimental-sdk remove \(bundleName)")
}

final class Swift59_UbuntuEndToEndTests: XCTestCase {
Expand Down
Loading