use std::path::Path;
pub const INSTALL_METHOD_ENV: &str = "CODEWHALE_INSTALL_METHOD";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InstallMethod {
Npm,
Homebrew,
Cargo,
Binary,
}
impl InstallMethod {
#[must_use]
pub fn detect(exe: &Path) -> Self {
if let Some(forced) = std::env::var(INSTALL_METHOD_ENV)
.ok()
.and_then(|raw| Self::from_token(&raw))
{
return forced;
}
Self::from_path(exe)
}
#[must_use]
pub fn from_path(exe: &Path) -> Self {
let components: Vec<String> = exe
.components()
.filter_map(|c| c.as_os_str().to_str())
.map(str::to_ascii_lowercase)
.collect();
let has = |name: &str| components.iter().any(|c| c == name);
if has("node_modules") {
return Self::Npm;
}
if has("cellar") || has(".linuxbrew") || has("linuxbrew") {
return Self::Homebrew;
}
if components
.windows(2)
.any(|pair| pair[0] == ".cargo" && pair[1] == "bin")
{
return Self::Cargo;
}
Self::Binary
}
fn from_token(raw: &str) -> Option<Self> {
match raw.trim().to_ascii_lowercase().as_str() {
"npm" => Some(Self::Npm),
"homebrew" | "brew" => Some(Self::Homebrew),
"cargo" => Some(Self::Cargo),
"binary" | "release" => Some(Self::Binary),
_ => None,
}
}
#[must_use]
pub fn update_command(self) -> &'static str {
match self {
Self::Npm => "npm install -g codewhale@latest",
Self::Homebrew => "brew upgrade codewhale",
Self::Cargo => "cargo install codewhale-cli --locked --force",
Self::Binary => "codewhale update",
}
}
#[must_use]
pub fn supports_self_update(self) -> bool {
matches!(self, Self::Binary)
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Npm => "npm",
Self::Homebrew => "Homebrew",
Self::Cargo => "cargo",
Self::Binary => "release binary",
}
}
}
#[must_use]
pub fn current_install_method() -> InstallMethod {
match std::env::current_exe() {
Ok(exe) => InstallMethod::detect(&exe),
Err(_) => InstallMethod::Binary,
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
#[test]
fn npm_global_install_is_detected_from_node_modules() {
let exe = PathBuf::from("/usr/local/lib/node_modules/codewhale/bin/codewhale");
assert_eq!(InstallMethod::from_path(&exe), InstallMethod::Npm);
assert_eq!(
InstallMethod::Npm.update_command(),
"npm install -g codewhale@latest"
);
assert!(!InstallMethod::Npm.supports_self_update());
}
#[test]
fn homebrew_install_is_detected_from_cellar_on_both_prefixes() {
for exe in [
"/opt/homebrew/Cellar/codewhale/0.9.8/bin/codewhale",
"/usr/local/Cellar/codewhale/0.9.8/bin/codewhale",
"/home/linuxbrew/.linuxbrew/Cellar/codewhale/0.9.8/bin/codewhale",
"/opt/homebrew/Cellar/deepseek-tui/0.9.4/bin/codewhale",
"/usr/local/Cellar/deepseek-tui/0.9.4/bin/codewhale",
"/home/linuxbrew/.linuxbrew/Cellar/deepseek-tui/0.9.4/bin/codewhale",
] {
assert_eq!(
InstallMethod::from_path(&PathBuf::from(exe)),
InstallMethod::Homebrew,
"{exe} should read as Homebrew"
);
}
assert_eq!(
InstallMethod::Homebrew.update_command(),
"brew upgrade codewhale"
);
assert!(!InstallMethod::Homebrew.supports_self_update());
}
#[test]
fn cargo_install_requires_the_cargo_bin_pair() {
assert_eq!(
InstallMethod::from_path(&PathBuf::from("/home/u/.cargo/bin/codewhale")),
InstallMethod::Cargo
);
assert_eq!(
InstallMethod::from_path(&PathBuf::from("/home/u/bin/codewhale")),
InstallMethod::Binary
);
assert!(!InstallMethod::Cargo.supports_self_update());
}
#[test]
fn npm_wins_over_an_enclosing_manager_prefix() {
let exe = PathBuf::from("/opt/homebrew/lib/node_modules/codewhale/bin/codewhale");
assert_eq!(InstallMethod::from_path(&exe), InstallMethod::Npm);
}
#[test]
fn termux_and_plain_release_binaries_self_update() {
for exe in [
"/data/data/com.termux/files/usr/bin/codewhale",
"/usr/local/bin/codewhale",
"/home/u/Downloads/codewhale",
] {
let method = InstallMethod::from_path(&PathBuf::from(exe));
assert_eq!(method, InstallMethod::Binary, "{exe} should self-update");
assert!(method.supports_self_update());
assert_eq!(method.update_command(), "codewhale update");
}
}
#[test]
fn env_tokens_map_to_methods_and_junk_is_ignored() {
assert_eq!(InstallMethod::from_token("npm"), Some(InstallMethod::Npm));
assert_eq!(
InstallMethod::from_token(" BREW "),
Some(InstallMethod::Homebrew)
);
assert_eq!(
InstallMethod::from_token("homebrew"),
Some(InstallMethod::Homebrew)
);
assert_eq!(
InstallMethod::from_token("cargo"),
Some(InstallMethod::Cargo)
);
assert_eq!(
InstallMethod::from_token("binary"),
Some(InstallMethod::Binary)
);
assert_eq!(InstallMethod::from_token("apt"), None);
}
}