cuelang.org/go@v0.13.0/internal/mod/modload/update.go (about)

     1  package modload
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io/fs"
     8  	"maps"
     9  	"path"
    10  	"path/filepath"
    11  	"runtime"
    12  	"slices"
    13  	"strings"
    14  	"sync/atomic"
    15  
    16  	"cuelang.org/go/cue/ast"
    17  	"cuelang.org/go/internal/mod/modpkgload"
    18  	"cuelang.org/go/internal/mod/modrequirements"
    19  	"cuelang.org/go/internal/mod/semver"
    20  	"cuelang.org/go/internal/par"
    21  	"cuelang.org/go/mod/modfile"
    22  	"cuelang.org/go/mod/modregistry"
    23  	"cuelang.org/go/mod/module"
    24  )
    25  
    26  // UpdateVersions returns the main module's module file with the specified module versions
    27  // updated if possible and added if not already present. It returns an error if asked
    28  // to downgrade a module below a version already required by an external dependency.
    29  //
    30  // A module in the versions slice can be specified as one of the following:
    31  //   - $module@$fullVersion: a specific exact version
    32  //   - $module@$partialVersion: a non-canonical version
    33  //     specifies the latest version that has the same major/minor numbers.
    34  //   - $module@latest: the latest non-prerelease version, or latest prerelease version if
    35  //     there is no non-prerelease version
    36  //   - $module: equivalent to $module@latest if $module doesn't have a default major
    37  //     version or $module@$majorVersion if it does, where $majorVersion is the
    38  //     default major version for $module.
    39  func UpdateVersions(ctx context.Context, fsys fs.FS, modRoot string, reg Registry, versions []string) (*modfile.File, error) {
    40  	mainModuleVersion, mf, err := readModuleFile(fsys, modRoot)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	rs := modrequirements.NewRequirements(mf.QualifiedModule(), reg, mf.DepVersions(), mf.DefaultMajorVersions())
    45  	mversions, err := resolveUpdateVersions(ctx, reg, rs, mainModuleVersion, versions)
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  	// Now we know what versions we want to update to, make a new set of
    50  	// requirements with these versions in place.
    51  
    52  	mversionsMap := make(map[string]module.Version)
    53  	for _, v := range mversions {
    54  		// Check existing membership of the map: if the same module has been specified
    55  		// twice, then choose t
    56  		if v1, ok := mversionsMap[v.Path()]; ok && v1.Version() != v.Version() {
    57  			// The same module has been specified twice with different requirements.
    58  			// Treat it as an error (an alternative approach might be to choose the greater
    59  			// version, but making it an error seems more appropriate to the "choose exact
    60  			// version" semantics of UpdateVersions.
    61  			return nil, fmt.Errorf("conflicting version update requirements %v vs %v", v1, v)
    62  		}
    63  		mversionsMap[v.Path()] = v
    64  	}
    65  	g, err := rs.Graph(ctx)
    66  	if err != nil {
    67  		return nil, fmt.Errorf("cannot determine module graph: %v", err)
    68  	}
    69  	var newVersions []module.Version
    70  	for _, v := range g.BuildList() {
    71  		if v.Path() == mainModuleVersion.Path() {
    72  			continue
    73  		}
    74  		if newv, ok := mversionsMap[v.Path()]; ok {
    75  			newVersions = append(newVersions, newv)
    76  			delete(mversionsMap, v.Path())
    77  		} else {
    78  			newVersions = append(newVersions, v)
    79  		}
    80  	}
    81  	newVersions = slices.AppendSeq(newVersions, maps.Values(mversionsMap))
    82  	slices.SortFunc(newVersions, module.Version.Compare)
    83  	rs = modrequirements.NewRequirements(mf.QualifiedModule(), reg, newVersions, mf.DefaultMajorVersions())
    84  	g, err = rs.Graph(ctx)
    85  	if err != nil {
    86  		return nil, fmt.Errorf("cannot determine new module graph: %v", err)
    87  	}
    88  	// Now check that the resulting versions are the ones we wanted.
    89  	for _, v := range mversions {
    90  		actualVers := g.Selected(v.Path())
    91  		if actualVers != v.Version() {
    92  			return nil, fmt.Errorf("other requirements prevent changing module %v to version %v (actual selected version: %v)", v.Path(), v.Version(), actualVers)
    93  		}
    94  	}
    95  	// Make a new requirements with the selected versions of the above as roots.
    96  	var finalVersions []module.Version
    97  	for _, v := range g.BuildList() {
    98  		if v.Path() != mainModuleVersion.Path() {
    99  			finalVersions = append(finalVersions, v)
   100  		}
   101  	}
   102  	rs = modrequirements.NewRequirements(mf.QualifiedModule(), reg, finalVersions, mf.DefaultMajorVersions())
   103  	return modfileFromRequirements(mf, rs), nil
   104  }
   105  
   106  // ResolveAbsolutePackage resolves a package in a standalone fashion, irrespective
   107  // of a module file. It returns the module containing that package and the location of the package.
   108  //
   109  // It tries to avoid hitting the network unless necessary by using cached results where available.
   110  func ResolveAbsolutePackage(ctx context.Context, reg Registry, p string) (module.Version, module.SourceLoc, error) {
   111  	fail := func(err error) (module.Version, module.SourceLoc, error) {
   112  		return module.Version{}, module.SourceLoc{}, err
   113  	}
   114  	failf := func(format string, args ...interface{}) (module.Version, module.SourceLoc, error) {
   115  		return fail(fmt.Errorf(format, args...))
   116  	}
   117  	if filepath.IsAbs(p) || path.IsAbs(p) {
   118  		return failf("%q is not a package path", p)
   119  	}
   120  	ip := ast.ParseImportPath(p)
   121  	// Before any further lookup, check that the path without the version specifier is valid;
   122  	// for example foo.com/bar/@latest would be an example of an invalid path.
   123  	ip1 := ip
   124  	ip1.Version = ""
   125  	if err := module.CheckImportPath(ip1.String()); err != nil {
   126  		return fail(err)
   127  	}
   128  
   129  	tryResolve := func(fetch func(m module.Version) (module.SourceLoc, error)) (module.Version, module.SourceLoc, error) {
   130  		locs, err := modpkgload.FindPackageLocations(ctx, p, func(ctx context.Context, prefixPath string) (module.Version, error) {
   131  			mv, err := resolveModuleVersion(ctx, reg, nil, prefixPath+"@"+ip.Version)
   132  			if errors.Is(err, errNoVersionsFound) {
   133  				return module.Version{}, nil
   134  			}
   135  			return mv, err
   136  		}, func(ctx context.Context, m module.Version) (loc module.SourceLoc, isLocal bool, err error) {
   137  			loc, err = fetch(m)
   138  			if errors.Is(err, modregistry.ErrNotFound) {
   139  				err = nil
   140  			}
   141  			return loc, false, err
   142  		})
   143  		if err != nil {
   144  			return fail(err)
   145  		}
   146  		if len(locs) == 1 {
   147  			// We've got exactly one cache hit. Use it.
   148  			return locs[0].Module, locs[0].Locs[0], nil
   149  		}
   150  		if len(locs) > 1 {
   151  			return fail(&modpkgload.AmbiguousImportError{ImportPath: p, Locations: locs})
   152  		}
   153  		return fail(&modpkgload.ImportMissingError{Path: p})
   154  	}
   155  
   156  	if reg, ok := reg.(modpkgload.CachedRegistry); ok && ip.Version != "" && semver.Canonical(ip.Version) == ip.Version {
   157  		// It's a canonical version and we're using a caching registry implementation.
   158  		// We might be able to avoid hitting the network.
   159  		mv, loc, err := tryResolve(reg.FetchFromCache)
   160  		if err == nil || !errors.As(err, new(*modpkgload.ImportMissingError)) {
   161  			return mv, loc, err
   162  		}
   163  		// Not found in cache. Try again with the non-cached version.
   164  	}
   165  	return tryResolve(func(m module.Version) (module.SourceLoc, error) {
   166  		return reg.Fetch(ctx, m)
   167  	})
   168  }
   169  
   170  var errNoVersionsFound = fmt.Errorf("no versions found")
   171  
   172  // resolveModuleVersion resolves a module/version query to a canonical module version.
   173  //
   174  // The version may take any of the following forms:
   175  //
   176  //	$module@v1.2.3	- absolute version.
   177  //	$module			- latest version
   178  //	$module@v1		- latest version at v1
   179  //	$module@v1.2	- latest version within v1.1
   180  //	$module@latest	- same as $module
   181  //	$module@v1.latest	- same as @v1
   182  //
   183  // If rs is non-nil, it will be used to choose a default major version when no
   184  // major version is specified.
   185  //
   186  // It returns an errNoVersionsFound error if there are no versions for the query but
   187  // all else is OK.
   188  //
   189  // TODO could support queries like <=v1.2.3 etc
   190  func resolveModuleVersion(ctx context.Context, reg Registry, rs *modrequirements.Requirements, v string) (module.Version, error) {
   191  	if mv, err := module.ParseVersion(v); err == nil {
   192  		// It's already a canonical version; nothing to do.
   193  		return mv, nil
   194  	}
   195  	mpath, vers, ok := strings.Cut(v, "@")
   196  	if !ok {
   197  		if rs != nil {
   198  			if major, status := rs.DefaultMajorVersion(mpath); status == modrequirements.ExplicitDefault {
   199  				// TODO allow a non-explicit default too?
   200  				vers = major
   201  			}
   202  		}
   203  		if vers == "" {
   204  			vers = "latest"
   205  		}
   206  	}
   207  
   208  	if err := module.CheckPathWithoutVersion(mpath); err != nil {
   209  		return module.Version{}, fmt.Errorf("%w: invalid module path in %q", errNoVersionsFound, v)
   210  	}
   211  	versionPrefix := ""
   212  	switch {
   213  	case vers == "latest":
   214  	case strings.HasSuffix(vers, ".latest"):
   215  		versionPrefix = strings.TrimSuffix(vers, ".latest")
   216  		if !semver.IsValid(versionPrefix) {
   217  			return module.Version{}, fmt.Errorf("invalid version specified %q", vers)
   218  		}
   219  		if semver.Canonical(versionPrefix) == versionPrefix {
   220  			// TODO maybe relax this a bit to allow v1.2.3.latest ?
   221  			return module.Version{}, fmt.Errorf("cannot use .latest on canonical version %q", vers)
   222  		}
   223  	default:
   224  		if !semver.IsValid(vers) {
   225  			return module.Version{}, fmt.Errorf("%q does not specify a valid semantic version", v)
   226  		}
   227  		if semver.Build(vers) != "" {
   228  			return module.Version{}, fmt.Errorf("build version suffixes not supported (%v)", v)
   229  		}
   230  		// It's a valid version but has no build suffix and it's not canonical,
   231  		// which means it must be either a major-only or major-minor, so
   232  		// the conforming canonical versions must have it as a prefix, with
   233  		// a dot separating the last component and the next.
   234  		versionPrefix = vers + "."
   235  	}
   236  	allVersions, err := reg.ModuleVersions(ctx, mpath)
   237  	if err != nil {
   238  		return module.Version{}, err
   239  	}
   240  	possibleVersions := make([]string, 0, len(allVersions))
   241  	for _, v := range allVersions {
   242  		if strings.HasPrefix(v, versionPrefix) {
   243  			possibleVersions = append(possibleVersions, v)
   244  		}
   245  	}
   246  	if len(possibleVersions) == 0 {
   247  		return module.Version{}, fmt.Errorf("%w for module %v", errNoVersionsFound, v)
   248  	}
   249  	chosen := LatestVersion(possibleVersions)
   250  	mv, err := module.NewVersion(mpath, chosen)
   251  	if err != nil {
   252  		// Should never happen, because we've checked that
   253  		// mpath is valid and ModuleVersions
   254  		// should always return valid semver versions.
   255  		return module.Version{}, err
   256  	}
   257  	return mv, nil
   258  }
   259  
   260  // resolveUpdateVersions resolves a set of version strings as accepted by [UpdateVersions]
   261  // into the actual module versions they represent.
   262  func resolveUpdateVersions(ctx context.Context, reg Registry, rs *modrequirements.Requirements, mainModuleVersion module.Version, versions []string) ([]module.Version, error) {
   263  	work := par.NewQueue(runtime.GOMAXPROCS(0))
   264  	mversions := make([]module.Version, len(versions))
   265  	var queryErr atomic.Pointer[error]
   266  	setError := func(err error) {
   267  		queryErr.CompareAndSwap(nil, &err)
   268  	}
   269  	for i, v := range versions {
   270  		if mv, err := module.ParseVersion(v); err == nil {
   271  			// It's already canonical: nothing more to do.
   272  			mversions[i] = mv
   273  			continue
   274  		}
   275  		work.Add(func() {
   276  			mv, err := resolveModuleVersion(ctx, reg, rs, v)
   277  			if err != nil {
   278  				setError(err)
   279  			} else {
   280  				mversions[i] = mv
   281  			}
   282  		})
   283  	}
   284  	<-work.Idle()
   285  	if errPtr := queryErr.Load(); errPtr != nil {
   286  		return nil, *errPtr
   287  	}
   288  	for _, v := range mversions {
   289  		if v.Path() == mainModuleVersion.Path() {
   290  			return nil, fmt.Errorf("cannot update version of main module")
   291  		}
   292  	}
   293  	return mversions, nil
   294  }