Source file src/cmd/go/internal/modload/list.go

     1  // Copyright 2018 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 modload
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"runtime"
    16  	"strings"
    17  
    18  	"cmd/go/internal/base"
    19  	"cmd/go/internal/cfg"
    20  	"cmd/go/internal/gover"
    21  	"cmd/go/internal/modfetch/codehost"
    22  	"cmd/go/internal/modinfo"
    23  	"cmd/go/internal/search"
    24  	"cmd/internal/par"
    25  	"cmd/internal/pkgpattern"
    26  
    27  	"golang.org/x/mod/module"
    28  )
    29  
    30  type ListMode int
    31  
    32  const (
    33  	ListU ListMode = 1 << iota
    34  	ListRetracted
    35  	ListDeprecated
    36  	ListVersions
    37  	ListRetractedVersions
    38  )
    39  
    40  // ListModules returns a description of the modules matching args, if known,
    41  // along with any error preventing additional matches from being identified.
    42  //
    43  // The returned slice can be nonempty even if the error is non-nil.
    44  func ListModules(ctx context.Context, args []string, mode ListMode, reuseFile string) ([]*modinfo.ModulePublic, error) {
    45  	var reuse map[module.Version]*modinfo.ModulePublic
    46  	if reuseFile != "" {
    47  		data, err := os.ReadFile(reuseFile)
    48  		if err != nil {
    49  			return nil, err
    50  		}
    51  		dec := json.NewDecoder(bytes.NewReader(data))
    52  		reuse = make(map[module.Version]*modinfo.ModulePublic)
    53  		for {
    54  			var m modinfo.ModulePublic
    55  			if err := dec.Decode(&m); err != nil {
    56  				if err == io.EOF {
    57  					break
    58  				}
    59  				return nil, fmt.Errorf("parsing %s: %v", reuseFile, err)
    60  			}
    61  			if m.Origin == nil {
    62  				continue
    63  			}
    64  			m.Reuse = true
    65  			reuse[module.Version{Path: m.Path, Version: m.Version}] = &m
    66  			if m.Query != "" {
    67  				reuse[module.Version{Path: m.Path, Version: m.Query}] = &m
    68  			}
    69  		}
    70  	}
    71  
    72  	rs, mods, err := listModules(ctx, LoadModFile(ctx), args, mode, reuse)
    73  
    74  	type token struct{}
    75  	sem := make(chan token, runtime.GOMAXPROCS(0))
    76  	if mode != 0 {
    77  		for _, m := range mods {
    78  			if m.Reuse {
    79  				continue
    80  			}
    81  			add := func(m *modinfo.ModulePublic) {
    82  				sem <- token{}
    83  				go func() {
    84  					if mode&ListU != 0 {
    85  						addUpdate(ctx, m)
    86  					}
    87  					if mode&ListVersions != 0 {
    88  						addVersions(ctx, m, mode&ListRetractedVersions != 0)
    89  					}
    90  					if mode&ListRetracted != 0 {
    91  						addRetraction(ctx, m)
    92  					}
    93  					if mode&ListDeprecated != 0 {
    94  						addDeprecation(ctx, m)
    95  					}
    96  					<-sem
    97  				}()
    98  			}
    99  
   100  			add(m)
   101  			if m.Replace != nil {
   102  				add(m.Replace)
   103  			}
   104  		}
   105  	}
   106  	// Fill semaphore channel to wait for all tasks to finish.
   107  	for n := cap(sem); n > 0; n-- {
   108  		sem <- token{}
   109  	}
   110  
   111  	if err == nil {
   112  		requirements = rs
   113  		// TODO(#61605): The extra ListU clause fixes a problem with Go 1.21rc3
   114  		// where "go mod tidy" and "go list -m -u all" fight over whether the go.sum
   115  		// should be considered up-to-date. The fix for now is to always treat the
   116  		// go.sum as up-to-date during list -m -u. Probably the right fix is more targeted,
   117  		// but in general list -u is looking up other checksums in the checksum database
   118  		// that won't be necessary later, so it makes sense not to write the go.sum back out.
   119  		if !ExplicitWriteGoMod && mode&ListU == 0 {
   120  			err = commitRequirements(ctx, WriteOpts{})
   121  		}
   122  	}
   123  	return mods, err
   124  }
   125  
   126  func listModules(ctx context.Context, rs *Requirements, args []string, mode ListMode, reuse map[module.Version]*modinfo.ModulePublic) (_ *Requirements, mods []*modinfo.ModulePublic, mgErr error) {
   127  	if len(args) == 0 {
   128  		var ms []*modinfo.ModulePublic
   129  		for _, m := range MainModules.Versions() {
   130  			if gover.IsToolchain(m.Path) {
   131  				continue
   132  			}
   133  			ms = append(ms, moduleInfo(ctx, rs, m, mode, reuse))
   134  		}
   135  		return rs, ms, nil
   136  	}
   137  
   138  	needFullGraph := false
   139  	for _, arg := range args {
   140  		if strings.Contains(arg, `\`) {
   141  			base.Fatalf("go: module paths never use backslash")
   142  		}
   143  		if search.IsRelativePath(arg) {
   144  			base.Fatalf("go: cannot use relative path %s to specify module", arg)
   145  		}
   146  		if arg == "all" || strings.Contains(arg, "...") {
   147  			needFullGraph = true
   148  			if !HasModRoot() {
   149  				base.Fatalf("go: cannot match %q: %v", arg, ErrNoModRoot)
   150  			}
   151  			continue
   152  		}
   153  		path, vers, found, err := ParsePathVersion(arg)
   154  		if err != nil {
   155  			base.Fatalf("go: %v", err)
   156  		}
   157  		if found {
   158  			if vers == "upgrade" || vers == "patch" {
   159  				if _, ok := rs.rootSelected(path); !ok || rs.pruning == unpruned {
   160  					needFullGraph = true
   161  					if !HasModRoot() {
   162  						base.Fatalf("go: cannot match %q: %v", arg, ErrNoModRoot)
   163  					}
   164  				}
   165  			}
   166  			continue
   167  		}
   168  		if _, ok := rs.rootSelected(arg); !ok || rs.pruning == unpruned {
   169  			needFullGraph = true
   170  			if mode&ListVersions == 0 && !HasModRoot() {
   171  				base.Fatalf("go: cannot match %q without -versions or an explicit version: %v", arg, ErrNoModRoot)
   172  			}
   173  		}
   174  	}
   175  
   176  	var mg *ModuleGraph
   177  	if needFullGraph {
   178  		rs, mg, mgErr = expandGraph(ctx, rs)
   179  	}
   180  
   181  	matchedModule := map[module.Version]bool{}
   182  	for _, arg := range args {
   183  		path, vers, found, err := ParsePathVersion(arg)
   184  		if err != nil {
   185  			base.Fatalf("go: %v", err)
   186  		}
   187  		if found {
   188  			var current string
   189  			if mg == nil {
   190  				current, _ = rs.rootSelected(path)
   191  			} else {
   192  				current = mg.Selected(path)
   193  			}
   194  			if current == "none" && mgErr != nil {
   195  				if vers == "upgrade" || vers == "patch" {
   196  					// The module graph is incomplete, so we don't know what version we're
   197  					// actually upgrading from.
   198  					// mgErr is already set, so just skip this module.
   199  					continue
   200  				}
   201  			}
   202  
   203  			allowed := CheckAllowed
   204  			if IsRevisionQuery(path, vers) || mode&ListRetracted != 0 {
   205  				// Allow excluded and retracted versions if the user asked for a
   206  				// specific revision or used 'go list -retracted'.
   207  				allowed = nil
   208  			}
   209  			info, err := queryReuse(ctx, path, vers, current, allowed, reuse)
   210  			if err != nil {
   211  				var origin *codehost.Origin
   212  				if info != nil {
   213  					origin = info.Origin
   214  				}
   215  				mods = append(mods, &modinfo.ModulePublic{
   216  					Path:    path,
   217  					Version: vers,
   218  					Error:   modinfoError(path, vers, err),
   219  					Origin:  origin,
   220  				})
   221  				continue
   222  			}
   223  
   224  			// Indicate that m was resolved from outside of rs by passing a nil
   225  			// *Requirements instead.
   226  			var noRS *Requirements
   227  
   228  			mod := moduleInfo(ctx, noRS, module.Version{Path: path, Version: info.Version}, mode, reuse)
   229  			if vers != mod.Version {
   230  				mod.Query = vers
   231  			}
   232  			mod.Origin = info.Origin
   233  			mods = append(mods, mod)
   234  			continue
   235  		}
   236  
   237  		// Module path or pattern.
   238  		var match func(string) bool
   239  		if arg == "all" {
   240  			match = func(p string) bool { return !gover.IsToolchain(p) }
   241  		} else if strings.Contains(arg, "...") {
   242  			mp := pkgpattern.MatchPattern(arg)
   243  			match = func(p string) bool { return mp(p) && !gover.IsToolchain(p) }
   244  		} else {
   245  			var v string
   246  			if mg == nil {
   247  				var ok bool
   248  				v, ok = rs.rootSelected(arg)
   249  				if !ok {
   250  					// We checked rootSelected(arg) in the earlier args loop, so if there
   251  					// is no such root we should have loaded a non-nil mg.
   252  					panic(fmt.Sprintf("internal error: root requirement expected but not found for %v", arg))
   253  				}
   254  			} else {
   255  				v = mg.Selected(arg)
   256  			}
   257  			if v == "none" && mgErr != nil {
   258  				// mgErr is already set, so just skip this module.
   259  				continue
   260  			}
   261  			if v != "none" {
   262  				mods = append(mods, moduleInfo(ctx, rs, module.Version{Path: arg, Version: v}, mode, reuse))
   263  			} else if cfg.BuildMod == "vendor" {
   264  				// In vendor mode, we can't determine whether a missing module is “a
   265  				// known dependency” because the module graph is incomplete.
   266  				// Give a more explicit error message.
   267  				mods = append(mods, &modinfo.ModulePublic{
   268  					Path:  arg,
   269  					Error: modinfoError(arg, "", errors.New("can't resolve module using the vendor directory\n\t(Use -mod=mod or -mod=readonly to bypass.)")),
   270  				})
   271  			} else if mode&ListVersions != 0 {
   272  				// Don't make the user provide an explicit '@latest' when they're
   273  				// explicitly asking what the available versions are. Instead, return a
   274  				// module with version "none", to which we can add the requested list.
   275  				mods = append(mods, &modinfo.ModulePublic{Path: arg})
   276  			} else {
   277  				mods = append(mods, &modinfo.ModulePublic{
   278  					Path:  arg,
   279  					Error: modinfoError(arg, "", errors.New("not a known dependency")),
   280  				})
   281  			}
   282  			continue
   283  		}
   284  
   285  		var matches []module.Version
   286  		for _, m := range mg.BuildList() {
   287  			if match(m.Path) {
   288  				if !matchedModule[m] {
   289  					matchedModule[m] = true
   290  					matches = append(matches, m)
   291  				}
   292  			}
   293  		}
   294  
   295  		if len(matches) == 0 {
   296  			fmt.Fprintf(os.Stderr, "warning: pattern %q matched no module dependencies\n", arg)
   297  		}
   298  
   299  		q := par.NewQueue(runtime.GOMAXPROCS(0))
   300  		fetchedMods := make([]*modinfo.ModulePublic, len(matches))
   301  		for i, m := range matches {
   302  			q.Add(func() {
   303  				fetchedMods[i] = moduleInfo(ctx, rs, m, mode, reuse)
   304  			})
   305  		}
   306  		<-q.Idle()
   307  		mods = append(mods, fetchedMods...)
   308  	}
   309  
   310  	return rs, mods, mgErr
   311  }
   312  
   313  // modinfoError wraps an error to create an error message in
   314  // modinfo.ModuleError with minimal redundancy.
   315  func modinfoError(path, vers string, err error) *modinfo.ModuleError {
   316  	var nerr *NoMatchingVersionError
   317  	var merr *module.ModuleError
   318  	if errors.As(err, &nerr) {
   319  		// NoMatchingVersionError contains the query, so we don't mention the
   320  		// query again in ModuleError.
   321  		err = &module.ModuleError{Path: path, Err: err}
   322  	} else if !errors.As(err, &merr) {
   323  		// If the error does not contain path and version, wrap it in a
   324  		// module.ModuleError.
   325  		err = &module.ModuleError{Path: path, Version: vers, Err: err}
   326  	}
   327  
   328  	return &modinfo.ModuleError{Err: err.Error()}
   329  }
   330  
   331  // ParsePathVersion parses arg expecting arg to be path@version. If there is no
   332  // '@' in arg, found is false, vers is "", and path is arg. This mirrors the
   333  // typical usage of strings.Cut. ParsePathVersion is meant to be a general
   334  // replacement for strings.Cut in module version parsing. If the version is
   335  // invalid, an error is returned. The version is considered invalid if it is
   336  // prefixed with '-' or '/', which can cause security problems when constructing
   337  // commands to execute that use the version.
   338  func ParsePathVersion(arg string) (path, vers string, found bool, err error) {
   339  	path, vers, found = strings.Cut(arg, "@")
   340  	if !found {
   341  		return arg, "", false, nil
   342  	}
   343  	if len(vers) > 0 && (vers[0] == '-' || vers[0] == '/') {
   344  		return "", "", false, fmt.Errorf("invalid module version %q", vers)
   345  	}
   346  	return path, vers, true, nil
   347  }
   348  

View as plain text