pub use cargo_metadata::Error as MetadataError;
use cargo_metadata::{CargoOpt, Dependency, Metadata, MetadataCommand, Package};
pub use crates_index::Error as IndexError;
pub use crates_index::sparse::URL as CRATES_IO;
use crates_index::{Crate, SparseIndex};
use quick_error::quick_error;
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex, RwLock};
use semver::Version;
use std::sync::atomic::{AtomicU32, Ordering};
use std::thread;
use std::time::Duration;
quick_error! {
#[derive(Debug)]
pub enum Error {
Index(err: IndexError, reg: Box<str>) {
display("can't fetch index {reg}")
source(err)
}
PackageNotFound {
display("package not found in the metadata")
}
Metadata(err: MetadataError) {
from()
display("can't get crate metadata")
source(err)
}
Network(err: reqwest::Error) {
from()
display("could not access the crates-io registry")
source(err)
}
}
}
#[cold]
fn index_err(err: IndexError, reg: &str) -> Error {
Error::Index(err, reg.into())
}
pub struct UpgradesChecker {
index_per_source: RwLock<HashMap<Box<str>, Arc<SparseIndex>>>,
refreshed_crates: Mutex<HashSet<String>>,
retries: AtomicU32,
client: reqwest::blocking::Client,
}
impl UpgradesChecker {
pub fn new() -> Result<Self, Error> {
let client = reqwest::blocking::Client::builder()
.https_only(true)
.user_agent(format!(
"cargo-upgrades/{} reqwest",
env!("CARGO_PKG_VERSION")
))
.timeout(Duration::from_secs(5))
.build()?;
let index = SparseIndex::from_url_with_hash_kind(CRATES_IO, &crates_index::HashKind::Stable).map_err(|e| index_err(e, CRATES_IO))?;
Ok(Self {
index_per_source: RwLock::new([(CRATES_IO.into(), Arc::new(index))].into_iter().collect()),
client,
refreshed_crates: Default::default(),
retries: AtomicU32::new(0),
})
}
fn get_index(&self, registry_url: &str) -> Result<Arc<SparseIndex>, Error> {
if let Some(index) = self.index_per_source.read().unwrap().get(registry_url) {
return Ok(Arc::clone(index));
}
let mut locked = self.index_per_source.write().unwrap();
Ok(match locked.entry(registry_url.into()) {
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))?))),
Entry::Occupied(e) => Arc::clone(e.get()),
})
}
}
pub struct Workspace {
packages: Vec<Package>,
}
pub struct Match {
pub matches: Option<Version>,
pub latest: Version,
}
impl Workspace {
pub fn new(manifest_path: Option<&str>) -> Result<Self, Error> {
let mut metadata = Self::new_metadata(manifest_path, CargoOpt::AllFeatures)
.or_else(move |e| {
Self::new_metadata(manifest_path, CargoOpt::SomeFeatures(vec![]))
.or_else(move |_| Self::new_metadata(manifest_path, CargoOpt::NoDefaultFeatures))
.map_err(|_| e)
})?;
let members: HashSet<_> = metadata.workspace_members.into_iter().collect();
metadata.packages.retain(|p| members.contains(&p.id));
Ok(Self {
packages: metadata.packages,
})
}
fn new_metadata(manifest_path: Option<&str>, features: CargoOpt) -> Result<Metadata, MetadataError> {
let mut cmd = MetadataCommand::new();
if let Some(path) = manifest_path {
cmd.manifest_path(path);
}
cmd.features(features);
cmd.exec()
}
}
impl UpgradesChecker {
pub fn check_package<'p>(&self, package: &'p Package, include_prerelease: bool) -> Option<Vec<(&'p Dependency, Result<Match, Error>)>> {
thread::scope(move |s| {
let spawned = package.dependencies.iter()
.filter_map(move |dep| {
let registry_url = match &dep.source {
Some(s) if s.is_crates_io() => CRATES_IO,
Some(s) if s.repr.starts_with("sparse+http") => &s.repr,
Some(_) => return None,
None if dep.path.is_some() => return None,
None => dep.registry.as_deref().unwrap_or(CRATES_IO),
};
Some((dep, thread::Builder::new().spawn_scoped(s, move || self.get_crate(&dep.name, registry_url)).ok()?))
})
.collect::<Vec<_>>();
let deps = spawned.into_iter().filter_map(|(dep, res)| Some((dep, res.join().expect("panic").and_then(|(c, fetch_err)| {
let fetch_err = fetch_err.map(Err).unwrap_or(Ok(()));
let (matching, non_matching): (Vec<_>, Vec<_>) = c.versions().iter()
.filter(|v| !v.is_yanked())
.filter_map(|v| Version::parse(v.version()).ok())
.partition(move |v| dep.req.matches(v));
let latest_stable = matching.iter().chain(&non_matching).filter(|v| v.pre.is_empty()).max();
let matches_latest_stable = latest_stable.is_some_and(move |v| dep.req.matches(v));
if !include_prerelease && matches_latest_stable {
fetch_err?;
return Ok(None);
}
let Some(latest_any) = matching.iter().chain(&non_matching).max() else {
fetch_err?;
return Ok(None)
};
let matches_any_unstable = matching.iter().any(|v| !v.pre.is_empty());
let latest = if include_prerelease || matches_any_unstable {
latest_any
} else {
latest_stable.unwrap_or(latest_any)
};
if dep.req.matches(latest) {
fetch_err?;
return Ok(None);
}
Ok(Some(Match {
latest: latest.clone(),
matches: matching.into_iter().max(),
}))
}).transpose()?)))
.collect::<Vec<_>>();
if deps.is_empty() {
return None;
}
Some(deps)
})
}
pub fn outdated_dependencies<'ws>(&self, workspace: &'ws Workspace, include_prerelease: bool) -> impl Iterator<Item=(&'ws Package, Vec<(&'ws Dependency, Result<Match, Error>)>)> {
workspace.packages.iter().filter_map(move |package| {
Some((package, self.check_package(package, include_prerelease)?))
})
}
pub fn get_crate(&self, crate_name: &str, registry_url: &str) -> Result<(Crate, Option<Error>), Error> {
let index = self.get_index(registry_url)?;
let not_updated_yet = self.refreshed_crates.lock().unwrap().insert(format!("{registry_url}:{crate_name}"));
let fetch_err = if not_updated_yet {
match self.fetch_crate(crate_name, &index) {
Ok(Some(c)) => return Ok((c, None)),
Ok(None) => None,
Err(e) => Some(e),
}
} else {
None
};
Ok((index.crate_from_cache(crate_name).map_err(|e| index_err(e, registry_url))?, fetch_err))
}
fn fetch_crate(&self, crate_name: &str, index: &SparseIndex) -> Result<Option<Crate>, Error> {
let index_err = |e| index_err(e, index.url());
let builder = index.make_cache_request(crate_name).map_err(index_err)?;
let body = builder.body(Vec::<u8>::new()).map_err(|e| IndexError::Io(std::io::Error::other(e))).map_err(index_err)?;
let reqwest_request = reqwest::blocking::Request::try_from(body)?;
let mut response = loop {
match self.client.execute(reqwest_request.try_clone().ok_or_else(|| IndexError::Io(std::io::Error::other("try_clone"))).map_err(index_err)?) {
Ok(response) => break response,
Err(e) => {
let r = self.retries.fetch_add(1, Ordering::Relaxed);
if r < 5 {
thread::sleep(Duration::from_millis(50 << r));
continue;
}
return Err(Error::Network(e));
},
}
};
let mut builder = http::Response::builder()
.status(response.status())
.version(response.version());
if let Some(headers) = builder.headers_mut() {
headers.extend(response.headers_mut().drain());
}
let body = response.bytes()?.to_vec();
let http_response = builder.body(body).map_err(|e| IndexError::Io(std::io::Error::other(e))).map_err(index_err)?;
index.parse_cache_response(crate_name, http_response, true).map_err(index_err)
}
}
#[test]
fn beta_vs_stable() {
let beta11 = Version::parse("1.0.1-beta.1").unwrap();
let beta1 = Version::parse("1.0.0-beta.1").unwrap();
let v100 = Version::parse("1.0.0").unwrap();
assert!(v100 > beta1);
assert!(beta11 > beta1);
assert!(beta11 > v100);
}
#[test]
fn test_self() {
let u = UpgradesChecker::new().unwrap();
let ws = Workspace::new(None).unwrap();
assert_eq!(0, u.outdated_dependencies(&ws, false).count());
}