pub use cargo_metadata::Error as MetadataError;
use cargo_metadata::{CargoOpt, Dependency, Metadata, MetadataCommand, Package, PackageId};
pub use crates_index::Error as IndexError;
use crates_index::{Crate, SparseIndex};
use quick_error::quick_error;
use semver::Version;
use std::collections::{HashMap, HashSet};
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::time::Duration;
use ureq::tls::{TlsConfig, TlsProvider};
use ureq::{Agent, http};
quick_error! {
#[derive(Debug)]
pub enum Error {
Index(err: IndexError) {
from()
display("can't fetch index")
source(err)
}
PackageNotFound {
display("package not found in the metadata")
}
Metadata(err: MetadataError) {
from()
display("can't get crate metadata")
source(err)
}
Network(err: ureq::Error) {
from()
display("could not access the crates-io registry")
source(err)
}
}
}
pub struct UpgradesChecker {
workspace: Workspace,
index: SparseIndex,
refreshed_crates: Mutex<HashSet<String>>,
retries: AtomicU32,
agent: Agent,
cloudfront_cant_do_http2_lol: AtomicBool,
}
impl UpgradesChecker {
pub fn new(manifest_path: Option<&str>) -> Result<Self, Error> {
#[cfg(feature = "aws_lc_rs")]
static INIT: std::sync::Once = std::sync::Once::new();
#[cfg(feature = "aws_lc_rs")]
INIT.call_once(|| {
rustls::crypto::aws_lc_rs::default_provider().install_default().unwrap();
});
let agent = ureq::config::Config::builder()
.https_only(true)
.user_agent(format!("cargo-upgrades/{} ureq", env!("CARGO_PKG_VERSION")))
.timeout_global(Some(Duration::from_secs(5)))
.tls_config(TlsConfig::builder()
.provider(if cfg!(feature = "aws_lc_rs") { TlsProvider::Rustls } else { TlsProvider::NativeTls })
.build())
.build()
.new_agent();
let manifest_path = manifest_path.map(|s| s.to_owned());
let t = std::thread::spawn(move || {
Workspace::new(manifest_path.as_deref())
});
let index = SparseIndex::new_cargo_default()?;
let workspace = t.join().unwrap()?;
Ok(Self {
workspace,
index,
agent,
refreshed_crates: Default::default(),
retries: AtomicU32::new(0),
cloudfront_cant_do_http2_lol: AtomicBool::new(false),
})
}
}
struct Workspace {
packages: HashMap<PackageId, Package>,
members: Vec<PackageId>,
}
pub struct Match<'a> {
pub dependency: &'a Dependency,
pub matches: Option<Version>,
pub latest: Version,
}
impl Workspace {
pub fn new(manifest_path: Option<&str>) -> Result<Self, MetadataError> {
let 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)
})?;
Ok(Self {
packages: metadata.packages.into_iter().map(|p| (p.id.clone(), p)).collect(),
members: metadata.workspace_members,
})
}
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()
}
pub fn check_package(&self, id: &PackageId, checker: &UpgradesChecker, include_prerelease: bool) -> Option<(&Package, Vec<Result<Match<'_>, Error>>)> {
std::thread::scope(move |s| {
let package = self.packages.get(id)?;
let threads = package.dependencies.iter().map(move |dep| std::thread::Builder::new().spawn_scoped(s, move || {
let is_from_crates_io = dep.source.as_ref().is_some_and(|s| s.is_crates_io());
if !is_from_crates_io {
return Ok(None);
}
let dep_name = dep.name.as_str();
let (c, fetch_err) = checker.get_crate(dep_name)?;
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(),
dependency: dep,
}))
}));
let deps: Vec<_> = threads.map(|t| t.unwrap().join().unwrap())
.filter_map(|res| res.transpose())
.collect();
if deps.is_empty() {
return None;
}
Some((package, deps))
})
}
}
impl UpgradesChecker {
pub fn outdated_dependencies(&self, include_prerelease: bool) -> impl Iterator<Item=(&Package, Vec<Result<Match<'_>, Error>>)> + '_ {
self.workspace.members.iter().filter_map(move |id| {
self.workspace.check_package(id, self, include_prerelease)
})
}
pub fn get_crate(&self, crate_name: &str) -> Result<(Crate, Option<Error>), Error> {
let not_updated_yet = self.refreshed_crates.lock().unwrap().insert(crate_name.into());
let fetch_err = if not_updated_yet {
match self.fetch_crate(crate_name) {
Ok(Some(c)) => return Ok((c, None)),
Ok(None) => None,
Err(e) => Some(e),
}
} else {
None
};
Ok((self.index.crate_from_cache(crate_name)?, fetch_err))
}
fn fetch_crate(&self, crate_name: &str) -> Result<Option<Crate>, Error> {
let start_downgraded = self.cloudfront_cant_do_http2_lol.load(Ordering::Relaxed);
let mut fallback_tried = false;
let mut request = self.request_for_crate(
crate_name,
if start_downgraded {
http::Version::HTTP_11
} else {
http::Version::HTTP_2
},
)?;
let response: Result<http::Response<_>, _> = loop {
break match self.agent.run(request.clone()) {
Ok(response) => Ok(response),
Err(e) => {
if !start_downgraded && request.version() == http::Version::HTTP_2 &&
let ureq::Error::StatusCode(505) | ureq::Error::Protocol(ureq_proto::Error::UnsupportedVersion) = e {
request = self.request_for_crate(crate_name, http::Version::HTTP_11)?;
fallback_tried = true;
continue;
}
let r = self.retries.fetch_add(1, Ordering::Relaxed);
if r < 5 {
std::thread::sleep(Duration::from_millis(50 << r));
continue;
}
Err(e)
},
};
};
let (parts, mut body) = response?.into_parts();
let response = http::Response::from_parts(parts, body.read_to_vec()?);
if fallback_tried {
self.cloudfront_cant_do_http2_lol.store(true, Ordering::Relaxed);
}
Ok(self.index.parse_cache_response(crate_name, response, true)?)
}
fn request_for_crate(&self, crate_name: &str, http_ver: http::Version) -> Result<http::Request<()>, Error> {
let request = self.index.make_cache_request(crate_name)?
.version(http_ver)
.body(())
.map_err(ureq::Error::from)?;
Ok(request)
}
}
#[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(None).unwrap();
assert_eq!(0, u.outdated_dependencies(false).count());
}