1pub 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 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}