mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 18:15:04 +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:
80
crates/nu-command/src/env/config/config_.rs
vendored
80
crates/nu-command/src/env/config/config_.rs
vendored
@ -1,4 +1,6 @@
|
||||
use nu_engine::{command_prelude::*, get_full_help};
|
||||
use nu_cmd_base::util::get_editor;
|
||||
use nu_engine::{command_prelude::*, env_to_strings, get_full_help};
|
||||
use nu_system::ForegroundChild;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigMeta;
|
||||
@ -36,3 +38,79 @@ impl Command for ConfigMeta {
|
||||
vec!["options", "setup"]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "os"))]
|
||||
pub(super) fn start_editor(
|
||||
_: &'static str,
|
||||
_: &EngineState,
|
||||
_: &mut Stack,
|
||||
call: &Call,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
Err(ShellError::DisabledOsSupport {
|
||||
msg: "Running external commands is not available without OS support.".to_string(),
|
||||
span: Some(call.head),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "os")]
|
||||
pub(super) fn start_editor(
|
||||
config_path: &'static str,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
// Find the editor executable.
|
||||
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
||||
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||
let cwd = engine_state.cwd(Some(stack))?;
|
||||
let editor_executable =
|
||||
crate::which(&editor_name, &paths, cwd.as_ref()).ok_or(ShellError::ExternalCommand {
|
||||
label: format!("`{editor_name}` not found"),
|
||||
help: "Failed to find the editor executable".into(),
|
||||
span: call.head,
|
||||
})?;
|
||||
|
||||
let Some(config_path) = engine_state.get_config_path(config_path) else {
|
||||
return Err(ShellError::GenericError {
|
||||
error: format!("Could not find $nu.{config_path}"),
|
||||
msg: format!("Could not find $nu.{config_path}"),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: vec![],
|
||||
});
|
||||
};
|
||||
let config_path = config_path.to_string_lossy().to_string();
|
||||
|
||||
// Create the command.
|
||||
let mut command = std::process::Command::new(editor_executable);
|
||||
|
||||
// Configure PWD.
|
||||
command.current_dir(cwd);
|
||||
|
||||
// Configure environment variables.
|
||||
let envs = env_to_strings(engine_state, stack)?;
|
||||
command.env_clear();
|
||||
command.envs(envs);
|
||||
|
||||
// Configure args.
|
||||
command.arg(config_path);
|
||||
command.args(editor_args);
|
||||
|
||||
// Spawn the child process. On Unix, also put the child process to
|
||||
// foreground if we're in an interactive session.
|
||||
#[cfg(windows)]
|
||||
let child = ForegroundChild::spawn(command)?;
|
||||
#[cfg(unix)]
|
||||
let child = ForegroundChild::spawn(
|
||||
command,
|
||||
engine_state.is_interactive,
|
||||
&engine_state.pipeline_externals_state,
|
||||
)?;
|
||||
|
||||
// Wrap the output into a `PipelineData::ByteStream`.
|
||||
let child = nu_protocol::process::ChildProcess::new(child, None, false, call.head)?;
|
||||
Ok(PipelineData::ByteStream(
|
||||
ByteStream::child(child, call.head),
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
61
crates/nu-command/src/env/config/config_env.rs
vendored
61
crates/nu-command/src/env/config/config_env.rs
vendored
@ -1,7 +1,4 @@
|
||||
use nu_cmd_base::util::get_editor;
|
||||
use nu_engine::{command_prelude::*, env_to_strings};
|
||||
use nu_protocol::{process::ChildProcess, ByteStream};
|
||||
use nu_system::ForegroundChild;
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigEnv;
|
||||
@ -81,60 +78,6 @@ impl Command for ConfigEnv {
|
||||
return Ok(Value::string(nu_utils::get_sample_env(), head).into_pipeline_data());
|
||||
}
|
||||
|
||||
// Find the editor executable.
|
||||
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
||||
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||
let cwd = engine_state.cwd(Some(stack))?;
|
||||
let editor_executable = crate::which(&editor_name, &paths, cwd.as_ref()).ok_or(
|
||||
ShellError::ExternalCommand {
|
||||
label: format!("`{editor_name}` not found"),
|
||||
help: "Failed to find the editor executable".into(),
|
||||
span: call.head,
|
||||
},
|
||||
)?;
|
||||
|
||||
let Some(env_path) = engine_state.get_config_path("env-path") else {
|
||||
return Err(ShellError::GenericError {
|
||||
error: "Could not find $nu.env-path".into(),
|
||||
msg: "Could not find $nu.env-path".into(),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: vec![],
|
||||
});
|
||||
};
|
||||
let env_path = env_path.to_string_lossy().to_string();
|
||||
|
||||
// Create the command.
|
||||
let mut command = std::process::Command::new(editor_executable);
|
||||
|
||||
// Configure PWD.
|
||||
command.current_dir(cwd);
|
||||
|
||||
// Configure environment variables.
|
||||
let envs = env_to_strings(engine_state, stack)?;
|
||||
command.env_clear();
|
||||
command.envs(envs);
|
||||
|
||||
// Configure args.
|
||||
command.arg(env_path);
|
||||
command.args(editor_args);
|
||||
|
||||
// Spawn the child process. On Unix, also put the child process to
|
||||
// foreground if we're in an interactive session.
|
||||
#[cfg(windows)]
|
||||
let child = ForegroundChild::spawn(command)?;
|
||||
#[cfg(unix)]
|
||||
let child = ForegroundChild::spawn(
|
||||
command,
|
||||
engine_state.is_interactive,
|
||||
&engine_state.pipeline_externals_state,
|
||||
)?;
|
||||
|
||||
// Wrap the output into a `PipelineData::ByteStream`.
|
||||
let child = ChildProcess::new(child, None, false, call.head)?;
|
||||
Ok(PipelineData::ByteStream(
|
||||
ByteStream::child(child, call.head),
|
||||
None,
|
||||
))
|
||||
super::config_::start_editor("env-path", engine_state, stack, call)
|
||||
}
|
||||
}
|
||||
|
61
crates/nu-command/src/env/config/config_nu.rs
vendored
61
crates/nu-command/src/env/config/config_nu.rs
vendored
@ -1,7 +1,4 @@
|
||||
use nu_cmd_base::util::get_editor;
|
||||
use nu_engine::{command_prelude::*, env_to_strings};
|
||||
use nu_protocol::{process::ChildProcess, ByteStream};
|
||||
use nu_system::ForegroundChild;
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigNu;
|
||||
@ -83,60 +80,6 @@ impl Command for ConfigNu {
|
||||
return Ok(Value::string(nu_utils::get_sample_config(), head).into_pipeline_data());
|
||||
}
|
||||
|
||||
// Find the editor executable.
|
||||
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
||||
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||
let cwd = engine_state.cwd(Some(stack))?;
|
||||
let editor_executable = crate::which(&editor_name, &paths, cwd.as_ref()).ok_or(
|
||||
ShellError::ExternalCommand {
|
||||
label: format!("`{editor_name}` not found"),
|
||||
help: "Failed to find the editor executable".into(),
|
||||
span: call.head,
|
||||
},
|
||||
)?;
|
||||
|
||||
let Some(config_path) = engine_state.get_config_path("config-path") else {
|
||||
return Err(ShellError::GenericError {
|
||||
error: "Could not find $nu.config-path".into(),
|
||||
msg: "Could not find $nu.config-path".into(),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: vec![],
|
||||
});
|
||||
};
|
||||
let config_path = config_path.to_string_lossy().to_string();
|
||||
|
||||
// Create the command.
|
||||
let mut command = std::process::Command::new(editor_executable);
|
||||
|
||||
// Configure PWD.
|
||||
command.current_dir(cwd);
|
||||
|
||||
// Configure environment variables.
|
||||
let envs = env_to_strings(engine_state, stack)?;
|
||||
command.env_clear();
|
||||
command.envs(envs);
|
||||
|
||||
// Configure args.
|
||||
command.arg(config_path);
|
||||
command.args(editor_args);
|
||||
|
||||
// Spawn the child process. On Unix, also put the child process to
|
||||
// foreground if we're in an interactive session.
|
||||
#[cfg(windows)]
|
||||
let child = ForegroundChild::spawn(command)?;
|
||||
#[cfg(unix)]
|
||||
let child = ForegroundChild::spawn(
|
||||
command,
|
||||
engine_state.is_interactive,
|
||||
&engine_state.pipeline_externals_state,
|
||||
)?;
|
||||
|
||||
// Wrap the output into a `PipelineData::ByteStream`.
|
||||
let child = ChildProcess::new(child, None, false, call.head)?;
|
||||
Ok(PipelineData::ByteStream(
|
||||
ByteStream::child(child, call.head),
|
||||
None,
|
||||
))
|
||||
super::config_::start_editor("config-path", engine_state, stack, call)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user