|
| 1 | +# cmd-go-js |
| 2 | + |
| 3 | +cmd-go-js is a "feature branch" of cmd/go command with experimental changes. The goal was to explore adding support for additional Go compilers and environments by making use of existing -compiler and -exec flags: |
| 4 | + |
| 5 | +> -compiler name - name of compiler to use, as in runtime.Compiler (gccgo or gc). |
| 6 | +> |
| 7 | +> -exec xprog - Run the test binary using xprog. The behavior is the same as in 'go run'. See 'go help run' for details. |
| 8 | +
|
| 9 | +Specifically, I wanted to try adding support for [GopherJS compiler](https://github.com/gopherjs/gopherjs) which targets `GOARCH=js` architecture. It compiles Go code to JavaScript which can be executed by a JavaScript engine (such as V8 JavaScript Engine, Node.js, or any browser with JavaScript support). |
| 10 | + |
| 11 | +# Results |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | +#### go build |
| 16 | + |
| 17 | +You can now use `go` to build for `GOARCH=js` architecture! That is the only architecture that all browsers can execute natively, without any plugins. |
| 18 | + |
| 19 | +```bash |
| 20 | +# Normal build for GOARCH=amd64. |
| 21 | +$ go build ./samples/hello-world |
| 22 | +# Note that above implicitly means: |
| 23 | +# GOARCH=amd64 go build -compiler=gc ./samples/hello-world |
| 24 | +$ ./hello-world |
| 25 | +Hello brave new world! It is working on go1.5.3 darwin/amd64! |
| 26 | + |
| 27 | +# Newly supported build for GOARCH=js. |
| 28 | +$ GOARCH=js go build -compiler=gopherjs ./samples/hello-world |
| 29 | +$ node ./hello-world |
| 30 | +Hello brave new world! It is working on go1.5.3 darwin/js! That means you can execute it in browsers. |
| 31 | +``` |
| 32 | + |
| 33 | +#### go run |
| 34 | + |
| 35 | +`go run` also works. You can use the `-exec` flag to have `node` execute the compiled JavaScript output. |
| 36 | + |
| 37 | +```bash |
| 38 | +$ go run ./samples/hello-world/main.go |
| 39 | +Hello brave new world! It is working on go1.5.3 darwin/amd64! |
| 40 | + |
| 41 | +$ GOARCH=js go run -compiler=gopherjs -exec=node ./samples/hello-world/main.go |
| 42 | +Hello brave new world! It is working on go1.5.3 darwin/js! That means you can execute it in browsers. |
| 43 | +``` |
| 44 | + |
| 45 | +From https://golang.org/cmd/go/#hdr-Compile_and_run_Go_program: |
| 46 | + |
| 47 | +> If the -exec flag is not given, GOOS or GOARCH is different from the system default, and a program named go_$GOOS_$GOARCH_exec can be found on the current search path, 'go run' invokes the binary using that program, for example 'go_nacl_386_exec a.out arguments...'. This allows execution of cross-compiled programs when a simulator or other execution method is available. |
| 48 | +
|
| 49 | +That means if you create a symlink to `node` binary named `go_darwin_js_exec`, then you can just: |
| 50 | + |
| 51 | +```bash |
| 52 | +$ GOARCH=js go run -compiler=gopherjs ./samples/hello-world/main.go |
| 53 | +Hello brave new world! It is working on go1.5.3 darwin/js! That means you can execute it in browsers. |
| 54 | +``` |
| 55 | + |
| 56 | +# Implementation details, lessons learned, other notes |
| 57 | + |
| 58 | +### go test |
| 59 | + |
| 60 | +`go test` can be made to support `-compiler=gopherjs` with `-exec=node`, but additional work needs to be done; it currently doesn't work. |
| 61 | + |
| 62 | +```bash |
| 63 | +$ GOARCH=js go test -v -compiler=gopherjs -exec=node ./samples/hello-world |
| 64 | +=== RUN TestBasic |
| 65 | +--- PASS: TestBasic (0.00s) |
| 66 | +PASS |
| 67 | +ok github.com/gophergala2016/cmd-go-js/samples/hello-world 0.015s |
| 68 | +``` |
| 69 | + |
| 70 | +That means all of go support for [testing](https://godoc.org/testing) would be availble. Including tests, executable examples, and benchmarks. |
| 71 | + |
| 72 | +### Toolchains |
| 73 | + |
| 74 | +`cmd/go` defines a very clean `toolchain` interface internally: |
| 75 | + |
| 76 | +```Go |
| 77 | +type toolchain interface { |
| 78 | + // gc runs the compiler in a specific directory on a set of files |
| 79 | + // and returns the name of the generated output file. |
| 80 | + // The compiler runs in the directory dir. |
| 81 | + gc(b *builder, p *Package, archive, obj string, asmhdr bool, importArgs []string, gofiles []string) (ofile string, out []byte, err error) |
| 82 | + // cc runs the toolchain's C compiler in a directory on a C file |
| 83 | + // to produce an output file. |
| 84 | + cc(b *builder, p *Package, objdir, ofile, cfile string) error |
| 85 | + // asm runs the assembler in a specific directory on a specific file |
| 86 | + // to generate the named output file. |
| 87 | + asm(b *builder, p *Package, obj, ofile, sfile string) error |
| 88 | + // pkgpath builds an appropriate path for a temporary package file. |
| 89 | + pkgpath(basedir string, p *Package) string |
| 90 | + // pack runs the archive packer in a specific directory to create |
| 91 | + // an archive from a set of object files. |
| 92 | + // typically it is run in the object directory. |
| 93 | + pack(b *builder, p *Package, objDir, afile string, ofiles []string) error |
| 94 | + // ld runs the linker to create an executable starting at mainpkg. |
| 95 | + ld(b *builder, root *action, out string, allactions []*action, mainpkg string, ofiles []string) error |
| 96 | + // ldShared runs the linker to create a shared library containing the pkgs built by toplevelactions |
| 97 | + ldShared(b *builder, toplevelactions []*action, out string, allactions []*action) error |
| 98 | + |
| 99 | + compiler() string |
| 100 | + linker() string |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +It currently has two main implementations `gcToolchain`, and `gccgoToolchain` (there's also a 3rd, `noToolchain`, that does nothing). Here you can see how Go currently supports only two values for -compiler flag. |
| 105 | + |
| 106 | +```Go |
| 107 | +switch value { |
| 108 | +case "gc": |
| 109 | + buildToolchain = gcToolchain{} |
| 110 | +case "gccgo": |
| 111 | + buildToolchain = gccgoToolchain{} |
| 112 | +default: |
| 113 | + return fmt.Errorf("unknown compiler %q", value) |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +It's possible to either add GopherJS specifically: |
| 118 | + |
| 119 | +```Go |
| 120 | +switch value { |
| 121 | +case "gc": |
| 122 | + buildToolchain = gcToolchain{} |
| 123 | +case "gccgo": |
| 124 | + buildToolchain = gccgoToolchain{} |
| 125 | +case "gopherjs": |
| 126 | + buildToolchain = gopherjsToolchain{} |
| 127 | +default: |
| 128 | + return fmt.Errorf("unknown compiler %q", value) |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +Or create a general toolchain that can accept any arbitrary -compiler flag value: |
| 133 | + |
| 134 | +```Go |
| 135 | +switch value { |
| 136 | +case "gc": |
| 137 | + buildToolchain = gcToolchain{} |
| 138 | +case "gccgo": |
| 139 | + buildToolchain = gccgoToolchain{} |
| 140 | +default: |
| 141 | + switch compilerBin, err := exec.LookPath(value); err { |
| 142 | + case nil: |
| 143 | + buildToolchain = generalToolchain{compilerBin: compilerBin} |
| 144 | + default: |
| 145 | + return fmt.Errorf("unknown compiler %q", value) |
| 146 | + } |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +### Go 1.6 vs 1.5 |
| 151 | + |
| 152 | +The code for `cmd/go` has a large volume of changes from 1.5 to tip. |
| 153 | + |
| 154 | +I've chosen to work with Go 1.5 for now, since GopherJS compiler only supports Go 1.5 at this time. There is an issue at [gopherjs/gopherjs#355](https://github.com/gopherjs/gopherjs/issues/355) tracking progress for adding Go 1.6 support, and @neelance says it's close. |
| 155 | + |
| 156 | +I initially tried Go 1.6 (see [`go1.6` branch](https://github.com/gophergala2016/cmd-go-js/commits/go1.6)), but switched to working with Go 1.5 at this time for the above reasons. |
| 157 | + |
| 158 | +### Code changes |
| 159 | + |
| 160 | +The first five commits of [`master` branch](https://github.com/gophergala2016/cmd-go-js/commits/master) simply check in the original `cmd/go` binary source at commit [`go1.5.3`](https://github.com/golang/go/tree/go1.5.3), and make minimal changes to allow it to build on OS X and be go-gettable. It also vendors `go/build` package from standard library, in order to be able to make changes to it. |
| 161 | + |
| 162 | +The rest of the changes are the relevant changes to add `-compiler=gopherjs` support to be able to build for `GOARCH=js` architecture. These changes are relatively minimal (a couple hundred lines) and not disruptive. The diff can be seen here: |
| 163 | + |
| 164 | +https://github.com/gophergala2016/cmd-go-js/compare/c900f90...master |
| 165 | + |
| 166 | +### Installation |
| 167 | + |
| 168 | +**Note:** Due to time limits, this project works on OS X only. Adding support for other other systems is easy, but it hasn't been done yet. See commit message of [`2dae5232`](https://github.com/gophergala2016/cmd-go-js/commit/2dae52322dcef1b91b9b363fa2301da735188370), that is the only blocker. |
| 169 | + |
| 170 | +Go 1.5 is required. |
| 171 | + |
| 172 | +```bash |
| 173 | +go get -u github.com/gopherjs/gopherjs |
| 174 | +GO15VENDOREXPERIMENT=1 go get -u github.com/gophergala2016/cmd-go-js/cmd/go |
| 175 | +``` |
| 176 | + |
| 177 | +Since the binary is also called `go`, make sure you run the right one. |
| 178 | + |
| 179 | +```bash |
| 180 | +$ $GOPATH/bin/go version |
| 181 | +go version go1.5.3 darwin/amd64 (with experimental changes) |
| 182 | +``` |
| 183 | + |
| 184 | +You may need Node if you want to execute generated JavaScript from the command line rather than inside a browser. |
| 185 | + |
| 186 | +```bash |
| 187 | +brew install node |
| 188 | +``` |
0 commit comments