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-wasip1")
34         .arg("--package=test-programs")
35         .env("CARGO_TARGET_DIR", &out_dir)
36         .env("CARGO_PROFILE_DEV_DEBUG", "2")
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-wasip1")
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             s if s.starts_with("dwarf_") => "dwarf",
79             s if s.starts_with("config_") => "config",
80             s if s.starts_with("keyvalue_") => "keyvalue",
81             // If you're reading this because you hit this panic, either add it
82             // to a test suite above or add a new "suite". The purpose of the
83             // categorization above is to have a static assertion that tests
84             // added are actually run somewhere, so as long as you're also
85             // 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(target);
92         }
93 
94         // Generate a component from each test.
95         if target == "dwarf_imported_memory"
96             || target == "dwarf_shared_memory"
97             || target.starts_with("nn_witx")
98         {
99             continue;
100         }
101         let adapter = match target.as_str() {
102             "reactor" => &reactor_adapter,
103             s if s.starts_with("api_proxy") => &proxy_adapter,
104             _ => &command_adapter,
105         };
106         let path = compile_component(&wasm, adapter);
107         generated_code += &format!("pub const {camel}_COMPONENT: &'static str = {path:?};\n");
108     }
109 
110     for (kind, targets) in kinds {
111         generated_code += &format!("#[macro_export]");
112         generated_code += &format!("macro_rules! foreach_{kind} {{\n");
113         generated_code += &format!("    ($mac:ident) => {{\n");
114         for target in targets {
115             generated_code += &format!("$mac!({target});\n")
116         }
117         generated_code += &format!("    }}\n");
118         generated_code += &format!("}}\n");
119     }
120 
121     std::fs::write(out_dir.join("gen.rs"), generated_code).unwrap();
122 }
123 
124 // Build the WASI Preview 1 adapter, and get the binary:
125 fn build_adapter(out_dir: &PathBuf, name: &str, features: &[&str]) -> Vec<u8> {
126     println!("cargo:rerun-if-changed=../../wasi-preview1-component-adapter");
127     let mut cmd = cargo();
128     cmd.arg("build")
129         .arg("--release")
130         .arg("--package=wasi-preview1-component-adapter")
131         .arg("--target=wasm32-unknown-unknown")
132         .env("CARGO_TARGET_DIR", out_dir)
133         .env("RUSTFLAGS", rustflags())
134         .env_remove("CARGO_ENCODED_RUSTFLAGS");
135     for f in features {
136         cmd.arg(f);
137     }
138     eprintln!("running: {cmd:?}");
139     let status = cmd.status().unwrap();
140     assert!(status.success());
141 
142     let adapter = out_dir.join(format!("wasi_snapshot_preview1.{name}.wasm"));
143     std::fs::copy(
144         out_dir
145             .join("wasm32-unknown-unknown")
146             .join("release")
147             .join("wasi_snapshot_preview1.wasm"),
148         &adapter,
149     )
150     .unwrap();
151     println!("wasi {name} adapter: {:?}", &adapter);
152     fs::read(&adapter).unwrap()
153 }
154 
155 fn rustflags() -> &'static str {
156     match option_env!("RUSTFLAGS") {
157         // If we're in CI which is denying warnings then deny warnings to code
158         // built here too to keep the tree warning-free.
159         Some(s) if s.contains("-D warnings") => "-D warnings",
160         _ => "",
161     }
162 }
163 
164 // Compile a component, return the path of the binary:
165 fn compile_component(wasm: &Path, adapter: &[u8]) -> PathBuf {
166     println!("creating a component from {wasm:?}");
167     let module = fs::read(wasm).expect("read wasm module");
168     let component = ComponentEncoder::default()
169         .module(module.as_slice())
170         .unwrap()
171         .validate(true)
172         .adapter("wasi_snapshot_preview1", adapter)
173         .unwrap()
174         .encode()
175         .expect("module can be translated to a component");
176     let out_dir = wasm.parent().unwrap();
177     let stem = wasm.file_stem().unwrap().to_str().unwrap();
178     let component_path = out_dir.join(format!("{stem}.component.wasm"));
179     fs::write(&component_path, component).expect("write component to disk");
180     component_path
181 }
182 
183 fn cargo() -> Command {
184     // Miri configures its own sysroot which we don't want to use, so remove
185     // miri's own wrappers around rustc to ensure that we're using the real
186     // rustc to build these programs.
187     let mut cargo = Command::new("cargo");
188     if std::env::var("CARGO_CFG_MIRI").is_ok() {
189         cargo.env_remove("RUSTC").env_remove("RUSTC_WRAPPER");
190     }
191     cargo
192 }
193