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