launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #29519
[Merge] ~artivis/lpcraft:feature/ros-plugins into lpcraft:main
jeremie has proposed merging ~artivis/lpcraft:feature/ros-plugins into lpcraft:main.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~artivis/lpcraft/+git/lpcraft/+merge/435371
This implements three new plugins for building and testing ROS & ROS 2 projects, namely,
- CatkinPlugin - based on the 'catkin' build tool
- CatkinToolsPlugin - based on the 'catkin-tools' build tool
- ColconPlugin - based on the 'colcon' build tool
These plugins all share a common base class - RosBasePlugin - factoring some common code.
The three plugins follow the same principle,
- Install the ROS project's dependencies during the 'run-before' step
- Build the ROS project during the 'run' step
- Build and run the tests during the 'run-after' step (optional)
The later is optional and depends on a plugin config parameter.
Beside some ROS-specific environment variables, the base class define the following env. vars.,
- ROS_PROJECT_BASE - the parent directory of the lpcraft build tree
- ROS_PROJECT_SRC - the directory of the project
- ROS_PROJECT_BUILD - the directory of the plugins build tree
- ROS_PROJECT_INSTALL - the directory of the plugins install tree
They somewhat mimic env. vars. in snapcraft allowing for out-of-source build/install. They also allow the end user to more easily override the run commands not having to figure out the internal paths. They could be replaced in the future by lpcraft-wide env. vars.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~artivis/lpcraft:feature/ros-plugins into lpcraft:main.
diff --git a/lpcraft/plugin/tests/test_plugins.py b/lpcraft/plugin/tests/test_plugins.py
index 1def73b..addaac0 100644
--- a/lpcraft/plugin/tests/test_plugins.py
+++ b/lpcraft/plugin/tests/test_plugins.py
@@ -5,7 +5,7 @@ import os
import subprocess
from pathlib import Path, PosixPath
from textwrap import dedent
-from typing import List, Optional, Union, cast
+from typing import Any, Dict, List, Optional, Union, cast
from unittest.mock import ANY, Mock, call, patch
from craft_providers.lxd import launch
@@ -1080,3 +1080,646 @@ class TestPlugins(CommandBaseTestCase):
lpcraft.config.Config.load,
config_path,
)
+
+ def _get_ros_before_run_call(self, expected_env: Dict[str, str]) -> Any:
+ return call(
+ [
+ "bash",
+ "--noprofile",
+ "--norc",
+ "-ec",
+ "if [ -f /opt/ros/*/setup.sh ]; then source /opt/ros/*/setup.sh; fi" # noqa:E501
+ "\nif [ ! -f /etc/ros/rosdep/sources.list.d/20-default.list ]; then\nrosdep init\nfi" # noqa:E501
+ "\nrosdep update --rosdistro ${ROS_DISTRO}"
+ "\nrosdep install -i -y --rosdistro ${ROS_DISTRO} --from-paths .", # noqa:E501
+ ],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ )
+
+ def _get_ros_packages_call(
+ self, expected_env: Dict[str, str], packages: List[str]
+ ) -> Any:
+ return call(
+ [
+ "apt",
+ "install",
+ "-y",
+ "build-essential",
+ "cmake",
+ "g++",
+ "python3-rosdep",
+ ]
+ + packages,
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ )
+
+ def _get_ros_expected_env(
+ self, ros_distro: str, ros_python_version: str, ros_version: str
+ ) -> Dict[str, str]:
+ return {
+ "ROS_PROJECT_BASE": "/build/lpcraft",
+ "ROS_PROJECT_SRC": "/build/lpcraft/project",
+ "ROS_PROJECT_BUILD": "/build/lpcraft/build",
+ "ROS_PROJECT_INSTALL": "/build/lpcraft/install",
+ "ROS_DISTRO": ros_distro,
+ "ROS_PYTHON_VERSION": ros_python_version,
+ "ROS_VERSION": ros_version,
+ }
+
+ @patch("lpcraft.commands.run.get_provider")
+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+ def test_catkin_plugin(
+ self, mock_get_host_architecture, mock_get_provider
+ ):
+ launcher = Mock(spec=launch)
+ provider = makeLXDProvider(lxd_launcher=launcher)
+ mock_get_provider.return_value = provider
+ execute_run = launcher.return_value.execute_run
+ execute_run.return_value = subprocess.CompletedProcess([], 0)
+ config = dedent(
+ """
+ pipeline:
+ - build
+
+ jobs:
+ build:
+ plugin: catkin
+ series: focal
+ architectures: amd64
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+
+ expected_env = self._get_ros_expected_env("noetic", "3", "1")
+
+ self.run_command("run")
+ self.assertEqual(
+ [
+ call(
+ ["apt", "update"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ self._get_ros_packages_call(
+ expected_env, ["ros-noetic-catkin"]
+ ),
+ self._get_ros_before_run_call(expected_env),
+ call(
+ [
+ "bash",
+ "--noprofile",
+ "--norc",
+ "-ec",
+ "if [ -f /opt/ros/*/setup.sh ]; then source /opt/ros/*/setup.sh; fi" # noqa:E501
+ "\ncatkin_make_isolated --source-space ${ROS_PROJECT_SRC} --directory ${ROS_PROJECT_BASE}", # noqa:E501
+ ],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ ],
+ execute_run.call_args_list,
+ )
+
+ @patch("lpcraft.commands.run.get_provider")
+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+ def test_catkin_plugin_with_tests(
+ self, mock_get_host_architecture, mock_get_provider
+ ):
+ launcher = Mock(spec=launch)
+ provider = makeLXDProvider(lxd_launcher=launcher)
+ mock_get_provider.return_value = provider
+ execute_run = launcher.return_value.execute_run
+ execute_run.return_value = subprocess.CompletedProcess([], 0)
+ config = dedent(
+ """
+ pipeline:
+ - build
+
+ jobs:
+ build:
+ plugin: catkin
+ run-tests: True
+ series: focal
+ architectures: amd64
+ packages: [file, git]
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+
+ expected_env = self._get_ros_expected_env("noetic", "3", "1")
+
+ self.run_command("run")
+ self.assertEqual(
+ [
+ call(
+ ["apt", "update"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ self._get_ros_packages_call(
+ expected_env, ["ros-noetic-catkin", "file", "git"]
+ ),
+ self._get_ros_before_run_call(expected_env),
+ call(
+ [
+ "bash",
+ "--noprofile",
+ "--norc",
+ "-ec",
+ "if [ -f /opt/ros/*/setup.sh ]; then source /opt/ros/*/setup.sh; fi" # noqa:E501
+ "\ncatkin_make_isolated --source-space ${ROS_PROJECT_SRC} --directory ${ROS_PROJECT_BASE}", # noqa:E501
+ ],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ call(
+ [
+ "bash",
+ "--noprofile",
+ "--norc",
+ "-ec",
+ "\nsource ${ROS_PROJECT_BASE}/devel_isolated/setup.sh"
+ "\ncatkin_make_isolated --source-space ${ROS_PROJECT_SRC} --directory ${ROS_PROJECT_BASE} --force-cmake --catkin-make-args run_tests" # noqa:E501
+ "\ncatkin_test_results --verbose ${ROS_PROJECT_BASE}/build_isolated/", # noqa:E501
+ ],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ ],
+ execute_run.call_args_list,
+ )
+
+ @patch("lpcraft.commands.run.get_provider")
+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+ def test_catkin_plugin_user_command(
+ self, mock_get_host_architecture, mock_get_provider
+ ):
+ launcher = Mock(spec=launch)
+ provider = makeLXDProvider(lxd_launcher=launcher)
+ mock_get_provider.return_value = provider
+ execute_run = launcher.return_value.execute_run
+ execute_run.return_value = subprocess.CompletedProcess([], 0)
+ config = dedent(
+ """
+ pipeline:
+ - build
+
+ jobs:
+ build:
+ plugin: catkin
+ run-tests: True
+ series: focal
+ architectures: amd64
+ packages: [file, git]
+ run-before: echo 'hello'
+ run: echo 'robot'
+ # run-after: "we shouldn't get anything unless uncommented"
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+
+ expected_env = self._get_ros_expected_env("noetic", "3", "1")
+
+ self.run_command("run")
+ self.assertEqual(
+ [
+ call(
+ ["apt", "update"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ self._get_ros_packages_call(
+ expected_env, ["ros-noetic-catkin", "file", "git"]
+ ),
+ call(
+ ["bash", "--noprofile", "--norc", "-ec", "echo 'hello'"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ call(
+ ["bash", "--noprofile", "--norc", "-ec", "echo 'robot'"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ ],
+ execute_run.call_args_list,
+ )
+
+ @patch("lpcraft.commands.run.get_provider")
+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+ def test_catkin_tools_plugin(
+ self, mock_get_host_architecture, mock_get_provider
+ ):
+ launcher = Mock(spec=launch)
+ provider = makeLXDProvider(lxd_launcher=launcher)
+ mock_get_provider.return_value = provider
+ execute_run = launcher.return_value.execute_run
+ execute_run.return_value = subprocess.CompletedProcess([], 0)
+ config = dedent(
+ """
+ pipeline:
+ - build
+
+ jobs:
+ build:
+ plugin: catkin-tools
+ series: focal
+ architectures: amd64
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+
+ expected_env = self._get_ros_expected_env("noetic", "3", "1")
+
+ self.run_command("run")
+ self.assertEqual(
+ [
+ call(
+ ["apt", "update"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ self._get_ros_packages_call(
+ expected_env, ["python3-catkin-tools"]
+ ),
+ self._get_ros_before_run_call(expected_env),
+ call(
+ [
+ "bash",
+ "--noprofile",
+ "--norc",
+ "-ec",
+ "if [ -f /opt/ros/*/setup.sh ]; then source /opt/ros/*/setup.sh; fi\n" # noqa:E501
+ "catkin init --workspace ${ROS_PROJECT_BASE}\n"
+ "catkin profile add --force lpcraft\n"
+ "catkin config --profile lpcraft --install --source-space ${ROS_PROJECT_SRC} --build-space ${ROS_PROJECT_BUILD} --install-space ${ROS_PROJECT_INSTALL}\n" # noqa:E501
+ "catkin build --no-notify --profile lpcraft", # noqa:E501
+ ],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ ],
+ execute_run.call_args_list,
+ )
+
+ @patch("lpcraft.commands.run.get_provider")
+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+ def test_catkin_tools_plugin_with_tests(
+ self, mock_get_host_architecture, mock_get_provider
+ ):
+ launcher = Mock(spec=launch)
+ provider = makeLXDProvider(lxd_launcher=launcher)
+ mock_get_provider.return_value = provider
+ execute_run = launcher.return_value.execute_run
+ execute_run.return_value = subprocess.CompletedProcess([], 0)
+ config = dedent(
+ """
+ pipeline:
+ - build
+
+ jobs:
+ build:
+ plugin: catkin-tools
+ run-tests: True
+ series: focal
+ architectures: amd64
+ packages: [file, git]
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+
+ expected_env = self._get_ros_expected_env("noetic", "3", "1")
+
+ self.run_command("run")
+ self.assertEqual(
+ [
+ call(
+ ["apt", "update"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ self._get_ros_packages_call(
+ expected_env, ["python3-catkin-tools", "file", "git"]
+ ),
+ self._get_ros_before_run_call(expected_env),
+ call(
+ [
+ "bash",
+ "--noprofile",
+ "--norc",
+ "-ec",
+ "if [ -f /opt/ros/*/setup.sh ]; then source /opt/ros/*/setup.sh; fi\n" # noqa:E501
+ "catkin init --workspace ${ROS_PROJECT_BASE}\n"
+ "catkin profile add --force lpcraft\n"
+ "catkin config --profile lpcraft --install --source-space ${ROS_PROJECT_SRC} --build-space ${ROS_PROJECT_BUILD} --install-space ${ROS_PROJECT_INSTALL}\n" # noqa:E501
+ "catkin build --no-notify --profile lpcraft", # noqa:E501
+ ],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ call(
+ [
+ "bash",
+ "--noprofile",
+ "--norc",
+ "-ec",
+ "\nsource ${ROS_PROJECT_BASE}/devel/setup.sh\n"
+ "catkin test --profile lpcraft --summary",
+ ],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ ],
+ execute_run.call_args_list,
+ )
+
+ @patch("lpcraft.commands.run.get_provider")
+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+ def test_catkin_tools_plugin_user_command(
+ self, mock_get_host_architecture, mock_get_provider
+ ):
+ launcher = Mock(spec=launch)
+ provider = makeLXDProvider(lxd_launcher=launcher)
+ mock_get_provider.return_value = provider
+ execute_run = launcher.return_value.execute_run
+ execute_run.return_value = subprocess.CompletedProcess([], 0)
+ config = dedent(
+ """
+ pipeline:
+ - build
+
+ jobs:
+ build:
+ plugin: catkin-tools
+ run-tests: True
+ series: focal
+ architectures: amd64
+ packages: [file, git]
+ run-before: echo 'hello'
+ run: echo 'robot'
+ # run-after: "we shouldn't get anything unless uncommented"
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+
+ expected_env = self._get_ros_expected_env("noetic", "3", "1")
+
+ self.run_command("run")
+ self.assertEqual(
+ [
+ call(
+ ["apt", "update"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ self._get_ros_packages_call(
+ expected_env, ["python3-catkin-tools", "file", "git"]
+ ),
+ call(
+ ["bash", "--noprofile", "--norc", "-ec", "echo 'hello'"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ call(
+ ["bash", "--noprofile", "--norc", "-ec", "echo 'robot'"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ ],
+ execute_run.call_args_list,
+ )
+
+ @patch("lpcraft.commands.run.get_provider")
+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+ def test_colcon_plugin(
+ self, mock_get_host_architecture, mock_get_provider
+ ):
+ launcher = Mock(spec=launch)
+ provider = makeLXDProvider(lxd_launcher=launcher)
+ mock_get_provider.return_value = provider
+ execute_run = launcher.return_value.execute_run
+ execute_run.return_value = subprocess.CompletedProcess([], 0)
+ config = dedent(
+ """
+ pipeline:
+ - build
+
+ jobs:
+ build:
+ plugin: colcon
+ series: focal
+ architectures: amd64
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+
+ expected_env = self._get_ros_expected_env("foxy", "3", "2")
+
+ self.run_command("run")
+ self.assertEqual(
+ [
+ call(
+ ["apt", "update"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ self._get_ros_packages_call(
+ expected_env, ["python3-colcon-common-extensions"]
+ ),
+ self._get_ros_before_run_call(expected_env),
+ call(
+ [
+ "bash",
+ "--noprofile",
+ "--norc",
+ "-ec",
+ "if [ -f /opt/ros/*/setup.sh ]; then source /opt/ros/*/setup.sh; fi\n" # noqa:E501
+ "colcon build --base-paths ${ROS_PROJECT_SRC} --build-base ${ROS_PROJECT_BUILD} --install-base ${ROS_PROJECT_INSTALL} --event-handlers console_direct+", # noqa:E501
+ ],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ ],
+ execute_run.call_args_list,
+ )
+
+ @patch("lpcraft.commands.run.get_provider")
+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+ def test_colcon_plugin_with_tests(
+ self, mock_get_host_architecture, mock_get_provider
+ ):
+ launcher = Mock(spec=launch)
+ provider = makeLXDProvider(lxd_launcher=launcher)
+ mock_get_provider.return_value = provider
+ execute_run = launcher.return_value.execute_run
+ execute_run.return_value = subprocess.CompletedProcess([], 0)
+ config = dedent(
+ """
+ pipeline:
+ - build
+
+ jobs:
+ build:
+ plugin: colcon
+ run-tests: True
+ series: focal
+ architectures: amd64
+ packages: [file, git]
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+
+ expected_env = self._get_ros_expected_env("foxy", "3", "2")
+
+ self.run_command("run")
+ self.assertEqual(
+ [
+ call(
+ ["apt", "update"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ self._get_ros_packages_call(
+ expected_env,
+ ["python3-colcon-common-extensions", "file", "git"],
+ ),
+ self._get_ros_before_run_call(expected_env),
+ call(
+ [
+ "bash",
+ "--noprofile",
+ "--norc",
+ "-ec",
+ "if [ -f /opt/ros/*/setup.sh ]; then source /opt/ros/*/setup.sh; fi\n" # noqa:E501
+ "colcon build --base-paths ${ROS_PROJECT_SRC} --build-base ${ROS_PROJECT_BUILD} --install-base ${ROS_PROJECT_INSTALL} --event-handlers console_direct+", # noqa:E501
+ ],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ call(
+ [
+ "bash",
+ "--noprofile",
+ "--norc",
+ "-ec",
+ "\nsource ${ROS_PROJECT_INSTALL}/setup.sh\n"
+ "colcon test --base-paths ${ROS_PROJECT_SRC} --build-base ${ROS_PROJECT_BUILD} --install-base ${ROS_PROJECT_INSTALL} --event-handlers console_direct+\n" # noqa:E501
+ "colcon test-result --all --verbose --test-result-base ${ROS_PROJECT_BUILD}", # noqa:E501
+ ],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ ],
+ execute_run.call_args_list,
+ )
+
+ @patch("lpcraft.commands.run.get_provider")
+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+ def test_colcon_plugin_user_command(
+ self, mock_get_host_architecture, mock_get_provider
+ ):
+ launcher = Mock(spec=launch)
+ provider = makeLXDProvider(lxd_launcher=launcher)
+ mock_get_provider.return_value = provider
+ execute_run = launcher.return_value.execute_run
+ execute_run.return_value = subprocess.CompletedProcess([], 0)
+ config = dedent(
+ """
+ pipeline:
+ - build
+
+ jobs:
+ build:
+ plugin: colcon
+ run-tests: True
+ series: focal
+ architectures: amd64
+ packages: [file, git]
+ run-before: echo 'hello'
+ run: echo 'robot'
+ # run-after: "we shouldn't get anything unless uncommented"
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+
+ expected_env = self._get_ros_expected_env("foxy", "3", "2")
+
+ self.run_command("run")
+ self.assertEqual(
+ [
+ call(
+ ["apt", "update"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ self._get_ros_packages_call(
+ expected_env,
+ ["python3-colcon-common-extensions", "file", "git"],
+ ),
+ call(
+ ["bash", "--noprofile", "--norc", "-ec", "echo 'hello'"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ call(
+ ["bash", "--noprofile", "--norc", "-ec", "echo 'robot'"],
+ cwd=PosixPath("/build/lpcraft/project"),
+ env=expected_env,
+ stdout=ANY,
+ stderr=ANY,
+ ),
+ ],
+ execute_run.call_args_list,
+ )
diff --git a/lpcraft/plugins/plugins.py b/lpcraft/plugins/plugins.py
index d4464ee..be140fc 100644
--- a/lpcraft/plugins/plugins.py
+++ b/lpcraft/plugins/plugins.py
@@ -9,6 +9,9 @@ __all__ = [
"MiniCondaPlugin",
"CondaBuildPlugin",
"GolangPlugin",
+ "CatkinPlugin",
+ "CatkinToolsPlugin",
+ "ColconPlugin",
]
import textwrap
@@ -18,6 +21,7 @@ from typing import TYPE_CHECKING, ClassVar, Dict, List, Optional, cast
import pydantic
from pydantic import StrictStr
+from lpcraft.env import get_managed_environment_project_path
from lpcraft.plugin import hookimpl
from lpcraft.plugins import register
@@ -451,3 +455,275 @@ class GolangPlugin(BasePlugin):
export PATH=/usr/lib/go-{version}/bin/:$PATH
{run_command}"""
)
+
+
+class RosBasePlugin(BasePlugin):
+ """A base class for ROS-related plugins."""
+
+ _MAP_SERIES_TO_ROSDISTRO = {
+ "xenial": "kinetic",
+ "bionic": "melodic",
+ "focal": "noetic",
+ }
+
+ _MAP_SERIES_TO_ROS2DISTRO = {
+ "bionic": "dashing",
+ "focal": "foxy",
+ "jammy": "humble",
+ }
+
+ class Config(BaseConfig):
+ run_tests: Optional[bool]
+
+ def get_plugin_config(self) -> "RosBasePlugin.Config":
+ return cast(RosBasePlugin.Config, self.config.plugin_config)
+
+ def _get_ros_version(self) -> int:
+ return 0
+
+ def _get_ros_distro(self) -> str:
+ """Get the ROS distro associated to the Ubuntu series in use."""
+ if self._get_ros_version() == 1:
+ return self._MAP_SERIES_TO_ROSDISTRO[self.config.series]
+ elif self._get_ros_version() == 2:
+ return self._MAP_SERIES_TO_ROS2DISTRO[self.config.series]
+ else:
+ raise Exception("Unknown ROS version.")
+
+ def _get_project_path(self) -> Path:
+ """Get the project path."""
+ return get_managed_environment_project_path()
+
+ def _get_build_path(self) -> Path:
+ """Get the out-of-source build path."""
+ return get_managed_environment_project_path().parent / "build"
+
+ def _get_install_path(self) -> Path:
+ """Get the out-of-source install path."""
+ return get_managed_environment_project_path().parent / "install"
+
+ def _get_ros_workspace_activation(self) -> str:
+ """Get the ROS system workspace activation command."""
+ # There should be only one ROS distro installed at any time
+ # therefore let's make use of the wildcard
+ return "if [ -f /opt/ros/*/setup.sh ]; then source /opt/ros/*/setup.sh; fi" # noqa:E501
+
+ @hookimpl # type: ignore
+ def lpcraft_set_environment(self) -> dict[str, str | None]:
+ ros_distro = self._get_ros_distro()
+ python_version = (
+ "3" if ros_distro not in ["kinetic", "melodic"] else "2"
+ )
+ return {
+ "ROS_DISTRO": ros_distro,
+ "ROS_PROJECT_BASE": str(self._get_project_path().parent),
+ "ROS_PROJECT_SRC": str(self._get_project_path()),
+ "ROS_PROJECT_BUILD": str(self._get_build_path()),
+ "ROS_PROJECT_INSTALL": str(self._get_install_path()),
+ "ROS_PYTHON_VERSION": python_version,
+ "ROS_VERSION": f"{self._get_ros_version()}",
+ }
+
+ @hookimpl # type: ignore
+ def lpcraft_install_packages(self) -> list[str]:
+ return ["build-essential", "cmake", "g++", "python3-rosdep"]
+
+ @hookimpl # type: ignore
+ def lpcraft_execute_before_run(self) -> str:
+ return self._get_ros_workspace_activation() + textwrap.dedent(
+ """
+ if [ ! -f /etc/ros/rosdep/sources.list.d/20-default.list ]; then
+ rosdep init
+ fi
+ rosdep update --rosdistro ${ROS_DISTRO}
+ rosdep install -i -y --rosdistro ${ROS_DISTRO} --from-paths ."""
+ )
+
+
+@register(name="catkin")
+class CatkinPlugin(RosBasePlugin):
+ """Installs ROS dependencies, builds and (optionally) tests with catkin.
+
+ Usage:
+ In `.launchpad.yaml`, create the following structure.
+
+ .. code-block:: yaml
+
+ pipeline:
+ - build
+
+ jobs:
+ build:
+ plugin: catkin
+ run-tests: True # optional
+ series: focal
+ architectures: amd64
+ packages: [file, git]
+ package-repositories:
+ - components: [main]
+ formats: [deb]
+ suites: [focal]
+ type: apt
+ url: http://packages.ros.org/ros/ubuntu
+ trusted: True
+
+ Please note that the ROS repository must be
+ set up in `package-repositories`.
+ """
+
+ def _get_ros_version(self) -> int:
+ return 1
+
+ @hookimpl # type: ignore
+ def lpcraft_install_packages(self) -> list[str]:
+ # XXX artivis 2022-12-9: mypy is struggling with the super() call
+ packages: list[str] = super().lpcraft_install_packages() + [
+ # "ros-${ROS_DISTRO}-catkin"
+ f"ros-{self._get_ros_distro()}-catkin"
+ ]
+ return packages
+
+ @hookimpl # type: ignore
+ def lpcraft_execute_run(self) -> str:
+ return self._get_ros_workspace_activation() + textwrap.dedent(
+ """
+ catkin_make_isolated --source-space ${ROS_PROJECT_SRC} --directory ${ROS_PROJECT_BASE}""" # noqa:E501
+ )
+
+ @hookimpl # type: ignore
+ def lpcraft_execute_after_run(self) -> str:
+ if not self.get_plugin_config().run_tests or self.config.run:
+ return ""
+
+ return textwrap.dedent(
+ """
+ source ${ROS_PROJECT_BASE}/devel_isolated/setup.sh
+ catkin_make_isolated --source-space ${ROS_PROJECT_SRC} --directory ${ROS_PROJECT_BASE} --force-cmake --catkin-make-args run_tests
+ catkin_test_results --verbose ${ROS_PROJECT_BASE}/build_isolated/""" # noqa:E501
+ )
+
+
+@register(name="catkin-tools")
+class CatkinToolsPlugin(RosBasePlugin):
+ """Installs ROS dependencies, builds and (optionally) tests with catkin-tools.
+
+ Usage:
+ In `.launchpad.yaml`, create the following structure.
+
+ .. code-block:: yaml
+
+ pipeline:
+ - build
+
+ jobs:
+ build:
+ plugin: catkin-tools
+ run-tests: True # optional
+ series: focal
+ architectures: amd64
+ packages: [file, git]
+ package-repositories:
+ - components: [main]
+ formats: [deb]
+ suites: [focal]
+ type: apt
+ url: http://packages.ros.org/ros/ubuntu
+ trusted: True
+
+ Please note that the ROS repository must be
+ set up in `package-repositories`.
+ """
+
+ def _get_ros_version(self) -> int:
+ return 1
+
+ @hookimpl # type: ignore
+ def lpcraft_install_packages(self) -> list[str]:
+ # XXX artivis 2022-12-9: mypy is struggling with the super() call
+ packages: list[str] = super().lpcraft_install_packages() + [
+ "python3-catkin-tools"
+ ]
+ return packages
+
+ @hookimpl # type: ignore
+ def lpcraft_execute_run(self) -> str:
+ return self._get_ros_workspace_activation() + textwrap.dedent(
+ """
+ catkin init --workspace ${ROS_PROJECT_BASE}
+ catkin profile add --force lpcraft
+ catkin config --profile lpcraft --install --source-space ${ROS_PROJECT_SRC} --build-space ${ROS_PROJECT_BUILD} --install-space ${ROS_PROJECT_INSTALL}
+ catkin build --no-notify --profile lpcraft""" # noqa:E501
+ )
+
+ @hookimpl # type: ignore
+ def lpcraft_execute_after_run(self) -> str:
+ if not self.get_plugin_config().run_tests or self.config.run:
+ return ""
+
+ return textwrap.dedent(
+ """
+ source ${ROS_PROJECT_BASE}/devel/setup.sh
+ catkin test --profile lpcraft --summary"""
+ )
+
+
+@register(name="colcon")
+class ColconPlugin(RosBasePlugin):
+ """Installs ROS dependencies, builds and (optionally) tests with colcon.
+
+ Usage:
+ In `.launchpad.yaml`, create the following structure.
+
+ .. code-block:: yaml
+
+ pipeline:
+ - build
+
+ jobs:
+ build:
+ plugin: colcon
+ run-tests: True # optional
+ series: focal
+ architectures: amd64
+ packages: [file, git]
+ package-repositories:
+ - components: [main]
+ formats: [deb]
+ suites: [focal]
+ type: apt
+ url: http://repo.ros2.org/ubuntu/main/
+ trusted: True
+
+ Please note that the ROS 2 repository must be
+ set up in `package-repositories`.
+ """
+
+ def _get_ros_version(self) -> int:
+ return 2
+
+ @hookimpl # type: ignore
+ def lpcraft_install_packages(self) -> list[str]:
+ # XXX artivis 2022-12-9: mypy is struggling with the super() call
+ packages: list[str] = super().lpcraft_install_packages() + [
+ "python3-colcon-common-extensions"
+ ]
+ return packages
+
+ @hookimpl # type: ignore
+ def lpcraft_execute_run(self) -> str:
+ return self._get_ros_workspace_activation() + textwrap.dedent(
+ """
+ colcon build --base-paths ${ROS_PROJECT_SRC} --build-base ${ROS_PROJECT_BUILD} --install-base ${ROS_PROJECT_INSTALL} --event-handlers console_direct+""" # noqa:E501
+ )
+
+ @hookimpl # type: ignore
+ def lpcraft_execute_after_run(self) -> str:
+ if not self.get_plugin_config().run_tests or self.config.run:
+ return ""
+
+ return textwrap.dedent(
+ """
+ source ${ROS_PROJECT_INSTALL}/setup.sh
+ colcon test --base-paths ${ROS_PROJECT_SRC} --build-base ${ROS_PROJECT_BUILD} --install-base ${ROS_PROJECT_INSTALL} --event-handlers console_direct+
+ colcon test-result --all --verbose --test-result-base ${ROS_PROJECT_BUILD}""" # noqa:E501
+ )
Follow ups