mod dnf5;
mod packagekit;
use crate::capabilities::{render, View};
use crate::cli::{Cli, PackagesAction};
use crate::error::{FezError, Result};
use crate::protocol::client::BridgeClient;
#[derive(Clone, Copy)]
enum Mutation {
Install,
Remove,
Upgrade,
}
impl Mutation {
fn verb(self) -> &'static str {
match self {
Mutation::Install => "install",
Mutation::Remove => "remove",
Mutation::Upgrade => "upgrade",
}
}
fn method(self) -> &'static str {
match self {
Mutation::Install => "install",
Mutation::Remove => "remove",
Mutation::Upgrade => "upgrade",
}
}
}
#[derive(Clone, Copy)]
enum ReadAction<'a> {
List(ListFilters<'a>),
Info { spec: &'a str },
Search { pattern: &'a str },
CheckUpdate,
Repolist { filter: RepoFilter },
}
#[derive(Clone, Copy)]
struct ListFilters<'a> {
available: bool,
repos: &'a [String],
name: Option<&'a str>,
limit: Option<usize>,
offset: usize,
}
#[derive(Clone, Copy)]
enum RepoFilter {
Enabled,
Disabled,
All,
}
impl RepoFilter {
fn enable_disable(self) -> &'static str {
match self {
RepoFilter::Enabled => "enabled",
RepoFilter::Disabled => "disabled",
RepoFilter::All => "all",
}
}
fn accepts(self, enabled: bool) -> bool {
match self {
RepoFilter::Enabled => enabled,
RepoFilter::Disabled => !enabled,
RepoFilter::All => true,
}
}
}
enum Plan<'a> {
Read(ReadAction<'a>),
Mutate {
mutation: Mutation,
specs: Vec<String>,
},
}
fn classify(action: &PackagesAction) -> Plan<'_> {
match action {
PackagesAction::List {
installed: _installed,
available,
repo,
name,
limit,
offset,
} => Plan::Read(ReadAction::List(ListFilters {
available: *available,
repos: repo,
name: name.as_deref(),
limit: *limit,
offset: *offset,
})),
PackagesAction::Info { spec } => Plan::Read(ReadAction::Info { spec }),
PackagesAction::Search { pattern } => Plan::Read(ReadAction::Search { pattern }),
PackagesAction::CheckUpdate => Plan::Read(ReadAction::CheckUpdate),
PackagesAction::Repolist {
enabled: _enabled,
disabled,
all,
} => {
let filter = if *all {
RepoFilter::All
} else if *disabled {
RepoFilter::Disabled
} else {
RepoFilter::Enabled
};
Plan::Read(ReadAction::Repolist { filter })
}
PackagesAction::Install { specs } => Plan::Mutate {
mutation: Mutation::Install,
specs: specs.clone(),
},
PackagesAction::Remove { specs } => Plan::Mutate {
mutation: Mutation::Remove,
specs: specs.clone(),
},
PackagesAction::Upgrade { specs } => Plan::Mutate {
mutation: Mutation::Upgrade,
specs: specs.clone(),
},
}
}
pub fn dispatch(cli: &Cli, action: &PackagesAction) -> i32 {
let view = match classify(action) {
Plan::Read(read) => run_read(cli, read),
Plan::Mutate { mutation, specs } => run_mutation(cli, mutation, &specs),
};
render(cli, view)
}
fn run_read(cli: &Cli, action: ReadAction<'_>) -> Result<View> {
let mut client = crate::capabilities::connect(cli)?;
let host = client.host().to_string();
let result = dnf5::run_read(&mut client, &host, action);
if matches!(result, Err(FezError::DependencyMissing { .. })) {
read_via_packagekit(&mut client, host, action)
} else {
result
}
}
fn run_mutation(cli: &Cli, mutation: Mutation, specs: &[String]) -> Result<View> {
let host = cli.resolved_host();
let mut client = crate::capabilities::connect(cli)?;
match dnf5::run_mutation(cli, &mut client, mutation, specs, &host) {
Ok(view) => Ok(view),
Err(FezError::DependencyMissing { .. }) => {
mutate_via_packagekit(&mut client, mutation, specs, &host, cli.dry_run, cli.force)
}
Err(err) => Err(err),
}
}
fn from_pk(pk: packagekit::PkView, host: String) -> View {
View::new(pk.kind, host, pk.data, pk.human).with_hints_opt(pk.hints)
}
fn both_missing() -> FezError {
FezError::DependencyMissing {
component: "dnf5daemon or PackageKit".into(),
dbus_name: "org.rpm.dnf.v0 / org.freedesktop.PackageKit".into(),
remediation: "Install a package backend: dnf5daemon-server (Fedora) providing org.rpm.dnf.v0, or PackageKit providing org.freedesktop.PackageKit, then retry.".into(),
}
}
fn read_via_packagekit(
client: &mut BridgeClient,
host: String,
action: ReadAction<'_>,
) -> Result<View> {
let result = match action {
ReadAction::List(filters) => packagekit::list(
client,
filters.available,
filters.repos,
filters.name,
filters.limit,
filters.offset,
),
ReadAction::Info { spec } => packagekit::info(client, spec),
ReadAction::Search { pattern } => packagekit::search(client, pattern),
ReadAction::CheckUpdate => packagekit::check_update(client),
ReadAction::Repolist { filter } => {
packagekit::repolist(client, move |enabled| filter.accepts(enabled))
}
};
let view = crate::capabilities::map_service_unknown(result, both_missing)?;
Ok(from_pk(view, host))
}
fn mutate_via_packagekit(
client: &mut BridgeClient,
mutation: Mutation,
specs: &[String],
host: &str,
dry_run: bool,
force: bool,
) -> Result<View> {
let result = packagekit::mutate(client, mutation.verb(), specs, host, dry_run, force);
let view = crate::capabilities::map_service_unknown(result, both_missing)?;
Ok(from_pk(view, host.to_string()))
}
pub(crate) fn plan_kind(dry_run: bool) -> &'static str {
if dry_run {
"PackagePlan"
} else {
"PackageMutation"
}
}
pub(crate) fn plan_human(
verb: &str,
specs: &[String],
host: &str,
counts: (usize, usize, usize, usize),
dry_run: bool,
) -> String {
let (install, remove, upgrade, downgrade) = counts;
let specs = specs.join(" ");
if dry_run {
format!(
"DRY-RUN: {verb} {specs} on {host} would install {install}, remove {remove}, upgrade {upgrade}, downgrade {downgrade} package(s)\n"
)
} else {
format!(
"{verb} {specs} on {host}: installed {install}, removed {remove}, upgraded {upgrade}, downgraded {downgrade} package(s)\n"
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn repo_filter_maps_to_backend_and_predicate() {
assert_eq!(RepoFilter::Enabled.enable_disable(), "enabled");
assert_eq!(RepoFilter::Disabled.enable_disable(), "disabled");
assert_eq!(RepoFilter::All.enable_disable(), "all");
assert!(RepoFilter::Enabled.accepts(true));
assert!(!RepoFilter::Enabled.accepts(false));
assert!(!RepoFilter::Disabled.accepts(true));
assert!(RepoFilter::Disabled.accepts(false));
assert!(RepoFilter::All.accepts(true));
assert!(RepoFilter::All.accepts(false));
}
#[test]
fn classify_repolist_prefers_all_then_disabled_then_enabled() {
let all = PackagesAction::Repolist {
enabled: true,
disabled: true,
all: true,
};
let disabled = PackagesAction::Repolist {
enabled: false,
disabled: true,
all: false,
};
let enabled = PackagesAction::Repolist {
enabled: true,
disabled: false,
all: false,
};
assert!(matches!(
classify(&all),
Plan::Read(ReadAction::Repolist {
filter: RepoFilter::All
})
));
assert!(matches!(
classify(&disabled),
Plan::Read(ReadAction::Repolist {
filter: RepoFilter::Disabled
})
));
assert!(matches!(
classify(&enabled),
Plan::Read(ReadAction::Repolist {
filter: RepoFilter::Enabled
})
));
}
}