use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{Args, Subcommand};
use watoots::Host;
use watoots::imports::ImportDecision;
use crate::{Failure, Outcome, read, read_manifest, verdict};
#[derive(Args)]
pub(crate) struct InspectArgs {
component: PathBuf,
#[arg(short, long)]
manifest: Option<PathBuf>,
#[arg(long = "provide", value_name = "INTERFACE")]
provide: Vec<String>,
#[arg(long)]
imports: bool,
#[arg(long, value_name = "WIT")]
targets: Option<PathBuf>,
#[arg(long, value_name = "NAME", requires = "targets")]
world: Option<String>,
}
#[derive(Args)]
pub(crate) struct DiffArgs {
previous: PathBuf,
current: PathBuf,
#[arg(short, long)]
manifest: Option<PathBuf>,
#[arg(long = "provide", value_name = "INTERFACE")]
provide: Vec<String>,
}
#[derive(Subcommand)]
pub(crate) enum WitCommand {
SemverCheck(SemverCheckArgs),
}
#[derive(Args)]
pub(crate) struct SemverCheckArgs {
#[arg(long, value_name = "WIT")]
previous: PathBuf,
#[arg(long, value_name = "WIT")]
current: PathBuf,
#[arg(long, value_name = "NAME")]
world: Option<String>,
}
fn inspection_host(manifest: Option<&Path>, provide: &[String]) -> Result<Host, Failure> {
let (manifest, _) = read_manifest(manifest)?;
let mut builder = Host::builder().manifest(manifest);
for interface in provide {
builder = builder.provide_interface(interface.clone());
}
Ok(builder.build()?)
}
pub(crate) fn inspect(args: &InspectArgs) -> Outcome {
let wasm = read(&args.component)?;
let host = inspection_host(args.manifest.as_deref(), &args.provide)?;
let report = host.inspect(&wasm)?;
if args.imports {
print!("{}", report.describe());
} else {
print!("{}", report.summarize(host.manifest()));
}
let mut ok = report.is_satisfied();
if ok {
println!("\nevery import is granted");
} else {
println!(
"\n{} import(s) are not granted; `--imports` lists them individually",
report.denied().count()
);
}
if let Some(wit) = &args.targets {
match host.check_targets(&wasm, wit, args.world.as_deref()) {
Ok(()) => println!("exports match the world in {}", wit.display()),
Err(err) => {
println!("{}", err.message());
ok = false;
}
}
}
verdict(ok)
}
enum Gained<'a> {
AlreadyGranted,
AppServes,
NotGranted { key: &'a str },
}
impl<'a> Gained<'a> {
fn of(decision: &'a ImportDecision) -> Self {
if decision.granted {
Self::AlreadyGranted
} else if decision.served_by_application() {
Self::AppServes
} else {
Self::NotGranted {
key: decision.requirement.grant_key(),
}
}
}
}
pub(crate) fn diff(args: &DiffArgs) -> Outcome {
let previous = read(&args.previous)?;
let current = read(&args.current)?;
let host = inspection_host(args.manifest.as_deref(), &args.provide)?;
let describe = |wasm: &[u8]| -> Result<(Vec<ImportDecision>, Vec<String>), Failure> {
Ok((host.inspect(wasm)?.decisions, host.export_functions(wasm)?))
};
let (previous_imports, previous_exports) = describe(&previous)?;
let (current_imports, current_exports) = describe(¤t)?;
let names = |decisions: &[ImportDecision]| -> BTreeSet<String> {
decisions.iter().map(|d| d.import.clone()).collect()
};
let (before, after) = (names(&previous_imports), names(¤t_imports));
let gained: Vec<(&ImportDecision, Gained<'_>)> = current_imports
.iter()
.filter(|decision| !before.contains(&decision.import))
.map(|decision| (decision, Gained::of(decision)))
.collect();
let lost: Vec<&String> = before.difference(&after).collect();
println!("imports");
if gained.is_empty() && lost.is_empty() {
println!(" (unchanged)");
}
for (decision, change) in &gained {
let name = &decision.import;
match change {
Gained::AlreadyGranted => println!(" + {name:<48} already granted"),
Gained::AppServes => println!(" + {name:<48} your application must serve this"),
Gained::NotGranted { key } => println!(" + {name:<48} NOT GRANTED -> {key}"),
}
}
for name in &lost {
println!(" - {name:<48} no longer needed");
}
let before_exports: BTreeSet<&String> = previous_exports.iter().collect();
let after_exports: BTreeSet<&String> = current_exports.iter().collect();
let added: Vec<&&String> = after_exports.difference(&before_exports).collect();
let removed: Vec<&&String> = before_exports.difference(&after_exports).collect();
println!("\nexports");
if added.is_empty() && removed.is_empty() {
println!(" (unchanged)");
}
for name in &added {
println!(" + {name}");
}
for name in &removed {
println!(" - {name:<48} callers of this break");
}
let count = |wanted: fn(&Gained<'_>) -> bool| gained.iter().filter(|(_, g)| wanted(g)).count();
let to_serve = count(|g| matches!(g, Gained::AppServes));
let ungranted = count(|g| matches!(g, Gained::NotGranted { .. }));
println!();
let mut ok = true;
if to_serve > 0 {
println!("{to_serve} new interface(s) your application must serve");
ok = false;
}
if args.manifest.is_some() {
if ungranted == 0 {
println!("this update needs no new grants");
} else {
println!(
"{ungranted} new import(s) the manifest does not grant; reload would refuse this build"
);
ok = false;
}
} else if gained.is_empty() {
println!("no new imports; pass -m to check the update against a manifest");
} else {
println!(
"{} new import(s); pass -m to find out whether a manifest grants them",
gained.len()
);
}
if !removed.is_empty() {
println!(
"{} export(s) removed; a host calling them breaks",
removed.len()
);
ok = false;
}
verdict(ok)
}
pub(crate) fn semver_check(args: &SemverCheckArgs) -> Outcome {
let mut resolve = wit_parser::Resolve::default();
let mut load = |path: &Path| -> Result<_, Failure> {
resolve
.push_path(path)
.map(|(package, _)| package)
.map_err(|err| format!("{}: {err:#}", path.display()).into())
};
let previous = load(&args.previous)?;
let current = load(&args.current)?;
let select = |package, label: &str| -> Result<_, Failure> {
resolve
.select_world(&[package], args.world.as_deref())
.map_err(|err| format!("{label}: {err:#}").into())
};
let previous_world = select(previous, "--previous")?;
let current_world = select(current, "--current")?;
match wit_component::semver_check(resolve, previous_world, current_world) {
Ok(()) => {
println!(
"{} is a compatible successor to {}",
args.current.display(),
args.previous.display()
);
Ok(ExitCode::SUCCESS)
}
Err(err) => {
println!("incompatible: {err:#}");
Ok(ExitCode::FAILURE)
}
}
}