fix(cargo_build_script): expand ${pwd} for execpaths and locations in build_script_env (#4088)

Singular `$(execpath ...)` and `$(location ...)` were already prefixed
with `${pwd}/` so build_script_runner could resolve them to absolute
paths at run time. The plural forms `$(execpaths ...)` and `$(locations
...)` were silently omitted from this handling, leaving their paths as
execroot-relative.

For plural forms each macro is expanded individually (via
ctx.expand_location) and every resulting space-separated path is
prefixed with `${pwd}/`.
diff --git a/rust/private/utils.bzl b/rust/private/utils.bzl
index eac3948..e54e513 100644
--- a/rust/private/utils.bzl
+++ b/rust/private/utils.bzl
@@ -286,6 +286,26 @@
         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_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 0000000..67475ee
--- /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 354735e..3c63355 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 @@
     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/<config>/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"],
+        compile_data = [
+            ":flag_generator_execpath",
+            ":flag_generator_execpaths",
+            ":flag_generator_location",
+            ":flag_generator_locations",
+        ],
+    )
+
+    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 @@
         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 @@
         name = name,
         tests = [
             ":location_expansion_rustc_flags_test",
+            ":location_expansion_build_script_env_test",
         ],
     )