diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000000..529fe26067 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,6 @@ +[target.wasm32-wasip1] +rustflags = [ + "-L/opt/wasi-sdk/share/wasi-sysroot/lib/wasm32-wasi", + "-Clink-arg=-lc++", + "-Clink-arg=-lc++abi" +] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a7df89522..3a72b7cf07 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,6 +37,8 @@ jobs: # detects that they need cross). - target: x86_64-unknown-linux-musl os: ubuntu-22.04 + - target: wasm32-wasip1 + os: ubuntu-24.04 - target: aarch64-unknown-linux-gnu os: ubuntu-22.04 - target: aarch64-pc-windows-msvc @@ -46,6 +48,13 @@ jobs: runs-on: ${{ matrix.os }} steps: # v4.2.2 + - run: | + wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-29/wasi-sdk-29.0-x86_64-linux.deb + sudo apt-get install ./wasi-sdk-29.0-x86_64-linux.deb + rm ./wasi-sdk-29.0-x86_64-linux.deb + rustup override set stable + rustup target add wasm32-wasip1 + if: ${{ matrix.target == 'wasm32-wasip1' }} - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: taiki-e/upload-rust-binary-action@v1 with: @@ -59,6 +68,11 @@ jobs: env: # (required) GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WASI_SDK_PATH: /opt/wasi-sdk + WASI_SYSROOT: /opt/wasi-sdk/share/wasi-sysroot + CC_wasm32_wasip1: /opt/wasi-sdk/bin/clang + AR_wasm32_wasip1: /opt/wasi-sdk/bin/llvm-ar + CFLAGS_wasm32_wasip1: "--sysroot=/opt/wasi-sdk/share/wasi-sysroot" push_crates_io: runs-on: ubuntu-22.04 diff --git a/Cargo.toml b/Cargo.toml index 4c3f893cee..a86367197c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,6 @@ tree_magic_mini = "3.1.6" bumpalo = "3.16.0" unicode-width = "0.1.9" -crossterm = { version = "0.28.0", features = ["windows"] } glob = "0.3.1" strum = { version = "0.26", features = ["derive"] } hashbrown = "0.14.0" @@ -128,7 +127,10 @@ tree-sitter-xml = "0.7.0" tree-sitter-yaml = "0.7.0" tree-sitter-zig = "1.1.2" -[target.'cfg(not(any(target_env = "msvc", target_os = "illumos", target_os = "freebsd")))'.dependencies] +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +crossterm = { version = "0.28.0", features = ["windows"] } + +[target.'cfg(not(any(target_env = "msvc", target_os = "illumos", target_os = "freebsd", target_arch = "wasm32")))'.dependencies] tikv-jemallocator = "0.6" [dev-dependencies] diff --git a/src/files.rs b/src/files.rs index 9534ab9f8a..5ca5d4bc53 100644 --- a/src/files.rs +++ b/src/files.rs @@ -78,6 +78,7 @@ fn read_file_arg(file_arg: &FileArgument) -> std::io::Result> { } /// Write a human-friendly description of `e` to stderr. +#[cfg(not(target_arch = "wasm32"))] fn eprint_read_error(file_arg: &FileArgument, e: &std::io::Error) { match e.kind() { std::io::ErrorKind::NotFound => { @@ -95,12 +96,26 @@ fn eprint_read_error(file_arg: &FileArgument, e: &std::io::Error) { }; } +#[cfg(target_arch = "wasm32")] +fn eprint_read_error(file_arg: &FileArgument, e: &std::io::Error) { + // For the browser/WASM demo, fail softly so we can keep running. + println!("WASM read error on {} ({:?})", file_arg, e.kind()); +} + pub(crate) fn read_or_die(path: &Path) -> Vec { match fs::read(path) { Ok(src) => src, Err(e) => { eprint_read_error(&FileArgument::NamedPath(path.to_path_buf()), &e); - std::process::exit(EXIT_BAD_ARGUMENTS); + #[cfg(target_arch = "wasm32")] + { + // Return empty content in WASM to avoid aborting the demo. + return Vec::new(); + } + #[cfg(not(target_arch = "wasm32"))] + { + std::process::exit(EXIT_BAD_ARGUMENTS); + } } } } diff --git a/src/main.rs b/src/main.rs index 27bbadb8e0..600206468d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,10 +98,20 @@ use crate::parse::syntax; /// /// For reference, Jemalloc uses 10-20% more time (although up to 33% /// more instructions) when testing on sample files. -#[cfg(not(any(target_env = "msvc", target_os = "illumos", target_os = "freebsd")))] +#[cfg(not(any( + target_env = "msvc", + target_os = "illumos", + target_os = "freebsd", + target_arch = "wasm32" +)))] use tikv_jemallocator::Jemalloc; -#[cfg(not(any(target_env = "msvc", target_os = "illumos", target_os = "freebsd")))] +#[cfg(not(any( + target_env = "msvc", + target_os = "illumos", + target_os = "freebsd", + target_arch = "wasm32" +)))] #[global_allocator] static GLOBAL: Jemalloc = Jemalloc; diff --git a/src/options.rs b/src/options.rs index 6231cf9472..52b14821b4 100644 --- a/src/options.rs +++ b/src/options.rs @@ -8,7 +8,6 @@ use std::{ }; use clap::{crate_authors, crate_description, value_parser, Arg, ArgAction, Command}; -use crossterm::tty::IsTty; use owo_colors::OwoColorize as _; use crate::{ @@ -51,6 +50,30 @@ pub(crate) struct DisplayOptions { pub(crate) const DEFAULT_TERMINAL_WIDTH: usize = 80; +#[cfg(not(target_arch = "wasm32"))] +fn stdout_is_tty() -> bool { + use crossterm::tty::IsTty; + std::io::stdout().is_tty() +} + +#[cfg(target_arch = "wasm32")] +fn stdout_is_tty() -> bool { + false +} + +#[cfg(not(target_arch = "wasm32"))] +fn terminal_columns() -> Option { + crossterm::terminal::size() + .ok() + .map(|(cols, _rows)| cols as usize) + .filter(|cols| *cols > 0) +} + +#[cfg(target_arch = "wasm32")] +fn terminal_columns() -> Option { + None +} + impl Default for DisplayOptions { fn default() -> Self { Self { @@ -125,7 +148,7 @@ fn app() -> clap::Command { )); after_help.push_str("\n\nSee the full manual at "); - if std::io::stdout().is_tty() { + if stdout_is_tty() { // Make the link to the manual clickable in terminals that // support OSC 8, the ANSI escape code for hyperlinks. // @@ -596,6 +619,7 @@ fn common_path_suffix(lhs_path: &Path, rhs_path: &Path) -> Option { } /// Does `path` look like "/tmp/git-blob-abcdef/modified_field.txt"? +#[cfg(not(target_arch = "wasm32"))] fn is_git_tmpfile(path: &Path) -> bool { let Ok(rel_path) = path.strip_prefix(std::env::temp_dir()) else { return false; @@ -612,6 +636,12 @@ fn is_git_tmpfile(path: &Path) -> bool { .starts_with("git-blob-") } +#[cfg(target_arch = "wasm32")] +fn is_git_tmpfile(_path: &Path) -> bool { + // WASI environments may not have a writable /tmp; skip special-casing. + false +} + fn build_display_path(lhs_path: &FileArgument, rhs_path: &FileArgument) -> String { match (lhs_path, rhs_path) { (FileArgument::NamedPath(lhs), FileArgument::NamedPath(rhs)) => { @@ -1002,10 +1032,8 @@ pub(crate) fn parse_args() -> Mode { /// Try to work out the width of the terminal we're on, or fall back /// to a sensible default value. fn detect_terminal_width() -> usize { - if let Ok((columns, _rows)) = crossterm::terminal::size() { - if columns > 0 { - return columns.into(); - } + if let Some(columns) = terminal_columns() { + return columns; } // If crossterm couldn't detect the terminal width, use the @@ -1036,7 +1064,7 @@ pub(crate) fn should_use_color(color_output: ColorOutput) -> bool { fn detect_color_support() -> bool { // TODO: consider following the env parsing logic in git_config_bool // in config.c. - std::io::stdout().is_tty() || env::var("GIT_PAGER_IN_USE").is_ok() + stdout_is_tty() || env::var("GIT_PAGER_IN_USE").is_ok() } #[cfg(test)]