]> Cypherpunks.ru repositories - gostls13.git/blob - src/cmd/go/internal/toolchain/toolchain.go
e6ff584480a0c398a7ca2a008808232bcca537af
[gostls13.git] / src / cmd / go / internal / toolchain / toolchain.go
1 // Copyright 2023 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4
5 // Package toolchain implements dynamic switching of Go toolchains.
6 package toolchain
7
8 import (
9         "context"
10         "errors"
11         "fmt"
12         "go/build"
13         "io/fs"
14         "log"
15         "os"
16         "os/exec"
17         "path/filepath"
18         "runtime"
19         "sort"
20         "strconv"
21         "strings"
22
23         "cmd/go/internal/base"
24         "cmd/go/internal/cfg"
25         "cmd/go/internal/gover"
26         "cmd/go/internal/modfetch"
27         "cmd/go/internal/modload"
28         "cmd/go/internal/run"
29
30         "golang.org/x/mod/module"
31 )
32
33 const (
34         // We download golang.org/toolchain version v0.0.1-<gotoolchain>.<goos>-<goarch>.
35         // If the 0.0.1 indicates anything at all, its the version of the toolchain packaging:
36         // if for some reason we needed to change the way toolchains are packaged into
37         // module zip files in a future version of Go, we could switch to v0.0.2 and then
38         // older versions expecting the old format could use v0.0.1 and newer versions
39         // would use v0.0.2. Of course, then we'd also have to publish two of each
40         // module zip file. It's not likely we'll ever need to change this.
41         gotoolchainModule  = "golang.org/toolchain"
42         gotoolchainVersion = "v0.0.1"
43
44         // targetEnv is a special environment variable set to the expected
45         // toolchain version during the toolchain switch by the parent
46         // process and cleared in the child process. When set, that indicates
47         // to the child to confirm that it provides the expected toolchain version.
48         targetEnv = "GOTOOLCHAIN_INTERNAL_SWITCH_VERSION"
49
50         // countEnv is a special environment variable
51         // that is incremented during each toolchain switch, to detect loops.
52         // It is cleared before invoking programs in 'go run', 'go test', 'go generate', and 'go tool'
53         // by invoking them in an environment filtered with FilterEnv,
54         // so user programs should not see this in their environment.
55         countEnv = "GOTOOLCHAIN_INTERNAL_SWITCH_COUNT"
56
57         // maxSwitch is the maximum toolchain switching depth.
58         // Most uses should never see more than three.
59         // (Perhaps one for the initial GOTOOLCHAIN dispatch,
60         // a second for go get doing an upgrade, and a third if
61         // for some reason the chosen upgrade version is too small
62         // by a little.)
63         // When the count reaches maxSwitch - 10, we start logging
64         // the switched versions for debugging before crashing with
65         // a fatal error upon reaching maxSwitch.
66         // That should be enough to see the repetition.
67         maxSwitch = 100
68 )
69
70 // FilterEnv returns a copy of env with internal GOTOOLCHAIN environment
71 // variables filtered out.
72 func FilterEnv(env []string) []string {
73         // Note: Don't need to filter out targetEnv because Switch does that.
74         var out []string
75         for _, e := range env {
76                 if strings.HasPrefix(e, countEnv+"=") {
77                         continue
78                 }
79                 out = append(out, e)
80         }
81         return out
82 }
83
84 // Switch invokes a different Go toolchain if directed by
85 // the GOTOOLCHAIN environment variable or the user's configuration
86 // or go.mod file.
87 // It must be called early in startup.
88 func Switch() {
89         log.SetPrefix("go: ")
90         defer log.SetPrefix("")
91
92         if !modload.WillBeEnabled() {
93                 return
94         }
95
96         gotoolchain := cfg.Getenv("GOTOOLCHAIN")
97         gover.Startup.GOTOOLCHAIN = gotoolchain
98         if gotoolchain == "" {
99                 // cfg.Getenv should fall back to $GOROOT/go.env,
100                 // so this should not happen, unless a packager
101                 // has deleted the GOTOOLCHAIN line from go.env.
102                 // It can also happen if GOROOT is missing or broken,
103                 // in which case best to let the go command keep running
104                 // and diagnose the problem.
105                 return
106         }
107
108         minToolchain := gover.LocalToolchain()
109         minVers := gover.Local()
110         if min, mode, ok := strings.Cut(gotoolchain, "+"); ok { // go1.2.3+auto
111                 v := gover.FromToolchain(min)
112                 if v == "" {
113                         base.Fatalf("invalid GOTOOLCHAIN %q: invalid minimum toolchain %q", gotoolchain, min)
114                 }
115                 minToolchain = min
116                 minVers = v
117                 if mode != "auto" && mode != "path" {
118                         base.Fatalf("invalid GOTOOLCHAIN %q: only version suffixes are +auto and +path", gotoolchain)
119                 }
120                 gotoolchain = mode
121         }
122
123         if gotoolchain == "auto" || gotoolchain == "path" {
124                 gotoolchain = minToolchain
125
126                 // Locate and read go.mod or go.work.
127                 // For go install m@v, it's the installed module's go.mod.
128                 if m, goVers, ok := goInstallVersion(); ok {
129                         if gover.Compare(goVers, minVers) > 0 {
130                                 // Always print, because otherwise there's no way for the user to know
131                                 // that a non-default toolchain version is being used here.
132                                 // (Normally you can run "go version", but go install m@v ignores the
133                                 // context that "go version" works in.)
134                                 var err error
135                                 gotoolchain, err = NewerToolchain(context.Background(), goVers)
136                                 if err != nil {
137                                         fmt.Fprintf(os.Stderr, "go: %v\n", err)
138                                         gotoolchain = "go" + goVers
139                                 }
140                                 fmt.Fprintf(os.Stderr, "go: using %s for %v\n", gotoolchain, m)
141                         }
142                 } else {
143                         file, goVers, toolchain := modGoToolchain()
144                         gover.Startup.AutoFile = file
145                         if toolchain == "local" {
146                                 // Local means always use the default local toolchain,
147                                 // which is already set, so nothing to do here.
148                                 // Note that if we have Go 1.21 installed originally,
149                                 // GOTOOLCHAIN=go1.30.0+auto or GOTOOLCHAIN=go1.30.0,
150                                 // and the go.mod  says "toolchain local", we use Go 1.30, not Go 1.21.
151                                 // That is, local overrides the "auto" part of the calculation
152                                 // but not the minimum that the user has set.
153                                 // Of course, if the go.mod also says "go 1.35", using Go 1.30
154                                 // will provoke an error about the toolchain being too old.
155                                 // That's what people who use toolchain local want:
156                                 // only ever use the toolchain configured in the local system
157                                 // (including its environment and go env -w file).
158                                 gover.Startup.AutoToolchain = toolchain
159                                 gotoolchain = "local"
160                         } else {
161                                 if toolchain != "" {
162                                         // Accept toolchain only if it is >= our min.
163                                         toolVers := gover.FromToolchain(toolchain)
164                                         if toolVers == "" || (!strings.HasPrefix(toolchain, "go") && !strings.Contains(toolchain, "-go")) {
165                                                 base.Fatalf("invalid toolchain %q in %s", toolchain, base.ShortPath(file))
166                                         }
167                                         if gover.Compare(toolVers, minVers) >= 0 {
168                                                 gotoolchain = toolchain
169                                                 minVers = toolVers
170                                                 gover.Startup.AutoToolchain = toolchain
171                                         }
172                                 }
173                                 if gover.Compare(goVers, minVers) > 0 {
174                                         gotoolchain = "go" + goVers
175                                         gover.Startup.AutoGoVersion = goVers
176                                         gover.Startup.AutoToolchain = "" // in case we are overriding it for being too old
177                                 }
178                         }
179                 }
180         }
181
182         // If we are invoked as a target toolchain, confirm that
183         // we provide the expected version and then run.
184         // This check is delayed until after the handling of auto and path
185         // so that we have initialized gover.Startup for use in error messages.
186         if target := os.Getenv(targetEnv); target != "" && TestVersionSwitch != "loop" {
187                 if gover.LocalToolchain() != target {
188                         base.Fatalf("toolchain %v invoked to provide %v", gover.LocalToolchain(), target)
189                 }
190                 os.Unsetenv(targetEnv)
191
192                 // Note: It is tempting to check that if gotoolchain != "local"
193                 // then target == gotoolchain here, as a sanity check that
194                 // the child has made the same version determination as the parent.
195                 // This turns out not always to be the case. Specifically, if we are
196                 // running Go 1.21 with GOTOOLCHAIN=go1.22+auto, which invokes
197                 // Go 1.22, then 'go get go@1.23.0' or 'go get needs_go_1_23'
198                 // will invoke Go 1.23, but as the Go 1.23 child the reason for that
199                 // will not be apparent here: it will look like we should be using Go 1.22.
200                 // We rely on the targetEnv being set to know not to downgrade.
201                 // A longer term problem with the sanity check is that the exact details
202                 // may change over time: there may be other reasons that a future Go
203                 // version might invoke an older one, and the older one won't know why.
204                 // Best to just accept that we were invoked to provide a specific toolchain
205                 // (which we just checked) and leave it at that.
206                 return
207         }
208
209         if gotoolchain == "local" || gotoolchain == gover.LocalToolchain() {
210                 // Let the current binary handle the command.
211                 return
212         }
213
214         // Minimal sanity check of GOTOOLCHAIN setting before search.
215         // We want to allow things like go1.20.3 but also gccgo-go1.20.3.
216         // We want to disallow mistakes / bad ideas like GOTOOLCHAIN=bash,
217         // since we will find that in the path lookup.
218         if !strings.HasPrefix(gotoolchain, "go1") && !strings.Contains(gotoolchain, "-go1") {
219                 base.Fatalf("invalid GOTOOLCHAIN %q", gotoolchain)
220         }
221
222         SwitchTo(gotoolchain)
223 }
224
225 // NewerToolchain returns the name of the toolchain to use when we need
226 // to reinvoke a newer toolchain that must support at least the given Go version.
227 //
228 // If the latest major release is 1.N.0, we use the latest patch release of 1.(N-1) if that's >= version.
229 // Otherwise we use the latest 1.N if that's allowed.
230 // Otherwise we use the latest release.
231 func NewerToolchain(ctx context.Context, version string) (string, error) {
232         fetch := autoToolchains
233         if !HasAuto() {
234                 fetch = pathToolchains
235         }
236         list, err := fetch(ctx)
237         if err != nil {
238                 return "", err
239         }
240         return newerToolchain(version, list)
241 }
242
243 // autoToolchains returns the list of toolchain versions available to GOTOOLCHAIN=auto or =min+auto mode.
244 func autoToolchains(ctx context.Context) ([]string, error) {
245         var versions *modfetch.Versions
246         err := modfetch.TryProxies(func(proxy string) error {
247                 v, err := modfetch.Lookup(ctx, proxy, "go").Versions(ctx, "")
248                 if err != nil {
249                         return err
250                 }
251                 versions = v
252                 return nil
253         })
254         if err != nil {
255                 return nil, err
256         }
257         return versions.List, nil
258 }
259
260 // pathToolchains returns the list of toolchain versions available to GOTOOLCHAIN=path or =min+path mode.
261 func pathToolchains(ctx context.Context) ([]string, error) {
262         have := make(map[string]bool)
263         var list []string
264         for _, dir := range pathDirs() {
265                 if dir == "" || !filepath.IsAbs(dir) {
266                         // Refuse to use local directories in $PATH (hard-coding exec.ErrDot).
267                         continue
268                 }
269                 entries, err := os.ReadDir(dir)
270                 if err != nil {
271                         continue
272                 }
273                 for _, de := range entries {
274                         if de.IsDir() || !strings.HasPrefix(de.Name(), "go1.") {
275                                 continue
276                         }
277                         info, err := de.Info()
278                         if err != nil {
279                                 continue
280                         }
281                         v, ok := pathVersion(dir, de, info)
282                         if !ok || !strings.HasPrefix(v, "1.") || have[v] {
283                                 continue
284                         }
285                         have[v] = true
286                         list = append(list, v)
287                 }
288         }
289         sort.Slice(list, func(i, j int) bool {
290                 return gover.Compare(list[i], list[j]) < 0
291         })
292         return list, nil
293 }
294
295 // newerToolchain implements NewerToolchain where the list of choices is known.
296 // It is separated out for easier testing of this logic.
297 func newerToolchain(need string, list []string) (string, error) {
298         // Consider each release in the list, from newest to oldest,
299         // considering only entries >= need and then only entries
300         // that are the latest in their language family
301         // (the latest 1.40, the latest 1.39, and so on).
302         // We prefer the latest patch release before the most recent release family,
303         // so if the latest release is 1.40.1 we'll take the latest 1.39.X.
304         // Failing that, we prefer the latest patch release before the most recent
305         // prerelease family, so if the latest release is 1.40rc1 is out but 1.39 is okay,
306         // we'll still take 1.39.X.
307         // Failing that we'll take the latest release.
308         latest := ""
309         for i := len(list) - 1; i >= 0; i-- {
310                 v := list[i]
311                 if gover.Compare(v, need) < 0 {
312                         break
313                 }
314                 if gover.Lang(latest) == gover.Lang(v) {
315                         continue
316                 }
317                 newer := latest
318                 latest = v
319                 if newer != "" && !gover.IsPrerelease(newer) {
320                         // latest is the last patch release of Go 1.X, and we saw a non-prerelease of Go 1.(X+1),
321                         // so latest is the one we want.
322                         break
323                 }
324         }
325         if latest == "" {
326                 return "", fmt.Errorf("no releases found for go >= %v", need)
327         }
328         return "go" + latest, nil
329 }
330
331 // HasAuto reports whether the GOTOOLCHAIN setting allows "auto" upgrades.
332 func HasAuto() bool {
333         env := cfg.Getenv("GOTOOLCHAIN")
334         return env == "auto" || strings.HasSuffix(env, "+auto")
335 }
336
337 // HasPath reports whether the GOTOOLCHAIN setting allows "path" upgrades.
338 func HasPath() bool {
339         env := cfg.Getenv("GOTOOLCHAIN")
340         return env == "path" || strings.HasSuffix(env, "+path")
341 }
342
343 // TestVersionSwitch is set in the test go binary to the value in $TESTGO_VERSION_SWITCH.
344 // Valid settings are:
345 //
346 //      "switch" - simulate version switches by reinvoking the test go binary with a different TESTGO_VERSION.
347 //      "mismatch" - like "switch" but forget to set TESTGO_VERSION, so it looks like we invoked a mismatched toolchain
348 //      "loop" - like "switch" but
349 var TestVersionSwitch string
350
351 // SwitchTo invokes the specified Go toolchain or else prints an error and exits the process.
352 // If $GOTOOLCHAIN is set to path or min+path, SwitchTo only considers the PATH
353 // as a source of Go toolchains. Otherwise SwitchTo tries the PATH but then downloads
354 // a toolchain if necessary.
355 func SwitchTo(gotoolchain string) {
356         log.SetPrefix("go: ")
357
358         count, _ := strconv.Atoi(os.Getenv(countEnv))
359         if count >= maxSwitch-10 {
360                 fmt.Fprintf(os.Stderr, "go: switching from go%v to %v [depth %d]\n", gover.Local(), gotoolchain, count)
361         }
362         if count >= maxSwitch {
363                 base.Fatalf("too many toolchain switches")
364         }
365         os.Setenv(countEnv, fmt.Sprint(count+1))
366
367         env := cfg.Getenv("GOTOOLCHAIN")
368         pathOnly := env == "path" || strings.HasSuffix(env, "+path")
369
370         // For testing, if TESTGO_VERSION is already in use
371         // (only happens in the cmd/go test binary)
372         // and TESTGO_VERSION_SWITCH=switch is set,
373         // "switch" toolchains by changing TESTGO_VERSION
374         // and reinvoking the current binary.
375         // The special cases =loop and =mismatch skip the
376         // setting of TESTGO_VERSION so that it looks like we
377         // accidentally invoked the wrong toolchain,
378         // to test detection of that failure mode.
379         switch TestVersionSwitch {
380         case "switch":
381                 os.Setenv("TESTGO_VERSION", gotoolchain)
382                 fallthrough
383         case "loop", "mismatch":
384                 exe, err := os.Executable()
385                 if err != nil {
386                         base.Fatalf("%v", err)
387                 }
388                 execGoToolchain(gotoolchain, os.Getenv("GOROOT"), exe)
389         }
390
391         // Look in PATH for the toolchain before we download one.
392         // This allows custom toolchains as well as reuse of toolchains
393         // already installed using go install golang.org/dl/go1.2.3@latest.
394         if exe, err := exec.LookPath(gotoolchain); err == nil {
395                 execGoToolchain(gotoolchain, "", exe)
396         }
397
398         // GOTOOLCHAIN=auto looks in PATH and then falls back to download.
399         // GOTOOLCHAIN=path only looks in PATH.
400         if pathOnly {
401                 base.Fatalf("cannot find %q in PATH", gotoolchain)
402         }
403
404         // Set up modules without an explicit go.mod, to download distribution.
405         modload.Reset()
406         modload.ForceUseModules = true
407         modload.RootMode = modload.NoRoot
408         modload.Init()
409
410         // Download and unpack toolchain module into module cache.
411         // Note that multiple go commands might be doing this at the same time,
412         // and that's OK: the module cache handles that case correctly.
413         m := module.Version{
414                 Path:    gotoolchainModule,
415                 Version: gotoolchainVersion + "-" + gotoolchain + "." + runtime.GOOS + "-" + runtime.GOARCH,
416         }
417         dir, err := modfetch.Download(context.Background(), m)
418         if err != nil {
419                 if errors.Is(err, fs.ErrNotExist) {
420                         base.Fatalf("download %s for %s/%s: toolchain not available", gotoolchain, runtime.GOOS, runtime.GOARCH)
421                 }
422                 base.Fatalf("download %s: %v", gotoolchain, err)
423         }
424
425         // On first use after download, set the execute bits on the commands
426         // so that we can run them. Note that multiple go commands might be
427         // doing this at the same time, but if so no harm done.
428         if runtime.GOOS != "windows" {
429                 info, err := os.Stat(filepath.Join(dir, "bin/go"))
430                 if err != nil {
431                         base.Fatalf("download %s: %v", gotoolchain, err)
432                 }
433                 if info.Mode()&0111 == 0 {
434                         // allowExec sets the exec permission bits on all files found in dir.
435                         allowExec := func(dir string) {
436                                 err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
437                                         if err != nil {
438                                                 return err
439                                         }
440                                         if !d.IsDir() {
441                                                 info, err := os.Stat(path)
442                                                 if err != nil {
443                                                         return err
444                                                 }
445                                                 if err := os.Chmod(path, info.Mode()&0777|0111); err != nil {
446                                                         return err
447                                                 }
448                                         }
449                                         return nil
450                                 })
451                                 if err != nil {
452                                         base.Fatalf("download %s: %v", gotoolchain, err)
453                                 }
454                         }
455
456                         // Set the bits in pkg/tool before bin/go.
457                         // If we are racing with another go command and do bin/go first,
458                         // then the check of bin/go above might succeed, the other go command
459                         // would skip its own mode-setting, and then the go command might
460                         // try to run a tool before we get to setting the bits on pkg/tool.
461                         // Setting pkg/tool before bin/go avoids that ordering problem.
462                         // The only other tool the go command invokes is gofmt,
463                         // so we set that one explicitly before handling bin (which will include bin/go).
464                         allowExec(filepath.Join(dir, "pkg/tool"))
465                         allowExec(filepath.Join(dir, "bin/gofmt"))
466                         allowExec(filepath.Join(dir, "bin"))
467                 }
468         }
469
470         // Reinvoke the go command.
471         execGoToolchain(gotoolchain, dir, filepath.Join(dir, "bin/go"))
472 }
473
474 // modGoToolchain finds the enclosing go.work or go.mod file
475 // and returns the go version and toolchain lines from the file.
476 // The toolchain line overrides the version line
477 func modGoToolchain() (file, goVers, toolchain string) {
478         wd := base.UncachedCwd()
479         file = modload.FindGoWork(wd)
480         // $GOWORK can be set to a file that does not yet exist, if we are running 'go work init'.
481         // Do not try to load the file in that case
482         if _, err := os.Stat(file); err != nil {
483                 file = ""
484         }
485         if file == "" {
486                 file = modload.FindGoMod(wd)
487         }
488         if file == "" {
489                 return "", "", ""
490         }
491
492         data, err := os.ReadFile(file)
493         if err != nil {
494                 base.Fatalf("%v", err)
495         }
496         return file, gover.GoModLookup(data, "go"), gover.GoModLookup(data, "toolchain")
497 }
498
499 // goInstallVersion looks at the command line to see if it is go install m@v or go run m@v.
500 // If so, it returns the m@v and the go version from that module's go.mod.
501 func goInstallVersion() (m module.Version, goVers string, found bool) {
502         // Note: We assume there are no flags between 'go' and 'install' or 'run'.
503         // During testing there are some debugging flags that are accepted
504         // in that position, but in production go binaries there are not.
505         if len(os.Args) < 3 || (os.Args[1] != "install" && os.Args[1] != "run") {
506                 return module.Version{}, "", false
507         }
508
509         var arg string
510         switch os.Args[1] {
511         case "install":
512                 // Cannot parse 'go install' command line precisely, because there
513                 // may be new flags we don't know about. Instead, assume the final
514                 // argument is a pkg@version we can use.
515                 arg = os.Args[len(os.Args)-1]
516         case "run":
517                 // For run, the pkg@version can be anywhere on the command line.
518                 // We don't know the flags, so we can't strictly speaking do this correctly.
519                 // We do the best we can by interrogating the CmdRun flags and assume
520                 // that any unknown flag does not take an argument.
521                 args := os.Args[2:]
522                 for i := 0; i < len(args); i++ {
523                         a := args[i]
524                         if !strings.HasPrefix(a, "-") {
525                                 arg = a
526                                 break
527                         }
528                         if a == "-" {
529                                 break
530                         }
531                         if a == "--" {
532                                 if i+1 < len(args) {
533                                         arg = args[i+1]
534                                 }
535                                 break
536                         }
537                         a = strings.TrimPrefix(a, "-")
538                         a = strings.TrimPrefix(a, "-")
539                         if strings.HasPrefix(a, "-") {
540                                 // non-flag but also non-m@v
541                                 break
542                         }
543                         if strings.Contains(a, "=") {
544                                 // already has value
545                                 continue
546                         }
547                         f := run.CmdRun.Flag.Lookup(a)
548                         if f == nil {
549                                 // Unknown flag. Assume it doesn't take a value: best we can do.
550                                 continue
551                         }
552                         if bf, ok := f.Value.(interface{ IsBoolFlag() bool }); ok && bf.IsBoolFlag() {
553                                 // Does not take value.
554                                 continue
555                         }
556                         i++ // Does take a value; skip it.
557                 }
558         }
559         if !strings.Contains(arg, "@") || build.IsLocalImport(arg) || filepath.IsAbs(arg) {
560                 return module.Version{}, "", false
561         }
562         m.Path, m.Version, _ = strings.Cut(arg, "@")
563         if m.Path == "" || m.Version == "" || gover.IsToolchain(m.Path) {
564                 return module.Version{}, "", false
565         }
566
567         // Set up modules without an explicit go.mod, to download go.mod.
568         modload.ForceUseModules = true
569         modload.RootMode = modload.NoRoot
570         modload.Init()
571         defer modload.Reset()
572
573         // See internal/load.PackagesAndErrorsOutsideModule
574         ctx := context.Background()
575         allowed := modload.CheckAllowed
576         if modload.IsRevisionQuery(m.Path, m.Version) {
577                 // Don't check for retractions if a specific revision is requested.
578                 allowed = nil
579         }
580         noneSelected := func(path string) (version string) { return "none" }
581         _, err := modload.QueryPackages(ctx, m.Path, m.Version, noneSelected, allowed)
582         if tooNew := (*gover.TooNewError)(nil); errors.As(err, &tooNew) {
583                 m.Path, m.Version, _ = strings.Cut(tooNew.What, "@")
584                 return m, tooNew.GoVersion, true
585         }
586
587         // QueryPackages succeeded, or it failed for a reason other than
588         // this Go toolchain being too old for the modules encountered.
589         // Either way, we identified the m@v on the command line,
590         // so return found == true so the caller does not fall back to
591         // consulting go.mod.
592         return m, "", true
593 }