From 2a70311de755bde31c9eaf032bd3320f01252537 Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Fri, 12 Jun 2026 21:03:59 -0400 Subject: [PATCH 01/17] Support cross-compiling to WebAssembly and Android with Swift SDKs Extends the `swift` module extension with `wasm_sdk` and `android_sdk` tags that download the official Swift SDK artifact bundles published by swift.org (the bundles consumed by `swift sdk install`), pinned by SHA-256, and define toolchains so that plain `swift_library` and `swift_binary` targets build for `wasm32-unknown-wasip1` and `{aarch64,x86_64}-unknown-linux-android` under `--platforms`. Each generated repository pairs one Swift SDK with one standalone host toolchain of exactly the same Swift release (the Swift module format is not stable across compiler versions) and defines: * a `swift_toolchain` that compiles against the SDK's sysroot and Swift resource directory, and statically links the SDK's Swift runtime via new generic `linkopts`/`linker_inputs` attributes; and * a rules_cc `cc_toolchain` driving the matching clang for the target (the host toolchain's clang for WebAssembly, the hermetically fetched Android NDK's clang for Android), following the same shape as the embedded toolchain. The Android NDK is only fetched when an Android target is actually built; WebAssembly-only builds do not download it. Also adds the `swift.no_entry_point_rename` feature: wasm-ld has no `--defsym`, so the WebAssembly toolchain cannot alias a renamed `swift_binary` entry point back to the symbol wasi-libc's startup code expects and instead skips the rename. The new `examples/cross_compilation` package builds a library and binary for all three targets through platform transitions; the resulting WebAssembly binary runs under wasmtime and the Android binaries are correctly formed PIE executables (16 KiB max page size, linked against the NDK's `libc++_shared.so`, which the NDK repository exposes for app packaging). --- MODULE.bazel | 32 +- doc/standalone_toolchain.md | 85 +++ examples/cross_compilation/BUILD.bazel | 65 +++ .../Sources/Binary/main.swift | 3 + .../Sources/Library/Library.swift | 12 + swift/extensions.bzl | 261 +++++++++ swift/internal/extensions/BUILD.bazel | 12 + swift/internal/extensions/BUILD.bazel.tpl | 48 ++ .../extensions/swift_sdk_releases.bzl | 70 +++ swift/internal/extensions/swift_sdks.bzl | 498 ++++++++++++++++++ swift/internal/extensions/toolchains.bzl | 90 +++- swift/internal/feature_names.bzl | 7 + swift/swift_binary.bzl | 30 +- swift/toolchains/swift_toolchain.bzl | 60 +++ 14 files changed, 1254 insertions(+), 19 deletions(-) create mode 100644 examples/cross_compilation/BUILD.bazel create mode 100644 examples/cross_compilation/Sources/Binary/main.swift create mode 100644 examples/cross_compilation/Sources/Library/Library.swift create mode 100644 swift/internal/extensions/swift_sdk_releases.bzl create mode 100644 swift/internal/extensions/swift_sdks.bzl diff --git a/MODULE.bazel b/MODULE.bazel index 30e9a8029..7b37d807d 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -82,7 +82,13 @@ use_repo(system_sdk, "system_sdk") swift = use_extension("//swift:extensions.bzl", "swift", dev_dependency = True) swift.toolchain( name = "swift_toolchain", - swift_version = "6.3", + swift_version = "6.3.2", +) +swift.wasm_sdk( + toolchain_name = "swift_toolchain", +) +swift.android_sdk( + toolchain_name = "swift_toolchain", ) use_repo( swift, @@ -110,6 +116,30 @@ register_toolchains( dev_dependency = True, ) +register_toolchains( + # Swift SDK toolchains for cross-compiling to WebAssembly and Android; + # used by //examples/cross_compilation. + "@swift_toolchain//:cc_toolchain_android_aarch64_ubuntu22.04", + "@swift_toolchain//:cc_toolchain_android_aarch64_ubuntu22.04-aarch64", + "@swift_toolchain//:cc_toolchain_android_aarch64_xcode", + "@swift_toolchain//:cc_toolchain_android_x86_64_ubuntu22.04", + "@swift_toolchain//:cc_toolchain_android_x86_64_ubuntu22.04-aarch64", + "@swift_toolchain//:cc_toolchain_android_x86_64_xcode", + "@swift_toolchain//:cc_toolchain_wasm32_ubuntu22.04", + "@swift_toolchain//:cc_toolchain_wasm32_ubuntu22.04-aarch64", + "@swift_toolchain//:cc_toolchain_wasm32_xcode", + "@swift_toolchain//:swift_toolchain_android_aarch64_ubuntu22.04", + "@swift_toolchain//:swift_toolchain_android_aarch64_ubuntu22.04-aarch64", + "@swift_toolchain//:swift_toolchain_android_aarch64_xcode", + "@swift_toolchain//:swift_toolchain_android_x86_64_ubuntu22.04", + "@swift_toolchain//:swift_toolchain_android_x86_64_ubuntu22.04-aarch64", + "@swift_toolchain//:swift_toolchain_android_x86_64_xcode", + "@swift_toolchain//:swift_toolchain_wasm32_ubuntu22.04", + "@swift_toolchain//:swift_toolchain_wasm32_ubuntu22.04-aarch64", + "@swift_toolchain//:swift_toolchain_wasm32_xcode", + dev_dependency = True, +) + # Dev dependencies bazel_dep(name = "bazel_skylib_gazelle_plugin", version = "1.5.0", dev_dependency = True) bazel_dep(name = "gazelle", version = "0.46.0", dev_dependency = True) diff --git a/doc/standalone_toolchain.md b/doc/standalone_toolchain.md index 7e96e51cf..c28cf118e 100644 --- a/doc/standalone_toolchain.md +++ b/doc/standalone_toolchain.md @@ -146,6 +146,91 @@ bazel run @rules_swift//tools/swift-releases -- list \ main-snapshot-2024-08-01 --platform xcode --platform ubuntu22.04 ``` +## Cross-compiling with Swift SDKs (WebAssembly and Android) + +swift.org publishes "Swift SDK" artifact bundles (the bundles consumed by +`swift sdk install`) that let the host compiler cross-compile for platforms +it cannot target by itself. The `swift` extension can download these and +define matching Swift and C/C++ toolchains, so that plain `swift_library` +and `swift_binary` targets build for those platforms under `--platforms`. + +Add the `wasm_sdk` and/or `android_sdk` tags, referencing the `toolchain` +tag by name (the Swift module format is not stable across compiler +versions, so the SDK is always downloaded for exactly the toolchain's +version): + +```bzl +swift.toolchain( + name = "swift_toolchain", + swift_version = "6.3.2", +) + +swift.wasm_sdk( + toolchain_name = "swift_toolchain", +) + +swift.android_sdk( + toolchain_name = "swift_toolchain", + # api_level = 28, # the default +) + +register_toolchains( + # WebAssembly (wasm32-unknown-wasip1), per host platform you build on. + "@swift_toolchain//:swift_toolchain_wasm32_xcode", + "@swift_toolchain//:cc_toolchain_wasm32_xcode", + # Android, per architecture and host platform. + "@swift_toolchain//:swift_toolchain_android_aarch64_xcode", + "@swift_toolchain//:cc_toolchain_android_aarch64_xcode", + "@swift_toolchain//:swift_toolchain_android_x86_64_xcode", + "@swift_toolchain//:cc_toolchain_android_x86_64_xcode", +) +``` + +Then build with a platform carrying the matching constraints, for example: + +```bzl +platform( + name = "wasm32-wasip1", + constraint_values = [ + "@platforms//cpu:wasm32", + "@platforms//os:wasi", + ], +) + +platform( + name = "android-aarch64", + constraint_values = [ + "@platforms//cpu:aarch64", + "@platforms//os:android", + ], +) +``` + +```sh +bazel build //my:binary --platforms=//:wasm32-wasip1 +``` + +See `examples/cross_compilation` for a complete example, including building +through a platform transition. + +Details worth knowing: + +* The Swift standard library is linked statically from the SDK, matching + the behavior of `swiftc` with these SDKs. WebAssembly binaries are + self-contained `wasm32-wasip1` modules (runnable with `wasmtime` et al.). +* Android binaries link against the NDK's `libc++_shared.so`, which must be + packaged with the application; the NDK repository exposes it as + `@_android_ndk_//:libcxx_shared_`. +* The `android_sdk` tag downloads the Android NDK (for its sysroot and + clang) in addition to the Swift SDK. The NDK is only fetched when an + Android target is actually built; WebAssembly-only builds do not download + it. The NDK version and checksums can be overridden with the + `ndk_version` and `ndk_sha256s` attributes. +* As with toolchains, checksums for the SDK bundles are bundled for a + curated list of releases (see + `swift/internal/extensions/swift_sdk_releases.bzl`); for other releases, + pass `sha256` explicitly. + ## Using the extension from a non-root module The extension is intended for the root module — it fails if a non-root diff --git a/examples/cross_compilation/BUILD.bazel b/examples/cross_compilation/BUILD.bazel new file mode 100644 index 000000000..41cc78205 --- /dev/null +++ b/examples/cross_compilation/BUILD.bazel @@ -0,0 +1,65 @@ +load("//examples/embedded:transition.bzl", "transition_binary") +load("//swift:swift.bzl", "swift_binary", "swift_library") + +package(default_visibility = ["//visibility:public"]) + +# Platforms covered by the Swift SDK toolchains that the `swift` module +# extension's `wasm_sdk` and `android_sdk` tags define (and that the dev +# MODULE.bazel of this repository registers). +platform( + name = "wasm32-wasip1", + constraint_values = [ + "@platforms//cpu:wasm32", + "@platforms//os:wasi", + ], +) + +platform( + name = "android-aarch64", + constraint_values = [ + "@platforms//cpu:aarch64", + "@platforms//os:android", + ], +) + +platform( + name = "android-x86_64", + constraint_values = [ + "@platforms//cpu:x86_64", + "@platforms//os:android", + ], +) + +swift_library( + name = "Library", + srcs = ["Sources/Library/Library.swift"], + module_name = "Library", + # Only built through the platform transitions below. + tags = ["manual"], +) + +swift_binary( + name = "Binary", + srcs = ["Sources/Binary/main.swift"], + # Only built through the platform transitions below. + tags = ["manual"], + deps = [":Library"], +) + +transition_binary( + name = "Binary.wasm32-wasip1", + binary = ":Binary", + platform = ":wasm32-wasip1", +) + +transition_binary( + name = "Binary.android-aarch64", + binary = ":Binary", + platform = ":android-aarch64", +) + +transition_binary( + name = "Binary.android-x86_64", + binary = ":Binary", + platform = ":android-x86_64", +) diff --git a/examples/cross_compilation/Sources/Binary/main.swift b/examples/cross_compilation/Sources/Binary/main.swift new file mode 100644 index 000000000..0b24404c7 --- /dev/null +++ b/examples/cross_compilation/Sources/Binary/main.swift @@ -0,0 +1,3 @@ +import Library + +print(Greeter(subject: "world").greeting()) diff --git a/examples/cross_compilation/Sources/Library/Library.swift b/examples/cross_compilation/Sources/Library/Library.swift new file mode 100644 index 000000000..33eaed0d4 --- /dev/null +++ b/examples/cross_compilation/Sources/Library/Library.swift @@ -0,0 +1,12 @@ +/// A trivial library used to validate cross-compilation with a Swift SDK. +public struct Greeter { + private let subject: String + + public init(subject: String) { + self.subject = subject + } + + public func greeting() -> String { + return "Hello, \(subject)!" + } +} diff --git a/swift/extensions.bzl b/swift/extensions.bzl index be6bbc0c1..a8540ca3f 100644 --- a/swift/extensions.bzl +++ b/swift/extensions.bzl @@ -15,13 +15,31 @@ """Definitions for bzlmod module extensions.""" load("@bazel_features//:features.bzl", "bazel_features") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") load("//swift/internal:repositories.bzl", "swift_rules_dependencies") load("//swift/internal/extensions:standalone_toolchain.bzl", _standalone_toolchain = "standalone_toolchain") load("//swift/internal/extensions:swift_releases.bzl", "SWIFT_RELEASES") +load( + "//swift/internal/extensions:swift_sdk_releases.bzl", + "ANDROID_NDK_RELEASES", + "DEFAULT_ANDROID_NDK_VERSION", + "SWIFT_SDK_RELEASES", + "android_ndk_download_url", + "swift_sdk_download_url", +) +load( + "//swift/internal/extensions:swift_sdks.bzl", + "ANDROID_ARCHS", + "ANDROID_NDK_BUILD_FILE_CONTENT", + "swift_android_sdk_repository", + "swift_wasm_sdk_repository", +) load( "//swift/internal/extensions:toolchains.bzl", + _android_sdk_toolchains_for_platform = "android_sdk_toolchains_for_platform", _toolchains_for_platform = "toolchains_for_platform", _toolchains_repository = "toolchains_repository", + _wasm_sdk_toolchains_for_platform = "wasm_sdk_toolchains_for_platform", ) load("//tools/explicit_modules:extensions.bzl", _system_sdk = "system_sdk") @@ -40,6 +58,124 @@ def _non_module_deps_impl(module_ctx): non_module_deps = module_extension(implementation = _non_module_deps_impl) +def _ndk_host_os(platform): + """Returns the Android NDK host OS for a host toolchain platform name.""" + return "darwin" if platform == "xcode" else "linux" + +def _setup_wasm_sdk(*, tag, toolchain_name, swift_version, platforms): + """Creates the repositories for a `swift.wasm_sdk` tag. + + Args: + tag: The `wasm_sdk` tag. + toolchain_name: The name of the `swift.toolchain` tag the SDK extends. + swift_version: The Swift release version of that toolchain. + platforms: The host platforms the toolchain was created for. + + Returns: + BUILD file content with the `toolchain` declarations to add to the + toolchains hub repository. + """ + sha256 = tag.sha256 + if not sha256: + if swift_version not in SWIFT_SDK_RELEASES: + fail("No known WebAssembly Swift SDK for version `{}`. Please choose one of {}, or provide the SDK's sha256.".format( + swift_version, + SWIFT_SDK_RELEASES.keys(), + )) + sha256 = SWIFT_SDK_RELEASES[swift_version]["wasm"] + + build_file_content = "" + for platform in platforms: + repository_name = "{}_wasm_sdk_{}".format(toolchain_name, platform) + swift_wasm_sdk_repository( + name = repository_name, + sha256 = sha256, + swift_version = swift_version, + toolchain_repo = "{}_{}".format(toolchain_name, platform), + url = swift_sdk_download_url(swift_version, "wasm"), + ) + build_file_content += _wasm_sdk_toolchains_for_platform( + platform = platform, + sdk_repository = repository_name, + ) + return build_file_content + +def _setup_android_sdk(*, tag, toolchain_name, swift_version, platforms): + """Creates the repositories for a `swift.android_sdk` tag. + + Args: + tag: The `android_sdk` tag. + toolchain_name: The name of the `swift.toolchain` tag the SDK extends. + swift_version: The Swift release version of that toolchain. + platforms: The host platforms the toolchain was created for. + + Returns: + BUILD file content with the `toolchain` declarations to add to the + toolchains hub repository. + """ + sha256 = tag.sha256 + if not sha256: + if swift_version not in SWIFT_SDK_RELEASES: + fail("No known Android Swift SDK for version `{}`. Please choose one of {}, or provide the SDK's sha256.".format( + swift_version, + SWIFT_SDK_RELEASES.keys(), + )) + sha256 = SWIFT_SDK_RELEASES[swift_version]["android"] + + ndk_version = tag.ndk_version or DEFAULT_ANDROID_NDK_VERSION + ndk_sha256s = tag.ndk_sha256s + if not ndk_sha256s: + if ndk_version not in ANDROID_NDK_RELEASES: + fail("No known Android NDK release `{}`. Please choose one of {}, or provide the NDK's sha256s.".format( + ndk_version, + ANDROID_NDK_RELEASES.keys(), + )) + ndk_sha256s = ANDROID_NDK_RELEASES[ndk_version] + + host_oses = {_ndk_host_os(platform): None for platform in platforms} + for host_os in host_oses: + http_archive( + name = "{}_android_ndk_{}".format(toolchain_name, host_os), + build_file_content = ANDROID_NDK_BUILD_FILE_CONTENT, + sha256 = ndk_sha256s.get(host_os, ""), + strip_prefix = "android-ndk-" + ndk_version, + url = android_ndk_download_url(ndk_version, host_os), + ) + + build_file_content = "" + for platform in platforms: + ndk_repo = "{}_android_ndk_{}".format(toolchain_name, _ndk_host_os(platform)) + repository_name = "{}_android_sdk_{}".format(toolchain_name, platform) + swift_android_sdk_repository( + name = repository_name, + api_level = tag.api_level, + host_swiftc = "@{}_{}//:usr/bin/swiftc".format(toolchain_name, platform), + ndk_repo = ndk_repo, + ndk_source_properties = "@{}//:source.properties".format(ndk_repo), + sha256 = sha256, + swift_version = swift_version, + toolchain_repo = "{}_{}".format(toolchain_name, platform), + url = swift_sdk_download_url(swift_version, "android"), + ) + build_file_content += _android_sdk_toolchains_for_platform( + platform = platform, + sdk_repository = repository_name, + archs = ANDROID_ARCHS, + ) + return build_file_content + +def _sdk_tags_by_toolchain_name(tags, kind): + """Groups SDK tags by the toolchain they extend, rejecting duplicates.""" + tags_by_name = {} + for tag in tags: + if tag.toolchain_name in tags_by_name: + fail("Only one `{}` tag may be used per toolchain, got multiple for `{}`.".format( + kind, + tag.toolchain_name, + )) + tags_by_name[tag.toolchain_name] = tag + return tags_by_name + def _standalone_toolchain_impl(module_ctx): root_module = None for mod in module_ctx.modules: @@ -50,6 +186,31 @@ def _standalone_toolchain_impl(module_ctx): if not root_module: fail("Could not find a root module. This should never happen.") + wasm_sdk_tags = _sdk_tags_by_toolchain_name( + root_module.tags.wasm_sdk, + "wasm_sdk", + ) + android_sdk_tags = _sdk_tags_by_toolchain_name( + root_module.tags.android_sdk, + "android_sdk", + ) + + toolchain_names = [ + toolchain.name + for toolchain in root_module.tags.toolchain + ] + for kind, tags in ( + ("wasm_sdk", wasm_sdk_tags), + ("android_sdk", android_sdk_tags), + ): + for toolchain_name in tags: + if toolchain_name not in toolchain_names: + fail("The `{}` tag references unknown toolchain `{}`. Please use the name of a `toolchain` tag: {}".format( + kind, + toolchain_name, + toolchain_names, + )) + toolchains_build_file_content = "" for toolchain in root_module.tags.toolchain: if toolchain.swift_version and toolchain.swift_version_file: @@ -81,6 +242,23 @@ def _standalone_toolchain_impl(module_ctx): platform = platform, toolchain_repository = repository_name, ) + + platforms = [platform for platform, _ in swift_releases] + if toolchain.name in wasm_sdk_tags: + toolchains_build_file_content += _setup_wasm_sdk( + tag = wasm_sdk_tags[toolchain.name], + toolchain_name = toolchain.name, + swift_version = swift_version, + platforms = platforms, + ) + if toolchain.name in android_sdk_tags: + toolchains_build_file_content += _setup_android_sdk( + tag = android_sdk_tags[toolchain.name], + toolchain_name = toolchain.name, + swift_version = swift_version, + platforms = platforms, + ) + _toolchains_repository( name = toolchain.name, build_file_content = toolchains_build_file_content, @@ -94,6 +272,87 @@ def _standalone_toolchain_impl(module_ctx): **metadata_kwargs ) +_wasm_sdk = tag_class( + attrs = { + "sha256": attr.string( + doc = """\ +The expected SHA-256 of the SDK artifact bundle. May be omitted for Swift +versions known to this version of rules_swift. +""", + ), + "toolchain_name": attr.string( + doc = "The name of the `toolchain` tag to add this Swift SDK to.", + mandatory = True, + ), + }, + doc = """\ +Downloads the WebAssembly Swift SDK matching a `toolchain` tag's Swift version +and defines Swift and C++ toolchains targeting `wasm32-unknown-wasip1`. + +Register the generated toolchains for the host platforms you build on, e.g.: + +```starlark +register_toolchains( + "@swift_toolchain//:swift_toolchain_wasm32_xcode", + "@swift_toolchain//:cc_toolchain_wasm32_xcode", +) +``` + +and build with a platform that has the `@platforms//os:wasi` and +`@platforms//cpu:wasm32` constraints. +""", +) + +_android_sdk = tag_class( + attrs = { + "api_level": attr.int( + default = 28, + doc = "The Android API level to target.", + ), + "ndk_sha256s": attr.string_dict( + doc = """\ +A dictionary of NDK host OS ("darwin", "linux") to the expected SHA-256 of the +NDK archive. May be omitted for NDK versions known to this version of +rules_swift. +""", + ), + "ndk_version": attr.string( + doc = """\ +The Android NDK release (e.g. "r27c") whose sysroot and clang are used. The +Android Swift SDK requires r27 or later. Defaults to a version known to work +with the supported Swift releases. +""", + ), + "sha256": attr.string( + doc = """\ +The expected SHA-256 of the SDK artifact bundle. May be omitted for Swift +versions known to this version of rules_swift. +""", + ), + "toolchain_name": attr.string( + doc = "The name of the `toolchain` tag to add this Swift SDK to.", + mandatory = True, + ), + }, + doc = """\ +Downloads the Android Swift SDK matching a `toolchain` tag's Swift version +(along with the Android NDK) and defines Swift and C++ toolchains targeting +`aarch64-unknown-linux-android` and `x86_64-unknown-linux-android`. + +Register the generated toolchains for the host platforms you build on, e.g.: + +```starlark +register_toolchains( + "@swift_toolchain//:swift_toolchain_android_aarch64_xcode", + "@swift_toolchain//:cc_toolchain_android_aarch64_xcode", +) +``` + +and build with a platform that has the `@platforms//os:android` and +`@platforms//cpu:aarch64` (or `x86_64`) constraints. +""", +) + _toolchain = tag_class(attrs = { "name": attr.string( doc = "Repository name of the generated toolchain", @@ -114,6 +373,8 @@ their hashes. For instance: swift = module_extension( implementation = _standalone_toolchain_impl, tag_classes = { + "android_sdk": _android_sdk, "toolchain": _toolchain, + "wasm_sdk": _wasm_sdk, }, ) diff --git a/swift/internal/extensions/BUILD.bazel b/swift/internal/extensions/BUILD.bazel index bbe55c646..87c28d3ab 100644 --- a/swift/internal/extensions/BUILD.bazel +++ b/swift/internal/extensions/BUILD.bazel @@ -18,6 +18,18 @@ bzl_library( visibility = ["//swift:__subpackages__"], ) +bzl_library( + name = "swift_sdk_releases", + srcs = ["swift_sdk_releases.bzl"], + visibility = ["//swift:__subpackages__"], +) + +bzl_library( + name = "swift_sdks", + srcs = ["swift_sdks.bzl"], + visibility = ["//swift:__subpackages__"], +) + bzl_library( name = "toolchains", srcs = ["toolchains.bzl"], diff --git a/swift/internal/extensions/BUILD.bazel.tpl b/swift/internal/extensions/BUILD.bazel.tpl index 231ecbcb9..f0e9c8192 100644 --- a/swift/internal/extensions/BUILD.bazel.tpl +++ b/swift/internal/extensions/BUILD.bazel.tpl @@ -13,6 +13,54 @@ exports_files([ "usr/bin/llvm-objcopy", ]) +### Tools referenced by Swift SDK cross-compilation repositories. ### +# See swift/internal/extensions/swift_sdks.bzl. +exports_files([ + "usr/bin/clang", + "usr/bin/llvm-ar", + "usr/bin/swift-autolink-extract", + "usr/bin/swift-symbolgraph-extract", + "usr/bin/swiftc", +]) + +# The subset of the toolchain that Swift compile actions for a Swift SDK +# target need: the driver/frontend and their libraries, plus clang's builtin +# headers for the clang importer. The Swift standard library for the target +# comes from the SDK, not from here. +filegroup( + name = "swift_sdk_compiler_inputs", + srcs = glob( + [ + "usr/bin/swift*", + "usr/lib/clang/**", + "usr/lib/lib*.dylib", + "usr/lib/lib*.so*", + "usr/lib/swift/host/**", + "usr/lib/swift/linux/**", + "usr/lib/swift/macosx/**", + ], + allow_empty = True, + ), +) + +# The subset of the toolchain that link actions driven by this toolchain's +# clang need. +filegroup( + name = "swift_sdk_linker_inputs", + srcs = glob( + [ + "usr/bin/clang*", + "usr/bin/ld.lld", + "usr/bin/ld64.lld", + "usr/bin/lld", + "usr/bin/llvm-ar", + "usr/bin/wasm-ld", + "usr/lib/clang/**", + ], + allow_empty = True, + ), +) + filegroup( name = "files", srcs = glob( diff --git a/swift/internal/extensions/swift_sdk_releases.bzl b/swift/internal/extensions/swift_sdk_releases.bzl new file mode 100644 index 000000000..dfd87ee59 --- /dev/null +++ b/swift/internal/extensions/swift_sdk_releases.bzl @@ -0,0 +1,70 @@ +"""Swift SDK and Android NDK release version mappings. + +This module defines checksums for the artifacts needed to cross-compile Swift +for platforms that are not covered by a host toolchain, using the official +"Swift SDK" artifact bundles published by swift.org (the bundles installed by +`swift sdk install`). + +The Swift module format is not stable across compiler versions, so a Swift SDK +can only be used with the host toolchain from exactly the same release; the +keys of `SWIFT_SDK_RELEASES` therefore mirror the keys of `SWIFT_RELEASES` in +`swift_releases.bzl`. Checksums are published in +https://www.swift.org/api/v1/install/releases.json. +""" + +SWIFT_SDK_RELEASES = { + "6.3.2": { + "android": "939e933549d12d28f2e0bf71019d734d309859e9773c572657ce565a81f85d68", + "wasm": "a61f0584c93283589f8b2f42db05c1f9a182b506c2957271402992655591dd7c", + }, +} + +# The Android Swift SDK bundle ships without an NDK sysroot (its +# setup-android-sdk.sh script normally symlinks one in from a local NDK +# install), so the NDK is fetched hermetically as well. Checksums are for the +# zips at https://dl.google.com/android/repository/android-ndk-{version}-{os}.zip +DEFAULT_ANDROID_NDK_VERSION = "r27c" + +ANDROID_NDK_RELEASES = { + "r27c": { + "darwin": "8c5685457c58a88527367d46d3f14e8c727d962c39f85344cff0c0768a73c3b7", + "linux": "59c2f6dc96743b5daf5d1626684640b20a6bd2b1d85b13156b90333741bad5cc", + }, +} + +def swift_sdk_download_url(swift_version, sdk): + """Returns the download URL for a Swift SDK artifact bundle. + + Args: + swift_version: The Swift release version (e.g. "6.3.2"). + sdk: The SDK kind; one of "wasm" or "android". + + Returns: + The URL of the `.artifactbundle.tar.gz` for the given release. + """ + if "-snapshot-" in swift_version: + fail("Swift SDKs are only supported for release versions, got `{}`".format( + swift_version, + )) + return ( + "https://download.swift.org/swift-{version}-release/{sdk}-sdk/" + + "swift-{version}-RELEASE/swift-{version}-RELEASE_{sdk}.artifactbundle.tar.gz" + ).format( + sdk = sdk, + version = swift_version, + ) + +def android_ndk_download_url(ndk_version, host_os): + """Returns the download URL for an Android NDK release. + + Args: + ndk_version: The NDK release name (e.g. "r27c"). + host_os: The host OS the NDK runs on; one of "darwin" or "linux". + + Returns: + The URL of the NDK zip for the given release and host. + """ + return "https://dl.google.com/android/repository/android-ndk-{version}-{host_os}.zip".format( + host_os = host_os, + version = ndk_version, + ) diff --git a/swift/internal/extensions/swift_sdks.bzl b/swift/internal/extensions/swift_sdks.bzl new file mode 100644 index 000000000..fb7796bcc --- /dev/null +++ b/swift/internal/extensions/swift_sdks.bzl @@ -0,0 +1,498 @@ +"""Repository rules for downloading and configuring Swift SDKs. + +A "Swift SDK" is the artifact bundle published by swift.org for +cross-compiling Swift to platforms that the host toolchain cannot target by +itself (currently WebAssembly and Android); they are the bundles that +`swift sdk install` consumes. + +Each repository created by these rules pairs one Swift SDK with one standalone +host toolchain repository (created by `standalone_toolchain`) and defines: + + * a `swift_toolchain` that compiles against the SDK's sysroot and Swift + resource directory, and links against its static Swift runtime; and + * a rules_cc `cc_toolchain` that drives the matching clang for the target + (the host toolchain's clang for WebAssembly, the Android NDK's clang for + Android), which `swift_binary`/`cc_*` rules use to link. + +The `toolchain` declarations that register these for a given target platform +are generated into the toolchains hub repository; see `toolchains.bzl`. + +Because the Swift module format is not stable across compiler versions, a +Swift SDK must come from exactly the same release as the host toolchain it is +paired with; the `swift` module extension enforces this by deriving both from +the same `swift.toolchain` tag. +""" + +# BUILD file written into the Android NDK repository fetched alongside the +# Android Swift SDK. The prebuilt directory name varies by host +# ("darwin-x86_64", "linux-x86_64"), hence the wildcards. +ANDROID_NDK_BUILD_FILE_CONTENT = """\ +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "clang", + srcs = glob(["toolchains/llvm/prebuilt/*/bin/clang"]), +) + +filegroup( + name = "llvm_ar", + srcs = glob(["toolchains/llvm/prebuilt/*/bin/llvm-ar"]), +) + +filegroup( + name = "toolchain_files", + srcs = glob([ + "toolchains/llvm/prebuilt/*/bin/*", + "toolchains/llvm/prebuilt/*/lib/**", + "toolchains/llvm/prebuilt/*/sysroot/**", + ]), +) + +# The shared C++ runtime that must be packaged into any Android application +# that contains Swift code. +filegroup( + name = "libcxx_shared_aarch64", + srcs = glob(["toolchains/llvm/prebuilt/*/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so"]), +) + +filegroup( + name = "libcxx_shared_x86_64", + srcs = glob(["toolchains/llvm/prebuilt/*/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so"]), +) +""" + +# Files in the host toolchain that compile actions need: the driver/frontend +# binaries, their libraries, and clang's builtin headers (used by the clang +# importer when the Swift SDK's resource directory does not bundle them). +_HOST_COMPILER_INPUTS = "swift_sdk_compiler_inputs" + +# Files in the host toolchain that link actions driven by its clang need. +_HOST_LINKER_INPUTS = "swift_sdk_linker_inputs" + +_CC_TOOLCHAIN_TEMPLATE = """ +cc_tool( + name = "clang", + src = "{clang}", + data = {clang_data}, + tags = ["manual"], +) + +cc_tool( + name = "ar", + src = "{ar}", + tags = ["manual"], +) + +cc_tool_map( + name = "cc_tools", + tags = ["manual"], + tools = {{ + "@rules_cc//cc/toolchains/actions:ar_actions": ":ar", + "@rules_cc//cc/toolchains/actions:assembly_actions": ":clang", + "@rules_cc//cc/toolchains/actions:c_compile": ":clang", + "@rules_cc//cc/toolchains/actions:cpp_compile_actions": ":clang", + "@rules_cc//cc/toolchains/actions:link_actions": ":clang", + }}, +) +""" + +_CC_TOOLCHAIN_FOR_TARGET_TEMPLATE = """ +cc_args( + name = "cc_args_{suffix}", + actions = [ + "@rules_cc//cc/toolchains/actions:compile_actions", + "@rules_cc//cc/toolchains/actions:link_actions", + ], + args = {args}, +) + +cc_args( + name = "cc_link_args_{suffix}", + actions = [ + "@rules_cc//cc/toolchains/actions:link_actions", + ], + args = {link_args}, +) + +cc_make_variable( + name = "cc_target_triple_{suffix}", + value = "{triple}", + variable_name = "CC_TARGET_TRIPLE", +) + +cc_toolchain( + name = "cc_toolchain_{suffix}", + args = [ + ":cc_args_{suffix}", + ":cc_link_args_{suffix}", + ], + compiler = "clang", + enabled_features = [ + "@rules_cc//cc/toolchains/args/archiver_flags:feature", + "@rules_cc//cc/toolchains/args/libraries_to_link:feature", + "@rules_cc//cc/toolchains/args/link_flags:feature", + ], + make_variables = [ + ":cc_target_triple_{suffix}", + ], + tool_map = ":cc_tools", +) +""" + +_SWIFT_TOOLCHAIN_TEMPLATE = """ +swift_toolchain( + name = "swift_toolchain_{suffix}", + arch = "{arch}", + copts = {copts}, + features = {features}, + linker_inputs = {linker_inputs}, + linkopts = {linkopts}, + os = "{os}", + parsed_version = "{swift_version}", + sdkroot = "{sdkroot}", + swift_tools = ":tools", + version_file = ".swift-version", +) +""" + +_BUILD_HEADER_TEMPLATE = """\ +load("@rules_cc//cc/toolchains:args.bzl", "cc_args") +load("@rules_cc//cc/toolchains:make_variable.bzl", "cc_make_variable") +load("@rules_cc//cc/toolchains:tool.bzl", "cc_tool") +load("@rules_cc//cc/toolchains:tool_map.bzl", "cc_tool_map") +load("@rules_cc//cc/toolchains:toolchain.bzl", "cc_toolchain") +load("@rules_swift//swift/toolchains:swift_toolchain.bzl", "swift_toolchain") +load("@rules_swift//swift/toolchains:swift_tools.bzl", "swift_tools") + +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "sdk_files", + srcs = glob(["{bundle_dir}/**"]), +) + +swift_tools( + name = "tools", + swift_driver = "@{toolchain_repo}//:usr/bin/swiftc", + swift_autolink_extract = "@{toolchain_repo}//:usr/bin/swift-autolink-extract", + swift_symbolgraph_extract = "@{toolchain_repo}//:usr/bin/swift-symbolgraph-extract", + additional_inputs = {compiler_inputs}, +) +""" + +def _execroot_relative_path(path): + """Returns the execution-root-relative path for an external repository path. + + Args: + path: An absolute `path` (or string) below the output base's + `external` directory. + + Returns: + The same path expressed relative to the execution root, suitable for + baking into command line flags. + """ + path_str = str(path) + if "/external/" not in path_str: + fail("Expected a path inside an external repository, got: " + path_str) + return "external/" + path_str.rsplit("/external/", 1)[1] + +def _build_list(items, indent = " "): + """Formats a list of strings as a multi-line BUILD file list literal.""" + if not items: + return "[]" + lines = ["["] + for item in items: + lines.append("{} \"{}\",".format(indent, item)) + lines.append(indent + "]") + return "\n".join(lines) + +def _download_sdk_bundle(repository_ctx): + """Downloads and extracts the Swift SDK artifact bundle for a repository. + + Returns: + The name of the top-level `.artifactbundle` directory. + """ + repository_ctx.download_and_extract( + url = repository_ctx.attr.url, + sha256 = repository_ctx.attr.sha256, + ) + repository_ctx.file(".swift-version", repository_ctx.attr.swift_version) + + bundles = [ + entry.basename + for entry in repository_ctx.path(".").readdir() + if entry.basename.endswith(".artifactbundle") + ] + if len(bundles) != 1: + fail(("Expected the archive at {} to contain exactly one " + + ".artifactbundle directory, found: {}").format( + repository_ctx.attr.url, + bundles, + )) + return bundles[0] + +def _common_attrs(): + return { + "sha256": attr.string( + doc = "The expected SHA-256 of the SDK artifact bundle.", + mandatory = True, + ), + "swift_version": attr.string( + doc = "The Swift release version the SDK belongs to.", + mandatory = True, + ), + "toolchain_repo": attr.string( + doc = """\ +Name of the `standalone_toolchain` repository providing the host tools that +this SDK is paired with. +""", + mandatory = True, + ), + "url": attr.string( + doc = "The download URL of the SDK artifact bundle.", + mandatory = True, + ), + } + +def _swift_wasm_sdk_impl(repository_ctx): + bundle_dir = _download_sdk_bundle(repository_ctx) + toolchain_repo = repository_ctx.attr.toolchain_repo + + repo_root = "external/" + repository_ctx.name + sdk_dir = "{}/{}/{}".format( + repo_root, + bundle_dir, + "{0}/wasm32-unknown-wasip1".format(bundle_dir.removesuffix(".artifactbundle")), + ) + if not repository_ctx.path(sdk_dir.removeprefix(repo_root + "/")).exists: + fail("The WebAssembly Swift SDK bundle has an unexpected layout; " + + "missing " + sdk_dir) + wasi_sdk = sdk_dir + "/WASI.sdk" + resource_dir = sdk_dir + "/swift.xctoolchain/usr/lib/swift_static" + + build_content = _BUILD_HEADER_TEMPLATE.format( + bundle_dir = bundle_dir, + compiler_inputs = _build_list([ + ":sdk_files", + "@{}//:{}".format(toolchain_repo, _HOST_COMPILER_INPUTS), + ]), + toolchain_repo = toolchain_repo, + ) + + build_content += _SWIFT_TOOLCHAIN_TEMPLATE.format( + arch = "wasm32", + copts = _build_list([ + "-resource-dir", + resource_dir, + ]), + features = _build_list([ + "swift.module_map_no_private_headers", + "swift.no_embed_debug_module", + # wasm-ld cannot alias a renamed entry point back to the symbol + # that wasi-libc's startup code expects. + "swift.no_entry_point_rename", + "swift.use_autolink_extract", + # The file prefix map would make the worker resolve the Xcode + # developer directory on macOS hosts, which this toolchain does + # not depend on. + "-swift.file_prefix_map", + ]), + linker_inputs = _build_list([":sdk_files"]), + # The runtime objects and libraries that `swiftc` would add when + # linking a static executable for WASI; see + # `swift_static/wasi/static-executable-args.lnk` in the SDK. + linkopts = _build_list([ + "{}/wasi/wasm32/swiftrt.o".format(resource_dir), + "-L{}/wasi".format(resource_dir), + "-lc++", + "-lc++abi", + "-lswiftSwiftOnoneSupport", + "-ldl", + "-lm", + "-lwasi-emulated-mman", + "-lwasi-emulated-signal", + "-lwasi-emulated-process-clocks", + ]), + os = "wasi", + sdkroot = wasi_sdk, + suffix = "wasm32", + swift_version = repository_ctx.attr.swift_version, + ) + + build_content += _CC_TOOLCHAIN_TEMPLATE.format( + ar = "@{}//:usr/bin/llvm-ar".format(toolchain_repo), + clang = "@{}//:usr/bin/clang".format(toolchain_repo), + clang_data = _build_list([ + ":sdk_files", + "@{}//:{}".format(toolchain_repo, _HOST_LINKER_INPUTS), + ]), + ) + + build_content += _CC_TOOLCHAIN_FOR_TARGET_TEMPLATE.format( + args = _build_list([ + "--target=wasm32-unknown-wasip1", + "--sysroot=" + wasi_sdk, + ]), + # The Swift SDK's clang resource directory provides the compiler + # builtins (libclang_rt) for wasm32, which the host toolchain's own + # resource directory does not include. + link_args = _build_list([ + "-resource-dir", + resource_dir + "/clang", + ]), + suffix = "wasm32", + triple = "wasm32-unknown-wasip1", + ) + + repository_ctx.file("BUILD.bazel", build_content) + +swift_wasm_sdk_repository = repository_rule( + attrs = _common_attrs(), + doc = """\ +Downloads the WebAssembly Swift SDK artifact bundle and defines Swift and C++ +toolchains that target `wasm32-unknown-wasip1` using a standalone host +toolchain's compiler. +""", + implementation = _swift_wasm_sdk_impl, +) + +# The architectures the Android Swift SDK provides resources for and that +# `@platforms//cpu` can express. (The SDK also supports armv7, which can be +# added on demand.) +ANDROID_ARCHS = ["aarch64", "x86_64"] + +def _swift_android_sdk_impl(repository_ctx): + bundle_dir = _download_sdk_bundle(repository_ctx) + toolchain_repo = repository_ctx.attr.toolchain_repo + ndk_repo = repository_ctx.attr.ndk_repo + api_level = repository_ctx.attr.api_level + + repo_root = "external/" + repository_ctx.name + sdk_dir_relative = bundle_dir + "/swift-android" + if not repository_ctx.path(sdk_dir_relative + "/swift-sdk.json").exists: + fail("The Android Swift SDK bundle has an unexpected layout; " + + "missing {}/{}/swift-sdk.json".format(repo_root, sdk_dir_relative)) + lib_dir = "{}/{}/swift-resources/usr/lib".format(repo_root, sdk_dir_relative) + + # The NDK's sysroot is the SDK to compile against; the Swift SDK bundle + # deliberately ships without one (its setup-android-sdk.sh script would + # symlink in a locally installed NDK). + ndk_root = repository_ctx.path(repository_ctx.attr.ndk_source_properties).dirname + prebuilts = ndk_root.get_child("toolchains", "llvm", "prebuilt").readdir() + if len(prebuilts) != 1: + fail("Expected exactly one prebuilt toolchain in the Android NDK, " + + "found: " + str(prebuilts)) + ndk_sysroot = _execroot_relative_path(prebuilts[0].get_child("sysroot")) + + # The Android Swift SDK's resource directories do not bundle clang's + # builtin headers, so the clang importer must be pointed at the host + # toolchain's copy (which matches the clang embedded in swiftc). + host_usr = repository_ctx.path(repository_ctx.attr.host_swiftc).dirname.dirname + clang_versions = host_usr.get_child("lib", "clang").readdir() + if len(clang_versions) != 1: + fail("Expected exactly one clang version directory in the host " + + "toolchain, found: " + str(clang_versions)) + clang_builtin_headers = _execroot_relative_path( + clang_versions[0].get_child("include"), + ) + + build_content = _BUILD_HEADER_TEMPLATE.format( + bundle_dir = bundle_dir, + compiler_inputs = _build_list([ + ":sdk_files", + "@{}//:{}".format(toolchain_repo, _HOST_COMPILER_INPUTS), + "@{}//:toolchain_files".format(ndk_repo), + ]), + toolchain_repo = toolchain_repo, + ) + + build_content += _CC_TOOLCHAIN_TEMPLATE.format( + ar = "@{}//:llvm_ar".format(ndk_repo), + clang = "@{}//:clang".format(ndk_repo), + clang_data = _build_list([ + ":sdk_files", + "@{}//:toolchain_files".format(ndk_repo), + ]), + ) + + for arch in ANDROID_ARCHS: + triple = "{}-unknown-linux-android{}".format(arch, api_level) + resource_dir = "{}/swift_static-{}".format(lib_dir, arch) + + build_content += _SWIFT_TOOLCHAIN_TEMPLATE.format( + arch = arch, + copts = _build_list([ + "-resource-dir", + resource_dir, + "-Xcc", + "-I" + clang_builtin_headers, + ]), + features = _build_list([ + "swift.lld_gc_workaround", + "swift.module_map_no_private_headers", + "swift.use_autolink_extract", + "swift.use_module_wrap", + # The file prefix map would make the worker resolve the Xcode + # developer directory on macOS hosts, which this toolchain + # does not depend on. + "-swift.file_prefix_map", + ]), + linker_inputs = _build_list([":sdk_files"]), + # The runtime objects and libraries that `swiftc` would add when + # statically linking the stdlib for Android; see + # `swift_static-{arch}/android/static-stdlib-args.lnk` in the SDK. + # The 16 KiB max page size is required by Android 15+. + linkopts = _build_list([ + "{}/android/{}/swiftrt.o".format(resource_dir, arch), + "-L{}/android".format(resource_dir), + "-ldl", + "-llog", + "-lm", + "-lstdc++", + "-Wl,--exclude-libs,ALL", + "-Wl,-z,max-page-size=16384", + ]), + os = "android", + sdkroot = ndk_sysroot, + suffix = arch, + swift_version = repository_ctx.attr.swift_version, + ) + + build_content += _CC_TOOLCHAIN_FOR_TARGET_TEMPLATE.format( + args = _build_list(["--target=" + triple]), + link_args = _build_list(["-Wl,-z,max-page-size=16384"]), + suffix = arch, + triple = triple, + ) + + repository_ctx.file("BUILD.bazel", build_content) + +swift_android_sdk_repository = repository_rule( + attrs = _common_attrs() | { + "api_level": attr.int( + doc = "The Android API level to target.", + mandatory = True, + ), + "host_swiftc": attr.label( + doc = """\ +The host toolchain's `swiftc`, used to locate the clang builtin headers that +match the clang embedded in the Swift compiler. +""", + mandatory = True, + ), + "ndk_repo": attr.string( + doc = "Name of the repository containing the Android NDK.", + mandatory = True, + ), + "ndk_source_properties": attr.label( + doc = "The NDK repository's `source.properties` file (its directory is the NDK root).", + mandatory = True, + ), + }, + doc = """\ +Downloads the Android Swift SDK artifact bundle and defines Swift and C++ +toolchains that target `{aarch64,x86_64}-unknown-linux-android` using a +standalone host toolchain's Swift compiler and the Android NDK's clang. +""", + implementation = _swift_android_sdk_impl, +) diff --git a/swift/internal/extensions/toolchains.bzl b/swift/internal/extensions/toolchains.bzl index 0caaaef37..a743eb2b0 100644 --- a/swift/internal/extensions/toolchains.bzl +++ b/swift/internal/extensions/toolchains.bzl @@ -43,24 +43,98 @@ toolchain( """ -def toolchains_for_platform(platform, toolchain_repository): +_SDK_TOOLCHAIN_PLATFORM = """ +# Swift SDK toolchains from repository: `{sdk_repository}` +toolchain( + name = "swift_toolchain_{target}_{platform}", + exec_compatible_with = {exec_compatible_with}, + target_compatible_with = {target_compatible_with}, + toolchain = "@{sdk_repository}//:swift_toolchain_{target_suffix}", + toolchain_type = "@rules_swift//toolchains:toolchain_type", + visibility = ["//visibility:public"], +) + +toolchain( + name = "cc_toolchain_{target}_{platform}", + exec_compatible_with = {exec_compatible_with}, + target_compatible_with = {target_compatible_with}, + toolchain = "@{sdk_repository}//:cc_toolchain_{target_suffix}", + toolchain_type = "@bazel_tools//tools/cpp:toolchain_type", + visibility = ["//visibility:public"], +) +""" + +def _exec_compatible_with_for_platform(platform): # This assumption is baked into the API so we have to go along with it if platform == "xcode": - exec_compatible_with = [ + return [ "@platforms//os:macos", ] - else: - exec_compatible_with = [ - "@platforms//os:linux", - "@platforms//cpu:{}".format("aarch64" if "aarch64" in platform else "x86_64"), - ] + return [ + "@platforms//os:linux", + "@platforms//cpu:{}".format("aarch64" if "aarch64" in platform else "x86_64"), + ] +def toolchains_for_platform(platform, toolchain_repository): return _TOOLCHAIN_PLATFORM.format( - exec_compatible_with = exec_compatible_with, + exec_compatible_with = _exec_compatible_with_for_platform(platform), platform = platform, toolchain_repository = toolchain_repository, ) +def wasm_sdk_toolchains_for_platform(platform, sdk_repository): + """Returns `toolchain` declarations for a WebAssembly Swift SDK. + + Args: + platform: The host platform name (e.g. "xcode" or "ubuntu22.04") whose + standalone toolchain the SDK is paired with. + sdk_repository: The name of the repository created by + `swift_wasm_sdk_repository`. + + Returns: + BUILD file content declaring the Swift and C++ toolchains. + """ + return _SDK_TOOLCHAIN_PLATFORM.format( + exec_compatible_with = _exec_compatible_with_for_platform(platform), + platform = platform, + sdk_repository = sdk_repository, + target = "wasm32", + target_compatible_with = [ + "@platforms//os:wasi", + "@platforms//cpu:wasm32", + ], + target_suffix = "wasm32", + ) + +def android_sdk_toolchains_for_platform(platform, sdk_repository, archs): + """Returns `toolchain` declarations for an Android Swift SDK. + + Args: + platform: The host platform name (e.g. "xcode" or "ubuntu22.04") whose + standalone toolchain the SDK is paired with. + sdk_repository: The name of the repository created by + `swift_android_sdk_repository`. + archs: The Android architectures ("aarch64", "x86_64") to declare + toolchains for. + + Returns: + BUILD file content declaring the Swift and C++ toolchains. + """ + content = "" + for arch in archs: + content += _SDK_TOOLCHAIN_PLATFORM.format( + exec_compatible_with = _exec_compatible_with_for_platform(platform), + platform = platform, + sdk_repository = sdk_repository, + target = "android_" + arch, + target_compatible_with = [ + "@platforms//os:android", + "@platforms//cpu:" + arch, + ], + target_suffix = arch, + ) + return content + def _toolchains_impl(repository_ctx): repository_ctx.file("BUILD.bazel", repository_ctx.attr.build_file_content) diff --git a/swift/internal/feature_names.bzl b/swift/internal/feature_names.bzl index e371f611a..c5b8928ae 100644 --- a/swift/internal/feature_names.bzl +++ b/swift/internal/feature_names.bzl @@ -321,6 +321,13 @@ SWIFT_FEATURE_DECLARE_SWIFTSOURCEINFO = "swift.emit_swiftsourceinfo" # system command line limit. SWIFT_FEATURE_NO_EMBED_DEBUG_MODULE = "swift.no_embed_debug_module" +# If enabled, the entry point of a `swift_binary` is not renamed to a +# target-specific symbol (which is otherwise aliased back to `main` at link +# time so that the binary's code can also be linked into another binary, such +# as a test executable). Toolchains whose linkers cannot create such aliases +# (e.g. wasm-ld, which has no `--defsym`) should enable this feature. +SWIFT_FEATURE_NO_ENTRY_POINT_RENAME = "swift.no_entry_point_rename" + # If enabled, the toolchain will directly generate from the raw proto files # and not from the DescriptorSets. # diff --git a/swift/swift_binary.bzl b/swift/swift_binary.bzl index 10689482c..5533e0b82 100644 --- a/swift/swift_binary.bzl +++ b/swift/swift_binary.bzl @@ -22,6 +22,7 @@ load("//swift/internal:compiling.bzl", "compile") load( "//swift/internal:feature_names.bzl", "SWIFT_FEATURE_ADD_TARGET_NAME_TO_OUTPUT", + "SWIFT_FEATURE_NO_ENTRY_POINT_RENAME", ) load("//swift/internal:features.bzl", "is_feature_enabled") load( @@ -99,7 +100,24 @@ def _swift_binary_impl(ctx): ctx.label, feature_configuration = feature_configuration, ) - entry_point_name = entry_point_function_name(module_name) + + if is_feature_enabled( + feature_configuration = feature_configuration, + feature_name = SWIFT_FEATURE_NO_ENTRY_POINT_RENAME, + ): + entry_point_name = None + entry_point_copts = [] + else: + # Use a custom entry point name so that the binary's code can + # also be linked into another process (like a test executable) + # without having its main function collide. + entry_point_name = entry_point_function_name(module_name) + entry_point_copts = [ + "-Xfrontend", + "-entry-point-function-name", + "-Xfrontend", + entry_point_name, + ] include_dev_srch_paths = include_developer_search_paths(ctx.attr) @@ -111,15 +129,7 @@ def _swift_binary_impl(ctx): ctx, ctx.attr.copts, ctx.attr.swiftc_inputs, - ) + _maybe_parse_as_library_copts(srcs) + [ - # Use a custom entry point name so that the binary's code can - # also be linked into another process (like a test executable) - # without having its main function collide. - "-Xfrontend", - "-entry-point-function-name", - "-Xfrontend", - entry_point_name, - ], + ) + _maybe_parse_as_library_copts(srcs) + entry_point_copts, defines = ctx.attr.defines, feature_configuration = feature_configuration, include_dev_srch_paths = include_dev_srch_paths, diff --git a/swift/toolchains/swift_toolchain.bzl b/swift/toolchains/swift_toolchain.bzl index af31dcec9..a9e659ee6 100644 --- a/swift/toolchains/swift_toolchain.bzl +++ b/swift/toolchains/swift_toolchain.bzl @@ -443,6 +443,41 @@ def _swift_unix_linkopts_cc_info( ), ) +def _swift_sdk_linkopts_cc_info( + toolchain_label, + linkopts, + linker_inputs): + """Returns a `CcInfo` with linker flags provided by the toolchain target. + + This is used for toolchains that target platforms whose Swift runtime + libraries come from a Swift SDK (such as WebAssembly or Android) rather + than from the host toolchain or system; the repository that defines the + toolchain provides the exact search paths and runtime objects to link. + + Args: + toolchain_label: The label of the Swift toolchain that will act as the + owner of the linker input propagating the flags. + linkopts: A list of linker flags from the toolchain's `linkopts` + attribute. + linker_inputs: A list of `File`s that should be available to link + actions using these flags. + + Returns: + A `CcInfo` provider that will provide linker flags to binaries that + depend on Swift targets. + """ + return CcInfo( + linking_context = cc_common.create_linking_context( + linker_inputs = depset([ + cc_common.create_linker_input( + owner = toolchain_label, + user_link_flags = depset(linkopts), + additional_inputs = depset(linker_inputs), + ), + ]), + ), + ) + def _entry_point_linkopts_provider(*, entry_point_name): """Returns linkopts to customize the entry point of a binary.""" return struct( @@ -509,6 +544,12 @@ def _swift_toolchain_impl(ctx): ) elif ctx.attr.os == "none": swift_linkopts_cc_info = CcInfo() + elif ctx.attr.linkopts or ctx.attr.linker_inputs: + swift_linkopts_cc_info = _swift_sdk_linkopts_cc_info( + ctx.label, + ctx.attr.linkopts, + ctx.files.linker_inputs, + ) else: swift_linkopts_cc_info = _swift_unix_linkopts_cc_info( ctx.attr.arch, @@ -763,6 +804,25 @@ normally. """, mandatory = False, ), + "linker_inputs": attr.label_list( + allow_files = True, + doc = """\ +Files that must be available to link actions when `linkopts` is set, such as +the Swift runtime libraries of a Swift SDK. +""", + ), + "linkopts": attr.string_list( + doc = """\ +Linker flags that must be passed when linking binaries that contain Swift +code, such as search paths for (and inputs from) the `linker_inputs` +attribute. + +When set, these flags *replace* the default flags that the toolchain would +otherwise compute for the target operating system; they are meant to be used +by toolchains whose Swift runtime libraries come from a Swift SDK (for +example, WebAssembly or Android) rather than from the host toolchain. +""", + ), "sdkroot": attr.string( doc = """\ The root of a SDK to be used for building the target. From 9278761f1f1317b38c38b6d3327104c307d40113 Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Fri, 12 Jun 2026 23:45:58 -0400 Subject: [PATCH 02/17] Support shared-library and reactor outputs for Swift SDK cross-compilation Addresses the gaps found while migrating a consumer onto the Swift SDK toolchains: plain executables are not what WebAssembly and Android actually load. * Add a `linkshared` attribute to `swift_binary` (mirroring `cc_binary`): - Android and other ELF/Mach-O targets get a `lib.so` / `.dylib` dynamic library, loadable via `System.loadLibrary` / `dlopen` (e.g. a JNI library; export entry points with `@_cdecl`). - WebAssembly gets a `.wasm` "reactor" module linked with `-mexec-model=reactor`: no `main`, initializers run via the exported `_initialize`, and functions are exposed for a JS host to call. Exports are retained with `-Xlinker --export=` in `linkopts`. The target is detected with `ctx.target_platform_has_constraint`, and `linkshared` disables the entry-point rename (no `main`). WebAssembly outputs also get the conventional `.wasm` extension. * Enable the rules_cc `shared_flag` feature in the generated Android/wasm C++ toolchains so the dynamic-library link passes `-shared`. * Expose the NDK's `libc++_shared.so` at a host-independent label (`@//:libcxx_shared_`) that selects the NDK for the build host, so an APK rule can bundle it without naming the host. * Document a one-line `register_toolchains("@//:all")` for single-host setups, and the `rules_apple` coexistence story (shared `compatibility_level = 3`). The `examples/cross_compilation` example is reworked to build a WebAssembly reactor and an Android JNI shared library, both from `swift_binary` targets depending on a shared `Greeter` `swift_library`. `android_app/` adds the Kotlin app (and a documented `rules_android` packaging recipe) that loads the JNI library, completing the Kotlin -> Swift (.so) -> Swift library chain. The JNI entry point is written in Swift using the SDK's `Android` module, so no C shim is needed. Verified locally: the reactor runs under wasmtime (exported functions call into the Swift library), and the Android `.so` is a shared object that exports the `Java_..._greetingFromSwift` JNI symbol and links `libc++_shared.so`. --- MODULE.bazel | 7 +- doc/rules.md | 6 +- doc/standalone_toolchain.md | 56 +++++++- examples/cross_compilation/BUILD.bazel | 73 +++++++---- examples/cross_compilation/README.md | 39 ++++++ .../Sources/Binary/main.swift | 3 - .../Sources/Greeter/Greeter.swift | 15 +++ .../Sources/Library/Library.swift | 12 -- .../Sources/Reactor/Reactor.swift | 27 ++++ .../Sources/SwiftJNI/SwiftJNI.swift | 24 ++++ .../android_app/AndroidManifest.xml | 19 +++ .../cross_compilation/android_app/README.md | 120 ++++++++++++++++++ .../java/com/example/swiftjni/MainActivity.kt | 15 +++ .../java/com/example/swiftjni/NativeBridge.kt | 19 +++ swift/extensions.bzl | 13 +- swift/internal/extensions/swift_sdks.bzl | 3 + swift/internal/extensions/toolchains.bzl | 51 ++++++++ swift/swift_binary.bzl | 96 ++++++++++++-- 18 files changed, 538 insertions(+), 60 deletions(-) create mode 100644 examples/cross_compilation/README.md delete mode 100644 examples/cross_compilation/Sources/Binary/main.swift create mode 100644 examples/cross_compilation/Sources/Greeter/Greeter.swift delete mode 100644 examples/cross_compilation/Sources/Library/Library.swift create mode 100644 examples/cross_compilation/Sources/Reactor/Reactor.swift create mode 100644 examples/cross_compilation/Sources/SwiftJNI/SwiftJNI.swift create mode 100644 examples/cross_compilation/android_app/AndroidManifest.xml create mode 100644 examples/cross_compilation/android_app/README.md create mode 100644 examples/cross_compilation/android_app/java/com/example/swiftjni/MainActivity.kt create mode 100644 examples/cross_compilation/android_app/java/com/example/swiftjni/NativeBridge.kt diff --git a/MODULE.bazel b/MODULE.bazel index 7b37d807d..ceb82f839 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -118,7 +118,12 @@ register_toolchains( register_toolchains( # Swift SDK toolchains for cross-compiling to WebAssembly and Android; - # used by //examples/cross_compilation. + # used by //examples/cross_compilation. As with the embedded toolchains + # above, we register only the host platforms used by CI rather than + # `@swift_toolchain//:all`, because rules_swift cannot yet auto-select a + # Linux distribution and `:all` would make the host/exec toolchain + # ambiguous across distros. A consumer that builds on a single host + # platform can simply register `@swift_toolchain//:all`. "@swift_toolchain//:cc_toolchain_android_aarch64_ubuntu22.04", "@swift_toolchain//:cc_toolchain_android_aarch64_ubuntu22.04-aarch64", "@swift_toolchain//:cc_toolchain_android_aarch64_xcode", diff --git a/doc/rules.md b/doc/rules.md index 08c42c885..2b687046b 100755 --- a/doc/rules.md +++ b/doc/rules.md @@ -41,7 +41,7 @@ On this page:
 swift_binary(name, deps, srcs, data, additional_linker_inputs, copts, defines, env, linkopts,
-             malloc, module_name, package_name, plugins, stamp, swiftc_inputs)
+             linkshared, malloc, module_name, package_name, plugins, stamp, swiftc_inputs)
 
Compiles and links Swift code into an executable binary. @@ -58,6 +58,9 @@ please use one of the platform-specific application rules in [rules_apple](https://github.com/bazelbuild/rules_apple) instead of `swift_binary`. +Setting `linkshared = True` links a shared library or (on WebAssembly) a +reactor module instead of an executable; see the `linkshared` attribute. + **ATTRIBUTES** @@ -72,6 +75,7 @@ please use one of the platform-specific application rules in | defines | A list of defines to add to the compilation command line.

Note that unlike C-family languages, Swift defines do not have values; they are simply identifiers that are either defined or undefined. So strings in this list should be simple identifiers, **not** `name=value` pairs.

Each string is prepended with `-D` and added to the command line. Unlike `copts`, these flags are added for the target and every target that depends on it, so use this attribute with caution. It is preferred that you add defines directly to `copts`, only using this feature in the rare case that a library needs to propagate a symbol up to those that depend on it. | List of strings | optional | `[]` | | env | Specifies additional environment variables to set when the test is executed by `bazel run` or `bazel test`.

The values of these environment variables are subject to `$(location)` and "Make variable" substitution.

NOTE: The environment variables are not set when you run the target outside of Bazel (for example, by manually executing the binary in `bazel-bin/`). | Dictionary: String -> String | optional | `{}` | | linkopts | Additional linker options that should be passed to `clang`. These strings are subject to `$(location ...)` expansion. | List of strings | optional | `[]` | +| linkshared | If `True`, link the target as a shared library / loadable module instead of an executable, similar to `cc_binary`'s `linkshared`. The binary has no `main` entry point and the renamed-entry-point machinery is disabled.

On most platforms this produces a dynamic library named `lib.so` (`.dylib` on Apple platforms) suitable for loading with `dlopen` / `System.loadLibrary` (e.g. an Android JNI library; export functions with `@_cdecl`).

When targeting WebAssembly it instead produces a "reactor" module (`.wasm`, linked with `-mexec-model=reactor`): the module has no `_start`, runs its initializers via the exported `_initialize`, and exposes the functions a host instantiates and calls. Force-export those functions by passing `-Xlinker --export=` (or `-Wl,--export=`) flags in `linkopts`. | Boolean | optional | `False` | | malloc | Override the default dependency on `malloc`.

By default, Swift binaries are linked against `@bazel_tools//tools/cpp:malloc"`, which is an empty library and the resulting binary will use libc's `malloc`. This label must refer to a `cc_library` rule. | Label | optional | `"@bazel_tools//tools/cpp:malloc"` | | module_name | The name of the Swift module being built.

If left unspecified, the module name will be computed based on the target's build label, by stripping the leading `//` and replacing `/`, `:`, and other non-identifier characters with underscores. | String | optional | `""` | | package_name | The semantic package of the Swift target being built. Targets with the same package_name can access APIs using the 'package' access control modifier in Swift 5.9+. | String | optional | `""` | diff --git a/doc/standalone_toolchain.md b/doc/standalone_toolchain.md index c28cf118e..413336918 100644 --- a/doc/standalone_toolchain.md +++ b/doc/standalone_toolchain.md @@ -186,6 +186,19 @@ register_toolchains( ) ``` +If you build on a single host platform, you can register everything the +extension generates (standalone, embedded, and Swift-SDK toolchains) in one +line instead of listing the matrix: + +```bzl +register_toolchains("@swift_toolchain//:all") +``` + +Avoid `:all` when you configure multiple Linux distributions, for the same +reason the standalone host toolchains are registered explicitly: rules_swift +cannot yet auto-select a distribution, so `:all` would make the host/exec +toolchain ambiguous across them. + Then build with a platform carrying the matching constraints, for example: ```bzl @@ -213,14 +226,39 @@ bazel build //my:binary --platforms=//:wasm32-wasip1 See `examples/cross_compilation` for a complete example, including building through a platform transition. +### Shared libraries and WebAssembly reactors + +A plain `swift_binary` links an executable: a WASI *command* module for +WebAssembly, or an ordinary executable for Android. To produce the artifacts +those ecosystems actually load, set `linkshared = True`: + +* **Android (JNI):** produces `lib.so`, loadable with + `System.loadLibrary`. Export functions with `@_cdecl`; the Android Swift + SDK's `Android` module provides the JNI types, so the entry points can be + written entirely in Swift. +* **WebAssembly (reactor):** produces `.wasm` linked with + `-mexec-model=reactor` — no `main`, initializers run via the exported + `_initialize`, and the module exposes the functions a JS host calls. Keep + each exported function with `linkopts = ["-Xlinker", "--export="]`. + +A `swift_binary(linkshared = True)` may depend on ordinary `swift_library` +targets (and link them statically), so the platform-specific entry point and +the shared business logic stay in separate, normal libraries. +`examples/cross_compilation` builds a reactor and an Android JNI library this +way, both depending on the same `Greeter` `swift_library`, and +`examples/cross_compilation/android_app` shows the Kotlin app that loads the +JNI library. + Details worth knowing: * The Swift standard library is linked statically from the SDK, matching the behavior of `swiftc` with these SDKs. WebAssembly binaries are self-contained `wasm32-wasip1` modules (runnable with `wasmtime` et al.). * Android binaries link against the NDK's `libc++_shared.so`, which must be - packaged with the application; the NDK repository exposes it as - `@_android_ndk_//:libcxx_shared_`. + packaged with the application. Reference it host-independently as + `@//:libcxx_shared_` (e.g. + `@swift_toolchain//:libcxx_shared_aarch64`); the alias selects the NDK for + the build host automatically. * The `android_sdk` tag downloads the Android NDK (for its sysroot and clang) in addition to the Swift SDK. The NDK is only fetched when an Android target is actually built; WebAssembly-only builds do not download @@ -231,6 +269,20 @@ Details worth knowing: `swift/internal/extensions/swift_sdk_releases.bzl`); for other releases, pass `sha256` explicitly. +### Coexistence with `rules_apple` + +A common setup cross-compiles to WebAssembly/Android *and* builds the same +app's Apple targets with `rules_apple`. The two resolve together cleanly: this +line of `rules_swift` is `compatibility_level = 3` (the same as released +`rules_swift` 3.x), so a current `rules_apple` release — 4.5.3 or the 5.0.0 +release candidates, both built against `rules_swift` 3.x — works alongside it. +Bazel's version resolution keeps the higher of each shared transitive +dependency (`apple_support`, `rules_cc`), which are backward compatible, so no +extra pinning is required. If you are tracking this work from a fork via +`git_override`, depend on such a `rules_apple` release; once the change lands +in a published `rules_swift` that `rules_apple` itself depends on, the +`git_override` is no longer needed. + ## Using the extension from a non-root module The extension is intended for the root module — it fails if a non-root diff --git a/examples/cross_compilation/BUILD.bazel b/examples/cross_compilation/BUILD.bazel index 41cc78205..0a16a0049 100644 --- a/examples/cross_compilation/BUILD.bazel +++ b/examples/cross_compilation/BUILD.bazel @@ -3,9 +3,9 @@ load("//swift:swift.bzl", "swift_binary", "swift_library") package(default_visibility = ["//visibility:public"]) -# Platforms covered by the Swift SDK toolchains that the `swift` module -# extension's `wasm_sdk` and `android_sdk` tags define (and that the dev -# MODULE.bazel of this repository registers). +# Platforms covered by the Swift SDK toolchains registered by this repository's +# dev `MODULE.bazel` (via the `swift` extension's `wasm_sdk`/`android_sdk` +# tags). Build the targets below with `--platforms` set to one of these. platform( name = "wasm32-wasip1", constraint_values = [ @@ -22,44 +22,61 @@ platform( ], ) -platform( - name = "android-x86_64", - constraint_values = [ - "@platforms//cpu:x86_64", - "@platforms//os:android", - ], -) - +# A plain library dependency, compiled for whichever platform depends on it. +# Both platform-specific entry points below call into it, demonstrating that a +# normal `swift_library` is reused unchanged across targets. swift_library( - name = "Library", - srcs = ["Sources/Library/Library.swift"], - module_name = "Library", - # Only built through the platform transitions below. + name = "Greeter", + srcs = ["Sources/Greeter/Greeter.swift"], + module_name = "Greeter", tags = ["manual"], ) +# --------------------------------------------------------------------------- +# WebAssembly: a reactor module (no `main`; exports functions for a JS host). +# --------------------------------------------------------------------------- + swift_binary( - name = "Binary", - srcs = ["Sources/Binary/main.swift"], - # Only built through the platform transitions below. + name = "Reactor", + srcs = ["Sources/Reactor/Reactor.swift"], + # Keep the exported functions in the linked module. `@_cdecl` names them; + # wasm-ld still needs an explicit `--export=` to retain each one. + linkopts = [ + "-Xlinker", + "--export=greeting_into", + "-Xlinker", + "--export=greeting_length", + ], + linkshared = True, tags = ["manual"], - deps = [":Library"], + deps = [":Greeter"], ) +# Build `:Reactor` for the wasm platform. Consumers can instead set +# `--platforms=//examples/cross_compilation:wasm32-wasip1` on the command line. transition_binary( - name = "Binary.wasm32-wasip1", - binary = ":Binary", + name = "Reactor.wasm", + binary = ":Reactor", platform = ":wasm32-wasip1", ) -transition_binary( - name = "Binary.android-aarch64", - binary = ":Binary", - platform = ":android-aarch64", +# --------------------------------------------------------------------------- +# Android: a JNI shared library, loaded by Kotlin via `System.loadLibrary`. +# --------------------------------------------------------------------------- + +# `linkshared` produces `libSwiftJNI.so`. The JNI entry point is written in +# Swift (it `import`s the SDK's `Android` module for the JNI types) and calls +# into the `Greeter` library. See `android_app/` for the APK that loads it. +swift_binary( + name = "SwiftJNI", + srcs = ["Sources/SwiftJNI/SwiftJNI.swift"], + linkshared = True, + tags = ["manual"], + deps = [":Greeter"], ) transition_binary( - name = "Binary.android-x86_64", - binary = ":Binary", - platform = ":android-x86_64", + name = "libSwiftJNI.so", + binary = ":SwiftJNI", + platform = ":android-aarch64", ) diff --git a/examples/cross_compilation/README.md b/examples/cross_compilation/README.md new file mode 100644 index 000000000..024af6574 --- /dev/null +++ b/examples/cross_compilation/README.md @@ -0,0 +1,39 @@ +# Cross-compilation example (WebAssembly + Android) + +Builds plain `swift_library` / `swift_binary` targets for non-host platforms +using the Swift SDK toolchains registered by this repository's `MODULE.bazel` +(via the `swift` extension's `wasm_sdk` and `android_sdk` tags). See +`doc/standalone_toolchain.md` for the toolchain setup. + +All targets are tagged `manual` because they download the Swift SDK bundles +(and, for Android, the NDK) and require the cross toolchains to be registered. + +## Targets + +| Target | Output | Demonstrates | +|---|---|---| +| `:Greeter` | `.swiftmodule` + `.a` | A normal `swift_library` reused by both entry points below | +| `:Reactor.wasm` | `Reactor.wasm` | A WebAssembly **reactor** (`swift_binary(linkshared)`), no `main`, with exported functions | +| `:libSwiftJNI.so` | `libSwiftJNI.so` | An Android **JNI shared library** (`swift_binary(linkshared)`) that calls `:Greeter` | + +```sh +# WebAssembly reactor (runnable with wasmtime): +bazel build //examples/cross_compilation:Reactor.wasm +wasmtime run --invoke greeting_length \ + bazel-bin/examples/cross_compilation/Reactor.wasm + +# Android JNI shared library: +bazel build //examples/cross_compilation:libSwiftJNI.so +``` + +Each `transition_binary` target builds its `swift_binary` under the matching +platform (`:wasm32-wasip1` / `:android-aarch64`); you can equivalently pass +`--platforms=//examples/cross_compilation:wasm32-wasip1` on the command line. + +## Android app + +`android_app/` contains the Kotlin app (and a documented packaging recipe) that +loads `libSwiftJNI.so` and calls into it, completing the +Kotlin → Swift (JNI `.so`) → Swift library chain. Packaging an APK uses +`rules_android` + an Android SDK in the consuming module; see +`android_app/README.md`. diff --git a/examples/cross_compilation/Sources/Binary/main.swift b/examples/cross_compilation/Sources/Binary/main.swift deleted file mode 100644 index 0b24404c7..000000000 --- a/examples/cross_compilation/Sources/Binary/main.swift +++ /dev/null @@ -1,3 +0,0 @@ -import Library - -print(Greeter(subject: "world").greeting()) diff --git a/examples/cross_compilation/Sources/Greeter/Greeter.swift b/examples/cross_compilation/Sources/Greeter/Greeter.swift new file mode 100644 index 000000000..e0ef934c8 --- /dev/null +++ b/examples/cross_compilation/Sources/Greeter/Greeter.swift @@ -0,0 +1,15 @@ +/// A plain `swift_library` used as a normal dependency of the +/// platform-specific entry points (the Android JNI shared library and the +/// WebAssembly reactor). Nothing in here is platform-specific; it is compiled +/// for whichever platform the depending target is built for. +public struct Greeter { + private let subject: String + + public init(subject: String) { + self.subject = subject + } + + public func greeting() -> String { + return "Hello from Swift, \(subject)!" + } +} diff --git a/examples/cross_compilation/Sources/Library/Library.swift b/examples/cross_compilation/Sources/Library/Library.swift deleted file mode 100644 index 33eaed0d4..000000000 --- a/examples/cross_compilation/Sources/Library/Library.swift +++ /dev/null @@ -1,12 +0,0 @@ -/// A trivial library used to validate cross-compilation with a Swift SDK. -public struct Greeter { - private let subject: String - - public init(subject: String) { - self.subject = subject - } - - public func greeting() -> String { - return "Hello, \(subject)!" - } -} diff --git a/examples/cross_compilation/Sources/Reactor/Reactor.swift b/examples/cross_compilation/Sources/Reactor/Reactor.swift new file mode 100644 index 000000000..c9f0bbba5 --- /dev/null +++ b/examples/cross_compilation/Sources/Reactor/Reactor.swift @@ -0,0 +1,27 @@ +import Greeter + +// A WebAssembly "reactor" module: it has no `main`/entry point. Instead it +// exports functions that a host (e.g. JavaScript via `WebAssembly.instantiate`) +// calls after instantiation. The `@_cdecl` attribute gives each function a +// plain C name; the linker still needs `--export=` (passed via `linkopts` in +// the BUILD file) to keep them in the final module. + +/// Writes the greeting into `buffer` (NUL-terminated, truncated to `capacity`) +/// and returns the number of bytes written, excluding the terminator. +@_cdecl("greeting_into") +public func greeting_into(_ buffer: UnsafeMutablePointer, _ capacity: Int32) -> Int32 { + let message = Greeter(subject: "WebAssembly").greeting() + let bytes = Array(message.utf8) + let limit = min(bytes.count, Int(capacity) - 1) + for index in 0 ..< limit { + buffer[index] = CChar(bitPattern: bytes[index]) + } + buffer[limit] = 0 + return Int32(limit) +} + +/// Returns the length the greeting would occupy (so the host can size a buffer). +@_cdecl("greeting_length") +public func greeting_length() -> Int32 { + return Int32(Greeter(subject: "WebAssembly").greeting().utf8.count) +} diff --git a/examples/cross_compilation/Sources/SwiftJNI/SwiftJNI.swift b/examples/cross_compilation/Sources/SwiftJNI/SwiftJNI.swift new file mode 100644 index 000000000..4b3336bd5 --- /dev/null +++ b/examples/cross_compilation/Sources/SwiftJNI/SwiftJNI.swift @@ -0,0 +1,24 @@ +import Android +import Greeter + +// The JNI entry point, written entirely in Swift. `import Android` provides the +// JNI types (`JNIEnv`, `jclass`, `jstring`, ...) from the Android Swift SDK, so +// no C shim is needed. `@_cdecl` gives the function the exact symbol name JNI +// looks up: `Java___`, with `.`/`_` escaped per the JNI +// spec. It is the Kotlin-callable native implementation of: +// +// package com.example.swiftjni +// class NativeBridge { external fun greetingFromSwift(): String } +// +// and it delegates to the `Greeter` `swift_library` (a normal dependency), +// completing the Kotlin -> Swift (JNI .so) -> Swift library call chain. +@_cdecl("Java_com_example_swiftjni_NativeBridge_greetingFromSwift") +public func greetingFromSwift( + _ env: UnsafeMutablePointer, + _ clazz: jclass +) -> jstring? { + let message = Greeter(subject: "Android").greeting() + return message.withCString { cString in + env.pointee!.pointee.NewStringUTF(env, cString) + } +} diff --git a/examples/cross_compilation/android_app/AndroidManifest.xml b/examples/cross_compilation/android_app/AndroidManifest.xml new file mode 100644 index 000000000..9fa94dbfb --- /dev/null +++ b/examples/cross_compilation/android_app/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/examples/cross_compilation/android_app/README.md b/examples/cross_compilation/android_app/README.md new file mode 100644 index 000000000..fafe64f0d --- /dev/null +++ b/examples/cross_compilation/android_app/README.md @@ -0,0 +1,120 @@ +# Android app: Kotlin → Swift JNI `.so` → Swift library + +This directory shows the application half of the +Kotlin → Swift → Swift call chain that `rules_swift` enables: + +``` +MainActivity.kt ──► NativeBridge.greetingFromSwift() (Kotlin) + │ JNI (System.loadLibrary("SwiftJNI")) + ▼ + //examples/cross_compilation:SwiftJNI → libSwiftJNI.so (swift_binary, linkshared) + @_cdecl("Java_..._greetingFromSwift") in SwiftJNI.swift (Swift) + │ + ▼ + //examples/cross_compilation:Greeter (swift_library) +``` + +The `rules_swift` side — building `libSwiftJNI.so` from a `swift_binary` that +depends on a normal `swift_library`, and exposing the NDK's `libc++_shared.so` +at a host-independent label — is fully implemented and exercised by +`//examples/cross_compilation:libSwiftJNI.so`. + +Packaging that `.so` into an APK is the job of the Android rules +(`rules_android` + `rules_kotlin`) and a local Android SDK. Those are heavy +dependencies (`rules_android` pulls in `rules_go`, `gazelle`, Robolectric, and a +conflicting protobuf), so `rules_swift` deliberately does **not** depend on them; +the APK target lives in *your* module instead. The sources in this directory +(`NativeBridge.kt`, `MainActivity.kt`, `AndroidManifest.xml`) are complete and +ready to drop into such a module. + +## `MODULE.bazel` (in your app's module) + +```starlark +bazel_dep(name = "rules_swift", version = "...") # or git_override to this fork +bazel_dep(name = "rules_android", version = "0.7.3") +bazel_dep(name = "rules_kotlin", version = "2.3.20") + +swift = use_extension("@rules_swift//swift:extensions.bzl", "swift") +swift.toolchain(name = "swift_toolchain", swift_version = "6.3.2") +swift.android_sdk(toolchain_name = "swift_toolchain") +use_repo(swift, "swift_toolchain") + +# One line registers every Swift SDK toolchain (and the standalone host ones). +register_toolchains("@swift_toolchain//:all") + +android_sdk = use_extension("@rules_android//rules/android_sdk_repository:rule.bzl", "android_sdk_repository_extension") +use_repo(android_sdk, "androidsdk") +register_toolchains("@androidsdk//:all") +``` + +Set `ANDROID_HOME` to a local Android SDK (with `platforms;android-34` and a +recent `build-tools`). + +## `BUILD.bazel` (in your app's module) + +```starlark +load("@rules_android//android:rules.bzl", "android_binary") +load("@rules_kotlin//kotlin:android.bzl", "kt_android_library") + +# Lay the Swift JNI library and the NDK C++ runtime out as jniLibs for the +# arm64-v8a ABI. `libSwiftJNI.so` is the swift_binary(linkshared) output; the +# libc++_shared alias is host-independent (it selects the NDK for the build +# host automatically). +genrule( + name = "jni_libs", + srcs = [ + "@rules_swift//examples/cross_compilation:libSwiftJNI.so", + "@swift_toolchain//:libcxx_shared_aarch64", + ], + outs = [ + "lib/arm64-v8a/libSwiftJNI.so", + "lib/arm64-v8a/libc++_shared.so", + ], + cmd = """ + srcs=($(SRCS)) + mkdir -p $(RULEDIR)/lib/arm64-v8a + cp "$${srcs[0]}" $(RULEDIR)/lib/arm64-v8a/libSwiftJNI.so + cp "$${srcs[1]}" $(RULEDIR)/lib/arm64-v8a/libc++_shared.so + """, +) + +kt_android_library( + name = "app_lib", + srcs = [ + "java/com/example/swiftjni/MainActivity.kt", + "java/com/example/swiftjni/NativeBridge.kt", + ], + manifest = "AndroidManifest.xml", +) + +android_binary( + name = "app", + manifest = "AndroidManifest.xml", + custom_package = "com.example.swiftjni", + # Bundle the native libraries laid out above. + resource_files = [], + deps = [":app_lib"], + # rules_android picks up `lib//*.so` produced by the genrule when it is + # provided as data; depending on your rules_android version you may instead + # place the .so files under `src/main/jniLibs//` or pass them through a + # `cc_library`/`android_library` `jni_libs` attribute. + data = [":jni_libs"], +) +``` + +> The exact mechanism for adding pre-built `.so`s to an `android_binary` varies +> by `rules_android` version; the constant is that `libSwiftJNI.so` and +> `libc++_shared.so` must land under `lib/arm64-v8a/` (and the corresponding +> directory for any other ABIs you build). Build the `.so` for `x86_64` with the +> `//examples/cross_compilation:android-x86_64`-equivalent platform and place it +> under `lib/x86_64/` to support the emulator. + +## Building + +```sh +# The rules_swift-side artifact (verified by this repo): +bazel build @rules_swift//examples/cross_compilation:libSwiftJNI.so + +# The APK (in your module, with the wiring above and ANDROID_HOME set): +bazel build //path/to/android_app:app +``` diff --git a/examples/cross_compilation/android_app/java/com/example/swiftjni/MainActivity.kt b/examples/cross_compilation/android_app/java/com/example/swiftjni/MainActivity.kt new file mode 100644 index 000000000..d1cb84bf0 --- /dev/null +++ b/examples/cross_compilation/android_app/java/com/example/swiftjni/MainActivity.kt @@ -0,0 +1,15 @@ +package com.example.swiftjni + +import android.app.Activity +import android.os.Bundle +import android.widget.TextView + +/** Displays the greeting computed by Swift, reached via JNI. */ +class MainActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val textView = TextView(this) + textView.text = NativeBridge.greetingFromSwift() + setContentView(textView) + } +} diff --git a/examples/cross_compilation/android_app/java/com/example/swiftjni/NativeBridge.kt b/examples/cross_compilation/android_app/java/com/example/swiftjni/NativeBridge.kt new file mode 100644 index 000000000..62c59afa0 --- /dev/null +++ b/examples/cross_compilation/android_app/java/com/example/swiftjni/NativeBridge.kt @@ -0,0 +1,19 @@ +package com.example.swiftjni + +/** + * Loads the Swift JNI shared library (`libSwiftJNI.so`, built by the + * `//examples/cross_compilation:SwiftJNI` `swift_binary(linkshared = True)`) + * and exposes its Swift entry point to Kotlin. + * + * The native method binds by name to the Swift `@_cdecl` function + * `Java_com_example_swiftjni_NativeBridge_greetingFromSwift`, which in turn + * calls the `Greeter` `swift_library` — completing the + * Kotlin -> Swift (in the `.so`) -> Swift library call chain. + */ +object NativeBridge { + init { + System.loadLibrary("SwiftJNI") + } + + external fun greetingFromSwift(): String +} diff --git a/swift/extensions.bzl b/swift/extensions.bzl index a8540ca3f..7249ec678 100644 --- a/swift/extensions.bzl +++ b/swift/extensions.bzl @@ -36,6 +36,7 @@ load( ) load( "//swift/internal/extensions:toolchains.bzl", + _android_libcxx_aliases = "android_libcxx_aliases", _android_sdk_toolchains_for_platform = "android_sdk_toolchains_for_platform", _toolchains_for_platform = "toolchains_for_platform", _toolchains_repository = "toolchains_repository", @@ -133,16 +134,24 @@ def _setup_android_sdk(*, tag, toolchain_name, swift_version, platforms): ndk_sha256s = ANDROID_NDK_RELEASES[ndk_version] host_oses = {_ndk_host_os(platform): None for platform in platforms} + ndk_repos_by_host = {} for host_os in host_oses: + ndk_repo = "{}_android_ndk_{}".format(toolchain_name, host_os) + ndk_repos_by_host[host_os] = ndk_repo http_archive( - name = "{}_android_ndk_{}".format(toolchain_name, host_os), + name = ndk_repo, build_file_content = ANDROID_NDK_BUILD_FILE_CONTENT, sha256 = ndk_sha256s.get(host_os, ""), strip_prefix = "android-ndk-" + ndk_version, url = android_ndk_download_url(ndk_version, host_os), ) - build_file_content = "" + # Host-independent aliases for the NDK's `libc++_shared.so`, so an APK rule + # can bundle it without naming the build host. + build_file_content = _android_libcxx_aliases( + ndk_repos_by_host = ndk_repos_by_host, + archs = ANDROID_ARCHS, + ) for platform in platforms: ndk_repo = "{}_android_ndk_{}".format(toolchain_name, _ndk_host_os(platform)) repository_name = "{}_android_sdk_{}".format(toolchain_name, platform) diff --git a/swift/internal/extensions/swift_sdks.bzl b/swift/internal/extensions/swift_sdks.bzl index fb7796bcc..3d86ee10d 100644 --- a/swift/internal/extensions/swift_sdks.bzl +++ b/swift/internal/extensions/swift_sdks.bzl @@ -131,6 +131,9 @@ cc_toolchain( "@rules_cc//cc/toolchains/args/archiver_flags:feature", "@rules_cc//cc/toolchains/args/libraries_to_link:feature", "@rules_cc//cc/toolchains/args/link_flags:feature", + # Needed so `swift_binary(linkshared = True)` links a shared library + # (passes `-shared` for the dynamic_library link action). + "@rules_cc//cc/toolchains/args/shared_flag:feature", ], make_variables = [ ":cc_target_triple_{suffix}", diff --git a/swift/internal/extensions/toolchains.bzl b/swift/internal/extensions/toolchains.bzl index a743eb2b0..cbd8e92f6 100644 --- a/swift/internal/extensions/toolchains.bzl +++ b/swift/internal/extensions/toolchains.bzl @@ -135,6 +135,57 @@ def android_sdk_toolchains_for_platform(platform, sdk_repository, archs): ) return content +_NDK_HOST_OS_CONSTRAINT = { + "darwin": "@platforms//os:macos", + "linux": "@platforms//os:linux", +} + +def android_libcxx_aliases(ndk_repos_by_host, archs): + """Returns host-independent aliases for the NDK's `libc++_shared.so`. + + The NDK is fetched into a host-specific repository, but its + `libc++_shared.so` (which an APK containing Swift code must bundle) is a + target artifact whose content does not depend on the build host. These + aliases let packaging rules reference it without naming the host, by + selecting the NDK repository for the host the build runs on. + + Args: + ndk_repos_by_host: A dict mapping NDK host OS ("darwin", "linux") to + the name of the corresponding NDK repository. + archs: The Android architectures ("aarch64", "x86_64"). + + Returns: + BUILD file content declaring one `libcxx_shared_` alias per arch. + """ + hosts = sorted(ndk_repos_by_host.keys()) + default_repo = ndk_repos_by_host[hosts[0]] + + content = "" + for arch in archs: + branches = "".join([ + ' "{}": "@{}//:libcxx_shared_{}",\n'.format( + _NDK_HOST_OS_CONSTRAINT[host], + ndk_repos_by_host[host], + arch, + ) + for host in hosts + ]) + content += """\ +alias( + name = "libcxx_shared_{arch}", + actual = select({{ +{branches} "//conditions:default": "@{default_repo}//:libcxx_shared_{arch}", + }}), + visibility = ["//visibility:public"], +) + +""".format( + arch = arch, + branches = branches, + default_repo = default_repo, + ) + return content + def _toolchains_impl(repository_ctx): repository_ctx.file("BUILD.bazel", repository_ctx.attr.build_file_content) diff --git a/swift/swift_binary.bzl b/swift/swift_binary.bzl index 5533e0b82..fefd1c3f4 100644 --- a/swift/swift_binary.bzl +++ b/swift/swift_binary.bzl @@ -14,6 +14,7 @@ """Implementation of the `swift_binary` rule.""" +load("@bazel_skylib//lib:dicts.bzl", "dicts") load("@bazel_skylib//lib:paths.bzl", "paths") load("@rules_cc//cc/common:cc_common.bzl", "cc_common") load("@rules_cc//cc/common:cc_info.bzl", "CcInfo") @@ -77,6 +78,12 @@ def _maybe_parse_as_library_copts(srcs): srcs[0].basename != "main.swift" return ["-parse-as-library"] if use_parse_as_library else [] +def _is_wasm(ctx): + """Returns True if the target platform is WebAssembly.""" + return ctx.target_platform_has_constraint( + ctx.attr._wasi_os_constraint[platform_common.ConstraintValueInfo], + ) + def _swift_binary_impl(ctx): toolchains = find_all_toolchains(ctx) feature_configuration = configure_features_for_binary( @@ -86,6 +93,17 @@ def _swift_binary_impl(ctx): unsupported_features = ctx.disabled_features, ) + is_wasm = _is_wasm(ctx) + + # A binary linked as a shared object (`linkshared`) or a WebAssembly + # reactor has no `main`, so the entry-point rename (and the matching + # `--defsym main=...` at link time) must be skipped, just as it is when the + # toolchain requests it via `swift.no_entry_point_rename`. + skip_entry_point = ctx.attr.linkshared or is_feature_enabled( + feature_configuration = feature_configuration, + feature_name = SWIFT_FEATURE_NO_ENTRY_POINT_RENAME, + ) + srcs = ctx.files.srcs output_groups = {} module_contexts = [] @@ -101,10 +119,7 @@ def _swift_binary_impl(ctx): feature_configuration = feature_configuration, ) - if is_feature_enabled( - feature_configuration = feature_configuration, - feature_name = SWIFT_FEATURE_NO_ENTRY_POINT_RENAME, - ): + if skip_entry_point: entry_point_name = None entry_point_copts = [] else: @@ -188,6 +203,24 @@ def _swift_binary_impl(ctx): else: name = ctx.label.name + # When targeting WebAssembly a `linkshared` binary is a "reactor" module: it + # is still produced as an `executable`-shaped wasm file (not a `-shared` + # dynamic library), but linked with the reactor execution model so it has no + # `_start`/`main` and instead exports functions for a host to call. + # Everywhere else, `linkshared` produces a real dynamic library + # (`lib.so` / `.dylib`), matching `cc_binary`'s `linkshared`. + shared_link_flags = [] + if ctx.attr.linkshared and not is_wasm: + output_type = "dynamic_library" + else: + output_type = "executable" + if ctx.attr.linkshared and is_wasm: + shared_link_flags = ["-mexec-model=reactor"] + + # Give WebAssembly outputs the conventional `.wasm` extension. + if is_wasm: + name = name + ".wasm" + linking_outputs = register_link_binary_action( actions = ctx.actions, additional_inputs = ctx.files.additional_linker_inputs, @@ -199,18 +232,29 @@ def _swift_binary_impl(ctx): label = ctx.label, module_contexts = module_contexts, name = name, - output_type = "executable", + output_type = output_type, stamp = ctx.attr.stamp, toolchains = toolchains, - user_link_flags = binary_link_flags + entry_point_linkopts, + user_link_flags = ( + binary_link_flags + entry_point_linkopts + shared_link_flags + ), variables_extension = variables_extension, ) + if output_type == "dynamic_library": + library_to_link = linking_outputs.library_to_link + output_file = ( + library_to_link.resolved_symlink_dynamic_library or + library_to_link.dynamic_library + ) + else: + output_file = linking_outputs.executable + providers = [ DefaultInfo( - executable = linking_outputs.executable, + executable = output_file, files = depset( - [linking_outputs.executable] + additional_debug_outputs, + [output_file] + additional_debug_outputs, ), runfiles = ctx.runfiles( collect_data = True, @@ -288,9 +332,36 @@ def _swift_binary_impl(ctx): return providers swift_binary = rule( - attrs = binary_rule_attrs( - additional_deps_providers = [[SwiftCompilerPluginInfo]], - stamp_default = -1, + attrs = dicts.add( + binary_rule_attrs( + additional_deps_providers = [[SwiftCompilerPluginInfo]], + stamp_default = -1, + ), + { + "linkshared": attr.bool( + default = False, + doc = """\ +If `True`, link the target as a shared library / loadable module instead of an +executable, similar to `cc_binary`'s `linkshared`. The binary has no `main` +entry point and the renamed-entry-point machinery is disabled. + +On most platforms this produces a dynamic library named `lib.so` +(`.dylib` on Apple platforms) suitable for loading with `dlopen` / +`System.loadLibrary` (e.g. an Android JNI library; export functions with +`@_cdecl`). + +When targeting WebAssembly it instead produces a "reactor" module +(`.wasm`, linked with `-mexec-model=reactor`): the module has no +`_start`, runs its initializers via the exported `_initialize`, and exposes +the functions a host instantiates and calls. Force-export those functions by +passing `-Xlinker --export=` (or `-Wl,--export=`) flags in +`linkopts`. +""", + ), + "_wasi_os_constraint": attr.label( + default = Label("@platforms//os:wasi"), + ), + }, ), doc = """\ Compiles and links Swift code into an executable binary. @@ -306,6 +377,9 @@ If you want to create a multi-architecture binary or a bundled application, please use one of the platform-specific application rules in [rules_apple](https://github.com/bazelbuild/rules_apple) instead of `swift_binary`. + +Setting `linkshared = True` links a shared library or (on WebAssembly) a +reactor module instead of an executable; see the `linkshared` attribute. """, exec_groups = { # The `plugins` attribute associates its `exec` transition with this From 6766f8f19075a51d806ee669a37ea555e4bd5c0f Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Sat, 13 Jun 2026 01:06:16 -0400 Subject: [PATCH 03/17] cross_compilation example: a browser web app for the wasm reactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a static site that embeds the `swift_binary(linkshared = True)` wasm reactor and drives it from JavaScript end-to-end: it instantiates the module with a minimal WASI shim, runs the reactor's `_initialize`, calls the exported `greeting_length`/`greeting_into`, reads the string Swift wrote into linear memory, and shows it. - `web/index.html` — the page (served via `:web_app`, which assembles index.html + Reactor.wasm into one directory). - `web/verify.mjs` — the same flow under Node for a headless check. - `web/README.md` + README/table entries. Verified in headless Chrome (shows "Hello from Swift, WebAssembly!") and via the Node/wasmtime headless flow. --- examples/cross_compilation/BUILD.bazel | 20 ++++++ examples/cross_compilation/README.md | 6 ++ examples/cross_compilation/web/README.md | 40 +++++++++++ examples/cross_compilation/web/index.html | 88 +++++++++++++++++++++++ examples/cross_compilation/web/verify.mjs | 58 +++++++++++++++ 5 files changed, 212 insertions(+) create mode 100644 examples/cross_compilation/web/README.md create mode 100644 examples/cross_compilation/web/index.html create mode 100644 examples/cross_compilation/web/verify.mjs diff --git a/examples/cross_compilation/BUILD.bazel b/examples/cross_compilation/BUILD.bazel index 0a16a0049..d12e5c659 100644 --- a/examples/cross_compilation/BUILD.bazel +++ b/examples/cross_compilation/BUILD.bazel @@ -60,6 +60,26 @@ transition_binary( platform = ":wasm32-wasip1", ) +# A static web app embedding the reactor: `index.html` + `Reactor.wasm` in one +# directory. Serve it (e.g. `python3 -m http.server -d +# bazel-bin/examples/cross_compilation/web_app`) and open it — the page calls +# the reactor's exports from JavaScript and shows the greeting Swift produced. +# `web/verify.mjs` does the same headlessly under Node. See `web/README.md`. +genrule( + name = "web_app", + srcs = [ + "web/index.html", + ":Reactor.wasm", + ], + outs = [ + "web_app/index.html", + "web_app/Reactor.wasm", + ], + cmd = "cp $(location web/index.html) $(RULEDIR)/web_app/index.html && " + + "cp $(location :Reactor.wasm) $(RULEDIR)/web_app/Reactor.wasm", + tags = ["manual"], +) + # --------------------------------------------------------------------------- # Android: a JNI shared library, loaded by Kotlin via `System.loadLibrary`. # --------------------------------------------------------------------------- diff --git a/examples/cross_compilation/README.md b/examples/cross_compilation/README.md index 024af6574..a22b4dd06 100644 --- a/examples/cross_compilation/README.md +++ b/examples/cross_compilation/README.md @@ -14,6 +14,7 @@ All targets are tagged `manual` because they download the Swift SDK bundles |---|---|---| | `:Greeter` | `.swiftmodule` + `.a` | A normal `swift_library` reused by both entry points below | | `:Reactor.wasm` | `Reactor.wasm` | A WebAssembly **reactor** (`swift_binary(linkshared)`), no `main`, with exported functions | +| `:web_app` | `web_app/` | A static site embedding `Reactor.wasm`, driven from JS — see [`web/README.md`](web/README.md) | | `:libSwiftJNI.so` | `libSwiftJNI.so` | An Android **JNI shared library** (`swift_binary(linkshared)`) that calls `:Greeter` | ```sh @@ -22,6 +23,11 @@ bazel build //examples/cross_compilation:Reactor.wasm wasmtime run --invoke greeting_length \ bazel-bin/examples/cross_compilation/Reactor.wasm +# WebAssembly in a browser: a static site that calls the reactor from JS. +bazel build //examples/cross_compilation:web_app +python3 -m http.server -d bazel-bin/examples/cross_compilation/web_app 8000 +# …then open http://localhost:8000 (see web/README.md) + # Android JNI shared library: bazel build //examples/cross_compilation:libSwiftJNI.so ``` diff --git a/examples/cross_compilation/web/README.md b/examples/cross_compilation/web/README.md new file mode 100644 index 000000000..03ed9e007 --- /dev/null +++ b/examples/cross_compilation/web/README.md @@ -0,0 +1,40 @@ +# Web app: a Swift WebAssembly reactor in the browser + +A tiny static site that embeds `:Reactor.wasm` (a `swift_binary(linkshared = +True)` reactor) and drives it from JavaScript — the page instantiates the +module, runs the WASI reactor's `_initialize`, calls the exported +`greeting_length` / `greeting_into`, reads the string Swift wrote into linear +memory, and displays it. + +```sh +# Assemble index.html + Reactor.wasm into one directory. +bazel build //examples/cross_compilation:web_app + +# Serve it and open http://localhost:8000 in a browser. +python3 -m http.server -d bazel-bin/examples/cross_compilation/web_app 8000 +``` + +The page shows `“Hello from Swift, WebAssembly!”`. + +### Headless check + +`verify.mjs` runs the same flow under Node (a minimal WASI shim, no browser), so +the example can be verified in CI / from the command line: + +```sh +bazel build //examples/cross_compilation:Reactor.wasm +node examples/cross_compilation/web/verify.mjs \ + bazel-bin/examples/cross_compilation/Reactor.wasm +# -> OK: Swift → WebAssembly greeting verified end-to-end +``` + +### Notes + +- The reactor imports `wasi_snapshot_preview1` for runtime startup; `index.html` + supplies a minimal shim (success stubs, `random_get` via Web Crypto). A real + app would use a WASI polyfill such as `@bjorn3/browser_wasi_shim`. +- `linkshared = True` on a wasm target produces a **reactor** (no `_start`); the + host must call `_initialize()` once before any other export so Swift/C global + initializers run. +- The output buffer is placed in a freshly `grow`n memory page, avoiding any + allocator import. diff --git a/examples/cross_compilation/web/index.html b/examples/cross_compilation/web/index.html new file mode 100644 index 000000000..cc7b1b1c7 --- /dev/null +++ b/examples/cross_compilation/web/index.html @@ -0,0 +1,88 @@ + + + + + + Swift → WebAssembly (rules_swift) + + + +
+

Swift, compiled to WebAssembly

+

A swift_binary(linkshared = True) reactor module + built with rules_swift — instantiated and driven from JavaScript:

+
loading…
+

The text above was produced by the Swift Greeter + library running in WebAssembly, read out of the module's linear memory.

+
+ + + diff --git a/examples/cross_compilation/web/verify.mjs b/examples/cross_compilation/web/verify.mjs new file mode 100644 index 000000000..0934bfdc4 --- /dev/null +++ b/examples/cross_compilation/web/verify.mjs @@ -0,0 +1,58 @@ +// Headless end-to-end check of the WebAssembly reactor, mirroring index.html: +// instantiate with a minimal WASI shim, run `_initialize`, then read the +// greeting that Swift writes into linear memory. Exits non-zero on mismatch. +// +// node examples/cross_compilation/web/verify.mjs \ +// bazel-bin/examples/cross_compilation/Reactor.wasm +// +// (index.html does exactly this in a browser.) + +import { readFileSync } from "node:fs"; +import { webcrypto as crypto } from "node:crypto"; + +const wasmPath = process.argv[2] ?? "bazel-bin/examples/cross_compilation/Reactor.wasm"; +const expected = "Hello from Swift, WebAssembly!"; + +let instance; +const dv = () => new DataView(instance.exports.memory.buffer); +const u8 = () => new Uint8Array(instance.exports.memory.buffer); +const SUCCESS = 0, BADF = 8; +const wasi = { + args_sizes_get: (a, b) => { dv().setUint32(a, 0, true); dv().setUint32(b, 0, true); return SUCCESS; }, + args_get: () => SUCCESS, + environ_sizes_get: (a, b) => { dv().setUint32(a, 0, true); dv().setUint32(b, 0, true); return SUCCESS; }, + environ_get: () => SUCCESS, + fd_fdstat_get: (fd, ptr) => { for (let i = 0; i < 24; i++) dv().setUint8(ptr + i, 0); return SUCCESS; }, + fd_prestat_get: () => BADF, + fd_prestat_dir_name: () => BADF, + fd_close: () => SUCCESS, + fd_read: (fd, iovs, n, nread) => { dv().setUint32(nread, 0, true); return SUCCESS; }, + fd_seek: (fd, off, whence, newOff) => { dv().setUint32(newOff, 0, true); return SUCCESS; }, + fd_write: (fd, iovs, n, nwritten) => { + let written = 0; + for (let i = 0; i < n; i++) written += dv().getUint32(iovs + i * 8 + 4, true); + dv().setUint32(nwritten, written, true); + return SUCCESS; + }, + path_open: () => BADF, + proc_exit: (code) => { throw new Error("proc_exit(" + code + ")"); }, + random_get: (ptr, len) => { crypto.getRandomValues(u8().subarray(ptr, ptr + len)); return SUCCESS; }, +}; + +const bytes = readFileSync(wasmPath); +instance = (await WebAssembly.instantiate(bytes, { wasi_snapshot_preview1: wasi })).instance; +instance.exports._initialize(); + +const length = instance.exports.greeting_length(); +const memory = instance.exports.memory; +const ptr = memory.buffer.byteLength; +memory.grow(Math.ceil((length + 1) / 65536)); +const written = instance.exports.greeting_into(ptr, length + 1); +const greeting = new TextDecoder().decode(new Uint8Array(memory.buffer, ptr, written)); + +console.log("greeting:", JSON.stringify(greeting)); +if (greeting !== expected) { + console.error(`FAIL: expected ${JSON.stringify(expected)}`); + process.exit(1); +} +console.log("OK: Swift → WebAssembly greeting verified end-to-end"); From d26c89d7193aceb3bb53b08a1745706bd056b968 Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Sat, 13 Jun 2026 12:56:40 -0400 Subject: [PATCH 04/17] wasm: link at the global/table bases swiftc uses (fixes -O metadata crash) The Swift SDK wasm toolchain linked binaries without the `--global-base` / `--table-base` flags that `swiftc` always passes to wasm-ld for its own wasm links. Optimized (`-O`) Swift relies on the indirect function table starting at index 4096 where the runtime/codegen expects it; without `--table-base=4096` generic-metadata instantiation reads out of bounds at runtime (a `memory access out of bounds` fault inside `__swift_instantiateGenericMetadata` the moment a generic type's metadata is instantiated). `-Onone` happens to tolerate the default table base, which masked the bug. Add `-Wl,--global-base=4096 -Wl,--table-base=4096` to the wasm toolchain's linkopts so cc-driven links reproduce swiftc's memory/table layout. Verified an optimized (`-c opt`) SwiftUI app that previously crashed on its first generic metadata access now boots and renders; the reactor example is unaffected (`greeting_length` still returns via wasmtime). --- swift/internal/extensions/swift_sdks.bzl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/swift/internal/extensions/swift_sdks.bzl b/swift/internal/extensions/swift_sdks.bzl index 3d86ee10d..9c7bb34d8 100644 --- a/swift/internal/extensions/swift_sdks.bzl +++ b/swift/internal/extensions/swift_sdks.bzl @@ -315,6 +315,16 @@ def _swift_wasm_sdk_impl(repository_ctx): "-lwasi-emulated-mman", "-lwasi-emulated-signal", "-lwasi-emulated-process-clocks", + # Place the linear-memory data and the indirect function table at the + # same bases `swiftc` uses for its own wasm links. The Swift driver + # always passes these to wasm-ld; in particular `--table-base=4096` + # is required — optimized (`-O`) Swift relies on the indirect + # function table starting where the runtime/codegen expects it, and + # without it generic-metadata instantiation reads out of bounds at + # runtime (`__swift_instantiateGenericMetadata` faults). `-Onone` + # happens to tolerate the default base, which masks the bug. + "-Wl,--global-base=4096", + "-Wl,--table-base=4096", ]), os = "wasi", sdkroot = wasi_sdk, From 95c7274e7acd0f075e58f8ce5f981c0aa214f34a Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Sat, 13 Jun 2026 14:52:05 -0400 Subject: [PATCH 05/17] ci: build the cross_compilation example (wasm + Android) The //examples/cross_compilation targets are tagged `manual` (they download the Swift SDK bundles and the Android NDK), so the `//examples/...` wildcard the other tasks build skips them and the Swift-SDK cross-compilation toolchains were never exercised in CI. Add a dedicated macOS task that builds the wasm reactor, the web app, and the Android JNI shared library explicitly, so a break in the toolchain wiring or link flags is caught in presubmit. --- .bazelci/presubmit.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 5626e8a51..437073707 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -62,6 +62,22 @@ tasks: bazel: last_green <<: *mac_common + macos_cross_compilation: + name: "Cross-compilation (wasm + Android)" + platform: macos_arm64 + xcode_version: "26.2" + bazel: latest + # The //examples/cross_compilation targets are tagged `manual` (they fetch + # the Swift SDK bundles and, for Android, the NDK), so they are excluded + # from the `//examples/...` wildcard the other tasks build. List them + # explicitly here so the Swift-SDK cross-compilation toolchains are exercised + # in CI. Build-only: the trivial reactor exercises the link path, while + # runtime behavior is covered downstream by real consumers. + build_targets: + - "//examples/cross_compilation:Reactor.wasm" + - "//examples/cross_compilation:web_app" + - "//examples/cross_compilation:libSwiftJNI.so" + macos_latest_shell_scripts: name: "macOS shell tests" platform: macos_arm64 From 5c45529b07ddf49c6dfbdf7bb33e7399d1e01315 Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Sat, 13 Jun 2026 15:27:14 -0400 Subject: [PATCH 06/17] =?UTF-8?q?docs:=20WINDOWS.md=20=E2=80=94=20native?= =?UTF-8?q?=20Windows=20host=20toolchain=20state=20and=20checklist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capture how building Swift on Windows works in rules_swift (the existing host autoconfiguration toolchain, discovered from an installed Swift + Visual Studio, in the same vein as the Xcode/apple_support model), the prerequisites, a verification checklist, and the known gaps (CI Windows task is commented out, so the path needs verifying rather than implementing). Orthogonal to the Swift-SDK cross-compilation in this branch; recorded here to pick the work up on an actual Windows machine. --- WINDOWS.md | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 WINDOWS.md diff --git a/WINDOWS.md b/WINDOWS.md new file mode 100644 index 000000000..a4d576e7e --- /dev/null +++ b/WINDOWS.md @@ -0,0 +1,119 @@ +# Building Swift on Windows with rules_swift + +This document is a working note on the state of **native Windows** support in +rules_swift and what it takes to build Swift code on a Windows host. + +## TL;DR + +Building Swift **on Windows, for Windows** is the same model as building for +Apple platforms on macOS: you do it **natively on the host**, using a Swift +toolchain that is *installed on the machine* (not downloaded by Bazel), and +Bazel achieves reproducibility by tracking/hashing the installed binaries it +invokes — exactly how `apple_support` treats Xcode. + +This is **orthogonal to the cross-compilation work in this PR.** The +`swift.wasm_sdk` / `swift.android_sdk` Swift-SDK mechanism is a *convenience* for +the targets that can be cross-compiled from any host (WebAssembly, Android, +static Linux). Apple and Windows are each built on their own platform, so they +use a **host toolchain**, not a downloaded destination-SDK. There is no official +Windows "Swift SDK" artifact bundle, and that's expected — Windows is a host +target, not a cross destination. + +## What already exists upstream + +rules_swift already has a Windows host toolchain path; it is **not** something +this PR needs to add: + +- **Autoconfiguration** — `swift/internal/swift_autoconfiguration.bzl` + (`_create_windows_toolchain`) discovers a locally-installed Swift toolchain + when `swiftc.exe` is on `PATH`. It derives the toolchain `root` from + `swiftc.exe`'s location, reads `SDKROOT` / `Path` / `ProgramData` from the + environment, and reads `XCTEST_VERSION` from the installed SDK's `Info.plist`. + On a non-Windows host (no `swiftc.exe` on `PATH`) it emits a no-op comment, so + it is safe cross-platform. +- **A Windows `swift_toolchain`** is generated with `os = "windows"`, + `arch = "x86_64"`, `tool_executable_suffix = ".exe"`, and the discovered + `root` / `sdkroot` / `env`. +- **Windows link flags** — `swift/toolchains/swift_toolchain.bzl` + (`_swift_windows_linkopts_cc_info`) supplies the MSVC-style linker flags: + - `-LIBPATH:/usr/lib/swift/windows/` + - `-LIBPATH:<...>/Library/XCTest-/usr/lib/swift/windows/` + - the runtime start object `/usr/lib/swift/windows//swiftrt.obj` +- **A registered toolchain** — `swift/toolchains/BUILD` registers + `windows-swift-toolchain-x86_64` (`exec`/`target` = `@platforms//os:windows` + + `@platforms//cpu:x86_64`) pointing at `@rules_swift_local_config//:windows-toolchain`. + +The C/C++ side is handled by Bazel's built-in **MSVC C++ toolchain**, which +discovers Visual Studio via `vswhere` / `BAZEL_VC`. rules_swift's Windows +`swift_toolchain` composes with that cc toolchain for linking. + +So the expectation on a properly-provisioned Windows box is that a plain +`swift_library` / `swift_binary` builds with no extra configuration. + +## Prerequisites on the Windows machine + +1. **Visual Studio 2022+ (Build Tools is enough)** — provides MSVC and the + Windows SDK (the C runtime, Win32 headers/libs, `link.exe`). This is what + Bazel's MSVC cc toolchain and the Swift linker consume. +2. **Swift for Windows** (the swift.org installer) — installs `swiftc.exe` and + the Swift Windows runtime/SDK, and sets `SDKROOT` (and the module maps for + `ucrt` / `winsdk` / `visualc`). Use the release that matches anything you + pin elsewhere. +3. **Run Bazel from an environment that has both** — i.e. a "x64 Native Tools + Command Prompt for VS" (or a shell that has sourced `vcvars64.bat`) **with the + Swift installer's environment** also present, so `swiftc.exe` is on `PATH` and + `SDKROOT` / `Path` / `ProgramData` are set when the repository rule runs. +4. **Python 3** on `PATH` — the autoconfiguration shells out to it to read the + XCTest version from the SDK `Info.plist`. + +## How to build / verify + +From the rules_swift checkout, in a provisioned VS+Swift shell: + +```bat +bazel build //examples/... +bazel test //test/... +``` + +A minimal smoke target is the embedded/simple `swift_binary` examples. If the +host toolchain resolves, `bazel cquery 'config(//examples/...)'` and the build +should select `windows-swift-toolchain-x86_64`. + +## Known gaps / things to verify (the actual PC work) + +The Windows scaffolding exists but is **not currently exercised in CI** — the +`windows_last_green` task in `.bazelci/presubmit.yml` is commented out, and the +`windows_common` config only builds `//tools/...`. So treat this as "verify and +fix bit-rot," not "implement from scratch." Concrete things to check: + +1. **Does autoconfiguration resolve cleanly** with a current Swift-for-Windows + layout? The `Info.plist` path math + (`SDKROOT/../../../Info.plist`) and the `usr/lib/swift/windows/` layout + may have shifted across Swift releases. +2. **End-to-end link** of a `swift_binary` (does `swiftrt.obj` + the `-LIBPATH:` + flags + the MSVC cc toolchain produce a runnable `.exe`?). +3. **`linkshared` → `.dll`.** Confirm `swift_binary(linkshared = True)` produces + a proper Windows DLL (with an import lib) — the same attribute used for the + Android `.so` / wasm reactor in this PR. This is the path a SwiftUI-on-Windows + renderer would consume (see the consumer-side counterpart doc). +4. **arm64 Windows.** Only `x86_64` is registered; `aarch64` Windows would be an + additive toolchain entry. +5. **XCTest** discovery/version on Windows (the `XCTest-` LIBPATH). +6. **Re-enable a Windows CI task** once the above passes, even if scoped to a + small example, so it doesn't regress again. + +## Hermeticity + +This follows the Xcode model: the toolchain is discovered from the environment +and referenced by absolute `root` / `sdkroot` paths. To make the *build outputs* +reproducible despite the external install, Bazel can digest the referenced +toolchain/SDK files (the binaries and libraries actually invoked), the same way +`apple_support` tracks Xcode. Tightening input tracking is a follow-up +refinement, not a blocker to building. + +## Relationship to this PR + +Nothing here depends on the Swift-SDK cross-compilation changes, and vice +versa. This note lives on the branch only to capture the current Windows-host +state and the verification checklist for picking the work up on an actual +Windows machine. From 322e8ee514af6940755a608ebecd47aeb8e34d13 Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Sat, 13 Jun 2026 19:19:15 -0400 Subject: [PATCH 07/17] Build and test Swift natively on Windows Work through the WINDOWS.md checklist on a native Windows host (Swift 6.3.2 + MSVC) and fix the bit-rot and missing Windows code paths it surfaced, so that swift_binary, swift_library, swift_binary(linkshared) and swift_test all build and run. Autoconfiguration: - Skip the Microsoft Store python3.exe execution-alias stub and probe for a working interpreter when reading the SDK Info.plist. - Normalize SDKROOT (forward slashes, no trailing separator) so it is valid inside the Python snippet that reads XCTEST_VERSION. - Detect the host CPU instead of hardcoding x86_64. Toolchain: - Don't require a clang CC toolchain on Windows; MSVC (msvc-cl) is expected. - Use MSVC /ALTERNATENAME instead of GNU ld --defsym for the entry point. - Emit the -msvc target-triple environment so swift-symbolgraph-extract can load modules built for *-windows-msvc. - Pass the XCTest include paths to the symbol-graph-extract action. - Suppress LNK4217 (benign for statically linked dllimport symbols). - Understand the aarch64 library / bin64a layout and register an aarch64 Windows toolchain. swift_test / test discovery: - Port tools/test_observer to Windows: SRWLOCK locking, GetProcAddress-based swift-testing entry point lookup, and a swift-corelibs XCTest runner shared with Linux (renamed from LinuxXCTestRunner). - Run the test discovery tool with the Swift runtime on PATH. Worker / general: - Make the persistent worker's filesystem operations long-path (\?\) aware; the _swift_incremental storage area exceeds MAX_PATH. - Sanitize spaces out of derived object paths so the MSVC archiver/linker response files parse (e.g. swift-argument-parser's "Parsable Properties"). - Disable worker sandboxing on Windows (build:windows in .bazelrc). Examples / CI / docs: - Add a shared_library linkshared -> .dll example. - Re-enable a Windows CI task that builds the examples and runs the xctest. - Update WINDOWS.md with the verified status. --- .bazelci/presubmit.yml | 31 +++++-- .bazelrc | 6 ++ WINDOWS.md | 92 +++++++++++++------ examples/xplatform/shared_library/BUILD | 12 +++ .../xplatform/shared_library/greeting.swift | 18 ++++ swift/internal/compiling.bzl | 9 +- swift/internal/swift_autoconfiguration.bzl | 66 ++++++++++--- swift/swift_test.bzl | 6 ++ swift/toolchains/BUILD | 15 +++ swift/toolchains/swift_toolchain.bzl | 40 +++++++- tools/test_observer/BUILD | 2 +- tools/test_observer/Locked.swift | 57 ++++++++++-- ....swift => SwiftCorelibsXCTestRunner.swift} | 12 ++- tools/test_observer/SwiftTestingRunner.swift | 51 +++++++--- tools/worker/work_processor.cc | 46 +++++++--- 15 files changed, 374 insertions(+), 89 deletions(-) create mode 100644 examples/xplatform/shared_library/BUILD create mode 100644 examples/xplatform/shared_library/greeting.swift rename tools/test_observer/{LinuxXCTestRunner.swift => SwiftCorelibsXCTestRunner.swift} (89%) diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 437073707..0e6ab7ae3 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -39,12 +39,16 @@ x_defaults: - "-//test:output_file_map_default" windows_common: &windows_common platform: windows - build_flags: - # Override 'sandboxed' strategy set in .bazelrc because it's not - # available on Windows - - "--strategy=SwiftCompile=" build_targets: - "//tools/..." + # Cross-platform Swift examples that exercise the Windows host toolchain: + # a `swift_binary` executable and a `linkshared` Windows DLL. + - "//examples/xplatform/hello_world" + - "//examples/xplatform/shared_library" + test_targets: + # Exercises XCTest discovery, the test runner, and `swift_test` execution + # on Windows. + - "//examples/xplatform/xctest" tasks: macos_latest: @@ -113,11 +117,20 @@ tasks: - "curl https://download.swift.org/swift-${SWIFT_VERSION}-release/ubuntu2204/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE-ubuntu22.04.tar.gz | tar xvz --strip-components=1 -C $SWIFT_HOME" <<: *linux_common - # TODO: re-enable when Windows in Bazel CI is properly configured for Swift. - # windows_last_green: - # name: "Last Green Bazel" - # bazel: last_green - # <<: *windows_common + windows: + name: "Current LTS" + bazel: latest + environment: + SWIFT_VERSION: "6.0.3" + # Install the Swift for Windows toolchain before building. The swift.org + # installer is a self-extracting bundle that supports an unattended install + # and sets `SDKROOT`/`Path` machine-wide, which the Swift autoconfiguration + # repository rule reads to discover the toolchain. Visual Studio (MSVC) is + # already present on the Bazel CI Windows image and is used for linking. + batch_commands: + - "curl -sSL -o %TEMP%\\swift-installer.exe https://download.swift.org/swift-%SWIFT_VERSION%-release/windows10/swift-%SWIFT_VERSION%-RELEASE/swift-%SWIFT_VERSION%-RELEASE-windows10.exe" + - "%TEMP%\\swift-installer.exe -q" + <<: *windows_common doc_tests: name: "Doc tests" diff --git a/.bazelrc b/.bazelrc index 0f9e3bf81..509ba4375 100644 --- a/.bazelrc +++ b/.bazelrc @@ -26,6 +26,12 @@ common:linux --repo_env=CC=clang build:linux --cxxopt='-std=c++17' --host_cxxopt='-std=c++17' common:linux --//test:apple_build_tests=False +# Worker sandboxing copies the worker into a sandbox exec root and cleans it +# between invocations. On Windows a running/recently-run executable cannot be +# deleted, so that cleanup fails with "Permission denied". Run Swift workers +# unsandboxed on Windows. +build:windows --noworker_sandboxing + # This C2K warning causes zlib to fail to compile. # There is an open issue about it on the zlib repository here: # https://github.com/madler/zlib/issues/633 diff --git a/WINDOWS.md b/WINDOWS.md index a4d576e7e..55a7adc0c 100644 --- a/WINDOWS.md +++ b/WINDOWS.md @@ -64,43 +64,77 @@ So the expectation on a properly-provisioned Windows box is that a plain Swift installer's environment** also present, so `swiftc.exe` is on `PATH` and `SDKROOT` / `Path` / `ProgramData` are set when the repository rule runs. 4. **Python 3** on `PATH` — the autoconfiguration shells out to it to read the - XCTest version from the SDK `Info.plist`. + XCTest version from the SDK `Info.plist`. A real interpreter is required; the + Microsoft Store `python3.exe` execution-alias stub is detected and skipped. +5. **`bash` on `PATH` and `BAZEL_SH` set** (e.g. Git for Windows' `bash.exe`) — + needed only for `bazel test`, whose generic test wrapper is a shell script. ## How to build / verify From the rules_swift checkout, in a provisioned VS+Swift shell: ```bat -bazel build //examples/... -bazel test //test/... +set BAZEL_SH=C:\Program Files\Git\bin\bash.exe +bazel build //examples/xplatform/hello_world //examples/xplatform/shared_library +bazel test //examples/xplatform/xctest ``` -A minimal smoke target is the embedded/simple `swift_binary` examples. If the -host toolchain resolves, `bazel cquery 'config(//examples/...)'` and the build -should select `windows-swift-toolchain-x86_64`. - -## Known gaps / things to verify (the actual PC work) - -The Windows scaffolding exists but is **not currently exercised in CI** — the -`windows_last_green` task in `.bazelci/presubmit.yml` is commented out, and the -`windows_common` config only builds `//tools/...`. So treat this as "verify and -fix bit-rot," not "implement from scratch." Concrete things to check: - -1. **Does autoconfiguration resolve cleanly** with a current Swift-for-Windows - layout? The `Info.plist` path math - (`SDKROOT/../../../Info.plist`) and the `usr/lib/swift/windows/` layout - may have shifted across Swift releases. -2. **End-to-end link** of a `swift_binary` (does `swiftrt.obj` + the `-LIBPATH:` - flags + the MSVC cc toolchain produce a runnable `.exe`?). -3. **`linkshared` → `.dll`.** Confirm `swift_binary(linkshared = True)` produces - a proper Windows DLL (with an import lib) — the same attribute used for the - Android `.so` / wasm reactor in this PR. This is the path a SwiftUI-on-Windows - renderer would consume (see the consumer-side counterpart doc). -4. **arm64 Windows.** Only `x86_64` is registered; `aarch64` Windows would be an - additive toolchain entry. -5. **XCTest** discovery/version on Windows (the `XCTest-` LIBPATH). -6. **Re-enable a Windows CI task** once the above passes, even if scoped to a - small example, so it doesn't regress again. +`hello_world` is the minimal `swift_binary` smoke target, `shared_library` +exercises the `linkshared` → `.dll` path, and `xctest` exercises `swift_test` +(XCTest discovery, the runner, and execution). If the host toolchain resolves, +the build selects `windows-swift-toolchain-x86_64`. + +## Status (verified on a native Windows host) + +The checklist below was worked through end-to-end on a real Windows 11 host +(Swift 6.3.2 for Windows + Visual Studio 2022 Build Tools / MSVC 14.44, Bazel +9.1.1). Verifying it surfaced several pieces of bit-rot and a handful of +genuinely missing Windows code paths; all are fixed on this branch. + +1. **Autoconfiguration resolves cleanly.** ✅ The `Info.plist` path math and the + `usr/lib/swift/windows/` layout still match a current Swift release. + Two latent bugs were fixed, both of which had left `xctest_version` empty: + - `_get_python_bin` returned the Microsoft Store `python3.exe` *execution + alias* (a stub that exits nonzero) instead of a real interpreter. It now + probes candidates and skips non-working ones. + - `SDKROOT` from the environment ends in a backslash, which was interpolated + into a Python raw-string literal (`r'...\'`) — a syntax error. The SDK root + is now normalized (forward slashes, no trailing separator) before use. +2. **End-to-end `swift_binary` link.** ✅ `//examples/xplatform/hello_world` + builds to a runnable `.exe` and prints "Hello, world!". Two fixes were + required: the toolchain rejected the MSVC cc toolchain (`msvc-cl`) because of + a hard `clang`-only check (now skipped on Windows), and the entry-point alias + used GNU `ld`'s `--defsym`, which MSVC `link.exe` rejects (now + `/ALTERNATENAME` on Windows). +3. **`linkshared` → `.dll`.** ✅ `swift_binary(linkshared = True)` produces a + Windows `.dll` plus an import `.lib`; see the new + `//examples/xplatform/shared_library` example. +4. **arm64 Windows.** ⚠️ Implemented but **unverified** (no arm64 host was + available). Autoconfiguration now detects the host CPU instead of hardcoding + `x86_64`, the toolchain understands the `aarch64` library/`bin64a` layout, + and a `windows-swift-toolchain-aarch64` toolchain is registered. +5. **XCTest.** ✅ The `XCTest-` `LIBPATH`/`-I` paths resolve. + `swift_test` runs end-to-end (`//examples/xplatform/xctest` passes). Getting + there required: adding the XCTest include paths to the symbol-graph-extract + action (test discovery), emitting the `-msvc` target-triple environment (the + symbol-graph tool matches the module triple exactly), porting the + `//tools/test_observer` runner to Windows (`SRWLOCK`, `GetProcAddress`, a + swift-corelibs XCTest runner shared with Linux), running the discovery tool + with the Swift runtime on `PATH`, sanitizing spaces out of object paths + (`lib.exe`/`link.exe` response-file parsing), making the persistent worker + long-path (`\\?\`) aware, and suppressing the benign `LNK4217` that static + linking of `dllimport` symbols produces. +6. **Windows CI.** ⚠️ Re-enabled in `.bazelci/presubmit.yml` (a `windows` task + that builds the Swift examples and runs the `xctest` test). The Swift-install + prologue and BazelCI Windows image provisioning are the one piece **not** + validated here, since that requires the CI infrastructure rather than a local + host; it may need adjustment when first run. + +A general (not Windows-only) requirement also surfaced: `bazel test` on Windows +needs a `bash` for the test wrapper, so `BAZEL_SH` must point at a `bash.exe` +(e.g. Git for Windows). `worker` sandboxing is disabled on Windows in `.bazelrc` +(`build:windows --noworker_sandboxing`) because a running executable cannot be +deleted to clean the sandbox. ## Hermeticity diff --git a/examples/xplatform/shared_library/BUILD b/examples/xplatform/shared_library/BUILD new file mode 100644 index 000000000..ca2e1bd7f --- /dev/null +++ b/examples/xplatform/shared_library/BUILD @@ -0,0 +1,12 @@ +load("//swift:swift_binary.bzl", "swift_binary") + +licenses(["notice"]) + +# `linkshared` links a native dynamic library instead of an executable: a +# `.dylib` on Apple platforms, a `.so` on Linux/Android, and a `.dll` (plus an +# import `.lib`) on Windows. +swift_binary( + name = "shared_library", + srcs = ["greeting.swift"], + linkshared = True, +) diff --git a/examples/xplatform/shared_library/greeting.swift b/examples/xplatform/shared_library/greeting.swift new file mode 100644 index 000000000..41a9a8e71 --- /dev/null +++ b/examples/xplatform/shared_library/greeting.swift @@ -0,0 +1,18 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@_cdecl("greeting") +public func greeting() -> Int32 { + return 42 +} diff --git a/swift/internal/compiling.bzl b/swift/internal/compiling.bzl index 0db3d6b62..208053449 100644 --- a/swift/internal/compiling.bzl +++ b/swift/internal/compiling.bzl @@ -1667,7 +1667,14 @@ def _declare_per_source_output_file(actions, extension, target_name, src): The declared `File`. """ objs_dir = "{}_objs".format(target_name) - owner_rel_path = owner_relative_path(src) + + # Spaces in object file paths break response-file parsing for the Windows + # archiver and linker (`lib.exe`/`link.exe`), which treat an unquoted space + # as an argument separator. Sanitize them in the derived output path so that + # targets whose sources live in directories containing spaces (for example + # swift-argument-parser's "Parsable Properties") can be archived and linked + # on Windows. Paths without spaces are unaffected. + owner_rel_path = owner_relative_path(src).replace(" ", "_") basename = paths.basename(owner_rel_path) dirname = paths.join(objs_dir, paths.dirname(owner_rel_path)) diff --git a/swift/internal/swift_autoconfiguration.bzl b/swift/internal/swift_autoconfiguration.bzl index 35b183cb8..fa3ed5d4d 100644 --- a/swift/internal/swift_autoconfiguration.bzl +++ b/swift/internal/swift_autoconfiguration.bzl @@ -179,6 +179,22 @@ def _normalized_linux_cpu(cpu): return "x86_64" return cpu +def _normalized_windows_cpu(cpu): + """Normalizes a host CPU name to the value Swift uses on Windows. + + The returned value is used both as the toolchain's `arch` and as the + architecture component of the Swift SDK's library layout (for example + `usr/lib/swift/windows/x86_64`) and the target triple. + """ + cpu = cpu.lower() + if cpu in ("amd64", "x86_64", "x64"): + return "x86_64" + if cpu in ("arm64", "aarch64"): + return "aarch64" + if cpu in ("x86", "i686"): + return "i686" + return cpu + def _resolve_toolchain_root(repository_ctx, swiftc_path): """Returns the Swift toolchain root directory for `swiftc_path`. @@ -269,15 +285,27 @@ xcode_swift_toolchain( ]), ) +def _python_executable_works(repository_ctx, python_bin): + """Returns True if `python_bin` is a real, runnable Python interpreter. + + On Windows, `python3.exe`/`python.exe` found on `PATH` are frequently the + Microsoft Store "App execution alias" stubs rather than real interpreters: + when run non-interactively they print a message pointing at the Store and + exit nonzero. Probe the candidate so those stubs are skipped in favor of a + working interpreter later on `PATH`. + """ + if not python_bin: + return False + result = repository_ctx.execute([python_bin, "-c", "print('ok')"]) + return result.return_code == 0 and result.stdout.strip() == "ok" + def _get_python_bin(repository_ctx): if "PYTHON_BIN_PATH" in repository_ctx.os.environ: return repository_ctx.os.environ.get("PYTHON_BIN_PATH").strip() - out = repository_ctx.which("python3.exe") - if out: - return out - out = repository_ctx.which("python.exe") - if out: - return out + for name in ("python3.exe", "python.exe", "python3", "python"): + candidate = repository_ctx.which(name) + if _python_executable_works(repository_ctx, candidate): + return candidate return None def _create_windows_toolchain(*, repository_ctx): @@ -294,6 +322,7 @@ Swift toolchain. """ root = path_to_swiftc.dirname.dirname + arch = _normalized_windows_cpu(repository_ctx.os.arch) enabled_features = [ SWIFT_FEATURE_CODEVIEW_DEBUG_INFO, SWIFT_FEATURE_DECLARE_SWIFTSOURCEINFO, @@ -305,11 +334,25 @@ Swift toolchain. disabled_features = [] version_file, parsed_version = _write_swift_version(repository_ctx, path_to_swiftc) + + # Normalize SDKROOT to forward slashes with no trailing separator: the raw + # environment value typically ends in a backslash, which is both invalid at + # the end of a Python raw-string literal (used below) and produces doubled + # separators when joined. + sdkroot = repository_ctx.os.environ["SDKROOT"].replace("\\", "/").rstrip("/") + + # The platform `Info.plist` (which records the bundled XCTest version) sits + # three levels above the SDK root, i.e. `/Info.plist`. + info_plist = sdkroot + "/../../../Info.plist" + python_bin = _get_python_bin(repository_ctx) + if not python_bin: + fail("Could not find a working Python 3 interpreter on PATH; it is " + + "required to read the XCTest version from the Swift SDK's Info.plist.") xctest_version = repository_ctx.execute([ - _get_python_bin(repository_ctx), + python_bin, "-c", - "import os, plistlib; " + - "print(plistlib.loads(open(os.path.join(r'{}', '..', '..', '..', 'Info.plist'), 'rb').read(), fmt=plistlib.FMT_XML)['DefaultProperties']['XCTEST_VERSION'])".format(repository_ctx.os.environ["SDKROOT"]), + "import plistlib; " + + "print(plistlib.load(open(r'{}', 'rb'))['DefaultProperties']['XCTEST_VERSION'])".format(info_plist), ]) env = { @@ -320,7 +363,7 @@ Swift toolchain. return """\ swift_toolchain( name = "windows-toolchain", - arch = "x86_64", + arch = "{arch}", features = [{features}], os = "windows", root = "{root}", @@ -332,11 +375,12 @@ swift_toolchain( xctest_version = "{xctest_version}", ) """.format( + arch = arch, features = ", ".join(['"{}"'.format(feature) for feature in enabled_features] + ['"-{}"'.format(feature) for feature in disabled_features]), root = root, env = env, parsed_version = parsed_version, - sdkroot = repository_ctx.os.environ["SDKROOT"].replace("\\", "/"), + sdkroot = sdkroot, xctest_version = xctest_version.stdout.rstrip(), version_file = version_file, ) diff --git a/swift/swift_test.bzl b/swift/swift_test.bzl index f6065bfa1..25cd3d8a9 100644 --- a/swift/swift_test.bzl +++ b/swift/swift_test.bzl @@ -94,6 +94,7 @@ def _generate_test_discovery_srcs( *, actions, deps, + env = {}, name, objc_test_discovery, owner_module_name, @@ -109,6 +110,9 @@ def _generate_test_discovery_srcs( Args: actions: The context's actions object. deps: The list of direct dependencies of the test target. + env: Environment variables to set when running the discovery tool. On + Windows this must include a `Path` that contains the Swift runtime + DLLs, otherwise the (Swift) discovery executable fails to launch. name: The name of the target being built, which will be used to derive the basename of the directory containing the generated files. objc_test_discovery: If `True`, the runner should use Objective-C-based @@ -197,6 +201,7 @@ def _generate_test_discovery_srcs( actions.run( arguments = [args], + env = env, executable = test_discoverer, exec_group = _DISCOVER_TESTS_EXEC_GROUP, inputs = inputs, @@ -411,6 +416,7 @@ def _swift_test_impl(ctx): discovery_srcs = _generate_test_discovery_srcs( actions = ctx.actions, deps = ctx.attr.deps, + env = toolchains.swift.test_configuration.env, name = ctx.label.name, objc_test_discovery = objc_test_discovery, owner_module_name = module_name, diff --git a/swift/toolchains/BUILD b/swift/toolchains/BUILD index 89d84641d..df9328c65 100644 --- a/swift/toolchains/BUILD +++ b/swift/toolchains/BUILD @@ -138,6 +138,21 @@ toolchain( visibility = ["//visibility:public"], ) +toolchain( + name = "windows-swift-toolchain-aarch64", + exec_compatible_with = [ + "@platforms//os:windows", + "@platforms//cpu:aarch64", + ], + target_compatible_with = [ + "@platforms//os:windows", + "@platforms//cpu:aarch64", + ], + toolchain = "@rules_swift_local_config//:windows-toolchain", + toolchain_type = "//toolchains:toolchain_type", + visibility = ["//visibility:public"], +) + # Consumed by Bazel integration tests. filegroup( name = "for_bazel_tests", diff --git a/swift/toolchains/swift_toolchain.bzl b/swift/toolchains/swift_toolchain.bzl index a9e659ee6..905c3e303 100644 --- a/swift/toolchains/swift_toolchain.bzl +++ b/swift/toolchains/swift_toolchain.bzl @@ -267,6 +267,7 @@ def _all_action_configs(os, arch, target_triple, sdkroot, xctest_version, additi actions = all_compile_action_names() + [ SWIFT_ACTION_DUMP_AST, SWIFT_ACTION_PRECOMPILE_C_MODULE, + SWIFT_ACTION_SYMBOL_GRAPH_EXTRACT, ], configurators = [ add_arg( @@ -294,6 +295,7 @@ def _all_action_configs(os, arch, target_triple, sdkroot, xctest_version, additi actions = all_compile_action_names() + [ SWIFT_ACTION_DUMP_AST, SWIFT_ACTION_PRECOMPILE_C_MODULE, + SWIFT_ACTION_SYMBOL_GRAPH_EXTRACT, ], configurators = [ add_arg( @@ -364,6 +366,13 @@ def _swift_windows_linkopts_cc_info( "-LIBPATH:{}".format(platform_lib_dir), "-LIBPATH:{}".format(paths.join(sdkroot, "..", "..", "Library", "XCTest-{}".format(xctest_version), "usr", "lib", "swift", "windows", arch)), runtime_object_path, + # Swift marks references to symbols in other modules (for example the + # type metadata accessors a generated test runner references via + # `@testable import`) as `dllimport`. Bazel links everything statically, + # so those symbols resolve locally and `link.exe` emits LNK4217. The + # warning is benign for static linking; suppress it so it is not fatal + # under `/WX` (treat-warnings-as-errors). + "-IGNORE:4217", ] return CcInfo( @@ -484,6 +493,17 @@ def _entry_point_linkopts_provider(*, entry_point_name): linkopts = ["-Wl,--defsym,main={}".format(entry_point_name)], ) +def _windows_entry_point_linkopts_provider(*, entry_point_name): + """Returns linkopts to customize the entry point of a binary on Windows. + + MSVC `link.exe` does not understand the GNU `ld` `--defsym` alias used on + other platforms; `/ALTERNATENAME` is the equivalent, resolving the + CRT-referenced `main` symbol to the renamed Swift entry point. + """ + return struct( + linkopts = ["/ALTERNATENAME:main={}".format(entry_point_name)], + ) + def _parse_target_system_name(*, arch, os, target_system_name): """Returns the target system name set by the CC toolchain or attempts to create one based on the OS and arch.""" @@ -492,6 +512,13 @@ def _parse_target_system_name(*, arch, os, target_system_name): if os == "linux": return "%s-unknown-linux-gnu" % arch + elif os == "windows": + # The MSVC cc toolchain reports a `target_gnu_system_name` of "local", + # so synthesize the triple. The `-msvc` environment is required: while + # `swiftc` defaults to it, `swift-symbolgraph-extract` (used for XCTest + # test discovery) matches the module layout's triple exactly and fails + # to load modules built for `*-windows-msvc` if it is omitted. + return "%s-unknown-windows-msvc" % arch else: return "%s-unknown-%s" % (arch, os) @@ -507,7 +534,12 @@ def _swift_toolchain_impl(ctx): target_triples.parse(ctx.var.get("CC_TARGET_TRIPLE") or target_system_name), ) - if "clang" not in cc_toolchain.compiler: + # On Windows the Swift toolchain composes with Bazel's MSVC C++ toolchain + # (`msvc-cl`): `swiftc` uses its bundled clang for the clang importer and + # links via MSVC `link.exe`, so a clang CC toolchain is neither present nor + # required. Elsewhere (Linux), Swift drives the configured cc toolchain for + # C/C++ interop and linking, which must be clang. + if ctx.attr.os != "windows" and "clang" not in cc_toolchain.compiler: fail("Swift requires the configured CC toolchain use clang. " + "Either use the locally installed LLVM by setting `CC=clang` in your environment " + "before invoking Bazel, or configure a Bazel LLVM CC toolchain. " + @@ -615,7 +647,7 @@ def _swift_toolchain_impl(ctx): bindir = "bin64" elif ctx.attr.arch == "i686": bindir = "bin32" - elif ctx.attr.arch == "arm64": + elif ctx.attr.arch in ("aarch64", "arm64"): bindir = "bin64a" else: fail("unsupported arch `{}`".format(ctx.attr.arch)) @@ -641,7 +673,9 @@ def _swift_toolchain_impl(ctx): cross_import_overlays = collect_cross_import_overlays(ctx.attr.cross_import_overlays), debug_outputs_provider = None, developer_dirs = [], - entry_point_linkopts_provider = _entry_point_linkopts_provider, + entry_point_linkopts_provider = ( + _windows_entry_point_linkopts_provider if ctx.attr.os == "windows" else _entry_point_linkopts_provider + ), feature_allowlists = [ target[SwiftFeatureAllowlistInfo] for target in ctx.attr.feature_allowlists diff --git a/tools/test_observer/BUILD b/tools/test_observer/BUILD index 241cd7a39..0c9ead01b 100644 --- a/tools/test_observer/BUILD +++ b/tools/test_observer/BUILD @@ -10,11 +10,11 @@ swift_library( "BazelXMLTestObserver.swift", "Concurrency.swift", "JSON.swift", - "LinuxXCTestRunner.swift", "Locked.swift", "ObjectiveCXCTestRunner.swift", "ShardingFilteringTestCollector.swift", "StringInterpolation+XMLEscaping.swift", + "SwiftCorelibsXCTestRunner.swift", "SwiftTestingRunner.swift", "XUnitTestRecorder.swift", ], diff --git a/tools/test_observer/Locked.swift b/tools/test_observer/Locked.swift index 06a897817..51b44b436 100644 --- a/tools/test_observer/Locked.swift +++ b/tools/test_observer/Locked.swift @@ -16,10 +16,53 @@ import Darwin #elseif canImport(Glibc) import Glibc +#elseif canImport(WinSDK) + import WinSDK #else #error("Unsupported platform") #endif +// The platform lock primitive stored alongside the value. POSIX platforms use a +// `pthread_mutex_t`; Windows uses a slim reader/writer lock (`SRWLOCK`), which +// needs no explicit destruction. +#if canImport(WinSDK) + private typealias LockPrimitive = SRWLOCK +#else + private typealias LockPrimitive = pthread_mutex_t +#endif + +private func _lockInitialize(_ lock: UnsafeMutablePointer) { + #if canImport(WinSDK) + InitializeSRWLock(lock) + #else + _ = pthread_mutex_init(lock, nil) + #endif +} + +private func _lockDestroy(_ lock: UnsafeMutablePointer) { + #if canImport(WinSDK) + // `SRWLOCK`s do not require destruction. + #else + _ = pthread_mutex_destroy(lock) + #endif +} + +private func _lockAcquire(_ lock: UnsafeMutablePointer) { + #if canImport(WinSDK) + AcquireSRWLockExclusive(lock) + #else + _ = pthread_mutex_lock(lock) + #endif +} + +private func _lockRelease(_ lock: UnsafeMutablePointer) { + #if canImport(WinSDK) + ReleaseSRWLockExclusive(lock) + #else + _ = pthread_mutex_unlock(lock) + #endif +} + /// A wrapper around a value that can be accessed safely from multiple threads in synchronized /// contexts. /// @@ -27,10 +70,10 @@ /// as XCTest's observer, both are called in synchronous contexts only, but we don't know what /// thread the calls are coming from. public struct Locked: Sendable where Value: Sendable { - private final class _Storage: ManagedBuffer { + private final class _Storage: ManagedBuffer { deinit { withUnsafeMutablePointerToElements { lock in - _ = pthread_mutex_destroy(lock) + _lockDestroy(lock) } } } @@ -38,9 +81,9 @@ public struct Locked: Sendable where Value: Sendable { // Swift 6 requires this to be declared as `nonisolated(unsafe)`, but older compilers emit a // warning claiming (incorrectly) that it's redundant. #if compiler(>=6) - private nonisolated(unsafe) var _storage: ManagedBuffer + private nonisolated(unsafe) var _storage: ManagedBuffer #else - private var _storage: ManagedBuffer + private var _storage: ManagedBuffer #endif /// The value behind the lock. @@ -52,7 +95,7 @@ public struct Locked: Sendable where Value: Sendable { public init(_ value: Value) { _storage = _Storage.create(minimumCapacity: 1, makingHeaderWith: { _ in value }) _storage.withUnsafeMutablePointerToElements { lock in - _ = pthread_mutex_init(lock, nil) + _lockInitialize(lock) } } @@ -65,8 +108,8 @@ public struct Locked: Sendable where Value: Sendable { _ body: (inout Value) throws -> Result ) rethrows -> Result { try _storage.withUnsafeMutablePointers { rawValue, lock in - _ = pthread_mutex_lock(lock) - defer { _ = pthread_mutex_unlock(lock) } + _lockAcquire(lock) + defer { _lockRelease(lock) } return try body(&rawValue.pointee) } } diff --git a/tools/test_observer/LinuxXCTestRunner.swift b/tools/test_observer/SwiftCorelibsXCTestRunner.swift similarity index 89% rename from tools/test_observer/LinuxXCTestRunner.swift rename to tools/test_observer/SwiftCorelibsXCTestRunner.swift index 0ffd9a5b1..811b0ddbc 100644 --- a/tools/test_observer/LinuxXCTestRunner.swift +++ b/tools/test_observer/SwiftCorelibsXCTestRunner.swift @@ -12,18 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -#if os(Linux) +#if os(Linux) || os(Windows) import Foundation import XCTest - public typealias XCTestRunner = LinuxXCTestRunner + public typealias XCTestRunner = SwiftCorelibsXCTestRunner - /// A test runner for tests that use the XCTest framework on Linux. + /// A test runner for tests that use the XCTest framework on platforms that use + /// swift-corelibs-xctest (Linux and Windows). /// /// This test runner uses test case entries that were constructed by scanning the symbol graph - /// output of the compiler. + /// output of the compiler, since those platforms lack the Objective-C runtime used for test + /// discovery on Apple platforms. @MainActor - public enum LinuxXCTestRunner { + public enum SwiftCorelibsXCTestRunner { /// A wrapper around a single test from an `XCTestCaseEntry` used by the test collector. private struct Test: Testable { /// The type of the `XCTestCase` that contains the test. diff --git a/tools/test_observer/SwiftTestingRunner.swift b/tools/test_observer/SwiftTestingRunner.swift index 87b461b0a..4bf6a71b2 100644 --- a/tools/test_observer/SwiftTestingRunner.swift +++ b/tools/test_observer/SwiftTestingRunner.swift @@ -18,6 +18,8 @@ import Foundation import Darwin #elseif canImport(Glibc) import Glibc +#elseif canImport(WinSDK) + import WinSDK #else #error("Unsupported platform") #endif @@ -293,7 +295,7 @@ private struct SwiftTestingEntryPoint { /// Creates the entry point by looking it up by name in the current process, or fails if the /// entry point is not found. init?() { - guard let entryPointRaw = dlsym(rtldDefault, "swt_abiv0_getEntryPoint") else { + guard let entryPointRaw = _loadSwiftTestingSymbol("swt_abiv0_getEntryPoint") else { return nil } let abiv0_getEntryPoint = unsafeBitCast( @@ -344,18 +346,43 @@ private struct SwiftTestingEntryPoint { } } -// `RTLD_DEFAULT` is only defined on Linux when `_GNU_SOURCE` is defined. Just redefine it -// here for convenience. -#if compiler(>=5.10) - #if os(Linux) - private nonisolated(unsafe) let rtldDefault = UnsafeMutableRawPointer(bitPattern: 0) - #else - private nonisolated(unsafe) let rtldDefault = UnsafeMutableRawPointer(bitPattern: -2) - #endif +/// Looks up a symbol exported by the swift-testing framework in the current process, returning +/// `nil` if it is not present (i.e. swift-testing was not linked into the test binary). +#if canImport(WinSDK) + private func _loadSwiftTestingSymbol(_ name: String) -> UnsafeMutableRawPointer? { + // `GetProcAddress` resolves a symbol from a specific module, so check the modules that may + // export the swift-testing ABI: the test executable itself (when statically linked) and the + // `Testing.dll` shared library. + let modules: [HMODULE?] = [ + GetModuleHandleW(nil), + "Testing.dll".withCString(encodedAs: UTF16.self) { GetModuleHandleW($0) }, + ] + for module in modules { + guard let module else { continue } + if let symbol = name.withCString({ GetProcAddress(module, $0) }) { + return unsafeBitCast(symbol, to: UnsafeMutableRawPointer.self) + } + } + return nil + } #else - #if os(Linux) - private let rtldDefault = UnsafeMutableRawPointer(bitPattern: 0) + // `RTLD_DEFAULT` is only defined on Linux when `_GNU_SOURCE` is defined. Just redefine it + // here for convenience. + #if compiler(>=5.10) + #if os(Linux) + private nonisolated(unsafe) let rtldDefault = UnsafeMutableRawPointer(bitPattern: 0) + #else + private nonisolated(unsafe) let rtldDefault = UnsafeMutableRawPointer(bitPattern: -2) + #endif #else - private let rtldDefault = UnsafeMutableRawPointer(bitPattern: -2) + #if os(Linux) + private let rtldDefault = UnsafeMutableRawPointer(bitPattern: 0) + #else + private let rtldDefault = UnsafeMutableRawPointer(bitPattern: -2) + #endif #endif + + private func _loadSwiftTestingSymbol(_ name: String) -> UnsafeMutableRawPointer? { + return dlsym(rtldDefault, name) + } #endif diff --git a/tools/worker/work_processor.cc b/tools/worker/work_processor.cc index df6021c84..5e79f94a4 100644 --- a/tools/worker/work_processor.cc +++ b/tools/worker/work_processor.cc @@ -33,6 +33,30 @@ namespace { +#if defined(_WIN32) +// On Windows, `std::filesystem` honors the legacy MAX_PATH (260 character) limit +// unless a path uses the extended-length "\\?\" prefix. The incremental storage +// area (`bazel-out/.../_swift_incremental/...`) routinely produces paths longer +// than that, so normalize to an absolute, normalized, backslash-separated path +// with the prefix applied before performing filesystem operations. +std::filesystem::path LongPath(const std::filesystem::path& path) { + std::error_code ec; + std::filesystem::path absolute = std::filesystem::absolute(path, ec); + if (ec) { + return path; + } + std::wstring native = absolute.lexically_normal().make_preferred().wstring(); + if (native.compare(0, 4, L"\\\\?\\") != 0) { + native.insert(0, L"\\\\?\\"); + } + return std::filesystem::path(native); +} +#else +std::filesystem::path LongPath(const std::filesystem::path& path) { + return path; +} +#endif + bool copy_file(const std::filesystem::path& from, const std::filesystem::path& to, std::error_code& ec) noexcept { #if defined(__APPLE__) @@ -44,7 +68,7 @@ bool copy_file(const std::filesystem::path& from, ec = std::error_code(); return true; #else - return std::filesystem::copy_file(from, to, ec); + return std::filesystem::copy_file(LongPath(from), LongPath(to), ec); #endif } @@ -52,7 +76,7 @@ static bool TouchFile(const std::filesystem::path& path, std::ostringstream& output) { std::error_code ec; if (!path.parent_path().empty()) { - std::filesystem::create_directories(path.parent_path(), ec); + std::filesystem::create_directories(LongPath(path.parent_path()), ec); if (ec) { output << "swift_worker: Could not create directory " << path.parent_path() << " (" << ec.message() << ")\n"; @@ -60,7 +84,7 @@ static bool TouchFile(const std::filesystem::path& path, } } - std::ofstream stream(path); + std::ofstream stream(LongPath(path)); if (!stream) { output << "swift_worker: Could not create " << path << "\n"; return false; @@ -227,7 +251,7 @@ void WorkProcessor::ProcessWorkRequest( // requested. if (!emit_swift_source_info && expected_object_path.extension() == ".swiftsourceinfo") { - std::filesystem::remove(expected_object_path); + std::filesystem::remove(LongPath(expected_object_path)); } // Bazel creates the intermediate directories for the files declared at @@ -251,7 +275,7 @@ void WorkProcessor::ProcessWorkRequest( for (const auto& dir_path : dir_paths) { std::error_code ec; - std::filesystem::create_directories(dir_path, ec); + std::filesystem::create_directories(LongPath(dir_path), ec); if (ec) { stderr_stream << "swift_worker: Could not create directory " << dir_path << " (" << ec.message() << ")\n"; @@ -267,7 +291,7 @@ void WorkProcessor::ProcessWorkRequest( auto inputs = output_file_map.incremental_inputs(); bool all_inputs_exist = std::all_of( inputs.cbegin(), inputs.cend(), [](const auto& expected_object_pair) { - return std::filesystem::exists(expected_object_pair.second); + return std::filesystem::exists(LongPath(expected_object_pair.second)); }); if (all_inputs_exist) { @@ -286,12 +310,12 @@ void WorkProcessor::ProcessWorkRequest( } else { auto cleanup_outputs = output_file_map.incremental_cleanup_outputs(); for (const auto& cleanup_output : cleanup_outputs) { - if (!std::filesystem::exists(cleanup_output)) { + if (!std::filesystem::exists(LongPath(cleanup_output))) { continue; } std::error_code ec; - std::filesystem::remove(cleanup_output, ec); + std::filesystem::remove(LongPath(cleanup_output), ec); if (ec) { stderr_stream << "swift_worker: Could not remove " << cleanup_output << " (" << ec.message() << ")\n"; @@ -337,10 +361,10 @@ void WorkProcessor::ProcessWorkRequest( // next run. for (const auto& expected_object_pair : output_file_map.incremental_inputs()) { - if (std::filesystem::exists(expected_object_pair.first)) { - if (std::filesystem::exists(expected_object_pair.second)) { + if (std::filesystem::exists(LongPath(expected_object_pair.first))) { + if (std::filesystem::exists(LongPath(expected_object_pair.second))) { // CopyFile fails if the file already exists - std::filesystem::remove(expected_object_pair.second); + std::filesystem::remove(LongPath(expected_object_pair.second)); } std::error_code ec; copy_file(expected_object_pair.first, expected_object_pair.second, ec); From 773259f5ac79d96a0757a018c7b1d490b67d9f9d Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Sun, 14 Jun 2026 01:18:51 -0400 Subject: [PATCH 08/17] docs: remove WINDOWS.md handoff note (Windows host work landed and verified on this branch) --- WINDOWS.md | 153 ----------------------------------------------------- 1 file changed, 153 deletions(-) delete mode 100644 WINDOWS.md diff --git a/WINDOWS.md b/WINDOWS.md deleted file mode 100644 index 55a7adc0c..000000000 --- a/WINDOWS.md +++ /dev/null @@ -1,153 +0,0 @@ -# Building Swift on Windows with rules_swift - -This document is a working note on the state of **native Windows** support in -rules_swift and what it takes to build Swift code on a Windows host. - -## TL;DR - -Building Swift **on Windows, for Windows** is the same model as building for -Apple platforms on macOS: you do it **natively on the host**, using a Swift -toolchain that is *installed on the machine* (not downloaded by Bazel), and -Bazel achieves reproducibility by tracking/hashing the installed binaries it -invokes — exactly how `apple_support` treats Xcode. - -This is **orthogonal to the cross-compilation work in this PR.** The -`swift.wasm_sdk` / `swift.android_sdk` Swift-SDK mechanism is a *convenience* for -the targets that can be cross-compiled from any host (WebAssembly, Android, -static Linux). Apple and Windows are each built on their own platform, so they -use a **host toolchain**, not a downloaded destination-SDK. There is no official -Windows "Swift SDK" artifact bundle, and that's expected — Windows is a host -target, not a cross destination. - -## What already exists upstream - -rules_swift already has a Windows host toolchain path; it is **not** something -this PR needs to add: - -- **Autoconfiguration** — `swift/internal/swift_autoconfiguration.bzl` - (`_create_windows_toolchain`) discovers a locally-installed Swift toolchain - when `swiftc.exe` is on `PATH`. It derives the toolchain `root` from - `swiftc.exe`'s location, reads `SDKROOT` / `Path` / `ProgramData` from the - environment, and reads `XCTEST_VERSION` from the installed SDK's `Info.plist`. - On a non-Windows host (no `swiftc.exe` on `PATH`) it emits a no-op comment, so - it is safe cross-platform. -- **A Windows `swift_toolchain`** is generated with `os = "windows"`, - `arch = "x86_64"`, `tool_executable_suffix = ".exe"`, and the discovered - `root` / `sdkroot` / `env`. -- **Windows link flags** — `swift/toolchains/swift_toolchain.bzl` - (`_swift_windows_linkopts_cc_info`) supplies the MSVC-style linker flags: - - `-LIBPATH:/usr/lib/swift/windows/` - - `-LIBPATH:<...>/Library/XCTest-/usr/lib/swift/windows/` - - the runtime start object `/usr/lib/swift/windows//swiftrt.obj` -- **A registered toolchain** — `swift/toolchains/BUILD` registers - `windows-swift-toolchain-x86_64` (`exec`/`target` = `@platforms//os:windows` + - `@platforms//cpu:x86_64`) pointing at `@rules_swift_local_config//:windows-toolchain`. - -The C/C++ side is handled by Bazel's built-in **MSVC C++ toolchain**, which -discovers Visual Studio via `vswhere` / `BAZEL_VC`. rules_swift's Windows -`swift_toolchain` composes with that cc toolchain for linking. - -So the expectation on a properly-provisioned Windows box is that a plain -`swift_library` / `swift_binary` builds with no extra configuration. - -## Prerequisites on the Windows machine - -1. **Visual Studio 2022+ (Build Tools is enough)** — provides MSVC and the - Windows SDK (the C runtime, Win32 headers/libs, `link.exe`). This is what - Bazel's MSVC cc toolchain and the Swift linker consume. -2. **Swift for Windows** (the swift.org installer) — installs `swiftc.exe` and - the Swift Windows runtime/SDK, and sets `SDKROOT` (and the module maps for - `ucrt` / `winsdk` / `visualc`). Use the release that matches anything you - pin elsewhere. -3. **Run Bazel from an environment that has both** — i.e. a "x64 Native Tools - Command Prompt for VS" (or a shell that has sourced `vcvars64.bat`) **with the - Swift installer's environment** also present, so `swiftc.exe` is on `PATH` and - `SDKROOT` / `Path` / `ProgramData` are set when the repository rule runs. -4. **Python 3** on `PATH` — the autoconfiguration shells out to it to read the - XCTest version from the SDK `Info.plist`. A real interpreter is required; the - Microsoft Store `python3.exe` execution-alias stub is detected and skipped. -5. **`bash` on `PATH` and `BAZEL_SH` set** (e.g. Git for Windows' `bash.exe`) — - needed only for `bazel test`, whose generic test wrapper is a shell script. - -## How to build / verify - -From the rules_swift checkout, in a provisioned VS+Swift shell: - -```bat -set BAZEL_SH=C:\Program Files\Git\bin\bash.exe -bazel build //examples/xplatform/hello_world //examples/xplatform/shared_library -bazel test //examples/xplatform/xctest -``` - -`hello_world` is the minimal `swift_binary` smoke target, `shared_library` -exercises the `linkshared` → `.dll` path, and `xctest` exercises `swift_test` -(XCTest discovery, the runner, and execution). If the host toolchain resolves, -the build selects `windows-swift-toolchain-x86_64`. - -## Status (verified on a native Windows host) - -The checklist below was worked through end-to-end on a real Windows 11 host -(Swift 6.3.2 for Windows + Visual Studio 2022 Build Tools / MSVC 14.44, Bazel -9.1.1). Verifying it surfaced several pieces of bit-rot and a handful of -genuinely missing Windows code paths; all are fixed on this branch. - -1. **Autoconfiguration resolves cleanly.** ✅ The `Info.plist` path math and the - `usr/lib/swift/windows/` layout still match a current Swift release. - Two latent bugs were fixed, both of which had left `xctest_version` empty: - - `_get_python_bin` returned the Microsoft Store `python3.exe` *execution - alias* (a stub that exits nonzero) instead of a real interpreter. It now - probes candidates and skips non-working ones. - - `SDKROOT` from the environment ends in a backslash, which was interpolated - into a Python raw-string literal (`r'...\'`) — a syntax error. The SDK root - is now normalized (forward slashes, no trailing separator) before use. -2. **End-to-end `swift_binary` link.** ✅ `//examples/xplatform/hello_world` - builds to a runnable `.exe` and prints "Hello, world!". Two fixes were - required: the toolchain rejected the MSVC cc toolchain (`msvc-cl`) because of - a hard `clang`-only check (now skipped on Windows), and the entry-point alias - used GNU `ld`'s `--defsym`, which MSVC `link.exe` rejects (now - `/ALTERNATENAME` on Windows). -3. **`linkshared` → `.dll`.** ✅ `swift_binary(linkshared = True)` produces a - Windows `.dll` plus an import `.lib`; see the new - `//examples/xplatform/shared_library` example. -4. **arm64 Windows.** ⚠️ Implemented but **unverified** (no arm64 host was - available). Autoconfiguration now detects the host CPU instead of hardcoding - `x86_64`, the toolchain understands the `aarch64` library/`bin64a` layout, - and a `windows-swift-toolchain-aarch64` toolchain is registered. -5. **XCTest.** ✅ The `XCTest-` `LIBPATH`/`-I` paths resolve. - `swift_test` runs end-to-end (`//examples/xplatform/xctest` passes). Getting - there required: adding the XCTest include paths to the symbol-graph-extract - action (test discovery), emitting the `-msvc` target-triple environment (the - symbol-graph tool matches the module triple exactly), porting the - `//tools/test_observer` runner to Windows (`SRWLOCK`, `GetProcAddress`, a - swift-corelibs XCTest runner shared with Linux), running the discovery tool - with the Swift runtime on `PATH`, sanitizing spaces out of object paths - (`lib.exe`/`link.exe` response-file parsing), making the persistent worker - long-path (`\\?\`) aware, and suppressing the benign `LNK4217` that static - linking of `dllimport` symbols produces. -6. **Windows CI.** ⚠️ Re-enabled in `.bazelci/presubmit.yml` (a `windows` task - that builds the Swift examples and runs the `xctest` test). The Swift-install - prologue and BazelCI Windows image provisioning are the one piece **not** - validated here, since that requires the CI infrastructure rather than a local - host; it may need adjustment when first run. - -A general (not Windows-only) requirement also surfaced: `bazel test` on Windows -needs a `bash` for the test wrapper, so `BAZEL_SH` must point at a `bash.exe` -(e.g. Git for Windows). `worker` sandboxing is disabled on Windows in `.bazelrc` -(`build:windows --noworker_sandboxing`) because a running executable cannot be -deleted to clean the sandbox. - -## Hermeticity - -This follows the Xcode model: the toolchain is discovered from the environment -and referenced by absolute `root` / `sdkroot` paths. To make the *build outputs* -reproducible despite the external install, Bazel can digest the referenced -toolchain/SDK files (the binaries and libraries actually invoked), the same way -`apple_support` tracks Xcode. Tightening input tracking is a follow-up -refinement, not a blocker to building. - -## Relationship to this PR - -Nothing here depends on the Swift-SDK cross-compilation changes, and vice -versa. This note lives on the branch only to capture the current Windows-host -state and the verification checklist for picking the work up on an actual -Windows machine. From 1bf9976242479902308c9446904b0645b83f580a Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Sun, 14 Jun 2026 22:34:06 -0400 Subject: [PATCH 09/17] android: don't --exclude-libs,ALL (let linkshared export library symbols) The Android Swift-SDK toolchain mirrored swiftc's static-stdlib-args.lnk, which passes -Wl,--exclude-libs,ALL to hide the static Swift runtime's symbols. That works for swiftc because the user's code is compiled into the main object files and only the runtime arrives via static archives. In the Bazel model a swift_binary's deps (swift_library) are themselves static archives, so --exclude-libs,ALL also demotes the user's own exported symbols to local -- including @_cdecl("Java_...") JNI entry points defined in a library. They drop out of .dynsym and System.loadLibrary can't bind them (UnsatisfiedLinkError on the first native call). Omit --exclude-libs,ALL so a linkshared library exports its symbols. A consumer that wants to hide the runtime can pass a linker version script listing the symbols to export, which is the standard way to control a JNI .so's exports. Verified: a swift_binary(linkshared) Android JNI library with @_cdecl JNI functions in a swift_library dep now exports all 15 Java_ symbols in .dynsym and the app binds them and launches on an emulator; a consumer version script (global: Java_*; local: *) cleanly exports only the JNI symbols with no runtime leakage. --- swift/internal/extensions/swift_sdks.bzl | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/swift/internal/extensions/swift_sdks.bzl b/swift/internal/extensions/swift_sdks.bzl index 9c7bb34d8..4d06ae17b 100644 --- a/swift/internal/extensions/swift_sdks.bzl +++ b/swift/internal/extensions/swift_sdks.bzl @@ -455,6 +455,19 @@ def _swift_android_sdk_impl(repository_ctx): # statically linking the stdlib for Android; see # `swift_static-{arch}/android/static-stdlib-args.lnk` in the SDK. # The 16 KiB max page size is required by Android 15+. + # + # NOTE: that `.lnk` also passes `-Wl,--exclude-libs,ALL`, which we + # deliberately omit. `swiftc` compiles the user's code into the main + # object files and only the Swift runtime arrives via static + # archives, so `--exclude-libs,ALL` hides just the runtime there. In + # the Bazel model a `swift_binary`'s deps (`swift_library`) are + # themselves static archives, so `--exclude-libs,ALL` also demotes + # the user's own exported symbols (e.g. `@_cdecl("Java_…")` JNI entry + # points defined in a library) to local — they vanish from `.dynsym` + # and `System.loadLibrary` can't bind them. Omitting it lets a + # `linkshared` library export its symbols; a consumer that wants to + # hide the runtime can pass a linker version script listing the + # symbols to export (the standard way to control a JNI `.so`). linkopts = _build_list([ "{}/android/{}/swiftrt.o".format(resource_dir, arch), "-L{}/android".format(resource_dir), @@ -462,7 +475,6 @@ def _swift_android_sdk_impl(repository_ctx): "-llog", "-lm", "-lstdc++", - "-Wl,--exclude-libs,ALL", "-Wl,-z,max-page-size=16384", ]), os = "android", From a34a38889da0a6200b53e921f1b21d68ed42830c Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Mon, 15 Jun 2026 10:38:11 -0400 Subject: [PATCH 10/17] buildifier: silence external-path on the execroot path helper buildifier 8.5.1's `external-path` lint flags the literal "/external/" in `_execroot_relative_path`, but that helper exists precisely to turn an absolute output-base path into an execroot-relative one, so the substring is intentional. Annotate the two lines with `# buildifier: disable=external-path` so the buildifier CI check passes. --- swift/internal/extensions/swift_sdks.bzl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/swift/internal/extensions/swift_sdks.bzl b/swift/internal/extensions/swift_sdks.bzl index 4d06ae17b..10be77ee7 100644 --- a/swift/internal/extensions/swift_sdks.bzl +++ b/swift/internal/extensions/swift_sdks.bzl @@ -195,8 +195,12 @@ def _execroot_relative_path(path): baking into command line flags. """ path_str = str(path) + + # buildifier: disable=external-path if "/external/" not in path_str: fail("Expected a path inside an external repository, got: " + path_str) + + # buildifier: disable=external-path return "external/" + path_str.rsplit("/external/", 1)[1] def _build_list(items, indent = " "): From 8193d6321e236ce85fd9d7331dd7b4a5dc8fbd3c Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Mon, 15 Jun 2026 11:20:48 -0400 Subject: [PATCH 11/17] ci: run the Windows Swift install via shell_commands (not batch_commands) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Windows task installed Swift under a `batch_commands:` key, which BazelCI doesn't recognize, so the "Setup (Batch Commands)" step ran empty — Swift was never installed, the autoconfiguration found no `swiftc.exe`, declared no `windows-toolchain`, and every `swift_*` target failed to resolve a toolchain. BazelCI runs a task's `shell_commands` as a batch script on Windows (that is the "Setup (Batch Commands)" step), so move the install there. --- .bazelci/presubmit.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 0e6ab7ae3..476eae156 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -127,7 +127,9 @@ tasks: # and sets `SDKROOT`/`Path` machine-wide, which the Swift autoconfiguration # repository rule reads to discover the toolchain. Visual Studio (MSVC) is # already present on the Bazel CI Windows image and is used for linking. - batch_commands: + # (BazelCI runs `shell_commands` as a batch script on Windows — the + # "Setup (Batch Commands)" step; `batch_commands` is not a recognized key.) + shell_commands: - "curl -sSL -o %TEMP%\\swift-installer.exe https://download.swift.org/swift-%SWIFT_VERSION%-release/windows10/swift-%SWIFT_VERSION%-RELEASE/swift-%SWIFT_VERSION%-RELEASE-windows10.exe" - "%TEMP%\\swift-installer.exe -q" <<: *windows_common From 127cbbb6e6f9f0b4241c79312142840c2aa88b93 Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Mon, 15 Jun 2026 12:05:59 -0400 Subject: [PATCH 12/17] ci: revert Windows key to batch_commands; add install-path diagnostics batch_commands is the correct Windows key (bazelci.py runs it on Windows; shell_commands is ignored there). The installer runs but the machine-wide Path/SDKROOT it sets don't reach the already-running CI process, so the Swift autoconfiguration finds no swiftc.exe. Add temporary DIAG lines to print the install location and the Path/SDKROOT the installer set, so they can be exposed to the build via the task's `environment:` block. --- .bazelci/presubmit.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 476eae156..83f506324 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -122,16 +122,20 @@ tasks: bazel: latest environment: SWIFT_VERSION: "6.0.3" - # Install the Swift for Windows toolchain before building. The swift.org - # installer is a self-extracting bundle that supports an unattended install - # and sets `SDKROOT`/`Path` machine-wide, which the Swift autoconfiguration - # repository rule reads to discover the toolchain. Visual Studio (MSVC) is - # already present on the Bazel CI Windows image and is used for linking. - # (BazelCI runs `shell_commands` as a batch script on Windows — the - # "Setup (Batch Commands)" step; `batch_commands` is not a recognized key.) - shell_commands: + # Install the Swift for Windows toolchain before building (BazelCI runs a + # task's `batch_commands` as a batch script on Windows). The DIAG lines + # below print where the installer landed swiftc.exe and what Path/SDKROOT it + # set, so we can expose them to the build via `environment:` (the installer + # sets them machine-wide, which the already-running CI process doesn't pick + # up). Visual Studio (MSVC) is already on the image and is used for linking. + batch_commands: - "curl -sSL -o %TEMP%\\swift-installer.exe https://download.swift.org/swift-%SWIFT_VERSION%-release/windows10/swift-%SWIFT_VERSION%-RELEASE/swift-%SWIFT_VERSION%-RELEASE-windows10.exe" - "%TEMP%\\swift-installer.exe -q" + - "echo ==DIAG where swiftc== & where swiftc.exe" + - "echo ==DIAG find swiftc== & dir /s /b \"%LOCALAPPDATA%\\Programs\\Swift\\swiftc.exe\" \"%ProgramFiles%\\Swift\\swiftc.exe\" \"%ProgramFiles(x86)%\\Swift\\swiftc.exe\" \"%SystemDrive%\\Library\\Developer\\Toolchains\\*\\usr\\bin\\swiftc.exe\"" + - "echo ==DIAG HKCU env== & reg query \"HKCU\\Environment\"" + - "echo ==DIAG HKLM env== & reg query \"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\"" + - "echo ==DIAG done==" <<: *windows_common doc_tests: From 7fb2f50914c4fa7646e03821d828055bf18ada3b Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Mon, 15 Jun 2026 12:40:39 -0400 Subject: [PATCH 13/17] ci: expose the installed Swift toolchain to the Windows build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The swift.org Windows installer puts swiftc.exe under %LOCALAPPDATA%\Programs\Swift\Toolchains\+Asserts\usr\bin and sets SDKROOT to the bundled Windows.sdk, but it records these on the user/machine env, which the already-running CI process never picks up — so the Swift autoconfiguration found no swiftc.exe and declared no windows-toolchain. Set Path (Toolchains + Runtimes + Tools bins) and SDKROOT in the task's environment block, which BazelCI applies (with %VAR% expansion) to the build. Drop the diagnostics now that the layout is known. --- .bazelci/presubmit.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 83f506324..bd000888e 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -120,22 +120,21 @@ tasks: windows: name: "Current LTS" bazel: latest + # The swift.org installer (`batch_commands` below) installs per-user and adds + # its Toolchains/Runtimes bin to the user's `Path` and sets `SDKROOT`, but + # those user/machine env changes don't reach the already-running CI process. + # Set them explicitly so the bazel build — and the Swift autoconfiguration's + # `swiftc.exe` lookup — finds the toolchain. BazelCI expands these with + # `os.path.expandvars`, so `%LOCALAPPDATA%` / `%SWIFT_VERSION%` / `%PATH%` + # resolve (SWIFT_VERSION is listed first so it is set before the others + # expand). Visual Studio (MSVC) is already on the image and is used to link. environment: SWIFT_VERSION: "6.0.3" - # Install the Swift for Windows toolchain before building (BazelCI runs a - # task's `batch_commands` as a batch script on Windows). The DIAG lines - # below print where the installer landed swiftc.exe and what Path/SDKROOT it - # set, so we can expose them to the build via `environment:` (the installer - # sets them machine-wide, which the already-running CI process doesn't pick - # up). Visual Studio (MSVC) is already on the image and is used for linking. + Path: "%LOCALAPPDATA%\\Programs\\Swift\\Toolchains\\%SWIFT_VERSION%+Asserts\\usr\\bin;%LOCALAPPDATA%\\Programs\\Swift\\Runtimes\\%SWIFT_VERSION%\\usr\\bin;%LOCALAPPDATA%\\Programs\\Swift\\Tools\\%SWIFT_VERSION%;%PATH%" + SDKROOT: "%LOCALAPPDATA%\\Programs\\Swift\\Platforms\\%SWIFT_VERSION%\\Windows.platform\\Developer\\SDKs\\Windows.sdk" batch_commands: - "curl -sSL -o %TEMP%\\swift-installer.exe https://download.swift.org/swift-%SWIFT_VERSION%-release/windows10/swift-%SWIFT_VERSION%-RELEASE/swift-%SWIFT_VERSION%-RELEASE-windows10.exe" - "%TEMP%\\swift-installer.exe -q" - - "echo ==DIAG where swiftc== & where swiftc.exe" - - "echo ==DIAG find swiftc== & dir /s /b \"%LOCALAPPDATA%\\Programs\\Swift\\swiftc.exe\" \"%ProgramFiles%\\Swift\\swiftc.exe\" \"%ProgramFiles(x86)%\\Swift\\swiftc.exe\" \"%SystemDrive%\\Library\\Developer\\Toolchains\\*\\usr\\bin\\swiftc.exe\"" - - "echo ==DIAG HKCU env== & reg query \"HKCU\\Environment\"" - - "echo ==DIAG HKLM env== & reg query \"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\"" - - "echo ==DIAG done==" <<: *windows_common doc_tests: From ba91db97b52db71b1f2ced2f53083e157db6a4c6 Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Mon, 15 Jun 2026 13:23:39 -0400 Subject: [PATCH 14/17] windows: default ProgramData when configuring the toolchain The Windows autoconfiguration read repository_ctx.os.environ["ProgramData"] unguarded, but that variable isn't always present in the build's environment (e.g. the service-account Buildkite CI agent doesn't set it), which failed toolchain configuration with `key "ProgramData" not found in dictionary`. Fall back to its conventional value `C:\ProgramData`, mirroring the existing defensive handling of `Path`/`PATH`. --- swift/internal/swift_autoconfiguration.bzl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/swift/internal/swift_autoconfiguration.bzl b/swift/internal/swift_autoconfiguration.bzl index fa3ed5d4d..15956bc10 100644 --- a/swift/internal/swift_autoconfiguration.bzl +++ b/swift/internal/swift_autoconfiguration.bzl @@ -357,7 +357,10 @@ Swift toolchain. env = { "Path": repository_ctx.os.environ["Path"] if "Path" in repository_ctx.os.environ else repository_ctx.os.environ["PATH"], - "ProgramData": repository_ctx.os.environ["ProgramData"], + # `ProgramData` is normally present in a Windows process environment, but + # is not guaranteed to be (e.g. a service-account CI agent), so fall back + # to its conventional value rather than failing toolchain configuration. + "ProgramData": repository_ctx.os.environ.get("ProgramData", "C:\\ProgramData"), } return """\ From df56380b9e9748ed29be9486e9140809c43e5760 Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Mon, 15 Jun 2026 13:34:46 -0400 Subject: [PATCH 15/17] =?UTF-8?q?ci:=20diagnostic=20=E2=80=94=20dump=20the?= =?UTF-8?q?=20MSVC=20vcvars=20env=20on=20the=20Windows=20runner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit swiftc's clang can't find the UCRT/MSVC C headers (errno.h) because the build doesn't run inside a Visual Studio developer environment, so INCLUDE/LIB aren't set. Print the vcvars-provided INCLUDE/LIB/LIBPATH (and the SDK/toolset versions) so they can be set in the task's environment block (the swift compile inherits os.environ). Temporary; removed once the values are wired in. --- .bazelci/presubmit.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index bd000888e..3a001f206 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -135,6 +135,7 @@ tasks: batch_commands: - "curl -sSL -o %TEMP%\\swift-installer.exe https://download.swift.org/swift-%SWIFT_VERSION%-release/windows10/swift-%SWIFT_VERSION%-RELEASE/swift-%SWIFT_VERSION%-RELEASE-windows10.exe" - "%TEMP%\\swift-installer.exe -q" + - "echo ==DIAG VCENV== & call \"%BAZEL_VC%\\Auxiliary\\Build\\vcvars64.bat\" >nul & set | findstr /B /I \"INCLUDE= LIB= LIBPATH= UCRTVersion= VCToolsInstallDir= WindowsSdkDir= WindowsSDKVersion=\" & echo ==DIAG END==" <<: *windows_common doc_tests: From 2c7143c30a2114e168c87475e8c11d11e0369174 Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Mon, 15 Jun 2026 14:21:18 -0400 Subject: [PATCH 16/17] ci: set the MSVC INCLUDE/LIB env for the Windows Swift build swiftc's clang could not find the C headers (errno.h) because the build does not run inside a Visual Studio developer environment, so INCLUDE/LIB were unset. Set them (and add the MSVC/SDK tool bins to Path for link.exe) from the image's VS 2022 BuildTools + Windows SDK, mirroring what vcvars64.bat exports; the swift compile inherits these via os.environ. Versions are pinned to the Bazel CI image. Drop the diagnostic now that the values are known. --- .bazelci/presubmit.yml | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 3a001f206..d2623c6c0 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -120,22 +120,27 @@ tasks: windows: name: "Current LTS" bazel: latest - # The swift.org installer (`batch_commands` below) installs per-user and adds - # its Toolchains/Runtimes bin to the user's `Path` and sets `SDKROOT`, but - # those user/machine env changes don't reach the already-running CI process. - # Set them explicitly so the bazel build — and the Swift autoconfiguration's - # `swiftc.exe` lookup — finds the toolchain. BazelCI expands these with - # `os.path.expandvars`, so `%LOCALAPPDATA%` / `%SWIFT_VERSION%` / `%PATH%` - # resolve (SWIFT_VERSION is listed first so it is set before the others - # expand). Visual Studio (MSVC) is already on the image and is used to link. + # Expose the installed Swift toolchain and the MSVC/Windows SDK environment to + # the build. The swift.org installer (`batch_commands` below) records its + # Toolchains/Runtimes bin on the user's Path and sets SDKROOT, and a Visual + # Studio "developer prompt" (vcvars) would set INCLUDE/LIB for swiftc's clang + # to find the C headers (errno.h, ...) and for the linker — but none of that + # reaches the already-running CI process. Set it all here. BazelCI expands + # these with os.path.expandvars, so %VAR% resolves and earlier keys (listed + # first) are available to later ones. The VS/SDK versions are pinned to the + # Bazel CI Windows image; update them if `vcvars64.bat`'s output changes. environment: SWIFT_VERSION: "6.0.3" - Path: "%LOCALAPPDATA%\\Programs\\Swift\\Toolchains\\%SWIFT_VERSION%+Asserts\\usr\\bin;%LOCALAPPDATA%\\Programs\\Swift\\Runtimes\\%SWIFT_VERSION%\\usr\\bin;%LOCALAPPDATA%\\Programs\\Swift\\Tools\\%SWIFT_VERSION%;%PATH%" + VCToolsInstallDir: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\VC\\Tools\\MSVC\\14.39.33519" + WindowsSdkDir: "C:\\Program Files (x86)\\Windows Kits\\10" + WindowsSDKVersion: "10.0.26100.0" + Path: "%LOCALAPPDATA%\\Programs\\Swift\\Toolchains\\%SWIFT_VERSION%+Asserts\\usr\\bin;%LOCALAPPDATA%\\Programs\\Swift\\Runtimes\\%SWIFT_VERSION%\\usr\\bin;%LOCALAPPDATA%\\Programs\\Swift\\Tools\\%SWIFT_VERSION%;%VCToolsInstallDir%\\bin\\Hostx64\\x64;%WindowsSdkDir%\\bin\\%WindowsSDKVersion%\\x64;%PATH%" SDKROOT: "%LOCALAPPDATA%\\Programs\\Swift\\Platforms\\%SWIFT_VERSION%\\Windows.platform\\Developer\\SDKs\\Windows.sdk" + INCLUDE: "%VCToolsInstallDir%\\include;%WindowsSdkDir%\\include\\%WindowsSDKVersion%\\ucrt;%WindowsSdkDir%\\include\\%WindowsSDKVersion%\\shared;%WindowsSdkDir%\\include\\%WindowsSDKVersion%\\um;%WindowsSdkDir%\\include\\%WindowsSDKVersion%\\winrt;%WindowsSdkDir%\\include\\%WindowsSDKVersion%\\cppwinrt" + LIB: "%VCToolsInstallDir%\\lib\\x64;%WindowsSdkDir%\\lib\\%WindowsSDKVersion%\\ucrt\\x64;%WindowsSdkDir%\\lib\\%WindowsSDKVersion%\\um\\x64" batch_commands: - "curl -sSL -o %TEMP%\\swift-installer.exe https://download.swift.org/swift-%SWIFT_VERSION%-release/windows10/swift-%SWIFT_VERSION%-RELEASE/swift-%SWIFT_VERSION%-RELEASE-windows10.exe" - "%TEMP%\\swift-installer.exe -q" - - "echo ==DIAG VCENV== & call \"%BAZEL_VC%\\Auxiliary\\Build\\vcvars64.bat\" >nul & set | findstr /B /I \"INCLUDE= LIB= LIBPATH= UCRTVersion= VCToolsInstallDir= WindowsSdkDir= WindowsSDKVersion=\" & echo ==DIAG END==" <<: *windows_common doc_tests: From e6a27ec7e7c12bb95d58f3667e3ec07347e1a0bf Mon Sep 17 00:00:00 2001 From: Logan Shire Date: Mon, 15 Jun 2026 14:35:11 -0400 Subject: [PATCH 17/17] ci: build Windows with Swift 6.3.2 (matches the cross-compile + verified host) Swift 6.0.3's clang module setup hit a cyclic dependency (ucrt -> _Builtin_intrinsics -> ucrt) against the CI image's recent Windows SDK (10.0.26100). 6.3.2 -- the version the Windows host support was verified on, and the one this PR's Swift SDK cross-compilation already uses -- carries the clang fixes for those Windows module cycles. Bump only the Windows task; Linux stays on its pinned version. --- .bazelci/presubmit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index d2623c6c0..e9bb38d1d 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -130,7 +130,7 @@ tasks: # first) are available to later ones. The VS/SDK versions are pinned to the # Bazel CI Windows image; update them if `vcvars64.bat`'s output changes. environment: - SWIFT_VERSION: "6.0.3" + SWIFT_VERSION: "6.3.2" VCToolsInstallDir: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\VC\\Tools\\MSVC\\14.39.33519" WindowsSdkDir: "C:\\Program Files (x86)\\Windows Kits\\10" WindowsSDKVersion: "10.0.26100.0"