use ament_rs::{search_paths::get_search_paths, AMENT_PREFIX_PATH_ENV_VAR};
use cargo_toml::Manifest;
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
fn is_marked_for_inclusion(path: &PathBuf) -> bool {
Manifest::from_path(path)
.map(|manifest| {
manifest
.package
.as_ref()
.and_then(|pkg| pkg.metadata.as_ref())
.and_then(|metadata| metadata.get("ros-env"))
.and_then(|ros_env| ros_env.get("include"))
.and_then(|include| include.as_bool())
.unwrap_or(false)
})
.unwrap_or(false)
}
fn star_dep_names(manifest: &Manifest) -> Vec<String> {
manifest
.dependencies
.iter()
.filter(|(_, version)| version.req() == "*")
.map(|(name, _)| name.to_owned())
.collect()
}
fn star_deps_to_use(manifest: &Manifest) -> String {
star_dep_names(manifest)
.into_iter()
.map(|name| format!("use crate::{name};\n"))
.collect()
}
fn crate_name_from_ament_package_dir(package_dir: &Path) -> &str {
package_dir
.parent()
.and_then(|parent| parent.file_name())
.and_then(|name| name.to_str())
.expect("AMENT package directory should be <prefix>/share/<package>/rust")
}
fn try_rustfmt(path: &Path) {
match Command::new("rustfmt")
.arg("--edition")
.arg("2021")
.arg(path)
.status()
{
Ok(status) if status.success() => {}
Ok(status) => {
println!("cargo:warning=rustfmt exited with status: {status}");
}
Err(err) => {
println!(
"cargo:warning=failed to run rustfmt for {}: {err}",
path.display()
);
}
}
}
fn main() {
println!("cargo:rerun-if-env-changed={AMENT_PREFIX_PATH_ENV_VAR}");
let ament_prefix_paths = get_search_paths().unwrap_or_default();
let mut discovered_packages = HashSet::new();
let export_candidates: Vec<PathBuf> = ament_prefix_paths
.iter()
.map(PathBuf::from)
.flat_map(|base_path| {
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| {
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(|path| {
path.parent()
.map(crate_name_from_ament_package_dir)
.map(|package| discovered_packages.insert(package.to_owned()))
.unwrap_or(false)
})
.collect();
let candidate_by_package: HashMap<String, PathBuf> = export_candidates
.iter()
.filter_map(|path| {
path.parent()
.map(crate_name_from_ament_package_dir)
.map(|package| (package.to_owned(), path.to_owned()))
})
.collect();
let dependencies_by_package: HashMap<String, Vec<String>> = candidate_by_package
.iter()
.filter_map(|(package, cargo_toml)| {
Manifest::from_path(cargo_toml)
.ok()
.map(|manifest| (package.to_owned(), star_dep_names(&manifest)))
})
.collect();
let mut included_packages: HashSet<String> = export_candidates
.iter()
.filter(|path| is_marked_for_inclusion(path))
.filter_map(|path| {
path.parent()
.map(crate_name_from_ament_package_dir)
.map(str::to_owned)
})
.collect();
let mut pending_packages: VecDeque<String> = included_packages.iter().cloned().collect();
while let Some(package) = pending_packages.pop_front() {
let Some(dependencies) = dependencies_by_package.get(&package) else {
continue;
};
for dependency in dependencies {
if candidate_by_package.contains_key(dependency)
&& included_packages.insert(dependency.clone())
{
pending_packages.push_back(dependency.clone());
}
}
}
loop {
let invalid_packages: Vec<String> = included_packages
.iter()
.filter(|package| {
dependencies_by_package
.get(*package)
.map(|dependencies| {
dependencies
.iter()
.any(|dependency| !included_packages.contains(dependency))
})
.unwrap_or(false)
})
.cloned()
.collect();
if invalid_packages.is_empty() {
break;
}
for package in invalid_packages {
included_packages.remove(&package);
}
}
let export_crate_tomls: Vec<PathBuf> = export_candidates
.into_iter()
.filter(|path| {
path.parent()
.map(crate_name_from_ament_package_dir)
.map(|package| included_packages.contains(package))
.unwrap_or(false)
})
.collect();
for cargo_toml in &export_crate_tomls {
println!("cargo:rerun-if-changed={}", cargo_toml.display());
if let Some(package_dir) = cargo_toml.parent() {
let src_dir = package_dir.join("src");
println!("cargo:rerun-if-changed={}", src_dir.display());
}
}
let content: String = export_crate_tomls
.iter()
.filter_map(|path| path.parent().map(|p| p.to_path_buf()))
.map(|package_dir| {
let package = crate_name_from_ament_package_dir(&package_dir);
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())
.filter(|entry| {
let name = entry.file_name();
name != "lib.rs" && name != "rmw.rs"
})
.filter_map(|e| {
let path = std::path::absolute(e.path()).expect("Failed to get absolute path for idiomatic module");
path.file_stem()
.and_then(|stem| stem.to_str())
.map(|stem| {
let idiomatic_path = path.to_string_lossy().replace('\\', "/");
let parent = path
.parent()
.expect("Failed to create rmw path");
let rmw_path = parent
.join(stem)
.join("rmw.rs")
.to_string_lossy()
.replace('\\', "/");
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");
try_rustfmt(&out_path);
}