pacaptr/
pm.rs

1//! Mapping from [`pacman`] commands to various operations of specific package
2//! managers.
3//!
4//! [`pacman`]: https://wiki.archlinux.org/index.php/Pacman
5
6#![allow(clippy::module_name_repetitions)]
7
8macro_rules! pm_mods {
9    ( $( $vis:vis $mod:ident; )+ ) => {
10        $(
11            $vis mod $mod;
12            paste! { pub use self::$mod::[<$mod:camel>]; }
13        )+
14    }
15}
16
17pm_mods! {
18    apk;
19    apt;
20    brew;
21    choco;
22    conda;
23    dnf;
24    emerge;
25    pip;
26    pkcon;
27    port;
28    scoop;
29    tlmgr;
30    unknown;
31    winget;
32    xbps;
33    zypper;
34}
35
36use std::env;
37
38use async_trait::async_trait;
39use itertools::Itertools;
40use macro_rules_attribute::macro_rules_attribute;
41use paste::paste;
42use tt_call::tt_call;
43
44use crate::{
45    config::Config,
46    error::Result,
47    exec::{self, Cmd, Mode, Output, is_exe},
48    print::{println_quoted, prompt},
49};
50
51/// The list of [`pacman`](https://wiki.archlinux.org/index.php/Pacman) methods supported by [`pacaptr`](crate).
52#[macro_export]
53#[doc(hidden)]
54macro_rules! methods {
55    ($caller:tt) => {
56        tt_call::tt_return! {
57            $caller
58            methods = [{
59                /// Q generates a list of installed packages.
60                async fn q;
61
62                /// Qc shows the changelog of a package.
63                async fn qc;
64
65                /// Qe lists packages installed explicitly (not as dependencies).
66                async fn qe;
67
68                /// Qi displays local package information: name, version, description, etc.
69                async fn qi;
70
71                /// Qii displays local packages which require X to be installed, aka local reverse dependencies.
72                async fn qii;
73
74                /// Qk verifies one or more packages.
75                async fn qk;
76
77                /// Ql displays files provided by local package.
78                async fn ql;
79
80                /// Qm lists packages that are installed but are not available in any installation source (anymore).
81                async fn qm;
82
83                /// Qo queries the package which provides FILE.
84                async fn qo;
85
86                /// Qp queries a package supplied through a file supplied on the command line rather than an entry in the package management database.
87                async fn qp;
88
89                /// Qs searches locally installed package for names or descriptions.
90                async fn qs;
91
92                /// Qu lists packages which have an update available.
93                async fn qu;
94
95                /// R removes a single package, leaving all of its dependencies installed.
96                async fn r;
97
98                /// Rn removes a package and skips the generation of configuration backup files.
99                async fn rn;
100
101                /// Rns removes a package and its dependencies which are not required by any other installed package,
102                /// and skips the generation of configuration backup files.
103                async fn rns;
104
105                /// Rs removes a package and its dependencies which are not required by any other installed package,
106                /// and not explicitly installed by the user.
107                async fn rs;
108
109                /// Rss removes a package and its dependencies which are not required by any other installed package.
110                async fn rss;
111
112                /// S installs one or more packages by name.
113                async fn s;
114
115                /// Sc removes all the cached packages that are not currently installed, and the unused sync database.
116                async fn sc;
117
118                /// Scc removes all files from the cache.
119                async fn scc;
120
121                /// Sccc performs a deeper cleaning of the cache than `Scc` (if applicable).
122                async fn sccc;
123
124                /// Sg lists all packages belonging to the GROUP.
125                async fn sg;
126
127                /// Si displays remote package information: name, version, description, etc.
128                async fn si;
129
130                /// Sii displays packages which require X to be installed, aka reverse dependencies.
131                async fn sii;
132
133                /// Sl displays a list of all packages in all installation sources that are handled by the package management.
134                async fn sl;
135
136                /// Ss searches for package(s) by searching the expression in name, description, short description.
137                async fn ss;
138
139                /// Su updates outdated packages.
140                async fn su;
141
142                /// Suy refreshes the local package database, then updates outdated packages.
143                async fn suy;
144
145                /// Sw retrieves all packages from the server, but does not install/upgrade anything.
146                async fn sw;
147
148                /// Sy refreshes the local package database.
149                async fn sy;
150
151                /// U upgrades or adds package(s) to the system and installs the required dependencies from sync repositories.
152                async fn u;
153            }]
154        }
155    };
156}
157
158macro_rules! make_op_body {
159    ($self:ident, $method:ident) => {{
160        Err(crate::error::Error::OperationUnimplementedError {
161            op: stringify!($method).into(),
162            pm: $self.name().into(),
163        })
164    }};
165}
166
167macro_rules! _decor_pm {(
168    def = [{
169        $( #[$meta0:meta] )*
170        $vis:vis trait $t:ident $(: $supert:ident)? {
171            $( $inner:tt )*
172        }
173    }]
174    methods = [{ $(
175        $( #[$meta1:meta] )*
176        async fn $method:ident;
177    )* }]
178) => {
179    $( #[$meta0] )*
180    $vis trait $t $(: $supert)? {
181        $( $inner )*
182
183        // * Automatically generated methods below... *
184        $( $( #[$meta1] )*
185        async fn $method(&self, _kws: &[&str], _flags: &[&str]) -> Result<()> {
186            make_op_body!(self, $method)
187        } )*
188    }
189};}
190
191/// Send `methods!()` to `_decor_pm`, that is:
192///
193/// ```txt
194/// _decor_pm! {
195///     def = [{ trait Pm { .. } }]
196///     methods = [{ q qc qe .. }] )
197/// }
198/// ```
199macro_rules! decor_pm {
200    ( $( $def:tt )* ) => {
201        tt_call! {
202            macro = [{ methods }]
203            ~~> _decor_pm! {
204                def = [{ $( $def )* }]
205            }
206        }
207    };
208}
209
210/// The feature set of a Package Manager defined by `pacman` commands.
211///
212/// For method explanation see:
213/// - <https://wiki.archlinux.org/index.php/Pacman>
214/// - <https://wiki.archlinux.org/index.php/Pacman/Rosetta>
215#[macro_rules_attribute(decor_pm!)]
216#[async_trait]
217pub trait Pm: Sync {
218    /// Gets the name of the package manager.
219    fn name(&self) -> &str;
220
221    /// Gets the config of the package manager.
222    fn cfg(&self) -> &Config;
223
224    /// Wraps the [`Pm`] instance in a [`Box`].
225    fn boxed<'a>(self) -> BoxPm<'a>
226    where
227        Self: Sized + Send + 'a,
228    {
229        Box::new(self)
230    }
231}
232
233/// An owned, dynamically typed [`Pm`].
234pub type BoxPm<'a> = Box<dyn Pm + Send + 'a>;
235
236impl From<Config> for BoxPm<'_> {
237    /// Generates the `Pm` instance according it's name, feeding it with the
238    /// current `Config`.
239    fn from(mut cfg: Config) -> Self {
240        // If the `Pm` to be used is not stated in any config,
241        // we should fall back to automatic detection and overwrite `cfg`.
242        let pm = cfg.default_pm.get_or_insert_with(|| detect_pm_str().into());
243
244        #[allow(clippy::match_single_binding)]
245        match pm.as_ref() {
246            // Chocolatey
247            "choco" => Choco::new(cfg).boxed(),
248
249            // Scoop
250            "scoop" => Scoop::new(cfg).boxed(),
251
252            // Winget
253            "winget" => Winget::new(cfg).boxed(),
254
255            // Homebrew/Linuxbrew
256            "brew" => Brew::new(cfg).boxed(),
257
258            // Macports
259            "port" if cfg!(target_os = "macos") => Port::new(cfg).boxed(),
260
261            // Apt for Debian/Ubuntu/Termux (newer versions)
262            "apt" | "pkg" => Apt::new(cfg).boxed(),
263
264            // Apk for Alpine
265            "apk" => Apk::new(cfg).boxed(),
266
267            // Dnf for RedHat
268            "dnf" => Dnf::new(cfg).boxed(),
269
270            // Portage for Gentoo
271            "emerge" => Emerge::new(cfg).boxed(),
272
273            // Xbps for Void Linux
274            "xbps" | "xbps-install" => Xbps::new(cfg).boxed(),
275
276            // Zypper for SUSE
277            "zypper" => Zypper::new(cfg).boxed(),
278
279            // -- External Package Managers --
280
281            // Conda
282            "conda" => Conda::new(cfg).boxed(),
283
284            // Pip
285            "pip" | "pip3" => Pip::new(cfg).boxed(),
286
287            // PackageKit
288            "pkcon" => Pkcon::new(cfg).boxed(),
289
290            // Tlmgr
291            "tlmgr" => Tlmgr::new(cfg).boxed(),
292
293            // Test-only mock package manager
294            #[cfg(feature = "test")]
295            "mockpm" => {
296                use self::tests::MockPm;
297                MockPm { cfg }.boxed()
298            }
299
300            // Unknown package manager X
301            x => Unknown::new(x).boxed(),
302        }
303    }
304}
305
306/// Detects the name of the package manager to be used in auto dispatch.
307#[must_use]
308fn detect_pm_str() -> &'static str {
309    /// Check if one of the following conditions are met:
310    /// - `$TERMUX_APP_PACKAGE_MANAGER` is `apt`;
311    /// - `$TERMUX_MAIN_PACKAGE_FORMAT` is `debian`.
312    ///
313    /// See: <https://github.com/rami3l/pacaptr/issues/576#issuecomment-1565122604>
314    fn is_termux_apt() -> bool {
315        env::var("TERMUX_APP_PACKAGE_MANAGER").as_deref() == Ok("apt")
316            || env::var("TERMUX_MAIN_PACKAGE_FORMAT").as_deref() == Ok("debian")
317    }
318
319    let pairs: &[(&str, &str)] = match () {
320        () if cfg!(windows) => &[("scoop", ""), ("choco", ""), ("winget", "")],
321
322        () if cfg!(target_os = "macos") => &[
323            ("brew", "/usr/local/bin/brew"),
324            ("port", "/opt/local/bin/port"),
325            ("apt", "/opt/procursus/bin/apt"),
326        ],
327
328        () if cfg!(target_os = "ios") => &[("apt", "/usr/bin/apt")],
329
330        () if cfg!(target_os = "linux") => &[
331            ("apk", "/sbin/apk"),
332            ("apt", "/usr/bin/apt"),
333            ("dnf", "/usr/bin/dnf"),
334            ("emerge", "/usr/bin/emerge"),
335            ("xbps-install", "/usr/bin/xbps-install"),
336            ("zypper", "/usr/bin/zypper"),
337        ],
338
339        () => &[],
340    };
341
342    pairs
343        .iter()
344        .find_map(|&(name, path)| is_exe(name, path).then_some(name))
345        .map_or("unknown", |name| {
346            if name == "apt" && is_termux_apt() {
347                return "pkg";
348            }
349            name
350        })
351}
352
353/// Extra implementation helper functions for [`Pm`],
354/// focusing on the ability to run commands ([`Cmd`]s) in a configured and
355/// [`Pm`]-specific context.
356#[async_trait]
357pub trait PmHelper: Pm {
358    /// Executes a command in the context of the [`Pm`] implementation. Returns
359    /// the [`Output`] of this command.
360    async fn check_output(&self, mut cmd: Cmd, mode: PmMode, strat: &Strategy) -> Result<Output> {
361        async fn run(cfg: &Config, cmd: &Cmd, mode: PmMode, strat: &Strategy) -> Result<Output> {
362            let mut curr_cmd = cmd.clone();
363            let no_confirm = cfg.no_confirm;
364            if cfg.no_cache {
365                if let NoCacheStrategy::WithFlags(v) = &strat.no_cache {
366                    curr_cmd.flags.extend(v.clone());
367                }
368            }
369            match &strat.prompt {
370                PromptStrategy::None => curr_cmd.exec(mode.into()).await,
371                PromptStrategy::CustomPrompt if no_confirm => curr_cmd.exec(mode.into()).await,
372                PromptStrategy::CustomPrompt => curr_cmd.exec(Mode::Prompt).await,
373                PromptStrategy::NativeNoConfirm(v) => {
374                    if no_confirm {
375                        curr_cmd.flags.extend(v.clone());
376                    }
377                    curr_cmd.exec(mode.into()).await
378                }
379                PromptStrategy::NativeConfirm(v) => {
380                    if !no_confirm {
381                        curr_cmd.flags.extend(v.clone());
382                    }
383                    curr_cmd.exec(mode.into()).await
384                }
385            }
386        }
387
388        let cfg = self.cfg();
389
390        // `--dry-run` should apply to both the main command and the cleanup.
391        let res = match &strat.dry_run {
392            DryRunStrategy::PrintCmd if cfg.dry_run => cmd.clone().exec(Mode::PrintCmd).await?,
393            DryRunStrategy::WithFlags(v) if cfg.dry_run => {
394                cmd.flags.extend(v.clone());
395                // -- A dry run with extra flags does not need `sudo`. --
396                cmd = cmd.sudo(false);
397                run(cfg, &cmd, mode, strat).await?
398            }
399            _ => run(cfg, &cmd, mode, strat).await?,
400        };
401
402        // Perform the cleanup.
403        if cfg.no_cache {
404            let flags = cmd.flags.iter().map(AsRef::as_ref).collect_vec();
405            match &strat.no_cache {
406                NoCacheStrategy::Sc => self.sc(&[], &flags).await?,
407                NoCacheStrategy::Scc => self.scc(&[], &flags).await?,
408                NoCacheStrategy::Sccc => self.sccc(&[], &flags).await?,
409                _ => (),
410            };
411        }
412
413        Ok(res)
414    }
415
416    /// Returns the default [`PmMode`] for this [`Pm`].
417    fn default_mode(&self) -> PmMode {
418        let quiet = self.cfg().quiet();
419        PmMode::CheckErr { quiet }
420    }
421
422    /// Executes a command in the context of the [`Pm`] implementation,
423    /// with custom [`PmMode`] and [`Strategy`].
424    async fn run_with(&self, cmd: Cmd, mode: PmMode, strat: &Strategy) -> Result<()> {
425        self.check_output(cmd, mode, strat).await.map(|_| ())
426    }
427
428    /// Executes a command in the context of the [`Pm`] implementation with
429    /// default settings.
430    async fn run(&self, cmd: Cmd) -> Result<()> {
431        self.run_with(cmd, self.default_mode(), &Strategy::default())
432            .await
433    }
434
435    /// Executes a command in [`PmMode::Mute`] and prints the output lines
436    /// that match against the given regex `patterns`.
437    async fn search_regex(&self, cmd: Cmd, patterns: &[&str]) -> Result<()> {
438        self.search_regex_with_header(cmd, patterns, 0).await
439    }
440
441    /// Executes a command in [`PmMode::Mute`] and prints `header_lines` of
442    /// header followed by the output lines that match against the given regex
443    /// `patterns`.
444    /// If `header_lines >= text.lines().count()`, then the
445    /// output lines are printed without changes.
446    async fn search_regex_with_header(
447        &self,
448        cmd: Cmd,
449        patterns: &[&str],
450        header_lines: usize,
451    ) -> Result<()> {
452        let cfg = self.cfg();
453        if !(cfg.dry_run || cfg.quiet()) {
454            println_quoted(&*prompt::RUNNING, &cmd);
455        }
456        let out_bytes = self
457            .check_output(cmd, PmMode::Mute, &Strategy::default())
458            .await?;
459        exec::grep_print_with_header(&String::from_utf8(out_bytes)?, patterns, header_lines)
460    }
461}
462
463impl<P: Pm> PmHelper for P {}
464
465/// Different ways in which a command shall be dealt with.
466///
467/// This is a [`Pm`] specified version intended to be used along with
468/// [`Strategy`].
469#[derive(Copy, Clone, Debug)]
470pub enum PmMode {
471    /// Silently collects all the `stdout`/`stderr` combined. Prints nothing.
472    Mute,
473
474    /// Prints out the command which should be executed, runs it and collects
475    /// its `stdout`/`stderr` combined.
476    ///
477    /// This is potentially dangerous as it destroys the colored `stdout`. Use
478    /// it only if really necessary.
479    #[allow(dead_code)]
480    CheckAll {
481        /// Whether the log output should be suppressed.
482        quiet: bool,
483    },
484
485    /// Prints out the command which should be executed, runs it and collects
486    /// its `stderr`.
487    ///
488    /// This will work with a colored `stdout`.
489    CheckErr {
490        /// Whether the log output should be suppressed.
491        quiet: bool,
492    },
493}
494
495impl From<PmMode> for Mode {
496    fn from(pm_mode: PmMode) -> Self {
497        match pm_mode {
498            PmMode::Mute => Self::Mute,
499            PmMode::CheckAll { quiet } => Self::CheckAll { quiet },
500            PmMode::CheckErr { quiet } => Self::CheckErr { quiet },
501        }
502    }
503}
504
505/// A set of intrinsic properties of a command in the context of a specific
506/// package manager, indicating how it is run.
507#[derive(Clone, Debug, Default)]
508#[must_use]
509pub struct Strategy {
510    /// How a dry run is dealt with.
511    dry_run: DryRunStrategy,
512
513    /// How the prompt is dealt with when running the package manager.
514    prompt: PromptStrategy,
515
516    /// How the cache is cleaned when `no_cache` is set to `true`.
517    no_cache: NoCacheStrategy,
518}
519
520/// How a dry run is dealt with.
521///
522/// Default value: [`DryRunStrategy::PrintCmd`].
523#[must_use]
524#[derive(Debug, Clone, Default)]
525pub enum DryRunStrategy {
526    /// Prints the command to be run, and stop.
527    #[default]
528    PrintCmd,
529    /// Invokes the corresponding package manager with the flags given.
530    WithFlags(Vec<String>),
531}
532
533impl DryRunStrategy {
534    /// Invokes the corresponding package manager with the flags given.
535    pub fn with_flags(flags: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
536        Self::WithFlags(flags.into_iter().map(|s| s.as_ref().into()).collect())
537    }
538}
539
540/// How the prompt is dealt with when running the package manager.
541///
542/// Default value: [`PromptStrategy::None`].
543#[must_use]
544#[derive(Debug, Clone, Default)]
545pub enum PromptStrategy {
546    /// There is no prompt.
547    #[default]
548    None,
549    /// There is no prompt, but a custom prompt is added.
550    CustomPrompt,
551    /// There is a native prompt provided by the package manager
552    /// that can be disabled with a flag.
553    NativeNoConfirm(Vec<String>),
554    /// There is a native prompt provided by the package manager
555    /// that can be enabled with a flag.
556    NativeConfirm(Vec<String>),
557}
558
559impl PromptStrategy {
560    /// There is a native prompt provided by the package manager
561    /// that can be disabled with a flag.
562    pub fn native_no_confirm(no_confirm: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
563        Self::NativeNoConfirm(no_confirm.into_iter().map(|s| s.as_ref().into()).collect())
564    }
565
566    /// There is a native prompt provided by the package manager
567    /// that can be enabled with a flag.
568    pub fn native_confirm(confirm: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
569        Self::NativeConfirm(confirm.into_iter().map(|s| s.as_ref().into()).collect())
570    }
571}
572
573/// How the cache is cleaned when `no_cache` is set to `true`.
574///
575/// Default value: [`PromptStrategy::None`].
576#[must_use]
577#[derive(Debug, Clone, Default)]
578pub enum NoCacheStrategy {
579    /// Does not clean cache.
580    /// This variant MUST be used when implementing cache cleaning methods like
581    /// `-Sc`.
582    #[default]
583    None,
584    /// Uses `-Sc` to clean the cache.
585    #[allow(dead_code)]
586    Sc,
587    /// Uses `-Scc`.
588    Scc,
589    /// Uses `-Sccc`.
590    Sccc,
591    /// Invokes the corresponding package manager with the flags given.
592    WithFlags(Vec<String>),
593}
594
595impl NoCacheStrategy {
596    /// Invokes the corresponding package manager with the flags given.
597    pub fn with_flags(flags: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
598        Self::WithFlags(flags.into_iter().map(|s| s.as_ref().into()).collect())
599    }
600}
601
602#[allow(missing_docs)]
603#[cfg(feature = "test")]
604pub mod tests {
605    use async_trait::async_trait;
606    use tt_call::tt_call;
607
608    use super::*;
609    use crate::config::Config;
610
611    #[derive(Debug)]
612    pub struct MockPm {
613        pub cfg: Config,
614    }
615
616    macro_rules! make_mock_op_body {
617        ($self:ident, $kws:ident, $flags:ident, $method:ident) => {{
618            let kws: Vec<_> = itertools::chain!($kws, $flags).collect();
619            panic!("should run: {} {:?}", stringify!($method), &kws)
620        }};
621    }
622
623    macro_rules! impl_pm_mock {(
624        methods = [{ $(
625            $( #[$meta:meta] )*
626            async fn $method:ident;
627        )* }]
628    ) => {
629        #[async_trait]
630        impl Pm for MockPm {
631            /// Gets the name of the package manager.
632            fn name(&self) -> &str {
633                "mockpm"
634            }
635
636            fn cfg(&self) -> &Config {
637                &self.cfg
638            }
639
640            // * Automatically generated methods below... *
641            $( async fn $method(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
642                    make_mock_op_body!(self, kws, flags, $method)
643            } )*
644        }
645    };}
646
647    tt_call! {
648        macro = [{ methods }]
649        ~~> impl_pm_mock
650    }
651}