Skip to content

Commit

Permalink
Tidy up crates checker tool (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
LiamGallagher737 authored Jul 15, 2024
1 parent 71d9a8c commit 89ad172
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 107 deletions.
211 changes: 104 additions & 107 deletions tools/check_crate_updates/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
use anyhow::anyhow;
//! This tool loops over each manifest for the images and for each Bevy crate, checks the support
//! table in its readme to see if it can be updated, if so it updates the manifest.
use crate::manifest::Manifest;
use anyhow::{anyhow, Context};
use cached::proc_macro::cached;
use std::{env, fs};
use std::{env, fs, path, str::FromStr};
use table_extract::Table;
use ureq::{Agent, AgentBuilder};

mod manifest;

const EXCLUDE_CRATES: &[&str] = &["bevy", "rand", "rand_chacha", "wasm-bindgen"];

fn main() -> anyhow::Result<()> {
Expand All @@ -25,127 +31,118 @@ fn main() -> anyhow::Result<()> {
.filter(|path| path.to_string_lossy().ends_with(".Cargo.toml"));

for path in manifest_paths {
let manifest_str = fs::read_to_string(&path)?;
let mut manifest = manifest_str
.parse::<toml_edit::DocumentMut>()
.map_err(|e| anyhow!("Failed to parse manifest at {path:?}\n{e}"))?;

// skip if no version - using main branch
if !manifest["dependencies"]["bevy"]
.as_inline_table()
.unwrap()
.contains_key("version")
{
println!("Skipping {path:?}");
continue;
}

let bevy_version = manifest["dependencies"]["bevy"]["version"]
.as_str()
.unwrap();

let crates = manifest["dependencies"]
.as_table()
.unwrap()
.iter()
.map(|(name, _)| name)
.filter(|name| !EXCLUDE_CRATES.contains(name))
.map(|name| fetch_crate(name, agent.clone()))
.inspect(|res| {
if let Err(e) = res {
eprintln!("Error getting crate: {e}");
}
})
.filter_map(|res| res.ok());

let mut newest_versions = Vec::new();

println!("Bevy: {bevy_version}");
for c in crates {
let readme = match fetch_readme(&c, agent.clone()) {
Ok(r) => r,
Err(e) => {
eprintln!("Error getting readme: {e}");
continue;
}
};

let table = match find_support_table(&readme) {
Ok(t) => t,
Err(e) => {
eprintln!("{e}");
continue;
}
};

// currently assuming the bevy column is first
let mut matching = Vec::new();
for row in table.iter().map(|r| r.as_slice()) {
let bevy = extract_version_from_cell(&row[0]);
let others = extract_versions_from_cell(&row[1]);
for other in others {
if bevy.starts_with(bevy_version) {
matching.push((bevy.clone(), other));
}
}
}

if matching.is_empty() {
eprintln!("{} has no matches for {bevy_version}", c.data.name);
continue;
}
if let Err(e) = handle_manifest(&path, agent.clone()) {
eprintln!("[ERROR] Error handling {path:?}: {e}");
};
}

let newest = matching
.iter()
.map(|(_, other)| other.parse::<semver::VersionReq>())
.inspect(|res| {
if let Err(e) = res {
eprintln!("Failed to parse: {e}");
}
})
.filter_map(Result::ok)
.map(|semver| {
c.versions
.iter()
.map(|v| v.version.parse::<semver::Version>().unwrap())
.filter(|v| semver.matches(v))
.max()
.unwrap()
})
.max()
.unwrap();
println!("Complete");

println!(
"The most recent version for {} compatible with Bevy {bevy_version} is {newest}",
c.data.name
);
Ok(())
}

newest_versions.push((c.data.name, format!("={newest}")));
fn handle_manifest(path: &path::PathBuf, agent: Agent) -> anyhow::Result<()> {
let manifest_str = fs::read_to_string(path)?;
let mut manifest = Manifest::from_str(&manifest_str)
.map_err(|e| anyhow!("Failed to parse manifest at {path:?}\n{e}"))?;

let bevy_version = manifest
.get_dependency("bevy")
.ok_or_else(|| anyhow!("Manifest does not contain Bevy"))?
.get_version()
.ok_or_else(|| anyhow!("Invalid Bevy version"))?
.to_owned();

let newest_versions = manifest
.get_dependency_names()
.unwrap() // we know bevy exists so it can't be empty
.filter(|name| !EXCLUDE_CRATES.contains(name))
.map(|name| fetch_crate(name, agent.clone()))
.inspect(|res| {
if let Err(e) = res {
eprintln!("Error getting crate: {e}");
}
})
.filter_map(|res| res.ok())
.map(|c| {
(
c.data.name.clone(),
get_newest_version(c, &bevy_version, agent.clone()),
)
})
.filter_map(|(name, version)| version.map(|v| (name, v)).ok())
.collect::<Vec<_>>();

for (name, version) in newest_versions {
if !manifest
.get_dependency_mut(&name)
.unwrap() // name is a result from dep list so it must exist
.set_version(&version)
{
eprintln!("[WARNING] Failed to set value of {name} to {version}");
}
}

for (name, version) in newest_versions {
if let Some(table) = manifest["dependencies"][&name].as_inline_table_mut() {
table["version"] = version.into();
} else {
manifest["dependencies"][name] = toml_edit::value(version);
fs::write(path, manifest.to_string()).with_context(|| "Failed to write manifest to disk")
}

fn get_newest_version(
c: CrateResponse,
bevy_version: &str,
agent: Agent,
) -> anyhow::Result<String> {
let readme = fetch_readme(&c, agent.clone()).with_context(|| "Failed to get readme")?;
let table = find_support_table(&readme).with_context(|| "Failed to find support table")?;

// currently assuming the bevy column is first
let mut matching = Vec::new();
for row in table.iter().map(|r| r.as_slice()) {
let bevy = extract_version_from_cell(&row[0]);
let others = extract_versions_from_cell(&row[1]);
for other in others {
if bevy.starts_with(bevy_version) {
matching.push((bevy.clone(), other));
}
}
}

if let Err(e) = fs::write(&path, manifest.to_string()) {
eprintln!("Failed to write to {path:?}: {e}");
}
if matching.is_empty() {
return Err(anyhow!("{} has no matches for {bevy_version}", c.data.name));
}

println!("Complete");
let newest = matching
.iter()
.map(|(_, other)| other.parse::<semver::VersionReq>())
.inspect(|res| {
if let Err(e) = res {
eprintln!("[WARNING] Failed to parse: {e}");
}
})
.filter_map(Result::ok)
.map(|semver| {
c.versions
.iter()
.map(|v| v.version.parse::<semver::Version>().unwrap())
.filter(|v| semver.matches(v))
.max()
.unwrap()
})
.max()
.unwrap();

Ok(())
println!(
"[INFO] The most recent version for {} compatible with Bevy {bevy_version} is {newest}",
c.data.name
);

Ok(format!("={newest}"))
}

#[cached(
result = true,
ty = "cached::SizedCache<String, CrateResponse>",
create = "{ cached::SizedCache::with_size(20) }",
convert = r#"{ name.to_string() }"#
convert = r#"{ name.to_owned() }"#
)]
fn fetch_crate(name: &str, agent: Agent) -> anyhow::Result<CrateResponse> {
agent
Expand All @@ -160,7 +157,7 @@ fn fetch_crate(name: &str, agent: Agent) -> anyhow::Result<CrateResponse> {
result = true,
ty = "cached::SizedCache<String, String>",
create = "{ cached::SizedCache::with_size(20) }",
convert = r#"{ c.data.name.to_string() }"#
convert = r#"{ c.data.name.clone() }"#
)]
fn fetch_readme(c: &CrateResponse, agent: Agent) -> anyhow::Result<String> {
let path = &c.versions[0].readme_path; // index 0 is latest
Expand Down
81 changes: 81 additions & 0 deletions tools/check_crate_updates/src/manifest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use std::{fmt::Display, str::FromStr};

pub struct Manifest(toml_edit::DocumentMut);

impl Manifest {
/// Gets the dependency from the manifest if it exists
pub fn get_dependency(&self, name: &str) -> Option<Dependency> {
self.0
.get("dependencies")
.and_then(|dep| dep.get(name))
.map(Dependency)
}

/// Gets the dependency mutably from the manifest if it exists
pub fn get_dependency_mut(&mut self, name: &str) -> Option<DependencyMut> {
self.0
.get_mut("dependencies")
.and_then(|dep| dep.get_mut(name))
.map(DependencyMut)
}

/// Gets the names of all the dependencies from the manifest
pub fn get_dependency_names(&self) -> Option<impl Iterator<Item = &'_ str>> {
self.0
.get("dependencies")
.and_then(|item| item.as_table())
.map(|table| table.iter().map(|(name, _)| name))
}
}

pub struct Dependency<'a>(&'a toml_edit::Item);
pub struct DependencyMut<'a>(&'a mut toml_edit::Item);

impl<'a> Dependency<'a> {
/// Gets the version of the dependency by searching first
/// the value of the entry and then as a inline table
pub fn get_version(&self) -> Option<&'a str> {
if let Some(version) = self.0.get("version").and_then(|i| i.as_str()) {
Some(version)
} else if let Some(table) = self.0.as_inline_table() {
table.get("version").and_then(|i| i.as_str())
} else {
None
}
}
}

impl<'a> DependencyMut<'a> {
/// Sets the version of the dependency by searching first
/// the value of the entry and then as a inline table.
/// Returns whether it was successful
pub fn set_version(&mut self, version: &str) -> bool {
if let Some(value) = self
.0
.as_inline_table_mut()
.and_then(|table| table.get_mut("version"))
{
*value = version.into();
true
} else if let Some(value) = self.0.as_value_mut() {
*value = version.into();
true
} else {
false
}
}
}

impl FromStr for Manifest {
type Err = toml_edit::TomlError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<toml_edit::DocumentMut>().map(Manifest)
}
}

impl Display for Manifest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0.to_string())
}
}

0 comments on commit 89ad172

Please sign in to comment.