Skip to main content

cargo_install/
lib.rs

1//! Wrapper around the `cargo install` command.
2//!
3//! The crate exposes a builder for the most common `cargo install`
4//! options, plus `extra_args` for unsupported flags.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use cargo_install::CargoInstallBuilder;
10//!
11//! fn main() -> Result<(), Box<dyn std::error::Error>> {
12//!     CargoInstallBuilder::default()
13//!         .crate_name("ripgrep")
14//!         .version("14.1.1")
15//!         .bin("rg")
16//!         .profile("release")
17//!         .locked(true)
18//!         .build()?
19//!         .run()?;
20//!
21//!     Ok(())
22//! }
23//! ```
24
25mod error;
26mod utils;
27
28pub use crate::error::CargoInstallError;
29
30use crate::utils::{push_flag, push_joined, push_option};
31use derive_builder::Builder;
32use std::ffi::OsString;
33use std::process::{Command, Stdio};
34use tap::Tap;
35
36#[derive(Builder, Debug, Default)]
37#[builder(pattern = "owned", default, setter(into, strip_option))]
38/// Configuration for a `cargo install` invocation.
39///
40/// Construct this type directly with [`CargoInstall::new`] or prefer the
41/// generated `CargoInstallBuilder` for a more ergonomic setup flow.
42pub struct CargoInstall {
43    /// Sets `--root` to override the installation root directory.
44    root: Option<std::path::PathBuf>,
45    /// Sets `--version` to install a specific crate version requirement.
46    version: Option<OsString>,
47    /// Sets `--git` to install from a git repository.
48    git: Option<OsString>,
49    /// Sets `--branch` when installing from git.
50    branch: Option<OsString>,
51    /// Sets `--tag` when installing from git.
52    tag: Option<OsString>,
53    /// Sets `--rev` when installing from git.
54    rev: Option<OsString>,
55    /// Sets `--target` to build for a specific compilation target.
56    target: Option<OsString>,
57    /// Sets `--bin` to install a specific binary target.
58    bin: Option<OsString>,
59    /// Sets `--profile` to select the build profile used for installation.
60    profile: Option<OsString>,
61    /// Sets `--path` to install from a local crate directory.
62    path: Option<std::path::PathBuf>,
63    /// Sets the registry crate name to install.
64    crate_name: Option<OsString>,
65    /// Enables `--force`.
66    force: bool,
67    /// Enables `--locked`.
68    locked: bool,
69    /// Enables `--debug`.
70    debug: bool,
71    /// Sets `--features` using a feature list.
72    features: Vec<OsString>,
73    /// Enables `--all-features`.
74    all_features: bool,
75    /// Enables `--no-default-features`.
76    no_default_features: bool,
77    /// Appends additional arguments after all typed options.
78    extra_args: Vec<OsString>,
79    /// Overrides the child process stdout configuration.
80    ///
81    /// When not set, stdout inherits from the current process.
82    stdout: Option<Stdio>,
83}
84
85impl CargoInstall {
86    /// Creates an empty `CargoInstall` configuration.
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    /// Builds the `cargo install` argument vector in canonical flag order.
92    ///
93    /// The returned list always starts with `install`, followed by typed
94    /// options and flags, then `crate_name`, followed by `extra_args`.
95    pub fn args(&self) -> Vec<OsString> {
96        vec![OsString::from("install")].tap_mut(|args| {
97            push_option(args, "--root", self.root.as_deref());
98            push_option(args, "--version", self.version.as_deref());
99            push_option(args, "--git", self.git.as_deref());
100            push_option(args, "--branch", self.branch.as_deref());
101            push_option(args, "--tag", self.tag.as_deref());
102            push_option(args, "--rev", self.rev.as_deref());
103            push_option(args, "--target", self.target.as_deref());
104            push_option(args, "--bin", self.bin.as_deref());
105            push_option(args, "--profile", self.profile.as_deref());
106            push_option(args, "--path", self.path.as_deref());
107            push_flag(args, "--force", self.force);
108            push_flag(args, "--locked", self.locked);
109            push_flag(args, "--debug", self.debug);
110            push_joined(args, "--features", &self.features, ",");
111            push_flag(args, "--all-features", self.all_features);
112            push_flag(args, "--no-default-features", self.no_default_features);
113            if let Some(crate_name) = self.crate_name.as_ref() {
114                args.push(crate_name.clone());
115            }
116            args.extend(self.extra_args.iter().cloned());
117        })
118    }
119
120    fn command(mut self) -> Command {
121        Command::new("cargo").tap_mut(|command| {
122            command.args(self.args());
123            command.stdout(self.stdout.take().unwrap_or_else(Stdio::inherit));
124            command.stderr(Stdio::piped());
125        })
126    }
127
128    /// Executes `cargo install` and maps common stderr patterns into
129    /// [`CargoInstallError`].
130    ///
131    /// `stdout` inherits from the current process unless overridden.
132    /// `stderr` is always captured so the crate can parse known failure modes.
133    ///
134    /// Error classification depends on the stderr text produced by the local
135    /// cargo version, so unrecognized messages fall back to
136    /// [`CargoInstallError::UnknownCargoError`].
137    pub fn run(self) -> Result<(), CargoInstallError> {
138        let output = self
139            .command()
140            .spawn()
141            .map_err(CargoInstallError::from_spawn_error)?
142            .wait_with_output()?;
143        let status = output.status;
144
145        if status.success() {
146            Ok(())
147        } else {
148            Err(CargoInstallError::from_output(status, output.stderr))
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests;