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;