userland: add portable du, stat, root id, and nohup
This commit is contained in:
Generated
+9
@@ -4455,6 +4455,15 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sausage-fs-tools"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"sausage-spawn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sausage-grep"
|
||||
version = "0.1.0"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# The default CLIs of a world, built for wasm32-wasip2. Its own workspace, outside the host's.
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["git", "coreutils", "procps", "cwd-init/probe", "ripgrep", "diffutils", "jaq", "sed", "grep", "fd", "spawn", "findutils", "exec-tools", "ssh-client", "ssh", "curl", "init", "box", "archive-tools"]
|
||||
members = ["git", "coreutils", "procps", "cwd-init/probe", "ripgrep", "diffutils", "jaq", "sed", "grep", "fd", "spawn", "findutils", "exec-tools", "ssh-client", "ssh", "curl", "init", "box", "archive-tools", "fs-tools"]
|
||||
|
||||
# WASI compatibility patches: git/vendor/PATCHES.md.
|
||||
[patch.crates-io]
|
||||
|
||||
@@ -17,6 +17,7 @@ Extraction supports regular files and directories. It rejects special files and
|
||||
Creation uses portable modes because WASI does not expose Unix mode bits.
|
||||
It includes symbolic links in archives, but world extraction does not create symbolic links or hardlinks.
|
||||
The selected `-C` applies to all input paths.
|
||||
With `-v`, creation prints names to stdout for named archives and to stderr for `-f -`.
|
||||
|
||||
Gzip supports `-d`, `-c`, `-k`, and `-f`.
|
||||
Gunzip defaults to decompression. Zcat defaults to decompression to stdout.
|
||||
|
||||
@@ -10,7 +10,7 @@ fn append<W: Write>(
|
||||
builder: &mut tar::Builder<W>,
|
||||
source: &Path,
|
||||
name: &Path,
|
||||
verbose: bool,
|
||||
verbose: Option<bool>,
|
||||
) -> io::Result<()> {
|
||||
let meta = std::fs::symlink_metadata(source)?;
|
||||
let mut header = tar::Header::new_gnu();
|
||||
@@ -22,8 +22,13 @@ fn append<W: Write>(
|
||||
.unwrap_or_default()
|
||||
.as_secs(),
|
||||
);
|
||||
if verbose {
|
||||
eprintln!("{}", name.display());
|
||||
if let Some(stdout) = verbose {
|
||||
let suffix = if meta.is_dir() { "/" } else { "" };
|
||||
if stdout {
|
||||
println!("{}{suffix}", name.display());
|
||||
} else {
|
||||
eprintln!("{}{suffix}", name.display());
|
||||
}
|
||||
}
|
||||
if meta.is_dir() {
|
||||
header.set_entry_type(tar::EntryType::Directory);
|
||||
@@ -57,7 +62,12 @@ fn append<W: Write>(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn create<W: Write>(writer: W, cwd: &Path, paths: &[String], verbose: bool) -> io::Result<W> {
|
||||
fn create<W: Write>(
|
||||
writer: W,
|
||||
cwd: &Path,
|
||||
paths: &[String],
|
||||
verbose: Option<bool>,
|
||||
) -> io::Result<W> {
|
||||
let mut builder = tar::Builder::new(writer);
|
||||
for path in paths {
|
||||
let name = Path::new(path).strip_prefix("/").unwrap_or(Path::new(path));
|
||||
@@ -127,6 +137,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if paths.is_empty() {
|
||||
return Err("refusing to create an empty archive".into());
|
||||
}
|
||||
let verbose = verbose.then_some(archive != "-");
|
||||
let writer: Box<dyn Write> = if archive == "-" {
|
||||
Box::new(io::stdout())
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,21 @@ def main [] {
|
||||
mkdir ($root | path join extracted)
|
||||
call $target $root tar [-xzf /native.tgz -C /extracted --strip-components 1] | success | ignore
|
||||
check ((open --raw ($root | path join extracted sub file)) == 'hello from a nested file') 'world extracts native archive with strip-components'
|
||||
let plain = (call $target $root tar [-cf /plain.tar -C /input/sub file] | success)
|
||||
check ($plain.stdout == '') 'tar -cf'
|
||||
let native_list = (^tar -tf ($root | path join plain.tar) | complete)
|
||||
let world_list = (call $target $root tar [-tf /plain.tar])
|
||||
check ($native_list.stdout == $world_list.stdout and $native_list.exit_code == $world_list.exit_code) 'tar -tf matches native'
|
||||
let native_verbose = (^tar -cvf ($root | path join verbose-native.tar) -C ($root | path join input sub) file | complete)
|
||||
let world_verbose = (call $target $root tar [-cvf /verbose.tar -C /input/sub file])
|
||||
check ($native_verbose.stdout == $world_verbose.stdout and $native_verbose.exit_code == $world_verbose.exit_code and $world_verbose.stderr == '') 'tar create verbose named archive'
|
||||
let stream = (call $target $root tar [-cvf - -C /input/sub file] | success)
|
||||
check (($stream.stderr | str trim) == 'file') 'tar create verbose stream uses stderr'
|
||||
let native_stream = ($stream.stdout | ^tar -tf - | complete)
|
||||
check ($native_stream.stdout == $native_list.stdout and $native_stream.exit_code == 0) 'native reads tar stdout stream'
|
||||
mkdir ($root | path join stream-extract)
|
||||
call $target $root tar [-xf - -C /stream-extract] $stream.stdout | success | ignore
|
||||
check ((open --raw ($root | path join stream-extract file)) == 'hello from a nested file') 'tar -xf stdin stream'
|
||||
let listed = (call $target $root tar [-tf /missing.tar])
|
||||
let native_error = (^tar -tf ($root | path join missing.tar) | complete)
|
||||
check ($listed.exit_code == $native_error.exit_code) 'tar missing archive exit code'
|
||||
@@ -28,6 +43,11 @@ def main [] {
|
||||
let native_compressed = ('reverse payload' | ^gzip -c | complete)
|
||||
let decoded = (call $target $root gunzip [-c] $native_compressed.stdout | success)
|
||||
check ($decoded.stdout == 'reverse payload') 'world gunzip decodes native stream'
|
||||
for tool in [gzip zcat] {
|
||||
let flags = if $tool == 'gzip' { [-dc] } else { [] }
|
||||
let decoded = (call $target $root $tool $flags $native_compressed.stdout | success)
|
||||
check ($decoded.stdout == 'reverse payload') $'($tool) native gzip stream'
|
||||
}
|
||||
call $target $root gzip [-k /input/sub/file] | success | ignore
|
||||
check (($root | path join input sub file.gz | path exists) and ($root | path join input sub file | path exists)) 'gzip -k keeps input'
|
||||
let conflict = (call $target $root gzip [/input/sub/file])
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "sausage-fs-tools"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "du"
|
||||
path = "src/du.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "stat"
|
||||
path = "src/stat.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "id"
|
||||
path = "src/id.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "nohup"
|
||||
path = "src/nohup.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
chrono = { version = "0.4", default-features = false, features = ["std"] }
|
||||
sausage-spawn = { path = "../spawn" }
|
||||
@@ -0,0 +1,36 @@
|
||||
# Filesystem and identity commands
|
||||
|
||||
These commands supplement the default coreutils build.
|
||||
|
||||
- `id` reports `uid=0(root) gid=0(root) groups=0(root)`. The world has one user.
|
||||
`-u` and `-g` print zero. `-n` prints `root`, including `-un` and `-gn`.
|
||||
- `du` sums apparent file sizes. Directories contribute zero bytes.
|
||||
It supports `-s`, `-h`, `-a`, `-c`, `-d`/`--max-depth`, `-b`, and `-k`.
|
||||
The default unit and `-k` use 1024 bytes. `-b` uses bytes.
|
||||
Symlinks contribute their own size. The command does not follow them.
|
||||
Native comparisons use `du --apparent-size` on files.
|
||||
Native directory totals include host directory storage, which the world does not expose.
|
||||
- `stat` supports `-c`/`--format`, `-L`, and multiple paths.
|
||||
Formats: `%n %s %F %Y %y %X %x %N %% %a %A %U %G %u %g`.
|
||||
Times use UTC. UID and GID are zero. User and group names are `root`.
|
||||
Modes are fixed at `755` for directories and `644` otherwise.
|
||||
The default output prints names, sizes, types, modes, identity, and timestamps.
|
||||
- `nohup COMMAND [ARG...]` uses the existing process interface.
|
||||
It starts an independent process group and returns the command's exit status.
|
||||
The world has no terminal or SIGHUP. Non-terminal streams remain inherited.
|
||||
It does not detach the command or create `nohup.out`, like native nohup with redirected streams.
|
||||
|
||||
## Tests
|
||||
|
||||
From `userland/`, use the executor's `CARGO_TARGET_DIR`:
|
||||
|
||||
```sh
|
||||
nix develop -c cargo build -p sausage-fs-tools --release --locked
|
||||
nix develop -c nu --no-config-file fs-tools/test.nu
|
||||
nix develop -c nu --no-config-file fs-tools/world-test.nu /tmp/s6-esbuild/app /tmp/s6-esbuild/esbuild-native
|
||||
```
|
||||
|
||||
The world test requires the host binary and brush in the same target folder.
|
||||
It also requires the pinned preview1 tools and the built archive tools.
|
||||
The fixture contains S6's app, `check.mjs`, and its installed dependencies.
|
||||
The test creates a fresh world under `/tmp`. It does not use the live world.
|
||||
@@ -0,0 +1,77 @@
|
||||
//! Report apparent file sizes. Directories contribute no storage of their own.
|
||||
use clap::Parser;
|
||||
use std::{fs, io, path::PathBuf};
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(short = 's', long)]
|
||||
summary: bool,
|
||||
#[arg(short = 'h', long)]
|
||||
human_readable: bool,
|
||||
#[arg(short = 'a', long)]
|
||||
all: bool,
|
||||
#[arg(short = 'c', long)]
|
||||
total: bool,
|
||||
#[arg(short = 'd', long = "max-depth")]
|
||||
depth: Option<usize>,
|
||||
#[arg(short = 'b', long)]
|
||||
bytes: bool,
|
||||
#[arg(short = 'k')]
|
||||
kibibytes: bool,
|
||||
#[arg(default_value = ".")]
|
||||
paths: Vec<PathBuf>,
|
||||
}
|
||||
fn size(bytes: u64, args: &Args) -> String {
|
||||
if args.human_readable && !args.bytes {
|
||||
let mut value = bytes as f64;
|
||||
let mut unit = 0;
|
||||
while value.ceil() >= 1024.0 && unit < 6 {
|
||||
value /= 1024.0;
|
||||
unit += 1;
|
||||
}
|
||||
let suffix = ["", "K", "M", "G", "T", "P", "E"][unit];
|
||||
let rounded = (value * 10.0).ceil() / 10.0;
|
||||
if unit > 0 && rounded < 10.0 {
|
||||
format!("{rounded:.1}{suffix}")
|
||||
} else {
|
||||
format!("{}{suffix}", value.ceil() as u64)
|
||||
}
|
||||
} else if args.bytes {
|
||||
bytes.to_string()
|
||||
} else {
|
||||
bytes.div_ceil(1024).to_string()
|
||||
}
|
||||
}
|
||||
fn visit(path: &PathBuf, depth: usize, args: &Args) -> io::Result<u64> {
|
||||
let meta = fs::symlink_metadata(path)?;
|
||||
let mut bytes = if meta.is_dir() { 0 } else { meta.len() };
|
||||
if meta.is_dir() {
|
||||
for entry in fs::read_dir(path)? {
|
||||
bytes += visit(&entry?.path(), depth + 1, args)?;
|
||||
}
|
||||
}
|
||||
if (depth == 0 || (!args.summary && (meta.is_dir() || args.all)))
|
||||
&& args.depth.is_none_or(|max| depth <= max)
|
||||
{
|
||||
println!("{}\t{}", size(bytes, args), path.display());
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
let mut total = 0;
|
||||
let mut failed = false;
|
||||
for path in &args.paths {
|
||||
match visit(path, 0, &args) {
|
||||
Ok(bytes) => total += bytes,
|
||||
Err(error) => {
|
||||
eprintln!("du: {}: {error}", path.display());
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if args.total {
|
||||
println!("{}\ttotal", size(total, &args));
|
||||
}
|
||||
std::process::exit(i32::from(failed));
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//! Report the world's single user: root, with UID and GID zero.
|
||||
use clap::Parser;
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(short = 'u', long, conflicts_with = "group")]
|
||||
user: bool,
|
||||
#[arg(short = 'g', long)]
|
||||
group: bool,
|
||||
#[arg(short = 'n', long)]
|
||||
name: bool,
|
||||
#[arg(value_parser = ["root", "0"])]
|
||||
account: Option<String>,
|
||||
}
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
println!(
|
||||
"{}",
|
||||
if args.name {
|
||||
"root"
|
||||
} else if args.user || args.group {
|
||||
"0"
|
||||
} else {
|
||||
"uid=0(root) gid=0(root) groups=0(root)"
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//! Run a command in an independent process group. The world has no terminal or SIGHUP.
|
||||
use sausage_spawn::Command;
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1).peekable();
|
||||
if args.peek().is_some_and(|arg| arg == "--") {
|
||||
args.next();
|
||||
}
|
||||
let Some(program) = args.next() else {
|
||||
eprintln!("nohup: missing command");
|
||||
std::process::exit(125);
|
||||
};
|
||||
let mut command = Command::new(program);
|
||||
command.args(args).new_group();
|
||||
std::process::exit(match command.status() {
|
||||
Ok(code) => code,
|
||||
Err(error) => {
|
||||
eprintln!("nohup: {error}");
|
||||
if error.kind() == std::io::ErrorKind::NotFound
|
||||
|| error.to_string().contains("not found")
|
||||
{
|
||||
127
|
||||
} else {
|
||||
126
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
//! Print portable filesystem metadata and the world's fixed root identity.
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::Parser;
|
||||
use std::{fs, io, path::PathBuf, time::SystemTime};
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(short = 'c', long = "format")]
|
||||
format: Option<String>,
|
||||
#[arg(short = 'L', long)]
|
||||
dereference: bool,
|
||||
#[arg(required = true)]
|
||||
paths: Vec<PathBuf>,
|
||||
}
|
||||
fn timestamp(value: io::Result<SystemTime>, seconds: bool) -> String {
|
||||
match value {
|
||||
Ok(time) => {
|
||||
let date: DateTime<Utc> = time.into();
|
||||
if seconds {
|
||||
date.timestamp().to_string()
|
||||
} else {
|
||||
date.format("%Y-%m-%d %H:%M:%S%.9f +0000").to_string()
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
if seconds {
|
||||
"0".into()
|
||||
} else {
|
||||
"-".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn render(path: &PathBuf, meta: &fs::Metadata, format: &str) -> io::Result<String> {
|
||||
let mut out = String::new();
|
||||
let mut chars = format.chars();
|
||||
while let Some(ch) = chars.next() {
|
||||
if ch != '%' {
|
||||
out.push(ch);
|
||||
continue;
|
||||
}
|
||||
let field = chars.next().unwrap_or('%');
|
||||
let value = match field {
|
||||
'%' => "%".into(),
|
||||
'n' => path.display().to_string(),
|
||||
'N' => {
|
||||
if meta.is_symlink() {
|
||||
format!(
|
||||
"'{}' -> '{}'",
|
||||
path.display(),
|
||||
fs::read_link(path)?.display()
|
||||
)
|
||||
} else {
|
||||
format!("'{}'", path.display())
|
||||
}
|
||||
}
|
||||
's' => meta.len().to_string(),
|
||||
'F' => if meta.is_dir() {
|
||||
"directory"
|
||||
} else if meta.is_symlink() {
|
||||
"symbolic link"
|
||||
} else if meta.len() == 0 {
|
||||
"regular empty file"
|
||||
} else {
|
||||
"regular file"
|
||||
}
|
||||
.into(),
|
||||
'Y' => timestamp(meta.modified(), true),
|
||||
'y' => timestamp(meta.modified(), false),
|
||||
'X' => timestamp(meta.accessed(), true),
|
||||
'x' => timestamp(meta.accessed(), false),
|
||||
'a' => if meta.is_dir() { "755" } else { "644" }.into(),
|
||||
'A' => if meta.is_dir() {
|
||||
"drwxr-xr-x"
|
||||
} else if meta.is_symlink() {
|
||||
"lrw-r--r--"
|
||||
} else {
|
||||
"-rw-r--r--"
|
||||
}
|
||||
.into(),
|
||||
'u' | 'g' => "0".into(),
|
||||
'U' | 'G' => "root".into(),
|
||||
_ => return Err(io::Error::other(format!("unsupported format %{field}"))),
|
||||
};
|
||||
out.push_str(&value);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
let format = args.format.as_deref().unwrap_or(" File: %N\n Size: %s\tType: %F\nAccess: (%a/%A) Uid: (%u/%U) Gid: (%g/%G)\nAccess: %x\nModify: %y");
|
||||
let mut failed = false;
|
||||
for path in &args.paths {
|
||||
let result = if args.dereference {
|
||||
fs::metadata(path)
|
||||
} else {
|
||||
fs::symlink_metadata(path)
|
||||
}
|
||||
.and_then(|meta| render(path, &meta, format));
|
||||
match result {
|
||||
Ok(text) => println!("{text}"),
|
||||
Err(e) => {
|
||||
eprintln!("stat: {}: {e}", path.display());
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::process::exit(i32::from(failed));
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env nu
|
||||
# Compare portable metadata, apparent file sizes, and the fixed root identity.
|
||||
def check [ok: bool, message: string] {
|
||||
if not $ok { error make {msg: $message} }
|
||||
print $'ok - ($message)'
|
||||
}
|
||||
def call [root: path, tool: string, args: list<string>] {
|
||||
^wasmtime run -S cli-exit-with-code=y --dir $root ($env.CARGO_TARGET_DIR | path join wasm32-wasip2 release $'($tool).wasm') ...$args | complete
|
||||
}
|
||||
def main [] {
|
||||
let root = (^mktemp -d /tmp/t42-fs-XXXXXXXX | str trim)
|
||||
mkdir ($root | path join tree sub)
|
||||
'hello' | save ($root | path join tree file)
|
||||
'world!' | save ($root | path join tree sub nested)
|
||||
let file = ($root | path join tree file)
|
||||
for flags in [[-b] [-k] [-sh] [-ab] [-bc]] {
|
||||
let native = (^du --apparent-size ...$flags $file | complete)
|
||||
let wasm = (call $root du ($flags | append $file))
|
||||
check ($native.stdout == $wasm.stdout and $native.exit_code == $wasm.exit_code) $'du ($flags | str join " ")'
|
||||
}
|
||||
let tree = ($root | path join tree)
|
||||
let total = (call $root du [-sb $tree])
|
||||
check ($total.stdout == $'11(char tab)($tree)(char newline)' and $total.exit_code == 0) 'du sums files without directory storage'
|
||||
let depth = (call $root du [--max-depth 0 -b $tree])
|
||||
check ($depth.stdout == $total.stdout and $depth.exit_code == 0) 'du max-depth'
|
||||
let all = (call $root du [-ab $tree])
|
||||
check (($all.stdout | lines | length) == 4 and $all.exit_code == 0) 'du all files and directories'
|
||||
let total_all = (call $root du [-sbc $tree])
|
||||
check ($total_all.stdout == $'11(char tab)($tree)(char newline)11(char tab)total(char newline)') 'du total'
|
||||
for flags in [[] [-u] [-g] [-un] [-gn]] {
|
||||
let native = (^id ...$flags root | complete)
|
||||
let wasm = (call $root id $flags)
|
||||
check ($native.stdout == $wasm.stdout and $native.exit_code == $wasm.exit_code) $'id ($flags | str join " ")'
|
||||
}
|
||||
check ((call $root id [-n]).stdout == $'root(char newline)') 'id name alone'
|
||||
for format in ['%n %s %F' '%Y %X' '%y %x' '%N' '%%'] {
|
||||
let native = (with-env {TZ: UTC} { ^stat -c $format $file | complete })
|
||||
let wasm = (call $root stat [-c $format $file])
|
||||
check ($native.stdout == $wasm.stdout and $native.exit_code == $wasm.exit_code) $'stat ($format)'
|
||||
}
|
||||
check ((call $root stat [-c '%a %A %u %g %U %G' $file]).stdout == $'644 -rw-r--r-- 0 0 root root(char newline)') 'stat virtual mode and identity'
|
||||
check ((call $root stat [-c '%a %A %F' $tree]).stdout == $'755 drwxr-xr-x directory(char newline)') 'stat directory mode'
|
||||
let link = ($root | path join link)
|
||||
^ln -s tree/file $link
|
||||
for flags in [[-c '%n %s %F %N'] [-L -c '%n %s %F %N']] {
|
||||
let native = (^stat ...$flags $link | complete)
|
||||
let wasm = (call $root stat ($flags | append $link))
|
||||
check ($native.stdout == $wasm.stdout and $native.exit_code == $wasm.exit_code) $'stat symlink ($flags | str join " ")'
|
||||
}
|
||||
let default_stat = (call $root stat [$file])
|
||||
check ($default_stat.exit_code == 0 and ($default_stat.stdout | str contains 'root')) 'stat default output'
|
||||
let sized = ($root | path join sized)
|
||||
for bytes in [1024 1025 10138 11000 1048575 1048576] {
|
||||
^truncate -s $bytes $sized
|
||||
let native = (^du --apparent-size -sh $sized | complete)
|
||||
let wasm = (call $root du [-sh $sized])
|
||||
check ($native.stdout == $wasm.stdout and $native.exit_code == $wasm.exit_code) $'du human size ($bytes)'
|
||||
}
|
||||
for tool in [du stat] {
|
||||
let native = (if $tool == 'du' { ^du --apparent-size /t42-does-not-exist | complete } else { ^stat /t42-does-not-exist | complete })
|
||||
let wasm = (call $root $tool [/t42-does-not-exist])
|
||||
check ($wasm.exit_code == $native.exit_code) $'($tool) missing path status'
|
||||
}
|
||||
print $'evidence: ($root)'
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env nu
|
||||
# Exercise the new commands through the real host and brush shell.
|
||||
# The S6 fixture must contain app.tsx and its pinned node_modules.
|
||||
def check [ok: bool, message: string] {
|
||||
if not $ok { error make {msg: $message} }
|
||||
print $'ok - ($message)'
|
||||
}
|
||||
def main [fixture: path, native_esbuild: path] {
|
||||
let target = ($env.CARGO_TARGET_DIR | path expand)
|
||||
let root = (^mktemp -d /tmp/t42-world-XXXXXXXX | str trim)
|
||||
let bundle = ($root | path join bundle)
|
||||
mkdir ($bundle | path join bin) ($bundle | path join src)
|
||||
for tool in [du stat id nohup tar gzip gunzip zcat] {
|
||||
cp ($target | path join wasm32-wasip2 release $'($tool).wasm') ($bundle | path join bin $'($tool).wasm')
|
||||
}
|
||||
cp ($target | path join wasm32-wasip2 release brush.wasm) ($bundle | path join bin sh.wasm)
|
||||
cp ($target | path join awk.wasm) ($bundle | path join bin awk.wasm)
|
||||
cp ($target | path join esbuild.wasm) ($bundle | path join bin esbuild.wasm)
|
||||
cp ($env.FILE_PWD | path join .. qjs qjs.wasm) ($bundle | path join bin qjs.wasm)
|
||||
cp -r $fixture ($bundle | path join src app)
|
||||
let host = ($target | path join debug sausage)
|
||||
let world = ($root | path join world)
|
||||
let script = 'id; id -un; printf hello > /payload; du -b /payload; stat -c "%n %s %F %U %G" /payload; printf "2 a\n3 b\n" | awk "{sum += \$1} END {print sum}"; tar -czf /world.tgz -C / payload; qjs -e "console.log(6*7)"; esbuild /src/app/app.tsx --bundle --minify --format=iife --platform=browser --jsx=automatic --jsx-import-source=preact --legal-comments=none --log-level=error --outfile=/app.js'
|
||||
let result = (^$host sh $world --userland $bundle -c $script | complete)
|
||||
print $result.stdout
|
||||
if $result.stderr != '' { print --stderr $result.stderr }
|
||||
check ($result.exit_code == 0) 'real world shell and esbuild'
|
||||
check ($result.stdout | str contains 'uid=0(root) gid=0(root) groups=0(root)') 'world root identity'
|
||||
check ($result.stdout | str contains '/payload 5 regular file root root') 'world portable stat'
|
||||
check ($result.stdout | str contains '42') 'world QuickJS'
|
||||
let native_bundle = ($root | path join native.js)
|
||||
let native = (do { cd ($bundle | path join src app); ^$native_esbuild app.tsx --bundle --minify --format=iife --platform=browser --jsx=automatic --jsx-import-source=preact --legal-comments=none --log-level=error $'--outfile=($native_bundle)' } | complete)
|
||||
let world_bundle = ($world | path join world app.js)
|
||||
check ($native.exit_code == 0 and (open --raw $native_bundle) == (open --raw $world_bundle)) 'world esbuild matches native bytes'
|
||||
let render = (^node ($world | path join world src app check.mjs) $world_bundle | complete)
|
||||
print $render.stdout
|
||||
check ($render.exit_code == 0) 'world esbuild bundle renders S6 app'
|
||||
let extracted = ($root | path join extracted)
|
||||
mkdir $extracted
|
||||
^tar -xzf ($world | path join world world.tgz) -C $extracted
|
||||
check ($env.LAST_EXIT_CODE == 0 and (open --raw ($extracted | path join payload)) == 'hello') 'native extracts real world archive'
|
||||
let native = ($root | path join native)
|
||||
mkdir $native
|
||||
'from native' | save ($native | path join native-file)
|
||||
^tar -czf ($world | path join world native.tgz) -C $native native-file
|
||||
let reverse = (^$host sh $world -c 'tar -xzf /native.tgz -C /; qjs --std -e "console.log(std.loadFile(\"/native-file\"))"' | complete)
|
||||
check ($reverse.exit_code == 0 and ($reverse.stdout | str trim) == 'from native') 'world extracts native archive'
|
||||
let nohup = (^$host sh $world -c 'nohup qjs --std -e "std.exit(7)"' | complete)
|
||||
check ($nohup.exit_code == 7) 'nohup preserves child status'
|
||||
let missing = (^$host sh $world -c 'nohup t42-missing-command' | complete)
|
||||
check ($missing.exit_code == 127) 'nohup missing command status'
|
||||
print $'evidence: ($root)'
|
||||
}
|
||||
Reference in New Issue
Block a user