diff --git a/.bazelignore b/.bazelignore
new file mode 100644
index 0000000..726996c
--- /dev/null
+++ b/.bazelignore
@@ -0,0 +1 @@
+bazel/integration_test
diff --git a/.bazelversion b/.bazelversion
new file mode 100644
index 0000000..815da58
--- /dev/null
+++ b/.bazelversion
@@ -0,0 +1 @@
+7.4.1
diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml
new file mode 100644
index 0000000..8dce3bd
--- /dev/null
+++ b/.github/workflows/bazel.yml
@@ -0,0 +1,21 @@
+name: Bazel CI
+
+on: [push, pull_request, workflow_dispatch]
+
+jobs:
+ test:
+ uses: bazel-contrib/.github/.github/workflows/bazel.yaml@v7
+ with:
+ folders: |
+ [
+ ".",
+ "bazel/integration_test",
+ ]
+ # Explicitly exclude build/test configurations where bzlmod is disabled.
+ # Since xacro only supports bzlmod, these will always fail.
+ # Remove these exclusions when workspace support is dropped.
+ exclude: |
+ [
+ {"folder": ".", "bzlmodEnabled": false},
+ {"folder": "bazel/integration_test", "bzlmodEnabled": false},
+ ]
diff --git a/.gitignore b/.gitignore
index 27ffc2f..7c1f968 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
*.pyc
build
+bazel-*
+MODULE.bazel.lock
diff --git a/BUILD.bazel b/BUILD.bazel
new file mode 100644
index 0000000..3fc6923
--- /dev/null
+++ b/BUILD.bazel
@@ -0,0 +1,81 @@
+load("@bazel_skylib//rules:write_file.bzl", "write_file")
+load("@rules_license//rules:license.bzl", "license")
+load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
+
+package(
+ default_applicable_licenses = [":license"],
+)
+
+licenses(["notice"])
+
+license(
+ name = "license",
+ license_kinds = [
+ "@rules_license//licenses/spdx:BSD-3-Clause",
+ ],
+ license_text = "LICENSE",
+)
+
+write_file(
+ name = "write_xacro_main",
+ out = "xacro_main.py",
+ # This is the same as scripts/xacro from upstream, except that we lose the
+ # unused shebang line and we use a filename that is not subject to import
+ # path conflicts.
+ content = ["import xacro; xacro.main()"],
+)
+
+py_library(
+ name = "xacro_lib",
+ srcs = [
+ "xacro/__init__.py",
+ "xacro/cli.py",
+ "xacro/color.py",
+ "xacro/substitution_args.py",
+ "xacro/xmlutils.py",
+ ],
+ imports = ["."],
+ visibility = ["//visibility:public"],
+ deps = [
+ "@rules_python//python/runfiles",
+ "@xacro_python_dependencies//pyyaml:pkg",
+ ],
+)
+
+py_binary(
+ name = "xacro_main",
+ srcs = ["xacro_main.py"],
+ main = "xacro_main.py",
+ deps = [":xacro_lib"],
+)
+
+alias(
+ name = "xacro",
+ actual = ":xacro_main",
+ visibility = ["//visibility:public"],
+)
+
+TEST_RESOURCES = glob([
+ "test/*.xacro",
+ "test/*.xml",
+ "test/*.yaml",
+ "test/subdir/**",
+ "test/robots/**",
+])
+
+filegroup(
+ name = "test_data",
+ srcs = TEST_RESOURCES,
+ data = TEST_RESOURCES,
+)
+
+py_test(
+ name = "test_xacro",
+ srcs = ["test/test_xacro.py"],
+ data = [":test_data"],
+ main = "test/test_xacro.py",
+ deps = [
+ ":xacro_main",
+ "@rules_python//python/runfiles",
+ ],
+)
diff --git a/MODULE.bazel b/MODULE.bazel
new file mode 100644
index 0000000..1bde870
--- /dev/null
+++ b/MODULE.bazel
@@ -0,0 +1,39 @@
+module(
+ name = "xacro",
+ version = "2.0.11",
+)
+
+bazel_dep(name = "rules_license", version = "1.0.0")
+bazel_dep(name = "bazel_skylib", version = "1.7.1")
+bazel_dep(name = "rules_python", version = "0.40.0")
+
+PYTHON_VERSIONS = [
+ "3.8",
+ "3.9",
+ "3.10",
+ "3.11",
+ "3.12",
+]
+
+python = use_extension("@rules_python//python/extensions:python.bzl", "python")
+
+[
+ python.toolchain(
+ is_default = python_version == PYTHON_VERSIONS[-1],
+ python_version = python_version,
+ )
+ for python_version in PYTHON_VERSIONS
+]
+
+pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
+
+[
+ pip.parse(
+ hub_name = "xacro_python_dependencies",
+ python_version = python_version,
+ requirements_lock = "//bazel:requirements_lock.txt",
+ )
+ for python_version in PYTHON_VERSIONS
+]
+
+use_repo(pip, "xacro_python_dependencies")
diff --git a/bazel/BUILD.bazel b/bazel/BUILD.bazel
new file mode 100644
index 0000000..183ba44
--- /dev/null
+++ b/bazel/BUILD.bazel
@@ -0,0 +1,20 @@
+load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("@rules_python//python:pip.bzl", "compile_pip_requirements")
+
+# This rule adds a convenient way to update the requirements file.
+compile_pip_requirements(
+ name = "requirements",
+ src = "requirements.in",
+ requirements_txt = "requirements_lock.txt",
+)
+
+bzl_library(
+ name = "defs",
+ srcs = ["defs.bzl"],
+ visibility = ["//visibility:public"],
+)
+
+exports_files([
+ "requirements.in",
+ "requirements_lock.txt",
+])
diff --git a/bazel/README.md b/bazel/README.md
new file mode 100644
index 0000000..ec41e86
--- /dev/null
+++ b/bazel/README.md
@@ -0,0 +1,66 @@
+# Bazel
+
+This directory contains support files and tests for the bazel build system.
+
+In addition to supporting building xacro with bazel, this also introduces two rules for build-time generation of xacro files.
+
+## xacro_file
+
+Allows you to transform a single xacro file into a generated output
+
+A simple example:
+
+```
+load("@xacro//bazel:defs.bzl", "xacro_file")
+
+# By default, will transform input filename with .xacro removed
+xacro_file(
+ name = "sample1",
+ src = "sample1.xml.xacro",
+)
+```
+
+A more complex example:
+
+```
+load("@xacro//bazel:defs.bzl", "xacro_file")
+
+# By default, will transform input filename with .xacro removed
+xacro_file(
+ name = "complex_example",
+ src = "complex.xml.xacro",
+ # Override the default output file name
+ out = "my_complex_model.xml",
+ # Depend on the XML file that we generated in the previous step
+ deps = [":sample1"],
+ # Set extra substitution args via the command line
+ extra_args = ["special_argument:=foo"]
+)
+```
+
+Note in the case of the more complex example, you can use bazel-specified filenames if they are specified in the `deps` field:
+
+```
+
+
+
+
+
+```
+
+## xacro_filegroup
+
+Allows you to transform multiple xacro files into a generated filegroup
+
+```
+xacro_filegroup(
+ name = "samples",
+ srcs = [
+ "sample1.xml.xacro",
+ "sample2.xml.xacro",
+ ],
+ data = [
+ "box.xml",
+ ],
+)
+```
diff --git a/bazel/defs.bzl b/bazel/defs.bzl
new file mode 100644
index 0000000..0adfc2c
--- /dev/null
+++ b/bazel/defs.bzl
@@ -0,0 +1,116 @@
+"""Provider and rules for generating xacro output at build time."""
+
+XacroInfo = provider(
+ "Provider holding the result of a xacro generation step.",
+ fields = ["result"],
+)
+
+XACRO_EXTENSION = ".xacro"
+
+def _xacro_impl(ctx):
+ # Use declared output or derive from source name
+ out = ctx.outputs.out or ctx.actions.declare_file(ctx.file.src.basename[:-len(XACRO_EXTENSION)])
+
+ # Gather inputs for the xacro command
+ direct_inputs = [ctx.file.src] + ctx.files.data
+ dep_inputs = [dep[XacroInfo].result for dep in ctx.attr.deps]
+ all_inputs = direct_inputs + dep_inputs
+
+ # Create a temporary directory
+ temp_dir = "TMP_XACRO/" + ctx.label.name
+
+ symlink_paths = []
+ for input in all_inputs:
+ symlink_path = ctx.actions.declare_file(temp_dir + "/" + input.basename)
+ ctx.actions.symlink(
+ output = symlink_path,
+ target_file = input,
+ )
+ symlink_paths.append(symlink_path)
+
+ arguments = [
+ "-o",
+ out.path,
+ "--root-dir",
+ ctx.bin_dir.path + "/" + temp_dir,
+ ctx.file.src.basename,
+ ]
+ arguments += ["{}:={}".format(arg, val) for arg, val in ctx.attr.arguments.items()]
+
+ ctx.actions.run(
+ inputs = symlink_paths,
+ outputs = [out],
+ arguments = arguments,
+ executable = ctx.executable._xacro,
+ progress_message = "Running xacro: %s -> %s" % (ctx.file.src.short_path, out.short_path),
+ mnemonic = "Xacro",
+ )
+
+ return [
+ XacroInfo(result = out),
+ DefaultInfo(
+ files = depset([out]),
+ data_runfiles = ctx.runfiles(files = [out]),
+ ),
+ ]
+
+xacro_file = rule(
+ attrs = {
+ "src": attr.label(
+ mandatory = True,
+ allow_single_file = True,
+ ),
+ "out": attr.output(),
+ "data": attr.label_list(
+ allow_files = True,
+ ),
+ "arguments": attr.string_dict(),
+ "deps": attr.label_list(providers = [XacroInfo]),
+ "_xacro": attr.label(
+ default = "@xacro//:xacro",
+ cfg = "host",
+ executable = True,
+ ),
+ },
+ implementation = _xacro_impl,
+ provides = [XacroInfo, DefaultInfo],
+)
+
+def xacro_filegroup(
+ name,
+ srcs = [],
+ data = [],
+ tags = [],
+ visibility = None):
+ """Runs xacro on several input files, creating a filegroup of the output.
+
+ The output filenames will match the input filenames but with the ".xacro"
+ suffix removed.
+
+ Xacro is the ROS XML macro tool; http://wiki.ros.org/xacro.
+
+ Args:
+ name: The name of the filegroup label.
+ srcs: The xacro input files of this rule.
+ data: Optional supplemental files required by the srcs.
+ """
+ outs = []
+ for src in srcs:
+ if not src.endswith(XACRO_EXTENSION):
+ fail("xacro_filegroup srcs should be named *.xacro not {}".format(
+ src,
+ ))
+ out = src[:-len(XACRO_EXTENSION)]
+ outs.append(out)
+ xacro_file(
+ name = out,
+ src = src,
+ data = data,
+ tags = tags,
+ visibility = ["//visibility:private"],
+ )
+ native.filegroup(
+ name = name,
+ srcs = outs,
+ visibility = visibility,
+ )
diff --git a/bazel/integration_test/.bazelversion b/bazel/integration_test/.bazelversion
new file mode 120000
index 0000000..96cf949
--- /dev/null
+++ b/bazel/integration_test/.bazelversion
@@ -0,0 +1 @@
+../../.bazelversion
\ No newline at end of file
diff --git a/bazel/integration_test/BUILD.bazel b/bazel/integration_test/BUILD.bazel
new file mode 100644
index 0000000..06aff5b
--- /dev/null
+++ b/bazel/integration_test/BUILD.bazel
@@ -0,0 +1,64 @@
+load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
+load("@xacro//bazel:defs.bzl", "xacro_file")
+
+xacro_file(
+ name = "sample1",
+ src = "inputs/sample1.xml.xacro",
+)
+
+xacro_file(
+ name = "sample2",
+ src = "inputs/sample2.xml.xacro",
+ data = ["inputs/box.xml"],
+)
+
+xacro_file(
+ name = "complex_example",
+ src = "inputs/complex.xml.xacro",
+ out = "my_complex_model.xml",
+ arguments = {"special_argument": "foo"},
+ deps = [":sample1"],
+)
+
+xacro_file(
+ name = "conditional_default",
+ src = "inputs/conditional.xml.xacro",
+ out = "conditional_default.xml",
+)
+
+xacro_file(
+ name = "conditional_false",
+ src = "inputs/conditional.xml.xacro",
+ out = "conditional_false.xml",
+ arguments = {"myarg": "false"},
+)
+
+diff_test(
+ name = "sample1_test",
+ file1 = ":sample1",
+ file2 = "expected/sample1.xml",
+)
+
+diff_test(
+ name = "sample2_test",
+ file1 = ":sample2",
+ file2 = "expected/sample2.xml",
+)
+
+diff_test(
+ name = "complex_example_test",
+ file1 = ":complex_example",
+ file2 = "expected/my_complex_model.xml",
+)
+
+diff_test(
+ name = "conditional_default_test",
+ file1 = ":conditional_default",
+ file2 = "expected/conditional_default.xml",
+)
+
+diff_test(
+ name = "conditional_false_test",
+ file1 = ":conditional_false",
+ file2 = "expected/conditional_false.xml",
+)
diff --git a/bazel/integration_test/MODULE.bazel b/bazel/integration_test/MODULE.bazel
new file mode 100644
index 0000000..0d7e621
--- /dev/null
+++ b/bazel/integration_test/MODULE.bazel
@@ -0,0 +1,6 @@
+bazel_dep(name = "bazel_skylib", version = "1.7.1")
+bazel_dep(name = "xacro")
+local_path_override(
+ module_name = "xacro",
+ path = "../..",
+)
diff --git a/bazel/integration_test/expected/conditional_default.xml b/bazel/integration_test/expected/conditional_default.xml
new file mode 100644
index 0000000..d6cf2e4
--- /dev/null
+++ b/bazel/integration_test/expected/conditional_default.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/bazel/integration_test/expected/conditional_false.xml b/bazel/integration_test/expected/conditional_false.xml
new file mode 100644
index 0000000..b8647c0
--- /dev/null
+++ b/bazel/integration_test/expected/conditional_false.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/bazel/integration_test/expected/my_complex_model.xml b/bazel/integration_test/expected/my_complex_model.xml
new file mode 100644
index 0000000..40c042f
--- /dev/null
+++ b/bazel/integration_test/expected/my_complex_model.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/bazel/integration_test/expected/sample1.xml b/bazel/integration_test/expected/sample1.xml
new file mode 100644
index 0000000..6bc5022
--- /dev/null
+++ b/bazel/integration_test/expected/sample1.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/bazel/integration_test/expected/sample2.xml b/bazel/integration_test/expected/sample2.xml
new file mode 100644
index 0000000..0d9b5cc
--- /dev/null
+++ b/bazel/integration_test/expected/sample2.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/bazel/integration_test/inputs/box.xml b/bazel/integration_test/inputs/box.xml
new file mode 100644
index 0000000..7caa375
--- /dev/null
+++ b/bazel/integration_test/inputs/box.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bazel/integration_test/inputs/complex.xml.xacro b/bazel/integration_test/inputs/complex.xml.xacro
new file mode 100644
index 0000000..7cf0981
--- /dev/null
+++ b/bazel/integration_test/inputs/complex.xml.xacro
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/bazel/integration_test/inputs/conditional.xml.xacro b/bazel/integration_test/inputs/conditional.xml.xacro
new file mode 100644
index 0000000..3591aa3
--- /dev/null
+++ b/bazel/integration_test/inputs/conditional.xml.xacro
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/bazel/integration_test/inputs/sample1.xml.xacro b/bazel/integration_test/inputs/sample1.xml.xacro
new file mode 100644
index 0000000..9b3aee5
--- /dev/null
+++ b/bazel/integration_test/inputs/sample1.xml.xacro
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/bazel/integration_test/inputs/sample2.xml.xacro b/bazel/integration_test/inputs/sample2.xml.xacro
new file mode 100644
index 0000000..994cfe0
--- /dev/null
+++ b/bazel/integration_test/inputs/sample2.xml.xacro
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/bazel/requirements.in b/bazel/requirements.in
new file mode 100644
index 0000000..c3726e8
--- /dev/null
+++ b/bazel/requirements.in
@@ -0,0 +1 @@
+pyyaml
diff --git a/bazel/requirements_lock.txt b/bazel/requirements_lock.txt
new file mode 100644
index 0000000..b12b056
--- /dev/null
+++ b/bazel/requirements_lock.txt
@@ -0,0 +1,61 @@
+#
+# This file is autogenerated by pip-compile with Python 3.12
+# by the following command:
+#
+# bazel run //bazel:requirements.update
+#
+pyyaml==6.0.2 \
+ --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
+ --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
+ --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
+ --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
+ --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
+ --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
+ --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
+ --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
+ --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
+ --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
+ --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
+ --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
+ --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
+ --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
+ --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
+ --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
+ --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
+ --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
+ --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
+ --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
+ --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
+ --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
+ --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
+ --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
+ --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
+ --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
+ --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
+ --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
+ --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
+ --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
+ --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
+ --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
+ --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
+ --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
+ --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
+ --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
+ --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
+ --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
+ --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
+ --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
+ --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
+ --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
+ --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
+ --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
+ --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
+ --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
+ --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
+ --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
+ --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
+ --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
+ --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
+ --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
+ --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
+ # via -r bazel/requirements.in
diff --git a/test/test_xacro.py b/test/test_xacro.py
index 60087e2..5f0ea41 100644
--- a/test/test_xacro.py
+++ b/test/test_xacro.py
@@ -57,6 +57,20 @@
def subTest(msg):
yield None
+try:
+ # Determine if we are running the test under bazel and switch to runfiles directory
+ from python.runfiles import runfiles
+
+ data_path = runfiles.Create().Rlocation("_main/test")
+ os.chdir(data_path)
+
+ # Set the executable path
+ XACRO_EXECUTABLE = runfiles.Create().Rlocation("_main/xacro_main")
+ BAZEL_TEST = True
+except ImportError:
+ XACRO_EXECUTABLE = 'xacro'
+ BAZEL_TEST = False
+
# regex to match whitespace
whitespace = re.compile(r'\s+')
@@ -383,7 +397,7 @@ def quick_xacro(self, xml, cli=None, **kwargs):
def run_xacro(self, input_path, *args):
args = list(args)
- subprocess.call(['xacro', input_path] + args)
+ subprocess.call([XACRO_EXECUTABLE, input_path] + args)
# class to match XML docs while ignoring any comments
@@ -392,6 +406,7 @@ def __init__(self, *args, **kwargs):
super(TestXacroCommentsIgnored, self).__init__(*args, **kwargs)
self.ignore_nodes = [xml.dom.Node.COMMENT_NODE]
+ @unittest.skipIf(BAZEL_TEST, "Bazel build does not support $(find pkg)")
def test_pr2(self):
# run xacro on the pr2 tree snapshot
test_dir = os.path.abspath(os.path.dirname(__file__))
@@ -551,6 +566,7 @@ def test_math_ignores_spaces(self):
src = ''''''
self.assert_matches(self.quick_xacro(src), '''''')
+ @unittest.skipIf(BAZEL_TEST, "Bazel build does not support $(find pkg)")
def test_substitution_args_find(self):
resolved = self.quick_xacro('''$(find xacro)/test/test_xacro.py''').firstChild.firstChild.data
self.assertEqual(os.path.realpath(resolved), os.path.realpath(__file__))
@@ -1125,6 +1141,46 @@ def test_create_subdirs(self):
self.assertTrue(output_file_created)
+ def test_set_root_directory(self):
+ # Run xacro in one directory, but specify the directory to resolve
+ # filenames in.
+ tmp_dir_name = tempfile.mkdtemp() # create directory we can trash
+
+ output_path = os.path.join(tmp_dir_name, "out")
+
+ # Generate a pair of files to be parsed by xacro, outside of the
+ # test directory, to ensure the root-dir argument works
+ file_foo = os.path.join(tmp_dir_name, 'foo.xml.xacro')
+ file_bar = os.path.join(tmp_dir_name, 'bar.xml.xacro')
+ with open(file_foo, 'w') as f:
+ f.write('''
+
+
+
+''')
+ with open(file_bar, 'w') as f:
+ f.write('''
+
+
+
+''')
+
+ # Run xacro with no --root-dir arg, which will then use the
+ # current path as the path to resolveto
+ self.run_xacro('foo.xml.xacro', '-o', output_path)
+ output_file_created = os.path.isfile(output_path)
+ self.assertFalse(output_file_created)
+
+ # Run xacro with --root-dir arg set to the new temp directory
+ self.run_xacro('foo.xml.xacro',
+ '--root-dir', tmp_dir_name,
+ '-o', output_path)
+
+ output_file_created = os.path.isfile(output_path)
+ shutil.rmtree(tmp_dir_name) # clean up after ourselves
+
+ self.assertTrue(output_file_created)
+
def test_iterable_literals_plain(self):
self.assert_matches(self.quick_xacro('''
diff --git a/xacro/__init__.py b/xacro/__init__.py
index 9ddbcc7..ea6d044 100644
--- a/xacro/__init__.py
+++ b/xacro/__init__.py
@@ -55,6 +55,10 @@
filestack = None
macrostack = None
+# Allow the user to override the root directory that relative
+# paths will be resolved to
+root_dir = os.curdir
+
def init_stacks(file):
global filestack
@@ -1018,7 +1022,8 @@ def parse(inp, filename=None):
f = None
if inp is None:
try:
- inp = f = open(filename)
+ global root_dir
+ inp = f = open(os.path.join(root_dir, filename))
except IOError as e:
# do not report currently processed file as "in file ..."
filestack.pop()
@@ -1123,6 +1128,10 @@ def process_file(input_file_name, **kwargs):
def _process(input_file_name, opts):
+ if 'root_dir' in opts and opts['root_dir']:
+ global root_dir
+ root_dir = opts['root_dir']
+
try:
# open and process file
doc = process_file(input_file_name, **opts)
diff --git a/xacro/cli.py b/xacro/cli.py
index 29a1637..82d032d 100644
--- a/xacro/cli.py
+++ b/xacro/cli.py
@@ -101,6 +101,8 @@ def process_args(argv, require_input=True):
help="print file dependencies")
parser.add_option("--inorder", "-i", action="store_true", dest="in_order",
help="processing in read order (default, can be omitted)")
+ parser.add_option("--root-dir", dest="root_dir", metavar="DIR", default=None,
+ help="set the root directory to resolve relative paths to")
# verbosity options
parser.add_option("-q", action="store_const", dest="verbosity", const=0,