refact(core,cli): declarative CLI approach

Use clap-derive to let it build the CLI and parse it automagically. This
allows to rework core and split up the monster Pacdef struct.

Switches groups from HashSet to BTreeSet for built-in sorting/more
deterministic general operation.

Refactored code base into a global type synonym Groups to remove
duplication of the HashSet/BTreeSet implementation detail to a single
location.

Implemented in #74.
This commit is contained in:
steven-omaha
2024-04-23 11:08:23 +02:00
30 changed files with 634 additions and 969 deletions
Generated
+19 -186
View File
@@ -84,12 +84,6 @@ version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
[[package]]
name = "autocfg"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
[[package]]
name = "bitflags"
version = "2.5.0"
@@ -102,12 +96,6 @@ version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.4"
@@ -115,6 +103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
@@ -129,6 +118,18 @@ dependencies = [
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
@@ -243,113 +244,18 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "futures"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-executor"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-macro"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[package]]
name = "futures-task"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "hashbrown"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.2.6"
@@ -411,10 +317,8 @@ dependencies = [
"libc",
"path-absolutize",
"regex",
"rstest",
"rust-apt",
"serde",
"serde_derive",
"serde_json",
"serde_yaml",
"termios",
@@ -439,18 +343,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.30"
@@ -504,41 +396,6 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
[[package]]
name = "relative-path"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc"
[[package]]
name = "rstest"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330"
dependencies = [
"futures",
"futures-timer",
"rstest_macros",
"rustc_version",
]
[[package]]
name = "rstest_macros"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25"
dependencies = [
"cfg-if",
"glob",
"proc-macro2",
"quote",
"regex",
"relative-path",
"rustc_version",
"syn",
"unicode-ident",
]
[[package]]
name = "rust-apt"
version = "0.7.0"
@@ -550,15 +407,6 @@ dependencies = [
"terminal_size",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.38.32"
@@ -593,12 +441,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
[[package]]
name = "semver"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]]
name = "serde"
version = "1.0.197"
@@ -643,15 +485,6 @@ dependencies = [
"unsafe-libyaml",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "strsim"
version = "0.11.1"
+2 -6
View File
@@ -12,7 +12,7 @@ categories.workspace = true
[dependencies]
anyhow = "1.0"
clap = "4.5"
clap = { version = "4.5", features = ["derive"] }
const_format = { version = "0.2", default-features = false }
path-absolutize = "3.1"
regex = { version = "1.10", default-features = false, features = ["std"] }
@@ -21,8 +21,7 @@ walkdir = "2.5"
libc = "0.2"
enum_dispatch = "0.3"
serde = "1.0"
serde_derive = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"
@@ -30,9 +29,6 @@ serde_yaml = "0.9"
alpm = { version = "3.0", optional = true }
rust-apt = { version = "0.7", optional = true }
[dev-dependencies]
rstest = "0.19"
[features]
default = []
arch = ["dep:alpm"]
-159
View File
@@ -1,159 +0,0 @@
use crate::core::get_version_string;
use clap::{Arg, ArgAction, Command};
#[must_use]
pub(super) fn build_cli() -> Command {
let package_cmd = get_package_cmd();
let group_cmd = get_group_cmd();
let version_cmd = Command::new("version").about("show version info");
Command::new("pacdef")
.about("multi-backend declarative package manager for Linux")
.version(get_version_string())
.arg_required_else_help(true)
.subcommand_required(true)
.subcommands([group_cmd, package_cmd, version_cmd])
.subcommand_value_name("subcommand")
.disable_help_subcommand(true)
.disable_version_flag(true)
}
/// Build the `pacdef group` subcommand.
fn get_group_cmd() -> Command {
let edit = Command::new("edit")
.about("edit one or more existing group")
.arg_required_else_help(true)
.arg(
Arg::new("groups")
.num_args(1..)
.required(true)
.help("a previously imported group"),
)
.visible_alias("ed");
let export = Command::new("export")
.about("export one or more group files")
.arg_required_else_help(true)
.arg(
Arg::new("force")
.short('f')
.long("force")
.action(clap::ArgAction::SetTrue)
.help("overwrite output files if they exist"),
)
.arg(
Arg::new("output_dir")
.short('o')
.long("output")
.help("(optional) the directory under which to save the group"),
)
.arg(
Arg::new("groups")
.num_args(1..)
.required(true)
.help("the file to export as group"),
)
.visible_alias("ex");
let import = Command::new("import")
.about("import one or more group files")
.arg_required_else_help(true)
.arg(
Arg::new("groups")
.num_args(1..)
.required(true)
.help("the file to import as group"),
)
.visible_alias("i");
let list = Command::new("list")
.about("list names of imported groups")
.visible_alias("l");
let new = Command::new("new")
.about("create new group files")
.arg_required_else_help(true)
.arg(
Arg::new("edit")
.short('e')
.long("edit")
.help("edit the new group files after creation")
.action(clap::ArgAction::SetTrue),
)
.arg(Arg::new("groups").num_args(1..).required(true))
.visible_alias("n");
let remove = Command::new("remove")
.about("remove one or more previously imported groups")
.arg_required_else_help(true)
.arg(
Arg::new("groups")
.num_args(1..)
.required(true)
.help("a previously imported group that will be removed"),
)
.visible_alias("r");
let show = Command::new("show")
.about("show packages under an imported group")
.arg_required_else_help(true)
.arg(
Arg::new("groups")
.num_args(1..)
.required(true)
.help("group file(s) to show"),
)
.visible_alias("s");
Command::new("group")
.arg_required_else_help(true)
.about("manage groups")
.visible_alias("g")
.subcommand_required(true)
.subcommands([edit, export, import, list, new, remove, show])
}
/// Build the `pacdef package` subcommand.
fn get_package_cmd() -> Command {
let sync = Command::new("sync")
.about("install packages from all imported groups")
.visible_alias("sy")
.arg(build_noconfirm_arg());
let clean = Command::new("clean")
.about("remove unmanaged packages")
.visible_alias("c")
.arg(build_noconfirm_arg());
let unmanaged = Command::new("unmanaged")
.about("show explicitly installed packages not managed by pacdef")
.visible_alias("u");
let review = Command::new("review")
.about("review unmanaged packages")
.visible_alias("r");
let search = Command::new("search")
.visible_alias("se")
.about("search for packages which match a provided regex")
.arg_required_else_help(true)
.arg(
Arg::new("regex")
.required(true)
.help("the regular expression the package must match"),
);
Command::new("package")
.arg_required_else_help(true)
.about("manage packages")
.visible_alias("p")
.subcommand_required(true)
.subcommands([clean, review, search, sync, unmanaged])
}
fn build_noconfirm_arg() -> Arg {
Arg::new("noconfirm")
.long("noconfirm")
.help("do not ask for any confirmation")
.action(ArgAction::SetTrue)
}
-47
View File
@@ -1,47 +0,0 @@
#[derive(Debug, PartialEq)]
pub enum Arguments {
Group(GroupAction),
Package(PackageAction),
Version,
}
#[derive(Debug, PartialEq)]
pub enum GroupAction {
Edit(Groups),
Export(Groups, OutputDir, Force),
Import(Groups),
List,
New(Groups, Edit),
Remove(Groups),
Show(Groups),
}
#[derive(Debug, PartialEq)]
pub struct Files(pub Vec<String>);
#[derive(Debug, PartialEq)]
pub struct Groups(pub Vec<String>);
#[derive(Debug, PartialEq)]
pub enum PackageAction {
Clean(Noconfirm),
Review,
Search(Regex),
Sync(Noconfirm),
Unmanaged,
}
#[derive(Debug, PartialEq)]
pub struct Regex(pub String);
#[derive(Debug, PartialEq)]
pub struct Edit(pub bool);
#[derive(Debug, PartialEq)]
pub struct Noconfirm(pub bool);
#[derive(Debug, PartialEq)]
pub struct Force(pub bool);
#[derive(Debug, PartialEq)]
pub struct OutputDir(pub Option<String>);
-14
View File
@@ -1,14 +0,0 @@
use self::datastructure::Arguments;
mod cli;
pub mod datastructure;
mod parsing;
#[cfg(test)]
mod tests;
/// Get and parse the CLI arguments.
#[must_use]
pub fn get() -> Arguments {
let args = cli::build_cli().get_matches();
parsing::parse(args)
}
-79
View File
@@ -1,79 +0,0 @@
use super::datastructure::{
Arguments, Edit, Force, GroupAction, Groups, Noconfirm, OutputDir, PackageAction, Regex,
};
const ARGS_CONSISTENT: &str = "argument declaration and parsing must be consistent";
pub(super) fn parse(args: clap::ArgMatches) -> Arguments {
match args.subcommand() {
Some(("group", args)) => Arguments::Group(parse_group_args(args)),
Some(("package", args)) => Arguments::Package(parse_package_args(args)),
Some(("version", _)) => Arguments::Version,
Some(value) => panic!("main subcommand was not matched: {value:?}"),
None => unreachable!("prevented by clap"),
}
}
fn parse_group_args(args: &clap::ArgMatches) -> GroupAction {
match args.subcommand() {
Some(("edit", args)) => GroupAction::Edit(get_groups(args)),
Some(("export", args)) => {
GroupAction::Export(get_groups(args), get_output_dir(args), get_force(args))
}
Some(("import", args)) => GroupAction::Import(get_groups(args)),
Some(("list", _)) => GroupAction::List,
Some(("new", args)) => GroupAction::New(get_groups(args), get_edit(args)),
Some(("remove", args)) => GroupAction::Remove(get_groups(args)),
Some(("show", args)) => GroupAction::Show(get_groups(args)),
Some(value) => panic!("group subcommand was not matched: {value:?}"),
None => unreachable!("prevented by clap"),
}
}
fn parse_package_args(args: &clap::ArgMatches) -> PackageAction {
match args.subcommand() {
Some(("clean", args)) => PackageAction::Clean(get_noconfirm(args)),
Some(("review", _)) => PackageAction::Review,
Some(("search", args)) => PackageAction::Search(get_regex(args)),
Some(("sync", args)) => PackageAction::Sync(get_noconfirm(args)),
Some(("unmanaged", _)) => PackageAction::Unmanaged,
Some(value) => panic!("package subcommand was not matched: {value:?}"),
None => unreachable!("prevented by clap"),
}
}
fn get_one_arg<T>(args: &clap::ArgMatches, id: &str) -> T
where
T: std::any::Any + Clone + Sync + Send + 'static,
{
args.get_one::<T>(id).expect(ARGS_CONSISTENT).to_owned()
}
fn get_regex(args: &clap::ArgMatches) -> Regex {
Regex(get_one_arg(args, "regex"))
}
fn get_noconfirm(args: &clap::ArgMatches) -> Noconfirm {
Noconfirm(get_one_arg(args, "noconfirm"))
}
fn get_edit(args: &clap::ArgMatches) -> Edit {
Edit(get_one_arg(args, "edit"))
}
fn get_groups(args: &clap::ArgMatches) -> Groups {
Groups(
args.get_many::<String>("groups")
.expect(ARGS_CONSISTENT)
.cloned()
.collect(),
)
}
fn get_output_dir(args: &clap::ArgMatches) -> OutputDir {
OutputDir(args.get_one::<String>("output_dir").cloned())
}
fn get_force(args: &clap::ArgMatches) -> Force {
Force(get_one_arg(args, "force"))
}
-38
View File
@@ -1,38 +0,0 @@
use super::cli::build_cli;
use super::datastructure::*;
use super::parsing::parse;
use rstest::rstest;
#[rstest]
#[case(vec!["pacdef", "group", "list"],
Arguments::Group(GroupAction::List))]
#[case(vec!["pacdef", "g", "l"],
Arguments::Group(GroupAction::List))]
#[case(vec!["pacdef", "version"],
Arguments::Version)]
#[case(vec!["pacdef", "group", "export", "-o", "/tmp", "--force", "base"],
Arguments::Group(GroupAction::Export(Groups(vec!["base".to_string()]), OutputDir(Some("/tmp".to_string())), Force(true))))]
#[case(vec!["pacdef", "package", "sync"],
Arguments::Package(PackageAction::Sync(Noconfirm(false))))]
#[case(vec!["pacdef", "package", "sync", "--noconfirm"],
Arguments::Package(PackageAction::Sync(Noconfirm(true))))]
#[case(vec!["pacdef", "p", "se", "myregex"],
Arguments::Package(PackageAction::Search(Regex("myregex".to_string()))))]
fn arg_parsing(#[case] input: Vec<&str>, #[case] expected: Arguments) {
let args = build_cli().get_matches_from(input);
let parsed = parse(args);
assert_eq!(parsed, expected);
}
#[rstest]
#[should_panic]
#[case(vec!["pacdef", "package", "search"], "regex missing")]
#[should_panic]
#[case(vec!["pacdef", "p", "s"], "can be either 'sync' or 'search'")]
#[should_panic]
#[case(vec!["pacdef", "group", "edit"], "group missing")]
fn arg_parsing_invalid(#[case] input: Vec<&str>, #[case] err_msg: &str) {
build_cli().try_get_matches_from(input).expect(err_msg);
}
+1 -1
View File
@@ -8,7 +8,7 @@ use anyhow::{Context, Result};
use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::cmd::run_external_command;
use crate::{Group, Package};
use crate::Package;
#[derive(Debug, Clone)]
pub struct Arch {
+1 -1
View File
@@ -8,7 +8,7 @@ use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::backend::root::build_base_command_with_privileges;
use crate::cmd::run_external_command;
use crate::{Group, Package};
use crate::Package;
#[derive(Debug, Clone)]
pub struct Debian {
+1 -1
View File
@@ -6,7 +6,7 @@ use anyhow::Result;
use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::cmd::run_external_command;
use crate::{Group, Package};
use crate::Package;
#[derive(Debug, Clone)]
pub struct Fedora {
+1 -1
View File
@@ -6,7 +6,7 @@ use anyhow::Result;
use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::cmd::run_external_command;
use crate::{Group, Package};
use crate::Package;
#[derive(Debug, Clone)]
pub struct Flatpak {
+1 -1
View File
@@ -7,7 +7,7 @@ use serde_json::Value;
use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::{Group, Package};
use crate::Package;
macro_rules! ERROR{
($bin:expr) => {
+1 -1
View File
@@ -8,7 +8,7 @@ use serde_json::Value;
use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::{Group, Package};
use crate::Package;
#[derive(Debug, Clone)]
pub struct Rust {
@@ -4,7 +4,7 @@ mod types;
use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::cmd::run_external_command;
use crate::{Group, Package};
use crate::Package;
use anyhow::{bail, Context, Result};
use std::collections::HashSet;
use std::process::Command;
+1 -1
View File
@@ -8,7 +8,7 @@ use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::backend::root::build_base_command_with_privileges;
use crate::cmd::run_external_command;
use crate::{Group, Package};
use crate::Package;
#[derive(Debug, Clone)]
pub struct Void {
+3 -4
View File
@@ -2,12 +2,11 @@ use std::cmp::{Eq, Ord};
use std::collections::{HashMap, HashSet};
use std::hash::Hash;
use std::process::Command;
use std::rc::Rc;
use anyhow::{Context, Result};
use crate::cmd::run_external_command;
use crate::{Group, Package};
use crate::{Group, Groups, Package};
pub type Switches = &'static [&'static str];
pub type Text = &'static str;
@@ -51,7 +50,7 @@ pub trait Backend {
/// Load all packages from a set of groups. The backend will visit all groups,
/// find its own section, and clone all packages into its own struct.
fn load(&mut self, groups: &HashSet<Group>);
fn load(&mut self, groups: &Groups);
/// Get all managed packages for this backend, i.e. all packages
/// under the corresponding section in all group files.
@@ -75,7 +74,7 @@ pub trait Backend {
/// Assign each of the packages to an individual group by editing the
/// group files.
fn assign_group(&self, to_assign: Vec<(Package, Rc<Group>)>) -> Result<()> {
fn assign_group(&self, to_assign: Vec<(Package, Group)>) -> Result<()> {
let group_package_map = to_hashmap(to_assign);
let section_header = format!("[{}]", self.get_section());
+1 -1
View File
@@ -34,7 +34,7 @@ macro_rules! impl_backend_constants {
&self.packages
}
fn load(&mut self, groups: &HashSet<Group>) {
fn load(&mut self, groups: &crate::Groups) {
let own_section_name = self.get_section();
groups
+1 -1
View File
@@ -7,11 +7,11 @@ pub mod todo_per_backend;
use crate::backend::backend_trait::Switches;
use crate::backend::backend_trait::Text;
use crate::Group;
use crate::Groups;
use crate::Package;
use anyhow::Result;
use backend_trait::Backend;
use std::collections::HashSet;
use std::rc::Rc;
use self::actual::{
fedora::Fedora, flatpak::Flatpak, python::Python, rust::Rust, rustup::Rustup, void::Void,
+182
View File
@@ -0,0 +1,182 @@
//! The clap declarative command line interface
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
#[derive(Parser)]
#[command(
version,
author,
arg_required_else_help(true),
subcommand_required(true),
disable_help_subcommand(true),
disable_version_flag(true)
)]
/// multi-backend declarative package manager for Linux
pub struct MainArguments {
#[command(subcommand)]
pub subcommand: MainSubcommand,
}
#[derive(Subcommand)]
pub enum MainSubcommand {
Group(GroupArguments),
Package(PackageArguments),
Version(VersionArguments),
}
#[derive(Args)]
#[command(
arg_required_else_help(true),
visible_alias("g"),
subcommand_required(true)
)]
/// manage groups
pub struct GroupArguments {
#[command(subcommand)]
pub group_action: GroupAction,
}
#[derive(Subcommand)]
pub enum GroupAction {
Edit(EditGroupAction),
Export(ExportGroupAction),
Import(ImportGroupAction),
List(ListGroupAction),
New(NewGroupAction),
Remove(RemoveGroupAction),
Show(ShowGroupAction),
}
#[derive(Args)]
#[command(arg_required_else_help(true), visible_alias("ed"))]
/// edit one or more existing group
pub struct EditGroupAction {
#[arg(required(true), num_args(1..))]
/// a previously imported group
pub edit_groups: Vec<String>,
}
#[derive(Args)]
#[command(arg_required_else_help(true), visible_alias("ex"))]
/// export one or more group files
pub struct ExportGroupAction {
#[arg(required(true), num_args(1..))]
/// the file to export as group
pub export_groups: Vec<String>,
#[arg(short, long)]
/// (optional) the directory under which to save the group
pub output_dir: Option<PathBuf>,
#[arg(short, long)]
/// overwrite output files if they exist
pub force: bool,
}
#[derive(Args)]
#[command(arg_required_else_help(true), visible_alias("i"))]
/// import one or more group files
pub struct ImportGroupAction {
#[arg(required(true), num_args(1..))]
/// the file to import as group
pub import_groups: Vec<String>,
}
#[derive(Args)]
#[command(visible_alias("l"))]
/// list names of imported groups
pub struct ListGroupAction {}
#[derive(Args)]
#[command(arg_required_else_help(true), visible_alias("n"))]
/// create new group files
pub struct NewGroupAction {
#[arg(required(true), num_args(1..))]
/// the groups to create
pub new_groups: Vec<String>,
#[arg(short, long)]
/// edit the new group files after creation
pub edit: bool,
}
#[derive(Args)]
#[command(arg_required_else_help(true), visible_alias("r"))]
/// remove one or more previously imported groups
pub struct RemoveGroupAction {
#[arg(required(true), num_args(1..))]
/// a previously imported group that will be removed
pub remove_groups: Vec<String>,
}
#[derive(Args)]
#[command(arg_required_else_help(true), visible_alias("s"))]
/// show packages under an imported group
pub struct ShowGroupAction {
#[arg(required(true), num_args(1..))]
/// group file(s) to show
pub show_groups: Vec<String>,
}
#[derive(Args)]
#[command(
arg_required_else_help(true),
subcommand_required(true),
visible_alias("p")
)]
/// manage packages
pub struct PackageArguments {
#[command(subcommand)]
pub package_action: PackageAction,
}
#[derive(Subcommand)]
pub enum PackageAction {
Clean(CleanPackageAction),
Review(ReviewPackageAction),
Search(SearchPackageAction),
Sync(SyncPackageAction),
Unmanaged(UnmanagedPackageAction),
}
#[derive(Args)]
#[command(visible_alias("c"))]
/// remove unmanaged packages
pub struct CleanPackageAction {
#[arg(long)]
/// do not ask for any confirmation
pub no_confirm: bool,
}
#[derive(Args)]
#[command(visible_alias("r"))]
/// review unmanaged packages
pub struct ReviewPackageAction {}
#[derive(Args)]
#[command(arg_required_else_help(true), visible_alias("se"))]
/// search for packages which match a provided regex
pub struct SearchPackageAction {
#[arg(required(true))]
/// the regular expression the package must match
pub regex: String,
}
#[derive(Args)]
#[command(visible_alias("sy"))]
/// install packages from all imported groups
pub struct SyncPackageAction {
#[arg(long)]
/// do not ask for any confirmation
pub no_confirm: bool,
}
#[derive(Args)]
#[command(visible_alias("u"))]
/// show explicitly installed packages not managed by pacdef
pub struct UnmanagedPackageAction {}
#[derive(Args)]
pub struct VersionArguments {}
+1 -1
View File
@@ -3,7 +3,7 @@ use std::io::{ErrorKind, Write};
use std::path::Path;
use anyhow::{bail, Context, Result};
use serde_derive::{Deserialize, Serialize};
use serde::{Deserialize, Serialize};
// Update the master README if fields change.
/// Config for the program, as listed in `$XDG_CONFIG_HOME/pacdef/pacdef.yaml`.
+357 -373
View File
@@ -1,49 +1,32 @@
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::env::current_dir;
use std::fs::{copy, create_dir_all, remove_file, rename, File};
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{bail, ensure, Context, Result};
use const_format::formatcp;
use crate::args::datastructure::{
Arguments, Edit, Force, GroupAction, Groups, Noconfirm, OutputDir, PackageAction, Regex,
};
use crate::backend::backend_trait::Backend;
use crate::backend::todo_per_backend::ToDoPerBackend;
use crate::backend::AnyBackend;
use crate::cmd::run_edit_command;
use crate::env::should_print_debug_info;
use crate::cli::{
CleanPackageAction, EditGroupAction, ExportGroupAction, GroupAction, GroupArguments,
ImportGroupAction, ListGroupAction, MainArguments, MainSubcommand, NewGroupAction,
PackageAction, PackageArguments, RemoveGroupAction, ReviewPackageAction, SearchPackageAction,
ShowGroupAction, SyncPackageAction, UnmanagedPackageAction, VersionArguments,
};
use crate::cmd::{run_edit_command, run_external_command};
use crate::env::{get_editor, should_print_debug_info};
use crate::path::{binary_in_path, get_absolutized_file_paths, get_group_dir};
use crate::search;
use crate::review::review;
use crate::search::search_packages;
use crate::ui::get_user_confirmation;
use crate::Config;
use crate::Group;
use crate::{review, Error};
/// Most data that is required during runtime of the program.
pub struct Pacdef {
/// The command line arguments. Is an `Option` so that we can take ownership later without cloning.
args: Option<Arguments>,
/// The config of the program.
config: Config,
/// The hashset of all groups.
groups: HashSet<Group>,
}
impl Pacdef {
/// Creates a new [`Pacdef`]. `config` should be passed from [`Config::load`], and `args` from
/// [`args::get`].
#[must_use]
pub const fn new(args: Arguments, config: Config, groups: HashSet<Group>) -> Self {
Self {
args: Some(args),
config,
groups,
}
}
use crate::{Config, Error, Groups};
impl MainArguments {
/// Run the action that was provided by the user as first argument.
///
/// For convenience sake, all called functions take a `&self` argument, even if
@@ -52,305 +35,132 @@ impl Pacdef {
/// # Errors
///
/// This function propagates errors from the underlying functions.
///
/// # Panics
///
/// This function panics if the `args` field is `None`.
pub fn run_action_from_arg(mut self) -> Result<()> {
match self
.args
.take()
.expect("if there were no args we would not get to here")
{
Arguments::Group(group_args) => self.run_group_subcommand(&group_args),
Arguments::Package(package_args) => self.run_package_subcommand(&package_args),
Arguments::Version => {
self.show_version();
Ok(())
}
pub fn run(self, groups: &Groups, config: &Config) -> Result<()> {
match self.subcommand {
MainSubcommand::Group(group) => group.run(groups),
MainSubcommand::Package(package) => package.run(groups, config),
MainSubcommand::Version(version) => version.run(),
}
}
}
fn run_group_subcommand(self, args: &GroupAction) -> Result<()> {
match args {
GroupAction::Edit(Groups(groups)) => self.edit_groups(groups),
GroupAction::Export(Groups(groups), OutputDir(dir), Force(force)) => {
self.export_groups(groups, dir.as_ref(), *force)
}
GroupAction::Import(Groups(groups)) => self.import_groups(groups),
GroupAction::List => self.show_groups(),
GroupAction::New(Groups(groups), Edit(edit)) => self.new_groups(groups, *edit),
GroupAction::Remove(Groups(groups)) => self.remove_groups(groups),
GroupAction::Show(Groups(groups)) => self.show_group_content(groups),
impl VersionArguments {
/// If the crate was compiled from git, return `pacdef, <version> (<hash>)`.
/// Otherwise return `pacdef, <version>`.
fn run(self) -> Result<()> {
let backends = get_included_backends();
let mut result = format!("pacdef, version: {}\n", get_version_string());
result.push_str("supported backends:");
for b in backends {
result.push_str("\n ");
result.push_str(b);
}
println!("{}", result);
Ok(())
}
}
impl GroupArguments {
fn run(self, groups: &Groups) -> Result<()> {
match self.group_action {
GroupAction::Edit(edit) => edit.run(groups),
GroupAction::Export(export) => export.run(groups),
GroupAction::Import(import) => import.run(),
GroupAction::List(list) => list.run(groups),
GroupAction::New(new) => new.run(),
GroupAction::Remove(remove) => remove.run(groups),
GroupAction::Show(show) => show.run(groups),
}
}
}
fn run_package_subcommand(mut self, args: &PackageAction) -> Result<()> {
match args {
PackageAction::Clean(Noconfirm(noconfirm)) => self.clean_packages(*noconfirm),
PackageAction::Review => review::review(self.get_unmanaged_packages()?, self.groups),
PackageAction::Search(Regex(regex)) => {
self.warn_about_groups_that_arent_symlinks();
search::search_packages(regex, &self.groups)
}
PackageAction::Sync(Noconfirm(noconfirm)) => self.install_packages(*noconfirm),
PackageAction::Unmanaged => self.show_unmanaged_packages(),
}
}
fn get_missing_packages(&mut self) -> Result<ToDoPerBackend> {
let mut to_install = ToDoPerBackend::new();
if self.groups.is_empty() {
impl EditGroupAction {
fn run(self, groups: &Groups) -> Result<()> {
if groups.is_empty() {
eprintln!("WARNING: no group files found");
}
for mut backend in AnyBackend::iter() {
if self
.config
.disabled_backends
.contains(&backend.get_section().to_string())
{
continue;
}
if !binary_in_path(backend.get_binary())? {
continue;
}
self.overwrite_values_from_config(&mut backend);
backend.load(&self.groups);
match backend.get_missing_packages_sorted() {
Ok(diff) => to_install.push((backend, diff)),
Err(error) => show_backend_query_error(&error, &backend),
};
}
Ok(to_install)
}
fn overwrite_values_from_config(&mut self, backend: &mut AnyBackend) {
#[cfg(feature = "arch")]
{
if let AnyBackend::Arch(arch) = backend {
arch.binary.clone_from(&self.config.aur_helper);
arch.aur_rm_args.clone_from(&self.config.aur_rm_args);
}
}
if let AnyBackend::Flatpak(flatpak) = backend {
flatpak.systemwide = self.config.flatpak_systemwide;
}
if let AnyBackend::Python(python) = backend {
python.binary.clone_from(&self.config.pip_binary);
}
}
fn install_packages(&mut self, noconfirm: bool) -> Result<()> {
self.warn_about_groups_that_arent_symlinks();
let to_install = self.get_missing_packages()?;
if to_install.nothing_to_do_for_all_backends() {
println!("nothing to do");
return Ok(());
}
println!("Would install the following packages:\n");
to_install.show().context("printing things to do")?;
println!();
if noconfirm {
println!("proceeding without confirmation");
} else if !get_user_confirmation()? {
return Ok(());
}
to_install.install_missing_packages(noconfirm)
}
fn warn_about_groups_that_arent_symlinks(&self) {
for group in &self.groups {
if group.warn_symlink {
eprintln!(
"WARNING: group file {} is not a symlink",
group.path.to_string_lossy()
);
}
}
}
fn edit_groups(&self, groups: &[String]) -> Result<()> {
self.warn_about_groups_that_arent_symlinks();
if self.groups.is_empty() {
eprintln!("WARNING: no group files found");
}
let group_files: Vec<_> = find_groups_by_name(groups, &self.groups)
let group_files: Vec<_> = find_groups_by_name(&self.edit_groups, groups)
.context("getting group files for args")?
.into_iter()
.map(|g| g.path.as_path())
.collect();
run_edit_command(&group_files).context("running editor")?;
let mut cmd = Command::new(get_editor().context("getting suitable editor")?);
cmd.current_dir(
group_files[0]
.parent()
.context("getting parent dir of first file argument")?,
);
for group_file in group_files {
cmd.arg(group_file.to_string_lossy().to_string());
}
run_external_command(cmd)?;
Ok(())
}
}
fn show_version(self) {
println!("{}", get_name_and_version());
}
fn show_unmanaged_packages(mut self) -> Result<()> {
let unmanaged_per_backend = &self.get_unmanaged_packages()?;
if unmanaged_per_backend.nothing_to_do_for_all_backends() {
return Ok(());
}
unmanaged_per_backend
.show()
.context("printing things to do")
}
/// Get a list of unmanaged packages per backend.
impl ExportGroupAction {
/// Export pacdef groups by moving a group file to an output dir. The path of the
/// group file relative to the group base dir will be replicated under the output
/// directory.
///
/// This method loops through all enabled `Backend`s whose binary is in `PATH`.
/// By default, the output dir is the current working directory. `output_dir` may be
/// specified to the path of another directory, in which case `output_dir` must
/// exist.
///
/// If `force` is `true`, the output file will be overwritten if it exists.
///
/// # Errors
///
/// This function will propagate errors from the individual backends.
fn get_unmanaged_packages(&mut self) -> Result<ToDoPerBackend> {
self.warn_about_groups_that_arent_symlinks();
if self.groups.is_empty() {
eprintln!("WARNING: no group files found");
}
let mut result = ToDoPerBackend::new();
for mut backend in AnyBackend::iter() {
if self
.config
.disabled_backends
.contains(&backend.get_section().to_string())
{
continue;
}
if !binary_in_path(backend.get_binary())? {
continue;
}
self.overwrite_values_from_config(&mut backend);
backend.load(&self.groups);
match backend.get_unmanaged_packages_sorted() {
Ok(unmanaged) => result.push((backend, unmanaged)),
Err(error) => show_backend_query_error(&error, &backend),
};
}
Ok(result)
}
/// Print the alphabetically sorted names of all groups to stdout.
/// This function will return an error if
/// - the group file is a symlink (in which case exporting makes no sense),
/// - the output file exists and `force` is not `true`, or
/// - the user does not have permission to write to the output dir.
///
/// This methods cannot return an error. It returns a `Result` to be consistent
/// with other methods.
fn show_groups(self) -> Result<()> {
self.warn_about_groups_that_arent_symlinks();
/// # Limitations
///
/// At the moment we cannot export nested group dirs. The user would have to
/// export every group file individually, or use a shell glob.
fn run(self, groups: &Groups) -> Result<()> {
let groups = find_groups_by_name(&self.export_groups, groups)?;
let output_dir = match self.output_dir {
Some(p) => p,
None => current_dir().context("no output dir specified, getting current directory")?,
};
if self.groups.is_empty() {
eprintln!("WARNING: no group files found");
}
let mut vec: Vec<_> = self.groups.iter().collect();
vec.sort_unstable();
for g in vec {
println!("{}", g.name);
}
Ok(())
}
fn clean_packages(&mut self, noconfirm: bool) -> Result<()> {
let to_remove = self.get_unmanaged_packages()?;
if to_remove.nothing_to_do_for_all_backends() {
println!("nothing to do");
return Ok(());
}
println!("Would remove the following packages:\n");
to_remove.show().context("printing things to do")?;
println!();
if noconfirm {
println!("proceeding without confirmation");
} else if !get_user_confirmation()? {
return Ok(());
}
to_remove.remove_unmanaged_packages(noconfirm)
}
fn show_group_content(&self, args: &[String]) -> Result<()> {
self.warn_about_groups_that_arent_symlinks();
if self.groups.is_empty() {
eprintln!("WARNING: no group files found");
}
let mut errors = vec![];
let mut groups = vec![];
// make sure all args exist before doing anything
for arg_group in args {
let possible_group = self.groups.iter().find(|g| g.name == **arg_group);
let Some(group) = possible_group else {
errors.push((*arg_group).clone());
continue;
};
groups.push(group);
}
// return an error if any arg was not found
ensure!(
errors.is_empty(),
crate::Error::MultipleGroupsNotFound(errors)
output_dir.exists() && output_dir.is_dir(),
"output must be a directory and exist"
);
let show_more_than_one_group = args.len() > 1;
for group in &groups {
ensure!(!&group.path.is_symlink(), "cannot export symlinks");
let mut iter = groups.into_iter().peekable();
let mut exported_path = output_dir.clone();
exported_path.push(PathBuf::from(&group.name));
while let Some(group) = iter.next() {
if show_more_than_one_group {
let name = &group.name;
println!("{name}");
for _ in 0..name.len() {
print!("-");
}
println!();
}
ensure!(
!self.force && !exported_path.exists(),
"{exported_path:?} already exists"
);
println!("{group}");
if iter.peek().is_some() {
println!();
}
create_parent(&exported_path)
.with_context(|| format!("creating parent dir of {exported_path:?}"))?;
move_file(&group.path, &exported_path).context("moving file")?;
symlink(&exported_path, &group.path).context("creating symlink to exported file")?;
}
Ok(())
}
}
fn import_groups(&self, file_names: &[String]) -> Result<()> {
let files = get_absolutized_file_paths(file_names)?;
impl ImportGroupAction {
fn run(self) -> Result<()> {
let files = get_absolutized_file_paths(&self.import_groups)?;
let groups_dir = get_group_dir()?;
for target in files {
@@ -377,21 +187,29 @@ impl Pacdef {
Ok(())
}
}
fn remove_groups(&self, groups: &[String]) -> Result<()> {
if self.groups.is_empty() {
impl ListGroupAction {
/// Print the alphabetically sorted names of all groups to stdout.
///
/// This methods cannot return an error. It returns a `Result` to be consistent
/// with other methods.
fn run(self, groups: &Groups) -> Result<()> {
if groups.is_empty() {
eprintln!("WARNING: no group files found");
}
let found = find_groups_by_name(groups, &self.groups)?;
for group in found {
remove_file(&group.path)?;
let mut vec: Vec<_> = groups.iter().collect();
vec.sort_unstable();
for g in vec {
println!("{}", g.name);
}
Ok(())
}
}
impl NewGroupAction {
/// Create empty group files.
///
/// If `edit` is `true`, the editor will be run to edit the files after they are
@@ -404,18 +222,19 @@ impl Pacdef {
/// - a group with the same name already exists,
/// - the editor cannot be run, or
/// - if we do not have permission to write to the group dir.
fn new_groups(&self, new_groups: &[String], edit: bool) -> Result<()> {
fn run(&self) -> Result<()> {
let group_path = get_group_dir()?;
// prevent group names that resolve to directories
for name in new_groups {
for new_group in &self.new_groups {
ensure!(
*name != "." && *name != "..",
crate::Error::InvalidGroupName(name.clone())
new_group != "." && new_group != "..",
crate::Error::InvalidGroupName(new_group.clone())
);
}
let paths: Vec<_> = new_groups
let paths: Vec<_> = self
.new_groups
.iter()
.map(|name| {
let mut base = group_path.clone();
@@ -435,72 +254,251 @@ impl Pacdef {
File::create(file)?;
}
if edit {
if self.edit {
run_edit_command(&paths).context("running editor")?;
}
Ok(())
}
}
/// Export pacdef groups by moving a group file to an output dir. The path of the
/// group file relative to the group base dir will be replicated under the output
/// directory.
///
/// By default, the output dir is the current working directory. `output_dir` may be
/// specified to the path of another directory, in which case `output_dir` must
/// exist.
///
/// If `force` is `true`, the output file will be overwritten if it exists.
///
/// # Errors
///
/// This function will return an error if
/// - the group file is a symlink (in which case exporting makes no sense),
/// - the output file exists and `force` is not `true`, or
/// - the user does not have permission to write to the output dir.
///
/// # Limitations
///
/// At the moment we cannot export nested group dirs. The user would have to
/// export every group file individually, or use a shell glob.
fn export_groups(
&self,
names: &[String],
output_dir: Option<&String>,
force: bool,
) -> Result<()> {
let groups = find_groups_by_name(names, &self.groups)?;
let output_dir = match output_dir.map(PathBuf::from) {
Some(p) => p,
None => current_dir().context("no output dir specified, getting current directory")?,
};
impl RemoveGroupAction {
fn run(self, groups: &Groups) -> Result<()> {
if groups.is_empty() {
eprintln!("WARNING: no group files found");
}
ensure!(
output_dir.exists() && output_dir.is_dir(),
"output must be a directory and exist"
);
let found = find_groups_by_name(&self.remove_groups, groups)?;
for group in &groups {
ensure!(!&group.path.is_symlink(), "cannot export symlinks");
let mut exported_path = output_dir.clone();
exported_path.push(PathBuf::from(&group.name));
ensure!(
!force && !exported_path.exists(),
"{exported_path:?} already exists"
);
create_parent(&exported_path)
.with_context(|| format!("creating parent dir of {exported_path:?}"))?;
move_file(&group.path, &exported_path).context("moving file")?;
symlink(&exported_path, &group.path).context("creating symlink to exported file")?;
for group in found {
remove_file(&group.path)?;
}
Ok(())
}
}
impl ShowGroupAction {
fn run(self, groups: &Groups) -> Result<()> {
if groups.is_empty() {
eprintln!("WARNING: no group files found");
}
let mut errors = vec![];
let mut found_groups = vec![];
// make sure all args exist before doing anything
for show_group in &self.show_groups {
let possible_group = groups.iter().find(|group| group.name == *show_group);
let Some(group) = possible_group else {
errors.push(show_group.to_string());
continue;
};
found_groups.push(group);
}
// return an error if any arg was not found
ensure!(
errors.is_empty(),
crate::Error::MultipleGroupsNotFound(errors)
);
let show_more_than_one_group = self.show_groups.len() > 1;
let mut iter = found_groups.into_iter().peekable();
while let Some(group) = iter.next() {
if show_more_than_one_group {
let name = &group.name;
println!("{name}");
for _ in 0..name.len() {
print!("-");
}
println!();
}
println!("{group}");
if iter.peek().is_some() {
println!();
}
}
Ok(())
}
}
impl PackageArguments {
fn run(self, groups: &Groups, config: &Config) -> Result<()> {
match self.package_action {
PackageAction::Clean(clean) => clean.run(groups, config),
PackageAction::Review(review) => review.run(groups, config),
PackageAction::Search(search) => search.run(groups),
PackageAction::Sync(sync) => sync.run(groups, config),
PackageAction::Unmanaged(unmanaged) => unmanaged.run(groups, config),
}
}
}
impl CleanPackageAction {
fn run(self, groups: &Groups, config: &Config) -> Result<()> {
let to_remove = get_unmanaged_packages(groups, config)?;
if to_remove.nothing_to_do_for_all_backends() {
println!("nothing to do");
return Ok(());
}
println!("Would remove the following packages:\n");
to_remove.show().context("printing things to do")?;
println!();
if self.no_confirm {
println!("proceeding without confirmation");
} else if !get_user_confirmation()? {
return Ok(());
}
to_remove.remove_unmanaged_packages(self.no_confirm)
}
}
impl ReviewPackageAction {
fn run(self, groups: &Groups, config: &Config) -> Result<()> {
review(get_unmanaged_packages(groups, config)?, groups)
}
}
impl SearchPackageAction {
fn run(self, groups: &Groups) -> Result<()> {
search_packages(&self.regex, groups)
}
}
impl SyncPackageAction {
fn run(self, groups: &Groups, config: &Config) -> Result<()> {
let to_install = get_missing_packages(groups, config)?;
if to_install.nothing_to_do_for_all_backends() {
println!("nothing to do");
return Ok(());
}
println!("Would install the following packages:\n");
to_install.show().context("printing things to do")?;
println!();
if self.no_confirm {
println!("proceeding without confirmation");
} else if !get_user_confirmation()? {
return Ok(());
}
to_install.install_missing_packages(self.no_confirm)
}
}
impl UnmanagedPackageAction {
fn run(self, groups: &Groups, config: &Config) -> Result<()> {
let unmanaged_per_backend = &get_unmanaged_packages(groups, config)?;
if unmanaged_per_backend.nothing_to_do_for_all_backends() {
return Ok(());
}
unmanaged_per_backend
.show()
.context("printing things to do")
}
}
fn get_missing_packages(groups: &Groups, config: &Config) -> Result<ToDoPerBackend> {
let mut to_install = ToDoPerBackend::new();
if groups.is_empty() {
eprintln!("WARNING: no group files found");
}
for mut backend in AnyBackend::iter() {
if config
.disabled_backends
.contains(&backend.get_section().to_string())
{
continue;
}
if !binary_in_path(backend.get_binary())? {
continue;
}
overwrite_values_from_config(&mut backend, config);
backend.load(groups);
match backend.get_missing_packages_sorted() {
Ok(diff) => to_install.push((backend, diff)),
Err(error) => show_backend_query_error(&error, &backend),
};
}
Ok(to_install)
}
fn overwrite_values_from_config(backend: &mut AnyBackend, config: &Config) {
#[cfg(feature = "arch")]
{
if let AnyBackend::Arch(arch) = backend {
arch.binary.clone_from(&config.aur_helper);
arch.aur_rm_args.clone_from(&config.aur_rm_args);
}
}
if let AnyBackend::Flatpak(flatpak) = backend {
flatpak.systemwide = config.flatpak_systemwide;
}
if let AnyBackend::Python(python) = backend {
python.binary.clone_from(&config.pip_binary);
}
}
/// Get a list of unmanaged packages per backend.
///
/// This method loops through all enabled `Backend`s whose binary is in `PATH`.
///
/// # Errors
///
/// This function will propagate errors from the individual backends.
fn get_unmanaged_packages(groups: &Groups, config: &Config) -> Result<ToDoPerBackend> {
if groups.is_empty() {
eprintln!("WARNING: no group files found");
}
let mut result = ToDoPerBackend::new();
for mut backend in AnyBackend::iter() {
if config
.disabled_backends
.contains(&backend.get_section().to_string())
{
continue;
}
if !binary_in_path(backend.get_binary())? {
continue;
}
overwrite_values_from_config(&mut backend, config);
backend.load(groups);
match backend.get_unmanaged_packages_sorted() {
Ok(unmanaged) => result.push((backend, unmanaged)),
Err(error) => show_backend_query_error(&error, &backend),
};
}
Ok(result)
}
/// Create the parent directory of the `path` if that directory does not exist.
///
/// Do nothing otherwise.
@@ -558,7 +556,7 @@ where
///
/// This function will return an error if any of the file names do not match one
/// of group names.
fn find_groups_by_name<'a>(names: &[String], groups: &'a HashSet<Group>) -> Result<Vec<&'a Group>> {
fn find_groups_by_name<'a>(names: &[String], groups: &'a Groups) -> Result<Vec<&'a Group>> {
let name_group_map: HashMap<&str, &Group> =
groups.iter().map(|g| (g.name.as_str(), g)).collect();
@@ -590,20 +588,6 @@ fn show_backend_query_error(error: &anyhow::Error, backend: &AnyBackend) {
}
}
/// If the crate was compiled from git, return `pacdef, <version> (<hash>)`.
/// Otherwise return `pacdef, <version>`.
fn get_name_and_version() -> String {
let backends = get_included_backends();
let mut result = format!("pacdef, version: {}\n", get_version_string());
result.push_str("supported backends:");
for b in backends {
result.push_str("\n ");
result.push_str(b);
}
result
}
/// If the crate was compiled from git, return `<version> (<hash>)`. Otherwise
/// return `<version>`.
pub const fn get_version_string() -> &'static str {
+7 -4
View File
@@ -1,4 +1,4 @@
use std::collections::HashSet;
use std::collections::{BTreeSet, HashSet};
use std::fmt::Display;
use std::fs::{create_dir, read_to_string, File};
use std::hash::Hash;
@@ -13,8 +13,11 @@ use crate::path::get_relative_path;
use super::{Package, Section};
/// A set of groups
pub type Groups = BTreeSet<Group>;
/// Representation of a group file.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Group {
/// Name of the group (file name from which it was read, relative to the group
/// base dir).
@@ -37,8 +40,8 @@ impl Group {
///
/// This function will return an error if any of the files under `group_dir` cannot
/// be accessed.
pub fn load(group_dir: &Path, warn_not_symlinks: bool) -> Result<HashSet<Self>> {
let mut result = HashSet::new();
pub fn load(group_dir: &Path, warn_not_symlinks: bool) -> Result<Groups> {
let mut result = Groups::new();
if !group_dir.is_dir() {
// we only need to create the innermost dir. The rest was already created from when
+1
View File
@@ -14,5 +14,6 @@ mod package;
mod section;
pub use group::Group;
pub use group::Groups;
pub use package::Package;
pub use section::Section;
+1 -1
View File
@@ -7,7 +7,7 @@ use anyhow::{ensure, Context, Result};
use super::Package;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Section {
pub name: String,
pub packages: HashSet<Package>,
+7 -4
View File
@@ -20,22 +20,25 @@
missing_docs
)]
mod args;
pub(crate) mod backend;
#[allow(missing_docs)]
pub mod cli;
mod cmd;
mod config;
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
mod core;
mod env;
mod errors;
mod grouping;
pub mod path;
mod review;
mod search;
mod ui;
pub use crate::args::get as get_args;
pub mod path;
pub use crate::config::Config;
pub use crate::core::Pacdef;
pub use crate::errors::Error;
pub use crate::grouping::Group;
pub use crate::grouping::Groups;
pub use crate::grouping::Package;
+14 -4
View File
@@ -19,8 +19,10 @@ use std::process::{ExitCode, Termination};
use anyhow::{bail, Context, Result};
use clap::Parser;
use pacdef::cli::MainArguments;
use pacdef::path::{get_config_path, get_config_path_old_version, get_group_dir};
use pacdef::{get_args, Config, Error as PacdefError, Group, Pacdef};
use pacdef::{Config, Error as PacdefError, Group};
const MAJOR_UPDATE_MESSAGE: &str = "VERSION UPGRADE
You seem to have used version 0.x of pacdef before.
@@ -51,7 +53,7 @@ fn handle_final_result(result: Result<()>) -> ExitCode {
}
fn main_inner() -> Result<()> {
let args = get_args();
let main_arguments = MainArguments::parse();
let config_file = get_config_path().context("getting config file")?;
@@ -73,8 +75,16 @@ fn main_inner() -> Result<()> {
let groups = Group::load(&group_dir, config.warn_not_symlinks)
.with_context(|| format!("loading groups under {}", group_dir.to_string_lossy()))?;
let pacdef = Pacdef::new(args, config, groups);
pacdef.run_action_from_arg().context("running action")
for group in groups.iter() {
if group.warn_symlink {
eprintln!(
"WARNING: group file {} is not a symlink",
group.path.to_string_lossy()
);
}
}
main_arguments.run(&groups, &config)
}
fn load_default_config(config_file: &Path) -> Result<Config> {
+10 -12
View File
@@ -1,18 +1,16 @@
use std::rc::Rc;
use crate::{backend::AnyBackend, Group, Package};
use super::strategy::Strategy;
#[derive(Debug, PartialEq)]
pub(super) enum ReviewAction {
pub enum ReviewAction {
AsDependency(Package),
Delete(Package),
AssignGroup(Package, Rc<Group>),
AssignGroup(Package, Group),
}
#[derive(Debug)]
pub(super) enum ReviewIntention {
pub enum ReviewIntention {
AsDependency,
AssignGroup,
Delete,
@@ -24,20 +22,20 @@ pub(super) enum ReviewIntention {
}
#[derive(Debug)]
pub(super) struct ReviewsPerBackend {
pub struct ReviewsPerBackend {
items: Vec<(AnyBackend, Vec<ReviewAction>)>,
}
impl ReviewsPerBackend {
pub(super) fn new() -> Self {
pub fn new() -> Self {
Self { items: vec![] }
}
pub(super) fn nothing_to_do(&self) -> bool {
pub fn nothing_to_do(&self) -> bool {
self.items.iter().all(|(_, vec)| vec.is_empty())
}
pub(super) fn push(&mut self, value: (AnyBackend, Vec<ReviewAction>)) {
pub fn push(&mut self, value: (AnyBackend, Vec<ReviewAction>)) {
self.items.push(value);
}
@@ -46,7 +44,7 @@ impl ReviewsPerBackend {
///
/// If there are no actions for a `Backend`, then that `Backend` is removed from the return
/// value.
pub(super) fn into_strategies(self) -> Vec<Strategy> {
pub fn into_strategies(self) -> Vec<Strategy> {
let mut result = vec![];
for (backend, actions) in self {
@@ -85,7 +83,7 @@ impl IntoIterator for ReviewsPerBackend {
}
}
pub(super) enum ContinueWithReview {
pub enum ContinueWithReview {
Yes,
No,
NoAndApply,
@@ -94,7 +92,7 @@ pub(super) enum ContinueWithReview {
fn extract_actions(
actions: Vec<ReviewAction>,
to_delete: &mut Vec<Package>,
assign_group: &mut Vec<(Package, Rc<Group>)>,
assign_group: &mut Vec<(Package, Group)>,
as_dependency: &mut Vec<Package>,
) {
for action in actions {
+8 -15
View File
@@ -2,26 +2,19 @@ mod datastructures;
mod strategy;
use std::io::{stdin, stdout, Write};
use std::rc::Rc;
use anyhow::Result;
use crate::backend::backend_trait::Backend;
use crate::backend::todo_per_backend::ToDoPerBackend;
use crate::ui::{get_user_confirmation, read_single_char_from_terminal};
use crate::{Group, Package};
use crate::{Group, Groups, Package};
use self::datastructures::{ContinueWithReview, ReviewAction, ReviewIntention, ReviewsPerBackend};
use self::strategy::Strategy;
pub fn review(
todo_per_backend: ToDoPerBackend,
groups: impl IntoIterator<Item = Group>,
) -> Result<()> {
pub fn review(todo_per_backend: ToDoPerBackend, groups: &Groups) -> Result<()> {
let mut reviews = ReviewsPerBackend::new();
let mut groups: Vec<Rc<Group>> = groups.into_iter().map(Rc::new).collect();
groups.sort_unstable();
if todo_per_backend.nothing_to_do_for_all_backends() {
println!("nothing to do");
@@ -32,7 +25,7 @@ pub fn review(
let mut actions = vec![];
for package in packages {
println!("{}: {package}", backend.get_section());
match get_action_for_package(package, &groups, &mut actions, &backend)? {
match get_action_for_package(package, groups, &mut actions, &backend)? {
ContinueWithReview::Yes => continue,
ContinueWithReview::No => return Ok(()),
ContinueWithReview::NoAndApply => {
@@ -76,7 +69,7 @@ pub fn review(
fn get_action_for_package(
package: Package,
groups: &[Rc<Group>],
groups: &Groups,
reviews: &mut Vec<ReviewAction>,
backend: &dyn Backend,
) -> Result<ContinueWithReview> {
@@ -92,7 +85,7 @@ fn get_action_for_package(
}
ReviewIntention::AssignGroup => {
if let Ok(Some(group)) = ask_group(groups) {
reviews.push(ReviewAction::AssignGroup(package, group));
reviews.push(ReviewAction::AssignGroup(package, group.clone()));
break;
};
}
@@ -154,7 +147,7 @@ fn print_query(supports_as_dependency: bool) -> Result<()> {
Ok(())
}
fn print_enumerated_groups(groups: &[Rc<Group>]) {
fn print_enumerated_groups(groups: &Groups) {
let number_digits = get_amount_of_digits_for_number(groups.len());
for (i, group) in groups.iter().enumerate() {
@@ -166,7 +159,7 @@ fn get_amount_of_digits_for_number(number: usize) -> usize {
number.to_string().len()
}
fn ask_group(groups: &[Rc<Group>]) -> Result<Option<Rc<Group>>> {
fn ask_group(groups: &Groups) -> Result<Option<&Group>> {
print_enumerated_groups(groups);
let mut buf = String::new();
stdin().read_line(&mut buf)?;
@@ -179,7 +172,7 @@ fn ask_group(groups: &[Rc<Group>]) -> Result<Option<Rc<Group>>> {
};
if idx < groups.len() {
Ok(Some(groups[idx].clone()))
Ok(groups.iter().nth(idx))
} else {
Ok(None)
}
+7 -9
View File
@@ -1,5 +1,3 @@
use std::rc::Rc;
use anyhow::Result;
use crate::{
@@ -8,19 +6,19 @@ use crate::{
};
#[derive(Debug)]
pub(super) struct Strategy {
pub struct Strategy {
backend: AnyBackend,
delete: Vec<Package>,
as_dependency: Vec<Package>,
assign_group: Vec<(Package, Rc<Group>)>,
assign_group: Vec<(Package, Group)>,
}
impl Strategy {
pub(super) fn new(
pub fn new(
backend: AnyBackend,
delete: Vec<Package>,
as_dependency: Vec<Package>,
assign_group: Vec<(Package, Rc<Group>)>,
assign_group: Vec<(Package, Group)>,
) -> Self {
Self {
backend,
@@ -30,7 +28,7 @@ impl Strategy {
}
}
pub(super) fn execute(self) -> Result<()> {
pub fn execute(self) -> Result<()> {
if !self.delete.is_empty() {
self.backend.remove_packages(&self.delete, false)?;
}
@@ -46,7 +44,7 @@ impl Strategy {
Ok(())
}
pub(super) fn show(&self) {
pub fn show(&self) {
if self.nothing_to_do() {
return;
}
@@ -75,7 +73,7 @@ impl Strategy {
}
}
pub(super) fn nothing_to_do(&self) -> bool {
pub fn nothing_to_do(&self) -> bool {
self.delete.is_empty() && self.as_dependency.is_empty() && self.assign_group.is_empty()
}
}
+5 -3
View File
@@ -1,11 +1,13 @@
use std::collections::HashSet;
use std::iter::Peekable;
use std::vec::IntoIter;
use anyhow::{bail, Result};
use regex::Regex;
use crate::grouping::{Group, Package, Section};
use crate::{
grouping::{Group, Package, Section},
Groups,
};
/// Find all packages in all groups whose name match the regex from the
/// command-line arguments. Print the name of the packages per group and
@@ -16,7 +18,7 @@ use crate::grouping::{Group, Package, Section};
/// This function will return an error if
/// - an invalid regex was provided, or
/// - no matching packages could be found.
pub fn search_packages(regex_str: &str, groups: &HashSet<Group>) -> Result<()> {
pub fn search_packages(regex_str: &str, groups: &Groups) -> Result<()> {
if groups.is_empty() {
eprintln!("WARNING: no group files found");
bail!(crate::errors::Error::NoPackagesFound);