cargo_upgrades/
lib.rs

1//! Install with:
2//!
3//! ```bash
4//! cargo install cargo-upgrades
5//! ```
6pub use cargo_metadata::Error as MetadataError;
7use cargo_metadata::{CargoOpt, Dependency, Metadata, MetadataCommand, Package, PackageId};
8pub use crates_index::Error as IndexError;
9use crates_index::{Crate, SparseIndex};
10use quick_error::quick_error;
11use semver::Version;
12use std::collections::{HashMap, HashSet};
13use std::sync::Mutex;
14use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
15use std::time::Duration;
16use ureq::tls::{TlsConfig, TlsProvider};
17use ureq::{Agent, http};
18
19quick_error! {
20    #[derive(Debug)]
21    pub enum Error {
22        Index(err: IndexError) {
23            from()
24            display("can't fetch index")
25            source(err)
26        }
27        PackageNotFound {
28            display("package not found in the metadata")
29        }
30        Metadata(err: MetadataError) {
31            from()
32            display("can't get crate metadata")
33            source(err)
34        }
35        Network(err: ureq::Error) {
36            from()
37            display("could not access the crates-io registry")
38            source(err)
39        }
40    }
41}
42
43pub struct UpgradesChecker {
44    workspace: Workspace,
45    index: SparseIndex,
46    refreshed_crates: Mutex<HashSet<String>>,
47    retries: AtomicU32,
48    agent: Agent,
49    cloudfront_cant_do_http2_lol: AtomicBool,
50}
51
52impl UpgradesChecker {
53    pub fn new(manifest_path: Option<&str>) -> Result<Self, Error> {
54        #[cfg(feature = "aws_lc_rs")]
55        static INIT: std::sync::Once = std::sync::Once::new();
56
57        #[cfg(feature = "aws_lc_rs")]
58        INIT.call_once(|| {
59            rustls::crypto::aws_lc_rs::default_provider().install_default().unwrap();
60        });
61
62        let agent = ureq::config::Config::builder()
63            .https_only(true)
64            .user_agent(format!("cargo-upgrades/{} ureq", env!("CARGO_PKG_VERSION")))
65            .timeout_global(Some(Duration::from_secs(5)))
66            .tls_config(TlsConfig::builder()
67                .provider(if cfg!(feature = "aws_lc_rs") { TlsProvider::Rustls } else { TlsProvider::NativeTls })
68                .build())
69            .build()
70            .new_agent();
71
72        let manifest_path = manifest_path.map(|s| s.to_owned());
73        let t = std::thread::spawn(move || {
74            Workspace::new(manifest_path.as_deref())
75        });
76
77        let index = SparseIndex::new_cargo_default()?;
78        let workspace = t.join().unwrap()?;
79
80        Ok(Self {
81            workspace,
82            index,
83            agent,
84            refreshed_crates: Default::default(),
85            retries: AtomicU32::new(0),
86            cloudfront_cant_do_http2_lol: AtomicBool::new(false),
87        })
88    }
89}
90
91struct Workspace {
92    packages: HashMap<PackageId, Package>,
93    members: Vec<PackageId>,
94}
95
96pub struct Match<'a> {
97    pub dependency: &'a Dependency,
98    pub matches: Option<Version>,
99    pub latest: Version,
100}
101
102impl Workspace {
103    pub fn new(manifest_path: Option<&str>) -> Result<Self, MetadataError> {
104        let metadata = Self::new_metadata(manifest_path, CargoOpt::AllFeatures)
105        .or_else(move |e| {
106            Self::new_metadata(manifest_path, CargoOpt::SomeFeatures(vec![]))
107                .or_else(move |_| Self::new_metadata(manifest_path, CargoOpt::NoDefaultFeatures))
108                .map_err(|_| e)
109        })?;
110        Ok(Self {
111            packages: metadata.packages.into_iter().map(|p| (p.id.clone(), p)).collect(),
112            members: metadata.workspace_members,
113        })
114    }
115
116    fn new_metadata(manifest_path: Option<&str>, features: CargoOpt) -> Result<Metadata, MetadataError> {
117        let mut cmd = MetadataCommand::new();
118        if let Some(path) = manifest_path {
119            cmd.manifest_path(path);
120        }
121        cmd.features(features);
122        cmd.exec()
123    }
124
125    pub fn check_package(&self, id: &PackageId, checker: &UpgradesChecker, include_prerelease: bool) -> Option<(&Package, Vec<Result<Match<'_>, Error>>)> {
126        std::thread::scope(move |s| {
127            let package = self.packages.get(id)?;
128            let threads = package.dependencies.iter().map(move |dep| std::thread::Builder::new().spawn_scoped(s, move || {
129                let is_from_crates_io = dep.source.as_ref().is_some_and(|s| s.is_crates_io());
130                if !is_from_crates_io {
131                    return Ok(None);
132                }
133                let dep_name = dep.name.as_str();
134                let (c, fetch_err) = checker.get_crate(dep_name)?;
135                let fetch_err = fetch_err.map(Err).unwrap_or(Ok(()));
136
137                let (matching, non_matching): (Vec<_>, Vec<_>) = c.versions().iter()
138                    .filter(|v| !v.is_yanked())
139                    .filter_map(|v| Version::parse(v.version()).ok())
140                    .partition(move |v| dep.req.matches(v));
141
142                let latest_stable = matching.iter().chain(&non_matching).filter(|v| v.pre.is_empty()).max();
143                let matches_latest_stable = latest_stable.is_some_and(move |v| dep.req.matches(v));
144                if !include_prerelease && matches_latest_stable {
145                    fetch_err?;
146                    return Ok(None);
147                }
148
149                let Some(latest_any) = matching.iter().chain(&non_matching).max() else {
150                    fetch_err?;
151                    return Ok(None)
152                };
153
154                // Using an unstable req is an opt-in to picking any latest version, even if unstable
155                let matches_any_unstable = matching.iter().any(|v| !v.pre.is_empty());
156                let latest = if include_prerelease || matches_any_unstable {
157                    latest_any
158                } else {
159                    latest_stable.unwrap_or(latest_any)
160                };
161
162                if dep.req.matches(latest) {
163                    fetch_err?;
164                    return Ok(None);
165                }
166
167                Ok(Some(Match {
168                    latest: latest.clone(),
169                    matches: matching.into_iter().max(),
170                    dependency: dep,
171                }))
172            }));
173
174            let deps: Vec<_> = threads.map(|t| t.unwrap().join().unwrap())
175                .filter_map(|res| res.transpose())
176                .collect();
177            if deps.is_empty() {
178                return None;
179            }
180            Some((package, deps))
181        })
182    }
183}
184
185impl UpgradesChecker {
186    pub fn outdated_dependencies(&self, include_prerelease: bool) -> impl Iterator<Item=(&Package, Vec<Result<Match<'_>, Error>>)> + '_ {
187        self.workspace.members.iter().filter_map(move |id| {
188            self.workspace.check_package(id, self, include_prerelease)
189        })
190    }
191
192    pub fn get_crate(&self, crate_name: &str) -> Result<(Crate, Option<Error>), Error> {
193        let not_updated_yet = self.refreshed_crates.lock().unwrap().insert(crate_name.into());
194        let fetch_err = if not_updated_yet {
195            match self.fetch_crate(crate_name) {
196                Ok(Some(c)) => return Ok((c, None)),
197                Ok(None) => None,
198                Err(e) => Some(e),
199            }
200        } else {
201            None
202        };
203
204        Ok((self.index.crate_from_cache(crate_name)?, fetch_err))
205    }
206
207    fn fetch_crate(&self, crate_name: &str) -> Result<Option<Crate>, Error> {
208        let start_downgraded = self.cloudfront_cant_do_http2_lol.load(Ordering::Relaxed);
209        let mut fallback_tried = false;
210        let mut request = self.request_for_crate(
211            crate_name,
212            if start_downgraded {
213                http::Version::HTTP_11
214            } else {
215                http::Version::HTTP_2
216            },
217        )?;
218
219        let response: Result<http::Response<_>, _> = loop {
220            break match self.agent.run(request.clone()) {
221                Ok(response) => Ok(response),
222                Err(e) => {
223                    // CloudFront sucks?
224                    if !start_downgraded && request.version() == http::Version::HTTP_2 &&
225                        let ureq::Error::StatusCode(505) | ureq::Error::Protocol(ureq_proto::Error::UnsupportedVersion) = e {
226                        request = self.request_for_crate(crate_name, http::Version::HTTP_11)?;
227                        fallback_tried = true;
228                        continue;
229                    }
230                    let r = self.retries.fetch_add(1, Ordering::Relaxed);
231                    if r < 5 {
232                        std::thread::sleep(Duration::from_millis(50 << r));
233                        continue;
234                    }
235                    Err(e)
236                },
237            };
238        };
239
240        let (parts, mut body) = response?.into_parts();
241        let response = http::Response::from_parts(parts, body.read_to_vec()?);
242        if fallback_tried {
243            self.cloudfront_cant_do_http2_lol.store(true, Ordering::Relaxed);
244        }
245        Ok(self.index.parse_cache_response(crate_name, response, true)?)
246    }
247
248    fn request_for_crate(&self, crate_name: &str, http_ver: http::Version) -> Result<http::Request<()>, Error> {
249        let request = self.index.make_cache_request(crate_name)?
250            .version(http_ver)
251            .body(())
252            .map_err(ureq::Error::from)?;
253        Ok(request)
254    }
255}
256
257#[test]
258fn beta_vs_stable() {
259    let beta11 = Version::parse("1.0.1-beta.1").unwrap();
260    let beta1 = Version::parse("1.0.0-beta.1").unwrap();
261    let v100 = Version::parse("1.0.0").unwrap();
262    assert!(v100 > beta1);
263    assert!(beta11 > beta1);
264    assert!(beta11 > v100);
265}
266
267#[test]
268fn test_self() {
269    let u = UpgradesChecker::new(None).unwrap();
270    assert_eq!(0, u.outdated_dependencies(false).count());
271}