Skip to content

feat!: include only requested interface packages - #10

Open
azerupi wants to merge 1 commit into
ros2-rust:mainfrom
azerupi:feat/build-only-required-msgs
Open

feat!: include only requested interface packages#10
azerupi wants to merge 1 commit into
ros2-rust:mainfrom
azerupi:feat/build-only-required-msgs

Conversation

@azerupi

@azerupi azerupi commented Sep 6, 2026

Copy link
Copy Markdown

The code changes in this PR have been assisted by Cursor but I have reviewed and iterated on the code.

This is an experiment in response to multiple discussions we've had about this and PR #6 and your comments on it.

ros-env currently includes every generated interface crate it can find. This PR makes packages declare the set of interface packages they want explicitly. A crate declares the interface packages it needs in its Cargo.toml and all dependencies do the same and then ros-env includes the union of what the whole dependency graph asked for.

I also think these changes would pair very well with our desire to use a build-time interface generator.

Problem

Today the selection is every interface package that ros-env can find under share/<package>/rust on AMENT_PREFIX_PATH.

On my setup on a (I believe) close to stock Jazzy install there are 39 generated interface crates that contain 161 files for about ~89k lines of Rust. This all gets include!()ed in ros-env.

This has two consequences:

  • Build time and disk scale with what is installed / sourced, not with what is used.
  • One broken generated package breaks every build in the environment. It does not matter if someone depends on it or not. It is included in ros-env so it has to compile.

Solution

Declare what you use:

[dependencies]
ros-env = "0.3"

[package.metadata.ros-env]
interfaces = ["sensor_msgs"]
use ros_env::sensor_msgs::msg::Imu;

We can't do it with features, because the set of interface packages is open-ended and features are static. Therefore we use a metadata section.

Every crate in the graph may declare dependencies on interface packages that way and ros-env includes the union. A library names only what it needs, an application that pulls in three such libraries gets all three sets without restating them.

A requested package that is not on AMENT_PREFIX_PATH fails the build and names who asked for it, rather than silently omitting a module:

ros-env: the following interface packages were not found on AMENT_PREFIX_PATH:
  - `stress_nav_msgs`, requested by nav-library
Make sure the packages are built and their workspace is sourced.

High-level steps

The build script:

  1. Discovers installed generated crates at <prefix>/share/<package>/rust/Cargo.toml, first prefix on the search path wins, so overlays shadow underlays.
  2. Locates the consuming workspace (see below).
  3. Runs cargo metadata --filter-platform <TARGET> against it and unions every [package.metadata.ros-env] interfaces plus the workspace-level table.
  4. Expands the set of required interfaces with interfaces that the included interfaces themselves need. They are recognised by the * version requirement rosidl_generator_rs emits for them.
  5. Writes $OUT_DIR/interfaces.rs, which src/lib.rs includes.

cargo metadata runs with the workspace as the working directory so .cargo/config.toml and [patch] tables are picked up.

If ROS is not sourced it will not generate a build error in ros-env itself, ros_env will be empty and the build emits a warning. Of course any crate using messages from ros-env will fail to build.

Locating the workspace

Cargo does not tell a dependency's build script which workspace is being built (rust-lang/cargo#3946). We derive it from OUT_DIR when the target dir is in the cargo workspace which happens when building with cargo. But when building with colcon the layout is not recognised and it will fail. We could expand the heuristics but I opted to use a trick from embuild::cargo::workspace_dir (esp-idf-sys).

[env]
CARGO_WORKSPACE_DIR = { value = "", relative = true }

Adding this in a .cargo/config.toml in the package allows us to deterministically find the workspace that is built.

colcon sets --target-dir outside the source tree, so colcon build needs CARGO_WORKSPACE_DIR. colcon-ros-cargo could set it automatically.

Breaking changes

Consumers must declare [package.metadata.ros-env] interfaces. A graph that declares nothing gets an empty ros_env and a warning. I have considered being more backward compatible by falling back to including everything if the package or one of its dependencies didn't include this metadata. But I feel like this will just make the transition period last much longer so since ros-env is still new and people are migrating to it it is better to force the new way immediately.

The [package.metadata.ros-env] include = true opt-in on generated crates is gone. Discovery is now purely "is there a share/<pkg>/rust/Cargo.toml".

Builds where the target directory is outside the workspace now need CARGO_WORKSPACE_DIR change mentioned above.

Known limitations

Two semver-incompatible ros-env versions in one graph still produce mutually incompatible types, even from identical .msg definitions. I don't think there is any way around this.

Union `[package.metadata.ros-env] interfaces` from the consuming Cargo graph
and include that closure instead of every generated crate on AMENT_PREFIX_PATH.

Assisted-by: Cursor Grok 4.6 [Cursor]
Co-authored-by: Cursor <cursoragent@cursor.com>
@azerupi
azerupi requested review from esteve and maspe36 September 6, 2026 17:25
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.

1 participant