From 2ce49ef335c8041689e60256da9981882c5db9eb Mon Sep 17 00:00:00 2001 From: drakeo338 Date: Thu, 24 Sep 2026 18:31:45 +0000 Subject: [PATCH 1/3] Set the Apple deployment target for rustc from the linker args An Apple cc_toolchain passes its deployment target to clang inside the `-target` triple, e.g. `arm64-apple-macosx26.0`, which rules_rust forwards to rustc as link args. rustc does not read those: it takes the deployment target from `MACOSX_DEPLOYMENT_TARGET` (or the per-OS sibling) and otherwise falls back to its own default, which it passes to the linker as `-mmacosx-version-min=11.0.0`. clang sees both, and since Rust 1.97 surfaces linker stderr every link prints `overriding '-mmacosx-version-min=11.0.0' option with '-target ...'`. Derive the variable from the forwarded link args and set it in the Rustc action's environment: `-target`/`--target=` triples for macos, ios (including simulator and Mac Catalyst), tvos, watchos and xros/visionos, or `-m-version-min=` when no triple carries a version. The link args are still forwarded unchanged, and a value the user sets through `--action_env`, `rustc_env` or the toolchain's `env` is kept. Fixes #4149 Assisted-by: Claude --- rust/private/rustc.bzl | 74 +++++++++ test/unit/apple_deployment_target/BUILD.bazel | 5 + .../apple_deployment_target_test.bzl | 150 ++++++++++++++++++ test/unit/apple_deployment_target/main.rs | 1 + 4 files changed, 230 insertions(+) create mode 100644 test/unit/apple_deployment_target/BUILD.bazel create mode 100644 test/unit/apple_deployment_target/apple_deployment_target_test.bzl create mode 100644 test/unit/apple_deployment_target/main.rs diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index d447c97303..4b24462e0b 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -518,6 +518,71 @@ def get_linker_and_args(ctx, crate_type, toolchain, cc_toolchain, feature_config return ld, ld_is_direct_driver, link_args, link_env +# The environment variables rustc reads the Apple deployment target from, +# keyed by the OS names clang accepts in a `-target` triple and in +# `-m-version-min=`. +_APPLE_DEPLOYMENT_TARGET_ENV = { + "ios": "IPHONEOS_DEPLOYMENT_TARGET", + "iphoneos": "IPHONEOS_DEPLOYMENT_TARGET", + "macos": "MACOSX_DEPLOYMENT_TARGET", + "macosx": "MACOSX_DEPLOYMENT_TARGET", + "tvos": "TVOS_DEPLOYMENT_TARGET", + "visionos": "XROS_DEPLOYMENT_TARGET", + "watchos": "WATCHOS_DEPLOYMENT_TARGET", + "xros": "XROS_DEPLOYMENT_TARGET", +} + +def _split_os_version(os_version): + """Splits `macosx26.0` into `("macosx", "26.0")`. The version may be empty.""" + for i in range(len(os_version)): + if os_version[i].isdigit(): + return os_version[:i], os_version[i:] + return os_version, "" + +def apple_deployment_target_env(link_args): + """Derives the Apple deployment target variable rustc expects from linker arguments. + + An Apple cc_toolchain tells clang its deployment target through the + `-target` triple (e.g. `arm64-apple-macosx26.0`) or `-m-version-min=`. + rustc does not read the link args it forwards: it takes the deployment + target from `MACOSX_DEPLOYMENT_TARGET` and its per-OS siblings and, when + those are unset, from its own default, which it passes to the linker as + `-m-version-min=`. clang then warns about the conflicting options on + every link (bazelbuild/rules_rust#4149). + + As in clang, a version carried by the last `-target` triple wins over + `-m-version-min=`. + + Args: + link_args (list): The linker arguments forwarded to rustc. + + Returns: + dict: The deployment target variable to set for rustc, if any. + """ + triple = None + version_min = None + for i, arg in enumerate(link_args): + if arg == "-target" and i + 1 < len(link_args): + triple = link_args[i + 1] + elif arg.startswith("--target="): + triple = arg[len("--target="):] + elif arg.startswith("-m") and "-version-min=" in arg: + version_min = arg[len("-m"):].split("-version-min=", 1) + + os, version = None, None + if triple: + parts = triple.split("-") + if len(parts) >= 3 and parts[1] == "apple": + os, version = _split_os_version(parts[2]) + if not version and version_min: + # `-mios-simulator-version-min=` names the OS before the environment. + os, version = version_min[0].split("-")[0], version_min[1] + + var = _APPLE_DEPLOYMENT_TARGET_ENV.get(os) + if not var or not version: + return {} + return {var: version} + def symlink_for_ambiguous_lib(actions, toolchain, crate_info, lib): """Constructs a disambiguating symlink for a library dependency. @@ -1360,6 +1425,15 @@ def construct_arguments( ) env.update(link_env) + + # rustc only learns the Apple deployment target from its environment, + # so mirror the one the cc_toolchain encodes in its link args. A value + # from `--action_env` is kept; `rustc_env` and the toolchain's `env` + # are applied below and override this one. + for key, value in apple_deployment_target_env(link_args).items(): + if key not in ctx.configuration.default_shell_env: + env[key] = value + rustc_flags.add(ld, format = "--codegen=linker=%s") # Split link args into individual "--codegen=link-arg=" flags to handle nested spaces. diff --git a/test/unit/apple_deployment_target/BUILD.bazel b/test/unit/apple_deployment_target/BUILD.bazel new file mode 100644 index 0000000000..684d89089d --- /dev/null +++ b/test/unit/apple_deployment_target/BUILD.bazel @@ -0,0 +1,5 @@ +load(":apple_deployment_target_test.bzl", "apple_deployment_target_test_suite") + +apple_deployment_target_test_suite( + name = "apple_deployment_target_test_suite", +) diff --git a/test/unit/apple_deployment_target/apple_deployment_target_test.bzl b/test/unit/apple_deployment_target/apple_deployment_target_test.bzl new file mode 100644 index 0000000000..5bbf44f0b2 --- /dev/null +++ b/test/unit/apple_deployment_target/apple_deployment_target_test.bzl @@ -0,0 +1,150 @@ +"""Unittests for the Apple deployment target rustc actions derive from linker args.""" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts", "unittest") +load("//rust:defs.bzl", "rust_binary") + +# buildifier: disable=bzl-visibility +load("//rust/private:rustc.bzl", "apple_deployment_target_env") +load("//test/unit:common.bzl", "assert_env_value") + +_MACOS_TARGET_LINKOPT = ["-target", "arm64-apple-macosx26.0"] + +def _rustc_action(env): + target = analysistest.target_under_test(env) + actions = [action for action in target.actions if action.mnemonic == "Rustc"] + asserts.equals(env, 1, len(actions)) + return actions[0] + +def _deployment_target_from_linkopt_test_impl(ctx): + env = analysistest.begin(ctx) + assert_env_value(env, _rustc_action(env), "MACOSX_DEPLOYMENT_TARGET", "26.0") + return analysistest.end(env) + +def _no_deployment_target_without_apple_target_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.false(env, "MACOSX_DEPLOYMENT_TARGET" in _rustc_action(env).env) + return analysistest.end(env) + +def _user_deployment_target_wins_test_impl(ctx): + env = analysistest.begin(ctx) + assert_env_value(env, _rustc_action(env), "MACOSX_DEPLOYMENT_TARGET", "15.0") + return analysistest.end(env) + +deployment_target_from_linkopt_test = analysistest.make( + _deployment_target_from_linkopt_test_impl, + config_settings = { + "//command_line_option:linkopt": _MACOS_TARGET_LINKOPT, + }, +) + +no_deployment_target_without_apple_target_test = analysistest.make( + _no_deployment_target_without_apple_target_test_impl, + config_settings = { + "//command_line_option:linkopt": ["-target", "x86_64-unknown-linux-gnu"], + }, +) + +rustc_env_deployment_target_wins_test = analysistest.make( + _user_deployment_target_wins_test_impl, + config_settings = { + "//command_line_option:linkopt": _MACOS_TARGET_LINKOPT, + }, +) + +action_env_deployment_target_wins_test = analysistest.make( + _user_deployment_target_wins_test_impl, + config_settings = { + "//command_line_option:action_env": ["MACOSX_DEPLOYMENT_TARGET=15.0"], + "//command_line_option:linkopt": _MACOS_TARGET_LINKOPT, + }, +) + +def _apple_deployment_target_env_test_impl(ctx): + env = unittest.begin(ctx) + + asserts.equals(env, {"MACOSX_DEPLOYMENT_TARGET": "26.0"}, apple_deployment_target_env(["-target", "arm64-apple-macosx26.0"])) + asserts.equals(env, {"MACOSX_DEPLOYMENT_TARGET": "14.0"}, apple_deployment_target_env(["--target=arm64-apple-macos14.0"])) + asserts.equals(env, {"IPHONEOS_DEPLOYMENT_TARGET": "17.0"}, apple_deployment_target_env(["-target", "arm64-apple-ios17.0"])) + asserts.equals(env, {"IPHONEOS_DEPLOYMENT_TARGET": "17.0"}, apple_deployment_target_env(["-target", "arm64-apple-ios17.0-simulator"])) + asserts.equals(env, {"IPHONEOS_DEPLOYMENT_TARGET": "17.0"}, apple_deployment_target_env(["-target", "arm64-apple-ios17.0-macabi"])) + asserts.equals(env, {"TVOS_DEPLOYMENT_TARGET": "17.0"}, apple_deployment_target_env(["-target", "arm64-apple-tvos17.0"])) + asserts.equals(env, {"WATCHOS_DEPLOYMENT_TARGET": "10.0"}, apple_deployment_target_env(["-target", "arm64_32-apple-watchos10.0"])) + asserts.equals(env, {"XROS_DEPLOYMENT_TARGET": "2.0"}, apple_deployment_target_env(["-target", "arm64-apple-xros2.0"])) + asserts.equals(env, {"XROS_DEPLOYMENT_TARGET": "2.0"}, apple_deployment_target_env(["-target", "arm64-apple-visionos2.0-simulator"])) + + # The version-min flag is used when no target triple carries a version. + asserts.equals(env, {"MACOSX_DEPLOYMENT_TARGET": "13.0"}, apple_deployment_target_env(["-mmacosx-version-min=13.0"])) + asserts.equals(env, {"IPHONEOS_DEPLOYMENT_TARGET": "16.0"}, apple_deployment_target_env(["-mios-simulator-version-min=16.0"])) + asserts.equals(env, {"MACOSX_DEPLOYMENT_TARGET": "13.0"}, apple_deployment_target_env(["-target", "arm64-apple-macosx", "-mmacosx-version-min=13.0"])) + + # A versioned triple wins over the version-min flag, and the last triple wins. + asserts.equals(env, {"MACOSX_DEPLOYMENT_TARGET": "26.0"}, apple_deployment_target_env(["-mmacosx-version-min=11.0", "-target", "arm64-apple-macosx26.0"])) + asserts.equals(env, {"MACOSX_DEPLOYMENT_TARGET": "26.0"}, apple_deployment_target_env(["-target", "arm64-apple-macosx13.0", "-target", "arm64-apple-macosx26.0"])) + + # Nothing to derive: no version, no Apple OS, or no target at all. + asserts.equals(env, {}, apple_deployment_target_env(["-target", "arm64-apple-macosx"])) + asserts.equals(env, {}, apple_deployment_target_env(["-target", "arm64-apple-darwin23"])) + asserts.equals(env, {}, apple_deployment_target_env(["-target", "x86_64-unknown-linux-gnu", "-lfoo"])) + asserts.equals(env, {}, apple_deployment_target_env(["-target"])) + asserts.equals(env, {}, apple_deployment_target_env([])) + + return unittest.end(env) + +apple_deployment_target_env_test = unittest.make(_apple_deployment_target_env_test_impl) + +def _define_test_targets(): + rust_binary( + name = "bin", + srcs = ["main.rs"], + edition = "2021", + ) + + rust_binary( + name = "bin_with_rustc_env", + srcs = ["main.rs"], + edition = "2021", + rustc_env = {"MACOSX_DEPLOYMENT_TARGET": "15.0"}, + ) + +def apple_deployment_target_test_suite(name): + """Entry-point macro called from the BUILD file. + + Args: + name (str): Name of the macro. + """ + _define_test_targets() + + deployment_target_from_linkopt_test( + name = "deployment_target_from_linkopt_test", + target_under_test = ":bin", + ) + + no_deployment_target_without_apple_target_test( + name = "no_deployment_target_without_apple_target_test", + target_under_test = ":bin", + ) + + rustc_env_deployment_target_wins_test( + name = "rustc_env_deployment_target_wins_test", + target_under_test = ":bin_with_rustc_env", + ) + + action_env_deployment_target_wins_test( + name = "action_env_deployment_target_wins_test", + target_under_test = ":bin", + ) + + apple_deployment_target_env_test( + name = "apple_deployment_target_env_test", + ) + + native.test_suite( + name = name, + tests = [ + ":deployment_target_from_linkopt_test", + ":no_deployment_target_without_apple_target_test", + ":rustc_env_deployment_target_wins_test", + ":action_env_deployment_target_wins_test", + ":apple_deployment_target_env_test", + ], + ) diff --git a/test/unit/apple_deployment_target/main.rs b/test/unit/apple_deployment_target/main.rs new file mode 100644 index 0000000000..f328e4d9d0 --- /dev/null +++ b/test/unit/apple_deployment_target/main.rs @@ -0,0 +1 @@ +fn main() {} From efa417813e67c1978313ea88055887a955ab6e6a Mon Sep 17 00:00:00 2001 From: drakeo338 Date: Thu, 24 Sep 2026 18:48:21 +0000 Subject: [PATCH 2/3] Skip the linkopt deployment target tests on macOS The Apple cc_toolchain from apple_support appends its own `-target` triple after the user's link flags, so on a macOS host the last triple in the Rustc action's link args is the toolchain's, not the one these tests pass through `--linkopt`. The derived `MACOSX_DEPLOYMENT_TARGET` then carries the toolchain's minimum OS rather than `26.0`, and the test that expects no variable at all sees one. Both would fail on every macOS CI task. Mark those two tests incompatible with macOS, as the other unit tests do for host-specific cases. The precedence tests and the parser unit test hold on every host and keep running everywhere. Assisted-by: Claude --- .../apple_deployment_target_test.bzl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/unit/apple_deployment_target/apple_deployment_target_test.bzl b/test/unit/apple_deployment_target/apple_deployment_target_test.bzl index 5bbf44f0b2..08df4bae5a 100644 --- a/test/unit/apple_deployment_target/apple_deployment_target_test.bzl +++ b/test/unit/apple_deployment_target/apple_deployment_target_test.bzl @@ -9,6 +9,15 @@ load("//test/unit:common.bzl", "assert_env_value") _MACOS_TARGET_LINKOPT = ["-target", "arm64-apple-macosx26.0"] +# The Apple cc_toolchain appends its own `-target` triple after the user's link +# flags, so on macOS the last triple in the link args is the toolchain's rather +# than the one passed through `--linkopt`. Tests that pin the derived value to +# the `--linkopt` triple only hold on other hosts. +NOT_MACOS = select({ + "@platforms//os:macos": ["@platforms//:incompatible"], + "//conditions:default": [], +}) + def _rustc_action(env): target = analysistest.target_under_test(env) actions = [action for action in target.actions if action.mnemonic == "Rustc"] @@ -117,11 +126,13 @@ def apple_deployment_target_test_suite(name): deployment_target_from_linkopt_test( name = "deployment_target_from_linkopt_test", target_under_test = ":bin", + target_compatible_with = NOT_MACOS, ) no_deployment_target_without_apple_target_test( name = "no_deployment_target_without_apple_target_test", target_under_test = ":bin", + target_compatible_with = NOT_MACOS, ) rustc_env_deployment_target_wins_test( From 98e0081e334ad48543eb908ccbbda3f58193ac13 Mon Sep 17 00:00:00 2001 From: drakeo338 Date: Thu, 24 Sep 2026 21:52:46 +0000 Subject: [PATCH 3/3] Never link the deployment target test binaries bazel coverage builds a test's dependencies, so the binaries analysed under an Apple -target linkopt were linked by gcc on Linux and failed. The tests now read the Rustc actions through a small wrapper rule that applies the link settings as a transition and forwards only the actions, with empty instrumented files so coverage does not walk into the binary. Assisted-by: Claude --- .../apple_deployment_target_test.bzl | 114 +++++++++++++----- 1 file changed, 81 insertions(+), 33 deletions(-) diff --git a/test/unit/apple_deployment_target/apple_deployment_target_test.bzl b/test/unit/apple_deployment_target/apple_deployment_target_test.bzl index 08df4bae5a..03c575dbe1 100644 --- a/test/unit/apple_deployment_target/apple_deployment_target_test.bzl +++ b/test/unit/apple_deployment_target/apple_deployment_target_test.bzl @@ -18,9 +18,49 @@ NOT_MACOS = select({ "//conditions:default": [], }) +def _with_link_settings_transition_impl(_settings, attr): + return { + "//command_line_option:action_env": attr.action_env, + "//command_line_option:linkopt": attr.linkopts, + } + +with_link_settings_transition = transition( + implementation = _with_link_settings_transition_impl, + inputs = [], + outputs = [ + "//command_line_option:action_env", + "//command_line_option:linkopt", + ], +) + +DepActionsInfo = provider( + "Contains information about dependencies actions.", + fields = {"actions": "List[Action]"}, +) + +def _with_link_settings_impl(ctx): + # Only the actions are forwarded. The `-target` flag under test is not one + # every host linker accepts, and `bazel coverage` builds the runfiles of a + # test's dependencies, so the transitioned binary must never be linked. + return [ + DepActionsInfo(actions = ctx.attr.target[0].actions), + # Without this, coverage walks every dependency attribute for + # instrumented files and builds the transitioned binary after all. + coverage_common.instrumented_files_info(ctx, dependency_attributes = []), + ] + +with_link_settings = rule( + implementation = _with_link_settings_impl, + attrs = { + "action_env": attr.string_list(), + "linkopts": attr.string_list(), + "target": attr.label(cfg = with_link_settings_transition), + }, +) + def _rustc_action(env): target = analysistest.target_under_test(env) - actions = [action for action in target.actions if action.mnemonic == "Rustc"] + actions = [action for action in target[DepActionsInfo].actions if action.mnemonic == "Rustc"] asserts.equals(env, 1, len(actions)) return actions[0] @@ -39,34 +79,11 @@ def _user_deployment_target_wins_test_impl(ctx): assert_env_value(env, _rustc_action(env), "MACOSX_DEPLOYMENT_TARGET", "15.0") return analysistest.end(env) -deployment_target_from_linkopt_test = analysistest.make( - _deployment_target_from_linkopt_test_impl, - config_settings = { - "//command_line_option:linkopt": _MACOS_TARGET_LINKOPT, - }, -) - -no_deployment_target_without_apple_target_test = analysistest.make( - _no_deployment_target_without_apple_target_test_impl, - config_settings = { - "//command_line_option:linkopt": ["-target", "x86_64-unknown-linux-gnu"], - }, -) +deployment_target_from_linkopt_test = analysistest.make(_deployment_target_from_linkopt_test_impl) -rustc_env_deployment_target_wins_test = analysistest.make( - _user_deployment_target_wins_test_impl, - config_settings = { - "//command_line_option:linkopt": _MACOS_TARGET_LINKOPT, - }, -) +no_deployment_target_without_apple_target_test = analysistest.make(_no_deployment_target_without_apple_target_test_impl) -action_env_deployment_target_wins_test = analysistest.make( - _user_deployment_target_wins_test_impl, - config_settings = { - "//command_line_option:action_env": ["MACOSX_DEPLOYMENT_TARGET=15.0"], - "//command_line_option:linkopt": _MACOS_TARGET_LINKOPT, - }, -) +user_deployment_target_wins_test = analysistest.make(_user_deployment_target_wins_test_impl) def _apple_deployment_target_env_test_impl(ctx): env = unittest.begin(ctx) @@ -106,6 +123,7 @@ def _define_test_targets(): name = "bin", srcs = ["main.rs"], edition = "2021", + tags = ["manual", "nobuild"], ) rust_binary( @@ -113,6 +131,36 @@ def _define_test_targets(): srcs = ["main.rs"], edition = "2021", rustc_env = {"MACOSX_DEPLOYMENT_TARGET": "15.0"}, + tags = ["manual", "nobuild"], + ) + + with_link_settings( + name = "bin_with_macos_target", + linkopts = _MACOS_TARGET_LINKOPT, + tags = ["manual"], + target = ":bin", + ) + + with_link_settings( + name = "bin_with_linux_target", + linkopts = ["-target", "x86_64-unknown-linux-gnu"], + tags = ["manual"], + target = ":bin", + ) + + with_link_settings( + name = "bin_with_rustc_env_and_macos_target", + linkopts = _MACOS_TARGET_LINKOPT, + tags = ["manual"], + target = ":bin_with_rustc_env", + ) + + with_link_settings( + name = "bin_with_action_env_and_macos_target", + action_env = ["MACOSX_DEPLOYMENT_TARGET=15.0"], + linkopts = _MACOS_TARGET_LINKOPT, + tags = ["manual"], + target = ":bin", ) def apple_deployment_target_test_suite(name): @@ -125,24 +173,24 @@ def apple_deployment_target_test_suite(name): deployment_target_from_linkopt_test( name = "deployment_target_from_linkopt_test", - target_under_test = ":bin", + target_under_test = ":bin_with_macos_target", target_compatible_with = NOT_MACOS, ) no_deployment_target_without_apple_target_test( name = "no_deployment_target_without_apple_target_test", - target_under_test = ":bin", + target_under_test = ":bin_with_linux_target", target_compatible_with = NOT_MACOS, ) - rustc_env_deployment_target_wins_test( + user_deployment_target_wins_test( name = "rustc_env_deployment_target_wins_test", - target_under_test = ":bin_with_rustc_env", + target_under_test = ":bin_with_rustc_env_and_macos_target", ) - action_env_deployment_target_wins_test( + user_deployment_target_wins_test( name = "action_env_deployment_target_wins_test", - target_under_test = ":bin", + target_under_test = ":bin_with_action_env_and_macos_target", ) apple_deployment_target_env_test(