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}