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  }