diff --git a/src/install.rs b/src/install.rs index 95204ff..583aaaf 100644 --- a/src/install.rs +++ b/src/install.rs @@ -80,6 +80,8 @@ async fn install_prebuilt(config: &Config, args: &Cli) -> Result<()> { } async fn install_from_local(config: &Config, local_path: &Path, args: &Cli) -> Result<()> { + need_cmd("cargo")?; + if args.repo.is_some() || args.branch.is_some() || args.version.is_some() { warn!("--branch, --install, --use, and --repo arguments are ignored during local install"); } @@ -134,6 +136,9 @@ fn profile_target_dir(profile: &str) -> &str { } async fn install_from_source(config: &Config, repo: &str, args: &Cli) -> Result<()> { + need_cmd("git")?; + need_cmd("cargo")?; + let branch = if let Some(pr) = args.pr { format!("refs/pull/{pr}/head") } else { @@ -721,6 +726,11 @@ fn bin_name(name: &str) -> String { if cfg!(windows) { format!("{name}.exe") } else { name.to_string() } } +/// Fails with a clear error if `cmd` is not available on `PATH`. +fn need_cmd(cmd: &str) -> Result<()> { + which::which(cmd).map(|_| ()).map_err(|_| eyre::eyre!("need '{cmd}' (command not found)")) +} + /// Removes `path` if it exists, including dangling symlinks. /// /// Uses `symlink_metadata` so a broken symlink is still detected and removed; diff --git a/src/platform.rs b/src/platform.rs index 72bb21a..330e6da 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -56,26 +56,21 @@ pub(crate) enum Arch { } impl Arch { - pub(crate) fn detect() -> Result { - let arch = std::env::consts::ARCH; - match arch { - "x86_64" => { - if is_rosetta() { - Ok(Self::Arm64) - } else { - Ok(Self::Amd64) - } - } - "aarch64" | "arm64" => Ok(Self::Arm64), - _ => bail!("unsupported architecture: {arch}"), - } + pub(crate) fn detect() -> Self { + Self::normalize(std::env::consts::ARCH) } - pub(crate) fn from_str(s: &str) -> Result { + pub(crate) fn from_str(s: &str) -> Self { + Self::normalize(s) + } + + /// Normalizes an arch name, defaulting to `amd64` for anything unrecognized. + /// A literal `x86_64` resolves to `arm64` when running under Rosetta. + fn normalize(s: &str) -> Self { match s.to_lowercase().as_str() { - "amd64" | "x86_64" | "x64" => Ok(Self::Amd64), - "arm64" | "aarch64" => Ok(Self::Arm64), - _ => bail!("unsupported architecture: {s}"), + "x86_64" if is_rosetta() => Self::Arm64, + "arm64" | "aarch64" => Self::Arm64, + _ => Self::Amd64, } } @@ -103,8 +98,8 @@ impl Target { None => Platform::detect()?, }; let arch = match arch_override { - Some(a) => Arch::from_str(a)?, - None => Arch::detect()?, + Some(a) => Arch::from_str(a), + None => Arch::detect(), }; Ok(Self { platform, arch }) } @@ -155,9 +150,14 @@ mod tests { #[test] fn arch_from_str_cases() { - assert_eq!(Arch::from_str("amd64").unwrap(), Arch::Amd64); - assert_eq!(Arch::from_str("x86_64").unwrap(), Arch::Amd64); - assert_eq!(Arch::from_str("arm64").unwrap(), Arch::Arm64); - assert_eq!(Arch::from_str("aarch64").unwrap(), Arch::Arm64); + assert_eq!(Arch::from_str("amd64"), Arch::Amd64); + assert_eq!(Arch::from_str("arm64"), Arch::Arm64); + assert_eq!(Arch::from_str("aarch64"), Arch::Arm64); + // Unknown values fall back to amd64 rather than erroring. + assert_eq!(Arch::from_str("riscv64"), Arch::Amd64); + // `x86_64` is amd64 unless running under Rosetta (not the case in tests). + if !super::is_rosetta() { + assert_eq!(Arch::from_str("x86_64"), Arch::Amd64); + } } }