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..03c575dbe1 --- /dev/null +++ b/test/unit/apple_deployment_target/apple_deployment_target_test.bzl @@ -0,0 +1,209 @@ +"""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"] + +# 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 _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[DepActionsInfo].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) + +no_deployment_target_without_apple_target_test = analysistest.make(_no_deployment_target_without_apple_target_test_impl) + +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) + + 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", + tags = ["manual", "nobuild"], + ) + + rust_binary( + name = "bin_with_rustc_env", + 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): + """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_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_with_linux_target", + target_compatible_with = NOT_MACOS, + ) + + user_deployment_target_wins_test( + name = "rustc_env_deployment_target_wins_test", + target_under_test = ":bin_with_rustc_env_and_macos_target", + ) + + user_deployment_target_wins_test( + name = "action_env_deployment_target_wins_test", + target_under_test = ":bin_with_action_env_and_macos_target", + ) + + 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() {}