diff --git a/rust/private/utils.bzl b/rust/private/utils.bzl index eac3948a67..e54e513fd5 100644 --- a/rust/private/utils.bzl +++ b/rust/private/utils.bzl @@ -286,6 +286,26 @@ def _expand_location_for_build_script_runner(ctx, v, data, known_variables): if directive in v: # build script runner will expand pwd to execroot for us v = v.replace(directive, "$${pwd}/" + directive) + + for directive in ("$(execpaths ", "$(locations "): + if directive in v: + # Plural forms expand to multiple space-separated paths, so we must + # expand each macro individually and prefix every resulting path. + # Split on the opening directive; each subsequent part begins with + # "label)rest", letting us reconstruct and expand one macro at a time. + parts = v.split(directive) + result = parts[0] + for part in parts[1:]: + end = part.find(")") + if end == -1: + result += directive + part + continue + macro = directive + part[:end] + ")" + expanded = ctx.expand_location(macro, data) + prefixed = " ".join(["$${pwd}/" + p for p in expanded.split(" ")]) + result += prefixed + part[end + 1:] + v = result + return ctx.expand_make_variables( v, ctx.expand_location(v, data), @@ -295,11 +315,14 @@ def _expand_location_for_build_script_runner(ctx, v, data, known_variables): def expand_dict_value_locations(ctx, env, data, known_variables): """Performs location-macro expansion on string values. - $(execroot ...) and $(location ...) are prefixed with ${pwd}, - which process_wrapper and build_script_runner will expand at run time - to the absolute path. This is necessary because include_str!() is relative - to the currently compiled file, and build scripts run relative to the - manifest dir, so we can not use execroot-relative paths. + $(execpath ...), $(execpaths ...), $(location ...) and $(locations ...) are + prefixed with ${pwd}, which process_wrapper and build_script_runner will + expand at run time to the absolute path. + This is necessary because include_str!() is relative to the currently + compiled file, and build scripts run relative to the manifest dir, so we + can not use execroot-relative paths. + Plural forms (execpaths/locations) expand to multiple space-separated paths; + each path receives its own ${pwd}/ prefix. $(rootpath ...) is unmodified, and is useful for passing in paths via rustc_env that are encoded in the binary with env!(), but utilized at diff --git a/test/unit/location_expansion/build_script.rs b/test/unit/location_expansion/build_script.rs new file mode 100644 index 0000000000..67475ee8e7 --- /dev/null +++ b/test/unit/location_expansion/build_script.rs @@ -0,0 +1,8 @@ +fn main() { + let path = std::env::var("MY_DATA").expect("MY_DATA env var must be set"); + assert!( + std::path::Path::new(&path).exists(), + "MY_DATA path does not exist: {}", + path + ); +} diff --git a/test/unit/location_expansion/location_expansion_test.bzl b/test/unit/location_expansion/location_expansion_test.bzl index 354735e5a8..3c63355077 100644 --- a/test/unit/location_expansion/location_expansion_test.bzl +++ b/test/unit/location_expansion/location_expansion_test.bzl @@ -2,8 +2,15 @@ load("@bazel_skylib//lib:unittest.bzl", "analysistest") load("@bazel_skylib//rules:write_file.bzl", "write_file") +load("//cargo:defs.bzl", "cargo_build_script") load("//rust:defs.bzl", "rust_library") -load("//test/unit:common.bzl", "assert_action_mnemonic", "assert_argv_contains") +load("//test/unit:common.bzl", "assert_action_mnemonic", "assert_argv_contains", "assert_env_value") + +def _find_action(tut, mnemonic): + for action in tut.actions: + if action.mnemonic == mnemonic: + return action + return None def _location_expansion_rustc_flags_test(ctx): env = analysistest.begin(ctx) @@ -11,43 +18,71 @@ def _location_expansion_rustc_flags_test(ctx): action = tut.actions[1] assert_action_mnemonic(env, action, "Rustc") - # Because target `rustc_flags` use `$(execpath ...)`, the action does + # Because target `rustc_flags` use location macros, the action does # not advertise `supports-path-mapping`, so file paths remain at their # configuration-specific `ctx.bin_dir` locations. assert_argv_contains(env, action, ctx.bin_dir.path + "/test/unit/location_expansion/mylibrary.rs") - # `$(location ...)` is expanded at analysis time into a literal - # configuration-dependent string (`bazel-out//bin/...`). - # Bazel does not rewrite raw argv strings under path mapping, so this - # arg keeps the un-mapped configuration prefix even when the rest of - # the Rustc command uses `bazel-out/cfg/bin/...`. The action will - # fail at execution time under path mapping because the file is - # materialized at the mapped path; we accept that as documented in - # the Rust action implementation. - assert_argv_contains(env, action, "@${pwd}/" + ctx.bin_dir.path + "/test/unit/location_expansion/generated_flag.data") + # All four forms must be prefixed with @${pwd}/ so that process_wrapper + # can resolve them to absolute paths at run time. Each form references a + # distinct generated file so every assertion targets a unique path. + base = "@${pwd}/" + ctx.bin_dir.path + "/test/unit/location_expansion/" + assert_argv_contains(env, action, base + "flag_execpath.data") + assert_argv_contains(env, action, base + "flag_execpaths.data") + assert_argv_contains(env, action, base + "flag_location.data") + assert_argv_contains(env, action, base + "flag_locations.data") return analysistest.end(env) location_expansion_rustc_flags_test = analysistest.make(_location_expansion_rustc_flags_test) +def _location_expansion_build_script_env_test(ctx): + env = analysistest.begin(ctx) + tut = analysistest.target_under_test(env) + action = _find_action(tut, "CargoBuildScriptRun") + expected = "${pwd}/" + ctx.bin_dir.path + "/test/unit/location_expansion/flag_execpaths.data" + assert_env_value(env, action, "MY_DATA", expected) + return analysistest.end(env) + +location_expansion_build_script_env_test = analysistest.make(_location_expansion_build_script_env_test) + def _location_expansion_test(): - write_file( - name = "flag_generator", - out = "generated_flag.data", - content = [ - "--cfg=test_flag", - "", - ], - newline = "unix", - ) + for suffix in ("execpath", "execpaths", "location", "locations"): + write_file( + name = "flag_generator_" + suffix, + out = "flag_" + suffix + ".data", + content = [ + "--cfg=test_flag", + "", + ], + newline = "unix", + ) rust_library( name = "mylibrary", srcs = ["mylibrary.rs"], edition = "2018", rustc_flags = [ - "@$(execpath :flag_generator)", + "@$(execpath :flag_generator_execpath)", + "@$(execpaths :flag_generator_execpaths)", + "@$(location :flag_generator_location)", + "@$(locations :flag_generator_locations)", + ], + compile_data = [ + ":flag_generator_execpath", + ":flag_generator_execpaths", + ":flag_generator_location", + ":flag_generator_locations", ], - compile_data = [":flag_generator"], + ) + + cargo_build_script( + name = "mybuildscript", + srcs = ["build_script.rs"], + edition = "2018", + data = [":flag_generator_execpaths"], + build_script_env = { + "MY_DATA": "$(execpaths :flag_generator_execpaths)", + }, ) location_expansion_rustc_flags_test( @@ -55,6 +90,11 @@ def _location_expansion_test(): target_under_test = ":mylibrary", ) + location_expansion_build_script_env_test( + name = "location_expansion_build_script_env_test", + target_under_test = ":mybuildscript", + ) + def location_expansion_test_suite(name): """Entry-point macro called from the BUILD file. @@ -67,5 +107,6 @@ def location_expansion_test_suite(name): name = name, tests = [ ":location_expansion_rustc_flags_test", + ":location_expansion_build_script_env_test", ], )