|
| 1 | +// Copyright 2026 the Prep Authors |
| 2 | +// SPDX-License-Identifier: Apache-2.0 OR MIT |
| 3 | + |
| 4 | +use std::collections::HashMap; |
| 5 | +use std::io::ErrorKind; |
| 6 | +use std::process::Command; |
| 7 | +use std::sync::{LazyLock, RwLock}; |
| 8 | + |
| 9 | +use anyhow::{Context, Result, bail, ensure}; |
| 10 | + |
| 11 | +use crate::tools::rustup; |
| 12 | +use crate::ui; |
| 13 | + |
| 14 | +/// Cargo executable name. |
| 15 | +const BIN: &str = "cargo"; |
| 16 | + |
| 17 | +/// Toolchain version -> Cargo path |
| 18 | +static PATHS: LazyLock<RwLock<HashMap<String, String>>> = |
| 19 | + LazyLock::new(|| RwLock::new(HashMap::new())); |
| 20 | + |
| 21 | +/// Returns the cargo command. |
| 22 | +/// |
| 23 | +/// Provide an empty `version` to pick the default cargo version. |
| 24 | +pub fn new(version: &str) -> Result<Command> { |
| 25 | + let paths = PATHS.read().expect("cargo setup lock poisoned"); |
| 26 | + if let Some(path) = paths.get(version) { |
| 27 | + return Ok(Command::new(path)); |
| 28 | + } |
| 29 | + drop(paths); |
| 30 | + let mut paths = PATHS.write().expect("cargo setup lock poisoned"); |
| 31 | + if let Some(path) = paths.get(version) { |
| 32 | + return Ok(Command::new(path)); |
| 33 | + } |
| 34 | + let path = set_up(version)?; |
| 35 | + let cmd = Command::new(&path); |
| 36 | + paths.insert(version.into(), path); |
| 37 | + Ok(cmd) |
| 38 | +} |
| 39 | + |
| 40 | +/// Ensures that Cargo is installed and ready to use. |
| 41 | +pub fn set_up(version: &str) -> Result<String> { |
| 42 | + // TODO: Call rustup toolchain install with correct components etc first |
| 43 | + |
| 44 | + let mut cmd = rustup::new()?; |
| 45 | + let mut cmd = cmd.arg("which").arg(BIN); |
| 46 | + if !version.is_empty() { |
| 47 | + cmd = cmd.args(["--toolchain", version]); |
| 48 | + } |
| 49 | + |
| 50 | + ui::print_cmd(cmd); |
| 51 | + |
| 52 | + let output = cmd.output().context("failed to run rustup")?; |
| 53 | + ensure!(output.status.success(), "rustup failed: {}", output.status); |
| 54 | + |
| 55 | + let path = String::from_utf8(output.stdout).context("rustup output not valid UTF-8")?; |
| 56 | + let path = path.trim(); |
| 57 | + |
| 58 | + if !verify(path, version)? { |
| 59 | + bail!("cargo not found"); |
| 60 | + } |
| 61 | + |
| 62 | + Ok(path.into()) |
| 63 | +} |
| 64 | + |
| 65 | +/// Returns `true` if Cargo was found, `false` if no Cargo was found. |
| 66 | +/// |
| 67 | +/// Other versions will return an error. |
| 68 | +pub fn verify(path: &str, version: &str) -> Result<bool> { |
| 69 | + let mut cmd = Command::new(path); |
| 70 | + let cmd = cmd.arg("-V"); |
| 71 | + |
| 72 | + ui::print_cmd(cmd); |
| 73 | + |
| 74 | + let output = cmd.output(); |
| 75 | + if output |
| 76 | + .as_ref() |
| 77 | + .is_err_and(|e| e.kind() == ErrorKind::NotFound) |
| 78 | + { |
| 79 | + return Ok(false); |
| 80 | + } |
| 81 | + let output = output.context("failed to run cargo")?; |
| 82 | + ensure!(output.status.success(), "cargo failed: {}", output.status); |
| 83 | + |
| 84 | + let cmd_version = String::from_utf8(output.stdout).context("cargo output not valid UTF-8")?; |
| 85 | + |
| 86 | + let expected = format!("cargo {version}"); |
| 87 | + if !cmd_version.starts_with(&expected) { |
| 88 | + bail!("expected {expected}, got: {cmd_version}"); |
| 89 | + } |
| 90 | + |
| 91 | + Ok(true) |
| 92 | +} |
0 commit comments