this repo has no description
at master 294 lines 11 kB view raw
1package modload 2 3import ( 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. 39func 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. 110func 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 170var 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 190func 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. 262func 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}