1package modpkgload
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io/fs"
8 "iter"
9 "path"
10 "path/filepath"
11 "slices"
12 "strings"
13
14 "cuelang.org/go/cue/ast"
15 "cuelang.org/go/internal/mod/modrequirements"
16 "cuelang.org/go/mod/module"
17)
18
19// importFromModules finds the module and source location in the dependency graph of
20// pkgs containing the package with the given import path.
21//
22// The answer must be unique: importFromModules returns an error if multiple
23// modules are observed to provide the same package.
24//
25// importFromModules can return a zero module version for packages in
26// the standard library.
27//
28// If the package is not present in any module selected from the requirement
29// graph, importFromModules returns an *ImportMissingError.
30//
31// If the package is present in exactly one module, importFromModules will
32// return the module, its root directory, and a list of other modules that
33// lexically could have provided the package but did not.
34func (pkgs *Packages) importFromModules(ctx context.Context, pkgPath string) (
35 m module.Version,
36 mroot module.SourceLoc,
37 pkgLocs []module.SourceLoc,
38 err error,
39) {
40 fail := func(err error) (module.Version, module.SourceLoc, []module.SourceLoc, error) {
41 return module.Version{}, module.SourceLoc{}, nil, err
42 }
43 failf := func(format string, args ...interface{}) (module.Version, module.SourceLoc, []module.SourceLoc, error) {
44 return fail(fmt.Errorf(format, args...))
45 }
46 // Note: we don't care about the package qualifier at this point
47 // because any directory with CUE files in counts as a possible
48 // candidate, regardless of what packages are in it.
49 pathParts := ast.ParseImportPath(pkgPath)
50 pkgPathOnly := pathParts.Path
51
52 if filepath.IsAbs(pkgPathOnly) || path.IsAbs(pkgPathOnly) {
53 return failf("%q is not a package path", pkgPath)
54 }
55 // TODO check that the path isn't relative.
56 // TODO check it's not a meta package name, such as "all".
57
58 // Before any further lookup, check that the path is valid.
59 if err := module.CheckImportPath(pkgPath); err != nil {
60 return fail(err)
61 }
62
63 // Check each module on the build list.
64 var locs []PackageLoc
65 var mg *modrequirements.ModuleGraph
66 versionForModule := func(ctx context.Context, prefix string) (module.Version, error) {
67 var (
68 v string
69 ok bool
70 )
71 pkgVersion := pathParts.Version
72 if pkgVersion == "" {
73 if pkgVersion, _ = pkgs.requirements.DefaultMajorVersion(prefix); pkgVersion == "" {
74 return module.Version{}, nil
75 }
76 }
77 prefixPath := prefix + "@" + pkgVersion
78 // Note: mg is nil the first time around the loop.
79 if mg == nil {
80 v, ok = pkgs.requirements.RootSelected(prefixPath)
81 } else {
82 v, ok = mg.Selected(prefixPath), true
83 }
84 if !ok || v == "none" {
85 // No possible module
86 return module.Version{}, nil
87 }
88 m, err := module.NewVersion(prefixPath, v)
89 if err != nil {
90 // Not all package paths are valid module versions,
91 // but a parent might be.
92 return module.Version{}, nil
93 }
94 return m, nil
95 }
96 localPkgLocs, err := pkgs.findLocalPackage(pkgPathOnly)
97 if err != nil {
98 return fail(err)
99 }
100 if len(localPkgLocs) > 0 {
101 locs = append(locs, PackageLoc{
102 Module: module.MustNewVersion("local", ""),
103 ModuleRoot: pkgs.mainModuleLoc,
104 Locs: localPkgLocs,
105 })
106 }
107
108 // Iterate over possible modules for the path, not all selected modules.
109 // Iterating over selected modules would make the overall loading time
110 // O(M × P) for M modules providing P imported packages, whereas iterating
111 // over path prefixes is only O(P × k) with maximum path depth k. For
112 // large projects both M and P may be very large (note that M ≤ P), but k
113 // will tend to remain smallish (if for no other reason than filesystem
114 // path limitations).
115 //
116 // We perform this iteration either one or two times.
117 // Firstly we attempt to load the package using only the main module and
118 // its root requirements. If that does not identify the package, then we attempt
119 // to load the package using the full
120 // requirements in mg.
121 for {
122 // Note: if fetch fails, we return an error:
123 // we don't know for sure this module is necessary,
124 // but it certainly _could_ provide the package, and even if we
125 // continue the loop and find the package in some other module,
126 // we need to look at this module to make sure the import is
127 // not ambiguous.
128 plocs, err := FindPackageLocations(ctx, pkgPath, versionForModule, pkgs.fetch)
129 if err != nil {
130 return fail(err)
131 }
132 locs = append(locs, plocs...)
133 if len(locs) > 1 {
134 // We produce the list of directories from longest to shortest candidate
135 // module path, but the AmbiguousImportError should report them from
136 // shortest to longest. Reverse them now.
137 slices.Reverse(locs)
138 return fail(&AmbiguousImportError{ImportPath: pkgPath, Locations: locs})
139 }
140 if len(locs) == 1 {
141 // We've found the unique module containing the package.
142 return locs[0].Module, locs[0].ModuleRoot, locs[0].Locs, nil
143 }
144
145 if mg != nil {
146 // We checked the full module graph and still didn't find the
147 // requested package.
148 return fail(&ImportMissingError{Path: pkgPath})
149 }
150
151 // So far we've checked the root dependencies.
152 // Load the full module graph and try again.
153 mg, err = pkgs.requirements.Graph(ctx)
154 if err != nil {
155 // We might be missing one or more transitive (implicit) dependencies from
156 // the module graph, so we can't return an ImportMissingError here — one
157 // of the missing modules might actually contain the package in question,
158 // in which case we shouldn't go looking for it in some new dependency.
159 return fail(fmt.Errorf("cannot expand module graph: %v", err))
160 }
161 }
162}
163
164// PackageLoc holds a module version and the module root location, and a location of a package
165// within that module.
166type PackageLoc struct {
167 Module module.Version
168 ModuleRoot module.SourceLoc
169 // Locs holds the source locations of the package. There is always
170 // at least one element; there can be more than one when the
171 // module path is "local" (for exampe packages inside cue.mod/pkg).
172 Locs []module.SourceLoc
173}
174
175// FindPackageLocations finds possible module candidates for a given import path.
176//
177// It tries each parent of the import path as a possible module location,
178// using versionForModule to determine a version for that module
179// and fetch to fetch the location for a given module version.
180//
181// versionForModule may indicate that there is no possible module
182// for a given path by returning the zero version and a nil error.
183//
184// The fetch function also reports whether the location is "local"
185// to the current module, allowing some checks to be skipped when false.
186//
187// It returns possible locations for the package. Each location may or may
188// not contain the package itself, although it will hold some CUE files.
189func FindPackageLocations(
190 ctx context.Context,
191 importPath string,
192 versionForModule func(ctx context.Context, prefixPath string) (module.Version, error),
193 fetch func(ctx context.Context, m module.Version) (loc module.SourceLoc, isLocal bool, err error),
194) ([]PackageLoc, error) {
195 ip := ast.ParseImportPath(importPath)
196 var locs []PackageLoc
197 for prefix := range pathAncestors(ip.Path) {
198 v, err := versionForModule(ctx, prefix)
199 if err != nil {
200 return nil, err
201 }
202 if !v.IsValid() {
203 continue
204 }
205 mloc, isLocal, err := fetch(ctx, v)
206 if err != nil {
207 return nil, fmt.Errorf("cannot fetch %v: %w", v, err)
208 }
209 if mloc.FS == nil {
210 // Not found but not an error.
211 continue
212 }
213 loc, ok, err := locInModule(ip.Path, prefix, mloc, isLocal)
214 if err != nil {
215 return nil, fmt.Errorf("cannot find package: %v", err)
216 }
217 if ok {
218 locs = append(locs, PackageLoc{
219 Module: v,
220 ModuleRoot: mloc,
221 Locs: []module.SourceLoc{loc},
222 })
223 }
224 }
225 return locs, nil
226}
227
228// locInModule returns the location that would hold the package named by
229// the given path, if it were in the module with module path mpath and
230// root location mloc. If pkgPath is syntactically not within mpath, or
231// if mdir is a local file tree (isLocal == true) and the directory that
232// would hold path is in a sub-module (covered by a cue.mod below mdir),
233// locInModule returns "", false, nil.
234//
235// Otherwise, locInModule returns the name of the directory where CUE
236// source files would be expected, along with a boolean indicating
237// whether there are in fact CUE source files in that directory. A
238// non-nil error indicates that the existence of the directory and/or
239// source files could not be determined, for example due to a permission
240// error.
241func locInModule(pkgPath, mpath string, mloc module.SourceLoc, isLocal bool) (loc module.SourceLoc, haveCUEFiles bool, err error) {
242 loc.FS = mloc.FS
243
244 // Determine where to expect the package.
245 if pkgPath == mpath {
246 loc = mloc
247 } else if len(pkgPath) > len(mpath) && pkgPath[len(mpath)] == '/' && pkgPath[:len(mpath)] == mpath {
248 loc.Dir = path.Join(mloc.Dir, pkgPath[len(mpath)+1:])
249 } else {
250 return module.SourceLoc{}, false, nil
251 }
252
253 // Check that there aren't other modules in the way.
254 // This check is unnecessary inside the module cache.
255 // So we only check local module trees
256 // (the main module and, in the future, any directory trees pointed at by replace directives).
257 if isLocal {
258 for d := loc.Dir; d != mloc.Dir && len(d) > len(mloc.Dir); {
259 _, err := fs.Stat(mloc.FS, path.Join(d, "cue.mod/module.cue"))
260 // TODO should we count it as a module file if it's a directory?
261 haveCUEMod := err == nil
262 if haveCUEMod {
263 return module.SourceLoc{}, false, nil
264 }
265 parent := path.Dir(d)
266 if parent == d {
267 // Break the loop, as otherwise we'd loop
268 // forever if d=="." and mdir=="".
269 break
270 }
271 d = parent
272 }
273 }
274
275 // Are there CUE source files in the directory?
276 // We don't care about build tags, not even "ignore".
277 // We're just looking for a plausible directory.
278 haveCUEFiles, err = isDirWithCUEFiles(loc)
279 if err != nil {
280 return module.SourceLoc{}, false, err
281 }
282 return loc, haveCUEFiles, err
283}
284
285var localPkgDirs = []string{"cue.mod/gen", "cue.mod/usr", "cue.mod/pkg"}
286
287func (pkgs *Packages) findLocalPackage(pkgPath string) ([]module.SourceLoc, error) {
288 var locs []module.SourceLoc
289 for _, d := range localPkgDirs {
290 loc := pkgs.mainModuleLoc
291 loc.Dir = path.Join(loc.Dir, d, pkgPath)
292 ok, err := isDirWithCUEFiles(loc)
293 if err != nil {
294 return nil, err
295 }
296 if ok {
297 locs = append(locs, loc)
298 }
299 }
300 return locs, nil
301}
302
303func isDirWithCUEFiles(loc module.SourceLoc) (bool, error) {
304 // It would be nice if we could inspect the error returned from ReadDir to see
305 // if it's failing because it's not a directory, but unfortunately that doesn't
306 // seem to be something defined by the Go fs interface.
307 // For now, catching fs.ErrNotExist seems to be enough.
308 entries, err := fs.ReadDir(loc.FS, loc.Dir)
309 if err != nil {
310 if errors.Is(err, fs.ErrNotExist) {
311 return false, nil
312 }
313 return false, err
314 }
315 for _, e := range entries {
316 if !strings.HasSuffix(e.Name(), ".cue") {
317 continue
318 }
319 ftype := e.Type()
320 // If the directory entry is a symlink, stat it to obtain the info for the
321 // link target instead of the link itself.
322 if ftype&fs.ModeSymlink != 0 {
323 info, err := fs.Stat(loc.FS, filepath.Join(loc.Dir, e.Name()))
324 if err != nil {
325 continue // Ignore broken symlinks.
326 }
327 ftype = info.Mode()
328 }
329 if ftype.IsRegular() {
330 return true, nil
331 }
332 }
333 return false, nil
334}
335
336// fetch downloads the given module (or its replacement)
337// and returns its location.
338//
339// The isLocal return value reports whether the replacement,
340// if any, is within the local main module.
341func (pkgs *Packages) fetch(ctx context.Context, mod module.Version) (loc module.SourceLoc, isLocal bool, err error) {
342 if mod == pkgs.mainModuleVersion {
343 return pkgs.mainModuleLoc, true, nil
344 }
345 loc, err = pkgs.registry.Fetch(ctx, mod)
346 return loc, false, err
347}
348
349// pathAncestors returns an iterator over all the ancestors
350// of p, including p itself.
351func pathAncestors(p string) iter.Seq[string] {
352 return func(yield func(s string) bool) {
353 for {
354 if !yield(p) {
355 return
356 }
357 prev := p
358 p = path.Dir(p)
359 if p == "." || p == prev {
360 return
361 }
362 }
363 }
364}
365
366// An AmbiguousImportError indicates an import of a package found in multiple
367// modules in the build list, or found in both the main module and its vendor
368// directory.
369type AmbiguousImportError struct {
370 ImportPath string
371 Locations []PackageLoc
372}
373
374func (e *AmbiguousImportError) Error() string {
375 var buf strings.Builder
376 fmt.Fprintf(&buf, "ambiguous import: found package %s in multiple locations:", e.ImportPath)
377
378 for _, loc := range e.Locations {
379 buf.WriteString("\n\t")
380 buf.WriteString(loc.Module.Path())
381 if v := loc.Module.Version(); v != "" {
382 fmt.Fprintf(&buf, " %s", v)
383 }
384 // TODO work out how to present source locations in error messages.
385 fmt.Fprintf(&buf, " (%s)", loc.Locs[0].Dir)
386 }
387 return buf.String()
388}
389
390// ImportMissingError is used for errors where an imported package cannot be found.
391type ImportMissingError struct {
392 Path string
393}
394
395func (e *ImportMissingError) Error() string {
396 return "cannot find module providing package " + e.Path
397}