mirror of
https://github.com/nushell/nushell.git
synced 2025-08-10 05:48:49 +02:00
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:
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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`,
|
||||
|
Reference in New Issue
Block a user