use std::collections::BTreeMap;
use std::env::{join_paths, split_paths};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::exit;
use std::time::Duration;
use clap::Command;
use color_eyre::eyre::Result;
use crate::cache::CacheManager;
use crate::cmd::CmdLineRunner;
use crate::config::{Config, Settings};
use crate::env::{RTX_EXE, RTX_NODE_CONCURRENCY, RTX_NODE_FORCE_COMPILE, RTX_NODE_VERBOSE_INSTALL};
use crate::file::create_dir_all;
use crate::git::Git;
use crate::lock_file::LockFile;
use crate::plugins::{Plugin, PluginName};
use crate::toolset::{ToolVersion, ToolVersionRequest};
use crate::ui::progress_report::ProgressReport;
use crate::{cmd, dirs, env, file};
#[derive(Debug)]
pub struct NodePlugin {
pub name: PluginName,
cache_path: PathBuf,
remote_version_cache: CacheManager<Vec<String>>,
legacy_file_support: bool,
}
impl NodePlugin {
pub fn new(name: PluginName) -> Self {
let cache_path = dirs::CACHE.join(&name);
let fresh_duration = Some(Duration::from_secs(60 * 60 * 12)); Self {
remote_version_cache: CacheManager::new(cache_path.join("remote_versions.msgpack.z"))
.with_fresh_duration(fresh_duration)
.with_fresh_file(RTX_EXE.clone()),
name,
cache_path,
legacy_file_support: false,
}
}
pub fn with_legacy_file_support(self) -> Self {
Self {
legacy_file_support: true,
..self
}
}
fn node_build_path(&self) -> PathBuf {
self.cache_path.join("node-build")
}
fn node_build_bin(&self) -> PathBuf {
self.node_build_path().join("bin/node-build")
}
fn install_or_update_node_build(&self) -> Result<()> {
let _lock = self.lock_node_build();
if self.node_build_path().exists() {
self.update_node_build()
} else {
self.install_node_build()
}
}
fn lock_node_build(&self) -> Result<fslock::LockFile, std::io::Error> {
LockFile::new(&self.node_build_path())
.with_callback(|l| {
trace!("install_or_update_node_build {}", l.display());
})
.lock()
}
fn install_node_build(&self) -> Result<()> {
if self.node_build_path().exists() {
return Ok(());
}
debug!(
"Installing node-build to {}",
self.node_build_path().display()
);
create_dir_all(self.node_build_path().parent().unwrap())?;
let git = Git::new(self.node_build_path());
git.clone("https://github.com/nodenv/node-build.git")?;
Ok(())
}
fn update_node_build(&self) -> Result<()> {
debug!(
"Updating node-build in {}",
self.node_build_path().display()
);
let git = Git::new(self.node_build_path());
git.update(None)?;
Ok(())
}
fn fetch_remote_versions(&self) -> Result<Vec<String>> {
self.install_or_update_node_build()?;
let output = cmd!(self.node_build_bin(), "--definitions").read()?;
let versions = output
.split('\n')
.filter(|s| regex!(r"^[0-9].+$").is_match(s))
.map(|s| s.to_string())
.collect();
Ok(versions)
}
fn node_path(&self, tv: &ToolVersion) -> PathBuf {
tv.install_path().join("bin/node")
}
fn npm_path(&self, tv: &ToolVersion) -> PathBuf {
tv.install_path().join("bin/npm")
}
fn install_default_packages(
&self,
settings: &Settings,
tv: &ToolVersion,
pr: &ProgressReport,
) -> Result<()> {
let body = fs::read_to_string(&*env::RTX_NODE_DEFAULT_PACKAGES_FILE).unwrap_or_default();
for package in body.lines() {
let package = package.split('#').next().unwrap_or_default().trim();
if package.is_empty() {
continue;
}
pr.set_message(format!("installing default package: {}", package));
let npm = self.npm_path(tv);
let mut cmd = CmdLineRunner::new(settings, npm);
cmd.with_pr(pr).arg("install").arg("--global").arg(package);
let mut path = split_paths(&env::var_os("PATH").unwrap()).collect::<Vec<_>>();
path.insert(0, tv.install_path().join("bin"));
cmd.env("PATH", join_paths(path)?);
cmd.execute()?;
}
Ok(())
}
fn install_npm_shim(&self, tv: &ToolVersion) -> Result<()> {
fs::remove_file(self.npm_path(tv)).ok();
fs::write(self.npm_path(tv), include_str!("assets/node_npm_shim"))?;
file::make_executable(&self.npm_path(tv))?;
Ok(())
}
fn test_node(&self, config: &Config, tv: &ToolVersion, pr: &ProgressReport) -> Result<()> {
let mut cmd = CmdLineRunner::new(&config.settings, self.node_path(tv));
cmd.with_pr(pr).arg("-v");
cmd.execute()
}
fn test_npm(&self, config: &Config, tv: &ToolVersion, pr: &ProgressReport) -> Result<()> {
let mut cmd = CmdLineRunner::new(&config.settings, self.npm_path(tv));
let mut path = split_paths(&env::var_os("PATH").unwrap()).collect::<Vec<_>>();
path.insert(0, tv.install_path().join("bin"));
cmd.env("PATH", join_paths(path)?);
cmd.with_pr(pr).arg("-v");
cmd.execute()
}
}
impl Plugin for NodePlugin {
fn name(&self) -> &PluginName {
&self.name
}
fn list_remote_versions(&self, _settings: &Settings) -> Result<Vec<String>> {
self.remote_version_cache
.get_or_try_init(|| self.fetch_remote_versions())
.cloned()
}
fn get_aliases(&self, _settings: &Settings) -> Result<BTreeMap<String, String>> {
let aliases = [
("lts/argon", "4"),
("lts/boron", "6"),
("lts/carbon", "8"),
("lts/dubnium", "10"),
("lts/erbium", "12"),
("lts/fermium", "14"),
("lts/gallium", "16"),
("lts/hydrogen", "18"),
("lts-argon", "4"),
("lts-boron", "6"),
("lts-carbon", "8"),
("lts-dubnium", "10"),
("lts-erbium", "12"),
("lts-fermium", "14"),
("lts-gallium", "16"),
("lts-hydrogen", "18"),
("lts", "18"),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
Ok(aliases)
}
fn legacy_filenames(&self, _settings: &Settings) -> Result<Vec<String>> {
if self.legacy_file_support {
Ok(vec![".node-version".into(), ".nvmrc".into()])
} else {
Ok(vec![])
}
}
fn external_commands(&self) -> Result<Vec<Command>> {
if self.legacy_file_support {
let topic = Command::new("node")
.about("Commands for the node plugin")
.subcommands(vec![Command::new("node-build")
.about("Use/manage rtx's internal node-build")
.arg(
clap::Arg::new("args")
.num_args(1..)
.allow_hyphen_values(true)
.trailing_var_arg(true),
)]);
Ok(vec![topic])
} else {
Ok(vec![])
}
}
fn execute_external_command(
&self,
_config: &Config,
command: &str,
args: Vec<String>,
) -> Result<()> {
match command {
"node-build" => {
self.install_or_update_node_build()?;
cmd::cmd(self.node_build_bin(), args).run()?;
}
_ => unreachable!(),
}
exit(0);
}
fn install_version(
&self,
config: &Config,
tv: &ToolVersion,
pr: &ProgressReport,
) -> Result<()> {
self.install_node_build()?;
pr.set_message("running node-build");
let mut cmd = CmdLineRunner::new(&config.settings, self.node_build_bin());
cmd.with_pr(pr).arg(tv.version.as_str());
if matches!(&tv.request, ToolVersionRequest::Ref { .. }) || *RTX_NODE_FORCE_COMPILE {
let make_opts = String::from(" -j") + &RTX_NODE_CONCURRENCY.to_string();
cmd.env(
"MAKE_OPTS",
env::var("MAKE_OPTS").unwrap_or_default() + &make_opts,
);
cmd.env(
"NODE_MAKE_OPTS",
env::var("NODE_MAKE_OPTS").unwrap_or_default() + &make_opts,
);
cmd.arg("--compile");
}
if config.settings.verbose || *RTX_NODE_VERBOSE_INSTALL {
cmd.arg("--verbose");
}
cmd.arg(tv.install_path());
cmd.execute()?;
self.test_node(config, tv, pr)?;
self.install_npm_shim(tv)?;
self.test_npm(config, tv, pr)?;
self.install_default_packages(&config.settings, tv, pr)?;
Ok(())
}
fn parse_legacy_file(&self, path: &Path, _settings: &Settings) -> Result<String> {
let body = fs::read_to_string(path)?;
Ok(body.trim().strip_prefix('v').unwrap_or(&body).to_string())
}
}