Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for uninstalling node #1882

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"editor.formatOnSave": true
"editor.formatOnSave": true,
"rust-analyzer.cargo.features": "all"
}
6 changes: 3 additions & 3 deletions crates/volta-core/src/run/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -543,14 +543,14 @@ impl UninstallCommand {
}

/// Runs the uninstall with Volta's internal uninstall logic
fn execute(self) -> Fallible<ExitStatus> {
fn execute(self, session: &mut Session) -> Fallible<ExitStatus> {
info!(
"{} using Volta to uninstall {}",
note_prefix(),
self.tool.name()
);

self.tool.uninstall()?;
self.tool.resolve(session)?.uninstall(session)?;

Ok(ExitStatus::from_raw(0))
}
Expand Down
34 changes: 2 additions & 32 deletions crates/volta-core/src/tool/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ pub trait Tool: Display {
fn install(self: Box<Self>, session: &mut Session) -> Fallible<()>;
/// Pin a tool in the local project so that it is usable within the project
fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()>;
/// Uninstall a tool
fn uninstall(self: Box<Self>, session: &mut Session) -> Fallible<()>;
}

/// Specification for a tool and its associated version.
Expand Down Expand Up @@ -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 {
Expand Down
46 changes: 43 additions & 3 deletions crates/volta-core/src/tool/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -282,6 +284,44 @@ impl Tool for Node {
Err(ErrorKind::NotInPackage.into())
}
}
fn uninstall(self: Box<Self>, _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, crate::error::VoltaError> = 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 {
Expand Down
13 changes: 13 additions & 0 deletions crates/volta-core/src/tool/npm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ impl Tool for Npm {
Err(ErrorKind::NotInPackage.into())
}
}
fn uninstall(self: Box<Self>, _session: &mut Session) -> Fallible<()> {
Err(ErrorKind::Unimplemented {
feature: "Uninstalling npm".into(),
}
.into())
}
}

impl Display for Npm {
Expand Down Expand Up @@ -169,6 +175,13 @@ impl Tool for BundledNpm {
None => Err(ErrorKind::NotInPackage.into()),
}
}

fn uninstall(self: Box<Self>, _session: &mut Session) -> Fallible<()> {
Err(ErrorKind::Unimplemented {
feature: "Uninstalling bundled npm".into(),
}
.into())
}
}

impl Display for BundledNpm {
Expand Down
14 changes: 14 additions & 0 deletions crates/volta-core/src/tool/package/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ impl Tool for Package {
fn pin(self: Box<Self>, _session: &mut Session) -> Fallible<()> {
Err(ErrorKind::CannotPinPackage { package: self.name }.into())
}

fn uninstall(self: Box<Self>, _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 {
Expand Down
14 changes: 14 additions & 0 deletions crates/volta-core/src/tool/pnpm/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use node_semver::Version;
use std::env;
use std::fmt::{self, Display};

use crate::error::{ErrorKind, Fallible};
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,
Expand All @@ -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
Expand Down Expand Up @@ -88,6 +91,17 @@ impl Tool for Pnpm {
Err(ErrorKind::NotInPackage.into())
}
}

fn uninstall(self: Box<Self>, _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 {
Expand Down
6 changes: 6 additions & 0 deletions crates/volta-core/src/tool/yarn/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ impl Tool for Yarn {
Err(ErrorKind::NotInPackage.into())
}
}
fn uninstall(self: Box<Self>, _session: &mut Session) -> Fallible<()> {
Err(ErrorKind::Unimplemented {
feature: "Uninstalling yarn".into(),
}
.into())
}
}

impl Display for Yarn {
Expand Down
27 changes: 7 additions & 20 deletions src/command/uninstall.rs
Original file line number Diff line number Diff line change
@@ -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 <package>
tool: String,
/// Tools to uninstall, like `node`, `yarn@latest` or `your-package`.
#[arg(value_name = "tool[@version]", required = true)]
tools: Vec<String>,
}

impl Command for Uninstall {
fn run(self, session: &mut Session) -> Fallible<ExitCode> {
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)
}
Expand Down
64 changes: 59 additions & 5 deletions tests/acceptance/volta_uninstall.rs
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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 [email protected]"),
execs()
.with_status(0)
.with_stdout_contains("[..]'[email protected]' uninstalled")
);
}