diff --git a/README.md b/README.md index ee504fd..f7659d0 100644 --- a/README.md +++ b/README.md @@ -168,15 +168,25 @@ FOR /f "tokens=*" %i IN ('fnm env --use-on-cd') DO CALL %i ### Global Options ```sh -fnm [--shell=fish|bash|zsh] [--node-dist-mirror=URI] [--fnm-dir=DIR] [--log-level=quiet|error|info] +fnm [--shell=fish|bash|zsh] [--node-dist-mirror=URI] [--fnm-dir=DIR] [--log-level=quiet|error|info] [--arch=ARCH] ``` -- Providing `--shell=fish` will output the Fish-compliant version. Omitting it and `fnm` will try to infer the current shell based on the process tree +- Providing `--shell=fish` will output the Fish-compliant version. Omit it and `fnm` will try to infer the current shell based on the process tree - Providing `--node-dist-mirror="https://npm.taobao.org/dist"` will use the Chinese mirror of Node.js - Providing `--fnm-dir="/tmp/fnm"` will install and use versions in `/tmp/fnm` directory +- Providing `--arch=x64` will install Node binaries with `x86-64` architecture. Omit it and `fnm` will default to your computer's architecture. You can always use `fnm --help` to read the docs: +#### Apple Silicon +Until [upstream support for darwin-arm64](https://github.com/nodejs/node/issues/37309) is complete, `fnm` defaults to installing the `darwin-x64` architecture for your selected version to be run with Rosetta 2. + +Enable Rosetta 2 via terminal command: +```sh +softwareupdate --install-rosetta +``` +The `--arch` option overrides this default. + ### `fnm install [VERSION]` Installs `[VERSION]`. If no version provided, it will install the version specified in the `.node-version` or `.nvmrc` files located in the current working directory. diff --git a/src/arch.rs b/src/arch.rs new file mode 100644 index 0000000..b1c24cb --- /dev/null +++ b/src/arch.rs @@ -0,0 +1,94 @@ +#[derive(Clone, Debug)] +pub enum Arch { + X86, + X64, + Arm64, + Armv7l, + Ppc64le, + Ppc64, + S390x, +} + +#[cfg(unix)] +/// Get a sane default architecture for the platform. +pub fn default_str() -> &'static str { + use crate::system_info::{platform_arch, platform_name}; + + // TODO: Handle (arch, name, version) when Node v15+ supports darwin-arm64 + match (platform_name(), platform_arch()) { + ("darwin", "arm64") => "x64", + (_, arch) => arch, + } +} + +#[cfg(windows)] +/// Get a sane default architecture for the platform. +pub fn default_str() -> &'static str { + return crate::system_info::platform_arch(); +} + +impl Default for Arch { + fn default() -> Arch { + match default_str().parse() { + Ok(arch) => arch, + Err(e) => panic!("{}", e.details), + } + } +} + +impl std::str::FromStr for Arch { + type Err = ArchError; + fn from_str(s: &str) -> Result { + match s { + "x86" => Ok(Arch::X86), + "x64" => Ok(Arch::X64), + "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))), + } + } +} + +impl std::fmt::Display for Arch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let arch_str = match self { + Arch::X86 => String::from("x86"), + Arch::X64 => String::from("x64"), + Arch::Arm64 => String::from("arm64"), + Arch::Armv7l => String::from("armv7l"), + Arch::Ppc64le => String::from("ppc64le"), + Arch::Ppc64 => String::from("ppc64"), + Arch::S390x => String::from("s390x"), + }; + + write!(f, "{}", arch_str) + } +} + +#[derive(Debug)] +pub struct ArchError { + details: String, +} + +impl ArchError { + fn new(msg: &str) -> ArchError { + ArchError { + details: msg.to_string(), + } + } +} + +impl std::fmt::Display for ArchError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", 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 1169e0a..3d2c4be 100644 --- a/src/commands/install.rs +++ b/src/commands/install.rs @@ -46,6 +46,7 @@ impl super::command::Command for Install { fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> { let current_dir = std::env::current_dir().unwrap(); + let current_version = self .version()? .or_else(|| get_user_version_for_directory(current_dir)) @@ -89,12 +90,14 @@ impl super::command::Command for Install { .clone() } }; + let version_str = format!("Node {}", &version); outln!(config#Info, "Installing {}", version_str.cyan()); match install_node_dist( &version, &config.node_dist_mirror, config.installations_dir(), + &config.arch, ) { Err(err @ DownloaderError::VersionAlreadyInstalled { .. }) => { outln!(config#Error, "{} {}", "warning:".bold().yellow(), err); @@ -123,7 +126,7 @@ impl super::command::Command for Install { #[derive(Debug, Snafu)] pub enum Error { - #[snafu(display("Can't download the requested version: {}", source))] + #[snafu(display("Can't download the requested binary: {}", source))] DownloadError { source: DownloaderError, }, diff --git a/src/config.rs b/src/config.rs index af9eb01..3f261db 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use crate::arch; use crate::log_level::LogLevel; use dirs::home_dir; use structopt::StructOpt; @@ -31,6 +32,11 @@ pub struct FnmConfig { /// The log level of fnm commands #[structopt(long, env = "FNM_LOGLEVEL", default_value = "info", global = true)] log_level: LogLevel, + + /// Override the architecture of the installed Node binary. + /// Defaults to arch of fnm binary. + #[structopt(long, env = "FNM_ARCH", default_value, global = true)] + pub arch: arch::Arch, } impl Default for FnmConfig { @@ -40,6 +46,7 @@ impl Default for FnmConfig { base_dir: None, multishell_path: None, log_level: LogLevel::Info, + arch: Default::default(), } } } diff --git a/src/downloader.rs b/src/downloader.rs index 3588a2d..a5faa65 100644 --- a/src/downloader.rs +++ b/src/downloader.rs @@ -1,3 +1,4 @@ +use crate::arch::Arch; use crate::archive; use crate::archive::{Error as ExtractError, Extract}; use crate::directory_portal::DirectoryPortal; @@ -22,8 +23,15 @@ pub enum Error { }, #[snafu(display("The downloaded archive is empty"))] TarIsEmpty, - #[snafu(display("Can't find version upstream"))] - VersionNotFound, + #[snafu(display( + "{} for {} not found upstream.\nYou can `fnm ls-remote` to see available versions or try a different `--arch`.", + version, + arch + ))] + VersionNotFound { + version: Version, + arch: Arch, + }, #[snafu(display("Version already installed at {:?}", path))] VersionAlreadyInstalled { path: PathBuf, @@ -31,31 +39,30 @@ pub enum Error { } #[cfg(unix)] -fn filename_for_version(version: &Version) -> String { - use crate::system_info::{platform_arch, platform_name}; +fn filename_for_version(version: &Version, arch: &Arch) -> String { format!( "node-{node_ver}-{platform}-{arch}.tar.xz", node_ver = &version, - platform = platform_name(), - arch = platform_arch(), + platform = crate::system_info::platform_name(), + arch = arch, ) } #[cfg(windows)] -fn filename_for_version(version: &Version) -> String { +fn filename_for_version(version: &Version, arch: &Arch) -> String { format!( "node-{node_ver}-win-{arch}.zip", node_ver = &version, - arch = crate::system_info::platform_arch(), + arch = arch, ) } -fn download_url(base_url: &Url, version: &Version) -> Url { +fn download_url(base_url: &Url, version: &Version, arch: &Arch) -> Url { Url::parse(&format!( "{}/{}/{}", base_url.as_str().trim_end_matches('/'), version, - filename_for_version(version) + filename_for_version(version, arch) )) .unwrap() } @@ -77,6 +84,7 @@ pub fn install_node_dist>( version: &Version, node_dist_mirror: &Url, installations_dir: P, + arch: &Arch, ) -> Result<(), Error> { let installation_dir = PathBuf::from(installations_dir.as_ref()).join(version.v_str()); @@ -94,12 +102,15 @@ pub fn install_node_dist>( let portal = DirectoryPortal::new_in(&temp_installations_dir, installation_dir); - let url = download_url(node_dist_mirror, version); + let url = download_url(node_dist_mirror, version, arch); debug!("Going to call for {}", &url); let response = reqwest::blocking::get(url).context(HttpError)?; if response.status() == 404 { - return Err(Error::VersionNotFound); + return Err(Error::VersionNotFound { + version: version.clone(), + arch: arch.clone(), + }); } debug!("Extracting response..."); @@ -167,8 +178,10 @@ mod tests { fn install_in(path: &Path) -> PathBuf { let version = Version::parse("12.0.0").unwrap(); + let arch = Arch::X64; let node_dist_mirror = Url::parse("https://nodejs.org/dist/").unwrap(); - install_node_dist(&version, &node_dist_mirror, &path).expect("Can't install Node 12"); + install_node_dist(&version, &node_dist_mirror, &path, &arch) + .expect("Can't install Node 12"); let mut location_path = path.join(version.v_str()).join("installation"); diff --git a/src/main.rs b/src/main.rs index c87a6e4..c85ac2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod alias; +mod arch; mod archive; mod choose_version_for_user_input; mod cli;