Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ROS 2 Jazzy content-sharing support #4828

Merged
merged 5 commits into from
Jun 6, 2024
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
119 changes: 119 additions & 0 deletions snapcraft/extensions/_ros2_jazzy_meta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

# Import types and tell flake8 to ignore the "unused" List.

"""Base for ROS 2 Jazzy extensions to the Colcon plugin using content-sharing."""

import dataclasses
from abc import abstractmethod
from typing import Any, Dict, Optional

from overrides import overrides

from .ros2_jazzy import ROS2JazzyExtension


@dataclasses.dataclass
class ROS2JazzySnaps:
"""A structure of ROS 2 Jazzy related snaps."""

sdk: str
content: str
variant: str


class ROS2JazzyMetaBase(ROS2JazzyExtension):
"""Drives ROS 2 build and runtime environment for snap using content-sharing."""

@property
@abstractmethod
def ros2_jazzy_snaps(self) -> ROS2JazzySnaps:
"""Return the ROS 2 Jazzy related snaps to use to construct the environment."""
raise NotImplementedError

@staticmethod
@overrides
def is_experimental(base: Optional[str]) -> bool:
return True

@overrides
def get_root_snippet(self) -> Dict[str, Any]:
root_snippet = super().get_root_snippet()
root_snippet["plugs"] = {
self.ros2_jazzy_snaps.content: {
"interface": "content",
"content": self.ros2_jazzy_snaps.content,
"target": "$SNAP/opt/ros/underlay_ws",
"default-provider": self.ros2_jazzy_snaps.content,
}
}
return root_snippet

@overrides
def get_app_snippet(self) -> Dict[str, Any]:
app_snippet = super().get_app_snippet()
python_paths = app_snippet["environment"]["PYTHONPATH"]
new_python_paths = [
f"$SNAP/opt/ros/underlay_ws/opt/ros/{self.ROS_DISTRO}/lib/python3.12/site-packages",
"$SNAP/opt/ros/underlay_ws/usr/lib/python3/dist-packages",
]

app_snippet["environment"][
"PYTHONPATH"
] = f'{python_paths}:{":".join(new_python_paths)}'

return app_snippet

@overrides
def get_part_snippet(self, *, plugin_name: str) -> Dict[str, Any]:
part_snippet = super().get_part_snippet(plugin_name=plugin_name)

# These are colcon-plugin specific entries
if plugin_name == "colcon":
part_snippet["colcon-ros-build-snaps"] = [self.ros2_jazzy_snaps.sdk]
part_snippet["colcon-cmake-args"] = [
f'-DCMAKE_SYSTEM_PREFIX_PATH="/snap/{self.ros2_jazzy_snaps.sdk}/current/usr"'
]

return part_snippet

@overrides
def get_parts_snippet(self) -> Dict[str, Any]:
parts_snippet = super().get_parts_snippet()
# Very unlikely but it may happen that the snapped application doesn't
# even pull those deps. In that case, there is no valid ROS 2 ws to source.
# We make sure here that they are staged no matter what.
parts_snippet[f"ros2-{self.ROS_DISTRO}/ros2-launch"]["stage-packages"] = [
f"ros-{self.ROS_DISTRO}-ros-environment",
f"ros-{self.ROS_DISTRO}-ros-workspace",
f"ros-{self.ROS_DISTRO}-ament-index-cpp",
f"ros-{self.ROS_DISTRO}-ament-index-python",
]

# Something in the ROS 2 build chain requires to find this lib during cmake call,
# however its cmake files ship with the '-dev' package.
parts_snippet[f"ros2-{self.ROS_DISTRO}/ros2-launch"]["build-packages"].append(
"libpython3.12-dev"
)

# The part name must follow the format <extension-name>/<part-name>
parts_snippet[
f"ros2-{self.ROS_DISTRO}-{self.ros2_jazzy_snaps.variant}/ros2-launch"
] = parts_snippet[f"ros2-{self.ROS_DISTRO}/ros2-launch"]
parts_snippet.pop(f"ros2-{self.ROS_DISTRO}/ros2-launch")

return parts_snippet
6 changes: 6 additions & 0 deletions snapcraft/extensions/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
from .ros2_humble_ros_base import ROS2HumbleRosBaseExtension
from .ros2_humble_ros_core import ROS2HumbleRosCoreExtension
from .ros2_jazzy import ROS2JazzyExtension
from .ros2_jazzy_desktop import ROS2JazzyDesktopExtension
from .ros2_jazzy_ros_base import ROS2JazzyRosBaseExtension
from .ros2_jazzy_ros_core import ROS2JazzyRosCoreExtension

if TYPE_CHECKING:
from .extension import Extension
Expand All @@ -41,6 +44,9 @@
"ros2-humble-ros-base": ROS2HumbleRosBaseExtension,
"ros2-humble-desktop": ROS2HumbleDesktopExtension,
"ros2-jazzy": ROS2JazzyExtension,
"ros2-jazzy-ros-core": ROS2JazzyRosCoreExtension,
"ros2-jazzy-ros-base": ROS2JazzyRosBaseExtension,
"ros2-jazzy-desktop": ROS2JazzyDesktopExtension,
"kde-neon": KDENeon,
"kde-neon-6": KDENeon6,
}
Expand Down
2 changes: 0 additions & 2 deletions snapcraft/extensions/ros2_jazzy.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

# Import types and tell flake8 to ignore the "unused" List.

"""Extension to the Colcon plugin for ROS 2 Jazzy."""

from typing import Any, Dict, Optional, Tuple
Expand Down
38 changes: 38 additions & 0 deletions snapcraft/extensions/ros2_jazzy_desktop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Extension to the Colcon plugin for ROS 2 Jazzy using content sharing."""

import functools

from overrides import overrides

from ._ros2_jazzy_meta import ROS2JazzyMetaBase, ROS2JazzySnaps


class ROS2JazzyDesktopExtension(ROS2JazzyMetaBase):
"""Drives ROS 2 build and runtime environment for snap using content-sharing."""

@functools.cached_property
@overrides
def ros2_jazzy_snaps( # type: ignore[reportIncompatibleMethodOverride]
self,
) -> ROS2JazzySnaps:
return ROS2JazzySnaps(
sdk="ros-jazzy-desktop-dev",
content="ros-jazzy-desktop",
variant="desktop",
)
38 changes: 38 additions & 0 deletions snapcraft/extensions/ros2_jazzy_ros_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Extension to the Colcon plugin for ROS 2 Jazzy using content sharing."""

import functools

from overrides import overrides

from ._ros2_jazzy_meta import ROS2JazzyMetaBase, ROS2JazzySnaps


class ROS2JazzyRosBaseExtension(ROS2JazzyMetaBase):
"""Drives ROS 2 build and runtime environment for snap using content-sharing."""

@functools.cached_property
@overrides
def ros2_jazzy_snaps( # type: ignore[reportIncompatibleMethodOverride]
self,
) -> ROS2JazzySnaps:
return ROS2JazzySnaps(
sdk="ros-jazzy-ros-base-dev",
content="ros-jazzy-ros-base",
variant="ros-base",
)
38 changes: 38 additions & 0 deletions snapcraft/extensions/ros2_jazzy_ros_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Extension to the Colcon plugin for ROS 2 Jazzy using content sharing."""

import functools

from overrides import overrides

from ._ros2_jazzy_meta import ROS2JazzyMetaBase, ROS2JazzySnaps


class ROS2JazzyRosCoreExtension(ROS2JazzyMetaBase):
"""Drives ROS 2 build and runtime environment for snap using content-sharing."""

@functools.cached_property
@overrides
def ros2_jazzy_snaps( # type: ignore[reportIncompatibleMethodOverride]
self,
) -> ROS2JazzySnaps:
return ROS2JazzySnaps(
sdk="ros-jazzy-ros-core-dev",
content="ros-jazzy-ros-core",
variant="ros-core",
)
21 changes: 14 additions & 7 deletions snapcraft/parts/plugins/_ros.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,23 +162,30 @@ def _get_list_packages_commands(self) -> List[str]:

for ros_build_snap in self._options.colcon_ros_build_snaps: # type: ignore
snap_name = _get_parsed_snap(ros_build_snap)[0]
path = f"/snap/{snap_name}/current/opt/ros"
base_path = f"/snap/{snap_name}/current/opt/ros"
path_ros_sys = f"{base_path}/${{ROS_DISTRO}}/"
path_ros_app = f"{base_path}/snap/"
# ros2 pkg does not crawl sub-folders
if base == "core22":
search_path = base_path
else:
search_path = f"{path_ros_sys}:{path_ros_app}"
# pylint: disable=line-too-long
cmd.extend(
[
# Retrieve the list of all ROS packages available in the build snap
f"if [ -d {path} ]; then",
f"{rospkg_search_env}={path} "
f"if [ -d {base_path} ]; then",
f"{rospkg_search_env}={search_path} "
f'{rospkg_cmd} | (xargs rosdep resolve --rosdistro "${{ROS_DISTRO}}" || echo "") | '
'awk "/#apt/{getline;print;}" >> "${CRAFT_PART_INSTALL}/.installed_packages.txt"',
"fi",
# Retrieve the list of all non-ROS packages available in the build snap
f'if [ -d "{path}/${{ROS_DISTRO}}/" ]; then',
f'rosdep keys --rosdistro "${{ROS_DISTRO}}" --from-paths "{path}/${{ROS_DISTRO}}" --ignore-packages-from-source '
f'if [ -d "{path_ros_sys}" ]; then',
f'rosdep keys --rosdistro "${{ROS_DISTRO}}" --from-paths "{path_ros_sys}" --ignore-packages-from-source '
'| (xargs rosdep resolve --rosdistro "${ROS_DISTRO}" || echo "") | grep -v "#" >> "${CRAFT_PART_INSTALL}"/.installed_packages.txt',
"fi",
f'if [ -d "{path}/snap/" ]; then',
f'rosdep keys --rosdistro "${{ROS_DISTRO}}" --from-paths "{path}/snap" --ignore-packages-from-source '
f'if [ -d "{path_ros_app}" ]; then',
f'rosdep keys --rosdistro "${{ROS_DISTRO}}" --from-paths "{path_ros_app}" --ignore-packages-from-source '
'| (xargs rosdep resolve --rosdistro "${ROS_DISTRO}" || echo "") | grep -v "#" >> "${CRAFT_PART_INSTALL}"/.installed_packages.txt',
"fi",
]
Expand Down
83 changes: 83 additions & 0 deletions tests/spread/extensions/ros2-jazzy-meta-hello/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
summary: Build and run a basic ROS 2 snap using meta-ros extension

kill-timeout: 180m

environment:

SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "1"

SNAP: ros2-jazzy-hello
SNAP_DIR: "../snaps/${SNAP}"

META_SNAP/colcon_jazzy_ros_core: ros-jazzy-ros-core
EXTENSION/colcon_jazzy_ros_core: ros2-jazzy-ros-core
INTERFACE/colcon_jazzy_ros_core: ros-jazzy-ros-core

META_SNAP/colcon_jazzy_ros_base: ros-jazzy-ros-base
EXTENSION/colcon_jazzy_ros_base: ros2-jazzy-ros-base
INTERFACE/colcon_jazzy_ros_base: ros-jazzy-ros-base

META_SNAP/colcon_jazzy_desktop: ros-jazzy-desktop
EXTENSION/colcon_jazzy_desktop: ros2-jazzy-desktop
INTERFACE/colcon_jazzy_desktop: ros-jazzy-desktop

# The content snap required for the test to succeed is only
# available on a subset of all the architectures this testbed
# can run on.
systems:
- ubuntu-24.04
- ubuntu-24.04-64
- ubuntu-24.04-amd64
# - ubuntu-24.04-arm64
mr-cal marked this conversation as resolved.
Show resolved Hide resolved

prepare: |
#shellcheck source=tests/spread/tools/snapcraft-yaml.sh
. "$TOOLS_DIR/snapcraft-yaml.sh"
set_base "$SNAP_DIR/snap/snapcraft.yaml"

# Overwrite the extension to test them all out of a single snap
sed -i "s|\[ros2-jazzy\]|\[${EXTENSION}\]|" "$SNAP_DIR/snap/snapcraft.yaml"

# The snap stages ros2run which will be available through content-sharing
sed -i "\|stage-packages|d" "$SNAP_DIR/snap/snapcraft.yaml"

# The actual ros2 bin is provided by the content-sharing snap.
# Use the wrapper instead.
sed -i "s|opt/ros/jazzy/bin/ros2|ros2|" "$SNAP_DIR/snap/snapcraft.yaml"

#shellcheck source=tests/spread/tools/package-utils.sh
. "$TOOLS_DIR/package-utils.sh"
create_dpkg_restore_point

restore: |
cd "$SNAP_DIR"
snapcraft clean
rm -f ./*.snap

#shellcheck source=tests/spread/tools/snapcraft-yaml.sh
. "$TOOLS_DIR/snapcraft-yaml.sh"
restore_yaml "snap/snapcraft.yaml"

#shellcheck source=tests/spread/tools/package-utils.sh
. "$TOOLS_DIR/package-utils.sh"
dpkg_restore_point

snap remove --purge "${META_SNAP}"

execute: |
cd "$SNAP_DIR"

# Build what we have and verify the snap runs as expected.
snapcraft
snap install "${SNAP}"_1.0_*.snap --dangerous

# Check that the snap size is fairly small
# The non-content sharing snap is ~90M
SNAP_SIZE=$(find . -maxdepth 1 -mindepth 1 -name '*_1.0_*.snap' -exec ls -s {} + | cut -d " " -f1)
[ "200" -gt "$SNAP_SIZE" ]

# The default providing snap is installed automatically
# snap install "${META_SNAP}"

snap connect "${SNAP}:${INTERFACE}" "${META_SNAP}:${INTERFACE}"
[ "$($SNAP)" = "hello world" ]
Loading
Loading