bazel-contrib / bazel-contrib/bazel-lib

write_source_file breaks when launched via multirun (PWD != runfiles dir)

Open
#1,250 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Starlark
Stars
182
Forks
134
Avg merge
1d 46m
Merged PRs (30d)
1

Description

write_source_file generated scripts use `current_working_dir=$PWD` to locate input files in the runfiles tree:

```
current_working_dir=$PWD
if [[ ! -z "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then
cd "$BUILD_WORKSPACE_DIRECTORY"
fi
...
in=$current_working_dir/{in_path}
```

This assumes `$PWD` is the runfiles directory when the script starts. That assumption breaks when the target is composed into a https://github.com/keith/rules_multirun multirun target. In that case:

1. bazel run launches the multirun binary, which inherits cwd from bazel
2. multirun spawns subcommands via `subprocess.check_call()`, inheriting cwd
3. The command() wrapper uses exec `$(rlocation ...)` to launch the actual script
4. The exec'd write_source_file script sees `$PWD = BUILD_WORKSPACE_DIRECTORY` (not the runfiles dir)
5. `in=$current_working_dir/{in_path}` resolves to a nonexistent path
6. The `-f "$in"` check fails, falling into the directory branch, which then fails with mkdir: cannot create directory '...': File exists

Reproduction:

```
load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_file")
load("@rules_multirun//:defs.bzl", "command", "multirun")

write_source_file(
name = "my_update",
in_file = "@some_repo//:generated.json",
out_file = "generated.json",
)

command(name = "cmd", command = ":my_update")
multirun(name = "update_all", commands = [":cmd"], jobs = 0)
```

bazel run :my_update # <-works
bazel run :update_all # <- fails

Tested with Bazel 8.2.1 and aspect_bazel_lib 2.22.5.

Suggested fix: Use RUNFILES_DIR or $0-based resolution instead of $PWD:

current_working_dir="${RUNFILES_DIR:-$PWD}"

Or use the bash runfiles library to resolve paths via rlocation.

------------------------------------------------------------------------

I'm also feeling sibling issue for multirun:
- bazel run :target sets cwd to the runfiles directory. That's part of the bazel run contract.
- multirun's purpose is to run multiple bazel run targets in one invocation. It should faithfully reproduce the environment each target would see if run individually.
- multirun's subprocess.check_call(args) doesn't set cwd — it inherits from the parent process. It could set cwd per-command to the command's runfiles subtree.

But for that reason I think that write_source_file should be also improved:
The case for write_source_file bug:
- $PWD is not the documented mechanism for finding runfiles. RUNFILES_DIR and the bash runfiles library (rlocation) exist for exactly this purpose.
- The command() wrapper correctly calls runfiles_export_envvars, so RUNFILES_DIR is correctly set when write_source_file runs. It just ignores it and uses $PWD instead.

That said, using RUNFILES_DIR would make write_source_file robust regardless — it's a good defensive fix.

Contributor guide

Open the contributing guide

Research direction

Start at lib:write_source_files.bzl and the write_source_file entry point; inspect how generated scripts set current_working_dir and how command() exports runfiles variables. Reproduce with bazel run :my_update and :update_all, then verify the generated script resolves the input file under multirun without the missing-file and mkdir failures.

Written by the indexing model from the issue text.

Assessment

Tech stack
bash
Domain
build-system
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.