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