From 25785991b8e92d716d3ab6b5a6317a6ac75a9cfe Mon Sep 17 00:00:00 2001 From: Mathieu Olivari Date: Tue, 30 Sep 2025 13:22:25 -0700 Subject: [PATCH] feat(swift): add embedded Swift feature Embedded swift is documented here: https://www.swift.org/get-started/embedded/ This change does multiple things required to get a little closer to being able to build embedded code in rules_swift: * add a new swift.enable_embedded feature that automatically add the "-enable-experimental-feature Embedded" option to all swift compile actions * when the feature is enabled, WMO will also be enabled automatically, as this is a requirement when using swift-embedded * default alwayslink to False when the embedded feature is enabled. Indeed, when bulding for embedded, the features that require -force_load cannot be used, and we want the resulted binaries to be as small as possible. --- doc/api.md | 2 +- doc/rules.md | 6 +++--- swift/internal/attrs.bzl | 7 ++++++- swift/internal/compiling.bzl | 5 +++++ swift/internal/feature_names.bzl | 9 +++++++++ swift/internal/linking.bzl | 15 +++++++++++++-- swift/toolchains/config/compile_config.bzl | 15 +++++++++++++++ test/features_tests.bzl | 19 +++++++++++++++++++ 8 files changed, 71 insertions(+), 7 deletions(-) diff --git a/doc/api.md b/doc/api.md index e06c35d65..37dac2de0 100644 --- a/doc/api.md +++ b/doc/api.md @@ -385,7 +385,7 @@ command line parameters file, those actions will be created here. | :------------- | :------------- | :------------- | | actions | The context's `actions` object. | none | | additional_inputs | A `list` of `File`s containing any additional files that are referenced by `user_link_flags` and therefore need to be propagated up to the linker. | `[]` | -| alwayslink | If `False`, any binary that depends on the providers returned by this function will link in all of the library's object files only if there are symbol references. See the discussion on `swift_library` `alwayslink` for why that behavior could result in undesired results. | `True` | +| alwayslink | If `False`, any binary that depends on the providers returned by this function will link in all of the library's object files only if there are symbol references. See the discussion on `swift_library` `alwayslink` for why that behavior could result in undesired results. If `True`, the opposite will happen and this library will be -force_load into the link actions that depend on it. By default (`None`), the value of alwayslink will be set to `True` in the general case, and to `False` if the embedded swift feature is turned on. | `None` | | compilation_outputs | A `CcCompilationOutputs` value containing the object files to link. Typically, this is the second tuple element in the value returned by `compile`. | none | | feature_configuration | A feature configuration obtained from `configure_features`. | none | | is_test | Deprecated. This argument will be removed in the next major release. Use the `include_dev_srch_paths` attribute instead. Represents if the `testonly` value of the context. | `None` | diff --git a/doc/rules.md b/doc/rules.md index 08dd995f2..0ccecf528 100644 --- a/doc/rules.md +++ b/doc/rules.md @@ -473,7 +473,7 @@ Compiles and links Swift code into a static library and Swift module. | srcs | A list of `.swift` source files that will be compiled into the library.

Except in very rare circumstances, a Swift source file should only appear in a single `swift_*` target. Adding the same source file to multiple `swift_*` targets can lead to binary bloat and/or symbol collisions. If specific sources need to be shared by multiple targets, consider factoring them out into their own `swift_library` instead. | List of labels | required | | | data | The list of files needed by this target at runtime.

Files and targets named in the `data` attribute will appear in the `*.runfiles` area of this target, if it has one. This may include data files needed by a binary or library, or other programs needed by it. | List of labels | optional | `[]` | | always_include_developer_search_paths | If `True`, the developer framework search paths will be added to the compilation command. This enables a Swift module to access `XCTest` without having to mark the target as `testonly = True`. | Boolean | optional | `False` | -| alwayslink | If `False`, any binary that depends (directly or indirectly) on this Swift module will only link in all the object files for the files listed in `srcs` when there is a direct symbol reference.

Swift protocol conformances don't create linker references. Likewise, if the Swift code has Objective-C classes/methods, their usage does not always result in linker references.

_"All the object files"_ for this module is also somewhat fuzzy. Unlike C, C++, and Objective-C, where each source file results in a `.o` file; for Swift the number of .o files depends on the compiler options (`-wmo`/`-whole-module-optimization`, `-num-threads`). That makes relying on linker reference more fragile, and any individual .swift file in `srcs` may/may not get picked up based on the linker references to other files that happen to get batched into a single `.o` by the compiler options used.

Swift Package Manager always passes the individual `.o` files to the linker instead of using intermediate static libraries, so it effectively is the same as `alwayslink = True`. | Boolean | optional | `True` | +| alwayslink | If `False`, any binary that depends (directly or indirectly) on this Swift module will only link in all the object files for the files listed in `srcs` when there is a direct symbol reference.

Swift protocol conformances don't create linker references. Likewise, if the Swift code has Objective-C classes/methods, their usage does not always result in linker references.

_"All the object files"_ for this module is also somewhat fuzzy. Unlike C, C++, and Objective-C, where each source file results in a `.o` file; for Swift the number of .o files depends on the compiler options (`-wmo`/`-whole-module-optimization`, `-num-threads`). That makes relying on linker reference more fragile, and any individual .swift file in `srcs` may/may not get picked up based on the linker references to other files that happen to get batched into a single `.o` by the compiler options used.

Swift Package Manager always passes the individual `.o` files to the linker instead of using intermediate static libraries, so it effectively is the same as `alwayslink = True`.

Note that by default, this value will default to True. But if the swift.enable_embedded feature is on, this value will default to False, as the swift features that cause -force_load to be required (such as reflection) are not available in that mode. | Boolean | optional | `False` | | copts | Additional compiler options that should be passed to `swiftc`. These strings are subject to `$(location ...)` and ["Make" variable](https://docs.bazel.build/versions/master/be/make-variables.html) expansion. | List of strings | optional | `[]` | | 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 | `[]` | | generated_header_name | The name of the generated Objective-C interface header. This name must end with a `.h` extension and cannot contain any path separators.

If this attribute is not specified, then the default behavior is to name the header `${target_name}-Swift.h`.

This attribute is ignored if the toolchain does not support generating headers. | String | optional | `""` | @@ -700,7 +700,7 @@ almost always an anti-pattern. | deps | A list of targets that are dependencies of the target being built, which will be linked into that target.

If the Swift toolchain supports implementation-only imports (`private_deps` on `swift_library`), then targets in `deps` are treated as regular (non-implementation-only) imports that are propagated both to their direct and indirect (transitive) dependents.

Allowed kinds of dependencies are:

* `swift_library` (or anything propagating `SwiftInfo`)

* `cc_library` and `objc_library` (or anything propagating `CcInfo`) | List of labels | optional | `[]` | | srcs | A list of `.swift` source files that will be compiled into the library.

Except in very rare circumstances, a Swift source file should only appear in a single `swift_*` target. Adding the same source file to multiple `swift_*` targets can lead to binary bloat and/or symbol collisions. If specific sources need to be shared by multiple targets, consider factoring them out into their own `swift_library` instead. | List of labels | required | | | always_include_developer_search_paths | If `True`, the developer framework search paths will be added to the compilation command. This enables a Swift module to access `XCTest` without having to mark the target as `testonly = True`. | Boolean | optional | `False` | -| alwayslink | If `False`, any binary that depends (directly or indirectly) on this Swift module will only link in all the object files for the files listed in `srcs` when there is a direct symbol reference.

Swift protocol conformances don't create linker references. Likewise, if the Swift code has Objective-C classes/methods, their usage does not always result in linker references.

_"All the object files"_ for this module is also somewhat fuzzy. Unlike C, C++, and Objective-C, where each source file results in a `.o` file; for Swift the number of .o files depends on the compiler options (`-wmo`/`-whole-module-optimization`, `-num-threads`). That makes relying on linker reference more fragile, and any individual .swift file in `srcs` may/may not get picked up based on the linker references to other files that happen to get batched into a single `.o` by the compiler options used.

Swift Package Manager always passes the individual `.o` files to the linker instead of using intermediate static libraries, so it effectively is the same as `alwayslink = True`. | Boolean | optional | `True` | +| alwayslink | If `False`, any binary that depends (directly or indirectly) on this Swift module will only link in all the object files for the files listed in `srcs` when there is a direct symbol reference.

Swift protocol conformances don't create linker references. Likewise, if the Swift code has Objective-C classes/methods, their usage does not always result in linker references.

_"All the object files"_ for this module is also somewhat fuzzy. Unlike C, C++, and Objective-C, where each source file results in a `.o` file; for Swift the number of .o files depends on the compiler options (`-wmo`/`-whole-module-optimization`, `-num-threads`). That makes relying on linker reference more fragile, and any individual .swift file in `srcs` may/may not get picked up based on the linker references to other files that happen to get batched into a single `.o` by the compiler options used.

Swift Package Manager always passes the individual `.o` files to the linker instead of using intermediate static libraries, so it effectively is the same as `alwayslink = True`.

Note that by default, this value will default to True. But if the swift.enable_embedded feature is on, this value will default to False, as the swift features that cause -force_load to be required (such as reflection) are not available in that mode. | Boolean | optional | `False` | | copts | Additional compiler options that should be passed to `swiftc`. These strings are subject to `$(location ...)` and ["Make" variable](https://docs.bazel.build/versions/master/be/make-variables.html) expansion. | List of strings | optional | `[]` | | 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 | `[]` | | library_evolution | Indicates whether the Swift code should be compiled with library evolution mode enabled.

This attribute should be used to compile a module that will be distributed as part of a client-facing (non-implementation-only) module in a library or framework that will be distributed for use outside of the Bazel build graph. Setting this to true will compile the module with the `-library-evolution` flag and emit a `.swiftinterface` file as one of the compilation outputs. | Boolean | optional | `False` | @@ -824,7 +824,7 @@ swift_proto_library( | additional_compiler_deps | List of additional dependencies required by the generated Swift code at compile time, whose SwiftProtoInfo will be ignored.

Allowed kinds of dependencies are:

* `swift_library` (or anything propagating `SwiftInfo`)

* `cc_library` and `objc_library` (or anything propagating `CcInfo`) | List of labels | optional | `[]` | | additional_compiler_info | Dictionary of additional information passed to the compiler targets. See the documentation of the respective compiler rules for more information on which fields are accepted and how they are used. | Dictionary: String -> String | optional | `{}` | | always_include_developer_search_paths | If `True`, the developer framework search paths will be added to the compilation command. This enables a Swift module to access `XCTest` without having to mark the target as `testonly = True`. | Boolean | optional | `False` | -| alwayslink | If `False`, any binary that depends (directly or indirectly) on this Swift module will only link in all the object files for the files listed in `srcs` when there is a direct symbol reference.

Swift protocol conformances don't create linker references. Likewise, if the Swift code has Objective-C classes/methods, their usage does not always result in linker references.

_"All the object files"_ for this module is also somewhat fuzzy. Unlike C, C++, and Objective-C, where each source file results in a `.o` file; for Swift the number of .o files depends on the compiler options (`-wmo`/`-whole-module-optimization`, `-num-threads`). That makes relying on linker reference more fragile, and any individual .swift file in `srcs` may/may not get picked up based on the linker references to other files that happen to get batched into a single `.o` by the compiler options used.

Swift Package Manager always passes the individual `.o` files to the linker instead of using intermediate static libraries, so it effectively is the same as `alwayslink = True`. | Boolean | optional | `True` | +| alwayslink | If `False`, any binary that depends (directly or indirectly) on this Swift module will only link in all the object files for the files listed in `srcs` when there is a direct symbol reference.

Swift protocol conformances don't create linker references. Likewise, if the Swift code has Objective-C classes/methods, their usage does not always result in linker references.

_"All the object files"_ for this module is also somewhat fuzzy. Unlike C, C++, and Objective-C, where each source file results in a `.o` file; for Swift the number of .o files depends on the compiler options (`-wmo`/`-whole-module-optimization`, `-num-threads`). That makes relying on linker reference more fragile, and any individual .swift file in `srcs` may/may not get picked up based on the linker references to other files that happen to get batched into a single `.o` by the compiler options used.

Swift Package Manager always passes the individual `.o` files to the linker instead of using intermediate static libraries, so it effectively is the same as `alwayslink = True`.

Note that by default, this value will default to True. But if the swift.enable_embedded feature is on, this value will default to False, as the swift features that cause -force_load to be required (such as reflection) are not available in that mode. | Boolean | optional | `False` | | compilers | One or more `swift_proto_compiler` targets (or targets producing `SwiftProtoCompilerInfo`), from which the Swift protos will be generated. | List of labels | optional | `["@rules_swift//proto/compilers:swift_proto"]` | | copts | Additional compiler options that should be passed to `swiftc`. These strings are subject to `$(location ...)` and ["Make" variable](https://docs.bazel.build/versions/master/be/make-variables.html) expansion. | List of strings | optional | `[]` | | 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 | `[]` | diff --git a/swift/internal/attrs.bzl b/swift/internal/attrs.bzl index 86330f0ec..2f960a0c3 100644 --- a/swift/internal/attrs.bzl +++ b/swift/internal/attrs.bzl @@ -297,7 +297,6 @@ and emit a `.swiftinterface` file as one of the compilation outputs. mandatory = False, ), "alwayslink": attr.bool( - default = True, doc = """\ If `False`, any binary that depends (directly or indirectly) on this Swift module will only link in all the object files for the files listed in `srcs` when there @@ -318,7 +317,13 @@ get batched into a single `.o` by the compiler options used. Swift Package Manager always passes the individual `.o` files to the linker instead of using intermediate static libraries, so it effectively is the same as `alwayslink = True`. + +Note that by default, this value will default to True. But if the +swift.enable_embedded feature is on, this value will default to False, as +the swift features that cause -force_load to be required (such as reflection) +are not available in that mode. """, + mandatory = False, ), "generated_header_name": attr.string( doc = """\ diff --git a/swift/internal/compiling.bzl b/swift/internal/compiling.bzl index 94818424d..dfbd725cf 100644 --- a/swift/internal/compiling.bzl +++ b/swift/internal/compiling.bzl @@ -44,6 +44,7 @@ load( "SWIFT_FEATURE_EMIT_PRIVATE_SWIFTINTERFACE", "SWIFT_FEATURE_EMIT_SWIFTDOC", "SWIFT_FEATURE_EMIT_SWIFTINTERFACE", + "SWIFT_FEATURE_ENABLE_EMBEDDED", "SWIFT_FEATURE_FULL_LTO", "SWIFT_FEATURE_HEADERS_ALWAYS_ACTION_INPUTS", "SWIFT_FEATURE_INDEX_WHILE_BUILDING", @@ -1684,6 +1685,10 @@ def _emitted_output_nature(feature_configuration, user_compile_flags): feature_configuration = feature_configuration, feature_name = SWIFT_FEATURE__WMO_IN_SWIFTCOPTS, ) or + is_feature_enabled( + feature_configuration = feature_configuration, + feature_name = SWIFT_FEATURE_ENABLE_EMBEDDED, + ) or are_all_features_enabled( feature_configuration = feature_configuration, feature_names = [SWIFT_FEATURE_OPT, SWIFT_FEATURE_OPT_USES_WMO], diff --git a/swift/internal/feature_names.bzl b/swift/internal/feature_names.bzl index c62bbd0c9..05dd43836 100644 --- a/swift/internal/feature_names.bzl +++ b/swift/internal/feature_names.bzl @@ -411,3 +411,12 @@ SWIFT_FEATURE_FULL_LTO = "swift.full_lto" SWIFT_FEATURE_ENABLE_CPP17_INTEROP = "swift.enable_cpp17_interop" SWIFT_FEATURE_ENABLE_CPP20_INTEROP = "swift.enable_cpp20_interop" SWIFT_FEATURE_ENABLE_CPP23_INTEROP = "swift.enable_cpp23_interop" + +# Enable embedded swift. More details at https://docs.swift.org/embedded/documentation/embedded/waystogetstarted/ +# When enabling that option, a few things will happen: +# * WMO will be enabled automatically, otherwise the compiler will throw an error as it is a requirement for embedded swift +# * none of the libraries in the toolchain's usr/lib/swift directory will be linked. Instead, the core swift functionalities +# will be provided by the swift embedded module in the usr/lib/swift/embedded toolchain directory, and swiftc will +# automatically include it at compile time, and provide it to the linker at link time +# * alwayslink on swift_library() target will now default to False, as it is not required in embedded mode +SWIFT_FEATURE_ENABLE_EMBEDDED = "swift.enable_embedded" diff --git a/swift/internal/linking.bzl b/swift/internal/linking.bzl index aa9e8a6ad..0a51d1504 100644 --- a/swift/internal/linking.bzl +++ b/swift/internal/linking.bzl @@ -28,6 +28,7 @@ load( load(":developer_dirs.bzl", "developer_dirs_linkopts") load( ":feature_names.bzl", + "SWIFT_FEATURE_ENABLE_EMBEDDED", "SWIFT_FEATURE_LLD_GC_WORKAROUND", "SWIFT_FEATURE_OBJC_LINK_FLAGS", ) @@ -191,7 +192,7 @@ def create_linking_context_from_compilation_outputs( *, actions, additional_inputs = [], - alwayslink = True, + alwayslink = None, compilation_outputs, feature_configuration, is_test = None, @@ -221,7 +222,11 @@ def create_linking_context_from_compilation_outputs( returned by this function will link in all of the library's object files only if there are symbol references. See the discussion on `swift_library` `alwayslink` for why that behavior could result - in undesired results. + in undesired results. If `True`, the opposite will happen and this + library will be -force_load into the link actions that depend on it. + By default (`None`), the value of alwayslink will be set to `True` + in the general case, and to `False` if the embedded swift feature is + turned on. compilation_outputs: A `CcCompilationOutputs` value containing the object files to link. Typically, this is the second tuple element in the value returned by `compile`. @@ -322,6 +327,12 @@ def create_linking_context_from_compilation_outputs( ), ) + if alwayslink == None: + alwayslink = not is_feature_enabled( + feature_configuration = feature_configuration, + feature_name = SWIFT_FEATURE_ENABLE_EMBEDDED, + ) + if include_dev_srch_paths != None and is_test != None: fail("""\ Both `include_dev_srch_paths` and `is_test` cannot be specified. Please select \ diff --git a/swift/toolchains/config/compile_config.bzl b/swift/toolchains/config/compile_config.bzl index 776eee3ef..48284477e 100644 --- a/swift/toolchains/config/compile_config.bzl +++ b/swift/toolchains/config/compile_config.bzl @@ -57,6 +57,7 @@ load( "SWIFT_FEATURE_ENABLE_CPP17_INTEROP", "SWIFT_FEATURE_ENABLE_CPP20_INTEROP", "SWIFT_FEATURE_ENABLE_CPP23_INTEROP", + "SWIFT_FEATURE_ENABLE_EMBEDDED", "SWIFT_FEATURE_ENABLE_LIBRARY_EVOLUTION", "SWIFT_FEATURE_ENABLE_SKIP_FUNCTION_BODIES", "SWIFT_FEATURE_ENABLE_TESTING", @@ -386,6 +387,7 @@ def compile_action_configs( features = [ [SWIFT_FEATURE_OPT, SWIFT_FEATURE_OPT_USES_WMO], [SWIFT_FEATURE__WMO_IN_SWIFTCOPTS], + [SWIFT_FEATURE_ENABLE_EMBEDDED], ], ), @@ -1092,6 +1094,7 @@ def compile_action_configs( not_features = [ [SWIFT_FEATURE_OPT, SWIFT_FEATURE_OPT_USES_WMO], [SWIFT_FEATURE__WMO_IN_SWIFTCOPTS], + [SWIFT_FEATURE_ENABLE_EMBEDDED], ], ), @@ -1112,6 +1115,7 @@ def compile_action_configs( features = [ [SWIFT_FEATURE_OPT, SWIFT_FEATURE_OPT_USES_WMO], [SWIFT_FEATURE__WMO_IN_SWIFTCOPTS], + [SWIFT_FEATURE_ENABLE_EMBEDDED], ], not_features = [SWIFT_FEATURE__NUM_THREADS_0_IN_SWIFTCOPTS], ), @@ -1337,6 +1341,17 @@ def compile_action_configs( SWIFT_FEATURE_ENABLE_CPP23_INTEROP, ], ), + ActionConfigInfo( + actions = [ + SWIFT_ACTION_COMPILE, + ], + configurators = [ + add_arg("-enable-experimental-feature", "Embedded"), + ], + features = [ + SWIFT_FEATURE_ENABLE_EMBEDDED, + ], + ), ] # NOTE: The positions of these action configs in the list are important, diff --git a/test/features_tests.bzl b/test/features_tests.bzl index d31d97b93..0ac3409d4 100644 --- a/test/features_tests.bzl +++ b/test/features_tests.bzl @@ -110,6 +110,14 @@ disable_objc_test = make_action_command_line_test_rule( }, ) +embedded_test = make_action_command_line_test_rule( + config_settings = { + "//command_line_option:features": [ + "swift.enable_embedded", + ], + }, +) + def features_test_suite(name, tags = []): """Test suite for various features. @@ -324,6 +332,17 @@ def features_test_suite(name, tags = []): ], ) + embedded_test( + name = "{}_embedded_test".format(name), + tags = all_tags, + expected_argv = [ + "-enable-experimental-feature", + "Embedded", + ], + mnemonic = "SwiftCompile", + target_under_test = "//test/fixtures/basic:second", + ) + native.test_suite( name = name, tags = all_tags,