ros2docker 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ros2docker/config.py ADDED
@@ -0,0 +1,316 @@
1
+ """Configuration loading and path resolution for ros2docker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import json
7
+ import os
8
+ from collections.abc import Mapping, Sequence
9
+ from functools import lru_cache
10
+ from importlib import resources
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+
15
+ class ConfigError(ValueError):
16
+ """Raised when a ros2docker config is invalid."""
17
+
18
+
19
+ DEFAULT_CONFIG: dict[str, Any] = {
20
+ "container_name": "ros2docker",
21
+ "image_name": "ros2docker",
22
+ "run_type": "bash",
23
+ "mount_ws": False,
24
+ "enable_gui_forwarding": False,
25
+ "forward_ssh_agent": False,
26
+ "run_args": [],
27
+ "extra_run_args": [],
28
+ "build_args": {},
29
+ "bake_ros_packages": [],
30
+ }
31
+
32
+ VALID_RUN_TYPES = {"bash", "catmux", "up", "command"}
33
+
34
+
35
+ @lru_cache
36
+ def public_config_keys() -> frozenset[str]:
37
+ """Return the supported top-level ros2docker config keys."""
38
+
39
+ schema_text = (
40
+ resources.files("ros2docker")
41
+ .joinpath("resources")
42
+ .joinpath("schema")
43
+ .joinpath("ros2docker.schema.json")
44
+ .read_text(encoding="utf-8")
45
+ )
46
+ schema = json.loads(schema_text)
47
+ properties = schema.get("properties", {})
48
+ if not isinstance(properties, dict):
49
+ raise ConfigError("ros2docker config schema must define object properties.")
50
+ return frozenset(str(key) for key in properties)
51
+
52
+
53
+ def strip_json_comments(text: str) -> str:
54
+ """Remove C/C++ style comments while preserving string literals."""
55
+
56
+ output: list[str] = []
57
+ in_string = False
58
+ escape = False
59
+ i = 0
60
+
61
+ while i < len(text):
62
+ char = text[i]
63
+ next_char = text[i + 1] if i + 1 < len(text) else ""
64
+
65
+ if in_string:
66
+ output.append(char)
67
+ if escape:
68
+ escape = False
69
+ elif char == "\\":
70
+ escape = True
71
+ elif char == '"':
72
+ in_string = False
73
+ i += 1
74
+ continue
75
+
76
+ if char == '"':
77
+ in_string = True
78
+ output.append(char)
79
+ i += 1
80
+ continue
81
+
82
+ if char == "/" and next_char == "/":
83
+ i += 2
84
+ while i < len(text) and text[i] not in "\r\n":
85
+ i += 1
86
+ continue
87
+
88
+ if char == "/" and next_char == "*":
89
+ i += 2
90
+ while i + 1 < len(text) and not (text[i] == "*" and text[i + 1] == "/"):
91
+ i += 1
92
+ i += 2
93
+ continue
94
+
95
+ output.append(char)
96
+ i += 1
97
+
98
+ return "".join(output)
99
+
100
+
101
+ def parse_override(override: str | Mapping[str, Any] | None) -> dict[str, Any]:
102
+ if override is None:
103
+ return {}
104
+ if isinstance(override, str):
105
+ try:
106
+ parsed = json.loads(strip_json_comments(override))
107
+ except json.JSONDecodeError as exc:
108
+ raise ConfigError(f"Invalid JSON override: {exc}") from exc
109
+ elif isinstance(override, Mapping):
110
+ parsed = dict(override)
111
+ else:
112
+ raise ConfigError(f"Override must be JSON text or a mapping, got {type(override).__name__}.")
113
+
114
+ if not isinstance(parsed, dict):
115
+ raise ConfigError("Override must decode to a JSON object.")
116
+ return parsed
117
+
118
+
119
+ def get_config_dir(config_file: str | os.PathLike[str] | None = None) -> str:
120
+ if config_file is None:
121
+ return os.getcwd()
122
+ return str(_resolve_config_file(config_file).parent)
123
+
124
+
125
+ def load_config(
126
+ config_file: str | os.PathLike[str] | None = None,
127
+ override: str | Mapping[str, Any] | None = None,
128
+ *,
129
+ resolve_run_args: bool = True,
130
+ ) -> dict[str, Any]:
131
+ config = copy.deepcopy(DEFAULT_CONFIG)
132
+ config_dir = Path.cwd()
133
+
134
+ if config_file is not None:
135
+ config_path = _resolve_config_file(config_file)
136
+ if not config_path.is_file():
137
+ raise FileNotFoundError(f"Config file not found: {config_path}")
138
+ config_dir = config_path.parent
139
+ try:
140
+ loaded = json.loads(strip_json_comments(config_path.read_text(encoding="utf-8")))
141
+ except json.JSONDecodeError as exc:
142
+ raise ConfigError(f"Invalid JSON in {config_path}: {exc}") from exc
143
+ if not isinstance(loaded, dict):
144
+ raise ConfigError(f"Config file must contain a JSON object: {config_path}")
145
+ _validate_public_config_keys(loaded)
146
+ config.update(loaded)
147
+
148
+ override_config = parse_override(override)
149
+ _validate_public_config_keys(override_config)
150
+ config.update(override_config)
151
+ _normalize_config_paths(config, config_dir, resolve_run_args=resolve_run_args)
152
+ _validate_config(config)
153
+ return config
154
+
155
+
156
+ def _resolve_config_file(config_file: str | os.PathLike[str]) -> Path:
157
+ path = Path(os.path.expandvars(os.path.expanduser(os.fspath(config_file))))
158
+ if not path.is_absolute():
159
+ path = Path.cwd() / path
160
+ return path.resolve()
161
+
162
+
163
+ def _validate_public_config_keys(config: Mapping[str, Any]) -> None:
164
+ unknown_keys = sorted(set(config) - public_config_keys())
165
+ if unknown_keys:
166
+ key = unknown_keys[0]
167
+ raise ConfigError(f"Unknown config key {key!r}. This key is not part of ros2docker core.")
168
+
169
+
170
+ def _validate_config(config: Mapping[str, Any]) -> None:
171
+ run_type = config.get("run_type", "bash")
172
+ if run_type not in VALID_RUN_TYPES:
173
+ raise ConfigError(f"Unsupported run_type {run_type!r}. Expected one of: {', '.join(sorted(VALID_RUN_TYPES))}.")
174
+
175
+ for list_key in ("run_args", "extra_run_args", "bake_ros_packages"):
176
+ if not isinstance(config.get(list_key, []), list):
177
+ raise ConfigError(f"{list_key!r} must be a list.")
178
+
179
+ if not isinstance(config.get("build_args", {}), dict):
180
+ raise ConfigError("'build_args' must be a JSON object.")
181
+
182
+ if run_type == "catmux" and not config.get("catmux_file"):
183
+ raise ConfigError("'catmux_file' is required when run_type is 'catmux'.")
184
+
185
+ if run_type == "command":
186
+ command = config.get("command")
187
+ if not isinstance(command, str | list):
188
+ raise ConfigError("'command' must be a string or list when run_type is 'command'.")
189
+
190
+
191
+ def _normalize_config_paths(config: dict[str, Any], config_dir: Path, *, resolve_run_args: bool) -> None:
192
+ if resolve_run_args:
193
+ config["run_args"] = normalize_docker_host_paths(config.get("run_args", []), config_dir)
194
+ config["extra_run_args"] = normalize_docker_host_paths(config.get("extra_run_args", []), config_dir)
195
+
196
+ bake_packages = []
197
+ for raw_path in config.get("bake_ros_packages", []):
198
+ resolved = resolve_host_path(raw_path, config_dir)
199
+ if not resolved.exists():
200
+ raise FileNotFoundError(f"bake_ros_packages path does not exist: {raw_path!r} (resolved to {resolved})")
201
+ if not resolved.is_dir():
202
+ raise NotADirectoryError(
203
+ f"bake_ros_packages path must be a directory: {raw_path!r} (resolved to {resolved})"
204
+ )
205
+ bake_packages.append(str(resolved))
206
+ config["bake_ros_packages"] = bake_packages
207
+
208
+
209
+ def normalize_docker_host_paths(
210
+ args: Sequence[str],
211
+ base_dir: str | os.PathLike[str],
212
+ ) -> list[str]:
213
+ normalized: list[str] = []
214
+ i = 0
215
+ base_path = Path(base_dir)
216
+ args_list = list(args)
217
+
218
+ while i < len(args_list):
219
+ arg = args_list[i]
220
+
221
+ if arg in {"-v", "--volume"}:
222
+ if i + 1 >= len(args_list):
223
+ raise ConfigError(f"{arg} requires a following volume spec.")
224
+ normalized.extend([arg, normalize_volume_spec(args_list[i + 1], base_path)])
225
+ i += 2
226
+ continue
227
+
228
+ if arg.startswith("--volume="):
229
+ volume = arg.split("=", 1)[1]
230
+ normalized.append(f"--volume={normalize_volume_spec(volume, base_path)}")
231
+ i += 1
232
+ continue
233
+
234
+ if arg == "--mount":
235
+ if i + 1 >= len(args_list):
236
+ raise ConfigError("--mount requires a following mount spec.")
237
+ normalized.extend([arg, normalize_mount_spec(args_list[i + 1], base_path)])
238
+ i += 2
239
+ continue
240
+
241
+ if arg.startswith("--mount="):
242
+ mount = arg.split("=", 1)[1]
243
+ normalized.append(f"--mount={normalize_mount_spec(mount, base_path)}")
244
+ i += 1
245
+ continue
246
+
247
+ normalized.append(arg)
248
+ i += 1
249
+
250
+ return normalized
251
+
252
+
253
+ def normalize_volume_spec(spec: str, base_dir: Path) -> str:
254
+ parts = spec.split(":")
255
+ if len(parts) < 2:
256
+ return spec
257
+
258
+ host_path = parts[0]
259
+ if not _looks_like_host_path(host_path):
260
+ return spec
261
+
262
+ resolved = resolve_host_path(host_path, base_dir)
263
+ parts[0] = str(resolved)
264
+ return ":".join(parts)
265
+
266
+
267
+ def normalize_mount_spec(spec: str, base_dir: Path) -> str:
268
+ fields = spec.split(",")
269
+ parsed: list[str] = []
270
+ is_bind = any(field == "type=bind" for field in fields)
271
+
272
+ for field in fields:
273
+ key, sep, value = field.partition("=")
274
+ if is_bind and sep and key in {"source", "src"}:
275
+ parsed.append(f"{key}={resolve_host_path(value, base_dir)}")
276
+ else:
277
+ parsed.append(field)
278
+
279
+ return ",".join(parsed)
280
+
281
+
282
+ def resolve_host_path(
283
+ raw_path: str | os.PathLike[str],
284
+ base_dir: str | os.PathLike[str],
285
+ ) -> Path:
286
+ text = os.fspath(raw_path)
287
+ expanded = os.path.expanduser(os.path.expandvars(text))
288
+ if "$" in expanded:
289
+ raise ConfigError(f"Could not expand environment variables in host path: {text!r}")
290
+
291
+ path = Path(expanded)
292
+ if _is_relative_path(text):
293
+ path = Path(base_dir) / expanded
294
+ elif not path.is_absolute() and _looks_like_host_path(text):
295
+ path = Path.cwd() / expanded
296
+
297
+ resolved = path.resolve()
298
+ if not resolved.exists():
299
+ raise FileNotFoundError(f"Host path does not exist: {text!r} (resolved to {resolved})")
300
+ return resolved
301
+
302
+
303
+ def _is_relative_path(path: str) -> bool:
304
+ return path in {".", ".."} or path.startswith("./") or path.startswith("../")
305
+
306
+
307
+ def _looks_like_host_path(path: str) -> bool:
308
+ expanded = os.path.expanduser(os.path.expandvars(path))
309
+ return (
310
+ path in {".", ".."}
311
+ or path.startswith("./")
312
+ or path.startswith("../")
313
+ or path.startswith("~")
314
+ or path.startswith("$")
315
+ or os.path.isabs(expanded)
316
+ )
@@ -0,0 +1,192 @@
1
+ # Use the official image for ROS2 Lyrical under Ubuntu 26.04
2
+ ARG BASE_IMAGE=osrf/ros:lyrical-desktop-full-resolute
3
+ # Pin digests for reproducibility. Leave empty to use the latest image. The digest can be found on Docker Hub for the respective tag, e.g. for osrf/ros:lyrical-desktop-full-resolute
4
+ # Default: Latest Digist as of 2026-06-10 for https://hub.docker.com/layers/osrf/ros/lyrical-desktop-full-resolute
5
+ ARG DIGEST=@sha256:ac6471b5c13e57674d9660630d0cb140f518cb5ea6add7169e5dd91e16bf0b50
6
+
7
+ FROM ${BASE_IMAGE}${DIGEST}
8
+
9
+ ##############################################################################
10
+ # 1) Base Environment
11
+ ##############################################################################
12
+ ARG TZ=Europe/Berlin
13
+ ENV TZ=${TZ}
14
+ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
15
+ ENV LANG=C.UTF-8
16
+ ENV LC_ALL=C.UTF-8
17
+ ENV DEBIAN_FRONTEND=noninteractive
18
+
19
+ ##############################################################################
20
+ # 2) Install Base Apt Dependencies
21
+ ##############################################################################
22
+ RUN apt-get update && apt-get install -y \
23
+ tmux \
24
+ python3-pip \
25
+ python3-venv \
26
+ gosu \
27
+ ssh \
28
+ curl \
29
+ sudo \
30
+ vim \
31
+ git \
32
+ wget \
33
+ unzip \
34
+ moreutils \
35
+ ca-certificates \
36
+ build-essential \
37
+ libyaml-cpp-dev \
38
+ x11-apps \
39
+ iperf3 \
40
+ # ros-$ROS_DISTRO-domain-bridge \ not available in Lyrical yet. include soon!
41
+ ros-$ROS_DISTRO-foxglove-bridge \
42
+ ros-$ROS_DISTRO-foxglove-msgs \
43
+ ros-$ROS_DISTRO-topic-tools \
44
+ ros-$ROS_DISTRO-rmw-cyclonedds-cpp \
45
+ ros-$ROS_DISTRO-rmw-zenoh-cpp \
46
+ ros-$ROS_DISTRO-ffmpeg-image-transport \
47
+ ros-$ROS_DISTRO-foxglove-compressed-video-transport \
48
+ ros-$ROS_DISTRO-gps-msgs \
49
+ ros-$ROS_DISTRO-ament-copyright \
50
+ ros-$ROS_DISTRO-ament-flake8 \
51
+ ros-$ROS_DISTRO-ament-pep257 \
52
+ && rm -rf /var/lib/apt/lists/*
53
+
54
+ ##############################################################################
55
+ # 2b) Custom overlay workspace – packages baked into the image
56
+ # (third-party source-only packages + project-specific bake_packages)
57
+ # Workspace chain: /opt/ros/$ROS_DISTRO → /opt/custom_ws/install
58
+ ##############################################################################
59
+ ENV CUSTOM_WS=/opt/custom_ws
60
+
61
+ # novatel_oem7_msgs: no binary deb for Kilted yet, build from source
62
+ RUN mkdir -p ${CUSTOM_WS}/src \
63
+ && git clone --depth 1 --branch kilted \
64
+ https://github.com/novatel/novatel_oem7_driver.git \
65
+ ${CUSTOM_WS}/src/novatel_oem7_driver
66
+
67
+ # bake_packages: project-specific ROS packages staged by build.py
68
+ # from paths listed in config "bake_ros_packages"
69
+ COPY bake_packages/ ${CUSTOM_WS}/src/
70
+
71
+ RUN . /opt/ros/${ROS_DISTRO}/setup.sh \
72
+ && cd ${CUSTOM_WS} \
73
+ && colcon build --merge-install --packages-select novatel_oem7_msgs \
74
+ && rm -rf ${CUSTOM_WS}/src/novatel_oem7_driver \
75
+ && colcon build --merge-install \
76
+ && rm -rf ${CUSTOM_WS}/src ${CUSTOM_WS}/build ${CUSTOM_WS}/log
77
+
78
+ # install zenoh and zenoh-plugin-ros2dds
79
+ ARG ZENOH_VERSION=1.9.0
80
+ ARG ZENOH_ROS2DDS_VERSION=${ZENOH_VERSION}
81
+ ENV ZENOH_PLATFORM=x86_64-unknown-linux-gnu
82
+ # Zenoh router
83
+ # - see: https://download.eclipse.org/zenoh/zenoh/latest/
84
+ RUN mkdir -p /opt/zenoh \
85
+ && wget -qO /tmp/zenoh.zip \
86
+ "https://download.eclipse.org/zenoh/zenoh/${ZENOH_VERSION}/zenoh-${ZENOH_VERSION}-${ZENOH_PLATFORM}-standalone.zip" \
87
+ && unzip -q /tmp/zenoh.zip -d /opt/zenoh \
88
+ && rm /tmp/zenoh.zip
89
+ # ROS2 bridge plugin
90
+ # - see: https://download.eclipse.org/zenoh/zenoh-plugin-ros2dds/latest/
91
+ RUN mkdir -p /opt/zenoh/lib \
92
+ && wget -qO /tmp/zenoh-plugin-ros2dds.zip \
93
+ "https://download.eclipse.org/zenoh/zenoh-plugin-ros2dds/${ZENOH_ROS2DDS_VERSION}/zenoh-plugin-ros2dds-${ZENOH_ROS2DDS_VERSION}-${ZENOH_PLATFORM}-standalone.zip" \
94
+ && unzip -q /tmp/zenoh-plugin-ros2dds.zip -d /opt/zenoh \
95
+ && rm /tmp/zenoh-plugin-ros2dds.zip
96
+ ENV PATH="/opt/zenoh:${PATH}"
97
+
98
+ # mcap CLI for fast filtering of rosbags
99
+ ARG MCAP_CLI_VERSION=0.0.62
100
+ RUN curl -L https://github.com/foxglove/mcap/releases/download/releases%2Fmcap-cli%2Fv${MCAP_CLI_VERSION}/mcap-linux-amd64 -o /usr/local/bin/mcap \
101
+ && chmod +x /usr/local/bin/mcap
102
+
103
+ # ##############################################################################
104
+ # # 3) Setup shared directory
105
+ # ##############################################################################
106
+ # RUN groupadd --system shared && usermod -aG shared root
107
+ # RUN mkdir -p /ros2ws && \
108
+ # chown root:shared /ros2ws && \
109
+ # chmod g+rwXs /ros2ws
110
+ # RUN echo 'umask 002' >> /etc/profile.d/umask.sh
111
+ # RUN setfacl -d -m g::rwx /ros2ws
112
+
113
+ ##############################################################################
114
+ # 5) Setup Virtual Python Environment
115
+ ##############################################################################
116
+ ENV PYTHON_VENV_PATH=/opt/ros_venv
117
+ RUN python3 -m venv $PYTHON_VENV_PATH --system-site-packages
118
+ ENV PATH="$PYTHON_VENV_PATH/bin:$PATH"
119
+ # set PYTHONPATH to include the venv site-packages. Hacky but might be required.
120
+ # RUN VENV_SITE="$("$PYTHON_VENV_PATH/bin/python" -c 'import sysconfig; print(sysconfig.get_path("purelib"))')" \
121
+ # && ln -sfn "$VENV_SITE" "$PYTHON_VENV_PATH/site-packages"
122
+ # ENV PYTHONPATH="${PYTHON_VENV_PATH}/site-packages"
123
+
124
+ ##############################################################################
125
+ # 6) Install Base Pip Dependencies
126
+ ##############################################################################
127
+ RUN pip install --no-cache-dir --upgrade pip && \
128
+ pip install --no-cache-dir \
129
+ PyYAML \
130
+ catmux \
131
+ rosbags
132
+
133
+ ##############################################################################
134
+ # 7) Hotfix: Replace deprecated rosdep version shipped with ROS 2 Jazzy image
135
+ ##############################################################################
136
+ # The base image includes an outdated rosdep installed via APT,
137
+ # which uses the deprecated 'pkg_resources' API and causes a DeprecationWarning.
138
+ # To avoid this warning, we remove the APT version and install the latest rosdep via pip.
139
+ RUN apt-get remove -y python3-rosdep && \
140
+ pip install --no-cache-dir --upgrade rosdep
141
+
142
+ ##############################################################################
143
+ # 8) Install user-specified APT dependencies
144
+ ##############################################################################
145
+ ARG APT_PACKAGES=""
146
+ RUN if [ -n "${APT_PACKAGES}" ]; then \
147
+ apt-get update && \
148
+ apt-get install -y ${APT_PACKAGES} && \
149
+ rm -rf /var/lib/apt/lists/*; \
150
+ fi
151
+
152
+ ##############################################################################
153
+ # 9) Install user-specified Python packages
154
+ ##############################################################################
155
+ ARG PIP_PACKAGES=""
156
+ RUN if [ -n "${PIP_PACKAGES}" ]; then \
157
+ pip install --no-cache-dir ${PIP_PACKAGES}; \
158
+ fi
159
+
160
+ ##############################################################################
161
+ # 10) Environment settings
162
+ ##############################################################################
163
+ RUN echo "source ${PYTHON_VENV_PATH}/bin/activate" >> /root/.bashrc
164
+ RUN echo "source ${CUSTOM_WS}/install/setup.bash" >> /root/.bashrc
165
+ RUN echo '[ -f /ros2ws/install/setup.bash ] && source /ros2ws/install/setup.bash' >> /root/.bashrc
166
+ RUN echo 'alias catmux="tmux -L catmux"' >> /root/.bashrc
167
+ ENV ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST
168
+ ENV RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
169
+ ENV SHELL=/bin/bash
170
+ SHELL ["/bin/bash", "-c"]
171
+
172
+ ##############################################################################
173
+ # 11) EntryPoint
174
+ ##############################################################################
175
+ COPY entrypoint.sh /entrypoint.sh
176
+ RUN chmod +x /entrypoint.sh
177
+ ENTRYPOINT ["/entrypoint.sh"]
178
+
179
+ ##############################################################################
180
+ # 12) Create Non-Root User
181
+ ##############################################################################
182
+ ARG USER_UID=1000
183
+ ARG USER_GID=$USER_UID
184
+ ENV CONTAINER_USERNAME=containeruser
185
+ RUN userdel -r $(id -un 1000) \
186
+ && groupadd --gid "$USER_GID" "$CONTAINER_USERNAME" \
187
+ && useradd --uid "$USER_UID" --gid "$USER_GID" -m "$CONTAINER_USERNAME" \
188
+ && echo "$CONTAINER_USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$CONTAINER_USERNAME \
189
+ && chmod 0440 /etc/sudoers.d/$CONTAINER_USERNAME \
190
+ && cp /root/.bashrc "/home/$CONTAINER_USERNAME/.bashrc" \
191
+ && chown "$USER_UID:$USER_GID" "/home/$CONTAINER_USERNAME/.bashrc"
192
+ # && usermod -aG shared "$CONTAINER_USERNAME" \
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ if [[ "${ROS2DOCKER_TRACE:-0}" == "1" ]]; then
5
+ set -x
6
+ fi
7
+
8
+ echo "**** Entrypoint script starting ****"
9
+
10
+ source "${CUSTOM_WS}/install/setup.bash"
11
+
12
+ source_ros2_workspace() {
13
+ local setup_file="$1"
14
+ if [[ ! -f "$setup_file" ]]; then
15
+ return 0
16
+ fi
17
+
18
+ source "$setup_file"
19
+ echo "Sourced ROS 2 workspace overlay: $setup_file"
20
+ }
21
+
22
+ if [ -d "/ws/ros2src" ]; then
23
+ echo "ROS 2 workspace found at /ws/ros2src"
24
+
25
+ sudo mkdir -p /ros2ws
26
+ sudo chown "$CONTAINER_USERNAME":"$CONTAINER_USERNAME" /ros2ws
27
+
28
+ if [[ -L /ros2ws/src ]]; then
29
+ ln -sfn /ws/ros2src /ros2ws/src
30
+ elif [[ -e /ros2ws/src ]]; then
31
+ echo "/ros2ws/src already exists and is not a symlink."
32
+ exit 1
33
+ else
34
+ ln -s /ws/ros2src /ros2ws/src
35
+ fi
36
+
37
+ if [[ "${CHECK_ROS2WS_DEPENDENCIES:-0}" == "1" ]]; then
38
+ echo "Checking for missing dependencies..."
39
+ # sudo /opt/ros_venv/bin/pip install --upgrade rosdep # not sure if needed
40
+ rosdep update --rosdistro "${ROS_DISTRO}"
41
+ if rosdep install --from-paths /ros2ws/src --ignore-src --simulate; then
42
+ echo "All dependencies are satisfied."
43
+ else
44
+ echo "Dependencies are missing. See rosdep output above."
45
+ exit 1
46
+ fi
47
+ else
48
+ echo "Skipping dependency check."
49
+ fi
50
+
51
+ if [[ "${BUILD_ROS2WS:-0}" == "1" ]]; then
52
+ echo "Building ROS 2 workspace..."
53
+ pushd /ros2ws >/dev/null
54
+ colcon_args=()
55
+ if [[ "${ROS2WS_SYMLINK_INSTALL:-1}" == "1" ]]; then
56
+ colcon_args+=(--symlink-install)
57
+ fi
58
+ if [[ "${ROS2WS_SUPPRESS_UNUSED_CMAKE_WARNINGS:-1}" == "1" ]]; then
59
+ colcon_args+=(--cmake-args --no-warn-unused-cli)
60
+ fi
61
+ colcon build "${colcon_args[@]}"
62
+ popd >/dev/null
63
+ else
64
+ echo "Skipping building ROS 2 workspace."
65
+ fi
66
+
67
+ source_ros2_workspace /ros2ws/install/setup.bash
68
+ else
69
+ echo "No ROS 2 workspace found at /ws/ros2src. Skipping"
70
+ fi
71
+
72
+ exec "$@"
@@ -0,0 +1,51 @@
1
+ {
2
+ // Run settings
3
+ "container_name": "example_ros2container",
4
+ "image_name": "ros2docker",
5
+
6
+ // Supported run types:
7
+ // "bash" - interactive shell
8
+ // "command" - run the configured command
9
+ // "catmux" - start a catmux session
10
+ // "up" - detached keepalive container
11
+ "run_type": "catmux",
12
+
13
+ // Used when run_type = "catmux".
14
+ "catmux_file": "/ws/catmux.yaml",
15
+ "catmux_params": {
16
+ "mock_param": "mock_value"
17
+ },
18
+
19
+ // Used when run_type = "command". String and argv-list forms are supported.
20
+ "command": ["ros2", "topic", "list"],
21
+
22
+ // Mount the config-adjacent ws directory into /ws.
23
+ "mount_ws": true,
24
+
25
+ // Enable X11 forwarding through /tmp/.X11-unix.
26
+ "enable_gui_forwarding": false,
27
+ "forward_ssh_agent": false,
28
+
29
+ // Extra docker run arguments.
30
+ "run_args": [
31
+ // "-e", "ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST", // (default)
32
+ // "-e", "RMW_IMPLEMENTATION=rmw_cyclonedds_cpp", // (default)
33
+ // "-e", "ROS_DOMAIN_ID=0", // (default)
34
+ // If /ws/ros2src exists, BUILD_ROS2WS uses symlink-install and sources
35
+ // /ros2ws/install/setup.bash before the configured command starts.
36
+ "-e", "BUILD_ROS2WS=1",
37
+ "-e", "CHECK_ROS2WS_DEPENDENCIES=1"
38
+ ],
39
+ "extra_run_args": [],
40
+
41
+ // ROS packages to pre-build into the Docker image, relative to this config.
42
+ // These are installed into /opt/ros/$ROS_DISTRO so every container gets them without
43
+ // needing them in each workspace's ros2src/.
44
+ "bake_ros_packages": [],
45
+
46
+ // Optional Docker build arguments.
47
+ "build_args": {
48
+ "APT_PACKAGES": "git wget curl",
49
+ "PIP_PACKAGES": "pytest pytest-cov"
50
+ }
51
+ }