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