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