Skip to content

Commit

Permalink
Introduce uninstall command
Browse files Browse the repository at this point in the history
  • Loading branch information
KonishchevDmitry committed Nov 23, 2024
1 parent 188994b commit cf2e5ba
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 29 deletions.
46 changes: 34 additions & 12 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::path::PathBuf;

use clap::{Arg, ArgAction, ArgMatches, Command, value_parser};
use clap::parser::ValuesRef;
use const_format::formatcp;
use log::Level;
use url::Url;
Expand Down Expand Up @@ -32,6 +31,9 @@ pub enum Action {
spec: ToolSpec,
force: bool,
},
Uninstall {
names: Vec<String>,
}
}

macro_rules! long_about {
Expand Down Expand Up @@ -125,6 +127,14 @@ pub fn parse_args() -> GenericResult<CliArgs> {
.action(ArgAction::Append)
.help("Tool name")))

.subcommand(Command::new("uninstall")
.about("Uninstall the specified tools")
.arg(Arg::new("name")
.value_name("NAME")
.action(ArgAction::Append)
.required(true)
.help("Tool name")))

.get_matches();

let log_level = match matches.get_count("verbose") {
Expand All @@ -147,16 +157,13 @@ pub fn parse_args() -> GenericResult<CliArgs> {
},

"install" if matches.contains_id("project") => {
let name = matches.get_many("name").map(|mut names: ValuesRef<'_, String>| -> GenericResult<String> {
let name = names.next().unwrap().clone();
if names.next().is_some() {
return Err!("A single tool name must be specified when project is specified");
}
Ok(name)
}).transpose()?;
let names = get_names(matches);
if names.len() > 1 {
return Err!("A single tool name must be specified when project is specified");
}

Action::InstallFromSpec {
name,
name: names.into_iter().next(),
spec: get_tool_spec(matches)?,
force: matches.get_flag("force"),
}
Expand All @@ -172,16 +179,31 @@ pub fn parse_args() -> GenericResult<CliArgs> {
_ => unreachable!(),
};

let names = matches.get_many("name").map(|tools| tools.cloned().collect()).unwrap_or_default();
Action::Install {mode, names}
}
Action::Install {mode, names: get_names(matches)}
},

"uninstall" => Action::Uninstall {names: get_names(matches)},

_ => unreachable!(),
};

Ok(CliArgs {log_level, config_path, custom_config, action})
}

fn get_names(matches: &ArgMatches) -> Vec<String> {
let mut names: Vec<String> = Vec::new();

if let Some(args) = matches.get_many("name") {
for name in args {
if !names.contains(name) {
names.push(name.clone());
}
}
}

names
}

fn get_tool_spec(matches: &ArgMatches) -> GenericResult<ToolSpec> {
let changelog = matches.get_one("changelog").map(|url: &String| {
Url::parse(url).map_err(|e| format!("Invalid changelog URL: {e}"))
Expand Down
22 changes: 18 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ impl Config {
Ok(config)
}

pub fn edit<E, P>(&mut self, edit: E, process: P) -> EmptyResult
pub fn edit<E, P, R>(&mut self, edit: E, process: P) -> GenericResult<R>
where
E: FnOnce(&mut Config, &mut Document) -> EmptyResult,
P: FnOnce(&Config) -> EmptyResult
P: FnOnce(&Config) -> GenericResult<R>
{
let error_prefix = "Failed to edit the configuration file: its current format is not supported by the underlaying library.";

Expand All @@ -78,7 +78,7 @@ impl Config {

source.data = result.into_bytes();
config.source.replace(source);
process(&config)?;
let result = process(&config)?;

let source = config.source.as_mut().unwrap();

Expand All @@ -93,7 +93,7 @@ impl Config {
Config::write(&source.path, &source.data)?;
*self = config;

Ok(())
Ok(result)
}

pub fn get_tool_path(&self, name: &str, spec: &ToolSpec) -> PathBuf {
Expand All @@ -119,6 +119,20 @@ impl Config {
Ok(())
}

pub fn remove_tool(&mut self, raw: &mut Document, name: &str) -> EmptyResult {
let removed = || -> Option<bool> {
Some(raw.as_mut().as_mapping_mut()?
.get_mut("tools")?.as_mapping_mut()?
.remove(name))
}().unwrap_or_default();

if !removed || self.tools.remove(name).is_none() {
return Err!("Unable to find the tool in the configuration file")
}

Ok(())
}

fn read<R: Read>(reader: R) -> GenericResult<Config> {
let config: Config = serde_yaml::from_reader(reader)?;
config.validate()?;
Expand Down
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod matcher;
mod project;
mod release;
mod tool;
mod uninstall;
mod util;
mod version;

Expand Down Expand Up @@ -59,5 +60,6 @@ fn run(config_path: &Path, custom_config: bool, action: Action) -> GenericResult
Action::List {full} => list::list(&config, full),
Action::Install {mode, names} => install::install(&config, mode, names),
Action::InstallFromSpec {name, spec, force} => install::install_spec(&mut config, name, spec, force),
Action::Uninstall {names} => uninstall::uninstall(&mut config, names),
}
}
58 changes: 58 additions & 0 deletions src/uninstall.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use std::fs;
use std::io::ErrorKind;
use std::path::Path;
use std::process::ExitCode;

use itertools::Itertools;
use log::{info, error};

use crate::config::Config;
use crate::core::GenericResult;
use crate::util;

pub fn uninstall(config: &mut Config, names: Vec<String>) -> GenericResult<ExitCode> {
let mut tools = Vec::new();
let mut invalid = Vec::new();

for name in &names {
match config.tools.get(name) {
Some(spec) => tools.push((name, config.get_tool_path(name, spec))),
None => invalid.push(name),
}
}

if !invalid.is_empty() {
return Err!("The following tools aren't specified in the configuration file: {}", invalid.iter().join(", "));
} else if !util::confirm("Are you sure want to uninstall the specified tools?") {
return Ok(ExitCode::FAILURE);
}

let mut exit_code = ExitCode::SUCCESS;

for (name, path) in tools {
match config.edit(
|config, raw| config.remove_tool(raw, name),
|_| uninstall_tool(&path),
) {
Ok(deleted) => if deleted {
info!("{name} ({}) is uninstalled.", path.display());
} else {
info!("{name} is uninstalled.");
},
Err(err) => {
error!("Failed to uninstall {name}: {err}.");
exit_code = ExitCode::FAILURE;
}
}
}

Ok(exit_code)
}

fn uninstall_tool(path: &Path) -> GenericResult<bool> {
Ok(match fs::remove_file(path) {
Ok(()) => true,
Err(err) if err.kind() == ErrorKind::NotFound => false,
Err(err) => return Err!("Unable to delete {path:?}: {err}"),
})
}
35 changes: 22 additions & 13 deletions test
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,19 @@ run() {
cargo run --quiet -- --config "$config_path" "$@"
}

ensure_post_install_marker() {
ensure_exists_rm() {
rm "$@"
}

ensure_no_post_install_marker() {
if [ -e "$1" ]; then
echo "An unexpected call of post-install command ($1 marker)." >&2
return 1
fi
ensure_missing() {
local path

for path in "$@"; do
if [ -e "$path" ]; then
echo "$path exists when not expected to be." >&2
return 1
fi
done
}

# Test work with missing default config
Expand All @@ -100,7 +104,7 @@ cargo run --quiet -- list
run list

# Should install the tool, but not change the config (fully matches)
for pass in ensure_post_install_marker ensure_no_post_install_marker; do
for pass in ensure_exists_rm ensure_missing; do
run install upgradable --project "$project" \
--release-matcher "$release_matcher" --binary-matcher "$binary_matcher" --changelog "$changelog" \
--path "$custom_install_path" --post "$post_command" < /dev/null
Expand Down Expand Up @@ -130,27 +134,32 @@ for command in install "install --force" upgrade; do
if [ "$command" = install ]; then
shasum "bin/$tool" > checksum
cmp custom-bin/upgradable old-release-mock
ensure_no_post_install_marker "$post_install_marker_path"
ensure_missing "$post_install_marker_path"
else
shasum -c checksum > /dev/null
cmp custom-bin/upgradable "bin/$tool"
ensure_post_install_marker "$post_install_marker_path"
ensure_exists_rm "$post_install_marker_path"
fi
)
done

# Should update existing entry, but not reinstall
updated_post_install_marker="$temp_dir/updated-post-install-marker"
yes | run install --project "$project" --post "touch '$updated_post_install_marker'"
ensure_no_post_install_marker "$updated_post_install_marker"
ensure_missing "$updated_post_install_marker"

# Should add a new entry and install
new_post_install_marker="$temp_dir/new-post-install-marker"
run install new --project "$project" --post "touch '$new_post_install_marker'"
ensure_post_install_marker "$new_post_install_marker"
ensure_exists_rm "$new_post_install_marker"

# Ensure persistence of the changes
run install --force "$tool" && ensure_post_install_marker "$updated_post_install_marker"
run install --force new && ensure_post_install_marker "$new_post_install_marker"
run install --force "$tool" && ensure_exists_rm "$updated_post_install_marker"
run install --force new && ensure_exists_rm "$new_post_install_marker"

# Uninstall simple and complex configurations
cmp "$install_path/new" "$custom_install_path/upgradable"
yes | run uninstall new upgradable
ensure_missing "$install_path/new" "$custom_install_path/upgradable"

run list --full

0 comments on commit cf2e5ba

Please sign in to comment.