this repo has no description
at master 397 lines 13 kB view raw
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}