cargo_run_bin/
binary.rs

1use std::env;
2use std::fs;
3use std::io;
4use std::path;
5use std::process;
6
7use anyhow::anyhow;
8use anyhow::bail;
9use anyhow::Result;
10use cfg_if::cfg_if;
11use version_check as rustc;
12use which::which;
13
14use crate::cargo_config;
15use crate::metadata;
16use crate::shims;
17
18#[rustversion::since(1.74)]
19fn stderr_to_stdio() -> io::Result<process::Stdio> {
20    return Ok(io::stderr().into());
21}
22
23#[rustversion::before(1.74)]
24#[cfg(target_family = "unix")]
25fn stderr_to_stdio() -> io::Result<process::Stdio> {
26    use std::os::fd::AsFd;
27
28    return Ok(io::stderr().as_fd().try_clone_to_owned()?.into());
29}
30
31#[rustversion::before(1.74)]
32#[cfg(target_family = "windows")]
33fn stderr_to_stdio() -> io::Result<process::Stdio> {
34    use std::os::windows::io::AsHandle;
35
36    return Ok(io::stderr().as_handle().try_clone_to_owned()?.into());
37}
38
39/// INTERNAL: Install binary with cargo install.
40pub fn cargo_install(
41    binary_package: metadata::BinaryPackage,
42    cache_path: path::PathBuf,
43) -> Result<()> {
44    let mut cmd_prefix = process::Command::new("cargo");
45
46    cmd_prefix
47        .stdout(stderr_to_stdio()?)
48        .stderr(process::Stdio::inherit())
49        .arg("install")
50        .arg("--root")
51        .arg(&cache_path)
52        .arg("--version")
53        .arg(binary_package.version);
54
55    if let Some(git) = &binary_package.git {
56        cmd_prefix.arg("--git").arg(git);
57        if let Some(branch) = &binary_package.branch {
58            cmd_prefix.arg("--branch").arg(branch);
59        } else if let Some(tag) = &binary_package.tag {
60            cmd_prefix.arg("--tag").arg(tag);
61        } else if let Some(rev) = &binary_package.rev {
62            cmd_prefix.arg("--rev").arg(rev);
63        }
64    } else if let Some(path) = &binary_package.path {
65        cmd_prefix.arg("--path").arg(path);
66    }
67
68    if let Some(bin_target) = &binary_package.bin_target {
69        cmd_prefix.arg("--bin").arg(bin_target);
70    }
71
72    if let Some(locked) = &binary_package.locked {
73        if *locked {
74            cmd_prefix.arg("--locked");
75        }
76    }
77
78    if let Some(features) = &binary_package.features {
79        cmd_prefix.arg("--features");
80        cmd_prefix.arg(features.join(","));
81    }
82
83    if let Some(default_features) = &binary_package.default_features {
84        if !*default_features {
85            cmd_prefix.arg("--no-default-features");
86        }
87    }
88
89    cmd_prefix.arg(binary_package.package).output()?;
90
91    return Ok(());
92}
93
94/// INTERNAL: Install binary with binstall
95pub fn binstall(binary_package: metadata::BinaryPackage, cache_path: path::PathBuf) -> Result<()> {
96    let mut cmd_prefix = process::Command::new("cargo");
97
98    cmd_prefix
99        .stdout(stderr_to_stdio()?)
100        .stderr(process::Stdio::inherit())
101        .arg("binstall")
102        .arg("--no-confirm")
103        .arg("--no-symlinks")
104        .arg("--root")
105        .arg(&cache_path)
106        .arg("--install-path")
107        .arg(cache_path.join("bin"));
108
109    if let Some(bin) = &binary_package.bin_target {
110        cmd_prefix.arg("--bin").arg(bin);
111    }
112
113    if let Some(git) = &binary_package.git {
114        cmd_prefix.arg("--git").arg(git);
115    }
116
117    if let Some(locked) = &binary_package.locked {
118        if *locked {
119            cmd_prefix.arg("--locked");
120        }
121    }
122
123    cmd_prefix.arg("--");
124
125    cmd_prefix.arg(format!(
126        "{package}@{version}",
127        package = binary_package.package,
128        version = binary_package.version,
129    ));
130
131    cmd_prefix.output()?;
132
133    return Ok(());
134}
135
136/// Install the provided binary package if it has not been built already.
137pub fn install(binary_package: metadata::BinaryPackage) -> Result<String> {
138    let mut rust_version = "unknown".to_string();
139    if let Some(res) = rustc::triple() {
140        if res.1.is_nightly() {
141            rust_version = "nightly".to_string();
142        } else {
143            rust_version = res.0.to_string();
144        }
145    }
146
147    let mut bin_name = binary_package.package.to_owned();
148    if let Some(bin_target) = &binary_package.bin_target {
149        bin_name = bin_target.to_string();
150    }
151
152    let cache_path = metadata::get_project_root()?
153        .join(".bin")
154        .join(format!("rust-{rust_version}"))
155        .join(binary_package.package.clone())
156        .join(binary_package.version.clone());
157
158    let mut cache_bin_path = cache_path.join("bin").join(bin_name);
159    cache_bin_path = cache_bin_path.clone();
160
161    cfg_if! {
162        if #[cfg(not(target_family = "unix"))] {
163            cache_bin_path.set_extension("exe");
164        }
165    }
166
167    if !path::Path::new(&cache_bin_path).exists() {
168        fs::create_dir_all(&cache_path)?;
169        if binary_package.features.is_none()
170            && binary_package.default_features.is_none()
171            && binary_package.branch.is_none()
172            && binary_package.tag.is_none()
173            && binary_package.rev.is_none()
174            && binary_package.package != "cargo-binstall"
175            && (cargo_config::binstall_alias_exists()? || which("cargo-binstall").is_ok())
176        {
177            binstall(binary_package, cache_path)?;
178        } else {
179            cargo_install(binary_package, cache_path)?;
180        }
181    }
182
183    return Ok(cache_bin_path.to_str().unwrap().to_string());
184}
185
186/// Executes provided binary and arguments, adding shims to PATH so any
187/// other run-bin configured binaries are available.
188pub fn run(bin_path: String, args: Vec<String>) -> Result<()> {
189    // Silly hack to make cargo commands parse arguments correctly.
190    let mut final_args = args.clone();
191    let bin_name = path::Path::new(&bin_path)
192        .file_name()
193        .unwrap()
194        .to_str()
195        .unwrap();
196    if bin_name.starts_with("cargo-") {
197        final_args = vec![bin_name
198            .to_string()
199            .replace("cargo-", "")
200            .replace(".exe", "")];
201        final_args.append(&mut args.clone());
202    }
203
204    let mut shell_paths = shims::get_shim_paths()?;
205    shell_paths.push(env::var("PATH").unwrap_or("".to_string()));
206
207    let spawn = process::Command::new(bin_path.clone())
208        .stdout(process::Stdio::inherit())
209        .stderr(process::Stdio::inherit())
210        .stdin(process::Stdio::inherit())
211        .args(&final_args)
212        .env("PATH", shell_paths.join(":"))
213        .spawn();
214
215    if let Ok(mut spawn) = spawn {
216        let status = spawn
217            .wait()?
218            .code()
219            .ok_or_else(|| return anyhow!("Failed to get spawn exit code"))?;
220
221        process::exit(status);
222    }
223
224    bail!(format!("Process failed to start: {bin_path}"));
225}