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  }