dev_scripts/sync/
pkgsrc.rsuse std::{
collections::{HashMap, HashSet},
fs::remove_dir_all,
path::{Path, PathBuf},
process::Command,
};
use anyhow::{Context, Result};
use log::{error, info};
use rayon::prelude::*;
use strum::Display;
use super::filenames_in_dir;
use crate::{cmd::ensure_success, ui::get_progress_bar};
const PKGBASE_MAINTAINER_URL: &str = "https://archlinux.org/packages/pkgbase-maintainer";
const SSH_HOST: &str = "git@gitlab.archlinux.org";
const SSH_BASE_URL: &str = "git@gitlab.archlinux.org:archlinux/packaging/packages";
pub struct PkgSrcDownloader {
pub dest: PathBuf,
}
impl PkgSrcDownloader {
pub fn download_package_source_repositories(&self) -> Result<()> {
let repos = reqwest::blocking::get(PKGBASE_MAINTAINER_URL)
.context("Failed to query pkgbase url.")?
.json::<HashMap<String, Vec<String>>>()
.context("Failed to deserialize archweb pkglist.")?;
let all_repo_names: Vec<String> = repos.keys().map(String::from).collect();
info!("Found {} official packages.", all_repo_names.len());
let download_dir = self.dest.join("download/pkgsrc");
self.parallel_update_or_clone(&all_repo_names, &download_dir)?;
self.remove_old_repos(&all_repo_names, &download_dir)?;
for repo in all_repo_names {
let download_path = download_dir.join(&repo);
if download_path.join(".SRCINFO").exists() {
let target_dir = self.dest.join("pkgsrc").join(&repo);
std::fs::create_dir_all(&target_dir)?;
std::fs::copy(download_path.join(".SRCINFO"), target_dir.join(".SRCINFO"))?;
}
}
Ok(())
}
fn remove_old_repos(&self, repos: &[String], download_dir: &Path) -> Result<()> {
let local_repositories = filenames_in_dir(download_dir)?;
let remote_pkgs: HashSet<String> = HashSet::from_iter(repos.iter().map(String::from));
let removed_pkgs: Vec<&String> = local_repositories.difference(&remote_pkgs).collect();
if !removed_pkgs.is_empty() {
info!("Found {} repositories for cleanup:", removed_pkgs.len());
for removed in removed_pkgs {
remove_dir_all(download_dir.join(removed))
.context("Failed to remove local repository {removed}")?;
}
}
Ok(())
}
fn parallel_update_or_clone(&self, repos: &[String], download_dir: &Path) -> Result<()> {
let progress_bar = get_progress_bar(repos.len() as u64);
warmup_ssh_session()?;
let results: Vec<Result<(), RepoUpdateError>> = repos
.par_iter()
.map(|repo| {
let target_dir = download_dir.join(repo);
let result = if target_dir.exists() {
update_repo(repo, &target_dir)
} else {
clone_repo(repo, &target_dir)
};
progress_bar.inc(1);
result
})
.collect();
progress_bar.finish_with_message("All repositories cloned or updated.");
let mut error_iter = results.into_iter().filter_map(Result::err).peekable();
if error_iter.peek().is_some() {
error!("The command failed for the following repositories:");
for error in error_iter {
error!(
"{} failed for repo {} with error:\n{:?}",
error.operation, error.repo, error.inner
);
}
}
Ok(())
}
}
pub fn warmup_ssh_session() -> Result<()> {
let output = &Command::new("ssh")
.args(vec!["-T", SSH_HOST])
.output()
.context("Failed to start ssh warmup command")?;
ensure_success(output).context("Failed to run ssh warmup command:")
}
#[derive(Display)]
enum RepoUpdateOperation {
Clone,
Update,
}
struct RepoUpdateError {
repo: String,
operation: RepoUpdateOperation,
inner: anyhow::Error,
}
fn update_repo(repo: &str, target_dir: &Path) -> Result<(), RepoUpdateError> {
let output = &Command::new("git")
.current_dir(target_dir)
.args(vec!["reset", "--hard"])
.output()
.map_err(|err| RepoUpdateError {
repo: repo.to_string(),
operation: RepoUpdateOperation::Update,
inner: err.into(),
})?;
ensure_success(output).map_err(|err| RepoUpdateError {
repo: repo.to_string(),
operation: RepoUpdateOperation::Update,
inner: err,
})?;
let output = Command::new("git")
.current_dir(target_dir)
.args(["pull", "--force"])
.output()
.map_err(|err| RepoUpdateError {
repo: repo.to_string(),
operation: RepoUpdateOperation::Update,
inner: err.into(),
})?;
ensure_success(&output).map_err(|err| RepoUpdateError {
repo: repo.to_string(),
operation: RepoUpdateOperation::Update,
inner: err,
})
}
fn clone_repo(repo: &str, target_dir: &Path) -> Result<(), RepoUpdateError> {
let ssh_url = format!("{SSH_HOST}{SSH_BASE_URL}/{repo}.git");
let output = &Command::new("git")
.arg("clone")
.arg(&ssh_url)
.arg(target_dir)
.output()
.map_err(|err| RepoUpdateError {
repo: repo.to_string(),
operation: RepoUpdateOperation::Clone,
inner: err.into(),
})?;
ensure_success(output).map_err(|err| RepoUpdateError {
repo: repo.to_string(),
operation: RepoUpdateOperation::Clone,
inner: err,
})
}