[WIP] Implement parallel aur install

Signed-off-by: trivernis <trivernis@protonmail.com>
i18n
trivernis 2 years ago
parent 362c2cf6ea
commit bef4fbcb02
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

67
Cargo.lock generated

@ -14,8 +14,10 @@ dependencies = [
"colored",
"crossterm",
"dialoguer",
"directories",
"futures",
"indicatif",
"lazy-regex",
"lazy_static",
"libc",
"native-tls",
@ -307,6 +309,26 @@ dependencies = [
"zeroize",
]
[[package]]
name = "directories"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "encode_unicode"
version = "0.3.6"
@ -467,6 +489,17 @@ dependencies = [
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "gimli"
version = "0.26.2"
@ -653,6 +686,29 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy-regex"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b12f2eb6ed7d39405c5eb25a034b4c106a9ad87a6d9be3298de6c5f32fd57d"
dependencies = [
"lazy-regex-proc_macros",
"once_cell",
"regex",
]
[[package]]
name = "lazy-regex-proc_macros"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2496e5264069bc726ccf37eb76b9cd89406ae110d836c3f76729f99c8a23293"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -949,6 +1005,17 @@ dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom",
"redox_syscall",
"thiserror",
]
[[package]]
name = "regex"
version = "1.6.0"

@ -45,6 +45,8 @@ indicatif = { version = "0.17.0", features = ["tokio"] }
lazy_static = "1.4.0"
parking_lot = { version = "0.12.1", features = ["deadlock_detection"] }
dialoguer = "0.10.2"
lazy-regex = "2.3.0"
directories = "4.0.1"
[dependencies.tokio]
version = "1.20.1"

@ -2,6 +2,6 @@
podman build . -t ame-debug
if [ $? -eq 0 ]; then
podman container exists ame-debug && podman container rm ame-debug
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))
}
}
}

@ -1 +1,2 @@
pub mod git;
pub mod pacman;

@ -160,3 +160,30 @@ 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)
}
}

@ -0,0 +1,207 @@
use std::collections::HashSet;
use aur_rpc::PackageInfo;
use futures::future;
use crate::builder::pacman::PacmanSearchBuilder;
use super::error::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(Self::map_dep_to_name)
.collect();
let mut already_searched = HashSet::new();
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_make_depends(&aur_packages, &already_searched);
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(Self::map_dep_to_name)
.collect();
let mut already_searched = HashSet::new();
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);
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: &Vec<PackageInfo>,
searched: &HashSet<String>,
) -> HashSet<String> {
aur_packages
.iter()
.flat_map(|p| p.make_depends.iter().filter_map(Self::map_dep_to_name))
.filter(|d| !searched.contains(d))
.collect()
}
fn get_filtered_depends(
aur_packages: &Vec<PackageInfo>,
searched: &HashSet<String>,
) -> HashSet<String> {
aur_packages
.iter()
.flat_map(|p| p.depends.iter().filter_map(Self::map_dep_to_name))
.filter(|d| !searched.contains(d))
.collect()
}
fn map_dep_to_name(dep: &String) -> 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)
}
}
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,
})
}
}

@ -10,12 +10,12 @@ pub use sudoloop::*;
mod clean;
pub mod commands;
pub mod config;
pub mod dependencies;
mod detect;
pub mod error;
pub mod exit_code;
pub mod fs_utils;
mod initialise;
pub mod resolve;
pub mod rpc;
mod sort;
pub mod structs;

@ -1,8 +1,13 @@
use std::fs;
use std::path::Path;
use std::process::exit;
use directories::ProjectDirs;
use crate::internal::exit_code::AppExitCode;
use crate::logging::get_logger;
use crate::logging::handler::PromptDefault;
use lazy_static::lazy_static;
#[macro_export]
/// Macro for printing a message and destructively exiting
@ -36,3 +41,21 @@ pub fn log_and_crash(msg: String, exit_code: AppExitCode) -> ! {
pub fn prompt_yn(question: String, prompt_default: PromptDefault) -> bool {
get_logger().prompt(question, prompt_default)
}
pub fn get_cache_dir() -> &'static Path {
let cache_dir = get_directories().cache_dir();
if !cache_dir.exists() {
fs::create_dir_all(cache_dir).unwrap();
}
cache_dir
}
fn get_directories() -> &'static ProjectDirs {
lazy_static! {
static ref DIRECTORIES: ProjectDirs = ProjectDirs::from("com", "crystal", "ame").unwrap();
}
&*DIRECTORIES
}

@ -114,30 +114,50 @@ impl LogHandler {
(*self.level.write()) = level;
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn reset_output_type(&self) {
self.set_output_type(OutputType::Stdout);
}
/// Creates a new progress spinner and registers it on the log handler
#[tracing::instrument(level = "trace", skip_all)]
pub fn new_progress_spinner(&self) -> Arc<ProgressBar> {
let progress_bar = ProgressBar::new_spinner().with_message("Scanning for pacnew files");
progress_bar.enable_steady_tick(Duration::from_millis(250));
let pb = Arc::new(progress_bar);
self.set_progress_bar(pb.clone());
let pb = ProgressBar::new_spinner();
pb.enable_steady_tick(Duration::from_millis(250));
pb
let mut output_type = self.output_type.write();
if let OutputType::MultiProgress(mp) = &*output_type {
Arc::new(mp.add(pb.clone()))
} else {
let pb = Arc::new(pb);
*output_type = OutputType::Progress(pb.clone());
pb
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn new_multi_progress(&self) -> Arc<MultiProgress> {
let mp = Arc::new(MultiProgress::new());
self.set_output_type(OutputType::MultiProgress(mp.clone()));
mp
}
/// Registeres a progress bar on the log handler
pub fn set_progress_bar(&self, pb: Arc<ProgressBar>) {
#[tracing::instrument(level = "trace", skip_all)]
fn set_progress_bar(&self, pb: Arc<ProgressBar>) {
self.set_output_type(OutputType::Progress(pb))
}
/// Sets the output type of the log handler to either stdout/stderr or a progress bar
#[tracing::instrument(level = "trace", skip_all)]
pub fn set_output_type(&self, output: OutputType) {
(*self.output_type.write()) = output;
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn set_uwu_enabled(&self, enabled: bool) {
self.uwu_enabled
.store(enabled, std::sync::atomic::Ordering::Relaxed);

@ -1,173 +1,130 @@
use async_recursion::async_recursion;
use aur_rpc::PackageInfo;
use crossterm::style::Stylize;
use futures::future;
use indicatif::ProgressBar;
use std::env;
use std::env::set_current_dir;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use std::time::Duration;
use tokio::fs;
use crate::builder::git::{GitCloneBuilder, GitPullBuilder};
use crate::internal::commands::ShellCommand;
use crate::internal::error::SilentUnwrap;
use crate::internal::dependencies::DependencyInformation;
use crate::internal::error::{AppError, AppResult, SilentUnwrap};
use crate::internal::exit_code::AppExitCode;
use crate::internal::rpc::rpcinfo;
use crate::internal::rpc::{self, rpcinfo};
use crate::internal::utils::get_cache_dir;
use crate::logging::get_logger;
use crate::{crash, internal::fs_utils::rmdir_recursive, prompt, Options};
/// Installs a given list of packages from the aur
#[tracing::instrument(level = "trace")]
#[async_recursion]
pub async fn aur_install(packages: Vec<String>, options: Options) {
let url = crate::internal::rpc::URL;
let cachedir = format!("{}/.cache/ame/", env::var("HOME").unwrap());
let noconfirm = options.noconfirm;
tracing::debug!("Installing from AUR: {:?}", &packages);
tracing::info!("Installing packages {} from the AUR", packages.join(", "));
for package_name in packages {
let rpcres = rpcinfo(&package_name)
.await
.silent_unwrap(AppExitCode::RpcError);
if rpcres.is_none() {
break;
}
let package = rpcres.unwrap();
let pkg_name = package.metadata.name;
tracing::debug!("Cloning {} into cachedir", pkg_name);
tracing::info!("Cloning package source");
set_current_dir(Path::new(&cachedir)).unwrap();
ShellCommand::git()
.arg("clone")
.arg(format!("{}/{}", url, pkg_name))
.wait()
.await
.silent_unwrap(AppExitCode::GitError);
tracing::debug!(
"Cloned {} into cachedir, moving on to resolving dependencies",
pkg_name
);
tracing::debug!(
"Raw dependencies for package {} are:\n{:?}",
pkg_name,
package.depends,
);
tracing::debug!(
"Raw makedepends for package {} are:\n{:?}",
pkg_name,
package.make_depends.join(", "),
let pb = get_logger().new_progress_spinner();
pb.set_message("Fetching package information");
let package_info = aur_rpc::info(&packages)
.await
.map_err(AppError::from)
.silent_unwrap(AppExitCode::RpcError);
tracing::debug!("package info = {package_info:?}");
tokio::time::sleep(Duration::from_secs(1)).await;
if package_info.len() != packages.len() {
let mut not_found = packages.clone();
package_info
.iter()
.for_each(|pkg| not_found.retain(|p| pkg.metadata.name != *p));
crash!(
AppExitCode::MissingDeps,
"Could not find the package: {}",
not_found.join(",").italic(),
);
}
// dep sorting
tracing::debug!("Sorting dependencies");
let sorted = crate::internal::sort(&package.depends, options).await;
tracing::debug!("Sorting make dependencies");
let md_sorted = crate::internal::sort(&package.make_depends, options).await;
tracing::debug!("Sorted dependencies for {} are:\n{:?}", pkg_name, &sorted);
tracing::debug!("Sorted makedepends for {} are:\n{:?}", pkg_name, &md_sorted);
let newopts = Options {
noconfirm,
asdeps: true,
};
if !sorted.nf.is_empty() || !md_sorted.nf.is_empty() {
crash!(
AppExitCode::MissingDeps,
"Could not find dependencies {} for package {}, aborting",
sorted.nf.join(", "),
pkg_name,
);
}
if !noconfirm {
let p1 = prompt!(default no,
"Would you like to review {}'s PKGBUILD (and any .install files if present)?",
pkg_name,
);
let editor: &str = &env::var("PAGER").unwrap_or_else(|_| "less".parse().unwrap());
if p1 {
Command::new(editor)
.arg(format!("{}/PKGBUILD", pkg_name))
.spawn()
.unwrap()
.wait()
.unwrap();
let status = ShellCommand::bash()
.arg("-c")
.arg(format!("ls {}/*.install &> /dev/null", pkg_name))
.wait()
.await
.silent_unwrap(AppExitCode::Other);
if status.success() {
ShellCommand::bash()
.arg("-c")
.arg(format!("{} {}/*.install", editor, pkg_name))
.wait()
.await
.silent_unwrap(AppExitCode::Other);
}
let p2 = prompt!(default yes, "Would you still like to install {}?", pkg_name);
if !p2 {
fs::remove_dir_all(format!("{}/{}", cachedir, pkg_name))
.await
.unwrap();
crash!(AppExitCode::UserCancellation, "Not proceeding");
}
}
}
// dep installing
tracing::info!("Moving on to install dependencies");
if !sorted.repo.is_empty() {
crate::operations::install(sorted.repo, newopts).await;
crate::operations::install(md_sorted.repo, newopts).await;
}
if !sorted.aur.is_empty() {
crate::operations::aur_install(sorted.aur, newopts).await;
crate::operations::aur_install(md_sorted.aur, newopts).await;
}
let mut makepkg_args = vec!["-rsci", "--skippgp"];
if options.asdeps {
makepkg_args.push("--asdeps")
}
if options.noconfirm {
makepkg_args.push("--noconfirm")
}
// package building and installing
tracing::info!("Building time!");
set_current_dir(format!("{}/{}", cachedir, pkg_name)).unwrap();
let status = ShellCommand::makepkg()
.args(makepkg_args)
.wait()
.await
.silent_unwrap(AppExitCode::MakePkgError);
if !status.success() {
fs::remove_dir_all(format!("{}/{}", cachedir, pkg_name))
.await
.unwrap();
crash!(
AppExitCode::PacmanError,
"Error encountered while installing {}, aborting",
pkg_name,
);
}
set_current_dir(&cachedir).unwrap();
let package_cache = PathBuf::from(format!("{cachedir}/{pkg_name}"));
rmdir_recursive(&package_cache).await.unwrap()
pb.finish_with_message("Found all packages in the aur");
get_logger().new_multi_progress();
future::try_join_all(package_info.iter().map(download_aur_source))
.await
.unwrap();
tokio::time::sleep(Duration::from_secs(1)).await;
let dependencies = future::try_join_all(package_info.iter().map(|pkg| async {
get_logger()
.new_progress_spinner()
.set_message(format!("{}: Fetching dependencies", pkg.metadata.name));
DependencyInformation::for_package(pkg).await
}))
.await
.silent_unwrap(AppExitCode::RpcError);
tokio::time::sleep(Duration::from_secs(1)).await;
let aur_build_dependencies: Vec<PackageInfo> = dependencies
.iter()
.flat_map(|d| d.make_depends.aur.clone())
.collect();
let aur_dependencies: Vec<PackageInfo> = dependencies
.iter()
.flat_map(|d| d.depends.aur.clone())
.collect();
get_logger().reset_output_type();
tracing::info!(
"Installing {} build dependencies",
aur_build_dependencies.len()
);
get_logger().new_multi_progress();
future::try_join_all(aur_build_dependencies.iter().map(download_aur_source))
.await
.unwrap();
}
#[tracing::instrument(level = "trace", skip_all)]
async fn download_aur_source(info: &PackageInfo) -> AppResult<PathBuf> {
let pb = get_logger().new_progress_spinner();
let pkg_name = &info.metadata.name;
pb.set_message(format!("{pkg_name}: Downloading sources"));
let cache_dir = get_cache_dir();
let pkg_dir = cache_dir.join(&pkg_name);
tokio::time::sleep(Duration::from_secs(1)).await;
if pkg_dir.exists() {
pb.set_message(format!("{pkg_name}: Pulling latest changes {pkg_name}"));
GitPullBuilder::default().directory(&pkg_dir).pull().await?;
tokio::time::sleep(Duration::from_secs(1)).await;
} else {
let aur_url = rpc::URL;
let repository_url = format!("{aur_url}/{pkg_name}");
pb.set_message(format!("{pkg_name}: Cloning aur repository"));
GitCloneBuilder::default()
.url(repository_url)
.directory(&pkg_dir)
.clone()
.await?;
tokio::time::sleep(Duration::from_secs(1)).await;
pb.set_message(format!("{pkg_name}: Downloading and extracting files"));
}
tokio::time::sleep(Duration::from_secs(1)).await;
pb.finish_with_message(format!("{pkg_name} is ready to build"));
Ok(pkg_dir)
}

Loading…
Cancel
Save