use std::path::{Path, PathBuf};
use serde::Deserialize;
use super::Layout;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Install {
Shell,
Homebrew,
Npm,
Pnpm,
Bun,
Npx,
Cargo,
Elsewhere,
}
#[derive(Debug, Clone, Default)]
pub struct Places {
pub versions: PathBuf,
pub receipt_prefix: Option<PathBuf>,
pub cargo_bin: Option<PathBuf>,
}
#[derive(Debug, Deserialize)]
struct Receipt {
install_prefix: PathBuf,
}
impl Places {
pub fn from_env(layout: &Layout) -> Self {
let home = std::env::var_os("HOME").map(PathBuf::from);
let config = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.filter(|dir| dir.is_absolute())
.or_else(|| home.as_ref().map(|home| home.join(".config")));
let receipt_prefix = config.and_then(|config| {
let text =
std::fs::read_to_string(config.join("argonauts-code/argonauts-code-receipt.json"))
.ok()?;
serde_json::from_str::<Receipt>(&text)
.ok()
.map(|receipt| receipt.install_prefix)
});
let cargo_bin = std::env::var_os("CARGO_HOME")
.map(PathBuf::from)
.or_else(|| home.map(|home| home.join(".cargo")))
.map(|cargo| cargo.join("bin"));
Self {
versions: layout.versions.clone(),
receipt_prefix,
cargo_bin,
}
}
}
impl Install {
pub fn detect(exe: &Path, places: &Places) -> Self {
let real = exe.canonicalize().unwrap_or_else(|_| exe.to_path_buf());
let under = |dir: &Path| {
let dir = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
real.starts_with(dir)
};
if !places.versions.as_os_str().is_empty() && under(&places.versions) {
return Self::Shell;
}
let text = real.to_string_lossy();
if text.contains("/Cellar/") {
return Self::Homebrew;
}
if text.contains("/_npx/") {
return Self::Npx;
}
if text.contains("/node_modules/") {
return if text.contains("/.bun/") {
Self::Bun
} else if text.contains("/pnpm/") || text.contains("/.pnpm/") {
Self::Pnpm
} else {
Self::Npm
};
}
if places.cargo_bin.as_deref().is_some_and(under) {
return Self::Cargo;
}
let installed = places.receipt_prefix.as_deref().is_some_and(|prefix| {
let prefix = prefix
.canonicalize()
.unwrap_or_else(|_| prefix.to_path_buf());
real.parent()
.is_some_and(|dir| dir == prefix || dir == prefix.join("bin"))
});
if installed {
return Self::Shell;
}
Self::Elsewhere
}
pub fn manager(self) -> Option<&'static str> {
match self {
Self::Homebrew => Some("Homebrew"),
Self::Npm => Some("npm"),
Self::Pnpm => Some("pnpm"),
Self::Bun => Some("Bun"),
Self::Cargo => Some("cargo"),
Self::Shell | Self::Npx | Self::Elsewhere => None,
}
}
pub fn command(self) -> Option<&'static str> {
match self {
Self::Homebrew => Some("brew upgrade argonauts-code"),
Self::Npm => Some("npm install -g argonauts-code@latest"),
Self::Pnpm => Some("pnpm add -g argonauts-code@latest"),
Self::Bun => Some("bun add -g argonauts-code@latest"),
Self::Cargo => Some("cargo install argonauts-code --locked"),
Self::Shell | Self::Npx | Self::Elsewhere => None,
}
}
}
pub const RELEASES: &str = concat!(env!("CARGO_PKG_REPOSITORY"), "/releases");
#[cfg(test)]
mod tests {
use super::*;
fn places(root: &Path) -> Places {
Places {
versions: root.join("share/argonauts/versions"),
receipt_prefix: None,
cargo_bin: Some(root.join(".cargo/bin")),
}
}
fn program(root: &Path, path: &str) -> PathBuf {
let path = root.join(path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, "").unwrap();
path
}
#[test]
fn each_way_of_installing_is_found_by_the_programs_real_place() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().canonicalize().unwrap();
let places = places(&root);
let cases = [
("share/argonauts/versions/0.1.0/argonauts", Install::Shell),
(
"opt/homebrew/Cellar/argonauts-code/0.1.0/bin/argonauts",
Install::Homebrew,
),
(
".npm/_npx/4f2a/node_modules/argonauts-code/bin/argonauts",
Install::Npx,
),
(
"lib/node_modules/argonauts-code/bin/argonauts",
Install::Npm,
),
(
"Library/pnpm/global/5/node_modules/argonauts-code/bin/argonauts",
Install::Pnpm,
),
(
".bun/install/global/node_modules/argonauts-code/bin/argonauts",
Install::Bun,
),
(".cargo/bin/argonauts", Install::Cargo),
("src/argonauts/target/release/argonauts", Install::Elsewhere),
];
for (path, install) in cases {
assert_eq!(
Install::detect(&program(&root, path), &places),
install,
"{path}"
);
}
}
#[test]
fn the_link_and_the_installers_own_file_update_themselves() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().canonicalize().unwrap();
let mut places = places(&root);
let version = program(&root, "share/argonauts/versions/0.1.1/argonauts");
std::fs::create_dir_all(root.join(".local/bin")).unwrap();
let link = root.join(".local/bin/argonauts");
std::os::unix::fs::symlink(&version, &link).unwrap();
assert_eq!(Install::detect(&link, &places), Install::Shell);
let installed = program(&root, "other/bin/argonauts");
assert_eq!(Install::detect(&installed, &places), Install::Elsewhere);
places.receipt_prefix = Some(root.join("other"));
assert_eq!(Install::detect(&installed, &places), Install::Shell);
}
#[test]
fn a_package_manager_names_its_own_command() {
assert_eq!(
Install::Homebrew.command(),
Some("brew upgrade argonauts-code")
);
assert_eq!(Install::Homebrew.manager(), Some("Homebrew"));
assert_eq!(Install::Npx.command(), None);
assert_eq!(Install::Shell.command(), None);
assert!(RELEASES.ends_with("/releases"), "{RELEASES}");
}
}