refact(backend): static dispatch

major overhaul of Backend from dynamic to static dispatch

See #73.
This commit is contained in:
steven-omaha
2024-04-22 17:40:18 +02:00
committed by GitHub
36 changed files with 382 additions and 590 deletions
+9 -1
View File
@@ -1,9 +1,17 @@
# Contributing
Thank you for considering to contribute to `pacdef`. The recommended workflow is this:
## General Steps
Thank you for considering to contribute to `pacdef`. The recommended workflow is
this:
1. Open a github issue, mention that you would like to fix the issue in a PR.
2. Wait for approval.
3. Fork the repository and implement your fix / feature.
4. Make sure your code generates no warnings, and passes `rustfmt` and `clippy`.
5. Open the pull request.
## Rust-Analyzer Issues
Rust Analyzer may not work unless both the `pacutils` and `apt` packages are
installed. On Arch that is, this may vary on other distros.
Generated
+13 -10
View File
@@ -215,6 +215,18 @@ dependencies = [
"syn",
]
[[package]]
name = "enum_dispatch"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@@ -395,8 +407,8 @@ dependencies = [
"anyhow",
"clap",
"const_format",
"enum_dispatch",
"libc",
"pacdef_macros",
"path-absolutize",
"regex",
"rstest",
@@ -409,15 +421,6 @@ dependencies = [
"walkdir",
]
[[package]]
name = "pacdef_macros"
version = "1.0.1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "path-absolutize"
version = "3.1.1"
+1 -2
View File
@@ -12,8 +12,7 @@ categories = ["command-line-utilities"]
rust-version = "1.74"
[workspace.dependencies]
pacdef_macros = { path = "crates/pacdef_macros", version = "1.0" }
pacdef = { path = "crates/pacdef", version = "1.6" }
pacdef = { path = "crates/pacdef" }
[profile.release]
lto = "off"
-83
View File
@@ -1,83 +0,0 @@
# This is a configuration file for the bacon tool
#
# Bacon repository: https://github.com/Canop/bacon
# Complete help on configuration: https://dystroy.org/bacon/config/
# You can also check bacon's own bacon.toml file
# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml
default_job = "check"
[jobs.check]
command = ["cargo", "check", "--color", "always", "--features", "arch"]
need_stdout = false
[jobs.check-all]
command = [
"cargo",
"check",
"--all-targets",
"--color",
"always",
"--features",
"arch",
]
need_stdout = false
[jobs.clippy]
command = [
"cargo",
"clippy",
"--all-targets",
"--color",
"always",
"--features",
"arch",
]
need_stdout = false
[jobs.test]
command = [
"cargo",
"test",
"--color",
"always",
"--package",
"pacdef",
"--",
"--color",
"always", # see https://github.com/Canop/bacon/issues/124
]
need_stdout = true
[jobs.doc]
command = ["cargo", "doc", "--color", "always", "--no-deps"]
need_stdout = false
# If the doc compiles, then it opens in your browser and bacon switches
# to the previous job
[jobs.doc-open]
command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"]
need_stdout = false
on_success = "back" # so that we don't open the browser at each change
# You can run your application and have the result displayed in bacon,
# *if* it makes sense for this crate. You can run an example the same
# way. Don't forget the `--color always` part or the errors won't be
# properly parsed.
[jobs.run]
command = [
"cargo",
"run",
"--color",
"always",
# put launch parameters for your program behind a `--` separator
]
need_stdout = true
allow_warnings = true
# You may define here keybindings that would be specific to
# a project, for example a shortcut to launch a specific job.
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead.
[keybindings]
# alt-m = "job:my-job"
+1 -2
View File
@@ -19,14 +19,13 @@ regex = { version = "1.10", default-features = false, features = ["std"] }
termios = "0.3"
walkdir = "2.5"
libc = "0.2"
enum_dispatch = "0.3"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
serde_yaml = "0.9"
pacdef_macros.workspace = true
# backends
alpm = { version = "3.0", optional = true }
rust-apt = { version = "0.7", optional = true }
+3 -3
View File
@@ -1,11 +1,11 @@
use self::datastructure::Arguments;
mod cli;
mod datastructure;
pub mod datastructure;
mod parsing;
#[cfg(test)]
mod tests;
pub use datastructure::*;
/// Get and parse the CLI arguments.
#[must_use]
pub fn get() -> Arguments {
+17 -17
View File
@@ -1,4 +1,6 @@
use super::datastructure::*;
use super::datastructure::{
Arguments, Edit, Force, GroupAction, Groups, Noconfirm, OutputDir, PackageAction, Regex,
};
const ARGS_CONSISTENT: &str = "argument declaration and parsing must be consistent";
@@ -13,30 +15,28 @@ pub(super) fn parse(args: clap::ArgMatches) -> Arguments {
}
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(("edit", args)) => GroupAction::Edit(get_groups(args)),
Some(("export", args)) => {
GroupAction::Export(get_groups(args), get_output_dir(args), get_force(args))
}
Some(("import", args)) => GroupAction::Import(get_groups(args)),
Some(("list", _)) => GroupAction::List,
Some(("new", args)) => GroupAction::New(get_groups(args), get_edit(args)),
Some(("remove", args)) => GroupAction::Remove(get_groups(args)),
Some(("show", args)) => GroupAction::Show(get_groups(args)),
Some(value) => panic!("group subcommand was not matched: {value:?}"),
None => unreachable!("prevented by clap"),
}
}
fn parse_package_args(args: &clap::ArgMatches) -> PackageAction {
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(("clean", args)) => PackageAction::Clean(get_noconfirm(args)),
Some(("review", _)) => PackageAction::Review,
Some(("search", args)) => PackageAction::Search(get_regex(args)),
Some(("sync", args)) => PackageAction::Sync(get_noconfirm(args)),
Some(("unmanaged", _)) => PackageAction::Unmanaged,
Some(value) => panic!("package subcommand was not matched: {value:?}"),
None => unreachable!("prevented by clap"),
}
+17 -13
View File
@@ -12,9 +12,23 @@ 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>,
pub binary: String,
pub aur_rm_args: Vec<String>,
pub packages: HashSet<Package>,
}
impl Arch {
pub fn new() -> Self {
Self {
binary: BINARY.to_string(),
aur_rm_args: vec![],
packages: HashSet::new(),
}
}
}
impl Default for Arch {
fn default() -> Self {
Self::new()
}
}
const BINARY: Text = "pacman";
@@ -117,13 +131,3 @@ fn convert_to_pacdef_packages(packages: HashSet<String>) -> HashSet<Package> {
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(),
}
}
}
+13 -9
View File
@@ -12,7 +12,19 @@ use crate::{Group, Package};
#[derive(Debug, Clone)]
pub struct Debian {
pub(crate) packages: HashSet<Package>,
pub packages: HashSet<Package>,
}
impl Debian {
pub fn new() -> Self {
Self {
packages: HashSet::new(),
}
}
}
impl Default for Debian {
fn default() -> Self {
Self::new()
}
}
const BINARY: Text = "apt";
@@ -94,11 +106,3 @@ impl Backend for Debian {
run_external_command(cmd)
}
}
impl Debian {
pub(crate) fn new() -> Self {
Self {
packages: HashSet::new(),
}
}
}
+13 -9
View File
@@ -10,7 +10,19 @@ use crate::{Group, Package};
#[derive(Debug, Clone)]
pub struct Fedora {
pub(crate) packages: HashSet<Package>,
pub packages: HashSet<Package>,
}
impl Fedora {
pub fn new() -> Self {
Self {
packages: HashSet::new(),
}
}
}
impl Default for Fedora {
fn default() -> Self {
Self::new()
}
}
const BINARY: Text = "dnf";
@@ -123,14 +135,6 @@ impl Backend for Fedora {
}
}
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!");
+40 -36
View File
@@ -10,8 +10,46 @@ use crate::{Group, Package};
#[derive(Debug, Clone)]
pub struct Flatpak {
pub(crate) packages: HashSet<Package>,
pub(crate) systemwide: bool,
pub packages: HashSet<Package>,
pub systemwide: bool,
}
impl Flatpak {
pub 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>>())
}
}
impl Default for Flatpak {
fn default() -> Self {
Self::new()
}
}
const BINARY: Text = "flatpak";
@@ -84,37 +122,3 @@ impl Backend for Flatpak {
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>>())
}
}
+44 -40
View File
@@ -9,10 +9,52 @@ use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::{Group, Package};
macro_rules! ERROR{
($bin:expr) => {
panic!("Cannot use {} for package management in python. Please use a valid package manager like pip or pipx.", $bin)
};
}
#[derive(Debug, Clone)]
pub struct Python {
pub(crate) binary: String,
pub(crate) packages: HashSet<Package>,
pub binary: String,
pub packages: HashSet<Package>,
}
impl Python {
pub 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()),
}
}
}
impl Default for Python {
fn default() -> Self {
Self::new()
}
}
const BINARY: Text = "pip";
@@ -26,12 +68,6 @@ 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!();
@@ -64,38 +100,6 @@ fn run_pip_command(cmd: &mut Command, args: &[&str]) -> Result<Value> {
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()
+13 -9
View File
@@ -12,7 +12,19 @@ use crate::{Group, Package};
#[derive(Debug, Clone)]
pub struct Rust {
pub(crate) packages: HashSet<Package>,
pub packages: HashSet<Package>,
}
impl Rust {
pub fn new() -> Self {
Self {
packages: HashSet::new(),
}
}
}
impl Default for Rust {
fn default() -> Self {
Self::new()
}
}
const BINARY: Text = "cargo";
@@ -76,14 +88,6 @@ fn extract_packages(json: &Value) -> Result<HashSet<Package>> {
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");
+17 -1
View File
@@ -12,9 +12,25 @@ 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};
#[derive(Debug, Clone)]
pub struct Rustup {
pub packages: HashSet<Package>,
}
impl Rustup {
pub fn new() -> Self {
Self {
packages: HashSet::new(),
}
}
}
impl Default for Rustup {
fn default() -> Self {
Self::new()
}
}
const BINARY: Text = "rustup";
const SECTION: Text = "rustup";
@@ -1,13 +1,7 @@
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,
@@ -28,14 +22,6 @@ pub struct RustupPackage {
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
+13 -9
View File
@@ -12,7 +12,19 @@ use crate::{Group, Package};
#[derive(Debug, Clone)]
pub struct Void {
pub(crate) packages: HashSet<Package>,
pub packages: HashSet<Package>,
}
impl Void {
pub fn new() -> Self {
Self {
packages: HashSet::new(),
}
}
}
impl Default for Void {
fn default() -> Self {
Self::new()
}
}
const BINARY: Text = "xbps-install";
@@ -124,11 +136,3 @@ impl Backend for Void {
run_external_command(cmd)
}
}
impl Void {
pub(crate) fn new() -> Self {
Self {
packages: HashSet::new(),
}
}
}
+5 -9
View File
@@ -1,7 +1,5 @@
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;
@@ -11,11 +9,12 @@ 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;
pub type Switches = &'static [&'static str];
pub type Text = &'static str;
/// The trait of a struct that is used as a backend.
pub trait Backend: Debug {
#[enum_dispatch::enum_dispatch]
pub trait Backend {
/// 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
@@ -47,7 +46,7 @@ pub trait Backend: Debug {
/// 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`].
/// [`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,
@@ -174,9 +173,6 @@ pub trait Backend: Debug {
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;
}
-21
View File
@@ -1,21 +0,0 @@
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)
}
}
}
}
-4
View File
@@ -47,10 +47,6 @@ macro_rules! impl_backend_constants {
})
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn supports_as_dependency(&self) -> bool {
SUPPORTS_AS_DEPENDENCY
}
+45 -19
View File
@@ -1,26 +1,52 @@
mod actual;
mod backend_trait;
mod iter;
mod macros;
pub mod actual;
pub mod backend_trait;
pub mod macros;
mod root;
mod todo_per_backend;
pub mod todo_per_backend;
pub use backend_trait::Backend;
pub use iter::BackendIter;
pub use todo_per_backend::ToDoPerBackend;
use crate::backend::backend_trait::Switches;
use crate::backend::backend_trait::Text;
use crate::Group;
use crate::Package;
use anyhow::Result;
use backend_trait::Backend;
use std::collections::HashSet;
use std::rc::Rc;
use pacdef_macros::Register;
use self::actual::{
fedora::Fedora, flatpak::Flatpak, python::Python, rust::Rust, rustup::Rustup, void::Void,
};
#[derive(Debug, Register)]
pub enum Backends {
#[derive(Debug)]
#[enum_dispatch::enum_dispatch(Backend)]
pub enum AnyBackend {
#[cfg(feature = "arch")]
Arch,
Arch(actual::arch::Arch),
#[cfg(feature = "debian")]
Debian,
Flatpak,
Fedora,
Python,
Rust,
Rustup,
Void,
Debian(actual::debian::Debian),
Flatpak(Flatpak),
Fedora(Fedora),
Python(Python),
Rust(Rust),
Rustup(Rustup),
Void(Void),
}
impl AnyBackend {
/// Returns an iterator of every variant of backend.
pub fn iter() -> impl Iterator<Item = Self> {
vec![
#[cfg(feature = "arch")]
Self::Arch(actual::arch::Arch::new()),
#[cfg(feature = "debian")]
Self::Debian(actual::debian::Debian::new()),
Self::Flatpak(Flatpak::new()),
Self::Fedora(Fedora::new()),
Self::Python(Python::new()),
Self::Rust(Rust::new()),
Self::Rustup(Rustup::new()),
Self::Void(Void::new()),
]
.into_iter()
}
}
+29 -17
View File
@@ -2,63 +2,60 @@ use std::fmt::Write;
use anyhow::{Context, Result};
use super::Backend;
use super::{AnyBackend, Backend};
use crate::Package;
/// A vector of tuples containing a `dyn Backend` and a vector of unmanaged packages
/// A vector of tuples containing a Backends 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>)>);
pub struct ToDoPerBackend(Vec<(AnyBackend, Vec<Package>)>);
impl ToDoPerBackend {
pub(crate) fn new() -> Self {
pub fn new() -> Self {
Self(vec![])
}
pub(crate) fn push(&mut self, item: (Box<dyn Backend>, Vec<Package>)) {
pub fn push(&mut self, item: (AnyBackend, 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>)> {
pub fn iter(&self) -> impl Iterator<Item = &(AnyBackend, Vec<Package>)> {
self.0.iter()
}
pub(crate) fn nothing_to_do_for_all_backends(&self) -> bool {
pub 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<()> {
pub 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)
backend
.install_packages(packages, noconfirm)
.with_context(|| format!("installing packages for {}", backend.get_section()))?;
}
Ok(())
}
pub(crate) fn remove_unmanaged_packages(&self, noconfirm: bool) -> Result<()> {
pub 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)
backend
.remove_packages(packages, noconfirm)
.with_context(|| format!("removing packages for {}", backend.get_section()))?;
}
Ok(())
}
pub(crate) fn show(&self) -> Result<()> {
pub fn show(&self) -> Result<()> {
let mut parts = vec![];
for (backend, packages) in self.iter() {
@@ -91,3 +88,18 @@ impl ToDoPerBackend {
Ok(())
}
}
impl Default for ToDoPerBackend {
fn default() -> Self {
Self::new()
}
}
impl IntoIterator for ToDoPerBackend {
type Item = (AnyBackend, Vec<Package>);
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
+43 -51
View File
@@ -7,8 +7,12 @@ 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::args::datastructure::{
Arguments, Edit, Force, GroupAction, Groups, Noconfirm, OutputDir, PackageAction, Regex,
};
use crate::backend::backend_trait::Backend;
use crate::backend::todo_per_backend::ToDoPerBackend;
use crate::backend::AnyBackend;
use crate::cmd::run_edit_command;
use crate::env::should_print_debug_info;
use crate::path::{binary_in_path, get_absolutized_file_paths, get_group_dir};
@@ -21,7 +25,7 @@ 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>,
args: Option<Arguments>,
/// The config of the program.
config: Config,
/// The hashset of all groups.
@@ -32,7 +36,7 @@ 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 {
pub const fn new(args: Arguments, config: Config, groups: HashSet<Group>) -> Self {
Self {
args: Some(args),
config,
@@ -52,47 +56,46 @@ impl Pacdef {
/// # 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()),
Arguments::Group(group_args) => self.run_group_subcommand(&group_args),
Arguments::Package(package_args) => self.run_package_subcommand(&package_args),
Arguments::Version => {
self.show_version();
Ok(())
}
}
}
fn run_group_subcommand(self, args: &args::GroupAction) -> Result<()> {
use args::GroupAction::*;
fn run_group_subcommand(self, args: &GroupAction) -> Result<()> {
match args {
Edit(args::Groups(groups)) => self.edit_groups(groups),
Export(args::Groups(groups), args::OutputDir(dir), args::Force(force)) => {
GroupAction::Edit(Groups(groups)) => self.edit_groups(groups),
GroupAction::Export(Groups(groups), OutputDir(dir), 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),
GroupAction::Import(Groups(groups)) => self.import_groups(groups),
GroupAction::List => self.show_groups(),
GroupAction::New(Groups(groups), Edit(edit)) => self.new_groups(groups, *edit),
GroupAction::Remove(Groups(groups)) => self.remove_groups(groups),
GroupAction::Show(Groups(groups)) => self.show_group_content(groups),
}
}
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)) => {
PackageAction::Clean(Noconfirm(noconfirm)) => self.clean_packages(*noconfirm),
PackageAction::Review => review::review(self.get_unmanaged_packages()?, self.groups),
PackageAction::Search(Regex(regex)) => {
self.warn_about_groups_that_arent_symlinks();
search::search_packages(regex, &self.groups)
}
Sync(args::Noconfirm(noconfirm)) => self.install_packages(*noconfirm),
Unmanaged => self.show_unmanaged_packages(),
PackageAction::Sync(Noconfirm(noconfirm)) => self.install_packages(*noconfirm),
PackageAction::Unmanaged => self.show_unmanaged_packages(),
}
}
@@ -103,7 +106,7 @@ impl Pacdef {
eprintln!("WARNING: no group files found");
}
for mut backend in Backends::iter() {
for mut backend in AnyBackend::iter() {
if self
.config
.disabled_backends
@@ -116,39 +119,33 @@ impl Pacdef {
continue;
}
self.overwrite_values_from_config(&mut *backend);
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),
Err(error) => show_backend_query_error(&error, &backend),
};
}
Ok(to_install)
}
fn overwrite_values_from_config(&mut self, backend: &mut dyn Backend) {
fn overwrite_values_from_config(&mut self, backend: &mut AnyBackend) {
#[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 AnyBackend::Arch(arch) = backend {
arch.binary.clone_from(&self.config.aur_helper);
arch.aur_rm_args.clone_from(&self.config.aur_rm_args);
}
}
if let Some(flatpak) = backend
.as_any_mut()
.downcast_mut::<crate::backend::Flatpak>()
{
if let AnyBackend::Flatpak(flatpak) = backend {
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();
if let AnyBackend::Python(python) = backend {
python.binary.clone_from(&self.config.pip_binary);
}
}
@@ -204,7 +201,6 @@ impl Pacdef {
Ok(())
}
#[allow(clippy::unused_self)]
fn show_version(self) {
println!("{}", get_name_and_version());
}
@@ -237,7 +233,7 @@ impl Pacdef {
let mut result = ToDoPerBackend::new();
for mut backend in Backends::iter() {
for mut backend in AnyBackend::iter() {
if self
.config
.disabled_backends
@@ -250,12 +246,12 @@ impl Pacdef {
continue;
}
self.overwrite_values_from_config(&mut *backend);
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),
Err(error) => show_backend_query_error(&error, &backend),
};
}
Ok(result)
@@ -265,7 +261,6 @@ impl Pacdef {
///
/// 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();
@@ -354,7 +349,6 @@ impl Pacdef {
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()?;
@@ -410,7 +404,6 @@ impl Pacdef {
/// - a group with the same name already exists,
/// - the editor cannot be run, or
/// - if we do not have permission to write to the group dir.
#[allow(clippy::unused_self)]
fn new_groups(&self, new_groups: &[String], edit: bool) -> Result<()> {
let group_path = get_group_dir()?;
@@ -585,8 +578,7 @@ fn find_groups_by_name<'a>(names: &[String], groups: &'a HashSet<Group>) -> Resu
/// 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) {
fn show_backend_query_error(error: &anyhow::Error, backend: &AnyBackend) {
let section = backend.get_section();
if should_print_debug_info() {
eprintln!("WARNING: skipping backend '{section}':");
@@ -628,7 +620,7 @@ pub const fn get_version_string() -> &'static str {
/// 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() {
for backend in AnyBackend::iter() {
result.push(backend.get_section());
}
result.sort_unstable();
+5 -5
View File
@@ -18,13 +18,13 @@ use super::{Package, Section};
pub struct Group {
/// Name of the group (file name from which it was read, relative to the group
/// base dir).
pub(crate) name: String,
pub name: String,
/// The sections in the file which in turn hold the packages.
pub(crate) sections: HashSet<Section>,
pub sections: HashSet<Section>,
/// The absolute path of the original file.
pub(crate) path: PathBuf,
pub path: PathBuf,
/// Whether the main program should warn this group being loaded from a symlink.
pub(crate) warn_symlink: bool,
pub warn_symlink: bool,
}
impl Group {
@@ -177,7 +177,7 @@ impl Group {
///
/// 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<()> {
pub 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))?;
+1
View File
@@ -8,6 +8,7 @@ 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;
+6 -5
View File
@@ -5,8 +5,10 @@ use std::hash::Hash;
/// optionally a `repo`.
#[derive(Debug, Eq, PartialOrd, Ord, Clone)]
pub struct Package {
pub(crate) name: String,
pub(crate) repo: Option<String>,
/// The name of the package
pub name: String,
/// Optionally, which repository the package belongs to
pub repo: Option<String>,
}
fn remove_comment_and_trim_whitespace(s: &str) -> &str {
@@ -51,7 +53,7 @@ impl Package {
/// 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>
pub fn try_from<S>(s: S) -> Option<Self>
where
S: AsRef<str>,
{
@@ -116,11 +118,10 @@ mod tests {
assert_eq!(repo, None);
}
#[allow(clippy::unwrap_used)]
#[test]
fn from() {
let x = "myrepo/somepackage # ".to_string();
let p = Package::try_from(x).unwrap();
let p = Package::try_from(x).expect("this should be a valid package line");
assert_eq!(p.name, "somepackage");
assert_eq!(p.repo, Some("myrepo".to_string()));
}
+2 -4
View File
@@ -14,13 +14,11 @@ pub struct Section {
}
impl Section {
pub(crate) fn new(name: String, packages: HashSet<Package>) -> Self {
pub 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> {
pub 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();
+3 -7
View File
@@ -1,6 +1,4 @@
/*!
This library contains all logic that happens in `pacdef` under the hood.
*/
//! This library contains all logic that happens in `pacdef`.
#![warn(
clippy::as_conversions,
@@ -23,7 +21,7 @@ This library contains all logic that happens in `pacdef` under the hood.
)]
mod args;
mod backend;
pub(crate) mod backend;
mod cmd;
mod config;
mod core;
@@ -40,6 +38,4 @@ 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;
pub use crate::grouping::Package;
+1 -3
View File
@@ -1,6 +1,4 @@
/*!
Main program for `pacdef`.
*/
//! Main program for `pacdef`.
#![warn(
clippy::as_conversions,
+10 -6
View File
@@ -28,7 +28,7 @@ pub fn get_group_dir() -> Result<PathBuf> {
/// # Errors
///
/// This function will return an error if `$XDG_CONFIG_HOME` cannot be determined.
pub(crate) fn get_pacdef_base_dir() -> Result<PathBuf> {
pub fn get_pacdef_base_dir() -> Result<PathBuf> {
let mut dir = get_xdg_config_home().context("getting XDG_CONFIG_HOME")?;
dir.push("pacdef");
Ok(dir)
@@ -40,7 +40,7 @@ pub(crate) fn get_pacdef_base_dir() -> Result<PathBuf> {
///
/// 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> {
pub fn get_cargo_home() -> Result<PathBuf> {
if let Ok(config) = env::var("CARGO_HOME") {
Ok(config.into())
} else {
@@ -72,7 +72,7 @@ fn get_xdg_config_home() -> Result<PathBuf> {
/// # Errors
///
/// This function will return an error if the `$HOME` variable is not set.
pub(crate) fn get_home_dir() -> Result<PathBuf> {
pub fn get_home_dir() -> Result<PathBuf> {
Ok(env::var("HOME").context("getting $HOME variable")?.into())
}
@@ -106,7 +106,7 @@ pub fn get_config_path_old_version() -> Result<PathBuf> {
/// # Errors
///
/// This function returns an error if `$PATH` is not set.
pub(crate) fn binary_in_path(name: &str) -> Result<bool> {
pub 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);
@@ -123,7 +123,7 @@ pub(crate) fn binary_in_path(name: &str) -> Result<bool> {
///
/// 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
pub fn get_relative_path<P>(full_path: P, base_path: P) -> PathBuf
where
P: AsRef<Path>,
{
@@ -138,7 +138,11 @@ where
}
/// For each file argument, return the absolute path to the file.
pub(crate) fn get_absolutized_file_paths(arg_match: &[String]) -> Result<Vec<PathBuf>> {
///
/// # Errors
///
/// Returns an error if any of the files cannot be absolutized.
pub fn get_absolutized_file_paths(arg_match: &[String]) -> Result<Vec<PathBuf>> {
let mut result = vec![];
for item in arg_match {
+5 -6
View File
@@ -1,7 +1,6 @@
use std::rc::Rc;
use crate::backend::Backend;
use crate::{Group, Package};
use crate::{backend::AnyBackend, Group, Package};
use super::strategy::Strategy;
@@ -26,7 +25,7 @@ pub(super) enum ReviewIntention {
#[derive(Debug)]
pub(super) struct ReviewsPerBackend {
items: Vec<(Box<dyn Backend>, Vec<ReviewAction>)>,
items: Vec<(AnyBackend, Vec<ReviewAction>)>,
}
impl ReviewsPerBackend {
@@ -38,7 +37,7 @@ impl ReviewsPerBackend {
self.items.iter().all(|(_, vec)| vec.is_empty())
}
pub(super) fn push(&mut self, value: (Box<dyn Backend>, Vec<ReviewAction>)) {
pub(super) fn push(&mut self, value: (AnyBackend, Vec<ReviewAction>)) {
self.items.push(value);
}
@@ -77,9 +76,9 @@ impl ReviewsPerBackend {
}
impl IntoIterator for ReviewsPerBackend {
type Item = (Box<dyn Backend>, Vec<ReviewAction>);
type Item = (AnyBackend, Vec<ReviewAction>);
type IntoIter = std::vec::IntoIter<(Box<dyn Backend>, Vec<ReviewAction>)>;
type IntoIter = std::vec::IntoIter<(AnyBackend, Vec<ReviewAction>)>;
fn into_iter(self) -> Self::IntoIter {
self.items.into_iter()
+5 -5
View File
@@ -6,7 +6,8 @@ use std::rc::Rc;
use anyhow::Result;
use crate::backend::{Backend, ToDoPerBackend};
use crate::backend::backend_trait::Backend;
use crate::backend::todo_per_backend::ToDoPerBackend;
use crate::ui::{get_user_confirmation, read_single_char_from_terminal};
use crate::{Group, Package};
@@ -27,11 +28,11 @@ pub fn review(
return Ok(());
}
'outer: for (backend, packages) in todo_per_backend.into_iter() {
'outer: for (backend, packages) in todo_per_backend {
let mut actions = vec![];
for package in packages {
println!("{}: {package}", backend.get_section());
match get_action_for_package(package, &groups, &mut actions, &*backend)? {
match get_action_for_package(package, &groups, &mut actions, &backend)? {
ContinueWithReview::Yes => continue,
ContinueWithReview::No => return Ok(()),
ContinueWithReview::NoAndApply => {
@@ -161,9 +162,8 @@ fn print_enumerated_groups(groups: &[Rc<Group>]) {
}
}
#[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
number.to_string().len()
}
fn ask_group(groups: &[Rc<Group>]) -> Result<Option<Rc<Group>>> {
+6 -4
View File
@@ -2,12 +2,14 @@ use std::rc::Rc;
use anyhow::Result;
use crate::backend::Backend;
use crate::{Group, Package};
use crate::{
backend::{backend_trait::Backend, AnyBackend},
Group, Package,
};
#[derive(Debug)]
pub(super) struct Strategy {
backend: Box<dyn Backend>,
backend: AnyBackend,
delete: Vec<Package>,
as_dependency: Vec<Package>,
assign_group: Vec<(Package, Rc<Group>)>,
@@ -15,7 +17,7 @@ pub(super) struct Strategy {
impl Strategy {
pub(super) fn new(
backend: Box<dyn Backend>,
backend: AnyBackend,
delete: Vec<Package>,
as_dependency: Vec<Package>,
assign_group: Vec<(Package, Rc<Group>)>,
+2 -2
View File
@@ -63,8 +63,8 @@ fn print_triples(mut vec: Vec<(&Group, &Section, &Package)>) {
}
fn save_group_and_section_name(g0: &mut String, g: &Group, s0: &mut String, s: &Section) {
*g0 = g.name.clone();
*s0 = s.name.clone();
g0.clone_from(&g.name);
s0.clone_from(&s.name);
}
fn print_separator_unless_exhausted(
-19
View File
@@ -1,19 +0,0 @@
[package]
name = "pacdef_macros"
description = "procedural macros for pacdef"
version = "1.0.1"
edition.workspace = true
license.workspace = true
repository.workspace = true
readme.workspace = true
keywords.workspace = true
categories.workspace = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = "2.0"
[lib]
proc-macro = true
-34
View File
@@ -1,34 +0,0 @@
/*!
Procedural macros for `pacdef`.
*/
#![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 register;
use proc_macro::TokenStream;
/// Derive (1) an iterator over the variants, (2) imports for the individual backends, and (3)
/// instantiationn code for each backend.
#[proc_macro_derive(Register)]
pub fn register(input: TokenStream) -> TokenStream {
register::register(input)
}
-111
View File
@@ -1,111 +0,0 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, __private::TokenStream2};
pub fn register(input: TokenStream) -> TokenStream {
let input = syn::parse::<DeriveInput>(input).expect("I don't know when this could fail");
let name = &input.ident;
let syn::Data::Enum(enum_data) = &input.data else {
panic!("`Register` can only be used on enums");
};
let first_variant = &enum_data.variants[0].ident;
let variant_matches_backend = generate_variant_backend(enum_data);
let variant_matches_next = generate_variant_matches_next(enum_data);
let variant_imports = generate_variant_imports(enum_data);
let expanded = compile_output(
name,
first_variant,
variant_matches_backend,
variant_matches_next,
variant_imports,
);
TokenStream::from(expanded)
}
fn compile_output<T, U, V>(
name: &syn::Ident,
first_variant: &syn::Ident,
variant_backend: T,
variant_next: U,
variant_imports: V,
) -> TokenStream2
where
T: Iterator<Item = TokenStream2>,
U: Iterator<Item = TokenStream2>,
V: Iterator<Item = TokenStream2>,
{
let expanded = quote! {
#(#variant_imports)*
impl #name {
pub fn iter() -> BackendIter {
BackendIter {
next: Some(Self::#first_variant),
}
}
fn get_backend(&self) -> Box<dyn Backend> {
match self {
#(#variant_backend)*
}
}
fn next(&self) -> Option<Self> {
match self {
#(#variant_next)*
}
}
}
};
expanded
}
fn generate_variant_imports(enum_data: &syn::DataEnum) -> impl Iterator<Item = TokenStream2> + '_ {
let variant_imports = enum_data.variants.iter().map(|variant| {
let variant_name = &variant.ident;
let variant_module = proc_macro2::Ident::new(
&variant_name.to_string().to_lowercase(),
proc_macro2::Span::call_site(),
);
quote! {
pub use actual::#variant_module::#variant_name;
}
});
variant_imports
}
fn generate_variant_matches_next(
enum_data: &syn::DataEnum,
) -> impl Iterator<Item = TokenStream2> + '_ {
let variant_matches_next = enum_data.variants.iter().enumerate().map(|(i, variant)| {
let variant_name = &variant.ident;
if i == enum_data.variants.len() - 1 {
quote! {
Self::#variant_name => None,
}
} else {
let next_variant = &enum_data.variants[i + 1].ident;
quote! {
Self::#variant_name => Some(Self::#next_variant),
}
}
});
variant_matches_next
}
fn generate_variant_backend(enum_data: &syn::DataEnum) -> impl Iterator<Item = TokenStream2> + '_ {
let variant_matches_backend = enum_data.variants.iter().map(|variant| {
let variant_name = &variant.ident;
quote! {
Self::#variant_name => Box::new(#variant_name::new()),
}
});
variant_matches_backend
}