From d52ed2e1e15b029e919ef47d1254ef370f36d2fb 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 --- .../workspace.wordlist.txt | 1 + src/uucore/src/lib/features/fs.rs | 35 +++++++++++------ src/uucore/src/lib/lib.rs | 38 +++++++++++++------ src/uucore/src/lib/mods/display.rs | 8 +++- src/uucore/src/lib/mods/error.rs | 1 + 5 files changed, 58 insertions(+), 25 deletions(-) diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index 508f6d15fa1..0a1e5b86b92 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -133,6 +133,7 @@ EBADF EBUSY EEXIST EINVAL +EISDIR ENODATA ENOENT ENOSPC diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 7cd9041dd62..7712eb1b334 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -18,7 +18,7 @@ use std::fs::read_dir; use std::hash::Hash; use std::io::Stdin; use std::io::{Error, ErrorKind, Result as IOResult}; -#[cfg(unix)] +#[cfg(any(unix, all(target_os = "wasi", target_env = "p2")))] use std::os::fd::AsFd; #[cfg(unix)] use std::os::unix::fs::MetadataExt; @@ -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)] @@ -786,13 +795,17 @@ pub fn path_ends_with_terminator(path: &Path) -> bool { /// /// * `bool` - Returns `true` if stdin is a directory, `false` otherwise. pub fn is_stdin_directory(stdin: &Stdin) -> bool { - #[cfg(unix)] + #[cfg(any(unix, all(target_os = "wasi", target_env = "p2")))] { use mode::{S_IFDIR, S_IFMT}; - let mode = rustix::fs::fstat(stdin).unwrap().st_mode as u32; - // We use the S_IFMT mask ala S_ISDIR() to avoid mistaking - // sockets for directories. - mode & S_IFMT == S_IFDIR + if let Ok(stat) = rustix::fs::fstat(stdin.as_fd()) { + #[allow(clippy::unnecessary_cast)] + let mode = stat.st_mode as u32; + // We use the S_IFMT mask ala S_ISDIR() to avoid mistaking + // sockets for directories. + return mode & S_IFMT == S_IFDIR; + } + false } #[cfg(windows)] @@ -805,8 +818,8 @@ pub fn is_stdin_directory(stdin: &Stdin) -> bool { false } - // WASI: stdin is never a directory - #[cfg(target_os = "wasi")] + // WASI P1: stdin is never a directory + #[cfg(all(target_os = "wasi", target_env = "p1"))] { let _ = stdin; false diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 4dfffed2995..7f838277da2 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() { @@ -486,12 +491,15 @@ pub fn os_str_as_bytes_lossy(os_string: &OsStr) -> Cow<'_, [u8]> { /// /// This always succeeds on unix platforms, /// 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))] +#[cfg_attr( + any(unix, all(target_os = "wasi", target_env = "p1")), + 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"), )?))) @@ -501,12 +509,15 @@ pub fn os_str_from_bytes(bytes: &[u8]) -> error::UResult> { /// /// This always succeeds on unix platforms, /// 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))] +#[cfg_attr( + any(unix, all(target_os = "wasi", target_env = "p1")), + 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") })?)) @@ -516,11 +527,14 @@ pub fn os_string_from_vec(vec: Vec) -> error::UResult { /// /// This always succeeds on unix platforms, /// 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))] +#[cfg_attr( + any(unix, all(target_os = "wasi", target_env = "p1")), + 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")))] { diff --git a/src/uucore/src/lib/mods/error.rs b/src/uucore/src/lib/mods/error.rs index d2239d12875..b4e35928c68 100644 --- a/src/uucore/src/lib/mods/error.rs +++ b/src/uucore/src/lib/mods/error.rs @@ -432,6 +432,7 @@ impl Display for UIoError { WriteZero => "Write zero", Interrupted => "Interrupted", UnexpectedEof => "Unexpected end of file", + IsADirectory => "Is a directory", _ => { // TODO: When the new error variants // (https://github.com/rust-lang/rust/issues/86442) From 2bfb3b3010e92bba31c0288211fc4d919344f6a4 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Tue, 9 Jun 2026 22:14:14 +0100 Subject: [PATCH 2/3] ci: add new wasm32-wasip2 target to workflow --- .github/workflows/CICD.yml | 1 + .github/workflows/code-quality.yml | 1 + .github/workflows/wasi.yml | 22 ++++++++++++++++------ docs/src/platforms.md | 2 +- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 8caad1c8233..ccb80e4c866 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 1f477c84c05..17092ed02b0 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@v7.0.0 with: diff --git a/.github/workflows/wasi.yml b/.github/workflows/wasi.yml index 1edb94db595..ad5ac99f1e1 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@v7.0.0 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,17 +43,19 @@ 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 + if: matrix.job.target == 'wasm32-wasip1' 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. @@ -55,7 +65,7 @@ jobs: # realpath rm rmdir 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:: \ 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: From 02c6ba873142002ce7b6323059731ce760a534d4 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Thu, 18 Jun 2026 18:50:14 +0100 Subject: [PATCH 3/3] yes: fix for non sigpipe in wasi --- .github/workflows/wasi.yml | 4 ++-- src/uu/yes/src/yes.rs | 18 ++++++++++++++++++ tests/by-util/test_yes.rs | 31 ++++++++++++------------------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/.github/workflows/wasi.yml b/.github/workflows/wasi.yml index ad5ac99f1e1..c578f6aa6dc 100644 --- a/.github/workflows/wasi.yml +++ b/.github/workflows/wasi.yml @@ -64,7 +64,7 @@ jobs: # ls md5sum mkdir mv nproc pathchk pr printenv ptx pwd readlink # realpath rm rmdir seq sha1sum sha224sum sha256sum sha384sum # sha512sum shred sleep sort split tail touch tsort uname uniq - # vdir yes + # vdir UUTESTS_BINARY_PATH="$(pwd)/target/${{ matrix.job.target }}/debug/coreutils.wasm" \ UUTESTS_WASM_RUNNER=wasmtime \ cargo test --test tests -- \ @@ -74,4 +74,4 @@ jobs: 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_unexpand:: test_unlink:: test_wc:: + test_unexpand:: test_unlink:: test_wc:: test_yes:: diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index 9d821feef5f..22b90d11ae6 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -29,6 +29,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // On Windows, silently handle broken pipe since there's no SIGPIPE #[cfg(windows)] Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(()), + // On WASI, there's no SIGPIPE signal. When the pipe closes, WASI may return various + // I/O error kinds. For the yes command's infinite write loop, any write error means we + // can't continue, so we treat common pipe-closure errors as normal termination. + #[cfg(target_os = "wasi")] + Err(err) + if matches!( + err.kind(), + io::ErrorKind::BrokenPipe + | io::ErrorKind::UnexpectedEof + | io::ErrorKind::WriteZero + | io::ErrorKind::ConnectionReset + | io::ErrorKind::ConnectionAborted + | io::ErrorKind::NotConnected + ) || err.raw_os_error().is_some() => + { + // Treat OS-level errors and connection-related errors as normal termination + Ok(()) + } Err(err) => Err(USimpleError::new( 1, translate!("yes-error-standard-output", "error" => strip_errno(&err)), diff --git a/tests/by-util/test_yes.rs b/tests/by-util/test_yes.rs index c638915be94..afc8b9e09ca 100644 --- a/tests/by-util/test_yes.rs +++ b/tests/by-util/test_yes.rs @@ -3,29 +3,22 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. use std::ffi::OsStr; -use std::process::ExitStatus; - -#[cfg(unix)] -use std::os::unix::process::ExitStatusExt; use uutests::new_ucmd; -#[cfg(unix)] -fn check_termination(result: ExitStatus) { - assert_eq!(result.signal(), Some(libc::SIGPIPE)); -} - -#[cfg(not(unix))] -fn check_termination(result: ExitStatus) { - assert!(result.success(), "yes did not exit successfully"); -} - const NO_ARGS: &[&str] = &[]; /// Run `yes`, capture some of the output, then check exit status. fn run(args: &[impl AsRef], expected: &[u8]) { let result = new_ucmd!().args(args).run_stdout_starts_with(expected); - check_termination(result.exit_status()); + + // On Unix systems (not WASI), yes should be terminated by SIGPIPE when the pipe closes. + // On WASI and Windows, there are no signals, so just check the process succeeded. + #[cfg(all(unix, not(wasi_runner)))] + result.signal_name_is("PIPE"); + + #[cfg(any(not(unix), wasi_runner))] + result.success(); } #[test] @@ -76,6 +69,7 @@ fn test_long_input() { #[test] #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +#[cfg_attr(wasi_runner, ignore)] fn test_piped_to_dev_full() { use std::fs::OpenOptions; @@ -96,12 +90,11 @@ fn test_piped_to_dev_full() { } #[test] -#[cfg(any(unix, target_os = "wasi"))] +#[cfg(unix)] +// WASI runners (wasmtime) require UTF-8 arguments, so skip this test when testing WASI binaries +#[cfg_attr(wasi_runner, ignore = "WASI: argv must be valid UTF-8")] fn test_non_utf8() { - #[cfg(unix)] use std::os::unix::ffi::OsStrExt; - #[cfg(target_os = "wasi")] - use std::os::wasi::ffi::OsStrExt; run( &[