Start to Add WASM Support Again (#14418)

<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->

# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->
The [nushell/demo](https://github.com/nushell/demo) project successfully
demonstrated running Nushell in the browser using WASM. However, the
current version of Nushell cannot be easily built for the
`wasm32-unknown-unknown` target, the default for `wasm-bindgen`.

This PR introduces initial support for the `wasm32-unknown-unknown`
target by disabling OS-dependent features such as filesystem access, IO,
and platform/system-specific functionality. This separation is achieved
using a new `os` feature in the following crates:

 - `nu-cmd-lang`
 - `nu-command`
 - `nu-engine`
 - `nu-protocol`

The `os` feature includes all functionality that interacts with an
operating system. It is enabled by default, but can be disabled using
`--no-default-features`. All crates that depend on these core crates now
use `--no-default-features` to allow compilation for WASM.

To demonstrate compatibility, the following script builds all crates
expected to work with WASM. Direct user interaction, running external
commands, working with plugins, and features requiring `openssl` are out
of scope for now due to their complexity or reliance on C libraries,
which are difficult to compile and link in a WASM environment.

```nushell
[ # compatible crates
	"nu-cmd-base",
	"nu-cmd-extra",
	"nu-cmd-lang",
	"nu-color-config",
	"nu-command",
	"nu-derive-value",
	"nu-engine",
	"nu-glob",
	"nu-json",
	"nu-parser",
	"nu-path",
	"nu-pretty-hex",
	"nu-protocol",
	"nu-std",
	"nu-system",
	"nu-table",
	"nu-term-grid",
	"nu-utils",
	"nuon"
] | each {cargo build -p $in --target wasm32-unknown-unknown --no-default-features}
```

## Caveats
This PR has a few caveats:
1. **`miette` and `terminal-size` Dependency Issue**
`miette` depends on `terminal-size`, which uses `rustix` when the target
is not Windows. However, `rustix` requires `std::os::unix`, which is
unavailable in WASM. To address this, I opened a
[PR](https://github.com/eminence/terminal-size/pull/68) for
`terminal-size` to conditionally compile `rustix` only when the target
is Unix. For now, the `Cargo.toml` includes patches to:
    - Use my forked version of `terminal-size`.
- ~~Use an unreleased version of `miette` that depends on
`terminal-size@0.4`.~~

These patches are temporary and can be removed once the upstream changes
are merged and released.

2. **Test Output Adjustments**
Due to the slight bump in the `miette` version, one test required
adjustments to accommodate minor formatting changes in the error output,
such as shifted newlines.

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->
This shouldn't break anything but allows using some crates for targeting
`wasm32-unknown-unknown` to revive the demo page eventually.

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`

I did not add any extra tests, I just checked that compiling works, also
when using the host target but unselecting the `os` feature.

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
~~Breaking the wasm support can be easily done by adding some `use`s or
by adding a new dependency, we should definitely add some CI that also
at least builds against wasm to make sure that building for it keep
working.~~
I added a job to build wasm.

---------

Co-authored-by: Ian Manske <ian.manske@pm.me>
This commit is contained in:
Piepmatz
2024-11-30 14:57:11 +01:00
committed by GitHub
parent 07a37f9b47
commit 3d5f853b03
48 changed files with 490 additions and 234 deletions

View File

@ -1458,6 +1458,17 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"#
#[label = "while running this code"]
span: Option<Span>,
},
#[error("OS feature is disabled: {msg}")]
#[diagnostic(
code(nu::shell::os_disabled),
help("You're probably running outside an OS like a browser, we cannot support this")
)]
DisabledOsSupport {
msg: String,
#[label = "while running this code"]
span: Option<Span>,
},
}
impl ShellError {

View File

@ -1,3 +1,4 @@
#![cfg_attr(not(feature = "os"), allow(unused))]
#![doc = include_str!("../README.md")]
mod alias;
pub mod ast;
@ -17,6 +18,7 @@ pub mod parser_path;
mod pipeline;
#[cfg(feature = "plugin")]
mod plugin;
#[cfg(feature = "os")]
pub mod process;
mod signature;
pub mod span;

View File

@ -1,8 +1,7 @@
//! Module managing the streaming of raw bytes between pipeline elements
use crate::{
process::{ChildPipe, ChildProcess},
ErrSpan, IntoSpanned, PipelineData, ShellError, Signals, Span, Type, Value,
};
#[cfg(feature = "os")]
use crate::process::{ChildPipe, ChildProcess};
use crate::{ErrSpan, IntoSpanned, PipelineData, ShellError, Signals, Span, Type, Value};
use serde::{Deserialize, Serialize};
#[cfg(unix)]
use std::os::fd::OwnedFd;
@ -24,6 +23,7 @@ use std::{
pub enum ByteStreamSource {
Read(Box<dyn Read + Send + 'static>),
File(File),
#[cfg(feature = "os")]
Child(Box<ChildProcess>),
}
@ -32,6 +32,7 @@ impl ByteStreamSource {
match self {
ByteStreamSource::Read(read) => Some(SourceReader::Read(read)),
ByteStreamSource::File(file) => Some(SourceReader::File(file)),
#[cfg(feature = "os")]
ByteStreamSource::Child(mut child) => child.stdout.take().map(|stdout| match stdout {
ChildPipe::Pipe(pipe) => SourceReader::File(convert_file(pipe)),
ChildPipe::Tee(tee) => SourceReader::Read(tee),
@ -40,9 +41,16 @@ impl ByteStreamSource {
}
/// Source is a `Child` or `File`, rather than `Read`. Currently affects trimming
fn is_external(&self) -> bool {
#[cfg(feature = "os")]
pub fn is_external(&self) -> bool {
matches!(self, ByteStreamSource::Child(..))
}
#[cfg(not(feature = "os"))]
pub fn is_external(&self) -> bool {
// without os support we never have externals
false
}
}
impl Debug for ByteStreamSource {
@ -50,6 +58,7 @@ impl Debug for ByteStreamSource {
match self {
ByteStreamSource::Read(_) => f.debug_tuple("Read").field(&"..").finish(),
ByteStreamSource::File(file) => f.debug_tuple("File").field(file).finish(),
#[cfg(feature = "os")]
ByteStreamSource::Child(child) => f.debug_tuple("Child").field(child).finish(),
}
}
@ -247,6 +256,7 @@ impl ByteStream {
///
/// The type is implicitly `Unknown`, as it's not typically known whether child processes will
/// return text or binary.
#[cfg(feature = "os")]
pub fn child(child: ChildProcess, span: Span) -> Self {
Self::new(
ByteStreamSource::Child(Box::new(child)),
@ -260,6 +270,7 @@ impl ByteStream {
///
/// The type is implicitly `Unknown`, as it's not typically known whether stdin is text or
/// binary.
#[cfg(feature = "os")]
pub fn stdin(span: Span) -> Result<Self, ShellError> {
let stdin = os_pipe::dup_stdin().err_span(span)?;
let source = ByteStreamSource::File(convert_file(stdin));
@ -271,6 +282,14 @@ impl ByteStream {
))
}
#[cfg(not(feature = "os"))]
pub fn stdin(span: Span) -> Result<Self, ShellError> {
Err(ShellError::DisabledOsSupport {
msg: "Stdin is not supported".to_string(),
span: Some(span),
})
}
/// Create a [`ByteStream`] from a generator function that writes data to the given buffer
/// when called, and returns `Ok(false)` on end of stream.
pub fn from_fn(
@ -432,6 +451,7 @@ impl ByteStream {
match self.stream {
ByteStreamSource::Read(..) => Err(self),
ByteStreamSource::File(file) => Ok(file.into()),
#[cfg(feature = "os")]
ByteStreamSource::Child(child) => {
if let ChildProcess {
stdout: Some(ChildPipe::Pipe(stdout)),
@ -453,6 +473,7 @@ impl ByteStream {
///
/// This will only succeed if the [`ByteStreamSource`] of the [`ByteStream`] is [`Child`](ByteStreamSource::Child).
/// All other cases return an `Err` with the original [`ByteStream`] in it.
#[cfg(feature = "os")]
pub fn into_child(self) -> Result<ChildProcess, Self> {
if let ByteStreamSource::Child(child) = self.stream {
Ok(*child)
@ -477,6 +498,7 @@ impl ByteStream {
file.read_to_end(&mut buf).err_span(self.span)?;
Ok(buf)
}
#[cfg(feature = "os")]
ByteStreamSource::Child(child) => child.into_bytes(),
}
}
@ -551,6 +573,7 @@ impl ByteStream {
Ok(())
}
ByteStreamSource::File(_) => Ok(()),
#[cfg(feature = "os")]
ByteStreamSource::Child(child) => child.wait(),
}
}
@ -575,6 +598,7 @@ impl ByteStream {
ByteStreamSource::File(file) => {
copy_with_signals(file, dest, span, signals)?;
}
#[cfg(feature = "os")]
ByteStreamSource::Child(mut child) => {
// All `OutDest`s except `OutDest::PipeSeparate` will cause `stderr` to be `None`.
// Only `save`, `tee`, and `complete` set the stderr `OutDest` to `OutDest::PipeSeparate`,