obelisk_component_builder/
lib.rs1use cargo_metadata::camino::Utf8Path;
2use std::{
3 path::{Path, PathBuf},
4 process::Command,
5};
6
7const WASI_P2: &str = "wasm32-wasip2";
8const WASM_CORE_MODULE: &str = "wasm32-unknown-unknown";
9
10#[derive(Debug, Clone, Default)]
11pub struct BuildConfig {
12 pub profile: Option<String>,
13 pub custom_dst_target_dir: Option<PathBuf>,
14}
15impl BuildConfig {
16 pub fn profile(profile: impl Into<String>) -> Self {
17 Self {
18 profile: Some(profile.into()),
19 custom_dst_target_dir: None,
20 }
21 }
22 pub fn target_subdir(target_dir: impl Into<PathBuf>) -> Self {
23 Self {
24 profile: None,
25 custom_dst_target_dir: Some(get_target_dir().join(target_dir.into())),
26 }
27 }
28 pub fn new(
29 profile: Option<impl Into<String>>,
30 custom_dst_target_dir: Option<impl Into<PathBuf>>,
31 ) -> Self {
32 Self {
33 profile: profile.map(Into::into),
34 custom_dst_target_dir: custom_dst_target_dir.map(Into::into),
35 }
36 }
37}
38
39#[allow(clippy::must_use_candidate)]
46pub fn build_activity(conf: BuildConfig) -> PathBuf {
47 build_internal(WASI_P2, ComponentType::ActivityWasm, conf)
48}
49
50#[allow(clippy::must_use_candidate)]
57pub fn build_webhook_endpoint(conf: BuildConfig) -> PathBuf {
58 build_internal(WASI_P2, ComponentType::WebhookEndpoint, conf)
59}
60
61#[allow(clippy::must_use_candidate)]
68pub fn build_workflow(conf: BuildConfig) -> PathBuf {
69 build_internal(WASM_CORE_MODULE, ComponentType::Workflow, conf)
70}
71
72enum ComponentType {
73 ActivityWasm,
74 WebhookEndpoint,
75 Workflow,
76}
77
78fn to_snake_case(input: &str) -> String {
79 input.replace(['-', '.'], "_")
80}
81
82fn is_transformation_to_wasm_component_needed(target_tripple: &str) -> bool {
83 target_tripple == WASM_CORE_MODULE
84}
85
86fn get_target_dir() -> PathBuf {
95 if let Ok(workspace_dir) = std::env::var("CARGO_WORKSPACE_DIR") {
97 Path::new(&workspace_dir).join("target")
98 } else {
99 unreachable!("CARGO_WORKSPACE_DIR must be set")
100 }
101}
102#[cfg(feature = "genrs")]
106fn get_out_dir() -> PathBuf {
107 PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR environment variable not set"))
108}
109
110fn build_internal(
111 target_tripple: &str,
112 component_type: ComponentType,
113 conf: BuildConfig,
114) -> PathBuf {
115 let dst_target_dir = conf.custom_dst_target_dir.unwrap_or_else(get_target_dir);
116 let pkg_name = std::env::var("CARGO_PKG_NAME").unwrap();
117 let pkg_name = pkg_name.strip_suffix("-builder").unwrap();
118 let wasm_path = run_cargo_build(
119 &dst_target_dir,
120 pkg_name,
121 target_tripple,
122 conf.profile.as_deref(),
123 );
124 if std::env::var("RUST_LOG").is_ok() {
125 println!("cargo:warning=Built `{pkg_name}` - {wasm_path:?}");
126 }
127
128 generate_code(&wasm_path, pkg_name, component_type);
129
130 let meta = cargo_metadata::MetadataCommand::new().exec().unwrap();
131 let package = meta
132 .packages
133 .iter()
134 .find(|p| p.name.as_str() == pkg_name)
135 .unwrap_or_else(|| panic!("package `{pkg_name}` must exist"));
136
137 add_dependency(&package.manifest_path); for src_path in package
139 .targets
140 .iter()
141 .map(|target| target.src_path.parent().unwrap())
142 {
143 add_dependency(src_path);
144 }
145 let wit_path = &package.manifest_path.parent().unwrap().join("wit");
146 if wit_path.exists() && wit_path.is_dir() {
147 add_dependency(wit_path);
148 }
149 wasm_path
150}
151
152#[cfg(not(feature = "genrs"))]
153fn generate_code(_wasm_path: &Path, _pkg_name: &str, _component_type: ComponentType) {}
154
155#[cfg(feature = "genrs")]
156impl From<ComponentType> for concepts::ComponentType {
157 fn from(value: ComponentType) -> Self {
158 match value {
159 ComponentType::ActivityWasm => Self::ActivityWasm,
160 ComponentType::Workflow => Self::Workflow,
161 ComponentType::WebhookEndpoint => Self::WebhookEndpoint,
162 }
163 }
164}
165
166#[cfg(feature = "genrs")]
167fn generate_code(wasm_path: &Path, pkg_name: &str, component_type: ComponentType) {
168 use concepts::FunctionMetadata;
169 use indexmap::IndexMap;
170 use std::fmt::Write as _;
171
172 enum Value {
173 Map(IndexMap<String, Value>),
174 Leaf(Vec<String>),
175 }
176
177 fn ser_map(map: &IndexMap<String, Value>, output: &mut String) {
178 for (k, v) in map {
179 match v {
180 Value::Leaf(vec) => {
181 for line in vec {
182 *output += line;
183 *output += "\n";
184 }
185 }
186 Value::Map(map) => {
187 write!(output, "#[allow(clippy::all)]\npub mod r#{k} {{\n").unwrap();
188 ser_map(map, output);
189 *output += "}\n";
190 }
191 }
192 }
193 }
194
195 let mut generated_code = String::new();
196 writeln!(
197 generated_code,
198 "pub const {name_upper}: &str = {wasm_path:?};",
199 name_upper = to_snake_case(pkg_name).to_uppercase()
200 )
201 .unwrap();
202
203 let component = utils::wasm_tools::WasmComponent::new(wasm_path, component_type.into())
204 .expect("cannot decode wasm component");
205 generated_code += "pub mod exports {\n";
206 let mut outer_map: IndexMap<String, Value> = IndexMap::new();
207 for export in component.exim.get_exports_hierarchy_ext() {
208 let ifc_fqn_split = export
209 .ifc_fqn
210 .split_terminator([':', '/', '@'])
211 .map(to_snake_case);
212 let mut map = &mut outer_map;
213 for mut split in ifc_fqn_split {
214 if split.starts_with(|c: char| c.is_numeric()) {
215 split = format!("_{split}");
216 }
217 if let Value::Map(m) = map
218 .entry(split)
219 .or_insert_with(|| Value::Map(IndexMap::new()))
220 {
221 map = m;
222 } else {
223 unreachable!()
224 }
225 }
226 let vec = export
227 .fns
228 .iter()
229 .filter(| (_, FunctionMetadata { submittable,.. }) | *submittable )
230 .map(|(function_name, FunctionMetadata{parameter_types, return_type, ..})| {
231 format!(
232 "/// {fn}: func{parameter_types}{arrow_ret_type};\npub const r#{name_upper}: (&str, &str) = (\"{ifc}\", \"{fn}\");\n",
233 name_upper = to_snake_case(function_name).to_uppercase(),
234 ifc = export.ifc_fqn,
235 fn = function_name,
236 arrow_ret_type = if let Some(ret_type) = return_type { format!(" -> {ret_type}") } else { String::new() }
237 )
238 })
239 .collect();
240 let old_val = map.insert(String::new(), Value::Leaf(vec));
241 assert!(old_val.is_none(), "same interface cannot appear twice");
242 }
243
244 ser_map(&outer_map, &mut generated_code);
245 generated_code += "}\n";
246 std::fs::write(get_out_dir().join("gen.rs"), generated_code).unwrap();
247}
248
249fn add_dependency(file: &Utf8Path) {
250 println!("cargo:rerun-if-changed={file}");
251}
252
253fn run_cargo_build(
254 dst_target_dir: &Path,
255 name: &str,
256 tripple: &str,
257 profile: Option<&str>,
258) -> PathBuf {
259 let mut cmd = Command::new("cargo");
260 let temp_str;
261 cmd.arg("build")
262 .arg(if let Some(profile) = profile {
263 temp_str = format!("--profile={profile}");
264 &temp_str
265 } else {
266 "--release"
267 })
268 .arg(format!("--target={tripple}"))
269 .arg(format!("--package={name}"))
270 .env("CARGO_TARGET_DIR", dst_target_dir)
271 .env("CARGO_PROFILE_RELEASE_DEBUG", "limited") .env_remove("CARGO_ENCODED_RUSTFLAGS")
273 .env_remove("CLIPPY_ARGS"); let status = cmd.status().unwrap();
275 assert!(status.success());
276 let name_snake_case = to_snake_case(name);
277 let target = dst_target_dir
278 .join(tripple)
279 .join("release")
280 .join(format!("{name_snake_case}.wasm",));
281 assert!(target.exists(), "Target path must exist: {target:?}");
282 if is_transformation_to_wasm_component_needed(tripple) {
283 let target_transformed = dst_target_dir
284 .join(tripple)
285 .join("release")
286 .join(format!("{name_snake_case}_component.wasm",));
287 let mut cmd = Command::new("wasm-tools");
288 cmd.arg("component")
289 .arg("new")
290 .arg(
291 target
292 .to_str()
293 .expect("only utf-8 encoded paths are supported"),
294 )
295 .arg("--output")
296 .arg(
297 target_transformed
298 .to_str()
299 .expect("only utf-8 encoded paths are supported"),
300 );
301 let status = cmd.status().unwrap();
302 assert!(status.success());
303 assert!(
304 target_transformed.exists(),
305 "Transformed target path must exist: {target_transformed:?}"
306 );
307 std::fs::remove_file(&target).expect("deletion must succeed");
309 std::fs::rename(target_transformed, &target).expect("rename must succeed");
310 }
311 target
312}