obelisk_component_builder/
lib.rsuse cargo_metadata::camino::Utf8Path;
use std::{
path::{Path, PathBuf},
process::Command,
};
const WASI_P2: &str = "wasm32-wasip2";
const WASM_CORE_MODULE: &str = "wasm32-unknown-unknown";
pub fn build_activity() {
build_internal(WASI_P2, ComponentType::ActivityWasm, &get_target_dir());
}
pub fn build_webhook_endpoint() {
build_internal(WASI_P2, ComponentType::WebhookEndpoint, &get_target_dir());
}
pub fn build_workflow() {
build_internal(WASM_CORE_MODULE, ComponentType::Workflow, &get_target_dir());
}
enum ComponentType {
ActivityWasm,
WebhookEndpoint,
Workflow,
}
fn to_snake_case(input: &str) -> String {
input.replace(['-', '.'], "_")
}
fn is_transformation_to_wasm_component_needed(target_tripple: &str) -> bool {
target_tripple == WASM_CORE_MODULE
}
fn get_target_dir() -> PathBuf {
if let Ok(workspace_dir) = std::env::var("CARGO_WORKSPACE_DIR") {
return Path::new(&workspace_dir).join("target");
}
let out_path = get_out_dir();
out_path
.ancestors()
.nth(4) .expect("Unable to determine target directory")
.to_path_buf()
}
fn get_out_dir() -> PathBuf {
PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR environment variable not set"))
}
fn build_internal(target_tripple: &str, component_type: ComponentType, dst_target_dir: &Path) {
let pkg_name = std::env::var("CARGO_PKG_NAME").unwrap();
let pkg_name = pkg_name.strip_suffix("-builder").unwrap();
let wasm_path = run_cargo_build(dst_target_dir, pkg_name, target_tripple);
if std::env::var("RUST_LOG").is_ok() {
println!("cargo:warning=Built `{pkg_name}` - {wasm_path:?}");
}
generate_code(&wasm_path, pkg_name, component_type);
let meta = cargo_metadata::MetadataCommand::new().exec().unwrap();
let package = meta
.packages
.iter()
.find(|p| p.name == pkg_name)
.unwrap_or_else(|| panic!("package `{pkg_name}` must exist"));
add_dependency(&package.manifest_path); for src_path in package
.targets
.iter()
.map(|target| target.src_path.parent().unwrap())
{
add_dependency(src_path);
}
let wit_path = &package.manifest_path.parent().unwrap().join("wit");
if wit_path.exists() && wit_path.is_dir() {
add_dependency(wit_path);
}
}
#[cfg(not(feature = "genrs"))]
fn generate_code(_wasm_path: &Path, _pkg_name: &str, _component_type: ComponentType) {}
#[cfg(feature = "genrs")]
impl From<ComponentType> for utils::wasm_tools::ComponentExportsType {
fn from(value: ComponentType) -> Self {
match value {
ComponentType::ActivityWasm | ComponentType::Workflow => Self::Enrichable,
ComponentType::WebhookEndpoint => Self::Plain,
}
}
}
#[cfg(feature = "genrs")]
fn generate_code(wasm_path: &Path, pkg_name: &str, component_type: ComponentType) {
use concepts::FunctionMetadata;
use indexmap::IndexMap;
enum Value {
Map(IndexMap<String, Value>),
Leaf(Vec<String>),
}
fn ser_map(map: &IndexMap<String, Value>, output: &mut String) {
for (k, v) in map {
match v {
Value::Leaf(vec) => {
for line in vec {
*output += line;
*output += "\n";
}
}
Value::Map(map) => {
*output += &format!("#[allow(clippy::all)]\npub mod r#{k} {{\n");
ser_map(map, output);
*output += "}\n";
}
}
}
}
let engine = {
let mut wasmtime_config = wasmtime::Config::new();
wasmtime_config.wasm_component_model(true);
wasmtime_config.async_support(true);
wasmtime::Engine::new(&wasmtime_config).unwrap()
};
let mut generated_code = String::new();
generated_code += &format!(
"pub const {name_upper}: &str = {wasm_path:?};\n",
name_upper = to_snake_case(pkg_name).to_uppercase()
);
let component =
utils::wasm_tools::WasmComponent::new(wasm_path, &engine, Some(component_type.into()))
.expect("cannot decode wasm component");
generated_code += "pub mod exports {\n";
let mut outer_map: IndexMap<String, Value> = IndexMap::new();
for export in component.exim.get_exports_hierarchy_ext() {
let ifc_fqn_split = export
.ifc_fqn
.split_terminator([':', '/', '@'])
.map(to_snake_case);
let mut map = &mut outer_map;
for mut split in ifc_fqn_split {
if split.starts_with(|c: char| c.is_numeric()) {
split = format!("_{split}");
}
if let Value::Map(m) = map
.entry(split)
.or_insert_with(|| Value::Map(IndexMap::new()))
{
map = m;
} else {
unreachable!()
}
}
let vec = export
.fns
.iter()
.filter(| (_, FunctionMetadata { submittable,.. }) | *submittable )
.map(|(function_name, FunctionMetadata{parameter_types, return_type, ..})| {
format!(
"/// {fn}: func{parameter_types}{arrow_ret_type};\npub const r#{name_upper}: (&str, &str) = (\"{ifc}\", \"{fn}\");\n",
name_upper = to_snake_case(function_name).to_uppercase(),
ifc = export.ifc_fqn,
fn = function_name,
arrow_ret_type = if let Some(ret_type) = return_type { format!(" -> {ret_type}") } else { String::new() }
)
})
.collect();
let old_val = map.insert(String::new(), Value::Leaf(vec));
assert!(old_val.is_none(), "same interface cannot appear twice");
}
ser_map(&outer_map, &mut generated_code);
generated_code += "}\n";
std::fs::write(get_out_dir().join("gen.rs"), generated_code).unwrap();
}
fn add_dependency(file: &Utf8Path) {
println!("cargo:rerun-if-changed={file}");
}
fn run_cargo_build(dst_target_dir: &Path, name: &str, tripple: &str) -> PathBuf {
let mut cmd = Command::new("cargo");
cmd.arg("build")
.arg("--release")
.arg(format!("--target={tripple}"))
.arg(format!("--package={name}"))
.env("CARGO_TARGET_DIR", dst_target_dir)
.env("CARGO_PROFILE_RELEASE_DEBUG", "limited") .env_remove("CARGO_ENCODED_RUSTFLAGS")
.env_remove("CLIPPY_ARGS"); let status = cmd.status().unwrap();
assert!(status.success());
let name_snake_case = to_snake_case(name);
let target = dst_target_dir
.join(tripple)
.join("release")
.join(format!("{name_snake_case}.wasm",));
assert!(target.exists(), "Target path must exist: {target:?}");
if is_transformation_to_wasm_component_needed(tripple) {
let target_transformed = dst_target_dir
.join(tripple)
.join("release")
.join(format!("{name_snake_case}_component.wasm",));
let mut cmd = Command::new("wasm-tools");
cmd.arg("component")
.arg("new")
.arg(
target
.to_str()
.expect("only utf-8 encoded paths are supported"),
)
.arg("--output")
.arg(
target_transformed
.to_str()
.expect("only utf-8 encoded paths are supported"),
);
let status = cmd.status().unwrap();
assert!(status.success());
assert!(
target_transformed.exists(),
"Transformed target path must exist: {target_transformed:?}"
);
std::fs::remove_file(&target).expect("deletion must succeed");
std::fs::rename(target_transformed, &target).expect("rename must succeed");
}
target
}