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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{},
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build unix
|
||||
//go:build !windows
|
||||
|
||||
package sysfs
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user