Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bazel_ros2_rules/lib/extensions.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def _local_ros2_implementation(module_ctx):
"rosidl_default_generators",
"service_msgs",
"unique_identifier_msgs",
"xacro",
]

underlay = find_local_ros2_distribution(module_ctx)
Expand Down
18 changes: 18 additions & 0 deletions bazel_ros2_rules/lib/private/common.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ def interfaces_filegroup(name, share_directory):
], allow_empty = True),
)

def _generate_file_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(out, ctx.attr.content, ctx.attr.is_executable)
return [DefaultInfo(
files = depset([out]),
data_runfiles = ctx.runfiles(files = [out]),
)]

generate_file = rule(
attrs = {
"content": attr.string(mandatory = True),
"is_executable": attr.bool(default = False),
},
output_to_genfiles = True,
implementation = _generate_file_impl,
)
"""Writes a string to a file at build time."""

def incorporate_rmw_implementation(kwargs, env_changes, rmw_implementation):
target = REPOSITORY_ROOT + ":%s_cc" % rmw_implementation
kwargs["data"] = kwargs.get("data", []) + [target]
Expand Down
1 change: 1 addition & 0 deletions bazel_ros2_rules/lib/private/repos.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ COMMON_FILES_MANIFEST = [
"ros_cc.bzl",
"ros_py.bzl",
"rosidl.bzl",
"xacro.bzl",
"cmake_tools/__init__.py",
"cmake_tools/file_api.py",
"cmake_tools/packages.py",
Expand Down
27 changes: 7 additions & 20 deletions bazel_ros2_rules/lib/private/ros_py.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ load(
)
load(
":common.bzl",
"generate_file",
"incorporate_rmw_implementation",
)
load(
Expand All @@ -23,6 +24,8 @@ load(
"RUNTIME_ENVIRONMENT",
)

_WORKSPACE_NAME = Label(REPOSITORY_ROOT + ":ros2").workspace_name

def ros_import_binary(
name,
executable,
Expand Down Expand Up @@ -144,24 +147,7 @@ def _add_deps(existing, new):
deps.append(dep)
return deps

def _generate_file_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(out, ctx.attr.content, ctx.attr.is_executable)
return [DefaultInfo(
files = depset([out]),
data_runfiles = ctx.runfiles(files = [out]),
)]

_generate_file = rule(
attrs = {
"content": attr.string(mandatory = True),
"is_executable": attr.bool(default = False),
},
output_to_genfiles = True,
implementation = _generate_file_impl,
)

_LAUNCH_PY_TEMPLATE = """
_LAUNCH_PY_TEMPLATE = """\
import os
import sys

Expand All @@ -170,7 +156,7 @@ from python.runfiles import runfiles as runfiles_api
assert __name__ == "__main__"
runfiles = runfiles_api.Create()
launch_file = runfiles.Rlocation({launch_respath}) # noqa
ros2_bin = runfiles.Rlocation("ros2/ros2")
ros2_bin = runfiles.Rlocation("{ros2_rlocation}")
args = [ros2_bin, "launch", launch_file] + sys.argv[1:]
os.execv(ros2_bin, args)
"""
Expand Down Expand Up @@ -215,8 +201,9 @@ def ros_launch(

content = _LAUNCH_PY_TEMPLATE.format(
launch_respath = repr(launch_respath),
ros2_rlocation = _WORKSPACE_NAME + "/ros2",
)
_generate_file(
generate_file(
name = main,
content = content,
visibility = ["//visibility:private"],
Expand Down
149 changes: 149 additions & 0 deletions bazel_ros2_rules/lib/private/xacro.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
""" Defines a rule and macro for transforming xacro files to a URDF.
"""

load("@bazel_ros2_rules//lib:ament_index.bzl", "ament_index_share_files")
load(":common.bzl", "generate_file")
load(":distro.bzl", "REPOSITORY_ROOT")
load(":ros_py.bzl", "ros_py_binary")

# Derive the plain workspace name for use in Rlocation paths ("name/target").
# Label.workspace_name handles both Bzlmod ("@@name//") and WORKSPACE ("@name//")
# formats correctly without manual string manipulation.
_WORKSPACE_NAME = Label(REPOSITORY_ROOT + ":xacro").workspace_name

# Runner script template. The outer dload shim (from ros_py_binary) has
# already set AMENT_PREFIX_PATH to include both system and user-package
# prefixes (all absolute via $RUNFILES_DIR) before this script runs. What is
# needed then, is to locate and exec the inner @ros2//:xacro shim.
_XACRO_RUNNER_TEMPLATE = """\
import os
import sys

from python.runfiles import runfiles as runfiles_api

assert __name__ == "__main__"
runfiles = runfiles_api.Create()
xacro_bin = runfiles.Rlocation("{xacro_rlocation}")
os.execv(xacro_bin, [xacro_bin] + sys.argv[1:])
"""

def _ros_xacro_impl(ctx):
output = ctx.actions.declare_file(ctx.label.name + ".urdf")
args = ctx.actions.args()
args.add(ctx.file.src)
args.add("-o", output)
args.add_all(ctx.attr.xacro_args)
ctx.actions.run(
inputs = [ctx.file.src] + ctx.files.data,
outputs = [output],
executable = ctx.executable.xacro_tool,
arguments = [args],
)
return [DefaultInfo(files = depset([output]))]

_ros_xacro_rule = rule(
attrs = {
"src": attr.label(
allow_single_file = [".xacro"],
mandatory = True,
doc = "The main .urdf.xacro file to process.",
),
"data": attr.label_list(
allow_files = True,
default = [],
doc = "Additional files included via relative paths (e.g. .xacro, .yaml).",
),
"xacro_args": attr.string_list(
default = [],
doc = "Extra key:=value arguments forwarded to xacro.",
),
"xacro_tool": attr.label(
executable = True,
cfg = "exec",
mandatory = True,
doc = "The per-invocation xacro runner binary.",
),
},
implementation = _ros_xacro_impl,
)

def ros_xacro(name, src, data = [], ros_packages = {}, xacro_args = [], visibility = None):
"""Transforms a .urdf.xacro file into a .urdf file.

User-defined packages are declared inline via the ros_packages dict. Each
entry maps a ROS package name to the list of files to place under
share/<package_name>/. The strip_prefix is derived automatically from the
calling BUILD file's package path, so files are placed relative to that
package directory.

The dload shim from ros_py_binary extends AMENT_PREFIX_PATH with all
registered package prefixes (absolute, via $RUNFILES_DIR) before xacro
runs, making $(find <pkg>) work for both local and system packages.

Example:
ros_xacro(
name = "example",
src = "robot.urdf.xacro",
data = ["base.xacro", "arm.xacro"],
ros_packages = {
"my_robot": glob(["urdf/**"]),
},
xacro_args = ["sim:=false"],
)

Args:
name: target name; the output file is named <name>.urdf
src: the .urdf.xacro source file
data: additional files included via relative paths
(e.g. plain <xacro:include filename="other.xacro"/>
or referenced .yaml configs); must be listed here so
Bazel sandboxes them and tracks them as dependencies
for incremental rebuilds
ros_packages: dict mapping ROS package name to list of share files;
files are stripped of the calling package's path prefix
automatically before being placed under share/<pkg>/
xacro_args: list of key:=value arguments forwarded to xacro
visibility: target visibility
"""
pkg_targets = []
strip_prefix = native.package_name() + "/" if native.package_name() else ""
for pkg_name, srcs in ros_packages.items():
index_name = "_{}_pkg_{}".format(name, pkg_name)
ament_index_share_files(
name = index_name,
package_name = pkg_name,
srcs = srcs,
strip_prefix = strip_prefix,
visibility = ["//visibility:private"],
)
pkg_targets.append(":" + index_name)

runner_data = [REPOSITORY_ROOT + ":xacro"] + pkg_targets

runner_main = "_{}_runner_main.py".format(name)
generate_file(
name = runner_main,
content = _XACRO_RUNNER_TEMPLATE.format(
xacro_rlocation = _WORKSPACE_NAME + "/xacro",
),
visibility = ["//visibility:private"],
)

runner_name = "_{}_runner".format(name)
ros_py_binary(
name = runner_name,
main = runner_main,
srcs = [runner_main],
data = runner_data,
deps = ["@bazel_ros2_rules//deps/python/runfiles"],
visibility = ["//visibility:private"],
)

_ros_xacro_rule(
name = name,
src = src,
data = data,
xacro_args = xacro_args,
xacro_tool = ":" + runner_name,
visibility = visibility,
)
2 changes: 2 additions & 0 deletions ros2_example_bazel_installed/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ ROS_REQUIRED_PACKAGES = [
"rclcpp_action",
"rclpy",
"rosbag2",
"realsense2_description",
"ros2bag_mcap_cli",
"ros2bag_sqlite3_cli",
"tf2_py",
"xacro",
] + [
# These are possible RMW implementations. Uncomment one and only one to
# change implementations
Expand Down
20 changes: 20 additions & 0 deletions ros2_example_bazel_installed/ros2_example_xacro/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
load("@ros2//:ros_py.bzl", "ros_py_test")
load("@ros2//:xacro.bzl", "ros_xacro")

ros_xacro(
name = "example",
src = "example.urdf.xacro",
data = ["snippet.xacro"],
ros_packages = {
"my_robot": glob(["urdf/**"]),
},
xacro_args = ["sim:=false"],
)

ros_py_test(
name = "xacro_test",
srcs = ["test/xacro_test.py"],
data = [":example"],
main = "test/xacro_test.py",
deps = ["@rules_python//python/runfiles"],
)
18 changes: 18 additions & 0 deletions ros2_example_bazel_installed/ros2_example_xacro/example.urdf.xacro
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!-- urdf/robot.urdf.xacro -->
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
<!-- Should be able to include `.xacros` via a relative path (data= attribute) -->
<xacro:include filename="snippet.xacro"/>
<!-- Should be able to compose with local `.xacros` -->
<xacro:include filename="$(find my_robot)/urdf/macros.xacro"/>
<!-- Should be able to bring `.xacros` installed in the system (via `realsense2_description` for this example)-->
<xacro:include filename="$(find realsense2_description)/urdf/_d435i.urdf.xacro"/>
<!-- Should be able to retrieve xacro args-->
<xacro:arg name="sim" default="true" />
<xacro:property name="sim_mode" value="$(arg sim)"/>
<sim>${sim_mode}</sim>
<!-- Instantiate the D435i sensor to verify $(find realsense2_description) actually resolved. -->
<link name="base_link"/>
<xacro:sensor_d435i parent="base_link" name="head_camera" use_nominal_extrinsics="true">
<origin xyz="0 0 0" rpy="0 0 0"/>
</xacro:sensor_d435i>
</robot>
5 changes: 5 additions & 0 deletions ros2_example_bazel_installed/ros2_example_xacro/snippet.xacro
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0"?>
<!-- A minimal xacro included via a relative path to test the data= attribute. -->
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
<link name="snippet_link"/>
</robot>
36 changes: 36 additions & 0 deletions ros2_example_bazel_installed/ros2_example_xacro/test/xacro_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Tests that ros_xacro correctly processes xacro args and includes."""

import unittest

from python.runfiles import runfiles


class XacroTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
r = runfiles.Create()
path = r.Rlocation(
"ros2_example_bazel_installed/ros2_example_xacro/example.urdf"
)
with open(path) as f:
cls.urdf = f.read()

def test_sim_arg_is_substituted(self):
"""The sim xacro arg value appears in the output URDF."""
self.assertIn("<sim>False</sim>", self.urdf)

def test_local_package_macros_expanded(self):
"""Macros from the local my_robot package are resolved and expanded."""
self.assertIn("aluminum", self.urdf)

def test_relative_include_expanded(self):
"""A xacro included via a relative path (data=)"""
self.assertIn('name="snippet_link"', self.urdf)

def test_system_package_include_resolved(self):
"""$(find realsense2_description) is resolved and expanded."""
self.assertIn('name="head_camera_link"', self.urdf)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:macro name="simple_link" params="name">
<link name="${name}"/>
</xacro:macro>
</robot>
2 changes: 2 additions & 0 deletions ros2_example_bazel_installed/setup/packages-apt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ ros-jazzy-rclpy
ros-jazzy-rosbag2
ros-jazzy-rosbag2-storage-mcap
ros-jazzy-rosbag2-storage-sqlite3
ros-jazzy-realsense2-description
ros-jazzy-tf2-py
ros-jazzy-rmw-cyclonedds-cpp
ros-jazzy-xacro

# APT-SOURCE: http://*.ubuntu.com/ubuntu
gdb
Expand Down
Loading