use crate::config::{Colors, Config, SortMode, YesNoAll}; use crate::exec::has_command; use crate::fmt::print_indent; use crate::util::is_arch_repo; use crate::RaurHandle; use crate::{exec, printtr}; use std::collections::btree_map::Entry; use std::collections::{BTreeMap, HashMap}; use std::env::current_dir; use std::fs::{read_to_string, remove_dir_all}; use std::io::Write; use std::iter::FromIterator; use std::process::{Command, Stdio}; use std::result::Result as StdResult; use alpm::Version; use alpm_utils::{AsTarg, DbListExt, Targ}; use ansiterm::Style; use anyhow::{bail, Context, Result}; use aur_depends::AurBase; use globset::GlobSet; use indicatif::{ProgressBar, ProgressStyle}; use raur::{ArcPackage as Package, Raur}; use srcinfo::Srcinfo; use tr::tr; use url::Url; #[derive(Debug, Clone, Default)] pub struct Bases { pub bases: Vec, } impl FromIterator for Bases { fn from_iter>(iter: T) -> Self { let mut bases = Bases::new(); bases.extend(iter); bases } } impl FromIterator for Bases { fn from_iter>(iter: T) -> Self { let mut bases = Bases::new(); bases.extend_aur(iter); bases } } impl Bases { pub fn new() -> Self { Self { bases: Vec::new() } } pub fn push(&mut self, pkg: Package) { self.push_aur(aur_depends::AurPackage { pkg, make: false, target: false, }) } pub fn push_aur(&mut self, pkg: aur_depends::AurPackage) { for base in &mut self.bases { if base.package_base() == pkg.pkg.package_base { base.pkgs.push(pkg); return; } } self.bases.push(AurBase { pkgs: vec![pkg], build: true, }) } pub fn extend_aur>(&mut self, iter: I) { iter.into_iter().for_each(|p| self.push_aur(p)) } pub fn extend>(&mut self, iter: I) { iter.into_iter().for_each(|p| self.push(p)) } } #[derive(Debug, Default)] pub struct Warnings<'a> { pub pkgs: Vec, pub missing: Vec<&'a str>, pub ood: Vec<&'a str>, pub orphans: Vec<&'a str>, } impl<'a> Warnings<'a> { pub fn missing(&self, color: Colors, cols: Option) -> &Self { if !self.missing.is_empty() { let b = color.bold; let e = color.error; let msg = tr!("packages not in the AUR: "); print!("{} {}", e.paint("::"), b.paint(&msg)); print_indent(Style::new(), msg.len() + 3, 4, cols, " ", &self.missing); } self } pub fn ood(&self, color: Colors, cols: Option) -> &Self { if !self.ood.is_empty() { let b = color.bold; let e = color.error; let msg = tr!("marked out of date: "); print!("{} {}", e.paint("::"), b.paint(&msg)); print_indent(Style::new(), msg.len() + 3, 4, cols, " ", &self.ood); } self } pub fn orphans(&self, color: Colors, cols: Option) -> &Self { if !self.orphans.is_empty() { let b = color.bold; let e = color.error; let msg = tr!("orphans: "); print!("{} {}", e.paint("::"), b.paint(&msg)); print_indent(Style::new(), msg.len() + 3, 4, cols, " ", &self.orphans); } self } pub fn all(&self, color: Colors, cols: Option) { self.missing(color, cols); self.ood(color, cols); self.orphans(color, cols); } } pub async fn cache_info_with_warnings<'a, S: AsRef + Send + Sync>( raur: &RaurHandle, cache: &'a mut raur::Cache, pkgs: &'a [S], ignore: &[String], no_warn: &GlobSet, ) -> StdResult, raur::Error> { let mut missing = Vec::new(); let mut ood = Vec::new(); let mut orphaned = Vec::new(); let mut aur_pkgs = raur.cache_info(cache, pkgs).await?; aur_pkgs.retain(|pkg1| pkgs.iter().any(|pkg2| pkg1.name == pkg2.as_ref())); let should_warn = |pkg: &str| !no_warn.is_match(pkg) && !ignore.iter().any(|ignored| ignored == pkg); for pkg in pkgs { let pkg_name = pkg.as_ref(); if should_warn(pkg_name) && !cache.contains(pkg_name) { missing.push(pkg_name); } } for pkg in &aur_pkgs { if should_warn(&pkg.name) { if pkg.out_of_date.is_some() { ood.push(cache.get(pkg.name.as_str()).unwrap().name.as_str()); } if pkg.maintainer.is_none() { orphaned.push(cache.get(pkg.name.as_str()).unwrap().name.as_str()); } } } let warnings = Warnings { pkgs: aur_pkgs, missing, ood, orphans: orphaned, }; Ok(warnings) } pub async fn getpkgbuilds(config: &mut Config) -> Result { let pkgs = config .targets .iter() .map(|t| t.as_str()) .collect::>(); let (repo, pkgbuild, aur) = split_target_pkgbuilds(config, &pkgs); let mut ret = 0; if !repo.is_empty() { ret = repo_pkgbuilds(config, &repo)?; } if !pkgbuild.is_empty() { ret = pkgbuild_pkgbuilds(config, &pkgbuild)?; } if !aur.is_empty() { let aur = aur.iter().map(|t| t.pkg).collect::>(); let action = config.color.action; let bold = config.color.bold; println!( "{} {}", action.paint("::"), bold.paint(tr!("Querying AUR...")) ); let warnings = cache_info_with_warnings( &config.raur, &mut config.cache, &aur, &config.ignore, &GlobSet::empty(), ) .await?; ret |= !warnings.missing.is_empty() as i32; warnings.missing(config.color, config.cols); let aur = warnings.pkgs; if !aur.is_empty() { let mut bases = Bases::new(); bases.extend(aur); config.fetch.clone_dir = std::env::current_dir()?; aur_pkgbuilds(config, &bases).await?; } } Ok(ret) } fn repo_pkgbuilds(config: &Config, pkgs: &[Targ<'_>]) -> Result { let pkgctl = &config.pkgctl_bin; for (n, targ) in pkgs.iter().enumerate() { let Ok(pkg) = config.alpm.syncdbs().find_target(*targ) else { continue; }; let base = pkg.base().unwrap_or_else(|| pkg.name()); print_download(config, n + 1, pkgs.len(), base); let mut cmd = Command::new(pkgctl); cmd.arg("repo") .arg("clone") .arg("--protocol") .arg("https") .arg(base); exec::command_output(&mut cmd)?; } Ok(0) } pub fn print_download(_config: &Config, n: usize, total: usize, pkg: &str) { let total = total.to_string(); println!( " ({n:>padding$}/{total}) {}", tr!("downloading: {pkg}", pkg), padding = total.len(), n = n, total = total, ); } fn pkgbuild_pkgbuilds(config: &Config, pkgbuild: &[Targ]) -> Result { let mut ret = 0; let cwd = current_dir()?; let color = config.color; let mut pkgs = BTreeMap::new(); for &targ in pkgbuild { let Some((base, _)) = config.pkgbuild_repos.target(config, targ) else { eprintln!( "{} {}", color.error.paint("error:"), tr!("package '{}' was not found", targ.pkg), ); ret = 1; continue; }; match pkgs.entry(base.srcinfo.pkgbase()) { Entry::Vacant(v) => { v.insert(base); } Entry::Occupied(o) => { if o.get().repo != base.repo { bail!(tr!("duplicate PKGBUILD: {}", base.srcinfo.pkgbase())) } } } } for (n, pkg) in pkgs.values().enumerate() { let path = cwd.join(pkg.srcinfo.pkgbase()); print_download(config, n + 1, pkgs.len(), pkg.srcinfo.pkgbase()); if path.exists() { if !path.join("PKGBUILD").exists() { eprintln!( "{} {}", color.error.paint("error:"), tr!( "package '{}' exists but has no PKGBUILD -- skipping", pkg.srcinfo.pkgbase(), ), ); ret = 1; continue; } remove_dir_all(&path)?; } let mut cmd = Command::new("cp"); cmd.arg("-r").arg("--").arg(&pkg.path).arg(path); exec::command_output(&mut cmd)?; } Ok(ret) } async fn aur_pkgbuilds(config: &Config, bases: &Bases) -> Result<()> { let download = bases .bases .iter() .map(|p| p.package_base()) .collect::>(); let cols = config.cols.unwrap_or(0); let action = config.color.action; let bold = config.color.bold; println!( "\n{} {}", action.paint("::"), bold.paint(tr!("Downloading PKGBUILDs...")) ); if bases.bases.is_empty() { printtr!(" PKGBUILDs up to date"); return Ok(()); } if cols < 80 { config.fetch.download_cb(&download, |cb| { let base = bases .bases .iter() .find(|b| b.package_base() == cb.pkg) .unwrap(); print_download(config, cb.n, download.len(), &base.to_string()); })?; } else { let total = download.len().to_string(); let template = format!( " ({{pos:>{}}}/{{len}}) {{prefix:45!}} [{{wide_bar}}]", total.len() ); let pb = ProgressBar::new(download.len() as u64); pb.set_style( ProgressStyle::default_bar() .template(&template)? .progress_chars("-> "), ); config.fetch.download_cb(&download, |cb| { let base = bases .bases .iter() .find(|b| b.package_base() == cb.pkg) .unwrap(); pb.inc(1); pb.set_prefix(base.to_string()); })?; pb.finish(); println!(); } Ok(()) } pub async fn new_aur_pkgbuilds( config: &Config, bases: &Bases, srcinfos: &HashMap, ) -> Result<()> { let mut pkgs = Vec::new(); if bases.bases.is_empty() { return Ok(()); } let all_pkgs = bases .bases .iter() .map(|b| b.package_base()) .collect::>(); if config.redownload == YesNoAll::All { aur_pkgbuilds(config, bases).await?; config.fetch.merge(&all_pkgs)?; return Ok(()); } for base in &bases.bases { if config.redownload == YesNoAll::Yes && base.pkgs.iter().any(|p| p.target) { pkgs.push(base.clone()); continue; } if let Some(pkg) = srcinfos.get(base.package_base()) { let upstream_ver = base.version(); if Version::new(pkg.version()) < Version::new(&*upstream_ver) { pkgs.push(base.clone()); } } else { pkgs.push(base.clone()); } } let new_bases = Bases { bases: pkgs }; aur_pkgbuilds(config, &new_bases).await?; config.fetch.merge(&all_pkgs)?; Ok(()) } pub async fn show_comments(config: &mut Config) -> Result { let client = config.raur.client(); let warnings = cache_info_with_warnings( &config.raur, &mut config.cache, &config.targets, &[], &GlobSet::empty(), ) .await?; warnings.missing(config.color, config.cols); let ret = !warnings.missing.is_empty() as i32; let bases = Bases::from_iter(warnings.pkgs); let c = config.color; for base in &bases.bases { let mut url = config .aur_url .join(&format!("packages/{}", base.package_base()))?; if config.comments >= 2 { url.set_query(Some("PP=250")); } let response = client .get(url.clone()) .send() .await .with_context(|| format!("{}: {}", base, url))?; if !response.status().is_success() { bail!("{}: {}: {}", base, url, response.status()); } let document = scraper::Html::parse_document(&response.text().await?); let titles_selector = scraper::Selector::parse("div.comments h4.comment-header").unwrap(); let comments_selector = scraper::Selector::parse("div.comments div.article-content").unwrap(); let titles = document .select(&titles_selector) .map(|node| node.text().collect::()); let comments = document .select(&comments_selector) .map(|node| node.text().collect::()); let iter = titles.zip(comments).collect::>(); if config.sort_mode == SortMode::TopDown { for (title, comment) in iter.into_iter() { print_indent(c.bold, 0, 0, config.cols, " ", title.split_whitespace()); for line in comment.trim().split('\n') { let line = line.split_whitespace(); print!(" "); print_indent(Style::new(), 4, 4, config.cols, " ", line); } println!(); } } else { for (title, comment) in iter.into_iter().rev() { print_indent(c.bold, 0, 0, config.cols, " ", title.split_whitespace()); for line in comment.trim().split('\n') { let line = line.split_whitespace(); print!(" "); print_indent(Style::new(), 4, 4, config.cols, " ", line); } println!(); } } } Ok(ret) } fn split_target_pkgbuilds<'a, T: AsTarg>( config: &Config, targets: &'a [T], ) -> (Vec>, Vec>, Vec>) { let mut local = Vec::new(); let mut pkgbuild = Vec::new(); let mut aur = Vec::new(); let db = config.alpm.syncdbs(); for targ in targets { let targ = targ.as_targ(); if config.mode.repo() && db.find_target(targ).is_ok() { local.push(targ); continue; } if config.mode.pkgbuild() { if let Some(repo) = targ.repo { if config.pkgbuild_repos.repo(repo).is_some() { pkgbuild.push(targ); continue; } } else if config.pkgbuild_repos.pkg(config, targ.pkg).is_some() { pkgbuild.push(targ); continue; } } if config.mode.repo() && targ.repo.is_some_and(is_arch_repo) { local.push(targ); continue; } if config.mode.aur() { aur.push(targ); } else if config.mode.repo() { local.push(targ) } else { pkgbuild.push(targ) } } (local, pkgbuild, aur) } pub async fn show_pkgbuilds(config: &mut Config) -> Result { let color = config.color; let stdout = std::io::stdout(); let mut stdout = stdout.lock(); let bat = config.color.enabled && has_command(&config.bat_bin); let client = config.raur.client(); let (repo, pkgbuild, aur) = split_target_pkgbuilds(config, &config.targets); if !repo.is_empty() { for pkg in &repo { let Ok(pkg) = config.alpm.syncdbs().find_target(*pkg) else { continue; }; let pkg = pkg.base().unwrap_or_else(|| pkg.name()); let url = Url::parse(&format!( "https://gitlab.archlinux.org/archlinux/packaging/packages/{}/-/raw/HEAD/PKGBUILD", pkg ))?; let response = client .get(url.clone()) .send() .await .with_context(|| format!("{}: {}", pkg, url))?; if !response.status().is_success() { bail!("{}: {}: {}", pkg, url, response.status()); } if bat { pipe_bat(config, &response.bytes().await?)?; } else { let _ = stdout.write_all(&response.bytes().await?); } let _ = stdout.write_all(b"\n"); } } if !pkgbuild.is_empty() { for pkg in pkgbuild { let Some(pkg) = config.pkgbuild_repos.target(config, pkg) else { eprintln!( "{} {}", color.error.paint("error:"), tr!("package '{}' was not found", pkg.pkg), ); continue; }; let pkgbuild = read_to_string(pkg.0.path.join("PKGBUILD"))?; if bat { pipe_bat(config, pkgbuild.as_bytes())?; } else { let _ = stdout.write_all(pkgbuild.as_bytes()); } let _ = stdout.write_all(b"\n"); } } if !aur.is_empty() { let aur = aur.iter().map(|t| t.pkg).collect::>(); let warnings = cache_info_with_warnings( &config.raur, &mut config.cache, &aur, &[], &GlobSet::empty(), ) .await?; warnings.missing(config.color, config.cols); let ret = !warnings.missing.is_empty() as i32; let bases = Bases::from_iter(warnings.pkgs); for base in &bases.bases { let base = base.package_base(); let url = config.aur_url.join("cgit/aur.git/plain/PKGBUILD").unwrap(); let url = Url::parse_with_params(url.as_str(), &[("h", base)]).unwrap(); let response = client .get(url.clone()) .send() .await .with_context(|| format!("{}: {}", base, url))?; if !response.status().is_success() { bail!("{}: {}: {}", base, url, response.status()); } if bat { pipe_bat(config, &response.bytes().await?)?; } else { let _ = stdout.write_all(&response.bytes().await?); } let _ = stdout.write_all(b"\n"); } return Ok(ret); } Ok(0) } fn pipe_bat(config: &Config, pkgbuild: &[u8]) -> Result<()> { let mut command = Command::new(&config.bat_bin); command .arg("-pp") .arg("--color=always") .arg("-lPKGBUILD") .args(&config.bat_flags) .stdin(Stdio::piped()); let mut child = exec::spawn(&mut command)?; let _ = child.stdin.as_mut().unwrap().write_all(pkgbuild); exec::wait(&command, &mut child)?; Ok(()) }