commit 026dbe08a91c97f1a555d0507f3c43e10768eb91
parent 5ba3d4d817e8fd17e20b791dcaae9a6869d483e9
Author: triesap <tyson@radroots.org>
Date: Mon, 6 Jul 2026 05:01:56 +0000
xtask: make WASM smoke deterministic
- resolve WASM generation through rust-toolchain tool paths
- preflight target-bound C compiler for WASM packages
- run knowledge smoke with resolved Cargo and exact dependency pins
- cover toolchain, compiler, and smoke determinism in tests
Diffstat:
2 files changed, 535 insertions(+), 63 deletions(-)
diff --git a/tools/xtask/src/smoke.rs b/tools/xtask/src/smoke.rs
@@ -1,6 +1,9 @@
use std::{fs, path::Path, process::Command};
-use crate::fs::workspace_root;
+use crate::{
+ fs::workspace_root,
+ wasm::{ResolvedRustToolchain, resolve_rust_toolchain},
+};
pub fn run(args: &[String]) -> Result<(), String> {
match args {
@@ -12,18 +15,22 @@ pub fn run(args: &[String]) -> Result<(), String> {
fn knowledge_rust_local() -> Result<(), String> {
let root = workspace_root()?;
let sdk_path = root.join("crates/sdk");
+ let toolchain = resolve_rust_toolchain(&root)?;
+ let nostr_version = exact_workspace_dependency_pin(&root.join("Cargo.toml"), "nostr")?;
let tempdir =
tempfile::tempdir().map_err(|error| format!("failed to create smoke tempdir: {error}"))?;
- write_consumer(&tempdir.path().join("Cargo.toml"), &sdk_path)?;
+ write_consumer(
+ &tempdir.path().join("Cargo.toml"),
+ &sdk_path,
+ &nostr_version,
+ )?;
let src_dir = tempdir.path().join("src");
fs::create_dir_all(&src_dir)
.map_err(|error| format!("failed to create {}: {error}", src_dir.display()))?;
fs::write(src_dir.join("main.rs"), CONSUMER_MAIN)
.map_err(|error| format!("failed to write smoke consumer main.rs: {error}"))?;
- let status = Command::new("cargo")
- .arg("check")
- .arg("--quiet")
- .current_dir(tempdir.path())
+ let mut command = smoke_cargo_check_command(tempdir.path(), &toolchain);
+ let status = command
.status()
.map_err(|error| format!("failed to run smoke cargo check: {error}"))?;
if status.success() {
@@ -35,10 +42,23 @@ fn knowledge_rust_local() -> Result<(), String> {
}
}
-fn write_consumer(path: &Path, sdk_path: &Path) -> Result<(), String> {
+fn smoke_cargo_check_command(path: &Path, toolchain: &ResolvedRustToolchain) -> Command {
+ let mut command = Command::new(&toolchain.cargo);
+ command.arg("check").arg("--quiet").current_dir(path);
+ toolchain.apply_to_command(&mut command);
+ command
+}
+
+fn write_consumer(path: &Path, sdk_path: &Path, nostr_version: &str) -> Result<(), String> {
+ let cargo_toml = render_consumer_manifest(sdk_path, nostr_version)?;
+ fs::write(path, cargo_toml)
+ .map_err(|error| format!("failed to write {}: {error}", path.display()))
+}
+
+fn render_consumer_manifest(sdk_path: &Path, nostr_version: &str) -> Result<String, String> {
let sdk_path = serde_json::to_string(&sdk_path.to_string_lossy())
.map_err(|error| format!("failed to render SDK path: {error}"))?;
- let cargo_toml = format!(
+ Ok(format!(
r#"[package]
name = "radroots-sdk-knowledge-smoke"
version = "0.1.0"
@@ -47,11 +67,56 @@ publish = false
[dependencies]
radroots_sdk = {{ path = {sdk_path}, features = ["std", "serde", "serde_json", "nostr", "knowledge"] }}
-nostr = "0.44.2"
+nostr = "{nostr_version}"
"#
- );
- fs::write(path, cargo_toml)
- .map_err(|error| format!("failed to write {}: {error}", path.display()))
+ ))
+}
+
+fn exact_workspace_dependency_pin(manifest_path: &Path, name: &str) -> Result<String, String> {
+ let contents = fs::read_to_string(manifest_path)
+ .map_err(|error| format!("failed to read {}: {error}", manifest_path.display()))?;
+ let version = workspace_dependency_version_from(&contents, name)?;
+ exact_dependency_pin(name, &version)
+}
+
+fn workspace_dependency_version_from(contents: &str, name: &str) -> Result<String, String> {
+ let value = contents
+ .parse::<toml::Value>()
+ .map_err(|error| format!("failed to parse workspace manifest: {error}"))?;
+ let dependency = value
+ .get("workspace")
+ .and_then(|workspace| workspace.get("dependencies"))
+ .and_then(|dependencies| dependencies.get(name))
+ .ok_or_else(|| format!("workspace dependency {name} is not defined"))?;
+ if let Some(version) = dependency.as_str() {
+ return Ok(version.to_owned());
+ }
+ dependency
+ .get("version")
+ .and_then(toml::Value::as_str)
+ .map(str::to_owned)
+ .ok_or_else(|| format!("workspace dependency {name} must define version"))
+}
+
+fn exact_dependency_pin(name: &str, version: &str) -> Result<String, String> {
+ let trimmed = version.trim().strip_prefix('=').unwrap_or(version.trim());
+ if !is_exact_crates_version(trimmed) {
+ return Err(format!(
+ "workspace dependency {name} must use a full exact version for smoke pinning: {version}"
+ ));
+ }
+ Ok(format!("={trimmed}"))
+}
+
+fn is_exact_crates_version(version: &str) -> bool {
+ let base = version
+ .split_once(['-', '+'])
+ .map_or(version, |(base, _)| base);
+ let parts = base.split('.').collect::<Vec<_>>();
+ parts.len() == 3
+ && parts.iter().all(|part| {
+ !part.is_empty() && part.chars().all(|character| character.is_ascii_digit())
+ })
}
const CONSUMER_MAIN: &str = r#"use nostr::{EventBuilder, Keys, Kind, Tag, Timestamp};
@@ -142,3 +207,59 @@ fn claim_builder() -> RadrootsKnowledgeClaimBuilder {
.author_asserted_confidence("medium")
}
"#;
+
+#[cfg(test)]
+mod tests {
+ use std::path::{Path, PathBuf};
+
+ use crate::wasm::ResolvedRustToolchain;
+
+ use super::{
+ exact_dependency_pin, render_consumer_manifest, smoke_cargo_check_command,
+ workspace_dependency_version_from,
+ };
+
+ #[test]
+ fn smoke_consumer_manifest_uses_exact_direct_dependency_pin() {
+ let manifest =
+ render_consumer_manifest(Path::new("/tmp/radroots-sdk"), "=0.44.2").expect("manifest");
+
+ assert!(manifest.contains("nostr = \"=0.44.2\""));
+ assert!(!manifest.contains("nostr = \"0.44.2\""));
+ }
+
+ #[test]
+ fn smoke_workspace_dependency_version_reads_table_version() {
+ let version = workspace_dependency_version_from(
+ r#"[workspace.dependencies]
+nostr = { version = "0.44.2" }
+"#,
+ "nostr",
+ )
+ .expect("version");
+
+ assert_eq!(version, "0.44.2");
+ }
+
+ #[test]
+ fn smoke_dependency_pin_rejects_floating_major_version() {
+ let error = exact_dependency_pin("nostr", "0.44").expect_err("floating version");
+
+ assert!(error.contains("full exact version"));
+ }
+
+ #[test]
+ fn smoke_command_uses_resolved_cargo_path() {
+ let cargo = PathBuf::from("/tmp/rust-toolchain/bin/cargo");
+ let toolchain = ResolvedRustToolchain {
+ channel: "1.92.0".to_owned(),
+ rustc: PathBuf::from("/tmp/rust-toolchain/bin/rustc"),
+ cargo: cargo.clone(),
+ bin_dir: PathBuf::from("/tmp/rust-toolchain/bin"),
+ };
+
+ let command = smoke_cargo_check_command(Path::new("/tmp/smoke"), &toolchain);
+
+ assert_eq!(command.get_program(), cargo.as_os_str());
+ }
+}
diff --git a/tools/xtask/src/wasm.rs b/tools/xtask/src/wasm.rs
@@ -1,8 +1,10 @@
use std::{
collections::BTreeSet,
- env, fs, io,
+ env,
+ ffi::OsStr,
+ fs, io,
path::{Path, PathBuf},
- process::Command,
+ process::{Command, Stdio},
};
use crate::{
@@ -12,13 +14,37 @@ use crate::{
wasm_declarations::write_declaration_files,
};
-const WASM_TARGET: &str = "wasm32-unknown-unknown";
+pub(crate) const WASM_TARGET: &str = "wasm32-unknown-unknown";
+const WASM_C_COMPILER_ENV: &str = "CC_wasm32_unknown_unknown";
+const WASM_CFLAGS_ENV: &str = "CFLAGS_wasm32_unknown_unknown";
+const WASM_C_TARGET_ARG: &str = "--target=wasm32-unknown-unknown";
pub fn generate(args: &[String]) -> Result<(), String> {
validate_package_matrix()?;
let specs = selected_specs(args)?;
let root = workspace_root()?;
- let toolchain = resolve_wasm_toolchain()?;
+ let toolchain = resolve_wasm_toolchain(&root)?;
+ println!(
+ "using Rust toolchain {} for {}: rustc={}, cargo={}",
+ toolchain.channel(),
+ WASM_TARGET,
+ toolchain.rustc().display(),
+ toolchain.cargo().display()
+ );
+ let wasm_c_compiler = if specs
+ .iter()
+ .any(|spec| wasm_package_requires_c_compiler(*spec))
+ {
+ let compiler = resolve_wasm_c_compiler()?;
+ println!(
+ "using WASM C compiler for {}: {}",
+ WASM_TARGET,
+ compiler.path.display()
+ );
+ Some(compiler)
+ } else {
+ None
+ };
for spec in specs {
let dist_dir = root.join(spec.package_dir).join("dist");
if dist_dir.exists() {
@@ -30,11 +56,12 @@ pub fn generate(args: &[String]) -> Result<(), String> {
for arg in wasm_pack_args(spec) {
command.arg(arg);
}
- if let Some(parent) = toolchain.rustc.parent() {
- prepend_path(&mut command, parent);
+ toolchain.apply_to_command(&mut command);
+ if let Some(compiler) = wasm_c_compiler.as_ref()
+ && wasm_package_requires_c_compiler(spec)
+ {
+ compiler.apply_to_command(&mut command);
}
- command.env("RUSTC", &toolchain.rustc);
- command.env("CARGO", &toolchain.cargo);
let status = command.status().map_err(|error| {
format!(
"failed to start wasm-pack for {} while generating {}: {error}",
@@ -57,19 +84,80 @@ pub fn generate(args: &[String]) -> Result<(), String> {
struct WasmToolchain {
wasm_pack: PathBuf,
- rustc: PathBuf,
- cargo: PathBuf,
+ rust: ResolvedRustToolchain,
+}
+
+impl WasmToolchain {
+ fn apply_to_command(&self, command: &mut Command) {
+ self.rust.apply_to_command(command);
+ }
+
+ fn rustc(&self) -> &Path {
+ &self.rust.rustc
+ }
+
+ fn cargo(&self) -> &Path {
+ &self.rust.cargo
+ }
+
+ fn channel(&self) -> &str {
+ &self.rust.channel
+ }
}
-fn resolve_wasm_toolchain() -> Result<WasmToolchain, String> {
+pub(crate) struct ResolvedRustToolchain {
+ pub(crate) channel: String,
+ pub(crate) rustc: PathBuf,
+ pub(crate) cargo: PathBuf,
+ pub(crate) bin_dir: PathBuf,
+}
+
+impl ResolvedRustToolchain {
+ pub(crate) fn apply_to_command(&self, command: &mut Command) {
+ prepend_path(command, &self.bin_dir);
+ command.env("RUSTC", &self.rustc);
+ command.env("CARGO", &self.cargo);
+ command.env("RUSTUP_TOOLCHAIN", &self.channel);
+ }
+}
+
+fn resolve_wasm_toolchain(root: &Path) -> Result<WasmToolchain, String> {
let wasm_pack = resolve_required_path_tool("wasm-pack")?;
- let rustc = resolve_required_rust_tool("rustc", "RUSTC")?;
- let cargo = resolve_required_rust_tool("cargo", "CARGO")?;
- ensure_wasm_target_installed()?;
- Ok(WasmToolchain {
- wasm_pack,
+ let rust = resolve_rust_toolchain(root)?;
+ Ok(WasmToolchain { wasm_pack, rust })
+}
+
+pub(crate) fn resolve_rust_toolchain(root: &Path) -> Result<ResolvedRustToolchain, String> {
+ let toolchain_path = root.join("rust-toolchain.toml");
+ let channel = read_rust_toolchain_channel(&toolchain_path)?;
+ let rustc = rustup_tool_for_channel("rustc", &channel)?;
+ let cargo = rustup_tool_for_channel("cargo", &channel)?;
+ let rustc_bin = rustc.parent().ok_or_else(|| {
+ format!(
+ "rustup resolved rustc for toolchain {channel} without a parent path: {}",
+ rustc.display()
+ )
+ })?;
+ let cargo_bin = cargo.parent().ok_or_else(|| {
+ format!(
+ "rustup resolved cargo for toolchain {channel} without a parent path: {}",
+ cargo.display()
+ )
+ })?;
+ if rustc_bin != cargo_bin {
+ return Err(format!(
+ "rustup resolved mismatched Rust tool paths for toolchain {channel}: rustc={}, cargo={}",
+ rustc.display(),
+ cargo.display()
+ ));
+ }
+ let bin_dir = rustc_bin.to_path_buf();
+ ensure_wasm_target_installed(&channel)?;
+ Ok(ResolvedRustToolchain {
+ channel,
rustc,
cargo,
+ bin_dir,
})
}
@@ -130,6 +218,10 @@ fn selected_specs(args: &[String]) -> Result<Vec<WasmPackageSpec>, String> {
}
}
+fn wasm_package_requires_c_compiler(spec: WasmPackageSpec) -> bool {
+ spec.key == "events_codec"
+}
+
fn resolve_required_path_tool(name: &str) -> Result<PathBuf, String> {
let path = env::var_os("PATH").ok_or_else(|| {
format!("missing {name}: PATH is not set; install {name} and expose it on PATH")
@@ -188,68 +280,245 @@ fn is_executable_file(path: &Path) -> bool {
}
}
-fn resolve_required_rust_tool(name: &str, env_var: &str) -> Result<PathBuf, String> {
- if let Some(path) = explicit_tool_path(env_var) {
- return Ok(PathBuf::from(path));
- }
- rustup_tool(name).ok_or_else(|| {
- format!(
- "missing rustup resolution for {name}: set {env_var} explicitly or install rustup with the {WASM_TARGET} target"
- )
- })
+fn read_rust_toolchain_channel(path: &Path) -> Result<String, String> {
+ let contents = fs::read_to_string(path)
+ .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
+ rust_toolchain_channel_from(&contents)
}
-fn explicit_tool_path(env_var: &str) -> Option<String> {
- let value = env::var(env_var).ok()?;
- let trimmed = value.trim();
- (!trimmed.is_empty()).then(|| trimmed.to_owned())
+fn rust_toolchain_channel_from(contents: &str) -> Result<String, String> {
+ let value = contents
+ .parse::<toml::Value>()
+ .map_err(|error| format!("failed to parse rust-toolchain.toml: {error}"))?;
+ let channel = value
+ .get("toolchain")
+ .and_then(|toolchain| toolchain.get("channel"))
+ .and_then(toml::Value::as_str)
+ .ok_or_else(|| "rust-toolchain.toml must define toolchain.channel".to_owned())?
+ .trim();
+ if channel.is_empty() {
+ return Err("rust-toolchain.toml toolchain.channel must not be empty".to_owned());
+ }
+ Ok(channel.to_owned())
}
-fn rustup_tool(name: &str) -> Option<PathBuf> {
+fn rustup_tool_for_channel(name: &str, channel: &str) -> Result<PathBuf, String> {
let output = Command::new("rustup")
.arg("which")
+ .arg("--toolchain")
+ .arg(channel)
.arg(name)
.output()
- .ok()?;
+ .map_err(|error| {
+ format!(
+ "failed to resolve {name} for Rust toolchain {channel} with rustup: {error}; install rustup toolchain {channel}"
+ )
+ })?;
if !output.status.success() {
- return None;
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ return Err(format!(
+ "failed to resolve {name} for Rust toolchain {channel}: {}; run `rustup toolchain install {channel}`",
+ stderr.trim()
+ ));
}
- let path = String::from_utf8(output.stdout).ok()?;
+ let path = String::from_utf8(output.stdout)
+ .map_err(|error| format!("rustup emitted non-UTF-8 {name} path: {error}"))?;
let trimmed = path.trim();
- (!trimmed.is_empty()).then(|| PathBuf::from(trimmed))
+ if trimmed.is_empty() {
+ return Err(format!(
+ "rustup returned an empty {name} path for Rust toolchain {channel}"
+ ));
+ }
+ Ok(PathBuf::from(trimmed))
}
-fn ensure_wasm_target_installed() -> Result<(), String> {
+fn ensure_wasm_target_installed(channel: &str) -> Result<(), String> {
let output = Command::new("rustup")
- .arg("target")
- .arg("list")
- .arg("--installed")
+ .args(rustup_target_list_args(channel))
.output()
.map_err(|error| {
format!(
- "failed to verify {WASM_TARGET} target with rustup: {error}; install rustup or set RUSTC/CARGO from a toolchain that supports {WASM_TARGET}"
+ "failed to verify {WASM_TARGET} target for Rust toolchain {channel} with rustup: {error}; install rustup toolchain {channel}"
)
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
- "failed to verify {WASM_TARGET} target with rustup: {}; run `rustup target add {WASM_TARGET}`",
+ "failed to verify {WASM_TARGET} target for Rust toolchain {channel}: {}; run `rustup target add {WASM_TARGET} --toolchain {channel}`",
stderr.trim()
));
}
let stdout = String::from_utf8_lossy(&output.stdout);
- if !target_list_contains(&stdout, WASM_TARGET) {
- return Err(format!(
- "missing Rust target {WASM_TARGET}: run `rustup target add {WASM_TARGET}`"
- ));
+ validate_target_list(&stdout, WASM_TARGET, channel)
+}
+
+fn rustup_target_list_args(channel: &str) -> [&str; 5] {
+ ["target", "list", "--installed", "--toolchain", channel]
+}
+
+fn validate_target_list(output: &str, target: &str, channel: &str) -> Result<(), String> {
+ if target_list_contains(output, target) {
+ Ok(())
+ } else {
+ Err(format!(
+ "missing Rust target {target} for Rust toolchain {channel}: run `rustup target add {target} --toolchain {channel}`"
+ ))
}
- Ok(())
}
fn target_list_contains(output: &str, target: &str) -> bool {
output.lines().any(|line| line.trim() == target)
}
+#[derive(Debug)]
+struct WasmCCompiler {
+ path: PathBuf,
+}
+
+impl WasmCCompiler {
+ fn apply_to_command(&self, command: &mut Command) {
+ command.env(WASM_C_COMPILER_ENV, &self.path);
+ if env::var_os(WASM_CFLAGS_ENV).is_none() {
+ command.env(WASM_CFLAGS_ENV, WASM_C_TARGET_ARG);
+ }
+ }
+}
+
+fn resolve_wasm_c_compiler() -> Result<WasmCCompiler, String> {
+ let path = env::var_os("PATH").unwrap_or_default();
+ let explicit = env::var_os(WASM_C_COMPILER_ENV);
+ let candidates = wasm_c_compiler_candidates(&path);
+ resolve_wasm_c_compiler_with(
+ explicit.as_deref(),
+ &path,
+ candidates,
+ verify_wasm_c_compiler,
+ )
+}
+
+fn resolve_wasm_c_compiler_with<F>(
+ explicit: Option<&OsStr>,
+ path: &OsStr,
+ candidates: Vec<PathBuf>,
+ verify: F,
+) -> Result<WasmCCompiler, String>
+where
+ F: Fn(&Path) -> Result<(), String>,
+{
+ if let Some(explicit) = explicit {
+ let compiler = resolve_explicit_wasm_c_compiler(explicit, path)?;
+ verify(&compiler).map_err(|error| {
+ format!(
+ "{WASM_C_COMPILER_ENV} does not name a WASM-capable C compiler: {}; {error}",
+ compiler.display()
+ )
+ })?;
+ return Ok(WasmCCompiler { path: compiler });
+ }
+
+ let mut checked = Vec::new();
+ for candidate in candidates {
+ if !is_executable_file(&candidate) {
+ continue;
+ }
+ checked.push(candidate.display().to_string());
+ if verify(&candidate).is_ok() {
+ return Ok(WasmCCompiler { path: candidate });
+ }
+ }
+ let checked = if checked.is_empty() {
+ "none".to_owned()
+ } else {
+ checked.join(", ")
+ };
+ Err(format!(
+ "missing suitable WASM C compiler for {WASM_TARGET}: set {WASM_C_COMPILER_ENV} to a clang-style compiler that accepts `{WASM_C_TARGET_ARG}`; checked candidates: {checked}"
+ ))
+}
+
+fn resolve_explicit_wasm_c_compiler(value: &OsStr, path: &OsStr) -> Result<PathBuf, String> {
+ if value.is_empty() {
+ return Err(format!(
+ "{WASM_C_COMPILER_ENV} is set but empty; set it to a compiler path that accepts `{WASM_C_TARGET_ARG}`"
+ ));
+ }
+ let candidate = PathBuf::from(value);
+ if candidate.components().count() > 1 {
+ if is_executable_file(&candidate) {
+ Ok(candidate)
+ } else {
+ Err(format!(
+ "{WASM_C_COMPILER_ENV} is not executable: {}",
+ candidate.display()
+ ))
+ }
+ } else {
+ first_executable_match(value, path).ok_or_else(|| {
+ format!(
+ "{WASM_C_COMPILER_ENV} command was not found on PATH: {}",
+ value.to_string_lossy()
+ )
+ })
+ }
+}
+
+fn first_executable_match(name: &OsStr, path: &OsStr) -> Option<PathBuf> {
+ for dir in env::split_paths(path) {
+ let candidate = dir.join(name);
+ if is_executable_file(&candidate) {
+ return Some(candidate);
+ }
+ }
+ None
+}
+
+fn wasm_c_compiler_candidates(path: &OsStr) -> Vec<PathBuf> {
+ let mut seen = BTreeSet::new();
+ let mut candidates = Vec::new();
+ for name in ["wasm32-unknown-unknown-clang", "clang"] {
+ for candidate in executable_matches(name, path) {
+ let key = fs::canonicalize(&candidate).unwrap_or_else(|_| candidate.clone());
+ if seen.insert(key) {
+ candidates.push(candidate);
+ }
+ }
+ }
+ candidates
+}
+
+fn verify_wasm_c_compiler(path: &Path) -> Result<(), String> {
+ let tempdir = tempfile::tempdir()
+ .map_err(|error| format!("failed to create C probe tempdir: {error}"))?;
+ let source_path = tempdir.path().join("wasm_probe.c");
+ let object_path = tempdir.path().join("wasm_probe.o");
+ fs::write(
+ &source_path,
+ "int radroots_wasm_probe(void) { return 0; }\n",
+ )
+ .map_err(|error| format!("failed to write C compiler probe: {error}"))?;
+ let output = Command::new(path)
+ .arg(WASM_C_TARGET_ARG)
+ .arg("-c")
+ .arg(&source_path)
+ .arg("-o")
+ .arg(&object_path)
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .map_err(|error| format!("failed to run {}: {error}", path.display()))?;
+ if output.status.success() && object_path.is_file() {
+ return Ok(());
+ }
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ Err(format!(
+ "{} rejected `{}` for {}: {}",
+ path.display(),
+ WASM_C_TARGET_ARG,
+ WASM_TARGET,
+ stderr.trim()
+ ))
+}
+
fn prepend_path(command: &mut Command, prefix: &Path) {
let existing = env::var_os("PATH").unwrap_or_default();
let mut paths = vec![prefix.to_path_buf()];
@@ -262,16 +531,20 @@ fn prepend_path(command: &mut Command, prefix: &Path) {
#[cfg(test)]
mod tests {
use std::{
- env, fs,
- path::PathBuf,
+ env,
+ ffi::OsStr,
+ fs,
+ path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
use crate::package_matrix::wasm_package_specs;
use super::{
- remove_wasm_pack_gitignore, resolve_path_tool_from_path, rustup_tool, selected_specs,
- target_list_contains, wasm_pack_args,
+ WASM_C_COMPILER_ENV, WASM_C_TARGET_ARG, WASM_TARGET, remove_wasm_pack_gitignore,
+ resolve_path_tool_from_path, resolve_wasm_c_compiler_with, rust_toolchain_channel_from,
+ rustup_target_list_args, selected_specs, target_list_contains, validate_target_list,
+ wasm_pack_args, wasm_package_requires_c_compiler,
};
#[test]
@@ -362,8 +635,86 @@ mod tests {
}
#[test]
- fn rustup_tool_resolution_is_non_panicking() {
- let _ = rustup_tool("rustc");
+ fn rustup_target_list_args_are_bound_to_selected_toolchain() {
+ assert_eq!(
+ rustup_target_list_args("1.92.0"),
+ ["target", "list", "--installed", "--toolchain", "1.92.0"]
+ );
+ }
+
+ #[test]
+ fn missing_wasm_target_error_names_selected_toolchain() {
+ let error = validate_target_list("aarch64-apple-darwin\n", WASM_TARGET, "1.92.0")
+ .expect_err("missing target");
+
+ assert!(error.contains("wasm32-unknown-unknown"));
+ assert!(error.contains("--toolchain 1.92.0"));
+ }
+
+ #[test]
+ fn parses_rust_toolchain_channel() {
+ let channel = rust_toolchain_channel_from(
+ r#"[toolchain]
+channel = "1.92.0"
+"#,
+ )
+ .expect("channel");
+
+ assert_eq!(channel, "1.92.0");
+ }
+
+ #[test]
+ fn rejects_missing_wasm_c_compiler() {
+ let error = resolve_wasm_c_compiler_with(None, OsStr::new(""), Vec::new(), |_| Ok(()))
+ .expect_err("missing compiler");
+
+ assert!(error.contains(WASM_C_COMPILER_ENV));
+ assert!(error.contains(WASM_C_TARGET_ARG));
+ }
+
+ #[test]
+ fn rejects_unsuitable_wasm_c_compiler_candidates() {
+ let root = test_root("unsuitable_wasm_c_compiler");
+ fs::create_dir_all(&root).expect("create root");
+ let compiler = root.join("clang");
+ write_executable(compiler.clone());
+
+ let error = resolve_wasm_c_compiler_with(None, OsStr::new(""), vec![compiler], |_| {
+ Err("target unsupported".to_owned())
+ })
+ .expect_err("unsuitable compiler");
+
+ assert!(error.contains("missing suitable WASM C compiler"));
+ assert!(error.contains(WASM_C_COMPILER_ENV));
+ let _ = fs::remove_dir_all(root);
+ }
+
+ #[test]
+ fn accepts_verified_wasm_c_compiler_candidate() {
+ let root = test_root("verified_wasm_c_compiler");
+ fs::create_dir_all(&root).expect("create root");
+ let compiler = root.join("clang");
+ write_executable(compiler.clone());
+
+ let resolved = resolve_wasm_c_compiler_with(
+ None,
+ OsStr::new(""),
+ vec![compiler.clone()],
+ |path: &Path| {
+ assert_eq!(path, compiler);
+ Ok(())
+ },
+ )
+ .expect("compiler");
+
+ assert_eq!(resolved.path, compiler);
+ let _ = fs::remove_dir_all(root);
+ }
+
+ #[test]
+ fn events_codec_wasm_requires_c_compiler() {
+ assert!(wasm_package_requires_c_compiler(wasm_package_specs()[0]));
+ assert!(!wasm_package_requires_c_compiler(wasm_package_specs()[1]));
}
fn write_executable(path: PathBuf) {