dev_scripts/sync/
pkgsrc.rs1use std::{
7 collections::{HashMap, HashSet},
8 fs::remove_dir_all,
9 path::{Path, PathBuf},
10 process::Command,
11};
12
13use anyhow::{Context, Result};
14use log::{error, info, trace};
15use rayon::prelude::*;
16use strum::Display;
17
18use super::filenames_in_dir;
19use crate::{cmd::ensure_success, ui::get_progress_bar};
20
21const PKGBASE_MAINTAINER_URL: &str = "https://archlinux.org/packages/pkgbase-maintainer";
22const SSH_HOST: &str = "git@gitlab.archlinux.org";
23const REPO_BASE_URL: &str = "archlinux/packaging/packages";
24
25const PACKAGE_REPO_RENAMES: [(&str, &str); 3] = [
28 ("gtk2+extra", "gtk2-extra"),
29 ("dvd+rw-tools", "dvd-rw-tools"),
30 ("tree", "unix-tree"),
31];
32
33#[derive(Clone, Debug)]
38pub struct PkgSrcDownloader {
39 pub dest: PathBuf,
41}
42
43impl PkgSrcDownloader {
44 pub fn download_package_source_repositories(&self) -> Result<()> {
46 let repos = reqwest::blocking::get(PKGBASE_MAINTAINER_URL)
50 .context("Failed to query pkgbase url.")?
51 .json::<HashMap<String, Vec<String>>>()
52 .context("Failed to deserialize archweb pkglist.")?;
53
54 let all_repo_names: Vec<String> = repos.keys().map(String::from).collect();
55 info!("Found {} official packages.", all_repo_names.len());
56
57 let download_dir = self.dest.join("download/pkgsrc");
58
59 self.remove_old_repos(&all_repo_names, &download_dir)?;
61
62 self.parallel_update_or_clone(&all_repo_names, &download_dir)?;
64
65 for repo in all_repo_names {
67 let download_path = download_dir.join(&repo);
68 for file in [".SRCINFO", "PKGBUILD"] {
69 if download_path.join(file).exists() {
70 let target_dir = self.dest.join("pkgsrc").join(&repo);
71 std::fs::create_dir_all(&target_dir)?;
72 std::fs::copy(download_path.join(file), target_dir.join(file))?;
73 }
74 }
75 }
76
77 Ok(())
78 }
79
80 fn remove_old_repos(&self, repos: &[String], download_dir: &Path) -> Result<()> {
86 let local_repositories = filenames_in_dir(download_dir)?;
88
89 let remote_pkgs: HashSet<String> = HashSet::from_iter(repos.iter().map(String::from));
91
92 let removed_pkgs: Vec<&String> = local_repositories.difference(&remote_pkgs).collect();
95
96 if !removed_pkgs.is_empty() {
98 info!("Found {} repositories for cleanup:", removed_pkgs.len());
99 for removed in removed_pkgs {
100 remove_dir_all(download_dir.join(removed))
101 .context("Failed to remove local repository {removed}")?;
102 }
103 }
104
105 Ok(())
106 }
107
108 fn parallel_update_or_clone(&self, repos: &[String], download_dir: &Path) -> Result<()> {
112 let progress_bar = get_progress_bar(repos.len() as u64);
113
114 warmup_ssh_session()?;
116
117 let results: Vec<Result<(), RepoUpdateError>> = repos
119 .par_iter()
120 .map(|repo| {
121 let target_dir = download_dir.join(repo);
122
123 let result = if target_dir.exists() {
126 update_repo(repo, &target_dir)
127 } else {
128 clone_repo(repo.to_string(), &target_dir)
129 };
130
131 progress_bar.inc(1);
133 result
134 })
135 .collect();
136
137 progress_bar.finish_with_message("All repositories cloned or updated.");
139
140 let mut error_iter = results.into_iter().filter_map(Result::err).peekable();
142 if error_iter.peek().is_some() {
143 error!("The command failed for the following repositories:");
144 for error in error_iter {
145 error!(
146 "{} failed for repo {} with error:\n{:?}",
147 error.operation, error.repo, error.inner
148 );
149 }
150 }
151
152 Ok(())
153 }
154}
155
156pub fn warmup_ssh_session() -> Result<()> {
163 let mut ssh_command = Command::new("ssh");
164 ssh_command.args(vec!["-T", SSH_HOST]);
165 trace!("Running command: {ssh_command:?}");
166 let output = &ssh_command
167 .output()
168 .context("Failed to start ssh warmup command")?;
169
170 ensure_success(output).context("Failed to run ssh warmup command:")
171}
172
173#[derive(Display)]
174enum RepoUpdateOperation {
175 Clone,
176 Update,
177}
178
179struct RepoUpdateError {
180 repo: String,
181 operation: RepoUpdateOperation,
182 inner: anyhow::Error,
183}
184
185fn update_repo(repo: &str, target_dir: &Path) -> Result<(), RepoUpdateError> {
188 let output = Command::new("git")
190 .current_dir(target_dir)
191 .args(vec!["reset", "--hard"])
192 .output()
193 .map_err(|err| RepoUpdateError {
194 repo: repo.to_string(),
195 operation: RepoUpdateOperation::Update,
196 inner: err.into(),
197 })?;
198
199 ensure_success(&output).map_err(|err| RepoUpdateError {
200 repo: repo.to_string(),
201 operation: RepoUpdateOperation::Update,
202 inner: err,
203 })?;
204
205 let output = &Command::new("git")
206 .current_dir(target_dir)
207 .args(["pull", "--force"])
208 .output()
209 .map_err(|err| RepoUpdateError {
210 repo: repo.to_string(),
211 operation: RepoUpdateOperation::Update,
212 inner: err.into(),
213 })?;
214
215 ensure_success(output).map_err(|err| RepoUpdateError {
216 repo: repo.to_string(),
217 operation: RepoUpdateOperation::Update,
218 inner: err,
219 })
220}
221
222fn clone_repo(mut repo: String, target_dir: &Path) -> Result<(), RepoUpdateError> {
224 for (to_replace, replace_with) in PACKAGE_REPO_RENAMES {
226 if repo == to_replace {
227 repo = replace_with.to_string();
228 }
229 }
230
231 repo = repo.replace("+", "plus");
234
235 let ssh_url = format!("{SSH_HOST}:{REPO_BASE_URL}/{repo}.git");
236
237 let output = &Command::new("git")
238 .arg("clone")
239 .arg(&ssh_url)
240 .arg(target_dir)
241 .output()
242 .map_err(|err| RepoUpdateError {
243 repo: repo.to_string(),
244 operation: RepoUpdateOperation::Clone,
245 inner: err.into(),
246 })?;
247
248 ensure_success(output).map_err(|err| RepoUpdateError {
249 repo: repo.to_string(),
250 operation: RepoUpdateOperation::Clone,
251 inner: err,
252 })
253}