Skip to content
Open
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
3 changes: 2 additions & 1 deletion .github/workflows/rust-minimal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,12 @@ jobs:

# Colcon can not be run in a venv which is required in Ubuntu Noble
# Removing the externally managed file
- name: Install colcon-cargo and colcon-ros-cargo
- name: Install colcon-cargo, colcon-ros-cargo, and cargo-ament-build
run: |
sudo rm -f /usr/lib/python3.12/EXTERNALLY-MANAGED
sudo pip3 install git+https://github.com/colcon/colcon-cargo.git
sudo pip3 install git+https://github.com/colcon/colcon-ros-cargo.git
cargo install --git https://[email protected]/maspe36/cargo-ament-build.git --branch feature/skip_reexport_marker_file

- name: Check formatting of Rust packages
run: |
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/rust-stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,12 @@ jobs:

# Colcon can not be run in a venv which is required in Ubuntu Noble
# Removing the externally managed file
- name: Install colcon-cargo and colcon-ros-cargo
- name: Install colcon-cargo, colcon-ros-cargo, and cargo-ament-build
run: |
sudo rm -f /usr/lib/python3.12/EXTERNALLY-MANAGED
sudo pip3 install git+https://github.com/colcon/colcon-cargo.git
sudo pip3 install git+https://github.com/colcon/colcon-ros-cargo.git
cargo install --git https://[email protected]/maspe36/cargo-ament-build.git --branch feature/skip_reexport_marker_file

- name: Check formatting of Rust packages
run: |
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/rust-win.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ jobs:
# prerequisites and fixes for windows build ros2_rust:
# * Libclang has to be added (from the ros2_rust instructions) and the dll has to be renamed
# * colcon-ros-cargo and colcon-cargo have to be added as PyPI packages
# * cargo-ament-build is manually installed from github for now to include the change to skip
# installing the marker file if the package should be reexported
run: |
pixi add libclang --manifest-path C:\pixi_ws\pixi.toml
$src = "C:\pixi_ws\.pixi\envs\default\Library\bin\libclang-13.dll"
Expand All @@ -48,6 +50,7 @@ jobs:
pixi add --pypi "colcon-ros-cargo@git+https://github.com/colcon/colcon-ros-cargo.git" --manifest-path C:\pixi_ws\pixi.toml
pixi add --pypi "colcon-cargo@git+https://github.com/colcon/colcon-cargo.git" --manifest-path C:\pixi_ws\pixi.toml
pixi upgrade colcon-core --manifest-path C:\pixi_ws\pixi.toml
cargo install --git https://[email protected]/maspe36/cargo-ament-build.git --branch feature/skip_reexport_marker_file

- name: Get prebuild ROS files and unzip
run: |
Expand Down
4 changes: 2 additions & 2 deletions docs/writing-your-first-rclrs-node.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Instead, you can store the node as a regular member. Let's add a struct that con

```rust
use std::sync::Arc;
use std_msgs::msg::String as StringMsg;
use rclrs::std_msgs::msg::String as StringMsg;

struct RepublisherNode {
node: Arc<rclrs::Node>,
Expand Down Expand Up @@ -111,7 +111,7 @@ So, to store the received data in the struct, the following things have to chang

```rust
use std::sync::{Arc, Mutex}; // (1)
use std_msgs::msg::String as StringMsg;
use rclrs::std_msgs::msg::String as StringMsg;

struct RepublisherNode {
node: Arc<rclrs::Node>,
Expand Down
8 changes: 4 additions & 4 deletions docs/writing_a_simple_publisher_and_subscriber.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ To construct a node, replace the code in your `main.rs` file with the following:
/// methods to publish a simple "Hello World" message on a loop in separate threads.
use rclrs::{create_node, Context, Node, Publisher, RclrsError, QOS_PROFILE_DEFAULT};
use std::{env, sync::Arc, thread, time::Duration};
use std_msgs::msg::String as StringMsg;
use rclrs::std_msgs::msg::String as StringMsg;
/// SimplePublisherNode struct contains node and publisher members.
/// Used to initialize a ROS 2 node and publisher, and publish messages.
struct SimplePublisherNode {
Expand Down Expand Up @@ -138,7 +138,7 @@ handling, iteration, threading, ROS 2 communication, and string message publishi
```rust
use rclrs::{create_node, Context, Node, Publisher, RclrsError, QOS_PROFILE_DEFAULT};
use std::{env, sync::Arc, thread, time::Duration};
use std_msgs::msg::String as StringMsg;
use rclrs::std_msgs::msg::String as StringMsg;
```
* `use std::{sync::Arc, time::Duration, iter, thread};`: Imports specific features from the standard library:
- `Arc` is for thread-safe shared ownership of data.
Expand All @@ -149,7 +149,7 @@ use std_msgs::msg::String as StringMsg;
- `RclrsError` for handling errors.
- `QOS_PROFILE_DEFAULT` for default Quality of Service settings.
- `Context, create_node, Node, Publisher` are for ROS 2 node creation and publishing.
* `use std_msgs::msg::String as StringMsg;`: Imports the `StringMsg` type for publishing string messages.
* `use rclrs::std_msgs::msg::String as StringMsg;`: Imports the `StringMsg` type for publishing string messages.

#### `SimplePublisherNode`
Next, this structure defines a `SimplePublisherNode` which holds references to a ROS 2 node and a publisher for string messages.
Expand Down Expand Up @@ -291,7 +291,7 @@ use std::{
thread,
time::Duration,
};
use std_msgs::msg::String as StringMsg;
use rclrs::std_msgs::msg::String as StringMsg;
pub struct SimpleSubscriptionNode {
node: Arc<Node>,
_subscriber: Arc<Subscription<StringMsg>>,
Expand Down
12 changes: 11 additions & 1 deletion rclrs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ futures-lite = { version = "2.6", features = ["std", "race"] }

# Needed for the runtime-agnostic timeout feature
async-std = "1.13"
# NOTE(sam): needed to make sure we can compile with rustc 1.75
async-lock = ">=3, <3.4.2"

# Needed for dynamic messages
libloading = { version = "0.8", optional = true }
Expand All @@ -54,6 +56,8 @@ tokio-stream = "0.1"
# Needed by action clients to generate UUID values for their goals
uuid = { version = "1", features = ["v4"] }

paste = { version = "1", optional = true}

[dev-dependencies]
# Needed for e.g. writing yaml files in tests
tempfile = "3.3.0"
Expand All @@ -65,13 +69,19 @@ tokio = { version = "1", features = ["rt", "time", "macros"] }
cfg-if = "1.0.0"
rustflags = "0.1"

# Used to read Cargo.toml dependencies when re-exporting generated crates
cargo_toml = "0.14"

# Helper crate for working with AMENT_PREFIX_PATH
ament_rs = "0.3"

[features]
default = []
dyn_msg = ["ament_rs", "libloading"]
serde = ["dep:serde", "dep:serde-big-array", "rosidl_runtime_rs/serde"]
# This feature is solely for the purpose of being able to generate documetation without a ROS installation
# The only intended usage of this feature is for docs.rs builders to work, and is not intended to be used by end users
use_ros_shim = ["rosidl_runtime_rs/use_ros_shim"]
use_ros_shim = ["paste", "rosidl_runtime_rs/use_ros_shim"]

[package.metadata.docs.rs]
features = ["use_ros_shim"]
Expand Down
190 changes: 141 additions & 49 deletions rclrs/build.rs
Original file line number Diff line number Diff line change
@@ -1,63 +1,155 @@
use std::{env, path::Path};
const AMENT_PREFIX_PATH: &str = "AMENT_PREFIX_PATH";
use std::{env, fs, path::PathBuf};

use ament_rs::{search_paths::get_search_paths, AMENT_PREFIX_PATH_ENV_VAR};
use cargo_toml::Manifest;

const ROS_DISTRO: &str = "ROS_DISTRO";
const KNOWN_DISTROS: &[&str] = &["humble", "jazzy", "kilted", "rolling"];

fn get_env_var_or_abort(env_var: &'static str) -> String {
if let Ok(value) = env::var(env_var) {
value
} else {
panic!(
"{} environment variable not set - please source ROS 2 installation first.",
env_var
);
}
fn get_ros_distro() -> String {
env::var(ROS_DISTRO)
.or_else(|_| {
if env::var("CARGO_FEATURE_USE_ROS_SHIM").is_ok() {
rustflags::from_env()
.find_map(|f| match f {
rustflags::Flag::Cfg { name, value } if name.as_str() == "ros_distro" => {
value
}
_ => None,
})
.ok_or_else(|| "Missing --cfg ros_distro in RUSTFLAGS".to_string())
} else {
Err(format!("Set {ROS_DISTRO} or use ROS shim"))
}
})
.expect("Failed to determine ROS distro")
}

fn is_marked_for_reexport(path: &PathBuf) -> bool {
fs::read_to_string(path)
.map(|s| s.contains("[package.metadata.rclrs]") && s.contains("reexport = true"))
.unwrap_or(false)
}

fn star_deps_to_use(manifest: &Manifest) -> String {
// Find all dependencies for this crate that have a `*` version requirement.
// We will assume that these are other exported dependencies that need symbols
// exposed in their module.
manifest
.dependencies
.iter()
.filter(|(_, version)| version.req() == "*")
.map(|(name, _)| format!("use crate::{name};\n"))
.collect::<String>()
}

fn main() {
println!(
"cargo:rustc-check-cfg=cfg(ros_distro, values(\"{}\"))",
vec!["humble", "jazzy", "kilted", "rolling"].join("\", \"")
KNOWN_DISTROS.join("\", \"")
);
let ros_distro = if let Ok(value) = env::var(ROS_DISTRO) {
value
} else {
cfg_if::cfg_if! {
if #[cfg(feature="use_ros_shim")] {
use rustflags;
// // Look for --cfg ros_distro=<ros_distro>
for flag in rustflags::from_env() {
if matches!(flag, rustflags::Flag::Cfg { ref name, value : _ } if name == "ros_distro") {
if let rustflags::Flag::Cfg {name:_, value: flag_value} = flag {
println!("cargo:rustc-cfg=ros_distro=\"{}\"", flag_value.unwrap());
return;
} else {
continue;
}
}
}
let error_msg =
"When using the use_ros_shim feature, you must pass the ROS distribution you are targeting as a compiler flag with --cfg ros_distro=\"<ros_distro>\"";
panic!("{}", error_msg);
} else {
let error_msg =
"ROS_DISTRO environment variable not set - please source ROS 2 installation first.";
panic!("{}", error_msg);
}
}
};
println!("cargo:rustc-cfg=ros_distro=\"{ros_distro}\"");
println!("cargo:rustc-cfg=ros_distro=\"{}\"", get_ros_distro());
println!("cargo:rerun-if-env-changed={ROS_DISTRO}");
println!("cargo:rerun-if-env-changed={AMENT_PREFIX_PATH_ENV_VAR}");

let ament_prefix_paths = get_search_paths().unwrap_or_default();

let ament_prefix_paths = get_env_var_or_abort(AMENT_PREFIX_PATH);
for ament_prefix_path in ament_prefix_paths.split(':').map(Path::new) {
for ament_prefix_path in &ament_prefix_paths {
// Link the native libraries
let library_path = ament_prefix_path.join("lib");
let library_path = PathBuf::from(ament_prefix_path).join("lib");
println!("cargo:rustc-link-search=native={}", library_path.display());
}

println!("cargo:rustc-link-lib=dylib=rcl");
println!("cargo:rustc-link-lib=dylib=rcl_action");
println!("cargo:rustc-link-lib=dylib=rcl_yaml_param_parser");
println!("cargo:rustc-link-lib=dylib=rcutils");
println!("cargo:rustc-link-lib=dylib=rmw");
println!("cargo:rustc-link-lib=dylib=rmw_implementation");
// Re-export any generated interface crates that we find
let export_crate_tomls: Vec<PathBuf> = ament_prefix_paths
.iter()
.map(PathBuf::from)
.flat_map(|base_path| {
// 1. Try to read share/ directory
fs::read_dir(base_path.join("share")).into_iter().flatten()
})
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().is_dir())
.flat_map(|package_dir| {
// 2. Try to read <package>/rust/ directory
fs::read_dir(package_dir.path().join("rust"))
.into_iter()
.flatten()
})
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| path.file_name() == Some(std::ffi::OsStr::new("Cargo.toml")))
.filter(is_marked_for_reexport)
.collect();

let content: String = export_crate_tomls
.iter()
.filter_map(|path| path.parent().map(|p| p.to_path_buf()))
.map(|package_dir| {
let package = package_dir
.parent()
.unwrap()
.file_name()
.unwrap()
.to_str()
.unwrap();

// Find all dependencies for this crate that have a `*` version requirement.
// We will assume that these are other exported dependencies that need symbols
// exposed in their module.
let dependencies: String = Manifest::from_path(package_dir.clone().join("Cargo.toml"))
.iter()
.map(star_deps_to_use)
.collect();

let internal_mods: String = fs::read_dir(package_dir.join("src"))
.into_iter()
.flatten()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().is_file())
// Ignore lib.rs and any rmw.rs. lib.rs is only used if the crate is consumed
// independently, and rmw.rs files need their top level module
// (i.e., msg, srv, action) to exist to be re-exported.
.filter(|entry| {
let name = entry.file_name();
name != "lib.rs" && name != "rmw.rs"
})
// Wrap the inclusion of each file in a module matching the file stem
// so that the generated code can be imported like `rclrs::std_msgs::msgs::Bool`
.filter_map(|e| {
e.path()
.file_stem()
.and_then(|stem| stem.to_str())
.map(|stem| {
let idiomatic_path = e.path().display().to_string();
let sep = std::path::MAIN_SEPARATOR;
let rmw_path = idiomatic_path
.rsplit_once(std::path::MAIN_SEPARATOR)
.map(|(dir, _)| format!("{dir}{sep}{stem}{sep}rmw.rs"))
.unwrap_or_else(|| "rmw.rs".to_string());

// TODO I would like to run rustfmt on this generated code, similar to how bindgen does it
format!("pub mod {stem} {{ {dependencies} include!(\"{idiomatic_path}\"); pub mod rmw {{ {dependencies} include!(\"{rmw_path}\"); }} }}")
})
})
.collect();

format!("#[allow(unused_imports, missing_docs)]\npub mod {package} {{ {internal_mods} }}")
})
.collect();

let out_path =
PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR not set")).join("interfaces.rs");
fs::write(out_path, content).expect("Failed to write interfaces.rs");

[
"rcl",
"rcl_action",
"rcl_yaml_param_parser",
"rcutils",
"rmw",
"rmw_implementation",
]
.iter()
.for_each(|lib| println!("cargo:rustc-link-lib=dylib={lib}"));
}
4 changes: 2 additions & 2 deletions rclrs/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub use action_goal_receiver::*;
pub(crate) mod action_server;
pub use action_server::*;

use crate::{log_error, rcl_bindings::*, vendor::builtin_interfaces::msg::Time, DropGuard};
use crate::{builtin_interfaces::msg::Time, log_error, rcl_bindings::*, DropGuard};
use std::fmt;

#[cfg(feature = "serde")]
Expand Down Expand Up @@ -256,7 +256,7 @@ fn empty_goal_status_array() -> DropGuard<rcl_action_goal_status_array_t> {
#[cfg(test)]
mod tests {
use crate::{
vendor::example_interfaces::action::{
example_interfaces::action::{
Fibonacci, Fibonacci_Feedback, Fibonacci_Goal, Fibonacci_Result,
},
*,
Expand Down
8 changes: 3 additions & 5 deletions rclrs/src/action/action_client.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
use super::empty_goal_status_array;
use crate::{
log_warn,
rcl_bindings::*,
vendor::{action_msgs::srv::CancelGoal_Response, builtin_interfaces::msg::Time},
CancelResponse, CancelResponseCode, DropGuard, GoalStatus, GoalStatusCode, GoalUuid,
MultiCancelResponse, Node, NodeHandle, QoSProfile, RclPrimitive, RclPrimitiveHandle,
action_msgs::srv::CancelGoal_Response, builtin_interfaces::msg::Time, log_warn,
rcl_bindings::*, CancelResponse, CancelResponseCode, DropGuard, GoalStatus, GoalStatusCode,
GoalUuid, MultiCancelResponse, Node, NodeHandle, QoSProfile, RclPrimitive, RclPrimitiveHandle,
RclPrimitiveKind, RclrsError, ReadyKind, TakeFailedAsNone, ToResult, Waitable,
WaitableLifecycle, ENTITY_LIFECYCLE_MUTEX,
};
Expand Down
6 changes: 3 additions & 3 deletions rclrs/src/action/action_client/goal_client.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
vendor::builtin_interfaces::msg::Time, CancellationClient, FeedbackClient, GoalStatus,
GoalStatusCode, ResultClient, StatusWatcher,
builtin_interfaces::msg::Time, CancellationClient, FeedbackClient, GoalStatus, GoalStatusCode,
ResultClient, StatusWatcher,
};
use rosidl_runtime_rs::Action;
use std::{
Expand Down Expand Up @@ -96,7 +96,7 @@ impl<A: Action> GoalClient<A> {
///
/// ```
/// use rclrs::*;
/// use crate::rclrs::vendor::example_interfaces::action::Fibonacci;
/// use crate::rclrs::example_interfaces::action::Fibonacci;
/// use futures::StreamExt;
///
/// async fn process_goal_client_stream(
Expand Down
Loading
Loading