Skip to main content

aube_runtime/
platform.rs

1//! Host platform detection and the two naming vocabularies it maps
2//! into:
3//!
4//! - **lockfile vocabulary** (pnpm / Node `process.platform`):
5//!   `darwin` / `linux` / `win32`, `x64` / `arm64`, `libc: musl`;
6//! - **dist-file vocabulary** (nodejs.org artifact names):
7//!   `node-v{V}-darwin-arm64.tar.gz`, `node-v{V}-linux-x64-musl.tar.gz`,
8//!   `node-v{V}-win-x64.zip`.
9
10use crate::error::Error;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Platform {
14    /// `process.platform` vocabulary: `darwin` / `linux` / `win32`.
15    pub os: String,
16    /// `process.arch` vocabulary: `x64` / `arm64` (others passed
17    /// through as-is from Rust's target arch mapping).
18    pub cpu: String,
19    /// `Some("musl")` on musl-libc Linux hosts.
20    pub libc: Option<String>,
21}
22
23impl Platform {
24    /// Detect the host platform.
25    ///
26    /// musl detection is a *runtime* check: aube ships static-musl
27    /// Linux binaries, so `cfg!(target_env = "musl")` is true even on
28    /// glibc hosts and cannot be trusted. The presence of musl's
29    /// dynamic loader (`/lib/ld-musl-<arch>.so.1`) is the signal mise
30    /// uses for the same decision.
31    pub fn current() -> Result<Platform, Error> {
32        let os = match std::env::consts::OS {
33            "macos" => "darwin",
34            "linux" => "linux",
35            "windows" => "win32",
36            other => {
37                return Err(Error::UnsupportedPlatform {
38                    platform: format!("{other}-{}", std::env::consts::ARCH),
39                });
40            }
41        };
42        let cpu = match std::env::consts::ARCH {
43            "x86_64" => "x64",
44            "aarch64" => "arm64",
45            "x86" => "x86",
46            "powerpc64" => "ppc64",
47            "s390x" => "s390x",
48            other => other,
49        };
50        let libc = (os == "linux" && detect_musl()).then(|| "musl".to_string());
51        Ok(Platform {
52            os: os.to_string(),
53            cpu: cpu.to_string(),
54            libc,
55        })
56    }
57
58    /// The platform segment of a dist artifact name:
59    /// `darwin-arm64`, `linux-x64-musl`, `win-x64`.
60    pub fn dist_slug(&self) -> String {
61        let os = if self.os == "win32" { "win" } else { &self.os };
62        let musl = if self.libc.as_deref() == Some("musl") {
63            "-musl"
64        } else {
65            ""
66        };
67        format!("{os}-{}{musl}", self.cpu)
68    }
69
70    /// The token nodejs.org's `index.json` `files[]` array uses for
71    /// this platform. macOS entries use the legacy `osx-*` prefix
72    /// with a `-tar` suffix; Windows uses `win-<arch>-zip`.
73    ///
74    /// musl builds never appear in the official index (they live on
75    /// unofficial-builds.nodejs.org, whose index has the same shape
76    /// but plain `linux-x64` tokens), so musl maps to the bare linux
77    /// token for `files[]` gating purposes.
78    pub fn index_files_token(&self) -> String {
79        match self.os.as_str() {
80            "darwin" => format!("osx-{}-tar", self.cpu),
81            "win32" => format!("win-{}-zip", self.cpu),
82            _ => format!("linux-{}", self.cpu),
83        }
84    }
85
86    /// Archive extension for this platform's dist artifact.
87    pub fn archive_ext(&self) -> &'static str {
88        if self.os == "win32" { "zip" } else { "tar.gz" }
89    }
90
91    /// pnpm's `archive:` vocabulary for this platform.
92    pub fn archive_kind(&self) -> &'static str {
93        if self.os == "win32" { "zip" } else { "tarball" }
94    }
95
96    /// Human-readable label for error messages.
97    pub fn label(&self) -> String {
98        match &self.libc {
99            Some(libc) => format!("{}-{} ({libc})", self.os, self.cpu),
100            None => format!("{}-{}", self.os, self.cpu),
101        }
102    }
103}
104
105#[cfg(target_os = "linux")]
106fn detect_musl() -> bool {
107    // Rust's arch names match musl's loader names for every
108    // architecture Node ships (x86_64, aarch64), so the constant is
109    // used verbatim.
110    std::path::Path::new(&format!("/lib/ld-musl-{}.so.1", std::env::consts::ARCH)).exists()
111}
112
113#[cfg(not(target_os = "linux"))]
114fn detect_musl() -> bool {
115    false
116}
117
118/// The artifact filename for `version` on `platform`:
119/// `node-v22.1.0-darwin-arm64.tar.gz`.
120pub fn artifact_filename(version: &node_semver::Version, platform: &Platform) -> String {
121    format!(
122        "node-v{version}-{}.{}",
123        platform.dist_slug(),
124        platform.archive_ext()
125    )
126}
127
128/// The top-level directory inside an artifact:
129/// `node-v22.1.0-darwin-arm64`.
130pub fn artifact_top_dir(version: &node_semver::Version, platform: &Platform) -> String {
131    format!("node-v{version}-{}", platform.dist_slug())
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    fn plat(os: &str, cpu: &str, libc: Option<&str>) -> Platform {
139        Platform {
140            os: os.into(),
141            cpu: cpu.into(),
142            libc: libc.map(String::from),
143        }
144    }
145
146    #[test]
147    fn dist_slugs() {
148        assert_eq!(plat("darwin", "arm64", None).dist_slug(), "darwin-arm64");
149        assert_eq!(plat("linux", "x64", None).dist_slug(), "linux-x64");
150        assert_eq!(
151            plat("linux", "x64", Some("musl")).dist_slug(),
152            "linux-x64-musl"
153        );
154        assert_eq!(plat("win32", "x64", None).dist_slug(), "win-x64");
155    }
156
157    #[test]
158    fn index_tokens() {
159        assert_eq!(
160            plat("darwin", "arm64", None).index_files_token(),
161            "osx-arm64-tar"
162        );
163        assert_eq!(
164            plat("win32", "x64", None).index_files_token(),
165            "win-x64-zip"
166        );
167        assert_eq!(
168            plat("linux", "arm64", None).index_files_token(),
169            "linux-arm64"
170        );
171        assert_eq!(
172            plat("linux", "x64", Some("musl")).index_files_token(),
173            "linux-x64"
174        );
175    }
176
177    #[test]
178    fn artifact_names() {
179        let v: node_semver::Version = "22.1.0".parse().unwrap();
180        assert_eq!(
181            artifact_filename(&v, &plat("win32", "x64", None)),
182            "node-v22.1.0-win-x64.zip"
183        );
184        assert_eq!(
185            artifact_top_dir(&v, &plat("darwin", "arm64", None)),
186            "node-v22.1.0-darwin-arm64"
187        );
188    }
189}