1#![expect(missing_docs)] use std::{
4 collections::HashMap,
5 fs, io,
6 path::{Path, PathBuf},
7 process::Command,
8 str::FromStr,
9};
10
11use clap::Parser;
12use semver::Version;
13
14#[derive(Parser, Debug)]
15#[command()]
16struct Args {
17 #[arg(name = "crate", long)]
19 crate_: String,
20
21 #[arg(long)]
23 version: Version,
24
25 #[arg(long)]
27 codec: String,
28
29 #[arg(long, short)]
31 output: PathBuf,
32}
33
34fn main() -> io::Result<()> {
35 let args = Args::parse();
36
37 let scratch_dir = scratch::path(concat!(
38 env!("CARGO_PKG_NAME"),
39 "-",
40 env!("CARGO_PKG_VERSION"),
41 ));
42 eprintln!("scratch_dir={scratch_dir:?}");
43
44 let target_dir = scratch_dir.join("target");
45 eprintln!("target_dir={target_dir:?}");
46 eprintln!("creating {target_dir:?}");
47 fs::create_dir_all(&target_dir)?;
48
49 let crate_dir =
50 create_codec_wasm_component_crate(&scratch_dir, &args.crate_, &args.version, &args.codec)?;
51 copy_buildenv_to_crate(&crate_dir)?;
52
53 let nix_env = NixEnv::new(&crate_dir)?;
54
55 let wasm = build_wasm_codec(
56 &nix_env,
57 &target_dir,
58 &crate_dir,
59 &format!("{}-wasm", args.crate_),
60 )?;
61 let wasm = optimize_wasm_codec(&wasm, &nix_env)?;
62 let wasm = adapt_wasi_snapshot_to_preview2(&wasm)?;
63
64 fs::copy(wasm, args.output)?;
65
66 Ok(())
67}
68
69fn create_codec_wasm_component_crate(
70 scratch_dir: &Path,
71 crate_: &str,
72 version: &Version,
73 codec: &str,
74) -> io::Result<PathBuf> {
75 let crate_dir = scratch_dir.join(format!("{crate_}-wasm-{version}"));
76 eprintln!("crate_dir={crate_dir:?}");
77 eprintln!("creating {crate_dir:?}");
78 if crate_dir.exists() {
79 fs::remove_dir_all(&crate_dir)?;
80 }
81 fs::create_dir_all(&crate_dir)?;
82
83 fs::write(
84 crate_dir.join("Cargo.toml"),
85 format!(
86 r#"
87[workspace]
88
89[package]
90name = "{crate_}-wasm"
91version = "{version}"
92edition = "2021"
93
94# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
95
96[dependencies]
97numcodecs-wasm-logging = {{ version = "0.1", default-features = false }}
98numcodecs-wasm-guest = {{ version = "0.2", default-features = false }}
99numcodecs-my-codec = {{ package = "{crate_}", version = "{version}", default-features = false }}
100 "#
101 ),
102 )?;
103
104 fs::create_dir_all(crate_dir.join("src"))?;
105
106 fs::write(
107 crate_dir.join("src").join("lib.rs"),
108 format!(
109 "
110#![cfg_attr(not(test), no_main)]
111
112numcodecs_wasm_guest::export_codec!(
113 numcodecs_wasm_logging::LoggingCodec<numcodecs_my_codec::{codec}>
114);
115 "
116 ),
117 )?;
118
119 Ok(crate_dir)
120}
121
122fn copy_buildenv_to_crate(crate_dir: &Path) -> io::Result<()> {
123 fs::write(
124 crate_dir.join("flake.nix"),
125 include_str!("../buildenv/flake.nix"),
126 )?;
127 fs::write(
128 crate_dir.join("flake.lock"),
129 include_str!("../buildenv/flake.lock"),
130 )?;
131
132 fs::write(
133 crate_dir.join("include.hpp"),
134 include_str!("../buildenv/include.hpp"),
135 )?;
136
137 fs::write(
138 crate_dir.join("rust-toolchain"),
139 include_str!("../buildenv/rust-toolchain"),
140 )?;
141
142 Ok(())
143}
144
145struct NixEnv {
146 llvm_version: String,
147 ar: PathBuf,
148 clang: PathBuf,
149 libclang: PathBuf,
150 lld: PathBuf,
151 nm: PathBuf,
152 wasi_sysroot: PathBuf,
153 wasm_opt: PathBuf,
154}
155
156impl NixEnv {
157 pub fn new(flake_parent_dir: &Path) -> io::Result<Self> {
158 fn try_read_env<T: FromStr<Err: std::error::Error>>(
159 env: &HashMap<&str, &str>,
160 key: &str,
161 ) -> Result<T, io::Error> {
162 let Some(var) = env.get(key).copied() else {
163 return Err(io::Error::new(
164 io::ErrorKind::InvalidData,
165 format!("missing flake env key: {key}"),
166 ));
167 };
168
169 T::from_str(var).map_err(|err| {
170 io::Error::new(
171 io::ErrorKind::InvalidData,
172 format!("invalid flake env variable {key}={var}: {err}"),
173 )
174 })
175 }
176
177 let mut env = Command::new("nix");
178 env.current_dir(flake_parent_dir);
179 env.arg("develop");
180 env.arg("path:.");
183 env.arg("--no-update-lock-file");
184 env.arg("--ignore-environment");
185 env.arg("--command");
186 env.arg("env");
187 eprintln!("executing {env:?}");
188 let env = env.output()?;
189 eprintln!(
190 "{}\n{}",
191 String::from_utf8_lossy(&env.stdout),
192 String::from_utf8_lossy(&env.stderr)
193 );
194 let env = std::str::from_utf8(&env.stdout).map_err(|err| {
195 io::Error::new(
196 io::ErrorKind::InvalidData,
197 format!("invalid flake env output: {err}"),
198 )
199 })?;
200 let env = env
201 .lines()
202 .filter_map(|line| line.split_once('='))
203 .collect::<HashMap<_, _>>();
204
205 Ok(Self {
206 llvm_version: try_read_env(&env, "MY_LLVM_VERSION")?,
207 ar: try_read_env(&env, "MY_AR")?,
208 clang: try_read_env(&env, "MY_CLANG")?,
209 libclang: try_read_env(&env, "MY_LIBCLANG")?,
210 lld: try_read_env(&env, "MY_LLD")?,
211 nm: try_read_env(&env, "MY_NM")?,
212 wasi_sysroot: try_read_env(&env, "MY_WASI_SYSROOT")?,
213 wasm_opt: try_read_env(&env, "MY_WASM_OPT")?,
214 })
215 }
216}
217
218#[expect(clippy::too_many_lines)]
219fn configure_cargo_cmd(nix_env: &NixEnv, target_dir: &Path, crate_dir: &Path) -> Command {
220 let NixEnv {
221 llvm_version,
222 ar,
223 clang,
224 libclang,
225 lld,
226 nm,
227 wasi_sysroot,
228 ..
229 } = nix_env;
230
231 let mut cmd = Command::new("nix");
232 cmd.current_dir(crate_dir);
233 cmd.arg("develop");
234 cmd.arg("--no-update-lock-file");
237 cmd.arg("--ignore-environment");
238 cmd.arg("path:.");
239 cmd.arg("--command");
240 cmd.arg("env");
241 cmd.arg(format!("CC={clang}", clang = clang.join("clang").display()));
242 cmd.arg(format!(
243 "CXX={clang}",
244 clang = clang.join("clang++").display()
245 ));
246 cmd.arg(format!("LD={lld}", lld = lld.join("lld").display()));
247 cmd.arg(format!("LLD={lld}", lld = lld.join("lld").display()));
248 cmd.arg(format!("AR={ar}", ar = ar.display()));
249 cmd.arg(format!("NM={nm}", nm = nm.display()));
250 cmd.arg(format!(
251 "LIBCLANG_PATH={libclang}",
252 libclang = libclang.display()
253 ));
254 cmd.arg(format!(
255 "CFLAGS=--target=wasm32-wasip1 -nodefaultlibs -resource-dir {resource_dir} \
256 --sysroot={wasi_sysroot} -isystem {clang_include} -isystem {wasi32_wasi_include} \
257 -isystem {include} -B {lld} -D_WASI_EMULATED_PROCESS_CLOCKS -O3",
258 resource_dir = libclang.join("clang").join(llvm_version).display(),
259 wasi_sysroot = wasi_sysroot.display(),
260 clang_include = libclang
261 .join("clang")
262 .join(llvm_version)
263 .join("include")
264 .display(),
265 wasi32_wasi_include = wasi_sysroot.join("include").join("wasm32-wasip1").display(),
266 include = wasi_sysroot.join("include").display(),
267 lld = lld.display(),
268 ));
269 cmd.arg(format!(
270 "CXXFLAGS=--target=wasm32-wasip1 -nodefaultlibs -resource-dir {resource_dir} \
271 --sysroot={wasi_sysroot} -isystem {wasm32_wasi_cxx_include} -isystem {cxx_include} \
272 -isystem {clang_include} -isystem {wasi32_wasi_include} -isystem {include} -B {lld} \
273 -D_WASI_EMULATED_PROCESS_CLOCKS -include {cpp_include_path} -O3",
274 resource_dir = libclang.join("clang").join(llvm_version).display(),
275 wasi_sysroot = wasi_sysroot.display(),
276 wasm32_wasi_cxx_include = wasi_sysroot
277 .join("include")
278 .join("wasm32-wasip1")
279 .join("c++")
280 .join("v1")
281 .display(),
282 cxx_include = wasi_sysroot
283 .join("include")
284 .join("c++")
285 .join("v1")
286 .display(),
287 clang_include = libclang
288 .join("clang")
289 .join(llvm_version)
290 .join("include")
291 .display(),
292 wasi32_wasi_include = wasi_sysroot.join("include").join("wasm32-wasip1").display(),
293 include = wasi_sysroot.join("include").display(),
294 lld = lld.display(),
295 cpp_include_path = crate_dir.join("include.hpp").display(),
296 ));
297 cmd.arg(format!(
298 "BINDGEN_EXTRA_CLANG_ARGS=--target=wasm32-wasip1 -nodefaultlibs -resource-dir \
299 {resource_dir} --sysroot={wasi_sysroot} -isystem {wasm32_wasi_cxx_include} -isystem \
300 {cxx_include} -isystem {clang_include} -isystem {wasi32_wasi_include} -isystem {include} \
301 -B {lld} -D_WASI_EMULATED_PROCESS_CLOCKS -fvisibility=default",
302 resource_dir = libclang.join("clang").join(llvm_version).display(),
303 wasi_sysroot = wasi_sysroot.display(),
304 wasm32_wasi_cxx_include = wasi_sysroot
305 .join("include")
306 .join("wasm32-wasip1")
307 .join("c++")
308 .join("v1")
309 .display(),
310 cxx_include = wasi_sysroot
311 .join("include")
312 .join("c++")
313 .join("v1")
314 .display(),
315 clang_include = libclang
316 .join("clang")
317 .join(llvm_version)
318 .join("include")
319 .display(),
320 wasi32_wasi_include = wasi_sysroot.join("include").join("wasm32-wasip1").display(),
321 include = wasi_sysroot.join("include").display(),
322 lld = lld.display(),
323 ));
324 cmd.arg("CXXSTDLIB=c++");
325 cmd.arg("CRATE_CC_NO_DEFAULTS=1");
327 cmd.arg("LDFLAGS=-lc -lwasi-emulated-process-clocks");
328 cmd.arg(format!(
329 "RUSTFLAGS=-C panic=abort -C strip=symbols -C link-arg=-L{wasm32_wasi_lib}",
330 wasm32_wasi_lib = wasi_sysroot.join("lib").join("wasm32-wasip1").display(),
331 ));
332 cmd.arg(format!(
333 "CARGO_TARGET_DIR={target_dir}",
334 target_dir = target_dir.display()
335 ));
336
337 cmd.arg("RUSTC_BOOTSTRAP=1");
341
342 cmd.arg("cargo");
343
344 cmd
345}
346
347fn build_wasm_codec(
348 nix_env: &NixEnv,
349 target_dir: &Path,
350 crate_dir: &Path,
351 crate_name: &str,
352) -> io::Result<PathBuf> {
353 let mut cmd = configure_cargo_cmd(nix_env, target_dir, crate_dir);
354 cmd.arg("rustc")
355 .arg("--crate-type=cdylib")
356 .arg("-Z")
357 .arg("build-std=std,panic_abort")
358 .arg("-Z")
359 .arg("build-std-features=panic_immediate_abort")
360 .arg("--release")
361 .arg("--target=wasm32-wasip1");
362
363 eprintln!("executing {cmd:?}");
364
365 let status = cmd.status()?;
366 if !status.success() {
367 return Err(io::Error::new(
368 io::ErrorKind::Other,
369 format!("cargo exited with code {status}"),
370 ));
371 }
372
373 Ok(target_dir
374 .join("wasm32-wasip1")
375 .join("release")
376 .join(crate_name.replace('-', "_"))
377 .with_extension("wasm"))
378}
379
380fn optimize_wasm_codec(wasm: &Path, nix_env: &NixEnv) -> io::Result<PathBuf> {
381 let NixEnv { wasm_opt, .. } = nix_env;
382
383 let opt_out = wasm.with_extension("opt.wasm");
384
385 let mut cmd = Command::new(wasm_opt);
386
387 cmd.arg("--enable-sign-ext")
388 .arg("--disable-threads")
389 .arg("--enable-mutable-globals")
390 .arg("--enable-nontrapping-float-to-int")
391 .arg("--enable-simd")
392 .arg("--enable-bulk-memory")
393 .arg("--disable-exception-handling")
394 .arg("--disable-tail-call")
395 .arg("--disable-reference-types")
396 .arg("--enable-multivalue")
397 .arg("--disable-gc")
398 .arg("--disable-memory64")
399 .arg("--disable-relaxed-simd")
400 .arg("--disable-extended-const")
401 .arg("--disable-strings")
402 .arg("--disable-multimemory");
403
404 cmd.arg("-O4").arg("-o").arg(&opt_out).arg(wasm);
405
406 eprintln!("executing {cmd:?}");
407
408 let status = cmd.status()?;
409 if !status.success() {
410 return Err(io::Error::new(
411 io::ErrorKind::Other,
412 format!("wasm-opt exited with code {status}"),
413 ));
414 }
415
416 Ok(opt_out)
417}
418
419fn adapt_wasi_snapshot_to_preview2(wasm: &Path) -> io::Result<PathBuf> {
420 let wasm_preview2 = wasm.with_extension("preview2.wasm");
421
422 eprintln!("reading from {wasm:?}");
423 let wasm = fs::read(wasm)?;
424
425 let mut encoder = wit_component::ComponentEncoder::default()
426 .module(&wasm)
427 .map_err(|err| {
428 io::Error::new(
429 io::ErrorKind::Other,
430 format!("wit_component::ComponentEncoder::module failed: {err:#}"),
432 )
433 })?
434 .adapter(
435 wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_ADAPTER_NAME,
436 wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER,
437 )
438 .map_err(|err| {
439 io::Error::new(
440 io::ErrorKind::Other,
441 format!("wit_component::ComponentEncoder::adapter failed: {err:#}"),
443 )
444 })?;
445
446 let wasm = encoder.encode().map_err(|err| {
447 io::Error::new(
448 io::ErrorKind::Other,
449 format!("wit_component::ComponentEncoder::encode failed: {err:#}"),
451 )
452 })?;
453
454 eprintln!("writing to {wasm_preview2:?}");
455 fs::write(&wasm_preview2, wasm)?;
456
457 Ok(wasm_preview2)
458}