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

View as plain text