cuelang.org/go@v0.13.0/internal/mod/modpkgload/pkgload.go (about)

     1  package modpkgload
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/fs"
     7  	"maps"
     8  	"runtime"
     9  	"slices"
    10  	"strings"
    11  	"sync/atomic"
    12  
    13  	"cuelang.org/go/cue/ast"
    14  	"cuelang.org/go/internal/mod/modimports"
    15  	"cuelang.org/go/internal/mod/modrequirements"
    16  	"cuelang.org/go/internal/par"
    17  	"cuelang.org/go/mod/module"
    18  )
    19  
    20  // Registry represents a module registry, or at least this package's view of it.
    21  type Registry interface {
    22  	// Fetch returns the location of the contents for the given module
    23  	// version, downloading it if necessary.
    24  	// It returns an error that satisfies [errors.Is]([modregistry.ErrNotFound]) if the
    25  	// module is not present in the store at this version.
    26  	Fetch(ctx context.Context, m module.Version) (module.SourceLoc, error)
    27  }
    28  
    29  // CachedRegistry is optionally implemented by a registry that
    30  // implements a cache.
    31  type CachedRegistry interface {
    32  	// FetchFromCache looks up the given module in the cache.
    33  	// It returns an error that satisfies [errors.Is]([modregistry.ErrNotFound]) if the
    34  	// module is not present in the cache at this version or if there
    35  	// is no cache.
    36  	FetchFromCache(mv module.Version) (module.SourceLoc, error)
    37  }
    38  
    39  // Flags is a set of flags tracking metadata about a package.
    40  type Flags int8
    41  
    42  const (
    43  	// PkgInAll indicates that the package is in the "all" package pattern,
    44  	// regardless of whether we are loading the "all" package pattern.
    45  	//
    46  	// When the PkgInAll flag and PkgImportsLoaded flags are both set, the caller
    47  	// who set the last of those flags must propagate the PkgInAll marking to all
    48  	// of the imports of the marked package.
    49  	PkgInAll Flags = 1 << iota
    50  
    51  	// PkgIsRoot indicates that the package matches one of the root package
    52  	// patterns requested by the caller.
    53  	PkgIsRoot
    54  
    55  	// PkgFromRoot indicates that the package is in the transitive closure of
    56  	// imports starting at the roots. (Note that every package marked as PkgIsRoot
    57  	// is also trivially marked PkgFromRoot.)
    58  	PkgFromRoot
    59  
    60  	// PkgImportsLoaded indicates that the Imports field of a
    61  	// Pkg have been populated.
    62  	PkgImportsLoaded
    63  )
    64  
    65  func (f Flags) String() string {
    66  	var buf strings.Builder
    67  	set := func(f1 Flags, s string) {
    68  		if (f & f1) == 0 {
    69  			return
    70  		}
    71  		if buf.Len() > 0 {
    72  			buf.WriteString(",")
    73  		}
    74  		buf.WriteString(s)
    75  		f &^= f1
    76  	}
    77  	set(PkgInAll, "inAll")
    78  	set(PkgIsRoot, "isRoot")
    79  	set(PkgFromRoot, "fromRoot")
    80  	set(PkgImportsLoaded, "importsLoaded")
    81  	if f != 0 {
    82  		set(f, fmt.Sprintf("extra%x", int(f)))
    83  	}
    84  	return buf.String()
    85  }
    86  
    87  // has reports whether all of the flags in cond are set in f.
    88  func (f Flags) has(cond Flags) bool {
    89  	return f&cond == cond
    90  }
    91  
    92  type Packages struct {
    93  	mainModuleVersion    module.Version
    94  	mainModuleLoc        module.SourceLoc
    95  	shouldIncludePkgFile func(pkgPath string, mod module.Version, fsys fs.FS, mf modimports.ModuleFile) bool
    96  	pkgCache             par.Cache[string, *Package]
    97  	pkgs                 []*Package
    98  	rootPkgs             []*Package
    99  	work                 *par.Queue
   100  	requirements         *modrequirements.Requirements
   101  	registry             Registry
   102  }
   103  
   104  type Package struct {
   105  	// Populated at construction time:
   106  	path string // import path
   107  
   108  	// Populated at construction time and updated by [loader.applyPkgFlags]:
   109  	flags atomicLoadPkgFlags
   110  
   111  	// Populated by [loader.load].
   112  	mod          module.Version     // module providing package
   113  	locs         []module.SourceLoc // location of source code directories
   114  	err          error              // error loading package
   115  	imports      []*Package         // packages imported by this one
   116  	inStd        bool
   117  	fromExternal bool
   118  
   119  	// Populated by postprocessing in [Packages.buildStacks]:
   120  	stack *Package // package importing this one in minimal import stack for this pkg
   121  }
   122  
   123  func (pkg *Package) ImportPath() string {
   124  	return pkg.path
   125  }
   126  
   127  func (pkg *Package) FromExternalModule() bool {
   128  	return pkg.fromExternal
   129  }
   130  
   131  func (pkg *Package) Locations() []module.SourceLoc {
   132  	return pkg.locs
   133  }
   134  
   135  func (pkg *Package) Error() error {
   136  	return pkg.err
   137  }
   138  
   139  func (pkg *Package) SetError(err error) {
   140  	pkg.err = err
   141  }
   142  
   143  func (pkg *Package) HasFlags(flags Flags) bool {
   144  	return pkg.flags.has(flags)
   145  }
   146  
   147  func (pkg *Package) Imports() []*Package {
   148  	return pkg.imports
   149  }
   150  
   151  func (pkg *Package) Flags() Flags {
   152  	return pkg.flags.get()
   153  }
   154  
   155  func (pkg *Package) Mod() module.Version {
   156  	return pkg.mod
   157  }
   158  
   159  // LoadPackages loads information about all the given packages and the
   160  // packages they import, recursively, using modules from the given
   161  // requirements to determine which modules they might be obtained from,
   162  // and reg to download module contents.
   163  //
   164  // rootPkgPaths should only contain canonical import paths.
   165  //
   166  // The shouldIncludePkgFile function is used to determine whether a
   167  // given file in a package should be considered to be part of the build.
   168  // If it returns true for a package, the file's imports will be followed.
   169  // A nil value corresponds to a function that always returns true.
   170  // It may be called concurrently.
   171  func LoadPackages(
   172  	ctx context.Context,
   173  	mainModulePath string,
   174  	mainModuleLoc module.SourceLoc,
   175  	rs *modrequirements.Requirements,
   176  	reg Registry,
   177  	rootPkgPaths []string,
   178  	shouldIncludePkgFile func(pkgPath string, mod module.Version, fsys fs.FS, mf modimports.ModuleFile) bool,
   179  ) *Packages {
   180  	pkgs := &Packages{
   181  		mainModuleVersion:    module.MustNewVersion(mainModulePath, ""),
   182  		mainModuleLoc:        mainModuleLoc,
   183  		shouldIncludePkgFile: shouldIncludePkgFile,
   184  		requirements:         rs,
   185  		registry:             reg,
   186  		work:                 par.NewQueue(runtime.GOMAXPROCS(0)),
   187  	}
   188  	inRoots := map[*Package]bool{}
   189  	pkgs.rootPkgs = make([]*Package, 0, len(rootPkgPaths))
   190  	for _, p := range rootPkgPaths {
   191  		// TODO the original logic didn't add PkgInAll here. Not sure why,
   192  		// and that might be a lurking problem.
   193  		if root := pkgs.addPkg(ctx, p, PkgIsRoot|PkgInAll); !inRoots[root] {
   194  			pkgs.rootPkgs = append(pkgs.rootPkgs, root)
   195  			inRoots[root] = true
   196  		}
   197  	}
   198  	<-pkgs.work.Idle()
   199  	pkgs.buildStacks()
   200  	return pkgs
   201  }
   202  
   203  // buildStacks computes minimal import stacks for each package,
   204  // for use in error messages. When it completes, packages that
   205  // are part of the original root set have pkg.stack == nil,
   206  // and other packages have pkg.stack pointing at the next
   207  // package up the import stack in their minimal chain.
   208  // As a side effect, buildStacks also constructs ld.pkgs,
   209  // the list of all packages loaded.
   210  func (pkgs *Packages) buildStacks() {
   211  	for _, pkg := range pkgs.rootPkgs {
   212  		pkg.stack = pkg // sentinel to avoid processing in next loop
   213  		pkgs.pkgs = append(pkgs.pkgs, pkg)
   214  	}
   215  	for i := 0; i < len(pkgs.pkgs); i++ { // not range: appending to pkgs.pkgs in loop
   216  		pkg := pkgs.pkgs[i]
   217  		for _, next := range pkg.imports {
   218  			if next.stack == nil {
   219  				next.stack = pkg
   220  				pkgs.pkgs = append(pkgs.pkgs, next)
   221  			}
   222  		}
   223  	}
   224  	for _, pkg := range pkgs.rootPkgs {
   225  		pkg.stack = nil
   226  	}
   227  }
   228  
   229  func (pkgs *Packages) Roots() []*Package {
   230  	return slices.Clip(pkgs.rootPkgs)
   231  }
   232  
   233  func (pkgs *Packages) All() []*Package {
   234  	return slices.Clip(pkgs.pkgs)
   235  }
   236  
   237  // Pkg obtains a given package given its canonical import path.
   238  func (pkgs *Packages) Pkg(canonicalPkgPath string) *Package {
   239  	pkg, _ := pkgs.pkgCache.Get(canonicalPkgPath)
   240  	return pkg
   241  }
   242  
   243  func (pkgs *Packages) addPkg(ctx context.Context, pkgPath string, flags Flags) *Package {
   244  	pkg := pkgs.pkgCache.Do(pkgPath, func() *Package {
   245  		pkg := &Package{
   246  			path: pkgPath,
   247  		}
   248  		pkgs.applyPkgFlags(pkg, flags)
   249  
   250  		pkgs.work.Add(func() { pkgs.load(ctx, pkg) })
   251  		return pkg
   252  	})
   253  
   254  	// Ensure the flags apply even if the package already existed.
   255  	pkgs.applyPkgFlags(pkg, flags)
   256  	return pkg
   257  }
   258  
   259  func (pkgs *Packages) load(ctx context.Context, pkg *Package) {
   260  	if IsStdlibPackage(pkg.path) {
   261  		pkg.inStd = true
   262  		return
   263  	}
   264  	pkg.fromExternal = pkg.mod != pkgs.mainModuleVersion
   265  	pkg.mod, pkg.locs, pkg.err = pkgs.importFromModules(ctx, pkg.path)
   266  	if pkg.err != nil {
   267  		return
   268  	}
   269  	if pkgs.mainModuleVersion.Path() == pkg.mod.Path() {
   270  		pkgs.applyPkgFlags(pkg, PkgInAll)
   271  	}
   272  	ip := ast.ParseImportPath(pkg.path)
   273  	pkgQual := ip.Qualifier
   274  	switch pkgQual {
   275  	case "":
   276  		// If we are tidying a module which imports "foo.com/bar-baz@v0",
   277  		// a qualifier is needed as no valid package name can be derived from the path.
   278  		// Don't fail here, however, as tidy can simply ensure that bar-baz is a dependency,
   279  		// much like how `cue mod get foo.com/bar-baz` works just fine to add a module.
   280  		// Any command which later attempts to actually import bar-baz without a qualifier
   281  		// will result in a helpful error which the user can resolve at that point.
   282  		return
   283  	case "_":
   284  		pkg.err = fmt.Errorf("_ is not a valid import path qualifier in %q", pkg.path)
   285  		return
   286  	}
   287  	importsMap := make(map[string]bool)
   288  	foundPackageFile := false
   289  	excludedPackageFiles := 0
   290  	for _, loc := range pkg.locs {
   291  		// Layer an iterator whose yield function keeps track of whether we have seen
   292  		// a single valid CUE file in the package directory.
   293  		// Otherwise we would have to iterate twice, causing twice as many io/fs operations.
   294  		pkgFileIter := func(yield func(modimports.ModuleFile, error) bool) {
   295  			modimports.PackageFiles(loc.FS, loc.Dir, pkgQual)(func(mf modimports.ModuleFile, err error) bool {
   296  				if err != nil {
   297  					return yield(mf, err)
   298  				}
   299  				ip1 := ip
   300  				ip1.Qualifier = mf.Syntax.PackageName()
   301  				if !pkgs.shouldIncludePkgFile(ip1.String(), pkg.mod, loc.FS, mf) {
   302  					excludedPackageFiles++
   303  					return true
   304  				}
   305  				foundPackageFile = true
   306  				return yield(mf, err)
   307  			})
   308  		}
   309  		imports, err := modimports.AllImports(pkgFileIter)
   310  		if err != nil {
   311  			pkg.err = fmt.Errorf("cannot get imports: %v", err)
   312  			return
   313  		}
   314  		for _, imp := range imports {
   315  			importsMap[imp] = true
   316  		}
   317  	}
   318  	if !foundPackageFile {
   319  		if excludedPackageFiles > 0 {
   320  			pkg.err = fmt.Errorf("no files in package directory with package name %q (%d files were excluded)", pkgQual, excludedPackageFiles)
   321  		} else {
   322  			pkg.err = fmt.Errorf("no files in package directory with package name %q", pkgQual)
   323  		}
   324  		return
   325  	}
   326  	// Make the algorithm deterministic for tests.
   327  	imports := slices.Sorted(maps.Keys(importsMap))
   328  
   329  	pkg.imports = make([]*Package, 0, len(imports))
   330  	var importFlags Flags
   331  	if pkg.flags.has(PkgInAll) {
   332  		importFlags = PkgInAll
   333  	}
   334  	for _, path := range imports {
   335  		pkg.imports = append(pkg.imports, pkgs.addPkg(ctx, path, importFlags))
   336  	}
   337  	pkgs.applyPkgFlags(pkg, PkgImportsLoaded)
   338  }
   339  
   340  // applyPkgFlags updates pkg.flags to set the given flags and propagate the
   341  // (transitive) effects of those flags, possibly loading or enqueueing further
   342  // packages as a result.
   343  func (pkgs *Packages) applyPkgFlags(pkg *Package, flags Flags) {
   344  	if flags == 0 {
   345  		return
   346  	}
   347  
   348  	if flags.has(PkgInAll) {
   349  		// This package matches a root pattern by virtue of being in "all".
   350  		flags |= PkgIsRoot
   351  	}
   352  	if flags.has(PkgIsRoot) {
   353  		flags |= PkgFromRoot
   354  	}
   355  
   356  	old := pkg.flags.update(flags)
   357  	new := old | flags
   358  	if new == old || !new.has(PkgImportsLoaded) {
   359  		// We either didn't change the state of pkg, or we don't know anything about
   360  		// its dependencies yet. Either way, we can't usefully load its test or
   361  		// update its dependencies.
   362  		return
   363  	}
   364  
   365  	if new.has(PkgInAll) && !old.has(PkgInAll|PkgImportsLoaded) {
   366  		// We have just marked pkg with pkgInAll, or we have just loaded its
   367  		// imports, or both. Now is the time to propagate pkgInAll to the imports.
   368  		for _, dep := range pkg.imports {
   369  			pkgs.applyPkgFlags(dep, PkgInAll)
   370  		}
   371  	}
   372  
   373  	if new.has(PkgFromRoot) && !old.has(PkgFromRoot|PkgImportsLoaded) {
   374  		for _, dep := range pkg.imports {
   375  			pkgs.applyPkgFlags(dep, PkgFromRoot)
   376  		}
   377  	}
   378  }
   379  
   380  // An atomicLoadPkgFlags stores a loadPkgFlags for which individual flags can be
   381  // added atomically.
   382  type atomicLoadPkgFlags struct {
   383  	bits atomic.Int32
   384  }
   385  
   386  // update sets the given flags in af (in addition to any flags already set).
   387  //
   388  // update returns the previous flag state so that the caller may determine which
   389  // flags were newly-set.
   390  func (af *atomicLoadPkgFlags) update(flags Flags) (old Flags) {
   391  	for {
   392  		old := af.bits.Load()
   393  		new := old | int32(flags)
   394  		if new == old || af.bits.CompareAndSwap(old, new) {
   395  			return Flags(old)
   396  		}
   397  	}
   398  }
   399  
   400  func (af *atomicLoadPkgFlags) get() Flags {
   401  	return Flags(af.bits.Load())
   402  }
   403  
   404  // has reports whether all of the flags in cond are set in af.
   405  func (af *atomicLoadPkgFlags) has(cond Flags) bool {
   406  	return Flags(af.bits.Load())&cond == cond
   407  }
   408  
   409  func IsStdlibPackage(pkgPath string) bool {
   410  	firstElem, _, _ := strings.Cut(pkgPath, "/")
   411  	return !strings.Contains(firstElem, ".")
   412  }