From dc6208d1a7c84a805f3c72a678976baec097fc54 Mon Sep 17 00:00:00 2001 From: Maximilian Schik Date: Mon, 10 Jun 2024 09:54:37 +0200 Subject: [PATCH] adds support for colcon args when building colcon workspace (#27) * adds support for colcon args when building colcon workspace * adds help message for colcon-args * adds documentation for colcon-args option * Added test for option helpers --------- Co-authored-by: Felix Exner --- docs/configuration.rst | 4 +- docs/usage.rst | 4 ++ src/robot_folders/commands/make.py | 2 +- src/robot_folders/helpers/build_helpers.py | 42 ++++++++++++++--- src/robot_folders/helpers/option_helpers.py | 31 +++++++++++++ tests/test_option_helpers.py | 50 +++++++++++++++++++++ 6 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 src/robot_folders/helpers/option_helpers.py create mode 100644 tests/test_option_helpers.py diff --git a/docs/configuration.rst b/docs/configuration.rst index b59cca9..7e1d88d 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -34,7 +34,9 @@ Build options Set to catkin_make by default but can be changed to catkin build. ``colcon_build_options`` - Options passed to each ``colcon build`` invocation that is piped through ``fzirob make``. + Options passed to each ``colcon build`` invocation that is piped through ``fzirob make``. These + options can be overriden by using the ``--colcon-args`` option when running + ``fzirob make colcon`` Directory options diff --git a/docs/usage.rst b/docs/usage.rst index bce995c..4e1cea9 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -150,6 +150,10 @@ You can also manually specify which workspace to build by using ``fzirob make ros`` or ``fzirob make colcon``. When using ``fzirob make`` you don't have to worry about the particular build command at all. +When building a colcon workspace, you can pass arguments to colcon using the ``--colcon-args`` +option. For example to selectivly build a package called ``foobar`` and install it using symlinks +you can call ``fzirob make --colcon-args --packages-select foobar --symlink-install``. + Default options for building environments such as the builder for catkin workspaces or cmake arguments for a colcon workspace can be set in the :ref:`configuration:Configuration`. diff --git a/src/robot_folders/commands/make.py b/src/robot_folders/commands/make.py index 6dfb36c..35c99b8 100644 --- a/src/robot_folders/commands/make.py +++ b/src/robot_folders/commands/make.py @@ -42,7 +42,7 @@ def get_command(self, ctx, name): if name == "ros": return build.CatkinBuilder(name=name, add_help_option=False) elif name == "colcon": - return build.ColconBuilder(name=name, add_help_option=False) + return build.ColconBuilder(name=name, add_help_option=True) else: click.echo("Did not find a workspace with the key < {} >.".format(name)) return None diff --git a/src/robot_folders/helpers/build_helpers.py b/src/robot_folders/helpers/build_helpers.py index 976eec7..84243f1 100644 --- a/src/robot_folders/helpers/build_helpers.py +++ b/src/robot_folders/helpers/build_helpers.py @@ -35,6 +35,7 @@ from robot_folders.helpers import compilation_db_helpers from robot_folders.helpers import config_helpers from robot_folders.helpers.exceptions import ModuleException +from robot_folders.helpers.option_helpers import SwallowAllOption def get_cmake_flags(): @@ -154,12 +155,31 @@ def get_install_default(cls): class ColconBuilder(Builder): """Builder class for colcon workspace""" - def get_build_command(self, ros_distro): - build_cmd = "colcon build" + def __init__(self, *args, **kwargs): + params = [ + SwallowAllOption( + ["--colcon-args"], + nargs=-1, + help="Arguments passed to colcon. Everything after this flag will be interpreted as" + " colcon arguments. If this option is set, colcon arguments in the config are " + "discarded", + type=click.UNPROCESSED, + ) + ] - colcon_options = config_helpers.get_value_safe_default( - section="build", value="colcon_build_options", default="" - ) + if "params" in kwargs and kwargs["params"]: + kwargs["params"].extend(params) + else: + kwargs["params"] = params + super().__init__(*args, **kwargs) + + def get_build_command(self, ros_distro, colcon_options): + if colcon_options is None: + colcon_options = config_helpers.get_value_safe_default( + section="build", value="colcon_build_options", default="" + ) + + build_cmd = "colcon build" generator_flag = "" generator = config_helpers.get_value_safe_default( @@ -193,6 +213,16 @@ def invoke(self, ctx): colcon_dir = get_colcon_dir() click.echo("Building colcon_ws in {}".format(colcon_dir)) + if ( + ctx is not None + and "colcon_args" in ctx.params + and ctx.params["colcon_args"] is not None + ): + colcon_args = ctx.params["colcon_args"] + colcon_args = " ".join(colcon_args) + else: + colcon_args = None + # Colcon needs to build in an env that does not have the current workspace sourced # See https://docs.ros.org/en/galactic/Tutorials/Workspace/Creating-A-Workspace.html#source-the-overlay my_env = os.environ.copy() @@ -203,7 +233,7 @@ def invoke(self, ctx): # We abuse the name to code the ros distribution if we're building for the first time. try: process = subprocess.check_call( - ["bash", "-c", self.get_build_command(self.name)], + ["bash", "-c", self.get_build_command(self.name, colcon_args)], cwd=colcon_dir, env=my_env, ) diff --git a/src/robot_folders/helpers/option_helpers.py b/src/robot_folders/helpers/option_helpers.py new file mode 100644 index 0000000..a4a77b2 --- /dev/null +++ b/src/robot_folders/helpers/option_helpers.py @@ -0,0 +1,31 @@ +import click + + +class SwallowAllOption(click.Option): + """Option that swallows all values after it""" + + def __init__(self, *args, **kwargs): + nargs = kwargs.pop("nargs", -1) + + if nargs != -1: + raise ValueError("nargs has to be -1") + + super(SwallowAllOption, self).__init__(*args, **kwargs) + + def add_to_parser(self, parser, ctx): + def parser_process(value, state): + value = [value] + state.rargs + value = tuple(value) + state.rargs[:] = [] + + self._previous_parser_process(value, state) + + retval = super(SwallowAllOption, self).add_to_parser(parser, ctx) + for name in self.opts: + our_parser = parser._long_opt.get(name) or parser._short_opt.get(name) + if our_parser: + self._eat_all_parser = our_parser + self._previous_parser_process = our_parser.process + our_parser.process = parser_process + break + return retval diff --git a/tests/test_option_helpers.py b/tests/test_option_helpers.py new file mode 100644 index 0000000..7670a88 --- /dev/null +++ b/tests/test_option_helpers.py @@ -0,0 +1,50 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import click + +import pytest + +from robot_folders.helpers.option_helpers import SwallowAllOption + + +def test_swallow_all_option(): + runner = click.testing.CliRunner() + + colcon_args = "--symlink-install --packages-select foobar" + + @click.command() + @click.option("--colcon_args", cls=SwallowAllOption) + def test_command(colcon_args): + click.echo(f"{colcon_args}!") + + result = runner.invoke(test_command, [f"--colcon_args={colcon_args}"]) + assert result.exit_code == 0 + assert result.output == f"('{colcon_args}',)!\n" + + with pytest.raises(ValueError): + + @click.command() + @click.option("--colcon_args", cls=SwallowAllOption, nargs=2) + def illegal_test_command(): + click.echo("hello") + + runner.invoke(illegal_test_command)