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