Skip to content

Keep build-only dependencies out of the main image (#356) - #2

Open
dv-picknik wants to merge 2 commits into
add-dockerfrom
reduce-image-size
Open

Keep build-only dependencies out of the main image (#356)#2
dv-picknik wants to merge 2 commits into
add-dockerfrom
reduce-image-size

Conversation

@dv-picknik

@dv-picknik dv-picknik commented Aug 11, 2026

Copy link
Copy Markdown
Member

[written by AI]

Addresses space-ros#356. Stacked on PickNikRobotics/space-ros#388 (add-docker) — review that first; this PR's base is add-docker, so the diff shown here is only the size work.

The Earthly version of this work is space-ros#357, which space-ros#388 conflicts with by deleting the Earthfile. This is that work ported to the Dockerfile, minus the parts that measurement or review killed.

Problem

Everything the main image needs to compile Space ROS also ships in it: GCC, CMake, binutils, ros-dev-tools (which drags in colcon, rosdep, vcstool, bloom), plus the headers and CMake config files of the built install. None of it is used to run Space ROS.

Approach

  • New pre-installation-build stage holding bison build-essential cmake git wget. setup, ikos-install, and rosdep derive from it; prepare-image — the final image's parent — stays on plain pre-installation and no longer installs ros-dev-tools. The dev image reinstalls ros-dev-tools in the image stage as before.
  • build strips the install for non-dev variants: include/, share/**/cmake/, *.cmake, CMakeLists.txt, *.pyc. Done in build, before image copies ${SPACEROS_DIR} — a delete in a later layer only writes a whiteout over the parent layer and reclaims nothing.
  • clang-format added to excluded-deps.txt, so main skips it and dev still gets it.
  • Runtime dependencies resolved with rosdep --dependency-types exec. rosdeps.sh resolved everything the workspace needs to compile, and the runtime image installed all of it. The rosdep stage now emits two scripts: rosdeps.sh (everything, unchanged, used by build and restored in the dev image) and rosdeps-exec.sh (exec deps only, installed by prepare-image). Credit to @sylvesterkaczmarek, whose parallel Earthfile-based attempt at Reduce image size by removing build dependencies unused at runtime space-ros/space-ros#356 (sylvesterkaczmarek/space-ros#1) is where this idea came from.
  • prepare-image runs the rosdep script with APT::Install-Recommends "false" and cleans /var/lib/apt/lists/* in that same layer. Without the first, dropping ros-dev-tools newly pulls 23 MB of Recommends (manpages-dev, DejaVu fonts, libgd/libjpeg/libtiff/X11) behind libc6-dev; the baseline dodged that only because build-essential had already installed libc6-dev under --no-install-recommends. Without the second, the rm -rf already in the image stage masks those bytes instead of reclaiming them.
  • docs/USAGE.md: one note that :latest carries no compiler, CMake, or colcon and no headers, and that building against Space ROS means :dev.

Two latent bugs this surfaced

Both are pre-existing and only fixed here because dropping ros-dev-tools exposed them. Both were found by running the image, not by reading it.

  • python3-yaml reached the image only through ros-dev-tools. Replace Earthly with Docker Buildx for Space ROS builds (#107) space-ros/space-ros#388 converted the Earthfile's pip3 install pyyaml lark packaging catkin_pkg psutil to apt packages but dropped pyyaml; it kept working by accident. It is not a resolved rosdep at all — only libyaml-dev is. Without it import rclpy raises ModuleNotFoundError: No module named 'yaml' and the whole ros2 CLI is dead. Now requested explicitly in prepare-image.
  • python3-argcomplete is a resolved rosdep that excluded-deps.txt filtered out, so it too arrived only via ros-dev-tools. It is a real ros2cli runtime dependency: without it ros2cli swallows the extension's ImportError and silently drops every ros2 topic verb (bw delay echo find hz info list pub type) and ros2 service echo. No error, the commands just cease to exist — and ros2 pkg list still returns all 212 packages, so a smoke test that only checks package discovery passes green on a broken image. Removed from excluded-deps.txt.

Verification

Both variants built locally and compared against add-docker at 8c27019.

add-docker this PR
main image (docker image inspect --format '{{.Size}}') 462,923,055 (462.9 MB) 203,126,159 (203.1 MB)
apt packages 423 285
ros2 pkg list 212 212, identical set

−259.8 MB, −56%. 138 apt packages gone (gcc/g++/binutils/cmake/make, git/wget/subversion/mercurial/bzr, all of colcon, rosdep, vcstool, bloom, clang-format, python3-pytest); zero packages added. Of the shrink, /opt/ros/spaceros drops 168 MB → 128 MB, include/ alone accounting for 40 MB. The exec-dependency split is 5.5 MB of that (296 → 285 packages): less than the 68 MB the dropped python3-dev chain measures on its own, because other retained -dev packages still pull libc6-dev.

Parity against the baseline image, not just liveness:

  • ros2 {topic,node,param,service,action,interface,lifecycle,pkg} --help expose byte-identical verb sets.
  • ros2 topic pub + ros2 topic echo --once round-trips a std_msgs/msg/String; rclpy.create_node works.
  • import rclpy, yaml, lark, catkin_pkg, psutil, netifaces, numpy, packaging, argcomplete all succeed.
  • Dev image (946 MB): 212 packages, gcc/cmake/colcon/git/vcs/clang-format all present, /opt/ikos/bin/ikos present, include/ and 154 *Config.cmake files retained. It compiles and runs a rclcpp node built against the install. Its add-docker baseline was not built, so no dev-image delta is claimed — but the exec split is verified against a dev image built from this branch without it: identical 606-package set, no additions, no removals, confirming rosdeps.sh restores exactly what prepare-image no longer installs.

pre-commit (including hadolint) passes. docker buildx build --call check is clean for main/dev × image/export-build-test/export-repos.

Deliberately not done

Follow-ups

  • A main-image smoke job in CI, as proposed in Reduce runtime image build dependencies (#356). sylvesterkaczmarek/space-ros#1. Assert ros2 topic list and a Python import check rather than ros2 pkg list — per the python3-argcomplete bug above, package discovery survives breakage that removes half the CLI, so a package-discovery assertion passes green on a broken image.
  • vcs import --retry 3 does not cover ref-type determination. A CI run on this branch failed in the sources stage with a one-off TLS error from vcstool's Could not determine ref type of version, which the retry did not apply to. Transient (siblings cloned fine in the same command, and both other jobs in the same run passed the same stage), but the retry gap is real.
  • rosdeps.sh's header line is dead code. The generator writes apt-get install -y \ followed by rosdep's #[apt] Installation commands: comment, so the continuation is swallowed and the real work is one apt-get install -y <pkg> per line, emitted verbatim by rosdep. That is why this PR sets the apt config instead of adding a flag there. Worth straightening out separately, in Replace Earthly with Docker Buildx for Space ROS builds (#107) space-ros/space-ros#388's code.

JWhitleyWork and others added 2 commits August 4, 2026 09:26
Co-authored-by: Josh Whitley <josh.whitley@picknik.ai>
Co-authored-by: David Vadovszki <david.vadovszki@picknik.ai>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dv-picknik

Copy link
Copy Markdown
Member Author

[written by AI]

Folded in the rosdep --dependency-types exec split from @sylvesterkaczmarek's sylvesterkaczmarek/space-ros#1 — a parallel attempt at space-ros#356 that landed on the Earthfile while this one was being ported to the Dockerfile. That split is their idea, and it is the right one: rosdeps.sh was resolving everything needed to compile the workspace and the runtime image was installing all of it.

Their PR also proposes a main-image CI smoke job. Worth doing, and it is listed as a follow-up here — with one adjustment. Its assertion is ros2 pkg list, and that specific check cannot see the more dangerous of the two bugs this work surfaced: without python3-argcomplete, ros2cli swallows the extension's ImportError and silently drops every ros2 topic verb plus ros2 service echo, while ros2 pkg list still returns all 212 packages. A package-discovery assertion goes green on that image. ros2 topic list plus a Python import check catches it.

Two other differences worth noting for whoever reconciles these:

Measured effect of the exec split on its own: 296 → 285 apt packages, 208.6 MB → 203.1 MB. Smaller than the 68 MB the dropped python3-dev chain measures in isolation, because other retained -dev packages still pull libc6-dev. The dev image is unaffected — rosdeps.sh is restored there, and its package set is byte-identical at 606 packages with the split in place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants