use std::path::Path;
use cratefield_manifest::{
BuildEnvViolation, BuildEnvironmentAttestation, Catalog, HarnessSource, Provenance,
ResourceLimits, VentureManifest, builtin, generate,
};
pub fn run(
manifest_path: &Path,
out: &Path,
harness_path: Option<&str>,
catalog_path: Option<&Path>,
built_at: Option<&str>,
builder: Option<&str>,
) -> Result<(), String> {
let raw = std::fs::read_to_string(manifest_path)
.map_err(|err| format!("cannot read {}: {err}", manifest_path.display()))?;
let manifest = parse(manifest_path, &raw)?;
let catalog = load_catalog(catalog_path)?;
let module_set = manifest.resolve(&catalog).map_err(|err| err.to_string())?;
let attestation = sandbox_attestation_from_env();
if let Some(env) = &attestation {
let violations = env.check();
if !violations.is_empty() {
return Err(BuildEnvViolation(violations).to_string());
}
}
let source = match harness_path {
Some(path) => HarnessSource::Path(path.to_owned()),
None => HarnessSource::default(),
};
let venture = generate(&manifest, &module_set, &source).map_err(|err| err.to_string())?;
for file in &venture.files {
let dest = out.join(&file.path);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)
.map_err(|err| format!("cannot create {}: {err}", parent.display()))?;
}
std::fs::write(&dest, &file.contents)
.map_err(|err| format!("cannot write {}: {err}", dest.display()))?;
}
let builder_identity = builder
.map(str::to_owned)
.or_else(|| std::env::var("FZ_BUILDER").ok())
.unwrap_or_else(|| "local".to_owned());
let built_at_record = built_at
.map(str::to_owned)
.or_else(|| std::env::var("FZ_BUILD_TIMESTAMP").ok());
let provenance = Provenance::record(
&venture,
&module_set,
&builder_identity,
built_at_record.as_deref(),
attestation.clone(),
)
.map_err(|err| err.to_string())?;
std::fs::write(out.join("provenance.json"), provenance.to_json().as_bytes())
.map_err(|err| format!("cannot write provenance.json: {err}"))?;
println!("fz build: {}", venture.name);
println!(" module set: {}", venture.content_key);
println!(" modules: {}", venture.modules.join(" -> "));
println!(" composition: sha256:{}", venture.composition_hash);
println!(" pins:");
for release in module_set.releases() {
println!(
" {} @ {} {}",
release.slug, release.version, release.digest
);
}
match &provenance.attestation {
Some(env) => println!(
" provenance: provenance.json — attested build (environment digest {})",
env.attestation_digest()
),
None => println!(
" provenance: provenance.json — local build, unattested (no \
FZ_BUILD_SANDBOX_DIGEST). A sanctioned build must satisfy the \
BuildEnvironmentAttestation contract and pin stamped digests."
),
}
println!(
" wrote {} files to {}:",
venture.files.len(),
out.display()
);
for file in &venture.files {
println!(" {}", file.path);
}
println!(" provenance.json");
println!();
println!("next:");
println!(
" 1. cd {} && cargo run --bin fz -- migrations collect # collect D1 migrations",
out.display()
);
println!(" 2. worker-build --release # compile the venture to wasm (needs the toolchain)");
println!(
" 3. wrangler deploy # stand up the Worker + D1 (needs Cloudflare creds — needs-human)"
);
Ok(())
}
pub(crate) fn load_catalog(path: Option<&Path>) -> Result<Catalog, String> {
let Some(path) = path else {
return Ok(builtin());
};
let raw = std::fs::read_to_string(path)
.map_err(|err| format!("cannot read {}: {err}", path.display()))?;
let catalog: Catalog = serde_json::from_str(&raw)
.map_err(|err| format!("invalid catalog {}: {err}", path.display()))?;
catalog.validate().map_err(|err| err.to_string())?;
Ok(catalog)
}
fn sandbox_attestation_from_env() -> Option<BuildEnvironmentAttestation> {
let sandbox_digest = std::env::var("FZ_BUILD_SANDBOX_DIGEST").ok()?;
Some(BuildEnvironmentAttestation {
sandbox_digest,
disposable: env_is("FZ_BUILD_SANDBOX", "disposable"),
workspace_scoped: env_is("FZ_BUILD_ISOLATION", "workspace-scoped"),
network_egress: std::env::var("FZ_BUILD_NETWORK").unwrap_or_default(),
deploy_credentials_withheld: !deploy_credentials_present(),
production_key_absent: env_is("FZ_BUILD_SECRETS", "production-absent"),
limits: parse_limits(&std::env::var("FZ_BUILD_LIMITS").unwrap_or_default()),
})
}
fn env_is(key: &str, expected: &str) -> bool {
std::env::var(key).is_ok_and(|value| value == expected)
}
fn deploy_credentials_present() -> bool {
std::env::var_os("CLOUDFLARE_API_TOKEN").is_some()
|| std::env::var_os("CF_API_TOKEN").is_some()
|| std::env::var_os("WRANGLER_API_TOKEN").is_some()
|| std::env::var("FZ_BUILD_HAS_DEPLOY_CREDENTIALS").is_ok_and(|v| v == "1")
}
fn parse_limits(raw: &str) -> ResourceLimits {
let mut parts = raw.split(',').map(str::trim);
ResourceLimits {
wall_clock: parts.next().unwrap_or("").to_owned(),
memory: parts.next().unwrap_or("").to_owned(),
disk: parts.next().unwrap_or("").to_owned(),
}
}
pub(crate) fn parse(path: &Path, raw: &str) -> Result<VentureManifest, String> {
match path.extension().and_then(|ext| ext.to_str()) {
Some("toml") => toml::from_str(raw).map_err(|err| format!("invalid TOML manifest: {err}")),
_ => VentureManifest::from_json_str(raw).map_err(|err| err.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn limits_are_positional_with_room_for_gaps() {
let limits = parse_limits("600s, 4Gi ,10Gi");
assert_eq!(limits.wall_clock, "600s");
assert_eq!(limits.memory, "4Gi");
assert_eq!(limits.disk, "10Gi");
let sparse = parse_limits("");
assert!(sparse.wall_clock.is_empty());
let partial = parse_limits("600s");
assert_eq!(partial.wall_clock, "600s");
assert!(partial.memory.is_empty());
}
#[test]
fn sparse_limits_fail_the_contract() {
let env = BuildEnvironmentAttestation {
sandbox_digest: format!("sha256:{}", "b".repeat(64)),
disposable: true,
workspace_scoped: true,
network_egress: "deny-all".to_owned(),
deploy_credentials_withheld: true,
production_key_absent: true,
limits: parse_limits("600s"),
};
let violations = env.check();
assert_eq!(violations.len(), 2, "{violations:?}");
assert!(violations.contains(&"no memory limit declared"));
assert!(violations.contains(&"no disk limit declared"));
}
}