cargo-upgrades 2.2.1

Checks if dependencies in Cargo.toml are up to date. Compatible with workspaces and path dependencies.
Documentation
//! Install with:
//!
//! ```bash
//! cargo install cargo-upgrades
//! ```
pub use cargo_metadata::Error as MetadataError;
pub use crates_index::Error as IndexError;
use cargo_metadata::CargoOpt;
use cargo_metadata::Dependency;
use cargo_metadata::Metadata;
use cargo_metadata::MetadataCommand;
use cargo_metadata::Package;
use cargo_metadata::PackageId;
use crates_index::Crate;
use crates_index::SparseIndex;
use quick_error::quick_error;
use semver::Version;
use ureq::http;
use ureq::tls::{TlsConfig, TlsProvider};
use ureq::Agent;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Mutex;
use std::time::Duration;

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,
}

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),
        })
    }
}

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)
                };

                // Using an unstable req is an opt-in to picking any latest version, even if unstable
                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 mut request = self.request_for_crate(crate_name, http::Version::HTTP_2)?;

        let response: Result<http::Response<_>, _> = loop {
            break match self.agent.run(request.clone()) {
                Ok(response) => Ok(response),
                Err(e) => {
                    // CloudFront sucks?
                    if let ureq::Error::StatusCode(505) = e {
                        if request.version() == http::Version::HTTP_2 {
                            request = self.request_for_crate(crate_name, http::Version::HTTP_11)?;
                            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()?);
        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());
}