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