diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 5626e8a51..e9bb38d1d 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: @@ -62,6 +66,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 @@ -97,11 +117,31 @@ 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 + # 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.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" + 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" + <<: *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/MODULE.bazel b/MODULE.bazel index 30e9a8029..ceb82f839 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,35 @@ register_toolchains( dev_dependency = True, ) +register_toolchains( + # Swift SDK toolchains for cross-compiling to WebAssembly and Android; + # 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", + "@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/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 7e96e51cf..413336918 100644 --- a/doc/standalone_toolchain.md +++ b/doc/standalone_toolchain.md @@ -146,6 +146,143 @@ 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", +) +``` + +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 +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. + +### 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. 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 + 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. + +### 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 new file mode 100644 index 000000000..d12e5c659 --- /dev/null +++ b/examples/cross_compilation/BUILD.bazel @@ -0,0 +1,102 @@ +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 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 = [ + "@platforms//cpu:wasm32", + "@platforms//os:wasi", + ], +) + +platform( + name = "android-aarch64", + constraint_values = [ + "@platforms//cpu:aarch64", + "@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 = "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 = "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 = [":Greeter"], +) + +# Build `:Reactor` for the wasm platform. Consumers can instead set +# `--platforms=//examples/cross_compilation:wasm32-wasip1` on the command line. +transition_binary( + name = "Reactor.wasm", + binary = ":Reactor", + 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`. +# --------------------------------------------------------------------------- + +# `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 = "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..a22b4dd06 --- /dev/null +++ b/examples/cross_compilation/README.md @@ -0,0 +1,45 @@ +# 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 | +| `: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 +# WebAssembly reactor (runnable with wasmtime): +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 +``` + +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/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/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/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"); 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/extensions.bzl b/swift/extensions.bzl index be6bbc0c1..7249ec678 100644 --- a/swift/extensions.bzl +++ b/swift/extensions.bzl @@ -15,13 +15,32 @@ """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_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", + _wasm_sdk_toolchains_for_platform = "wasm_sdk_toolchains_for_platform", ) load("//tools/explicit_modules:extensions.bzl", _system_sdk = "system_sdk") @@ -40,6 +59,132 @@ 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} + 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 = 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), + ) + + # 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) + 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 +195,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 +251,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 +281,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 +382,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/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/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..10be77ee7 --- /dev/null +++ b/swift/internal/extensions/swift_sdks.bzl @@ -0,0 +1,527 @@ +"""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", + # 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}", + ], + 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) + + # 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 = " "): + """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", + # 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, + 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+. + # + # 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), + "-ldl", + "-llog", + "-lm", + "-lstdc++", + "-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..cbd8e92f6 100644 --- a/swift/internal/extensions/toolchains.bzl +++ b/swift/internal/extensions/toolchains.bzl @@ -43,24 +43,149 @@ 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 + +_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/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/internal/swift_autoconfiguration.bzl b/swift/internal/swift_autoconfiguration.bzl index 35b183cb8..15956bc10 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,22 +334,39 @@ 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 = { "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 """\ swift_toolchain( name = "windows-toolchain", - arch = "x86_64", + arch = "{arch}", features = [{features}], os = "windows", root = "{root}", @@ -332,11 +378,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_binary.bzl b/swift/swift_binary.bzl index 10689482c..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") @@ -22,6 +23,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( @@ -76,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( @@ -85,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 = [] @@ -99,7 +118,21 @@ def _swift_binary_impl(ctx): ctx.label, feature_configuration = feature_configuration, ) - entry_point_name = entry_point_function_name(module_name) + + if skip_entry_point: + 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 +144,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, @@ -178,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, @@ -189,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, @@ -278,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. @@ -296,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 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 af31dcec9..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( @@ -443,12 +452,58 @@ 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( 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.""" @@ -457,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) @@ -472,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. " + @@ -509,6 +576,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, @@ -574,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)) @@ -600,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 @@ -763,6 +838,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. 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);