use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use serde::Deserialize;
use crate::fetch;
use crate::versions::Version;
const INDEX_URL: &str = "https://nodejs.org/dist/index.json";
fn platform() -> Result<(&'static str, &'static str)> {
let pair = match (std::env::consts::OS, std::env::consts::ARCH) {
("macos", "aarch64") => ("osx-arm64-tar", "darwin-arm64"),
("macos", "x86_64") => ("osx-x64-tar", "darwin-x64"),
("linux", _) if cfg!(target_env = "musl") => {
bail!(
"official Node.js builds require glibc; on musl systems use the distro package (e.g. apk add nodejs)"
)
}
("linux", "aarch64") => ("linux-arm64", "linux-arm64"),
("linux", "x86_64") => ("linux-x64", "linux-x64"),
("windows", "x86_64") => ("win-x64-zip", "win-x64"),
("windows", "aarch64") => ("win-arm64-zip", "win-arm64"),
(os, arch) => bail!("unsupported platform for node: {os}/{arch}"),
};
Ok(pair)
}
#[derive(Debug, Deserialize)]
struct IndexEntry {
version: String,
files: Vec<String>,
lts: Lts,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum Lts {
Codename(String),
NotLts(#[allow(dead_code)] bool),
}
pub struct AvailableBuild {
pub version: Version,
pub lts: Option<String>,
}
pub fn fetch_available() -> Result<Vec<AvailableBuild>> {
let (file_key, _) = platform()?;
let index: Vec<IndexEntry> = fetch::client()?
.get(INDEX_URL)
.send()
.context("failed to query nodejs.org/dist")?
.error_for_status()
.context("nodejs.org/dist index query failed")?
.json()
.context("failed to parse nodejs.org version index")?;
let mut builds: Vec<AvailableBuild> = index
.into_iter()
.filter(|entry| entry.files.iter().any(|f| f == file_key))
.filter_map(|entry| {
let version: Version = entry.version.strip_prefix('v')?.parse().ok()?;
let lts = match entry.lts {
Lts::Codename(name) => Some(name),
Lts::NotLts(_) => None,
};
Some(AvailableBuild { version, lts })
})
.collect();
builds.sort_by_key(|b| b.version);
Ok(builds)
}
pub fn install_build(version: &Version, dest: &Path) -> Result<()> {
let (_, suffix) = platform()?;
let dirname = format!("node-v{version}-{suffix}");
let ext = if cfg!(windows) { "zip" } else { "tar.gz" };
let archive_name = format!("{dirname}.{ext}");
let base = format!("https://nodejs.org/dist/v{version}");
let http = fetch::client()?;
let sums_url = format!("{base}/SHASUMS256.txt");
let sums = http
.get(&sums_url)
.send()
.and_then(|r| r.error_for_status())
.with_context(|| format!("failed to fetch checksums from {sums_url}"))?
.text()?;
let expected = fetch::find_sha256(&sums, &archive_name)
.with_context(|| format!("no SHASUMS256.txt entry for {archive_name}"))?;
let url = format!("{base}/{archive_name}");
eprintln!("downloading {url}");
let archive = fetch::download(&http, &url)?;
fetch::verify_sha256(&archive, &expected, &archive_name)?;
fetch::extract_archive_subdir(&archive, &archive_name, &dirname, dest)
}
pub fn npm_exe() -> &'static str {
if cfg!(windows) { "npm.cmd" } else { "npm" }
}
pub fn bin_dir(toolchain: &Path) -> PathBuf {
if cfg!(windows) {
toolchain.to_path_buf()
} else {
toolchain.join("bin")
}
}