bazel-contrib / bazel-contrib/bazel-lib
write_source_file breaks when launched via multirun (PWD != runfiles dir)
- 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
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