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