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 )