From 574bf1dc37a6868c0155827b6fc25159a6f292d6 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Sat, 6 Jun 2026 02:08:02 +0100 Subject: [PATCH 1/3] feat(wasi): ensure build works for wasm32-wasip2 --- src/uu/yes/src/yes.rs | 11 ++++++++--- src/uucore/src/lib/features/fs.rs | 15 ++++++++++++--- src/uucore/src/lib/lib.rs | 23 ++++++++++++++--------- src/uucore/src/lib/mods/display.rs | 8 ++++++-- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index 98be0919381..1b150d69d26 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -58,15 +58,20 @@ pub fn uu_app() -> Command { fn args_into_buffer<'a>(i: impl Iterator) -> UResult> { #[cfg(unix)] use std::os::unix::ffi::OsStrExt; - #[cfg(target_os = "wasi")] + #[cfg(all(target_os = "wasi", target_env = "p1"))] use std::os::wasi::ffi::OsStrExt; let mut buf = Vec::with_capacity(BUF_SIZE); - // On Unix (and wasi), OsStrs are just &[u8]'s underneath... - #[cfg(any(unix, target_os = "wasi"))] + // On Unix (and wasip1), OsStrs are just &[u8]'s underneath... + #[cfg(any(unix, all(target_os = "wasi", target_env = "p1")))] for part in itertools::intersperse(i.map(|a| a.as_bytes()), b" ") { buf.extend_from_slice(part); } + // On WASI Preview2, OsStrs are guaranteed to be UTF-8 encoded + #[cfg(all(target_os = "wasi", target_env = "p2"))] + for part in itertools::intersperse(i.map(|a| a.as_encoded_bytes()), b" ") { + buf.extend_from_slice(part); + } // But, on Windows, we must hop through a String. #[cfg(not(any(unix, target_os = "wasi")))] for part in itertools::intersperse(i.map(|a| a.to_str()), Some(" ")) { diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 28544d65711..c8cc144068b 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -759,12 +759,21 @@ pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Pat pub fn path_ends_with_terminator(path: &Path) -> bool { #[cfg(unix)] use std::os::unix::prelude::OsStrExt; - #[cfg(target_os = "wasi")] + #[cfg(all(target_os = "wasi", target_env = "p1"))] use std::os::wasi::ffi::OsStrExt; - path.as_os_str() + + #[cfg(all(target_os = "wasi", target_env = "p2"))] + return path + .as_os_str() + .as_encoded_bytes() + .last() + .is_some_and(|&byte| byte == b'/'); + #[cfg(not(all(target_os = "wasi", target_env = "p2")))] + return path + .as_os_str() .as_bytes() .last() - .is_some_and(|&byte| byte == b'/') + .is_some_and(|&byte| byte == b'/'); } #[cfg(windows)] diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 4dfffed2995..16c511b419a 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -147,7 +147,7 @@ use std::io::{BufRead, BufReader}; use std::iter; #[cfg(unix)] use std::os::unix::ffi::{OsStrExt, OsStringExt}; -#[cfg(target_os = "wasi")] +#[cfg(all(target_os = "wasi", target_env = "p1"))] use std::os::wasi::ffi::{OsStrExt, OsStringExt}; use std::str; use std::str::Utf8Chunk; @@ -454,9 +454,12 @@ impl error::UError for NonUtf8OsStrError {} /// and fails on other platforms if the string can't be coerced to UTF-8. #[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_str_as_bytes(os_string: &OsStr) -> Result<&[u8], NonUtf8OsStrError> { - #[cfg(any(unix, target_os = "wasi"))] + #[cfg(any(unix, all(target_os = "wasi", target_env = "p1")))] return Ok(os_string.as_bytes()); + #[cfg(all(target_os = "wasi", target_env = "p2"))] + return Ok(os_string.as_encoded_bytes()); + #[cfg(not(any(unix, target_os = "wasi")))] os_string .to_str() @@ -471,8 +474,10 @@ pub fn os_str_as_bytes(os_string: &OsStr) -> Result<&[u8], NonUtf8OsStrError> { /// This is always lossless on unix platforms, /// and wraps [`OsStr::to_string_lossy`] on non-unix platforms. pub fn os_str_as_bytes_lossy(os_string: &OsStr) -> Cow<'_, [u8]> { - #[cfg(any(unix, target_os = "wasi"))] + #[cfg(any(unix, all(target_os = "wasi", target_env = "p1")))] return Cow::from(os_string.as_bytes()); + #[cfg(all(target_os = "wasi", target_env = "p2"))] + return Cow::from(os_string.as_encoded_bytes()); #[cfg(not(any(unix, target_os = "wasi")))] match os_string.to_string_lossy() { @@ -488,10 +493,10 @@ pub fn os_str_as_bytes_lossy(os_string: &OsStr) -> Cow<'_, [u8]> { /// and fails on other platforms if the bytes can't be parsed as UTF-8. #[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_str_from_bytes(bytes: &[u8]) -> error::UResult> { - #[cfg(any(unix, target_os = "wasi"))] + #[cfg(any(unix, all(target_os = "wasi", target_env = "p1")))] return Ok(Cow::Borrowed(OsStr::from_bytes(bytes))); - #[cfg(not(any(unix, target_os = "wasi")))] + #[cfg(not(any(unix, all(target_os = "wasi", target_env = "p1"))))] Ok(Cow::Owned(OsString::from(str::from_utf8(bytes).map_err( |_| error::UUsageError::new(1, "Unable to transform bytes into OsStr"), )?))) @@ -503,10 +508,10 @@ pub fn os_str_from_bytes(bytes: &[u8]) -> error::UResult> { /// and fails on other platforms if the bytes can't be parsed as UTF-8. #[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_string_from_vec(vec: Vec) -> error::UResult { - #[cfg(any(unix, target_os = "wasi"))] + #[cfg(any(unix, all(target_os = "wasi", target_env = "p1")))] return Ok(OsString::from_vec(vec)); - #[cfg(not(any(unix, target_os = "wasi")))] + #[cfg(not(any(unix, all(target_os = "wasi", target_env = "p1"))))] Ok(OsString::from(String::from_utf8(vec).map_err(|_| { error::UUsageError::new(1, "invalid UTF-8 was detected in one or more arguments") })?)) @@ -518,9 +523,9 @@ pub fn os_string_from_vec(vec: Vec) -> error::UResult { /// and fails on other platforms if the bytes can't be parsed as UTF-8. #[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_string_to_vec(s: OsString) -> error::UResult> { - #[cfg(any(unix, target_os = "wasi"))] + #[cfg(any(unix, all(target_os = "wasi", target_env = "p1")))] let v = s.into_vec(); - #[cfg(not(any(unix, target_os = "wasi")))] + #[cfg(not(any(unix, all(target_os = "wasi", target_env = "p1"))))] let v = s .into_string() .map_err(|_| { diff --git a/src/uucore/src/lib/mods/display.rs b/src/uucore/src/lib/mods/display.rs index 165070e9a53..249fed48d9a 100644 --- a/src/uucore/src/lib/mods/display.rs +++ b/src/uucore/src/lib/mods/display.rs @@ -32,7 +32,7 @@ use std::io::{self, BufWriter, Stdout, StdoutLock, Write as _}; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; -#[cfg(target_os = "wasi")] +#[cfg(all(target_os = "wasi", target_env = "p1"))] use std::os::wasi::ffi::OsStrExt; // These used to be defined here, but they live in their own crate now. @@ -76,10 +76,14 @@ pub trait OsWrite: io::Write { /// On Windows, if the OS string is not valid Unicode, an error of kind /// [`io::ErrorKind::InvalidData`] is returned. fn write_all_os(&mut self, buf: &OsStr) -> io::Result<()> { - #[cfg(any(unix, target_os = "wasi"))] + #[cfg(any(unix, all(target_os = "wasi", target_env = "p1")))] { self.write_all(buf.as_bytes()) } + #[cfg(all(target_os = "wasi", target_env = "p2"))] + { + self.write_all(buf.as_encoded_bytes()) + } #[cfg(not(any(unix, target_os = "wasi")))] { From 7409654d3f44c77e78865f7b4b67ed924bbc0f05 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Sat, 6 Jun 2026 02:10:12 +0100 Subject: [PATCH 2/3] tests: ensure tests are passing for wasm32-wasip2 --- Cargo.toml | 1 + docs/src/wasi-test-gaps.md | 6 ++++- tests/by-util/test_cat.rs | 20 ++++++++++++--- tests/by-util/test_numfmt.rs | 50 +++++++++++++++++++----------------- tests/by-util/test_rm.rs | 41 +++++++++++++++++++++++++---- tests/by-util/test_tee.rs | 10 +++++--- tests/by-util/test_tr.rs | 6 ++++- tests/by-util/test_wc.rs | 6 ++++- tests/by-util/test_yes.rs | 3 ++- 9 files changed, 103 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cb46a50c8e9..58e9714deb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -506,6 +506,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(fuzzing)', 'cfg(target_os, values("cygwin"))', 'cfg(wasi_runner)', + 'cfg(wasip2_runner)', ] } unused_qualifications = "warn" diff --git a/docs/src/wasi-test-gaps.md b/docs/src/wasi-test-gaps.md index 4789ef03dc9..819d21b6631 100644 --- a/docs/src/wasi-test-gaps.md +++ b/docs/src/wasi-test-gaps.md @@ -1,6 +1,6 @@ # WASI integration test gaps -Tests annotated with `#[cfg_attr(wasi_runner, ignore = "...")]` are skipped when running integration tests against a WASI binary via wasmtime. This document tracks the reasons so that gaps in WASI support are visible in one place. +Tests annotated with `#[cfg_attr(wasi_runner, ignore = "...")]` or `#[cfg_attr(wasip2_runner, ignore = "...")]` are skipped when running integration tests against a WASI binary via wasmtime. This document tracks the reasons so that gaps in WASI support are visible in one place. To find all annotated tests: `grep -rn 'wasi_runner, ignore' tests/` @@ -35,3 +35,7 @@ When stdin is a seekable file, wasmtime does not preserve the file position betw ## WASI: read_link on absolute paths fails under wasmtime via spawned test harness `fs::read_link` on an absolute path inside the sandbox (e.g. `/file2`) returns `EPERM` when the WASI binary is launched through `std::process::Command` from the test harness, even though the same call works when wasmtime is invoked directly. This breaks `uucore::fs::canonicalize` for symlink sources, so tests that rely on following a symlink to compute a relative path are skipped. + +## WASI Preview2: exit with code has not been implemented + +Until the [wasi:cli/exit#exit-with-code](https://github.com/WebAssembly/WASI/blob/a1fc383d01eabaf3fac01de03c0ab1a01bfdd099/proposals/cli/wit/exit.wit#L16) will be made available in Rust stable toolchain, we will have to consider error exit code only as 1. diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index df2765b79b6..ff96f6375e2 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -87,6 +87,7 @@ fn test_no_options_big_input() { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore)] fn test_fifo_symlink() { use std::io::Write; use std::thread; @@ -138,6 +139,7 @@ fn test_closes_file_descriptors() { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore)] fn test_broken_pipe() { let mut cmd = new_ucmd!(); let mut child = cmd @@ -514,6 +516,7 @@ fn test_squeeze_blank_before_numbering() { /// This tests reading from Unix character devices #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore)] fn test_dev_random() { #[cfg(any(target_os = "linux", target_os = "android"))] const DEV_RANDOM: &str = "/dev/urandom"; @@ -603,10 +606,15 @@ fn test_domain_socket() { s.ucmd() .args(&[socket_path]) .fails() - .stderr_contains("No such device or address"); + .stderr_contains(if cfg!(wasi_runner) { + "No such file or directory" + } else { + "No such device or address" + }); } #[test] +#[cfg_attr(wasi_runner, ignore)] fn test_write_to_self_empty() { // it's ok if the input file is also the output file if it's empty let s = TestScenario::new(util_name!()); @@ -622,6 +630,7 @@ fn test_write_to_self_empty() { } #[test] +#[cfg_attr(wasi_runner, ignore)] fn test_write_to_self() { let s = TestScenario::new(util_name!()); let file_path = s.fixtures.plus("first_file"); @@ -678,6 +687,7 @@ fn test_successful_write_to_read_write_self() { /// /// `cat fx fx3 1<>fx3` #[test] +#[cfg_attr(wasi_runner, ignore)] fn test_failed_write_to_read_write_self() { let (at, mut ucmd) = at_and_ucmd!(); at.write("fx", "g"); @@ -708,9 +718,11 @@ fn test_error_loop() { at.symlink_file("2", "1"); at.symlink_file("3", "2"); at.symlink_file("1", "3"); - ucmd.arg("1") - .fails() - .stderr_is("cat: 1: Too many levels of symbolic links\n"); + ucmd.arg("1").fails().stderr_is(if cfg!(wasi_runner) { + "cat: 1: Operation not permitted\n" + } else { + "cat: 1: Too many levels of symbolic links\n" + }); } #[test] diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 7e6f940bc81..ebf6c7afecf 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -61,7 +61,7 @@ fn test_from_iec_i() { fn test_from_iec_i_requires_suffix() { new_ucmd!() .args(&["--from=iec-i", "10M"]) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_is("numfmt: missing 'i' suffix in input: '10M' (e.g Ki/Mi/Gi)\n"); } @@ -69,7 +69,7 @@ fn test_from_iec_i_requires_suffix() { fn test_from_iec_fails_if_i_suffix() { new_ucmd!() .args(&["--from=iec", "10Mi"]) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_is("numfmt: invalid suffix in input '10Mi': 'i'\n"); } @@ -266,7 +266,7 @@ fn test_suffixes() { } else { new_ucmd!() .args(&args) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_only(format!("numfmt: invalid suffix in input: '1{c}'\n")); } } @@ -282,7 +282,7 @@ fn test_invalid_following_valid_suffix() { new_ucmd!() .args(&args) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_only(format!( "numfmt: invalid suffix in input '1{valid_suffix}{c}': '{c}'\n" )); @@ -296,7 +296,7 @@ fn test_long_invalid_suffix() { new_ucmd!() .args(&args) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_only("numfmt: invalid suffix in input: '1500VVVVVVVV'\n"); } @@ -323,7 +323,7 @@ fn test_should_report_invalid_number_with_interior_junk() { fn test_should_report_invalid_number_with_sign_after_decimal() { new_ucmd!() .args(&["--", "-0.-1"]) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_is("numfmt: invalid number: '-0.-1'\n"); } @@ -753,7 +753,9 @@ fn test_suffix_with_padding() { #[test] fn test_invalid_stdin_number_returns_status_2() { - new_ucmd!().pipe_in("hello").fails_with_code(2); + new_ucmd!() + .pipe_in("hello") + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }); } #[test] @@ -761,7 +763,7 @@ fn test_invalid_stdin_number_in_middle_of_input() { new_ucmd!() .pipe_in("100\nhello\n200") .ignore_stdin_write_error() - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stdout_is("100\n"); } @@ -789,7 +791,7 @@ fn test_invalid_stdin_number_with_abort_returns_status_2() { new_ucmd!() .args(&["--invalid=abort"]) .pipe_in("4Q") - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_only("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } @@ -798,7 +800,7 @@ fn test_invalid_stdin_number_with_fail_returns_status_2() { new_ucmd!() .args(&["--invalid=fail"]) .pipe_in("4Q") - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stdout_is("4Q") .stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } @@ -824,7 +826,7 @@ fn test_invalid_arg_number_with_ignore_returns_status_0() { fn test_invalid_arg_number_with_abort_returns_status_2() { new_ucmd!() .args(&["--invalid=abort", "4Q"]) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_only("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } @@ -832,7 +834,7 @@ fn test_invalid_arg_number_with_abort_returns_status_2() { fn test_invalid_arg_number_with_fail_returns_status_2() { new_ucmd!() .args(&["--invalid=fail", "4Q"]) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stdout_is("4Q\n") .stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } @@ -897,7 +899,7 @@ fn test_valid_but_forbidden_suffix() { for number in numbers { new_ucmd!() .arg(number) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_contains(format!( "rejecting suffix in input: '{number}' (consider using --from)" )); @@ -1281,7 +1283,7 @@ fn test_debug_reports_failed_conversions_summary() { "Foo", "3000", ]) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stdout_is("1.0k\nFoo\n3.0k\n") .stderr_is( "numfmt: invalid number: 'Foo'\nnumfmt: failed to convert some of the input numbers\n", @@ -1293,7 +1295,7 @@ fn test_invalid_fail_with_fields_does_not_duplicate_output() { new_ucmd!() .args(&["--invalid=fail", "--field=2", "--from=si", "--to=iec"]) .pipe_in("A 1K x\nB Foo y\nC 3G z\n") - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stdout_is("A 1000 x\nB Foo y\nC 2.8G z\n") .stderr_is("numfmt: invalid number: 'Foo'\n"); } @@ -1302,7 +1304,7 @@ fn test_invalid_fail_with_fields_does_not_duplicate_output() { fn test_abort_with_fields_preserves_partial_output() { new_ucmd!() .args(&["--field=3", "--from=auto", "Hello 40M World 90G"]) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stdout_is("Hello 40M ") .stderr_is("numfmt: invalid number: 'World'\n"); } @@ -1311,17 +1313,17 @@ fn test_abort_with_fields_preserves_partial_output() { fn test_rejects_malformed_number_forms() { new_ucmd!() .args(&["--from=si", "12.K"]) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_contains("invalid number: '12.K'"); new_ucmd!() .args(&["--from=si", "--delimiter=,", "12. 2"]) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_contains("invalid number: '12. 2'"); new_ucmd!() .arg("..1") - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_contains("invalid suffix in input: '..1'"); } @@ -1379,12 +1381,12 @@ fn test_whitespace_mode_parses_custom_unit_separator_inputs() { fn test_empty_delimiter_whitespace_rejection() { new_ucmd!() .args(&["-d", "", "--from=auto", "2 K"]) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_contains("invalid suffix in input"); new_ucmd!() .args(&["-d", "", "--from=si", "--unit-separator=", "1 K"]) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_contains("invalid suffix in input"); } @@ -1450,7 +1452,7 @@ fn test_large_integer_precision_loss_issue_11654() { fn test_scientific_notation_rejected_by_gnu_issue_11655() { new_ucmd!() .arg("1e9") - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_contains("invalid suffix in input"); } @@ -1508,7 +1510,7 @@ fn test_invalid_utf8_input() { // 0xFF is invalid UTF-8 new_ucmd!() .pipe_in([b'1', b'0', b'\n', b'\xFF']) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stdout_is("10\n") .stderr_is("numfmt: invalid number: '\\377'\n"); } @@ -1524,7 +1526,7 @@ fn test_format_value_too_large_issue_11936() { for (args, hint) in cases { new_ucmd!() .args(&args) - .fails_with_code(2) + .fails_with_code(if cfg!(wasip2_runner) { 1 } else { 2 }) .stderr_contains("value/precision too large") .stderr_contains(hint); } diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index 43c712e9c4b..d915107a220 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -337,6 +337,7 @@ fn test_no_operand() { } #[test] +#[cfg_attr(wasi_runner, ignore)] fn test_verbose_slash() { let (at, mut ucmd) = at_and_ucmd!(); let dir = "test_rm_verbose_slash_directory"; @@ -774,6 +775,7 @@ fn test_current_or_parent_dir_rm4() { at.touch(file_1); at.touch(file_2); + #[cfg(not(wasi_runner))] let answers = [ "rm: refusing to remove '.' or '..' directory: skipping 'd/.'", "rm: refusing to remove '.' or '..' directory: skipping 'd/./'", @@ -785,6 +787,20 @@ fn test_current_or_parent_dir_rm4() { "rm: refusing to remove '.' or '..' directory: skipping '../'", "rm: refusing to remove '.' or '..' directory: skipping '..'", ]; + #[cfg(wasi_runner)] + let answers = [ + "rm: refusing to remove '.' or '..' directory: skipping 'd/.'", + "rm: refusing to remove '.' or '..' directory: skipping 'd/./'", + "rm: refusing to remove '.' or '..' directory: skipping 'd/./'", + "rm: refusing to remove '.' or '..' directory: skipping 'd/..'", + "rm: it is dangerous to operate recursively on 'd/../' (same as '/')", + "rm: use --no-preserve-root to override this failsafe", + "rm: refusing to remove '.' or '..' directory: skipping '.'", + "rm: it is dangerous to operate recursively on './' (same as '/')", + "rm: use --no-preserve-root to override this failsafe", + "rm: it is dangerous to operate recursively on '../' (same as '/')", + "rm: use --no-preserve-root to override this failsafe", + ]; let std_err_str = ts .ucmd() .arg("-rf") @@ -1001,7 +1017,11 @@ fn test_unreadable_and_nonempty_dir() { ucmd.args(&["-r", "-f", "a"]) .fails() - .stderr_only("rm: cannot remove 'a': Permission denied\n"); + .stderr_only(if cfg!(wasi_runner) { + "rm: cannot remove 'a': Directory not empty\n" + } else { + "rm: cannot remove 'a': Permission denied\n" + }); assert!(at.dir_exists("a/b")); assert!(at.dir_exists("a")); } @@ -1016,7 +1036,11 @@ fn test_recursive_remove_unreadable_subdir() { at.set_mode("foo/bar", 0o0000); let result = ucmd.args(&["-r", "-f", "foo"]).fails(); + + #[cfg(not(wasi_runner))] result.stderr_contains("Permission denied"); + #[cfg(wasi_runner)] + result.stderr_contains("Directory not empty"); result.stderr_contains("foo/bar"); at.set_mode("foo/bar", 0o0755); @@ -1055,7 +1079,11 @@ fn test_inaccessible_dir_interactive() { ucmd.args(&["-i", "-d", "dir"]) .pipe_in("y\n") .succeeds() - .stderr_only("rm: attempt removal of inaccessible directory 'dir'? "); + .stderr_only(if cfg!(wasi_runner) { + "rm: remove directory 'dir'? " + } else { + "rm: attempt removal of inaccessible directory 'dir'? " + }); assert!(!at.dir_exists("dir")); } @@ -1072,12 +1100,13 @@ fn test_inaccessible_dir_recursive() { } #[test] +#[cfg_attr(wasip2_runner, ignore)] #[cfg(any(target_os = "linux", target_os = "wasi"))] fn test_non_utf8_paths() { use std::ffi::OsStr; #[cfg(target_os = "linux")] use std::os::unix::ffi::OsStrExt; - #[cfg(target_os = "wasi")] + #[cfg(all(target_os = "wasi", target_env = "p1"))] use std::os::wasi::ffi::OsStrExt; let scene = TestScenario::new(util_name!()); @@ -1309,8 +1338,9 @@ fn test_symlink_to_readonly_no_prompt() { } /// Test that --preserve-root properly detects symlinks pointing to root. -#[cfg(unix)] #[test] +#[cfg(unix)] +#[cfg_attr(wasi_runner, ignore)] fn test_preserve_root_symlink_to_root() { let (at, mut ucmd) = at_and_ucmd!(); @@ -1331,8 +1361,9 @@ fn test_preserve_root_symlink_to_root() { } /// Test that --preserve-root properly detects nested symlinks pointing to root. -#[cfg(unix)] #[test] +#[cfg(unix)] +#[cfg_attr(wasi_runner, ignore)] fn test_preserve_root_nested_symlink_to_root() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_tee.rs b/tests/by-util/test_tee.rs index 2948eaa045b..6a0b70b1bfd 100644 --- a/tests/by-util/test_tee.rs +++ b/tests/by-util/test_tee.rs @@ -19,10 +19,14 @@ use std::time::Duration; #[test] #[cfg(unix)] fn test_error_stdin_directory() { - new_ucmd!() + let cmd = new_ucmd!() .set_stdin(std::fs::File::open(".").unwrap()) - .fails_with_code(1) - .stderr_is("tee: read error: Is a directory\n"); + .fails_with_code(1); + if cfg!(wasip2_runner) { + cmd.stderr_is("tee: read error: I/O error\n"); + } else { + cmd.stderr_is("tee: read error: Is a directory\n"); + } } #[test] diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index bfb490a56a5..d87455b04ac 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -30,7 +30,11 @@ fn test_invalid_input() { if std::env::var("UUTESTS_WASM_RUNNER").is_ok() { // On WASI the fluent translation key may appear instead of the // translated text, but the OS error string is still present. - cmd.stderr_contains("Is a directory"); + if cfg!(wasip2_runner) { + cmd.stderr_contains("tr: tr-error-read-error: I/O error"); + } else { + cmd.stderr_contains("Is a directory"); + } } else { cmd.stderr_contains("tr: read error: Is a directory"); } diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index 62f100b4293..35ee50ed7a2 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -797,7 +797,11 @@ fn files0_from_dir() { #[cfg(not(windows))] macro_rules! dir_err { ($p:literal) => { - concat!("wc: ", $p, ": read error: Is a directory\n") + if cfg!(wasip2_runner) { + concat!("wc: ", $p, ": read error: I/O error\n") + } else { + concat!("wc: ", $p, ": read error: Is a directory\n") + } }; } #[cfg(windows)] diff --git a/tests/by-util/test_yes.rs b/tests/by-util/test_yes.rs index c638915be94..6752fbc2def 100644 --- a/tests/by-util/test_yes.rs +++ b/tests/by-util/test_yes.rs @@ -96,11 +96,12 @@ fn test_piped_to_dev_full() { } #[test] +#[cfg_attr(wasip2_runner, ignore)] #[cfg(any(unix, target_os = "wasi"))] fn test_non_utf8() { #[cfg(unix)] use std::os::unix::ffi::OsStrExt; - #[cfg(target_os = "wasi")] + #[cfg(all(target_os = "wasi", target_env = "p1"))] use std::os::wasi::ffi::OsStrExt; run( From 5feb45eb6a5cb75235ed9243ed6b637797edf69f Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Sat, 6 Jun 2026 02:23:45 +0100 Subject: [PATCH 3/3] ci: add new wasm32-wasip2 target to workflow --- .github/workflows/CICD.yml | 1 + .github/workflows/code-quality.yml | 1 + .github/workflows/wasi.yml | 35 +++++++++++++++++++----------- docs/src/platforms.md | 2 +- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index ec6493e9ed0..7efd1e59084 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -381,6 +381,7 @@ jobs: - { os: ubuntu-latest , target: x86_64-unknown-netbsd, features: "feat_os_unix", use-cross: use-cross , skip-tests: true , check-only: true } - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true , check-only: true } - { os: ubuntu-latest , target: wasm32-wasip1, default-features: false, features: feat_wasm, skip-tests: true } + - { os: ubuntu-latest , target: wasm32-wasip2, default-features: false, features: feat_wasm, skip-tests: true } - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_unix, workspace-tests: true } # M1 CPU # PR #7964: chcon should not break build without the feature. cargo check is enough to detect it. - { os: macos-latest , target: aarch64-apple-darwin , workspace-tests: true, check-only: true } # M1 CPU diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index b450b3f5883..00b1e91579a 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -75,6 +75,7 @@ jobs: - { os: macos-latest , features: all , workspace: true } - { os: windows-latest , features: feat_os_windows } - { os: ubuntu-latest , features: feat_wasm , target: wasm32-wasip1 } + - { os: ubuntu-latest , features: feat_wasm , target: wasm32-wasip2 } steps: - uses: actions/checkout@v6 with: diff --git a/.github/workflows/wasi.yml b/.github/workflows/wasi.yml index 320c0943f55..58bce32a3d3 100644 --- a/.github/workflows/wasi.yml +++ b/.github/workflows/wasi.yml @@ -20,14 +20,22 @@ jobs: test_wasi: name: Tests runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + job: + - { target: wasm32-wasip1, rust-flags: "--cfg wasi_runner" } + - { target: wasm32-wasip2, rust-flags: "--cfg wasi_runner --cfg wasip2_runner" } steps: - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable with: - targets: wasm32-wasip1 + targets: ${{ matrix.job.target }} - uses: Swatinem/rust-cache@v2 + with: + key: "${{ matrix.job.target }}" - name: Install wasmtime run: | curl https://wasmtime.dev/install.sh -sSf | bash @@ -35,33 +43,34 @@ jobs: - name: Run unit tests env: CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime + CARGO_TARGET_WASM32_WASIP2_RUNNER: wasmtime run: | - # Get all utilities and exclude ones that don't compile for wasm32-wasip1 + # Get all utilities and exclude ones that don't compile for ${{ matrix.job.target }} EXCLUDE="df|du|env|expr|more|tac|test" UTILS=$(./util/show-utils.sh | tr ' ' '\n' | grep -vE "^($EXCLUDE)$" | sed 's/^/-p uu_/' | tr '\n' ' ') - cargo test --target wasm32-wasip1 --no-default-features $UTILS + cargo test --target ${{ matrix.job.target }} --no-default-features $UTILS - name: Run integration tests via wasmtime env: - RUSTFLAGS: --cfg wasi_runner + RUSTFLAGS: ${{ matrix.job.rust-flags }} run: | # Build the WASI binary - cargo build --target wasm32-wasip1 --no-default-features --features feat_wasm + cargo build --target ${{ matrix.job.target }} --no-default-features --features feat_wasm # Run host-compiled integration tests against the WASI binary. # Tests incompatible with WASI are annotated with # #[cfg_attr(wasi_runner, ignore)] in the test source files. # TODO: add integration tests for these tools as WASI support is extended: - # arch b2sum cat cksum cp csplit date dir dircolors fmt join + # arch b2sum cksum cp csplit date dir dircolors fmt join # ls md5sum mkdir mv nproc pathchk pr printenv ptx pwd readlink - # realpath rm rmdir seq sha1sum sha224sum sha256sum sha384sum + # realpath seq sha1sum sha224sum sha256sum sha384sum # sha512sum shred sleep sort split tail touch tsort uname uniq # vdir yes - UUTESTS_BINARY_PATH="$(pwd)/target/wasm32-wasip1/debug/coreutils.wasm" \ + UUTESTS_BINARY_PATH="$(pwd)/target/${{ matrix.job.target }}/debug/coreutils.wasm" \ UUTESTS_WASM_RUNNER=wasmtime \ cargo test --test tests -- \ test_base32:: test_base64:: test_basenc:: test_basename:: \ - test_comm:: test_cut:: test_dirname:: test_echo:: \ - test_expand:: test_factor:: test_false:: test_fold:: \ - test_head:: test_link:: test_ln:: test_nl:: test_numfmt:: \ - test_od:: test_paste:: test_printf:: test_shuf:: test_sum:: \ - test_tee:: test_tr:: test_true:: test_truncate:: \ + test_cat:: test_comm:: test_cut:: test_dirname:: test_echo:: \ + test_expand:: test_factor:: test_false:: test_fold:: test_head:: \ + test_link:: test_ln:: test_nl:: test_numfmt:: test_od:: \ + test_paste:: test_printf:: test_rm:: test_rmdir:: test_shuf:: \ + test_sum:: test_tee:: test_tr:: test_true:: test_truncate:: \ test_unexpand:: test_unlink:: test_wc:: diff --git a/docs/src/platforms.md b/docs/src/platforms.md index 3bf70b52a42..3aa5bba7ec6 100644 --- a/docs/src/platforms.md +++ b/docs/src/platforms.md @@ -27,7 +27,7 @@ The platforms in tier 1 and the platforms that we test in CI are listed below. | **FreeBSD** | `x86_64-unknown-freebsd` | | **OpenBSD** | `x86_64-unknown-openbsd` | | **Android** | `x86_64-linux-android` | -| **wasm32** | `wasm32-wasip1` | +| **wasm32** | `wasm32-wasip1`
`wasm32-wasip2` | The platforms in tier 2 are more vague, but include: