From 88ed52fa89dc02b799eaef4086ad1a9431eefc92 Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Mon, 18 May 2026 09:58:27 +0300 Subject: [PATCH] feat: allow any arch --- .changeset/arch-accept-arbitrary.md | 5 ++ src/arch.rs | 90 +++++++---------------------- src/commands/install.rs | 4 +- src/config.rs | 7 ++- src/downloader.rs | 14 ++--- 5 files changed, 39 insertions(+), 81 deletions(-) create mode 100644 .changeset/arch-accept-arbitrary.md diff --git a/.changeset/arch-accept-arbitrary.md b/.changeset/arch-accept-arbitrary.md new file mode 100644 index 000000000..668585a23 --- /dev/null +++ b/.changeset/arch-accept-arbitrary.md @@ -0,0 +1,5 @@ +--- +"fnm": minor +--- + +Accept any user-provided `FNM_ARCH` / `--arch` value. The string is passed through to the download URL, allowing use of mirrors (such as `https://unofficial-builds.nodejs.org/download/release/`) that publish targets beyond Node's official set — for example `FNM_ARCH=linux-riscv64`. Well-known values (`x64`, `arm64`, …) keep their existing behaviour. diff --git a/src/arch.rs b/src/arch.rs index cb73a5a5b..b83999a9f 100644 --- a/src/arch.rs +++ b/src/arch.rs @@ -1,76 +1,49 @@ use crate::version::Version; -use clap::ValueEnum; -#[derive(Clone, Copy, PartialEq, Eq, Debug, ValueEnum)] -pub enum Arch { - X86, - X64, - X64Musl, - X64Glibc217, - Arm64, - Armv7l, - Ppc64le, - Ppc64, - S390x, -} +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Arch(String); impl Arch { - pub fn as_str(self) -> &'static str { - match self { - Arch::X86 => "x86", - Arch::X64 => "x64", - Arch::X64Musl => "x64-musl", - Arch::X64Glibc217 => "x64-glibc-217", - Arch::Arm64 => "arm64", - Arch::Armv7l => "armv7l", - Arch::Ppc64le => "ppc64le", - Arch::Ppc64 => "ppc64", - Arch::S390x => "s390x", - } + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } + + pub fn x64() -> Self { + Self::new("x64") + } + + pub fn as_str(&self) -> &str { + &self.0 } } #[cfg(unix)] /// handle common case: Apple Silicon / Node < 16 -pub fn get_safe_arch(arch: Arch, version: &Version) -> Arch { +pub fn get_safe_arch(arch: &Arch, version: &Version) -> Arch { use crate::system_info::{platform_arch, platform_name}; match (platform_name(), platform_arch(), version) { - ("darwin", "arm64", Version::Semver(v)) if v.major < 16 => Arch::X64, - _ => arch, + ("darwin", "arm64", Version::Semver(v)) if v.major < 16 => Arch::x64(), + _ => arch.clone(), } } #[cfg(windows)] /// handle common case: Apple Silicon / Node < 16 -pub fn get_safe_arch(arch: Arch, _version: &Version) -> Arch { - arch +pub fn get_safe_arch(arch: &Arch, _version: &Version) -> Arch { + arch.clone() } impl Default for Arch { fn default() -> Arch { - match crate::system_info::platform_arch().parse() { - Ok(arch) => arch, - Err(e) => panic!("{}", e.details), - } + Arch::new(crate::system_info::platform_arch()) } } impl std::str::FromStr for Arch { - type Err = ArchError; + type Err = std::convert::Infallible; fn from_str(s: &str) -> Result { - match s { - "x86" => Ok(Arch::X86), - "x64" => Ok(Arch::X64), - "x64-musl" => Ok(Arch::X64Musl), - "x64-glibc-217" => Ok(Arch::X64Glibc217), - "arm64" => Ok(Arch::Arm64), - "armv7l" => Ok(Arch::Armv7l), - "ppc64le" => Ok(Arch::Ppc64le), - "ppc64" => Ok(Arch::Ppc64), - "s390x" => Ok(Arch::S390x), - unknown => Err(ArchError::new(format!("Unknown Arch: {unknown}"))), - } + Ok(Arch::new(s)) } } @@ -79,26 +52,3 @@ impl std::fmt::Display for Arch { f.write_str(self.as_str()) } } - -#[derive(Debug)] -pub struct ArchError { - details: String, -} - -impl ArchError { - fn new(msg: String) -> ArchError { - ArchError { details: msg } - } -} - -impl std::fmt::Display for ArchError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.details) - } -} - -impl std::error::Error for ArchError { - fn description(&self) -> &str { - &self.details - } -} diff --git a/src/commands/install.rs b/src/commands/install.rs index 594fcff87..1ccf1a5d3 100644 --- a/src/commands/install.rs +++ b/src/commands/install.rs @@ -133,7 +133,7 @@ impl Command for Install { }; // Automatically swap Apple Silicon to x64 arch for appropriate versions. - let safe_arch = get_safe_arch(config.arch, &version); + let safe_arch = get_safe_arch(&config.arch, &version); let version_str = format!("Node {}", &version); outln!( @@ -148,7 +148,7 @@ impl Command for Install { &version, &config.node_dist_mirror, config.installations_dir(), - safe_arch, + &safe_arch, show_progress, ) { Err(err @ DownloaderError::VersionAlreadyInstalled { .. }) => { diff --git a/src/config.rs b/src/config.rs index 6d482f9ed..5f26c093d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -43,7 +43,10 @@ pub struct FnmConfig { log_level: LogLevel, /// Override the architecture of the installed Node binary. - /// Defaults to arch of fnm binary. + /// Defaults to arch of fnm binary. Any string is accepted (it is + /// substituted directly into the download URL); well-known values are + /// `x86`, `x64`, `x64-musl`, `x64-glibc-217`, `arm64`, + /// `armv7l`, `ppc64le`, `ppc64`, `s390x`. #[clap( long, env = "FNM_ARCH", @@ -103,7 +106,7 @@ pub struct FnmConfig { impl Default for FnmConfig { fn default() -> Self { Self { - node_dist_mirror: Url::parse("https://nodejs.org/dist/").unwrap(), + node_dist_mirror: Url::parse("https://nodejs.org/dist").unwrap(), base_dir: None, multishell_path: None, log_level: LogLevel::Info, diff --git a/src/downloader.rs b/src/downloader.rs index 6fb6b6c25..0b57d7b44 100644 --- a/src/downloader.rs +++ b/src/downloader.rs @@ -36,7 +36,7 @@ pub enum Error { } #[cfg(unix)] -fn filename_for_version(version: &Version, arch: Arch, ext: &str) -> String { +fn filename_for_version(version: &Version, arch: &Arch, ext: &str) -> String { format!( "node-{node_ver}-{platform}-{arch}.{ext}", node_ver = &version, @@ -47,7 +47,7 @@ fn filename_for_version(version: &Version, arch: Arch, ext: &str) -> String { } #[cfg(windows)] -fn filename_for_version(version: &Version, arch: Arch, ext: &str) -> String { +fn filename_for_version(version: &Version, arch: &Arch, ext: &str) -> String { format!( "node-{node_ver}-win-{arch}.{ext}", node_ver = &version, @@ -56,7 +56,7 @@ fn filename_for_version(version: &Version, arch: Arch, ext: &str) -> String { ) } -fn download_url(base_url: &Url, version: &Version, arch: Arch, ext: &str) -> Url { +fn download_url(base_url: &Url, version: &Version, arch: &Arch, ext: &str) -> Url { Url::parse(&format!( "{}/{}/{}", base_url.as_str().trim_end_matches('/'), @@ -71,7 +71,7 @@ pub fn install_node_dist>( version: &Version, node_dist_mirror: &Url, installations_dir: P, - arch: Arch, + arch: &Arch, show_progress: bool, ) -> Result<(), Error> { let installation_dir = PathBuf::from(installations_dir.as_ref()).join(version.v_str()); @@ -125,7 +125,7 @@ pub fn install_node_dist>( Err(Error::VersionNotFound { version: version.clone(), - arch, + arch: arch.clone(), }) } @@ -173,9 +173,9 @@ mod tests { fn install_in(path: &Path) -> PathBuf { let version = Version::parse("12.0.0").unwrap(); - let arch = Arch::X64; + let arch = Arch::x64(); let node_dist_mirror = Url::parse("https://nodejs.org/dist/").unwrap(); - install_node_dist(&version, &node_dist_mirror, path, arch, false) + install_node_dist(&version, &node_dist_mirror, path, &arch, false) .expect("Can't install Node 12"); let mut location_path = path.join(version.v_str()).join("installation");