use crate::flows::FlowAsset;
use crate::templates::TemplateAsset;
use anyhow::{Context, Result, anyhow};
use pack_component_template::{CARGO_TOML, DATA_RS_PLACEHOLDER, LIB_RS};
use std::fmt::Write;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tracing::info;
pub fn generate_component_data(
manifest_bytes: &[u8],
flows: &[FlowAsset],
templates: &[TemplateAsset],
) -> Result<String> {
let mut buffer = String::new();
writeln!(
&mut buffer,
"// @generated by packc -- DO NOT EDIT BY HAND."
)?;
writeln!(&mut buffer, "#![allow(dead_code)]")?;
writeln!(&mut buffer, "#![allow(clippy::all)]\n")?;
writeln!(&mut buffer, "pub static MANIFEST_CBOR: &[u8] = &[")?;
buffer.push_str(&indent_byte_literal(manifest_bytes, 4));
writeln!(&mut buffer, "\n];\n")?;
writeln!(
&mut buffer,
"pub static FLOWS: &[(&'static str, &'static str)] = &["
)?;
for flow in flows {
let raw_literal = rust_string_literal(&flow.raw);
writeln!(
&mut buffer,
" (\"{}\", {}),",
flow.bundle.id, raw_literal
)?;
}
writeln!(&mut buffer, "];\n")?;
writeln!(
&mut buffer,
"pub static TEMPLATES: &[(&'static str, &'static [u8])] = &["
)?;
for template in templates {
writeln!(&mut buffer, " (\"{}\", &[", template.logical_path)?;
buffer.push_str(&indent_byte_literal(&template.bytes, 8));
writeln!(&mut buffer, "\n ]),")?;
}
writeln!(&mut buffer, "];\n")?;
Ok(buffer)
}
pub fn compile_component(component_data: &Path, output_wasm: &Path) -> Result<()> {
let crate_root = prepare_component_crate(component_data)?;
info!(
component_data = %component_data.display(),
crate_root = %crate_root.display(),
output = %output_wasm.display(),
"compiling pack_component"
);
ensure_target_installed("wasm32-wasip2")?;
let metadata_status = Command::new("cargo")
.args(["metadata", "--format-version", "1"])
.current_dir(&crate_root)
.status()
.with_context(|| "failed to invoke `cargo metadata` for pack_component")?;
if !metadata_status.success() {
anyhow::bail!(
"unable to run cargo metadata for pack_component; reinstall packc or ensure the Rust toolchain is installed (crate path: {})",
crate_root.display()
);
}
let build_status = Command::new("cargo")
.args(["build", "--target", "wasm32-wasip2", "--release"])
.current_dir(&crate_root)
.status()
.with_context(|| "failed to invoke cargo build for pack_component")?;
if !build_status.success() {
anyhow::bail!("cargo build failed with status {}", build_status);
}
let artifact = crate_root
.join("target")
.join("wasm32-wasip2")
.join("release")
.join("pack_component.wasm");
if !artifact.exists() {
anyhow::bail!(
"expected wasm artifact at {} but it was not produced",
artifact.display()
);
}
if let Some(parent) = output_wasm.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create directory {}", parent.display()))?;
}
fs::copy(&artifact, output_wasm).with_context(|| {
format!(
"failed to copy wasm artifact from {} to {}",
artifact.display(),
output_wasm.display()
)
})?;
info!(artifact = %artifact.display(), output = %output_wasm.display(), "component artifact ready");
Ok(())
}
fn ensure_target_installed(target: &str) -> Result<()> {
let output = Command::new("rustup")
.args(["target", "list", "--installed"])
.output();
let Ok(output) = output else {
info!("skipping rustup target check; rustup not available");
return Ok(());
};
if !output.status.success() {
info!("rustup target list --installed failed; assuming target is configured");
return Ok(());
}
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.lines().any(|line| line.trim() == target) {
Ok(())
} else {
anyhow::bail!(
"Rust target `{}` is not installed. Install it via `rustup target add {}` before running packc.",
target,
target
);
}
}
fn indent_byte_literal(bytes: &[u8], indent: usize) -> String {
if bytes.is_empty() {
return String::new();
}
let mut lines = Vec::new();
for chunk in bytes.chunks(12) {
let mut line = String::new();
for (idx, byte) in chunk.iter().enumerate() {
if idx > 0 {
line.push_str(", ");
}
write!(&mut line, "0x{:02x}", byte).unwrap();
}
lines.push(format!(
"{indentation}{line}",
indentation = " ".repeat(indent),
line = line
));
}
lines.join(",\n")
}
fn rust_string_literal(value: &str) -> String {
let mut literal = String::from("\"");
for ch in value.chars() {
for escaped in ch.escape_default() {
literal.push(escaped);
}
}
literal.push('"');
literal
}
fn prepare_component_crate(component_data: &Path) -> Result<PathBuf> {
let src_dir = component_data
.parent()
.ok_or_else(|| anyhow!("component data path lacks parent"))?;
fs::create_dir_all(src_dir)
.with_context(|| format!("failed to ensure src directory {}", src_dir.display()))?;
let crate_root = src_dir
.parent()
.ok_or_else(|| anyhow!("component data path lacks crate root"))?;
write_template_file(crate_root.join("Cargo.toml"), CARGO_TOML)?;
write_template_file(src_dir.join("lib.rs"), LIB_RS)?;
if !component_data.exists() {
write_template_file(component_data.to_path_buf(), DATA_RS_PLACEHOLDER)?;
}
Ok(crate_root.to_path_buf())
}
fn write_template_file(path: PathBuf, contents: &str) -> Result<()> {
if path.exists() {
let existing = fs::read_to_string(&path)
.with_context(|| format!("failed to read {}", path.display()))?;
if existing == contents {
return Ok(());
}
} else if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create directory {}", parent.display()))?;
}
fs::write(&path, contents).with_context(|| format!("failed to write {}", path.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{flows, manifest, templates};
fn demo_pack_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("examples/weather-demo")
}
#[test]
fn component_data_contains_mcp_exec_flow_source() {
let pack_dir = demo_pack_dir();
let spec = manifest::load_spec(&pack_dir).expect("spec");
let flow_assets = flows::load_flows(&pack_dir, &spec.spec).expect("flows");
let template_assets =
templates::collect_templates(&pack_dir, &spec.spec).expect("templates");
let manifest_model = manifest::build_manifest(&spec, &flow_assets, &template_assets);
let manifest_bytes = manifest::encode_manifest(&manifest_model).expect("manifest encoding");
let generated =
generate_component_data(&manifest_bytes, &flow_assets, &template_assets).unwrap();
assert!(
generated.contains("pub static MANIFEST_CBOR"),
"generated source should expose MANIFEST_CBOR constant"
);
assert!(
generated.contains("mcp.exec"),
"generated flow bundle should retain mcp.exec reference"
);
assert!(
generated.contains("templates/weather_now.hbs"),
"template logical path should be present"
);
}
#[test]
fn indent_byte_literal_outputs_comma_separated_rows() {
let bytes = [0x01u8, 0x02, 0x03, 0x04];
let rendered = indent_byte_literal(&bytes, 4);
assert_eq!(
rendered, " 0x01, 0x02, 0x03, 0x04",
"literal should match expected formatting"
);
}
}