use anyhow::{Context, Result};
use builtin_protocol::{Manifest, Tier};
use heck::ToUpperCamelCase;
use include_dir::Dir;
use std::path::{Path, PathBuf};
use crate::adapter::typed::{
build_wrapper, target_wit_for_codegen, Behavior, BuildConfig, GenerateWrapperInput, TargetWit,
};
include!(concat!(env!("OUT_DIR"), "/embedded_strategies.rs"));
const PREVIEW1_ADAPTER: &[u8] = include_bytes!("../builtins/wasi_snapshot_preview1.reactor.wasm");
const BUILTIN_SUBDIR: &str = "builtins";
pub fn names() -> Vec<&'static str> {
EMBEDDED.iter().map(|(n, _)| *n).collect()
}
pub fn is_embedded_builtin(name: &str) -> bool {
EMBEDDED.iter().any(|(n, _)| *n == name)
}
pub fn read_manifest(name: &str) -> Result<Manifest> {
let dir = lookup(name)?;
let file = dir
.get_file("manifest.toml")
.with_context(|| format!("embedded tier-3/4 builtin '{name}' has no manifest.toml"))?;
let text = file.contents_utf8().context("manifest.toml is not UTF-8")?;
toml::from_str(text).with_context(|| format!("failed to parse manifest.toml for '{name}'"))
}
pub fn extract(name: &str, dest_dir: &Path) -> Result<PathBuf> {
let dir = lookup(name)?;
std::fs::create_dir_all(dest_dir)
.with_context(|| format!("could not create {}", dest_dir.display()))?;
dir.extract(dest_dir)
.with_context(|| format!("could not extract '{name}' into {}", dest_dir.display()))?;
Ok(dest_dir.to_path_buf())
}
fn lookup(name: &str) -> Result<&'static Dir<'static>> {
EMBEDDED
.iter()
.find_map(|(n, d)| (*n == name).then_some(*d))
.with_context(|| format!("no embedded tier-3/4 builtin named '{name}'"))
}
pub enum Tier3_4Source<'a> {
Builtin(&'a str),
User {
wac_name: &'a str,
strategy_dir: &'a Path,
},
}
pub fn is_user_strategy_dir(path: &Path) -> bool {
path.is_dir() && path.join("manifest.toml").is_file()
}
pub fn materialize_tier3_4(
splits_dir: &Path,
source: Tier3_4Source<'_>,
split_bytes: &[u8],
target_interface: &str,
) -> Result<(PathBuf, Tier)> {
let prep = match source {
Tier3_4Source::Builtin(name) => prepare_builtin_strategy(name)?,
Tier3_4Source::User {
wac_name,
strategy_dir,
} => prepare_user_strategy(wac_name, strategy_dir)?,
};
materialize_from_prepared(splits_dir, prep, split_bytes, target_interface)
}
struct PreparedStrategy {
manifest: Manifest,
out_name: String,
strategy_dir: PathBuf,
sdk_version: String,
strategy_crate_name: String,
}
fn prepare_builtin_strategy(name: &str) -> Result<PreparedStrategy> {
let manifest = read_manifest(name)?;
let cache_root = typed_cache_root()?;
let strategy_dir = cache_root.join("strategies").join(name);
extract(name, &strategy_dir)?;
let sdk_version = read_sdk_version_from(&strategy_dir)?;
Ok(PreparedStrategy {
manifest,
out_name: name.to_string(),
strategy_dir,
sdk_version,
strategy_crate_name: name.to_string(),
})
}
fn prepare_user_strategy(wac_name: &str, strategy_dir: &Path) -> Result<PreparedStrategy> {
let manifest = read_user_manifest(strategy_dir)?;
let meta = read_user_strategy_metadata(strategy_dir)?;
Ok(PreparedStrategy {
manifest,
out_name: wac_name.to_string(),
strategy_dir: strategy_dir.to_path_buf(),
sdk_version: meta.sdk_version,
strategy_crate_name: meta.crate_name,
})
}
fn materialize_from_prepared(
splits_dir: &Path,
prep: PreparedStrategy,
split_bytes: &[u8],
target_interface: &str,
) -> Result<(PathBuf, Tier)> {
let behavior = behavior_for(&prep.manifest, &prep.out_name)?;
let target = target_wit_for_codegen(split_bytes, target_interface, behavior)?;
let cache_root = typed_cache_root()?;
let adapter_path = ensure_preview1_adapter(&cache_root)?;
let strategy_type = prep.strategy_crate_name.to_upper_camel_case();
let plan = BuildPlan {
out_name: &prep.out_name,
strategy_dir: &prep.strategy_dir,
sdk_version: &prep.sdk_version,
strategy_crate_name: &prep.strategy_crate_name,
strategy_type: &strategy_type,
adapter_path: &adapter_path,
cache_root: &cache_root,
};
let path = run_codegen_build(splits_dir, &plan, behavior, &target)?;
Ok((path, prep.manifest.builtin.tier))
}
fn read_user_manifest(strategy_dir: &Path) -> Result<Manifest> {
let manifest_path = strategy_dir.join("manifest.toml");
let text = std::fs::read_to_string(&manifest_path).with_context(|| {
format!(
"could not read manifest.toml at {}; user-supplied tier-3/4 strategy crates must \
ship a manifest.toml at their crate root",
manifest_path.display()
)
})?;
toml::from_str(&text).with_context(|| format!("failed to parse {}", manifest_path.display()))
}
#[derive(Debug)]
struct UserStrategyMetadata {
crate_name: String,
sdk_version: String,
}
fn read_user_strategy_metadata(strategy_dir: &Path) -> Result<UserStrategyMetadata> {
let cargo_path = strategy_dir.join("Cargo.toml");
let cargo_text = std::fs::read_to_string(&cargo_path)
.with_context(|| format!("could not read {}", cargo_path.display()))?;
let parsed: toml::Value = toml::from_str(&cargo_text)
.with_context(|| format!("failed to parse {}", cargo_path.display()))?;
let crate_name = parsed
.get("package")
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
.with_context(|| {
format!(
"{} has no [package].name; user strategy crates must declare a package name",
cargo_path.display()
)
})?
.to_string();
let sdk_version = extract_sdk_version(&parsed).with_context(|| {
format!(
"user strategy crate at {} must declare `splicer-tool-sdk = \"<version>\"` (or the \
equivalent table form) in [dependencies]; the wrapper splicer synthesizes uses the \
same version so cargo dedupes both deps into one source",
cargo_path.display()
)
})?;
Ok(UserStrategyMetadata {
crate_name,
sdk_version,
})
}
fn read_sdk_version_from(strategy_dir: &Path) -> Result<String> {
let cargo_path = strategy_dir.join("Cargo.toml");
let cargo_text = std::fs::read_to_string(&cargo_path)
.with_context(|| format!("could not read {}", cargo_path.display()))?;
let parsed: toml::Value = toml::from_str(&cargo_text)
.with_context(|| format!("failed to parse {}", cargo_path.display()))?;
extract_sdk_version(&parsed).with_context(|| {
format!(
"strategy crate at {} is missing a `splicer-tool-sdk` version in [dependencies]",
cargo_path.display()
)
})
}
fn extract_sdk_version(parsed: &toml::Value) -> Option<String> {
let dep = parsed.get("dependencies")?.get("splicer-tool-sdk")?;
match dep {
toml::Value::String(s) => Some(s.clone()),
toml::Value::Table(t) => t
.get("version")
.and_then(|v| v.as_str())
.map(str::to_string),
_ => None,
}
}
struct BuildPlan<'a> {
out_name: &'a str,
strategy_dir: &'a Path,
sdk_version: &'a str,
strategy_crate_name: &'a str,
strategy_type: &'a str,
adapter_path: &'a Path,
cache_root: &'a Path,
}
fn run_codegen_build(
splits_dir: &Path,
plan: &BuildPlan<'_>,
behavior: Behavior,
target: &TargetWit,
) -> Result<PathBuf> {
let strategy_path_str = plan.strategy_dir.to_str().with_context(|| {
format!(
"strategy path is not UTF-8: {}",
plan.strategy_dir.display()
)
})?;
let input = GenerateWrapperInput {
target_wit: &target.wit_text,
world_name: Some(&target.world_name),
interface_qualified_name: &target.qualified_name,
behavior,
strategy_crate_name: plan.strategy_crate_name,
strategy_crate_path: strategy_path_str,
strategy_type: plan.strategy_type,
splicer_tool_sdk_version: plan.sdk_version,
};
let built = build_wrapper(
&input,
&BuildConfig {
build_root: plan.cache_root,
adapter_wasm: plan.adapter_path,
target: None,
},
)?;
let out_dir = splits_dir.join(BUILTIN_SUBDIR);
std::fs::create_dir_all(&out_dir)
.with_context(|| format!("could not create {}", out_dir.display()))?;
let out = out_dir.join(format!("{}.wasm", plan.out_name));
std::fs::copy(&built, &out).with_context(|| {
format!(
"could not copy built wrapper {} -> {}",
built.display(),
out.display()
)
})?;
Ok(out)
}
fn ensure_preview1_adapter(cache_root: &Path) -> Result<PathBuf> {
let path = cache_root.join("wasi_snapshot_preview1.reactor.wasm");
if !path.exists() {
std::fs::write(&path, PREVIEW1_ADAPTER)
.with_context(|| format!("could not write preview1 adapter to {}", path.display()))?;
}
Ok(path)
}
fn behavior_for(manifest: &Manifest, name: &str) -> Result<Behavior> {
match manifest.builtin.tier {
Tier::Tier3 => Ok(Behavior::Transform),
Tier::Tier4 => Ok(Behavior::Virtualize),
other => anyhow::bail!(
"tier-3/4 codegen invoked on '{name}' but its manifest declares tier {other:?}"
),
}
}
fn typed_cache_root() -> Result<PathBuf> {
let base = crate::builtins::user_cache_dir().context(
"no user cache directory available; \
set XDG_CACHE_HOME or HOME to enable tier-3/4 codegen",
)?;
Ok(base.join("splicer").join("typed-builtins"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_include_both_smoke_builtins() {
let names = names();
assert!(names.contains(&"hello-tier3"), "got: {names:?}");
assert!(names.contains(&"hello-tier4"), "got: {names:?}");
}
#[test]
fn is_embedded_builtin_recognizes_embedded_names() {
assert!(is_embedded_builtin("hello-tier3"));
assert!(is_embedded_builtin("hello-tier4"));
assert!(!is_embedded_builtin("hello-tier1"));
assert!(!is_embedded_builtin("does-not-exist"));
}
#[test]
fn read_manifest_returns_tier3_or_tier4_manifest() {
for name in names() {
let manifest = read_manifest(name).unwrap();
assert!(
matches!(manifest.builtin.tier, Tier::Tier3 | Tier::Tier4),
"{name} manifest is not tier-3 or tier-4: got {:?}",
manifest.builtin.tier
);
}
}
#[test]
fn read_manifest_unknown_name_errors() {
let err = read_manifest("not-a-real-builtin").unwrap_err();
assert!(err.to_string().contains("not-a-real-builtin"));
}
#[test]
fn extract_writes_manifest_and_lib() {
let tmp = tempfile::tempdir().unwrap();
let dest = tmp.path().join("hello-tier3");
extract("hello-tier3", &dest).expect("extract succeeds");
assert!(dest.join("Cargo.toml").exists());
assert!(dest.join("manifest.toml").exists());
assert!(dest.join("src/lib.rs").exists());
}
#[test]
fn extract_unknown_name_errors() {
let tmp = tempfile::tempdir().unwrap();
let err = extract("not-a-real-builtin", tmp.path()).unwrap_err();
assert!(err.to_string().contains("not-a-real-builtin"));
}
#[cfg(test)]
fn write_user_strategy_dir(
dest: &Path,
crate_name: &str,
tier: u8,
sdk_dep: &str,
) -> std::io::Result<()> {
std::fs::create_dir_all(dest.join("src"))?;
std::fs::write(
dest.join("Cargo.toml"),
format!(
"[package]\nname = \"{crate_name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\npublish = false\n\
\n[workspace]\n\n[lib]\n\n[dependencies]\nsplicer-tool-sdk = {sdk_dep}\n"
),
)?;
std::fs::write(
dest.join("manifest.toml"),
format!("[builtin]\ndescription = \"test strategy\"\ntier = {tier}\n"),
)?;
std::fs::write(dest.join("src/lib.rs"), "// stub\n")?;
Ok(())
}
#[test]
fn is_user_strategy_dir_requires_manifest() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("strat");
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(dir.join("Cargo.toml"), "[package]\nname = \"s\"\n").unwrap();
assert!(!is_user_strategy_dir(&dir));
std::fs::write(dir.join("manifest.toml"), "[builtin]\n").unwrap();
assert!(is_user_strategy_dir(&dir));
let wasm = tmp.path().join("mw.wasm");
std::fs::write(&wasm, b"\0asm\x0d\0\0\0").unwrap();
assert!(!is_user_strategy_dir(&wasm));
assert!(!is_user_strategy_dir(&tmp.path().join("ghost")));
}
#[test]
fn read_user_strategy_metadata_accepts_string_dep() {
let tmp = tempfile::tempdir().unwrap();
let strat = tmp.path().join("strat");
write_user_strategy_dir(&strat, "my-strategy", 3, "\"0.1.0\"").unwrap();
let meta = read_user_strategy_metadata(&strat).expect("read metadata");
assert_eq!(meta.crate_name, "my-strategy");
assert_eq!(meta.sdk_version, "0.1.0");
}
#[test]
fn read_user_strategy_metadata_accepts_table_dep_with_version_and_path() {
let tmp = tempfile::tempdir().unwrap();
let strat = tmp.path().join("strat");
write_user_strategy_dir(
&strat,
"my-strategy",
3,
"{ version = \"0.1.0\", path = \"../sdk\" }",
)
.unwrap();
let meta = read_user_strategy_metadata(&strat).expect("read metadata");
assert_eq!(meta.sdk_version, "0.1.0");
}
#[test]
fn read_user_strategy_metadata_errors_without_sdk_dep() {
let tmp = tempfile::tempdir().unwrap();
let strat = tmp.path().join("strat");
std::fs::create_dir_all(strat.join("src")).unwrap();
std::fs::write(
strat.join("Cargo.toml"),
"[package]\nname = \"s\"\nversion = \"0.1.0\"\nedition = \"2021\"\n[dependencies]\n",
)
.unwrap();
std::fs::write(strat.join("manifest.toml"), "[builtin]\ntier = 3\n").unwrap();
let err = read_user_strategy_metadata(&strat).unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("splicer-tool-sdk"),
"error should name the missing dep: {msg}"
);
}
#[test]
fn read_user_manifest_classifies_tier() {
let tmp = tempfile::tempdir().unwrap();
let strat = tmp.path().join("strat");
write_user_strategy_dir(&strat, "s", 4, "\"0.1.0\"").unwrap();
let manifest = read_user_manifest(&strat).expect("read manifest");
assert!(matches!(manifest.builtin.tier, Tier::Tier4));
}
#[test]
fn read_user_manifest_errors_when_missing() {
let tmp = tempfile::tempdir().unwrap();
let err = read_user_manifest(tmp.path()).unwrap_err();
assert!(format!("{err:#}").contains("manifest.toml"));
}
#[test]
#[ignore = "shells out to cargo + wasm32-wasip1; run with --ignored"]
fn materialize_tier3_produces_a_component() {
use crate::adapter::typed::target_wit::test_fixture::component_from_wit;
const FIXTURE_WIT: &str = r#"
package test:demo@0.1.0;
interface ops {
add: async func(a: u32, b: u32) -> u32;
}
world demo {
export ops;
}
"#;
let composition = component_from_wit(FIXTURE_WIT, "demo").expect("synthesize fixture");
let splits = tempfile::tempdir().unwrap();
let (out, tier) = materialize_tier3_4(
splits.path(),
Tier3_4Source::Builtin("hello-tier3"),
&composition,
"test:demo/ops@0.1.0",
)
.expect("materialize");
assert_eq!(tier, Tier::Tier3);
assert!(out.ends_with("builtins/hello-tier3.wasm"));
let bytes = std::fs::read(&out).expect("read");
assert!(bytes.starts_with(&[0x00, 0x61, 0x73, 0x6d]), "wasm magic");
let parser = wasmparser::Parser::new(0);
for payload in parser.parse_all(&bytes) {
payload.expect("component payload parses");
}
}
#[test]
#[ignore = "shells out to cargo + wasm32-wasip1; run with --ignored"]
fn materialize_user_tier3_produces_a_component() {
use crate::adapter::typed::target_wit::test_fixture::component_from_wit;
const FIXTURE_WIT: &str = r#"
package test:demo@0.1.0;
interface ops {
add: async func(a: u32, b: u32) -> u32;
}
world demo {
export ops;
}
"#;
let composition = component_from_wit(FIXTURE_WIT, "demo").expect("synthesize fixture");
let tmp = tempfile::tempdir().unwrap();
let strat = tmp.path().join("hello-tier3");
extract("hello-tier3", &strat).expect("extract strategy");
let splits = tempfile::tempdir().unwrap();
let (out, tier) = materialize_tier3_4(
splits.path(),
Tier3_4Source::User {
wac_name: "my-greeter",
strategy_dir: &strat,
},
&composition,
"test:demo/ops@0.1.0",
)
.expect("materialize_tier3_4(user)");
assert_eq!(tier, Tier::Tier3);
assert!(out.ends_with("builtins/my-greeter.wasm"));
let bytes = std::fs::read(&out).expect("read");
assert!(bytes.starts_with(&[0x00, 0x61, 0x73, 0x6d]), "wasm magic");
}
}