github.com/vugu/vugu@v0.3.6-0.20240430171613-3f6f402e014b/devutil/tinygo-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 // DefaultTinygoDockerImage is used as the docker image for Tinygo unless overridden. 15 var DefaultTinygoDockerImage = "tinygo/tinygo:0.30.0" 16 17 // MustNewTinygoCompiler is like NewTinygoCompiler but panics upon error. 18 func MustNewTinygoCompiler() *TinygoCompiler { 19 c, err := NewTinygoCompiler() 20 if err != nil { 21 panic(err) 22 } 23 return c 24 } 25 26 // NewTinygoCompiler returns a new TinygoCompiler instance. 27 func NewTinygoCompiler() (*TinygoCompiler, error) { 28 return &TinygoCompiler{ 29 logWriter: os.Stderr, 30 tinygoDockerImage: DefaultTinygoDockerImage, 31 }, nil 32 } 33 34 // TinygoCompiler provides a convenient way to build a program via Tinygo into Wasm. 35 // This implementation by default uses Docker to download and run Tinygo, and provides methods 36 // to handle mapping local directories into the Tinygo docker filesystem and for 37 // making other dependencies available by calling `go get` on them. This approach 38 // might change once Tinygo has module support, but for now the idea is it 39 // makes it reasonably convenient to integration Tinygo into the workflow for Vugu app. 40 type TinygoCompiler struct { 41 beforeFunc func() error 42 generateCmdFunc func() *exec.Cmd 43 buildCmdFunc func(outpath string) *exec.Cmd 44 dockerBuildCmdFunc func(outpath string) *exec.Cmd 45 afterFunc func(outpath string, err error) error 46 logWriter io.Writer 47 //nolint:golint,unused 48 dlTmpGopath string // temporary directory that we download dependencies into with go get 49 tinygoDockerImage string // docker image name to use, if empty then it is run directly 50 wasmExecJS []byte // contents of wasm_exec.js 51 tinygoArgs []string // additional arguments to pass to the tinygo build cmd 52 } 53 54 // Close performs any cleanup. For now it removes the temporary directory created by NewTinygoCompiler. 55 func (c *TinygoCompiler) Close() error { 56 return nil 57 } 58 59 // SetTinygoArgs sets arguments to be passed to tinygo, e.g. -no-debug 60 func (c *TinygoCompiler) SetTinygoArgs(tinygoArgs ...string) *TinygoCompiler { 61 c.tinygoArgs = tinygoArgs 62 return c 63 } 64 65 // SetLogWriter sets the writer to use for logging output. Setting it to nil disables logging. 66 // The default from NewCompiler is os.Stderr 67 func (c *TinygoCompiler) SetLogWriter(w io.Writer) *TinygoCompiler { 68 if w == nil { 69 w = io.Discard 70 } 71 c.logWriter = w 72 return c 73 } 74 75 // SetDir sets both the build and generate directories. 76 func (c *TinygoCompiler) SetDir(dir string) *TinygoCompiler { 77 return c.SetBuildDir(dir).SetGenerateDir(dir) 78 } 79 80 // SetBuildDir sets the directory of the main package, where `go build` will be run. 81 // Relative paths are okay and will be resolved with filepath.Abs. 82 func (c *TinygoCompiler) SetBuildDir(dir string) *TinygoCompiler { 83 return c.SetBuildCmdFunc(func(outpath string) *exec.Cmd { 84 cmd := exec.Command("tinygo", "build", "-target=wasm", "-o", outpath, ".") 85 cmd.Dir = dir 86 return cmd 87 }).SetDockerBuildCmdFunc(func(outpath string) *exec.Cmd { 88 buildDir := dir 89 buildDirAbs, err := filepath.Abs(buildDir) 90 if err != nil { 91 panic(err) 92 } 93 buildDirAbs, err = filepath.EvalSymlinks(buildDirAbs) // Mac OS /var -> /var/private bs 94 if err != nil { 95 panic(err) 96 } 97 98 tinygoDockerImage := c.tinygoDockerImage 99 100 // run tinygo via docker 101 // example: docker run --rm \ 102 // -v /:/src \ 103 // -w /src/`pwd` \ 104 // tinygo/tinygo:0.30.0 tinygo build -o /root/go/src/example.com/tgtest1/out.wasm \ 105 // -target=wasm . 106 107 args := make([]string, 0, 20) 108 args = append(args, "run", "--rm") 109 args = append(args, "-v", "/:/src") // map dir for dependencies 110 args = append(args, "-e", "HOME=/tmp") 111 args = append(args, fmt.Sprintf("--user=%d", os.Getuid())) 112 args = append(args, "-w", "/src"+buildDirAbs) 113 114 args = append(args, tinygoDockerImage) 115 args = append(args, "tinygo", "build") 116 args = append(args, "-o", "/src/"+outpath) 117 args = append(args, "-target=wasm") 118 args = append(args, c.tinygoArgs...) 119 args = append(args, ".") 120 121 return exec.Command("docker", args...) 122 }) 123 } 124 125 // SetBuildCmdFunc provides a function to create the exec.Cmd used when running `go build`. 126 // It overrides any other build-related setting. 127 func (c *TinygoCompiler) SetBuildCmdFunc(cmdf func(outpath string) *exec.Cmd) *TinygoCompiler { 128 c.buildCmdFunc = cmdf 129 return c 130 } 131 132 // SetDockerBuildCmdFunc provides a function to create the exec.Cmd used when running 133 // `tinygo build` in docker. 134 func (c *TinygoCompiler) SetDockerBuildCmdFunc(cmdf func(outpath string) *exec.Cmd) *TinygoCompiler { 135 c.dockerBuildCmdFunc = cmdf 136 return c 137 } 138 139 // SetGenerateDir sets the directory of where `go generate` will be run. 140 // Relative paths are okay and will be resolved with filepath.Abs. 141 func (c *TinygoCompiler) SetGenerateDir(dir string) *TinygoCompiler { 142 return c.SetGenerateCmdFunc(func() *exec.Cmd { 143 cmd := exec.Command("go", "generate") 144 cmd.Dir = dir 145 return cmd 146 }) 147 } 148 149 // SetGenerateCmdFunc provides a function to create the exec.Cmd used when running `go generate`. 150 // It overrides any other generate-related setting. 151 func (c *TinygoCompiler) SetGenerateCmdFunc(cmdf func() *exec.Cmd) *TinygoCompiler { 152 c.generateCmdFunc = cmdf 153 return c 154 } 155 156 // SetBeforeFunc specifies a function to be executed before anything else during Execute(). 157 func (c *TinygoCompiler) SetBeforeFunc(f func() error) *TinygoCompiler { 158 c.beforeFunc = f 159 return c 160 } 161 162 // SetAfterFunc specifies a function to be executed after everthing else during Execute(). 163 func (c *TinygoCompiler) SetAfterFunc(f func(outpath string, err error) error) *TinygoCompiler { 164 c.afterFunc = f 165 return c 166 } 167 168 // NoDocker is an alias for SetTinygoDockerImage("") and will result in the tinygo 169 // executable being run on the local system instead of via docker image. 170 func (c *TinygoCompiler) NoDocker() *TinygoCompiler { 171 return c.SetTinygoDockerImage("") 172 } 173 174 // SetTinygoDockerImage will specify the docker image to use when invoking Tinygo. 175 // The default value is the value of when NewTinygoCompiler was called. 176 // If you specify an empty string then the "tinygo" command will be run directly 177 // on the local system. 178 func (c *TinygoCompiler) SetTinygoDockerImage(img string) *TinygoCompiler { 179 c.tinygoDockerImage = img 180 return c 181 } 182 183 // Execute runs the generate command (if any) and then invokes the Tinygo compiler 184 // and produces a wasm executable (or an error). 185 // The value of outpath is the absolute path to the output file on disk. 186 // It will be created with a temporary name and if no error is returned 187 // it is the caller's responsibility to delete the file when it is no longer needed. 188 // If an error occurs during any of the steps it will be returned with (possibly multi-line) 189 // descriptive output in it's error message, as produced by the underlying tool. 190 func (c *TinygoCompiler) Execute() (outpath string, err error) { 191 192 logerr := func(e error) error { 193 if e == nil { 194 return nil 195 } 196 fmt.Fprintln(c.logWriter, e) 197 return e 198 } 199 200 if c.buildCmdFunc == nil { 201 return "", logerr(errors.New("TinygoCompiler: no build directory set, cannot continue (did you forget to call SetBulidDir?)")) 202 } 203 204 if c.beforeFunc != nil { 205 err := c.beforeFunc() 206 if err != nil { 207 return "", logerr(err) 208 } 209 } 210 211 if c.generateCmdFunc != nil { 212 cmd := c.generateCmdFunc() 213 b, err := cmd.CombinedOutput() 214 if err != nil { 215 return "", logerr(fmt.Errorf("TinygoCompiler: generate error: %w; full output:\n%s", err, b)) 216 } 217 fmt.Fprintln(c.logWriter, "TinygoCompiler: Successful generate") 218 } 219 220 tmpf, err := os.CreateTemp("", "WasmCompiler") 221 if err != nil { 222 return "", logerr(fmt.Errorf("WasmCompiler: error creating temporary file: %w", err)) 223 } 224 225 outpath = tmpf.Name() 226 227 err = tmpf.Close() 228 if err != nil { 229 return outpath, logerr(fmt.Errorf("WasmCompiler: error closing temporary file: %w", err)) 230 } 231 232 os.Remove(outpath) 233 234 if c.tinygoDockerImage == "" { 235 cmd := c.buildCmdFunc(outpath) 236 b, err := cmd.CombinedOutput() 237 if err != nil { 238 return "", logerr(fmt.Errorf("TinygoCompiler: build error: %w; cmd.args: %v, full output:\n%s", err, cmd.Args, b)) 239 } 240 fmt.Fprintf(c.logWriter, "TinygoCompiler: successful build\n") 241 242 } else { 243 cmd := c.dockerBuildCmdFunc(outpath) 244 b, err := cmd.CombinedOutput() 245 if err != nil { 246 return "", logerr(fmt.Errorf("TinygoCompiler: build error: %w; cmd.args: %v, full output:\n%s", err, cmd.Args, b)) 247 } 248 fmt.Fprintf(c.logWriter, "TinygoCompiler: successful build. Output: %s\n", b) 249 } 250 251 return outpath, nil 252 } 253 254 // WasmExecJS returns the contents of the wasm_exec.js file bundled with Tinygo. 255 func (c *TinygoCompiler) WasmExecJS() (r io.Reader, err error) { 256 257 if c.wasmExecJS != nil { 258 return bytes.NewReader(c.wasmExecJS), nil 259 } 260 261 tinygoDockerImage := c.tinygoDockerImage 262 263 // direct way, not via docker 264 if tinygoDockerImage == "" { 265 266 cmd := exec.Command("tinygo", "env", "TINYGOROOT") 267 resb, err := cmd.CombinedOutput() 268 if err != nil { 269 return nil, fmt.Errorf("TinygoCompiler: WasmExecJS error getting TINYGOROOT: %w; full output:\n%s", err, resb) 270 } 271 272 wasmExecJSPath := filepath.Join(strings.TrimSpace(string(resb)), "targets/wasm_exec.js") 273 b, err := os.ReadFile(wasmExecJSPath) 274 if err != nil { 275 return nil, fmt.Errorf("TinygoCompiler: WasmExecJS error reading %q: %w", wasmExecJSPath, err) 276 } 277 278 c.wasmExecJS = b 279 280 return bytes.NewReader(c.wasmExecJS), nil 281 } 282 283 // via docker 284 285 args := make([]string, 0, 20) 286 args = append(args, "run", "--rm", "-i") 287 args = append(args, tinygoDockerImage) 288 args = append(args, "/bin/bash", "-c") 289 // different locations between tinygo and tinygo-dev, check them both 290 args = append(args, "cat `tinygo env TINYGOROOT`/targets/wasm_exec.js") 291 292 cmd := exec.Command("docker", args...) 293 b, err := cmd.CombinedOutput() 294 if err != nil { 295 return nil, fmt.Errorf("TinygoCompiler: wasm_exec.js error (cmd=docker %v): %w; full output:\n%s", args, err, b) 296 } 297 // fmt.Fprintf(c.logWriter, "TinygoCompiler: successful wasm_exec.js: docker %v; output: %s\n", args, b) 298 299 c.wasmExecJS = b 300 301 return bytes.NewReader(c.wasmExecJS), nil 302 303 }