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