Skip to content

Bazel: xacro_file rule fails with [Errno 2] No such file or directory due to incorrect --root-dir calculation in subpackages #381

@udaya2899

Description

@udaya2899

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_dir

This 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions