Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/CICD.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
26 changes: 18 additions & 8 deletions .github/workflows/wasi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,42 @@ 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
echo "$HOME/.wasmtime/bin" >> $GITHUB_PATH
- 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.
Expand All @@ -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:: \
Expand All @@ -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::
1 change: 1 addition & 0 deletions .vscode/cspell.dictionaries/workspace.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ EBADF
EBUSY
EEXIST
EINVAL
EISDIR
ENODATA
ENOENT
ENOSPC
Expand Down
2 changes: 1 addition & 1 deletion docs/src/platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` <br> `wasm32-wasip2` |

The platforms in tier 2 are more vague, but include:

Expand Down
18 changes: 18 additions & 0 deletions src/uu/yes/src/yes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
35 changes: 24 additions & 11 deletions src/uucore/src/lib/features/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)]
Expand All @@ -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)]
Expand All @@ -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
Expand Down
38 changes: 26 additions & 12 deletions src/uucore/src/lib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand All @@ -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() {
Expand All @@ -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<Cow<'_, OsStr>> {
#[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"),
)?)))
Expand All @@ -501,12 +509,15 @@ pub fn os_str_from_bytes(bytes: &[u8]) -> error::UResult<Cow<'_, OsStr>> {
///
/// 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<u8>) -> error::UResult<OsString> {
#[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")
})?))
Expand All @@ -516,11 +527,14 @@ pub fn os_string_from_vec(vec: Vec<u8>) -> error::UResult<OsString> {
///
/// 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<Vec<u8>> {
#[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(|_| {
Expand Down
8 changes: 6 additions & 2 deletions src/uucore/src/lib/mods/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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")))]
{
Expand Down
1 change: 1 addition & 0 deletions src/uucore/src/lib/mods/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
31 changes: 12 additions & 19 deletions tests/by-util/test_yes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OsStr>], 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]
Expand Down Expand Up @@ -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;

Expand All @@ -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(
&[
Expand Down
Loading