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..c578f6aa6dc 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.
@@ -54,8 +64,8 @@ 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
- UUTESTS_BINARY_PATH="$(pwd)/target/wasm32-wasip1/debug/coreutils.wasm" \
+ # vdir
+ 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:: \
@@ -64,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/.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/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:
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/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)
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(
&[