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}