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) {