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