#![allow(clippy::module_name_repetitions)]
mod apk;
mod apt;
mod brew;
mod choco;
mod conda;
mod dnf;
mod emerge;
mod pip;
mod pkcon;
mod port;
mod scoop;
mod tlmgr;
mod unknown;
mod winget;
mod xbps;
mod zypper;
use std::env;
use async_trait::async_trait;
use itertools::Itertools;
use macro_rules_attribute::macro_rules_attribute;
use tt_call::tt_call;
use self::{
apk::Apk, apt::Apt, brew::Brew, choco::Choco, conda::Conda, dnf::Dnf, emerge::Emerge, pip::Pip,
pkcon::Pkcon, port::Port, scoop::Scoop, tlmgr::Tlmgr, unknown::Unknown, winget::Winget,
xbps::Xbps, zypper::Zypper,
};
use crate::{
config::Config,
error::Result,
exec::{self, Cmd, Mode, Output, is_exe},
print::{println_quoted, prompt},
};
#[macro_export]
#[doc(hidden)]
macro_rules! methods {
($caller:tt) => {
tt_call::tt_return! {
$caller
methods = [{
async fn q;
async fn qc;
async fn qe;
async fn qi;
async fn qii;
async fn qk;
async fn ql;
async fn qm;
async fn qo;
async fn qp;
async fn qs;
async fn qu;
async fn r;
async fn rn;
async fn rns;
async fn rs;
async fn rss;
async fn s;
async fn sc;
async fn scc;
async fn sccc;
async fn sg;
async fn si;
async fn sii;
async fn sl;
async fn ss;
async fn su;
async fn suy;
async fn sw;
async fn sy;
async fn u;
}]
}
};
}
macro_rules! make_op_body {
($self:ident, $method:ident) => {{
Err(crate::error::Error::OperationUnimplementedError {
op: stringify!($method).into(),
pm: $self.name().into(),
})
}};
}
macro_rules! _decor_pm {(
def = [{
$( #[$meta0:meta] )*
$vis:vis trait $t:ident $(: $supert:ident)? {
$( $inner:tt )*
}
}]
methods = [{ $(
$( #[$meta1:meta] )*
async fn $method:ident;
)* }]
) => {
$( #[$meta0] )*
$vis trait $t $(: $supert)? {
$( $inner )*
$( $( #[$meta1] )*
async fn $method(&self, _kws: &[&str], _flags: &[&str]) -> Result<()> {
make_op_body!(self, $method)
} )*
}
};}
macro_rules! decor_pm {
( $( $def:tt )* ) => {
tt_call! {
macro = [{ methods }]
~~> _decor_pm! {
def = [{ $( $def )* }]
}
}
};
}
#[macro_rules_attribute(decor_pm!)]
#[async_trait]
pub trait Pm: Sync {
fn name(&self) -> &str;
fn cfg(&self) -> &Config;
fn boxed<'a>(self) -> BoxPm<'a>
where
Self: Sized + Send + 'a,
{
Box::new(self)
}
}
pub type BoxPm<'a> = Box<dyn Pm + Send + 'a>;
impl From<Config> for BoxPm<'_> {
fn from(mut cfg: Config) -> Self {
let pm = cfg.default_pm.get_or_insert_with(|| detect_pm_str().into());
#[allow(clippy::match_single_binding)]
match pm.as_ref() {
"choco" => Choco::new(cfg).boxed(),
"scoop" => Scoop::new(cfg).boxed(),
"winget" => Winget::new(cfg).boxed(),
"brew" => Brew::new(cfg).boxed(),
"port" if cfg!(target_os = "macos") => Port::new(cfg).boxed(),
"apt" | "pkg" => Apt::new(cfg).boxed(),
"apk" => Apk::new(cfg).boxed(),
"dnf" => Dnf::new(cfg).boxed(),
"emerge" => Emerge::new(cfg).boxed(),
"xbps" | "xbps-install" => Xbps::new(cfg).boxed(),
"zypper" => Zypper::new(cfg).boxed(),
"conda" => Conda::new(cfg).boxed(),
"pip" | "pip3" => Pip::new(cfg).boxed(),
"pkcon" => Pkcon::new(cfg).boxed(),
"tlmgr" => Tlmgr::new(cfg).boxed(),
#[cfg(feature = "test")]
"mockpm" => {
use self::tests::MockPm;
MockPm { cfg }.boxed()
}
x => Unknown::new(x).boxed(),
}
}
}
#[must_use]
fn detect_pm_str() -> &'static str {
fn is_termux_apt() -> bool {
env::var("TERMUX_APP_PACKAGE_MANAGER").as_deref() == Ok("apt")
|| env::var("TERMUX_MAIN_PACKAGE_FORMAT").as_deref() == Ok("debian")
}
let pairs: &[(&str, &str)] = match () {
() if cfg!(windows) => &[("scoop", ""), ("choco", ""), ("winget", "")],
() if cfg!(target_os = "macos") => &[
("brew", "/usr/local/bin/brew"),
("port", "/opt/local/bin/port"),
("apt", "/opt/procursus/bin/apt"),
],
() if cfg!(target_os = "ios") => &[("apt", "/usr/bin/apt")],
() if cfg!(target_os = "linux") => &[
("apk", "/sbin/apk"),
("apt", "/usr/bin/apt"),
("dnf", "/usr/bin/dnf"),
("emerge", "/usr/bin/emerge"),
("xbps-install", "/usr/bin/xbps-install"),
("zypper", "/usr/bin/zypper"),
],
() => &[],
};
pairs
.iter()
.find_map(|&(name, path)| is_exe(name, path).then_some(name))
.map_or("unknown", |name| {
if name == "apt" && is_termux_apt() {
return "pkg";
}
name
})
}
#[async_trait]
pub trait PmHelper: Pm {
async fn check_output(&self, mut cmd: Cmd, mode: PmMode, strat: &Strategy) -> Result<Output> {
async fn run(cfg: &Config, cmd: &Cmd, mode: PmMode, strat: &Strategy) -> Result<Output> {
let mut curr_cmd = cmd.clone();
let no_confirm = cfg.no_confirm;
if cfg.no_cache
&& let NoCacheStrategy::WithFlags(v) = &strat.no_cache
{
curr_cmd.flags.extend(v.clone());
}
match &strat.prompt {
PromptStrategy::None => curr_cmd.exec(mode.into()).await,
PromptStrategy::CustomPrompt if no_confirm => curr_cmd.exec(mode.into()).await,
PromptStrategy::CustomPrompt => curr_cmd.exec(Mode::Prompt).await,
PromptStrategy::NativeNoConfirm(v) => {
if no_confirm {
curr_cmd.flags.extend(v.clone());
}
curr_cmd.exec(mode.into()).await
}
PromptStrategy::NativeConfirm(v) => {
if !no_confirm {
curr_cmd.flags.extend(v.clone());
}
curr_cmd.exec(mode.into()).await
}
}
}
let cfg = self.cfg();
let res = match &strat.dry_run {
DryRunStrategy::PrintCmd if cfg.dry_run => cmd.clone().exec(Mode::PrintCmd).await?,
DryRunStrategy::WithFlags(v) if cfg.dry_run => {
cmd.flags.extend(v.clone());
cmd = cmd.sudo(false);
run(cfg, &cmd, mode, strat).await?
}
_ => run(cfg, &cmd, mode, strat).await?,
};
if cfg.no_cache {
let flags = cmd.flags.iter().map(AsRef::as_ref).collect_vec();
match &strat.no_cache {
NoCacheStrategy::Sc => self.sc(&[], &flags).await?,
NoCacheStrategy::Scc => self.scc(&[], &flags).await?,
NoCacheStrategy::Sccc => self.sccc(&[], &flags).await?,
_ => (),
}
}
Ok(res)
}
fn default_mode(&self) -> PmMode {
let quiet = self.cfg().quiet();
PmMode::CheckErr { quiet }
}
async fn run_with(&self, cmd: Cmd, mode: PmMode, strat: &Strategy) -> Result<()> {
self.check_output(cmd, mode, strat).await.map(|_| ())
}
async fn run(&self, cmd: Cmd) -> Result<()> {
self.run_with(cmd, self.default_mode(), &Strategy::default())
.await
}
async fn search_regex(&self, cmd: Cmd, patterns: &[&str]) -> Result<()> {
self.search_regex_with_header(cmd, patterns, 0).await
}
async fn search_regex_with_header(
&self,
cmd: Cmd,
patterns: &[&str],
header_lines: usize,
) -> Result<()> {
let cfg = self.cfg();
if !(cfg.dry_run || cfg.quiet()) {
println_quoted(&*prompt::RUNNING, &cmd);
}
let out_bytes = self
.check_output(cmd, PmMode::Mute, &Strategy::default())
.await?;
exec::grep_print_with_header(&String::from_utf8(out_bytes)?, patterns, header_lines)
}
}
impl<P: Pm> PmHelper for P {}
#[derive(Copy, Clone, Debug)]
pub enum PmMode {
Mute,
#[allow(dead_code)]
CheckAll {
quiet: bool,
},
CheckErr {
quiet: bool,
},
}
impl From<PmMode> for Mode {
fn from(pm_mode: PmMode) -> Self {
match pm_mode {
PmMode::Mute => Self::Mute,
PmMode::CheckAll { quiet } => Self::CheckAll { quiet },
PmMode::CheckErr { quiet } => Self::CheckErr { quiet },
}
}
}
#[derive(Clone, Debug, Default)]
#[must_use]
pub struct Strategy {
dry_run: DryRunStrategy,
prompt: PromptStrategy,
no_cache: NoCacheStrategy,
}
#[must_use]
#[derive(Debug, Clone, Default)]
pub enum DryRunStrategy {
#[default]
PrintCmd,
WithFlags(Vec<String>),
}
impl DryRunStrategy {
pub fn with_flags(flags: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
Self::WithFlags(flags.into_iter().map(|s| s.as_ref().into()).collect())
}
}
#[must_use]
#[derive(Debug, Clone, Default)]
pub enum PromptStrategy {
#[default]
None,
CustomPrompt,
NativeNoConfirm(Vec<String>),
NativeConfirm(Vec<String>),
}
impl PromptStrategy {
pub fn native_no_confirm(no_confirm: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
Self::NativeNoConfirm(no_confirm.into_iter().map(|s| s.as_ref().into()).collect())
}
pub fn native_confirm(confirm: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
Self::NativeConfirm(confirm.into_iter().map(|s| s.as_ref().into()).collect())
}
}
#[must_use]
#[derive(Debug, Clone, Default)]
pub enum NoCacheStrategy {
#[default]
None,
#[allow(dead_code)]
Sc,
Scc,
Sccc,
WithFlags(Vec<String>),
}
impl NoCacheStrategy {
pub fn with_flags(flags: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
Self::WithFlags(flags.into_iter().map(|s| s.as_ref().into()).collect())
}
}
#[allow(missing_docs)]
#[cfg(feature = "test")]
pub mod tests {
use async_trait::async_trait;
use tt_call::tt_call;
use super::*;
use crate::config::Config;
#[derive(Debug)]
pub struct MockPm {
pub cfg: Config,
}
macro_rules! make_mock_op_body {
($self:ident, $kws:ident, $flags:ident, $method:ident) => {{
let kws: Vec<_> = itertools::chain!($kws, $flags).collect();
panic!("should run: {} {:?}", stringify!($method), &kws)
}};
}
macro_rules! impl_pm_mock {(
methods = [{ $(
$( #[$meta:meta] )*
async fn $method:ident;
)* }]
) => {
#[async_trait]
impl Pm for MockPm {
fn name(&self) -> &str {
"mockpm"
}
fn cfg(&self) -> &Config {
&self.cfg
}
$( async fn $method(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
make_mock_op_body!(self, kws, flags, $method)
} )*
}
};}
tt_call! {
macro = [{ methods }]
~~> impl_pm_mock
}
}