commit
30ad30b87a
File diff suppressed because it is too large
Load Diff
@ -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
|
@ -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.
|
||||
pub fn clean(a: &[String], options: Options) -> Vec<String> {
|
||||
pub fn clean(a: &[String]) -> Vec<String> {
|
||||
// Strip versioning from package names
|
||||
let r = Regex::new(r"(\S+)((?:>=|<=|>|<|=\W)\S+$)").unwrap();
|
||||
let mut cleaned: Vec<String> = vec![];
|
||||
let verbosity = options.verbosity;
|
||||
|
||||
// Push cleaned package names to vector
|
||||
for b in a {
|
||||
if r.captures_iter(b).count() > 0 {
|
||||
let c = r.captures(b).unwrap().get(1).map_or("", |m| m.as_str());
|
||||
cleaned.push(c.to_string());
|
||||
} else {
|
||||
cleaned.push(b.to_string());
|
||||
}
|
||||
}
|
||||
let cleaned = a
|
||||
.iter()
|
||||
.map(|name| {
|
||||
name.split_once('=')
|
||||
.map(|n| n.0.to_string())
|
||||
.unwrap_or_else(|| name.to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
if verbosity >= 1 {
|
||||
log!("Cleaned: {:?}\nInto: {:?}", a, cleaned);
|
||||
}
|
||||
tracing::debug!("Cleaned: {:?}\nInto: {:?}", a, cleaned);
|
||||
|
||||
cleaned
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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,97 +1,23 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
#[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 aur_rpc::{PackageInfo, PackageMetadata, SearchField};
|
||||
|
||||
use super::error::AppResult;
|
||||
pub const URL: &str = "https://aur.archlinux.org/";
|
||||
|
||||
/// Return a struct of type [`InfoResults`] from the AUR.
|
||||
pub fn rpcinfo(pkg: &str) -> InfoResults {
|
||||
// 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();
|
||||
pub async fn rpcinfo(pkg: &str) -> AppResult<Option<PackageInfo>> {
|
||||
let packages = aur_rpc::info(vec![pkg]).await?;
|
||||
|
||||
// Send request and parse results into json
|
||||
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()),
|
||||
}
|
||||
Ok(packages.into_iter().next())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
let agent = ureq::AgentBuilder::new()
|
||||
.tls_connector(tls_connector)
|
||||
.build();
|
||||
pub async fn rpcsearch(
|
||||
query: String,
|
||||
by_field: Option<SearchField>,
|
||||
) -> 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
|
||||
agent
|
||||
.get(&format!(
|
||||
"https://aur.archlinux.org/rpc/?v=5&type=search&arg={}",
|
||||
pkg
|
||||
))
|
||||
.call()
|
||||
.unwrap()
|
||||
.into_json::<SearchResults>()
|
||||
.unwrap()
|
||||
Ok(search_results)
|
||||
}
|
||||
|
@ -1,52 +1,44 @@
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use crate::internal::{clean, rpc, structs};
|
||||
use crate::{log, Options};
|
||||
use crate::Options;
|
||||
|
||||
/// Sorts the given packages into an [`crate::internal::structs::Sorted`]
|
||||
pub fn sort(input: &[String], options: Options) -> structs::Sorted {
|
||||
// Initialise variables
|
||||
let mut repo: Vec<String> = vec![];
|
||||
let mut aur: Vec<String> = vec![];
|
||||
let mut nf: Vec<String> = vec![];
|
||||
let verbosity = options.verbosity;
|
||||
use super::error::SilentUnwrap;
|
||||
use super::exit_code::AppExitCode;
|
||||
|
||||
// Sanitise all packages passed in
|
||||
let a = clean(input, options);
|
||||
#[tracing::instrument(level = "trace")]
|
||||
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 {
|
||||
log!("Sorting: {:?}", a.join(" "));
|
||||
}
|
||||
let packages = clean(input);
|
||||
|
||||
tracing::debug!("Sorting: {:?}", packages.join(" "));
|
||||
|
||||
for b in a {
|
||||
// Check if package is in the repos
|
||||
for package in packages {
|
||||
let rs = Command::new("pacman")
|
||||
.arg("-Ss")
|
||||
.arg(format!("^{}$", &b))
|
||||
.arg(format!("^{}$", &package))
|
||||
.stdout(Stdio::null())
|
||||
.status()
|
||||
.expect("Something has gone wrong");
|
||||
|
||||
if rs.code() == Some(0) {
|
||||
// If it is, add it to the repo vector
|
||||
if verbosity >= 1 {
|
||||
log!("{} found in repos", b);
|
||||
}
|
||||
repo.push(b.to_string());
|
||||
} else if rpc::rpcinfo(&b).found {
|
||||
// Otherwise, check if it is in the AUR, if it is, add it to the AUR vector
|
||||
if verbosity >= 1 {
|
||||
log!("{} found in AUR", b);
|
||||
}
|
||||
aur.push(b.to_string());
|
||||
if let Some(0) = rs.code() {
|
||||
tracing::debug!("{} found in repos", package);
|
||||
repo_packages.push(package.to_string());
|
||||
} else if rpc::rpcinfo(&package)
|
||||
.await
|
||||
.silent_unwrap(AppExitCode::RpcError)
|
||||
.is_some()
|
||||
{
|
||||
tracing::debug!("{} found in AUR", package);
|
||||
aur_packages.push(package.to_string());
|
||||
} else {
|
||||
// Otherwise, add it to the not found vector
|
||||
if verbosity >= 1 {
|
||||
log!("{} not found", b);
|
||||
}
|
||||
nf.push(b.to_string());
|
||||
tracing::debug!("{} not found", package);
|
||||
missing_packages.push(package.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
structs::Sorted::new(repo, aur, nf)
|
||||
structs::Sorted::new(repo_packages, aur_packages, missing_packages)
|
||||
}
|
||||
|
@ -1,18 +1,27 @@
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::ShellCommand;
|
||||
|
||||
/// Loop sudo so longer builds don't time out
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub fn start_sudoloop() {
|
||||
prompt_sudo();
|
||||
std::thread::spawn(|| loop {
|
||||
prompt_sudo();
|
||||
thread::sleep(Duration::from_secs(3 * 60));
|
||||
use super::error::AppResult;
|
||||
|
||||
/// Loop sudo so it doesn't time out
|
||||
#[tracing::instrument(level = "trace")]
|
||||
pub async fn start_sudoloop() {
|
||||
prompt_sudo().await;
|
||||
tokio::task::spawn(async move {
|
||||
loop {
|
||||
prompt_sudo().await;
|
||||
tokio::time::sleep(Duration::from_secs(3 * 60)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn prompt_sudo() {
|
||||
while ShellCommand::sudo().arg("-v").wait_success().is_err() {}
|
||||
#[tracing::instrument(level = "trace")]
|
||||
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
|
||||
}
|
||||
|
@ -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,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,146 +1,97 @@
|
||||
use chrono::{Local, TimeZone};
|
||||
use colored::Colorize;
|
||||
use textwrap::wrap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::internal::commands::ShellCommand;
|
||||
use crate::internal::error::SilentUnwrap;
|
||||
use crate::internal::exit_code::AppExitCode;
|
||||
use crate::internal::rpc::rpcsearch;
|
||||
use crate::{log, Options};
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
/// Searches for packages from the AUR and returns wrapped results
|
||||
pub fn aur_search(query: &str, options: Options) -> String {
|
||||
// Query AUR for package info
|
||||
let res = rpcsearch(query);
|
||||
|
||||
// Get verbosity
|
||||
let verbosity = options.verbosity;
|
||||
|
||||
// Format output
|
||||
let mut results_vec = vec![];
|
||||
for package in &res.results {
|
||||
// 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,
|
||||
use crate::Options;
|
||||
use aur_rpc::SearchField;
|
||||
|
||||
#[tracing::instrument(level = "trace")]
|
||||
pub async fn aur_search(query: &str, by_field: Option<SearchBy>, options: Options) {
|
||||
let packages = rpcsearch(query.to_string(), by_field.map(SearchBy::into))
|
||||
.await
|
||||
.silent_unwrap(AppExitCode::RpcError);
|
||||
let total_results = packages.len();
|
||||
|
||||
for package in &packages {
|
||||
println!(
|
||||
"aur/{} {}\n {}",
|
||||
package.name, package.version, package.description
|
||||
)
|
||||
.join("\n"),
|
||||
);
|
||||
results_vec.push(result);
|
||||
}
|
||||
|
||||
if verbosity > 1 {
|
||||
log!(
|
||||
"Found {} results for \"{}\" in the AUR",
|
||||
res.results.len(),
|
||||
query
|
||||
);
|
||||
}
|
||||
|
||||
results_vec.join("\n")
|
||||
tracing::debug!("Found {total_results} resuls for \"{query}\" in AUR",);
|
||||
}
|
||||
|
||||
struct SearchResult {
|
||||
repo: String,
|
||||
name: String,
|
||||
version: String,
|
||||
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)])
|
||||
#[tracing::instrument(level = "trace")]
|
||||
pub async fn repo_search(query: &str, options: Options) {
|
||||
let output = ShellCommand::pacman()
|
||||
.arg("-Ss")
|
||||
.arg(query)
|
||||
.wait_with_output()
|
||||
.await
|
||||
.silent_unwrap(AppExitCode::PacmanError)
|
||||
.stdout;
|
||||
|
||||
// Split output into lines
|
||||
let lines = output.trim().split('\n');
|
||||
tracing::debug!(
|
||||
"Found {} results for \"{}\" in repos",
|
||||
&output.split('\n').count() / 2,
|
||||
&query
|
||||
);
|
||||
|
||||
// Initialise results vector
|
||||
let mut results_vec: Vec<SearchResult> = vec![];
|
||||
println!("{}", output)
|
||||
}
|
||||
|
||||
let clone = lines.clone().collect::<Vec<&str>>();
|
||||
if clone.len() == 1 && clone[0].is_empty() {
|
||||
// If no results, return empty string
|
||||
return "".to_string();
|
||||
/// Represents a field to search by
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SearchBy {
|
||||
/// 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
|
||||
for line in lines {
|
||||
let parts: Vec<&str> = line.split('\\').collect();
|
||||
let res = SearchResult {
|
||||
repo: parts[0].to_string(),
|
||||
name: parts[1].to_string(),
|
||||
version: parts[2].to_string(),
|
||||
description: parts[3].to_string(),
|
||||
impl FromStr for SearchBy {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let arg = match s {
|
||||
"name" => Self::Name,
|
||||
"name-desc" => Self::NameDesc,
|
||||
"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 {
|
||||
log!(
|
||||
"Found {} results for \"{}\" in repos",
|
||||
&results_vec.len(),
|
||||
&query
|
||||
);
|
||||
Ok(arg)
|
||||
}
|
||||
}
|
||||
|
||||
// Format output
|
||||
let results_vec = results_vec
|
||||
.into_iter()
|
||||
.map(|res| {
|
||||
let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 4)
|
||||
.subsequent_indent(" ");
|
||||
format!(
|
||||
"{}{}{} {}\n {}",
|
||||
res.repo.purple().bold(),
|
||||
"/".purple().bold(),
|
||||
res.name.bold(),
|
||||
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")
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<SearchField> for SearchBy {
|
||||
fn into(self) -> SearchField {
|
||||
match self {
|
||||
SearchBy::Name => SearchField::Name,
|
||||
SearchBy::NameDesc => SearchField::NameDesc,
|
||||
SearchBy::Maintainer => SearchField::Maintainer,
|
||||
SearchBy::Depends => SearchField::Depends,
|
||||
SearchBy::MakeDepends => SearchField::MakeDepends,
|
||||
SearchBy::OptDepends => SearchField::OptDepends,
|
||||
SearchBy::CheckDepends => SearchField::CheckDepends,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,47 @@
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::internal::commands::ShellCommand;
|
||||
use crate::internal::error::SilentUnwrap;
|
||||
use crate::internal::exit_code::AppExitCode;
|
||||
use crate::{log, Options};
|
||||
use crate::Options;
|
||||
|
||||
/// Helps the user in uninstalling installed packages.
|
||||
pub fn uninstall(packages: &[String], options: Options) {
|
||||
// Build pacman args
|
||||
#[tracing::instrument(level = "trace")]
|
||||
pub async fn uninstall(packages: Vec<String>, options: Options) {
|
||||
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 {
|
||||
pacman_args.push("--noconfirm");
|
||||
}
|
||||
let verbosity = options.verbosity;
|
||||
if verbosity >= 1 {
|
||||
log!("Uninstalling: {:?}", &packages);
|
||||
}
|
||||
tracing::debug!("Uninstalling: {:?}", &packages);
|
||||
|
||||
// Uninstall packages
|
||||
ShellCommand::pacman()
|
||||
.elevated()
|
||||
.args(pacman_args)
|
||||
.wait_success()
|
||||
.await
|
||||
.silent_unwrap(AppExitCode::PacmanError);
|
||||
|
||||
if verbosity >= 1 {
|
||||
log!("Uninstalling packages: {:?} exited with code 0", &packages);
|
||||
tracing::debug!("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::builder::pacman::{PacmanColor, PacmanQueryBuilder};
|
||||
use crate::internal::commands::ShellCommand;
|
||||
use crate::internal::detect;
|
||||
use crate::internal::error::SilentUnwrap;
|
||||
use crate::internal::exit_code::AppExitCode;
|
||||
use crate::internal::rpc::rpcinfo;
|
||||
use crate::operations::aur_install::aur_install;
|
||||
use crate::{info, log, prompt, spinner, warn, Options};
|
||||
use crate::{prompt, Options};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct QueriedPackage {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
/// Upgrades all installed packages
|
||||
#[tracing::instrument(level = "trace")]
|
||||
pub async fn upgrade(args: UpgradeArgs, options: Options) {
|
||||
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.
|
||||
pub fn upgrade(options: Options, args: UpgradeArgs, cachedir: &str) {
|
||||
// Initialise variables
|
||||
let verbosity = options.verbosity;
|
||||
#[tracing::instrument(level = "trace")]
|
||||
async fn upgrade_repo(options: Options) {
|
||||
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"];
|
||||
if noconfirm {
|
||||
pacman_args.push("--noconfirm");
|
||||
}
|
||||
|
||||
if verbosity >= 1 {
|
||||
log!("Upgrading repo packages");
|
||||
}
|
||||
tracing::debug!("Upgrading repo packages");
|
||||
|
||||
// Upgrade repo packages
|
||||
let pacman_result = ShellCommand::pacman()
|
||||
.elevated()
|
||||
.args(pacman_args)
|
||||
.wait()
|
||||
.await
|
||||
.silent_unwrap(AppExitCode::PacmanError);
|
||||
|
||||
if pacman_result.success() {
|
||||
// If pacman was successful, notify user
|
||||
info!("Successfully upgraded repo packages");
|
||||
tracing::info!("Successfully upgraded repo packages");
|
||||
} else {
|
||||
// Otherwise warn user
|
||||
warn!("Failed to upgrade repo packages.",);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
let continue_upgrading = prompt!(default no,
|
||||
"Failed to upgrade repo packages, continue to upgrading AUR packages?",
|
||||
);
|
||||
if !continue_upgrading {
|
||||
tracing::info!("Exiting");
|
||||
std::process::exit(AppExitCode::PacmanError as i32);
|
||||
}
|
||||
}
|
||||
|
||||
if args.aur {
|
||||
if verbosity >= 1 {
|
||||
log!("Checking AUR upgrades...");
|
||||
}
|
||||
|
||||
// Start spinner
|
||||
let sp = spinner!("Checking AUR upgrades...");
|
||||
#[tracing::instrument(level = "trace")]
|
||||
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 = ShellCommand::pacman()
|
||||
.arg("-Qm")
|
||||
.args(&["--color", "never"])
|
||||
.wait_with_output()
|
||||
let non_native_pkgs = PacmanQueryBuilder::foreign()
|
||||
.color(PacmanColor::Never)
|
||||
.query_with_output()
|
||||
.await
|
||||
.silent_unwrap(AppExitCode::PacmanError);
|
||||
|
||||
// Collect by lines to a Vec<String>
|
||||
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
|
||||
tracing::debug!("aur packages: {non_native_pkgs:?}");
|
||||
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
|
||||
let vercmp_result = std::process::Command::new("vercmp")
|
||||
.arg(&pkg.version)
|
||||
.arg(&rpc_result.package.unwrap().version)
|
||||
.output()
|
||||
.unwrap();
|
||||
let vercmp_result = String::from_utf8(vercmp_result.stdout).unwrap();
|
||||
if verbosity >= 1 {
|
||||
log!("Vercmp returned {:?}", vercmp_result);
|
||||
}
|
||||
|
||||
// If versions differ, push to a vector
|
||||
if vercmp_result.trim() == "-1" {
|
||||
for pkg in non_native_pkgs {
|
||||
let remote_package = rpcinfo(&pkg.name)
|
||||
.await
|
||||
.silent_unwrap(AppExitCode::RpcError);
|
||||
|
||||
if let Some(remote_package) = remote_package {
|
||||
if remote_package.metadata.version != pkg.version {
|
||||
tracing::debug!(
|
||||
"local version: {}, remote version: {}",
|
||||
pkg.version,
|
||||
remote_package.metadata.version
|
||||
);
|
||||
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 {
|
||||
let cont = prompt!(default true,
|
||||
"AUR packages {} have new versions available, upgrade?",
|
||||
aur_upgrades.join(", "),
|
||||
);
|
||||
if cont {
|
||||
aur_install(aur_upgrades, options, cachedir);
|
||||
};
|
||||
tracing::warn!("Could not find the remote package for {}", pkg.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for .pacnew files
|
||||
detect();
|
||||
if !aur_upgrades.is_empty() {
|
||||
aur_install(aur_upgrades, options).await;
|
||||
} else {
|
||||
tracing::info!("No upgrades available for installed AUR packages");
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue