// SPDX-FileCopyrightText: 2023 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later use std::{fs, path::PathBuf}; use clap::*; use log::*; use semver::Version; use semver::VersionReq; #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] struct CLI { path: PathBuf, } fn init_logger() { // if std::env::var("RUST_LOG").is_err() { // std::env::set_var("RUST_LOG", "info"); // } pretty_env_logger::init(); } fn skip_entry(name: &str, v: &toml::Value) -> bool { if v.is_table() { if v.get("git").is_some() { info!("[{name}] Skipping, Found Git src: {:?}", v); return true; } else if v.get("path").is_some() { info!("[{name}] Skipping, Found local dep: {:?}", v); return true; } } false } async fn find_latest_version(c: &reqwest::Client, name: &str) -> Version { let url = if name.len() == 1 { format!("https://index.crates.io/1/{}", name) } else if name.len() == 2 { format!("https://index.crates.io/2/{}", name) } else if name.len() == 3 { format!("https://index.crates.io/3/{}/{}", &name[0..1], name) } else { format!( "https://index.crates.io/{}/{}/{}", &name[0..2].to_lowercase(), &name[2..4].to_lowercase(), name ) }; let resp = c.get(&url).send().await.unwrap(); let text = resp.text().await.unwrap(); let mut latest = Version::parse("0.0.0").unwrap(); for line in text.lines() { if line.trim().is_empty() { continue; } let this: serde_json::Value = serde_json::from_str(&line).unwrap_or_else(|_| panic!("{}", line)); let vers = this.get("vers").unwrap(); let vers = Version::parse(vers.as_str().unwrap()).unwrap(); if vers > latest { latest = vers; } } latest } #[tokio::main] async fn main() { init_logger(); let opts = CLI::parse(); let contents = fs::read_to_string(opts.path).unwrap(); let manifest: toml::Value = toml::from_str(&contents).unwrap(); let c = reqwest::Client::builder(); let c = c.build().unwrap(); let mut updates = Vec::default(); if let Some(toml::Value::Table(deps)) = manifest.get("dependencies") { for d in deps.iter() { let name = d.0.as_str(); if skip_entry(name, d.1) { continue; } let using = if d.1.is_str() { d.1.as_str().unwrap() } else { d.1.get("version") .unwrap_or_else(|| panic!("version not found: {:?}", d)) .as_str() .unwrap() }; let using = VersionReq::parse(using).unwrap(); debug!("name: {name} version: {using}"); let latest = find_latest_version(&c, name).await; info!("{name} using: {using} latest {latest}"); // since remove version can only increase, failing to match can be assumed to be the // same as "new version available" if !using.matches(&latest) { updates.push((name, (using, latest))); } } } for (name, (using, latest)) in updates.iter() { println!("[{name}] {using} < {latest}") } }