To access a
genrule's output directory in Bazel, you must reference the generated file directly as an input to your build step rather than trying to parse filesystem paths. Since Bazel manages outputs in isolated sandboxed directories that can change between builds, relying on static paths is unreliable. The correct approach involves using
$(location ...) expansion within the
cmd attribute of a rule or passing the file as a dependency to ensure the build system resolves the correct temporary path at runtime.
The Problem with Static Paths
When you define a
genrule, Bazel places its outputs in a unique subdirectory under the execution root (often referred to as the
bazel-bin or
bazel-out structure). If you attempt to hardcode this path, such as
bazel-bin/my_package/output.txt, it will fail when building in different environments like CI/CD pipelines, Docker containers, or when using remote caching. Furthermore, if the
genrule is part of an external repository, the path becomes even more complex and unpredictable.
Bazel’s design philosophy emphasizes hermeticity. This means your build actions should not depend on the absolute location of files on the host machine. Instead, they should depend on the inputs provided by Bazel. Therefore, any script or command that needs to read the output of a
genrule must be given the file itself, not the directory containing it.
Accessing Outputs Within the Same Build
The most common scenario is needing to use the output of one
genrule as an input for another action. You should never try to calculate the path manually. Instead, use the
$(location ...) function inside the
cmd string. This function tells Bazel to substitute the actual path of the target at execution time.
For example, suppose you have a
genrule that generates a file named
data.json:
genrule(
name = "generate_data",
srcs = [],
outs = ["data.json"],
cmd = "echo '{\"key\": \"value\"}' > $@",
)
If you have a shell script that needs to read this file, do not write a path to
data.json. Pass the label of the
genrule as a dependency and use
$(location) in your command:
genrule(
name = "use_data",
srcs = ["process.sh"],
outs = ["result.txt"],
data = [":generate_data"],
cmd = "$(location :generate_data) && ./process.sh $(location :generate_data/data.json) > $@",
)
In this setup,
$(location :generate_data/data.json) expands to the exact path where Bazel has placed the generated file for this specific execution. This works seamlessly whether the target is in the current workspace or an external repository.
Handling External Repositories
When dealing with external repositories, the logic remains the same, but the labels change. An external repository is typically accessed via
@repo_name//path/to:target. If you need to access the output of a
genrule defined in an external dependency, you must include that external target in your
data or
srcs list and then reference it with
$(location).
This ensures that Bazel correctly resolves the symlink or copy operation required to bring the external artifact into your current execution sandbox. Without this explicit dependency, Bazel might not trigger the generation of the external artifact before your rule runs, leading to missing file errors.
As discussed in
Understanding Bazel: How to Retrieve All Providers of a Target, understanding how targets expose their artifacts is crucial for advanced dependency management. While
$(location) handles standard file outputs, providers allow you to pass structured metadata about those outputs, which can be useful for more complex toolchains.
Why Not Use Output Groups?
Some users might consider using
output_groups to expose directories. However,
output_groups are primarily designed for exposing sets of files to other rules or for packaging purposes (like creating a zip file). They do not provide a stable directory path for scripting commands. The
cmd attribute still requires individual file references. Using
$(location) on the specific output file is the most direct and reliable method for accessing the content.
If you are working with video processing or media files, note that Bazel's handling of large binary outputs follows the same principles. Just as you would handle a text file, you pass the generated media file to your encoding tools using location expansion. For more details on handling complex media workflows in build systems, see
Muxing AC3 with x264, timing & seeking challenges, which highlights the importance of precise input/output management in multimedia pipelines.
Best Practices for Robust Builds
Always treat the output of a
genrule as a black box. Do not assume its name or location. Rely entirely on Bazel’s dependency graph. If your script fails because it cannot find a file, check if the
genrule is listed in the
data attribute of the consuming rule. If it is not there, Bazel will not ensure it is built first, and the path resolution will fail.
Additionally, avoid using environment variables to store paths. These are often cleared or modified by the sandbox. The
$(location) expansion is the only guaranteed way to get the correct path during the execution phase. This approach keeps your build reproducible and portable across different machines and CI environments.
Frequently Asked Questions
Can I use environment variables to store the genrule output path?
No, you should not rely on environment variables because Bazel’s sandbox may clear them or alter the working directory. Always use
$(location ...) within the
cmd string to ensure the path is correctly resolved at runtime.
What happens if I forget to add the genrule to the data attribute?
If you do not list the
genrule as a dependency in
data or
srcs, Bazel will not build it before your rule executes. This leads to "file not found" errors because the output does not exist in the expected location when your script runs.
Does this method work for rules written in Starlark?
Yes, if you are writing custom rules in Starlark, you can access the outputs of other rules using the
ctx.files attribute or by passing
File objects. You can then construct the path using
file.path, which serves a similar purpose to
$(location) in shell commands.
How do I debug path issues in genrules?
Enable verbose logging by running
bazel build --verbose_failures //your:target. This will show the exact command line Bazel is executing, allowing you to see the expanded paths from
$(location) and verify that the files are being passed correctly.
Are there performance penalties for using many genrules?
genrules can be slower than native Bazel rules because they invoke external processes. If possible, prefer native rules or Skylark macros for generating code. However, for simple file manipulations,
genrule is acceptable and widely used.