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