Skip to content

Commit 8d38668

Browse files
Add WebAssembly platform to Swift build system based on Swift SDK (swiftlang#51)
* Add WebAssembly platform to Swift build system based on Swift SDK This change adds a new platform, `webassembly`, to the Swift build system. The platform is based on the existing Swift SDK artifact bundles, which can be installed by SwiftPM. To build your Xcode project for WebAssembly, you need to install a Swift SDK for WebAssembly and Swift OSS toolchain that supports WebAssembly compilation target, then set the `SDKROOT`, `SUPPORTED_PLATFORMS` and `TOOLCHAINS` macros in your Xcode project file like below: ``` SDKROOT = DEVELOPMENT-SNAPSHOT-2025-01-11-a-wasm32-unknown-wasip1-threads SUPPORTED_PLATFORMS = webassembly TOOLCHAINS = org.swift.62202501101a ``` * Move `SwiftSDK` type to shared `SWBCore` module * Remove deployment target concept from WebAssembly platform The deployment target concept is not applicable to WebAssembly, as it does not have platform versions like other platforms. * Add a issue link to the linker flags hack * Gate the rpath trick for Swift-in-the-OS platforms only * Add test case for C/C++-based WASI target * Define Wasm-specific xcspecs and remove several workarounds * Comment about a future direction for generalizing the plugin * Update Package.swift to adopt Swift 6 mode for Wasm platform targets
1 parent 61c1351 commit 8d38668

13 files changed

+793
-1
lines changed

Package.swift

+9
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ let package = Package(
208208
name: "SWBUniversalPlatform",
209209
dependencies: ["SWBCore"],
210210
swiftSettings: swiftSettings(languageMode: .v6)),
211+
.target(
212+
name: "SWBWebAssemblyPlatform",
213+
dependencies: ["SWBCore"],
214+
swiftSettings: swiftSettings(languageMode: .v6)),
211215
.target(
212216
name: "SWBWindowsPlatform",
213217
dependencies: ["SWBCore"],
@@ -256,6 +260,10 @@ let package = Package(
256260
name: "SWBUniversalPlatformTests",
257261
dependencies: ["SWBUniversalPlatform", "SWBTestSupport"],
258262
swiftSettings: swiftSettings(languageMode: .v6)),
263+
.testTarget(
264+
name: "SWBWebAssemblyPlatformTests",
265+
dependencies: ["SWBWebAssemblyPlatform", "SWBTestSupport"],
266+
swiftSettings: swiftSettings(languageMode: .v6)),
259267
.testTarget(
260268
name: "SWBWindowsPlatformTests",
261269
dependencies: ["SWBWindowsPlatform", "SWBTestSupport"],
@@ -371,6 +379,7 @@ let pluginTargetNames = [
371379
"SWBGenericUnixPlatform",
372380
"SWBQNXPlatform",
373381
"SWBUniversalPlatform",
382+
"SWBWebAssemblyPlatform",
374383
"SWBWindowsPlatform",
375384
]
376385

Sources/SWBBuildService/BuildServiceEntryPoint.swift

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ private import SWBApplePlatform
3232
private import SWBGenericUnixPlatform
3333
private import SWBQNXPlatform
3434
private import SWBUniversalPlatform
35+
private import SWBWebAssemblyPlatform
3536
private import SWBWindowsPlatform
3637
#endif
3738

@@ -128,6 +129,7 @@ extension BuildService {
128129
SWBGenericUnixPlatform.initializePlugin(pluginManager)
129130
SWBQNXPlatform.initializePlugin(pluginManager)
130131
SWBUniversalPlatform.initializePlugin(pluginManager)
132+
SWBWebAssemblyPlatform.initializePlugin(pluginManager)
131133
SWBWindowsPlatform.initializePlugin(pluginManager)
132134
#else
133135
// Otherwise, load the normal plugins.

Sources/SWBCore/Specs/Tools/LinkerTools.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,10 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType {
291291
// NOTE: For swift.org toolchains, we always add the search paths to the Swift SDK location as the overlays do not have the install name set. This also works when `SWIFT_USE_DEVELOPMENT_TOOLCHAIN_RUNTIME=YES` as `DYLD_LIBRARY_PATH` is used to override these settings during debug time. If users wish to use the development runtime while not debugging, they need to manually set their rpaths as this is not a supported configuration.
292292
// Also, if the deployment target does not support Swift in the OS, the rpath entries need to be added as well.
293293
// And, if the deployment target does not support Swift Concurrency natively, then the rpath needs to be added as well so that the shim library can find the real implementation. Note that we assume `true` in the case where `supportsSwiftInTheOS` is `nil` as we don't have the platform data to make the correct choice; so fallback to existing behavior.
294+
// The all above discussion is only relevant for platforms that support Swift in the OS.
294295
let supportsSwiftConcurrencyNatively = cbc.producer.platform?.supportsSwiftConcurrencyNatively(cbc.scope, forceNextMajorVersion: false, considerTargetDeviceOSVersion: false) ?? true
295296
let shouldEmitRPathForSwiftConcurrency = UserDefaults.allowRuntimeSearchPathAdditionForSwiftConcurrency && !supportsSwiftConcurrencyNatively
296-
if (cbc.producer.platform?.supportsSwiftInTheOS(cbc.scope, forceNextMajorVersion: true, considerTargetDeviceOSVersion: false) != true || cbc.producer.toolchains.usesSwiftOpenSourceToolchain || shouldEmitRPathForSwiftConcurrency) && isUsingSwift {
297+
if (cbc.producer.platform?.supportsSwiftInTheOS(cbc.scope, forceNextMajorVersion: true, considerTargetDeviceOSVersion: false) != true || cbc.producer.toolchains.usesSwiftOpenSourceToolchain || shouldEmitRPathForSwiftConcurrency) && isUsingSwift && cbc.producer.platform?.minimumOSForSwiftInTheOS != nil {
297298
// NOTE: For swift.org toolchains, this is fine as `DYLD_LIBRARY_PATH` is used to override these settings.
298299
let swiftABIVersion = await (cbc.producer.swiftCompilerSpec.discoveredCommandLineToolSpecInfo(cbc.producer, cbc.scope, delegate) as? DiscoveredSwiftCompilerToolSpecInfo)?.swiftABIVersion
299300
runpathSearchPaths.insert( swiftABIVersion.flatMap { "/usr/lib/swift-\($0)" } ?? "/usr/lib/swift", at: 0)

Sources/SWBCore/SwiftSDK.swift

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
public import SWBUtil
14+
import Foundation
15+
16+
/// Represents a Swift SDK
17+
///
18+
/// See https://github.com/swiftlang/swift-evolution/blob/main/proposals/0387-cross-compilation-destinations.md
19+
public struct SwiftSDK: Sendable {
20+
struct SchemaVersionInfo: Codable {
21+
let schemaVersion: String
22+
}
23+
24+
public struct TripleProperties: Codable, Sendable {
25+
public var sdkRootPath: String
26+
public var swiftResourcesPath: String?
27+
public var swiftStaticResourcesPath: String?
28+
public var includeSearchPaths: [String]?
29+
public var librarySearchPaths: [String]?
30+
public var toolsetPaths: [String]?
31+
}
32+
struct MetadataV4: Codable {
33+
let targetTriples: [String: TripleProperties]
34+
}
35+
36+
struct Toolset: Codable {
37+
struct ToolProperties: Codable {
38+
var path: String?
39+
var extraCLIOptions: [String]
40+
}
41+
42+
var knownTools: [String: ToolProperties] = [:]
43+
var rootPaths: [String] = []
44+
}
45+
46+
/// The identifier of the artifact bundle containing this SDK.
47+
public let identifier: String
48+
/// The version of the artifact bundle containing this SDK.
49+
public let version: String
50+
/// The path to the SDK.
51+
public let path: Path
52+
/// Target-specific properties for this SDK.
53+
public let targetTriples: [String: TripleProperties]
54+
55+
init?(identifier: String, version: String, path: Path, fs: any FSProxy) throws {
56+
self.identifier = identifier
57+
self.version = version
58+
self.path = path
59+
60+
let metadataPath = path.join("swift-sdk.json")
61+
guard fs.exists(metadataPath) else { return nil }
62+
63+
let metadataData = try Data(fs.read(metadataPath))
64+
let schema = try JSONDecoder().decode(SchemaVersionInfo.self, from: metadataData)
65+
guard schema.schemaVersion == "4.0" else { return nil }
66+
67+
let metadata = try JSONDecoder().decode(MetadataV4.self, from: metadataData)
68+
self.targetTriples = metadata.targetTriples
69+
}
70+
71+
/// The default location storing Swift SDKs installed by SwiftPM.
72+
static var defaultSwiftSDKsDirectory: Path {
73+
get throws {
74+
try FileManager.default.url(
75+
for: .libraryDirectory,
76+
in: .userDomainMask,
77+
appropriateFor: nil,
78+
create: false
79+
).appendingPathComponent("org.swift.swiftpm").appendingPathComponent("swift-sdks").filePath
80+
}
81+
}
82+
83+
/// Find Swift SDKs installed by SwiftPM.
84+
public static func findSDKs(targetTriples: [String], fs: any FSProxy) throws -> [SwiftSDK] {
85+
return try findSDKs(swiftSDKsDirectory: defaultSwiftSDKsDirectory, targetTriples: targetTriples, fs: fs)
86+
}
87+
88+
private static func findSDKs(swiftSDKsDirectory: Path, targetTriples: [String], fs: any FSProxy) throws -> [SwiftSDK] {
89+
var sdks: [SwiftSDK] = []
90+
// Find .artifactbundle in the SDK directory (e.g. ~/Library/org.swift.swiftpm/swift-sdks)
91+
for artifactBundle in try fs.listdir(swiftSDKsDirectory) {
92+
guard artifactBundle.hasSuffix(".artifactbundle") else { continue }
93+
let artifactBundlePath = swiftSDKsDirectory.join(artifactBundle)
94+
guard fs.isDirectory(artifactBundlePath) else { continue }
95+
96+
sdks.append(contentsOf: (try? findSDKs(artifactBundle: artifactBundlePath, targetTriples: targetTriples, fs: fs)) ?? [])
97+
}
98+
return sdks
99+
}
100+
101+
private struct BundleInfo: Codable {
102+
let artifacts: [String: Artifact]
103+
104+
struct Artifact: Codable {
105+
let type: String
106+
let version: String
107+
let variants: [Variant]
108+
}
109+
110+
struct Variant: Codable {
111+
let path: String
112+
let supportedTriples: [String]?
113+
}
114+
}
115+
116+
/// Find Swift SDKs in an artifact bundle supporting one of the given targets.
117+
private static func findSDKs(artifactBundle: Path, targetTriples: [String], fs: any FSProxy) throws -> [SwiftSDK] {
118+
// Load info.json from the artifact bundle
119+
let infoPath = artifactBundle.join("info.json")
120+
guard try fs.isFile(infoPath) else { return [] }
121+
122+
let infoData = try Data(fs.read(infoPath))
123+
124+
let schema = try JSONDecoder().decode(SchemaVersionInfo.self, from: infoData)
125+
guard schema.schemaVersion == "1.0" else {
126+
// Ignore unknown artifact bundle format
127+
return []
128+
}
129+
130+
let info = try JSONDecoder().decode(BundleInfo.self, from: infoData)
131+
132+
var sdks: [SwiftSDK] = []
133+
134+
for (identifier, artifact) in info.artifacts {
135+
for variant in artifact.variants {
136+
let sdkPath = artifactBundle.join(variant.path)
137+
guard fs.isDirectory(sdkPath) else { continue }
138+
139+
// FIXME: For now, we only support SDKs that are compatible with any host triple.
140+
guard variant.supportedTriples?.isEmpty ?? true else { continue }
141+
142+
guard let sdk = try SwiftSDK(identifier: identifier, version: artifact.version, path: sdkPath, fs: fs) else { continue }
143+
// Filter out SDKs that don't support any of the target triples.
144+
guard targetTriples.contains(where: { sdk.targetTriples[$0] != nil }) else { continue }
145+
sdks.append(sdk)
146+
}
147+
}
148+
149+
return sdks
150+
}
151+
}

Sources/SWBTestSupport/CoreTestSupport.swift

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ private import SWBApplePlatform
2121
private import SWBGenericUnixPlatform
2222
private import SWBQNXPlatform
2323
private import SWBUniversalPlatform
24+
private import SWBWebAssemblyPlatform
2425
private import SWBWindowsPlatform
2526
#endif
2627

@@ -96,6 +97,9 @@ extension Core {
9697
if !skipLoadingPluginsNamed.contains("com.apple.dt.SWBUniversalPlatformPlugin") {
9798
SWBUniversalPlatform.initializePlugin(pluginManager)
9899
}
100+
if !skipLoadingPluginsNamed.contains("com.apple.dt.SWBWebAssemblyPlatformPlugin") {
101+
SWBWebAssemblyPlatform.initializePlugin(pluginManager)
102+
}
99103
if !skipLoadingPluginsNamed.contains("com.apple.dt.SWBWindowsPlatformPlugin") {
100104
SWBWindowsPlatform.initializePlugin(pluginManager)
101105
}

Sources/SWBTestSupport/RunDestinationTestSupport.swift

+5
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,11 @@ extension _RunDestinationInfo {
268268
package static var qnx: Self {
269269
return .init(platform: "qnx", sdk: "qnx", sdkVariant: "qnx", targetArchitecture: "undefined_arch", supportedArchitectures: ["aarch64", "x86_64"], disableOnlyActiveArch: true)
270270
}
271+
272+
/// A run destination targeting WebAssembly/WASI generic device, using the public SDK.
273+
package static var wasm: Self {
274+
return .init(platform: "webassembly", sdk: "webassembly", sdkVariant: "webassembly", targetArchitecture: "wasm32", supportedArchitectures: ["wasm32"], disableOnlyActiveArch: true)
275+
}
271276
}
272277

273278
extension RunDestinationInfo {

Sources/SWBTestSupport/SkippedTestSupport.swift

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ extension KnownSDK {
7070
package static let linux: Self = "linux"
7171
package static let android: Self = "android"
7272
package static let qnx: Self = "qnx"
73+
package static let wasi: Self = "wasi"
7374
}
7475

7576
package final class ConditionTraitContext: CoreBasedTests, Sendable {
+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
public import SWBUtil
14+
import SWBCore
15+
import SWBMacro
16+
import Foundation
17+
18+
@PluginExtensionSystemActor public func initializePlugin(_ manager: PluginManager) {
19+
manager.register(WebAssemblyPlatformSpecsExtension(), type: SpecificationsExtensionPoint.self)
20+
manager.register(WebAssemblyPlatformExtension(), type: PlatformInfoExtensionPoint.self)
21+
manager.register(WebAssemblySDKRegistryExtension(), type: SDKRegistryExtensionPoint.self)
22+
}
23+
24+
struct WebAssemblyPlatformSpecsExtension: SpecificationsExtension {
25+
func specificationFiles() -> Bundle? {
26+
.module
27+
}
28+
}
29+
30+
struct WebAssemblyPlatformExtension: PlatformInfoExtension {
31+
func additionalPlatforms() -> [(path: Path, data: [String: PropertyListItem])] {
32+
[
33+
(.root, [
34+
"Type": .plString("Platform"),
35+
"Name": .plString("webassembly"),
36+
"Identifier": .plString("webassembly"),
37+
"Description": .plString("webassembly"),
38+
"FamilyName": .plString("WebAssembly"),
39+
"FamilyIdentifier": .plString("webassembly"),
40+
"IsDeploymentPlatform": .plString("YES"),
41+
])
42+
]
43+
}
44+
}
45+
46+
// TODO: We currently hardcode WebAssembly-specific information here but
47+
// ideally we should be able to generalize this to any Swift SDK. Some of
48+
// issues including https://github.com/swiftlang/swift-build/issues/3 prevent
49+
// us from doing this today but we should revisit this later and consider
50+
// renaming this plugin to something like `SWBSwiftSDKPlatform`.
51+
struct WebAssemblySDKRegistryExtension: SDKRegistryExtension {
52+
func additionalSDKs(platformRegistry: PlatformRegistry) async -> [(path: Path, platform: SWBCore.Platform?, data: [String: PropertyListItem])] {
53+
guard let host = try? ProcessInfo.processInfo.hostOperatingSystem() else {
54+
return []
55+
}
56+
57+
guard let wasmPlatform = platformRegistry.lookup(name: "webassembly") else {
58+
return []
59+
}
60+
61+
let defaultProperties: [String: PropertyListItem] = [
62+
"SDK_STAT_CACHE_ENABLE": "NO",
63+
64+
// Workaround to avoid `-add_ast_path` on WebAssembly, apparently this needs to perform some "swift modulewrap" step instead.
65+
"GCC_GENERATE_DEBUGGING_SYMBOLS": .plString("NO"),
66+
67+
"GENERATE_TEXT_BASED_STUBS": "NO",
68+
"GENERATE_INTERMEDIATE_TEXT_BASED_STUBS": "NO",
69+
70+
"CHOWN": "/usr/bin/chown",
71+
72+
"LIBTOOL": .plString(host.imageFormat.executableName(basename: "llvm-lib")),
73+
"AR": .plString(host.imageFormat.executableName(basename: "llvm-ar")),
74+
]
75+
76+
// Map triple to parsed triple components
77+
let supportedTriples: [String: (arch: String, os: String, env: String?)] = [
78+
"wasm32-unknown-wasi": ("wasm32", "wasi", nil),
79+
"wasm32-unknown-wasip1": ("wasm32", "wasip1", nil),
80+
"wasm32-unknown-wasip1-threads": ("wasm32", "wasip1", "threads"),
81+
]
82+
83+
let wasmSwiftSDKs = (try? SwiftSDK.findSDKs(
84+
targetTriples: Array(supportedTriples.keys),
85+
fs: localFS
86+
)) ?? []
87+
88+
var wasmSDKs: [(path: Path, platform: SWBCore.Platform?, data: [String: PropertyListItem])] = []
89+
90+
for wasmSDK in wasmSwiftSDKs {
91+
for (triple, tripleProperties) in wasmSDK.targetTriples {
92+
guard let (arch, os, env) = supportedTriples[triple] else {
93+
continue
94+
}
95+
96+
let wasiSysroot = wasmSDK.path.join(tripleProperties.sdkRootPath)
97+
let swiftResourceDir = wasmSDK.path.join(tripleProperties.swiftResourcesPath)
98+
99+
wasmSDKs.append((wasiSysroot, wasmPlatform, [
100+
"Type": .plString("SDK"),
101+
"Version": .plString("1.0.0"),
102+
"CanonicalName": .plString(wasmSDK.identifier),
103+
"IsBaseSDK": .plBool(true),
104+
"DefaultProperties": .plDict([
105+
"PLATFORM_NAME": .plString("webassembly"),
106+
].merging(defaultProperties, uniquingKeysWith: { _, new in new })),
107+
"CustomProperties": .plDict([
108+
"LLVM_TARGET_TRIPLE_OS_VERSION": .plString(os),
109+
"SWIFT_LIBRARY_PATH": .plString(swiftResourceDir.join("wasi").str),
110+
"SWIFT_RESOURCE_DIR": .plString(swiftResourceDir.str),
111+
// HACK: Ld step does not use swiftc as linker driver but instead uses clang, so we need to add some Swift specific flags
112+
// assuming static linking.
113+
// Tracked in https://github.com/swiftlang/swift-build/issues/3
114+
"OTHER_LDFLAGS": .plArray(["-lc++", "-lc++abi", "-resource-dir", "$(SWIFT_RESOURCE_DIR)/clang", "@$(SWIFT_LIBRARY_PATH)/static-executable-args.lnk"]),
115+
]),
116+
"SupportedTargets": .plDict([
117+
"webassembly": .plDict([
118+
"Archs": .plArray([.plString(arch)]),
119+
"LLVMTargetTripleEnvironment": .plString(env ?? ""),
120+
"LLVMTargetTripleSys": .plString(os),
121+
"LLVMTargetTripleVendor": .plString("unknown"),
122+
])
123+
]),
124+
// TODO: Leave compatible toolchain information in Swift SDKs
125+
// "Toolchains": .plArray([])
126+
]))
127+
}
128+
}
129+
130+
return wasmSDKs
131+
}
132+
}

0 commit comments

Comments
 (0)