Skip to content

Commit 522f228

Browse files
committed
loader: merge roots from both Go and TinyGo in a cached directory
This commit changes the way that packages are looked up. Instead of working around the loader package by modifying the GOROOT variable for specific packages, create a new GOROOT using symlinks. This GOROOT is cached for the specified configuration (Go version, underlying GOROOT path, TinyGo path, whether to override the syscall package). This will also enable go module support in the future. Windows is a bit harder to support, because it only allows the creation of symlinks when developer mode is enabled. This is worked around by using symlinks and if that fails, using directory junctions or hardlinks instead. This should work in the vast majority of cases. The only case it doesn't work, is if developer mode is disabled and TinyGo, the Go toolchain, and the cache directory are not all on the same filesystem. If this is a problem, it is still possible to improve the code by using file copies instead.
1 parent 3b794c8 commit 522f228

File tree

4 files changed

+284
-55
lines changed

4 files changed

+284
-55
lines changed

compiler/compiler.go

Lines changed: 5 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -137,64 +137,25 @@ func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Con
137137
c.funcPtrAddrSpace = dummyFunc.Type().PointerAddressSpace()
138138
dummyFunc.EraseFromParentAsFunction()
139139

140-
// Prefix the GOPATH with the system GOROOT, as GOROOT is already set to
141-
// the TinyGo root.
142-
overlayGopath := goenv.Get("GOPATH")
143-
if overlayGopath == "" {
144-
overlayGopath = goenv.Get("GOROOT")
145-
} else {
146-
overlayGopath = goenv.Get("GOROOT") + string(filepath.ListSeparator) + overlayGopath
147-
}
148-
149140
wd, err := os.Getwd()
150141
if err != nil {
151142
return c.mod, nil, []error{err}
152143
}
144+
goroot, err := loader.GetCachedGoroot(c.Config)
145+
if err != nil {
146+
return c.mod, nil, []error{err}
147+
}
153148
lprogram := &loader.Program{
154149
Build: &build.Context{
155150
GOARCH: c.GOARCH(),
156151
GOOS: c.GOOS(),
157-
GOROOT: goenv.Get("GOROOT"),
152+
GOROOT: goroot,
158153
GOPATH: goenv.Get("GOPATH"),
159154
CgoEnabled: c.CgoEnabled(),
160155
UseAllFiles: false,
161156
Compiler: "gc", // must be one of the recognized compilers
162157
BuildTags: c.BuildTags(),
163158
},
164-
OverlayBuild: &build.Context{
165-
GOARCH: c.GOARCH(),
166-
GOOS: c.GOOS(),
167-
GOROOT: goenv.Get("TINYGOROOT"),
168-
GOPATH: overlayGopath,
169-
CgoEnabled: c.CgoEnabled(),
170-
UseAllFiles: false,
171-
Compiler: "gc", // must be one of the recognized compilers
172-
BuildTags: c.BuildTags(),
173-
},
174-
OverlayPath: func(path string) string {
175-
// Return the (overlay) import path when it should be overlaid, and
176-
// "" if it should not.
177-
if strings.HasPrefix(path, tinygoPath+"/src/") {
178-
// Avoid issues with packages that are imported twice, one from
179-
// GOPATH and one from TINYGOPATH.
180-
path = path[len(tinygoPath+"/src/"):]
181-
}
182-
switch path {
183-
case "machine", "os", "reflect", "runtime", "runtime/interrupt", "runtime/volatile", "sync", "testing", "internal/reflectlite", "internal/task":
184-
return path
185-
default:
186-
if strings.HasPrefix(path, "device/") || strings.HasPrefix(path, "examples/") {
187-
return path
188-
} else if path == "syscall" {
189-
for _, tag := range c.BuildTags() {
190-
if tag == "baremetal" || tag == "darwin" {
191-
return path
192-
}
193-
}
194-
}
195-
}
196-
return ""
197-
},
198159
TypeChecker: types.Config{
199160
Sizes: &stdSizes{
200161
IntSize: int64(c.targetData.TypeAllocSize(c.intType)),

loader/goroot.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package loader
2+
3+
// This file constructs a new temporary GOROOT directory by merging both the
4+
// standard Go GOROOT and the GOROOT from TinyGo using symlinks.
5+
6+
import (
7+
"crypto/sha512"
8+
"encoding/hex"
9+
"errors"
10+
"io/ioutil"
11+
"math/rand"
12+
"os"
13+
"os/exec"
14+
"path"
15+
"path/filepath"
16+
"runtime"
17+
"strconv"
18+
19+
"github.com/tinygo-org/tinygo/compileopts"
20+
"github.com/tinygo-org/tinygo/goenv"
21+
)
22+
23+
// GetCachedGoroot creates a new GOROOT by merging both the standard GOROOT and
24+
// the GOROOT from TinyGo using lots of symbolic links.
25+
func GetCachedGoroot(config *compileopts.Config) (string, error) {
26+
goroot := goenv.Get("GOROOT")
27+
if goroot == "" {
28+
return "", errors.New("could not determine GOROOT")
29+
}
30+
tinygoroot := goenv.Get("TINYGOROOT")
31+
if tinygoroot == "" {
32+
return "", errors.New("could not determine TINYGOROOT")
33+
}
34+
35+
needsSyscallPackage := false
36+
for _, tag := range config.BuildTags() {
37+
if tag == "baremetal" || tag == "darwin" {
38+
needsSyscallPackage = true
39+
}
40+
}
41+
42+
// Determine the location of the cached GOROOT.
43+
version, err := goenv.GorootVersionString(goroot)
44+
if err != nil {
45+
return "", err
46+
}
47+
gorootsHash := sha512.Sum512_256([]byte(goroot + "\x00" + tinygoroot))
48+
gorootsHashHex := hex.EncodeToString(gorootsHash[:])
49+
cachedgoroot := filepath.Join(goenv.Get("GOCACHE"), "goroot-"+version+"-"+gorootsHashHex)
50+
if needsSyscallPackage {
51+
cachedgoroot += "-syscall"
52+
}
53+
54+
if _, err := os.Stat(cachedgoroot); err == nil {
55+
return cachedgoroot, nil
56+
}
57+
tmpgoroot := cachedgoroot + ".tmp" + strconv.Itoa(rand.Int())
58+
err = os.MkdirAll(tmpgoroot, 0777)
59+
if err != nil {
60+
return "", err
61+
}
62+
63+
// Remove the temporary directory if it wasn't moved to the right place
64+
// (for example, when there was an error).
65+
defer os.RemoveAll(tmpgoroot)
66+
67+
for _, name := range []string{"bin", "lib", "pkg"} {
68+
err = symlink(filepath.Join(goroot, name), filepath.Join(tmpgoroot, name))
69+
if err != nil {
70+
return "", err
71+
}
72+
}
73+
err = mergeDirectory(goroot, tinygoroot, tmpgoroot, "", pathsToOverride(needsSyscallPackage))
74+
if err != nil {
75+
return "", err
76+
}
77+
err = os.Rename(tmpgoroot, cachedgoroot)
78+
if err != nil {
79+
if os.IsExist(err) {
80+
// Another invocation of TinyGo also seems to have created a GOROOT.
81+
// Use that one instead. Our new GOROOT will be automatically
82+
// deleted by the defer above.
83+
return cachedgoroot, nil
84+
}
85+
return "", err
86+
}
87+
return cachedgoroot, nil
88+
}
89+
90+
// mergeDirectory merges two roots recursively. The tmpgoroot is the directory
91+
// that will be created by this call by either symlinking the directory from
92+
// goroot or tinygoroot, or by creating the directory and merging the contents.
93+
func mergeDirectory(goroot, tinygoroot, tmpgoroot, importPath string, overrides map[string]bool) error {
94+
if mergeSubdirs, ok := overrides[importPath+"/"]; ok {
95+
if !mergeSubdirs {
96+
// This directory and all subdirectories should come from the TinyGo
97+
// root, so simply make a symlink.
98+
newname := filepath.Join(tmpgoroot, "src", importPath)
99+
oldname := filepath.Join(tinygoroot, "src", importPath)
100+
return symlink(oldname, newname)
101+
}
102+
103+
// Merge subdirectories. Start by making the directory to merge.
104+
err := os.Mkdir(filepath.Join(tmpgoroot, "src", importPath), 0777)
105+
if err != nil {
106+
return err
107+
}
108+
109+
// Symlink all files from TinyGo, and symlink directories from TinyGo
110+
// that need to be overridden.
111+
tinygoEntries, err := ioutil.ReadDir(filepath.Join(tinygoroot, "src", importPath))
112+
if err != nil {
113+
return err
114+
}
115+
for _, e := range tinygoEntries {
116+
if e.IsDir() {
117+
// A directory, so merge this thing.
118+
err := mergeDirectory(goroot, tinygoroot, tmpgoroot, path.Join(importPath, e.Name()), overrides)
119+
if err != nil {
120+
return err
121+
}
122+
} else {
123+
// A file, so symlink this.
124+
newname := filepath.Join(tmpgoroot, "src", importPath, e.Name())
125+
oldname := filepath.Join(tinygoroot, "src", importPath, e.Name())
126+
err := symlink(oldname, newname)
127+
if err != nil {
128+
return err
129+
}
130+
}
131+
}
132+
133+
// Symlink all directories from $GOROOT that are not part of the TinyGo
134+
// overrides.
135+
gorootEntries, err := ioutil.ReadDir(filepath.Join(goroot, "src", importPath))
136+
if err != nil {
137+
return err
138+
}
139+
for _, e := range gorootEntries {
140+
if !e.IsDir() {
141+
// Don't merge in files from Go. Otherwise we'd end up with a
142+
// weird syscall package with files from both roots.
143+
continue
144+
}
145+
if _, ok := overrides[path.Join(importPath, e.Name())+"/"]; ok {
146+
// Already included above, so don't bother trying to create this
147+
// symlink.
148+
continue
149+
}
150+
newname := filepath.Join(tmpgoroot, "src", importPath, e.Name())
151+
oldname := filepath.Join(goroot, "src", importPath, e.Name())
152+
err := symlink(oldname, newname)
153+
if err != nil {
154+
return err
155+
}
156+
}
157+
}
158+
return nil
159+
}
160+
161+
// The boolean indicates whether to merge the subdirs. True means merge, false
162+
// means use the TinyGo version.
163+
func pathsToOverride(needsSyscallPackage bool) map[string]bool {
164+
paths := map[string]bool{
165+
"/": true,
166+
"device/": false,
167+
"examples/": false,
168+
"internal/": true,
169+
"internal/reflectlite/": false,
170+
"internal/task/": false,
171+
"machine/": false,
172+
"os/": true,
173+
"reflect/": false,
174+
"runtime/": false,
175+
"sync/": true,
176+
"testing/": false,
177+
}
178+
if needsSyscallPackage {
179+
paths["syscall/"] = true // include syscall/js
180+
}
181+
return paths
182+
}
183+
184+
// symlink creates a symlink or something similar. On Unix-like systems, it
185+
// always creates a symlink. On Windows, it tries to create a symlink and if
186+
// that fails, creates a hardlink or directory junction instead.
187+
//
188+
// Note that while Windows 10 does support symlinks and allows them to be
189+
// created using os.Symlink, it requires developer mode to be enabled.
190+
// Therefore provide a fallback for when symlinking is not possible.
191+
// Unfortunately this fallback only works when TinyGo is installed on the same
192+
// filesystem as the TinyGo cache and the Go installation (which is usually the
193+
// C drive).
194+
func symlink(oldname, newname string) error {
195+
symlinkErr := os.Symlink(oldname, newname)
196+
if runtime.GOOS == "windows" && symlinkErr != nil {
197+
// Fallback for when developer mode is disabled.
198+
// Note that we return the symlink error even if something else fails
199+
// later on. This is because symlinks are the easiest to support
200+
// (they're also used on Linux and MacOS) and enabling them is easy:
201+
// just enable developer mode.
202+
st, err := os.Stat(oldname)
203+
if err != nil {
204+
return symlinkErr
205+
}
206+
if st.IsDir() {
207+
// Make a directory junction. There may be a way to do this
208+
// programmatically, but it involves a lot of magic. Use the mklink
209+
// command built into cmd instead (mklink is a builtin, not an
210+
// external command).
211+
err := exec.Command("cmd", "/k", "mklink", "/J", newname, oldname).Run()
212+
if err != nil {
213+
return symlinkErr
214+
}
215+
} else {
216+
// Make a hard link.
217+
err := os.Link(oldname, newname)
218+
if err != nil {
219+
return symlinkErr
220+
}
221+
}
222+
return nil // success
223+
}
224+
return symlinkErr
225+
}

loader/loader.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import (
2222
type Program struct {
2323
mainPkg string
2424
Build *build.Context
25-
OverlayBuild *build.Context
26-
OverlayPath func(path string) string
2725
Packages map[string]*Package
2826
sorted []*Package
2927
fset *token.FileSet
@@ -54,10 +52,6 @@ func (p *Program) Import(path, srcDir string, pos token.Position) (*Package, err
5452

5553
// Load this package.
5654
ctx := p.Build
57-
if newPath := p.OverlayPath(path); newPath != "" {
58-
ctx = p.OverlayBuild
59-
path = newPath
60-
}
6155
buildPkg, err := ctx.Import(path, srcDir, build.ImportComment)
6256
if err != nil {
6357
return nil, scanner.Error{

0 commit comments

Comments
 (0)