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  }