feat: parse Cargo manifest and cmp version against Crates.io index

This commit is contained in:
Aravinth Manivannan 2023-10-14 16:19:16 +05:30
parent b225153dad
commit befc7ba705
Signed by: realaravinth
GPG key ID: F8F50389936984FF
3 changed files with 1620 additions and 0 deletions

1484
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

18
Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "deps-manager"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.4.6", features = ["derive"] }
log = "0.4.20"
pretty_env_logger = "0.5.0"
reqwest = { version = "0.11.22", features = ["json", "gzip"] }
semver = { version = "1.0.20", features = ["serde"] }
serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.107"
tokio = { version = "1.33.0", features = ["rt", "rt-multi-thread", "macros"] }
tokio-rustls = "0.24.1"
toml = "0.8.2"

118
src/main.rs Normal file
View file

@ -0,0 +1,118 @@
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// 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}")
}
}