diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0aff0b5..4d3e761 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,11 @@ jobs: docs/src/**/*.md src/**/*.md - run: cargo clippy --all-targets -- -D warnings + - name: Clippy (WASI) + if: matrix.os == 'ubuntu-latest' + run: | + rustup target add wasm32-wasip1 + cargo clippy --target wasm32-wasip1 -- -D warnings style_spellcheck: name: Style/spellcheck @@ -108,6 +113,28 @@ jobs: env: RUST_BACKTRACE: '1' + test_wasi: + name: Build/Test (WASI) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip1 + - uses: Swatinem/rust-cache@v2 + - uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install wasmtime + run: | + curl https://wasmtime.dev/install.sh -sSf | bash + echo "$HOME/.wasmtime/bin" >> $GITHUB_PATH + - name: Build for WASI + run: cargo build --target wasm32-wasip1 + - name: Test with wasmtime + env: + CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime + # WASI does not support threads, so tests must run sequentially + run: cargo test --target wasm32-wasip1 -- --test-threads 1 + coverage: name: Code Coverage runs-on: ${{ matrix.job.os }} diff --git a/README.md b/README.md index ede0582..198d568 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # platform-info + [![Crates.io](https://img.shields.io/crates/v/platform-info.svg)](https://crates.io/crates/platform-info) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) @@ -48,6 +49,34 @@ GNU/Linux > Using `cargo run --example ex` will build and execute this [example code](examples/ex.rs). +### WASI + +To cross-compile and run the example under [WASI](https://wasi.dev/), first install +[wasmtime](https://wasmtime.dev/): + +```shell +curl https://wasmtime.dev/install.sh -sSf | bash +``` + +Then build and run: + +```shell +cargo build --target wasm32-wasip1 --example ex +wasmtime target/wasm32-wasip1/debug/examples/ex.wasm +``` + +This should display: + +```text +wasi +localhost +0.0.0 +0.0.0 +wasm32 +wasm32 +WASI +``` + Other examples can be found in the [examples](examples) directory. ## License diff --git a/src/lib_impl.rs b/src/lib_impl.rs index a48dcfd..4dfd2e0 100644 --- a/src/lib_impl.rs +++ b/src/lib_impl.rs @@ -99,19 +99,24 @@ const HOST_OS_NAME: &str = if cfg!(all( "solaris" } else if cfg!(target_os = "cygwin") { "Cygwin" +} else if cfg!(target_os = "wasi") { + "WASI" } else { "unknown" }; //=== platform-specific module code -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "wasi")))] #[path = "platform/unix.rs"] mod target; #[cfg(windows)] #[path = "platform/windows.rs"] mod target; -#[cfg(not(any(unix, windows)))] +#[cfg(target_os = "wasi")] +#[path = "platform/wasi.rs"] +mod target; +#[cfg(not(any(unix, windows, target_os = "wasi")))] #[path = "platform/unknown.rs"] mod target; diff --git a/src/platform/unix.rs b/src/platform/unix.rs index aa866f5..aa4585a 100644 --- a/src/platform/unix.rs +++ b/src/platform/unix.rs @@ -123,7 +123,7 @@ impl Debug for UTSName { .field("release", &oss_from_cstr(&self.0.release)) .field("version", &oss_from_cstr(&self.0.version)) .field("machine", &oss_from_cstr(&self.0.machine)); - // The domainname field is not part of the POSIX standard but a GNU extension. Therefor + // The domainname field is not part of the POSIX standard but a GNU extension. Therefore // BSD-like platforms and solaris/illumos are missing the domainname field. #[cfg(not(any( target_os = "aix", @@ -161,7 +161,7 @@ impl PartialEq for UTSName { other.0.version, other.0.machine, ); - // The domainname field is not part of the POSIX standard but a GNU extension. Therefor + // The domainname field is not part of the POSIX standard but a GNU extension. Therefore // BSD-like platforms and solaris/illumos are missing the domainname field. #[cfg(not(any( target_os = "aix", diff --git a/src/platform/wasi.rs b/src/platform/wasi.rs new file mode 100644 index 0000000..8e266de --- /dev/null +++ b/src/platform/wasi.rs @@ -0,0 +1,140 @@ +// This file is part of the platform-info package. +// +// For the full copyright and license information, please view the LICENSE file +// that was distributed with this source code. + +// spell-checker:ignore (API) nodename osname sysname gethostname +// spell-checker:ignore (runtimes) wasmtime wasmer wazero + +#![warn(unused_results)] // enable warnings for unused results + +use std::ffi::{OsStr, OsString}; + +use crate::{PlatformInfoAPI, PlatformInfoError, UNameAPI}; + +/// Platform information for WASI (WebAssembly System Interface). +/// +/// WASI does not provide a `uname()` syscall, so we return values +/// that describe the WebAssembly platform. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PlatformInfo { + sysname: OsString, + nodename: OsString, + release: OsString, + version: OsString, + machine: OsString, + processor: OsString, + osname: OsString, +} + +/// Try to detect the hostname from environment variables. +/// +/// WASI (neither preview 1 nor preview 2) provides a `gethostname` syscall or +/// equivalent WIT interface. The `wasi-sockets` proposal lists hostname as a +/// future goal. Until then, the best we can do is check environment variables +/// that the host runtime may pass (e.g. `wasmtime --env HOSTNAME=$(hostname)`). +fn detect_nodename() -> OsString { + for var in &["HOSTNAME", "COMPUTERNAME"] { + if let Ok(val) = std::env::var(var) { + if !val.is_empty() { + return OsString::from(val); + } + } + } + OsString::from("localhost") +} + +impl PlatformInfoAPI for PlatformInfo { + /// Create a new `PlatformInfo` for WASI. + /// + /// WASI does not provide a `uname()` syscall, and no WASI runtime (wasmtime, + /// wasmer, WasmEdge, wazero) exposes its identity or version to guest modules. + /// There is no standard or convention for runtime self-identification. + /// + /// We follow the same approach as wasi-libc's `uname()` implementation, which + /// returns hardcoded values: `sysname="wasi"`, `release="0.0.0"`, + /// `version="0.0.0"`, `machine="wasm32"`. + /// + /// ref: + fn new() -> Result { + Ok(Self { + sysname: OsString::from("wasi"), + nodename: detect_nodename(), + release: OsString::from("0.0.0"), + version: OsString::from("0.0.0"), + machine: OsString::from(std::env::consts::ARCH), + processor: OsString::from(crate::lib_impl::map_processor(std::env::consts::ARCH)), + osname: OsString::from(crate::lib_impl::HOST_OS_NAME), + }) + } +} + +impl UNameAPI for PlatformInfo { + fn sysname(&self) -> &OsStr { + &self.sysname + } + + fn nodename(&self) -> &OsStr { + &self.nodename + } + + fn release(&self) -> &OsStr { + &self.release + } + + fn version(&self) -> &OsStr { + &self.version + } + + fn machine(&self) -> &OsStr { + &self.machine + } + + fn processor(&self) -> &OsStr { + &self.processor + } + + fn osname(&self) -> &OsStr { + &self.osname + } +} + +#[test] +fn test_wasi() { + let info = PlatformInfo::new().unwrap(); + + assert_eq!(info.sysname().to_string_lossy(), "wasi"); + // nodename comes from env or defaults to "localhost" + assert!(!info.nodename().to_string_lossy().is_empty()); + assert_eq!(info.release().to_string_lossy(), "0.0.0"); + assert_eq!(info.version().to_string_lossy(), "0.0.0"); + assert_eq!(info.machine().to_string_lossy(), std::env::consts::ARCH); +} + +#[test] +fn test_wasi_nodename_env() { + let orig = std::env::var("HOSTNAME").ok(); + + std::env::set_var("HOSTNAME", "my-wasi-host"); + let info = PlatformInfo::new().unwrap(); + assert_eq!(info.nodename().to_string_lossy(), "my-wasi-host"); + + std::env::remove_var("HOSTNAME"); + std::env::remove_var("COMPUTERNAME"); + let info = PlatformInfo::new().unwrap(); + assert_eq!(info.nodename().to_string_lossy(), "localhost"); + + // Restore + match orig { + Some(v) => std::env::set_var("HOSTNAME", v), + None => std::env::remove_var("HOSTNAME"), + } +} + +#[test] +fn structure_clone() { + let info = PlatformInfo::new().unwrap(); + println!("{:?}", info); + let info_copy = info.clone(); + assert_eq!(info_copy, info); +}