arch_updates_rs/lib.rs
1//! # arch_updates_rs
2//! Library to query arch linux packaging tools to see if updates are available.
3//! Designed for cosmic-applet-arch, but could be used in similar apps as well.
4//!
5//! # Usage example
6//! This example shows how to check for updates online and print them to the
7//! terminal. It also shows how to check for updates offline, using the cache
8//! returned from the online check. If a system update is run in between as per
9//! the example, the offline check should return 0 updates due.
10//!
11//!```no_run
12//! use arch_updates_rs::*;
13//!
14//! #[tokio::main]
15//! pub async fn main() {
16//! let (Ok((pacman, pacman_cache)), Ok((aur, aur_cache)), Ok((devel, devel_cache))) = tokio::join!(
17//! check_pacman_updates_online(),
18//! check_aur_updates_online(),
19//! check_devel_updates_online(),
20//! ) else {
21//! panic!();
22//! };
23//! println!("pacman: {:#?}", pacman);
24//! println!("aur: {:#?}", aur);
25//! println!("devel: {:#?}", devel);
26//! std::process::Command::new("paru")
27//! .arg("-Syu")
28//! .spawn()
29//! .unwrap()
30//! .wait()
31//! .unwrap();
32//! let (Ok(pacman), Ok(aur), Ok(devel)) = tokio::join!(
33//! check_pacman_updates_offline(&pacman_cache),
34//! check_aur_updates_offline(&aur_cache),
35//! check_devel_updates_offline(&devel_cache),
36//! ) else {
37//! panic!();
38//! };
39//! assert!(pacman.is_empty() && aur.is_empty() && devel.is_empty());
40//! }
41//! ```
42use core::str;
43use futures::future::try_join;
44use futures::stream::FuturesOrdered;
45use futures::{StreamExt, TryStreamExt};
46use get_updates::{
47 aur_update_due, checkupdates, devel_update_due, get_aur_packages, get_aur_srcinfo,
48 get_devel_packages, get_head_identifier, parse_url, parse_ver_and_rel, CheckupdatesMode,
49 PackageUrl,
50};
51use raur::Raur;
52use source_repo::{add_sources_to_updates, get_sources_list, SourcesList};
53use std::io;
54use std::str::Utf8Error;
55use thiserror::Error;
56
57mod get_updates;
58mod source_repo;
59
60pub use source_repo::SourceRepo;
61
62/// Packages ending with one of the devel suffixes will be checked against the
63/// repository, as well as just the pkgver and pkgrel.
64pub const DEVEL_SUFFIXES: [&str; 1] = ["-git"];
65
66pub type Result<T> = std::result::Result<T, Error>;
67
68#[derive(Error, Debug)]
69pub enum Error {
70 #[error("IO error running command `{0}`")]
71 Io(#[from] io::Error),
72 #[error("Web error `{0}`")]
73 Web(#[from] reqwest::Error),
74 #[error("Error parsing stdout from command")]
75 Stdout(#[from] Utf8Error),
76 #[error("Failed to get ignored packages")]
77 GetIgnoredPackagesFailed,
78 #[error("Head identifier too short")]
79 HeadIdentifierTooShort,
80 #[error("Failed to get package from AUR `{0:?}`")]
81 /// # Note
82 /// Due to the API design, it's not always possible to know the name of the
83 /// aur package we failed to get.
84 GetAurPackageFailed(Option<String>),
85 #[error("Error parsing .SRCINFO")]
86 ParseErrorSrcinfo(#[from] srcinfo::Error),
87 #[error("checkupdates returned an error: `{0}`")]
88 CheckUpdatesReturnedError(String),
89 #[error("Failed to parse update from checkupdates string: `{0}`")]
90 ParseErrorCheckUpdates(String),
91 #[error("Failed to parse update from pacman string: `{0}`")]
92 ParseErrorPacman(String),
93 #[error("Failed to parse pkgver and pkgrel from string `{0}`")]
94 ParseErrorPkgverPkgrel(String),
95}
96
97/// Current status of an installed pacman package, vs the status of the latest
98/// version.
99#[derive(Clone, Debug, Eq, PartialEq)]
100pub struct PacmanUpdate {
101 pub pkgname: String,
102 pub pkgver_cur: String,
103 pub pkgrel_cur: String,
104 pub pkgver_new: String,
105 pub pkgrel_new: String,
106 pub source_repo: Option<SourceRepo>,
107}
108
109/// Current status of an installed AUR package, vs the status of the latest
110/// version.
111#[derive(Clone, Debug, Eq, PartialEq)]
112pub struct AurUpdate {
113 pub pkgname: String,
114 pub pkgver_cur: String,
115 pub pkgrel_cur: String,
116 pub pkgver_new: String,
117 pub pkgrel_new: String,
118}
119
120/// Current status of an installed devel package, vs latest commit hash on the
121/// source repo.
122#[derive(Clone, Debug, Eq, PartialEq)]
123pub struct DevelUpdate {
124 pub pkgname: String,
125 pub pkgver_cur: String,
126 pub pkgrel_cur: String,
127 /// When checking a devel update, we don't get a pkgver/pkgrel so-to-speak,
128 /// we instead get the github ref.
129 pub ref_id_new: String,
130}
131
132/// Cached state for offline updates check
133#[derive(Default, Clone)]
134pub struct PacmanUpdatesCache(SourcesList);
135/// Cached state for offline updates check
136#[derive(Default, Clone)]
137pub struct AurUpdatesCache(Vec<AurUpdate>);
138#[derive(Default, Clone)]
139/// Cached state for offline updates check
140pub struct DevelUpdatesCache(Vec<DevelUpdate>);
141
142/// Use the `checkupdates` function to check if any pacman-managed packages have
143/// updates due.
144///
145/// Online version - this function uses the network.
146/// Returns a tuple of:
147/// - Packages that are not up to date.
148/// - Cache that can be stored in memory to make next query more efficient.
149///
150/// # Note
151/// This will fail with an error if somebody else is running 'checkupdates' in
152/// sync mode at the same time.
153/// # Usage
154/// ```no_run
155/// # use arch_updates_rs::*;
156/// # async {
157/// let updates = check_pacman_updates_online().await.unwrap();
158/// // Run `sudo pacman -Syu` in the terminal
159/// let (updates, _) = check_pacman_updates_online().await.unwrap();
160/// assert!(updates.is_empty());
161/// # };
162pub async fn check_pacman_updates_online() -> Result<(Vec<PacmanUpdate>, PacmanUpdatesCache)> {
163 let (parsed_updates, source_info) =
164 try_join(checkupdates(CheckupdatesMode::Sync), get_sources_list()).await?;
165 let updates = add_sources_to_updates(parsed_updates, &source_info);
166 Ok((updates, PacmanUpdatesCache(source_info)))
167}
168
169/// Use the `checkupdates` function to check if any pacman-managed packages have
170/// updates due.
171///
172/// Offline version - this function doesn't use the network, it takes the cache
173/// returned from `check_pacman_updates_online()` to avoid too many queries to
174/// pacman's sync db.
175///
176///
177/// # Usage
178/// ```no_run
179/// # use arch_updates_rs::*;
180/// # async {
181/// let (online, cache) = check_pacman_updates_online().await.unwrap();
182/// let offline = check_pacman_updates_offline(&cache).await.unwrap();
183/// assert_eq!(online, offline);
184/// // Run `sudo pacman -Syu` in the terminal
185/// let offline = check_pacman_updates_offline(&cache).await.unwrap();
186/// assert!(offline.is_empty());
187/// # };
188pub async fn check_pacman_updates_offline(cache: &PacmanUpdatesCache) -> Result<Vec<PacmanUpdate>> {
189 let parsed_updates = checkupdates(CheckupdatesMode::NoSync).await?;
190 Ok(add_sources_to_updates(parsed_updates, &cache.0))
191}
192
193/// Check if any packages ending in `DEVEL_SUFFIXES` have updates to their
194/// source repositories.
195///
196/// Online version - this function checks the network.
197/// Returns a tuple of:
198/// - Packages that are not up to date.
199/// - Cache for offline use.
200///
201/// # Notes
202/// - For this to be accurate, it's reliant on each devel package having only
203/// one source URL. If this is not the case, the function will produce a
204/// DevelUpdate for each source url, and may assume one or more are out of
205/// date.
206/// - This is also reliant on VCS packages being good
207/// citizens and following the VCS Packaging Guidelines.
208/// <https://wiki.archlinux.org/title/VCS_package_guidelines>
209/// # Usage
210/// ```no_run
211/// # use arch_updates_rs::*;
212/// # async {
213/// let (updates, _) = check_devel_updates_online().await.unwrap();
214/// // Run `paru -Syu` in the terminal
215/// let (updates, _) = check_devel_updates_online().await.unwrap();
216/// assert!(updates.is_empty());
217/// # };
218pub async fn check_devel_updates_online() -> Result<(Vec<DevelUpdate>, DevelUpdatesCache)> {
219 let devel_packages = get_devel_packages().await?;
220 let devel_updates = futures::stream::iter(devel_packages.into_iter())
221 // Get the SRCINFO for each package (as Result<Option<_>>).
222 .then(|pkg| async move {
223 let srcinfo = get_aur_srcinfo(&pkg.pkgname).await;
224 (pkg, srcinfo)
225 })
226 // Remove any None values from the list - these are where the aurweb
227 // api call was succesful but the package wasn't found (ie, package is not an AUR package).
228 .filter_map(|(pkg, maybe_srcinfo)| async { Some((pkg, maybe_srcinfo.transpose()?)) })
229 .then(|(pkg, srcinfo)| async move {
230 let updates = srcinfo?
231 .base
232 .source
233 .into_iter()
234 .flat_map(move |arch| arch.vec.into_iter())
235 .filter_map(|url| {
236 let url = parse_url(&url)?;
237 let PackageUrl { remote, branch, .. } = url;
238 // This allocation isn't ideal, but it's here to work around lifetime issues
239 // with nested streams that I've been unable to resolve. Spent a few hours on it
240 // so far!
241 Some((remote, branch.map(ToString::to_string)))
242 })
243 .map(move |(remote, branch)| {
244 let pkgver_cur = pkg.pkgver.to_owned();
245 let pkgrel_cur = pkg.pkgrel.to_owned();
246 let pkgname = pkg.pkgname.to_owned();
247 async move {
248 let ref_id_new = get_head_identifier(remote, branch.as_deref()).await?;
249 Ok::<_, crate::Error>(DevelUpdate {
250 pkgname,
251 pkgver_cur,
252 ref_id_new,
253 pkgrel_cur,
254 })
255 }
256 })
257 .collect::<FuturesOrdered<_>>();
258 Ok::<_, Error>(updates)
259 })
260 .try_flatten()
261 .try_collect::<Vec<_>>()
262 .await?;
263 Ok((
264 devel_updates
265 .iter()
266 .filter(|update| devel_update_due(update))
267 .cloned()
268 .collect::<Vec<_>>(),
269 DevelUpdatesCache(devel_updates),
270 ))
271}
272
273/// Check if any packages ending in `DEVEL_SUFFIXES` have updates to their
274/// source repositories.
275///
276/// Offline version - this function takes the cache returned from
277/// `check_devel_updates_online()`.
278///
279/// # Usage
280/// ```no_run
281/// # use arch_updates_rs::*;
282/// # async {
283/// let (online, cache) = check_devel_updates_online().await.unwrap();
284/// let offline = check_devel_updates_offline(&cache).await.unwrap();
285/// assert_eq!(online, offline);
286/// // Run `paru -Syu` in the terminal
287/// let offline = check_devel_updates_offline(&cache).await.unwrap();
288/// assert!(offline.is_empty());
289/// # };
290pub async fn check_devel_updates_offline(cache: &DevelUpdatesCache) -> Result<Vec<DevelUpdate>> {
291 let devel_packages = get_devel_packages().await?;
292 let devel_updates = devel_packages
293 .iter()
294 .flat_map(|package| {
295 cache
296 .0
297 .iter()
298 .filter(|cache_package| cache_package.pkgname == package.pkgname)
299 .map(move |cache_package| DevelUpdate {
300 pkgname: package.pkgname.to_owned(),
301 pkgver_cur: package.pkgver.to_owned(),
302 pkgrel_cur: package.pkgrel.to_owned(),
303 ref_id_new: cache_package.ref_id_new.to_owned(),
304 })
305 })
306 .filter(devel_update_due)
307 .collect();
308 Ok(devel_updates)
309}
310
311/// Check if any AUR packages have updates to their pkgver-pkgrel.
312///
313/// Online version - this function checks the network.
314/// Returns a tuple of:
315/// - Packages that are not up to date.
316/// - Latest version of all aur packages - for offline use.
317///
318/// # Notes
319/// - Locally installed packages that aren't in the AUR are currently not
320/// implemented and may return an error.
321/// # Usage
322/// ```no_run
323/// # use arch_updates_rs::*;
324/// # async {
325/// let (updates, _) = check_aur_updates_online().await.unwrap();
326/// // Run `paru -Syu` in the terminal
327/// let (updates, _) = check_aur_updates_online().await.unwrap();
328/// assert!(updates.is_empty());
329/// # };
330pub async fn check_aur_updates_online() -> Result<(Vec<AurUpdate>, AurUpdatesCache)> {
331 let old = get_aur_packages().await?;
332 let aur = raur::Handle::new();
333 let cache: Vec<AurUpdate> = aur
334 .info(
335 old.iter()
336 .map(|pkg| pkg.pkgname.to_owned())
337 .collect::<Vec<_>>()
338 .as_slice(),
339 )
340 .await
341 .map_err(|_| Error::GetAurPackageFailed(None))?
342 .into_iter()
343 .filter_map(|new| {
344 let matching_old = old.iter().find(|old| old.pkgname == new.name)?.clone();
345 let maybe_old_ver_and_rel = parse_ver_and_rel(new.version);
346 Some((matching_old, maybe_old_ver_and_rel))
347 })
348 .map(|(matching_old, maybe_old_ver_and_rel)| -> Result<_> {
349 let (pkgver_new, pkgrel_new) = maybe_old_ver_and_rel?;
350 Ok(AurUpdate {
351 pkgname: matching_old.pkgname.to_owned(),
352 pkgver_cur: matching_old.pkgver.to_owned(),
353 pkgrel_cur: matching_old.pkgrel.to_owned(),
354 pkgver_new,
355 pkgrel_new,
356 })
357 })
358 .collect::<Result<_>>()?;
359 Ok((
360 cache
361 .iter()
362 .filter(|update| aur_update_due(update))
363 .cloned()
364 .collect(),
365 AurUpdatesCache(cache),
366 ))
367}
368
369/// Check if any AUR packages have updates to their pkgver-pkgrel.
370///
371/// Offline version - this function needs a reference to the latest version of
372/// all aur packages (returned from `check_aur_updates_online()`.
373///
374/// # Usage
375/// ```no_run
376/// # use arch_updates_rs::*;
377/// # async {
378/// let (online, cache) = check_aur_updates_online().await.unwrap();
379/// let offline = check_aur_updates_offline(&cache).await.unwrap();
380/// assert_eq!(online, offline);
381/// // Run `paru -Syu` in the terminal
382/// let offline = check_aur_updates_offline(&cache).await.unwrap();
383/// assert!(offline.is_empty());
384/// # };
385pub async fn check_aur_updates_offline(cache: &AurUpdatesCache) -> Result<Vec<AurUpdate>> {
386 let old = get_aur_packages().await?;
387 let updates = old
388 .iter()
389 .map(|old_package| {
390 let matching_cached = cache
391 .0
392 .iter()
393 .find(|cache_package| cache_package.pkgname == old_package.pkgname);
394 let (pkgver_new, pkgrel_new) = match matching_cached {
395 Some(cache_package) => (
396 cache_package.pkgver_new.to_owned(),
397 cache_package.pkgrel_new.to_owned(),
398 ),
399 None => (old_package.pkgver.to_owned(), old_package.pkgrel.to_owned()),
400 };
401 AurUpdate {
402 pkgname: old_package.pkgname.to_owned(),
403 pkgver_cur: old_package.pkgver.to_owned(),
404 pkgrel_cur: old_package.pkgrel.to_owned(),
405 pkgver_new,
406 pkgrel_new,
407 }
408 })
409 .filter(aur_update_due)
410 .collect();
411 Ok(updates)
412}
413
414#[cfg(test)]
415mod tests {
416 use crate::{
417 check_aur_updates_offline, check_aur_updates_online, check_devel_updates_offline,
418 check_devel_updates_online, check_pacman_updates_offline, check_pacman_updates_online,
419 };
420
421 #[tokio::test]
422 async fn test_check_pacman_updates() {
423 let (online, cache) = check_pacman_updates_online().await.unwrap();
424 let offline = check_pacman_updates_offline(&cache).await.unwrap();
425 assert_eq!(online, offline);
426 }
427 #[tokio::test]
428 async fn test_check_aur_updates() {
429 let (online, cache) = check_aur_updates_online().await.unwrap();
430 let offline = check_aur_updates_offline(&cache).await.unwrap();
431 assert_eq!(online, offline);
432 eprintln!("aur {:#?}", online);
433 }
434 #[tokio::test]
435 async fn test_check_devel_updates() {
436 let (online, cache) = check_devel_updates_online().await.unwrap();
437 let offline = check_devel_updates_offline(&cache).await.unwrap();
438 assert_eq!(online, offline);
439 eprintln!("devel {:#?}", online);
440 }
441}