Skip to main content

check_updates/
lib.rs

1use std::collections::HashSet;
2use std::path::PathBuf;
3use std::rc::Rc;
4
5use reqwest::Client;
6use semver::VersionReq;
7
8use crate::registry::*;
9
10mod package;
11mod registry;
12
13pub use package::{DepKind, Package, PackageVersion, Packages, Unit, Usage};
14
15type Purl = purl::GenericPurl<String>;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum RegistryCachePolicy {
19    PreferLocal,
20    #[default]
21    Refresh,
22    NoCache,
23}
24
25#[derive(Debug, Clone, Copy, Default)]
26pub struct Options {
27    pub registry_cache_policy: RegistryCachePolicy,
28}
29
30pub struct State {
31    client: Client,
32    root: Option<PathBuf>,
33    registry_cache_policy: RegistryCachePolicy,
34}
35
36impl State {
37    pub fn new(root: Option<PathBuf>, options: Options) -> Self {
38        let client = Client::builder()
39            .http2_adaptive_window(true)
40            .user_agent(concat!(
41                env!("CARGO_PKG_NAME"),
42                "/",
43                env!("CARGO_PKG_VERSION"),
44                " (+",
45                env!("CARGO_PKG_REPOSITORY"),
46                ")"
47            ))
48            .build()
49            .expect("failed to initialize HTTP client");
50        Self {
51            client,
52            root,
53            registry_cache_policy: options.registry_cache_policy,
54        }
55    }
56
57    pub fn client(&self) -> &Client {
58        &self.client
59    }
60
61    pub fn root(&self) -> Option<&PathBuf> {
62        self.root.as_ref()
63    }
64
65    pub fn registry_cache_policy(&self) -> RegistryCachePolicy {
66        self.registry_cache_policy
67    }
68}
69
70#[derive(Debug, thiserror::Error)]
71pub enum Error {
72    #[error("registry error: {0}")]
73    Registry(#[from] RegistryError),
74}
75
76pub struct CheckUpdates {
77    cargo: Registry,
78}
79
80impl CheckUpdates {
81    pub fn new(root: Option<PathBuf>) -> Self {
82        Self::with_options(root, Options::default())
83    }
84
85    pub fn with_options(root: Option<PathBuf>, options: Options) -> Self {
86        let state = Rc::new(State::new(root, options));
87        let cargo = CargoRegistry::new(state.clone());
88
89        Self {
90            cargo: cargo.into(),
91        }
92    }
93
94    pub async fn packages(&self) -> Result<Packages, Error> {
95        let mut res: Packages = Default::default();
96        // Track (unit, package_name, req, kind) to deduplicate while still
97        // preserving distinct dep sections in output.
98        let mut seen: HashSet<(Unit, String, String, DepKind)> = HashSet::new();
99        for package in self.cargo.packages().await? {
100            for usage in &package.usages {
101                // Wildcard requirements have nothing to update.
102                if usage.req == VersionReq::STAR {
103                    continue;
104                }
105                let key = (
106                    usage.unit.clone(),
107                    package.purl.name().to_string(),
108                    usage.req.to_string(),
109                    usage.kind,
110                );
111                if !seen.insert(key) {
112                    continue;
113                }
114                res.entry(usage.unit.clone()).or_default().push((
115                    usage.req.clone(),
116                    usage.kind,
117                    package.clone(),
118                ));
119            }
120        }
121        Ok(res)
122    }
123
124    /// Update the locally installed versions of the given packages to the one specified
125    pub fn update_versions<'a>(
126        &self,
127        packages: impl IntoIterator<Item = (&'a Usage, &'a Package, VersionReq)>,
128    ) -> Result<(), Error> {
129        self.cargo.update_versions(packages)?;
130        Ok(())
131    }
132}