1 use heck::*;
2 use std::collections::{BTreeMap, HashSet};
3 use std::env;
4 use std::fs;
5 use std::path::{Path, PathBuf};
6 use std::process::Command;
7 use wit_component::ComponentEncoder;
8
main()9 fn main() {
10 let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
11
12 Artifacts {
13 out_dir,
14 deps: HashSet::default(),
15 }
16 .build();
17 }
18
19 struct Artifacts {
20 out_dir: PathBuf,
21 deps: HashSet<String>,
22 }
23
24 struct Test {
25 /// Not all tests can be built at build-time, for example C/C++ tests require
26 /// the `WASI_SDK_PATH` environment variable which isn't available on all
27 /// machines. The `Option` here encapsulates tests that were not able to be
28 /// built.
29 ///
30 /// For tests that were not able to be built their error is deferred to
31 /// test-time when the test is actually run. For C/C++ tests this means that
32 /// only when running debuginfo tests does the error show up, for example.
33 core_wasm: Option<PathBuf>,
34
35 name: String,
36 }
37
38 impl Artifacts {
build(&mut self)39 fn build(&mut self) {
40 let mut generated_code = String::new();
41 // Build adapters used below for componentization.
42 let reactor_adapter = self.build_adapter(&mut generated_code, "reactor", &[]);
43 let command_adapter = self.build_adapter(
44 &mut generated_code,
45 "command",
46 &["--no-default-features", "--features=command"],
47 );
48 let proxy_adapter = self.build_adapter(
49 &mut generated_code,
50 "proxy",
51 &["--no-default-features", "--features=proxy"],
52 );
53
54 // Build all test programs both in Rust and C/C++.
55 let mut tests = Vec::new();
56 self.build_rust_tests(&mut tests);
57 self.build_non_rust_tests(&mut tests);
58
59 // With all our `tests` now compiled generate various macos for each
60 // test along with constants pointing to various paths. Note that
61 // components are created here as well from core modules.
62 let mut kinds = BTreeMap::new();
63 let missing_sdk_path =
64 PathBuf::from("Asset not compiled, WASI_SDK_PATH missing at compile time");
65 for test in tests.iter() {
66 let shouty_snake = test.name.to_shouty_snake_case();
67 let snake = test.name.to_snake_case();
68
69 let core_wasm = test.core_wasm.as_deref().unwrap_or(&missing_sdk_path);
70 generated_code +=
71 &format!("pub const {shouty_snake}: &'static str = {core_wasm:?};\n",);
72 generated_code += &format!(
73 "#[macro_export] macro_rules! {snake}_bytes {{
74 () => {{ include_bytes!({core_wasm:?}) }}
75 }}",
76 );
77
78 // Bucket, based on the name of the test, into a "kind" which
79 // generates a `foreach_*` macro below.
80 let kind = match test.name.as_str() {
81 s if s.starts_with("p1_cli_")
82 || s.starts_with("p2_cli_")
83 || s.starts_with("p3_cli_") =>
84 {
85 "cli"
86 }
87 s if s.starts_with("p1_") => "p1",
88 s if s.starts_with("p2_http_") => "p2_http",
89 s if s.starts_with("p2_api_") => "p2_api",
90 s if s.starts_with("p2_tls_") => "p2_tls",
91 s if s.starts_with("p2_") => "p2",
92 s if s.starts_with("p3_http_") => "p3_http",
93 s if s.starts_with("p3_api_") => "p3_api",
94 s if s.starts_with("p3_tls_") => "p3_tls",
95 s if s.starts_with("p3_") => "p3",
96 s if s.starts_with("nn_") => "nn",
97 s if s.starts_with("piped_") => "piped",
98 s if s.starts_with("debugger_") => "debugger",
99 s if s.starts_with("guest_debug_") => "guest_debug",
100 s if s.starts_with("dwarf_") => "dwarf",
101 s if s.starts_with("config_") => "config",
102 s if s.starts_with("keyvalue_") => "keyvalue",
103 s if s.starts_with("async_") => "async",
104 s if s.starts_with("fuzz_") => "fuzz",
105 // If you're reading this because you hit this panic, either add
106 // it to a test suite above or add a new "suite". The purpose of
107 // the categorization above is to have a static assertion that
108 // tests added are actually run somewhere, so as long as you're
109 // also adding test code somewhere that's ok.
110 other => {
111 panic!("don't know how to classify test name `{other}` to a kind")
112 }
113 };
114 if !kind.is_empty() {
115 kinds.entry(kind).or_insert(Vec::new()).push(&test.name);
116 }
117
118 // Generate a component from each test.
119 if test.name == "dwarf_imported_memory"
120 || test.name == "dwarf_shared_memory"
121 || test.name.starts_with("nn_witx")
122 {
123 continue;
124 }
125 let adapter = match test.name.as_str() {
126 "reactor" => &reactor_adapter,
127 s if s.starts_with("p3_") => &reactor_adapter,
128 s if s.starts_with("p2_api_proxy") => &proxy_adapter,
129 _ => &command_adapter,
130 };
131 let path = match &test.core_wasm {
132 Some(path) => self.compile_component(path, adapter),
133 None => missing_sdk_path.clone(),
134 };
135 generated_code +=
136 &format!("pub const {shouty_snake}_COMPONENT: &'static str = {path:?};\n");
137 generated_code += &format!(
138 "#[macro_export] macro_rules! {snake}_component_bytes {{
139 () => {{ include_bytes!({path:?}) }}
140 }}",
141 );
142 }
143
144 for (kind, targets) in kinds {
145 generated_code += &format!("#[macro_export]");
146 generated_code += &format!("macro_rules! foreach_{kind} {{\n");
147 generated_code += &format!(" ($mac:ident) => {{\n");
148 for target in targets {
149 generated_code += &format!("$mac!({target});\n")
150 }
151 generated_code += &format!(" }}\n");
152 generated_code += &format!("}}\n");
153 }
154
155 std::fs::write(self.out_dir.join("gen.rs"), generated_code).unwrap();
156 }
157
build_rust_tests(&mut self, tests: &mut Vec<Test>)158 fn build_rust_tests(&mut self, tests: &mut Vec<Test>) {
159 println!("cargo:rerun-if-env-changed=MIRI_TEST_CWASM_DIR");
160 let release_mode = env::var_os("MIRI_TEST_CWASM_DIR").is_some();
161
162 let mut cmd = cargo();
163 cmd.arg("build");
164 if release_mode {
165 cmd.arg("--release");
166 }
167 cmd.arg("--target=wasm32-wasip1")
168 .arg("--package=test-programs")
169 .env("CARGO_TARGET_DIR", &self.out_dir)
170 .env("CARGO_PROFILE_DEV_DEBUG", "2")
171 .env("RUSTFLAGS", rustflags())
172 .env_remove("CARGO_ENCODED_RUSTFLAGS");
173 eprintln!("running: {cmd:?}");
174 let status = cmd.status().unwrap();
175 assert!(status.success());
176
177 let meta = cargo_metadata::MetadataCommand::new().exec().unwrap();
178 let targets = meta
179 .packages
180 .iter()
181 .find(|p| p.name == "test-programs")
182 .unwrap()
183 .targets
184 .iter()
185 .filter(move |t| t.kind == &[cargo_metadata::TargetKind::Bin])
186 .map(|t| &t.name)
187 .collect::<Vec<_>>();
188
189 for target in targets {
190 let wasm = self
191 .out_dir
192 .join("wasm32-wasip1")
193 .join(if release_mode { "release" } else { "debug" })
194 .join(format!("{target}.wasm"));
195 self.read_deps_of(&wasm);
196 tests.push(Test {
197 core_wasm: Some(wasm),
198 name: target.to_string(),
199 })
200 }
201 }
202
203 // Build the WASI Preview 1 adapter, and get the binary:
build_adapter( &mut self, generated_code: &mut String, name: &str, features: &[&str], ) -> Vec<u8>204 fn build_adapter(
205 &mut self,
206 generated_code: &mut String,
207 name: &str,
208 features: &[&str],
209 ) -> Vec<u8> {
210 let mut cmd = cargo();
211 cmd.arg("build")
212 .arg("--release")
213 .arg("--package=wasi-preview1-component-adapter")
214 .arg("--target=wasm32-unknown-unknown")
215 .env("CARGO_TARGET_DIR", &self.out_dir)
216 .env("RUSTFLAGS", rustflags())
217 .env_remove("CARGO_ENCODED_RUSTFLAGS");
218 for f in features {
219 cmd.arg(f);
220 }
221 eprintln!("running: {cmd:?}");
222 let status = cmd.status().unwrap();
223 assert!(status.success());
224
225 let artifact = self
226 .out_dir
227 .join("wasm32-unknown-unknown")
228 .join("release")
229 .join("wasi_snapshot_preview1.wasm");
230 let adapter = self
231 .out_dir
232 .join(format!("wasi_snapshot_preview1.{name}.wasm"));
233 std::fs::copy(&artifact, &adapter).unwrap();
234 self.read_deps_of(&artifact);
235 println!("wasi {name} adapter: {:?}", &adapter);
236 generated_code.push_str(&format!(
237 "pub const ADAPTER_{}: &'static str = {adapter:?};\n",
238 name.to_shouty_snake_case(),
239 ));
240 fs::read(&adapter).unwrap()
241 }
242
243 // Compile a component, return the path of the binary:
compile_component(&self, wasm: &Path, adapter: &[u8]) -> PathBuf244 fn compile_component(&self, wasm: &Path, adapter: &[u8]) -> PathBuf {
245 println!("creating a component from {wasm:?}");
246 let module = fs::read(wasm).expect("read wasm module");
247 let component = ComponentEncoder::default()
248 .module(module.as_slice())
249 .unwrap()
250 .validate(true)
251 .adapter("wasi_snapshot_preview1", adapter)
252 .unwrap()
253 .encode()
254 .expect("module can be translated to a component");
255 let out_dir = wasm.parent().unwrap();
256 let stem = wasm.file_stem().unwrap().to_str().unwrap();
257 let component_path = out_dir.join(format!("{stem}.component.wasm"));
258 fs::write(&component_path, component).expect("write component to disk");
259 component_path
260 }
261
build_non_rust_tests(&mut self, tests: &mut Vec<Test>)262 fn build_non_rust_tests(&mut self, tests: &mut Vec<Test>) {
263 const ASSETS_REL_SRC_DIR: &'static str = "../src/bin";
264 println!("cargo:rerun-if-changed={ASSETS_REL_SRC_DIR}");
265
266 for entry in fs::read_dir(ASSETS_REL_SRC_DIR).unwrap() {
267 let entry = entry.unwrap();
268 let path = entry.path();
269 let name = path.file_stem().unwrap().to_str().unwrap().to_owned();
270 match path.extension().and_then(|s| s.to_str()) {
271 // Compile C/C++ tests with clang
272 Some("c") | Some("cc") => self.build_c_or_cpp_test(path, name, tests),
273
274 // just a header, part of another test.
275 Some("h") => {}
276
277 // Convert the text format to binary and use it as a test.
278 Some("wat") => {
279 let wasm = wat::parse_file(&path).unwrap();
280 let core_wasm = self.out_dir.join(&name).with_extension("wasm");
281 fs::write(&core_wasm, &wasm).unwrap();
282 tests.push(Test {
283 name,
284 core_wasm: Some(core_wasm),
285 });
286 }
287
288 // these are built above in `build_rust_tests`
289 Some("rs") => {}
290
291 // Prevent stray files for now that we don't understand.
292 Some(_) => panic!("unknown file extension on {path:?}"),
293
294 None => unreachable!("no extension in path {path:?}"),
295 }
296 }
297 }
298
build_c_or_cpp_test(&mut self, path: PathBuf, name: String, tests: &mut Vec<Test>)299 fn build_c_or_cpp_test(&mut self, path: PathBuf, name: String, tests: &mut Vec<Test>) {
300 println!("compiling {path:?}");
301 println!("cargo:rerun-if-changed={}", path.display());
302 let contents = std::fs::read_to_string(&path).unwrap();
303 let config =
304 wasmtime_test_util::wast::parse_test_config::<CTestConfig>(&contents, "//!").unwrap();
305
306 if config.skip {
307 return;
308 }
309
310 // The debug tests relying on these assets are ignored by default,
311 // so we cannot force the requirement of having a working WASI SDK
312 // install on everyone. At the same time, those tests (due to their
313 // monolithic nature), are always compiled, so we still have to
314 // produce the path constants. To solve this, we move the failure
315 // of missing WASI SDK from compile time to runtime by producing
316 // fake paths (that themselves will serve as diagnostic messages).
317 let wasi_sdk_path = match env::var_os("WASI_SDK_PATH") {
318 Some(path) => PathBuf::from(path),
319 None => {
320 tests.push(Test {
321 name,
322 core_wasm: None,
323 });
324 return;
325 }
326 };
327
328 let wasm_path = self.out_dir.join(&name).with_extension("wasm");
329
330 let mut cmd = Command::new(wasi_sdk_path.join("bin/wasm32-wasip1-clang"));
331 cmd.arg(&path);
332 for file in config.extra_files.iter() {
333 cmd.arg(path.parent().unwrap().join(file));
334 }
335 cmd.arg("-g");
336 cmd.args(&config.flags);
337 cmd.arg("-o");
338 cmd.arg(&wasm_path);
339 // If optimizations are enabled, clang will look for wasm-opt in PATH
340 // and run it. This will strip DWARF debug info, which we don't want.
341 cmd.env("PATH", "");
342 println!("running: {cmd:?}");
343 let result = cmd.status().expect("failed to spawn clang");
344 assert!(result.success());
345
346 if config.dwp {
347 let mut dwp = Command::new(wasi_sdk_path.join("bin/llvm-dwp"));
348 dwp.arg("-e")
349 .arg(&wasm_path)
350 .arg("-o")
351 .arg(self.out_dir.join(&name).with_extension("dwp"));
352 assert!(dwp.status().expect("failed to spawn llvm-dwp").success());
353 }
354
355 tests.push(Test {
356 name,
357 core_wasm: Some(wasm_path),
358 });
359 }
360
361 /// Helper function to read the `*.d` file that corresponds to `artifact`, an
362 /// artifact of a Cargo compilation.
363 ///
364 /// This function will "parse" the makefile-based dep-info format to learn about
365 /// what files each binary depended on to ensure that this build script reruns
366 /// if any of these files change.
367 ///
368 /// See
369 /// <https://doc.rust-lang.org/nightly/cargo/reference/build-cache.html#dep-info-files>
370 /// for more info.
read_deps_of(&mut self, artifact: &Path)371 fn read_deps_of(&mut self, artifact: &Path) {
372 let deps_file = artifact.with_extension("d");
373 let contents = std::fs::read_to_string(&deps_file).expect("failed to read deps file");
374 for line in contents.lines() {
375 let Some(pos) = line.find(": ") else {
376 continue;
377 };
378 let line = &line[pos + 2..];
379 let mut parts = line.split_whitespace();
380 while let Some(part) = parts.next() {
381 let mut file = part.to_string();
382 while file.ends_with('\\') {
383 file.pop();
384 file.push(' ');
385 file.push_str(parts.next().unwrap());
386 }
387 if !self.deps.contains(&file) {
388 println!("cargo:rerun-if-changed={file}");
389 self.deps.insert(file);
390 }
391 }
392 }
393 }
394 }
395
396 #[derive(serde_derive::Deserialize)]
397 #[serde(deny_unknown_fields, rename_all = "kebab-case")]
398 struct CTestConfig {
399 #[serde(default)]
400 flags: Vec<String>,
401 #[serde(default)]
402 extra_files: Vec<String>,
403 #[serde(default)]
404 dwp: bool,
405 #[serde(default)]
406 skip: bool,
407 }
408
cargo() -> Command409 fn cargo() -> Command {
410 // Miri configures its own sysroot which we don't want to use, so remove
411 // miri's own wrappers around rustc to ensure that we're using the real
412 // rustc to build these programs.
413 let mut cargo = Command::new("cargo");
414 if std::env::var("CARGO_CFG_MIRI").is_ok() {
415 cargo.env_remove("RUSTC").env_remove("RUSTC_WRAPPER");
416 }
417 cargo
418 }
419
rustflags() -> &'static str420 fn rustflags() -> &'static str {
421 match option_env!("RUSTFLAGS") {
422 // If we're in CI which is denying warnings then deny warnings to code
423 // built here too to keep the tree warning-free.
424 Some(s) if s.contains("-D warnings") => "-D warnings",
425 _ => "",
426 }
427 }
428