diff --git a/cmd/wazero/compositefs.go b/cmd/wazero/compositefs.go new file mode 100644 index 0000000000..eaaa04b113 --- /dev/null +++ b/cmd/wazero/compositefs.go @@ -0,0 +1,37 @@ +package main + +import ( + "io/fs" + "strings" +) + +type compositeFS struct { + paths map[string]fs.FS +} + +func (c *compositeFS) Open(name string) (fs.File, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} + } + for path, f := range c.paths { + if !strings.HasPrefix(name, path) { + continue + } + rest := name[len(path):] + if len(rest) == 0 { + // Special case reading directory + rest = "." + } else { + // fs.Open requires a relative path + if rest[0] == '/' { + rest = rest[1:] + } + } + file, err := f.Open(rest) + if err == nil { + return file, err + } + } + + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} +} diff --git a/cmd/wazero/compositefs_test.go b/cmd/wazero/compositefs_test.go new file mode 100644 index 0000000000..e0ec0facd8 --- /dev/null +++ b/cmd/wazero/compositefs_test.go @@ -0,0 +1,234 @@ +package main + +import ( + "bytes" + "embed" + "fmt" + "io/fs" + "testing" + "testing/fstest" + + "github.com/tetratelabs/wazero/internal/testing/require" +) + +//go:embed testdata/fs +var testFS embed.FS + +func TestCompositeFS(t *testing.T) { + tests := []struct { + name string + fs *compositeFS + path string + content string + }{ + { + name: "empty", + fs: &compositeFS{}, + path: "bear.txt", + }, + { + name: "single mount to root", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "": testFSSub(""), + }, + }, + path: "bear.txt", + content: "pooh", + }, + { + name: "single mount to root", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "": testFSSub(""), + }, + }, + path: "fish/clownfish.txt", + content: "nemo", + }, + { + name: "single mount to root", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "": testFSSub(""), + }, + }, + path: "mammals/primates/ape.txt", + content: "king kong", + }, + { + name: "single mount to path", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "mammals": testFSSub(""), + }, + }, + path: "mammals/bear.txt", + content: "pooh", + }, + { + name: "single mount to path", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "mammals": testFSSub(""), + }, + }, + path: "mammals/whale.txt", + }, + { + name: "single mount to path", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "mammals": testFSSub(""), + }, + }, + path: "bear.txt", + }, + { + name: "non-overlapping mounts", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "fish": testFSSub("fish"), + "mammals": testFSSub("mammals"), + }, + }, + path: "fish/clownfish.txt", + content: "nemo", + }, + { + name: "non-overlapping mounts", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "fish": testFSSub("fish"), + "mammals": testFSSub("mammals"), + }, + }, + path: "mammals/whale.txt", + content: "moby dick", + }, + { + name: "non-overlapping mounts", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "fish": testFSSub("fish"), + "mammals": testFSSub("mammals"), + }, + }, + path: "mammals/primates/ape.txt", + content: "king kong", + }, + { + name: "non-overlapping mounts", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "fish": testFSSub("fish"), + "mammals": testFSSub("mammals"), + }, + }, + path: "bear.txt", + }, + { + name: "overlapping mounts, deep first", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "animals/fish": testFSSub("fish"), + "animals": testFSSub("mammals"), + }, + }, + path: "animals/fish/clownfish.txt", + content: "nemo", + }, + { + name: "overlapping mounts, deep first", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "animals/fish": testFSSub("fish"), + "animals": testFSSub("mammals"), + }, + }, + path: "animals/whale.txt", + content: "moby dick", + }, + { + name: "overlapping mounts, deep first", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "animals/fish": testFSSub("fish"), + "animals": testFSSub("mammals"), + }, + }, + path: "animals/bear.txt", + }, + { + name: "overlapping mounts, shallow first", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "animals": testFSSub("mammals"), + "animals/fish": testFSSub("fish"), + }, + }, + path: "animals/fish/clownfish.txt", + content: "nemo", + }, + { + name: "overlapping mounts, shallow first", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "animals": testFSSub("mammals"), + "animals/fish": testFSSub("fish"), + }, + }, + path: "animals/whale.txt", + content: "moby dick", + }, + { + name: "overlapping mounts, shallow first", + fs: &compositeFS{ + paths: map[string]fs.FS{ + "animals": testFSSub("mammals"), + "animals/fish": testFSSub("fish"), + }, + }, + path: "animals/bear.txt", + }, + } + + for _, tc := range tests { + tt := tc + t.Run(fmt.Sprintf("%s - %s", tt.name, tt.path), func(t *testing.T) { + content, err := fs.ReadFile(tt.fs, tt.path) + if tt.content != "" { + require.NoError(t, err) + require.Equal(t, tt.content, string(bytes.TrimSpace(content))) + } else { + require.ErrorIs(t, err, fs.ErrNotExist) + } + }) + } +} + +func TestFSTest(t *testing.T) { + fs := &compositeFS{ + paths: map[string]fs.FS{ + // TestFS requires non-rooted paths to be read from current directory so we mount + // both with . and no prefix. + ".": testFSSub(""), + "": testFSSub(""), + }, + } + + require.NoError(t, fstest.TestFS(fs, "bear.txt")) +} + +func testFSSub(path string) fs.FS { + // Can't use filepath.Join because we need unix behavior even on Windows. + p := "testdata/fs" + if len(path) > 0 { + p = fmt.Sprintf("%s/%s", p, path) + } + f, err := fs.Sub(testFS, p) + if err != nil { + panic(err) + } + return f +} diff --git a/cmd/wazero/testdata/fs/bear.txt b/cmd/wazero/testdata/fs/bear.txt new file mode 100644 index 0000000000..8beaadd1b6 --- /dev/null +++ b/cmd/wazero/testdata/fs/bear.txt @@ -0,0 +1 @@ +pooh diff --git a/cmd/wazero/testdata/fs/fish/clownfish.txt b/cmd/wazero/testdata/fs/fish/clownfish.txt new file mode 100644 index 0000000000..84e0b7dbae --- /dev/null +++ b/cmd/wazero/testdata/fs/fish/clownfish.txt @@ -0,0 +1 @@ +nemo diff --git a/cmd/wazero/testdata/fs/mammals/primates/ape.txt b/cmd/wazero/testdata/fs/mammals/primates/ape.txt new file mode 100644 index 0000000000..834c5b6258 --- /dev/null +++ b/cmd/wazero/testdata/fs/mammals/primates/ape.txt @@ -0,0 +1 @@ +king kong diff --git a/cmd/wazero/testdata/fs/mammals/whale.txt b/cmd/wazero/testdata/fs/mammals/whale.txt new file mode 100644 index 0000000000..775adeb94c --- /dev/null +++ b/cmd/wazero/testdata/fs/mammals/whale.txt @@ -0,0 +1 @@ +moby dick diff --git a/cmd/wazero/testdata/wasi_env.wasm b/cmd/wazero/testdata/wasi_env.wasm new file mode 100644 index 0000000000..ba3a40de31 Binary files /dev/null and b/cmd/wazero/testdata/wasi_env.wasm differ diff --git a/cmd/wazero/testdata/wasi_env.wat b/cmd/wazero/testdata/wasi_env.wat new file mode 100644 index 0000000000..5f44ccb4d2 --- /dev/null +++ b/cmd/wazero/testdata/wasi_env.wat @@ -0,0 +1,61 @@ +;; $wasi_env is a WASI command which copies null-terminated environ to stdout. +(module $wasi_env + ;; environ_get reads environment variables. + ;; + ;; See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-environ_getenviron-pointerpointeru8-environ_buf-pointeru8---errno + (import "wasi_snapshot_preview1" "environ_get" + (func $wasi.environ_get (param $environ i32) (param $environ_buf i32) (result (;errno;) i32))) + + ;; environ_sizes_get returns environment variables sizes. + ;; + ;; See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-environ_sizes_get---errno-size-size + (import "wasi_snapshot_preview1" "environ_sizes_get" + (func $wasi.environ_sizes_get (param $result.environc i32) (param $result.environ_buf_size i32) (result (;errno;) i32))) + + ;; fd_write write bytes to a file descriptor. + ;; + ;; See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_write + (import "wasi_snapshot_preview1" "fd_write" + (func $wasi.fd_write (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $result.size i32) (result (;errno;) i32))) + + ;; WASI commands are required to export "memory". Particularly, imported functions mutate this. + ;; + ;; Note: 1 is the size in pages (64KB), not bytes! + ;; See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memories%E2%91%A7 + (memory (export "memory") 1) + + ;; $iovs are offset/length pairs in memory fd_write copies to the file descriptor. + ;; $main will only write one offset/length pair, corresponding to null-terminated environ. + (global $iovs i32 i32.const 1024) ;; 1024 is an arbitrary offset larger than the environ. + + ;; WASI parameters are usually memory offsets, you can ignore values by writing them to an unread offset. + (global $ignored i32 i32.const 32768) + + ;; _start is a special function defined by a WASI Command that runs like a main function would. + ;; + ;; See https://github.com/WebAssembly/WASI/blob/snapshot-01/design/application-abi.md#current-unstable-abi + (func $main (export "_start") + ;; To copy an env to a file, we first need to load it into memory. + (call $wasi.environ_get + (global.get $ignored) ;; ignore $environ as we only read the environ_buf + (i32.const 0) ;; Write $environ_buf (null-terminated environ) to memory offset zero. + ) + drop ;; ignore the errno returned + + ;; Next, we need to know how many bytes were loaded, as that's how much we'll copy to the file. + (call $wasi.environ_sizes_get + (global.get $ignored) ;; ignore $result.environc as we only read the environ_buf. + (i32.add (global.get $iovs) (i32.const 4)) ;; store $result.environ_buf_size as the length to copy + ) + drop ;; ignore the errno returned + + ;; Finally, write the memory region to the file. + (call $wasi.fd_write + (i32.const 1) ;; $fd is a file descriptor and 1 is stdout (console). + (global.get $iovs) ;; $iovs is the start offset of the IO vectors to copy. + (i32.const 1) ;; $iovs_len is the count of offset/length pairs to copy to memory. + (global.get $ignored) ;; ignore $result.size as we aren't verifying it. + ) + drop ;; ignore the errno returned + ) +) diff --git a/cmd/wazero/testdata/wasi_fd.wasm b/cmd/wazero/testdata/wasi_fd.wasm new file mode 100644 index 0000000000..46c36244d7 Binary files /dev/null and b/cmd/wazero/testdata/wasi_fd.wasm differ diff --git a/cmd/wazero/testdata/wasi_fd.wat b/cmd/wazero/testdata/wasi_fd.wat new file mode 100644 index 0000000000..fdabe14892 --- /dev/null +++ b/cmd/wazero/testdata/wasi_fd.wat @@ -0,0 +1,74 @@ +;; $wasi_fd is a WASI command which reads from bear.txt +(module $wasi_fd + ;; path_open returns a file descriptor to a path + ;; + ;; See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-args_sizes_get---errno-size-size + (import "wasi_snapshot_preview1" "path_open" + (func $wasi.path_open (param $fd i32) (param $dirflags i32) (param $path i32) (param $path_len i32) (param $oflags i32) (param $fs_rights_base i64) (param $fs_rights_inheriting i64) (param $fdflags i32) (param $result.opened_fd i32) (result (;errno;) i32))) + + ;; fd_read reads bytes from a file descriptor. + ;; + ;; See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_read + (import "wasi_snapshot_preview1" "fd_read" + (func $wasi.fd_read (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $result.size i32) (result (;errno;) i32))) + + ;; fd_write write bytes to a file descriptor. + ;; + ;; See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_write + (import "wasi_snapshot_preview1" "fd_write" + (func $wasi.fd_write (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $result.size i32) (result (;errno;) i32))) + + ;; WASI commands are required to export "memory". Particularly, imported functions mutate this. + ;; + ;; Note: 1 is the size in pages (64KB), not bytes! + ;; See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memories%E2%91%A7 + (memory (export "memory") 1) + + ;; $iovs are offset/length pairs in memory fd_write copies to the file descriptor. + ;; $main will only write one offset/length pair, corresponding to a read buffer + (global $iovs i32 i32.const 1024) ;; 1024 is an arbitrary offset + + ;; WASI parameters are usually memory offsets, you can ignore values by writing them to an unread offset. + (global $ignored i32 i32.const 32768) + + ;; _start is a special function defined by a WASI Command that runs like a main function would. + ;; + ;; See https://github.com/WebAssembly/WASI/blob/snapshot-01/design/application-abi.md#current-unstable-abi + (func $main (export "_start") + ;; First open the path + (call $wasi.path_open + (i32.const 3) ;; fd of fs root + (i32.const 0) ;; ignore $dirflags + (i32.const 5000) ;; path is in data + (i32.const 8) ;; path len + (i32.const 0) ;; ignore $oflags + (i64.const 0) ;; ignore $fs_rights_base + (i64.const 0) ;; ignore $fs_rights_inheriting + (i32.const 0) ;; ignore $fdflags + (i32.const 0) ;; write result fd to memory offset 0 + ) + drop ;; ignore the errno returned + + ;; set iovs to a 50 byte read buffer at offset 100 + (i32.store (global.get $iovs) (i32.const 100)) + (i32.store (i32.add (global.get $iovs) (i32.const 4)) (i32.const 50)) + + (call $wasi.fd_read + (i32.load (i32.const 0)) ;; load from offset 0 which has fd of file + (global.get $iovs) ;; $iovs is the start offset of the IO vectors to read into. + (i32.const 1) ;; $iovs_len is the count of offset/length pairs to read from iovs + (i32.add (global.get $iovs) (i32.const 4)) ;; set number of bytes read as buffer length for fd_write below + ) + drop ;; ignore the errno returned + + ;; Finally, write the memory region to the file. + (call $wasi.fd_write + (i32.const 1) ;; $fd is a file descriptor and 1 is stdout (console). + (global.get $iovs) ;; $iovs is the start offset of the IO vectors to copy. + (i32.const 1) ;; $iovs_len is the count of offset/length pairs to copy to memory. + (global.get $ignored) ;; ignore $result.size as we aren't verifying it. + ) + drop ;; ignore the errno returned + ) + (data $.rodata (i32.const 5000) "bear.txt") +) diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go index cf2de7951f..67c936966e 100644 --- a/cmd/wazero/wazero.go +++ b/cmd/wazero/wazero.go @@ -6,8 +6,10 @@ import ( "flag" "fmt" "io" + "io/fs" "os" "path/filepath" + "strings" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" @@ -54,8 +56,25 @@ func doRun(args []string, stdOut io.Writer, stdErr io.Writer, exit func(code int flags := flag.NewFlagSet("run", flag.ExitOnError) flags.SetOutput(stdErr) + var help bool + flags.BoolVar(&help, "h", false, "print usage") + + var envs sliceFlag + flags.Var(&envs, "env", "key=value pair of environment variable to expose to the binary. "+ + "Can be specified multiple times.") + + var mounts sliceFlag + flags.Var(&mounts, "mount", + "filesystem path to expose to the binary in the form of [:]. If wasm path is not "+ + "provided, the host path will be used. Can be specified multiple times.") + _ = flags.Parse(args) + if help { + printRunUsage(stdErr, flags) + exit(0) + } + if flags.NArg() < 1 { fmt.Fprintln(stdErr, "missing path to wasm file") printRunUsage(stdErr, flags) @@ -71,6 +90,51 @@ func doRun(args []string, stdOut io.Writer, stdErr io.Writer, exit func(code int } } + // Don't use map to preserve order + var env []string + for _, e := range envs { + key, value, ok := strings.Cut(e, "=") + if !ok { + fmt.Fprintf(stdErr, "invalid environment variable: %s\n", e) + exit(1) + } + env = append(env, key, value) + } + + var mountFS fs.FS + if len(mounts) > 0 { + cfs := &compositeFS{ + paths: map[string]fs.FS{}, + } + for _, mount := range mounts { + if len(mount) == 0 { + fmt.Fprintln(stdErr, "invalid mount: empty string") + exit(1) + } + + // TODO(anuraaga): Support wasm paths with colon in them. + var host, guest string + if clnIdx := strings.LastIndexByte(mount, ':'); clnIdx != -1 { + host, guest = mount[:clnIdx], mount[clnIdx+1:] + } else { + host = mount + guest = host + } + + if guest[0] == '.' { + fmt.Fprintf(stdErr, "invalid mount: guest path must not start with .: %s\n", guest) + exit(1) + } + + // wazero always calls fs.Open with a relative path. + if guest[0] == '/' { + guest = guest[1:] + } + cfs.paths[guest] = os.DirFS(host) + } + mountFS = cfs + } + wasm, err := os.ReadFile(wasmPath) if err != nil { fmt.Fprintf(stdErr, "error reading wasm binary: %v\n", err) @@ -94,6 +158,13 @@ func doRun(args []string, stdOut io.Writer, stdErr io.Writer, exit func(code int WithSysNanotime(). WithSysWalltime(). WithArgs(append([]string{wasmExe}, wasmArgs...)...) + for i := 0; i < len(env); i += 2 { + conf = conf.WithEnv(env[i], env[i+1]) + } + if mountFS != nil { + conf = conf.WithFS(mountFS) + } + code, err := rt.CompileModule(ctx, wasm) if err != nil { fmt.Fprintf(stdErr, "error compiling wasm binary: %v\n", err) @@ -129,6 +200,19 @@ func printUsage(stdErr io.Writer) { func printRunUsage(stdErr io.Writer, flags *flag.FlagSet) { fmt.Fprintln(stdErr, "wazero CLI") fmt.Fprintln(stdErr) - fmt.Fprintln(stdErr, "Usage:\n wazero run [--] ") + fmt.Fprintln(stdErr, "Usage:\n wazero run [--] ") fmt.Fprintln(stdErr) + fmt.Fprintln(stdErr, "Options:") + flags.PrintDefaults() +} + +type sliceFlag []string + +func (f *sliceFlag) String() string { + return strings.Join(*f, ",") +} + +func (f *sliceFlag) Set(s string) error { + *f = append(*f, s) + return nil } diff --git a/cmd/wazero/wazero_test.go b/cmd/wazero/wazero_test.go index 3b7880db07..c0a6d8288f 100644 --- a/cmd/wazero/wazero_test.go +++ b/cmd/wazero/wazero_test.go @@ -2,9 +2,9 @@ package main import ( "bytes" - "embed" + _ "embed" "flag" - "io/fs" + "fmt" "os" "path/filepath" "testing" @@ -12,41 +12,69 @@ import ( "github.com/tetratelabs/wazero/internal/testing/require" ) -//go:embed testdata -var testdata embed.FS +//go:embed testdata/wasi_arg.wasm +var wasmWasiArg []byte + +//go:embed testdata/wasi_env.wasm +var wasmWasiEnv []byte + +//go:embed testdata/wasi_fd.wasm +var wasmWasiFd []byte + +//go:embed testdata/fs/bear.txt +var bearTxt []byte func TestRun(t *testing.T) { + bearPath := filepath.Join(t.TempDir(), "bear.txt") + require.NoError(t, os.WriteFile(bearPath, bearTxt, 0755)) + tests := []struct { - wasmPath string - wasmArgs []string - stdOut string - stdErr string + name string + wazeroOpts []string + wasm []byte + wasmArgs []string + stdOut string + stdErr string }{ { - wasmPath: "testdata/wasi_arg.wasm", + name: "args", + wasm: wasmWasiArg, wasmArgs: []string{"hello world"}, // Executable name is first arg so is printed. stdOut: "test.wasm\x00hello world\x00", }, { - wasmPath: "testdata/wasi_arg.wasm", + name: "-- args", + wasm: wasmWasiArg, wasmArgs: []string{"--", "hello world"}, // Executable name is first arg so is printed. stdOut: "test.wasm\x00hello world\x00", }, + { + name: "env", + wasm: wasmWasiEnv, + wazeroOpts: []string{"--env=ANIMAL=bear", "--env=FOOD=sushi"}, + stdOut: "ANIMAL=bear\x00FOOD=sushi\x00", + }, + { + name: "fd", + wasm: wasmWasiFd, + wazeroOpts: []string{fmt.Sprintf("--mount=%s:/", filepath.Dir(bearPath))}, + stdOut: "pooh\n", + }, } for _, tc := range tests { tt := tc - t.Run(tt.wasmPath, func(t *testing.T) { - wasmBytes, err := fs.ReadFile(testdata, tt.wasmPath) - require.NoError(t, err) - + t.Run(tt.name, func(t *testing.T) { wasmPath := filepath.Join(t.TempDir(), "test.wasm") - require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0755)) + require.NoError(t, os.WriteFile(wasmPath, tt.wasm, 0755)) - exitCode, stdOut, stdErr := runMain(t, append([]string{"run", wasmPath}, tt.wasmArgs...)) - require.Equal(t, 0, exitCode) + args := append([]string{"run"}, tt.wazeroOpts...) + args = append(args, wasmPath) + args = append(args, tt.wasmArgs...) + exitCode, stdOut, stdErr := runMain(t, args) + require.Equal(t, 0, exitCode, stdErr) require.Equal(t, tt.stdOut, stdOut) require.Equal(t, tt.stdErr, stdErr) }) @@ -79,6 +107,14 @@ func TestErrors(t *testing.T) { message: "error compiling wasm binary", args: []string{notWasmPath}, }, + { + message: "invalid environment variable", + args: []string{"--env=ANIMAL", "testdata/wasi_env.wasm"}, + }, + { + message: "invalid mount", + args: []string{"--mount=.", "testdata/wasi_env.wasm"}, + }, } for _, tc := range tests { diff --git a/imports/wasi_snapshot_preview1/example/cat_test.go b/imports/wasi_snapshot_preview1/example/cat_test.go index 1a60fdd4ae..c8e8f1c363 100644 --- a/imports/wasi_snapshot_preview1/example/cat_test.go +++ b/imports/wasi_snapshot_preview1/example/cat_test.go @@ -1,6 +1,13 @@ package main import ( + "bytes" + "fmt" + "io/fs" + "os" + "os/exec" + "path/filepath" + "runtime" "testing" "github.com/tetratelabs/wazero/internal/testing/maintester" @@ -21,3 +28,68 @@ func Test_main(t *testing.T) { }) } } + +// Test_cli ensures the following will work: +// +// go run github.com/tetratelabs/wazero/cmd/wazero run -mount=testdata:/ cat.wasm /test.txt +func Test_cli(t *testing.T) { + tests := []struct { + toolchain string + wasm []byte + }{ + { + toolchain: "cargo-wasi", + wasm: catWasmCargoWasi, + }, + { + toolchain: "tinygo", + wasm: catWasmTinyGo, + }, + { + toolchain: "zig-cc", + wasm: catWasmZigCc, + }, + } + + for _, tc := range tests { + tt := tc + t.Run(tt.toolchain, func(t *testing.T) { + for _, testPath := range []string{"/test.txt", "/testcases/test.txt"} { + t.Run(testPath, func(t *testing.T) { + // Write out embedded files instead of accessing directly for docker cross-architecture tests. + wasmPath := filepath.Join(t.TempDir(), "cat.wasm") + require.NoError(t, os.WriteFile(wasmPath, tt.wasm, 0755)) + + testTxt, err := fs.ReadFile(catFS, "testdata/test.txt") + require.NoError(t, err) + testTxtPath := filepath.Join(t.TempDir(), "test.txt") + require.NoError(t, os.WriteFile(testTxtPath, testTxt, 0755)) + + // We can't invoke go run in our docker based cross-architecture tests. We do want to use + // otherwise so running unit tests normally does not require special build steps. + var cmdExe string + var cmdArgs []string + if cmdPath := os.Getenv("WAZEROCLI"); cmdPath != "" { + cmdExe = cmdPath + } else { + cmdExe = filepath.Join(runtime.GOROOT(), "bin", "go") + cmdArgs = []string{"run", "../../../cmd/wazero"} + } + + cmdArgs = append(cmdArgs, "run", + fmt.Sprintf("-mount=%s:/", filepath.Dir(testTxtPath)), + fmt.Sprintf("-mount=%s:/testcases", filepath.Dir(testTxtPath)), + wasmPath, testPath) + + stdOut := &bytes.Buffer{} + stdErr := &bytes.Buffer{} + cmd := exec.Command(cmdExe, cmdArgs...) + cmd.Stdout = stdOut + cmd.Stderr = stdErr + require.NoError(t, cmd.Run(), stdErr.String()) + require.Equal(t, "greet filesystem\n", stdOut.String()) + }) + } + }) + } +}