-
-
Notifications
You must be signed in to change notification settings - Fork 106
Bazel: xacro_file rule fails with [Errno 2] No such file or directory due to incorrect --root-dir calculation in subpackages #381
Description
DISCLAIMER: Following text is generated by AI, but I guided the debug process, verified the fix myself on our codebase, and double checked the below issue description.
Description
When defining a xacro_file target inside a subpackage (e.g., //my/robot/description:xacro), the build fails because the xacro python script is given an incorrect --root-dir argument. The rule calculates a --root-dir that completely omits the Bazel package path, causing xacro to look for its temporary symlinks at the execution root instead of inside the package's output directory.
Additionally, when out is not explicitly provided (such as when using the xacro_filegroup macro), the rule double-nests the output files deep inside the workspace path because it passes ctx.file.src.path into declare_file.
Expected Behavior
The xacro_file rule should successfully find its symlinked dependencies and generate the output .sdf/.urdf file regardless of how deep in the repository the BUILD file is located.
Actual Behavior
The build crashes with a Python traceback from xacro:
xacro.XacroException: No such file or directory: bazel-out/k8-opt/bin//TMP_XACRO/my_target/my/robot/description/my_file.xacro [Errno 2] No such file or directory: 'bazel-out/k8-opt/bin//TMP_XACRO/my_target/my/robot/description/my_file.xacro'
(Notice the // and the missing package path before TMP_XACRO)
Root Cause Analysis
1. The --root-dir pathing bug:
When symlinks are created, the rule uses ctx.actions.declare_file(temp_dir + "/" + input_path). Bazel automatically prepends the package's output path to this (e.g., bazel-out/k8-opt/bin/my/robot/description/TMP_XACRO/...).
However, the --root-dir passed to xacro is calculated manually by stripping the short_path from out.path:
if out.path.endswith(out.short_path):
output_path = out.path[:-len(out.short_path)] # Evaluates to purely 'bazel-out/k8-opt/bin/'
root_dir = output_path + '/' + temp_dirThis sets --root-dir to bazel-out/k8-opt/bin//TMP_XACRO/..., completely losing the my/robot/description/ package path.
2. The xacro_filegroup output bug:
At the top of the rule, out is defaulted to:
out = ctx.outputs.out or ctx.actions.declare_file(ctx.file.src.path[:-len(XACRO_EXTENSION)])Because ctx.file.src.path includes the workspace path (e.g., my/robot/description/file.xacro), declare_file nests this again inside the package output directory: bazel-out/.../bin/my/robot/description/my/robot/description/file.sdf.
Proposed Fix
I was able to resolve both issues locally with the following changes to _xacro_impl:
Fix 1: Use .basename for default outputs
Change the default output declaration to use .basename so it doesn't double-nest paths:
out = ctx.outputs.out or ctx.actions.declare_file(ctx.file.src.basename[:-len(XACRO_EXTENSION)])Fix 2: Derive --root-dir dynamically from the generated symlinks
Remove the hardcoded output_path block entirely, and instead extract the true absolute root directory directly from the first symlink Bazel creates:
# Recompute the primary input_path just like we did for the symlinks
input_path = ctx.file.src.path
if input_path.startswith(prefix):
input_path = input_path[len(prefix):]
# symlink_paths[0] is guaranteed to be the symlink for ctx.file.src.
# By stripping the input_path from the end, we get the exact, correct root_dir
# regardless of whether it's an internal or external module.
root_dir = symlink_paths[0].path[:-len(input_path) - 1]I can propose an upstream PR