Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions rust/private/rustc.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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<os>-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<os>-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<os>-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<os>-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.

Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions test/unit/apple_deployment_target/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
)
209 changes: 209 additions & 0 deletions test/unit/apple_deployment_target/apple_deployment_target_test.bzl
Original file line number Diff line number Diff line change
@@ -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",
],
)
1 change: 1 addition & 0 deletions test/unit/apple_deployment_target/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn main() {}
Loading