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