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