cargo_ament_build/
lib.rs

1// Licensed under the Apache License, Version 2.0
2
3use 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
12/// Arguments for both the wrapper and for `cargo build`.
13pub struct Args {
14    /// The install base for this package (i.e. directory containing `lib`, `share` etc.)
15    pub install_base: PathBuf,
16    /// The build base for this package, corresponding to the --target-dir option
17    pub build_base: PathBuf,
18    /// Arguments to be forwarded to `cargo build`.
19    pub forwarded_args: Vec<OsString>,
20    /// "debug", "release" etc.
21    pub profile: String,
22    /// The absolute path to the Cargo.toml file. Currently the --manifest-path option is not implemented.
23    pub manifest_path: PathBuf,
24}
25
26/// Wrapper around [`Args`] that can also indicate the --help flag.
27pub enum ArgsOrHelp {
28    Args(Args),
29    Help,
30}
31
32impl ArgsOrHelp {
33    /// This binary not only reads arguments before the --, but also selected arguments after
34    /// the --, so that it knows where the resulting binaries will be located.
35    pub fn parse() -> Result<Self> {
36        let mut args: Vec<_> = std::env::args_os().collect();
37        args.remove(0); // Remove the executable path.
38
39        // Find and process `--`.
40        let forwarded_args = if let Some(dash_dash) = args.iter().position(|arg| arg == "--") {
41            // Store all arguments following ...
42            let later_args: Vec<_> = args[dash_dash + 1..].to_vec();
43            // .. then remove the `--`
44            args.remove(dash_dash);
45            later_args
46        } else {
47            Vec::new()
48        };
49
50        // Now pass all the arguments (without `--`) through to `pico_args`.
51        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
95/// Run a certain cargo verb
96pub fn cargo(args: &[OsString], verb: &str) -> Result<Option<i32>> {
97    let mut cmd = Command::new("cargo");
98    // "check" and "build" have compatible arguments
99    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
109/// This is comparable to ament_index_register_resource() in CMake
110pub 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
134/// Copies files or directories.
135fn 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
162/// Copy the source code of the package to the install space
163///
164/// Specifically, `${install_base}/share/${package}/rust`.
165pub 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    // Install source code
175    // This is special-cased (and not simply added to the list of things to install below)
176    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    // unwrap is ok since it has been validated in main
185    let package = manifest.package.as_ref().unwrap();
186    // The entry for the build script can be empty (in which case build.rs is implicitly used if it
187    // exists), or a path, or false (in which case build.rs is not implicitly used).
188    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    // unwrap is ok since we pushed to the path before
203    copy(
204        package_path.as_ref().join("package.xml"),
205        dest_dir.parent().unwrap(),
206    )?;
207
208    // The lockfile may not exist in the case that the package is in a
209    // Cargo workspace. The lockfile is alongside the top-level
210    // virtual Cargo.toml.
211    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
219/// Copy the binaries to a location where they will be found by ROS 2 tools (the lib dir)
220pub 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    // Copy binaries
233    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        // Create destination directory
245        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    // If there is a shared or static library, copy it too
250    // See https://doc.rust-lang.org/reference/linkage.html for an explanation of suffixes
251    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            // Create destination directory
264            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
272/// Copy selected files/directories to the share dir.
273pub 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    // Unpack the metadata entry
280    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}