github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/loader/goroot.go (about) 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 // The goal is to replace specific packages from Go with a TinyGo version. It's 7 // never a partial replacement, either a package is fully replaced or it is not. 8 // This is important because if we did allow to merge packages (e.g. by adding 9 // files to a package), it would lead to a dependency on implementation details 10 // with all the maintenance burden that results in. Only allowing to replace 11 // packages as a whole avoids this as packages are already designed to have a 12 // public (backwards-compatible) API. 13 14 import ( 15 "crypto/sha512" 16 "encoding/hex" 17 "encoding/json" 18 "errors" 19 "io" 20 "io/fs" 21 "os" 22 "os/exec" 23 "path" 24 "path/filepath" 25 "runtime" 26 "sort" 27 "sync" 28 29 "github.com/tinygo-org/tinygo/compileopts" 30 "github.com/tinygo-org/tinygo/goenv" 31 ) 32 33 var gorootCreateMutex sync.Mutex 34 35 // GetCachedGoroot creates a new GOROOT by merging both the standard GOROOT and 36 // the GOROOT from TinyGo using lots of symbolic links. 37 func GetCachedGoroot(config *compileopts.Config) (string, error) { 38 goroot := goenv.Get("GOROOT") 39 if goroot == "" { 40 return "", errors.New("could not determine GOROOT") 41 } 42 tinygoroot := goenv.Get("TINYGOROOT") 43 if tinygoroot == "" { 44 return "", errors.New("could not determine TINYGOROOT") 45 } 46 47 // Find the overrides needed for the goroot. 48 overrides := pathsToOverride(config.GoMinorVersion, needsSyscallPackage(config.BuildTags())) 49 50 // Resolve the merge links within the goroot. 51 merge, err := listGorootMergeLinks(goroot, tinygoroot, overrides) 52 if err != nil { 53 return "", err 54 } 55 56 // Hash the merge links to create a cache key. 57 data, err := json.Marshal(merge) 58 if err != nil { 59 return "", err 60 } 61 hash := sha512.Sum512_256(data) 62 63 // Do not try to create the cached GOROOT in parallel, that's only a waste 64 // of I/O bandwidth and thus speed. Instead, use a mutex to make sure only 65 // one goroutine does it at a time. 66 // This is not a way to ensure atomicity (a different TinyGo invocation 67 // could be creating the same directory), but instead a way to avoid 68 // creating it many times in parallel when running tests in parallel. 69 gorootCreateMutex.Lock() 70 defer gorootCreateMutex.Unlock() 71 72 // Check if the goroot already exists. 73 cachedGorootName := "goroot-" + hex.EncodeToString(hash[:]) 74 cachedgoroot := filepath.Join(goenv.Get("GOCACHE"), cachedGorootName) 75 if _, err := os.Stat(cachedgoroot); err == nil { 76 return cachedgoroot, nil 77 } 78 79 // Create the cache directory if it does not already exist. 80 err = os.MkdirAll(goenv.Get("GOCACHE"), 0777) 81 if err != nil { 82 return "", err 83 } 84 85 // Create a temporary directory to construct the goroot within. 86 tmpgoroot, err := os.MkdirTemp(goenv.Get("GOCACHE"), cachedGorootName+".tmp") 87 if err != nil { 88 return "", err 89 } 90 91 // Remove the temporary directory if it wasn't moved to the right place 92 // (for example, when there was an error). 93 defer os.RemoveAll(tmpgoroot) 94 95 // Create the directory structure. 96 // The directories are created in sorted order so that nested directories are created without extra work. 97 { 98 var dirs []string 99 for dir, merge := range overrides { 100 if merge { 101 dirs = append(dirs, filepath.Join(tmpgoroot, "src", dir)) 102 } 103 } 104 sort.Strings(dirs) 105 106 for _, dir := range dirs { 107 err := os.Mkdir(dir, 0777) 108 if err != nil { 109 return "", err 110 } 111 } 112 } 113 114 // Create all symlinks. 115 for dst, src := range merge { 116 err := symlink(src, filepath.Join(tmpgoroot, dst)) 117 if err != nil { 118 return "", err 119 } 120 } 121 122 // Rename the new merged gorooot into place. 123 err = os.Rename(tmpgoroot, cachedgoroot) 124 if err != nil { 125 if errors.Is(err, fs.ErrExist) { 126 // Another invocation of TinyGo also seems to have created a GOROOT. 127 // Use that one instead. Our new GOROOT will be automatically 128 // deleted by the defer above. 129 return cachedgoroot, nil 130 } 131 if runtime.GOOS == "windows" && errors.Is(err, fs.ErrPermission) { 132 // On Windows, a rename with a destination directory that already 133 // exists does not result in an IsExist error, but rather in an 134 // access denied error. To be sure, check for this case by checking 135 // whether the target directory exists. 136 if _, err := os.Stat(cachedgoroot); err == nil { 137 return cachedgoroot, nil 138 } 139 } 140 return "", err 141 } 142 return cachedgoroot, nil 143 } 144 145 // listGorootMergeLinks searches goroot and tinygoroot for all symlinks that must be created within the merged goroot. 146 func listGorootMergeLinks(goroot, tinygoroot string, overrides map[string]bool) (map[string]string, error) { 147 goSrc := filepath.Join(goroot, "src") 148 tinygoSrc := filepath.Join(tinygoroot, "src") 149 merges := make(map[string]string) 150 for dir, merge := range overrides { 151 if !merge { 152 // Use the TinyGo version. 153 merges[filepath.Join("src", dir)] = filepath.Join(tinygoSrc, dir) 154 continue 155 } 156 157 // Add files from TinyGo. 158 tinygoDir := filepath.Join(tinygoSrc, dir) 159 tinygoEntries, err := os.ReadDir(tinygoDir) 160 if err != nil { 161 return nil, err 162 } 163 var hasTinyGoFiles bool 164 for _, e := range tinygoEntries { 165 if e.IsDir() { 166 continue 167 } 168 169 // Link this file. 170 name := e.Name() 171 merges[filepath.Join("src", dir, name)] = filepath.Join(tinygoDir, name) 172 173 hasTinyGoFiles = true 174 } 175 176 // Add all directories from $GOROOT that are not part of the TinyGo 177 // overrides. 178 goDir := filepath.Join(goSrc, dir) 179 goEntries, err := os.ReadDir(goDir) 180 if err != nil { 181 return nil, err 182 } 183 for _, e := range goEntries { 184 isDir := e.IsDir() 185 if hasTinyGoFiles && !isDir { 186 // Only merge files from Go if TinyGo does not have any files. 187 // Otherwise we'd end up with a weird mix from both Go 188 // implementations. 189 continue 190 } 191 192 name := e.Name() 193 if _, ok := overrides[path.Join(dir, name)+"/"]; ok { 194 // This entry is overridden by TinyGo. 195 // It has/will be merged elsewhere. 196 continue 197 } 198 199 // Add a link to this entry 200 merges[filepath.Join("src", dir, name)] = filepath.Join(goDir, name) 201 } 202 } 203 204 // Merge the special directories from goroot. 205 for _, dir := range []string{"bin", "lib", "pkg"} { 206 merges[dir] = filepath.Join(goroot, dir) 207 } 208 209 // Required starting in Go 1.21 due to https://github.com/golang/go/issues/61928 210 if _, err := os.Stat(filepath.Join(goroot, "go.env")); err == nil { 211 merges["go.env"] = filepath.Join(goroot, "go.env") 212 } 213 214 return merges, nil 215 } 216 217 // needsSyscallPackage returns whether the syscall package should be overriden 218 // with the TinyGo version. This is the case on some targets. 219 func needsSyscallPackage(buildTags []string) bool { 220 for _, tag := range buildTags { 221 if tag == "baremetal" || tag == "darwin" || tag == "nintendoswitch" || tag == "tinygo.wasm" { 222 return true 223 } 224 } 225 return false 226 } 227 228 // The boolean indicates whether to merge the subdirs. True means merge, false 229 // means use the TinyGo version. 230 func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool { 231 paths := map[string]bool{ 232 "": true, 233 "crypto/": true, 234 "crypto/rand/": false, 235 "crypto/tls/": false, 236 "device/": false, 237 "examples/": false, 238 "internal/": true, 239 "internal/bytealg/": false, 240 "internal/fuzz/": false, 241 "internal/reflectlite/": false, 242 "internal/task/": false, 243 "machine/": false, 244 "net/": true, 245 "net/http/": false, 246 "os/": true, 247 "os/user/": false, 248 "reflect/": false, 249 "runtime/": false, 250 "sync/": true, 251 "testing/": true, 252 } 253 254 if goMinor >= 19 { 255 paths["crypto/internal/"] = true 256 paths["crypto/internal/boring/"] = true 257 paths["crypto/internal/boring/sig/"] = false 258 } 259 260 if needsSyscallPackage { 261 paths["syscall/"] = true // include syscall/js 262 } 263 return paths 264 } 265 266 // symlink creates a symlink or something similar. On Unix-like systems, it 267 // always creates a symlink. On Windows, it tries to create a symlink and if 268 // that fails, creates a hardlink or directory junction instead. 269 // 270 // Note that while Windows 10 does support symlinks and allows them to be 271 // created using os.Symlink, it requires developer mode to be enabled. 272 // Therefore provide a fallback for when symlinking is not possible. 273 // Unfortunately this fallback only works when TinyGo is installed on the same 274 // filesystem as the TinyGo cache and the Go installation (which is usually the 275 // C drive). 276 func symlink(oldname, newname string) error { 277 symlinkErr := os.Symlink(oldname, newname) 278 if runtime.GOOS == "windows" && symlinkErr != nil { 279 // Fallback for when developer mode is disabled. 280 // Note that we return the symlink error even if something else fails 281 // later on. This is because symlinks are the easiest to support 282 // (they're also used on Linux and MacOS) and enabling them is easy: 283 // just enable developer mode. 284 st, err := os.Stat(oldname) 285 if err != nil { 286 return symlinkErr 287 } 288 if st.IsDir() { 289 // Make a directory junction. There may be a way to do this 290 // programmatically, but it involves a lot of magic. Use the mklink 291 // command built into cmd instead (mklink is a builtin, not an 292 // external command). 293 err := exec.Command("cmd", "/k", "mklink", "/J", newname, oldname).Run() 294 if err != nil { 295 return symlinkErr 296 } 297 } else { 298 // Try making a hard link. 299 err := os.Link(oldname, newname) 300 if err != nil { 301 // Making a hardlink failed. Try copying the file as a last 302 // fallback. 303 inf, err := os.Open(oldname) 304 if err != nil { 305 return err 306 } 307 defer inf.Close() 308 outf, err := os.Create(newname) 309 if err != nil { 310 return err 311 } 312 defer outf.Close() 313 _, err = io.Copy(outf, inf) 314 if err != nil { 315 os.Remove(newname) 316 return err 317 } 318 // File was copied. 319 } 320 } 321 return nil // success 322 } 323 return symlinkErr 324 }