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
5package toolchain
6
7import (
8	"context"
9	"fmt"
10	"os"
11	"path/filepath"
12	"sort"
13	"strings"
14
15	"cmd/go/internal/base"
16	"cmd/go/internal/cfg"
17	"cmd/go/internal/gover"
18	"cmd/go/internal/modfetch"
19	"cmd/internal/telemetry/counter"
20)
21
22// A Switcher collects errors to be reported and then decides
23// between reporting the errors or switching to a new toolchain
24// to resolve them.
25//
26// The client calls [Switcher.Error] repeatedly with errors encountered
27// and then calls [Switcher.Switch]. If the errors included any
28// *gover.TooNewErrors (potentially wrapped) and switching is
29// permitted by GOTOOLCHAIN, Switch switches to a new toolchain.
30// Otherwise Switch prints all the errors using base.Error.
31//
32// See https://go.dev/doc/toolchain#switch.
33type Switcher struct {
34	TooNew *gover.TooNewError // max go requirement observed
35	Errors []error            // errors collected so far
36}
37
38// Error reports the error to the Switcher,
39// which saves it for processing during Switch.
40func (s *Switcher) Error(err error) {
41	s.Errors = append(s.Errors, err)
42	s.addTooNew(err)
43}
44
45// addTooNew adds any TooNew errors that can be found in err.
46func (s *Switcher) addTooNew(err error) {
47	switch err := err.(type) {
48	case interface{ Unwrap() []error }:
49		for _, e := range err.Unwrap() {
50			s.addTooNew(e)
51		}
52
53	case interface{ Unwrap() error }:
54		s.addTooNew(err.Unwrap())
55
56	case *gover.TooNewError:
57		if s.TooNew == nil ||
58			gover.Compare(err.GoVersion, s.TooNew.GoVersion) > 0 ||
59			gover.Compare(err.GoVersion, s.TooNew.GoVersion) == 0 && err.What < s.TooNew.What {
60			s.TooNew = err
61		}
62	}
63}
64
65// NeedSwitch reports whether Switch would attempt to switch toolchains.
66func (s *Switcher) NeedSwitch() bool {
67	return s.TooNew != nil && (HasAuto() || HasPath())
68}
69
70// Switch decides whether to switch to a newer toolchain
71// to resolve any of the saved errors.
72// It switches if toolchain switches are permitted and there is at least one TooNewError.
73//
74// If Switch decides not to switch toolchains, it prints the errors using base.Error and returns.
75//
76// If Switch decides to switch toolchains but cannot identify a toolchain to use.
77// it prints the errors along with one more about not being able to find the toolchain
78// and returns.
79//
80// Otherwise, Switch prints an informational message giving a reason for the
81// switch and the toolchain being invoked and then switches toolchains.
82// This operation never returns.
83func (s *Switcher) Switch(ctx context.Context) {
84	if !s.NeedSwitch() {
85		for _, err := range s.Errors {
86			base.Error(err)
87		}
88		return
89	}
90
91	// Switch to newer Go toolchain if necessary and possible.
92	tv, err := NewerToolchain(ctx, s.TooNew.GoVersion)
93	if err != nil {
94		for _, err := range s.Errors {
95			base.Error(err)
96		}
97		base.Error(fmt.Errorf("switching to go >= %v: %w", s.TooNew.GoVersion, err))
98		return
99	}
100
101	fmt.Fprintf(os.Stderr, "go: %v requires go >= %v; switching to %v\n", s.TooNew.What, s.TooNew.GoVersion, tv)
102	counterSwitchExec.Inc()
103	Exec(tv)
104	panic("unreachable")
105}
106
107var counterSwitchExec = counter.New("go/toolchain/switch-exec")
108
109// SwitchOrFatal attempts a toolchain switch based on the information in err
110// and otherwise falls back to base.Fatal(err).
111func SwitchOrFatal(ctx context.Context, err error) {
112	var s Switcher
113	s.Error(err)
114	s.Switch(ctx)
115	base.Exit()
116}
117
118// NewerToolchain returns the name of the toolchain to use when we need
119// to switch to a newer toolchain that must support at least the given Go version.
120// See https://go.dev/doc/toolchain#switch.
121//
122// If the latest major release is 1.N.0, we use the latest patch release of 1.(N-1) if that's >= version.
123// Otherwise we use the latest 1.N if that's allowed.
124// Otherwise we use the latest release.
125func NewerToolchain(ctx context.Context, version string) (string, error) {
126	fetch := autoToolchains
127	if !HasAuto() {
128		fetch = pathToolchains
129	}
130	list, err := fetch(ctx)
131	if err != nil {
132		return "", err
133	}
134	return newerToolchain(version, list)
135}
136
137// autoToolchains returns the list of toolchain versions available to GOTOOLCHAIN=auto or =min+auto mode.
138func autoToolchains(ctx context.Context) ([]string, error) {
139	var versions *modfetch.Versions
140	err := modfetch.TryProxies(func(proxy string) error {
141		v, err := modfetch.Lookup(ctx, proxy, "go").Versions(ctx, "")
142		if err != nil {
143			return err
144		}
145		versions = v
146		return nil
147	})
148	if err != nil {
149		return nil, err
150	}
151	return versions.List, nil
152}
153
154// pathToolchains returns the list of toolchain versions available to GOTOOLCHAIN=path or =min+path mode.
155func pathToolchains(ctx context.Context) ([]string, error) {
156	have := make(map[string]bool)
157	var list []string
158	for _, dir := range pathDirs() {
159		if dir == "" || !filepath.IsAbs(dir) {
160			// Refuse to use local directories in $PATH (hard-coding exec.ErrDot).
161			continue
162		}
163		entries, err := os.ReadDir(dir)
164		if err != nil {
165			continue
166		}
167		for _, de := range entries {
168			if de.IsDir() || !strings.HasPrefix(de.Name(), "go1.") {
169				continue
170			}
171			info, err := de.Info()
172			if err != nil {
173				continue
174			}
175			v, ok := pathVersion(dir, de, info)
176			if !ok || !strings.HasPrefix(v, "1.") || have[v] {
177				continue
178			}
179			have[v] = true
180			list = append(list, v)
181		}
182	}
183	sort.Slice(list, func(i, j int) bool {
184		return gover.Compare(list[i], list[j]) < 0
185	})
186	return list, nil
187}
188
189// newerToolchain implements NewerToolchain where the list of choices is known.
190// It is separated out for easier testing of this logic.
191func newerToolchain(need string, list []string) (string, error) {
192	// Consider each release in the list, from newest to oldest,
193	// considering only entries >= need and then only entries
194	// that are the latest in their language family
195	// (the latest 1.40, the latest 1.39, and so on).
196	// We prefer the latest patch release before the most recent release family,
197	// so if the latest release is 1.40.1 we'll take the latest 1.39.X.
198	// Failing that, we prefer the latest patch release before the most recent
199	// prerelease family, so if the latest release is 1.40rc1 is out but 1.39 is okay,
200	// we'll still take 1.39.X.
201	// Failing that we'll take the latest release.
202	latest := ""
203	for i := len(list) - 1; i >= 0; i-- {
204		v := list[i]
205		if gover.Compare(v, need) < 0 {
206			break
207		}
208		if gover.Lang(latest) == gover.Lang(v) {
209			continue
210		}
211		newer := latest
212		latest = v
213		if newer != "" && !gover.IsPrerelease(newer) {
214			// latest is the last patch release of Go 1.X, and we saw a non-prerelease of Go 1.(X+1),
215			// so latest is the one we want.
216			break
217		}
218	}
219	if latest == "" {
220		return "", fmt.Errorf("no releases found for go >= %v", need)
221	}
222	return "go" + latest, nil
223}
224
225// HasAuto reports whether the GOTOOLCHAIN setting allows "auto" upgrades.
226func HasAuto() bool {
227	env := cfg.Getenv("GOTOOLCHAIN")
228	return env == "auto" || strings.HasSuffix(env, "+auto")
229}
230
231// HasPath reports whether the GOTOOLCHAIN setting allows "path" upgrades.
232func HasPath() bool {
233	env := cfg.Getenv("GOTOOLCHAIN")
234	return env == "path" || strings.HasSuffix(env, "+path")
235}
236