1 use heck::*; 2 use std::collections::BTreeMap; 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 build_and_generate_tests(); 11 } 12 13 fn build_and_generate_tests() { 14 let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); 15 16 let reactor_adapter = build_adapter(&out_dir, "reactor", &[]); 17 let command_adapter = build_adapter( 18 &out_dir, 19 "command", 20 &["--no-default-features", "--features=command"], 21 ); 22 let proxy_adapter = build_adapter( 23 &out_dir, 24 "proxy", 25 &["--no-default-features", "--features=proxy"], 26 ); 27 28 println!("cargo:rerun-if-changed=../src"); 29 30 // Build the test programs: 31 let mut cmd = cargo(); 32 cmd.arg("build") 33 .arg("--target=wasm32-wasi") 34 .arg("--package=test-programs") 35 .env("CARGO_TARGET_DIR", &out_dir) 36 .env("CARGO_PROFILE_DEV_DEBUG", "1") 37 .env("RUSTFLAGS", rustflags()) 38 .env_remove("CARGO_ENCODED_RUSTFLAGS"); 39 eprintln!("running: {cmd:?}"); 40 let status = cmd.status().unwrap(); 41 assert!(status.success()); 42 43 let meta = cargo_metadata::MetadataCommand::new().exec().unwrap(); 44 let targets = meta 45 .packages 46 .iter() 47 .find(|p| p.name == "test-programs") 48 .unwrap() 49 .targets 50 .iter() 51 .filter(move |t| t.kind == &["bin"]) 52 .map(|t| &t.name) 53 .collect::<Vec<_>>(); 54 55 let mut generated_code = String::new(); 56 57 let mut kinds = BTreeMap::new(); 58 59 for target in targets { 60 let camel = target.to_shouty_snake_case(); 61 let wasm = out_dir 62 .join("wasm32-wasi") 63 .join("debug") 64 .join(format!("{target}.wasm")); 65 66 generated_code += &format!("pub const {camel}: &'static str = {wasm:?};\n"); 67 68 // Bucket, based on the name of the test, into a "kind" which generates 69 // a `foreach_*` macro below. 70 let kind = match target.as_str() { 71 s if s.starts_with("http_") => "http", 72 s if s.starts_with("preview1_") => "preview1", 73 s if s.starts_with("preview2_") => "preview2", 74 s if s.starts_with("cli_") => "cli", 75 s if s.starts_with("api_") => "api", 76 s if s.starts_with("nn_") => "nn", 77 s if s.starts_with("piped_") => "piped", 78 // If you're reading this because you hit this panic, either add it 79 // to a test suite above or add a new "suite". The purpose of the 80 // categorization above is to have a static assertion that tests 81 // added are actually run somewhere, so as long as you're also 82 // adding test code somewhere that's ok. 83 other => { 84 panic!("don't know how to classify test name `{other}` to a kind") 85 } 86 }; 87 if !kind.is_empty() { 88 kinds.entry(kind).or_insert(Vec::new()).push(target); 89 } 90 91 // Generate a component from each test. 92 if kind == "nn" { 93 continue; 94 } 95 let adapter = match target.as_str() { 96 "reactor" => &reactor_adapter, 97 s if s.starts_with("api_proxy") => &proxy_adapter, 98 _ => &command_adapter, 99 }; 100 let path = compile_component(&wasm, adapter); 101 generated_code += &format!("pub const {camel}_COMPONENT: &'static str = {path:?};\n"); 102 } 103 104 for (kind, targets) in kinds { 105 generated_code += &format!("#[macro_export]"); 106 generated_code += &format!("macro_rules! foreach_{kind} {{\n"); 107 generated_code += &format!(" ($mac:ident) => {{\n"); 108 for target in targets { 109 generated_code += &format!("$mac!({target});\n") 110 } 111 generated_code += &format!(" }}\n"); 112 generated_code += &format!("}}\n"); 113 } 114 115 std::fs::write(out_dir.join("gen.rs"), generated_code).unwrap(); 116 } 117 118 // Build the WASI Preview 1 adapter, and get the binary: 119 fn build_adapter(out_dir: &PathBuf, name: &str, features: &[&str]) -> Vec<u8> { 120 println!("cargo:rerun-if-changed=../../wasi-preview1-component-adapter"); 121 let mut cmd = cargo(); 122 cmd.arg("build") 123 .arg("--release") 124 .arg("--package=wasi-preview1-component-adapter") 125 .arg("--target=wasm32-unknown-unknown") 126 .env("CARGO_TARGET_DIR", out_dir) 127 .env("RUSTFLAGS", rustflags()) 128 .env_remove("CARGO_ENCODED_RUSTFLAGS"); 129 for f in features { 130 cmd.arg(f); 131 } 132 eprintln!("running: {cmd:?}"); 133 let status = cmd.status().unwrap(); 134 assert!(status.success()); 135 136 let adapter = out_dir.join(format!("wasi_snapshot_preview1.{name}.wasm")); 137 std::fs::copy( 138 out_dir 139 .join("wasm32-unknown-unknown") 140 .join("release") 141 .join("wasi_snapshot_preview1.wasm"), 142 &adapter, 143 ) 144 .unwrap(); 145 println!("wasi {name} adapter: {:?}", &adapter); 146 fs::read(&adapter).unwrap() 147 } 148 149 fn rustflags() -> &'static str { 150 match option_env!("RUSTFLAGS") { 151 // If we're in CI which is denying warnings then deny warnings to code 152 // built here too to keep the tree warning-free. 153 Some(s) if s.contains("-D warnings") => "-D warnings", 154 _ => "", 155 } 156 } 157 158 // Compile a component, return the path of the binary: 159 fn compile_component(wasm: &Path, adapter: &[u8]) -> PathBuf { 160 println!("creating a component from {wasm:?}"); 161 let module = fs::read(wasm).expect("read wasm module"); 162 let component = ComponentEncoder::default() 163 .module(module.as_slice()) 164 .unwrap() 165 .validate(true) 166 .adapter("wasi_snapshot_preview1", adapter) 167 .unwrap() 168 .encode() 169 .expect("module can be translated to a component"); 170 let out_dir = wasm.parent().unwrap(); 171 let stem = wasm.file_stem().unwrap().to_str().unwrap(); 172 let component_path = out_dir.join(format!("{stem}.component.wasm")); 173 fs::write(&component_path, component).expect("write component to disk"); 174 component_path 175 } 176 177 fn cargo() -> Command { 178 // Miri configures its own sysroot which we don't want to use, so remove 179 // miri's own wrappers around rustc to ensure that we're using the real 180 // rustc to build these programs. 181 let mut cargo = Command::new("cargo"); 182 if std::env::var("CARGO_CFG_MIRI").is_ok() { 183 cargo.env_remove("RUSTC").env_remove("RUSTC_WRAPPER"); 184 } 185 cargo 186 } 187