use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result, bail, ensure};
use greentic_deployer::contract::{
DeployerContractV1, get_deployer_contract_v1, resolve_deployer_contract_assets,
};
use greentic_deployer::pack_introspect::read_manifest_from_gtpack;
fn main() -> Result<()> {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixtures_root = root.join("fixtures/packs");
let scaffold_root = root.join("target/replayed-pack-scaffolds");
let output_root = root.join("dist");
fs::create_dir_all(&output_root).context("create output directory")?;
let mut fixture_dirs = fs::read_dir(&fixtures_root)
.with_context(|| format!("read fixture root {}", fixtures_root.display()))?
.flatten()
.map(|entry| entry.path())
.filter(|path| path.is_dir())
.collect::<Vec<_>>();
fixture_dirs.sort();
if fixture_dirs.is_empty() {
bail!("no fixture packs found under {}", fixtures_root.display());
}
ensure_replayed_scaffolds(&root, &scaffold_root, &fixture_dirs)?;
for fixture_dir in fixture_dirs {
let fixture_name = fixture_dir
.file_name()
.and_then(|name| name.to_str())
.context("fixture name missing")?;
let pack_root = scaffold_root.join(fixture_name);
let output_path = output_root.join(format!("{fixture_name}.gtpack"));
build_fixture_gtpack(&pack_root, &output_path)?;
validate_fixture_gtpack(&fixture_dir, &output_path)?;
let manifest = read_manifest_from_gtpack(&output_path)
.with_context(|| format!("read manifest from {}", output_path.display()))?;
println!("built and validated {}", output_path.display());
let relative_output_path = output_path.strip_prefix(&root).with_context(|| {
format!("compute relative output path for {}", output_path.display())
})?;
println!(
"PACK\t{}\t{}\t{}",
manifest.pack_id,
manifest.version,
relative_output_path.display()
);
}
Ok(())
}
fn ensure_replayed_scaffolds(
root: &Path,
scaffold_root: &Path,
fixture_dirs: &[PathBuf],
) -> Result<()> {
let missing_before = missing_replayed_scaffolds(scaffold_root, fixture_dirs)?;
if missing_before.is_empty() {
return Ok(());
}
eprintln!(
"replaying deployer scaffolds before fixture gtpack build; missing: {}",
missing_before.join(", ")
);
run_command(
"cargo",
&[
"run",
"--features",
"internal-tools",
"--bin",
"replay_deployer_scaffolds",
],
Some(root),
)
.context("replay deployer scaffolds before building fixture gtpacks")?;
let missing_after = missing_replayed_scaffolds(scaffold_root, fixture_dirs)?;
ensure!(
missing_after.is_empty(),
"replay_deployer_scaffolds completed but did not create pack.yaml for: {}",
missing_after.join(", ")
);
Ok(())
}
fn missing_replayed_scaffolds(
scaffold_root: &Path,
fixture_dirs: &[PathBuf],
) -> Result<Vec<String>> {
let mut missing = Vec::new();
for fixture_dir in fixture_dirs {
let fixture_name = fixture_dir
.file_name()
.and_then(|name| name.to_str())
.with_context(|| format!("fixture name missing for {}", fixture_dir.display()))?;
if !scaffold_root.join(fixture_name).join("pack.yaml").is_file() {
missing.push(fixture_name.to_string());
}
}
Ok(missing)
}
fn build_fixture_gtpack(pack_root: &Path, output_path: &Path) -> Result<()> {
ensure!(
pack_root.join("pack.yaml").is_file(),
"missing replayed scaffold at {}; run `cargo run --features internal-tools --bin replay_deployer_scaffolds` first",
pack_root.display()
);
run_command(
"greentic-pack",
&["build", "--in", pack_root.to_str().unwrap()],
None,
)?;
let fixture_name = pack_root
.file_name()
.and_then(|name| name.to_str())
.context("pack root name missing")?;
let built_path = pack_root
.join("dist")
.join(format!("{fixture_name}.gtpack"));
ensure!(
built_path.is_file(),
"greentic-pack did not produce {}",
built_path.display()
);
fs::copy(&built_path, output_path).with_context(|| {
format!(
"copy built gtpack {} -> {}",
built_path.display(),
output_path.display()
)
})?;
Ok(())
}
fn validate_fixture_gtpack(fixture_dir: &Path, gtpack_path: &Path) -> Result<()> {
let manifest = read_manifest_from_gtpack(gtpack_path)
.with_context(|| format!("read manifest from {}", gtpack_path.display()))?;
let contract = get_deployer_contract_v1(&manifest)
.context("decode embedded deployer contract")?
.context("missing embedded deployer contract")?;
let resolved = resolve_deployer_contract_assets(&manifest, gtpack_path)
.with_context(|| format!("resolve contract assets from {}", gtpack_path.display()))?;
let expected = load_contract(fixture_dir)?;
ensure!(
contract == expected,
"embedded contract mismatch for {}",
fixture_dir.display()
);
ensure!(
resolved
.as_ref()
.context("missing resolved deployer contract")?
.capabilities
.len()
== expected.capabilities.len(),
"resolved capability count mismatch for {}",
fixture_dir.display()
);
ensure!(
gtpack_path.is_file(),
"archive missing after build: {}",
gtpack_path.display()
);
Ok(())
}
fn load_contract(fixture_dir: &Path) -> Result<DeployerContractV1> {
let path = fixture_dir.join("contract.greentic.deployer.v1.json");
let bytes = fs::read(&path).with_context(|| format!("read {}", path.display()))?;
serde_json::from_slice(&bytes).with_context(|| format!("parse {}", path.display()))
}
fn run_command(program: &str, args: &[&str], current_dir: Option<&Path>) -> Result<()> {
let mut command = Command::new(program);
command.args(args);
if let Some(current_dir) = current_dir {
command.current_dir(current_dir);
}
let output = command
.output()
.with_context(|| format!("run {} {}", program, args.join(" ")))?;
if output.status.success() {
return Ok(());
}
bail!(
"{} {} failed:\n{}",
program,
args.join(" "),
String::from_utf8_lossy(&output.stderr)
);
}