From 7e953d7483c79cc4b508c8a890d9ebf2e13df28e Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Mon, 13 Mar 2023 15:43:45 +0800 Subject: [PATCH] gojs: introduces `--experimental-workdir` CLI arg (#1226) When compiled to `GOOS=js`, wasm does not maintain the working directory: it is defined by the host. While not explicitly documented, `os.TestDirFSRootDir` in Go suggests the working directory must be valid to pass (literally the directory holding the file). This adds an experimental CLI flag that gives the initial working directory. This is experimental because while GOOS=js uses this, current WASI compilers will not, as they maintain working directory in code managed by wasi-libc, or as a convention (e.g. in Zig). It is not yet known if wasi-cli will maintain working directory externally or not. Signed-off-by: Adrian Cole --- cmd/wazero/wazero.go | 21 ++++++++++++++++----- cmd/wazero/wazero_test.go | 11 +++++++++++ experimental/gojs/gojs.go | 15 +++++++++++++++ internal/gojs/compiler_test.go | 6 +++--- internal/gojs/fs.go | 2 +- internal/gojs/goos/goos.go | 3 ++- internal/gojs/state.go | 11 ++++++++++- internal/platform/open_file_test.go | 14 ++++++++++++++ internal/sysfs/dirfs_test.go | 11 +++++++++++ internal/sysfs/dirfs_unix_test.go | 2 +- internal/sysfs/sysfs_test.go | 12 ++++++++++++ 11 files changed, 96 insertions(+), 12 deletions(-) diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go index da3c8eba..c0be1f9b 100644 --- a/cmd/wazero/wazero.go +++ b/cmd/wazero/wazero.go @@ -123,11 +123,16 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod flags.Var(&envs, "env", "key=value pair of environment variable to expose to the binary. "+ "Can be specified multiple times.") - var envExport bool - flags.BoolVar(&envExport, "env-inherit", false, + var envInherit bool + flags.BoolVar(&envInherit, "env-inherit", false, "inherits any environment variables from the calling process."+ "Variables specified with the flag are appended to the inherited list.") + var workdir string + flags.StringVar(&workdir, "experimental-workdir", "", + "inherits the working directory from the calling process."+ + "Note: This only applies to wasm compiled with `GOARCH=wasm GOOS=js` a.k.a. gojs.") + var mounts sliceFlag flags.Var(&mounts, "mount", "filesystem path to expose to the binary in the form of [:][:ro]. "+ @@ -173,7 +178,7 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod // Don't use map to preserve order var env []string - if envExport { + if envInherit { envs = append(os.Environ(), envs...) } for _, e := range envs { @@ -195,17 +200,19 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod wasmExe := filepath.Base(wasmPath) - ctx := maybeHostLogging(context.Background(), logging.LogScopes(hostlogging), stdErr) - var rtc wazero.RuntimeConfig if useInterpreter { rtc = wazero.NewRuntimeConfigInterpreter() } else { rtc = wazero.NewRuntimeConfig() } + + ctx := maybeHostLogging(context.Background(), logging.LogScopes(hostlogging), stdErr) + if cache := maybeUseCacheDir(cacheDir, stdErr, exit); cache != nil { rtc = rtc.WithCompilationCache(cache) } + if timeout > 0 { newCtx, cancel := context.WithTimeout(ctx, timeout) ctx = newCtx @@ -217,6 +224,10 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod exit(1) } + if workdir != "" { + ctx = gojs.WithWorkdir(ctx, workdir) + } + rt := wazero.NewRuntimeWithConfig(ctx, rtc) defer rt.Close(ctx) diff --git a/cmd/wazero/wazero_test.go b/cmd/wazero/wazero_test.go index 393ecdfd..43440f05 100644 --- a/cmd/wazero/wazero_test.go +++ b/cmd/wazero/wazero_test.go @@ -344,6 +344,13 @@ func TestRun(t *testing.T) { wasmArgs: []string{"/bear.txt"}, expectedStdout: "pooh\n", }, + { + name: "GOARCH=wasm GOOS=js workdir", + wasm: wasmCatGo, + wazeroOpts: []string{"--mount=/:/", fmt.Sprintf("--experimental-workdir=%s", bearDir)}, + wasmArgs: []string{"bear.txt"}, + expectedStdout: "pooh\n", + }, { name: "GOARCH=wasm GOOS=js readonly", wasm: wasmCatGo, @@ -479,6 +486,10 @@ func TestRun(t *testing.T) { for _, tt := range append(tests, cryptoTest) { tc := tt + if runtime.GOOS == "windows" && tc.name == "GOARCH=wasm GOOS=js workdir" { + continue // TODO: Adrian fix this before next RC + } + if tc.wasm == nil { // We should only skip when the runtime is a scratch image. require.False(t, platform.CompilerSupported()) diff --git a/experimental/gojs/gojs.go b/experimental/gojs/gojs.go index 730bc47d..671ae510 100644 --- a/experimental/gojs/gojs.go +++ b/experimental/gojs/gojs.go @@ -16,6 +16,7 @@ package gojs import ( "context" "net/http" + "path/filepath" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" @@ -106,6 +107,20 @@ func WithRoundTripper(ctx context.Context, rt http.RoundTripper) context.Context return context.WithValue(ctx, RoundTripperKey{}, rt) } +// WithWorkdir sets the initial working directory used to Run Wasm. This +// defaults to root "/". +// +// Here's an example that overrides this to "/usr/local/go/src/os". +// +// ctx = gojs.WithWorkdir(ctx, "/usr/local/go/src/os") +// err = gojs.Run(ctx, r, compiled, config) +func WithWorkdir(ctx context.Context, workdir string) context.Context { + // Ensure if used on windows, the input path is translated to a POSIX one. + workdir = workdir[len(filepath.VolumeName(workdir)):] // trim volume prefix (C:) on Windows + workdir = filepath.ToSlash(workdir) // convert \ to / + return context.WithValue(ctx, WorkdirKey{}, workdir) +} + // Run instantiates a new module and calls "run" with the given config. // // # Parameters diff --git a/internal/gojs/compiler_test.go b/internal/gojs/compiler_test.go index a2392c7c..dbe5e81d 100644 --- a/internal/gojs/compiler_test.go +++ b/internal/gojs/compiler_test.go @@ -141,16 +141,16 @@ func TestMain(m *testing.M) { // For example, this allows testing both Go 1.18 and 1.19 in CI. func compileJsWasm(goBin string) error { // Prepare the working directory. - workDir, err := os.MkdirTemp("", "example") + workdir, err := os.MkdirTemp("", "example") if err != nil { return err } - defer os.RemoveAll(workDir) + defer os.RemoveAll(workdir) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - bin := path.Join(workDir, "out.wasm") + bin := path.Join(workdir, "out.wasm") cmd := exec.CommandContext(ctx, goBin, "build", "-o", bin, ".") //nolint:gosec cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", "GOWASM=satconv,signext") cmd.Dir = "testdata" diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go index 41bdbb83..4f545348 100644 --- a/internal/gojs/fs.go +++ b/internal/gojs/fs.go @@ -815,7 +815,7 @@ func jsfsInvoke(ctx context.Context, mod api.Module, callback funcWrapper, err e // resolvePath is needed when a non-absolute path is given to a function. // Unlike other host ABI, GOOS=js maintains the CWD host side. func resolvePath(ctx context.Context, path string) string { - if len(path) == 0 || path[0] == '/' || path[0] == '.' { + if len(path) == 0 || path[0] == '/' { return path // leave alone .. or absolute paths. } return joinPath(getState(ctx).cwd, path) diff --git a/internal/gojs/goos/goos.go b/internal/gojs/goos/goos.go index d46d79ab..3f7eb0ac 100644 --- a/internal/gojs/goos/goos.go +++ b/internal/gojs/goos/goos.go @@ -233,7 +233,8 @@ func (s *stack) SetResultUint32(i int, v uint32) { } func NewFunc(name string, goFunc Func) *wasm.HostFunc { - return util.NewFunc(name, (&stackFunc{name: name, f: goFunc}).Call) + sf := &stackFunc{name: name, f: goFunc} + return util.NewFunc(name, sf.Call) } type Func func(context.Context, api.Module, Stack) diff --git a/internal/gojs/state.go b/internal/gojs/state.go index ae83dc97..17dc9fff 100644 --- a/internal/gojs/state.go +++ b/internal/gojs/state.go @@ -10,11 +10,20 @@ import ( "github.com/tetratelabs/wazero/internal/gojs/values" ) +type WorkdirKey struct{} + +func getWorkdir(ctx context.Context) string { + if wd, ok := ctx.Value(WorkdirKey{}).(string); ok { + return wd + } + return "/" +} + func NewState(ctx context.Context) *State { return &State{ values: values.NewValues(), valueGlobal: newJsGlobal(getRoundTripper(ctx)), - cwd: "/", + cwd: getWorkdir(ctx), _nextCallbackTimeoutID: 1, _scheduledTimeouts: map[uint32]chan bool{}, } diff --git a/internal/platform/open_file_test.go b/internal/platform/open_file_test.go index cc6f754a..468c03d3 100644 --- a/internal/platform/open_file_test.go +++ b/internal/platform/open_file_test.go @@ -3,12 +3,26 @@ package platform import ( "os" path "path/filepath" + "runtime" "syscall" "testing" "github.com/tetratelabs/wazero/internal/testing/require" ) +func TestOpenFile(t *testing.T) { + tmpDir := t.TempDir() + + // from os.TestDirFSPathsValid + if runtime.GOOS != "windows" { + t.Run("strange name", func(t *testing.T) { + f, err := OpenFile(path.Join(tmpDir, `e:xperi\ment.txt`), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + require.NoError(t, err) + require.NoError(t, f.Close()) + }) + } +} + func TestOpenFile_Errors(t *testing.T) { tmpDir := t.TempDir() diff --git a/internal/sysfs/dirfs_test.go b/internal/sysfs/dirfs_test.go index 74e07c12..67019698 100644 --- a/internal/sysfs/dirfs_test.go +++ b/internal/sysfs/dirfs_test.go @@ -690,6 +690,17 @@ func TestDirFS_Stat(t *testing.T) { testFS := NewDirFS(tmpDir) testStat(t, testFS) + + // from os.TestDirFSPathsValid + if runtime.GOOS != "windows" { + t.Run("strange name", func(t *testing.T) { + name := `e:xperi\ment.txt` + require.NoError(t, os.WriteFile(path.Join(tmpDir, name), nil, 0o600)) + + var st platform.Stat_t + require.NoError(t, testFS.Stat(name, &st)) + }) + } } func TestDirFS_Truncate(t *testing.T) { diff --git a/internal/sysfs/dirfs_unix_test.go b/internal/sysfs/dirfs_unix_test.go index f08df51c..2cc7ef97 100644 --- a/internal/sysfs/dirfs_unix_test.go +++ b/internal/sysfs/dirfs_unix_test.go @@ -1,4 +1,4 @@ -//go:build unix +//go:build !windows package sysfs diff --git a/internal/sysfs/sysfs_test.go b/internal/sysfs/sysfs_test.go index 279f5fba..c1bc5e9b 100644 --- a/internal/sysfs/sysfs_test.go +++ b/internal/sysfs/sysfs_test.go @@ -63,6 +63,18 @@ func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS FS) { stat, err := f.Stat() require.NoError(t, err) require.Equal(t, fs.FileMode(0o444), stat.Mode().Perm()) + + // from os.TestDirFSPathsValid + if runtime.GOOS != "windows" { + t.Run("strange name", func(t *testing.T) { + f, err := testFS.OpenFile(`e:xperi\ment.txt`, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + require.NoError(t, err) + defer f.Close() + + var st platform.Stat_t + require.NoError(t, platform.StatFile(f, &st)) + }) + } } func testOpen_Read(t *testing.T, testFS FS, expectIno bool) {