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/swift_sdk_releases.bzl b/swift/internal/extensions/swift_sdk_releases.bzl new file mode 100644 index 000000000..4a1c2e02f --- /dev/null +++ b/swift/internal/extensions/swift_sdk_releases.bzl @@ -0,0 +1,38 @@ +"""Swift SDK release version mappings. + +Checksums for the official "Swift SDK" artifact bundles published by swift.org +(the bundles installed by `swift sdk install`), used to cross-compile Swift to +platforms not covered by a host toolchain. + +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. +""" + +# Populated by the per-platform extensions (e.g. `wasm_sdk`, `android_sdk`), +# which add their SDK checksums under each supported Swift release. +SWIFT_SDK_RELEASES = {} + +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 (e.g. "wasm"). + + 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, + ) diff --git a/swift/internal/extensions/swift_sdks.bzl b/swift/internal/extensions/swift_sdks.bzl new file mode 100644 index 000000000..12d36fc4b --- /dev/null +++ b/swift/internal/extensions/swift_sdks.bzl @@ -0,0 +1,203 @@ +"""Shared building blocks for Swift SDK repository rules. + +A "Swift SDK" is an artifact bundle published by swift.org for cross-compiling +Swift to a platform the host toolchain cannot target by itself (the bundles that +`swift sdk install` consumes). The per-platform extensions (e.g. `wasm_sdk`, +`android_sdk`) define repository rules that download such a bundle and generate +a `swift_toolchain` + rules_cc `cc_toolchain` for the target; this module holds +the helpers and BUILD-file templates they share. + +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. +""" + +# 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). +# buildifier: disable=unused-variable +_HOST_COMPILER_INPUTS = "swift_sdk_compiler_inputs" + +# Files in the host toolchain that link actions driven by its clang need. +# buildifier: disable=unused-variable +_HOST_LINKER_INPUTS = "swift_sdk_linker_inputs" + +# buildifier: disable=unused-variable +_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", + }}, +) +""" + +# buildifier: disable=unused-variable +_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", +) +""" + +# buildifier: disable=unused-variable +_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", +) +""" + +# buildifier: disable=unused-variable +_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}, +) +""" + +# buildifier: disable=unused-variable +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) + +# buildifier: disable=unused-variable +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] + +# buildifier: disable=unused-variable +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, + ), + } diff --git a/swift/internal/extensions/toolchains.bzl b/swift/internal/extensions/toolchains.bzl index 0caaaef37..f01f91c4c 100644 --- a/swift/internal/extensions/toolchains.bzl +++ b/swift/internal/extensions/toolchains.bzl @@ -43,20 +43,42 @@ toolchain( """ -def toolchains_for_platform(platform, toolchain_repository): +# buildifier: disable=unused-variable +_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, ) diff --git a/swift/toolchains/swift_toolchain.bzl b/swift/toolchains/swift_toolchain.bzl index af31dcec9..ab8845574 100644 --- a/swift/toolchains/swift_toolchain.bzl +++ b/swift/toolchains/swift_toolchain.bzl @@ -443,6 +443,37 @@ def _swift_unix_linkopts_cc_info( ), ) +def _swift_sdk_linkopts_cc_info( + toolchain_label, + linkopts, + linker_inputs): + """Returns a `CcInfo` with linker flags supplied by the toolchain target. + + Used for toolchains whose Swift runtime libraries come from a Swift SDK + 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 that owns the propagated linker input. + linkopts: Linker flags from the toolchain's `linkopts` attribute. + linker_inputs: `File`s that link actions using these flags need. + + Returns: + A `CcInfo` that propagates the flags to binaries depending on Swift + targets. + """ + return CcInfo( + linking_context = cc_common.create_linking_context( + linker_inputs = depset([ + cc_common.create_linker_input( + owner = toolchain_label, + user_link_flags = depset(linkopts), + additional_inputs = depset(linker_inputs), + ), + ]), + ), + ) + def _entry_point_linkopts_provider(*, entry_point_name): """Returns linkopts to customize the entry point of a binary.""" return struct( @@ -509,6 +540,20 @@ def _swift_toolchain_impl(ctx): ) elif ctx.attr.os == "none": swift_linkopts_cc_info = CcInfo() + elif ctx.attr.linkopts or ctx.attr.linker_inputs: + # A toolchain whose Swift runtime comes from a Swift SDK (e.g. + # WebAssembly or Android) provides the *complete* set of runtime link + # flags via `linkopts`/`linker_inputs`, replacing — not adding to — the + # defaults computed below. Those defaults are specific to a Linux *host* + # toolchain (`-pie`, `-static-libgcc`, `-lrt`, and the host toolchain's + # own `-L`/rpath and `swiftrt.o`); for a cross-compiled SDK target they + # are at best wrong and at worst invalid (wasm-ld rejects `-pie`, and a + # second `swiftrt.o` would conflict with the SDK's own). + swift_linkopts_cc_info = _swift_sdk_linkopts_cc_info( + ctx.label, + ctx.attr.linkopts, + ctx.files.linker_inputs, + ) else: swift_linkopts_cc_info = _swift_unix_linkopts_cc_info( ctx.attr.arch, @@ -763,6 +808,26 @@ 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 = """\ +The *complete* set of linker flags for the Swift runtime when that runtime is +provided by a Swift SDK (for example WebAssembly or Android) rather than by the +host toolchain — typically search paths for, and inputs from, `linker_inputs`, +plus the SDK's runtime objects. + +This is not additive: when set, it *replaces* the runtime link flags the +toolchain would otherwise compute, because those are specific to a Linux host +toolchain and do not apply to a cross-compiled SDK target. Leave it unset for an +ordinary host toolchain, which computes its own flags. +""", + ), "sdkroot": attr.string( doc = """\ The root of a SDK to be used for building the target. diff --git a/test/utils_tests.bzl b/test/utils_tests.bzl index e62764335..7a8530667 100644 --- a/test/utils_tests.bzl +++ b/test/utils_tests.bzl @@ -11,6 +11,9 @@ load("//swift/internal:utils.bzl", "include_developer_search_paths") # buildifier: disable=bzl-visibility load("//swift/internal/extensions:standalone_toolchain.bzl", "get_download_url") +# buildifier: disable=bzl-visibility +load("//swift/internal/extensions:swift_sdk_releases.bzl", "swift_sdk_download_url") + def _include_developer_search_paths_test(ctx): env = unittest.begin(ctx) @@ -168,10 +171,33 @@ def _standalone_toolchain_download_url_test(ctx): standalone_toolchain_download_url_test = unittest.make(_standalone_toolchain_download_url_test) +def _swift_sdk_download_url_test(ctx): + env = unittest.begin(ctx) + + tests = [ + struct( + version = "6.3.2", + sdk = "wasm", + expected = "https://download.swift.org/swift-6.3.2-release/wasm-sdk/swift-6.3.2-RELEASE/swift-6.3.2-RELEASE_wasm.artifactbundle.tar.gz", + ), + struct( + version = "6.3.2", + sdk = "android", + expected = "https://download.swift.org/swift-6.3.2-release/android-sdk/swift-6.3.2-RELEASE/swift-6.3.2-RELEASE_android.artifactbundle.tar.gz", + ), + ] + for t in tests: + asserts.equals(env, t.expected, swift_sdk_download_url(t.version, t.sdk)) + + return unittest.end(env) + +swift_sdk_download_url_test = unittest.make(_swift_sdk_download_url_test) + def utils_test_suite(name): return unittest.suite( name, developer_dirs_linkopts_test, include_developer_search_paths_test, standalone_toolchain_download_url_test, + swift_sdk_download_url_test, )