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