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  }