diff --git a/cli/flags.rs b/cli/flags.rs index 208bfdca81786a..aaff45388ba866 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -110,6 +110,12 @@ pub struct InstallFlags { pub force: bool, } +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct UninstallFlags { + pub name: String, + pub root: Option, +} + #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct LintFlags { pub files: Vec, @@ -166,6 +172,7 @@ pub enum DenoSubcommand { Fmt(FmtFlags), Info(InfoFlags), Install(InstallFlags), + Uninstall(UninstallFlags), Lsp, Lint(LintFlags), Repl(ReplFlags), @@ -428,6 +435,8 @@ pub fn flags_from_vec(args: Vec) -> clap::Result { bundle_parse(&mut flags, m); } else if let Some(m) = matches.subcommand_matches("install") { install_parse(&mut flags, m); + } else if let Some(m) = matches.subcommand_matches("uninstall") { + uninstall_parse(&mut flags, m); } else if let Some(m) = matches.subcommand_matches("completions") { completions_parse(&mut flags, m); } else if let Some(m) = matches.subcommand_matches("test") { @@ -499,6 +508,7 @@ If the flag is set, restrict these messages to errors.", .subcommand(fmt_subcommand()) .subcommand(info_subcommand()) .subcommand(install_subcommand()) + .subcommand(uninstall_subcommand()) .subcommand(lsp_subcommand()) .subcommand(lint_subcommand()) .subcommand(repl_subcommand()) @@ -995,6 +1005,36 @@ The installation root is determined, in order of precedence: These must be added to the path manually if required.") } +fn uninstall_subcommand<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("uninstall") + .setting(AppSettings::TrailingVarArg) + .arg( + Arg::with_name("name") + .required(true) + .multiple(false) + .allow_hyphen_values(true)) + .arg( + Arg::with_name("root") + .long("root") + .help("Installation root") + .takes_value(true) + .multiple(false)) + .about("Uninstall a script previously installed with deno install") + .long_about( + "Uninstalls an executable script in the installation root's bin directory. + + deno uninstall serve + +To change the installation root, use --root: + + deno uninstall --root /usr/local serve + +The installation root is determined, in order of precedence: + - --root option + - DENO_INSTALL_ROOT environment variable + - $HOME/.deno") +} + fn lsp_subcommand<'a, 'b>() -> App<'a, 'b> { SubCommand::with_name("lsp") .about("Start the language server") @@ -1896,6 +1936,18 @@ fn install_parse(flags: &mut Flags, matches: &clap::ArgMatches) { }); } +fn uninstall_parse(flags: &mut Flags, matches: &clap::ArgMatches) { + let root = if matches.is_present("root") { + let install_root = matches.value_of("root").unwrap(); + Some(PathBuf::from(install_root)) + } else { + None + }; + + let name = matches.value_of("name").unwrap().to_string(); + flags.subcommand = DenoSubcommand::Uninstall(UninstallFlags { name, root }); +} + fn lsp_parse(flags: &mut Flags, _matches: &clap::ArgMatches) { flags.subcommand = DenoSubcommand::Lsp; } diff --git a/cli/main.rs b/cli/main.rs index ba16ea590d63df..9176aca6f0b0bb 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -53,6 +53,7 @@ use crate::flags::LintFlags; use crate::flags::ReplFlags; use crate::flags::RunFlags; use crate::flags::TestFlags; +use crate::flags::UninstallFlags; use crate::flags::UpgradeFlags; use crate::fmt_errors::PrettyJsError; use crate::module_loader::CliModuleLoader; @@ -487,6 +488,12 @@ async fn install_command( ) } +async fn uninstall_command( + uninstall_flags: UninstallFlags, +) -> Result<(), AnyError> { + tools::installer::uninstall(uninstall_flags.name, uninstall_flags.root) +} + async fn lsp_command() -> Result<(), AnyError> { lsp::start().await } @@ -1149,6 +1156,9 @@ fn get_subcommand( DenoSubcommand::Install(install_flags) => { install_command(flags, install_flags).boxed_local() } + DenoSubcommand::Uninstall(uninstall_flags) => { + uninstall_command(uninstall_flags).boxed_local() + } DenoSubcommand::Lsp => lsp_command().boxed_local(), DenoSubcommand::Lint(lint_flags) => { lint_command(flags, lint_flags).boxed_local() diff --git a/cli/tools/installer.rs b/cli/tools/installer.rs index 4d4709e28f93f7..18d2d20e740c8b 100644 --- a/cli/tools/installer.rs +++ b/cli/tools/installer.rs @@ -139,6 +139,57 @@ pub fn infer_name_from_url(url: &Url) -> Option { Some(stem) } +pub fn uninstall(name: String, root: Option) -> Result<(), AnyError> { + let root = if let Some(root) = root { + canonicalize_path(&root)? + } else { + get_installer_root()? + }; + let installation_dir = root.join("bin"); + + // ensure directory exists + if let Ok(metadata) = fs::metadata(&installation_dir) { + if !metadata.is_dir() { + return Err(generic_error("Installation path is not a directory")); + } + } + + let mut file_path = installation_dir.join(&name); + + let mut removed = false; + + if file_path.exists() { + fs::remove_file(&file_path)?; + println!("deleted {}", file_path.to_string_lossy()); + removed = true + }; + + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + if file_path.exists() { + fs::remove_file(&file_path)?; + println!("deleted {}", file_path.to_string_lossy()); + removed = true + } + } + + if !removed { + return Err(generic_error(format!("No installation found for {}", name))); + } + + // There might be some extra files to delete + for ext in ["tsconfig.json", "lock.json"] { + file_path = file_path.with_extension(ext); + if file_path.exists() { + fs::remove_file(&file_path)?; + println!("deleted {}", file_path.to_string_lossy()); + } + } + + println!("✅ Successfully uninstalled {}", name); + Ok(()) +} + pub fn install( flags: Flags, module_url: &str, @@ -926,4 +977,36 @@ mod tests { let content = fs::read_to_string(file_path).unwrap(); assert!(content.contains(&expected_string)); } + + #[test] + fn uninstall_basic() { + let temp_dir = TempDir::new().expect("tempdir fail"); + let bin_dir = temp_dir.path().join("bin"); + std::fs::create_dir(&bin_dir).unwrap(); + + let mut file_path = bin_dir.join("echo_test"); + File::create(&file_path).unwrap(); + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + File::create(&file_path).unwrap(); + } + + // create extra files + file_path = file_path.with_extension("tsconfig.json"); + File::create(&file_path).unwrap(); + file_path = file_path.with_extension("lock.json"); + File::create(&file_path).unwrap(); + + uninstall("echo_test".to_string(), Some(temp_dir.path().to_path_buf())) + .expect("Uninstall failed"); + + assert!(!file_path.exists()); + assert!(!file_path.with_extension("tsconfig.json").exists()); + assert!(!file_path.with_extension("lock.json").exists()); + + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + assert!(!file_path.exists()); + } + } }