diff --git a/Cargo.lock b/Cargo.lock index fd16aec..fbbed4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,7 @@ dependencies = [ "clap_complete", "color-eyre", "colored", + "console", "crossterm", "dialoguer", "directories", diff --git a/Cargo.toml b/Cargo.toml index 8f8160c..16e5a51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ parking_lot = { version = "0.12.1", features = ["deadlock_detection"] } dialoguer = "0.10.2" lazy-regex = "2.3.0" directories = "4.0.1" +console = "0.15.1" [dependencies.tokio] version = "1.20.1" diff --git a/src/builder/makepkg.rs b/src/builder/makepkg.rs index 9bee86d..e0ee16d 100644 --- a/src/builder/makepkg.rs +++ b/src/builder/makepkg.rs @@ -1,6 +1,8 @@ use std::fmt::Debug; use std::path::{Path, PathBuf}; +use tokio::process::Child; + use crate::internal::{ commands::ShellCommand, error::{AppError, AppResult}, @@ -73,9 +75,23 @@ impl MakePkgBuilder { self } + pub async fn run(self) -> AppResult<()> { + let output = self.build().wait_with_output().await?; + + if output.status.success() { + Ok(()) + } else { + Err(AppError::Other(output.stderr)) + } + } + + pub fn spawn(self) -> AppResult { + self.build().spawn(true) + } + /// Executes the makepkg command #[tracing::instrument(level = "trace")] - pub async fn run(self) -> AppResult<()> { + fn build(self) -> ShellCommand { let mut command = ShellCommand::makepkg().working_dir(self.directory); if self.clean { @@ -106,13 +122,7 @@ impl MakePkgBuilder { command = command.arg("--noprepare") } - let output = command.wait_with_output().await?; - - if output.status.success() { - Ok(()) - } else { - Err(AppError::Other(output.stderr)) - } + command } #[tracing::instrument(level = "trace")] diff --git a/src/internal/clean.rs b/src/internal/clean.rs index b37039e..558f278 100644 --- a/src/internal/clean.rs +++ b/src/internal/clean.rs @@ -1,14 +1,13 @@ -use regex::Regex; - /// Strips packages from versioning and other extraneous information. pub fn clean(a: &[String]) -> Vec { // Strip versioning from package names - let cleaned = a.iter() - .map(|name| - name - .split_once("=") + let cleaned = a + .iter() + .map(|name| { + name.split_once("=") .map(|n| n.0.to_string()) - .unwrap_or(name.to_string())) + .unwrap_or(name.to_string()) + }) .collect(); tracing::debug!("Cleaned: {:?}\nInto: {:?}", a, cleaned); diff --git a/src/internal/commands.rs b/src/internal/commands.rs index 6af8a0a..a93dd7a 100644 --- a/src/internal/commands.rs +++ b/src/internal/commands.rs @@ -125,7 +125,7 @@ impl ShellCommand { }) } - fn spawn(self, piped: bool) -> AppResult { + pub fn spawn(self, piped: bool) -> AppResult { tracing::debug!("Running {} {:?}", self.command, self.args); let (stdout, stderr) = if piped { diff --git a/src/internal/exit_code.rs b/src/internal/exit_code.rs index a6e2ec4..ec7b781 100644 --- a/src/internal/exit_code.rs +++ b/src/internal/exit_code.rs @@ -6,7 +6,6 @@ pub enum AppExitCode { MissingDeps = 3, UserCancellation = 4, PacmanError = 5, - GitError = 6, MakePkgError = 7, RpcError = 9, Other = 63, diff --git a/src/internal/utils.rs b/src/internal/utils.rs index 3f19d94..83b0340 100644 --- a/src/internal/utils.rs +++ b/src/internal/utils.rs @@ -3,6 +3,7 @@ use std::path::Path; use std::process::exit; use directories::ProjectDirs; +use textwrap::wrap; use crate::internal::exit_code::AppExitCode; use crate::logging::get_logger; @@ -17,6 +18,17 @@ macro_rules! crash { } } +#[macro_export] +/// Cancelles the process +macro_rules! cancelled { + () => { + crash!( + $crate::internal::exit_code::AppExitCode::UserCancellation, + "Installation cancelled" + ) + }; +} + #[macro_export] /// Macro for prompting the user with a yes/no question. macro_rules! prompt { @@ -59,3 +71,15 @@ fn get_directories() -> &'static ProjectDirs { &*DIRECTORIES } + +pub fn wrap_text>(s: S) -> Vec { + wrap(s.as_ref(), get_wrap_options()) + .into_iter() + .map(String::from) + .collect() +} + +fn get_wrap_options() -> textwrap::Options<'static> { + textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2) + .subsequent_indent(" ") +} diff --git a/src/logging/handler.rs b/src/logging/handler.rs index dfdbb66..8e09795 100644 --- a/src/logging/handler.rs +++ b/src/logging/handler.rs @@ -1,11 +1,12 @@ use colored::Colorize; use indicatif::{MultiProgress, ProgressBar}; use std::{ + fmt::Display, sync::{atomic::AtomicBool, Arc}, time::Duration, }; -use crate::uwu; +use crate::{internal::utils::wrap_text, uwu}; use dialoguer::Confirm; use super::Verbosity; @@ -110,6 +111,22 @@ impl LogHandler { confirm.interact().unwrap() } + pub fn print_list, 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("\n")) + } + pub fn set_verbosity(&self, level: Verbosity) { (*self.level.write()) = level; } @@ -169,10 +186,8 @@ impl LogHandler { fn preformat_msg(&self, msg: String) -> String { let msg = self.apply_uwu(msg); - let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2) - .subsequent_indent(" "); - textwrap::wrap(&msg, opts).join("\n") + wrap_text(msg).join("\n") } fn apply_uwu(&self, msg: String) -> String { diff --git a/src/logging/mod.rs b/src/logging/mod.rs index b93210e..47eb543 100644 --- a/src/logging/mod.rs +++ b/src/logging/mod.rs @@ -12,6 +12,7 @@ use crate::internal::uwu_enabled; use self::handler::LogHandler; pub mod handler; +pub mod output; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Verbosity { diff --git a/src/logging/output.rs b/src/logging/output.rs new file mode 100644 index 0000000..c3910db --- /dev/null +++ b/src/logging/output.rs @@ -0,0 +1,75 @@ +use aur_rpc::PackageInfo; +use console::Alignment; +use crossterm::style::Stylize; + +use crate::internal::dependencies::DependencyInformation; + +use super::get_logger; + +pub fn print_dependency_list(dependencies: &Vec) -> bool { + let (deps_repo, makedeps_repo, deps_aur, makedeps_aur) = dependencies + .iter() + .map(|d| { + ( + d.depends.repo.clone(), + d.make_depends.repo.clone(), + d.depends.aur.clone(), + d.make_depends.aur.clone(), + ) + }) + .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 + }, + ); + + let mut empty = true; + if !deps_repo.is_empty() { + get_logger().print_newline(); + tracing::info!("Repo dependencies"); + get_logger().print_list(&deps_repo, " "); + empty = false; + } + if !deps_aur.is_empty() { + get_logger().print_newline(); + tracing::info!("AUR dependencies"); + print_aur_package_list(&deps_aur); + empty = false; + } + + if !makedeps_repo.is_empty() { + get_logger().print_newline(); + tracing::info!("Repo make dependencies"); + get_logger().print_list(&makedeps_repo, " "); + empty = false; + } + + if !makedeps_aur.is_empty() { + get_logger().print_newline(); + tracing::info!("AUR make dependencies"); + print_aur_package_list(&makedeps_aur); + empty = false; + } + + empty +} + +pub fn print_aur_package_list(packages: &Vec) { + 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, + ) + }), + "\n ", + ); +} diff --git a/src/operations/aur_install.rs b/src/operations/aur_install.rs index 1d08558..b3d26b5 100644 --- a/src/operations/aur_install.rs +++ b/src/operations/aur_install.rs @@ -2,8 +2,13 @@ use async_recursion::async_recursion; use aur_rpc::PackageInfo; use crossterm::style::Stylize; use futures::future; -use std::collections::HashSet; +use indicatif::ProgressBar; +use std::collections::{HashMap, HashSet}; +use std::mem; use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::io::{AsyncRead, AsyncReadExt, BufReader}; +use tokio::task::JoinHandle; use crate::builder::git::{GitCloneBuilder, GitPullBuilder}; use crate::builder::makepkg::MakePkgBuilder; @@ -11,9 +16,10 @@ use crate::builder::pacman::PacmanInstallBuilder; use crate::internal::dependencies::DependencyInformation; use crate::internal::error::{AppError, AppResult, SilentUnwrap}; use crate::internal::exit_code::AppExitCode; -use crate::internal::utils::get_cache_dir; +use crate::internal::utils::{get_cache_dir, wrap_text}; use crate::logging::get_logger; -use crate::{crash, Options}; +use crate::logging::output::{print_aur_package_list, print_dependency_list}; +use crate::{cancelled, crash, prompt, Options}; #[derive(Debug)] pub struct BuildContext { @@ -66,12 +72,8 @@ impl BuildContext { #[tracing::instrument(level = "trace")] #[async_recursion] pub async fn aur_install(packages: Vec, options: Options) { - let noconfirm = options.noconfirm; - tracing::debug!("Installing from AUR: {:?}", &packages); - tracing::info!("Installing packages {} from the AUR", packages.join(", ")); - let pb = get_logger().new_progress_spinner(); pb.set_message("Fetching package information"); @@ -97,18 +99,33 @@ pub async fn aur_install(packages: Vec, options: Options) { get_logger().reset_output_type(); pb.finish_with_message("Found all packages in the aur"); + print_aur_package_list(&package_info); + + if !options.noconfirm + && !prompt!(default yes, "Do you want to install those packages from the AUR?") + { + cancelled!(); + } + tracing::info!("Downloading aur packages"); get_logger().new_multi_progress(); 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)); + get_logger().new_progress_spinner().set_message(format!( + "{}: Fetching dependencies", + pkg.metadata.name.clone().bold() + )); DependencyInformation::for_package(pkg).await })) .await .silent_unwrap(AppExitCode::RpcError); + if !print_dependency_list(&dependencies) && !options.noconfirm { + get_logger().print_newline(); + if !prompt!(default yes, "Do you want to install those dependencies?") { + cancelled!(); + } + } get_logger().new_multi_progress(); let contexts = future::try_join_all( @@ -123,9 +140,14 @@ pub async fn aur_install(packages: Vec, options: Options) { get_logger().reset_output_type(); tracing::info!("All sources are ready."); - let aur_build_dependencies: Vec = dependencies + let aur_dependencies: Vec = dependencies .iter() - .flat_map(|d| d.make_depends.aur.clone()) + .flat_map(|d| { + let mut deps = d.make_depends.aur.clone(); + deps.append(&mut d.depends.aur.clone()); + + deps + }) .collect(); let repo_dependencies: HashSet = dependencies @@ -138,26 +160,28 @@ pub async fn aur_install(packages: Vec, options: Options) { }) .collect(); - get_logger().reset_output_type(); - if !repo_dependencies.is_empty() { tracing::info!("Installing repo dependencies"); PacmanInstallBuilder::default() .as_deps(true) + .no_confirm(true) .packages(repo_dependencies) .install() .await .silent_unwrap(AppExitCode::PacmanError); } - if !aur_build_dependencies.is_empty() { + if !aur_dependencies.is_empty() { tracing::info!( - "Installing {} build dependencies from the aur", - aur_build_dependencies.len() + "Installing {} dependencies from the aur", + aur_dependencies.len() ); - install_aur_build_dependencies(aur_build_dependencies) - .await - .unwrap(); + let batches = create_dependency_batches(aur_dependencies); + tracing::debug!("aur install batches: {batches:?}"); + + for batch in batches { + install_aur_deps(batch).await.unwrap(); + } } tracing::info!("Installing {} packages", contexts.len()); @@ -165,13 +189,14 @@ pub async fn aur_install(packages: Vec, options: Options) { build_and_install( contexts, MakePkgBuilder::default(), - PacmanInstallBuilder::default(), + PacmanInstallBuilder::default().no_confirm(true), ) .await .silent_unwrap(AppExitCode::MakePkgError); + tracing::info!("Done!"); } -async fn install_aur_build_dependencies(deps: Vec) -> AppResult<()> { +async fn install_aur_deps(deps: Vec) -> AppResult<()> { get_logger().new_multi_progress(); let dep_contexts = future::try_join_all( @@ -221,18 +246,24 @@ async fn build_and_install( async fn download_aur_source(mut ctx: BuildContext) -> AppResult { let pb = get_logger().new_progress_spinner(); let pkg_name = &ctx.package.metadata.name; - pb.set_message(format!("{pkg_name}: Downloading sources")); + pb.set_message(format!("{}: 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!("{pkg_name}: Pulling latest changes {pkg_name}")); + 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}/{pkg_name}"); - pb.set_message(format!("{pkg_name}: Cloning aur repository")); + pb.set_message(format!( + "{}: Cloning aur repository", + pkg_name.clone().bold() + )); GitCloneBuilder::default() .url(repository_url) @@ -240,7 +271,10 @@ async fn download_aur_source(mut ctx: BuildContext) -> AppResult { .clone() .await?; - pb.set_message(format!("{pkg_name}: Downloading and extracting files")); + pb.set_message(format!( + "{}: Downloading and extracting files", + pkg_name.clone().bold() + )); MakePkgBuilder::default() .directory(&pkg_dir) @@ -251,7 +285,11 @@ async fn download_aur_source(mut ctx: BuildContext) -> AppResult { .run() .await?; } - pb.finish_with_message(format!("{pkg_name}: Downloaded!")); + pb.finish_with_message(format!( + "{}: {}", + pkg_name.clone().bold(), + "Downloaded!".green() + )); ctx.step = BuildStep::Build(BuildPath(pkg_dir)); Ok(ctx) @@ -264,19 +302,47 @@ async fn build_package( let pb = get_logger().new_progress_spinner(); let pkg_name = &ctx.package.metadata.name; let build_path = ctx.build_path()?; - pb.set_message(format!("{pkg_name}: Building Package")); + pb.set_message(format!("{}: Building Package", pkg_name.clone().bold())); - make_opts + let mut child = make_opts .directory(build_path) .clean(true) .no_deps(true) .skip_pgp(true) .needed(true) - .run() - .await?; + .spawn()?; + + let stderr = child.stderr.take().unwrap(); + let stdout = child.stdout.take().unwrap(); + let h1 = show_stdio_on_pb(stdout, pb.clone(), { + let pkg_name = pkg_name.clone(); + move |s| format!("{}: {s}", pkg_name.clone().bold()) + }); + let h2 = show_stdio_on_pb(stderr, pb.clone(), { + let pkg_name = pkg_name.clone(); + move |s| format!("{}: {s}", pkg_name.clone().bold()) + }); + + loop { + if let Some(exit_code) = child.try_wait()? { + h1.abort(); + h2.abort(); + + if !exit_code.success() { + pb.set_message(format!( + "{}: {}", + "Build failed!".red(), + pkg_name.clone().bold() + )); + return Err(AppError::from("Failed to build package")); + } else { + break; + } + } + } let packages = MakePkgBuilder::package_list(build_path).await?; - pb.finish_with_message(format!("{pkg_name}: Built!")); + pb.finish_with_message(format!("{}: {}", pkg_name.clone().bold(), "Built!".green())); ctx.step = BuildStep::Install(PackageArchives(packages)); Ok(ctx) @@ -297,3 +363,64 @@ async fn install_packages( Ok(ctxs) } + +#[tracing::instrument(level = "trace")] +fn create_dependency_batches(deps: Vec) -> Vec> { + let mut deps: HashMap = deps + .into_iter() + .map(|d| (d.metadata.name.clone(), d)) + .collect(); + let mut batches = Vec::new(); + + 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 && !contains_make_dep { + deps.remove(&key); + current_batch.insert(key, info); + } + } + + batches.push(current_batch.into_iter().map(|(_, v)| v).collect()); + } + + batches +} + +fn show_stdio_on_pb< + R: AsyncRead + Unpin + Send + 'static, + F: Fn(String) -> String + 'static + Send, +>( + stdout: R, + pb: Arc, + fmt: F, +) -> JoinHandle<()> { + tokio::task::spawn({ + async move { + let mut stdout = BufReader::new(stdout); + let mut line = String::new(); + + while let Ok(ch) = stdout.read_u8().await { + if ch == b'\n' { + let line = fmt(mem::take(&mut line)); + let lines = wrap_text(line); + let line = lines.into_iter().next().unwrap(); + pb.set_message(line); + } else { + line.push(ch as char); + } + } + } + }) +}