...

Source file src/cuelang.org/go/internal/mod/modpkgload/import.go

Documentation: cuelang.org/go/internal/mod/modpkgload

     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
   241  	// ReadDir to see if it's failing because it's not a directory,
   242  	// but unfortunately that doesn't seem to be something defined
   243  	// by the Go fs interface.
   244  	fi, err := fs.Stat(loc.FS, loc.Dir)
   245  	if err != nil {
   246  		if !errors.Is(err, fs.ErrNotExist) {
   247  			return false, err
   248  		}
   249  		return false, nil
   250  	}
   251  	if !fi.IsDir() {
   252  		return false, nil
   253  	}
   254  	entries, err := fs.ReadDir(loc.FS, loc.Dir)
   255  	if err != nil {
   256  		return false, err
   257  	}
   258  	for _, e := range entries {
   259  		if strings.HasSuffix(e.Name(), ".cue") && e.Type().IsRegular() {
   260  			return true, nil
   261  		}
   262  	}
   263  	return false, nil
   264  }
   265  
   266  // fetch downloads the given module (or its replacement)
   267  // and returns its location.
   268  //
   269  // The isLocal return value reports whether the replacement,
   270  // if any, is within the local main module.
   271  func (pkgs *Packages) fetch(ctx context.Context, mod module.Version) (loc module.SourceLoc, isLocal bool, err error) {
   272  	if mod == pkgs.mainModuleVersion {
   273  		return pkgs.mainModuleLoc, true, nil
   274  	}
   275  
   276  	loc, err = pkgs.registry.Fetch(ctx, mod)
   277  	return loc, false, err
   278  }
   279  
   280  // An AmbiguousImportError indicates an import of a package found in multiple
   281  // modules in the build list, or found in both the main module and its vendor
   282  // directory.
   283  type AmbiguousImportError struct {
   284  	ImportPath string
   285  	Locations  [][]module.SourceLoc
   286  	Modules    []module.Version // Either empty or 1:1 with Dirs.
   287  }
   288  
   289  func (e *AmbiguousImportError) Error() string {
   290  	locType := "modules"
   291  	if len(e.Modules) == 0 {
   292  		locType = "locations"
   293  	}
   294  
   295  	var buf strings.Builder
   296  	fmt.Fprintf(&buf, "ambiguous import: found package %s in multiple %s:", e.ImportPath, locType)
   297  
   298  	for i, loc := range e.Locations {
   299  		buf.WriteString("\n\t")
   300  		if i < len(e.Modules) {
   301  			m := e.Modules[i]
   302  			buf.WriteString(m.Path())
   303  			if m.Version() != "" {
   304  				fmt.Fprintf(&buf, " %s", m.Version())
   305  			}
   306  			// TODO work out how to present source locations in error messages.
   307  			fmt.Fprintf(&buf, " (%s)", loc[0].Dir)
   308  		} else {
   309  			buf.WriteString(loc[0].Dir)
   310  		}
   311  	}
   312  
   313  	return buf.String()
   314  }
   315  
   316  // ImportMissingError is used for errors where an imported package cannot be found.
   317  type ImportMissingError struct {
   318  	Path string
   319  }
   320  
   321  func (e *ImportMissingError) Error() string {
   322  	return "cannot find module providing package " + e.Path
   323  }
   324  

View as plain text