Skip to content

Commit

Permalink
Package identification & build tasks for colcon-ros-cargo
Browse files Browse the repository at this point in the history
This includes cargo-ros-install, a wrapper around cargo build which installs files into the correct places for ROS 2 tools to find them.
  • Loading branch information
nnmm committed Feb 11, 2022
1 parent 26a6779 commit 52d68b7
Show file tree
Hide file tree
Showing 18 changed files with 759 additions and 0 deletions.
14 changes: 14 additions & 0 deletions cargo-ros-install/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
32 changes: 32 additions & 0 deletions cargo-ros-install/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
cmake_minimum_required(VERSION 3.5)
project(cargo-ros-install NONE)

find_package(ament_cmake REQUIRED)

configure_file(${CMAKE_SOURCE_DIR}/Cargo.toml ${CMAKE_BINARY_DIR}/Cargo.toml COPYONLY)
configure_file(${CMAKE_SOURCE_DIR}/src/lib.rs ${CMAKE_BINARY_DIR}/src/lib.rs COPYONLY)
configure_file(${CMAKE_SOURCE_DIR}/src/main.rs ${CMAKE_BINARY_DIR}/src/main.rs COPYONLY)
add_custom_command(
OUTPUT
${CMAKE_BINARY_DIR}/target/release/cargo-ros-install
COMMAND cargo build -q --release
DEPENDS
src/lib.rs
src/main.rs
Cargo.toml
WORKING_DIRECTORY
${CMAKE_BINARY_DIR}
)

add_custom_target(
build_crate ALL
DEPENDS
${CMAKE_BINARY_DIR}/target/release/cargo-ros-install
)

install(FILES
${CMAKE_BINARY_DIR}/target/release/cargo-ros-install
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
DESTINATION bin
)
ament_package()
13 changes: 13 additions & 0 deletions cargo-ros-install/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "cargo-ros-install"
version = "0.1.0"
authors = ["nnmm <nnmmgit@gmail.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1"
cargo-manifest = "0.2"
fs_extra = "1"
pico-args = "0.4"
14 changes: 14 additions & 0 deletions cargo-ros-install/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<package format="2">
<name>cargo-ros-install</name>
<version>0.0.0</version>
<description>Cargo build/check wrapper that installs files as described in REP 122</description>
<maintainer email="nnmmgit@gmail.com">nnmm</maintainer>
<license>Apache License 2.0</license>

<buildtool_depend>ament_cmake</buildtool_depend>

<export>
<build_type>ament_cmake</build_type>
</export>
</package>
173 changes: 173 additions & 0 deletions cargo-ros-install/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use anyhow::{anyhow, Context, Result};
use cargo_manifest::Product;

use std::ffi::OsString;
use std::fs::{DirBuilder, File};
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;

/// Arguments for both the wrapper and for `cargo build`.
pub struct Args {
/// The install base for this package (i.e. directory containing `lib`, `share` etc.)
pub install_base: PathBuf,
/// The build base for this package, corresponding to the --target-dir option
pub build_base: PathBuf,
/// Arguments to be forwarded to `cargo build`.
pub forwarded_args: Vec<OsString>,
/// "debug", "release" etc.
pub profile: String,
/// The absolute path to the Cargo.toml file. Currently the --manifest-path option is not implemented.
pub manifest_path: PathBuf,
}

impl Args {
pub fn parse() -> Result<Self> {
let mut args: Vec<_> = std::env::args_os().collect();
args.remove(0); // remove the executable path.

// Find and process `--`.
let forwarded_args = if let Some(dash_dash) = args.iter().position(|arg| arg == "--") {
// Store all arguments following ...
let later_args: Vec<_> = args[dash_dash + 1..].to_vec();
// .. then remove the `--`
args.remove(dash_dash);
later_args
} else {
Vec::new()
};

// Now pass all the arguments (without `--`) through to `pico_args`.
let mut args = pico_args::Arguments::from_vec(args);
let profile = if args.contains("--release") {
String::from("release")
} else if let Ok(p) = args.value_from_str("--profile") {
p
} else {
String::from("debug")
};

let build_base = args
.opt_value_from_str("--target-dir")?
.unwrap_or_else(|| "target".into());
let install_base = args.value_from_str("--install-base")?;

let manifest_path = if let Ok(p) = args.value_from_str("--manifest-path") {
p
} else {
PathBuf::from("Cargo.toml")
.canonicalize()
.context("Package manifest does not exist.")?
};

let res = Args {
install_base,
build_base,
forwarded_args,
profile,
manifest_path,
};

Ok(res)
}
}

pub fn cargo_build(args: &[OsString], is_pure_library: bool) -> Result<Option<i32>> {
let mut cmd = Command::new("cargo");
// "check" and "build" are compatible
if is_pure_library {
cmd.arg("check");
} else {
cmd.arg("build");
}
for arg in args {
cmd.arg(arg);
}
let exit_status = cmd
.status()
.context("Failed to spawn 'cargo build' subprocess.")?;
Ok(exit_status.code())
}

/// This is comparable to ament_index_register_resource() in CMake
pub fn create_package_marker(
install_base: impl AsRef<Path>,
marker_dir: &str,
package_name: &str,
) -> Result<()> {
let mut path = install_base
.as_ref()
.join("share/ament_index/resource_index");
path.push(marker_dir);
DirBuilder::new()
.recursive(true)
.create(&path)
.with_context(|| {
format!(
"Failed to create package marker directory '{}'.",
path.display()
)
})?;
path.push(package_name);
File::create(&path)
.with_context(|| format!("Failed to create package marker '{}'.", path.display()))?;
Ok(())
}

/// Copy the source code of the package to the install space
pub fn install_package(
install_base: impl AsRef<Path>,
package_path: impl AsRef<Path>,
package_name: &str,
) -> Result<()> {
let mut dest = install_base.as_ref().to_owned();
dest.push("share");
dest.push(package_name);
dest.push("Rust");
fs_extra::dir::remove(&dest)?;
DirBuilder::new().recursive(true).create(&dest)?;
let mut opt = fs_extra::dir::CopyOptions::new();
opt.overwrite = true;
for dir_entry in std::fs::read_dir(package_path)? {
let dir_entry = dir_entry?;
let src = dir_entry.path();
let filename = dir_entry.file_name();
// There might be a target directory after a manual build with cargo
if filename == "target" {
continue;
}
if src.is_dir() {
fs_extra::dir::copy(&src, &dest, &opt).context("Failed to install package.")?;
} else {
let dest_file = dest.join(filename);
std::fs::copy(&src, &dest_file).context("Failed to install package.")?;
}
}
Ok(())
}

/// Copy the binaries to a location where they are found by ROS 2 tools (the lib dir)
pub fn install_binaries(
install_base: impl AsRef<Path>,
build_base: impl AsRef<Path>,
package_name: &str,
profile: &str,
binaries: &[Product],
) -> Result<()> {
for binary in binaries {
let name = binary
.name
.as_ref()
.ok_or(anyhow!("Binary without name found."))?;
let src_location = build_base.as_ref().join(profile).join(name);
let mut dest = install_base.as_ref().join("lib").join(package_name);
// Create destination directory
DirBuilder::new().recursive(true).create(&dest)?;
dest.push(name);
std::fs::copy(&src_location, &dest).context(format!(
"Failed to copy binary from '{}'.",
src_location.display()
))?;
}
Ok(())
}
55 changes: 55 additions & 0 deletions cargo-ros-install/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use anyhow::{anyhow, Context, Result};

use cargo_manifest::Manifest;
use cargo_ros_install::*;

fn main() {
let exitcode = match fallible_main().context("Error in cargo-ros-install") {
Ok(Some(code)) => code,
Ok(None) => {
eprintln!("'cargo build' was terminated by signal.");
1
}
Err(e) => {
eprintln!("{:?}", e);
1
}
};
// No destructors left to run, so it's fine to exit.
std::process::exit(exitcode);
}

fn fallible_main() -> Result<Option<i32>> {
let args = Args::parse()?;
let mut manifest = Manifest::from_path(&args.manifest_path)?;
manifest.complete_from_path(&args.manifest_path)?;

// Unwrap is safe since complete_from_path() has been called
let is_pure_library = manifest.bin.as_ref().unwrap().is_empty();
let exitcode = cargo_build(&args.forwarded_args, is_pure_library)?;

let package_name = &manifest
.package
.ok_or(anyhow!("Package has no name."))?
.name;
let package_path = args
.manifest_path
.parent()
.ok_or(anyhow!("Manifest path must have a parent."))?;

// Putting marker file creation after the actual build command means that
// we create less garbage if the build command failed.
create_package_marker(&args.install_base, "packages", package_name)?;
// This marker is used by colcon-ros-cargo when looking for dependencies
create_package_marker(&args.install_base, "rust_packages", package_name)?;
install_package(&args.install_base, package_path, package_name)?;
install_binaries(
&args.install_base,
&args.build_base,
package_name,
&args.profile,
// Unwrap is safe since complete_from_path() has been called
&manifest.bin.unwrap(),
)?;
Ok(exitcode)
}
Loading

0 comments on commit 52d68b7

Please sign in to comment.