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

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