mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-03 13:39:38 +00:00
The IDL `types` array was emitted in HashMap iteration order by spel-framework-core, which is non-deterministic across processes. Two independent regenerations of the same source could therefore disagree on type ordering, producing different bytes. This is what makes the check-idl CI job flaky: a PR's committed IDL is generated locally with one ordering, but CI regenerates with a different ordering and the diff fails — including PRs that were green when posted then breaking main after merge. Sort the top-level `types` array by name before serializing so output is byte-stable regardless of where idl-gen runs. Enable serde_json's `preserve_order` feature so the Value round-trip preserves struct-field key order (otherwise all keys would alphabetize and churn every artifact). Only top-level `types` was unstable; variants and fields already follow source order. Committed artifacts are unchanged — they happened to already be in sorted order.
97 lines
3.2 KiB
Rust
97 lines
3.2 KiB
Rust
use std::{
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
process,
|
|
};
|
|
|
|
fn main() {
|
|
let path: PathBuf = match std::env::args().nth(1) {
|
|
Some(p) => PathBuf::from(p),
|
|
None => {
|
|
eprintln!("Usage: idl-gen <source-file>");
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
let dep_dirs = find_path_dep_dirs(&path);
|
|
|
|
match spel_framework_core::idl_gen::generate_idl_from_file_with_deps(&path, &dep_dirs) {
|
|
Ok(idl) => {
|
|
// spel-framework emits the top-level `types` array in HashMap
|
|
// iteration order, which is non-deterministic across processes.
|
|
// Sort it by name so regenerated IDL is byte-stable regardless of
|
|
// where it runs (local `make idl` vs CI vs another contributor).
|
|
let mut value = match serde_json::to_value(&idl) {
|
|
Ok(value) => value,
|
|
Err(e) => {
|
|
eprintln!("Error converting IDL to JSON value: {e}");
|
|
process::exit(1);
|
|
}
|
|
};
|
|
if let Some(types) = value.get_mut("types").and_then(|t| t.as_array_mut()) {
|
|
types.sort_by(|a, b| {
|
|
let name = |v: &serde_json::Value| {
|
|
v.get("name")
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("")
|
|
.to_owned()
|
|
};
|
|
name(a).cmp(&name(b))
|
|
});
|
|
}
|
|
match serde_json::to_string_pretty(&value) {
|
|
Ok(json) => println!("{json}"),
|
|
Err(e) => {
|
|
eprintln!("Error serializing IDL to JSON: {e}");
|
|
process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error: {e}");
|
|
process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Return the crate-root directories of all `path = "..."` entries in the
|
|
/// `[dependencies]` table of the `Cargo.toml` nearest to `source_path`.
|
|
fn find_path_dep_dirs(source_path: &Path) -> Vec<PathBuf> {
|
|
(|| -> Option<Vec<PathBuf>> {
|
|
let manifest = find_crate_manifest(source_path)?;
|
|
let content = fs::read_to_string(&manifest).ok()?;
|
|
let value: toml::Value = toml::from_str(&content).ok()?;
|
|
let manifest_dir = manifest.parent()?;
|
|
|
|
let mut dirs = Vec::new();
|
|
if let Some(table) = value.get("dependencies").and_then(|v| v.as_table()) {
|
|
for (_name, dep) in table {
|
|
if let Some(rel) = dep.get("path").and_then(|v| v.as_str()) {
|
|
let dep_dir = manifest_dir.join(rel);
|
|
if dep_dir.is_dir() {
|
|
dirs.push(dep_dir);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Some(dirs)
|
|
})()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Walk up from `start` to find the nearest `Cargo.toml`.
|
|
fn find_crate_manifest(start: &Path) -> Option<PathBuf> {
|
|
let mut dir: &Path = if start.is_file() {
|
|
start.parent()?
|
|
} else {
|
|
start
|
|
};
|
|
loop {
|
|
let candidate = dir.join("Cargo.toml");
|
|
if candidate.exists() {
|
|
return Some(candidate);
|
|
}
|
|
dir = dir.parent()?;
|
|
}
|
|
}
|