diff --git a/.vscode/settings.json b/.vscode/settings.json index ad92582bd..597d1f000 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "editor.formatOnSave": true + "editor.formatOnSave": true, + "rust-analyzer.cargo.features": "all" } diff --git a/crates/volta-core/src/run/executor.rs b/crates/volta-core/src/run/executor.rs index 175223ed4..b2c210bee 100644 --- a/crates/volta-core/src/run/executor.rs +++ b/crates/volta-core/src/run/executor.rs @@ -77,7 +77,7 @@ impl Executor { Executor::PackageLink(cmd) => cmd.execute(session), Executor::PackageUpgrade(cmd) => cmd.execute(session), Executor::InternalInstall(cmd) => cmd.execute(session), - Executor::Uninstall(cmd) => cmd.execute(), + Executor::Uninstall(cmd) => cmd.execute(session), Executor::Multiple(executors) => { info!( "{} Volta is processing each package separately", @@ -543,14 +543,14 @@ impl UninstallCommand { } /// Runs the uninstall with Volta's internal uninstall logic - fn execute(self) -> Fallible { + fn execute(self, session: &mut Session) -> Fallible { info!( "{} using Volta to uninstall {}", note_prefix(), self.tool.name() ); - self.tool.uninstall()?; + self.tool.resolve(session)?.uninstall(session)?; Ok(ExitStatus::from_raw(0)) } diff --git a/crates/volta-core/src/tool/mod.rs b/crates/volta-core/src/tool/mod.rs index a464107df..8f2de8bbe 100644 --- a/crates/volta-core/src/tool/mod.rs +++ b/crates/volta-core/src/tool/mod.rs @@ -65,6 +65,8 @@ pub trait Tool: Display { fn install(self: Box, session: &mut Session) -> Fallible<()>; /// Pin a tool in the local project so that it is usable within the project fn pin(self: Box, session: &mut Session) -> Fallible<()>; + /// Uninstall a tool + fn uninstall(self: Box, session: &mut Session) -> Fallible<()>; } /// Specification for a tool and its associated version. @@ -115,38 +117,6 @@ impl Spec { } } - /// Uninstall a tool, removing it from the local inventory - /// - /// This is implemented on Spec, instead of Resolved, because there is currently no need to - /// resolve the specific version before uninstalling a tool. - pub fn uninstall(self) -> Fallible<()> { - match self { - Spec::Node(_) => Err(ErrorKind::Unimplemented { - feature: "Uninstalling node".into(), - } - .into()), - Spec::Npm(_) => Err(ErrorKind::Unimplemented { - feature: "Uninstalling npm".into(), - } - .into()), - Spec::Pnpm(_) => { - if env::var_os(VOLTA_FEATURE_PNPM).is_some() { - Err(ErrorKind::Unimplemented { - feature: "Uninstalling pnpm".into(), - } - .into()) - } else { - package::uninstall("pnpm") - } - } - Spec::Yarn(_) => Err(ErrorKind::Unimplemented { - feature: "Uninstalling yarn".into(), - } - .into()), - Spec::Package(name, _) => package::uninstall(&name), - } - } - /// The name of the tool, without the version, used for messaging pub fn name(&self) -> &str { match self { diff --git a/crates/volta-core/src/tool/node/mod.rs b/crates/volta-core/src/tool/node/mod.rs index c9788be59..a44d43c46 100644 --- a/crates/volta-core/src/tool/node/mod.rs +++ b/crates/volta-core/src/tool/node/mod.rs @@ -4,13 +4,15 @@ use super::{ check_fetched, check_shim_reachable, debug_already_fetched, info_fetched, info_installed, info_pinned, info_project_version, FetchStatus, Tool, }; -use crate::error::{ErrorKind, Fallible}; +use crate::error::{Context, ErrorKind, Fallible}; +use crate::fs::{dir_entry_match, ok_if_not_found, remove_dir_if_exists, remove_file_if_exists}; use crate::inventory::node_available; +use crate::layout::volta_home; use crate::session::Session; -use crate::style::{note_prefix, tool_version}; +use crate::style::{note_prefix, success_prefix, tool_version}; use crate::sync::VoltaLock; use cfg_if::cfg_if; -use log::info; +use log::{info, warn}; use node_semver::Version; mod fetch; @@ -282,6 +284,44 @@ impl Tool for Node { Err(ErrorKind::NotInPackage.into()) } } + fn uninstall(self: Box, _session: &mut Session) -> Fallible<()> { + let home = volta_home()?; + // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes + let _lock: Result = VoltaLock::acquire(); + + let node_dir = home.node_image_root_dir().join(self.version.to_string()); + + dir_entry_match(home.node_inventory_dir(), |entry| { + let path = entry.path(); + + if path.is_file() { + match path.file_name().and_then(|name| name.to_str()) { + Some(file_name) if file_name.contains(&self.version.to_string()) => Some(path), + _ => None, + } + } else { + None + } + }) + .or_else(ok_if_not_found) + .with_context(|| ErrorKind::ReadDirError { + dir: home.node_inventory_dir().to_path_buf(), + }) + .map(|files| { + files.iter().for_each(|file| { + remove_file_if_exists(file); + }) + }); + + if node_dir.exists() { + remove_dir_if_exists(&node_dir)?; + info!("{} 'node@{}' uninstalled", success_prefix(), self.version); + } else { + warn!("No version 'node@{}' found to uninstall", self.version); + } + + Ok(()) + } } impl Display for Node { diff --git a/crates/volta-core/src/tool/npm/mod.rs b/crates/volta-core/src/tool/npm/mod.rs index 2e4ca81fa..ec141b594 100644 --- a/crates/volta-core/src/tool/npm/mod.rs +++ b/crates/volta-core/src/tool/npm/mod.rs @@ -87,6 +87,12 @@ impl Tool for Npm { Err(ErrorKind::NotInPackage.into()) } } + fn uninstall(self: Box, _session: &mut Session) -> Fallible<()> { + Err(ErrorKind::Unimplemented { + feature: "Uninstalling npm".into(), + } + .into()) + } } impl Display for Npm { @@ -169,6 +175,13 @@ impl Tool for BundledNpm { None => Err(ErrorKind::NotInPackage.into()), } } + + fn uninstall(self: Box, _session: &mut Session) -> Fallible<()> { + Err(ErrorKind::Unimplemented { + feature: "Uninstalling bundled npm".into(), + } + .into()) + } } impl Display for BundledNpm { diff --git a/crates/volta-core/src/tool/package/mod.rs b/crates/volta-core/src/tool/package/mod.rs index c8ae39fc5..cad2b0c1e 100644 --- a/crates/volta-core/src/tool/package/mod.rs +++ b/crates/volta-core/src/tool/package/mod.rs @@ -108,6 +108,20 @@ impl Tool for Package { fn pin(self: Box, _session: &mut Session) -> Fallible<()> { Err(ErrorKind::CannotPinPackage { package: self.name }.into()) } + + fn uninstall(self: Box, _session: &mut Session) -> Fallible<()> { + // For packages, specifically report that we do not support uninstalling + // specific versions. For package managers, we currently + // *intentionally* let this fall through to inform the user that we do + // not support uninstalling those *at all*. + let VersionSpec::None = &self.version else { + return Err(ErrorKind::Unimplemented { + feature: "uninstalling specific versions of tools".into(), + } + .into()); + }; + uninstall(&self.name) + } } impl Display for Package { diff --git a/crates/volta-core/src/tool/pnpm/mod.rs b/crates/volta-core/src/tool/pnpm/mod.rs index fda2d34c5..96a3ad835 100644 --- a/crates/volta-core/src/tool/pnpm/mod.rs +++ b/crates/volta-core/src/tool/pnpm/mod.rs @@ -1,4 +1,5 @@ use node_semver::Version; +use std::env; use std::fmt::{self, Display}; use crate::error::{ErrorKind, Fallible}; @@ -6,6 +7,7 @@ use crate::inventory::pnpm_available; use crate::session::Session; use crate::style::tool_version; use crate::sync::VoltaLock; +use crate::VOLTA_FEATURE_PNPM; use super::{ check_fetched, check_shim_reachable, debug_already_fetched, info_fetched, info_installed, @@ -15,6 +17,7 @@ use super::{ mod fetch; mod resolve; +use super::package::uninstall; pub use resolve::resolve; /// The Tool implementation for fetching and installing pnpm @@ -88,6 +91,17 @@ impl Tool for Pnpm { Err(ErrorKind::NotInPackage.into()) } } + + fn uninstall(self: Box, _session: &mut Session) -> Fallible<()> { + if env::var_os(VOLTA_FEATURE_PNPM).is_some() { + Err(ErrorKind::Unimplemented { + feature: "Uninstalling pnpm".into(), + } + .into()) + } else { + uninstall("pnpm") + } + } } impl Display for Pnpm { diff --git a/crates/volta-core/src/tool/yarn/mod.rs b/crates/volta-core/src/tool/yarn/mod.rs index ae18be030..3903cc744 100644 --- a/crates/volta-core/src/tool/yarn/mod.rs +++ b/crates/volta-core/src/tool/yarn/mod.rs @@ -86,6 +86,12 @@ impl Tool for Yarn { Err(ErrorKind::NotInPackage.into()) } } + fn uninstall(self: Box, _session: &mut Session) -> Fallible<()> { + Err(ErrorKind::Unimplemented { + feature: "Uninstalling yarn".into(), + } + .into()) + } } impl Display for Yarn { diff --git a/src/command/uninstall.rs b/src/command/uninstall.rs index 6ed1a83b1..20d1beadd 100644 --- a/src/command/uninstall.rs +++ b/src/command/uninstall.rs @@ -1,37 +1,24 @@ -use volta_core::error::{ErrorKind, ExitCode, Fallible}; +use volta_core::error::{ExitCode, Fallible}; use volta_core::session::{ActivityKind, Session}; -use volta_core::tool; -use volta_core::version::VersionSpec; +use volta_core::tool::Spec; use crate::command::Command; #[derive(clap::Args)] pub(crate) struct Uninstall { - /// The tool to uninstall, like `ember-cli-update`, `typescript`, or - tool: String, + /// Tools to uninstall, like `node`, `yarn@latest` or `your-package`. + #[arg(value_name = "tool[@version]", required = true)] + tools: Vec, } impl Command for Uninstall { fn run(self, session: &mut Session) -> Fallible { session.add_event_start(ActivityKind::Uninstall); - let tool = tool::Spec::try_from_str(&self.tool)?; - - // For packages, specifically report that we do not support uninstalling - // specific versions. For runtimes and package managers, we currently - // *intentionally* let this fall through to inform the user that we do - // not support uninstalling those *at all*. - if let tool::Spec::Package(_name, version) = &tool { - let VersionSpec::None = version else { - return Err(ErrorKind::Unimplemented { - feature: "uninstalling specific versions of tools".into(), - } - .into()); - }; + for tool in Spec::from_strings(&self.tools, "uninstall")? { + tool.resolve(session)?.uninstall(session)?; } - tool.uninstall()?; - session.add_event_end(ActivityKind::Uninstall, ExitCode::Success); Ok(ExitCode::Success) } diff --git a/tests/acceptance/volta_uninstall.rs b/tests/acceptance/volta_uninstall.rs index 131b7e779..e838efeff 100644 --- a/tests/acceptance/volta_uninstall.rs +++ b/tests/acceptance/volta_uninstall.rs @@ -1,10 +1,46 @@ //! Tests for `volta uninstall`. use crate::support::sandbox::{sandbox, Sandbox}; +use cfg_if::cfg_if; use hamcrest2::assert_that; use hamcrest2::prelude::*; use test_support::matchers::execs; +fn platform_with_node_npm(node: &str, npm: &str) -> String { + format!( + r#"{{ + "node": {{ + "runtime": "{}", + "npm": "{}" + }}, + "pnpm": null, + "yarn": null +}}"#, + node, npm + ) +} +fn node_bin(version: &str) -> String { + cfg_if! { + if #[cfg(target_os = "windows")] { + format!( + r#"@echo off +echo Node version {} +echo node args: %* +"#, + version + ) + } else { + format!( + r#"#!/bin/sh +echo "Node version {}" +echo "node args: $@" +"#, + version + ) + } + } +} + const PKG_CONFIG_BASIC: &str = r#"{ "name": "cowsay", "version": "1.4.0", @@ -206,12 +242,30 @@ fn uninstall_package_orphaned_bins() { } #[test] -fn uninstall_runtime() { - let s = sandbox().build(); +fn uninstall_nonexistent_runtime() { + let s = sandbox().env(VOLTA_LOGLEVEL, "info").build(); assert_that!( - s.volta("uninstall node"), + s.volta("uninstall node@20.16.0"), execs() - .with_status(1) - .with_stderr_contains("[..]error: Uninstalling node is not supported yet.") + .with_status(0) + .with_stderr_contains("[..]No version 'node@20.16.0' found to uninstall") ) } + +#[test] +fn uninstall_runtime_basic() { + // basic uninstall - everything exists, and everything except the cached + // inventory files should not be deleted + let s = sandbox() + .platform(&platform_with_node_npm("20.16.0", "10.8.1")) + .setup_node_binary("20.16.0", "10.8.1", &node_bin("20.16.0")) + .env(VOLTA_LOGLEVEL, "info") + .build(); + + assert_that!( + s.volta("uninstall node@20.16.0"), + execs() + .with_status(0) + .with_stdout_contains("[..]'node@20.16.0' uninstalled") + ); +}