diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c02ab90f83..be8e2ef490 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -303,7 +303,7 @@ jobs: toolchain: stable override: true - uses: Swatinem/rust-cache@v2 - - run: apk add -U build-base + - run: apk add -U build-base tar - name: Build run: cargo build --verbose - name: Run unit tests @@ -313,7 +313,7 @@ jobs: - name: Run heavy tests run: cargo test apk --verbose -- --ignored - pip-conda-test: + pkcon-pip-conda-test: runs-on: ubuntu-latest needs: skip-check if: ${{ needs.skip-check.outputs.should_skip != 'true' }} @@ -325,15 +325,18 @@ jobs: toolchain: stable override: true - uses: Swatinem/rust-cache@v2 + - run: | + sudo apt update + sudo apt install -y packagekit packagekit-tools - name: Build run: cargo build --verbose - # - name: Run unit tests - # run: cargo test tests --verbose - name: Run smoke tests run: | + cargo test pkcon --verbose cargo test pip --verbose cargo test conda --verbose - name: Run heavy tests run: | + cargo test pkcon --verbose -- --ignored cargo test pip --verbose -- --ignored cargo test conda --verbose -- --ignored diff --git a/.vscode/settings.json b/.vscode/settings.json index 4638ae0a2e..04a1e66452 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -38,6 +38,7 @@ "olegtarasov", "pacaptr", "phinx", + "pkcon", "Pkgng", "pkgver", "printf", diff --git a/README.md b/README.md index 04d92af6f2..b77c56687a 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ That's why I decided to take inspiration from the existing `sh`-based [icy/pacap - Windows: `scoop`, [`choco`](#choco), `winget` - macOS: [`brew`](#brew), `port`, `apt` (through [Procursus]) - Linux: `apt`, `apk`, `dnf`, `emerge`, `xbps`, `zypper` -- External: `brew`, `conda`, `pip`/`pip3`, `tlmgr` +- External: `brew`, `conda`, `pip`/`pip3`, `pkcon`, `tlmgr` - These are only available with the [`pacaptr --using `](#--using---pm) syntax. As for now, the precedence is still (unfortunately) hard-coded. For example, if both `scoop` and `choco` are installed, `scoop` will be the default. You can however edit the default package manager in your [config](#configuration). diff --git a/src/dispatch.rs b/src/dispatch.rs index b1b04fe3c4..5882ca83b4 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -36,8 +36,8 @@ fn detect_pm_str<'s>() -> &'s str { _ if cfg!(target_os = "linux") => &[ ("apk", "/sbin/apk"), ("apt", "/usr/bin/apt"), - ("emerge", "/usr/bin/emerge"), ("dnf", "/usr/bin/dnf"), + ("emerge", "/usr/bin/emerge"), ("xbps-install", "/usr/bin/xbps-install"), ("zypper", "/usr/bin/zypper"), ], @@ -56,7 +56,7 @@ impl From for BoxPm<'_> { /// current `Config`. fn from(mut cfg: Config) -> Self { use crate::pm::{ - Apk, Apt, Brew, Choco, Conda, Dnf, Emerge, Pip, Pm, Port, Scoop, Tlmgr, Unknown, + Apk, Apt, Brew, Choco, Conda, Dnf, Emerge, Pip, Pkcon, Pm, Port, Scoop, Tlmgr, Unknown, Winget, Xbps, Zypper, }; @@ -107,6 +107,9 @@ impl From for BoxPm<'_> { // Pip "pip" | "pip3" => Pip::new(cfg).boxed(), + // PackageKit + "pkcon" => Pkcon::new(cfg).boxed(), + // Tlmgr "tlmgr" => Tlmgr::new(cfg).boxed(), diff --git a/src/pm.rs b/src/pm.rs index 58e9c091bc..600cb58873 100644 --- a/src/pm.rs +++ b/src/pm.rs @@ -23,6 +23,7 @@ pm_mods! { dnf; emerge; pip; + pkcon; port; scoop; tlmgr; @@ -128,7 +129,7 @@ macro_rules! methods { /// Sii displays packages which require X to be installed, aka reverse dependencies. async fn sii; - /// Sl displays a list of all packages in all installation sources that are handled by the packages management. + /// Sl displays a list of all packages in all installation sources that are handled by the package management. async fn sl; /// Ss searches for package(s) by searching the expression in name, description, short description. diff --git a/src/pm/apk.rs b/src/pm/apk.rs index feeec78dbc..80527ce502 100644 --- a/src/pm/apk.rs +++ b/src/pm/apk.rs @@ -179,7 +179,7 @@ impl Pm for Apk { } /// Sl displays a list of all packages in all installation sources that are - /// handled by the packages management. + /// handled by the package management. async fn sl(&self, kws: &[&str], flags: &[&str]) -> Result<()> { self.run(Cmd::new(["apk", "search"]).kws(kws).flags(flags)) .await diff --git a/src/pm/dnf.rs b/src/pm/dnf.rs index 8bac254eb6..305703e18d 100644 --- a/src/pm/dnf.rs +++ b/src/pm/dnf.rs @@ -88,12 +88,12 @@ impl Pm for Dnf { /// Qi displays local package information: name, version, description, etc. async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { - stream::iter(&[ - &["dnf", "info", "--installed"], - &["dnf", "repoquery", "--deplist"], + stream::iter([ + ["dnf", "info", "--installed"], + ["dnf", "repoquery", "--deplist"], ]) .map(Ok) - .try_for_each(|&cmd| self.run(Cmd::new(cmd).kws(kws).flags(flags))) + .try_for_each(|cmd| self.run(Cmd::new(cmd).kws(kws).flags(flags))) .await } @@ -219,7 +219,7 @@ impl Pm for Dnf { } /// Sl displays a list of all packages in all installation sources that are - /// handled by the packages management. + /// handled by the package management. async fn sl(&self, kws: &[&str], flags: &[&str]) -> Result<()> { Cmd::new(["dnf", "list", "--available"]) .kws(kws) diff --git a/src/pm/pkcon.rs b/src/pm/pkcon.rs new file mode 100644 index 0000000000..0efb653107 --- /dev/null +++ b/src/pm/pkcon.rs @@ -0,0 +1,210 @@ +#![doc = docs_self!()] + +use async_trait::async_trait; +use futures::prelude::*; +use indoc::indoc; +use once_cell::sync::Lazy; +use tap::prelude::*; + +use super::{Pm, PmHelper, PmMode, PromptStrategy, Strategy}; +use crate::{dispatch::Config, error::Result, exec::Cmd}; + +macro_rules! docs_self { + () => { + indoc! {" + The [PackageKit Console Client](https://www.freedesktop.org/software/PackageKit). + "} + }; +} + +#[doc = docs_self!()] +#[derive(Debug)] +pub struct Pkcon { + cfg: Config, +} + +static STRAT_PROMPT: Lazy = Lazy::new(|| Strategy { + prompt: PromptStrategy::native_no_confirm(["-y"]), + ..Strategy::default() +}); + +impl Pkcon { + #[must_use] + #[allow(missing_docs)] + pub const fn new(cfg: Config) -> Self { + Self { cfg } + } +} + +#[async_trait] +impl Pm for Pkcon { + /// Gets the name of the package manager. + fn name(&self) -> &str { + "pkcon" + } + + fn cfg(&self) -> &Config { + &self.cfg + } + + /// Q generates a list of installed packages. + async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + if kws.is_empty() { + Cmd::new(["pkcon", "get-packages", "--filter", "installed"]) + .kws(kws) + .flags(flags) + .pipe(|cmd| self.run(cmd)) + .await + } else { + self.qs(kws, flags).await + } + } + + /// Qc shows the changelog of a package. + async fn qc(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + Cmd::new(["pkcon", "get-update-detail"]) + .kws(kws) + .flags(flags) + .pipe(|cmd| self.run(cmd)) + .await + } + + /// Qi displays local package information: name, version, description, etc. + async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + self.si(kws, flags).await + } + + /// Qii displays local packages which require X to be installed, aka local + /// reverse dependencies. + async fn qii(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + self.sii(kws, flags).await + } + + /// Ql displays files provided by local package. + async fn ql(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + self.run(Cmd::new(["pkcon", "get-files"]).kws(kws).flags(flags)) + .await + } + + /// Qo queries the package which provides FILE. + async fn qo(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + self.run(Cmd::new(["pkcon", "what-provides"]).kws(kws).flags(flags)) + .await + } + + /// Qs searches locally installed package for names or descriptions. + // According to https://www.archlinux.org/pacman/pacman.8.html#_query_options_apply_to_em_q_em_a_id_qo_a, + // when including multiple search terms, only packages with descriptions + // matching ALL of those terms are returned. + async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + Cmd::new(["pkcon", "get-packages", "--filter", "installed"]) + .flags(flags) + .pipe(|cmd| self.search_regex(cmd, kws)) + .await + } + + /// Qu lists packages which have an update available. + async fn qu(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + Cmd::with_sudo(["pkcon", "get-updates"]) + .kws(kws) + .flags(flags) + .pipe(|cmd| self.run(cmd)) + .await + } + + /// R removes a single package, leaving all of its dependencies installed. + async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + stream::iter(kws) + .map(Ok) + .try_for_each(|kw| { + Cmd::with_sudo(["pkcon", "remove"]) + .kws([kw]) + .flags(flags) + .pipe(|cmd| self.run_with(cmd, PmMode::default(), &STRAT_PROMPT)) + }) + .await + } + + /// Rs removes a package and its dependencies which are not required by any + /// other installed package, and not explicitly installed by the user. + async fn rs(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + stream::iter(kws) + .map(Ok) + .try_for_each(|kw| { + Cmd::with_sudo(["pkcon", "remove", "--autoremove"]) + .kws([kw]) + .flags(flags) + .pipe(|cmd| self.run_with(cmd, PmMode::default(), &STRAT_PROMPT)) + }) + .await + } + + /// S installs one or more packages by name. + async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + Cmd::with_sudo(if self.cfg.needed { + &["pkcon", "install"][..] + } else { + &["pkcon", "install", "--allow-reinstall"][..] + }) + .kws(kws) + .flags(flags) + .pipe(|cmd| self.run_with(cmd, PmMode::default(), &STRAT_PROMPT)) + .await + } + + /// Si displays remote package information: name, version, description, etc. + async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + self.run(Cmd::new(["pkcon", "get-details"]).kws(kws).flags(flags)) + .await + } + + /// Sii displays packages which require X to be installed, aka reverse + /// dependencies. + async fn sii(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + self.run(Cmd::new(["pkcon", "required-by"]).kws(kws).flags(flags)) + .await + } + + /// Ss searches for package(s) by searching the expression in name, + /// description, short description. + async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + self.run(Cmd::new(["pkcon", "search", "name"]).kws(kws).flags(flags)) + .await + } + + /// Su updates outdated packages. + async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + Cmd::with_sudo(["pkcon", "update"]) + .kws(kws) + .flags(flags) + .pipe(|cmd| self.run_with(cmd, PmMode::default(), &STRAT_PROMPT)) + .await + } + + /// Suy refreshes the local package database, then updates outdated + /// packages. + async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + self.sy(&[], flags).await?; + self.su(kws, flags).await + } + + /// Sw retrieves all packages from the server, but does not install/upgrade + /// anything. + async fn sw(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + Cmd::with_sudo(["pkcon", "install", "--only-download"]) + .kws(kws) + .flags(flags) + .pipe(|cmd| self.run_with(cmd, PmMode::default(), &STRAT_PROMPT)) + .await + } + + /// Sy refreshes the local package database. + async fn sy(&self, kws: &[&str], flags: &[&str]) -> Result<()> { + self.run(Cmd::with_sudo(["pkcon", "refresh"]).flags(flags)) + .await?; + if !kws.is_empty() { + self.s(kws, flags).await?; + } + Ok(()) + } +} diff --git a/src/pm/tlmgr.rs b/src/pm/tlmgr.rs index d010d970dd..779848f8ad 100644 --- a/src/pm/tlmgr.rs +++ b/src/pm/tlmgr.rs @@ -100,7 +100,7 @@ impl Pm for Tlmgr { } /// Sl displays a list of all packages in all installation sources that are - /// handled by the packages management. + /// handled by the package management. async fn sl(&self, _kws: &[&str], flags: &[&str]) -> Result<()> { self.run(Cmd::new(["tlmgr", "info"]).flags(flags)).await } diff --git a/src/pm/zypper.rs b/src/pm/zypper.rs index 90a728f285..3d46565e04 100644 --- a/src/pm/zypper.rs +++ b/src/pm/zypper.rs @@ -212,7 +212,7 @@ impl Pm for Zypper { } /// Sl displays a list of all packages in all installation sources that are - /// handled by the packages management. + /// handled by the package management. async fn sl(&self, kws: &[&str], flags: &[&str]) -> Result<()> { let cmd = &["zypper", "packages", "-R"]; if kws.is_empty() { diff --git a/tests/pkcon.rs b/tests/pkcon.rs new file mode 100644 index 0000000000..be9049c89c --- /dev/null +++ b/tests/pkcon.rs @@ -0,0 +1,55 @@ +#![cfg(unix)] + +mod common; +use common::*; + +#[test] +#[should_panic(expected = "Failed with pattern `^Package: wget$`")] +fn pkcon_fail() { + test_dsl! { r##" + in --using pkcon -Si fish + ou ^Package: wget$ + "## } +} + +#[test] +fn pkcon_q() { + test_dsl! { r##" + in --using pkcon -Q + ou apt + "## } +} + +#[test] +fn pkcon_qs() { + test_dsl! { r##" + in --using pkcon -Qs apt + ou Installed + "## } +} + +#[test] +#[ignore] +fn pkcon_r_s() { + test_dsl! { r##" + # Update package databases + in --using pkcon -Sy + + # Now installation + in --using pkcon -S fish --yes + in ! which fish + ou /bin/fish + + # Now remove the package + in --using pkcon -R fish --yes + ou Finished + "## } +} + +#[test] +fn pkcon_si() { + test_dsl! { r##" + in --using pkcon -Si wget + ou retrieves files from the web + "## } +}