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 }