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