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,