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}