1use guess_host_triple::guess_host_triple;
7use std::{fs::File, path::Path, process};
8use tempfile::NamedTempFile;
9use tinyjson::JsonValue;
10
11pub mod install_error;
12
13use install_error::*;
14
15mod command_ext;
16pub use command_ext::{ChildWithCommand, CommandExt, CommandFormattable};
17
18mod json_value_ext;
19pub use json_value_ext::{JsonExtError, JsonKey, JsonValueExt};
20
21mod utils;
22pub use utils::{get_cargo_bin_dir, utf8_to_string_lossy};
23
24#[derive(Debug)]
25pub struct CommandFailed {
26 pub command: String,
27 pub stdout: String,
28 pub stderr: String,
29}
30
31#[derive(Clone)]
32pub struct CrateDetails {
33 pub crate_name: String,
34 pub version: String,
35 pub target: String,
36}
37
38fn get_binstall_upstream_url(target: &str) -> (&'static str, String) {
40 let archive_format = if target.contains("linux") {
41 "tgz"
42 } else {
43 "zip"
44 };
45 let url = format!("https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-{target}.{archive_format}");
46
47 (archive_format, url)
48}
49
50pub fn download_and_install_binstall_from_upstream(target: &str) -> Result<(), InstallError> {
52 let (archive_format, url) = get_binstall_upstream_url(target);
53
54 if archive_format == "tgz" {
55 untar(curl(&url)?)?;
56
57 Ok(())
58 } else {
59 assert_eq!(archive_format, "zip");
60
61 let (zip_file, zip_file_temp_path) = NamedTempFile::new()?.into_parts();
62
63 curl_file(&url, zip_file)?;
64
65 unzip(&zip_file_temp_path)?;
66
67 Ok(())
68 }
69}
70
71pub fn do_dry_run_download_and_install_binstall_from_upstream(
72 target: &str,
73) -> Result<String, InstallError> {
74 let (archive_format, url) = get_binstall_upstream_url(target);
75
76 curl_head(&url)?;
77
78 let cargo_bin_dir = get_cargo_bin_dir()?;
79
80 if archive_format == "tgz" {
81 Ok(format_curl_and_untar_cmd(&url, &cargo_bin_dir))
82 } else {
83 Ok(format!(
84 "temp=\"$(mktemp)\"\n{curl_cmd} >\"$temp\"\nunzip \"$temp\" -d {extdir}",
85 curl_cmd = prepare_curl_bytes_cmd(&url).formattable(),
86 extdir = cargo_bin_dir.display(),
87 ))
88 }
89}
90
91pub fn unzip(zip_file: &Path) -> Result<(), InstallError> {
92 let bin_dir = get_cargo_bin_dir()?;
93
94 process::Command::new("unzip")
95 .arg(zip_file)
96 .arg("-d")
97 .arg(bin_dir)
98 .output_checked_status()?;
99
100 Ok(())
101}
102
103pub fn get_cargo_binstall_version() -> Option<String> {
104 let output = std::process::Command::new("cargo")
105 .args(["binstall", "-V"])
106 .output()
107 .ok()?;
108 if !output.status.success() {
109 return None;
110 }
111
112 String::from_utf8(output.stdout).ok()
113}
114
115pub fn install_crate_curl(
116 details: &CrateDetails,
117 fallback: bool,
118) -> Result<InstallSuccess, InstallError> {
119 let urls = get_quickinstall_download_urls(details);
120
121 let res = match curl_and_untar(&urls[0]) {
122 Err(err) if err.is_curl_404() => {
123 println!("Fallback to old release schema");
124
125 curl_and_untar(&urls[1])
126 }
127 res => res,
128 };
129
130 match res {
131 Ok(tar_output) => {
132 let bin_dir = get_cargo_bin_dir()?;
133
134 print!(
136 "Installed {crate_name}@{version} to {bin_dir}:\n{tar_output}",
137 crate_name = details.crate_name,
138 version = details.version,
139 bin_dir = bin_dir.display(),
140 );
141 Ok(InstallSuccess::InstalledFromTarball)
142 }
143 Err(err) if err.is_curl_404() => {
144 if !fallback {
145 return Err(InstallError::NoFallback(details.clone()));
146 }
147
148 println!(
149 "Could not find a pre-built package for {} {} on {}.",
150 details.crate_name, details.version, details.target
151 );
152 println!("We have reported your installation request, so it should be built soon.");
153
154 println!("Falling back to `cargo install`.");
155
156 let status = prepare_cargo_install_cmd(details).status()?;
157
158 if status.success() {
159 Ok(InstallSuccess::BuiltFromSource)
160 } else {
161 Err(InstallError::CargoInstallFailed)
162 }
163 }
164 Err(err) => Err(err),
165 }
166}
167
168fn curl_and_untar(url: &str) -> Result<String, InstallError> {
169 untar(curl(url)?)
170}
171
172pub fn get_latest_version(crate_name: &str) -> Result<String, InstallError> {
173 let url = format!("https://crates.io/api/v1/crates/{crate_name}");
174
175 curl_json(&url)
176 .map_err(|e| {
177 if e.is_curl_404() {
178 InstallError::CrateDoesNotExist {
179 crate_name: crate_name.to_string(),
180 }
181 } else {
182 e
183 }
184 })?
185 .get_owned(&"crate")?
186 .get_owned(&"max_stable_version")?
187 .try_into_string()
188 .map_err(From::from)
189}
190
191pub fn get_target_triple() -> Result<String, InstallError> {
192 match get_target_triple_from_rustc() {
193 Ok(target) => Ok(target),
194 Err(err) => {
195 if let Some(target) = guess_host_triple() {
196 println!("get_target_triple_from_rustc() failed due to {err}, fallback to guess_host_triple");
197 Ok(target.to_string())
198 } else {
199 println!("get_target_triple_from_rustc() failed due to {err}, fallback to guess_host_triple also failed");
200 Err(err)
201 }
202 }
203 }
204}
205
206fn get_target_triple_from_rustc() -> Result<String, InstallError> {
207 let output = std::process::Command::new("rustc")
209 .arg("--version")
210 .arg("--verbose")
211 .output_checked_status()?;
212
213 let stdout = String::from_utf8_lossy(&output.stdout);
214 let target = stdout
215 .lines()
216 .find_map(|line| line.strip_prefix("host: "))
217 .ok_or(InstallError::FailToParseRustcOutput {
218 reason: "Fail to find any line starts with 'host: '.",
219 })?;
220
221 let mut parts: Vec<&str> = target.splitn(4, '-').collect();
234 let os = *parts.get(2).ok_or(InstallError::FailToParseRustcOutput {
235 reason: "rustc returned an invalid triple, contains less than three parts",
236 })?;
237 if os == "linux" {
238 parts[1] = "unknown";
239 }
240 Ok(parts.join("-"))
241}
242
243pub fn report_stats_in_background(
244 details: &CrateDetails,
245 result: &Result<InstallSuccess, InstallError>,
246) {
247 let stats_url = format!(
248 "https://cargo-quickinstall-stats-server.fly.dev/record-install?crate={crate}&version={version}&target={target}&agent={agent}&status={status}",
249 crate = url_encode(&details.crate_name),
250 version = url_encode(&details.version),
251 target = url_encode(&details.target),
252 agent = url_encode(concat!("cargo-quickinstall/", env!("CARGO_PKG_VERSION"))),
253 status = install_result_to_status_str(result),
254 );
255
256 prepare_curl_post_cmd(&stats_url)
261 .stdin(process::Stdio::null())
262 .stdout(process::Stdio::null())
263 .stderr(process::Stdio::null())
264 .spawn()
265 .ok();
266}
267
268fn url_encode(input: &str) -> String {
269 let mut encoded = String::with_capacity(input.len());
270
271 for c in input.chars() {
272 match c {
273 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => encoded.push(c),
274 _ => encoded.push_str(&format!("%{:02X}", c as u8)),
275 }
276 }
277
278 encoded
279}
280
281fn format_curl_and_untar_cmd(url: &str, bin_dir: &Path) -> String {
282 format!(
283 "{curl_cmd} | {untar_cmd}",
284 curl_cmd = prepare_curl_bytes_cmd(url).formattable(),
285 untar_cmd = prepare_untar_cmd(bin_dir).formattable(),
286 )
287}
288
289pub fn do_dry_run_curl(
290 crate_details: &CrateDetails,
291 fallback: bool,
292) -> Result<String, InstallError> {
293 let urls = get_quickinstall_download_urls(crate_details);
294
295 let (url, res) = match curl_head(&urls[0]) {
296 Err(err) if err.is_curl_404() => (&urls[1], curl_head(&urls[1])),
297 res => (&urls[0], res),
298 };
299
300 match res {
301 Ok(_) => {
302 let cargo_bin_dir = get_cargo_bin_dir()?;
303
304 Ok(format_curl_and_untar_cmd(url, &cargo_bin_dir))
305 }
306 Err(err) if err.is_curl_404() && fallback => {
307 let cargo_install_cmd = prepare_cargo_install_cmd(crate_details);
308 Ok(format!("{}", cargo_install_cmd.formattable()))
309 }
310 Err(err) => Err(err),
311 }
312}
313
314fn untar(mut curl: ChildWithCommand) -> Result<String, InstallError> {
315 let bin_dir = get_cargo_bin_dir()?;
316
317 let res = prepare_untar_cmd(&bin_dir)
318 .stdin(curl.stdout().take().unwrap())
319 .output_checked_status();
320
321 curl.wait_with_output_checked_status()?;
327
328 let output = res?;
329
330 let stdout = utf8_to_string_lossy(output.stdout);
331 let stderr = String::from_utf8_lossy(&output.stderr);
332
333 let mut s = stdout;
334 s += &stderr;
335
336 Ok(s)
337}
338
339fn prepare_curl_head_cmd(url: &str) -> std::process::Command {
340 let mut cmd = prepare_curl_cmd();
341 cmd.arg("--head").arg(url);
342 cmd
343}
344
345fn prepare_curl_post_cmd(url: &str) -> std::process::Command {
346 let mut cmd = prepare_curl_cmd();
347 cmd.args(["-X", "POST"]).arg(url);
348 cmd
349}
350
351fn curl_head(url: &str) -> Result<Vec<u8>, InstallError> {
352 prepare_curl_head_cmd(url)
353 .output_checked_status()
354 .map(|output| output.stdout)
355}
356
357fn curl(url: &str) -> Result<ChildWithCommand, InstallError> {
358 let mut cmd = prepare_curl_bytes_cmd(url);
359 cmd.stdin(process::Stdio::null())
360 .stdout(process::Stdio::piped())
361 .stderr(process::Stdio::piped());
362 cmd.spawn_with_cmd()
363}
364
365fn curl_file(url: &str, file: File) -> Result<(), InstallError> {
366 prepare_curl_bytes_cmd(url)
367 .stdin(process::Stdio::null())
368 .stdout(file)
369 .stderr(process::Stdio::piped())
370 .output_checked_status()?;
371
372 Ok(())
373}
374
375fn curl_bytes(url: &str) -> Result<Vec<u8>, InstallError> {
376 prepare_curl_bytes_cmd(url)
377 .output_checked_status()
378 .map(|output| output.stdout)
379}
380
381fn curl_string(url: &str) -> Result<String, InstallError> {
382 curl_bytes(url).map(utf8_to_string_lossy)
383}
384
385pub fn curl_json(url: &str) -> Result<JsonValue, InstallError> {
386 curl_string(url)?
387 .parse()
388 .map_err(|err| InstallError::InvalidJson {
389 url: url.to_string(),
390 err,
391 })
392}
393
394fn get_quickinstall_download_urls(
395 CrateDetails {
396 crate_name,
397 version,
398 target,
399 }: &CrateDetails,
400) -> [String; 2] {
401 [format!(
402 "https://github.com/cargo-bins/cargo-quickinstall/releases/download/{crate_name}-{version}/{crate_name}-{version}-{target}.tar.gz",
403 ),
404 format!(
405 "https://github.com/cargo-bins/cargo-quickinstall/releases/download/{crate_name}-{version}-{target}/{crate_name}-{version}-{target}.tar.gz",
406 )]
407}
408
409fn prepare_curl_cmd() -> std::process::Command {
410 let mut cmd = std::process::Command::new("curl");
411 cmd.args([
412 "--user-agent",
413 concat!(
414 "cargo-quickinstall/",
415 env!("CARGO_PKG_VERSION"),
416 " client (alsuren@gmail.com)",
417 ),
418 "--location",
419 "--silent",
420 "--show-error",
421 "--fail",
422 ]);
423 cmd
424}
425
426fn prepare_curl_bytes_cmd(url: &str) -> std::process::Command {
427 let mut cmd = prepare_curl_cmd();
428 cmd.arg(url);
429 cmd
430}
431
432fn prepare_untar_cmd(cargo_bin_dir: &Path) -> std::process::Command {
433 let mut cmd = std::process::Command::new("tar");
434 cmd.arg("-xzvvf").arg("-").arg("-C").arg(cargo_bin_dir);
435 cmd
436}
437
438fn prepare_cargo_install_cmd(details: &CrateDetails) -> std::process::Command {
439 let mut cmd = std::process::Command::new("cargo");
440 cmd.arg("install")
441 .arg(&details.crate_name)
442 .arg("--version")
443 .arg(&details.version);
444 cmd
445}