github.com/vugu/vugu@v0.3.5/devutil/compiler.go (about) 1 package devutil 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strings" 13 ) 14 15 // NOTE: https://webassembly.org/ says "Wasm" not "WASM" or "WAsm", so that's what I went with on the name. 16 17 // NewWasmCompiler returns a WasmCompiler instance. 18 func NewWasmCompiler() *WasmCompiler { 19 return &WasmCompiler{ 20 logWriter: os.Stderr, 21 } 22 } 23 24 // WasmCompiler provides a convenient way to call `go generate` and `go build` and produce Wasm executables for your system. 25 type WasmCompiler struct { 26 beforeFunc func() error 27 generateCmdFunc func() *exec.Cmd 28 buildCmdFunc func(outpath string) *exec.Cmd 29 afterFunc func(outpath string, err error) error 30 logWriter io.Writer 31 } 32 33 // SetLogWriter sets the writer to use for logging output. Setting it to nil disables logging. 34 // The default from NewWasmCompiler is os.Stderr 35 func (c *WasmCompiler) SetLogWriter(w io.Writer) *WasmCompiler { 36 if w == nil { 37 w = ioutil.Discard 38 } 39 c.logWriter = w 40 return c 41 } 42 43 // SetDir sets both the build and generate directories. 44 func (c *WasmCompiler) SetDir(dir string) *WasmCompiler { 45 return c.SetBuildDir(dir).SetGenerateDir(dir) 46 } 47 48 // SetBuildDir sets the directory of the main package, where `go build` will be run. 49 // Relative paths are okay and will be resolved with filepath.Abs. 50 func (c *WasmCompiler) SetBuildDir(dir string) *WasmCompiler { 51 return c.SetBuildCmdFunc(func(outpath string) *exec.Cmd { 52 cmd := exec.Command("go", "build", "-o", outpath) 53 cmd.Dir = dir 54 cmd.Env = os.Environ() 55 cmd.Env = append(cmd.Env, "GOOS=js", "GOARCH=wasm") 56 return cmd 57 }) 58 } 59 60 // SetBuildCmdFunc provides a function to create the exec.Cmd used when running `go build`. 61 // It overrides any other build-related setting. 62 func (c *WasmCompiler) SetBuildCmdFunc(cmdf func(outpath string) *exec.Cmd) *WasmCompiler { 63 c.buildCmdFunc = cmdf 64 return c 65 } 66 67 // SetGenerateDir sets the directory of where `go generate` will be run. 68 // Relative paths are okay and will be resolved with filepath.Abs. 69 func (c *WasmCompiler) SetGenerateDir(dir string) *WasmCompiler { 70 return c.SetGenerateCmdFunc(func() *exec.Cmd { 71 cmd := exec.Command("go", "generate") 72 cmd.Dir = dir 73 return cmd 74 }) 75 } 76 77 // SetGenerateCmdFunc provides a function to create the exec.Cmd used when running `go generate`. 78 // It overrides any other generate-related setting. 79 func (c *WasmCompiler) SetGenerateCmdFunc(cmdf func() *exec.Cmd) *WasmCompiler { 80 c.generateCmdFunc = cmdf 81 return c 82 } 83 84 // SetBeforeFunc specifies a function to be executed before anything else during Execute(). 85 func (c *WasmCompiler) SetBeforeFunc(f func() error) *WasmCompiler { 86 c.beforeFunc = f 87 return c 88 } 89 90 // SetAfterFunc specifies a function to be executed after everthing else during Execute(). 91 func (c *WasmCompiler) SetAfterFunc(f func(outpath string, err error) error) *WasmCompiler { 92 c.afterFunc = f 93 return c 94 } 95 96 // Execute runs the generate command (if any) and then invokes the Go compiler 97 // and produces a wasm executable (or an error). 98 // The value of outpath is the absolute path to the output file on disk. 99 // It will be created with a temporary name and if no error is returned 100 // it is the caller's responsibility to delete the file when it is no longer needed. 101 // If an error occurs during any of the steps it will be returned with (possibly multi-line) 102 // descriptive output in it's error message, as produced by the underlying tool. 103 func (c *WasmCompiler) Execute() (outpath string, err error) { 104 105 logerr := func(e error) error { 106 if e == nil { 107 return nil 108 } 109 fmt.Fprintln(c.logWriter, e) 110 return e 111 } 112 113 if c.buildCmdFunc == nil { 114 return "", logerr(errors.New("WasmCompiler: no build command set, cannot continue (did you forget to call SetBulidDir?)")) 115 } 116 117 if c.beforeFunc != nil { 118 err := c.beforeFunc() 119 if err != nil { 120 return "", logerr(err) 121 } 122 } 123 124 if c.generateCmdFunc != nil { 125 cmd := c.generateCmdFunc() 126 b, err := cmd.CombinedOutput() 127 if err != nil { 128 return "", logerr(fmt.Errorf("WasmCompiler: generate error: %w; full output:\n%s", err, b)) 129 } 130 fmt.Fprintln(c.logWriter, "WasmCompiler: Successful generate") 131 } 132 133 tmpf, err := ioutil.TempFile("", "WasmCompiler") 134 if err != nil { 135 return "", logerr(fmt.Errorf("WasmCompiler: error creating temporary file: %w", err)) 136 } 137 138 outpath = tmpf.Name() 139 140 err = tmpf.Close() 141 if err != nil { 142 return outpath, logerr(fmt.Errorf("WasmCompiler: error closing temporary file: %w", err)) 143 } 144 145 cmd := c.buildCmdFunc(outpath) 146 b, err := cmd.CombinedOutput() 147 if err != nil { 148 return "", logerr(fmt.Errorf("WasmCompiler: build error: %w; full output:\n%s", err, b)) 149 } 150 fmt.Fprintln(c.logWriter, "WasmCompiler: Successful build") 151 152 if c.afterFunc != nil { 153 err = c.afterFunc(outpath, err) 154 } 155 156 return outpath, logerr(err) 157 158 } 159 160 // WasmExecJS returns the contents of the wasm_exec.js file bundled with the Go compiler. 161 func (c *WasmCompiler) WasmExecJS() (r io.Reader, err error) { 162 163 b1, err := exec.Command("go", "env", "GOROOT").CombinedOutput() 164 if err != nil { 165 return nil, err 166 } 167 168 b2, err := ioutil.ReadFile(filepath.Join(strings.TrimSpace(string(b1)), "misc/wasm/wasm_exec.js")) 169 return bytes.NewReader(b2), err 170 171 }