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 <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-03-13 15:43:45 +08:00
committed by GitHub
parent e42987a17a
commit 7e953d7483
11 changed files with 96 additions and 12 deletions

View File

@@ -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 <env> 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 <path>[:<wasm path>][: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)

View File

@@ -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())

View File

@@ -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

View File

@@ -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"

View File

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

View File

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

View File

@@ -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{},
}

View File

@@ -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()

View File

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

View File

@@ -1,4 +1,4 @@
//go:build unix
//go:build !windows
package sysfs

View File

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