#![forbid(unsafe_code)]
use std::path::{Path, PathBuf};
use std::process::Command;
use forge_host::{Engine, GenerationOutput, Limits, Plugin, StageError, TransformOutput};
use forge_ir::Ir;
#[derive(Debug, thiserror::Error)]
pub enum HarnessError {
#[error("failed to build plugin: {0}")]
Build(String),
#[error("plugin io: {0}")]
Io(#[from] std::io::Error),
#[error("could not locate built .wasm under {0:?}")]
NotFound(PathBuf),
#[error("engine init: {0}")]
Engine(String),
#[error("plugin load: {0}")]
Load(String),
}
#[derive(Debug)]
pub struct PluginRunner {
engine: Engine,
plugin: Plugin,
}
impl PluginRunner {
pub fn build_and_load(manifest_dir: impl AsRef<Path>) -> Result<Self, HarnessError> {
let manifest_dir = manifest_dir.as_ref();
build(manifest_dir)?;
let wasm = locate_artifact(manifest_dir)?;
Self::load(wasm)
}
pub fn load(wasm_path: impl AsRef<Path>) -> Result<Self, HarnessError> {
let bytes = std::fs::read(wasm_path.as_ref())?;
let engine = Engine::new().map_err(|e| HarnessError::Engine(e.to_string()))?;
match Plugin::load_transformer(&engine, &bytes) {
Ok(p) => Ok(Self { engine, plugin: p }),
Err(_) => {
let p = Plugin::load_generator(&engine, &bytes)
.map_err(|e| HarnessError::Load(e.to_string()))?;
Ok(Self { engine, plugin: p })
}
}
}
pub fn info(&self) -> &forge_ir::PluginInfo {
self.plugin.info()
}
pub fn engine(&self) -> &Engine {
&self.engine
}
pub fn plugin(&self) -> &Plugin {
&self.plugin
}
pub fn transform(
&self,
ir: Ir,
config: serde_json::Value,
) -> Result<TransformOutput, StageError> {
let s = config.to_string();
self.plugin.transform(ir, &s, Limits::transformer())
}
pub fn generate(
&self,
ir: Ir,
config: serde_json::Value,
) -> Result<GenerationOutput, StageError> {
let s = config.to_string();
self.plugin.generate(ir, &s, Limits::generator())
}
}
fn build(manifest_dir: &Path) -> Result<(), HarnessError> {
let manifest = manifest_dir.join("Cargo.toml");
if !manifest.exists() {
return Err(HarnessError::Build(format!(
"no Cargo.toml at {}",
manifest.display()
)));
}
let status = Command::new(env!("CARGO"))
.args([
"build",
"--release",
"--target",
"wasm32-wasip2",
"--manifest-path",
])
.arg(&manifest)
.status()
.map_err(|e| HarnessError::Build(format!("spawn cargo: {e}")))?;
if !status.success() {
return Err(HarnessError::Build(format!(
"cargo build exited with {status}"
)));
}
Ok(())
}
fn locate_artifact(manifest_dir: &Path) -> Result<PathBuf, HarnessError> {
let crate_name = read_crate_name(manifest_dir)?;
let underscore = crate_name.replace('-', "_");
let mut search = manifest_dir.to_path_buf();
loop {
let candidate = search
.join("target")
.join("wasm32-wasip2")
.join("release")
.join(format!("{underscore}.wasm"));
if candidate.exists() {
return Ok(candidate);
}
let Some(parent) = search.parent() else {
return Err(HarnessError::NotFound(
manifest_dir
.join("target/wasm32-wasip2/release")
.join(format!("{underscore}.wasm")),
));
};
search = parent.to_path_buf();
}
}
fn read_crate_name(manifest_dir: &Path) -> Result<String, HarnessError> {
let manifest = std::fs::read_to_string(manifest_dir.join("Cargo.toml"))?;
let mut in_package = false;
for raw in manifest.lines() {
let line = raw.trim();
if line.starts_with('#') {
continue;
}
if line.starts_with('[') {
in_package = line == "[package]";
continue;
}
if in_package {
if let Some(rest) = line.strip_prefix("name") {
let rest = rest.trim_start_matches(|c: char| c.is_whitespace() || c == '=');
if let Some(name) = rest
.trim()
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
{
return Ok(name.to_string());
}
}
}
}
Err(HarnessError::Build(format!(
"no [package] name in {}",
manifest_dir.join("Cargo.toml").display()
)))
}