wa-lang.org/wazero@v1.0.2/cmd/wazero/wazero.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"flag"
     7  	"fmt"
     8  	"io"
     9  	"io/fs"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  
    14  	"wa-lang.org/wazero"
    15  	"wa-lang.org/wazero/imports/wasi_snapshot_preview1"
    16  	"wa-lang.org/wazero/sys"
    17  )
    18  
    19  func main() {
    20  	doMain(os.Stdout, os.Stderr, os.Exit)
    21  }
    22  
    23  // doMain is separated out for the purpose of unit testing.
    24  func doMain(stdOut io.Writer, stdErr io.Writer, exit func(code int)) {
    25  	flag.CommandLine.SetOutput(stdErr)
    26  
    27  	var help bool
    28  	flag.BoolVar(&help, "h", false, "print usage")
    29  
    30  	flag.Parse()
    31  
    32  	if help || flag.NArg() == 0 {
    33  		printUsage(stdErr)
    34  		exit(0)
    35  	}
    36  
    37  	if flag.NArg() < 1 {
    38  		fmt.Fprintln(stdErr, "missing path to wasm file")
    39  		printUsage(stdErr)
    40  		exit(1)
    41  	}
    42  
    43  	subCmd := flag.Arg(0)
    44  	switch subCmd {
    45  	case "run":
    46  		doRun(flag.Args()[1:], stdOut, stdErr, exit)
    47  	default:
    48  		fmt.Fprintln(stdErr, "invalid command")
    49  		printUsage(stdErr)
    50  		exit(1)
    51  	}
    52  }
    53  
    54  func doRun(args []string, stdOut io.Writer, stdErr io.Writer, exit func(code int)) {
    55  	flags := flag.NewFlagSet("run", flag.ExitOnError)
    56  	flags.SetOutput(stdErr)
    57  
    58  	var help bool
    59  	flags.BoolVar(&help, "h", false, "print usage")
    60  
    61  	var envs sliceFlag
    62  	flags.Var(&envs, "env", "key=value pair of environment variable to expose to the binary. "+
    63  		"Can be specified multiple times.")
    64  
    65  	var mounts sliceFlag
    66  	flags.Var(&mounts, "mount",
    67  		"filesystem path to expose to the binary in the form of <host path>[:<wasm path>]. If wasm path is not "+
    68  			"provided, the host path will be used. Can be specified multiple times.")
    69  
    70  	_ = flags.Parse(args)
    71  
    72  	if help {
    73  		printRunUsage(stdErr, flags)
    74  		exit(0)
    75  	}
    76  
    77  	if flags.NArg() < 1 {
    78  		fmt.Fprintln(stdErr, "missing path to wasm file")
    79  		printRunUsage(stdErr, flags)
    80  		exit(1)
    81  	}
    82  	wasmPath := flags.Arg(0)
    83  
    84  	wasmArgs := flags.Args()[1:]
    85  	if len(wasmArgs) > 1 {
    86  		// Skip "--" if provided
    87  		if wasmArgs[0] == "--" {
    88  			wasmArgs = wasmArgs[1:]
    89  		}
    90  	}
    91  
    92  	// Don't use map to preserve order
    93  	var env []string
    94  	for _, e := range envs {
    95  		fields := strings.SplitN(e, "=", 2)
    96  		if len(fields) != 2 {
    97  			fmt.Fprintf(stdErr, "invalid environment variable: %s\n", e)
    98  			exit(1)
    99  		}
   100  		env = append(env, fields[0], fields[1])
   101  	}
   102  
   103  	var mountFS fs.FS
   104  	if len(mounts) > 0 {
   105  		cfs := &compositeFS{
   106  			paths: map[string]fs.FS{},
   107  		}
   108  		for _, mount := range mounts {
   109  			if len(mount) == 0 {
   110  				fmt.Fprintln(stdErr, "invalid mount: empty string")
   111  				exit(1)
   112  			}
   113  
   114  			// TODO(anuraaga): Support wasm paths with colon in them.
   115  			var host, guest string
   116  			if clnIdx := strings.LastIndexByte(mount, ':'); clnIdx != -1 {
   117  				host, guest = mount[:clnIdx], mount[clnIdx+1:]
   118  			} else {
   119  				host = mount
   120  				guest = host
   121  			}
   122  
   123  			if guest[0] == '.' {
   124  				fmt.Fprintf(stdErr, "invalid mount: guest path must not start with .: %s\n", guest)
   125  				exit(1)
   126  			}
   127  
   128  			// wazero always calls fs.Open with a relative path.
   129  			if guest[0] == '/' {
   130  				guest = guest[1:]
   131  			}
   132  			cfs.paths[guest] = os.DirFS(host)
   133  		}
   134  		mountFS = cfs
   135  	}
   136  
   137  	wasm, err := os.ReadFile(wasmPath)
   138  	if err != nil {
   139  		fmt.Fprintf(stdErr, "error reading wasm binary: %v\n", err)
   140  		exit(1)
   141  	}
   142  
   143  	wasmExe := filepath.Base(wasmPath)
   144  
   145  	ctx := context.Background()
   146  	rt := wazero.NewRuntime(ctx)
   147  	defer rt.Close(ctx)
   148  
   149  	// Because we are running a binary directly rather than embedding in an application,
   150  	// we default to wiring up commonly used OS functionality.
   151  	conf := wazero.NewModuleConfig().
   152  		WithStdout(stdOut).
   153  		WithStderr(stdErr).
   154  		WithStdin(os.Stdin).
   155  		WithRandSource(rand.Reader).
   156  		WithSysNanosleep().
   157  		WithSysNanotime().
   158  		WithSysWalltime().
   159  		WithArgs(append([]string{wasmExe}, wasmArgs...)...)
   160  	for i := 0; i < len(env); i += 2 {
   161  		conf = conf.WithEnv(env[i], env[i+1])
   162  	}
   163  	if mountFS != nil {
   164  		conf = conf.WithFS(mountFS)
   165  	}
   166  
   167  	code, err := rt.CompileModule(ctx, wasm)
   168  	if err != nil {
   169  		fmt.Fprintf(stdErr, "error compiling wasm binary: %v\n", err)
   170  		exit(1)
   171  	}
   172  
   173  	// WASI is needed to access args and very commonly required by self-contained wasm
   174  	// binaries, so we instantiate it by default.
   175  	wasi_snapshot_preview1.MustInstantiate(ctx, rt)
   176  
   177  	_, err = rt.InstantiateModule(ctx, code, conf)
   178  	if err != nil {
   179  		if exitErr, ok := err.(*sys.ExitError); ok {
   180  			exit(int(exitErr.ExitCode()))
   181  		}
   182  		fmt.Fprintf(stdErr, "error instantiating wasm binary: %v\n", err)
   183  		exit(1)
   184  	}
   185  
   186  	// We're done, _start was called as part of instantiating the module.
   187  	exit(0)
   188  }
   189  
   190  func printUsage(stdErr io.Writer) {
   191  	fmt.Fprintln(stdErr, "wazero CLI")
   192  	fmt.Fprintln(stdErr)
   193  	fmt.Fprintln(stdErr, "Usage:\n  wazero <command>")
   194  	fmt.Fprintln(stdErr)
   195  	fmt.Fprintln(stdErr, "Commands:")
   196  	fmt.Fprintln(stdErr, "  run\t\tRuns a WebAssembly binary")
   197  }
   198  
   199  func printRunUsage(stdErr io.Writer, flags *flag.FlagSet) {
   200  	fmt.Fprintln(stdErr, "wazero CLI")
   201  	fmt.Fprintln(stdErr)
   202  	fmt.Fprintln(stdErr, "Usage:\n  wazero run <options> <path to wasm file> [--] <wasm args>")
   203  	fmt.Fprintln(stdErr)
   204  	fmt.Fprintln(stdErr, "Options:")
   205  	flags.PrintDefaults()
   206  }
   207  
   208  type sliceFlag []string
   209  
   210  func (f *sliceFlag) String() string {
   211  	return strings.Join(*f, ",")
   212  }
   213  
   214  func (f *sliceFlag) Set(s string) error {
   215  	*f = append(*f, s)
   216  	return nil
   217  }