use gen_gomod::adopt::{AdoptionRefusal, Census, FsAdoptEnv, census};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
pub const DEFAULT_FLEET_GO: &str = "1.26.5";
const IGNORE_MARKER: &str = ".gen-adopt-ignore";
fn discover(root: &Path, out: &mut Vec<String>) {
if root.join(IGNORE_MARKER).is_file() {
return;
}
let Ok(entries) = std::fs::read_dir(root) else {
return;
};
if root.join("go.mod").is_file() {
out.push(root.display().to_string());
}
for e in entries.flatten() {
let p = e.path();
if !p.is_dir() {
continue;
}
let name = e.file_name();
let name = name.to_string_lossy();
if name.starts_with('.') || name == "vendor" || name == "node_modules" {
continue;
}
discover(&p, out);
}
}
pub struct AdoptGoArgs {
pub roots: Vec<PathBuf>,
pub fleet_go: String,
pub json: bool,
}
pub fn run(args: &AdoptGoArgs) -> Result<i32, String> {
let mut roots: Vec<String> = Vec::new();
for r in &args.roots {
discover(r, &mut roots);
}
roots.sort();
roots.dedup();
let c = census(&FsAdoptEnv, &roots, &args.fleet_go);
if c.scanned == 0 {
return Err(
"adopt-go: scanned 0 module roots. Nothing was measured, which is not the \
same as nothing being wrong — check the --root paths."
.into(),
);
}
if args.json {
print_json(&c, &args.fleet_go);
} else {
print_human(&c, &args.fleet_go);
}
Ok(i32::from(!c.refused.is_empty()))
}
fn by_reason(c: &Census) -> BTreeMap<&'static str, Vec<&str>> {
let mut m: BTreeMap<&'static str, Vec<&str>> = BTreeMap::new();
for (root, reason) in &c.refused {
m.entry(reason.as_str()).or_default().push(root.as_str());
}
m
}
fn print_human(c: &Census, fleet_go: &str) {
println!("gen adopt-go --dry-run (fleet Go {fleet_go})");
println!(" scanned {}", c.scanned);
println!(" eligible {}", c.eligible);
println!(" refused {}", c.refused.len());
for (reason, roots) in by_reason(c) {
println!("\n {reason} ({})", roots.len());
for r in roots {
println!(" {r}");
}
if reason == AdoptionRefusal::BareMinorDirective.as_str() {
println!(
" ^ remediation is one line per module: `go 1.N` -> `go 1.N.0`.\n\
\x20 Builds under -mod=mod (go rewrites go.mod silently); FAILS under\n\
\x20 -mod=readonly / -mod=vendor, which is every hermetic Nix build.\n\
\x20 substrate's build-time trace escalates to a throw when this reaches 0."
);
}
}
println!(
"\n bare-minor-directive == {} <- the escalation predicate",
c.bare_minor_directive()
);
}
fn print_json(c: &Census, fleet_go: &str) {
let refused: Vec<serde_json::Value> = c
.refused
.iter()
.map(|(root, reason)| serde_json::json!({ "root": root, "reason": reason.as_str() }))
.collect();
let v = serde_json::json!({
"fleet_go": fleet_go,
"scanned": c.scanned,
"eligible": c.eligible,
"refused": refused,
"bare_minor_directive": c.bare_minor_directive(),
});
println!("{}", serde_json::to_string_pretty(&v).unwrap_or_default());
}