Skip to main content

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};
8pub use crates_index::Error as IndexError;
9pub use crates_index::sparse::URL as CRATES_IO;
10use crates_index::{Crate, SparseIndex};
11use quick_error::quick_error;
12use std::collections::hash_map::Entry;
13use std::collections::{HashMap, HashSet};
14use std::sync::{Arc, Mutex, RwLock};
15use semver::Version;
16use std::sync::atomic::{AtomicU32, Ordering};
17use std::thread;
18use std::time::Duration;
19
20quick_error! {
21    #[derive(Debug)]
22    pub enum Error {
23        Index(err: IndexError, reg: Box<str>) {
24            display("can't fetch index {reg}")
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: reqwest::Error) {
36            from()
37            display("could not access the crates-io registry")
38            source(err)
39        }
40    }
41}
42
43#[cold]
44fn index_err(err: IndexError, reg: &str) -> Error {
45    Error::Index(err, reg.into())
46}
47
48pub struct UpgradesChecker {
49    index_per_source: RwLock<HashMap<Box<str>, Arc<SparseIndex>>>,
50    refreshed_crates: Mutex<HashSet<String>>,
51    retries: AtomicU32,
52    client: reqwest::blocking::Client,
53}
54
55impl UpgradesChecker {
56    pub fn new() -> Result<Self, Error> {
57        let client = reqwest::blocking::Client::builder()
58            .https_only(true)
59            .user_agent(format!(
60                "cargo-upgrades/{} reqwest",
61                env!("CARGO_PKG_VERSION")
62            ))
63            .timeout(Duration::from_secs(5))
64            .build()?;
65
66        let index = SparseIndex::from_url_with_hash_kind(CRATES_IO, &crates_index::HashKind::Stable).map_err(|e| index_err(e, CRATES_IO))?;
67
68        Ok(Self {
69            index_per_source: RwLock::new([(CRATES_IO.into(), Arc::new(index))].into_iter().collect()),
70            client,
71            refreshed_crates: Default::default(),
72            retries: AtomicU32::new(0),
73        })
74    }
75
76    fn get_index(&self, registry_url: &str) -> Result<Arc<SparseIndex>, Error> {
77        if let Some(index) = self.index_per_source.read().unwrap().get(registry_url) {
78            return Ok(Arc::clone(index));
79        }
80
81        let mut locked = self.index_per_source.write().unwrap();
82        Ok(match locked.entry(registry_url.into()) {
83            Entry::Vacant(e) => Arc::clone(e.insert(Arc::new(SparseIndex::from_url_with_hash_kind(registry_url, &crates_index::HashKind::Stable).map_err(|e| index_err(e, registry_url))?))),
84            Entry::Occupied(e) => Arc::clone(e.get()),
85        })
86    }
87}
88
89pub struct Workspace {
90    packages: Vec<Package>,
91}
92
93pub struct Match {
94    pub matches: Option<Version>,
95    pub latest: Version,
96}
97
98impl Workspace {
99    pub fn new(manifest_path: Option<&str>) -> Result<Self, Error> {
100        let mut metadata = Self::new_metadata(manifest_path, CargoOpt::AllFeatures)
101        .or_else(move |e| {
102            Self::new_metadata(manifest_path, CargoOpt::SomeFeatures(vec![]))
103                .or_else(move |_| Self::new_metadata(manifest_path, CargoOpt::NoDefaultFeatures))
104                .map_err(|_| e)
105        })?;
106        let members: HashSet<_> = metadata.workspace_members.into_iter().collect();
107        metadata.packages.retain(|p| members.contains(&p.id));
108        Ok(Self {
109            packages: metadata.packages,
110        })
111    }
112
113    fn new_metadata(manifest_path: Option<&str>, features: CargoOpt) -> Result<Metadata, MetadataError> {
114        let mut cmd = MetadataCommand::new();
115        if let Some(path) = manifest_path {
116            cmd.manifest_path(path);
117        }
118        cmd.features(features);
119        cmd.exec()
120    }
121}
122
123impl UpgradesChecker {
124    pub fn check_package<'p>(&self, package: &'p Package, include_prerelease: bool) -> Option<Vec<(&'p Dependency, Result<Match, Error>)>> {
125        thread::scope(move |s| {
126            let spawned = package.dependencies.iter()
127                .filter_map(move |dep| {
128                    let registry_url = match &dep.source {
129                        Some(s) if s.is_crates_io() => CRATES_IO,
130                        Some(s) if s.repr.starts_with("sparse+http") => &s.repr,
131                        Some(_) => return None,
132                        None if dep.path.is_some() => return None,
133                        None => dep.registry.as_deref().unwrap_or(CRATES_IO),
134                    };
135                    Some((dep, thread::Builder::new().spawn_scoped(s, move || self.get_crate(&dep.name, registry_url)).ok()?))
136                })
137                .collect::<Vec<_>>();
138
139            let deps = spawned.into_iter().filter_map(|(dep, res)| Some((dep, res.join().expect("panic").and_then(|(c, fetch_err)| {
140                let fetch_err = fetch_err.map(Err).unwrap_or(Ok(()));
141
142                let (matching, non_matching): (Vec<_>, Vec<_>) = c.versions().iter()
143                    .filter(|v| !v.is_yanked())
144                    .filter_map(|v| Version::parse(v.version()).ok())
145                    .partition(move |v| dep.req.matches(v));
146
147                let latest_stable = matching.iter().chain(&non_matching).filter(|v| v.pre.is_empty()).max();
148                let matches_latest_stable = latest_stable.is_some_and(move |v| dep.req.matches(v));
149                if !include_prerelease && matches_latest_stable {
150                    fetch_err?;
151                    return Ok(None);
152                }
153
154                let Some(latest_any) = matching.iter().chain(&non_matching).max() else {
155                    fetch_err?;
156                    return Ok(None)
157                };
158
159                // Using an unstable req is an opt-in to picking any latest version, even if unstable
160                let matches_any_unstable = matching.iter().any(|v| !v.pre.is_empty());
161                let latest = if include_prerelease || matches_any_unstable {
162                    latest_any
163                } else {
164                    latest_stable.unwrap_or(latest_any)
165                };
166
167                if dep.req.matches(latest) {
168                    fetch_err?;
169                    return Ok(None);
170                }
171
172                Ok(Some(Match {
173                    latest: latest.clone(),
174                    matches: matching.into_iter().max(),
175                }))
176            }).transpose()?)))
177            .collect::<Vec<_>>();
178
179            if deps.is_empty() {
180                return None;
181            }
182            Some(deps)
183        })
184    }
185
186    pub fn outdated_dependencies<'ws>(&self, workspace: &'ws Workspace, include_prerelease: bool) -> impl Iterator<Item=(&'ws Package, Vec<(&'ws Dependency, Result<Match, Error>)>)> {
187        workspace.packages.iter().filter_map(move |package| {
188            Some((package, self.check_package(package, include_prerelease)?))
189        })
190    }
191
192    pub fn get_crate(&self, crate_name: &str, registry_url: &str) -> Result<(Crate, Option<Error>), Error> {
193        let index = self.get_index(registry_url)?;
194        let not_updated_yet = self.refreshed_crates.lock().unwrap().insert(format!("{registry_url}:{crate_name}"));
195        let fetch_err = if not_updated_yet {
196            match self.fetch_crate(crate_name, &index) {
197                Ok(Some(c)) => return Ok((c, None)),
198                Ok(None) => None,
199                Err(e) => Some(e),
200            }
201        } else {
202            None
203        };
204
205        Ok((index.crate_from_cache(crate_name).map_err(|e| index_err(e, registry_url))?, fetch_err))
206    }
207
208    fn fetch_crate(&self, crate_name: &str, index: &SparseIndex) -> Result<Option<Crate>, Error> {
209        let index_err = |e| index_err(e, index.url());
210        let builder = index.make_cache_request(crate_name).map_err(index_err)?;
211        let body = builder.body(Vec::<u8>::new()).map_err(|e| IndexError::Io(std::io::Error::other(e))).map_err(index_err)?;
212        let reqwest_request = reqwest::blocking::Request::try_from(body)?;
213
214        let mut response = loop {
215            match self.client.execute(reqwest_request.try_clone().ok_or_else(|| IndexError::Io(std::io::Error::other("try_clone"))).map_err(index_err)?) {
216                Ok(response) => break response,
217                Err(e) => {
218                    let r = self.retries.fetch_add(1, Ordering::Relaxed);
219                    if r < 5 {
220                        thread::sleep(Duration::from_millis(50 << r));
221                        continue;
222                    }
223                    return Err(Error::Network(e));
224                },
225            }
226        };
227
228        let mut builder = http::Response::builder()
229            .status(response.status())
230            .version(response.version());
231
232        if let Some(headers) = builder.headers_mut() {
233            headers.extend(response.headers_mut().drain());
234        }
235
236        let body = response.bytes()?.to_vec();
237        let http_response = builder.body(body).map_err(|e| IndexError::Io(std::io::Error::other(e))).map_err(index_err)?;
238
239        index.parse_cache_response(crate_name, http_response, true).map_err(index_err)
240    }
241}
242
243#[test]
244fn beta_vs_stable() {
245    let beta11 = Version::parse("1.0.1-beta.1").unwrap();
246    let beta1 = Version::parse("1.0.0-beta.1").unwrap();
247    let v100 = Version::parse("1.0.0").unwrap();
248    assert!(v100 > beta1);
249    assert!(beta11 > beta1);
250    assert!(beta11 > v100);
251}
252
253#[test]
254fn test_self() {
255    let u = UpgradesChecker::new().unwrap();
256    let ws = Workspace::new(None).unwrap();
257    assert_eq!(0, u.outdated_dependencies(&ws, false).count());
258}