Merge pull request #29 from crystal-linux/development

Development
i18n
Michal 2 years ago committed by GitHub
commit 30ad30b87a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,9 +2,9 @@ name: Run checks and tests
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: [ main, develop, feature/gh-actions ] branches: [ main, development, feature/gh-actions ]
pull_request: pull_request:
branches: [ main, develop ] branches: [ main, development ]
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always

976
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,14 +1,13 @@
[package] [package]
name = "Amethyst" name = "Amethyst"
version = "3.6.0" version = "4.0.0"
authors = ["michal <michal@tar.black>", "axtlos <axtlos@tar.black>"] authors = ["michal <michal@tar.black>", "axtlos <axtlos@tar.black>", "trivernis <trivernis@protonmail.com>"]
edition = "2021" edition = "2021"
description = "A fast and efficient AUR helper" description = "A fast and efficient AUR helper"
repository = "https://github.com/crystal-linux/amethyst"
license-file = "LICENSE.md" license-file = "LICENSE.md"
default-run = "ame"
keywords = ["aur", "crystal-linux", "pacman", "aur-helper"] keywords = ["aur", "crystal-linux", "pacman", "aur-helper"]
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
default-run = "ame"
[[bin]] [[bin]]
name = "ame" name = "ame"
@ -20,19 +19,36 @@ debug = false
lto = "fat" lto = "fat"
codegen-units = 1 codegen-units = 1
[profile.dev]
opt-level = 0
[dependencies] [dependencies]
mimalloc = { version = "0.1.29", default-features = false } clap = { version = "3.2.17", features = [ "derive", "wrap_help" ] }
clap = { version = "3.2.8", features = [ "derive", "wrap_help" ] } regex = { version = "1.6.0", default-features = false, features = [ "std", "unicode-perl" ] }
clap_complete = "3.2.4"
regex = { version = "1.5.6", default-features = false, features = [ "std", "unicode-perl" ] }
colored = "2.0.0" colored = "2.0.0"
ureq = { version = "2.4.0", default-features = false, features = [ "native-tls", "json" ] }
serde = { version = "1.0.144", default-features = false, features = [ "derive", "serde_derive" ] } serde = { version = "1.0.144", default-features = false, features = [ "derive", "serde_derive" ] }
native-tls = { version = "0.2.10", default-features = false } native-tls = { version = "0.2.10", default-features = false }
libc = { version = "0.2.126", default-features = false } libc = { version = "0.2.132", default-features = false }
rm_rf = { version = "0.6.2", default-features = false } async-recursion = "1.0.0"
spinoff = { version = "0.5.2", default-features = false } aur-rpc = "0.1.3"
textwrap = { version = "0.15.0", features = [ "terminal_size", "smawk" ] } futures = "0.3.23"
chrono = { version = "0.4.22", default-features = false, features = [ "clock", "std", "wasmbind" ] } tracing = "0.1.36"
toml = { version = "0.5.9", default-features = false } tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
crossterm = { version = "0.25.0", default-features = false } textwrap = "0.15.0"
crossterm = "0.25.0"
toml = "0.5.9"
clap_complete = "3.2.4"
color-eyre = { version = "0.6.2", features = ["issue-url", "url"] }
indicatif = { version = "0.17.0", features = ["tokio"] }
lazy_static = "1.4.0"
parking_lot = { version = "0.12.1", features = ["deadlock_detection"] }
dialoguer = { version = "0.10.2", features = ["fuzzy-select"] }
lazy-regex = "2.3.0"
directories = "4.0.1"
console = "0.15.1"
tracing-error = "0.2.0"
fuzzy-matcher = "0.3.7"
[dependencies.tokio]
version = "1.21.0"
features = ["rt", "rt-multi-thread", "io-std", "io-util", "process", "time", "macros", "tracing", "fs"]

@ -0,0 +1,33 @@
ARG BASE_IMAGE=docker.io/archlinux:latest
FROM ${BASE_IMAGE} as build_base
RUN pacman -Syu --noconfirm
RUN pacman -S --noconfirm base-devel curl bash
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
FROM build_base as builder
WORKDIR /usr/src
RUN cargo new amethyst
WORKDIR /usr/src/amethyst
COPY Cargo.toml Cargo.lock ./
RUN mkdir target
RUN cargo fetch
COPY src ./src
RUN cargo build --frozen
RUN mkdir /tmp/ame
RUN cp target/debug/ame /tmp/ame/
FROM ${BASE_IMAGE} as runtime
RUN pacman -Syu --noconfirm
RUN pacman -S --noconfirm base-devel zsh wget vim git binutils fakeroot pacman-contrib sudo
RUN useradd -r -d /home/ame -p $(echo "ame" | openssl passwd -1 -stdin) ame -G wheel
RUN echo '%wheel ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
RUN mkdir /home/ame
RUN chown ame:ame /home/ame
COPY --from=builder /tmp/ame/ame /usr/bin/
RUN rm -f $(pacdiff -o -f)
USER ame
RUN mkdir -p /home/ame/.local/share
RUN touch /home/ame/.zshrc
ENV AME_LOG=debug,hyper=info,mio=info,want=info
ENTRYPOINT ["zsh"]

@ -0,0 +1,7 @@
#!/bin/bash
podman build . -t ame-debug
if [ $? -eq 0 ]; then
podman container exists ame-debug && podman container rm -f ame-debug
podman run -i -t --name ame-debug ame-debug
fi

@ -1,5 +1,6 @@
#![allow(clippy::module_name_repetitions)] #![allow(clippy::module_name_repetitions)]
use crate::operations::SearchBy;
use clap::{Parser, Subcommand, ValueHint}; use clap::{Parser, Subcommand, ValueHint};
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
@ -28,19 +29,19 @@ pub struct Args {
#[derive(Debug, Clone, Subcommand)] #[derive(Debug, Clone, Subcommand)]
pub enum Operation { pub enum Operation {
/// Installs a package from either the AUR or the Pacman-defined repositories /// Installs a package from either the AUR or the Pacman-defined repositories
#[clap(bin_name = "ame", name = "install", visible_aliases = & ["-S"], aliases = & ["-Sa", "-Sr"])] #[clap(bin_name = "ame", name = "install", visible_aliases = & ["-S", "i"], aliases = & ["-Sa", "-Sr"])]
Install(InstallArgs), Install(InstallArgs),
/// Removes a previously installed package /// Removes a previously installed package
#[clap(bin_name = "ame", name = "remove", visible_aliases = & ["rm", "-Rs"])] #[clap(bin_name = "ame", name = "remove", visible_aliases = & ["rm", "r", "-Rs"])]
Remove(RemoveArgs), Remove(RemoveArgs),
/// Searches for packages matching a regex-supported pattern in the AUR and/or the repos /// Searches for packages matching a regex-supported pattern in the AUR and/or the repos
#[clap(bin_name = "ame", name = "search", visible_aliases = & ["-Ss"], aliases = & ["-Ssa", "-Ssr"])] #[clap(bin_name = "ame", name = "search", visible_aliases = & ["-Ss", "s"], aliases = & ["-Ssa", "-Ssr"])]
Search(SearchArgs), Search(SearchArgs),
/// Queries installed packages /// Queries installed packages
#[clap(bin_name = "ame", name = "query", visible_aliases = & ["-Q"], aliases = & ["-Qa", "-Qr", "-Qm", "-Qn"])] #[clap(bin_name = "ame", name = "query", visible_aliases = & ["-Q", "q"], aliases = & ["-Qa", "-Qr", "-Qm", "-Qn"])]
Query(QueryArgs), Query(QueryArgs),
/// Gets info about a package /// Gets info about a package
@ -48,7 +49,7 @@ pub enum Operation {
Info(InfoArgs), Info(InfoArgs),
/// Upgrades locally installed packages to their latest versions (Default) /// Upgrades locally installed packages to their latest versions (Default)
#[clap(bin_name = "ame", name = "upgrade", visible_aliases = & ["-Syu"])] #[clap(bin_name = "ame", name = "upgrade", visible_aliases = & ["-Syu", "u"])]
Upgrade(UpgradeArgs), Upgrade(UpgradeArgs),
/// Generates shell completions for supported shells (bash, fish, elvish, pwsh) /// Generates shell completions for supported shells (bash, fish, elvish, pwsh)
@ -56,7 +57,7 @@ pub enum Operation {
GenComp(GenCompArgs), GenComp(GenCompArgs),
/// Removes all orphaned packages /// Removes all orphaned packages
#[clap(bin_name = "ame", name = "clean", visible_aliases = & ["-Sc"])] #[clap(bin_name = "ame", name = "clean", visible_aliases = & ["-Sc", "c"])]
Clean, Clean,
/// Runs pacdiff /// Runs pacdiff
@ -104,7 +105,11 @@ pub struct SearchArgs {
/// The string the package must match in the search /// The string the package must match in the search
#[clap(required = true)] #[clap(required = true)]
pub search: Vec<String>, pub search: String,
/// Searches by a specific field
#[clap(long, short)]
pub by: Option<SearchBy>,
} }
#[derive(Default, Debug, Clone, Parser)] #[derive(Default, Debug, Clone, Parser)]

@ -0,0 +1,69 @@
use std::path::{Path, PathBuf};
use crate::internal::{
commands::ShellCommand,
error::{AppError, AppResult},
};
#[derive(Debug, Default)]
pub struct GitCloneBuilder {
url: String,
directory: PathBuf,
}
impl GitCloneBuilder {
pub fn url<S: ToString>(mut self, url: S) -> Self {
self.url = url.to_string();
self
}
pub fn directory<P: AsRef<Path>>(mut self, path: P) -> Self {
self.directory = path.as_ref().into();
self
}
pub async fn clone(self) -> AppResult<()> {
let result = ShellCommand::git()
.arg("clone")
.arg(self.url)
.arg(self.directory)
.wait_with_output()
.await?;
if result.status.success() {
Ok(())
} else {
Err(AppError::Other(result.stderr))
}
}
}
#[derive(Debug, Default)]
pub struct GitPullBuilder {
directory: PathBuf,
}
impl GitPullBuilder {
pub fn directory<P: AsRef<Path>>(mut self, path: P) -> Self {
self.directory = path.as_ref().into();
self
}
pub async fn pull(self) -> AppResult<()> {
let result = ShellCommand::git()
.arg("-C")
.arg(self.directory)
.arg("pull")
.wait_with_output()
.await?;
if result.status.success() {
Ok(())
} else {
Err(AppError::Other(result.stderr))
}
}
}

@ -0,0 +1,155 @@
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use tokio::process::Child;
use crate::internal::{
commands::ShellCommand,
error::{AppError, AppResult},
};
#[derive(Default, Debug, Clone)]
pub struct MakePkgBuilder {
directory: PathBuf,
clean: bool,
no_deps: bool,
install: bool,
no_build: bool,
no_confirm: bool,
as_deps: bool,
skip_pgp: bool,
needed: bool,
no_prepare: bool,
force: bool,
}
impl MakePkgBuilder {
/// Sets the working directory
pub fn directory<D: AsRef<Path>>(mut self, dir: D) -> Self {
self.directory = dir.as_ref().into();
self
}
pub fn clean(mut self, clean: bool) -> Self {
self.clean = clean;
self
}
pub fn no_deps(mut self, no_deps: bool) -> Self {
self.no_deps = no_deps;
self
}
pub fn no_build(mut self, no_build: bool) -> Self {
self.no_build = no_build;
self
}
pub fn no_prepare(mut self, no_prepare: bool) -> Self {
self.no_prepare = no_prepare;
self
}
/// Mark packages as non-explicitly installed
#[allow(clippy::wrong_self_convention)]
pub fn as_deps(mut self, as_deps: bool) -> Self {
self.as_deps = as_deps;
self
}
/// Skip PGP signature checks
pub fn skip_pgp(mut self, skip: bool) -> Self {
self.skip_pgp = skip;
self
}
/// Do not reinstall up to date packages
pub fn needed(mut self, needed: bool) -> Self {
self.needed = needed;
self
}
pub fn force(mut self, force: bool) -> Self {
self.force = force;
self
}
pub async fn run(self) -> AppResult<()> {
let output = self.build().wait_with_output().await?;
if output.status.success() {
Ok(())
} else {
Err(AppError::MakePkg(output.stderr))
}
}
pub fn spawn(self) -> AppResult<Child> {
self.build().spawn(true)
}
/// Executes the makepkg command
#[tracing::instrument(level = "trace")]
fn build(self) -> ShellCommand {
let mut command = ShellCommand::makepkg().working_dir(self.directory);
if self.clean {
command = command.arg("-c");
}
if self.no_deps {
command = command.arg("-d")
}
if self.install {
command = command.arg("-c");
}
if self.no_build {
command = command.arg("-o");
}
if self.no_confirm {
command = command.arg("--noconfirm")
}
if self.as_deps {
command = command.arg("--asdeps")
}
if self.skip_pgp {
command = command.arg("--skippgp")
}
if self.needed {
command = command.arg("--needed");
}
if self.no_prepare {
command = command.arg("--noprepare")
}
if self.force {
command = command.arg("-f")
}
command
}
#[tracing::instrument(level = "trace")]
pub async fn package_list<D: AsRef<Path> + Debug>(dir: D) -> AppResult<Vec<PathBuf>> {
let result = ShellCommand::makepkg()
.working_dir(dir.as_ref())
.arg("--packagelist")
.wait_with_output()
.await?;
if result.status.success() {
let packages = result.stdout.lines().map(PathBuf::from).collect();
Ok(packages)
} else {
Err(AppError::Other(result.stderr))
}
}
}

@ -0,0 +1,4 @@
pub mod git;
pub mod makepkg;
pub mod pacman;
pub mod pager;

@ -0,0 +1,267 @@
use std::path::{Path, PathBuf};
use crate::internal::{commands::ShellCommand, error::AppResult, structs::Options};
#[derive(Debug, Default)]
pub struct PacmanInstallBuilder {
packages: Vec<String>,
files: Vec<PathBuf>,
as_deps: bool,
no_confirm: bool,
needed: bool,
}
impl PacmanInstallBuilder {
pub fn from_options(options: Options) -> Self {
Self::default()
.as_deps(options.asdeps)
.no_confirm(options.noconfirm)
}
pub fn packages<I: IntoIterator<Item = S>, S: ToString>(mut self, packages: I) -> Self {
let mut packages = packages.into_iter().map(|p| p.to_string()).collect();
self.packages.append(&mut packages);
self
}
pub fn files<I: IntoIterator<Item = T>, T: AsRef<Path>>(mut self, files: I) -> Self {
let mut files = files.into_iter().map(|f| f.as_ref().into()).collect();
self.files.append(&mut files);
self
}
pub fn no_confirm(mut self, no_confirm: bool) -> Self {
self.no_confirm = no_confirm;
self
}
#[allow(clippy::wrong_self_convention)]
pub fn as_deps(mut self, as_deps: bool) -> Self {
self.as_deps = as_deps;
self
}
pub fn needed(mut self, needed: bool) -> Self {
self.needed = needed;
self
}
#[tracing::instrument(level = "debug")]
pub async fn install(self) -> AppResult<()> {
let mut command = ShellCommand::pacman().elevated();
if !self.packages.is_empty() {
command = command.arg("-S");
} else if !self.files.is_empty() {
command = command.arg("-U");
}
if self.no_confirm {
command = command.arg("--noconfirm")
}
if self.as_deps {
command = command.arg("--asdeps")
}
if self.needed {
command = command.arg("--needed")
}
command
.args(self.packages)
.args(self.files)
.wait_success()
.await
}
}
#[derive(Debug)]
pub struct PacmanQueryBuilder {
query_type: PacmanQueryType,
color: PacmanColor,
packages: Vec<String>,
}
#[derive(Debug)]
enum PacmanQueryType {
Foreign,
All,
Info,
Native,
}
#[derive(Clone, Copy, Debug)]
pub enum PacmanColor {
#[allow(dead_code)]
Always,
Auto,
Never,
}
impl Default for PacmanColor {
fn default() -> Self {
Self::Auto
}
}
impl PacmanQueryBuilder {
fn new(query_type: PacmanQueryType) -> Self {
Self {
query_type,
color: PacmanColor::default(),
packages: Vec::new(),
}
}
pub fn all() -> Self {
Self::new(PacmanQueryType::All)
}
pub fn foreign() -> Self {
Self::new(PacmanQueryType::Foreign)
}
pub fn native() -> Self {
Self::new(PacmanQueryType::Native)
}
pub fn info() -> Self {
Self::new(PacmanQueryType::Info)
}
pub fn package(mut self, package: String) -> Self {
self.packages.push(package);
self
}
#[allow(dead_code)]
pub fn packages<I: IntoIterator<Item = String>>(mut self, packages: I) -> Self {
let mut packages = packages.into_iter().collect::<Vec<String>>();
self.packages.append(&mut packages);
self
}
pub fn color(mut self, color: PacmanColor) -> Self {
self.color = color;
self
}
#[tracing::instrument(level = "trace")]
pub async fn query(self) -> AppResult<()> {
self.build_command().wait_success().await
}
#[tracing::instrument(level = "trace")]
pub async fn query_with_output(self) -> AppResult<Vec<BasicPackageInfo>> {
let output = self.build_command().wait_with_output().await?;
let packages = output
.stdout
.split('\n')
.filter(|p| !p.is_empty())
.filter_map(|p| p.split_once(' '))
.map(|(name, version)| BasicPackageInfo {
name: name.to_string(),
version: version.to_string(),
})
.collect();
tracing::debug!("Query result: {packages:?}");
Ok(packages)
}
fn build_command(self) -> ShellCommand {
let mut command = ShellCommand::pacman().arg("-Q").arg("--color").arg("never");
command = match self.query_type {
PacmanQueryType::Foreign => command.arg("-m"),
PacmanQueryType::Info => command.arg("-i"),
PacmanQueryType::Native => command.arg("-n"),
PacmanQueryType::All => command,
};
command = command.arg("--color");
command = match self.color {
PacmanColor::Always => command.arg("always"),
PacmanColor::Auto => command.arg("auto"),
PacmanColor::Never => command.arg("never"),
};
command.args(self.packages)
}
}
#[derive(Clone, Debug)]
pub struct BasicPackageInfo {
pub name: String,
pub version: String,
}
#[derive(Default)]
pub struct PacmanSearchBuilder {
query: String,
}
impl PacmanSearchBuilder {
pub fn query<S: AsRef<str>>(mut self, query: S) -> Self {
if !self.query.is_empty() {
self.query.push(' ');
}
self.query.push_str(query.as_ref());
self
}
/// Searches and returns if the execution result was ok
pub async fn search(self) -> AppResult<bool> {
let result = self.build_command().wait_with_output().await?;
Ok(result.status.success())
}
fn build_command(self) -> ShellCommand {
ShellCommand::pacman().arg("-Ss").arg(self.query)
}
}
#[derive(Default, Debug, Clone)]
pub struct PacmanUninstallBuilder {
packages: Vec<String>,
no_confirm: bool,
}
impl PacmanUninstallBuilder {
pub fn packages<I: IntoIterator<Item = S>, S: ToString>(mut self, packages: I) -> Self {
let mut packages = packages.into_iter().map(|p| p.to_string()).collect();
self.packages.append(&mut packages);
self
}
pub fn no_confirm(mut self, no_confirm: bool) -> Self {
self.no_confirm = no_confirm;
self
}
#[tracing::instrument(level = "trace")]
pub async fn uninstall(self) -> AppResult<()> {
let mut command = ShellCommand::pacman()
.elevated()
.arg("-R")
.args(self.packages);
if self.no_confirm {
command = command.arg("--noconfirm");
}
command.wait_success().await
}
}

@ -0,0 +1,23 @@
use std::path::{Path, PathBuf};
use crate::{
internal::{commands::ShellCommand, error::AppResult},
with_suspended_output,
};
#[derive(Default)]
pub struct PagerBuilder {
path: PathBuf,
}
impl PagerBuilder {
pub fn path<P: AsRef<Path>>(mut self, path: P) -> Self {
self.path = path.as_ref().into();
self
}
pub async fn open(self) -> AppResult<()> {
with_suspended_output!({ ShellCommand::pager().arg(self.path).wait_success().await })
}
}

@ -0,0 +1,123 @@
#[macro_export]
/// Macro for prompting the user with a yes/no question.
macro_rules! prompt {
(default yes, $($arg:tt)+) => {
$crate::interact::Interact::interact($crate::interact::AmePrompt::new(format!($($arg)+)).default_yes())
};
(default no, $($arg:tt)+) => {
$crate::interact::Interact::interact($crate::interact::AmePrompt::new(format!($($arg)+)).default_no())
};
(no default, $($arg:tt)+) => {
$crate::interact::Interact::interact($crate::interact::AmePrompt::new(format!($($arg)+)))
}
}
#[macro_export]
/// Macro for prompting the user with a multi select
macro_rules! multi_select {
($items:expr, $($arg:tt)+) => {
$crate::interact::Interact::interact($crate::interact::AmeMultiSelect::new(format!($($arg)+)).items($items))
}
}
#[macro_export]
macro_rules! select_opt {
($items:expr, $($arg:tt)+) => {
$crate::interact::InteractOpt::interact_opt($crate::interact::AmeFuzzySelect::new(format!($($arg)+)).items($items))
};
}
#[macro_export]
/// Returns a singular or plural expression depending on the given len
/// Usage:
/// ```rust
/// let some_list = vec!["a", "b", "c"];
/// format!("The list has {}", numeric!(some_list.len(), "element"["s"]));
/// // result: The list has 3 elements
///
/// let some_other_list = vec!["a"];
/// format!("The list has {}", numeric!(some_other_list.len(), "element"["s"]));
/// // result: The list has 1 element
/// ```
macro_rules! numeric {
($len:expr, $sin:literal[$plu:literal]) => {
if $len == 1 {
format!("{} {}", $len, $sin)
} else {
format!("{} {}{}", $len, $sin, $plu)
}
};
($len:expr, $sin:literal or $plu:literal) => {
if $len == 1 {
format!("{} {}", $len, $sin)
} else {
format!("{} {}", $len, plu)
}
};
}
#[macro_export]
/// Creates a new multiprogress bar
macro_rules! multi_progress {
() => {
$crate::logging::get_logger().new_multi_progress();
};
}
#[macro_export]
/// Creates a new progress spinner
macro_rules! spinner {
() => {
$crate::logging::get_logger().new_progress_spinner()
};
($($arg:tt)+) => {
{
let spinner = $crate::spinner!();
spinner.set_message(format!($($arg)+));
spinner
}
}
}
#[macro_export]
/// Resets the output to normal text output (erases all progress bars and spinners)
macro_rules! normal_output {
() => {
$crate::logging::get_logger().reset_output_type();
};
}
#[macro_export]
/// Suspends the output so that nothing is being written to stdout/stderr
/// Returns a handle that unsuspend the output when it's dropped
macro_rules! suspend_output {
() => {
$crate::logging::get_logger().suspend()
};
}
#[macro_export]
/// Unsuspends the output and writes everything buffered to stdout/stderr
macro_rules! unsuspend_output {
() => {
$crate::logging::get_logger().unsuspend();
};
}
#[macro_export]
/// Suspend all output logging inside the given block
/// Note: This only works as long as the block itself doesn't unsuspend
/// the output
macro_rules! with_suspended_output {
($expr:block) => {{
let _handle = $crate::suspend_output!();
$expr
}};
}
#[macro_export]
macro_rules! newline {
() => {
$crate::logging::get_logger().print_newline();
};
}

@ -0,0 +1,19 @@
pub mod macros;
mod multi_select;
mod prompt;
mod select;
mod theme;
pub use multi_select::AmeMultiSelect;
pub use prompt::AmePrompt;
pub use select::AmeFuzzySelect;
pub trait Interact {
type Result;
fn interact(&mut self) -> Self::Result;
}
pub trait InteractOpt: Interact {
fn interact_opt(&mut self) -> Option<Self::Result>;
}

@ -0,0 +1,41 @@
use std::mem;
use crate::with_suspended_output;
use super::{theme::AmeTheme, Interact};
pub struct AmeMultiSelect {
prompt: String,
items: Vec<String>,
}
impl AmeMultiSelect {
/// Creates a new multi select prompt
pub fn new<S: ToString>(prompt: S) -> Self {
Self {
prompt: prompt.to_string(),
items: Vec::new(),
}
}
/// Adds/replaces the items of this multi select
pub fn items<I: IntoIterator<Item = S>, S: ToString>(&mut self, items: I) -> &mut Self {
self.items = items.into_iter().map(|i| i.to_string()).collect();
self
}
}
impl Interact for AmeMultiSelect {
type Result = Vec<usize>;
fn interact(&mut self) -> Self::Result {
with_suspended_output!({
dialoguer::MultiSelect::with_theme(AmeTheme::get())
.with_prompt(mem::take(&mut self.prompt))
.items(&self.items)
.interact()
.unwrap()
})
}
}

@ -0,0 +1,51 @@
use std::mem;
use crate::with_suspended_output;
use super::{theme::AmeTheme, Interact};
pub struct AmePrompt {
question: String,
default_yes: Option<bool>,
}
impl AmePrompt {
/// Creates a new prompt
pub fn new<Q: ToString>(question: Q) -> Self {
Self {
question: question.to_string(),
default_yes: None,
}
}
/// Sets the prompt to default to yes
pub fn default_yes(&mut self) -> &mut Self {
self.default_yes = Some(true);
self
}
/// Sets the prompt to default to yes
pub fn default_no(&mut self) -> &mut Self {
self.default_yes = Some(false);
self
}
}
impl Interact for AmePrompt {
type Result = bool;
fn interact(&mut self) -> Self::Result {
let mut dialog = dialoguer::Confirm::with_theme(AmeTheme::get());
if let Some(def) = self.default_yes.take() {
dialog.default(def);
}
dialog
.with_prompt(mem::take(&mut self.question))
.wait_for_newline(true);
with_suspended_output!({ dialog.interact().unwrap() })
}
}

@ -0,0 +1,51 @@
use std::mem;
use crate::with_suspended_output;
use super::{theme::AmeTheme, Interact, InteractOpt};
pub struct AmeFuzzySelect {
prompt: String,
items: Vec<String>,
}
impl AmeFuzzySelect {
/// Creates a new multi select prompt
pub fn new<S: ToString>(prompt: S) -> Self {
Self {
prompt: prompt.to_string(),
items: Vec::new(),
}
}
/// Adds/replaces the items of this multi select
pub fn items<I: IntoIterator<Item = S>, S: ToString>(&mut self, items: I) -> &mut Self {
self.items = items.into_iter().map(|i| i.to_string()).collect();
self
}
fn build(&mut self) -> dialoguer::FuzzySelect {
let mut select = dialoguer::FuzzySelect::with_theme(AmeTheme::get());
select
.with_prompt(mem::take(&mut self.prompt))
.items(&self.items)
.default(0);
select
}
}
impl Interact for AmeFuzzySelect {
type Result = usize;
fn interact(&mut self) -> Self::Result {
with_suspended_output!({ self.build().interact().unwrap() })
}
}
impl InteractOpt for AmeFuzzySelect {
fn interact_opt(&mut self) -> Option<Self::Result> {
with_suspended_output!({ self.build().interact_opt().unwrap() })
}
}

@ -0,0 +1,261 @@
use crossterm::style::Stylize;
use dialoguer::theme::Theme;
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use crate::internal::utils::wrap_text;
const ERR_SYMBOL: &str = "X";
const PROMPT_SYMBOL: &str = "?";
pub struct AmeTheme;
impl AmeTheme {
pub fn get() -> &'static Self {
static AME_THEME: AmeTheme = AmeTheme;
&AME_THEME
}
}
impl Theme for AmeTheme {
fn format_prompt(&self, f: &mut dyn std::fmt::Write, prompt: &str) -> std::fmt::Result {
let prompt = wrap_text(prompt).join("\n ");
write!(f, "{} {}:", PROMPT_SYMBOL.magenta(), prompt.bold())
}
fn format_error(&self, f: &mut dyn std::fmt::Write, err: &str) -> std::fmt::Result {
write!(f, "{} error: {}", ERR_SYMBOL.red(), err)
}
fn format_confirm_prompt(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
default: Option<bool>,
) -> std::fmt::Result {
let prompt = wrap_text(prompt).join("\n ");
if !prompt.is_empty() {
write!(f, "{} {} ", PROMPT_SYMBOL.magenta(), &prompt.bold())?;
}
match default {
None => write!(f, "[y/n] ")?,
Some(true) => write!(f, "[{}/n] ", "Y".bold())?,
Some(false) => write!(f, "[y/{}] ", "N".bold())?,
}
Ok(())
}
fn format_confirm_prompt_selection(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
selection: Option<bool>,
) -> std::fmt::Result {
let prompt = wrap_text(prompt).join("\n ");
let selection = selection.map(|b| if b { "yes" } else { "no" });
match selection {
Some(selection) if prompt.is_empty() => {
write!(f, "{}", selection.italic())
}
Some(selection) => {
write!(f, "{} {}", &prompt.bold(), selection.italic())
}
None if prompt.is_empty() => Ok(()),
None => {
write!(f, "{}", &prompt.bold())
}
}
}
fn format_input_prompt(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
default: Option<&str>,
) -> std::fmt::Result {
match default {
Some(default) if prompt.is_empty() => {
write!(f, "{} [{}]: ", PROMPT_SYMBOL.magenta(), default)
}
Some(default) => write!(
f,
"{} {} [{}]: ",
PROMPT_SYMBOL.magenta(),
prompt.bold(),
default
),
None => write!(f, "{} {}: ", PROMPT_SYMBOL.magenta(), prompt.bold()),
}
}
fn format_input_prompt_selection(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
sel: &str,
) -> std::fmt::Result {
write!(
f,
"{} {}: {}",
PROMPT_SYMBOL.magenta(),
prompt.bold(),
sel.italic()
)
}
fn format_password_prompt(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
) -> std::fmt::Result {
self.format_input_prompt(f, prompt, None)
}
fn format_password_prompt_selection(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
) -> std::fmt::Result {
self.format_input_prompt_selection(f, prompt, "[hidden]")
}
fn format_select_prompt(&self, f: &mut dyn std::fmt::Write, prompt: &str) -> std::fmt::Result {
self.format_prompt(f, prompt)
}
fn format_select_prompt_selection(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
sel: &str,
) -> std::fmt::Result {
self.format_input_prompt_selection(f, prompt, sel)
}
fn format_multi_select_prompt(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
) -> std::fmt::Result {
self.format_prompt(f, prompt)
}
fn format_sort_prompt(&self, f: &mut dyn std::fmt::Write, prompt: &str) -> std::fmt::Result {
self.format_prompt(f, prompt)
}
fn format_multi_select_prompt_selection(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
selections: &[&str],
) -> std::fmt::Result {
write!(f, "{}: ", prompt.bold())?;
if selections.is_empty() {
write!(f, "{}", "No selections".italic())?;
} else {
for (idx, sel) in selections.iter().enumerate() {
write!(f, "{}{}", if idx == 0 { "" } else { ", " }, sel)?;
}
}
Ok(())
}
fn format_sort_prompt_selection(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
selections: &[&str],
) -> std::fmt::Result {
self.format_multi_select_prompt_selection(f, prompt, selections)
}
fn format_select_prompt_item(
&self,
f: &mut dyn std::fmt::Write,
text: &str,
active: bool,
) -> std::fmt::Result {
write!(f, "{} {}", if active { ">" } else { " " }, text)
}
fn format_multi_select_prompt_item(
&self,
f: &mut dyn std::fmt::Write,
text: &str,
checked: bool,
active: bool,
) -> std::fmt::Result {
let active_symbol = if active { ">" } else { " " };
let checked_symbol = if checked { "x" } else { " " }.magenta();
write!(f, "{active_symbol} [{checked_symbol}] {text}")
}
fn format_sort_prompt_item(
&self,
f: &mut dyn std::fmt::Write,
text: &str,
picked: bool,
active: bool,
) -> std::fmt::Result {
write!(
f,
"{} {}",
match (picked, active) {
(true, true) => "> [x]",
(false, true) => "> [ ]",
(_, false) => " [ ]",
},
text
)
}
fn format_fuzzy_select_prompt(
&self,
f: &mut dyn std::fmt::Write,
prompt: &str,
search_term: &str,
cursor_pos: usize,
) -> std::fmt::Result {
if !prompt.is_empty() {
write!(f, "{} {} ", PROMPT_SYMBOL.magenta(), prompt.bold())?;
}
if cursor_pos < search_term.len() {
let st_head = search_term[0..cursor_pos].to_string();
let st_tail = search_term[cursor_pos..search_term.len()].to_string();
let st_cursor = "|".to_string();
write!(f, "{}{}{}", st_head, st_cursor, st_tail)
} else {
let cursor = "|".to_string();
write!(f, "{}{}", search_term, cursor)
}
}
fn format_fuzzy_select_prompt_item(
&self,
f: &mut dyn std::fmt::Write,
text: &str,
active: bool,
highlight_matches: bool,
matcher: &SkimMatcherV2,
search_term: &str,
) -> std::fmt::Result {
write!(f, "{} ", if active { ">" } else { " " }.magenta().bold())?;
if highlight_matches {
if let Some((_score, indices)) = matcher.fuzzy_indices(text, search_term) {
for (idx, c) in text.chars().into_iter().enumerate() {
if indices.contains(&idx) {
write!(f, "{}", c.bold())?;
} else {
write!(f, "{}", c)?;
}
}
return Ok(());
}
}
write!(f, "{}", text)
}
}

@ -1,27 +1,16 @@
use regex::Regex;
use crate::{log, Options};
/// Strips packages from versioning and other extraneous information. /// Strips packages from versioning and other extraneous information.
pub fn clean(a: &[String], options: Options) -> Vec<String> { pub fn clean(a: &[String]) -> Vec<String> {
// Strip versioning from package names // Strip versioning from package names
let r = Regex::new(r"(\S+)((?:>=|<=|>|<|=\W)\S+$)").unwrap(); let cleaned = a
let mut cleaned: Vec<String> = vec![]; .iter()
let verbosity = options.verbosity; .map(|name| {
name.split_once('=')
// Push cleaned package names to vector .map(|n| n.0.to_string())
for b in a { .unwrap_or_else(|| name.to_string())
if r.captures_iter(b).count() > 0 { })
let c = r.captures(b).unwrap().get(1).map_or("", |m| m.as_str()); .collect();
cleaned.push(c.to_string());
} else {
cleaned.push(b.to_string());
}
}
if verbosity >= 1 { tracing::debug!("Cleaned: {:?}\nInto: {:?}", a, cleaned);
log!("Cleaned: {:?}\nInto: {:?}", a, cleaned);
}
cleaned cleaned
} }

@ -1,8 +1,9 @@
use std::env;
use std::ffi::{OsStr, OsString}; use std::ffi::{OsStr, OsString};
use std::fs; use std::path::{Path, PathBuf};
use std::process::{Child, Command, ExitStatus, Stdio}; use std::process::{ExitStatus, Stdio};
use tokio::process::{Child, Command};
use crate::internal::config;
use crate::internal::error::{AppError, AppResult}; use crate::internal::error::{AppError, AppResult};
use crate::internal::is_tty; use crate::internal::is_tty;
@ -12,22 +13,18 @@ pub struct StringOutput {
pub status: ExitStatus, pub status: ExitStatus,
} }
/// A wrapper around [`std::process::Command`] with predefined /// A wrapper around [std::process::Command] with predefined
/// commands used in this project as well as elevated access. /// commands used in this project as well as elevated access.
pub struct ShellCommand { pub struct ShellCommand {
command: String, command: String,
args: Vec<OsString>, args: Vec<OsString>,
elevated: bool, elevated: bool,
working_dir: Option<PathBuf>,
} }
impl ShellCommand { impl ShellCommand {
pub fn pacman() -> Self { pub fn pacman() -> Self {
let config = config::read(); let pacman_cmd = Self::new("pacman");
let pacman_cmd = if config.base.powerpill && fs::metadata("/usr/bin/powerpill").is_ok() {
Self::new("powerpill")
} else {
Self::new("pacman")
};
if is_tty() { if is_tty() {
pacman_cmd.arg("--color=always") pacman_cmd.arg("--color=always")
@ -56,11 +53,22 @@ impl ShellCommand {
Self::new("sudo") Self::new("sudo")
} }
fn new(command: &str) -> Self { pub fn rm() -> Self {
Self::new("rm")
}
pub fn pager() -> Self {
let pager = env::var("PAGER").unwrap_or_else(|_| String::from("less"));
Self::new(pager)
}
fn new<S: ToString>(command: S) -> Self {
Self { Self {
command: command.to_string(), command: command.to_string(),
args: Vec::new(), args: Vec::new(),
elevated: false, elevated: false,
working_dir: None,
} }
} }
@ -83,16 +91,22 @@ impl ShellCommand {
self self
} }
pub fn working_dir<D: AsRef<Path>>(mut self, dir: D) -> Self {
self.working_dir = Some(dir.as_ref().into());
self
}
/// Runs the command with sudo /// Runs the command with sudo
pub const fn elevated(mut self) -> Self { pub fn elevated(mut self) -> Self {
self.elevated = true; self.elevated = true;
self self
} }
/// Waits for the child to exit but returns an error when it exists with a non-zero status code /// Waits for the child to exit but returns an error when it exists with a non-zero status code
pub fn wait_success(self) -> AppResult<()> { pub async fn wait_success(self) -> AppResult<()> {
let status = self.wait()?; let status = self.wait().await?;
if status.success() { if status.success() {
Ok(()) Ok(())
} else { } else {
@ -101,17 +115,17 @@ impl ShellCommand {
} }
/// Waits for the child to exit and returns the output status /// Waits for the child to exit and returns the output status
pub fn wait(self) -> AppResult<ExitStatus> { pub async fn wait(self) -> AppResult<ExitStatus> {
let mut child = self.spawn(false)?; let mut child = self.spawn(false)?;
child.wait().map_err(AppError::from) child.wait().await.map_err(AppError::from)
} }
/// Waits with output until the program completed and /// Waits with output until the program completed and
/// returns the string output object /// returns the string output object
pub fn wait_with_output(self) -> AppResult<StringOutput> { pub async fn wait_with_output(self) -> AppResult<StringOutput> {
let child = self.spawn(true)?; let child = self.spawn(true)?;
let output = child.wait_with_output()?; let output = child.wait_with_output().await?;
let stdout = String::from_utf8(output.stdout).map_err(|e| AppError::from(e.to_string()))?; let stdout = String::from_utf8(output.stdout).map_err(|e| AppError::from(e.to_string()))?;
let stderr = String::from_utf8(output.stderr).map_err(|e| AppError::from(e.to_string()))?; let stderr = String::from_utf8(output.stderr).map_err(|e| AppError::from(e.to_string()))?;
@ -122,26 +136,32 @@ impl ShellCommand {
}) })
} }
fn spawn(self, piped: bool) -> AppResult<Child> { pub fn spawn(self, piped: bool) -> AppResult<Child> {
tracing::debug!("Running {} {:?}", self.command, self.args);
let (stdout, stderr) = if piped { let (stdout, stderr) = if piped {
(Stdio::piped(), Stdio::piped()) (Stdio::piped(), Stdio::piped())
} else { } else {
(Stdio::inherit(), Stdio::inherit()) (Stdio::inherit(), Stdio::inherit())
}; };
let child = if self.elevated { let mut command = if self.elevated {
Command::new("sudo") let mut cmd = Command::new("sudo");
.arg(self.command) cmd.arg(self.command);
.args(self.args)
.stdout(stdout) cmd
.stderr(stderr)
.spawn()?
} else { } else {
Command::new(self.command) Command::new(self.command)
};
if let Some(dir) = self.working_dir {
command.current_dir(dir);
}
let child = command
.args(self.args) .args(self.args)
.stdout(stdout) .stdout(stdout)
.stderr(stderr) .stderr(stderr)
.spawn()? .kill_on_drop(true)
}; .spawn()?;
Ok(child) Ok(child)
} }

@ -1,9 +1,7 @@
#![allow(clippy::module_name_repetitions)] #![allow(clippy::module_name_repetitions)]
use serde::Deserialize; use serde::Deserialize;
use std::{env, fs}; use std::{env, fs, path::PathBuf};
use crate::{crash, AppExitCode};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Config { pub struct Config {
@ -43,23 +41,9 @@ impl Default for Config {
} }
pub fn read() -> Config { pub fn read() -> Config {
let file = fs::read_to_string(format!( let config_path = PathBuf::from(env::var("HOME").unwrap()).join(".config/ame/config.toml");
"{}/{}", match fs::read_to_string(config_path) {
env::var("HOME").unwrap(), Ok(contents) => toml::from_str(&contents).expect("Could not parse the config file"),
".config/ame/config.toml" Err(_) => Config::default(),
)) }
.unwrap_or_else(|e| {
crash!(
AppExitCode::ConfigParseError,
"Couldn't find config file: {}",
e
);
});
toml::from_str(&file).unwrap_or_else(|e| {
crash!(
AppExitCode::ConfigParseError,
"Could not parse config file: {}",
e
);
})
} }

@ -0,0 +1,283 @@
use std::collections::HashSet;
use aur_rpc::PackageInfo;
use futures::future;
use crate::builder::pacman::{PacmanQueryBuilder, PacmanSearchBuilder};
use super::error::{AppError, AppResult};
use lazy_regex::regex;
#[derive(Clone, Debug)]
pub struct DependencyInformation {
pub depends: DependencyCollection,
pub make_depends: DependencyCollection,
}
#[derive(Clone, Debug, Default)]
pub struct DependencyCollection {
pub aur: Vec<PackageInfo>,
pub repo: Vec<String>,
pub not_found: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct Dependency {
pub name: String,
#[allow(unused)]
pub condition: Option<Condition>,
#[allow(unused)]
pub version: Option<String>,
}
#[derive(Clone, Debug)]
pub enum Condition {
Gt,
Ge,
Eq,
Le,
Lt,
}
impl Condition {
pub fn try_from_str(s: &str) -> Option<Self> {
match s {
"=" => Some(Self::Eq),
"<=" => Some(Self::Le),
">=" => Some(Self::Ge),
">" => Some(Self::Gt),
"<" => Some(Self::Lt),
_ => None,
}
}
}
impl DependencyInformation {
/// Resolves all dependency information for a given package
#[tracing::instrument(level = "trace")]
pub async fn for_package(package: &PackageInfo) -> AppResult<Self> {
let make_depends = Self::resolve_make_depends(package).await?;
let depends = Self::resolve_depends(package).await?;
Ok(Self {
depends,
make_depends,
})
}
/// Resolves all make dependencies for a package
#[tracing::instrument(level = "trace")]
async fn resolve_make_depends(package: &PackageInfo) -> AppResult<DependencyCollection> {
let mut packages_to_resolve: HashSet<String> = package
.make_depends
.iter()
.filter_map(|d| Self::map_dep_to_name(d))
.collect();
Self::filter_fulfilled_dependencies(&mut packages_to_resolve).await?;
let mut already_searched = HashSet::new();
already_searched.insert(package.metadata.name.to_owned());
let mut dependencies = DependencyCollection::default();
while !packages_to_resolve.is_empty() {
already_searched.extend(packages_to_resolve.iter().cloned());
Self::extend_by_repo_packages(&mut packages_to_resolve, &mut dependencies).await?;
let mut aur_packages = aur_rpc::info(&packages_to_resolve).await.map_err(|_| {
AppError::MissingDependencies(packages_to_resolve.iter().cloned().collect())
})?;
aur_packages.iter().for_each(|p| {
packages_to_resolve.remove(&p.metadata.name);
});
let not_found = std::mem::take(&mut packages_to_resolve);
dependencies
.not_found
.append(&mut not_found.into_iter().collect());
packages_to_resolve = Self::get_filtered_make_depends(&aur_packages, &already_searched);
Self::filter_fulfilled_dependencies(&mut packages_to_resolve).await?;
dependencies.aur.append(&mut aur_packages);
}
Ok(dependencies)
}
/// Resolves all dependencies for a package
#[tracing::instrument(level = "trace")]
async fn resolve_depends(package: &PackageInfo) -> AppResult<DependencyCollection> {
let mut packages_to_resolve: HashSet<String> = package
.depends
.iter()
.filter_map(|d| Self::map_dep_to_name(d))
.collect();
Self::filter_fulfilled_dependencies(&mut packages_to_resolve).await?;
let mut already_searched = HashSet::new();
already_searched.insert(package.metadata.name.to_owned());
let mut dependencies = DependencyCollection::default();
while !packages_to_resolve.is_empty() {
already_searched.extend(packages_to_resolve.iter().cloned());
Self::extend_by_repo_packages(&mut packages_to_resolve, &mut dependencies).await?;
let mut aur_packages = aur_rpc::info(&packages_to_resolve).await?;
aur_packages.iter().for_each(|p| {
packages_to_resolve.remove(&p.metadata.name);
});
let not_found = std::mem::take(&mut packages_to_resolve);
dependencies
.not_found
.append(&mut not_found.into_iter().collect());
packages_to_resolve = Self::get_filtered_depends(&aur_packages, &already_searched);
Self::filter_fulfilled_dependencies(&mut packages_to_resolve).await?;
dependencies.aur.append(&mut aur_packages);
}
Ok(dependencies)
}
async fn extend_by_repo_packages(
to_resolve: &mut HashSet<String>,
dependencies: &mut DependencyCollection,
) -> AppResult<()> {
let repo_deps = Self::find_repo_packages(to_resolve.clone()).await?;
to_resolve.retain(|p| !repo_deps.contains(p));
dependencies
.repo
.append(&mut repo_deps.into_iter().collect());
Ok(())
}
fn get_filtered_make_depends(
aur_packages: &[PackageInfo],
searched: &HashSet<String>,
) -> HashSet<String> {
aur_packages
.iter()
.flat_map(|p| {
p.make_depends
.iter()
.filter_map(|d| Self::map_dep_to_name(d))
})
.filter(|d| !searched.contains(d))
.collect()
}
fn get_filtered_depends(
aur_packages: &[PackageInfo],
searched: &HashSet<String>,
) -> HashSet<String> {
aur_packages
.iter()
.flat_map(|p| p.depends.iter().filter_map(|d| Self::map_dep_to_name(d)))
.filter(|d| !searched.contains(d))
.collect()
}
async fn filter_fulfilled_dependencies(deps: &mut HashSet<String>) -> AppResult<()> {
let mut fulfilled = HashSet::new();
for dep in deps.iter() {
if get_dependency_fulfilled(dep.clone()).await? {
fulfilled.insert(dep.clone());
}
}
deps.retain(|pkg| !fulfilled.contains(pkg));
Ok(())
}
fn map_dep_to_name(dep: &str) -> Option<String> {
Dependency::try_from_str(dep).map(|d| d.name)
}
#[tracing::instrument(level = "trace")]
async fn find_repo_packages(pkg_names: HashSet<String>) -> AppResult<HashSet<String>> {
let repo_searches = pkg_names.iter().cloned().map(|p| async {
let search_result = PacmanSearchBuilder::default().query(&p).search().await?;
AppResult::Ok((p, search_result))
});
let repo_deps = future::try_join_all(repo_searches).await?;
let repo_deps: HashSet<String> = repo_deps
.into_iter()
.filter_map(|(p, found)| if found { Some(p) } else { None })
.collect();
Ok(repo_deps)
}
pub fn make_depends(&self) -> HashSet<&str> {
let depends = self.depends();
self.make_depends
.aur
.iter()
.map(|p| p.metadata.name.as_str())
.chain(self.make_depends.repo.iter().map(String::as_str))
.filter(|d| !depends.contains(d))
.collect()
}
pub fn depends(&self) -> HashSet<&str> {
self.depends
.aur
.iter()
.map(|d| d.metadata.name.as_str())
.chain(self.depends.repo.iter().map(String::as_str))
.collect()
}
pub fn all_aur_depends(&self) -> Vec<&PackageInfo> {
self.make_depends
.aur
.iter()
.chain(self.depends.aur.iter())
.collect()
}
pub fn all_repo_depends(&self) -> Vec<&str> {
self.make_depends
.repo
.iter()
.chain(self.depends.repo.iter())
.map(String::as_str)
.collect()
}
}
impl Dependency {
#[tracing::instrument(level = "trace")]
pub fn try_from_str(s: &str) -> Option<Self> {
let r =
regex!(r#"^(?P<name>[\w\-]+)((?P<condition><=|=|>=|>|<)(?P<version>\d+(\.\d+)*))?$"#);
let caps = r.captures(s)?;
let name = caps["name"].to_string();
let condition = caps
.name("condition")
.map(|c| c.as_str())
.and_then(Condition::try_from_str);
let version = caps.name("version").map(|v| v.as_str().into());
tracing::debug!("Parsed dependency to {name} {condition:?} {version:?}");
Some(Dependency {
name,
condition,
version,
})
}
}
#[tracing::instrument(level = "trace")]
async fn get_dependency_fulfilled(name: String) -> AppResult<bool> {
let not_found = PacmanQueryBuilder::all()
.package(name)
.query_with_output()
.await?
.is_empty();
Ok(!not_found)
}

@ -1,13 +1,19 @@
use crossterm::style::Stylize;
use crate::internal::commands::ShellCommand; use crate::internal::commands::ShellCommand;
use crate::internal::config; use crate::internal::config;
use crate::internal::error::SilentUnwrap; use crate::internal::error::SilentUnwrap;
use crate::internal::exit_code::AppExitCode; use crate::internal::exit_code::AppExitCode;
use crate::{prompt, spinner, warn}; use crate::logging::get_logger;
use crate::prompt;
use super::prompt_sudo_single;
/// Searches the filesystem for .pacnew files and helps the user deal with them. /// Searches the filesystem for .pacnew files and helps the user deal with them.
pub fn detect() { pub async fn detect() {
// Start spinner prompt_sudo_single().await.expect("Sudo prompt failed");
let sp = spinner!("Scanning for pacnew files"); let pb = get_logger().new_progress_spinner();
pb.set_message("Scanning for pacnew files");
let mut pacnew = vec![]; let mut pacnew = vec![];
@ -16,6 +22,7 @@ pub fn detect() {
.args(&["-o", "-f"]) .args(&["-o", "-f"])
.elevated() .elevated()
.wait_with_output() .wait_with_output()
.await
.silent_unwrap(AppExitCode::PacmanError); .silent_unwrap(AppExitCode::PacmanError);
let find_lines = find.stdout.split('\n'); let find_lines = find.stdout.split('\n');
for line in find_lines { for line in find_lines {
@ -26,26 +33,36 @@ pub fn detect() {
// If pacnew files are found, warn the user and prompt to pacdiff // If pacnew files are found, warn the user and prompt to pacdiff
if pacnew.is_empty() { if pacnew.is_empty() {
sp.stop_bold("No pacnew files found"); pb.finish_with_message("No .pacnew files found".bold().to_string());
get_logger().reset_output_type();
} else { } else {
sp.stop_bold("It appears that at least one program you have installed / upgraded has installed a .pacnew config file. These are created when you have modified a program's configuration, and a package upgrade could not automatically merge the new file."); pb.finish_with_message("pacnew files found".bold().to_string());
get_logger().reset_output_type();
tracing::info!(
"It appears that at least one program you have installed / upgraded has installed a .pacnew config file. \
These are created when you have modified a program's configuration, and a package upgrade could not automatically merge the new file. \
You can deal with those files by running {}.",
"sudo pacdiff".reset().magenta()
);
let choice = prompt!(default false, "Would you like Amethyst to run pacdiff to deal with this? You can always deal with this later by running `sudo pacdiff`"); let choice = prompt!(default no, "Would you like to run pacdiff now?");
if choice { if choice {
let config = config::read(); let config = config::read();
if config.base.pacdiff_warn { if config.base.pacdiff_warn {
ShellCommand::pacdiff() ShellCommand::pacdiff()
.elevated() .elevated()
.wait() .wait()
.await
.silent_unwrap(AppExitCode::PacmanError); .silent_unwrap(AppExitCode::PacmanError);
} else { } else {
warn!("Pacdiff uses vimdiff by default to edit files for merging. You can focus panes by mousing over them and pressing left click, and scroll up and down using your mouse's scroll wheel (or the arrow keys). To exit vimdiff, press the following key combination: ESC, :qa!, ENTER"); tracing::warn!("Pacdiff uses vimdiff by default to edit files for merging. You can focus panes by mousing over them and pressing left click, and scroll up and down using your mouse's scroll wheel (or the arrow keys). To exit vimdiff, press the following key combination: ESC, :qa!, ENTER");
warn!("You can surpress this warning in the future by setting `pacdiff_warn` to \"false\" in ~/.config/ame/config.toml"); tracing::warn!("You can surpress this warning in the future by setting `pacdiff_warn` to \"false\" in ~/.config/ame/config.toml");
let cont = prompt!(default false, "Continue?");
if cont { if prompt!(default no, "Continue?") {
ShellCommand::pacdiff() ShellCommand::pacdiff()
.elevated() .elevated()
.wait() .wait()
.await
.silent_unwrap(AppExitCode::PacmanError); .silent_unwrap(AppExitCode::PacmanError);
} }
} }

@ -12,15 +12,29 @@ pub type AppResult<T> = Result<T, AppError>;
pub enum AppError { pub enum AppError {
Io(std::io::Error), Io(std::io::Error),
Other(String), Other(String),
Rpc(aur_rpc::error::RPCError),
NonZeroExit, NonZeroExit,
BuildStepViolation,
BuildError { pkg_name: String },
UserCancellation,
MissingDependencies(Vec<String>),
MakePkg(String),
} }
impl Display for AppError { impl Display for AppError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Io(io) => Display::fmt(io, f), AppError::Io(io) => Display::fmt(io, f),
Self::Other(s) => Display::fmt(s, f), AppError::Rpc(e) => Display::fmt(e, f),
Self::NonZeroExit => Display::fmt("Exited with non-zero exit code", f), AppError::Other(s) => Display::fmt(s, f),
AppError::NonZeroExit => Display::fmt("exited with non zero code", f),
AppError::BuildStepViolation => Display::fmt("AUR build violated build steps", f),
AppError::BuildError { pkg_name } => write!(f, "Failed to build package {pkg_name}"),
AppError::UserCancellation => write!(f, "Cancelled by user"),
AppError::MissingDependencies(deps) => {
write!(f, "Missing dependencies {}", deps.join(", "))
}
AppError::MakePkg(msg) => write!(f, "Failed to ru makepkg {msg}"),
} }
} }
} }
@ -33,6 +47,12 @@ impl From<io::Error> for AppError {
} }
} }
impl From<aur_rpc::error::RPCError> for AppError {
fn from(e: aur_rpc::error::RPCError) -> Self {
Self::Rpc(e)
}
}
impl From<String> for AppError { impl From<String> for AppError {
fn from(string: String) -> Self { fn from(string: String) -> Self {
Self::Other(string) Self::Other(string)
@ -53,7 +73,10 @@ impl<T> SilentUnwrap<T> for AppResult<T> {
fn silent_unwrap(self, exit_code: AppExitCode) -> T { fn silent_unwrap(self, exit_code: AppExitCode) -> T {
match self { match self {
Ok(val) => val, Ok(val) => val,
Err(_) => crash!(exit_code, "An error occurred"), Err(e) => {
tracing::debug!("{e}");
crash!(exit_code, "An error occurred")
}
} }
} }
} }

@ -6,8 +6,7 @@ pub enum AppExitCode {
MissingDeps = 3, MissingDeps = 3,
UserCancellation = 4, UserCancellation = 4,
PacmanError = 5, PacmanError = 5,
GitError = 6,
MakePkgError = 7, MakePkgError = 7,
ConfigParseError = 8, RpcError = 9,
Other = 63, Other = 63,
} }

@ -0,0 +1,49 @@
use std::{
collections::VecDeque,
path::{Path, PathBuf},
};
use futures::future;
use tokio::fs;
#[tracing::instrument(level = "trace")]
pub async fn rmdir_recursive(path: &Path) -> std::io::Result<()> {
let mut files: Vec<PathBuf> = Vec::new();
let mut folders: Vec<PathBuf> = Vec::new();
if path.is_dir() {
folders.push(path.into());
} else {
files.push(path.into());
}
let mut folders_to_scan: VecDeque<_> = folders.clone().into();
while let Some(path) = folders_to_scan.pop_front() {
let mut dir_content = fs::read_dir(&path).await?;
while let Some(entry) = dir_content.next_entry().await? {
let entry = entry.path();
if entry.is_dir() {
folders_to_scan.push_back(entry.clone());
folders.push(entry);
} else {
files.push(entry);
}
}
}
tracing::debug!("Deleting {} files", files.len());
future::try_join_all(files.into_iter().map(fs::remove_file)).await?;
tracing::debug!("Deleting {} folders", folders.len());
folders.reverse();
for folder in folders {
tracing::trace!("Deleting {folder:?}");
fs::remove_dir(folder).await?;
}
Ok(())
}

@ -1,83 +0,0 @@
use std::env;
use std::path::Path;
use crate::{crash, internal::exit_code::AppExitCode, log, Options};
/// Ensure all required directories and files exist.
pub fn init(options: Options) {
// Initialise variables
let verbosity = options.verbosity;
let homedir = env::var("HOME").unwrap();
// If stateful dir doesn't exist, create it
if !Path::new(&format!("{}/.local/share/ame/", homedir)).exists() {
if verbosity >= 1 {
log!("Initialising stateful directory");
}
std::fs::create_dir_all(format!("{}/.local/share/ame", homedir)).unwrap_or_else(|e| {
crash!(
AppExitCode::FailedCreatingPaths,
"Couldn't create path: {}/.local/share/ame: {}",
homedir,
e,
);
});
}
// If cache dir doesn't exist, create it
if !Path::new(&format!("{}/.cache/ame", homedir)).exists() {
if verbosity >= 1 {
log!("Initialising cache directory");
}
std::fs::create_dir_all(format!("{}/.cache/ame", homedir)).unwrap_or_else(|e| {
crash!(
AppExitCode::FailedCreatingPaths,
"Couldn't create path: {}/.cache/ame: {}",
homedir,
e,
);
});
}
// If config dir doesn't exist, create it
if !Path::new(&format!("{}/.config/ame/", homedir)).exists() {
if verbosity >= 1 {
log!("Initialising config directory");
}
std::fs::create_dir_all(format!("{}/.config/ame", homedir)).unwrap_or_else(|e| {
crash!(
AppExitCode::FailedCreatingPaths,
"Couldn't create path: {}/.config/ame: {}",
homedir,
e,
);
});
}
// If config file doesn't exist, create it
let config = "\
[base]
pacdiff_warn = true
highlight_optdepends = true
powerpill = false
[extra]
review_user_shell = false
";
if !Path::new(&format!("{}/.config/ame/config.toml", homedir)).exists() {
if verbosity >= 1 {
log!("Initialising config file");
}
std::fs::write(format!("{}/.config/ame/config.toml", homedir), config).unwrap_or_else(
|e| {
crash!(
AppExitCode::FailedCreatingPaths,
"Couldn't create path: {}/.config/ame/config.toml: {}",
homedir,
e,
);
},
);
}
}

@ -1,19 +1,17 @@
pub use clean::*; pub use clean::*;
pub use clean::*; pub use clean::*;
pub use detect::*; pub use detect::*;
pub use initialise::*;
pub use initialise::*;
pub use sort::*;
pub use sort::*; pub use sort::*;
pub use sudoloop::*; pub use sudoloop::*;
mod clean; mod clean;
pub mod commands; pub mod commands;
pub mod config; pub mod config;
pub mod dependencies;
mod detect; mod detect;
pub mod error; pub mod error;
pub mod exit_code; pub mod exit_code;
mod initialise; pub mod fs_utils;
pub mod rpc; pub mod rpc;
mod sort; mod sort;
pub mod structs; pub mod structs;
@ -43,11 +41,6 @@ pub fn uwu_enabled() -> bool {
config.extra.uwu.unwrap_or(false) config.extra.uwu.unwrap_or(false)
} }
pub fn uwu_debug_enabled() -> bool {
let config = config::read();
config.extra.uwu_debug.unwrap_or(false)
}
/// Checks if we're running in a tty. If we do we can assume that /// Checks if we're running in a tty. If we do we can assume that
/// the output can safely be colorized. /// the output can safely be colorized.
pub fn is_tty() -> bool { pub fn is_tty() -> bool {

@ -1,97 +1,23 @@
use std::sync::Arc; use aur_rpc::{PackageInfo, PackageMetadata, SearchField};
#[derive(serde::Deserialize, Debug, Clone)]
/// Struct for deserializing RPC results.
pub struct Package {
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "Version")]
pub version: String,
#[serde(rename = "Description")]
pub description: Option<String>,
#[serde(rename = "Depends")]
#[serde(default)]
pub depends: Vec<String>,
#[serde(rename = "MakeDepends")]
#[serde(default)]
pub make_depends: Vec<String>,
#[serde(rename = "OptDepends")]
#[serde(default)]
pub opt_depends: Vec<String>,
#[serde(rename = "OutOfDate")]
#[serde(default)]
pub out_of_date: Option<usize>,
}
#[derive(serde::Deserialize)]
/// Struct for retreiving search results from the AUR.
pub struct SearchResults {
pub resultcount: u32,
pub results: Vec<Package>,
}
#[derive(Clone)]
/// Struct for retreiving package information from the AUR.
pub struct InfoResults {
pub found: bool,
pub package: Option<Package>,
}
use super::error::AppResult;
pub const URL: &str = "https://aur.archlinux.org/"; pub const URL: &str = "https://aur.archlinux.org/";
/// Return a struct of type [`InfoResults`] from the AUR. pub async fn rpcinfo(pkg: &str) -> AppResult<Option<PackageInfo>> {
pub fn rpcinfo(pkg: &str) -> InfoResults { let packages = aur_rpc::info(vec![pkg]).await?;
// Initialise TLS connector
let tls_connector = Arc::new(native_tls::TlsConnector::new().unwrap());
// Build request agent
let agent = ureq::AgentBuilder::new()
.tls_connector(tls_connector)
.build();
// Send request and parse results into json Ok(packages.into_iter().next())
let res: SearchResults = agent
.get(&format!(
"https://aur.archlinux.org/rpc/?v=5&type=info&arg={}",
pkg
))
.call()
.unwrap()
.into_json()
.unwrap();
// Check if package was found
if res.results.is_empty() {
InfoResults {
found: false,
package: None,
}
} else {
InfoResults {
found: true,
package: Some(res.results[0].clone()),
}
} }
}
/// Return a struct of type [`SearchResults`] from the AUR.
pub fn rpcsearch(pkg: &str) -> SearchResults {
// Initialise TLS connector
let tls_connector = Arc::new(native_tls::TlsConnector::new().unwrap());
// Build request agent pub async fn rpcsearch(
let agent = ureq::AgentBuilder::new() query: String,
.tls_connector(tls_connector) by_field: Option<SearchField>,
.build(); ) -> AppResult<Vec<PackageMetadata>> {
let search_results = if let Some(field) = by_field {
aur_rpc::search_by(field, query).await?
} else {
aur_rpc::search(query).await?
};
// Send request and parse results into json Ok(search_results)
agent
.get(&format!(
"https://aur.archlinux.org/rpc/?v=5&type=search&arg={}",
pkg
))
.call()
.unwrap()
.into_json::<SearchResults>()
.unwrap()
} }

@ -1,52 +1,44 @@
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use crate::internal::{clean, rpc, structs}; use crate::internal::{clean, rpc, structs};
use crate::{log, Options}; use crate::Options;
/// Sorts the given packages into an [`crate::internal::structs::Sorted`] use super::error::SilentUnwrap;
pub fn sort(input: &[String], options: Options) -> structs::Sorted { use super::exit_code::AppExitCode;
// Initialise variables
let mut repo: Vec<String> = vec![];
let mut aur: Vec<String> = vec![];
let mut nf: Vec<String> = vec![];
let verbosity = options.verbosity;
// Sanitise all packages passed in #[tracing::instrument(level = "trace")]
let a = clean(input, options); pub async fn sort(input: &[String], options: Options) -> structs::Sorted {
let mut repo_packages: Vec<String> = vec![];
let mut aur_packages: Vec<String> = vec![];
let mut missing_packages: Vec<String> = vec![];
if verbosity >= 1 { let packages = clean(input);
log!("Sorting: {:?}", a.join(" "));
} tracing::debug!("Sorting: {:?}", packages.join(" "));
for b in a { for package in packages {
// Check if package is in the repos
let rs = Command::new("pacman") let rs = Command::new("pacman")
.arg("-Ss") .arg("-Ss")
.arg(format!("^{}$", &b)) .arg(format!("^{}$", &package))
.stdout(Stdio::null()) .stdout(Stdio::null())
.status() .status()
.expect("Something has gone wrong"); .expect("Something has gone wrong");
if rs.code() == Some(0) { if let Some(0) = rs.code() {
// If it is, add it to the repo vector tracing::debug!("{} found in repos", package);
if verbosity >= 1 { repo_packages.push(package.to_string());
log!("{} found in repos", b); } else if rpc::rpcinfo(&package)
} .await
repo.push(b.to_string()); .silent_unwrap(AppExitCode::RpcError)
} else if rpc::rpcinfo(&b).found { .is_some()
// Otherwise, check if it is in the AUR, if it is, add it to the AUR vector {
if verbosity >= 1 { tracing::debug!("{} found in AUR", package);
log!("{} found in AUR", b); aur_packages.push(package.to_string());
}
aur.push(b.to_string());
} else { } else {
// Otherwise, add it to the not found vector tracing::debug!("{} not found", package);
if verbosity >= 1 { missing_packages.push(package.to_string());
log!("{} not found", b);
}
nf.push(b.to_string());
} }
} }
structs::Sorted::new(repo, aur, nf) structs::Sorted::new(repo_packages, aur_packages, missing_packages)
} }

@ -15,10 +15,9 @@ impl Sorted {
} }
} }
#[derive(Clone, Copy)] #[derive(Clone, Debug, Copy)]
/// Options to be passed down to internal functions /// Options to be passed down to internal functions
pub struct Options { pub struct Options {
pub verbosity: usize,
pub noconfirm: bool, pub noconfirm: bool,
pub asdeps: bool, pub asdeps: bool,
} }

@ -1,18 +1,27 @@
use std::thread;
use std::time::Duration; use std::time::Duration;
use crate::ShellCommand; use crate::ShellCommand;
/// Loop sudo so longer builds don't time out use super::error::AppResult;
#[allow(clippy::module_name_repetitions)]
pub fn start_sudoloop() { /// Loop sudo so it doesn't time out
prompt_sudo(); #[tracing::instrument(level = "trace")]
std::thread::spawn(|| loop { pub async fn start_sudoloop() {
prompt_sudo(); prompt_sudo().await;
thread::sleep(Duration::from_secs(3 * 60)); tokio::task::spawn(async move {
loop {
prompt_sudo().await;
tokio::time::sleep(Duration::from_secs(3 * 60)).await;
}
}); });
} }
fn prompt_sudo() { #[tracing::instrument(level = "trace")]
while ShellCommand::sudo().arg("-v").wait_success().is_err() {} async fn prompt_sudo() {
while prompt_sudo_single().await.is_err() {}
}
#[tracing::instrument(level = "trace")]
pub async fn prompt_sudo_single() -> AppResult<()> {
ShellCommand::sudo().arg("-v").wait_success().await
} }

@ -1,36 +1,14 @@
use colored::Colorize; use std::fs;
use std::io; use std::path::Path;
use std::io::Write; use std::process::exit;
use std::process::{exit, Command, Stdio};
use std::time::UNIX_EPOCH;
use textwrap::wrap;
use crate::internal::exit_code::AppExitCode;
use crate::{internal, uwu};
const OK_SYMBOL: &str = "❖"; use directories::ProjectDirs;
const ERR_SYMBOL: &str = "X"; use textwrap::wrap;
const WARN_SYMBOL: &str = "!";
const PROMPT_SYMBOL: &str = "?";
const PROMPT_YN_DEFAULT_TRUE: &str = "[Y/n]";
const PROMPT_YN_DEFAULT_FALSE: &str = "[y/N]";
#[macro_export] use crate::{internal::exit_code::AppExitCode, logging::get_logger};
/// Macro for printing a message to stdout. use lazy_static::lazy_static;
macro_rules! info {
($($arg:tt)+) => {
$crate::internal::utils::log_info(format!($($arg)+))
}
}
#[macro_export] use super::error::{AppError, SilentUnwrap};
/// Macro for printing a warning message non-destructively.
macro_rules! warn {
($($arg:tt)+) => {
$crate::internal::utils::log_warn(format!($($arg)+))
}
}
#[macro_export] #[macro_export]
/// Macro for printing a message and destructively exiting /// Macro for printing a message and destructively exiting
@ -41,199 +19,58 @@ macro_rules! crash {
} }
#[macro_export] #[macro_export]
/// Macro for logging to stderr /// Cancelles the process
macro_rules! log { macro_rules! cancelled {
($($arg:tt)+) => { () => {
$crate::internal::utils::log_debug(format!($($arg)+)) crash!(
} $crate::internal::exit_code::AppExitCode::UserCancellation,
} "Installation cancelled"
)
#[macro_export]
/// Macro for prompting the user with a yes/no question.
macro_rules! prompt {
(default $default:expr, $($arg:tt)+) => {
$crate::internal::utils::prompt_yn(format!($($arg)+), $default)
}
}
#[macro_export]
/// Macro for creating a spinner.
macro_rules! spinner {
($($arg:tt)+) => {
$crate::internal::utils::spinner_fn(format!($($arg)+))
}
}
/// Print a formatted message to stdout.
pub fn log_info(msg: String) {
let msg = if internal::uwu_enabled() {
uwu!(&msg)
} else {
msg
}; };
let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2)
.subsequent_indent(" ");
println!(
"{} {}",
OK_SYMBOL.purple(),
wrap(&msg, opts).join("\n").bold()
);
}
/// Print a non-destructive warning message
pub fn log_warn(msg: String) {
let msg = if internal::uwu_enabled() {
uwu!(&msg)
} else {
msg
};
let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2)
.subsequent_indent(" ");
println!(
"{} {}",
WARN_SYMBOL.yellow(),
wrap(&msg, opts).join("\n").yellow().bold()
);
} }
/// Logs a message and exits the program with the given exit code. /// Logs a message and exits the program with the given exit code.
pub fn log_and_crash(msg: String, exit_code: AppExitCode) -> ! { pub fn log_and_crash(msg: String, exit_code: AppExitCode) -> ! {
let msg = if internal::uwu_enabled() { get_logger().reset_output_type();
uwu!(&msg) get_logger().log_error(msg);
} else { get_logger().flush();
msg
};
let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2)
.subsequent_indent(" ");
println!(
"{} {}",
ERR_SYMBOL.red().bold(),
wrap(&msg, opts).join("\n").red().bold()
);
exit(exit_code as i32); exit(exit_code as i32);
} }
/// Logs a message to stderr with timestamp pub fn get_cache_dir() -> &'static Path {
pub fn log_debug(msg: String) { lazy_static! {
let msg = if internal::uwu_enabled() && internal::uwu_debug_enabled() { static ref CACHE_DIR: &'static Path = create_if_not_exist(get_directories().cache_dir());
uwu!(&msg)
} else {
msg
};
eprintln!(
"{} {}",
std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
msg
);
} }
/// Prompts the user for a yes/no answer. *CACHE_DIR
pub fn prompt_yn(question: String, default_true: bool) -> bool {
let yn_prompt = if default_true {
PROMPT_YN_DEFAULT_TRUE
} else {
PROMPT_YN_DEFAULT_FALSE
};
let question = if internal::uwu_enabled() {
uwu!(&question)
} else {
question
};
let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2)
.subsequent_indent(" ");
print!(
"{} {} {}: ",
PROMPT_SYMBOL.purple(),
wrap(&question, opts).join("\n").bold(),
yn_prompt
);
let mut yn: String = String::new();
io::stdout().flush().ok();
io::stdin().read_line(&mut yn).unwrap();
if yn.trim().to_lowercase() == "n" || yn.trim().to_lowercase() == "no" {
false
} else if yn.trim().to_lowercase() == "y" || yn.trim().to_lowercase() == "yes" {
true
} else {
default_true
}
} }
pub struct Spinner { fn get_directories() -> &'static ProjectDirs {
spinner: spinoff::Spinner, lazy_static! {
static ref DIRECTORIES: ProjectDirs = ProjectDirs::from("com", "crystal", "ame").unwrap();
} }
impl Spinner { &*DIRECTORIES
pub fn stop_bold(self, text: &str) {
let text = if internal::uwu_enabled() {
uwu!(text)
} else {
text.to_string()
};
let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2)
.subsequent_indent(" ");
let symbol = format!("{}", OK_SYMBOL.purple());
let text = format!("{}", wrap(&text, opts).join("\n").bold());
self.spinner.stop_and_persist(&symbol, &text);
} }
}
/// Returns a spinner that can be used to display progress.
pub fn spinner_fn(text: String) -> Spinner {
let text = if internal::uwu_enabled() {
uwu!(&text)
} else {
text
};
let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2) fn create_if_not_exist(dir: &Path) -> &Path {
.subsequent_indent(" "); if !dir.exists() {
fs::create_dir_all(dir)
Spinner { .map_err(AppError::from)
spinner: spinoff::Spinner::new( .silent_unwrap(AppExitCode::FailedCreatingPaths)
spinoff::Spinners::Line,
format!("{}", wrap(&text, opts).join("\n").bold()),
spinoff::Color::Magenta,
),
}
} }
/// Opens a String in `less`. dir
pub fn pager(text: &String) -> io::Result<()> { }
let text = if internal::uwu_enabled() {
uwu!(text)
} else {
text.to_string()
};
let mut pager = Command::new("less")
.arg("-R")
.stdin(Stdio::piped())
.spawn()?;
let stdin = pager.stdin.as_mut().unwrap(); pub fn wrap_text<S: AsRef<str>>(s: S) -> Vec<String> {
stdin.write_all(text.as_bytes())?; wrap(s.as_ref(), get_wrap_options())
stdin.flush()?; .into_iter()
pager.wait()?; .map(String::from)
.collect()
}
Ok(()) fn get_wrap_options() -> textwrap::Options<'static> {
textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2)
.subsequent_indent(" ")
} }

@ -0,0 +1,146 @@
use colored::Colorize;
use std::collections::HashMap;
use std::sync::Arc;
use tracing_subscriber::registry::LookupSpan;
use tracing::field::Visit;
use tracing::{span, Level, Metadata, Subscriber};
use tracing_subscriber::Layer;
use super::handler::LogHandler;
use super::Verbosity;
const ENABLED_MODULES: &[&str] = &["ame"];
pub struct AmeFormatLayer {
logger: Arc<LogHandler>,
}
impl AmeFormatLayer {
pub fn new(logger: Arc<LogHandler>) -> Self {
Self { logger }
}
fn is_level_loggable(&self, level: &Level) -> bool {
self.logger.is_loggable(Verbosity::from_level(level))
}
fn is_enabled(&self, metadata: &Metadata) -> bool {
let level = metadata.level();
if !self.is_level_loggable(level) {
false
} else if let Some(module_path) = metadata.module_path() {
ENABLED_MODULES.iter().any(|m| module_path.starts_with(m))
} else {
false
}
}
fn log(&self, msg: String, level: &Level) {
match Verbosity::from_level(level) {
Verbosity::Error => self.logger.log_error(msg),
Verbosity::Warning => self.logger.log_warning(msg),
Verbosity::Info => self.logger.log_info(msg),
Verbosity::Debug => self.logger.log_debug(msg),
Verbosity::Trace => self.logger.log_trace(msg),
}
}
}
impl<S: Subscriber + for<'a> LookupSpan<'a>> Layer<S> for AmeFormatLayer {
/// When entering a span
fn on_new_span(
&self,
attrs: &span::Attributes<'_>,
_id: &span::Id,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let metadata = attrs.metadata();
if self.is_enabled(metadata) {
let mut visitor = ValueDebugStorage::default();
attrs.record(&mut visitor);
let fields: Vec<String> = visitor
.values
.into_iter()
.map(|(k, v)| format!("{k} = {v}"))
.collect();
let mut fields_str = fields.join("\n ");
if !fields_str.is_empty() {
fields_str = format!("\n {fields_str}");
}
if let Some(module) = metadata.module_path() {
self.log(
format!(
"{} {}::{} {}",
"ENTER".italic(),
module,
metadata.name(),
fields_str.dimmed()
),
metadata.level(),
)
} else {
self.log(
format!(
"{} {} {}",
"ENTER".italic(),
metadata.name(),
fields_str.dimmed()
),
metadata.level(),
)
}
}
}
fn on_close(&self, id: span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
let span = ctx.span(&id).unwrap();
let metadata = span.metadata();
if self.is_enabled(metadata) {
if let Some(module) = metadata.module_path() {
self.log(
format!("{} {}::{}", "EXIT".italic(), module, metadata.name(),),
metadata.level(),
);
} else {
self.log(
format!("{} {}", "EXIT".italic(), metadata.name()),
metadata.level(),
);
}
}
}
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let metadata = event.metadata();
if self.is_enabled(metadata) {
let mut visitor = ValueDebugStorage::default();
event.record(&mut visitor);
let mut values = visitor.values;
if let Some(msg) = values.remove("message") {
self.log(msg, metadata.level())
}
}
}
}
#[derive(Default)]
pub struct ValueDebugStorage {
pub values: HashMap<String, String>,
}
impl Visit for ValueDebugStorage {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
self.values
.insert(field.name().to_string(), format!("{:?}", value));
}
}

@ -0,0 +1,249 @@
use colored::Colorize;
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget};
use parking_lot::{Mutex, RwLock};
use std::{
fmt::Display,
io::{self, Write},
mem,
sync::{atomic::AtomicBool, Arc},
time::Duration,
};
use crate::{internal::utils::wrap_text, uwu};
use super::{get_logger, Verbosity};
const OK_SYMBOL: &str = "❖";
const ERR_SYMBOL: &str = "X";
const WARN_SYMBOL: &str = "!";
const DEBUG_SYMBOL: &str = "⌘";
const TRACE_SYMBOL: &str = "🗲";
pub struct LogHandler {
level: Arc<RwLock<Verbosity>>,
output_type: Arc<RwLock<OutputType>>,
uwu_enabled: Arc<AtomicBool>,
}
impl Default for LogHandler {
fn default() -> Self {
Self {
level: Arc::new(RwLock::new(Verbosity::Info)),
output_type: Arc::new(RwLock::new(OutputType::Stderr)),
uwu_enabled: Arc::new(AtomicBool::new(false)),
}
}
}
#[allow(unused)]
pub enum OutputType {
Stdout,
Stderr,
MultiProgress(Arc<MultiProgress>),
Progress(Arc<ProgressBar>),
Buffer {
buffer: Arc<Mutex<Vec<String>>>,
suspended: Box<OutputType>,
},
}
pub struct SuspendHandle;
impl LogHandler {
pub fn log_error(&self, msg: String) {
if self.is_loggable(Verbosity::Error) {
let msg = self.preformat_msg(msg);
let msg = format!("{} {}", ERR_SYMBOL.red().bold(), msg.bold().red());
self.log(msg);
}
}
pub fn log_warning(&self, msg: String) {
if self.is_loggable(Verbosity::Warning) {
let msg = self.preformat_msg(msg);
let msg = format!("{} {}", WARN_SYMBOL.yellow(), msg.yellow().bold());
self.log(msg);
}
}
pub fn log_info(&self, msg: String) {
if self.is_loggable(Verbosity::Info) {
let msg = self.preformat_msg(msg);
let msg = format!("{} {}", OK_SYMBOL.purple(), msg.bold());
self.log(msg);
}
}
pub fn log_debug(&self, msg: String) {
if self.is_loggable(Verbosity::Debug) {
let msg = self.preformat_msg(msg);
let msg = format!("{} {}", DEBUG_SYMBOL.blue(), msg);
self.log(msg);
}
}
pub fn log_trace(&self, msg: String) {
if self.is_loggable(Verbosity::Trace) {
let msg = self.preformat_msg(msg);
let msg = format!("{} {}", TRACE_SYMBOL.cyan(), msg.dimmed());
self.log(msg);
}
}
pub fn print_list<I: IntoIterator<Item = T>, T: Display>(&self, list: I, separator: &str) {
let lines = list
.into_iter()
.map(|l| self.preformat_msg(l.to_string()))
.fold(String::new(), |acc, line| {
format!("{}{}{}", acc, separator, line)
});
let lines = wrap_text(lines).join("\n");
self.log(lines)
}
pub fn print_newline(&self) {
self.log(String::from(""))
}
pub fn set_verbosity(&self, level: Verbosity) {
(*self.level.write()) = level;
}
pub fn reset_output_type(&self) {
self.set_output_type(OutputType::Stdout);
}
#[must_use]
pub fn suspend(&self) -> SuspendHandle {
let mut output_type = self.output_type.write();
let mut old_output_type = OutputType::Stdout;
mem::swap(&mut *output_type, &mut old_output_type);
(*output_type) = OutputType::Buffer {
buffer: Arc::new(Mutex::new(Vec::new())),
suspended: Box::new(old_output_type),
};
SuspendHandle
}
pub fn unsuspend(&self) {
let mut buffered = Vec::new();
{
let mut output_type = self.output_type.write();
let mut old_output_type = OutputType::Stdout;
mem::swap(&mut *output_type, &mut old_output_type);
if let OutputType::Buffer { buffer, suspended } = old_output_type {
(*output_type) = *suspended;
buffered = mem::take(&mut *buffer.lock());
}
}
buffered.into_iter().for_each(|msg| self.log(msg));
}
/// Creates a new progress spinner and registers it on the log handler
pub fn new_progress_spinner(&self) -> Arc<ProgressBar> {
let pb = ProgressBar::new_spinner();
pb.enable_steady_tick(Duration::from_millis(250));
let mut output_type = self.output_type.write();
if let OutputType::MultiProgress(mp) = &*output_type {
Arc::new(mp.add(pb))
} else {
let pb = Arc::new(pb);
*output_type = OutputType::Progress(pb.clone());
pb
}
}
pub fn new_multi_progress(&self) -> Arc<MultiProgress> {
let mp = Arc::new(MultiProgress::new());
self.set_output_type(OutputType::MultiProgress(mp.clone()));
mp
}
/// Sets the output type of the log handler to either stdout/stderr or a progress bar
pub fn set_output_type(&self, mut output: OutputType) {
{
let mut output_type = self.output_type.write();
mem::swap(&mut *output_type, &mut output);
}
match &mut output {
OutputType::MultiProgress(mp) => mp.set_draw_target(ProgressDrawTarget::hidden()),
OutputType::Progress(p) => p.set_draw_target(ProgressDrawTarget::hidden()),
OutputType::Buffer {
buffer,
suspended: _,
} => {
let buffered = mem::take(&mut *buffer.lock());
buffered.into_iter().for_each(|c| self.log(c));
}
_ => {}
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn set_uwu_enabled(&self, enabled: bool) {
self.uwu_enabled
.store(enabled, std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn is_loggable(&self, level: Verbosity) -> bool {
(*self.level.read()) >= level
}
/// Flushes the output buffer
pub fn flush(&self) {
let output = self.output_type.read();
match &*output {
OutputType::Stdout => io::stdout().flush().unwrap(),
OutputType::Stderr => io::stderr().flush().unwrap(),
OutputType::Progress(p) => p.tick(),
_ => {}
}
}
fn preformat_msg(&self, msg: String) -> String {
let msg = self.apply_uwu(msg);
wrap_text(msg).join("\n")
}
fn apply_uwu(&self, msg: String) -> String {
if self.uwu_enabled.load(std::sync::atomic::Ordering::Relaxed) {
uwu!(msg)
} else {
msg
}
}
fn log(&self, msg: String) {
let output_type = self.output_type.read();
match &*output_type {
OutputType::Stdout => println!("{}", msg),
OutputType::Stderr => eprintln!("{}", msg),
OutputType::MultiProgress(m) => {
let _ = m.println(msg);
}
OutputType::Progress(p) => p.println(msg),
OutputType::Buffer {
buffer,
suspended: _,
} => buffer.lock().push(msg),
};
}
}
impl Drop for SuspendHandle {
fn drop(&mut self) {
get_logger().unsuspend();
}
}

@ -0,0 +1,74 @@
use std::sync::Arc;
use lazy_static::lazy_static;
use tracing::Level;
use tracing_error::ErrorLayer;
use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt;
use tracing_subscriber::Registry;
mod fmt_layer;
use fmt_layer::AmeFormatLayer;
use crate::internal::uwu_enabled;
use self::handler::LogHandler;
pub mod handler;
pub mod output;
pub mod piped_stdio;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Verbosity {
#[allow(dead_code)]
Error = 0,
#[allow(dead_code)]
Warning = 1,
Info = 2,
Debug = 3,
Trace = 4,
}
impl From<usize> for Verbosity {
fn from(num_verbosity: usize) -> Self {
match num_verbosity {
0 => Self::Info,
1 => Self::Debug,
2 => Self::Trace,
_ => Self::Info,
}
}
}
impl Verbosity {
fn from_level(l: &Level) -> Self {
match *l {
Level::ERROR => Self::Error,
Level::WARN => Self::Warning,
Level::INFO => Self::Info,
Level::DEBUG => Self::Debug,
Level::TRACE => Self::Trace,
}
}
}
/// Initializes the tracing logger
/// Can be used for debug purposes _or_ verbose output
pub fn init_logger(verbosity: Verbosity) {
let logger = get_logger();
logger.set_verbosity(verbosity);
logger.set_uwu_enabled(uwu_enabled());
let ame_layer = AmeFormatLayer::new(logger);
let subscriber = Registry::default()
.with(ErrorLayer::default())
.with(ame_layer);
tracing::subscriber::set_global_default(subscriber).unwrap();
}
/// Returns the global logger instance
pub fn get_logger() -> Arc<LogHandler> {
lazy_static! {
static ref LOGGER: Arc<LogHandler> = Arc::new(LogHandler::default());
}
Arc::clone(&LOGGER)
}

@ -0,0 +1,101 @@
use std::collections::{HashMap, HashSet};
use aur_rpc::PackageInfo;
use console::Alignment;
use crossterm::style::Stylize;
use crate::{builder::pacman::PacmanQueryBuilder, internal::dependencies::DependencyInformation};
use super::get_logger;
pub async fn print_dependency_list(dependencies: &[DependencyInformation]) -> bool {
let (mut deps_repo, mut makedeps_repo, deps_aur, makedeps_aur) = dependencies
.iter()
.map(|d| {
(
d.depends.repo.iter().collect(),
d.make_depends.repo.iter().collect(),
d.depends.aur.iter().collect(),
d.make_depends.aur.iter().collect(),
)
})
.fold(
(Vec::new(), Vec::new(), Vec::new(), Vec::new()),
|mut acc, mut deps| {
acc.0.append(&mut deps.0);
acc.1.append(&mut deps.1);
acc.2.append(&mut deps.2);
acc.3.append(&mut deps.3);
acc
},
);
deps_repo.dedup();
makedeps_repo.dedup();
let mut empty = true;
if !deps_repo.is_empty() {
tracing::info!("Repo dependencies");
get_logger().print_list(&deps_repo, " ");
empty = false;
get_logger().print_newline();
}
if !deps_aur.is_empty() {
tracing::info!("AUR dependencies");
print_aur_package_list(&deps_aur).await;
empty = false;
get_logger().print_newline();
}
if !makedeps_repo.is_empty() {
tracing::info!("Repo make dependencies");
get_logger().print_list(&makedeps_repo, " ");
empty = false;
get_logger().print_newline();
}
if !makedeps_aur.is_empty() {
tracing::info!("AUR make dependencies");
print_aur_package_list(&makedeps_aur).await;
empty = false;
get_logger().print_newline();
}
empty
}
pub async fn print_aur_package_list(packages: &[&PackageInfo]) -> bool {
let pkgs = packages
.iter()
.map(|p| p.metadata.name.clone())
.collect::<HashSet<_>>();
let installed = PacmanQueryBuilder::all()
.query_with_output()
.await
.unwrap()
.into_iter()
.filter(|p| pkgs.contains(&p.name))
.map(|p| (p.name.clone(), p))
.collect::<HashMap<_, _>>();
get_logger().print_list(
packages.iter().map(|pkg| {
format!(
"{} version {} ({} votes) {}",
console::pad_str(&pkg.metadata.name, 30, Alignment::Left, Some("...")).bold(),
pkg.metadata.version.clone().dim(),
pkg.metadata.num_votes,
if installed.contains_key(&pkg.metadata.name) {
"(Installed)"
} else {
""
}
.bold()
.magenta()
)
}),
"\n ",
);
!installed.is_empty()
}

@ -0,0 +1,57 @@
use std::mem;
use tokio::{
io::{AsyncRead, AsyncReadExt},
process::{ChildStderr, ChildStdout},
};
use crate::internal::error::{AppError, AppResult};
pub struct StdioReader {
stdout: ChildStdout,
stderr: ChildStderr,
stdout_line: Vec<u8>,
stderr_line: Vec<u8>,
}
impl StdioReader {
pub fn new(stdout: ChildStdout, stderr: ChildStderr) -> Self {
Self {
stdout,
stderr,
stdout_line: Vec::new(),
stderr_line: Vec::new(),
}
}
pub async fn read_line(&mut self) -> AppResult<String> {
let line = tokio::select! {
l = Self::read_stdio(&mut self.stdout, &mut self.stdout_line) => {l?}
l = Self::read_stdio(&mut self.stderr, &mut self.stderr_line) => {l?}
};
Ok(line)
}
pub async fn read_stdio<R: AsyncRead + Unpin>(
reader: &mut R,
buf: &mut Vec<u8>,
) -> AppResult<String> {
while let Ok(ch) = reader.read_u8().await {
if ch == b'\n' {
if !buf.is_empty() {
break;
}
} else {
buf.push(ch);
}
}
let line = mem::take(buf);
if line.is_empty() {
Err(AppError::from("stdio exhausted"))
} else {
Ok(String::from_utf8(line).unwrap())
}
}
}

@ -1,145 +1,78 @@
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] use args::{Args, GenCompArgs, InfoArgs};
#![allow(clippy::too_many_lines)] use builder::pacman::{PacmanColor, PacmanQueryBuilder};
use clap::Parser;
use args::Args;
use clap::{CommandFactory, Parser};
use clap_complete::{Generator, Shell};
use internal::commands::ShellCommand; use internal::commands::ShellCommand;
use internal::error::SilentUnwrap; use internal::error::SilentUnwrap;
use std::env;
use std::fs;
use std::path::Path;
use std::str::FromStr;
use crate::args::{ use crate::args::{InstallArgs, Operation, QueryArgs, RemoveArgs, SearchArgs};
GenCompArgs, InfoArgs, InstallArgs, Operation, QueryArgs, RemoveArgs, SearchArgs, UpgradeArgs, use crate::internal::detect;
};
use crate::internal::exit_code::AppExitCode; use crate::internal::exit_code::AppExitCode;
use crate::internal::utils::pager; use crate::internal::{sort, start_sudoloop, structs::Options};
use crate::internal::{detect, init, sort, start_sudoloop, structs::Options}; use clap_complete::{Generator, Shell};
use std::str::FromStr;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
mod args; mod args;
mod builder;
mod interact;
mod internal; mod internal;
mod logging;
mod operations; mod operations;
use logging::init_logger;
fn main() { #[tokio::main]
// Break if we are running as root async fn main() {
color_eyre::install().unwrap();
if unsafe { libc::geteuid() } == 0 { if unsafe { libc::geteuid() } == 0 {
crash!( AppExitCode::RunAsRoot, "Running amethyst as root is disallowed as it can lead to system breakage. Instead, amethyst will prompt you when it needs superuser permissions"); crash!( AppExitCode::RunAsRoot, "Running amethyst as root is disallowed as it can lead to system breakage. Instead, amethyst will prompt you when it needs superuser permissions");
} }
// Parse arguments
let args: Args = Args::parse(); let args: Args = Args::parse();
init_logger(args.verbose.into());
// Initialize variables
let verbosity = args.verbose;
let noconfirm = args.no_confirm; let noconfirm = args.no_confirm;
// Get options struct
let options = Options { let options = Options {
verbosity,
noconfirm, noconfirm,
asdeps: false, asdeps: false,
}; };
// Ensure amethyst is initialized
init(options);
// Start sudoloop if specified
if args.sudoloop { if args.sudoloop {
start_sudoloop(); start_sudoloop().await;
}
let cachedir = if args.cachedir.is_none() {
"".to_string()
} else {
// Create cache directory if it doesn't exist
if fs::metadata(&args.cachedir.as_ref().unwrap()).is_err() {
fs::create_dir(&args.cachedir.as_ref().unwrap()).unwrap_or_else(|err| {
crash!(
AppExitCode::FailedCreatingPaths,
"Could not create cache directory: {}",
err
);
});
}
Path::new(&args.cachedir.unwrap())
.canonicalize()
.unwrap()
.to_str()
.unwrap()
.to_string()
};
// List of possible options
let opers = vec![
"install", "remove", "upgrade", "search", "query", "info", "clean", "diff", "gencomp",
];
// If arg is completely unrecognized, attempt to pass it to pacman
if let Some((ext, ext_m)) = args::Args::command().get_matches().subcommand() {
if !opers.contains(&ext) {
let mut m = ext_m
.values_of("")
.unwrap_or_default()
.collect::<Vec<&str>>();
m.insert(0, ext);
info!("Passing unrecognized flags \"{}\" to pacman", m.join(" "));
let child = ShellCommand::pacman()
.args(m)
.elevated()
.wait()
.silent_unwrap(AppExitCode::PacmanError);
std::process::exit(child.code().unwrap_or(1));
}
} }
// Match args
match args.subcommand.unwrap_or_default() { match args.subcommand.unwrap_or_default() {
Operation::Install(install_args) => cmd_install(install_args, options, &cachedir), Operation::Install(install_args) => cmd_install(install_args, options).await,
Operation::Remove(remove_args) => cmd_remove(remove_args, options), Operation::Remove(remove_args) => cmd_remove(remove_args, options).await,
Operation::Search(search_args) => cmd_search(&search_args, options), Operation::Search(search_args) => cmd_search(search_args, options).await,
Operation::Query(query_args) => cmd_query(&query_args), Operation::Query(query_args) => cmd_query(query_args).await,
Operation::Info(info_args) => cmd_info(info_args), Operation::Upgrade(upgrade_args) => {
Operation::Upgrade(upgrade_args) => cmd_upgrade(upgrade_args, options, &cachedir), tracing::info!("Performing system upgrade");
Operation::Clean => { operations::upgrade(upgrade_args, options).await;
info!("Removing orphaned packages");
operations::clean(options);
}
Operation::Diff => {
info!("Running pacdiff");
detect();
} }
Operation::GenComp(gencomp_args) => { Operation::Clean => {
info!("Generating shell completions for {}. Please pipe `stderr` to a file to get completions as a file, e.g. `ame gencomp fish 2> file.fish`", gencomp_args.shell); tracing::info!("Removing orphaned packages");
cmd_gencomp(&gencomp_args); operations::clean(options).await;
} }
Operation::Info(info_args) => cmd_info(info_args).await,
Operation::GenComp(gen_args) => cmd_gencomp(&gen_args),
Operation::Diff => todo!(),
} }
detect().await;
} }
fn cmd_install(args: InstallArgs, options: Options, cachedir: &str) { #[tracing::instrument(level = "trace")]
// Initialise variables async fn cmd_install(args: InstallArgs, options: Options) {
let packages = args.packages; let packages = args.packages;
let sorted = sort(&packages, options).await;
if args.aur && args.repo { if !sorted.repo.is_empty() {
crash!(AppExitCode::Other, "Cannot specify both --aur and --repo"); operations::install(sorted.repo, options).await;
}
if !sorted.aur.is_empty() {
operations::aur_install(sorted.aur, options).await;
} }
let aur = args.aur || env::args().collect::<Vec<String>>()[1] == "-Sa";
let repo = args.repo || env::args().collect::<Vec<String>>()[1] == "-Sr";
let sorted = sort(&packages, options);
let config = internal::config::read();
info!("Attempting to install packages: {}", packages.join(", "));
if !sorted.nf.is_empty() { if !sorted.nf.is_empty() {
// If some packages are not found, crash
crash!( crash!(
AppExitCode::PacmanError, AppExitCode::PacmanError,
"Couldn't find packages: {} in repos or the AUR", "Couldn't find packages: {} in repos or the AUR",
@ -147,149 +80,80 @@ fn cmd_install(args: InstallArgs, options: Options, cachedir: &str) {
); );
} }
if !repo && !aur && !sorted.repo.is_empty() || repo && !sorted.repo.is_empty() { let bash_output = ShellCommand::bash()
// If repo packages found, install them .arg("-c")
operations::install(&sorted.repo, options); .arg("sudo find /etc -name *.pacnew")
} .wait_with_output()
if !repo && !aur && !sorted.aur.is_empty() || aur && !sorted.aur.is_empty() { .await
// If AUR packages found, install them .silent_unwrap(AppExitCode::Other)
operations::aur_install(sorted.aur, options, cachedir);
}
// Show optional dependencies for installed packages
if packages.len() > 1 && config.base.highlight_optdepends {
info!("Showing optional dependencies for installed packages");
for p in packages {
let out = std::process::Command::new("expac")
.args(&["-Q", "-l", "\n ", " %O", &p])
.output()
.unwrap()
.stdout; .stdout;
let out = String::from_utf8(out).unwrap().trim().to_string();
if !out.is_empty() { if !bash_output.is_empty() {
info!("{}:", p); let pacnew_files = bash_output
println!(" {}", out); .split_whitespace()
} .collect::<Vec<&str>>()
} .join(", ");
tracing::info!("You have .pacnew files in /etc ({pacnew_files}) that you haven't removed or acted upon, it is recommended you do that now" );
} }
} }
fn cmd_remove(args: RemoveArgs, options: Options) { #[tracing::instrument(level = "trace")]
// Initialise variables async fn cmd_remove(args: RemoveArgs, options: Options) {
let packages = args.packages; let packages = args.packages;
tracing::info!("Uninstalling packages: {}", &packages.join(", "));
info!("Uninstalling packages: {}", &packages.join(", ")); operations::uninstall(packages, options).await;
// Remove packages
operations::uninstall(&packages, options);
} }
fn cmd_search(args: &SearchArgs, options: Options) { #[tracing::instrument(level = "trace")]
// Initialise variables async fn cmd_search(args: SearchArgs, options: Options) {
let query_string = args.search.join(" "); let query_string = args.search;
// Logic for searching
let repo = args.repo || env::args().collect::<Vec<String>>()[1] == "-Ssr";
let aur = args.aur || env::args().collect::<Vec<String>>()[1] == "-Ssa";
let both = !repo && !aur;
// Start repo spinner
let repo_results = if repo || both {
let rsp = spinner!("Searching repos for {}", query_string);
// Search repos
let ret = operations::search(&query_string, options);
rsp.stop_bold("Repo search complete");
ret
} else {
"".to_string()
};
// Start AUR spinner
let aur_results = if aur || both {
// Strip query of any non-alphanumeric characters
let query_string = query_string.replace(|c: char| !c.is_alphanumeric() && c != '-', "");
let asp = spinner!("Searching AUR for {}", query_string);
// Search AUR if args.aur {
let ret = operations::aur_search(&query_string, options); tracing::info!("Searching AUR for {}", &query_string);
asp.stop_bold("AUR search complete"); operations::aur_search(&query_string, args.by, options).await;
ret
} else {
"".to_string()
};
let results = repo_results + "\n" + &aur_results;
// Print results either way, so that the user can see the results after they exit `less`
let text = if internal::uwu_enabled() {
uwu!(results.trim())
} else {
results.trim().to_string()
};
println!("{}", text);
// Check if results are longer than terminal height
if results.lines().count() > crossterm::terminal::size().unwrap().1 as usize {
// If so, paginate results
#[allow(clippy::let_underscore_drop)]
let _ = pager(&results.trim().to_string());
} }
if args.repo {
tracing::info!("Searching repos for {}", &query_string);
operations::search(&query_string, options).await;
} }
fn cmd_query(args: &QueryArgs) { if !args.aur && !args.repo {
let aur = args.aur tracing::info!("Searching AUR and repos for {}", &query_string);
|| env::args().collect::<Vec<String>>()[1] == "-Qa" operations::search(&query_string, options).await;
|| env::args().collect::<Vec<String>>()[1] == "-Qm"; operations::aur_search(&query_string, args.by, options).await;
let repo = args.repo
|| env::args().collect::<Vec<String>>()[1] == "-Qr"
|| env::args().collect::<Vec<String>>()[1] == "-Qn";
let both = !aur && !repo;
if aur {
// If AUR query, query AUR
ShellCommand::pacman()
.arg("-Qm")
.wait_success()
.silent_unwrap(AppExitCode::PacmanError);
} }
if repo {
// If repo query, query repos
ShellCommand::pacman()
.arg("-Qn")
.wait_success()
.silent_unwrap(AppExitCode::PacmanError);
} }
if both {
// If no query type specified, query both #[tracing::instrument(level = "trace")]
ShellCommand::pacman() async fn cmd_query(args: QueryArgs) {
.arg("-Qn") if args.repo || !args.aur {
.wait_success() tracing::info!("Installed Repo Packages: ");
PacmanQueryBuilder::native()
.color(PacmanColor::Always)
.query()
.await
.silent_unwrap(AppExitCode::PacmanError); .silent_unwrap(AppExitCode::PacmanError);
ShellCommand::pacman() }
.arg("-Qm") if args.aur || !args.repo {
.wait_success() tracing::info!("Installed AUR Packages: ");
PacmanQueryBuilder::foreign()
.color(PacmanColor::Always)
.query()
.await
.silent_unwrap(AppExitCode::PacmanError); .silent_unwrap(AppExitCode::PacmanError);
} }
} }
fn cmd_info(args: InfoArgs) { #[tracing::instrument(level = "trace")]
ShellCommand::pacman() async fn cmd_info(args: InfoArgs) {
.arg("-Qi") PacmanQueryBuilder::info()
.arg(args.package) .package(args.package)
.wait() .query()
.await
.silent_unwrap(AppExitCode::PacmanError); .silent_unwrap(AppExitCode::PacmanError);
} }
fn cmd_upgrade(args: UpgradeArgs, options: Options, cachedir: &str) { #[tracing::instrument(level = "trace")]
info!("Performing system upgrade");
operations::upgrade(options, args, cachedir);
}
fn cmd_gencomp(args: &GenCompArgs) { fn cmd_gencomp(args: &GenCompArgs) {
let shell: Shell = Shell::from_str(&args.shell).unwrap_or_else(|e| { let shell: Shell = Shell::from_str(&args.shell).unwrap_or_else(|e| {
crash!(AppExitCode::Other, "Invalid shell: {}", e); crash!(AppExitCode::Other, "Invalid shell: {}", e);

@ -1,427 +0,0 @@
use chrono::{Local, TimeZone};
use std::env::set_current_dir;
use std::path::Path;
use std::process::Command;
use std::{env, fs};
use crate::internal::commands::ShellCommand;
use crate::internal::config;
use crate::internal::error::SilentUnwrap;
use crate::internal::exit_code::AppExitCode;
use crate::internal::rpc::rpcinfo;
use crate::internal::sort;
use crate::operations::install;
use crate::{crash, info, log, prompt, warn, Options};
const AUR_CACHE: &str = ".cache/ame";
/// Return a list of all files/dirs in a directory.
fn list(dir: &str) -> Vec<String> {
let dirs = fs::read_dir(Path::new(&dir)).unwrap();
let dirs: Vec<String> = dirs
.map(|dir| {
(*dir
.unwrap()
.path()
.to_str()
.unwrap()
.split('/')
.collect::<Vec<&str>>()
.last()
.unwrap())
.to_string()
})
.collect();
dirs
}
/// Returns and creates a temporary directory for amethyst to use
fn mktemp() -> String {
let tempdir = Command::new("mktemp")
.args(&["-d", "/tmp/ame.XXXXXX.tmp"])
.output()
.unwrap()
.stdout;
String::from_utf8(tempdir).unwrap().trim().to_string()
}
/// Help the user review and/or edit an AUR package before installing
fn review(cachedir: &str, pkg: &str, orig_cachedir: &str) {
// Prompt user to view PKGBUILD
let p0 = prompt!(default false, "Would you like to review and/or edit {}'s PKGBUILD (and any adjacent build files if present)?", pkg);
if p0 {
info!("This will drop you into a standard `bash` shell (unless set otherwise in the config) in the package's cache directory. If any changes are made, you will be prompted whether to save them to your home directory. To stop reviewing/editing, just run `exit`");
let p1 = prompt!(default true,
"Continue?"
);
if p1 {
let config = config::read();
let cdir = env::current_dir().unwrap().to_str().unwrap().to_string();
set_current_dir(Path::new(&format!("{}/{}", &cachedir, pkg))).unwrap();
if config.extra.review_user_shell {
Command::new(&env::var("SHELL").unwrap())
.spawn()
.unwrap()
.wait()
.unwrap();
} else {
ShellCommand::bash().wait().unwrap();
}
set_current_dir(Path::new(&cdir)).unwrap();
// Prompt user to save changes
let p2 = prompt!(default false,
"Save changes to package {}?",
pkg
);
if p2 {
// Save changes to ~/.local/share
let dest = format!(
"{}-saved-{}",
pkg,
chrono::Local::now()
.naive_local()
.format("%Y-%m-%d_%H-%M-%S")
);
Command::new("cp")
.arg("-r")
.arg(format!("{}/{}", cachedir, pkg))
.arg(format!(
"{}/.local/share/ame/{}",
env::var("HOME").unwrap(),
dest
))
.spawn()
.unwrap()
.wait()
.unwrap();
// Alert user
info!("Saved changes to ~/.local/share/ame/{}", dest);
};
}
}
// Prompt user to continue
let p = prompt!(default true, "Would you still like to install {}?", pkg);
if !p {
// If not, crash
if orig_cachedir.is_empty() {
fs::remove_dir_all(format!("{}/{}", cachedir, pkg)).unwrap();
}
crash!(AppExitCode::UserCancellation, "Not proceeding");
};
}
/// Finalize a build/install process
fn finish(cachedir: &str, pkg: &str, options: &Options) {
// Install all packages from cachedir except `pkg` using --asdeps
let dirs = list(cachedir);
// Get a list of packages in cachedir
if dirs.len() > 1 {
info!("Installing AUR dependencies for {}", pkg);
let cmd = std::process::Command::new("bash")
.args(&[
"-cO",
"extglob",
format!(
"sudo pacman -U --asdeps {}/!({})/*.pkg.tar.* {}",
cachedir,
pkg,
if options.noconfirm { "--noconfirm" } else { "" }
)
.as_str(),
])
.spawn()
.unwrap()
.wait()
.unwrap();
if cmd.success() {
info!("All AUR dependencies for package {} installed", pkg);
} else {
crash!(
AppExitCode::PacmanError,
"AUR dependencies failed to install"
);
}
}
// Install package explicitly
info!("Installing {}", pkg);
let cmd = std::process::Command::new("bash")
.args(&[
"-c",
format!(
"sudo pacman -U {}/{}/*.pkg.tar.* {}",
cachedir,
pkg,
if options.noconfirm { "--noconfirm" } else { "" }
)
.as_str(),
])
.spawn()
.unwrap()
.wait()
.unwrap();
if cmd.success() {
info!("{} installed!", pkg);
} else {
crash!(AppExitCode::PacmanError, "{} failed to install", pkg);
}
}
/// Clone a package from the AUR
fn clone(pkg: &String, pkgcache: &str, options: &Options) {
let url = crate::internal::rpc::URL;
// See if package is already cloned to AUR_CACHE
let dirs = list(pkgcache);
if dirs.contains(pkg) {
// Enter directory and git pull
if options.verbosity > 1 {
log!("Updating cached PKGBUILD for {}", pkg);
}
info!("Updating cached package source");
set_current_dir(Path::new(&format!(
"{}/{}/{}",
env::var("HOME").unwrap(),
AUR_CACHE,
pkg
)))
.unwrap();
ShellCommand::git()
.arg("pull")
.wait()
.silent_unwrap(AppExitCode::GitError);
} else {
// Clone package into cachedir
if options.verbosity >= 1 {
log!("Cloning {} into cachedir", pkg);
}
info!("Cloning package source");
set_current_dir(Path::new(&pkgcache)).unwrap();
ShellCommand::git()
.arg("clone")
.arg(format!("{}/{}", url, pkg))
.wait()
.silent_unwrap(AppExitCode::GitError);
// Enter directory and `makepkg -o` to fetch sources
if options.verbosity > 1 {
log!("Fetching sources for {}", pkg);
}
info!("Fetching sources");
set_current_dir(Path::new(&format!(
"{}/{}/{}",
env::var("HOME").unwrap(),
AUR_CACHE,
pkg
)))
.unwrap();
ShellCommand::makepkg()
.arg("-od")
.wait()
.silent_unwrap(AppExitCode::MakePkgError);
}
}
/// General function to handle installing AUR packages.
pub fn aur_install(a: Vec<String>, options: Options, orig_cachedir: &str) {
// Initialise variables
let cachedir = if options.asdeps || !orig_cachedir.is_empty() {
orig_cachedir.to_string()
} else {
mktemp()
};
let pkgcache = format!("{}/{}", env::var("HOME").unwrap(), AUR_CACHE);
let verbosity = options.verbosity;
let noconfirm = options.noconfirm;
if verbosity >= 1 {
log!("Installing from AUR: {:?}", &a);
}
info!("Installing packages {} from the AUR", a.join(", "));
let mut failed: Vec<String> = vec![];
for package in a {
// Don't process packages if they are already in the cachedir
let dirs = list(&cachedir);
if dirs.contains(&package) {
continue;
}
// Query AUR for package info
let rpcres = rpcinfo(&package);
if !rpcres.found {
// If package isn't found, break
break;
}
// Get package name
let pkg = &rpcres.package.as_ref().unwrap().name;
let ood = rpcres.package.as_ref().unwrap().out_of_date;
// If package is out of date, warn user
if ood.is_some() {
warn!(
"Package {} is marked as out of date since [{}], it might be broken, not install or not build properly",
pkg,
Local.timestamp(ood.unwrap().try_into().unwrap(), 0).date_naive()
);
let p = prompt!(default false, "Would you like to continue?");
if !p {
break;
}
}
// Clone package into cachedir
clone(pkg, &pkgcache, &options);
// Copy package from AUR_CACHE to cachedir
Command::new("cp")
.arg("-r")
.arg(format!(
"{}/{}/{}",
env::var("HOME").unwrap(),
AUR_CACHE,
pkg
))
.arg(format!("{}/{}", cachedir, pkg))
.spawn()
.unwrap()
.wait()
.unwrap();
// Sort dependencies and makedepends
if verbosity >= 1 {
log!("Sorting dependencies and makedepends");
}
let mut sorted = sort(&rpcres.package.as_ref().unwrap().depends, options);
let mut md_sorted = sort(&rpcres.package.as_ref().unwrap().make_depends, options);
if verbosity >= 1 {
log!("Sorted dependencies for {} are:\n{:?}", pkg, &sorted);
log!("Sorted makedepends for {} are:\n{:?}", pkg, &md_sorted);
}
// If any dependencies are not found in AUR or repos, crash
if !sorted.nf.is_empty() || !md_sorted.nf.is_empty() {
crash!(
AppExitCode::MissingDeps,
"Could not find dependencies {} for package {}, aborting",
sorted.nf.join(", "),
pkg,
);
}
// Create newopts struct for installing dependencies
let newopts = Options {
verbosity,
noconfirm,
asdeps: true,
};
// Get a list of installed packages
let installed = ShellCommand::pacman()
.elevated()
.args(&["-Qq"])
.wait_with_output()
.silent_unwrap(AppExitCode::PacmanError)
.stdout
.split_whitespace()
.collect::<Vec<&str>>()
.iter()
.map(|s| (*s).to_string())
.collect::<Vec<String>>();
// Remove installed packages from sorted dependencies and makedepends
if verbosity >= 1 {
log!("Removing installed packages from sorted dependencies and makedepends");
}
sorted.aur.retain(|x| !installed.contains(x));
sorted.repo.retain(|x| !installed.contains(x));
md_sorted.aur.retain(|x| !installed.contains(x));
md_sorted.repo.retain(|x| !installed.contains(x));
// Prompt user to review/edit PKGBUILD
if !noconfirm {
review(&cachedir, pkg, orig_cachedir);
}
// Install dependencies and makedepends
info!("Moving on to install dependencies");
if !sorted.repo.is_empty() {
install(&sorted.repo, newopts);
}
if !sorted.aur.is_empty() {
aur_install(sorted.aur, newopts, &cachedir.clone());
}
if !md_sorted.repo.is_empty() {
install(&md_sorted.repo, newopts);
}
if !md_sorted.aur.is_empty() {
aur_install(md_sorted.aur, newopts, &cachedir.clone());
}
// Build makepkg args
let mut makepkg_args = vec!["-rcd", "--skippgp", "--needed"];
if options.asdeps {
makepkg_args.push("--asdeps");
}
if options.noconfirm {
makepkg_args.push("--noconfirm");
}
// Enter cachedir and build package
info!("Building time!");
set_current_dir(format!("{}/{}", cachedir, pkg)).unwrap();
let status = ShellCommand::makepkg()
.args(makepkg_args)
.wait()
.silent_unwrap(AppExitCode::MakePkgError);
if !status.success() {
// If build failed, push to failed vec
failed.push(pkg.clone());
return;
}
// Return to cachedir
set_current_dir(&cachedir).unwrap();
// Finish installation process
if !options.asdeps {
finish(&cachedir, pkg, &options);
}
}
// If any packages failed to build, warn user with failed packages
if !failed.is_empty() {
let failed_str = format!("{}.failed", cachedir);
warn!(
"Failed to build packages {}, keeping cache directory at {} for manual inspection",
failed.join(", "),
if orig_cachedir.is_empty() {
&cachedir
} else {
&failed_str
}
);
if orig_cachedir.is_empty() {
Command::new("mv")
.args(&[&cachedir, &format!("{}.failed", cachedir)])
.spawn()
.unwrap()
.wait()
.unwrap();
}
} else if !options.asdeps && orig_cachedir.is_empty() {
rm_rf::remove(&cachedir).unwrap_or_else(|e|
crash!(AppExitCode::Other, "Could not remove cache directory at {}: {}. This could be a permissions issue with fakeroot, try running `sudo rm -rf {}`", cachedir, e, cachedir)
);
}
}

@ -0,0 +1,75 @@
use aur_rpc::PackageInfo;
use futures::future;
use crate::{
builder::{makepkg::MakePkgBuilder, pacman::PacmanInstallBuilder},
internal::{dependencies::DependencyInformation, error::AppResult},
multi_progress, normal_output, numeric,
operations::{
aur_install::common::{build_and_install, create_dependency_batches, download_aur_source},
BuildContext,
},
};
use super::aur_package_install::AurPackageInstall;
pub struct AurDependencyInstallation {
pub options: crate::internal::structs::Options,
pub dependencies: Vec<DependencyInformation>,
pub contexts: Vec<BuildContext>,
}
impl AurDependencyInstallation {
#[tracing::instrument(level = "trace", skip_all)]
pub async fn install_aur_dependencies(self) -> AppResult<AurPackageInstall> {
let aur_dependencies: Vec<&PackageInfo> = self
.dependencies
.iter()
.flat_map(DependencyInformation::all_aur_depends)
.collect();
if !aur_dependencies.is_empty() {
tracing::info!(
"Installing {} from the aur",
numeric!(aur_dependencies.len(), "package"["s"])
);
let batches = create_dependency_batches(aur_dependencies);
tracing::debug!("aur install batches: {batches:?}");
for batch in batches {
self.install(batch).await.unwrap();
}
}
Ok(AurPackageInstall {
options: self.options,
dependencies: self.dependencies,
contexts: self.contexts,
})
}
#[tracing::instrument(level = "trace", skip(self))]
async fn install(&self, deps: Vec<&PackageInfo>) -> AppResult<()> {
multi_progress!();
let dep_contexts = future::try_join_all(
deps.into_iter()
.map(BuildContext::from)
.map(download_aur_source),
)
.await?;
normal_output!();
build_and_install(
dep_contexts,
MakePkgBuilder::default().as_deps(true),
PacmanInstallBuilder::default()
.no_confirm(self.options.noconfirm)
.as_deps(true),
)
.await?;
Ok(())
}
}

@ -0,0 +1,44 @@
use aur_rpc::PackageInfo;
use futures::future;
use crate::{
internal::{dependencies::DependencyInformation, error::AppResult, structs::Options},
multi_progress, normal_output,
operations::BuildContext,
};
use super::aur_review::AurReview;
pub struct AurDownload {
pub options: Options,
pub package_infos: Vec<PackageInfo>,
pub packages: Vec<String>,
pub dependencies: Vec<DependencyInformation>,
}
impl AurDownload {
#[tracing::instrument(level = "trace", skip_all)]
pub async fn download_sources(self) -> AppResult<AurReview> {
tracing::info!("Downloading sources");
multi_progress!();
let contexts = future::try_join_all(
self.package_infos
.into_iter()
.map(BuildContext::from)
.map(super::common::download_aur_source),
)
.await?;
normal_output!();
tracing::info!("All sources are ready.");
Ok(AurReview {
options: self.options,
packages: self.packages,
dependencies: self.dependencies,
contexts,
})
}
}

@ -0,0 +1,76 @@
use crossterm::style::Stylize;
use futures::future;
use crate::{
internal::{
dependencies::DependencyInformation,
error::{AppError, AppResult},
structs::Options,
},
logging::output::{print_aur_package_list, print_dependency_list},
normal_output, prompt, spinner,
};
use super::aur_download::AurDownload;
pub struct AurFetch {
pub options: Options,
pub packages: Vec<String>,
}
impl AurFetch {
#[tracing::instrument(level = "trace", skip_all)]
pub async fn fetch_package_info(self) -> AppResult<AurDownload> {
let pb = spinner!("Fetching package information");
let package_infos = aur_rpc::info(&self.packages).await?;
tracing::debug!("package info = {package_infos:?}");
if package_infos.len() != self.packages.len() {
pb.finish_with_message("Couldn't find all packages".red().to_string());
let mut not_found = self.packages.clone();
package_infos
.iter()
.for_each(|pkg| not_found.retain(|p| pkg.metadata.name != *p));
return Err(AppError::MissingDependencies(not_found));
}
pb.finish_with_message("All packages found".green().to_string());
normal_output!();
if print_aur_package_list(&package_infos.iter().collect::<Vec<_>>()).await
&& !self.options.noconfirm
&& !prompt!(default yes, "Some packages are already installed. Continue anyway?")
{
return Err(AppError::UserCancellation);
}
let pb = spinner!("Fetching package information");
let dependencies = future::try_join_all(
package_infos
.iter()
.map(|pkg| async { DependencyInformation::for_package(pkg).await }),
)
.await?;
pb.finish_and_clear();
normal_output!();
print_dependency_list(&dependencies).await;
if !self.options.noconfirm
&& !prompt!(default yes, "Do you want to install these packages and package dependencies?")
{
Err(AppError::UserCancellation)
} else {
Ok(AurDownload {
options: self.options,
packages: self.packages,
package_infos,
dependencies,
})
}
}
}

@ -0,0 +1,37 @@
use crate::{
builder::{makepkg::MakePkgBuilder, pacman::PacmanInstallBuilder},
internal::{dependencies::DependencyInformation, error::AppResult, structs::Options},
numeric,
operations::aur_install::{
common::build_and_install, make_dependency_removal::MakeDependencyRemoval,
},
};
use super::BuildContext;
pub struct AurPackageInstall {
pub options: Options,
pub dependencies: Vec<DependencyInformation>,
pub contexts: Vec<BuildContext>,
}
impl AurPackageInstall {
#[tracing::instrument(level = "trace", skip_all)]
pub async fn install_packages(self) -> AppResult<MakeDependencyRemoval> {
tracing::info!(
"Installing {}",
numeric!(self.contexts.len(), "package"["s"])
);
build_and_install(
self.contexts,
MakePkgBuilder::default(),
PacmanInstallBuilder::default().no_confirm(self.options.noconfirm),
)
.await?;
Ok(MakeDependencyRemoval {
options: self.options,
dependencies: self.dependencies,
})
}
}

@ -0,0 +1,77 @@
use tokio::fs;
use crate::{
builder::pager::PagerBuilder,
internal::{
dependencies::DependencyInformation,
error::{AppError, AppResult},
structs::Options,
utils::get_cache_dir,
},
multi_select, newline, prompt, select_opt,
};
use super::{repo_dependency_installation::RepoDependencyInstallation, BuildContext};
pub struct AurReview {
pub options: Options,
pub packages: Vec<String>,
pub dependencies: Vec<DependencyInformation>,
pub contexts: Vec<BuildContext>,
}
impl AurReview {
#[tracing::instrument(level = "trace", skip_all)]
pub async fn review_pkgbuild(self) -> AppResult<RepoDependencyInstallation> {
if !self.options.noconfirm {
let to_review = multi_select!(&self.packages, "Select packages to review");
for pkg in to_review.into_iter().filter_map(|i| self.packages.get(i)) {
self.review_single_package(pkg).await?;
}
if !prompt!(default yes, "Do you still want to install those packages?") {
return Err(AppError::UserCancellation);
}
}
Ok(RepoDependencyInstallation {
options: self.options,
dependencies: self.dependencies,
contexts: self.contexts,
})
}
async fn review_single_package(&self, pkg: &str) -> AppResult<()> {
newline!();
tracing::info!("Reviewing {pkg}");
let mut files_iter = fs::read_dir(get_cache_dir().join(pkg)).await?;
let mut files = Vec::new();
while let Some(file) = files_iter.next_entry().await? {
let path = file.path();
if path.is_file() {
files.push(file.path());
}
}
let file_names = files
.iter()
.map(|f| f.file_name().unwrap())
.map(|f| f.to_string_lossy())
.collect::<Vec<_>>();
while let Some(selection) = select_opt!(&file_names, "Select a file to review") {
if let Some(path) = files.get(selection) {
if let Err(e) = PagerBuilder::default().path(path).open().await {
tracing::debug!("Pager error {e}");
}
} else {
break;
}
}
tracing::info!("Done reviewing {pkg}");
Ok(())
}
}

@ -0,0 +1,294 @@
use std::{collections::HashMap, path::Path, sync::Arc};
use aur_rpc::PackageInfo;
use crossterm::style::Stylize;
use futures::future;
use indicatif::ProgressBar;
use tokio::{
fs::OpenOptions,
io::{AsyncWriteExt, BufWriter},
process::{ChildStderr, ChildStdout},
task,
};
use crate::{
builder::{
git::{GitCloneBuilder, GitPullBuilder},
makepkg::MakePkgBuilder,
pacman::PacmanInstallBuilder,
pager::PagerBuilder,
},
internal::{
error::{AppError, AppResult},
utils::{get_cache_dir, wrap_text},
},
logging::piped_stdio::StdioReader,
multi_progress, normal_output, numeric,
operations::PackageArchives,
prompt, spinner,
};
use super::{BuildContext, BuildPath, BuildStep};
#[tracing::instrument(level = "trace", skip_all)]
pub async fn download_aur_source(mut ctx: BuildContext) -> AppResult<BuildContext> {
let pkg_name = &ctx.package.metadata.name;
let base_pkg = &ctx.package.metadata.package_base;
let pb = spinner!("{}: Downloading sources", pkg_name.clone().bold());
let cache_dir = get_cache_dir();
let pkg_dir = cache_dir.join(&pkg_name);
if pkg_dir.exists() {
pb.set_message(format!(
"{}: Pulling latest changes",
pkg_name.clone().bold()
));
GitPullBuilder::default().directory(&pkg_dir).pull().await?;
} else {
let aur_url = crate::internal::rpc::URL;
let repository_url = format!("{aur_url}/{base_pkg}");
pb.set_message(format!(
"{}: Cloning aur repository",
pkg_name.clone().bold()
));
GitCloneBuilder::default()
.url(repository_url)
.directory(&pkg_dir)
.clone()
.await?;
pb.set_message(format!(
"{}: Downloading and extracting files",
pkg_name.clone().bold()
));
MakePkgBuilder::default()
.directory(&pkg_dir)
.no_build(true)
.no_deps(true)
.no_prepare(true)
.skip_pgp(true)
.run()
.await?;
}
pb.finish_with_message(format!(
"{}: {}",
pkg_name.clone().bold(),
"Downloaded!".green()
));
ctx.step = BuildStep::Build(BuildPath(pkg_dir));
Ok(ctx)
}
#[tracing::instrument(level = "trace")]
pub fn create_dependency_batches(deps: Vec<&PackageInfo>) -> Vec<Vec<&PackageInfo>> {
let mut deps: HashMap<String, &PackageInfo> = deps
.into_iter()
.map(|d| (d.metadata.name.clone(), d))
.collect();
let mut batches = Vec::new();
let mut relaxed = false;
while !deps.is_empty() {
let mut current_batch = HashMap::new();
for (key, info) in deps.clone() {
let contains_make_dep = info
.make_depends
.iter()
.any(|d| current_batch.contains_key(d) || deps.contains_key(d));
let contains_dep = info
.depends
.iter()
.any(|d| current_batch.contains_key(d) || deps.contains_key(d));
if (!contains_dep || relaxed) && contains_make_dep {
deps.remove(&key);
current_batch.insert(key, info);
if relaxed {
break;
}
}
}
if current_batch.is_empty() {
relaxed = true;
} else {
batches.push(current_batch.into_iter().map(|(_, v)| v).collect());
relaxed = false;
}
}
batches
}
#[tracing::instrument(level = "trace")]
pub async fn build_and_install(
ctxs: Vec<BuildContext>,
make_opts: MakePkgBuilder,
install_opts: PacmanInstallBuilder,
) -> AppResult<()> {
tracing::info!("Building packages");
multi_progress!();
let results = future::join_all(
ctxs.into_iter()
.map(|ctx| build_package(ctx, make_opts.clone())),
)
.await;
normal_output!();
let mut ctxs = Vec::new();
for result in results {
match result {
Ok(ctx) => ctxs.push(ctx),
Err(e) => handle_build_error(e).await?,
}
}
tracing::info!("Built {}", numeric!(ctxs.len(), "package"["s"]));
tracing::info!("Installing packages");
install_packages(ctxs, install_opts).await?;
Ok(())
}
#[tracing::instrument(level = "trace")]
async fn build_package(
mut ctx: BuildContext,
make_opts: MakePkgBuilder,
) -> AppResult<BuildContext> {
let pkg_name = &ctx.package.metadata.name;
let build_path = ctx.build_path()?;
let pb = spinner!("{}: Building Package", pkg_name.as_str().bold());
let mut child = make_opts
.directory(build_path)
.clean(true)
.no_deps(true)
.skip_pgp(true)
.needed(true)
.force(true)
.spawn()?;
let stderr = child.stderr.take().unwrap();
let stdout = child.stdout.take().unwrap();
let handle = task::spawn({
let pb = pb.clone();
let pkg_name = pkg_name.clone();
async move { show_and_log_stdio(stdout, stderr, pb, pkg_name).await }
});
let exit_status = child.wait().await?;
handle.abort();
if !exit_status.success() {
pb.finish_with_message(format!(
"{}: {}",
pkg_name.as_str().bold(),
"Build failed!".red(),
));
return Err(AppError::BuildError {
pkg_name: pkg_name.to_owned(),
});
}
let mut packages = MakePkgBuilder::package_list(build_path).await?;
let match_version = ctx
.package
.metadata
.version
.rsplit_once('_')
.map(|v| v.0)
.unwrap_or(&ctx.package.metadata.version);
let match_name = format!("{pkg_name}-{match_version}");
tracing::debug!("Match name {match_name}");
packages.retain(|name| {
name.file_name()
.and_then(|n| n.to_str())
.unwrap()
.starts_with(&match_name)
});
tracing::debug!("Archives: {packages:?}");
pb.finish_with_message(format!("{}: {}", pkg_name.clone().bold(), "Built!".green()));
ctx.step = BuildStep::Install(PackageArchives(packages));
Ok(ctx)
}
#[tracing::instrument(level = "trace")]
async fn install_packages(
mut ctxs: Vec<BuildContext>,
install_opts: PacmanInstallBuilder,
) -> AppResult<Vec<BuildContext>> {
let mut packages = Vec::new();
for ctx in &mut ctxs {
packages.append(&mut ctx.packages()?.clone());
ctx.step = BuildStep::Done;
}
install_opts.files(packages).needed(false).install().await?;
Ok(ctxs)
}
#[tracing::instrument(level = "trace")]
async fn show_and_log_stdio(
stdout: ChildStdout,
stderr: ChildStderr,
pb: Arc<ProgressBar>,
package_name: String,
) -> AppResult<()> {
let mut reader = StdioReader::new(stdout, stderr);
let out_file = get_cache_dir().join(format!("{package_name}-build.log"));
let mut out_writer = BufWriter::new(
OpenOptions::new()
.create(true)
.write(true)
.open(out_file)
.await?,
);
while let Ok(line) = reader.read_line().await {
let _ = out_writer.write(line.as_bytes()).await?;
let _ = out_writer.write(&[b'\n']).await?;
tracing::trace!("{package_name}: {line}");
let line = format!("{}: {}", package_name.clone().bold(), line);
let lines = wrap_text(line);
let line = lines.into_iter().next().unwrap();
pb.set_message(line);
}
out_writer.flush().await?;
Ok(())
}
#[tracing::instrument(level = "trace", skip_all)]
async fn handle_build_error<E: Into<AppError>>(err: E) -> AppResult<()> {
normal_output!();
let err = err.into();
match err {
AppError::BuildError { pkg_name } => {
tracing::error!("Failed to build package {pkg_name}!");
let log_path = get_cache_dir().join(format!("{pkg_name}-build.log"));
review_build_log(&log_path).await?;
Ok(())
}
e => Err(e),
}
}
#[tracing::instrument(level = "trace")]
async fn review_build_log(log_file: &Path) -> AppResult<()> {
if prompt!(default yes, "Do you want to review the build log?") {
PagerBuilder::default().path(log_file).open().await?;
}
Ok(())
}

@ -0,0 +1,35 @@
use crate::{
builder::pacman::PacmanUninstallBuilder,
internal::{dependencies::DependencyInformation, error::AppResult, structs::Options},
prompt,
};
pub struct MakeDependencyRemoval {
pub options: Options,
pub dependencies: Vec<DependencyInformation>,
}
impl MakeDependencyRemoval {
#[tracing::instrument(level = "trace", skip_all)]
pub async fn remove_make_deps(self) -> AppResult<()> {
let make_depends = self
.dependencies
.iter()
.flat_map(DependencyInformation::make_depends)
.collect::<Vec<_>>();
if !make_depends.is_empty()
&& !self.options.noconfirm
&& prompt!(default yes, "Do you want to remove the installed make dependencies?")
{
PacmanUninstallBuilder::default()
.packages(make_depends)
.no_confirm(true)
.uninstall()
.await?;
}
tracing::info!("Done!");
Ok(())
}
}

@ -0,0 +1,142 @@
use aur_rpc::PackageInfo;
use std::path::{Path, PathBuf};
use crate::internal::error::{AppError, AppResult};
use crate::internal::exit_code::AppExitCode;
use crate::{cancelled, crash, Options};
use self::aur_fetch::AurFetch;
mod aur_dependency_installation;
mod aur_download;
mod aur_fetch;
mod aur_package_install;
mod aur_review;
mod common;
mod make_dependency_removal;
mod repo_dependency_installation;
#[derive(Debug)]
pub struct BuildContext {
pub package: PackageInfo,
pub step: BuildStep,
}
#[derive(Debug)]
pub enum BuildStep {
Download,
Build(BuildPath),
Install(PackageArchives),
Done,
}
#[derive(Debug)]
pub struct BuildPath(pub PathBuf);
#[derive(Debug)]
pub struct PackageArchives(pub Vec<PathBuf>);
impl From<PackageInfo> for BuildContext {
fn from(package: PackageInfo) -> Self {
Self {
package,
step: BuildStep::Download,
}
}
}
impl From<&PackageInfo> for BuildContext {
fn from(p: &PackageInfo) -> Self {
Self::from(p.to_owned())
}
}
impl BuildContext {
pub fn build_path(&self) -> AppResult<&Path> {
if let BuildStep::Build(path) = &self.step {
Ok(&path.0)
} else {
Err(AppError::BuildStepViolation)
}
}
pub fn packages(&self) -> AppResult<&Vec<PathBuf>> {
if let BuildStep::Install(pkgs) = &self.step {
Ok(&pkgs.0)
} else {
Err(AppError::BuildStepViolation)
}
}
}
pub struct AurInstall {
options: Options,
packages: Vec<String>,
}
impl AurInstall {
pub fn new(options: Options, packages: Vec<String>) -> Self {
Self { options, packages }
}
pub fn start(self) -> AurFetch {
tracing::debug!("Installing from AUR: {:?}", &self.packages);
AurFetch {
options: self.options,
packages: self.packages,
}
}
}
/// Installs a given list of packages from the aur
#[tracing::instrument(level = "trace")]
pub async fn aur_install(packages: Vec<String>, options: Options) {
if let Err(e) = aur_install_internal(AurInstall::new(options, packages)).await {
match e {
AppError::Rpc(e) => {
crash!(AppExitCode::RpcError, "AUR RPC Call failed with {e}")
}
AppError::BuildStepViolation => {
crash!(AppExitCode::MakePkgError, "Failed to build")
}
AppError::BuildError { pkg_name } => {
crash!(AppExitCode::MakePkgError, "Failed to build {pkg_name}")
}
AppError::UserCancellation => {
cancelled!();
}
AppError::MissingDependencies(deps) => {
crash!(
AppExitCode::MissingDeps,
"Missing dependencies {}",
deps.join(", ")
)
}
AppError::MakePkg(msg) => {
crash!(AppExitCode::MakePkgError, "makepgk failed {msg}")
}
_ => crash!(AppExitCode::Other, "Unknown error"),
}
}
}
async fn aur_install_internal(install: AurInstall) -> AppResult<()> {
install
.start()
.fetch_package_info()
.await?
.download_sources()
.await?
.review_pkgbuild()
.await?
.install_repo_dependencies()
.await?
.install_aur_dependencies()
.await?
.install_packages()
.await?
.remove_make_deps()
.await
}

@ -0,0 +1,40 @@
use std::collections::HashSet;
use crate::{
builder::pacman::PacmanInstallBuilder,
internal::{dependencies::DependencyInformation, error::AppResult, structs::Options},
};
use super::{aur_dependency_installation::AurDependencyInstallation, BuildContext};
pub struct RepoDependencyInstallation {
pub options: Options,
pub dependencies: Vec<DependencyInformation>,
pub contexts: Vec<BuildContext>,
}
impl RepoDependencyInstallation {
#[tracing::instrument(level = "trace", skip_all)]
pub async fn install_repo_dependencies(self) -> AppResult<AurDependencyInstallation> {
let repo_dependencies: HashSet<&str> = self
.dependencies
.iter()
.flat_map(DependencyInformation::all_repo_depends)
.collect();
if !repo_dependencies.is_empty() {
tracing::info!("Installing repo dependencies");
PacmanInstallBuilder::default()
.as_deps(true)
.packages(repo_dependencies)
.no_confirm(self.options.noconfirm)
.install()
.await?;
}
Ok(AurDependencyInstallation {
options: self.options,
dependencies: self.dependencies,
contexts: self.contexts,
})
}
}

@ -1,38 +1,40 @@
use std::process::Command; use tokio::process::Command;
use crate::crash; use crate::crash;
use crate::info;
use crate::internal::commands::ShellCommand; use crate::internal::commands::ShellCommand;
use crate::internal::error::SilentUnwrap; use crate::internal::error::SilentUnwrap;
use crate::internal::exit_code::AppExitCode; use crate::internal::exit_code::AppExitCode;
use crate::log;
use crate::internal::utils::get_cache_dir;
use crate::prompt; use crate::prompt;
use crate::Options; use crate::Options;
/// Help the user in clearing orphaned packages and pacman cache. /// Removes orphaned packages and cache
pub fn clean(options: Options) { #[tracing::instrument(level = "trace")]
let verbosity = options.verbosity; pub async fn clean(options: Options) {
let noconfirm = options.noconfirm; let noconfirm = options.noconfirm;
// Check for orphaned packages // Check for orphaned packages
let orphaned_packages = ShellCommand::pacman() let orphaned_packages = ShellCommand::pacman()
.arg("-Qdtq") .arg("-Qdtq")
.wait_with_output() .wait_with_output()
.await
.silent_unwrap(AppExitCode::PacmanError); .silent_unwrap(AppExitCode::PacmanError);
if orphaned_packages.stdout.as_str().is_empty() { if orphaned_packages.stdout.as_str().is_empty() {
// If no orphaned packages found, do nothing // If no orphaned packages found, do nothing
info!("No orphaned packages found"); tracing::info!("No orphaned packages found");
} else { } else {
// Prompt users whether to remove orphaned packages // Prompt users whether to remove orphaned packages
info!( tracing::info!(
"Removing orphans would uninstall the following packages: \n{}", "Removing orphans would uninstall the following packages: \n{}",
&orphaned_packages.stdout &orphaned_packages.stdout
); );
let cont = prompt!(default false, "Continue?"); let cont = prompt!(default no, "Continue?");
if !cont { if !cont {
// If user doesn't want to continue, break // If user doesn't want to continue, break
info!("Exiting"); tracing::info!("Exiting");
std::process::exit(AppExitCode::PacmanError as i32); std::process::exit(AppExitCode::PacmanError as i32);
} }
@ -50,20 +52,19 @@ pub fn clean(options: Options) {
} }
} }
if verbosity >= 1 { tracing::debug!("Removing orphans: {:?}", orphaned_packages_vec);
log!("Removing orphans: {:?}", orphaned_packages_vec);
}
// Remove orphaned packages // Remove orphaned packages
let pacman_result = ShellCommand::pacman() let pacman_result = ShellCommand::pacman()
.elevated() .elevated()
.args(pacman_args) .args(pacman_args)
.wait() .wait()
.await
.silent_unwrap(AppExitCode::PacmanError); .silent_unwrap(AppExitCode::PacmanError);
if pacman_result.success() { if pacman_result.success() {
// If pacman succeeded, notify user // If pacman succeeded, notify user
info!("Successfully removed orphans"); tracing::info!("Successfully removed orphans");
} else { } else {
// If pacman failed, crash // If pacman failed, crash
crash!(AppExitCode::PacmanError, "Failed to remove orphans",); crash!(AppExitCode::PacmanError, "Failed to remove orphans",);
@ -71,15 +72,16 @@ pub fn clean(options: Options) {
} }
// Prompt the user whether to clear the Amethyst cache // Prompt the user whether to clear the Amethyst cache
let clear_ame_cache = prompt!(default false, "Clear Amethyst's internal PKGBUILD cache?"); let clear_ame_cache = prompt!(default no, "Clear Amethyst's internal PKGBUILD cache?");
if clear_ame_cache { if clear_ame_cache {
// Remove ~/.cache/ame let cache_dir = get_cache_dir();
Command::new("rm") ShellCommand::rm()
.arg("-rf") .arg(cache_dir)
.arg("~/.cache/ame") .arg("-r")
.spawn() .arg("-f")
.unwrap() .elevated()
.wait() .wait_success()
.await
.unwrap(); .unwrap();
} }
@ -87,7 +89,7 @@ pub fn clean(options: Options) {
let clear_pacman_cache = if noconfirm { let clear_pacman_cache = if noconfirm {
true true
} else { } else {
prompt!(default false, "Also clear pacman's package cache?") prompt!(default no, "Also clear pacman's package cache?")
}; };
if clear_pacman_cache { if clear_pacman_cache {
@ -103,9 +105,7 @@ pub fn clean(options: Options) {
paccache_args.push("--noconfirm"); paccache_args.push("--noconfirm");
} }
if verbosity >= 1 { tracing::debug!("Clearing using `paccache -r`");
log!("Clearing using `paccache -r`");
}
// Clear pacman's cache (keeping latest 3 versions of installed packages) // Clear pacman's cache (keeping latest 3 versions of installed packages)
Command::new("sudo") Command::new("sudo")
@ -120,22 +120,22 @@ pub fn clean(options: Options) {
) )
}) })
.wait() .wait()
.await
.unwrap(); .unwrap();
if verbosity >= 1 { tracing::debug!("Clearing using `pacman -Sc`");
log!("Clearing using `pacman -Sc`");
}
// Clear pacman's cache (keeping only installed packages) // Clear pacman's cache (keeping only installed packages)
let pacman_result = ShellCommand::pacman() let pacman_result = ShellCommand::pacman()
.elevated() .elevated()
.args(pacman_args) .args(pacman_args)
.wait() .wait()
.await
.silent_unwrap(AppExitCode::PacmanError); .silent_unwrap(AppExitCode::PacmanError);
if pacman_result.success() { if pacman_result.success() {
// If pacman succeeded, notify user // If pacman succeeded, notify user
info!("Successfully cleared package cache"); tracing::info!("Successfully cleared package cache");
} else { } else {
// If pacman failed, crash // If pacman failed, crash
crash!(AppExitCode::PacmanError, "Failed to clear package cache",); crash!(AppExitCode::PacmanError, "Failed to clear package cache",);

@ -1,36 +1,20 @@
use crate::internal::commands::ShellCommand; use crate::builder::pacman::PacmanInstallBuilder;
use crate::internal::error::SilentUnwrap;
use crate::internal::exit_code::AppExitCode; use crate::internal::exit_code::AppExitCode;
use crate::{crash, info, log, Options}; use crate::{crash, Options};
/// Help the user install a package from the pacman repos #[tracing::instrument(level = "trace")]
pub fn install(packages: &[String], options: Options) { pub async fn install(packages: Vec<String>, options: Options) {
info!("Installing packages {} from repos", &packages.join(", ")); tracing::info!("Installing packages {} from repos", &packages.join(", "));
// Build pacman args
let mut opers = vec!["-S", "--needed"];
if options.noconfirm {
opers.push("--noconfirm");
}
if options.asdeps {
opers.push("--asdeps");
}
let verbosity = options.verbosity;
if !packages.is_empty() { if !packages.is_empty() {
if verbosity >= 1 { tracing::debug!("Installing from repos: {:?}", &packages);
log!("Installing from repos: {:?}", &packages);
} let result = PacmanInstallBuilder::from_options(options)
.packages(packages.clone())
.install()
.await;
// Install packages if result.is_err() {
let status = ShellCommand::pacman()
.elevated()
.args(opers)
.args(packages)
.wait()
.silent_unwrap(AppExitCode::PacmanError);
if !status.success() {
// If pacman failed, crash
crash!( crash!(
AppExitCode::PacmanError, AppExitCode::PacmanError,
"An error occured while installing packages: {}, aborting", "An error occured while installing packages: {}, aborting",
@ -38,8 +22,6 @@ pub fn install(packages: &[String], options: Options) {
); );
} }
if verbosity >= 1 { tracing::debug!("Installing packages: {:?} was successful", &packages);
log!("Installing packages: {:?} was successful", &packages);
}
} }
} }

@ -1,7 +1,7 @@
pub use aur_install::*; pub use aur_install::*;
pub use clean::*; pub use clean::*;
pub use install::*; pub use install::*;
pub use search::{aur_search, repo_search as search}; pub use search::{aur_search, repo_search as search, SearchBy};
pub use uninstall::*; pub use uninstall::*;
pub use upgrade::*; pub use upgrade::*;

@ -1,146 +1,97 @@
use chrono::{Local, TimeZone}; use std::str::FromStr;
use colored::Colorize;
use textwrap::wrap;
use crate::internal::commands::ShellCommand; use crate::internal::commands::ShellCommand;
use crate::internal::error::SilentUnwrap; use crate::internal::error::SilentUnwrap;
use crate::internal::exit_code::AppExitCode; use crate::internal::exit_code::AppExitCode;
use crate::internal::rpc::rpcsearch; use crate::internal::rpc::rpcsearch;
use crate::{log, Options}; use crate::Options;
use aur_rpc::SearchField;
#[allow(clippy::module_name_repetitions)]
/// Searches for packages from the AUR and returns wrapped results #[tracing::instrument(level = "trace")]
pub fn aur_search(query: &str, options: Options) -> String { pub async fn aur_search(query: &str, by_field: Option<SearchBy>, options: Options) {
// Query AUR for package info let packages = rpcsearch(query.to_string(), by_field.map(SearchBy::into))
let res = rpcsearch(query); .await
.silent_unwrap(AppExitCode::RpcError);
// Get verbosity let total_results = packages.len();
let verbosity = options.verbosity;
for package in &packages {
// Format output println!(
let mut results_vec = vec![]; "aur/{} {}\n {}",
for package in &res.results { package.name, package.version, package.description
// Define wrapping options
let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 4)
.subsequent_indent(" ");
let result = format!(
"{}{} {} {}\n {}",
"aur/".cyan().bold(),
package.name.bold(),
package.version.green().bold(),
if package.out_of_date.is_some() {
format!(
"[out of date: since {}]",
Local
.timestamp(package.out_of_date.unwrap().try_into().unwrap(), 0)
.date_naive()
)
.red()
.bold()
} else {
"".bold()
},
wrap(
package
.description
.as_ref()
.unwrap_or(&"No description".to_string()),
opts,
) )
.join("\n"),
);
results_vec.push(result);
} }
if verbosity > 1 { tracing::debug!("Found {total_results} resuls for \"{query}\" in AUR",);
log!(
"Found {} results for \"{}\" in the AUR",
res.results.len(),
query
);
}
results_vec.join("\n")
} }
struct SearchResult { #[tracing::instrument(level = "trace")]
repo: String, pub async fn repo_search(query: &str, options: Options) {
name: String, let output = ShellCommand::pacman()
version: String, .arg("-Ss")
description: String,
}
#[allow(clippy::module_name_repetitions)]
/// Searches for packages from the repos and returns wrapped results
pub fn repo_search(query: &str, options: Options) -> String {
// Initialise variables
let verbosity = options.verbosity;
// Query pacman for package info
let output = ShellCommand::bash()
.args(&["-c", &format!("expac -Ss '%r\\\\%n\\\\%v\\\\%d' {}", query)])
.arg(query) .arg(query)
.wait_with_output() .wait_with_output()
.await
.silent_unwrap(AppExitCode::PacmanError) .silent_unwrap(AppExitCode::PacmanError)
.stdout; .stdout;
// Split output into lines tracing::debug!(
let lines = output.trim().split('\n'); "Found {} results for \"{}\" in repos",
&output.split('\n').count() / 2,
&query
);
// Initialise results vector println!("{}", output)
let mut results_vec: Vec<SearchResult> = vec![]; }
let clone = lines.clone().collect::<Vec<&str>>(); /// Represents a field to search by
if clone.len() == 1 && clone[0].is_empty() { #[derive(Debug, Clone, Copy)]
// If no results, return empty string pub enum SearchBy {
return "".to_string(); /// Searches by name
Name,
/// Searches name and description
NameDesc,
/// Searches by package maintainer
Maintainer,
/// Searches for packages that depend on the given keywods
Depends,
/// Searches for packages that require the given keywords to be build
MakeDepends,
/// Searches for packages that optionally depend on the given keywods
OptDepends,
/// Searches for packages that require the given keywods to be present
CheckDepends,
} }
// Iterate over lines impl FromStr for SearchBy {
for line in lines { type Err = String;
let parts: Vec<&str> = line.split('\\').collect();
let res = SearchResult { fn from_str(s: &str) -> Result<Self, Self::Err> {
repo: parts[0].to_string(), let arg = match s {
name: parts[1].to_string(), "name" => Self::Name,
version: parts[2].to_string(), "name-desc" => Self::NameDesc,
description: parts[3].to_string(), "maintainer" => Self::Maintainer,
"depends" => Self::Depends,
"makedepends" | "make-depends" => Self::MakeDepends,
"optdepends" | "opt-depends" => Self::OptDepends,
"checkdepends" | "check-depends" => Self::CheckDepends,
directive => return Err(format!("Invalid search by directive '{directive}'")),
}; };
results_vec.push(res);
}
if verbosity >= 1 { Ok(arg)
log!( }
"Found {} results for \"{}\" in repos",
&results_vec.len(),
&query
);
} }
// Format output #[allow(clippy::from_over_into)]
let results_vec = results_vec impl Into<SearchField> for SearchBy {
.into_iter() fn into(self) -> SearchField {
.map(|res| { match self {
let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 4) SearchBy::Name => SearchField::Name,
.subsequent_indent(" "); SearchBy::NameDesc => SearchField::NameDesc,
format!( SearchBy::Maintainer => SearchField::Maintainer,
"{}{}{} {}\n {}", SearchBy::Depends => SearchField::Depends,
res.repo.purple().bold(), SearchBy::MakeDepends => SearchField::MakeDepends,
"/".purple().bold(), SearchBy::OptDepends => SearchField::OptDepends,
res.name.bold(), SearchBy::CheckDepends => SearchField::CheckDepends,
res.version.green().bold(), }
if res.description.is_empty() {
"No description".to_string()
} else {
wrap(&res.description, opts).join("\n")
},
)
})
.collect::<Vec<String>>();
if output.trim().is_empty() {
"".to_string()
} else {
results_vec.join("\n")
} }
} }

@ -1,29 +1,47 @@
use std::env;
use std::path::Path;
use tokio::fs;
use crate::internal::commands::ShellCommand; use crate::internal::commands::ShellCommand;
use crate::internal::error::SilentUnwrap; use crate::internal::error::SilentUnwrap;
use crate::internal::exit_code::AppExitCode; use crate::internal::exit_code::AppExitCode;
use crate::{log, Options}; use crate::Options;
/// Helps the user in uninstalling installed packages. #[tracing::instrument(level = "trace")]
pub fn uninstall(packages: &[String], options: Options) { pub async fn uninstall(packages: Vec<String>, options: Options) {
// Build pacman args
let mut pacman_args = vec!["-Rs"]; let mut pacman_args = vec!["-Rs"];
pacman_args.append(&mut packages.iter().map(String::as_str).collect()); pacman_args.append(&mut packages.iter().map(|s| s.as_str()).collect());
if options.noconfirm { if options.noconfirm {
pacman_args.push("--noconfirm"); pacman_args.push("--noconfirm");
} }
let verbosity = options.verbosity; tracing::debug!("Uninstalling: {:?}", &packages);
if verbosity >= 1 {
log!("Uninstalling: {:?}", &packages);
}
// Uninstall packages
ShellCommand::pacman() ShellCommand::pacman()
.elevated() .elevated()
.args(pacman_args) .args(pacman_args)
.wait_success() .wait_success()
.await
.silent_unwrap(AppExitCode::PacmanError); .silent_unwrap(AppExitCode::PacmanError);
if verbosity >= 1 { tracing::debug!("Uninstalling packages: {:?} exited with code 0", &packages);
log!("Uninstalling packages: {:?} exited with code 0", &packages);
for package in packages {
if Path::new(&format!(
"{}/.cache/ame/{}",
env::var("HOME").unwrap(),
package
))
.exists()
{
tracing::debug!("Old cache directory found, deleting");
fs::remove_dir_all(Path::new(&format!(
"{}/.cache/ame/{}",
env::var("HOME").unwrap(),
package
)))
.await
.unwrap();
}
} }
} }

@ -1,152 +1,93 @@
use crate::args::UpgradeArgs; use crate::args::UpgradeArgs;
use crate::builder::pacman::{PacmanColor, PacmanQueryBuilder};
use crate::internal::commands::ShellCommand; use crate::internal::commands::ShellCommand;
use crate::internal::detect;
use crate::internal::error::SilentUnwrap; use crate::internal::error::SilentUnwrap;
use crate::internal::exit_code::AppExitCode; use crate::internal::exit_code::AppExitCode;
use crate::internal::rpc::rpcinfo; use crate::internal::rpc::rpcinfo;
use crate::operations::aur_install::aur_install; use crate::operations::aur_install::aur_install;
use crate::{info, log, prompt, spinner, warn, Options}; use crate::{prompt, Options};
#[derive(Debug)] /// Upgrades all installed packages
struct QueriedPackage { #[tracing::instrument(level = "trace")]
pub name: String, pub async fn upgrade(args: UpgradeArgs, options: Options) {
pub version: String, if args.repo {
upgrade_repo(options).await;
}
if args.aur {
upgrade_aur(options).await;
}
if !args.aur && !args.repo {
upgrade_repo(options).await;
upgrade_aur(options).await;
}
} }
/// Helps the user upgrade installed packages, repo and AUR. #[tracing::instrument(level = "trace")]
pub fn upgrade(options: Options, args: UpgradeArgs, cachedir: &str) { async fn upgrade_repo(options: Options) {
// Initialise variables
let verbosity = options.verbosity;
let noconfirm = options.noconfirm; let noconfirm = options.noconfirm;
let args = if !args.aur && !args.repo {
UpgradeArgs {
aur: true,
repo: true,
}
} else {
args
};
if args.repo {
// Build pacman args
let mut pacman_args = vec!["-Syu"]; let mut pacman_args = vec!["-Syu"];
if noconfirm { if noconfirm {
pacman_args.push("--noconfirm"); pacman_args.push("--noconfirm");
} }
if verbosity >= 1 { tracing::debug!("Upgrading repo packages");
log!("Upgrading repo packages");
}
// Upgrade repo packages
let pacman_result = ShellCommand::pacman() let pacman_result = ShellCommand::pacman()
.elevated() .elevated()
.args(pacman_args) .args(pacman_args)
.wait() .wait()
.await
.silent_unwrap(AppExitCode::PacmanError); .silent_unwrap(AppExitCode::PacmanError);
if pacman_result.success() { if pacman_result.success() {
// If pacman was successful, notify user tracing::info!("Successfully upgraded repo packages");
info!("Successfully upgraded repo packages");
} else { } else {
// Otherwise warn user let continue_upgrading = prompt!(default no,
warn!("Failed to upgrade repo packages.",); "Failed to upgrade repo packages, continue to upgrading AUR packages?",
} );
} if !continue_upgrading {
tracing::info!("Exiting");
if args.repo && args.aur {
let cont = prompt!(default true, "Continue to upgrade AUR packages?");
if !cont {
// If user doesn't want to continue, break
info!("Exiting");
std::process::exit(AppExitCode::PacmanError as i32); std::process::exit(AppExitCode::PacmanError as i32);
} }
} }
if args.aur {
if verbosity >= 1 {
log!("Checking AUR upgrades...");
} }
// Start spinner #[tracing::instrument(level = "trace")]
let sp = spinner!("Checking AUR upgrades..."); async fn upgrade_aur(options: Options) {
tracing::debug!("Upgrading AUR packages");
// List non-native packages using `pacman -Qm` and collect to a Vec<String> let non_native_pkgs = PacmanQueryBuilder::foreign()
let non_native = ShellCommand::pacman() .color(PacmanColor::Never)
.arg("-Qm") .query_with_output()
.args(&["--color", "never"]) .await
.wait_with_output()
.silent_unwrap(AppExitCode::PacmanError); .silent_unwrap(AppExitCode::PacmanError);
// Collect by lines to a Vec<String> tracing::debug!("aur packages: {non_native_pkgs:?}");
let mut non_native = non_native.stdout.split('\n').collect::<Vec<&str>>();
// Remove last element, which is an empty line
non_native.pop();
// Parse non-native packages into a Vec<QueriedPackage>
let mut parsed_non_native: Vec<QueriedPackage> = vec![];
for pkg in non_native {
// Split by space
let split = pkg.split(' ').collect::<Vec<&str>>();
if verbosity >= 1 {
log!("{:?}", split);
}
// Create QueriedPackage and push it to parsed_non_native
let name = split[0].to_string();
let version = split[1].to_string();
parsed_non_native.push(QueriedPackage { name, version });
}
if verbosity >= 1 {
log!("{:?}", &parsed_non_native);
}
// Check if AUR package versions are the same as installed
let mut aur_upgrades = vec![]; let mut aur_upgrades = vec![];
for pkg in parsed_non_native {
// Query AUR
let rpc_result = rpcinfo(&pkg.name);
if !rpc_result.found {
// If package not found, skip
continue;
}
// Run `vercmp` to compare versions for pkg in non_native_pkgs {
let vercmp_result = std::process::Command::new("vercmp") let remote_package = rpcinfo(&pkg.name)
.arg(&pkg.version) .await
.arg(&rpc_result.package.unwrap().version) .silent_unwrap(AppExitCode::RpcError);
.output()
.unwrap(); if let Some(remote_package) = remote_package {
let vercmp_result = String::from_utf8(vercmp_result.stdout).unwrap(); if remote_package.metadata.version != pkg.version {
if verbosity >= 1 { tracing::debug!(
log!("Vercmp returned {:?}", vercmp_result); "local version: {}, remote version: {}",
} pkg.version,
remote_package.metadata.version
// If versions differ, push to a vector );
if vercmp_result.trim() == "-1" {
aur_upgrades.push(pkg.name); aur_upgrades.push(pkg.name);
} }
}
sp.stop_bold("Finished!");
// If vector isn't empty, prompt to install AUR packages from vector, effectively upgrading
if aur_upgrades.is_empty() {
info!("No upgrades available for installed AUR packages");
} else { } else {
let cont = prompt!(default true, tracing::warn!("Could not find the remote package for {}", pkg.name);
"AUR packages {} have new versions available, upgrade?",
aur_upgrades.join(", "),
);
if cont {
aur_install(aur_upgrades, options, cachedir);
};
} }
} }
// Check for .pacnew files if !aur_upgrades.is_empty() {
detect(); aur_install(aur_upgrades, options).await;
} else {
tracing::info!("No upgrades available for installed AUR packages");
}
} }

Loading…
Cancel
Save