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