diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8ade91ebfc..b821e5f1a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -163,7 +163,13 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install zsh/fish/direnv/fd - run: sudo apt-get update; sudo apt-get install zsh fish direnv fd-find + run: sudo apt-get update; sudo apt-get install \ + direnv \ + fd-find \ + fish \ + libstdc++6 \ + musl \ + zsh - name: Install fd-find run: | mkdir -p "$HOME/.local/bin" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7fff11a37e..fdc1212bf7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -148,6 +148,8 @@ jobs: direnv \ fd-find \ fish \ + libstdc++6 \ + musl \ pipx \ python3-venv \ zsh diff --git a/docs/lang/node.md b/docs/lang/node.md index a54cb5534c..495a6b2da9 100644 --- a/docs/lang/node.md +++ b/docs/lang/node.md @@ -57,3 +57,26 @@ mise uses a `.tool-versions` or `.mise.toml` file for auto-switching between sof You cannot install/use a plugin named "nodejs". If you attempt this, mise will just rename it to "node". See the [FAQ](https://github.com/jdx/mise#what-is-the-difference-between-nodejs-and-node-or-golang-and-go) for an explanation. + +## Unofficial Builds + +Nodejs.org offers a set of [unofficial builds](https://unofficial-builds.nodejs.org/) which are +compatible with some platforms are not supported by the official binaries. These are a nice alternative to +compiling from source for these platforms. + +To use, first set the mirror url to point to the unofficial builds: + +```sh +mise settings set node.mirror_url https://unofficial-builds.nodejs.org/download/release/ +``` + +If your goal is to simply support an alternative arch/os like linux-loong64 or linux-armv6l, this is +all that is required. Node also provides flavors such as musl or glibc-217 (an older glibc version +than what the official binaries are built with). + +To use these, set `node.flavor`: + +```sh +mise settings set node.flavor musl +mise settings set node.flavor glibc-217 +``` diff --git a/e2e/plugins/core/test_node b/e2e/plugins/core/test_node index 63e90f19b3..e24439fd31 100644 --- a/e2e/plugins/core/test_node +++ b/e2e/plugins/core/test_node @@ -31,3 +31,10 @@ assert_not_contains "mise plugins --user" "node" # disable nodejs plugin assert_not_contains "MISE_DISABLE_TOOLS=node mise plugins --core" "node" + +if [[ "$(uname -s)-$(uname -m)" == "Linux-x86_64" ]]; then + # test unofficial builds + mise settings set node.mirror_url https://unofficial-builds.nodejs.org/download/release/ + mise settings set node.flavor musl + mise install node -f +fi diff --git a/schema/mise.json b/schema/mise.json index 7532b8e03b..9f862d7e52 100644 --- a/schema/mise.json +++ b/schema/mise.json @@ -434,6 +434,26 @@ "description": "path to file containing shorthand mappings", "type": "string" }, + "node": { + "description": "settings specific to node", + "type": "object", + "additionalProperties": false, + "properties": { + "compile": { + "type": "boolean", + "description": "do not use precompiled binaries for node" + }, + "flavor": { + "type": "string", + "description": "node flavor to use, generally used for unofficial builds" + }, + "mirror_url": { + "type": "string", + "description": "url to use as a mirror for node downloads", + "default": "https://nodejs.org/dist" + } + } + }, "ruby": { "description": "settings specific to ruby", "type": "object", diff --git a/src/backend/mod.rs b/src/backend/mod.rs index bef2dc66f2..8bcdcb4dbd 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -199,7 +199,10 @@ pub trait Backend: Debug + Send + Sync { false } }) - .collect(); + .collect_vec(); + if versions.is_empty() { + warn!("No versions found for {}", self.id()); + } Ok(versions) } fn _list_remote_versions(&self) -> eyre::Result>; diff --git a/src/cli/settings/ls.rs b/src/cli/settings/ls.rs index 9f11d5f689..dae272a0a5 100644 --- a/src/cli/settings/ls.rs +++ b/src/cli/settings/ls.rs @@ -83,7 +83,6 @@ mod tests { legacy_version_file = true legacy_version_file_disable_tools = [] libgit2 = true - node_compile = false not_found_auto_install = true paranoid = false pipx_uvx = false @@ -98,6 +97,8 @@ mod tests { vfox = false yes = true + [node] + [ruby] default_packages_file = "~/.default-gems" ruby_build_repo = "https://github.com/rbenv/ruby-build.git" @@ -139,7 +140,7 @@ mod tests { legacy_version_file legacy_version_file_disable_tools libgit2 - node_compile + node not_found_auto_install paranoid pipx_uvx diff --git a/src/cli/settings/set.rs b/src/cli/settings/set.rs index d0360931c3..f267be517c 100644 --- a/src/cli/settings/set.rs +++ b/src/cli/settings/set.rs @@ -46,7 +46,9 @@ impl SettingsSet { self.value.split(',').map(|s| s.to_string()).collect() } "libgit2" => parse_bool(&self.value)?, - "node_compile" => parse_bool(&self.value)?, + "node.compile" => parse_bool(&self.value)?, + "node.flavor" => self.value.into(), + "node.mirror_url" => self.value.into(), "not_found_auto_install" => parse_bool(&self.value)?, "paranoid" => parse_bool(&self.value)?, "pipx_uvx" => parse_bool(&self.value)?, @@ -57,6 +59,14 @@ impl SettingsSet { "python_venv_auto_create" => parse_bool(&self.value)?, "quiet" => parse_bool(&self.value)?, "raw" => parse_bool(&self.value)?, + "ruby.apply_patches" => self.value.into(), + "ruby.default_packages_file" => self.value.into(), + "ruby.ruby_build_repo" => self.value.into(), + "ruby.ruby_build_opts" => self.value.into(), + "ruby.ruby_install" => parse_bool(&self.value)?, + "ruby.ruby_install_repo" => self.value.into(), + "ruby.ruby_install_opts" => self.value.into(), + "ruby.verbose_install" => parse_bool(&self.value)?, "shorthands_file" => self.value.into(), "status.missing_tools" => self.value.into(), "status.show_env" => parse_bool(&self.value)?, @@ -77,13 +87,14 @@ impl SettingsSet { config["settings"] = toml_edit::Item::Table(toml_edit::Table::new()); } let settings = config["settings"].as_table_mut().unwrap(); - if self.setting.as_str().starts_with("status.") { + if self.setting.as_str().contains(".") { + let mut parts = self.setting.splitn(2, '.'); let status = settings - .entry("status") + .entry(parts.next().unwrap()) .or_insert(toml_edit::Item::Table(toml_edit::Table::new())) .as_table_mut() .unwrap(); - status.insert(&self.setting[7..], toml_edit::Item::Value(value)); + status.insert(parts.next().unwrap(), toml_edit::Item::Value(value)); } else { settings.insert(&self.setting, toml_edit::Item::Value(value)); } @@ -159,7 +170,6 @@ pub mod tests { legacy_version_file = false legacy_version_file_disable_tools = [] libgit2 = true - node_compile = false not_found_auto_install = true paranoid = false pipx_uvx = false @@ -174,6 +184,8 @@ pub mod tests { vfox = false yes = true + [node] + [ruby] default_packages_file = "~/.default-gems" ruby_build_repo = "https://github.com/rbenv/ruby-build.git" diff --git a/src/cli/settings/unset.rs b/src/cli/settings/unset.rs index 8f1936fdea..20a1a81e77 100644 --- a/src/cli/settings/unset.rs +++ b/src/cli/settings/unset.rs @@ -74,7 +74,6 @@ mod tests { legacy_version_file = true legacy_version_file_disable_tools = [] libgit2 = true - node_compile = false not_found_auto_install = true paranoid = false pipx_uvx = false @@ -89,6 +88,8 @@ mod tests { vfox = false yes = true + [node] + [ruby] default_packages_file = "~/.default-gems" ruby_build_repo = "https://github.com/rbenv/ruby-build.git" diff --git a/src/config/settings.rs b/src/config/settings.rs index b640e70f19..67f7ef8e3f 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -14,6 +14,9 @@ use std::iter::once; use std::path::PathBuf; use std::sync::{Arc, Mutex, RwLock}; use std::time::Duration; +use url::Url; + +pub static SETTINGS: Lazy> = Lazy::new(Settings::get); #[rustfmt::skip] #[derive(Config, Default, Debug, Clone, Serialize)] @@ -93,8 +96,8 @@ pub struct Settings { pub legacy_version_file_disable_tools: BTreeSet, #[config(env = "MISE_LIBGIT2", default = true)] pub libgit2: bool, - #[config(env = "MISE_NODE_COMPILE", default = false)] - pub node_compile: bool, + #[config(nested)] + pub node: SettingsNode, #[config(env = "MISE_NOT_FOUND_AUTO_INSTALL", default = true)] pub not_found_auto_install: bool, #[config(env = "MISE_PARANOID", default = false)] @@ -163,6 +166,19 @@ pub struct Settings { pub python_venv_auto_create: bool, } +#[derive(Config, Default, Debug, Clone, Serialize)] +#[config(partial_attr(derive(Clone, Serialize, Default)))] +#[config(partial_attr(serde(deny_unknown_fields)))] +#[rustfmt::skip] +pub struct SettingsNode { + #[config(env = "MISE_NODE_COMPILE")] + pub compile: Option, + #[config(env = "MISE_NODE_FLAVOR")] + pub flavor: Option, + #[config(env = "MISE_NODE_MIRROR_URL")] + pub mirror_url: Option +} + #[derive(Config, Default, Debug, Clone, Serialize)] #[config(partial_attr(derive(Clone, Serialize, Default)))] #[config(partial_attr(serde(deny_unknown_fields)))] @@ -221,7 +237,7 @@ pub enum SettingsStatusMissingTools { pub type SettingsPartial = ::Partial; -static SETTINGS: RwLock>> = RwLock::new(None); +static BASE_SETTINGS: RwLock>> = RwLock::new(None); static CLI_SETTINGS: Mutex> = Mutex::new(None); static DEFAULT_SETTINGS: Lazy = Lazy::new(|| { let mut s = SettingsPartial::empty(); @@ -245,7 +261,7 @@ impl Settings { Self::try_get().unwrap() } pub fn try_get() -> Result> { - if let Some(settings) = SETTINGS.read().unwrap().as_ref() { + if let Some(settings) = BASE_SETTINGS.read().unwrap().as_ref() { return Ok(settings.clone()); } @@ -306,13 +322,13 @@ impl Settings { settings.yes = true; } if settings.all_compile { - settings.node_compile = true; + settings.node.compile = Some(true); if settings.python_compile.is_none() { settings.python_compile = Some(true); } } let settings = Arc::new(settings); - *SETTINGS.write().unwrap() = Some(settings.clone()); + *BASE_SETTINGS.write().unwrap() = Some(settings.clone()); Ok(settings) } pub fn add_cli_matches(m: &clap::ArgMatches) { @@ -429,7 +445,7 @@ impl Settings { pub fn reset(cli_settings: Option) { *CLI_SETTINGS.lock().unwrap() = cli_settings; - *SETTINGS.write().unwrap() = None; + *BASE_SETTINGS.write().unwrap() = None; } pub fn ensure_experimental(&self, what: &str) -> Result<()> { @@ -494,3 +510,16 @@ impl Display for Settings { pub fn ensure_experimental(what: &str) -> Result<()> { Settings::get().ensure_experimental(what) } + +pub const DEFAULT_NODE_MIRROR_URL: &str = "https://nodejs.org/dist/"; + +impl SettingsNode { + pub fn mirror_url(&self) -> Url { + let s = self + .mirror_url + .clone() + .or(env::var("NODE_BUILD_MIRROR_URL").ok()) + .unwrap_or_else(|| DEFAULT_NODE_MIRROR_URL.to_string()); + Url::parse(&s).unwrap() + } +} diff --git a/src/env.rs b/src/env.rs index e3670f7a45..b108586c96 100644 --- a/src/env.rs +++ b/src/env.rs @@ -9,7 +9,6 @@ use std::{path, process}; use itertools::Itertools; use log::LevelFilter; use once_cell::sync::Lazy; -use url::Url; use crate::cli::args::ProfileArg; use crate::duration::HOURLY; @@ -173,11 +172,6 @@ pub static PYENV_ROOT: Lazy = Lazy::new(|| var_path("PYENV_ROOT").unwrap_or_else(|| HOME.join(".pyenv"))); // node -pub static MISE_NODE_MIRROR_URL: Lazy = Lazy::new(|| { - var_url("MISE_NODE_MIRROR_URL") - .or_else(|| var_url("NODE_BUILD_MIRROR_URL")) - .unwrap_or_else(|| Url::parse("https://nodejs.org/dist/").unwrap()) -}); pub static MISE_NODE_CONCURRENCY: Lazy> = Lazy::new(|| { var("MISE_NODE_CONCURRENCY") .ok() @@ -279,10 +273,6 @@ pub fn var_path(key: &str) -> Option { var_os(key).map(PathBuf::from).map(replace_path) } -fn var_url(key: &str) -> Option { - var(key).ok().map(|v| Url::parse(&v).unwrap()) -} - fn var_duration(key: &str) -> Option { var(key) .ok() diff --git a/src/plugins/core/node.rs b/src/plugins/core/node.rs index d48a8ad611..df4a88b554 100644 --- a/src/plugins/core/node.rs +++ b/src/plugins/core/node.rs @@ -10,8 +10,8 @@ use crate::backend::Backend; use crate::build_time::built_info; use crate::cli::args::BackendArg; use crate::cmd::CmdLineRunner; +use crate::config::settings::{DEFAULT_NODE_MIRROR_URL, SETTINGS}; use crate::config::{Config, Settings}; -use crate::env::{MISE_NODE_MIRROR_URL, PATH_KEY}; use crate::http::{HTTP, HTTP_FETCH}; use crate::install_context::InstallContext; use crate::plugins::core::CorePlugin; @@ -32,9 +32,8 @@ impl NodePlugin { } fn fetch_remote_versions(&self) -> Result> { - let node_url_overridden = env::var("MISE_NODE_MIRROR_URL") - .or(env::var("NODE_BUILD_MIRROR_URL")) - .is_ok(); + let settings = Settings::get(); + let node_url_overridden = settings.node.mirror_url().as_str() != DEFAULT_NODE_MIRROR_URL; if !node_url_overridden { match self.core.fetch_remote_versions_from_mise() { Ok(Some(versions)) => return Ok(versions), @@ -42,12 +41,22 @@ impl NodePlugin { Err(e) => warn!("failed to fetch remote versions: {}", e), } } - self.fetch_remote_versions_from_node(&MISE_NODE_MIRROR_URL) + self.fetch_remote_versions_from_node(&settings.node.mirror_url()) } fn fetch_remote_versions_from_node(&self, base: &Url) -> Result> { + let settings = Settings::get(); let versions = HTTP_FETCH .json::, _>(base.join("index.json")?)? .into_iter() + .filter(|v| { + if let Some(flavor) = &settings.node.flavor { + v.files + .iter() + .any(|f| f == &format!("{}-{}-{}", os(), arch(), flavor)) + } else { + true + } + }) .map(|v| { if regex!(r"^v\d+\.").is_match(&v.version) { v.version.strip_prefix('v').unwrap().to_string() @@ -61,13 +70,17 @@ impl NodePlugin { } fn install_precompiled(&self, ctx: &InstallContext, opts: &BuildOpts) -> Result<()> { + let settings = Settings::get(); match self.fetch_tarball( ctx.pr.as_ref(), &opts.binary_tarball_url, &opts.binary_tarball_path, &opts.version, ) { - Err(e) if matches!(http::error_code(&e), Some(404)) => { + Err(e) + if settings.node.compile != Some(false) + && matches!(http::error_code(&e), Some(404)) => + { debug!("precompiled node not found"); return self.install_compiled(ctx, opts); } @@ -78,8 +91,10 @@ impl NodePlugin { let tmp_extract_path = tempdir_in(opts.install_path.parent().unwrap())?; file::untar(&opts.binary_tarball_path, tmp_extract_path.path())?; file::remove_all(&opts.install_path)?; - let slug = format!("node-v{}-{}-{}", &opts.version, os(), arch()); - file::rename(tmp_extract_path.path().join(slug), &opts.install_path)?; + file::rename( + tmp_extract_path.path().join(slug(&opts.version)), + &opts.install_path, + )?; Ok(()) } @@ -100,8 +115,10 @@ impl NodePlugin { let tmp_extract_path = tempdir_in(opts.install_path.parent().unwrap())?; file::unzip(&opts.binary_tarball_path, tmp_extract_path.path())?; file::remove_all(&opts.install_path)?; - let slug = format!("node-v{}-{}-{}", &opts.version, os(), arch()); - file::rename(tmp_extract_path.path().join(slug), &opts.install_path)?; + file::rename( + tmp_extract_path.path().join(slug(&opts.version)), + &opts.install_path, + )?; Ok(()) } @@ -218,7 +235,7 @@ impl NodePlugin { .arg("--global") .arg(package) .envs(config.env()?) - .env(&*PATH_KEY, CorePlugin::path_env_with_tv_path(tv)?) + .env(&*env::PATH_KEY, CorePlugin::path_env_with_tv_path(tv)?) .execute()?; } Ok(()) @@ -237,7 +254,7 @@ impl NodePlugin { CmdLineRunner::new(corepack) .with_pr(pr) .arg("enable") - .env(&*PATH_KEY, CorePlugin::path_env_with_tv_path(tv)?) + .env(&*env::PATH_KEY, CorePlugin::path_env_with_tv_path(tv)?) .execute()?; Ok(()) } @@ -254,7 +271,7 @@ impl NodePlugin { fn test_npm(&self, config: &Config, tv: &ToolVersion, pr: &dyn SingleReport) -> Result<()> { pr.set_message("npm -v".into()); CmdLineRunner::new(self.npm_path(tv)) - .env(&*PATH_KEY, CorePlugin::path_env_with_tv_path(tv)?) + .env(&*env::PATH_KEY, CorePlugin::path_env_with_tv_path(tv)?) .with_pr(pr) .arg("-v") .envs(config.env()?) @@ -263,7 +280,11 @@ impl NodePlugin { fn shasums_url(&self, v: &str) -> Result { // let url = MISE_NODE_MIRROR_URL.join(&format!("v{v}/SHASUMS256.txt.asc"))?; - let url = MISE_NODE_MIRROR_URL.join(&format!("v{v}/SHASUMS256.txt"))?; + let settings = Settings::get(); + let url = settings + .node + .mirror_url() + .join(&format!("v{v}/SHASUMS256.txt"))?; Ok(url) } } @@ -334,7 +355,7 @@ impl Backend for NodePlugin { trace!("node build opts: {:#?}", opts); if cfg!(windows) { self.install_windows(ctx, &opts)?; - } else if settings.node_compile { + } else if settings.node.compile == Some(true) { self.install_compiled(ctx, &opts)?; } else { self.install_precompiled(ctx, &opts)?; @@ -383,10 +404,11 @@ impl BuildOpts { let install_path = ctx.tv.install_path(); let source_tarball_name = format!("node-v{v}.tar.gz"); + let slug = slug(v); #[cfg(windows)] - let binary_tarball_name = format!("node-v{v}-{}-{}.zip", os(), arch()); + let binary_tarball_name = format!("{slug}.zip"); #[cfg(not(windows))] - let binary_tarball_name = format!("node-v{v}-{}-{}.tar.gz", os(), arch()); + let binary_tarball_name = format!("{slug}.tar.gz"); Ok(Self { version: v.clone(), @@ -396,11 +418,15 @@ impl BuildOpts { make_cmd: make_cmd(), make_install_cmd: make_install_cmd(), source_tarball_path: ctx.tv.download_path().join(&source_tarball_name), - source_tarball_url: env::MISE_NODE_MIRROR_URL + source_tarball_url: SETTINGS + .node + .mirror_url() .join(&format!("v{v}/{source_tarball_name}"))?, source_tarball_name, binary_tarball_path: ctx.tv.download_path().join(&binary_tarball_name), - binary_tarball_url: env::MISE_NODE_MIRROR_URL + binary_tarball_url: SETTINGS + .node + .mirror_url() .join(&format!("v{v}/{binary_tarball_name}"))?, binary_tarball_name, install_path, @@ -456,7 +482,15 @@ fn arch() -> &'static str { } else if cfg!(target_arch = "x86_64") { "x64" } else if cfg!(target_arch = "arm") { - "armv7l" + if cfg!(target_feature = "v6") { + "armv6l" + } else { + "armv7l" + } + } else if cfg!(target_arch = "loongarch64") { + "loong64" + } else if cfg!(target_arch = "riscv64") { + "riscv64" } else if cfg!(target_arch = "aarch64") { "arm64" } else { @@ -464,7 +498,16 @@ fn arch() -> &'static str { } } +fn slug(v: &str) -> String { + if let Some(flavor) = &SETTINGS.node.flavor { + format!("node-v{v}-{}-{}-{flavor}", os(), arch()) + } else { + format!("node-v{v}-{}-{}", os(), arch()) + } +} + #[derive(Debug, Deserialize)] struct NodeVersion { version: String, + files: Vec, } diff --git a/src/toolset/mod.rs b/src/toolset/mod.rs index dc959b0a24..0cf2b1b0a9 100644 --- a/src/toolset/mod.rs +++ b/src/toolset/mod.rs @@ -202,17 +202,17 @@ impl Toolset { let mut installed = vec![]; while let Some((t, versions)) = next_job() { installing.lock().unwrap().insert(t.id().into()); - for tv in versions { + for tr in versions { // TODO: this logic should be able to be removed now I think - for dep in t.get_all_dependencies(&tv)? { + for dep in t.get_all_dependencies(&tr)? { while installing.lock().unwrap().contains(&dep.to_string()) { trace!( - "{tv} waiting for dependency {dep} to finish installing" + "{tr} waiting for dependency {dep} to finish installing" ); sleep(Duration::from_millis(100)); } } - let tv = tv.resolve(t.as_ref(), opts.latest_versions)?; + let tv = tr.resolve(t.as_ref(), opts.latest_versions)?; let ctx = InstallContext { ts, pr: mpr.add(&tv.style()),