use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cargo_manifest = Path::new(
&std::env::var_os("CARGO_MANIFEST_DIR")
.expect("cargo always sets CARGO_MANIFEST_DIR for build.rs"),
)
.join("Cargo.toml");
println!("cargo:rerun-if-changed={}", cargo_manifest.display());
verify_wasm_feature_isolation(&cargo_manifest)?;
println!("cargo:rustc-env=KERNAL_API_WASM_FEATURE_ISOLATION=verified");
let manifest_path = Path::new(
&std::env::var_os("CARGO_MANIFEST_DIR")
.expect("cargo always sets CARGO_MANIFEST_DIR for build.rs"),
)
.join("conpty-sidecar.sha256.toml");
println!("cargo:rerun-if-changed={}", manifest_path.display());
let rendered = render_hash_table(&read_manifest(&manifest_path));
let out_dir = std::env::var_os("OUT_DIR").expect("cargo always sets OUT_DIR for build.rs");
std::fs::write(
Path::new(&out_dir).join("conpty_sidecar_hashes.rs"),
rendered,
)?;
Ok(())
}
fn verify_wasm_feature_isolation(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let raw = std::fs::read_to_string(path)?;
let manifest = raw.parse::<toml::Value>()?;
let features = manifest
.get("features")
.and_then(toml::Value::as_table)
.ok_or("Cargo.toml is missing [features]")?;
let sketch_host = features
.get("wasm-sketch-host")
.and_then(toml::Value::as_array)
.ok_or("wasm-sketch-host must be an array feature")?;
let has_required_dependency =
|name| sketch_host.iter().any(|value| value.as_str() == Some(name));
if !has_required_dependency("dep:wasmtime") || !has_required_dependency("dep:wasmparser") {
return Err("wasm-sketch-host must opt into wasmtime and wasmparser".into());
}
if features
.get("full")
.and_then(toml::Value::as_array)
.is_some_and(|full| {
full.iter()
.any(|value| value.as_str() == Some("wasm-sketch-host"))
})
{
return Err("full must not opt into wasm-sketch-host".into());
}
let dependencies = manifest
.get("dependencies")
.and_then(toml::Value::as_table)
.ok_or("Cargo.toml is missing [dependencies]")?;
if dependencies.contains_key("wasmtime-wasi") {
return Err("ambient wasmtime-wasi must not be a direct dependency".into());
}
let wasmtime = dependencies
.get("wasmtime")
.and_then(toml::Value::as_table)
.ok_or("wasmtime must be an optional table dependency")?;
if wasmtime.get("version").and_then(toml::Value::as_str) != Some("=45.0.0")
|| wasmtime
.get("default-features")
.and_then(toml::Value::as_bool)
!= Some(false)
|| wasmtime.get("optional").and_then(toml::Value::as_bool) != Some(true)
{
return Err("wasmtime must remain exact-pinned, optional, and defaults-disabled".into());
}
Ok(())
}
#[derive(Default)]
struct ParsedManifest {
x64: Option<(String, u64)>,
arm64: Option<(String, u64)>,
x86: Option<(String, u64)>,
arm: Option<(String, u64)>,
}
fn read_manifest(path: &Path) -> ParsedManifest {
let Ok(raw) = std::fs::read_to_string(path) else {
return ParsedManifest::default();
};
let Ok(table) = raw.parse::<toml::Value>() else {
return ParsedManifest::default();
};
let mut parsed = ParsedManifest::default();
let Some(assets) = table.get("asset").and_then(toml::Value::as_table) else {
return parsed;
};
for arch in ["x64", "arm64", "x86", "arm"] {
let Some(entry) = assets.get(arch).and_then(toml::Value::as_table) else {
continue;
};
let Some(sha) = entry.get("sha256").and_then(toml::Value::as_str) else {
continue;
};
let size = entry
.get("size_bytes")
.and_then(toml::Value::as_integer)
.unwrap_or_default()
.max(0) as u64;
let slot = match arch {
"x64" => &mut parsed.x64,
"arm64" => &mut parsed.arm64,
"x86" => &mut parsed.x86,
"arm" => &mut parsed.arm,
_ => unreachable!(),
};
*slot = Some((sha.to_owned(), size));
}
parsed
}
fn render_hash_table(parsed: &ParsedManifest) -> String {
let mut output = String::from(
"pub(super) struct ExpectedAsset {\n pub sha256_hex: &'static str,\n pub size_bytes: u64,\n}\n\n",
);
for (name, value) in [
("EXPECTED_X64", &parsed.x64),
("EXPECTED_ARM64", &parsed.arm64),
("EXPECTED_X86", &parsed.x86),
("EXPECTED_ARM", &parsed.arm),
] {
let body = value.as_ref().map_or_else(
|| "None".to_owned(),
|(sha, size)| {
format!("Some(ExpectedAsset {{ sha256_hex: \"{sha}\", size_bytes: {size} }})")
},
);
output.push_str(&format!(
"#[allow(dead_code)]\npub(super) const {name}: Option<ExpectedAsset> = {body};\n"
));
}
output
}