goroot.go raw

   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 Moxie using symlinks.
   5  //
   6  // The goal is to replace specific packages from Go with a Moxie 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  	"fmt"
  20  	"io"
  21  	"io/fs"
  22  	"os"
  23  	"os/exec"
  24  	"path"
  25  	"path/filepath"
  26  	"runtime"
  27  	"sort"
  28  
  29  	"moxie/compileopts"
  30  	"moxie/goenv"
  31  )
  32  
  33  // gorootCreateMutex removed: serialized by caller, no concurrent creation.
  34  
  35  // GetCachedGoroot creates a new GOROOT by merging both the standard GOROOT and
  36  // the GOROOT from Moxie 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  	moxieroot := goenv.Get("MOXIEROOT")
  43  	if moxieroot == "" {
  44  		return "", errors.New("could not determine MOXIEROOT")
  45  	}
  46  	// Resolve to absolute path — relative paths break cached goroot symlinks.
  47  	if !filepath.IsAbs(moxieroot) {
  48  		abs, err := filepath.Abs(moxieroot)
  49  		if err != nil {
  50  			return "", fmt.Errorf("could not resolve MOXIEROOT: %w", err)
  51  		}
  52  		moxieroot = abs
  53  	}
  54  
  55  	// Find the overrides needed for the goroot.
  56  	overrides := pathsToOverride(config.GoMinorVersion, needsSyscallPackage(config.BuildTags()))
  57  
  58  	// Resolve the merge links within the goroot.
  59  	merge, err := listGorootMergeLinks(goroot, moxieroot, overrides)
  60  	if err != nil {
  61  		return "", err
  62  	}
  63  
  64  	// Hash the merge links to create a cache key.
  65  	data, err := json.Marshal(merge)
  66  	if err != nil {
  67  		return "", err
  68  	}
  69  	hash := sha512.Sum512_256(data)
  70  
  71  	// Check if the goroot already exists.
  72  	cachedGorootName := "goroot-" + hex.EncodeToString(hash[:])
  73  	cachedgoroot := filepath.Join(goenv.Get("GOCACHE"), cachedGorootName)
  74  	if _, err := os.Stat(cachedgoroot); err == nil {
  75  		return cachedgoroot, nil
  76  	}
  77  
  78  	// Create the cache directory if it does not already exist.
  79  	err = os.MkdirAll(goenv.Get("GOCACHE"), 0777)
  80  	if err != nil {
  81  		return "", err
  82  	}
  83  
  84  	// Create a temporary directory to construct the goroot within.
  85  	tmpgoroot, err := os.MkdirTemp(goenv.Get("GOCACHE"), cachedGorootName+".tmp")
  86  	if err != nil {
  87  		return "", err
  88  	}
  89  
  90  	// Remove the temporary directory if it wasn't moved to the right place
  91  	// (for example, when there was an error).
  92  	defer os.RemoveAll(tmpgoroot)
  93  
  94  	// Create the directory structure.
  95  	// The directories are created in sorted order so that nested directories are created without extra work.
  96  	{
  97  		var dirs []string
  98  		for dir, merge := range overrides {
  99  			if merge {
 100  				dirs = append(dirs, filepath.Join(tmpgoroot, "src", dir))
 101  			}
 102  		}
 103  		sort.Strings(dirs)
 104  
 105  		for _, dir := range dirs {
 106  			err := os.Mkdir(dir, 0777)
 107  			if err != nil {
 108  				return "", err
 109  			}
 110  		}
 111  	}
 112  
 113  	// Create all symlinks.
 114  	for dst, src := range merge {
 115  		err := symlink(src, filepath.Join(tmpgoroot, dst))
 116  		if err != nil {
 117  			return "", err
 118  		}
 119  	}
 120  
 121  	// Rename the new merged gorooot into place.
 122  	err = os.Rename(tmpgoroot, cachedgoroot)
 123  	if err != nil {
 124  		if errors.Is(err, fs.ErrExist) {
 125  			// Another invocation of Moxie also seems to have created a GOROOT.
 126  			// Use that one instead. Our new GOROOT will be automatically
 127  			// deleted by the defer above.
 128  			return cachedgoroot, nil
 129  		}
 130  		if runtime.GOOS == "windows" && errors.Is(err, fs.ErrPermission) {
 131  			// On Windows, a rename with a destination directory that already
 132  			// exists does not result in an IsExist error, but rather in an
 133  			// access denied error. To be sure, check for this case by checking
 134  			// whether the target directory exists.
 135  			if _, err := os.Stat(cachedgoroot); err == nil {
 136  				return cachedgoroot, nil
 137  			}
 138  		}
 139  		return "", err
 140  	}
 141  	return cachedgoroot, nil
 142  }
 143  
 144  // listGorootMergeLinks searches goroot and moxieroot for all symlinks that must be created within the merged goroot.
 145  func listGorootMergeLinks(goroot, moxieroot string, overrides map[string]bool) (map[string]string, error) {
 146  	goSrc := filepath.Join(goroot, "src")
 147  	moxieSrc := filepath.Join(moxieroot, "src")
 148  	merges := make(map[string]string)
 149  	for dir, merge := range overrides {
 150  		if !merge {
 151  			// Use the Moxie version.
 152  			merges[filepath.Join("src", dir)] = filepath.Join(moxieSrc, dir)
 153  			continue
 154  		}
 155  
 156  		// Add files from Moxie.
 157  		moxieDir := filepath.Join(moxieSrc, dir)
 158  		moxieEntries, err := os.ReadDir(moxieDir)
 159  		if err != nil {
 160  			return nil, err
 161  		}
 162  		var hasMoxieFiles bool
 163  		for _, e := range moxieEntries {
 164  			if e.IsDir() {
 165  				continue
 166  			}
 167  
 168  			// Link this file.
 169  			name := e.Name()
 170  			merges[filepath.Join("src", dir, name)] = filepath.Join(moxieDir, name)
 171  
 172  			hasMoxieFiles = true
 173  		}
 174  
 175  		// Add all directories from $GOROOT that are not part of the Moxie
 176  		// overrides.
 177  		goDir := filepath.Join(goSrc, dir)
 178  		goEntries, err := os.ReadDir(goDir)
 179  		if err != nil {
 180  			return nil, err
 181  		}
 182  		for _, e := range goEntries {
 183  			isDir := e.IsDir()
 184  			if hasMoxieFiles && !isDir {
 185  				// Only merge files from Go if Moxie does not have any files.
 186  				// Otherwise we'd end up with a weird mix from both Go
 187  				// implementations.
 188  				continue
 189  			}
 190  
 191  			name := e.Name()
 192  			if _, ok := overrides[path.Join(dir, name)+"/"]; ok {
 193  				// This entry is overridden by Moxie.
 194  				// It has/will be merged elsewhere.
 195  				continue
 196  			}
 197  
 198  			// Add a link to this entry
 199  			merges[filepath.Join("src", dir, name)] = filepath.Join(goDir, name)
 200  		}
 201  	}
 202  
 203  	// Merge the special directories from goroot.
 204  	for _, dir := range []string{"bin", "lib", "pkg"} {
 205  		merges[dir] = filepath.Join(goroot, dir)
 206  	}
 207  
 208  	// Required starting in Go 1.21 due to https://github.com/golang/go/issues/61928
 209  	if _, err := os.Stat(filepath.Join(goroot, "go.env")); err == nil {
 210  		merges["go.env"] = filepath.Join(goroot, "go.env")
 211  	}
 212  
 213  	return merges, nil
 214  }
 215  
 216  // needsSyscallPackage returns whether the syscall package should be overridden
 217  // with the Moxie version. This is the case on some targets.
 218  func needsSyscallPackage(buildTags []string) bool {
 219  	for _, tag := range buildTags {
 220  		if tag == "baremetal" || tag == "nintendoswitch" || tag == "moxie.wasm" {
 221  			return true
 222  		}
 223  	}
 224  	return false
 225  }
 226  
 227  // The boolean indicates whether to merge the subdirs. True means merge, false
 228  // means use the moxie version entirely.
 229  //
 230  // Moxie's src/ tree is self-contained — all stdlib source is in-tree.
 231  // The root entry (false) means use moxie's src/ for everything.
 232  // syscall/ is the one exception: moxie has compiler-intrinsic stubs that
 233  // conflict with Go's full implementation. The merge system provides Go's
 234  // syscall files for hosted targets while moxie's stubs serve baremetal.
 235  func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool {
 236  	return map[string]bool{
 237  		"": false,
 238  	}
 239  }
 240  
 241  // symlink creates a symlink or something similar. On Unix-like systems, it
 242  // always creates a symlink. On Windows, it tries to create a symlink and if
 243  // that fails, creates a hardlink or directory junction instead.
 244  //
 245  // Note that while Windows 10 does support symlinks and allows them to be
 246  // created using os.Symlink, it requires developer mode to be enabled.
 247  // Therefore provide a fallback for when symlinking is not possible.
 248  // Unfortunately this fallback only works when Moxie is installed on the same
 249  // filesystem as the Moxie cache and the Go installation (which is usually the
 250  // C drive).
 251  func symlink(oldname, newname string) error {
 252  	symlinkErr := os.Symlink(oldname, newname)
 253  	if runtime.GOOS == "windows" && symlinkErr != nil {
 254  		// Fallback for when developer mode is disabled.
 255  		// Note that we return the symlink error even if something else fails
 256  		// later on. This is because symlinks are the easiest to support
 257  		// (they're also used on Linux and MacOS) and enabling them is easy:
 258  		// just enable developer mode.
 259  		st, err := os.Stat(oldname)
 260  		if err != nil {
 261  			return symlinkErr
 262  		}
 263  		if st.IsDir() {
 264  			// Make a directory junction. There may be a way to do this
 265  			// programmatically, but it involves a lot of magic. Use the mklink
 266  			// command built into cmd instead (mklink is a builtin, not an
 267  			// external command).
 268  			err := exec.Command("cmd", "/k", "mklink", "/J", newname, oldname).Run()
 269  			if err != nil {
 270  				return symlinkErr
 271  			}
 272  		} else {
 273  			// Try making a hard link.
 274  			err := os.Link(oldname, newname)
 275  			if err != nil {
 276  				// Making a hardlink failed. Try copying the file as a last
 277  				// fallback.
 278  				inf, err := os.Open(oldname)
 279  				if err != nil {
 280  					return err
 281  				}
 282  				defer inf.Close()
 283  				outf, err := os.Create(newname)
 284  				if err != nil {
 285  					return err
 286  				}
 287  				defer outf.Close()
 288  				_, err = io.Copy(outf, inf)
 289  				if err != nil {
 290  					os.Remove(newname)
 291  					return err
 292  				}
 293  				// File was copied.
 294  			}
 295  		}
 296  		return nil // success
 297  	}
 298  	return symlinkErr
 299  }
 300