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