use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
#[cfg(windows)]
pub const BIN_NAME: &str = "accent.exe";
#[cfg(not(windows))]
pub const BIN_NAME: &str = "accent";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArchiveKind {
TarGz,
Zip,
}
impl ArchiveKind {
pub fn extension(self) -> &'static str {
match self {
ArchiveKind::TarGz => "tar.gz",
ArchiveKind::Zip => "zip",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Libc {
Gnu,
Musl,
}
impl Libc {
pub fn as_str(self) -> &'static str {
match self {
Libc::Gnu => "gnu",
Libc::Musl => "musl",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Platform {
pub target: &'static str,
pub archive: ArchiveKind,
pub os: &'static str,
pub arch: &'static str,
pub libc: Option<Libc>,
}
impl Platform {
pub fn asset_name(&self, tag: &str) -> String {
format!("accent-{tag}-{}.{}", self.target, self.archive.extension())
}
}
pub fn detect() -> Result<Platform> {
let arch = std::env::consts::ARCH;
for_host_with_libc(std::env::consts::OS, arch, detect_libc(arch))
}
pub fn for_host(os: &str, arch: &str) -> Result<Platform> {
for_host_with_libc(os, arch, Libc::Gnu)
}
pub fn for_host_with_libc(os: &str, arch: &str, libc: Libc) -> Result<Platform> {
let arch_name = match arch {
"x86_64" => "x86_64",
"aarch64" => "aarch64",
other => bail!("unsupported architecture: {other} — Accent CMS ships x86_64 and aarch64"),
};
let (os_name, triple, archive, libc) = match (os, arch_name, libc) {
("linux", "x86_64", Libc::Gnu) => {
("linux", "x86_64-unknown-linux-gnu", ArchiveKind::TarGz, Some(Libc::Gnu))
}
("linux", "aarch64", Libc::Gnu) => {
("linux", "aarch64-unknown-linux-gnu", ArchiveKind::TarGz, Some(Libc::Gnu))
}
("linux", "x86_64", Libc::Musl) => {
("linux", "x86_64-unknown-linux-musl", ArchiveKind::TarGz, Some(Libc::Musl))
}
("linux", "aarch64", Libc::Musl) => {
("linux", "aarch64-unknown-linux-musl", ArchiveKind::TarGz, Some(Libc::Musl))
}
("macos", "x86_64", _) => ("macos", "x86_64-apple-darwin", ArchiveKind::TarGz, None),
("macos", "aarch64", _) => ("macos", "aarch64-apple-darwin", ArchiveKind::TarGz, None),
("windows", "x86_64", _) => ("windows", "x86_64-pc-windows-msvc", ArchiveKind::Zip, None),
("windows", "aarch64", _) => ("windows", "aarch64-pc-windows-msvc", ArchiveKind::Zip, None),
(other, _, _) => bail!(
"unsupported operating system: {other} — Accent CMS ships Linux, macOS and Windows builds"
),
};
Ok(Platform {
target: triple,
archive,
os: os_name,
arch: arch_name,
libc,
})
}
pub fn detect_libc(arch: &str) -> Libc {
if std::env::consts::OS != "linux" {
return Libc::Gnu;
}
let loader = Path::new("/lib")
.join(format!("ld-musl-{arch}.so.1"))
.exists();
let ldd = std::process::Command::new("ldd")
.arg("--version")
.output()
.ok()
.map(|out| {
let mut text = String::from_utf8_lossy(&out.stdout).into_owned();
text.push_str(&String::from_utf8_lossy(&out.stderr));
text
});
libc_from_markers(loader, ldd.as_deref())
}
pub fn libc_from_markers(musl_loader_present: bool, ldd_version_output: Option<&str>) -> Libc {
if musl_loader_present {
return Libc::Musl;
}
match ldd_version_output {
Some(text) if text.to_ascii_lowercase().contains("musl") => Libc::Musl,
_ => Libc::Gnu,
}
}
pub fn default_install_dir() -> Result<PathBuf> {
#[cfg(windows)]
{
let local = dirs::data_local_dir().context("could not determine %LOCALAPPDATA%")?;
Ok(local.join("accent"))
}
#[cfg(not(windows))]
{
let home = dirs::home_dir().context("could not determine the home directory")?;
Ok(home.join(".local").join("bin"))
}
}
pub fn data_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new();
if let Some(config) = dirs::config_dir() {
dirs.push(config.join("accent"));
}
if let Some(cache) = dirs::cache_dir() {
dirs.push(cache.join("accent"));
}
dirs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_every_published_target() {
let cases = [
(
"linux",
"x86_64",
"x86_64-unknown-linux-gnu",
ArchiveKind::TarGz,
),
(
"linux",
"aarch64",
"aarch64-unknown-linux-gnu",
ArchiveKind::TarGz,
),
("macos", "x86_64", "x86_64-apple-darwin", ArchiveKind::TarGz),
(
"macos",
"aarch64",
"aarch64-apple-darwin",
ArchiveKind::TarGz,
),
(
"windows",
"x86_64",
"x86_64-pc-windows-msvc",
ArchiveKind::Zip,
),
(
"windows",
"aarch64",
"aarch64-pc-windows-msvc",
ArchiveKind::Zip,
),
];
for (os, arch, triple, archive) in cases {
let plat = for_host(os, arch).unwrap();
assert_eq!(plat.target, triple);
assert_eq!(plat.archive, archive);
}
}
#[test]
fn linux_takes_the_musl_build_only_when_asked() {
let musl = for_host_with_libc("linux", "x86_64", Libc::Musl).unwrap();
assert_eq!(musl.target, "x86_64-unknown-linux-musl");
assert_eq!(musl.libc, Some(Libc::Musl));
let musl = for_host_with_libc("linux", "aarch64", Libc::Musl).unwrap();
assert_eq!(musl.target, "aarch64-unknown-linux-musl");
let gnu = for_host_with_libc("linux", "x86_64", Libc::Gnu).unwrap();
assert_eq!(gnu.target, "x86_64-unknown-linux-gnu");
assert_eq!(gnu.libc, Some(Libc::Gnu));
assert_eq!(for_host("linux", "aarch64").unwrap().libc, Some(Libc::Gnu));
let mac = for_host_with_libc("macos", "aarch64", Libc::Musl).unwrap();
assert_eq!(mac.target, "aarch64-apple-darwin");
assert_eq!(mac.libc, None);
let win = for_host_with_libc("windows", "x86_64", Libc::Musl).unwrap();
assert_eq!(win.target, "x86_64-pc-windows-msvc");
}
#[test]
fn the_musl_loader_decides_and_ldd_is_the_fallback() {
assert_eq!(libc_from_markers(true, None), Libc::Musl);
assert_eq!(
libc_from_markers(true, Some("ldd (GNU libc) 2.36")),
Libc::Musl
);
assert_eq!(
libc_from_markers(false, Some("musl libc (x86_64)\nVersion 1.2.5")),
Libc::Musl
);
assert_eq!(
libc_from_markers(false, Some("ldd (Debian GLIBC 2.36-9+deb12u10) 2.36")),
Libc::Gnu
);
assert_eq!(
libc_from_markers(false, Some("sh: ldd: not found")),
Libc::Gnu
);
assert_eq!(libc_from_markers(false, None), Libc::Gnu);
}
#[test]
fn musl_asset_names_follow_the_same_layout() {
let musl = for_host_with_libc("linux", "aarch64", Libc::Musl).unwrap();
assert_eq!(
musl.asset_name("v0.26.0"),
"accent-v0.26.0-aarch64-unknown-linux-musl.tar.gz"
);
}
#[test]
fn rejects_targets_that_are_not_published() {
assert!(for_host("linux", "riscv64").is_err());
assert!(for_host("freebsd", "x86_64").is_err());
}
#[test]
fn asset_names_match_the_published_layout() {
let unix = for_host("macos", "aarch64").unwrap();
assert_eq!(
unix.asset_name("v0.25.0"),
"accent-v0.25.0-aarch64-apple-darwin.tar.gz"
);
let win = for_host("windows", "x86_64").unwrap();
assert_eq!(
win.asset_name("v0.25.0"),
"accent-v0.25.0-x86_64-pc-windows-msvc.zip"
);
}
#[test]
fn the_host_running_these_tests_is_supported() {
detect().unwrap();
}
}