github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/cmd/gogio/jsbuild.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package main
     4  
     5  import (
     6  	"bytes"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"strings"
    14  	"text/template"
    15  
    16  	"golang.org/x/tools/go/packages"
    17  )
    18  
    19  func buildJS(bi *buildInfo) error {
    20  	out := *destPath
    21  	if out == "" {
    22  		out = bi.name
    23  	}
    24  	if err := os.MkdirAll(out, 0700); err != nil {
    25  		return err
    26  	}
    27  	cmd := exec.Command(
    28  		"go",
    29  		"build",
    30  		"-ldflags="+bi.ldflags,
    31  		"-tags="+bi.tags,
    32  		"-o", filepath.Join(out, "main.wasm"),
    33  		bi.pkgPath,
    34  	)
    35  	cmd.Env = append(
    36  		os.Environ(),
    37  		"GOOS=js",
    38  		"GOARCH=wasm",
    39  	)
    40  	_, err := runCmd(cmd)
    41  	if err != nil {
    42  		return err
    43  	}
    44  
    45  	var faviconPath string
    46  	if _, err := os.Stat(bi.iconPath); err == nil {
    47  		// Copy icon to the output folder
    48  		icon, err := ioutil.ReadFile(bi.iconPath)
    49  		if err != nil {
    50  			return err
    51  		}
    52  		if err := ioutil.WriteFile(filepath.Join(out, filepath.Base(bi.iconPath)), icon, 0600); err != nil {
    53  			return err
    54  		}
    55  		faviconPath = filepath.Base(bi.iconPath)
    56  	}
    57  
    58  	indexTemplate, err := template.New("").Parse(jsIndex)
    59  	if err != nil {
    60  		return err
    61  	}
    62  
    63  	var b bytes.Buffer
    64  	if err := indexTemplate.Execute(&b, struct {
    65  		Name string
    66  		Icon string
    67  	}{
    68  		Name: bi.name,
    69  		Icon: faviconPath,
    70  	}); err != nil {
    71  		return err
    72  	}
    73  
    74  	if err := ioutil.WriteFile(filepath.Join(out, "index.html"), b.Bytes(), 0600); err != nil {
    75  		return err
    76  	}
    77  
    78  	goroot, err := runCmd(exec.Command("go", "env", "GOROOT"))
    79  	if err != nil {
    80  		return err
    81  	}
    82  	wasmJS := filepath.Join(goroot, "misc", "wasm", "wasm_exec.js")
    83  	if _, err := os.Stat(wasmJS); err != nil {
    84  		return fmt.Errorf("failed to find $GOROOT/misc/wasm/wasm_exec.js driver: %v", err)
    85  	}
    86  	pkgs, err := packages.Load(&packages.Config{
    87  		Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps,
    88  		Env:  append(os.Environ(), "GOOS=js", "GOARCH=wasm"),
    89  	}, bi.pkgPath)
    90  	if err != nil {
    91  		return err
    92  	}
    93  	extraJS, err := findPackagesJS(pkgs[0], make(map[string]bool))
    94  	if err != nil {
    95  		return err
    96  	}
    97  
    98  	return mergeJSFiles(filepath.Join(out, "wasm.js"), append([]string{wasmJS}, extraJS...)...)
    99  }
   100  
   101  func findPackagesJS(p *packages.Package, visited map[string]bool) (extraJS []string, err error) {
   102  	if len(p.GoFiles) == 0 {
   103  		return nil, nil
   104  	}
   105  	js, err := filepath.Glob(filepath.Join(filepath.Dir(p.GoFiles[0]), "*_js.js"))
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	extraJS = append(extraJS, js...)
   110  	for _, imp := range p.Imports {
   111  		if !visited[imp.ID] {
   112  			extra, err := findPackagesJS(imp, visited)
   113  			if err != nil {
   114  				return nil, err
   115  			}
   116  			extraJS = append(extraJS, extra...)
   117  			visited[imp.ID] = true
   118  		}
   119  	}
   120  	return extraJS, nil
   121  }
   122  
   123  // mergeJSFiles will merge all files into a single `wasm.js`. It will prepend the jsSetGo
   124  // and append the jsStartGo.
   125  func mergeJSFiles(dst string, files ...string) (err error) {
   126  	w, err := os.Create(dst)
   127  	if err != nil {
   128  		return err
   129  	}
   130  	defer func() {
   131  		if cerr := w.Close(); err != nil {
   132  			err = cerr
   133  		}
   134  	}()
   135  	_, err = io.Copy(w, strings.NewReader(jsSetGo))
   136  	if err != nil {
   137  		return err
   138  	}
   139  	for i := range files {
   140  		r, err := os.Open(files[i])
   141  		if err != nil {
   142  			return err
   143  		}
   144  		_, err = io.Copy(w, r)
   145  		r.Close()
   146  		if err != nil {
   147  			return err
   148  		}
   149  	}
   150  	_, err = io.Copy(w, strings.NewReader(jsStartGo))
   151  	return err
   152  }
   153  
   154  const (
   155  	jsIndex = `<!doctype html>
   156  <html>
   157  	<head>
   158  		<meta charset="utf-8">
   159  		<meta name="viewport" content="width=device-width, user-scalable=no">
   160  		<meta name="mobile-web-app-capable" content="yes">
   161  		{{ if .Icon }}<link rel="icon" href="{{.Icon}}" type="image/x-icon" />{{ end }}
   162  		{{ if .Name }}<title>{{.Name}}</title>{{ end }}
   163  		<script src="wasm.js"></script>
   164  		<style>
   165  			body,pre { margin:0;padding:0; }
   166  		</style>
   167  	</head>
   168  	<body>
   169  	</body>
   170  </html>`
   171  	// jsSetGo sets the `window.go` variable.
   172  	jsSetGo = `(() => {
   173      window.go = {argv: [], env: {}, importObject: {go: {}}};
   174  	const argv = new URLSearchParams(location.search).get("argv");
   175  	if (argv) {
   176  		window.go["argv"] = argv.split(" ");
   177  	}
   178  })();`
   179  	// jsStartGo initializes the main.wasm.
   180  	jsStartGo = `(() => {
   181  	defaultGo = new Go();
   182  	Object.assign(defaultGo["argv"], defaultGo["argv"].concat(go["argv"]));
   183  	Object.assign(defaultGo["env"], go["env"]);
   184  	for (let key in go["importObject"]) {
   185  		if (typeof defaultGo["importObject"][key] === "undefined") {
   186  			defaultGo["importObject"][key] = {};
   187  		}
   188  		Object.assign(defaultGo["importObject"][key], go["importObject"][key]);
   189  	}
   190  	window.go = defaultGo;
   191      if (!WebAssembly.instantiateStreaming) { // polyfill
   192          WebAssembly.instantiateStreaming = async (resp, importObject) => {
   193              const source = await (await resp).arrayBuffer();
   194              return await WebAssembly.instantiate(source, importObject);
   195          };
   196      }
   197      WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
   198          go.run(result.instance);
   199      });
   200  })();`
   201  )