restructure project into a virtual manifest
- remove redundant readme - de-duplicate Cargo.toml information using workspace inheritance - rename "pacdef_core" to "pacdef" - move "crates/main/main.rs" to "crates/pacdef/src/main.rs"
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#[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>);
|
||||
@@ -0,0 +1,14 @@
|
||||
mod cli;
|
||||
mod datastructure;
|
||||
mod parsing;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use datastructure::*;
|
||||
|
||||
/// Get and parse the CLI arguments.
|
||||
#[must_use]
|
||||
pub fn get() -> Arguments {
|
||||
let args = cli::build_cli().get_matches();
|
||||
parsing::parse(args)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use super::datastructure::*;
|
||||
|
||||
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 {
|
||||
use GroupAction::*;
|
||||
|
||||
match args.subcommand() {
|
||||
Some(("edit", args)) => Edit(get_groups(args)),
|
||||
Some(("export", args)) => Export(get_groups(args), get_output_dir(args), get_force(args)),
|
||||
Some(("import", args)) => Import(get_groups(args)),
|
||||
Some(("list", _)) => List,
|
||||
Some(("new", args)) => New(get_groups(args), get_edit(args)),
|
||||
Some(("remove", args)) => Remove(get_groups(args)),
|
||||
Some(("show", args)) => 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 {
|
||||
use PackageAction::*;
|
||||
|
||||
match args.subcommand() {
|
||||
Some(("clean", args)) => Clean(get_noconfirm(args)),
|
||||
Some(("review", _)) => Review,
|
||||
Some(("search", args)) => Search(get_regex(args)),
|
||||
Some(("sync", args)) => Sync(get_noconfirm(args)),
|
||||
Some(("unmanaged", _)) => 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"))
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
use std::collections::HashSet;
|
||||
use std::process::Command;
|
||||
|
||||
use alpm::Alpm;
|
||||
use alpm::PackageReason::Explicit;
|
||||
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};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Arch {
|
||||
pub(crate) binary: String,
|
||||
pub(crate) aur_rm_args: Vec<String>,
|
||||
pub(crate) packages: HashSet<Package>,
|
||||
}
|
||||
|
||||
const BINARY: Text = "pacman";
|
||||
const SECTION: Text = "arch";
|
||||
|
||||
const SWITCHES_INFO: Switches = &["--query", "--info"];
|
||||
const SWITCHES_INSTALL: Switches = &["--sync"];
|
||||
const SWITCHES_MAKE_DEPENDENCY: Switches = &["--database", "--asdeps"];
|
||||
const SWITCHES_NOCONFIRM: Switches = &["--noconfirm"];
|
||||
const SWITCHES_REMOVE: Switches = &["--remove", "--recursive"];
|
||||
|
||||
const SUPPORTS_AS_DEPENDENCY: bool = true;
|
||||
|
||||
impl Backend for Arch {
|
||||
impl_backend_constants!();
|
||||
|
||||
fn get_binary(&self) -> Text {
|
||||
let r#box = self.binary.clone().into_boxed_str();
|
||||
Box::leak(r#box)
|
||||
}
|
||||
|
||||
fn get_all_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
let alpm_packages = get_all_installed_packages_from_alpm()
|
||||
.context("getting all installed packages from alpm")?;
|
||||
|
||||
let result = convert_to_pacdef_packages(alpm_packages);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn get_explicitly_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
let alpm_packages = get_explicitly_installed_packages_from_alpm()
|
||||
.context("getting all installed packages from alpm")?;
|
||||
let result = convert_to_pacdef_packages(alpm_packages);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Install the specified packages.
|
||||
fn install_packages(&self, packages: &[Package], noconfirm: bool) -> Result<()> {
|
||||
let mut cmd = Command::new(&self.binary);
|
||||
|
||||
cmd.args(self.get_switches_install());
|
||||
|
||||
if noconfirm {
|
||||
cmd.args(self.get_switches_noconfirm());
|
||||
}
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
/// Remove the specified packages.
|
||||
fn remove_packages(&self, packages: &[Package], noconfirm: bool) -> Result<()> {
|
||||
let mut cmd = Command::new(&self.binary);
|
||||
|
||||
cmd.args(self.get_switches_remove());
|
||||
cmd.args(&self.aur_rm_args);
|
||||
|
||||
if noconfirm {
|
||||
cmd.args(self.get_switches_noconfirm());
|
||||
}
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_all_installed_packages_from_alpm() -> Result<HashSet<String>> {
|
||||
let db = get_db_handle().context("getting DB handle")?;
|
||||
let result = db
|
||||
.localdb()
|
||||
.pkgs()
|
||||
.iter()
|
||||
.map(|p| p.name().to_string())
|
||||
.collect();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn get_explicitly_installed_packages_from_alpm() -> Result<HashSet<String>> {
|
||||
let db = get_db_handle().context("getting DB handle")?;
|
||||
let result = db
|
||||
.localdb()
|
||||
.pkgs()
|
||||
.iter()
|
||||
.filter(|p| p.reason() == Explicit)
|
||||
.map(|p| p.name().to_string())
|
||||
.collect();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn convert_to_pacdef_packages(packages: HashSet<String>) -> HashSet<Package> {
|
||||
packages.into_iter().map(Package::from).collect()
|
||||
}
|
||||
|
||||
fn get_db_handle() -> Result<Alpm> {
|
||||
Alpm::new("/", "/var/lib/pacman").context("connecting to DB using expected default values")
|
||||
}
|
||||
|
||||
impl Arch {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
binary: BINARY.to_string(),
|
||||
aur_rm_args: vec![],
|
||||
packages: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::Result;
|
||||
use rust_apt::cache::PackageSort;
|
||||
use rust_apt::new_cache;
|
||||
|
||||
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};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Debian {
|
||||
pub(crate) packages: HashSet<Package>,
|
||||
}
|
||||
|
||||
const BINARY: Text = "apt";
|
||||
const SECTION: Text = "debian";
|
||||
|
||||
const SWITCHES_INFO: Switches = &["show"];
|
||||
const SWITCHES_INSTALL: Switches = &["install"];
|
||||
const SWITCHES_MAKE_DEPENDENCY: Switches = &[]; // not needed
|
||||
const SWITCHES_NOCONFIRM: Switches = &["--yes"];
|
||||
const SWITCHES_REMOVE: Switches = &["remove"];
|
||||
|
||||
const SUPPORTS_AS_DEPENDENCY: bool = true;
|
||||
|
||||
impl Backend for Debian {
|
||||
impl_backend_constants!();
|
||||
|
||||
fn get_all_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
let cache = new_cache!()?;
|
||||
let sort = PackageSort::default().installed();
|
||||
|
||||
let mut result = HashSet::new();
|
||||
for pkg in cache.packages(&sort)? {
|
||||
result.insert(Package::from(pkg.name().to_string()));
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn get_explicitly_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
let cache = new_cache!()?;
|
||||
let sort = PackageSort::default().installed().manually_installed();
|
||||
|
||||
let mut result = HashSet::new();
|
||||
for pkg in cache.packages(&sort)? {
|
||||
result.insert(Package::from(pkg.name().to_string()));
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn make_dependency(&self, packages: &[Package]) -> Result<()> {
|
||||
let mut cmd = build_base_command_with_privileges("apt-mark");
|
||||
cmd.arg("auto");
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
/// Install the specified packages.
|
||||
fn install_packages(&self, packages: &[Package], noconfirm: bool) -> Result<()> {
|
||||
let mut cmd = build_base_command_with_privileges(self.get_binary());
|
||||
|
||||
cmd.args(self.get_switches_install());
|
||||
|
||||
if noconfirm {
|
||||
cmd.args(self.get_switches_noconfirm());
|
||||
}
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
/// Remove the specified packages.
|
||||
fn remove_packages(&self, packages: &[Package], noconfirm: bool) -> Result<()> {
|
||||
let mut cmd = build_base_command_with_privileges(self.get_binary());
|
||||
cmd.args(self.get_switches_remove());
|
||||
|
||||
if noconfirm {
|
||||
cmd.args(self.get_switches_noconfirm());
|
||||
}
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debian {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
packages: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
use std::collections::HashSet;
|
||||
use std::process::Command;
|
||||
|
||||
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};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Fedora {
|
||||
pub(crate) packages: HashSet<Package>,
|
||||
}
|
||||
|
||||
const BINARY: Text = "dnf";
|
||||
const SECTION: Text = "fedora";
|
||||
|
||||
const SWITCHES_INFO: Switches = &["info"];
|
||||
const SWITCHES_INSTALL: Switches = &["install"];
|
||||
const SWITCHES_MAKE_DEPENDENCY: Switches = &[];
|
||||
const SWITCHES_NOCONFIRM: Switches = &["--assumeyes"];
|
||||
const SWITCHES_REMOVE: Switches = &["remove"];
|
||||
|
||||
const SUPPORTS_AS_DEPENDENCY: bool = false;
|
||||
|
||||
/// These switches are responsible for
|
||||
/// getting the packages explicitly installed by the user
|
||||
const SWITCHES_FETCH_USER: Switches = &[
|
||||
"repoquery",
|
||||
"--userinstalled",
|
||||
"--queryformat",
|
||||
"%{from_repo}/%{name}",
|
||||
];
|
||||
|
||||
/// These switches are responsible for
|
||||
/// getting all the packages installed on the system
|
||||
const SWITCHES_FETCH_GLOBAL: Switches = &[
|
||||
"repoquery",
|
||||
"--installed",
|
||||
"--queryformat",
|
||||
"%{from_repo}/%{name}",
|
||||
];
|
||||
|
||||
/// These repositories are ignored when storing the packages
|
||||
/// as these are present by default on any sane fedora system
|
||||
const DEFAULT_REPOS: [&str; 5] = ["koji", "fedora", "updates", "anaconda", "@"];
|
||||
|
||||
impl Backend for Fedora {
|
||||
impl_backend_constants!();
|
||||
|
||||
fn get_all_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(SWITCHES_FETCH_GLOBAL);
|
||||
|
||||
let output = String::from_utf8(cmd.output()?.stdout)?;
|
||||
let packages = output.lines().map(create_package).collect();
|
||||
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn get_explicitly_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(SWITCHES_FETCH_USER);
|
||||
|
||||
let output = String::from_utf8(cmd.output()?.stdout)?;
|
||||
let packages = output.lines().map(create_package).collect();
|
||||
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
/// Install the specified packages.
|
||||
fn install_packages(&self, packages: &[Package], noconfirm: bool) -> Result<()> {
|
||||
let mut cmd = Command::new("sudo");
|
||||
cmd.arg(self.get_binary());
|
||||
cmd.args(self.get_switches_install());
|
||||
|
||||
if noconfirm {
|
||||
cmd.args(self.get_switches_noconfirm());
|
||||
}
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(&p.name);
|
||||
if let Some(repo) = p.repo.as_ref() {
|
||||
cmd.args(["--repo", repo]);
|
||||
}
|
||||
}
|
||||
|
||||
// add these two repositories as these are needed for many dependencies
|
||||
cmd.args(["--repo", "updates"]);
|
||||
cmd.args(["--repo", "fedora"]);
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
/// Show information from package manager for package.
|
||||
fn remove_packages(&self, packages: &[Package], noconfirm: bool) -> Result<()> {
|
||||
let mut cmd = Command::new("sudo");
|
||||
cmd.arg(self.get_binary());
|
||||
cmd.args(self.get_switches_remove());
|
||||
|
||||
if noconfirm {
|
||||
cmd.args(self.get_switches_noconfirm());
|
||||
}
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(&p.name);
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
fn show_package_info(&self, package: &Package) -> Result<()> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(self.get_switches_info());
|
||||
cmd.arg(&package.name);
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
fn make_dependency(&self, _: &[Package]) -> Result<()> {
|
||||
panic!("Not supported by the package manager!")
|
||||
}
|
||||
}
|
||||
|
||||
impl Fedora {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
packages: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_package(package: &str) -> Package {
|
||||
if DEFAULT_REPOS.iter().any(|repo| package.contains(repo)) && !package.contains("copr") {
|
||||
let package = package.split('/').nth(1).expect("Cannot be empty!");
|
||||
package.into()
|
||||
} else {
|
||||
package.into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
use std::collections::HashSet;
|
||||
use std::process::Command;
|
||||
|
||||
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};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Flatpak {
|
||||
pub(crate) packages: HashSet<Package>,
|
||||
pub(crate) systemwide: bool,
|
||||
}
|
||||
|
||||
const BINARY: Text = "flatpak";
|
||||
const SECTION: Text = "flatpak";
|
||||
|
||||
const SWITCHES_INSTALL: Switches = &["install"];
|
||||
const SWITCHES_INFO: Switches = &["info"];
|
||||
const SWITCHES_MAKE_DEPENDENCY: Switches = &[];
|
||||
const SWITCHES_NOCONFIRM: Switches = &["--assumeyes"];
|
||||
const SWITCHES_REMOVE: Switches = &["uninstall"];
|
||||
|
||||
const SUPPORTS_AS_DEPENDENCY: bool = false;
|
||||
|
||||
impl Backend for Flatpak {
|
||||
impl_backend_constants!();
|
||||
|
||||
fn get_all_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
self.get_installed_packages(true)
|
||||
}
|
||||
|
||||
fn get_explicitly_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
self.get_installed_packages(false)
|
||||
}
|
||||
|
||||
/// Install the specified packages.
|
||||
fn install_packages(&self, packages: &[Package], noconfirm: bool) -> Result<()> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(self.get_switches_install());
|
||||
cmd.args(self.get_switches_runtime());
|
||||
|
||||
if noconfirm {
|
||||
cmd.args(self.get_switches_noconfirm());
|
||||
}
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
fn make_dependency(&self, _: &[Package]) -> Result<()> {
|
||||
panic!("not supported by {}", BINARY)
|
||||
}
|
||||
|
||||
/// Remove the specified packages.
|
||||
fn remove_packages(&self, packages: &[Package], noconfirm: bool) -> Result<()> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(self.get_switches_remove());
|
||||
cmd.args(self.get_switches_runtime());
|
||||
|
||||
if noconfirm {
|
||||
cmd.args(self.get_switches_noconfirm());
|
||||
}
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
/// Show information from package manager for package.
|
||||
fn show_package_info(&self, package: &Package) -> Result<()> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(self.get_switches_info());
|
||||
cmd.args(self.get_switches_runtime());
|
||||
cmd.arg(format!("{package}"));
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl Flatpak {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
packages: HashSet::new(),
|
||||
systemwide: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_switches_runtime(&self) -> Switches {
|
||||
if self.systemwide {
|
||||
&[]
|
||||
} else {
|
||||
&["--user"]
|
||||
}
|
||||
}
|
||||
|
||||
fn get_installed_packages(&self, include_implicit: bool) -> Result<HashSet<Package>> {
|
||||
let mut cmd = Command::new(BINARY);
|
||||
cmd.args(["list", "--columns=application"]);
|
||||
if !include_implicit {
|
||||
cmd.arg("--app");
|
||||
}
|
||||
if !self.systemwide {
|
||||
cmd.arg("--user");
|
||||
}
|
||||
|
||||
let output = String::from_utf8(cmd.output()?.stdout)?;
|
||||
Ok(output
|
||||
.lines()
|
||||
.map(Package::from)
|
||||
.collect::<HashSet<Package>>())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#[cfg(feature = "arch")]
|
||||
pub mod arch;
|
||||
#[cfg(feature = "debian")]
|
||||
pub mod debian;
|
||||
pub mod fedora;
|
||||
pub mod flatpak;
|
||||
pub mod python;
|
||||
pub mod rust;
|
||||
pub mod rustup;
|
||||
pub mod void;
|
||||
@@ -0,0 +1,118 @@
|
||||
use std::collections::HashSet;
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::backend::backend_trait::{Backend, Switches, Text};
|
||||
use crate::backend::macros::impl_backend_constants;
|
||||
use crate::{Group, Package};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Python {
|
||||
pub(crate) binary: String,
|
||||
pub(crate) packages: HashSet<Package>,
|
||||
}
|
||||
|
||||
const BINARY: Text = "pip";
|
||||
const SECTION: Text = "python";
|
||||
|
||||
const SWITCHES_INFO: Switches = &["show"];
|
||||
const SWITCHES_INSTALL: Switches = &["install"];
|
||||
const SWITCHES_MAKE_DEPENDENCY: Switches = &[]; // not needed
|
||||
const SWITCHES_NOCONFIRM: Switches = &[]; // not needed
|
||||
const SWITCHES_REMOVE: Switches = &["uninstall"];
|
||||
|
||||
const SUPPORTS_AS_DEPENDENCY: bool = false;
|
||||
|
||||
macro_rules! ERROR{
|
||||
($bin:expr) => {
|
||||
panic!("Cannot use {} for package management in python. Please use a valid package manager like pip or pipx.", $bin)
|
||||
};
|
||||
}
|
||||
|
||||
impl Backend for Python {
|
||||
impl_backend_constants!();
|
||||
|
||||
fn get_binary(&self) -> Text {
|
||||
let r#box = self.binary.clone().into_boxed_str();
|
||||
Box::leak(r#box)
|
||||
}
|
||||
|
||||
fn get_all_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
let output = run_pip_command(&mut cmd, self.get_switches_runtime())?;
|
||||
self.extract_packages(output)
|
||||
}
|
||||
|
||||
fn get_explicitly_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
let output = run_pip_command(&mut cmd, self.get_switches_explicit())?;
|
||||
self.extract_packages(output)
|
||||
}
|
||||
|
||||
fn make_dependency(&self, _packages: &[Package]) -> Result<()> {
|
||||
panic!("not supported by {}", BINARY)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_pip_command(cmd: &mut Command, args: &[&str]) -> Result<Value> {
|
||||
cmd.args(args);
|
||||
let output = String::from_utf8(cmd.output()?.stdout)?;
|
||||
let val: Value = serde_json::from_str(&output)?;
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
impl Python {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
binary: BINARY.to_string(),
|
||||
packages: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_switches_runtime(&self) -> Switches {
|
||||
match self.get_binary() {
|
||||
"pip" => &["list", "--format", "json", "--not-required", "--user"],
|
||||
"pipx" => &["list", "--json"],
|
||||
_ => ERROR!(self.get_binary()),
|
||||
}
|
||||
}
|
||||
fn get_switches_explicit(&self) -> Switches {
|
||||
match self.get_binary() {
|
||||
"pip" => &["list", "--format", "json", "--user"],
|
||||
"pipx" => &["list", "--json"],
|
||||
_ => ERROR!(self.get_binary()),
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_packages(&self, output: Value) -> Result<HashSet<Package>> {
|
||||
match self.get_binary() {
|
||||
"pip" => extract_pacdef_packages(output),
|
||||
"pipx" => extract_pacdef_packages_pipx(output),
|
||||
_ => ERROR!(self.get_binary()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_pacdef_packages(value: Value) -> Result<HashSet<Package>> {
|
||||
let result = value
|
||||
.as_array()
|
||||
.context("getting inner json array")?
|
||||
.iter()
|
||||
.map(|node| node["name"].as_str().expect("should always be a string"))
|
||||
.map(Package::from)
|
||||
.collect();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn extract_pacdef_packages_pipx(value: Value) -> Result<HashSet<Package>> {
|
||||
let result = value["venvs"]
|
||||
.as_object()
|
||||
.context("getting inner json object")?
|
||||
.iter()
|
||||
.map(|(name, _)| Package::from(name.as_str()))
|
||||
.collect();
|
||||
Ok(result)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fs::read_to_string;
|
||||
use std::io::ErrorKind::NotFound;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::backend::backend_trait::{Backend, Switches, Text};
|
||||
use crate::backend::macros::impl_backend_constants;
|
||||
use crate::{Group, Package};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rust {
|
||||
pub(crate) packages: HashSet<Package>,
|
||||
}
|
||||
|
||||
const BINARY: Text = "cargo";
|
||||
const SECTION: Text = "rust";
|
||||
|
||||
const SWITCHES_INSTALL: Switches = &["install"];
|
||||
const SWITCHES_INFO: Switches = &["search", "--limit", "1"];
|
||||
const SWITCHES_MAKE_DEPENDENCY: Switches = &[];
|
||||
const SWITCHES_NOCONFIRM: Switches = &[]; // not needed
|
||||
const SWITCHES_REMOVE: Switches = &["uninstall"];
|
||||
|
||||
const SUPPORTS_AS_DEPENDENCY: bool = false;
|
||||
|
||||
impl Backend for Rust {
|
||||
impl_backend_constants!();
|
||||
|
||||
fn get_all_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
let file = get_crates_file().context("getting path to crates file")?;
|
||||
|
||||
let content = match read_to_string(file) {
|
||||
Ok(string) => string,
|
||||
Err(err) if err.kind() == NotFound => {
|
||||
eprintln!(
|
||||
"WARNING: no crates file found for cargo. Assuming no crates installed yet."
|
||||
);
|
||||
return Ok(HashSet::new());
|
||||
}
|
||||
Err(err) => bail!(err),
|
||||
};
|
||||
|
||||
let json: Value =
|
||||
serde_json::from_str(&content).context("parsing JSON from crates file")?;
|
||||
extract_packages(&json).context("extracing packages from crates file")
|
||||
}
|
||||
|
||||
fn get_explicitly_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
self.get_all_installed_packages()
|
||||
.context("getting all installed packages")
|
||||
}
|
||||
|
||||
fn make_dependency(&self, _: &[Package]) -> Result<()> {
|
||||
panic!("not supported by {}", BINARY)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_packages(json: &Value) -> Result<HashSet<Package>> {
|
||||
let result: HashSet<_> = json
|
||||
.get("installs")
|
||||
.context("get 'installs' field from json")?
|
||||
.as_object()
|
||||
.context("getting object")?
|
||||
.into_iter()
|
||||
.map(|(name, _)| name)
|
||||
.map(|name| {
|
||||
name.split_whitespace()
|
||||
.next()
|
||||
.expect("identifier is whitespace-delimited")
|
||||
})
|
||||
.map(|name| Package::try_from(name).expect("name is valid"))
|
||||
.collect();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
impl Rust {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
packages: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_crates_file() -> Result<PathBuf> {
|
||||
let mut result = crate::path::get_cargo_home().context("getting cargo home dir")?;
|
||||
result.push(".crates2.json");
|
||||
Ok(result)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use super::types::RustupPackage;
|
||||
|
||||
pub fn toolchain_of_component_was_already_removed(
|
||||
removed_toolchains: &[String],
|
||||
component: &RustupPackage,
|
||||
) -> bool {
|
||||
removed_toolchains.contains(&component.toolchain)
|
||||
}
|
||||
|
||||
pub fn install_components(line: &str, toolchain: &str, val: &mut Vec<String>) {
|
||||
let mut chunks = line.splitn(3, '-');
|
||||
let component = chunks.next().expect("Component name is empty!");
|
||||
match component {
|
||||
// these are the only components that have a single word name
|
||||
"cargo" | "rustfmt" | "clippy" | "miri" | "rls" | "rustc" => {
|
||||
val.push([toolchain, component].join("/"));
|
||||
}
|
||||
// all the others have two words hyphenated as component names
|
||||
_ => {
|
||||
let component = [
|
||||
component,
|
||||
chunks
|
||||
.next()
|
||||
.expect("No such component is managed by rustup"),
|
||||
]
|
||||
.join("-");
|
||||
val.push([toolchain, component.as_str()].join("/"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn group_components_by_toolchains(components: Vec<RustupPackage>) -> Vec<Vec<RustupPackage>> {
|
||||
let mut result = vec![];
|
||||
|
||||
let mut toolchains: Vec<String> = vec![];
|
||||
|
||||
for component in components {
|
||||
let index = toolchains
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, toolchain)| toolchain == &&component.toolchain)
|
||||
.map(|(idx, _)| idx)
|
||||
.unwrap_or_else(|| {
|
||||
toolchains.push(component.toolchain.clone());
|
||||
result.push(vec![]);
|
||||
toolchains.len() - 1
|
||||
});
|
||||
|
||||
result
|
||||
.get_mut(index)
|
||||
.expect(
|
||||
"either the index already existed or we just pushed the element with that index",
|
||||
)
|
||||
.push(component);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
mod helpers;
|
||||
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 anyhow::{bail, Context, Result};
|
||||
use std::collections::HashSet;
|
||||
use std::process::Command;
|
||||
|
||||
use self::helpers::{
|
||||
group_components_by_toolchains, install_components, toolchain_of_component_was_already_removed,
|
||||
};
|
||||
pub use self::types::Rustup;
|
||||
use self::types::{Repotype, RustupPackage};
|
||||
|
||||
const BINARY: Text = "rustup";
|
||||
const SECTION: Text = "rustup";
|
||||
|
||||
const SWITCHES_INSTALL: Switches = &["component", "add"];
|
||||
const SWITCHES_INFO: Switches = &["component", "list", "--installed"];
|
||||
const SWITCHES_MAKE_DEPENDENCY: Switches = &[];
|
||||
const SWITCHES_NOCONFIRM: Switches = &[];
|
||||
const SWITCHES_REMOVE: Switches = &["component", "remove"];
|
||||
|
||||
const SUPPORTS_AS_DEPENDENCY: bool = false;
|
||||
|
||||
impl Backend for Rustup {
|
||||
impl_backend_constants!();
|
||||
|
||||
fn get_all_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
let toolchains_vec = self
|
||||
.run_toolchain_command(Repotype::Toolchain.get_info_switches())
|
||||
.context("Getting installed toolchains")?;
|
||||
|
||||
let toolchains: HashSet<Package> = toolchains_vec
|
||||
.iter()
|
||||
.map(|name| ["toolchain", name].join("/").into())
|
||||
.collect();
|
||||
|
||||
let components: HashSet<Package> = self
|
||||
.run_component_command(Repotype::Component.get_info_switches(), &toolchains_vec)
|
||||
.context("Getting installed components")?
|
||||
.iter()
|
||||
.map(|name| ["component", name].join("/").into())
|
||||
.collect();
|
||||
|
||||
let mut packages = HashSet::new();
|
||||
|
||||
packages.extend(toolchains);
|
||||
packages.extend(components);
|
||||
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn get_explicitly_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
self.get_all_installed_packages()
|
||||
.context("Getting all installed packages")
|
||||
}
|
||||
|
||||
fn make_dependency(&self, _: &[Package]) -> Result<()> {
|
||||
panic!("Not supported by {}", self.get_binary())
|
||||
}
|
||||
|
||||
fn install_packages(&self, packages: &[Package], _: bool) -> Result<()> {
|
||||
let packages = RustupPackage::from_pacdef_packages(packages)?;
|
||||
|
||||
let (toolchains, components) =
|
||||
RustupPackage::sort_packages_into_toolchains_and_components(packages);
|
||||
|
||||
self.install_toolchains(toolchains)?;
|
||||
self.install_components(components)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_packages(&self, packages: &[Package], _: bool) -> Result<()> {
|
||||
let rustup_packages = RustupPackage::from_pacdef_packages(packages)?;
|
||||
|
||||
let (toolchains, components) =
|
||||
RustupPackage::sort_packages_into_toolchains_and_components(rustup_packages);
|
||||
|
||||
let removed_toolchains = self.remove_toolchains(toolchains)?;
|
||||
|
||||
self.remove_components(components, removed_toolchains)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Rustup {
|
||||
fn run_component_command(&self, args: &[&str], toolchains: &[String]) -> Result<Vec<String>> {
|
||||
let mut val = Vec::new();
|
||||
|
||||
for toolchain in toolchains {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(args).arg(toolchain);
|
||||
|
||||
let output = String::from_utf8(cmd.output()?.stdout)?;
|
||||
|
||||
for component in output.lines() {
|
||||
install_components(component, toolchain, &mut val);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
fn run_toolchain_command(&self, args: &[&str]) -> Result<Vec<String>> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(args);
|
||||
|
||||
let output = String::from_utf8(cmd.output()?.stdout)?;
|
||||
|
||||
let mut val = Vec::new();
|
||||
|
||||
for line in output.lines() {
|
||||
let toolchain = line.split('-').next();
|
||||
match toolchain {
|
||||
Some(name) => val.push(name.to_string()),
|
||||
None => bail!("Toolchain name not provided!"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
fn install_toolchains(&self, toolchains: Vec<RustupPackage>) -> Result<()> {
|
||||
if toolchains.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(Repotype::Toolchain.get_install_switches());
|
||||
|
||||
for toolchain in toolchains {
|
||||
cmd.arg(&toolchain.toolchain);
|
||||
}
|
||||
|
||||
run_external_command(cmd).context("installing toolchains")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_components(&self, components: Vec<RustupPackage>) -> Result<()> {
|
||||
if components.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let components_by_toolchain = group_components_by_toolchains(components);
|
||||
|
||||
for components_for_one_toolchain in components_by_toolchain {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(Repotype::Component.get_install_switches());
|
||||
|
||||
let the_toolchain = &components_for_one_toolchain
|
||||
.first()
|
||||
.expect("will have at least one element")
|
||||
.toolchain;
|
||||
|
||||
cmd.arg(the_toolchain);
|
||||
|
||||
for component_package in &components_for_one_toolchain {
|
||||
let actual_component = component_package
|
||||
.component
|
||||
.as_ref()
|
||||
.expect("constructor makes sure this is Some");
|
||||
|
||||
cmd.arg(actual_component);
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
.with_context(|| format!("installing [{components_for_one_toolchain:?}]"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_toolchains(&self, toolchains: Vec<RustupPackage>) -> Result<Vec<String>> {
|
||||
let mut removed_toolchains = vec![];
|
||||
if !toolchains.is_empty() {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(Repotype::Toolchain.get_remove_switches());
|
||||
|
||||
for toolchain_package in &toolchains {
|
||||
let name = toolchain_package.toolchain.as_str();
|
||||
cmd.arg(name);
|
||||
removed_toolchains.push(name.to_string());
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
.with_context(|| format!("removing toolchains [{toolchains:?}]"))?;
|
||||
}
|
||||
Ok(removed_toolchains)
|
||||
}
|
||||
|
||||
fn remove_components(
|
||||
&self,
|
||||
components: Vec<RustupPackage>,
|
||||
removed_toolchains: Vec<String>,
|
||||
) -> Result<()> {
|
||||
for component_package in components {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(Repotype::Component.get_remove_switches());
|
||||
|
||||
if toolchain_of_component_was_already_removed(&removed_toolchains, &component_package) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cmd.arg(&component_package.toolchain);
|
||||
cmd.arg(
|
||||
component_package
|
||||
.component
|
||||
.as_ref()
|
||||
.expect("the constructor ensures this cannot be None"),
|
||||
);
|
||||
|
||||
run_external_command(cmd)
|
||||
.with_context(|| format!("removing component {component_package:?}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{backend::backend_trait::Switches, Package};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rustup {
|
||||
pub(crate) packages: HashSet<Package>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Repotype {
|
||||
Toolchain,
|
||||
Component,
|
||||
}
|
||||
|
||||
/// A package as used exclusively in the rustup backend. Contrary to other packages, this does not
|
||||
/// have an (optional) repository and a name, but is either a component or a toolchain, has a
|
||||
/// toolchain version, and if it is a toolchain also a name.
|
||||
#[derive(Debug)]
|
||||
pub struct RustupPackage {
|
||||
/// Whether it is a toolchain or a component.
|
||||
pub repotype: Repotype,
|
||||
/// The name of the toolchain this belongs to (stable, nightly, a pinned version)
|
||||
pub toolchain: String,
|
||||
/// If it is a toolchain, it will not have a component name.
|
||||
/// If it is a component, this will be its name.
|
||||
pub component: Option<String>,
|
||||
}
|
||||
|
||||
impl Rustup {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
packages: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Repotype {
|
||||
fn try_from<T>(value: T) -> Result<Self>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
let value = value.as_ref();
|
||||
let result = match value {
|
||||
"toolchain" => Self::Toolchain,
|
||||
"component" => Self::Component,
|
||||
_ => bail!("{} is neither toolchain nor component", value),
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn get_install_switches(self) -> Switches {
|
||||
match self {
|
||||
Self::Toolchain => &["toolchain", "install"],
|
||||
Self::Component => &["component", "add", "--toolchain"],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_remove_switches(self) -> Switches {
|
||||
match self {
|
||||
Self::Toolchain => &["toolchain", "uninstall"],
|
||||
Self::Component => &["component", "remove", "--toolchain"],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_info_switches(self) -> Switches {
|
||||
match self {
|
||||
Self::Toolchain => &["toolchain", "list"],
|
||||
Self::Component => &["component", "list", "--installed", "--toolchain"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RustupPackage {
|
||||
/// Creates a new [`RustupPackage`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if
|
||||
/// - repotype is Toolchain and component is Some, or
|
||||
/// - repotype is Component and component is None.
|
||||
fn new(repotype: Repotype, toolchain: String, component: Option<String>) -> Self {
|
||||
match repotype {
|
||||
Repotype::Toolchain => assert!(component.is_none()),
|
||||
Repotype::Component => assert!(component.is_some()),
|
||||
};
|
||||
|
||||
Self {
|
||||
repotype,
|
||||
toolchain,
|
||||
component,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sort_packages_into_toolchains_and_components(
|
||||
packages: Vec<Self>,
|
||||
) -> (Vec<Self>, Vec<Self>) {
|
||||
let mut toolchains = vec![];
|
||||
let mut components = vec![];
|
||||
|
||||
for package in packages {
|
||||
match package.repotype {
|
||||
Repotype::Toolchain => toolchains.push(package),
|
||||
Repotype::Component => components.push(package),
|
||||
}
|
||||
}
|
||||
|
||||
(toolchains, components)
|
||||
}
|
||||
|
||||
pub fn from_pacdef_packages(packages: &[Package]) -> Result<Vec<Self>> {
|
||||
let mut result = vec![];
|
||||
|
||||
for package in packages {
|
||||
let rustup_package = Self::try_from(package).with_context(|| {
|
||||
format!(
|
||||
"converting pacdef package {} to rustup package",
|
||||
package.name
|
||||
)
|
||||
})?;
|
||||
result.push(rustup_package);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Package> for RustupPackage {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(package: &Package) -> Result<Self> {
|
||||
let repo = package.repo.as_ref().context("getting repo from package")?;
|
||||
let repotype = Repotype::try_from(repo).context("getting repotype")?;
|
||||
|
||||
let (toolchain, component) = match repotype {
|
||||
Repotype::Toolchain => (package.name.to_string(), None),
|
||||
Repotype::Component => {
|
||||
let (toolchain, component) = package
|
||||
.name
|
||||
.split_once('/')
|
||||
.context("splitting package into toolchain and component")?;
|
||||
(toolchain.to_string(), Some(component.into()))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self::new(repotype, toolchain, component))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
use std::collections::HashSet;
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Result;
|
||||
use regex::Regex;
|
||||
|
||||
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};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Void {
|
||||
pub(crate) packages: HashSet<Package>,
|
||||
}
|
||||
|
||||
const BINARY: Text = "xbps-install";
|
||||
const INSTALL_BINARY: Text = "xbps-install";
|
||||
const REMOVE_BINARY: Text = "xbps-remove";
|
||||
const QUERY_BINARY: Text = "xbps-query";
|
||||
const PKGDB_BINARY: Text = "xbps-pkgdb";
|
||||
const SECTION: Text = "void";
|
||||
|
||||
const SWITCHES_INFO: Switches = &[];
|
||||
const SWITCHES_INSTALL: Switches = &["-S"];
|
||||
const SWITCHES_MAKE_DEPENDENCY: Switches = &["-m", "auto"];
|
||||
const SWITCHES_NOCONFIRM: Switches = &["-y"];
|
||||
const SWITCHES_REMOVE: Switches = &["-R"];
|
||||
|
||||
const SUPPORTS_AS_DEPENDENCY: bool = true;
|
||||
|
||||
impl Backend for Void {
|
||||
impl_backend_constants!();
|
||||
|
||||
fn get_all_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
// Removes the package status and description from output
|
||||
let re_str_1 = r"^ii |^uu |^hr |^\?\? | .*";
|
||||
// Removes the package version from output
|
||||
let re_str_2 = r"-[^-]*$";
|
||||
let re1 = Regex::new(re_str_1)?;
|
||||
let re2 = Regex::new(re_str_2)?;
|
||||
let mut cmd = Command::new(QUERY_BINARY);
|
||||
cmd.args(["-l"]);
|
||||
let output = String::from_utf8(cmd.output()?.stdout)?;
|
||||
|
||||
let packages = output
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let result = re1.replace_all(line, "");
|
||||
let result = re2.replace_all(&result, "");
|
||||
result.to_string().into()
|
||||
})
|
||||
.collect();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn get_explicitly_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
// Removes the package version from output
|
||||
let re_str = r"-[^-]*$";
|
||||
let re = Regex::new(re_str)?;
|
||||
let mut cmd = Command::new(QUERY_BINARY);
|
||||
cmd.args(["-m"]);
|
||||
let output = String::from_utf8(cmd.output()?.stdout)?;
|
||||
|
||||
let packages = output
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let result = re.replace_all(line, "").to_string();
|
||||
result.into()
|
||||
})
|
||||
.collect();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
/// Install the specified packages.
|
||||
fn install_packages(&self, packages: &[Package], noconfirm: bool) -> Result<()> {
|
||||
let mut cmd = build_base_command_with_privileges(INSTALL_BINARY);
|
||||
cmd.args(self.get_switches_install());
|
||||
|
||||
if noconfirm {
|
||||
cmd.args(self.get_switches_noconfirm());
|
||||
}
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
fn remove_packages(&self, packages: &[Package], noconfirm: bool) -> Result<()> {
|
||||
let mut cmd = build_base_command_with_privileges(REMOVE_BINARY);
|
||||
cmd.args(self.get_switches_remove());
|
||||
|
||||
if noconfirm {
|
||||
cmd.args(self.get_switches_noconfirm());
|
||||
}
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
fn make_dependency(&self, packages: &[Package]) -> Result<()> {
|
||||
let mut cmd = build_base_command_with_privileges(PKGDB_BINARY);
|
||||
cmd.args(self.get_switches_make_dependency());
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
/// Show information from package manager for package.
|
||||
fn show_package_info(&self, package: &Package) -> Result<()> {
|
||||
let mut cmd = Command::new(QUERY_BINARY);
|
||||
cmd.args(self.get_switches_info());
|
||||
cmd.arg(format!("{package}"));
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl Void {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
packages: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
use std::any::Any;
|
||||
use std::cmp::{Eq, Ord};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Debug;
|
||||
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};
|
||||
|
||||
pub(in crate::backend) type Switches = &'static [&'static str];
|
||||
pub(in crate::backend) type Text = &'static str;
|
||||
|
||||
/// The trait of a struct that is used as a backend.
|
||||
pub trait Backend: Debug {
|
||||
/// Return the actual binary. Iff the backend supports different
|
||||
/// binaries, you will need to overwrite this implementation to return
|
||||
/// the binary that was loaded at runtime. See
|
||||
/// [`Backend::get_binary_default()`].
|
||||
fn get_binary(&self) -> Text {
|
||||
self.get_binary_default()
|
||||
}
|
||||
|
||||
/// Return the default binary as defined in the constant of the module.
|
||||
/// See [`Backend::get_binary()`].
|
||||
fn get_binary_default(&self) -> Text;
|
||||
|
||||
/// Get the name of the section in the group files.
|
||||
fn get_section(&self) -> Text;
|
||||
|
||||
/// Get CLI switches for the package manager to show information for
|
||||
/// packages.
|
||||
fn get_switches_info(&self) -> Switches;
|
||||
|
||||
/// Get CLI switches for the package manager to install packages.
|
||||
fn get_switches_install(&self) -> Switches;
|
||||
|
||||
/// Get CLI switches for the package manager to perform `sync` and `clean` without
|
||||
/// confirmation.
|
||||
fn get_switches_noconfirm(&self) -> Switches;
|
||||
|
||||
/// Get CLI switches for the package manager to remove packages.
|
||||
fn get_switches_remove(&self) -> Switches;
|
||||
|
||||
/// Get CLI switches for the package manager to mark packages as
|
||||
/// dependency. This is not supported by all package managers. See
|
||||
/// [`Backend::supports_as_dependency`].
|
||||
fn get_switches_make_dependency(&self) -> Switches;
|
||||
|
||||
/// 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>);
|
||||
|
||||
/// Get all managed packages for this backend, i.e. all packages
|
||||
/// under the corresponding section in all group files.
|
||||
fn get_managed_packages(&self) -> &HashSet<Package>;
|
||||
|
||||
/// Get all packages that are installed in the system.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function shall return an error if the installed packages cannot be
|
||||
/// determined.
|
||||
fn get_all_installed_packages(&self) -> Result<HashSet<Package>>;
|
||||
|
||||
/// Get all packages that were installed in the system explicitly.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function shall return an error if the explicitly installed packages
|
||||
/// cannot be determined.
|
||||
fn get_explicitly_installed_packages(&self) -> Result<HashSet<Package>>;
|
||||
|
||||
/// 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<()> {
|
||||
let group_package_map = to_hashmap(to_assign);
|
||||
let section_header = format!("[{}]", self.get_section());
|
||||
|
||||
for (group, packages) in group_package_map {
|
||||
group.save_packages(§ion_header, &packages)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install the specified packages. If `noconfirm` is `true`, pass the corresponding
|
||||
/// switch to the package manager. Return the [`ExitStatus`] from the package manager.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the package manager cannot be run or it
|
||||
/// returns an error.
|
||||
fn install_packages(&self, packages: &[Package], noconfirm: bool) -> Result<()> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(self.get_switches_install());
|
||||
|
||||
if noconfirm {
|
||||
cmd.args(self.get_switches_noconfirm());
|
||||
}
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
/// Mark the packages as non-explicit / dependency using the underlying
|
||||
/// package manager.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method shall panic when the backend does not support depedent packages.
|
||||
fn make_dependency(&self, packages: &[Package]) -> Result<()> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(self.get_switches_make_dependency());
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
/// Remove the specified packages.
|
||||
///
|
||||
fn remove_packages(&self, packages: &[Package], noconfirm: bool) -> Result<()> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(self.get_switches_remove());
|
||||
|
||||
if noconfirm {
|
||||
cmd.args(self.get_switches_noconfirm());
|
||||
}
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
/// Get missing packages, sorted alphabetically.
|
||||
fn get_missing_packages_sorted(&self) -> Result<Vec<Package>> {
|
||||
let installed = self
|
||||
.get_all_installed_packages()
|
||||
.context("could not get installed packages")?;
|
||||
let managed = self.get_managed_packages();
|
||||
let mut diff: Vec<_> = managed.difference(&installed).cloned().collect();
|
||||
diff.sort_unstable();
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
/// Show information from package manager for package.
|
||||
fn show_package_info(&self, package: &Package) -> Result<()> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(self.get_switches_info());
|
||||
cmd.arg(format!("{package}"));
|
||||
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
/// Get unmanaged packages, sorted alphabetically.
|
||||
fn get_unmanaged_packages_sorted(&self) -> Result<Vec<Package>> {
|
||||
let installed = self
|
||||
.get_explicitly_installed_packages()
|
||||
.context("could not get explicitly installed packages")?;
|
||||
let required = self.get_managed_packages();
|
||||
let mut diff: Vec<_> = installed.difference(required).cloned().collect();
|
||||
diff.sort_unstable();
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
/// Return a mutable reference to self as `Any`. Required for downcasting.
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
|
||||
/// Whether the underlying package manager supports dependency packages.
|
||||
fn supports_as_dependency(&self) -> bool;
|
||||
}
|
||||
|
||||
/// For a vector of tuples containing a `V` and `K`, where a `K` may occur more than
|
||||
/// once and each `V` exactly once, create a `HashMap` that associates each `K` with
|
||||
/// a `Vec<V>`.
|
||||
fn to_hashmap<K, V>(to_assign: Vec<(V, K)>) -> HashMap<K, Vec<V>>
|
||||
where
|
||||
K: Hash + Eq,
|
||||
V: Ord,
|
||||
{
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for (value, key) in to_assign {
|
||||
let inner: &mut Vec<V> = map.entry(key).or_default();
|
||||
inner.push(value);
|
||||
}
|
||||
|
||||
for vecs in map.values_mut() {
|
||||
vecs.sort_unstable();
|
||||
}
|
||||
map
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use super::{Backend, Backends};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BackendIter {
|
||||
pub(crate) next: Option<Backends>,
|
||||
}
|
||||
|
||||
impl Iterator for BackendIter {
|
||||
type Item = Box<dyn Backend>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match &self.next {
|
||||
None => None,
|
||||
Some(b) => {
|
||||
let result = b.get_backend();
|
||||
self.next = b.next();
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/// Used to implement parts of the `Backend` trait that should not change between the actual
|
||||
/// backends (boilerplate).
|
||||
macro_rules! impl_backend_constants {
|
||||
() => {
|
||||
fn get_binary_default(&self) -> Text {
|
||||
BINARY
|
||||
}
|
||||
|
||||
fn get_section(&self) -> Text {
|
||||
SECTION
|
||||
}
|
||||
|
||||
fn get_switches_info(&self) -> Switches {
|
||||
SWITCHES_INFO
|
||||
}
|
||||
|
||||
fn get_switches_install(&self) -> Switches {
|
||||
SWITCHES_INSTALL
|
||||
}
|
||||
|
||||
fn get_switches_noconfirm(&self) -> Switches {
|
||||
SWITCHES_NOCONFIRM
|
||||
}
|
||||
|
||||
fn get_switches_remove(&self) -> Switches {
|
||||
SWITCHES_REMOVE
|
||||
}
|
||||
|
||||
fn get_switches_make_dependency(&self) -> Switches {
|
||||
SWITCHES_MAKE_DEPENDENCY
|
||||
}
|
||||
|
||||
fn get_managed_packages(&self) -> &HashSet<Package> {
|
||||
&self.packages
|
||||
}
|
||||
|
||||
fn load(&mut self, groups: &HashSet<Group>) {
|
||||
let own_section_name = self.get_section();
|
||||
|
||||
groups
|
||||
.iter()
|
||||
.flat_map(|g| &g.sections)
|
||||
.filter(|section| section.name == own_section_name)
|
||||
.flat_map(|section| §ion.packages)
|
||||
.for_each(|package| {
|
||||
self.packages.insert(package.clone());
|
||||
})
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn supports_as_dependency(&self) -> bool {
|
||||
SUPPORTS_AS_DEPENDENCY
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use impl_backend_constants;
|
||||
@@ -0,0 +1,26 @@
|
||||
mod actual;
|
||||
mod backend_trait;
|
||||
mod iter;
|
||||
mod macros;
|
||||
mod root;
|
||||
mod todo_per_backend;
|
||||
|
||||
pub use backend_trait::Backend;
|
||||
pub use iter::BackendIter;
|
||||
pub use todo_per_backend::ToDoPerBackend;
|
||||
|
||||
use pacdef_macros::Register;
|
||||
|
||||
#[derive(Debug, Register)]
|
||||
pub enum Backends {
|
||||
#[cfg(feature = "arch")]
|
||||
Arch,
|
||||
#[cfg(feature = "debian")]
|
||||
Debian,
|
||||
Flatpak,
|
||||
Fedora,
|
||||
Python,
|
||||
Rust,
|
||||
Rustup,
|
||||
Void,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
use std::process::Command;
|
||||
|
||||
pub fn we_are_root() -> bool {
|
||||
let uid = unsafe { libc::geteuid() };
|
||||
uid == 0
|
||||
}
|
||||
|
||||
pub fn build_base_command_with_privileges(binary: &str) -> Command {
|
||||
let cmd = if we_are_root() {
|
||||
Command::new(binary)
|
||||
} else {
|
||||
let mut cmd = Command::new("sudo");
|
||||
cmd.arg(binary);
|
||||
cmd
|
||||
};
|
||||
cmd
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use super::Backend;
|
||||
use crate::Package;
|
||||
|
||||
/// A vector of tuples containing a `dyn Backend` and a vector of unmanaged packages
|
||||
/// for that backend.
|
||||
///
|
||||
/// This struct is used to store a list of unmanaged packages or missing packages
|
||||
/// for all backends.
|
||||
#[derive(Debug)]
|
||||
pub struct ToDoPerBackend(Vec<(Box<dyn Backend>, Vec<Package>)>);
|
||||
|
||||
impl ToDoPerBackend {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
|
||||
pub(crate) fn push(&mut self, item: (Box<dyn Backend>, Vec<Package>)) {
|
||||
self.0.push(item);
|
||||
}
|
||||
|
||||
pub(crate) fn into_iter(self) -> impl Iterator<Item = (Box<dyn Backend>, Vec<Package>)> {
|
||||
self.0.into_iter()
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = &(Box<dyn Backend>, Vec<Package>)> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn nothing_to_do_for_all_backends(&self) -> bool {
|
||||
self.0.iter().all(|(_, diff)| diff.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn install_missing_packages(&self, noconfirm: bool) -> Result<()> {
|
||||
for (backend, packages) in &self.0 {
|
||||
if packages.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
Backend::install_packages(&**backend, packages, noconfirm)
|
||||
.with_context(|| format!("installing packages for {}", backend.get_section()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn remove_unmanaged_packages(&self, noconfirm: bool) -> Result<()> {
|
||||
for (backend, packages) in &self.0 {
|
||||
if packages.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
Backend::remove_packages(&**backend, packages, noconfirm)
|
||||
.with_context(|| format!("removing packages for {}", backend.get_section()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn show(&self) -> Result<()> {
|
||||
let mut parts = vec![];
|
||||
|
||||
for (backend, packages) in self.iter() {
|
||||
if packages.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut segment = String::new();
|
||||
|
||||
segment.write_str(&format!("[{}]", backend.get_section()))?;
|
||||
for package in packages {
|
||||
segment.write_str(&format!("\n{package}"))?;
|
||||
}
|
||||
|
||||
parts.push(segment);
|
||||
}
|
||||
|
||||
let mut output = String::new();
|
||||
let mut iter = parts.iter().peekable();
|
||||
|
||||
while let Some(part) = iter.next() {
|
||||
output.write_str(part)?;
|
||||
if iter.peek().is_some() {
|
||||
output.write_str("\n\n")?;
|
||||
}
|
||||
}
|
||||
|
||||
println!("{output}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{ensure, Context, Result};
|
||||
|
||||
use crate::env::{get_editor, should_print_debug_info};
|
||||
|
||||
/// Run the editor and pass the provided files as arguments. The workdir is set
|
||||
/// to the parent of the first file.
|
||||
pub fn run_edit_command<P>(files: &[P]) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
fn inner(files: &[&Path]) -> Result<()> {
|
||||
let mut cmd = Command::new(get_editor().context("getting suitable editor")?);
|
||||
cmd.current_dir(
|
||||
files[0]
|
||||
.parent()
|
||||
.context("getting parent dir of first file argument")?,
|
||||
);
|
||||
for f in files {
|
||||
cmd.arg(f.to_string_lossy().to_string());
|
||||
}
|
||||
run_external_command(cmd)
|
||||
}
|
||||
|
||||
let files: Vec<_> = files.iter().map(|p| p.as_ref()).collect();
|
||||
inner(&files)
|
||||
}
|
||||
|
||||
/// Run an external command. Use the anyhow framework to bubble up errors if they occur. Will print
|
||||
/// the full command to be executed when pacdef is in debug mode.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the command cannot be run or if it returns a non-zero
|
||||
/// exit status. In case of an error the full command will be part of the error message.
|
||||
pub fn run_external_command(mut cmd: Command) -> Result<()> {
|
||||
if should_print_debug_info() {
|
||||
println!("will run the following command");
|
||||
dbg!(&cmd);
|
||||
}
|
||||
|
||||
let exit_status = cmd
|
||||
.status()
|
||||
.with_context(|| format!("running command [{cmd:?}]"))?;
|
||||
|
||||
let success = exit_status.success();
|
||||
ensure!(
|
||||
success,
|
||||
"command [{cmd:?}] returned non-zero exit status {success}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
use std::fs::{create_dir_all, read_to_string, File};
|
||||
use std::io::{ErrorKind, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
// Update the master README if fields change.
|
||||
/// Config for the program, as listed in `$XDG_CONFIG_HOME/pacdef/pacdef.yaml`.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// The AUR helper to use for Arch Linux.
|
||||
#[serde(default = "aur_helper")]
|
||||
pub aur_helper: String,
|
||||
/// Additional arguments to pass to `aur_helper` when removing a package.
|
||||
#[serde(default)]
|
||||
pub aur_rm_args: Vec<String>,
|
||||
/// Install Flatpak packages system-wide
|
||||
#[serde(default = "yes")]
|
||||
pub flatpak_systemwide: bool,
|
||||
/// Warn the user when a group is not a symlink.
|
||||
#[serde(default = "yes")]
|
||||
pub warn_not_symlinks: bool,
|
||||
/// Backends the user does not want to use even though the binary exists.
|
||||
#[serde(default)]
|
||||
pub disabled_backends: Vec<String>,
|
||||
/// Choose whether to use pipx instead of pip for python package management
|
||||
#[serde(default = "pip")]
|
||||
pub pip_binary: String,
|
||||
}
|
||||
|
||||
fn yes() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn aur_helper() -> String {
|
||||
"paru".into()
|
||||
}
|
||||
|
||||
fn pip() -> String {
|
||||
"pip".into()
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load the config from the associated file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the config file exists but cannot be
|
||||
/// read, its contents are not UTF-8, or the file is malformed.
|
||||
pub fn load(config_file: &Path) -> Result<Self> {
|
||||
let from_file = read_to_string(config_file);
|
||||
|
||||
let content = match from_file {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
if e.kind() == ErrorKind::NotFound {
|
||||
bail!(crate::Error::ConfigFileNotFound)
|
||||
}
|
||||
bail!("unexpected error occured: {e:?}");
|
||||
}
|
||||
};
|
||||
|
||||
serde_yaml::from_str(&content).context("parsing yaml config")
|
||||
}
|
||||
|
||||
/// Save the instance of [`Config`] to disk.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the config file cannot be saved to disk.
|
||||
pub fn save(&self, file: &Path) -> Result<()> {
|
||||
let content = serde_yaml::to_string(&self).context("converting Config to yaml")?;
|
||||
|
||||
let parent = file.parent().context("getting parent of config dir")?;
|
||||
if !parent.is_dir() {
|
||||
create_dir_all(parent)
|
||||
.with_context(|| format!("creating dir {}", parent.to_string_lossy()))?;
|
||||
}
|
||||
|
||||
let mut output = File::create(file).context("creating default config file")?;
|
||||
write!(output, "{content}").context("writing default config")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
aur_helper: "paru".into(),
|
||||
aur_rm_args: vec![],
|
||||
flatpak_systemwide: true,
|
||||
warn_not_symlinks: true,
|
||||
disabled_backends: vec![],
|
||||
pip_binary: "pip".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
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 anyhow::{bail, ensure, Context, Result};
|
||||
use const_format::formatcp;
|
||||
|
||||
use crate::args::{self, PackageAction};
|
||||
use crate::backend::{Backend, Backends, ToDoPerBackend};
|
||||
use crate::cmd::run_edit_command;
|
||||
use crate::env::should_print_debug_info;
|
||||
use crate::path::{binary_in_path, get_absolutized_file_paths, get_group_dir};
|
||||
use crate::search;
|
||||
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<args::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: args::Arguments, config: Config, groups: HashSet<Group>) -> Self {
|
||||
Self {
|
||||
args: Some(args),
|
||||
config,
|
||||
groups,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the action that was provided by the user as first argument.
|
||||
///
|
||||
/// For convenience sake, all called functions take a `&self` argument, even if
|
||||
/// these are not strictly required.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function propagates errors from the underlying functions.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics if the `args` field is `None`.
|
||||
#[allow(clippy::unit_arg)]
|
||||
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")
|
||||
{
|
||||
args::Arguments::Group(group_args) => self.run_group_subcommand(&group_args),
|
||||
args::Arguments::Package(package_args) => self.run_package_subcommand(&package_args),
|
||||
args::Arguments::Version => Ok(self.show_version()),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_group_subcommand(self, args: &args::GroupAction) -> Result<()> {
|
||||
use args::GroupAction::*;
|
||||
|
||||
match args {
|
||||
Edit(args::Groups(groups)) => self.edit_groups(groups),
|
||||
Export(args::Groups(groups), args::OutputDir(dir), args::Force(force)) => {
|
||||
self.export_groups(groups, dir.as_ref(), *force)
|
||||
}
|
||||
Import(args::Groups(groups)) => self.import_groups(groups),
|
||||
List => self.show_groups(),
|
||||
New(args::Groups(groups), args::Edit(edit)) => self.new_groups(groups, *edit),
|
||||
Remove(args::Groups(groups)) => self.remove_groups(groups),
|
||||
Show(args::Groups(groups)) => self.show_group_content(groups),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_package_subcommand(mut self, args: &PackageAction) -> Result<()> {
|
||||
use args::PackageAction::*;
|
||||
|
||||
match args {
|
||||
Clean(args::Noconfirm(noconfirm)) => self.clean_packages(*noconfirm),
|
||||
Review => review::review(self.get_unmanaged_packages()?, self.groups),
|
||||
Search(args::Regex(regex)) => {
|
||||
self.warn_about_groups_that_arent_symlinks();
|
||||
search::search_packages(regex, &self.groups)
|
||||
}
|
||||
Sync(args::Noconfirm(noconfirm)) => self.install_packages(*noconfirm),
|
||||
Unmanaged => self.show_unmanaged_packages(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_missing_packages(&mut self) -> Result<ToDoPerBackend> {
|
||||
let mut to_install = ToDoPerBackend::new();
|
||||
|
||||
if self.groups.is_empty() {
|
||||
eprintln!("WARNING: no group files found");
|
||||
}
|
||||
|
||||
for mut backend in Backends::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 dyn Backend) {
|
||||
#[cfg(feature = "arch")]
|
||||
{
|
||||
if let Some(arch) = backend.as_any_mut().downcast_mut::<crate::backend::Arch>() {
|
||||
arch.binary = self.config.aur_helper.clone();
|
||||
arch.aur_rm_args = self.config.aur_rm_args.clone();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(flatpak) = backend
|
||||
.as_any_mut()
|
||||
.downcast_mut::<crate::backend::Flatpak>()
|
||||
{
|
||||
flatpak.systemwide = self.config.flatpak_systemwide;
|
||||
}
|
||||
|
||||
if let Some(python) = backend
|
||||
.as_any_mut()
|
||||
.downcast_mut::<crate::backend::Python>()
|
||||
{
|
||||
python.binary = self.config.pip_binary.clone();
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.context("getting group files for args")?
|
||||
.into_iter()
|
||||
.map(|g| g.path.as_path())
|
||||
.collect();
|
||||
|
||||
run_edit_command(&group_files).context("running editor")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
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.
|
||||
///
|
||||
/// 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(&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 Backends::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 methods cannot return an error. It returns a `Result` to be consistent
|
||||
/// with other methods.
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn show_groups(self) -> Result<()> {
|
||||
self.warn_about_groups_that_arent_symlinks();
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
let show_more_than_one_group = args.len() > 1;
|
||||
|
||||
let mut iter = 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(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
fn import_groups(&self, file_names: &[String]) -> Result<()> {
|
||||
let files = get_absolutized_file_paths(file_names)?;
|
||||
let groups_dir = get_group_dir()?;
|
||||
|
||||
for target in files {
|
||||
let target_name = target
|
||||
.file_name()
|
||||
.context("path should not end in '..'")?
|
||||
.to_str()
|
||||
.context("filename is not valid UTF-8")?;
|
||||
|
||||
if !target.exists() {
|
||||
eprintln!("file {target_name} does not exist, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut link = groups_dir.clone();
|
||||
link.push(target_name);
|
||||
|
||||
if link.exists() {
|
||||
eprintln!("group {target_name} already exists, skipping");
|
||||
} else {
|
||||
symlink(target, link)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_groups(&self, groups: &[String]) -> Result<()> {
|
||||
if self.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)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create empty group files.
|
||||
///
|
||||
/// If `edit` is `true`, the editor will be run to edit the files after they are
|
||||
/// created.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if
|
||||
/// - a group name is `.` or `..`,
|
||||
/// - 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.
|
||||
#[allow(clippy::unused_self)]
|
||||
fn new_groups(&self, new_groups: &[String], edit: bool) -> Result<()> {
|
||||
let group_path = get_group_dir()?;
|
||||
|
||||
// prevent group names that resolve to directories
|
||||
for name in new_groups {
|
||||
ensure!(
|
||||
*name != "." && *name != "..",
|
||||
crate::Error::InvalidGroupName(name.clone())
|
||||
);
|
||||
}
|
||||
|
||||
let paths: Vec<_> = new_groups
|
||||
.iter()
|
||||
.map(|name| {
|
||||
let mut base = group_path.clone();
|
||||
base.push(name);
|
||||
base
|
||||
})
|
||||
.collect();
|
||||
|
||||
for file in &paths {
|
||||
ensure!(
|
||||
!file.exists(),
|
||||
crate::Error::GroupAlreadyExists(file.clone())
|
||||
);
|
||||
}
|
||||
|
||||
for file in &paths {
|
||||
File::create(file)?;
|
||||
}
|
||||
|
||||
if 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")?,
|
||||
};
|
||||
|
||||
ensure!(
|
||||
output_dir.exists() && output_dir.is_dir(),
|
||||
"output must be a directory and exist"
|
||||
);
|
||||
|
||||
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")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the parent directory of the `path` if that directory does not exist.
|
||||
///
|
||||
/// Do nothing otherwise.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the path does not have a parent.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will propagate errors from [`std::fs::create_dir_all`].
|
||||
fn create_parent(path: &Path) -> Result<()> {
|
||||
let parent = &path.parent().expect("this should never be /");
|
||||
if !parent.is_dir() {
|
||||
create_dir_all(parent).context("creating parent dir")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Move a file from one place to another.
|
||||
///
|
||||
/// At first [`std::fs::rename`] is used, which fails if `from` and `to` reside under
|
||||
/// different filesystems. In case that happens, we will resort to copying the files
|
||||
/// and then removing `from`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if we lack permission to write the file.
|
||||
fn move_file<P, Q>(from: P, to: Q) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
Q: AsRef<Path>,
|
||||
{
|
||||
let from = from.as_ref();
|
||||
let to = to.as_ref();
|
||||
match rename(from, to) {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
// CrossesDevices is nightly. See rust #86442.
|
||||
// We cannot check that here, so we just assume that
|
||||
// that would be the error if permissions are okay.
|
||||
if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
bail!(e);
|
||||
}
|
||||
copy(from, to).with_context(|| format!("copying {from:?} to {to:?}"))?;
|
||||
remove_file(from).with_context(|| format!("deleting {from:?}"))?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// For the provided names, get the group with the same name.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 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>> {
|
||||
let name_group_map: HashMap<&str, &Group> =
|
||||
groups.iter().map(|g| (g.name.as_str(), g)).collect();
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for file in names {
|
||||
match name_group_map.get(file.as_str()) {
|
||||
Some(group) => {
|
||||
result.push(*group);
|
||||
}
|
||||
None => bail!(Error::GroupFileNotFound(file.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Show the error chain for an error that has occurred when a backend was queried
|
||||
/// if the `RUST_BACKTRACE` env variable is set to `1` or `full`.
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
fn show_backend_query_error(error: &anyhow::Error, backend: &dyn Backend) {
|
||||
let section = backend.get_section();
|
||||
if should_print_debug_info() {
|
||||
eprintln!("WARNING: skipping backend '{section}':");
|
||||
for err in error.chain() {
|
||||
eprintln!(" {err}");
|
||||
}
|
||||
} else {
|
||||
eprintln!("WARNING: skipping backend '{section}': {error}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const HASH: &str = env!("GIT_HASH");
|
||||
|
||||
if HASH.is_empty() {
|
||||
VERSION
|
||||
} else {
|
||||
formatcp!("{VERSION} ({HASH})")
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a vector with the names of all backends, sorted alphabetically.
|
||||
fn get_included_backends() -> Vec<&'static str> {
|
||||
let mut result = vec![];
|
||||
for backend in Backends::iter() {
|
||||
result.push(backend.get_section());
|
||||
}
|
||||
result.sort_unstable();
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use std::env::var;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
pub fn get_editor() -> Result<String> {
|
||||
check_vars_in_order(&["EDITOR", "VISUAL"]).ok_or_else(|| anyhow!("could not find editor"))
|
||||
}
|
||||
|
||||
fn check_vars_in_order(vars: &[&str]) -> Option<String> {
|
||||
vars.iter().find_map(|v| var(v).ok())
|
||||
}
|
||||
|
||||
fn get_single_var(variable: &str) -> Option<String> {
|
||||
var(variable).ok()
|
||||
}
|
||||
|
||||
/// Determine if debug information should be printed. Will return `true` if RUST_BACKTRACE equals
|
||||
/// "s" or "full".
|
||||
pub fn should_print_debug_info() -> bool {
|
||||
match get_single_var("RUST_BACKTRACE") {
|
||||
Some(value) if ["s", "full"].contains(&value.as_str()) => true,
|
||||
Some(_) => false,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use std::error::Error as ErrorTrait;
|
||||
use std::fmt::{Display, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Error types for pacdef.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// Package search yields no results.
|
||||
NoPackagesFound,
|
||||
/// Config file not found.
|
||||
ConfigFileNotFound,
|
||||
/// Group file not found.
|
||||
GroupFileNotFound(String),
|
||||
/// Group already exists.
|
||||
GroupAlreadyExists(PathBuf),
|
||||
/// Invalid group name ('.' or '..')
|
||||
InvalidGroupName(String),
|
||||
/// Multiple groups not found.
|
||||
MultipleGroupsNotFound(Vec<String>),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoPackagesFound => f.write_str("no packages matching query"),
|
||||
Self::ConfigFileNotFound => f.write_str("config file not found"),
|
||||
Self::GroupFileNotFound(name) => f.write_str(&format!("group file '{name}' not found")),
|
||||
Self::GroupAlreadyExists(path) => f.write_str(&format!(
|
||||
"group file '{}' already exists",
|
||||
path.to_string_lossy()
|
||||
)),
|
||||
Self::InvalidGroupName(name) => {
|
||||
f.write_str(&format!("group name '{name}' is not valid"))
|
||||
}
|
||||
Self::MultipleGroupsNotFound(vec) => {
|
||||
f.write_str("could not find the following groups:\n")?;
|
||||
let mut iter = vec.iter().peekable();
|
||||
while let Some(group) = iter.next() {
|
||||
f.write_str(&format!(" {group}"))?;
|
||||
if iter.peek().is_some() {
|
||||
f.write_char('\n')?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorTrait for Error {}
|
||||
@@ -0,0 +1,328 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Display;
|
||||
use std::fs::{create_dir, read_to_string, File};
|
||||
use std::hash::Hash;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use path_absolutize::Absolutize;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::path::get_relative_path;
|
||||
|
||||
use super::{Package, Section};
|
||||
|
||||
/// Representation of a group file.
|
||||
#[derive(Debug)]
|
||||
pub struct Group {
|
||||
/// Name of the group (file name from which it was read, relative to the group
|
||||
/// base dir).
|
||||
pub(crate) name: String,
|
||||
/// The sections in the file which in turn hold the packages.
|
||||
pub(crate) sections: HashSet<Section>,
|
||||
/// The absolute path of the original file.
|
||||
pub(crate) path: PathBuf,
|
||||
/// Whether the main program should warn this group being loaded from a symlink.
|
||||
pub(crate) warn_symlink: bool,
|
||||
}
|
||||
|
||||
impl Group {
|
||||
/// Load all group files from the pacdef group dir by traversing through the group dir.
|
||||
///
|
||||
/// This method will print a warning if `warn_not_symlinks` is true and a group
|
||||
/// file is not a symlink or does not reside under a symlink dir.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 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();
|
||||
|
||||
if !group_dir.is_dir() {
|
||||
// we only need to create the innermost dir. The rest was already created from when
|
||||
// we loaded the config
|
||||
create_dir(group_dir).context("group dir does not exist, creating")?;
|
||||
}
|
||||
|
||||
let mut symlink_dirs = Vec::new();
|
||||
|
||||
for entry in WalkDir::new(group_dir).follow_links(true).min_depth(1) {
|
||||
let file = entry?;
|
||||
let path = file.path().absolutize_from(group_dir)?.to_path_buf();
|
||||
|
||||
if path.is_dir() {
|
||||
if warn_not_symlinks && path.is_symlink() {
|
||||
symlink_dirs.push(path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let should_warn_about_symlinks = warn_not_symlinks
|
||||
&& !path.is_symlink()
|
||||
&& !is_child_of_any_dir(&path, &symlink_dirs);
|
||||
|
||||
let group = Self::try_from(path.as_path(), group_dir, should_warn_about_symlinks)
|
||||
.with_context(|| format!("reading group file {path:?}"))?;
|
||||
|
||||
result.insert(group);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if `path` is a child of any of the [`PathBuf`] in `dirs`. All paths should be
|
||||
/// absolute.
|
||||
fn is_child_of_any_dir(path: &Path, dirs: &[PathBuf]) -> bool {
|
||||
dirs.iter()
|
||||
// pair `path` with every item from `symlink_dirs`
|
||||
.zip([path].iter().cycle())
|
||||
// for every pair, test if all path elements of the dir are present in the file path
|
||||
.map(|(dir, file)| {
|
||||
dir.iter()
|
||||
.zip(file.iter())
|
||||
.map(|(dir_elem, file_elem)| dir_elem == file_elem)
|
||||
.all(|path_element_equal| path_element_equal)
|
||||
})
|
||||
// it suffices if that holds for any of the generated pairs
|
||||
.any(|is_child| is_child)
|
||||
}
|
||||
|
||||
impl PartialOrd for Group {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Group {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.name.cmp(&other.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Group {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Group {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Group {
|
||||
fn assert_receiver_is_total_eq(&self) {}
|
||||
}
|
||||
|
||||
impl Group {
|
||||
/// Load the group from `path`. Determine the name from the path relative to the
|
||||
/// `group_dir`.
|
||||
///
|
||||
/// # Warnings
|
||||
///
|
||||
/// This function will print a warning if any section in the group file cannot
|
||||
/// be processed, or the file contains no sections.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the group file cannot be read.
|
||||
fn try_from<P>(path: P, group_dir: P, warn_symlink: bool) -> Result<Self>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let content = read_to_string(path).context("reading file content")?;
|
||||
|
||||
let name = extract_group_name(path, group_dir.as_ref());
|
||||
|
||||
let mut lines = content.lines().peekable();
|
||||
let mut sections = HashSet::new();
|
||||
|
||||
while lines.peek().is_some() {
|
||||
let result = Section::try_from_lines(&mut lines).context("reading section");
|
||||
match result {
|
||||
Ok(section) => {
|
||||
sections.insert(section);
|
||||
}
|
||||
Err(e) => {
|
||||
let err = e.root_cause();
|
||||
eprintln!("WARNING: could not process a section under group '{name}': {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sections.is_empty() {
|
||||
eprintln!("WARNING: no sections found in group '{name}'");
|
||||
}
|
||||
|
||||
let path = path.into();
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
sections,
|
||||
path,
|
||||
warn_symlink,
|
||||
})
|
||||
}
|
||||
|
||||
/// Add the new `packages` to the group file under the section `section_header`. If
|
||||
/// the section header does not yet exist, it is created. The packages are written
|
||||
/// in the provided order immediately after the header.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function returns an error if the group file cannot be read, or if the
|
||||
/// file cannot be written to.
|
||||
pub(crate) fn save_packages(&self, section_header: &str, packages: &[Package]) -> Result<()> {
|
||||
let mut content = read_to_string(&self.path)
|
||||
.with_context(|| format!("reading existing file contents from {:?}", &self.path))?;
|
||||
|
||||
if content.contains(section_header) {
|
||||
write_packages_to_existing_section(&mut content, section_header, packages)
|
||||
.context("existing section")?;
|
||||
} else {
|
||||
add_new_section_with_packages(&mut content, section_header, packages);
|
||||
}
|
||||
|
||||
let mut file = File::create(&self.path)
|
||||
.with_context(|| format!("creating descriptor to output file {:?}", &self.path))?;
|
||||
|
||||
write!(file, "{content}").with_context(|| format!("writing file {:?}", &self.path))
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the group name from its path relative to the group path.
|
||||
/// All subdirectories are concatenated using `'/'`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// If the group dir is `~/.config/pacdef/groups`, and the group file is
|
||||
/// `~/.config/pacdef/groups/generic/base`, then the group name is
|
||||
/// `"generic/base"`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `path` and `group_path` are identical.
|
||||
fn extract_group_name(path: &Path, group_path: &Path) -> String {
|
||||
get_relative_path(path, group_path)
|
||||
.iter()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.reduce(|mut a, b| {
|
||||
a.push('/');
|
||||
a.push_str(&b);
|
||||
a
|
||||
})
|
||||
.expect("must have at least one element")
|
||||
}
|
||||
|
||||
impl Display for Group {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut sections: Vec<_> = self.sections.iter().collect();
|
||||
sections.sort_unstable();
|
||||
|
||||
let mut iter = sections.into_iter().peekable();
|
||||
|
||||
while let Some(section) = iter.next() {
|
||||
section.fmt(f)?;
|
||||
if iter.peek().is_some() {
|
||||
f.write_str("\n\n")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Add some packages to an existing section in the content of a group file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the header cannot be found in
|
||||
/// the file content.
|
||||
fn write_packages_to_existing_section(
|
||||
group_file_content: &mut String,
|
||||
section_header: &str,
|
||||
packages: &[Package],
|
||||
) -> Result<()> {
|
||||
let idx_of_first_package_line_in_section =
|
||||
find_first_package_line_in_section(group_file_content, section_header)?;
|
||||
|
||||
let after = group_file_content.split_off(idx_of_first_package_line_in_section);
|
||||
|
||||
for p in packages {
|
||||
group_file_content.push_str(&format!("{p}\n"));
|
||||
}
|
||||
|
||||
group_file_content.push_str(&after);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find the index to the first line in `group_file_content` after the
|
||||
/// given `section_header`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the `section_header` does not
|
||||
/// exist in `group_file_content`, or if the line containing the
|
||||
/// `section_header` is not newline-terminated.
|
||||
fn find_first_package_line_in_section(
|
||||
group_file_content: &str,
|
||||
section_header: &str,
|
||||
) -> Result<usize> {
|
||||
let section_start = group_file_content
|
||||
.find(section_header)
|
||||
.context("finding first package after section header")?;
|
||||
|
||||
let distance_to_next_newline = group_file_content[section_start..]
|
||||
.find('\n')
|
||||
.context("getting next newline")?;
|
||||
|
||||
Ok(section_start + distance_to_next_newline + 1) // + 1 to be after the newline
|
||||
}
|
||||
|
||||
/// Append a new section with some packages to the content of a group file.
|
||||
fn add_new_section_with_packages(
|
||||
group_file_content: &mut String,
|
||||
section_header: &str,
|
||||
packages: &[Package],
|
||||
) {
|
||||
group_file_content.push('\n');
|
||||
group_file_content.push_str(section_header);
|
||||
group_file_content.push('\n');
|
||||
for p in packages {
|
||||
group_file_content.push_str(&format!("{p}\n"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn extract_group_name() {
|
||||
let path = PathBuf::from("/a/b/c/d/e");
|
||||
let group_path = PathBuf::from("/a/b/c");
|
||||
let expected = String::from("d/e");
|
||||
|
||||
let result = super::extract_group_name(&path, &group_path);
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_child_of_any_symlink_dir() {
|
||||
let path = PathBuf::from("/a/b/c/d/e");
|
||||
let dir = PathBuf::from("/z");
|
||||
let mut symlink_dirs = vec![dir];
|
||||
|
||||
let result = super::is_child_of_any_dir(&path, &symlink_dirs);
|
||||
assert!(!result);
|
||||
|
||||
symlink_dirs.push(PathBuf::from("/a/b/c"));
|
||||
|
||||
let result = super::is_child_of_any_dir(&path, &symlink_dirs);
|
||||
assert!(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*!
|
||||
This module reflects the relationship between groups, sections / backends and
|
||||
packages.
|
||||
|
||||
A [`Group`] contains one (strictly spoken zero, but this doesn't make sense) or
|
||||
more [`Section`]s, which relate to individual backends. Each section contains
|
||||
one (strictly spoken zero) or more [`Package`]s. On start-up `pacdef` will load
|
||||
all groups using [`Group::load`], which in turn will get all packages from all
|
||||
sections.
|
||||
*/
|
||||
mod group;
|
||||
mod package;
|
||||
mod section;
|
||||
|
||||
pub use group::Group;
|
||||
pub use package::Package;
|
||||
pub use section::Section;
|
||||
@@ -0,0 +1,127 @@
|
||||
use std::fmt::{Display, Write};
|
||||
use std::hash::Hash;
|
||||
|
||||
/// A struct to represent a single package, consiting of a `name`, and
|
||||
/// optionally a `repo`.
|
||||
#[derive(Debug, Eq, PartialOrd, Ord, Clone)]
|
||||
pub struct Package {
|
||||
pub(crate) name: String,
|
||||
pub(crate) repo: Option<String>,
|
||||
}
|
||||
|
||||
fn remove_comment_and_trim_whitespace(s: &str) -> &str {
|
||||
s.split('#') // remove comment
|
||||
.next()
|
||||
.expect("line contains something")
|
||||
.trim() // remove whitespace
|
||||
}
|
||||
|
||||
impl From<String> for Package {
|
||||
fn from(value: String) -> Self {
|
||||
let trimmed = remove_comment_and_trim_whitespace(&value);
|
||||
debug_assert!(!trimmed.is_empty(), "empty package names are not allowed");
|
||||
|
||||
let (name, repo) = Self::split_into_name_and_repo(trimmed);
|
||||
Self { name, repo }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Package {
|
||||
fn from(value: &str) -> Self {
|
||||
Self::from(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Package {
|
||||
/// From a string that contains a package name, optionally prefixed by a
|
||||
/// repository, return the package name as well as the repository if it
|
||||
/// exists.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `string` is empty.
|
||||
fn split_into_name_and_repo(string: &str) -> (String, Option<String>) {
|
||||
if let Some((before, after)) = string.split_once('/') {
|
||||
(after.to_string(), Some(before.to_string()))
|
||||
} else {
|
||||
(string.to_string(), None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to parse a string (from a line in a group file) and return a package.
|
||||
/// From the string, any possible comment is removed and whitespace is trimmed.
|
||||
/// Returns `None` if there is nothing left after trimming.
|
||||
pub(crate) fn try_from<S>(s: S) -> Option<Self>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let trimmed = remove_comment_and_trim_whitespace(s.as_ref());
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (name, repo) = Self::split_into_name_and_repo(trimmed);
|
||||
Some(Self { name, repo })
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Package {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let self_repo = self.repo.as_ref();
|
||||
let other_repo = other.repo.as_ref();
|
||||
|
||||
// iff both packages have repos, they must be identical, otherwise we don't care
|
||||
let repos_are_identical =
|
||||
self_repo.map_or(true, |sr| other_repo.map_or(true, |or| sr == or));
|
||||
|
||||
let names_are_identical = self.name == other.name;
|
||||
|
||||
names_are_identical && repos_are_identical
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Package {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Package {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.repo {
|
||||
None => (),
|
||||
Some(repo) => {
|
||||
f.write_str(repo)?;
|
||||
f.write_char('/')?;
|
||||
}
|
||||
}
|
||||
f.write_str(&self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Package;
|
||||
|
||||
#[test]
|
||||
fn split_into_name_and_repo() {
|
||||
let x = "repo/name".to_string();
|
||||
let (name, repo) = Package::split_into_name_and_repo(&x);
|
||||
assert_eq!(name, "name");
|
||||
assert_eq!(repo, Some("repo".to_string()));
|
||||
|
||||
let x = "something".to_string();
|
||||
let (name, repo) = super::Package::split_into_name_and_repo(&x);
|
||||
assert_eq!(name, "something");
|
||||
assert_eq!(repo, None);
|
||||
}
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[test]
|
||||
fn from() {
|
||||
let x = "myrepo/somepackage # ".to_string();
|
||||
let p = Package::try_from(x).unwrap();
|
||||
assert_eq!(p.name, "somepackage");
|
||||
assert_eq!(p.repo, Some("myrepo".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::{Display, Write};
|
||||
use std::hash::Hash;
|
||||
use std::iter::Peekable;
|
||||
|
||||
use anyhow::{ensure, Context, Result};
|
||||
|
||||
use super::Package;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Section {
|
||||
pub name: String,
|
||||
pub packages: HashSet<Package>,
|
||||
}
|
||||
|
||||
impl Section {
|
||||
pub(crate) fn new(name: String, packages: HashSet<Package>) -> Self {
|
||||
Self { name, packages }
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_lines<'a>(
|
||||
iter: &mut Peekable<impl Iterator<Item = &'a str>>,
|
||||
) -> Result<Self> {
|
||||
let name = find_next_section_name(iter)?;
|
||||
|
||||
let mut packages = HashSet::new();
|
||||
|
||||
while next_line_might_be_package(iter) {
|
||||
if let Some(package) = Package::try_from(iter.next().expect("we checked this is some"))
|
||||
{
|
||||
insert_package(package, &mut packages);
|
||||
}
|
||||
}
|
||||
|
||||
ensure!(!packages.is_empty(), "[{name}] is empty");
|
||||
|
||||
Ok(Self::new(name, packages))
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_package(package: Package, packages: &mut HashSet<Package>) {
|
||||
let package_name = package.name.clone();
|
||||
let newly_inserted = packages.insert(package);
|
||||
|
||||
if !newly_inserted {
|
||||
eprintln!("warning: {package_name} occurs twice in the same section");
|
||||
}
|
||||
}
|
||||
|
||||
fn next_line_might_be_package<'a>(iter: &mut Peekable<impl Iterator<Item = &'a str>>) -> bool {
|
||||
// `while let` chains are unstable, unfortunately
|
||||
iter.peek().is_some()
|
||||
&& !iter
|
||||
.peek()
|
||||
.expect("we checked this is some")
|
||||
.starts_with('[')
|
||||
}
|
||||
|
||||
fn find_next_section_name<'a>(
|
||||
iter: &mut Peekable<impl Iterator<Item = &'a str>>,
|
||||
) -> Result<String> {
|
||||
let name = iter
|
||||
.find(|line| line.starts_with('['))
|
||||
.context("finding beginning of next section")?
|
||||
.trim()
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.to_string();
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
impl Hash for Section {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Section {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Section {
|
||||
fn assert_receiver_is_total_eq(&self) {}
|
||||
}
|
||||
|
||||
impl PartialOrd for Section {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Section {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.name.cmp(&other.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Section {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("[{}]\n", &self.name))?;
|
||||
|
||||
let mut packages: Vec<_> = self.packages.iter().collect();
|
||||
packages.sort_unstable();
|
||||
|
||||
let mut iter = packages.iter().peekable();
|
||||
|
||||
while let Some(package) = iter.next() {
|
||||
package.fmt(f)?;
|
||||
if iter.peek().is_some() {
|
||||
f.write_char('\n')?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*!
|
||||
This library contains all logic that happens in `pacdef` under the hood.
|
||||
*/
|
||||
|
||||
#![warn(
|
||||
clippy::as_conversions,
|
||||
clippy::cognitive_complexity,
|
||||
clippy::explicit_iter_loop,
|
||||
clippy::explicit_into_iter_loop,
|
||||
clippy::map_entry,
|
||||
clippy::missing_errors_doc,
|
||||
clippy::missing_panics_doc,
|
||||
clippy::option_if_let_else,
|
||||
clippy::redundant_pub_crate,
|
||||
clippy::semicolon_if_nothing_returned,
|
||||
clippy::unnecessary_wraps,
|
||||
clippy::unused_self,
|
||||
clippy::unwrap_used,
|
||||
clippy::use_debug,
|
||||
clippy::use_self,
|
||||
clippy::wildcard_dependencies,
|
||||
missing_docs
|
||||
)]
|
||||
|
||||
mod args;
|
||||
mod backend;
|
||||
mod cmd;
|
||||
mod config;
|
||||
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 use crate::config::Config;
|
||||
pub use crate::core::Pacdef;
|
||||
pub use crate::errors::Error;
|
||||
pub use crate::grouping::Group;
|
||||
pub(crate) use crate::grouping::Package;
|
||||
|
||||
extern crate pacdef_macros;
|
||||
@@ -0,0 +1,99 @@
|
||||
/*!
|
||||
Main program for `pacdef`.
|
||||
*/
|
||||
|
||||
#![warn(
|
||||
clippy::as_conversions,
|
||||
clippy::option_if_let_else,
|
||||
clippy::redundant_pub_crate,
|
||||
clippy::semicolon_if_nothing_returned,
|
||||
clippy::unnecessary_wraps,
|
||||
clippy::unused_self,
|
||||
clippy::unwrap_used,
|
||||
clippy::use_debug,
|
||||
clippy::use_self,
|
||||
clippy::wildcard_dependencies,
|
||||
missing_docs
|
||||
)]
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::{ExitCode, Termination};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
|
||||
use pacdef::path::{get_config_path, get_config_path_old_version, get_group_dir};
|
||||
use pacdef::{get_args, Config, Error as PacdefError, Group, Pacdef};
|
||||
|
||||
const MAJOR_UPDATE_MESSAGE: &str = "VERSION UPGRADE
|
||||
You seem to have used version 0.x of pacdef before.
|
||||
Version 1.x changes the syntax of the config files and the command line arguments.
|
||||
Check out https://github.com/steven-omaha/pacdef for new syntax information.
|
||||
This message will not appear again.
|
||||
------";
|
||||
|
||||
fn main() -> ExitCode {
|
||||
handle_final_result(main_inner())
|
||||
}
|
||||
|
||||
/// Skip printing the error chain when searching packages yields no results,
|
||||
/// otherwise report error chain.
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
fn handle_final_result(result: Result<()>) -> ExitCode {
|
||||
match result {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Err(ref e) => {
|
||||
if let Some(root_error) = e.root_cause().downcast_ref::<PacdefError>() {
|
||||
eprintln!("{root_error}");
|
||||
ExitCode::FAILURE
|
||||
} else {
|
||||
result.report()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main_inner() -> Result<()> {
|
||||
let args = get_args();
|
||||
|
||||
let config_file = get_config_path().context("getting config file")?;
|
||||
|
||||
let config = match Config::load(&config_file).context("loading config file") {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
if let Some(crate_error) = e.downcast_ref::<PacdefError>() {
|
||||
match crate_error {
|
||||
PacdefError::ConfigFileNotFound => load_default_config(&config_file)?,
|
||||
_ => bail!("unexpected error: {crate_error}"),
|
||||
}
|
||||
} else {
|
||||
bail!("unexpected error: {e:?}");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let group_dir = get_group_dir().context("resolving group dir")?;
|
||||
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")
|
||||
}
|
||||
|
||||
fn load_default_config(config_file: &Path) -> Result<Config> {
|
||||
if get_config_path_old_version()?.exists() {
|
||||
println!("{MAJOR_UPDATE_MESSAGE}");
|
||||
}
|
||||
|
||||
if !config_file.exists() {
|
||||
create_empty_config_file(config_file)?;
|
||||
}
|
||||
|
||||
Ok(Config::default())
|
||||
}
|
||||
|
||||
fn create_empty_config_file(config_file: &Path) -> Result<()> {
|
||||
let config_dir = &config_file.parent().context("getting parent dir")?;
|
||||
std::fs::create_dir_all(config_dir).context("creating parent dir")?;
|
||||
std::fs::File::create(config_file).context("creating empty config file")?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*!
|
||||
All functions related to `pacdef`'s internal paths.
|
||||
*/
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{env, path::Path};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use path_absolutize::Absolutize;
|
||||
|
||||
const CONFIG_FILE_NAME: &str = "pacdef.yaml";
|
||||
const CONFIG_FILE_NAME_OLD: &str = "pacdef.conf";
|
||||
|
||||
/// Get the group directory where all group files are located. This is
|
||||
/// `$XDG_CONFIG_HOME/pacdef/groups`, which defaults to `$HOME/.config/pacdef/groups`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function returns an error if both `$XDG_CONFIG_HOME` and `$HOME` are undefined.
|
||||
pub fn get_group_dir() -> Result<PathBuf> {
|
||||
let mut result = get_pacdef_base_dir().context("getting pacdef base dir")?;
|
||||
result.push("groups");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get the base directory for `pacdef`'s config files.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if `$XDG_CONFIG_HOME` cannot be determined.
|
||||
pub(crate) fn get_pacdef_base_dir() -> Result<PathBuf> {
|
||||
let mut dir = get_xdg_config_home().context("getting XDG_CONFIG_HOME")?;
|
||||
dir.push("pacdef");
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
/// Get the path to the cargo home directory.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if neither the `$CARGO_HOME` nor
|
||||
/// the `$HOME` environment variables are set.
|
||||
pub(crate) fn get_cargo_home() -> Result<PathBuf> {
|
||||
if let Ok(config) = env::var("CARGO_HOME") {
|
||||
Ok(config.into())
|
||||
} else {
|
||||
let mut config = get_home_dir().context("falling back to $HOME/.cargo")?;
|
||||
config.push(".cargo");
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to the XDG config directory.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if neither the `$XDG_CONFIG_HOME` nor
|
||||
/// the `$HOME` environment variables are set.
|
||||
fn get_xdg_config_home() -> Result<PathBuf> {
|
||||
if let Ok(config) = env::var("XDG_CONFIG_HOME") {
|
||||
Ok(config.into())
|
||||
} else {
|
||||
let mut config = get_home_dir().context("falling back to $HOME/.config")?;
|
||||
config.push(".config");
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the home directory of the current user from the `$HOME` environment
|
||||
/// variable.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the `$HOME` variable is not set.
|
||||
pub(crate) fn get_home_dir() -> Result<PathBuf> {
|
||||
Ok(env::var("HOME").context("getting $HOME variable")?.into())
|
||||
}
|
||||
|
||||
/// Get the path to the pacdef config file. This is `$XDG_CONFIG_HOME/pacdef/pacdef.yaml`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function returns an error if both `$XDG_CONFIG_HOME` and `$HOME` are
|
||||
/// undefined.
|
||||
pub fn get_config_path() -> Result<PathBuf> {
|
||||
let mut file = get_pacdef_base_dir().context("getting pacdef base dir for config file")?;
|
||||
file.push(CONFIG_FILE_NAME);
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
/// Get the path to the pacdef config file from version 0.x. This is
|
||||
/// `$XDG_CONFIG_HOME/pacdef/pacdef.conf`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function returns an error if both `$XDG_CONFIG_HOME` and `$HOME` are
|
||||
/// undefined.
|
||||
pub fn get_config_path_old_version() -> Result<PathBuf> {
|
||||
let mut file = get_pacdef_base_dir().context("getting pacdef base dir for config file")?;
|
||||
file.push(CONFIG_FILE_NAME_OLD);
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
/// Determine if a program `name` exists in the folders in the `$PATH` variable.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function returns an error if `$PATH` is not set.
|
||||
pub(crate) fn binary_in_path(name: &str) -> Result<bool> {
|
||||
let paths = env::var_os("PATH").context("getting $PATH")?;
|
||||
for dir in env::split_paths(&paths) {
|
||||
let full_path = dir.join(name);
|
||||
if full_path.is_file() {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Determine the relative path of `full_path` in relation to `base_path`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if at least one element in `base_path` does not match the corresponding
|
||||
/// element in `full_path`.
|
||||
pub(crate) fn get_relative_path<P>(full_path: P, base_path: P) -> PathBuf
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let mut file_iter = full_path.as_ref().iter();
|
||||
base_path
|
||||
.as_ref()
|
||||
.iter()
|
||||
.zip(&mut file_iter)
|
||||
.for_each(|(a, b)| assert_eq!(a, b));
|
||||
let relative_path: PathBuf = file_iter.collect();
|
||||
relative_path
|
||||
}
|
||||
|
||||
/// For each file argument, return the absolute path to the file.
|
||||
pub(crate) fn get_absolutized_file_paths(arg_match: &[String]) -> Result<Vec<PathBuf>> {
|
||||
let mut result = vec![];
|
||||
|
||||
for item in arg_match {
|
||||
let path: PathBuf = item.into();
|
||||
let absolute = path
|
||||
.absolutize()
|
||||
.with_context(|| format!("absolutizing {path:?}"))?
|
||||
.into_owned();
|
||||
result.push(absolute);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::get_relative_path;
|
||||
|
||||
#[test]
|
||||
fn relative_path() {
|
||||
let full = PathBuf::from("/a/b/c/d/e");
|
||||
let base = PathBuf::from("/a/b/c");
|
||||
let relative = get_relative_path(full, base);
|
||||
assert_eq!(relative, PathBuf::from("d/e"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn relative_path_panic() {
|
||||
let full = PathBuf::from("/a/b/z/d/e");
|
||||
let base = PathBuf::from("/a/b/c");
|
||||
get_relative_path(full, base);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::backend::Backend;
|
||||
use crate::{Group, Package};
|
||||
|
||||
use super::strategy::Strategy;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(super) enum ReviewAction {
|
||||
AsDependency(Package),
|
||||
Delete(Package),
|
||||
AssignGroup(Package, Rc<Group>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) enum ReviewIntention {
|
||||
AsDependency,
|
||||
AssignGroup,
|
||||
Delete,
|
||||
Info,
|
||||
Invalid,
|
||||
Skip,
|
||||
Quit,
|
||||
Apply,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct ReviewsPerBackend {
|
||||
items: Vec<(Box<dyn Backend>, Vec<ReviewAction>)>,
|
||||
}
|
||||
|
||||
impl ReviewsPerBackend {
|
||||
pub(super) fn new() -> Self {
|
||||
Self { items: vec![] }
|
||||
}
|
||||
|
||||
pub(super) fn nothing_to_do(&self) -> bool {
|
||||
self.items.iter().all(|(_, vec)| vec.is_empty())
|
||||
}
|
||||
|
||||
pub(super) fn push(&mut self, value: (Box<dyn Backend>, Vec<ReviewAction>)) {
|
||||
self.items.push(value);
|
||||
}
|
||||
|
||||
/// Convert the reviews per backend to a vector of [`Strategy`], where one `Strategy` contains
|
||||
/// all actions that must be executed for a [`Backend`].
|
||||
///
|
||||
/// 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> {
|
||||
let mut result = vec![];
|
||||
|
||||
for (backend, actions) in self {
|
||||
let mut to_delete = vec![];
|
||||
let mut assign_group = vec![];
|
||||
let mut as_dependency = vec![];
|
||||
|
||||
extract_actions(
|
||||
actions,
|
||||
&mut to_delete,
|
||||
&mut assign_group,
|
||||
&mut as_dependency,
|
||||
);
|
||||
|
||||
result.push(Strategy::new(
|
||||
backend,
|
||||
to_delete,
|
||||
as_dependency,
|
||||
assign_group,
|
||||
));
|
||||
}
|
||||
|
||||
result.retain(|s| !s.nothing_to_do());
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for ReviewsPerBackend {
|
||||
type Item = (Box<dyn Backend>, Vec<ReviewAction>);
|
||||
|
||||
type IntoIter = std::vec::IntoIter<(Box<dyn Backend>, Vec<ReviewAction>)>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.items.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) enum ContinueWithReview {
|
||||
Yes,
|
||||
No,
|
||||
NoAndApply,
|
||||
}
|
||||
|
||||
fn extract_actions(
|
||||
actions: Vec<ReviewAction>,
|
||||
to_delete: &mut Vec<Package>,
|
||||
assign_group: &mut Vec<(Package, Rc<Group>)>,
|
||||
as_dependency: &mut Vec<Package>,
|
||||
) {
|
||||
for action in actions {
|
||||
match action {
|
||||
ReviewAction::Delete(package) => to_delete.push(package),
|
||||
ReviewAction::AssignGroup(package, group) => assign_group.push((package, group)),
|
||||
ReviewAction::AsDependency(package) => as_dependency.push(package),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
mod datastructures;
|
||||
mod strategy;
|
||||
|
||||
use std::io::{stdin, stdout, Write};
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::backend::{Backend, ToDoPerBackend};
|
||||
use crate::ui::{get_user_confirmation, read_single_char_from_terminal};
|
||||
use crate::{Group, 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<()> {
|
||||
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");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
'outer: for (backend, packages) in todo_per_backend.into_iter() {
|
||||
let mut actions = vec![];
|
||||
for package in packages {
|
||||
println!("{}: {package}", backend.get_section());
|
||||
match get_action_for_package(package, &groups, &mut actions, &*backend)? {
|
||||
ContinueWithReview::Yes => continue,
|
||||
ContinueWithReview::No => return Ok(()),
|
||||
ContinueWithReview::NoAndApply => {
|
||||
reviews.push((backend, actions));
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
reviews.push((backend, actions));
|
||||
}
|
||||
|
||||
if reviews.nothing_to_do() {
|
||||
println!("nothing to do");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let strategies: Vec<Strategy> = reviews.into_strategies();
|
||||
|
||||
println!();
|
||||
let mut iter = strategies.iter().peekable();
|
||||
|
||||
while let Some(strategy) = iter.next() {
|
||||
strategy.show();
|
||||
|
||||
if iter.peek().is_some() {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
if !get_user_confirmation()? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for strategy in strategies {
|
||||
strategy.execute()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_action_for_package(
|
||||
package: Package,
|
||||
groups: &[Rc<Group>],
|
||||
reviews: &mut Vec<ReviewAction>,
|
||||
backend: &dyn Backend,
|
||||
) -> Result<ContinueWithReview> {
|
||||
loop {
|
||||
match ask_user_action_for_package(backend.supports_as_dependency())? {
|
||||
ReviewIntention::AsDependency => {
|
||||
assert!(
|
||||
backend.supports_as_dependency(),
|
||||
"backend does not support dependencies"
|
||||
);
|
||||
reviews.push(ReviewAction::AsDependency(package));
|
||||
break;
|
||||
}
|
||||
ReviewIntention::AssignGroup => {
|
||||
if let Ok(Some(group)) = ask_group(groups) {
|
||||
reviews.push(ReviewAction::AssignGroup(package, group));
|
||||
break;
|
||||
};
|
||||
}
|
||||
ReviewIntention::Delete => {
|
||||
reviews.push(ReviewAction::Delete(package));
|
||||
break;
|
||||
}
|
||||
ReviewIntention::Info => {
|
||||
backend.show_package_info(&package)?;
|
||||
}
|
||||
ReviewIntention::Invalid => (),
|
||||
ReviewIntention::Skip => break,
|
||||
ReviewIntention::Quit => return Ok(ContinueWithReview::No),
|
||||
ReviewIntention::Apply => return Ok(ContinueWithReview::NoAndApply),
|
||||
}
|
||||
}
|
||||
Ok(ContinueWithReview::Yes)
|
||||
}
|
||||
|
||||
/// Ask the user for the desired action, and return the associated
|
||||
/// [`ReviewIntention`]. The query depends on the capabilities of the backend.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if stdin or stdout cannot be accessed.
|
||||
fn ask_user_action_for_package(supports_as_dependency: bool) -> Result<ReviewIntention> {
|
||||
print_query(supports_as_dependency)?;
|
||||
|
||||
match read_single_char_from_terminal()?.to_ascii_lowercase() {
|
||||
'a' if supports_as_dependency => Ok(ReviewIntention::AsDependency),
|
||||
'd' => Ok(ReviewIntention::Delete),
|
||||
'g' => Ok(ReviewIntention::AssignGroup),
|
||||
'i' => Ok(ReviewIntention::Info),
|
||||
'q' => Ok(ReviewIntention::Quit),
|
||||
's' => Ok(ReviewIntention::Skip),
|
||||
'p' => Ok(ReviewIntention::Apply),
|
||||
_ => Ok(ReviewIntention::Invalid),
|
||||
}
|
||||
}
|
||||
|
||||
/// Print a space-terminated string that asks the user for the desired action.
|
||||
/// The items of the string depend on whether the backend supports dependent
|
||||
/// packages.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if stdout cannot be flushed.
|
||||
fn print_query(supports_as_dependency: bool) -> Result<()> {
|
||||
let mut query = String::from("assign to (g)roup, (d)elete, (s)kip, (i)nfo, ");
|
||||
|
||||
if supports_as_dependency {
|
||||
query.push_str("(a)s dependency, ");
|
||||
}
|
||||
|
||||
query.push_str("a(p)ply, (q)uit? ");
|
||||
|
||||
print!("{query}");
|
||||
stdout().lock().flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_enumerated_groups(groups: &[Rc<Group>]) {
|
||||
let number_digits = get_amount_of_digits_for_number(groups.len());
|
||||
|
||||
for (i, group) in groups.iter().enumerate() {
|
||||
println!("{i:>number_digits$}: {}", group.name);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::as_conversions)] // this cannot introduce errors for any reasonably sized numbers.
|
||||
fn get_amount_of_digits_for_number(number: usize) -> usize {
|
||||
(number as f64).log10().trunc() as usize + 1
|
||||
}
|
||||
|
||||
fn ask_group(groups: &[Rc<Group>]) -> Result<Option<Rc<Group>>> {
|
||||
print_enumerated_groups(groups);
|
||||
let mut buf = String::new();
|
||||
stdin().read_line(&mut buf)?;
|
||||
let reply = buf.trim();
|
||||
|
||||
let idx: usize = if let Ok(idx) = reply.parse() {
|
||||
idx
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if idx < groups.len() {
|
||||
Ok(Some(groups[idx].clone()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::backend::Backend;
|
||||
use crate::{Group, Package};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Strategy {
|
||||
backend: Box<dyn Backend>,
|
||||
delete: Vec<Package>,
|
||||
as_dependency: Vec<Package>,
|
||||
assign_group: Vec<(Package, Rc<Group>)>,
|
||||
}
|
||||
|
||||
impl Strategy {
|
||||
pub(super) fn new(
|
||||
backend: Box<dyn Backend>,
|
||||
delete: Vec<Package>,
|
||||
as_dependency: Vec<Package>,
|
||||
assign_group: Vec<(Package, Rc<Group>)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
backend,
|
||||
delete,
|
||||
as_dependency,
|
||||
assign_group,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn execute(self) -> Result<()> {
|
||||
if !self.delete.is_empty() {
|
||||
self.backend.remove_packages(&self.delete, false)?;
|
||||
}
|
||||
|
||||
if !self.as_dependency.is_empty() {
|
||||
self.backend.make_dependency(&self.as_dependency)?;
|
||||
}
|
||||
|
||||
if !self.assign_group.is_empty() {
|
||||
self.backend.assign_group(self.assign_group)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn show(&self) {
|
||||
if self.nothing_to_do() {
|
||||
return;
|
||||
}
|
||||
|
||||
println!("[{}]", self.backend.get_section());
|
||||
|
||||
if !self.delete.is_empty() {
|
||||
println!("delete:");
|
||||
for p in &self.delete {
|
||||
println!(" {p}");
|
||||
}
|
||||
}
|
||||
|
||||
if !self.as_dependency.is_empty() {
|
||||
println!("as dependency:");
|
||||
for p in &self.as_dependency {
|
||||
println!(" {p}");
|
||||
}
|
||||
}
|
||||
|
||||
if !self.assign_group.is_empty() {
|
||||
println!("assign groups:");
|
||||
for (p, g) in &self.assign_group {
|
||||
println!(" {p} -> {}", g.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn nothing_to_do(&self) -> bool {
|
||||
self.delete.is_empty() && self.as_dependency.is_empty() && self.assign_group.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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};
|
||||
|
||||
/// 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
|
||||
/// section.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 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<()> {
|
||||
if groups.is_empty() {
|
||||
eprintln!("WARNING: no group files found");
|
||||
bail!(crate::errors::Error::NoPackagesFound);
|
||||
}
|
||||
|
||||
let re = Regex::new(regex_str)?;
|
||||
|
||||
let mut vec = vec![];
|
||||
|
||||
for group in groups {
|
||||
for section in &group.sections {
|
||||
for package in §ion.packages {
|
||||
if re.is_match(&package.name) {
|
||||
vec.push((group, section, package));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if vec.is_empty() {
|
||||
bail!(crate::errors::Error::NoPackagesFound);
|
||||
}
|
||||
|
||||
print_triples(vec);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_triples(mut vec: Vec<(&Group, &Section, &Package)>) {
|
||||
vec.sort_unstable();
|
||||
|
||||
let mut g0 = String::new();
|
||||
let mut s0 = String::new();
|
||||
|
||||
let mut iter = vec.into_iter().peekable();
|
||||
|
||||
while let Some((g, s, p)) = iter.next() {
|
||||
print_group_if_changed(g, &g0, &mut s0);
|
||||
print_section_if_changed(s, &s0);
|
||||
println!("{p}");
|
||||
save_group_and_section_name(&mut g0, g, &mut s0, s);
|
||||
print_separator_unless_exhausted(&mut iter, &g0);
|
||||
}
|
||||
}
|
||||
|
||||
fn save_group_and_section_name(g0: &mut String, g: &Group, s0: &mut String, s: &Section) {
|
||||
*g0 = g.name.clone();
|
||||
*s0 = s.name.clone();
|
||||
}
|
||||
|
||||
fn print_separator_unless_exhausted(
|
||||
iter: &mut Peekable<IntoIter<(&Group, &Section, &Package)>>,
|
||||
g0: &String,
|
||||
) {
|
||||
if let Some((g, _, _)) = iter.peek() {
|
||||
if g.name != *g0 {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_section_if_changed(current: &Section, previous_name: &String) {
|
||||
if current.name != *previous_name {
|
||||
println!("[{}]", current.name);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_group_if_changed(
|
||||
current_group: &Group,
|
||||
previous_group_name: &String,
|
||||
previous_section_name: &mut String,
|
||||
) {
|
||||
if current_group.name != *previous_group_name {
|
||||
println!("{}", current_group.name);
|
||||
for _ in 0..current_group.name.len() {
|
||||
print!("-");
|
||||
}
|
||||
println!();
|
||||
previous_section_name.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use std::io::{self, Read, Write};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use termios::*;
|
||||
|
||||
pub fn get_user_confirmation() -> Result<bool> {
|
||||
print!("Continue? [Y/n] ");
|
||||
std::io::stdout().flush().context("flushing stdout")?;
|
||||
|
||||
let mut reply = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut reply)
|
||||
.context("reading stdin")?;
|
||||
|
||||
Ok(reply.trim().is_empty() || reply.to_lowercase().contains('y'))
|
||||
}
|
||||
|
||||
/// Read a single byte from stdin and interpret it as `char`. Use the
|
||||
/// `termios` library to switch the terminal to raw mode before reading,
|
||||
/// and restore the original terminal mode afterwards.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the data cannot be read or the
|
||||
/// terminal settings cannot be changed.
|
||||
pub fn read_single_char_from_terminal() -> Result<char> {
|
||||
// 0 is the file descriptor for stdin
|
||||
let fd = 0;
|
||||
let termios = Termios::from_fd(fd).context("getting stdin fd")?;
|
||||
let mut new_termios = termios;
|
||||
new_termios.c_lflag &= !(ICANON | ECHO);
|
||||
new_termios.c_cc[VMIN] = 1;
|
||||
new_termios.c_cc[VTIME] = 0;
|
||||
tcsetattr(fd, TCSANOW, &new_termios).context("setting terminal mode")?;
|
||||
|
||||
let mut input_buffer = [0u8; 1];
|
||||
io::stdin()
|
||||
.read_exact(&mut input_buffer[..])
|
||||
.context("reading one byte from stdin")?;
|
||||
let result: char = input_buffer[0].into();
|
||||
|
||||
// stdin is not echoed automatically in this terminal mode
|
||||
println!("{result}");
|
||||
|
||||
// restore previous settings
|
||||
tcsetattr(fd, TCSANOW, &termios).context("restoring terminal mode")?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
Reference in New Issue
Block a user