Path: blob/main/crates/test-programs/artifacts/build.rs
3068 views
use heck::*;1use std::collections::{BTreeMap, HashSet};2use std::env;3use std::fs;4use std::path::{Path, PathBuf};5use std::process::Command;6use wit_component::ComponentEncoder;78fn main() {9let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());1011Artifacts {12out_dir,13deps: HashSet::default(),14}15.build();16}1718struct Artifacts {19out_dir: PathBuf,20deps: HashSet<String>,21}2223struct Test {24/// Not all tests can be built at build-time, for example C/C++ tests require25/// the `WASI_SDK_PATH` environment variable which isn't available on all26/// machines. The `Option` here encapsulates tests that were not able to be27/// built.28///29/// For tests that were not able to be built their error is deferred to30/// test-time when the test is actually run. For C/C++ tests this means that31/// only when running debuginfo tests does the error show up, for example.32core_wasm: Option<PathBuf>,3334name: String,35}3637impl Artifacts {38fn build(&mut self) {39let mut generated_code = String::new();40// Build adapters used below for componentization.41let reactor_adapter = self.build_adapter(&mut generated_code, "reactor", &[]);42let command_adapter = self.build_adapter(43&mut generated_code,44"command",45&["--no-default-features", "--features=command"],46);47let proxy_adapter = self.build_adapter(48&mut generated_code,49"proxy",50&["--no-default-features", "--features=proxy"],51);5253// Build all test programs both in Rust and C/C++.54let mut tests = Vec::new();55self.build_rust_tests(&mut tests);56self.build_non_rust_tests(&mut tests);5758// With all our `tests` now compiled generate various macos for each59// test along with constants pointing to various paths. Note that60// components are created here as well from core modules.61let mut kinds = BTreeMap::new();62let missing_sdk_path =63PathBuf::from("Asset not compiled, WASI_SDK_PATH missing at compile time");64for test in tests.iter() {65let shouty_snake = test.name.to_shouty_snake_case();66let snake = test.name.to_snake_case();6768let core_wasm = test.core_wasm.as_deref().unwrap_or(&missing_sdk_path);69generated_code +=70&format!("pub const {shouty_snake}: &'static str = {core_wasm:?};\n",);71generated_code += &format!(72"#[macro_export] macro_rules! {snake}_bytes {{73() => {{ include_bytes!({core_wasm:?}) }}74}}",75);7677// Bucket, based on the name of the test, into a "kind" which78// generates a `foreach_*` macro below.79let kind = match test.name.as_str() {80s if s.starts_with("p1_") => "p1",81s if s.starts_with("p2_http_") => "p2_http",82s if s.starts_with("p2_cli_") => "p2_cli",83s if s.starts_with("p2_api_") => "p2_api",84s if s.starts_with("p2_") => "p2",85s if s.starts_with("nn_") => "nn",86s if s.starts_with("piped_") => "piped",87s if s.starts_with("dwarf_") => "dwarf",88s if s.starts_with("config_") => "config",89s if s.starts_with("keyvalue_") => "keyvalue",90s if s.starts_with("tls_") => "tls",91s if s.starts_with("async_") => "async",92s if s.starts_with("p3_http_") => "p3_http",93s if s.starts_with("p3_api_") => "p3_api",94s if s.starts_with("p3_") => "p3",95s if s.starts_with("fuzz_") => "fuzz",96// If you're reading this because you hit this panic, either add97// it to a test suite above or add a new "suite". The purpose of98// the categorization above is to have a static assertion that99// tests added are actually run somewhere, so as long as you're100// also adding test code somewhere that's ok.101other => {102panic!("don't know how to classify test name `{other}` to a kind")103}104};105if !kind.is_empty() {106kinds.entry(kind).or_insert(Vec::new()).push(&test.name);107}108109// Generate a component from each test.110if test.name == "dwarf_imported_memory"111|| test.name == "dwarf_shared_memory"112|| test.name.starts_with("nn_witx")113{114continue;115}116let adapter = match test.name.as_str() {117"reactor" => &reactor_adapter,118s if s.starts_with("p3_") => &reactor_adapter,119s if s.starts_with("p2_api_proxy") => &proxy_adapter,120_ => &command_adapter,121};122let path = match &test.core_wasm {123Some(path) => self.compile_component(path, adapter),124None => missing_sdk_path.clone(),125};126generated_code +=127&format!("pub const {shouty_snake}_COMPONENT: &'static str = {path:?};\n");128generated_code += &format!(129"#[macro_export] macro_rules! {snake}_component_bytes {{130() => {{ include_bytes!({path:?}) }}131}}",132);133}134135for (kind, targets) in kinds {136generated_code += &format!("#[macro_export]");137generated_code += &format!("macro_rules! foreach_{kind} {{\n");138generated_code += &format!(" ($mac:ident) => {{\n");139for target in targets {140generated_code += &format!("$mac!({target});\n")141}142generated_code += &format!(" }}\n");143generated_code += &format!("}}\n");144}145146std::fs::write(self.out_dir.join("gen.rs"), generated_code).unwrap();147}148149fn build_rust_tests(&mut self, tests: &mut Vec<Test>) {150println!("cargo:rerun-if-env-changed=MIRI_TEST_CWASM_DIR");151let release_mode = env::var_os("MIRI_TEST_CWASM_DIR").is_some();152153let mut cmd = cargo();154cmd.arg("build");155if release_mode {156cmd.arg("--release");157}158cmd.arg("--target=wasm32-wasip1")159.arg("--package=test-programs")160.env("CARGO_TARGET_DIR", &self.out_dir)161.env("CARGO_PROFILE_DEV_DEBUG", "2")162.env("RUSTFLAGS", rustflags())163.env_remove("CARGO_ENCODED_RUSTFLAGS");164eprintln!("running: {cmd:?}");165let status = cmd.status().unwrap();166assert!(status.success());167168let meta = cargo_metadata::MetadataCommand::new().exec().unwrap();169let targets = meta170.packages171.iter()172.find(|p| p.name == "test-programs")173.unwrap()174.targets175.iter()176.filter(move |t| t.kind == &[cargo_metadata::TargetKind::Bin])177.map(|t| &t.name)178.collect::<Vec<_>>();179180for target in targets {181let wasm = self182.out_dir183.join("wasm32-wasip1")184.join(if release_mode { "release" } else { "debug" })185.join(format!("{target}.wasm"));186self.read_deps_of(&wasm);187tests.push(Test {188core_wasm: Some(wasm),189name: target.to_string(),190})191}192}193194// Build the WASI Preview 1 adapter, and get the binary:195fn build_adapter(196&mut self,197generated_code: &mut String,198name: &str,199features: &[&str],200) -> Vec<u8> {201let mut cmd = cargo();202cmd.arg("build")203.arg("--release")204.arg("--package=wasi-preview1-component-adapter")205.arg("--target=wasm32-unknown-unknown")206.env("CARGO_TARGET_DIR", &self.out_dir)207.env("RUSTFLAGS", rustflags())208.env_remove("CARGO_ENCODED_RUSTFLAGS");209for f in features {210cmd.arg(f);211}212eprintln!("running: {cmd:?}");213let status = cmd.status().unwrap();214assert!(status.success());215216let artifact = self217.out_dir218.join("wasm32-unknown-unknown")219.join("release")220.join("wasi_snapshot_preview1.wasm");221let adapter = self222.out_dir223.join(format!("wasi_snapshot_preview1.{name}.wasm"));224std::fs::copy(&artifact, &adapter).unwrap();225self.read_deps_of(&artifact);226println!("wasi {name} adapter: {:?}", &adapter);227generated_code.push_str(&format!(228"pub const ADAPTER_{}: &'static str = {adapter:?};\n",229name.to_shouty_snake_case(),230));231fs::read(&adapter).unwrap()232}233234// Compile a component, return the path of the binary:235fn compile_component(&self, wasm: &Path, adapter: &[u8]) -> PathBuf {236println!("creating a component from {wasm:?}");237let module = fs::read(wasm).expect("read wasm module");238let component = ComponentEncoder::default()239.module(module.as_slice())240.unwrap()241.validate(true)242.adapter("wasi_snapshot_preview1", adapter)243.unwrap()244.encode()245.expect("module can be translated to a component");246let out_dir = wasm.parent().unwrap();247let stem = wasm.file_stem().unwrap().to_str().unwrap();248let component_path = out_dir.join(format!("{stem}.component.wasm"));249fs::write(&component_path, component).expect("write component to disk");250component_path251}252253fn build_non_rust_tests(&mut self, tests: &mut Vec<Test>) {254const ASSETS_REL_SRC_DIR: &'static str = "../src/bin";255println!("cargo:rerun-if-changed={ASSETS_REL_SRC_DIR}");256257for entry in fs::read_dir(ASSETS_REL_SRC_DIR).unwrap() {258let entry = entry.unwrap();259let path = entry.path();260let name = path.file_stem().unwrap().to_str().unwrap().to_owned();261match path.extension().and_then(|s| s.to_str()) {262// Compile C/C++ tests with clang263Some("c") | Some("cc") => self.build_c_or_cpp_test(path, name, tests),264265// just a header, part of another test.266Some("h") => {}267268// Convert the text format to binary and use it as a test.269Some("wat") => {270let wasm = wat::parse_file(&path).unwrap();271let core_wasm = self.out_dir.join(&name).with_extension("wasm");272fs::write(&core_wasm, &wasm).unwrap();273tests.push(Test {274name,275core_wasm: Some(core_wasm),276});277}278279// these are built above in `build_rust_tests`280Some("rs") => {}281282// Prevent stray files for now that we don't understand.283Some(_) => panic!("unknown file extension on {path:?}"),284285None => unreachable!(),286}287}288}289290fn build_c_or_cpp_test(&mut self, path: PathBuf, name: String, tests: &mut Vec<Test>) {291println!("compiling {path:?}");292println!("cargo:rerun-if-changed={}", path.display());293let contents = std::fs::read_to_string(&path).unwrap();294let config =295wasmtime_test_util::wast::parse_test_config::<CTestConfig>(&contents, "//!").unwrap();296297if config.skip {298return;299}300301// The debug tests relying on these assets are ignored by default,302// so we cannot force the requirement of having a working WASI SDK303// install on everyone. At the same time, those tests (due to their304// monolithic nature), are always compiled, so we still have to305// produce the path constants. To solve this, we move the failure306// of missing WASI SDK from compile time to runtime by producing307// fake paths (that themselves will serve as diagnostic messages).308let wasi_sdk_path = match env::var_os("WASI_SDK_PATH") {309Some(path) => PathBuf::from(path),310None => {311tests.push(Test {312name,313core_wasm: None,314});315return;316}317};318319let wasm_path = self.out_dir.join(&name).with_extension("wasm");320321let mut cmd = Command::new(wasi_sdk_path.join("bin/wasm32-wasip1-clang"));322cmd.arg(&path);323for file in config.extra_files.iter() {324cmd.arg(path.parent().unwrap().join(file));325}326cmd.arg("-g");327cmd.args(&config.flags);328cmd.arg("-o");329cmd.arg(&wasm_path);330// If optimizations are enabled, clang will look for wasm-opt in PATH331// and run it. This will strip DWARF debug info, which we don't want.332cmd.env("PATH", "");333println!("running: {cmd:?}");334let result = cmd.status().expect("failed to spawn clang");335assert!(result.success());336337if config.dwp {338let mut dwp = Command::new(wasi_sdk_path.join("bin/llvm-dwp"));339dwp.arg("-e")340.arg(&wasm_path)341.arg("-o")342.arg(self.out_dir.join(&name).with_extension("dwp"));343assert!(dwp.status().expect("failed to spawn llvm-dwp").success());344}345346tests.push(Test {347name,348core_wasm: Some(wasm_path),349});350}351352/// Helper function to read the `*.d` file that corresponds to `artifact`, an353/// artifact of a Cargo compilation.354///355/// This function will "parse" the makefile-based dep-info format to learn about356/// what files each binary depended on to ensure that this build script reruns357/// if any of these files change.358///359/// See360/// <https://doc.rust-lang.org/nightly/cargo/reference/build-cache.html#dep-info-files>361/// for more info.362fn read_deps_of(&mut self, artifact: &Path) {363let deps_file = artifact.with_extension("d");364let contents = std::fs::read_to_string(&deps_file).expect("failed to read deps file");365for line in contents.lines() {366let Some(pos) = line.find(": ") else {367continue;368};369let line = &line[pos + 2..];370let mut parts = line.split_whitespace();371while let Some(part) = parts.next() {372let mut file = part.to_string();373while file.ends_with('\\') {374file.pop();375file.push(' ');376file.push_str(parts.next().unwrap());377}378if !self.deps.contains(&file) {379println!("cargo:rerun-if-changed={file}");380self.deps.insert(file);381}382}383}384}385}386387#[derive(serde_derive::Deserialize)]388#[serde(deny_unknown_fields, rename_all = "kebab-case")]389struct CTestConfig {390#[serde(default)]391flags: Vec<String>,392#[serde(default)]393extra_files: Vec<String>,394#[serde(default)]395dwp: bool,396#[serde(default)]397skip: bool,398}399400fn cargo() -> Command {401// Miri configures its own sysroot which we don't want to use, so remove402// miri's own wrappers around rustc to ensure that we're using the real403// rustc to build these programs.404let mut cargo = Command::new("cargo");405if std::env::var("CARGO_CFG_MIRI").is_ok() {406cargo.env_remove("RUSTC").env_remove("RUSTC_WRAPPER");407}408cargo409}410411fn rustflags() -> &'static str {412match option_env!("RUSTFLAGS") {413// If we're in CI which is denying warnings then deny warnings to code414// built here too to keep the tree warning-free.415Some(s) if s.contains("-D warnings") => "-D warnings",416_ => "",417}418}419420421