cuelang.org/go@v0.13.0/cue/load/instances.go (about)

     1  // Copyright 2018 The CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package load
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io/fs"
    21  	"maps"
    22  	"path"
    23  	"slices"
    24  	"strconv"
    25  	"strings"
    26  
    27  	"cuelang.org/go/cue/ast"
    28  	"cuelang.org/go/cue/build"
    29  	"cuelang.org/go/internal/filetypes"
    30  	"cuelang.org/go/internal/mod/modimports"
    31  	"cuelang.org/go/internal/mod/modload"
    32  	"cuelang.org/go/internal/mod/modpkgload"
    33  	"cuelang.org/go/internal/mod/modrequirements"
    34  	"cuelang.org/go/internal/mod/semver"
    35  	"cuelang.org/go/mod/modfile"
    36  	"cuelang.org/go/mod/module"
    37  )
    38  
    39  // Instances returns the instances named by the command line arguments 'args'.
    40  // If errors occur trying to load an instance it is returned with Incomplete
    41  // set. Errors directly related to loading the instance are recorded in this
    42  // instance, but errors that occur loading dependencies are recorded in these
    43  // dependencies.
    44  func Instances(args []string, c *Config) []*build.Instance {
    45  	if len(args) == 0 {
    46  		args = []string{"."}
    47  	}
    48  	// TODO: This requires packages to be placed before files. At some point this
    49  	// could be relaxed.
    50  	i := 0
    51  	isAbsPkg := false
    52  	for ; i < len(args) && filetypes.IsPackage(args[i]); i++ {
    53  		if isAbsVersionPackage(args[i]) {
    54  			if i > 0 {
    55  				return []*build.Instance{c.newErrInstance(fmt.Errorf("only a single package with absolute version may be specified"))}
    56  			}
    57  			isAbsPkg = true
    58  		}
    59  	}
    60  	pkgArgs := args[:i]
    61  	otherArgs := args[i:]
    62  	otherFiles, err := filetypes.ParseArgs(otherArgs)
    63  	if err != nil {
    64  		return []*build.Instance{c.newErrInstance(err)}
    65  	}
    66  	ctx := context.TODO()
    67  	if c == nil {
    68  		c = &Config{}
    69  	}
    70  	newC, err := c.complete()
    71  	if err != nil {
    72  		return []*build.Instance{c.newErrInstance(err)}
    73  	}
    74  	c = newC
    75  	for _, f := range otherFiles {
    76  		if err := setFileSource(c, f); err != nil {
    77  			return []*build.Instance{c.newErrInstance(err)}
    78  		}
    79  	}
    80  	if c.Package != "" && c.Package != "_" && c.Package != "*" {
    81  		// The caller has specified an explicit package to load.
    82  		// This is essentially the same as passing an explicit package
    83  		// qualifier to all package arguments that don't already have
    84  		// one. We add that qualifier here so that there's a distinction
    85  		// between package paths specified as arguments, which
    86  		// have the qualifier added, and package paths that are dependencies
    87  		// of those, which don't.
    88  		pkgArgs1 := make([]string, 0, len(pkgArgs))
    89  		for _, p := range pkgArgs {
    90  			if ip := ast.ParseImportPath(p); !ip.ExplicitQualifier {
    91  				ip.Qualifier = c.Package
    92  				p = ip.String()
    93  			}
    94  			pkgArgs1 = append(pkgArgs1, p)
    95  		}
    96  		pkgArgs = pkgArgs1
    97  	}
    98  
    99  	tg := newTagger(c)
   100  
   101  	var pkgs *modpkgload.Packages
   102  	if !c.SkipImports {
   103  		if isAbsPkg {
   104  			// Note: replace the absolute package (which isn't actually a valid
   105  			// import path and may contain a version query like @latest)
   106  			// with the actual resolved import path.
   107  			pkgArgs[0], pkgs, err = loadAbsPackage(ctx, c, pkgArgs[0], tg)
   108  		} else {
   109  			// Pass all arguments that look like packages to loadPackages
   110  			// so that they'll be available when looking up the packages
   111  			// that are specified on the command line.
   112  			expandedPaths, err1 := expandPackageArgs(c, pkgArgs, c.Package, tg)
   113  			if err1 != nil {
   114  				return []*build.Instance{c.newErrInstance(err1)}
   115  			}
   116  			pkgs, err = loadPackagesFromArgs(ctx, c, expandedPaths, otherFiles, tg)
   117  		}
   118  		if err != nil {
   119  			return []*build.Instance{c.newErrInstance(err)}
   120  		}
   121  	}
   122  	l := newLoader(c, tg, pkgs)
   123  
   124  	if c.Context == nil {
   125  		opts := []build.Option{
   126  			build.ParseFile(c.ParseFile),
   127  		}
   128  		if f := l.loadFunc(); l != nil {
   129  			opts = append(opts, build.Loader(f))
   130  		}
   131  		c.Context = build.NewContext(opts...)
   132  	}
   133  
   134  	a := []*build.Instance{}
   135  	if len(pkgArgs) > 0 {
   136  		for _, m := range l.importPaths(pkgArgs) {
   137  			if m.Err != nil {
   138  				inst := c.newErrInstance(m.Err)
   139  				a = append(a, inst)
   140  				continue
   141  			}
   142  			a = append(a, m.Pkgs...)
   143  		}
   144  	}
   145  
   146  	if len(otherFiles) > 0 {
   147  		a = append(a, l.cueFilesPackage(otherFiles))
   148  	}
   149  
   150  	for _, p := range a {
   151  		tags, err := findTags(p)
   152  		if err != nil {
   153  			p.ReportError(err)
   154  		}
   155  		tg.tags = append(tg.tags, tags...)
   156  	}
   157  
   158  	// TODO(api): have API call that returns an error which is the aggregate
   159  	// of all build errors. Certain errors, like these, hold across builds.
   160  	if err := tg.injectTags(c.Tags); err != nil {
   161  		for _, p := range a {
   162  			p.ReportError(err)
   163  		}
   164  		return a
   165  	}
   166  
   167  	if tg.replacements == nil {
   168  		return a
   169  	}
   170  
   171  	for _, p := range a {
   172  		for _, f := range p.Files {
   173  			ast.Walk(f, nil, func(n ast.Node) {
   174  				if ident, ok := n.(*ast.Ident); ok {
   175  					if v, ok := tg.replacements[ident.Node]; ok {
   176  						ident.Node = v
   177  					}
   178  				}
   179  			})
   180  		}
   181  	}
   182  
   183  	return a
   184  }
   185  
   186  // loadAbsPackage loads a single $package@$version package
   187  // as the main module and returns its actual import path
   188  // and the packages instance representing its module.
   189  func loadAbsPackage(
   190  	ctx context.Context,
   191  	cfg *Config,
   192  	pkg string,
   193  	tg *tagger,
   194  ) (string, *modpkgload.Packages, error) {
   195  	// First find the module that contains the package.
   196  	mv, _, err := modload.ResolveAbsolutePackage(ctx, cfg.Registry, pkg)
   197  	if err != nil {
   198  		return "", nil, err
   199  	}
   200  	// ResolveAbsolutePackage should already have fetched the module
   201  	// so this should be quick.
   202  	loc, err := cfg.Registry.Fetch(ctx, mv)
   203  	if err != nil {
   204  		return "", nil, err
   205  	}
   206  	modFilePath := path.Join(loc.Dir, modDir, moduleFile)
   207  	modFileData, err := fs.ReadFile(loc.FS, modFilePath)
   208  	if err != nil {
   209  		return "", nil, err
   210  	}
   211  	mf, err := modfile.Parse(modFileData, modFilePath)
   212  	if err != nil {
   213  		return "", nil, err
   214  	}
   215  	// Make the package path into a regular import path
   216  	// with only the major version suffix.
   217  	ip := ast.ParseImportPath(pkg)
   218  	ip.Version = semver.Major(mv.Version())
   219  
   220  	pkgs, err := loadPackages(ctx, cfg, mf, loc, []string{ip.String()}, tg)
   221  	if err != nil {
   222  		return "", nil, err
   223  	}
   224  	return ip.String(), pkgs, nil
   225  }
   226  
   227  // loadPackages returns packages loaded from the given package list and also
   228  // including imports from the given build files.
   229  func loadPackagesFromArgs(
   230  	ctx context.Context,
   231  	cfg *Config,
   232  	pkgs []resolvedPackageArg,
   233  	otherFiles []*build.File,
   234  	tg *tagger,
   235  ) (*modpkgload.Packages, error) {
   236  	if cfg.modFile == nil || cfg.modFile.Module == "" {
   237  		return nil, nil
   238  	}
   239  	pkgPaths := make(map[string]bool)
   240  	// Add any packages specified directly on the command line.
   241  	for _, pkg := range pkgs {
   242  		pkgPaths[pkg.resolvedCanonical] = true
   243  	}
   244  	// Add any imports found in other files.
   245  	for _, f := range otherFiles {
   246  		if f.Encoding != build.CUE {
   247  			// not a CUE file; assume it has no imports for now.
   248  			continue
   249  		}
   250  		syntax, err := cfg.fileSystem.getCUESyntax(f)
   251  		if err != nil {
   252  			return nil, fmt.Errorf("cannot get syntax for %q: %w", f.Filename, err)
   253  		}
   254  		for _, imp := range syntax.Imports {
   255  			pkgPath, err := strconv.Unquote(imp.Path.Value)
   256  			if err != nil {
   257  				// Should never happen.
   258  				return nil, fmt.Errorf("invalid import path %q in %s", imp.Path.Value, f.Filename)
   259  			}
   260  			// Canonicalize the path.
   261  			pkgPath = ast.ParseImportPath(pkgPath).Canonical().String()
   262  			pkgPaths[pkgPath] = true
   263  		}
   264  	}
   265  	return loadPackages(ctx, cfg, cfg.modFile,
   266  		module.SourceLoc{
   267  			FS:  cfg.fileSystem.ioFS(cfg.ModuleRoot),
   268  			Dir: ".",
   269  		},
   270  		slices.Sorted(maps.Keys(pkgPaths)),
   271  		tg,
   272  	)
   273  }
   274  
   275  func loadPackages(
   276  	ctx context.Context,
   277  	cfg *Config,
   278  	mainMod *modfile.File,
   279  	mainModLoc module.SourceLoc,
   280  	pkgPaths []string,
   281  	tg *tagger,
   282  ) (*modpkgload.Packages, error) {
   283  	mainModPath := mainMod.QualifiedModule()
   284  	reqs := modrequirements.NewRequirements(
   285  		mainModPath,
   286  		cfg.Registry,
   287  		mainMod.DepVersions(),
   288  		mainMod.DefaultMajorVersions(),
   289  	)
   290  	return modpkgload.LoadPackages(
   291  		ctx,
   292  		mainModPath,
   293  		mainModLoc,
   294  		reqs,
   295  		cfg.Registry,
   296  		pkgPaths,
   297  		func(pkgPath string, mod module.Version, fsys fs.FS, mf modimports.ModuleFile) bool {
   298  			if !cfg.Tools && strings.HasSuffix(mf.FilePath, "_tool.cue") {
   299  				return false
   300  			}
   301  			isTest := strings.HasSuffix(mf.FilePath, "_test.cue")
   302  			var tagIsSet func(string) bool
   303  			if mod.Path() == mainModPath {
   304  				// In the main module.
   305  				if isTest && !cfg.Tests {
   306  					return false
   307  				}
   308  				tagIsSet = tg.tagIsSet
   309  			} else {
   310  				// Outside the main module.
   311  				if isTest {
   312  					// Don't traverse test files outside the main module
   313  					return false
   314  				}
   315  				// Treat all build tag keys as unset.
   316  				tagIsSet = func(string) bool {
   317  					return false
   318  				}
   319  			}
   320  			if err := shouldBuildFile(mf.Syntax, tagIsSet); err != nil {
   321  				// Later build logic should pick up and report the same error.
   322  				return false
   323  			}
   324  			return true
   325  		},
   326  	), nil
   327  }
   328  
   329  func isAbsVersionPackage(p string) bool {
   330  	ip := ast.ParseImportPath(p)
   331  	if ip.Version == "" {
   332  		return false
   333  	}
   334  	if semver.Major(ip.Version) == ip.Version {
   335  		return false
   336  	}
   337  	// Anything other than a simple major version suffix counts
   338  	// as an absolute version.
   339  	return true
   340  }