1use anyhow::{anyhow, bail, Context, Result};
4use cargo_manifest::{Manifest, Product, Value};
5
6use std::ffi::OsString;
7use std::fs::{DirBuilder, File};
8use std::path::Path;
9use std::path::PathBuf;
10use std::process::Command;
11
12pub struct Args {
14 pub install_base: PathBuf,
16 pub build_base: PathBuf,
18 pub forwarded_args: Vec<OsString>,
20 pub profile: String,
22 pub manifest_path: PathBuf,
24}
25
26pub enum ArgsOrHelp {
28 Args(Args),
29 Help,
30}
31
32impl ArgsOrHelp {
33 pub fn parse() -> Result<Self> {
36 let mut args: Vec<_> = std::env::args_os().collect();
37 args.remove(0); let forwarded_args = if let Some(dash_dash) = args.iter().position(|arg| arg == "--") {
41 let later_args: Vec<_> = args[dash_dash + 1..].to_vec();
43 args.remove(dash_dash);
45 later_args
46 } else {
47 Vec::new()
48 };
49
50 let mut args = pico_args::Arguments::from_vec(args);
52 if args.contains("--help") {
53 return Ok(ArgsOrHelp::Help);
54 }
55 let profile = if args.contains("--release") {
56 String::from("release")
57 } else if let Ok(p) = args.value_from_str("--profile") {
58 p
59 } else {
60 String::from("debug")
61 };
62
63 let build_base = args
64 .opt_value_from_str("--target-dir")?
65 .unwrap_or_else(|| "target".into());
66 let install_base = args.value_from_str("--install-base")?;
67
68 let manifest_path = if let Ok(p) = args.value_from_str("--manifest-path") {
69 p
70 } else {
71 PathBuf::from("Cargo.toml")
72 .canonicalize()
73 .context("Package manifest does not exist")?
74 };
75
76 let res = Args {
77 install_base,
78 build_base,
79 forwarded_args,
80 profile,
81 manifest_path,
82 };
83
84 Ok(ArgsOrHelp::Args(res))
85 }
86
87 pub fn print_help() {
88 println!("cargo-ament-build");
89 println!("Wrapper around cargo-build that installs compilation results and extra files to an ament/ROS 2 install space.\n");
90 println!("USAGE:");
91 println!(" cargo ament-build --install-base <INSTALL_DIR> -- <CARGO-BUILD-OPTIONS>");
92 }
93}
94
95pub fn cargo(args: &[OsString], verb: &str) -> Result<Option<i32>> {
97 let mut cmd = Command::new("cargo");
98 cmd.arg(verb);
100 for arg in args {
101 cmd.arg(arg);
102 }
103 let exit_status = cmd
104 .status()
105 .context("Failed to spawn 'cargo build' subprocess")?;
106 Ok(exit_status.code())
107}
108
109pub fn create_package_marker(
111 install_base: impl AsRef<Path>,
112 marker_dir: &str,
113 package_name: &str,
114) -> Result<()> {
115 let mut path = install_base
116 .as_ref()
117 .join("share/ament_index/resource_index");
118 path.push(marker_dir);
119 DirBuilder::new()
120 .recursive(true)
121 .create(&path)
122 .with_context(|| {
123 format!(
124 "Failed to create package marker directory '{}'",
125 path.display()
126 )
127 })?;
128 path.push(package_name);
129 File::create(&path)
130 .with_context(|| format!("Failed to create package marker '{}'", path.display()))?;
131 Ok(())
132}
133
134fn copy(src: impl AsRef<Path>, dest_dir: impl AsRef<Path>) -> Result<()> {
136 let src = src.as_ref();
137 let dest = dest_dir.as_ref().join(src.file_name().unwrap());
138 if src.is_dir() {
139 std::fs::create_dir_all(&dest)?;
140 for entry in std::fs::read_dir(src)? {
141 let entry = entry?;
142 if entry.file_type()?.is_dir() {
143 copy(entry.path(), &dest)?;
144 } else {
145 std::fs::copy(entry.path(), dest.join(entry.file_name()))?;
146 }
147 }
148 } else if src.is_file() {
149 std::fs::copy(&src, &dest).with_context(|| {
150 format!(
151 "Failed to copy '{}' to '{}'.",
152 src.display(),
153 dest.display()
154 )
155 })?;
156 } else {
157 bail!("File or dir '{}' does not exist", src.display())
158 }
159 Ok(())
160}
161
162pub fn install_package(
166 install_base: impl AsRef<Path>,
167 package_path: impl AsRef<Path>,
168 manifest_path: impl AsRef<Path>,
169 package_name: &str,
170 manifest: &Manifest,
171) -> Result<()> {
172 let manifest_path = manifest_path.as_ref();
173
174 let mut dest_dir = install_base.as_ref().to_owned();
177 dest_dir.push("share");
178 dest_dir.push(package_name);
179 dest_dir.push("rust");
180 if dest_dir.is_dir() {
181 std::fs::remove_dir_all(&dest_dir)?;
182 }
183 DirBuilder::new().recursive(true).create(&dest_dir)?;
184 let package = manifest.package.as_ref().unwrap();
186 let build = match &package.build {
189 Some(Value::Boolean(false)) => None,
190 Some(Value::String(path)) => Some(path.as_str()),
191 Some(_) => bail!("Value of 'build' is not a string or boolean"),
192 None => None,
193 };
194 if let Some(filename) = build {
195 let src = package_path.as_ref().join(filename);
196 copy(src, &dest_dir)?;
197 }
198
199 copy(package_path.as_ref().join("src"), &dest_dir)?;
200 copy(manifest_path, &dest_dir)?;
201
202 copy(
204 package_path.as_ref().join("package.xml"),
205 dest_dir.parent().unwrap(),
206 )?;
207
208 let lockfile_path = manifest_path.with_extension("lock");
212 if lockfile_path.is_file() {
213 copy(&lockfile_path, &dest_dir)?;
214 }
215
216 Ok(())
217}
218
219pub fn install_binaries(
221 install_base: impl AsRef<Path>,
222 build_base: impl AsRef<Path>,
223 package_name: &str,
224 profile: &str,
225 binaries: &[Product],
226) -> Result<()> {
227 let src_dir = build_base.as_ref().join(profile);
228 let dest_dir = install_base.as_ref().join("lib").join(package_name);
229 if dest_dir.is_dir() {
230 std::fs::remove_dir_all(&dest_dir)?;
231 }
232 for binary in binaries {
234 let name = binary
235 .name
236 .as_ref()
237 .ok_or(anyhow!("Binary without name found"))?;
238 let src = src_dir.join(name);
239 let dest = dest_dir.join(name);
240 #[cfg(target_os = "windows")]
241 let dest = dest.with_extension("exe");
242 #[cfg(target_os = "windows")]
243 let src = src.with_extension("exe");
244 DirBuilder::new().recursive(true).create(&dest_dir)?;
246 std::fs::copy(&src, &dest)
247 .context(format!("Failed to copy binary from '{}'", src.display()))?;
248 }
249 let prefix_suffix_combinations = [
252 ("lib", "so"),
253 ("lib", "dylib"),
254 ("lib", "a"),
255 ("", "dll"),
256 ("", "lib"),
257 ];
258 for (prefix, suffix) in prefix_suffix_combinations {
259 let filename = String::from(prefix) + package_name + "." + suffix;
260 let src = src_dir.join(&filename);
261 let dest = dest_dir.join(filename);
262 if src.is_file() {
263 DirBuilder::new().recursive(true).create(&dest_dir)?;
265 std::fs::copy(&src, &dest)
266 .context(format!("Failed to copy library from '{}'", src.display()))?;
267 }
268 }
269 Ok(())
270}
271
272pub fn install_files_from_metadata(
274 install_base: impl AsRef<Path>,
275 package_path: impl AsRef<Path>,
276 package_name: &str,
277 metadata: Option<&Value>,
278) -> Result<()> {
279 let metadata_table = match metadata {
281 Some(Value::Table(tab)) => tab,
282 _ => return Ok(()),
283 };
284 let metadata_ros_table = match metadata_table.get("ros") {
285 Some(Value::Table(tab)) => tab,
286 _ => return Ok(()),
287 };
288 for subdir in ["share", "include", "lib"] {
289 let dest = install_base.as_ref().join(subdir).join(package_name);
290 DirBuilder::new().recursive(true).create(&dest)?;
291 let key = format!("install_to_{subdir}");
292 let install_array = match metadata_ros_table.get(&key) {
293 Some(Value::Array(arr)) => arr,
294 Some(_) => bail!("The [package.metadata.ros.{key}] entry is not an array"),
295 _ => return Ok(()),
296 };
297 let install_entries = install_array
298 .iter()
299 .map(|entry| match entry {
300 Value::String(dir) => Ok(dir.clone()),
301 _ => {
302 bail!("The elements of the [package.metadata.ros.{key}] array must be strings")
303 }
304 })
305 .collect::<Result<Vec<_>, _>>()?;
306 for rel_path in install_entries {
307 let src = package_path.as_ref().join(&rel_path);
308 copy(&src, &dest).with_context(|| {
309 format!(
310 "Could not process [package.metadata.ros.{key}] entry '{rel_path}'",
311 )
312 })?;
313 }
314 }
315 Ok(())
316}