diff --git a/Sources/CoreCommands/Options.swift b/Sources/CoreCommands/Options.swift index c892694f383..e74c0248891 100644 --- a/Sources/CoreCommands/Options.swift +++ b/Sources/CoreCommands/Options.swift @@ -483,6 +483,12 @@ public struct BuildOptions: ParsableArguments { ) public var swiftSDKSelector: String? + @Option( + name: .customLong("experimental-swift-sdk-alias"), + help: "Alias for a compatible Swift SDK to build with." + ) + public var swiftSDKAlias: String? + /// Which compile-time sanitizers should be enabled. @Option( name: .customLong("sanitize"), diff --git a/Sources/CoreCommands/SwiftCommandState.swift b/Sources/CoreCommands/SwiftCommandState.swift index 51d483ab980..78360ff532d 100644 --- a/Sources/CoreCommands/SwiftCommandState.swift +++ b/Sources/CoreCommands/SwiftCommandState.swift @@ -945,6 +945,13 @@ public final class SwiftCommandState { outputHandler: { print($0.description) } ) + let swiftSDKSelector = if let swiftSDKAliasString = self.options.build.swiftSDKAlias { + try SwiftToolchainVersion(toolchain: hostToolchain, fileSystem: self.fileSystem) + .idForSwiftSDK(aliasString: swiftSDKAliasString) + } else { + self.options.build.swiftSDKSelector ?? self.options.build.deprecatedSwiftSDKSelector + } + swiftSDK = try SwiftSDK.deriveTargetSwiftSDK( hostSwiftSDK: hostSwiftSDK, hostTriple: hostToolchain.targetTriple, @@ -953,7 +960,7 @@ public final class SwiftCommandState { customCompileTriple: self.options.build.customCompileTriple, customCompileToolchain: self.options.build.customCompileToolchain, customCompileSDK: self.options.build.customCompileSDK, - swiftSDKSelector: self.options.build.swiftSDKSelector ?? self.options.build.deprecatedSwiftSDKSelector, + swiftSDKSelector: swiftSDKSelector, architectures: self.options.build.architectures, store: store, observabilityScope: self.observabilityScope, diff --git a/Sources/PackageModel/CMakeLists.txt b/Sources/PackageModel/CMakeLists.txt index 3d5f2f509d3..fb33b037228 100644 --- a/Sources/PackageModel/CMakeLists.txt +++ b/Sources/PackageModel/CMakeLists.txt @@ -53,9 +53,11 @@ add_library(PackageModel SupportedLanguageExtension.swift SwiftLanguageVersion.swift SwiftSDKs/SwiftSDK.swift + SwiftSDKs/SwiftSDKAlias.swift SwiftSDKs/SwiftSDKConfigurationStore.swift SwiftSDKs/SwiftSDKBundle.swift SwiftSDKs/SwiftSDKBundleStore.swift + SwiftToolchainVersion.swift Toolchain.swift ToolchainConfiguration.swift Toolchain+SupportedFeatures.swift diff --git a/Sources/PackageModel/SwiftSDKs/SwiftSDKAlias.swift b/Sources/PackageModel/SwiftSDKs/SwiftSDKAlias.swift new file mode 100644 index 00000000000..d9026d2f599 --- /dev/null +++ b/Sources/PackageModel/SwiftSDKs/SwiftSDKAlias.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +package struct SwiftSDKAlias { + init?(_ string: String) { + guard let kind = Kind(rawValue: string) else { return nil } + self.kind = kind + } + + enum Kind: String, CaseIterable { + case staticLinux = "static-linux" + case wasi = "wasi" + case wasiEmbedded = "embedded-wasi" + + var urlFileComponent: String { + switch self { + case .staticLinux, .wasi: + return self.rawValue + case .wasiEmbedded: + return Self.wasi.rawValue + } + } + + var idComponent: String { + self.rawValue + } + + var urlDirComponent: String { + switch self { + case .staticLinux: + "static-sdk" + case .wasi, .wasiEmbedded: + "wasi" + } + } + } + + struct Version: CustomStringConvertible { + let rawValue = "0.0.1" + + var description: String { self.rawValue } + } + + let kind: Kind + let defaultVersion = Version() + + var urlFileComponent: String { + "\(self.kind.urlFileComponent)-\(self.defaultVersion.rawValue)" + } +} + +extension SwiftToolchainVersion { + package func urlForSwiftSDK(aliasString: String) throws -> String { + guard let swiftSDKAlias = SwiftSDKAlias(aliasString) else { + throw Error.unknownSwiftSDKAlias(aliasString) + } + + return """ + https://download.swift.org/\( + self.branch + )/\( + swiftSDKAlias.kind.urlDirComponent + )/\( + self.tag + )/\( + self.tag + )_\(swiftSDKAlias.urlFileComponent).artifactbundle.tar.gz + """ + } + + package func idForSwiftSDK(aliasString: String) throws -> String { + guard let swiftSDKAlias = SwiftSDKAlias(aliasString) else { + throw Error.unknownSwiftSDKAlias(aliasString) + } + + switch swiftSDKAlias.kind { + case .staticLinux: + return "\(self.tag)_\(swiftSDKAlias.kind.idComponent)-\(swiftSDKAlias.defaultVersion)" + case .wasi, .wasiEmbedded: + return "\(self.tag.replacing("swift-", with: ""))-wasm32-\(swiftSDKAlias.kind.idComponent)" + } + } +} diff --git a/Sources/PackageModel/SwiftToolchainVersion.swift b/Sources/PackageModel/SwiftToolchainVersion.swift new file mode 100644 index 00000000000..3322b73d4af --- /dev/null +++ b/Sources/PackageModel/SwiftToolchainVersion.swift @@ -0,0 +1,152 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Basics +import class Foundation.JSONDecoder +import struct Foundation.URL + +package struct SwiftToolchainVersion: Equatable, Decodable { + package enum Error: Swift.Error, Equatable, CustomStringConvertible { + case versionMetadataNotFound(AbsolutePath) + case unknownSwiftSDKAlias(String) + + package var description: String { + switch self { + case .versionMetadataNotFound(let absolutePath): + """ + Toolchain version metadata file not found at path `\(absolutePath.pathString)`. \ + Install a newer version of the Swift toolchain that includes this file. + """ + case .unknownSwiftSDKAlias(let string): + """ + Unknown alias for a Swift SDK: `\(string)`. Supported aliases: \( + SwiftSDKAlias.Kind.allCases.map { "`\($0.rawValue)`" }.joined(separator: ", ") + ). + """ + } + } + } + + package init( + tag: String, + branch: String, + architecture: SwiftToolchainVersion.Architecture, + platform: SwiftToolchainVersion.Platform + ) { + self.tag = tag + self.branch = branch + self.architecture = architecture + self.platform = platform + } + + /// Since triples don't encode the platform, we use platform identifiers + /// that match swift.org toolchain distribution names. + package enum Platform: String, Decodable { + case macOS + case ubuntu2004 + case ubuntu2204 + case ubuntu2404 + case debian12 + case amazonLinux2 + case fedora39 + case fedora41 + case ubi9 + + var urlDirComponent: String { + switch self { + case .macOS: + "xcode" + case .ubuntu2004: + "ubuntu2004" + case .ubuntu2204: + "ubuntu2204" + case .ubuntu2404: + "ubuntu2404" + case .debian12: + "debian12" + case .amazonLinux2: + "amazonlinux2" + case .fedora39: + "fedora39" + case .fedora41: + "fedora41" + case .ubi9: + "ubi9" + } + } + + var urlFileComponent: String { + switch self { + case .macOS: + "osx" + case .ubuntu2004: + "ubuntu20.04" + case .ubuntu2204: + "ubuntu22.04" + case .ubuntu2404: + "ubuntu24.04" + case .debian12: + "debian12" + case .amazonLinux2: + "amazonlinux2" + case .fedora39: + "fedora39" + case .fedora41: + "fedora41" + case .ubi9: + "ubi9" + } + } + } + + package enum Architecture: String, Decodable { + case aarch64 + case x86_64 + + var urlFileComponent: String { + switch self { + case .aarch64: + "-aarch64" + case .x86_64: + "" + } + } + } + + /// A Git tag from which this toolchain was built. + package let tag: String + + /// Branch from which this toolchain was built. + package let branch: String + + /// CPU architecture on which this toolchain runs. + package let architecture: Architecture + + /// Platform identifier on which this toolchain runs. + package let platform: Platform + + package init(toolchain: some Toolchain, fileSystem: any FileSystem) throws { + let versionMetadataPath = try toolchain.swiftCompilerPath.parentDirectory.parentDirectory.appending( + RelativePath(validating: "lib/swift/version.json") + ) + guard fileSystem.exists(versionMetadataPath) else { + throw Error.versionMetadataNotFound(versionMetadataPath) + } + + self = try JSONDecoder().decode( + path: versionMetadataPath, + fileSystem: fileSystem, + as: Self.self + ) + } +} + diff --git a/Sources/SwiftSDKCommand/CMakeLists.txt b/Sources/SwiftSDKCommand/CMakeLists.txt index 4dcfa36a2af..62540b8e255 100644 --- a/Sources/SwiftSDKCommand/CMakeLists.txt +++ b/Sources/SwiftSDKCommand/CMakeLists.txt @@ -12,11 +12,11 @@ add_library(SwiftSDKCommand Configuration/ResetConfiguration.swift Configuration/SetConfiguration.swift Configuration/ShowConfiguration.swift - ConfigureSwiftSDK.swift + SwiftSDKConfigure.swift SwiftSDKSubcommand.swift - InstallSwiftSDK.swift - ListSwiftSDKs.swift - RemoveSwiftSDK.swift + SwiftSDKInstall.swift + SwiftSDKList.swift + SwiftSDKRemove.swift SwiftSDKCommand.swift) target_link_libraries(SwiftSDKCommand PUBLIC ArgumentParser diff --git a/Sources/SwiftSDKCommand/SwiftSDKCommand.swift b/Sources/SwiftSDKCommand/SwiftSDKCommand.swift index e167cc548ac..922b5a0f9b3 100644 --- a/Sources/SwiftSDKCommand/SwiftSDKCommand.swift +++ b/Sources/SwiftSDKCommand/SwiftSDKCommand.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2022-2023 Apple Inc. and the Swift project authors +// Copyright (c) 2022-2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See http://swift.org/LICENSE.txt for license information @@ -20,11 +20,11 @@ package struct SwiftSDKCommand: AsyncParsableCommand { abstract: "Perform operations on Swift SDKs.", version: SwiftVersion.current.completeDisplayString, subcommands: [ - ConfigureSwiftSDK.self, + SwiftSDKConfigure.self, DeprecatedSwiftSDKConfigurationCommand.self, - InstallSwiftSDK.self, - ListSwiftSDKs.self, - RemoveSwiftSDK.self, + SwiftSDKInstall.self, + SwiftSDKList.self, + SwiftSDKRemove.self, ], helpNames: [.short, .long, .customLong("help", withSingleDash: true)] ) diff --git a/Sources/SwiftSDKCommand/ConfigureSwiftSDK.swift b/Sources/SwiftSDKCommand/SwiftSDKConfigure.swift similarity index 98% rename from Sources/SwiftSDKCommand/ConfigureSwiftSDK.swift rename to Sources/SwiftSDKCommand/SwiftSDKConfigure.swift index 63503962043..016f9860f83 100644 --- a/Sources/SwiftSDKCommand/ConfigureSwiftSDK.swift +++ b/Sources/SwiftSDKCommand/SwiftSDKConfigure.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Copyright (c) 2023-2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See http://swift.org/LICENSE.txt for license information @@ -18,7 +18,7 @@ import PackageModel import var TSCBasic.stdoutStream -struct ConfigureSwiftSDK: AsyncParsableCommand { +struct SwiftSDKConfigure: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "configure", abstract: """ diff --git a/Sources/SwiftSDKCommand/InstallSwiftSDK.swift b/Sources/SwiftSDKCommand/SwiftSDKInstall.swift similarity index 74% rename from Sources/SwiftSDKCommand/InstallSwiftSDK.swift rename to Sources/SwiftSDKCommand/SwiftSDKInstall.swift index 21fcd8f4beb..654f9d0891e 100644 --- a/Sources/SwiftSDKCommand/InstallSwiftSDK.swift +++ b/Sources/SwiftSDKCommand/SwiftSDKInstall.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Copyright (c) 2023-2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See http://swift.org/LICENSE.txt for license information @@ -20,7 +20,18 @@ import PackageModel import var TSCBasic.stdoutStream import class Workspace.Workspace -struct InstallSwiftSDK: SwiftSDKSubcommand { +struct SwiftSDKInstall: SwiftSDKSubcommand { + enum Error: Swift.Error, CustomStringConvertible { + case swiftSDKNotSpecified + + var description: String { + switch self { + case .swiftSDKNotSpecified: + "Specify either a URL or a local path to a Swift SDK bundle as a positional argument." + } + } + } + static let configuration = CommandConfiguration( commandName: "install", abstract: """ @@ -33,11 +44,15 @@ struct InstallSwiftSDK: SwiftSDKSubcommand { var locations: LocationOptions @Argument(help: "A local filesystem path or a URL of a Swift SDK bundle to install.") - var bundlePathOrURL: String + var bundlePathOrURL: String? @Option(help: "The checksum of the bundle generated with `swift package compute-checksum`.") var checksum: String? = nil + /// Alias of a Swift SDK to install, which automatically resolves installation URL based on host toolchain version. + @Option(help: .hidden) + var experimentalAlias: String? = nil + @Flag( name: .customLong("color-diagnostics"), inversion: .prefixedNo, @@ -73,6 +88,17 @@ struct InstallSwiftSDK: SwiftSDKSubcommand { .throttled(interval: .milliseconds(300)) ) + let bundlePathOrURL = if let experimentalAlias { + try SwiftToolchainVersion( + toolchain: hostToolchain, + fileSystem: self.fileSystem + ).urlForSwiftSDK(aliasString: experimentalAlias) + } else if let bundlePathOrURL { + bundlePathOrURL + } else { + throw Error.swiftSDKNotSpecified + } + try await store.install( bundlePathOrURL: bundlePathOrURL, checksum: self.checksum, diff --git a/Sources/SwiftSDKCommand/ListSwiftSDKs.swift b/Sources/SwiftSDKCommand/SwiftSDKList.swift similarity index 93% rename from Sources/SwiftSDKCommand/ListSwiftSDKs.swift rename to Sources/SwiftSDKCommand/SwiftSDKList.swift index 9e19b471195..b11dc192bd7 100644 --- a/Sources/SwiftSDKCommand/ListSwiftSDKs.swift +++ b/Sources/SwiftSDKCommand/SwiftSDKList.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2014-2022 Apple Inc. and the Swift project authors +// Copyright (c) 2014-2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See http://swift.org/LICENSE.txt for license information @@ -16,7 +16,7 @@ import CoreCommands import PackageModel import SPMBuildCore -package struct ListSwiftSDKs: SwiftSDKSubcommand { +package struct SwiftSDKList: SwiftSDKSubcommand { package static let configuration = CommandConfiguration( commandName: "list", abstract: diff --git a/Sources/SwiftSDKCommand/RemoveSwiftSDK.swift b/Sources/SwiftSDKCommand/SwiftSDKRemove.swift similarity index 97% rename from Sources/SwiftSDKCommand/RemoveSwiftSDK.swift rename to Sources/SwiftSDKCommand/SwiftSDKRemove.swift index 5e8c61530ce..76d98cc83cc 100644 --- a/Sources/SwiftSDKCommand/RemoveSwiftSDK.swift +++ b/Sources/SwiftSDKCommand/SwiftSDKRemove.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Copyright (c) 2023-2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See http://swift.org/LICENSE.txt for license information @@ -16,7 +16,7 @@ import CoreCommands import PackageModel import TSCBasic -package struct RemoveSwiftSDK: SwiftSDKSubcommand { +package struct SwiftSDKRemove: SwiftSDKSubcommand { package static let configuration = CommandConfiguration( commandName: "remove", abstract: """ diff --git a/Tests/PackageModelTests/SwiftToolchainVersionTests.swift b/Tests/PackageModelTests/SwiftToolchainVersionTests.swift new file mode 100644 index 00000000000..c4d7a8e5e3d --- /dev/null +++ b/Tests/PackageModelTests/SwiftToolchainVersionTests.swift @@ -0,0 +1,107 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import _InternalTestSupport +import Testing + +import Foundation +import Basics +import struct TSCBasic.ByteString +import PackageModel + +@Suite +struct SwiftToolchainVersionTests { + let toolchain = MockToolchain() + let versionFilePath: AbsolutePath + let mockFileSystem: InMemoryFileSystem + let version: SwiftToolchainVersion + + init() throws { + self.versionFilePath = self.toolchain.swiftCompilerPath.parentDirectory.parentDirectory.appending( + RelativePath("lib/swift/version.json") + ) + + self.mockFileSystem = InMemoryFileSystem( + files: [self.versionFilePath.pathString: ByteString(encodingAsUTF8: """ + { + "tag": "swift-6.1-RELEASE", + "branch": "swift-6.1-release", + "architecture": "aarch64", + "platform": "ubuntu2004" + } + """)] + ) + + self.version = try SwiftToolchainVersion( + toolchain: self.toolchain, + fileSystem: self.mockFileSystem + ) + } + + @Test + func versionDecoding() throws { + #expect(self.version == SwiftToolchainVersion( + tag: "swift-6.1-RELEASE", + branch: "swift-6.1-release", + architecture: .aarch64, + platform: .ubuntu2004 + )) + } + + @Test + func versionMetadataMissing() { + #expect(throws: SwiftToolchainVersion.Error.versionMetadataNotFound(self.versionFilePath)) { + try SwiftToolchainVersion(toolchain: self.toolchain, fileSystem: InMemoryFileSystem()) + } + } + + @Test + func idForSwiftSDKGeneration() throws { + #expect(throws: SwiftToolchainVersion.Error.unknownSwiftSDKAlias("foo")) { + try self.version.idForSwiftSDK(aliasString: "foo") + } + + var id = try self.version.idForSwiftSDK(aliasString: "wasi") + #expect(id == "6.1-RELEASE-wasm32-wasi") + + id = try self.version.idForSwiftSDK(aliasString: "embedded-wasi") + #expect(id == "6.1-RELEASE-wasm32-embedded-wasi") + + id = try self.version.idForSwiftSDK(aliasString: "static-linux") + #expect(id == "swift-6.1-RELEASE_static-linux-0.0.1") + } + + @Test + func urlForSwiftSDKGeneration() throws { + #expect(throws: SwiftToolchainVersion.Error.unknownSwiftSDKAlias("foo")) { + try self.version.urlForSwiftSDK(aliasString: "foo") + } + + var url = try self.version.urlForSwiftSDK(aliasString: "wasi") + #expect(url == """ + https://download.swift.org/swift-6.1-release/wasi/swift-6.1-RELEASE/swift-6.1-RELEASE_wasi-0.0.1.artifactbundle.tar.gz + """ + ) + + url = try self.version.urlForSwiftSDK(aliasString: "embedded-wasi") + #expect(url == """ + https://download.swift.org/swift-6.1-release/wasi/swift-6.1-RELEASE/swift-6.1-RELEASE_wasi-0.0.1.artifactbundle.tar.gz + """ + ) + + url = try version.urlForSwiftSDK(aliasString: "static-linux") + #expect(url == """ + https://download.swift.org/swift-6.1-release/static-sdk/swift-6.1-RELEASE/swift-6.1-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz + """ + ) + } +}