Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tidy up crates checker tool #78

Merged
merged 6 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())
}
}
Loading