From 838ee8a11ea2ceec6857d3ca12db92a7627a54b9 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Fri, 29 May 2026 09:36:19 -0700 Subject: [PATCH] Consume cc deps with PIC when rustc emits PIE binaries --- rust/private/rustc.bzl | 196 +++++++++++++++++++++++- rust/private/semver.bzl | 46 ++++-- rust/toolchain.bzl | 19 +++ test/unit/rustc_pie/BUILD.bazel | 3 + test/unit/rustc_pie/rustc_pie_test.bzl | 171 +++++++++++++++++++++ test/unit/rustdoc/rustdoc_unit_test.bzl | 11 +- test/unit/semver/semver_test.bzl | 112 +++++++++++--- 7 files changed, 515 insertions(+), 43 deletions(-) create mode 100644 test/unit/rustc_pie/BUILD.bazel create mode 100644 test/unit/rustc_pie/rustc_pie_test.bzl diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index 5795861dd5..9ba7b64820 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -157,7 +157,184 @@ def _are_linkstamps_supported(feature_configuration): # Is Bazel recent enough to support Starlark linkstamps? hasattr(cc_common, "register_linkstamp_compile_action")) -def _should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation_mode): +_NON_PIE_OS_KEYWORDS = [ + "windows", + "uefi", + "vxworks", + "solaris", + "illumos", + "helenos", + "cygwin", + "l4re", + "lynxos178", + "aix", + "cuda", +] + +_NON_PIE_ARCH_PREFIXES = [ + "wasm", + "xtensa", + "nvptx", + "amdgcn", + "msp430", + "avr", +] + +_NON_PIE_ENV_KEYWORDS = [ + "nuttx", + "espidf", + "rtems", + "xous", + "zkvm", + "qurt", + "psp", + "psx", + "vita", + "3ds", + "vex", +] + +def parse_rustc_version(version_semver): + """Convert a parsed semver struct into the (major, minor, patch) tuple used for version comparisons. + + A `None` input (channel labels like "nightly"/"beta" and unset versions + don't parse as semver) returns a sentinel representing "latest", since + nightly and beta are always at or ahead of the most recent stable release. + + Args: + version_semver: A semver struct from `rust/private/semver.bzl`, typically + `toolchain.version_semver`, or `None`. + + Returns: + A (major, minor, patch) tuple of ints. + """ + if not version_semver: + return (999, 0, 0) + return (version_semver.major, version_semver.minor, version_semver.patch) + +def produces_pie_binaries(target_triple, rust_version): + """Returns True if rustc links binaries as Position Independent Executables. + + The truth table here is a port of the per-target `position_independent_executables` + field that rustc sets in its target specs. The constants and special cases + below were derived from analyzing + https://github.com/rust-lang/rust/tree/1.95.0/compiler/rustc_target/src/spec/base + across Rust 1.0.0-1.95.0. + + ## When to update + + Add or revise an entry here whenever any of the following happens upstream: + + - A new Rust release flips `position_independent_executables` on or off for + some target (most commonly via a base spec like `linux_musl_base.rs`). + - A new target triple is stabilized. + - rustc's PIE default itself changes (rare, but happens — e.g. the bpf + targets gained PIE in 1.75.0). + + A divergence here typically shows up as a link error: rustc-emitted PIC/PIE + objects mixed with non-PIE intermediate rlibs (or vice versa). + + ## How to perform the analysis + + rustc target specs live under `compiler/rustc_target/src/spec/`. Each + triple has a `targets/.rs` that returns a `Target` whose `options` + field is usually built from one of the shared bases under `spec/base/` + (e.g. `linux_gnu_base.rs`, `apple_base.rs`, `freebsd_base.rs`). + + The decision tree for a given triple is: + + 1. Open `compiler/rustc_target/src/spec/targets/.rs` and follow + which `base::*` it calls into. + 2. In that base file, look for `position_independent_executables: true` + (or the field being overridden back to `false` after the base sets it). + Some triples override the base's value directly in their own + `targets/.rs` — check there too. + 3. To find the Rust release where the value changed, `git log -p -S + 'position_independent_executables' compiler/rustc_target/src/spec/` + in a rust-lang/rust checkout and cross-reference the commit's first + containing release tag (e.g. `git tag --contains | sort -V | head -1`). + 4. Cross-check with the `arm-linux-androideabi`, `x86_64-unknown-haiku`, + and `nto` entries below — those are the existing examples of + version-gated PIE flips and are the right shape to copy. + + Three buckets cover the bulk of the matrix without per-triple cases; only + add a special case here when a triple is genuinely an outlier: + + - **`_NON_PIE_OS_KEYWORDS`** — operating systems whose base spec sets + PIE off. Matched against any `-` separated component of the triple + (i.e. exact-segment match). Add an entry if a *new OS* lands and its + base spec sets `position_independent_executables: false` (or omits it, + since the default is false). + - **`_NON_PIE_ARCH_PREFIXES`** — architectures whose base spec sets PIE + off. Matched as a prefix of the first triple component (the arch), + because variants like `wasm32`/`wasm64` and `nvptx64` share a base. + - **`_NON_PIE_ENV_KEYWORDS`** — embedded/environment markers whose base + spec sets PIE off. Substring match against the full triple, because + these often appear concatenated with the OS (e.g. `nuttx` in + `aarch64-nuttx-elf`). + + Args: + target_triple: A parsed target triple struct (see `rust/platform/triple.bzl`), + typically `toolchain.target_triple`. Provides `arch`, `vendor`, `system`, + `abi`, and `str` fields. + rust_version: (major, minor, patch) tuple, e.g. (1, 93, 0). + + Returns: + True if rustc will pass -pie to the linker for this target. + """ + if target_triple.vendor == "apple": + return True + + if target_triple.system == "none": + if target_triple.arch in ("bpfeb", "bpfel"): + return rust_version >= (1, 75, 0) + if target_triple.str == "x86_64-unknown-none": + return True + return False + + if target_triple.system in _NON_PIE_OS_KEYWORDS: + return False + + for prefix in _NON_PIE_ARCH_PREFIXES: + if target_triple.arch.startswith(prefix): + return False + + # Env keywords use substring match against the full triple string because + # they sometimes appear in 4+-segment triples (e.g. "rtems" in + # "armv7-unknown-trappist-rtems-eabihf") that don't land cleanly in any + # single struct field. + for keyword in _NON_PIE_ENV_KEYWORDS: + if keyword in target_triple.str: + return False + if "solid" in target_triple.str: + return False + + if target_triple.str == "i686-unknown-haiku": + return False + if target_triple.vendor == "unikraft": + return False + + if target_triple.str == "x86_64-unknown-haiku": + return rust_version >= (1, 29, 0) + if target_triple.str == "arm-linux-androideabi": + return rust_version >= (1, 1, 0) + + if target_triple.abi == "musl" and target_triple.system == "linux": + if target_triple.arch.startswith("mips"): + return True + return rust_version >= (1, 21, 0) + if target_triple.system == "freebsd": + return rust_version >= (1, 8, 0) + if target_triple.system == "hermit": + return rust_version >= (1, 40, 0) + if target_triple.system == "redox": + return rust_version >= (1, 38, 0) + if target_triple.system == "nto": + return rust_version >= (1, 75, 0) + + return True + +def _should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation_mode, toolchain): """Whether or not [PIC][pic] should be enabled [pic]: https://en.wikipedia.org/wiki/Position-independent_code @@ -167,6 +344,8 @@ def _should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation feature_configuration (FeatureConfiguration): Feature configuration to be queried. crate_type (str): A Rust target's crate type. compilation_mode: The compilation mode. + toolchain (rust_toolchain): The current `rust_toolchain`, used to derive + the target triple and rustc version for PIE detection. Returns: bool: Whether or not [PIC][pic] should be enabled. @@ -180,6 +359,15 @@ def _should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation return cc_toolchain.needs_pic_for_dynamic_libraries(feature_configuration = feature_configuration) elif compilation_mode in ("fastbuild", "dbg"): return True + + # In opt mode, rustc links executables with -pie on most platforms. + # Any CC objects linked into a PIE binary must be PIC, including those + # embedded in intermediate rlibs, so this applies to all crate types. + # target_triple is None when a custom target JSON spec is used. + if toolchain.target_triple: + rust_version = parse_rustc_version(toolchain.version_semver) + if produces_pie_binaries(toolchain.target_triple, rust_version): + return True return False def _is_proc_macro(crate_info): @@ -721,7 +909,7 @@ def collect_inputs( linker_depset = cc_toolchain.linker_files() compilation_mode = ctx.var["COMPILATION_MODE"] - use_pic = _should_use_pic(cc_toolchain, feature_configuration, crate_info.type, compilation_mode) + use_pic = _should_use_pic(cc_toolchain, feature_configuration, crate_info.type, compilation_mode, toolchain) # Pass linker inputs only for linking-like actions, not for example where # the output is rlib. This avoids quadratic behavior where transitive noncrates are @@ -1253,7 +1441,7 @@ def construct_arguments( compilation_mode = ctx.var["COMPILATION_MODE"] if toolchain.target_arch not in ("wasm32", "wasm64"): if output_dir: - use_pic = _should_use_pic(cc_toolchain, feature_configuration, crate_info.type, compilation_mode) + use_pic = _should_use_pic(cc_toolchain, feature_configuration, crate_info.type, compilation_mode, toolchain) rpaths = _compute_rpaths(toolchain, output_dir, dep_info, use_pic) else: rpaths = depset() @@ -2637,7 +2825,7 @@ def _add_native_link_flags( if crate_type in ["lib", "rlib"]: return - use_pic = _should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation_mode) + use_pic = _should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation_mode, toolchain) make_link_flags, get_lib_name = _get_make_link_flag_funcs( target_os = toolchain.target_os, diff --git a/rust/private/semver.bzl b/rust/private/semver.bzl index 91f3c64203..d252553b3f 100644 --- a/rust/private/semver.bzl +++ b/rust/private/semver.bzl @@ -3,31 +3,49 @@ def semver(version): """Constructs a struct containing separated sections of a semantic version value. + Parses per [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html): + `MAJOR.MINOR.PATCH[-PRE-RELEASE][+BUILD-METADATA]`. + Args: version (str): The semver value. Returns: struct: - - major (int): The semver's major component. E.g. `1` from `1.2.3` - - minor (int): The semver's minor component. E.g. `2` from `1.2.3` - - patch (int): The semver's patch component. E.g. `3` from `1.2.3` - - pre (optional str): The semver's pre component. E.g. `rc4` from `1.2.3-rc4` or None if absent. + - major (int): The semver's major component. E.g. `1` from `1.2.3`. + - minor (int): The semver's minor component. E.g. `2` from `1.2.3`. + - patch (int): The semver's patch component. E.g. `3` from `1.2.3`. + - pre (optional str): The semver's pre-release identifier. E.g. `rc4` + from `1.2.3-rc4`, `beta.1` from `1.0.0-beta.1+exp.sha`. `None` when + no `-` is present. + - build (optional str): The semver's build metadata identifier. E.g. + `exp.sha.5114f85` from `1.0.0-beta+exp.sha.5114f85`. `None` when no + `+` is present. - str (str): The full string value of the semver. """ - parts = version.split(".", 2) - if len(parts) < 3: - fail("Unexpected number of parts for semver value: {}".format(version)) - major = parts[0] - minor = parts[1] - patch, split, pre = parts[2].partition("-") - if not split: + # Build metadata is everything after the first `+`. Per the spec, `+` cannot + # appear inside MAJOR.MINOR.PATCH or the pre-release identifier, so this is + # always a clean split. + core, plus, build = version.partition("+") + if not plus: + build = None + + # Pre-release is everything after the first `-` in the core. Multiple dashes + # are allowed in the pre-release identifier itself (e.g. `1.2.3-alpha-test`), + # so we only split on the first one. + main, dash, pre = core.partition("-") + if not dash: pre = None + parts = main.split(".") + if len(parts) != 3: + fail("Unexpected number of parts for semver value: {}".format(version)) + return struct( - major = int(major), - minor = int(minor), - patch = int(patch), + major = int(parts[0]), + minor = int(parts[1]), + patch = int(parts[2]), pre = pre, + build = build, str = version, ) diff --git a/rust/toolchain.bzl b/rust/toolchain.bzl index c62263daf7..081ddb57d0 100644 --- a/rust/toolchain.bzl +++ b/rust/toolchain.bzl @@ -23,6 +23,7 @@ load( _current_rustfmt_toolchain = "current_rustfmt_toolchain", _rustfmt_toolchain = "rustfmt_toolchain", ) +load("//rust/private:semver.bzl", "semver") load( "//rust/private:utils.bzl", "deduplicate", @@ -370,6 +371,18 @@ def _experimental_use_cc_common_link(ctx): def _require_explicit_unstable_features(ctx): return ctx.attr.require_explicit_unstable_features[BuildSettingInfo].value +_DIGITS = "0123456789" + +def _is_semver_string(version): + """Whether `version` looks like a `MAJOR.MINOR.PATCH[-pre][+build]` semver string. + + The `rust_toolchain.version` attribute also accepts channel labels like + `"nightly"` or `"beta"` (and is sometimes the empty string), neither of + which are valid input for `semver()`. This filter exists so we can populate + `version_semver` opportunistically. + """ + return version != "" and version[0] in _DIGITS + def _expand_flags(ctx, attr_name, targets, make_variables): targets = deduplicate(targets) expanded_flags = [] @@ -584,6 +597,11 @@ def _rust_toolchain_impl(ctx): if cc_toolchain and cc_toolchain.all_files: all_files_depsets.append(cc_toolchain.all_files) + # Parse the version string once so downstream rules can branch on the + # semver components without re-parsing. `None` for empty or non-semver + # values (e.g. unset, or channel labels like "nightly" without a version). + version_semver = semver(ctx.attr.version) if _is_semver_string(ctx.attr.version) else None + toolchain = platform_common.ToolchainInfo( all_files = depset(transitive = all_files_depsets), binary_ext = ctx.attr.binary_ext, @@ -633,6 +651,7 @@ def _rust_toolchain_impl(ctx): target_abi = target_abi, target_triple = target_triple, version = ctx.attr.version, + version_semver = version_semver, require_explicit_unstable_features = _require_explicit_unstable_features(ctx), # Experimental and incompatible flags diff --git a/test/unit/rustc_pie/BUILD.bazel b/test/unit/rustc_pie/BUILD.bazel new file mode 100644 index 0000000000..1187a2f598 --- /dev/null +++ b/test/unit/rustc_pie/BUILD.bazel @@ -0,0 +1,3 @@ +load(":rustc_pie_test.bzl", "rustc_pie_test_suite") + +rustc_pie_test_suite(name = "rustc_pie_test_suite") diff --git a/test/unit/rustc_pie/rustc_pie_test.bzl b/test/unit/rustc_pie/rustc_pie_test.bzl new file mode 100644 index 0000000000..2bde47b830 --- /dev/null +++ b/test/unit/rustc_pie/rustc_pie_test.bzl @@ -0,0 +1,171 @@ +"""Unit tests for the PIE detection helpers in rustc.bzl.""" + +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load("//rust/platform:triple.bzl", "triple") + +# buildifier: disable=bzl-visibility +load( + "//rust/private:rustc.bzl", + "parse_rustc_version", + "produces_pie_binaries", +) + +# buildifier: disable=bzl-visibility +load("//rust/private:semver.bzl", "semver") + +_LATEST = (999, 0, 0) + +def _parse_rustc_version_test_impl(ctx): + env = unittest.begin(ctx) + + # A parsed semver struct collapses to its (major, minor, patch) tuple. + asserts.equals(env, (1, 94, 1), parse_rustc_version(semver("1.94.1"))) + asserts.equals(env, (1, 0, 0), parse_rustc_version(semver("1.0.0"))) + asserts.equals(env, (10, 20, 30), parse_rustc_version(semver("10.20.30"))) + + # Pre-release and build metadata don't affect the tuple — comparisons + # operate on the (major, minor, patch) prefix only. + asserts.equals(env, (1, 87, 0), parse_rustc_version(semver("1.87.0-nightly"))) + asserts.equals(env, (1, 87, 0), parse_rustc_version(semver("1.87.0-beta.1"))) + asserts.equals(env, (1, 0, 0), parse_rustc_version(semver("1.0.0+exp.sha"))) + + # `None` (what `rust_toolchain.version_semver` is when the toolchain's + # version is empty or a channel label like "nightly"/"beta") falls back to + # the "latest" sentinel. + asserts.equals(env, _LATEST, parse_rustc_version(None)) + + return unittest.end(env) + +def _produces_pie_binaries_always_test_impl(ctx): + env = unittest.begin(ctx) + v = _LATEST + + # Apple platforms are always PIE. + asserts.equals(env, True, produces_pie_binaries(triple("x86_64-apple-darwin"), v)) + asserts.equals(env, True, produces_pie_binaries(triple("aarch64-apple-darwin"), v)) + asserts.equals(env, True, produces_pie_binaries(triple("aarch64-apple-ios"), v)) + asserts.equals(env, True, produces_pie_binaries(triple("x86_64-apple-tvos"), v)) + + # Default linux-gnu falls through to True. + asserts.equals(env, True, produces_pie_binaries(triple("x86_64-unknown-linux-gnu"), v)) + asserts.equals(env, True, produces_pie_binaries(triple("aarch64-unknown-linux-gnu"), v)) + + # OS keywords that disable PIE. + asserts.equals(env, False, produces_pie_binaries(triple("x86_64-pc-windows-msvc"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("x86_64-unknown-uefi"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("powerpc-wrs-vxworks"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("x86_64-pc-solaris"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("x86_64-unknown-illumos"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("x86_64-pc-cygwin"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("powerpc64-ibm-aix"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("nvptx64-nvidia-cuda"), v)) + + # Arch prefixes that disable PIE. + asserts.equals(env, False, produces_pie_binaries(triple("wasm32-unknown-unknown"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("xtensa-esp32-none-elf"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("msp430-none-elf"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("avr-unknown-gnu-atmega328"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("amdgcn-amd-amdhsa"), v)) + + # Env keywords that disable PIE. + asserts.equals(env, False, produces_pie_binaries(triple("riscv32imc-unknown-nuttx-elf"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("riscv32imc-esp-espidf"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("armv7-unknown-trappist-rtems-eabihf"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("riscv32im-risc0-zkvm-elf"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("mipsel-sony-psp"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("armv7-sony-vita-newlibeabihf"), v)) + asserts.equals(env, False, produces_pie_binaries(triple("armv6k-nintendo-3ds"), v)) + + # `solid` substring match. + asserts.equals(env, False, produces_pie_binaries(triple("aarch64-kmc-solid_asp3"), v)) + + return unittest.end(env) + +def _produces_pie_binaries_none_targets_test_impl(ctx): + env = unittest.begin(ctx) + + # bpf is PIE starting at 1.75.0. + asserts.equals(env, False, produces_pie_binaries(triple("bpfeb-unknown-none"), (1, 74, 0))) + asserts.equals(env, True, produces_pie_binaries(triple("bpfeb-unknown-none"), (1, 75, 0))) + asserts.equals(env, True, produces_pie_binaries(triple("bpfel-unknown-none"), (1, 75, 0))) + + # x86_64-unknown-none is always PIE. + asserts.equals(env, True, produces_pie_binaries(triple("x86_64-unknown-none"), _LATEST)) + + # Other bare-metal `none` targets are not PIE. + asserts.equals(env, False, produces_pie_binaries(triple("aarch64-unknown-none"), _LATEST)) + asserts.equals(env, False, produces_pie_binaries(triple("thumbv7m-none-eabi"), _LATEST)) + asserts.equals(env, False, produces_pie_binaries(triple("riscv32i-unknown-none-elf"), _LATEST)) + + return unittest.end(env) + +def _produces_pie_binaries_version_thresholds_test_impl(ctx): + env = unittest.begin(ctx) + + # x86_64-unknown-haiku: PIE starting at 1.29.0. + asserts.equals(env, False, produces_pie_binaries(triple("x86_64-unknown-haiku"), (1, 28, 0))) + asserts.equals(env, True, produces_pie_binaries(triple("x86_64-unknown-haiku"), (1, 29, 0))) + + # i686-unknown-haiku is always non-PIE. + asserts.equals(env, False, produces_pie_binaries(triple("i686-unknown-haiku"), _LATEST)) + + # arm-linux-androideabi: PIE starting at 1.1.0. + asserts.equals(env, False, produces_pie_binaries(triple("arm-linux-androideabi"), (1, 0, 0))) + asserts.equals(env, True, produces_pie_binaries(triple("arm-linux-androideabi"), (1, 1, 0))) + + # freebsd: PIE starting at 1.8.0. + asserts.equals(env, False, produces_pie_binaries(triple("x86_64-unknown-freebsd"), (1, 7, 0))) + asserts.equals(env, True, produces_pie_binaries(triple("x86_64-unknown-freebsd"), (1, 8, 0))) + + # hermit: PIE starting at 1.40.0. + asserts.equals(env, False, produces_pie_binaries(triple("x86_64-unknown-hermit"), (1, 39, 0))) + asserts.equals(env, True, produces_pie_binaries(triple("x86_64-unknown-hermit"), (1, 40, 0))) + + # redox: PIE starting at 1.38.0. + asserts.equals(env, False, produces_pie_binaries(triple("x86_64-unknown-redox"), (1, 37, 0))) + asserts.equals(env, True, produces_pie_binaries(triple("x86_64-unknown-redox"), (1, 38, 0))) + + # nto (QNX): PIE starting at 1.75.0. + asserts.equals(env, False, produces_pie_binaries(triple("aarch64-unknown-nto-qnx710"), (1, 74, 0))) + asserts.equals(env, True, produces_pie_binaries(triple("aarch64-unknown-nto-qnx710"), (1, 75, 0))) + + return unittest.end(env) + +def _produces_pie_binaries_musl_test_impl(ctx): + env = unittest.begin(ctx) + + # Standard musl: PIE starting at 1.21.0. + asserts.equals(env, False, produces_pie_binaries(triple("x86_64-unknown-linux-musl"), (1, 20, 0))) + asserts.equals(env, True, produces_pie_binaries(triple("x86_64-unknown-linux-musl"), (1, 21, 0))) + asserts.equals(env, False, produces_pie_binaries(triple("aarch64-unknown-linux-musl"), (1, 20, 0))) + asserts.equals(env, True, produces_pie_binaries(triple("aarch64-unknown-linux-musl"), (1, 21, 0))) + + # mips-musl is PIE at any version. + asserts.equals(env, True, produces_pie_binaries(triple("mips-unknown-linux-musl"), (1, 0, 0))) + asserts.equals(env, True, produces_pie_binaries(triple("mipsel-unknown-linux-musl"), (1, 0, 0))) + + # unikraft is always non-PIE. + asserts.equals(env, False, produces_pie_binaries(triple("x86_64-unikraft-linux-musl"), _LATEST)) + + return unittest.end(env) + +parse_rustc_version_test = unittest.make(_parse_rustc_version_test_impl) +produces_pie_binaries_always_test = unittest.make(_produces_pie_binaries_always_test_impl) +produces_pie_binaries_none_targets_test = unittest.make(_produces_pie_binaries_none_targets_test_impl) +produces_pie_binaries_version_thresholds_test = unittest.make(_produces_pie_binaries_version_thresholds_test_impl) +produces_pie_binaries_musl_test = unittest.make(_produces_pie_binaries_musl_test_impl) + +def rustc_pie_test_suite(name): + """Entry-point macro called from the BUILD file. + + Args: + name (str): Name of the test suite. + """ + unittest.suite( + name, + parse_rustc_version_test, + produces_pie_binaries_always_test, + produces_pie_binaries_none_targets_test, + produces_pie_binaries_version_thresholds_test, + produces_pie_binaries_musl_test, + ) diff --git a/test/unit/rustdoc/rustdoc_unit_test.bzl b/test/unit/rustdoc/rustdoc_unit_test.bzl index aa1f9b2f02..f1026082d2 100644 --- a/test/unit/rustdoc/rustdoc_unit_test.bzl +++ b/test/unit/rustdoc/rustdoc_unit_test.bzl @@ -294,9 +294,14 @@ def _define_targets(): hdrs = ["rustdoc.h"], srcs = ["rustdoc.cc"], deps = [":cc_lib"], - # This is not needed for :cc_lib, but it is needed in other - # circumstances to link in system libraries. - linkopts = ["-lcc_lib"], + # Exercises propagation of system-library `-l` linkopts. Picks a + # library guaranteed to exist on each platform so the `-l` flag + # resolves without depending on any cc_library artifact name (which + # would gain a `.pic` suffix when consumers request PIC). + linkopts = select({ + "@platforms//os:windows": ["-luser32"], + "//conditions:default": ["-lm"], + }), linkstatic = True, ) diff --git a/test/unit/semver/semver_test.bzl b/test/unit/semver/semver_test.bzl index 1f45d44220..b7caee61ea 100644 --- a/test/unit/semver/semver_test.bzl +++ b/test/unit/semver/semver_test.bzl @@ -14,6 +14,7 @@ def _semver_basic_test_impl(ctx): asserts.equals(env, 2, result.minor) asserts.equals(env, 3, result.patch) asserts.equals(env, None, result.pre) + asserts.equals(env, None, result.build) asserts.equals(env, "1.2.3", result.str) # Test with zeros @@ -22,6 +23,7 @@ def _semver_basic_test_impl(ctx): asserts.equals(env, 0, result.minor) asserts.equals(env, 0, result.patch) asserts.equals(env, None, result.pre) + asserts.equals(env, None, result.build) asserts.equals(env, "0.0.0", result.str) # Test larger version numbers @@ -30,6 +32,7 @@ def _semver_basic_test_impl(ctx): asserts.equals(env, 20, result.minor) asserts.equals(env, 30, result.patch) asserts.equals(env, None, result.pre) + asserts.equals(env, None, result.build) asserts.equals(env, "10.20.30", result.str) return unittest.end(env) @@ -43,6 +46,7 @@ def _semver_with_pre_test_impl(ctx): asserts.equals(env, 2, result.minor) asserts.equals(env, 3, result.patch) asserts.equals(env, "rc4", result.pre) + asserts.equals(env, None, result.build) asserts.equals(env, "1.2.3-rc4", result.str) # Test semver with alpha pre-release @@ -51,14 +55,16 @@ def _semver_with_pre_test_impl(ctx): asserts.equals(env, 0, result.minor) asserts.equals(env, 0, result.patch) asserts.equals(env, "alpha", result.pre) + asserts.equals(env, None, result.build) asserts.equals(env, "2.0.0-alpha", result.str) - # Test semver with beta pre-release + # Test semver with beta pre-release with dot-separated identifier result = semver("1.5.0-beta.1") asserts.equals(env, 1, result.major) asserts.equals(env, 5, result.minor) asserts.equals(env, 0, result.patch) asserts.equals(env, "beta.1", result.pre) + asserts.equals(env, None, result.build) asserts.equals(env, "1.5.0-beta.1", result.str) # Test semver with nightly pre-release @@ -67,29 +73,83 @@ def _semver_with_pre_test_impl(ctx): asserts.equals(env, 70, result.minor) asserts.equals(env, 0, result.patch) asserts.equals(env, "nightly", result.pre) + asserts.equals(env, None, result.build) asserts.equals(env, "1.70.0-nightly", result.str) return unittest.end(env) +def _semver_with_build_test_impl(ctx): + env = unittest.begin(ctx) + + # Plain build metadata, no pre-release. + result = semver("1.0.0+20130313144700") + asserts.equals(env, 1, result.major) + asserts.equals(env, 0, result.minor) + asserts.equals(env, 0, result.patch) + asserts.equals(env, None, result.pre) + asserts.equals(env, "20130313144700", result.build) + asserts.equals(env, "1.0.0+20130313144700", result.str) + + # Build metadata with dot-separated identifiers (a real semver 2.0 example). + result = semver("1.0.0+exp.sha.5114f85") + asserts.equals(env, 1, result.major) + asserts.equals(env, 0, result.minor) + asserts.equals(env, 0, result.patch) + asserts.equals(env, None, result.pre) + asserts.equals(env, "exp.sha.5114f85", result.build) + + # Pre-release AND build metadata. + result = semver("1.0.0-beta+exp.sha.5114f85") + asserts.equals(env, 1, result.major) + asserts.equals(env, 0, result.minor) + asserts.equals(env, 0, result.patch) + asserts.equals(env, "beta", result.pre) + asserts.equals(env, "exp.sha.5114f85", result.build) + asserts.equals(env, "1.0.0-beta+exp.sha.5114f85", result.str) + + # Dotted pre-release AND build metadata. + result = semver("1.2.3-rc.4+build.42") + asserts.equals(env, 1, result.major) + asserts.equals(env, 2, result.minor) + asserts.equals(env, 3, result.patch) + asserts.equals(env, "rc.4", result.pre) + asserts.equals(env, "build.42", result.build) + + # Build metadata may itself contain `-` — only the first `+` is the + # separator; once we're in build metadata, `-` is just a content char. + result = semver("1.0.0+sha-abcdef") + asserts.equals(env, None, result.pre) + asserts.equals(env, "sha-abcdef", result.build) + + return unittest.end(env) + def _semver_edge_cases_test_impl(ctx): env = unittest.begin(ctx) - # Test semver with empty pre-release (trailing dash) - # When there's a trailing dash, partition returns empty string for pre, - # but "pre or None" converts it to None + # Trailing dash: partition keeps an empty pre-release rather than None, + # since the `-` separator was present. result = semver("1.2.3-") asserts.equals(env, 1, result.major) asserts.equals(env, 2, result.minor) asserts.equals(env, 3, result.patch) asserts.equals(env, "", result.pre) + asserts.equals(env, None, result.build) asserts.equals(env, "1.2.3-", result.str) - # Test semver with multiple dashes in pre-release + # Trailing plus: similarly, empty build rather than None. + result = semver("1.2.3+") + asserts.equals(env, None, result.pre) + asserts.equals(env, "", result.build) + asserts.equals(env, "1.2.3+", result.str) + + # Multiple dashes inside the pre-release: only the first `-` is the + # separator, so the rest are part of the pre-release identifier. result = semver("1.2.3-alpha-test") asserts.equals(env, 1, result.major) asserts.equals(env, 2, result.minor) asserts.equals(env, 3, result.patch) asserts.equals(env, "alpha-test", result.pre) + asserts.equals(env, None, result.build) asserts.equals(env, "1.2.3-alpha-test", result.str) return unittest.end(env) @@ -97,30 +157,37 @@ def _semver_edge_cases_test_impl(ctx): def _semver_real_world_examples_test_impl(ctx): env = unittest.begin(ctx) - # Test real Rust version examples - result = semver("1.80.0") + # A representative sample of versions we ship `rust_toolchain` against. + for version_str, expected_minor in [ + ("1.54.0", 54), + ("1.70.0", 70), + ("1.80.0", 80), + ("1.87.0", 87), + ("1.94.1", 94), + ]: + result = semver(version_str) + asserts.equals(env, 1, result.major) + asserts.equals(env, expected_minor, result.minor) + asserts.equals(env, None, result.pre) + asserts.equals(env, None, result.build) + asserts.equals(env, version_str, result.str) + + # Rust pre-release / nightly shapes that flow through `rust_toolchain` + # when the version embeds a channel marker rather than a `channel/date` + # tuple (which is handled separately, before `semver()` is called). + result = semver("1.87.0-nightly") asserts.equals(env, 1, result.major) - asserts.equals(env, 80, result.minor) - asserts.equals(env, 0, result.patch) - asserts.equals(env, None, result.pre) - asserts.equals(env, "1.80.0", result.str) - - result = semver("1.70.0") - asserts.equals(env, 1, result.major) - asserts.equals(env, 70, result.minor) - asserts.equals(env, 0, result.patch) - asserts.equals(env, None, result.pre) + asserts.equals(env, 87, result.minor) + asserts.equals(env, "nightly", result.pre) - result = semver("1.54.0") - asserts.equals(env, 1, result.major) - asserts.equals(env, 54, result.minor) - asserts.equals(env, 0, result.patch) - asserts.equals(env, None, result.pre) + result = semver("1.87.0-beta.1") + asserts.equals(env, "beta.1", result.pre) return unittest.end(env) semver_basic_test = unittest.make(_semver_basic_test_impl) semver_with_pre_test = unittest.make(_semver_with_pre_test_impl) +semver_with_build_test = unittest.make(_semver_with_build_test_impl) semver_edge_cases_test = unittest.make(_semver_edge_cases_test_impl) semver_real_world_examples_test = unittest.make(_semver_real_world_examples_test_impl) @@ -134,6 +201,7 @@ def semver_test_suite(name): name, semver_basic_test, semver_with_pre_test, + semver_with_build_test, semver_edge_cases_test, semver_real_world_examples_test, )