diff --git a/.github/workflows/rust-codegen.yml b/.github/workflows/rust-codegen.yml index 11f45aa63..3b92f220a 100644 --- a/.github/workflows/rust-codegen.yml +++ b/.github/workflows/rust-codegen.yml @@ -12,12 +12,16 @@ name: Rust codegen # fn signatures, wrong trait bounds, generic drift. The # `tests/compile_spec.rs` cancel-mode end-to-end probe runs in # `Check & Test`; this suite is the breadth check across the core -# example corpus, run nightly so the per-example `cargo check` -# (~30-60 s each) doesn't burden every PR. +# example corpus, run only where Rust codegen can change so the +# per-example `cargo check` (~30-60 s each) doesn't burden every PR. # -# Sister workflow to `proof.yml`. Both consolidate heavy toolchain -# / build work into one nightly run instead of adding latency to -# every compiler-touching PR. +# Sister workflow to `proof.yml`. It runs nightly against main and on +# pull requests that touch the Rust backend or what feeds it: the Rust +# emitter, shared codegen, MIR and its ownership passes, the native +# providers and capability contracts, the Aver runtime crate, yield +# lowering, and the self-hosted sources. Other PRs skip it, so ordinary +# compiler changes do not pay for the per-example `cargo check`. Neither +# job is a required check; their names are distinct from every `CI` job. on: schedule: @@ -25,8 +29,32 @@ on: # as fuzz nightly + proof nightly. Three independent workflows # each reading main, no cross-artefact deps. - cron: '0 3 * * *' + pull_request: + branches: [main] + paths: + - '.github/workflows/rust-codegen.yml' + - 'src/codegen/rust/**' + - 'src/codegen/common.rs' + - 'src/codegen/mod.rs' + - 'src/codegen/builtin*.rs' + - 'src/ir/mir/**' + - 'src/ir/alias.rs' + - 'src/ir/escape.rs' + - 'src/ir/last_use.rs' + - 'src/yield_lowering/**' + - 'src/provider/**' + - 'stdlib/capabilities/**' + - 'aver-rt/**' + - 'self_hosted/**' + - 'tests/rust_codegen_regression.rs' workflow_dispatch: +concurrency: + # A new push to the same PR supersedes the running canary; nightly and + # dispatched runs keep their own group and are never cancelled. + group: rust-codegen-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: CARGO_TERM_COLOR: always diff --git a/tests/wasm_host_work_spec.rs b/tests/wasm_host_work_spec.rs index f1aa5cc53..9c6e2f708 100644 --- a/tests/wasm_host_work_spec.rs +++ b/tests/wasm_host_work_spec.rs @@ -65,25 +65,102 @@ fn wasm_workers_transport_unit_tasks_and_unit_results() { ); } -#[test] -fn javascript_workers_run_the_coordinator_and_progress_independently() { - let node = std::env::var_os("AVER_NODE").unwrap_or_else(|| "node".into()); - let version = Command::new(&node) - .arg("--version") - .output() - .expect("Node is required for the public JS Work host test (set AVER_NODE)"); - let major: u32 = String::from_utf8_lossy(&version.stdout) +/// Oldest Node whose V8 runs the emitted module: WasmGC and tail calls are +/// on by default from Node 22. +const MIN_NODE_MAJOR: u32 = 22; + +fn node_major(node: &std::ffi::OsStr) -> Option { + let output = Command::new(node).arg("--version").output().ok()?; + if !output.status.success() { + return None; + } + String::from_utf8_lossy(&output.stdout) .trim() .trim_start_matches('v') .split('.') - .next() - .unwrap_or("") + .next()? .parse() - .unwrap_or(0); - assert!( - major >= 22, - "the JS host test needs Node 22+ with WasmGC and tail calls; set AVER_NODE" - ); + .ok() +} + +/// The Node that runs the JS host test and its major version. +/// +/// `AVER_NODE` wins when set, and a too-old value is reported rather than +/// silently replaced by another install. Otherwise `node` on `PATH` is used when it is new enough, +/// then the newest suitable install under `~/.nvm/versions/node`. The +/// error names every candidate so a skip or failure says what was tried. +fn suitable_node() -> Result<(std::ffi::OsString, u32), String> { + if let Some(node) = std::env::var_os("AVER_NODE") { + return match node_major(&node) { + Some(major) if major >= MIN_NODE_MAJOR => Ok((node, major)), + Some(major) => Err(format!( + "AVER_NODE={} is Node {major}; Node {MIN_NODE_MAJOR}+ is required", + node.to_string_lossy() + )), + None => Err(format!( + "AVER_NODE={} did not run `--version`", + node.to_string_lossy() + )), + }; + } + let mut tried = Vec::new(); + let path_node = std::ffi::OsString::from("node"); + match node_major(&path_node) { + Some(major) if major >= MIN_NODE_MAJOR => return Ok((path_node, major)), + Some(major) => tried.push(format!("`node` on PATH is Node {major}")), + None => tried.push("no `node` on PATH".to_string()), + } + if let Some(home) = std::env::var_os("HOME") { + let nvm = std::path::PathBuf::from(home).join(".nvm/versions/node"); + let mut installs: Vec<(u32, std::path::PathBuf)> = std::fs::read_dir(&nvm) + .into_iter() + .flatten() + .filter_map(|entry| entry.ok()) + .filter_map(|entry| { + let name = entry.file_name(); + let major = name + .to_str()? + .trim_start_matches('v') + .split('.') + .next()? + .parse() + .ok()?; + Some((major, entry.path().join("bin/node"))) + }) + .filter(|(major, bin)| *major >= MIN_NODE_MAJOR && bin.is_file()) + .collect(); + installs.sort(); + if let Some((_, bin)) = installs.pop() { + let node = bin.into_os_string(); + if let Some(major) = node_major(&node) { + return Ok((node, major)); + } + } + tried.push(format!("no Node {MIN_NODE_MAJOR}+ under {}", nvm.display())); + } + Err(tried.join("; ")) +} + +#[test] +fn javascript_workers_run_the_coordinator_and_progress_independently() { + let (node, major) = match suitable_node() { + Ok(found) => found, + // CI installs a suitable Node for this lane, so a missing one there + // is a broken lane, not a reason to skip. + Err(why) if std::env::var_os("CI").is_some() => { + panic!( + "the JS Work host test needs Node {MIN_NODE_MAJOR}+ with WasmGC and tail calls: {why}" + ) + } + Err(why) => { + eprintln!( + "SKIP javascript_workers_run_the_coordinator_and_progress_independently: \ + needs Node {MIN_NODE_MAJOR}+ with WasmGC and tail calls ({why}); \ + set AVER_NODE to a suitable node" + ); + return; + } + }; let out = tempfile::tempdir().expect("wasm artifacts"); for name in [ "work_jobs_parallel",