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