dev_scripts/sync/
aur.rs

1//! Handles the download of packages from the AUR.
2//!
3//! This requires interaction with `git` and the experimental aur.git GitHub mirror.
4
5use std::{
6    fs::{File, create_dir_all},
7    io::Read,
8    path::PathBuf,
9    process::{Command, Stdio},
10};
11
12use anyhow::{Context, Result};
13use flate2::read::GzDecoder;
14use log::{error, info};
15use rayon::prelude::*;
16
17use crate::{cmd::ensure_success, ui::get_progress_bar};
18
19const AUR_PKGBASE_URL: &str = "https://aur.archlinux.org/pkgbase.gz";
20const AUR_GIT_MIRROR_URL: &str = "https://github.com/archlinux/aur.git";
21
22/// The entry point for downloading packages from the AUR.
23///
24/// See [`AurDownloader::download_packages`] for more information.
25#[derive(Clone, Debug)]
26pub struct AurDownloader {
27    /// The destination folder into which files should be downloaded.
28    pub dest: PathBuf,
29}
30
31impl AurDownloader {
32    /// Clone (or update) the AUR git repo and extract
33    /// .SRCINFO + PKGBUILD for every package
34    pub fn download_packages(&self) -> Result<()> {
35        self.update_or_clone()?;
36        self.parallel_extract_files()?;
37        Ok(())
38    }
39
40    /// Ensure we have a bare clone of aur.git locally. Update if already present.
41    fn update_or_clone(&self) -> Result<()> {
42        if self.repo_dir().exists() {
43            info!("Updating aur.git mirror...");
44            let output = Command::new("git")
45                .arg("-C")
46                .arg(self.repo_dir())
47                .args(["fetch", "--depth=1", "--prune"])
48                .output()
49                .context("Failed to update aur.git.")?;
50            ensure_success(&output).context("Failed to update aur.git.")?;
51        } else {
52            create_dir_all(self.download_dir())?;
53            info!("Cloning aur.git mirror...");
54            let output = Command::new("git")
55                .args([
56                    // We want all remote branches locally,
57                    // using a mirror clone is the simplest solution.
58                    "clone",
59                    "--mirror",
60                    "--depth=1",
61                    "--no-single-branch",
62                    AUR_GIT_MIRROR_URL,
63                ])
64                .arg(self.repo_dir())
65                .output()
66                .context("Failed to clone aur.git.")?;
67            ensure_success(&output).context("Failed to clone aur.git.")?;
68        }
69        Ok(())
70    }
71
72    /// Extract .SRCINFO and PKGBUILD files from aur.git branches.
73    fn parallel_extract_files(&self) -> Result<()> {
74        let packages: Vec<String> = get_packages_list()?;
75
76        let progress_bar = get_progress_bar(packages.len() as u64);
77
78        create_dir_all(self.target_dir())?;
79
80        let results: Vec<Result<()>> = packages
81            .par_iter()
82            .map(|pkg| {
83                let pkg_dir = self.target_dir().join(pkg);
84                create_dir_all(&pkg_dir)
85                    .context(format!("Failed to create package directory {pkg_dir:?}."))?;
86
87                for file_type in [".SRCINFO", "PKGBUILD"] {
88                    let out_file = File::create(pkg_dir.join(file_type))?;
89                    let output = Command::new("git")
90                        .arg("show")
91                        .arg(format!("{pkg}:{file_type}"))
92                        .current_dir(self.repo_dir())
93                        .stdout(Stdio::from(out_file))
94                        .output()
95                        .context(format!(
96                            "Failed to extract {file_type:?} from AUR package {pkg:?}."
97                        ))?;
98                    ensure_success(&output).context(format!(
99                        "Failed to extract {file_type:?} for package {pkg:?}."
100                    ))?;
101                }
102                progress_bar.inc(1);
103                Ok(())
104            })
105            .collect();
106
107        progress_bar.finish_with_message("All files extracted.");
108
109        // Log all errors during parallel extraction.
110        for error in results.into_iter().filter_map(Result::err) {
111            error!("{error:?}");
112        }
113
114        Ok(())
115    }
116
117    fn download_dir(&self) -> PathBuf {
118        self.dest.join("download")
119    }
120
121    fn repo_dir(&self) -> PathBuf {
122        self.download_dir().join("aur")
123    }
124
125    fn target_dir(&self) -> PathBuf {
126        self.dest.join("aur")
127    }
128}
129
130/// Downloads pkgbase.gz from aurweb and extracts the package names.
131fn get_packages_list() -> Result<Vec<String>> {
132    let resp = reqwest::blocking::get(AUR_PKGBASE_URL)?.error_for_status()?;
133    let bytes = resp.bytes()?;
134    let mut decoder = GzDecoder::new(&bytes[..]);
135    let mut aur_packages_raw = String::new();
136    decoder.read_to_string(&mut aur_packages_raw)?;
137    Ok(aur_packages_raw.lines().map(|s| s.to_string()).collect())
138}