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