cuelang.org/go@v0.10.1/cue/load/config.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"
    21  	"os"
    22  	"path/filepath"
    23  
    24  	"cuelang.org/go/cue/ast"
    25  	"cuelang.org/go/cue/build"
    26  	"cuelang.org/go/cue/errors"
    27  	"cuelang.org/go/cue/token"
    28  	"cuelang.org/go/internal"
    29  	"cuelang.org/go/internal/cueexperiment"
    30  	"cuelang.org/go/mod/modconfig"
    31  	"cuelang.org/go/mod/modfile"
    32  	"cuelang.org/go/mod/module"
    33  )
    34  
    35  const (
    36  	cueSuffix  = ".cue"
    37  	modDir     = "cue.mod"
    38  	moduleFile = "module.cue"
    39  )
    40  
    41  // FromArgsUsage is a partial usage message that applications calling
    42  // FromArgs may wish to include in their -help output.
    43  //
    44  // Some of the aspects of this documentation, like flags and handling '--' need
    45  // to be implemented by the tools.
    46  const FromArgsUsage = `
    47  <args> is a list of arguments denoting a set of instances of the form:
    48  
    49     <package>* <file_args>*
    50  
    51  1. A list of source files
    52  
    53     CUE files are parsed, loaded and unified into a single instance. All files
    54     must have the same package name.
    55  
    56     Data files, like YAML or JSON, are handled in one of two ways:
    57  
    58     a. Explicitly mapped into a single CUE namespace, using the --path, --files
    59        and --list flags. In this case these are unified into a single instance
    60        along with any other CUE files.
    61  
    62     b. Treated as a stream of data elements that each is optionally unified with
    63        a single instance, which either consists of the other CUE files specified
    64         on the command line or a single package.
    65  
    66     By default, the format of files is derived from the file extension.
    67     This behavior may be modified with file arguments of the form <qualifiers>:
    68     For instance,
    69  
    70        cue eval foo.cue json: bar.data
    71  
    72     indicates that the bar.data file should be interpreted as a JSON file.
    73     A qualifier applies to all files following it until the next qualifier.
    74  
    75     The following qualifiers are available:
    76  
    77        encodings
    78        cue           CUE definitions and data
    79        json          JSON data, one value only
    80        jsonl         newline-separated JSON values
    81        yaml          a YAML file, may contain a stream
    82        proto         Protobuf definitions
    83  
    84        interpretations
    85        jsonschema   data encoding describes JSON Schema
    86        openapi      data encoding describes Open API
    87  
    88        formats
    89        data         output as -- or only accept -- data
    90        graph        data allowing references or anchors
    91        schema       output as schema; defaults JSON files to JSON Schema
    92        def          full definitions, including documentation
    93  
    94  2. A list of relative directories to denote a package instance.
    95  
    96     Each directory matching the pattern is loaded as a separate instance.
    97     The instance contains all files in this directory and ancestor directories,
    98     up to the module root, with the same package name. The package name must
    99     be either uniquely determined by the files in the given directory, or
   100     explicitly defined using a package name qualifier. For instance, ./...:foo
   101     selects all packages named foo in the any subdirectory of the current
   102     working directory.
   103  
   104  3. An import path referring to a directory within the current module
   105  
   106     All CUE files in that directory, and all the ancestor directories up to the
   107     module root (if applicable), with a package name corresponding to the base
   108     name of the directory or the optional explicit package name are loaded into
   109     a single instance.
   110  
   111     Examples, assume a module name of acme.org/root:
   112        mod.test/foo   package in cue.mod
   113        ./foo             package corresponding to foo directory
   114        .:bar             package in current directory with package name bar
   115  `
   116  
   117  // GenPath reports the directory in which to store generated
   118  // files.
   119  func GenPath(root string) string {
   120  	return internal.GenPath(root)
   121  }
   122  
   123  // A Config configures load behavior.
   124  type Config struct {
   125  	// TODO: allow passing a cuecontext to be able to lookup and verify builtin
   126  	// packages at loading time.
   127  
   128  	// Context specifies the context for the load operation.
   129  	Context *build.Context
   130  
   131  	// ModuleRoot is the directory that contains the cue.mod directory
   132  	// as well as all the packages which form part of the module being loaded.
   133  	//
   134  	// If left as the empty string, a module root is found by walking parent directories
   135  	// starting from [Config.Dir] until one is found containing a cue.mod directory.
   136  	// If it is a relative path, it will be interpreted relative to [Config.Dir].
   137  	ModuleRoot string
   138  
   139  	// Module specifies the module prefix. If not empty, this value must match
   140  	// the module field of an existing cue.mod file.
   141  	Module string
   142  
   143  	// AcceptLegacyModules causes the module resolution code
   144  	// to accept module files that lack a language.version field.
   145  	AcceptLegacyModules bool
   146  
   147  	// modFile holds the contents of the module file, or nil
   148  	// if no module file was present. If non-nil, then
   149  	// after calling Config.complete, modFile.Module will be
   150  	// equal to Module.
   151  	modFile *modfile.File
   152  
   153  	// Package defines the name of the package to be loaded. If this is not set,
   154  	// the package must be uniquely defined from its context. Special values:
   155  	//    _    load files without a package
   156  	//    *    load all packages. Files without packages are loaded
   157  	//         in the _ package.
   158  	Package string
   159  
   160  	// Dir is the base directory for import path resolution.
   161  	// For example, it is used to determine the main module,
   162  	// and rooted import paths starting with "./" are relative to it.
   163  	// If Dir is empty, the current directory is used.
   164  	Dir string
   165  
   166  	// Tags defines boolean tags or key-value pairs to select files to build
   167  	// or be injected as values in fields.
   168  	//
   169  	// Each string is of the form
   170  	//
   171  	//     key [ "=" value ]
   172  	//
   173  	// where key is a valid CUE identifier and value valid CUE scalar.
   174  	//
   175  	// The Tags values are used to both select which files get included in a
   176  	// build and to inject values into the AST.
   177  	//
   178  	//
   179  	// File selection
   180  	//
   181  	// Files with an attribute of the form @if(expr) before a package clause
   182  	// are conditionally included if expr resolves to true, where expr refers to
   183  	// boolean values in Tags.
   184  	//
   185  	// It is an error for a file to have more than one @if attribute or to
   186  	// have a @if attribute without or after a package clause.
   187  	//
   188  	//
   189  	// Value injection
   190  	//
   191  	// The Tags values are also used to inject values into fields with a
   192  	// @tag attribute.
   193  	//
   194  	// For any field of the form
   195  	//
   196  	//    field: x @tag(key)
   197  	//
   198  	// and Tags value for which the name matches key, the field will be
   199  	// modified to
   200  	//
   201  	//   field: x & "value"
   202  	//
   203  	// By default, the injected value is treated as a string. Alternatively, a
   204  	// "type" option of the @tag attribute allows a value to be interpreted as
   205  	// an int, number, or bool. For instance, for a field
   206  	//
   207  	//    field: x @tag(key,type=int)
   208  	//
   209  	// an entry "key=2" modifies the field to
   210  	//
   211  	//    field: x & 2
   212  	//
   213  	// Valid values for type are "int", "number", "bool", and "string".
   214  	//
   215  	// A @tag attribute can also define shorthand values, which can be injected
   216  	// into the fields without having to specify the key. For instance, for
   217  	//
   218  	//    environment: string @tag(env,short=prod|staging)
   219  	//
   220  	// the Tags entry "prod" sets the environment field to the value "prod".
   221  	// This is equivalent to a Tags entry of "env=prod".
   222  	//
   223  	// The use of @tag does not preclude using any of the usual CUE constraints
   224  	// to limit the possible values of a field. For instance
   225  	//
   226  	//    environment: "prod" | "staging" @tag(env,short=prod|staging)
   227  	//
   228  	// ensures the user may only specify "prod" or "staging".
   229  	Tags []string
   230  
   231  	// TagVars defines a set of key value pair the values of which may be
   232  	// referenced by tags.
   233  	//
   234  	// Use DefaultTagVars to get a pre-loaded map with supported values.
   235  	TagVars map[string]TagVar
   236  
   237  	// Include all files, regardless of tags.
   238  	AllCUEFiles bool
   239  
   240  	// Deprecated: use Tags
   241  	BuildTags   []string
   242  	releaseTags []string
   243  
   244  	// If Tests is set, the loader includes not just the packages
   245  	// matching a particular pattern but also any related test packages.
   246  	Tests bool
   247  
   248  	// If Tools is set, the loader includes tool files associated with
   249  	// a package.
   250  	Tools bool
   251  
   252  	// SkipImports causes the loading to ignore all imports and dependencies.
   253  	// The registry will never be consulted. Any external package paths
   254  	// mentioned on the command line will result in an error.
   255  	// The [cue/build.Instance.Imports] field will be empty.
   256  	SkipImports bool
   257  
   258  	// If DataFiles is set, the loader includes entries for directories that
   259  	// have no CUE files, but have recognized data files that could be converted
   260  	// to CUE.
   261  	DataFiles bool
   262  
   263  	// StdRoot specifies an alternative directory for standard libraries.
   264  	// Deprecated: this has no effect.
   265  	StdRoot string
   266  
   267  	// ParseFile is called to read and parse each file when preparing a
   268  	// package's syntax tree. It must be safe to call ParseFile simultaneously
   269  	// from multiple goroutines. If ParseFile is nil, the loader will uses
   270  	// parser.ParseFile.
   271  	//
   272  	// ParseFile should parse the source from src and use filename only for
   273  	// recording position information.
   274  	//
   275  	// An application may supply a custom implementation of ParseFile to change
   276  	// the effective file contents or the behavior of the parser, or to modify
   277  	// the syntax tree.
   278  	ParseFile func(name string, src interface{}) (*ast.File, error)
   279  
   280  	// Overlay provides a mapping of absolute file paths to file contents.  If
   281  	// the file with the given path already exists, the parser will use the
   282  	// alternative file contents provided by the map.
   283  	Overlay map[string]Source
   284  
   285  	// Stdin defines an alternative for os.Stdin for the file "-". When used,
   286  	// the corresponding build.File will be associated with the full buffer.
   287  	Stdin io.Reader
   288  
   289  	// Registry is used to fetch CUE module dependencies.
   290  	//
   291  	// When nil, if the modules experiment is enabled
   292  	// (CUE_EXPERIMENT=modules), [modconfig.NewRegistry]
   293  	// will be used to create a registry instance using the
   294  	// usual cmd/cue conventions for environment variables
   295  	// (but see the Env field below).
   296  	//
   297  	// THIS IS EXPERIMENTAL. API MIGHT CHANGE.
   298  	Registry modconfig.Registry
   299  
   300  	// Env provides environment variables for use in the configuration.
   301  	// Currently this is only used in the construction of the Registry
   302  	// value (see above). If this is nil, the current process's environment
   303  	// will be used.
   304  	Env []string
   305  
   306  	fileSystem *fileSystem
   307  }
   308  
   309  func (c *Config) stdin() io.Reader {
   310  	if c.Stdin == nil {
   311  		return os.Stdin
   312  	}
   313  	return c.Stdin
   314  }
   315  
   316  type importPath string
   317  
   318  func addImportQualifier(pkg importPath, name string) (importPath, error) {
   319  	if name == "" {
   320  		return pkg, nil
   321  	}
   322  	ip := module.ParseImportPath(string(pkg))
   323  	if ip.Qualifier == "_" {
   324  		return "", fmt.Errorf("invalid import qualifier _ in %q", pkg)
   325  	}
   326  	if ip.ExplicitQualifier && ip.Qualifier != name {
   327  		return "", fmt.Errorf("non-matching package names (%s != %s)", ip.Qualifier, name)
   328  	}
   329  	ip.Qualifier = name
   330  	return importPath(ip.String()), nil
   331  }
   332  
   333  // Complete updates the configuration information. After calling complete,
   334  // the following invariants hold:
   335  //   - c.Dir is an absolute path.
   336  //   - c.ModuleRoot is an absolute path
   337  //   - c.Module is set to the module import prefix if there is a cue.mod file
   338  //     with the module property.
   339  //   - c.loader != nil
   340  //   - c.cache != ""
   341  //
   342  // It does not initialize c.Context, because that requires the
   343  // loader in order to use for build.Loader.
   344  func (c Config) complete() (cfg *Config, err error) {
   345  	// Each major CUE release should add a tag here.
   346  	// Old tags should not be removed. That is, the cue1.x tag is present
   347  	// in all releases >= CUE 1.x. Code that requires CUE 1.x or later should
   348  	// say "+build cue1.x", and code that should only be built before CUE 1.x
   349  	// (perhaps it is the stub to use in that case) should say "+build !cue1.x".
   350  	c.releaseTags = []string{"cue0.1"}
   351  
   352  	if c.Dir == "" {
   353  		c.Dir, err = os.Getwd()
   354  		if err != nil {
   355  			return nil, err
   356  		}
   357  	} else if c.Dir, err = filepath.Abs(c.Dir); err != nil {
   358  		return nil, err
   359  	}
   360  
   361  	// TODO: we could populate this already with absolute file paths,
   362  	// but relative paths cannot be added. Consider what is reasonable.
   363  	fsys, err := newFileSystem(&c)
   364  	if err != nil {
   365  		return nil, err
   366  	}
   367  	c.fileSystem = fsys
   368  
   369  	// TODO: determine root on a package basis. Maybe we even need a
   370  	// pkgname.cue.mod
   371  	// Look to see if there is a cue.mod.
   372  	//
   373  	// TODO(mvdan): note that setting Config.ModuleRoot to a directory
   374  	// without a cue.mod file does not result in any error, which is confusing
   375  	// or can lead to not using the right CUE module silently.
   376  	if c.ModuleRoot == "" {
   377  		// Only consider the current directory by default
   378  		c.ModuleRoot = c.Dir
   379  		if root := c.findModRoot(c.Dir); root != "" {
   380  			c.ModuleRoot = root
   381  		}
   382  	} else if !filepath.IsAbs(c.ModuleRoot) {
   383  		c.ModuleRoot = filepath.Join(c.Dir, c.ModuleRoot)
   384  	}
   385  	if c.SkipImports {
   386  		// We should never use the registry in SkipImports mode
   387  		// but nil it out to be sure.
   388  		c.Registry = nil
   389  	} else {
   390  		// Note: if cueexperiment.Flags.Modules _isn't_ set but c.Registry
   391  		// is, we consider that a good enough hint that modules support
   392  		// should be enabled and hence don't return an error in that case.
   393  		if cueexperiment.Flags.Modules && c.Registry == nil {
   394  			registry, err := modconfig.NewRegistry(&modconfig.Config{
   395  				Env: c.Env,
   396  			})
   397  			if err != nil {
   398  				// If there's an error in the registry configuration,
   399  				// don't error immediately, but only when we actually
   400  				// need to resolve modules.
   401  				registry = errorRegistry{err}
   402  			}
   403  			c.Registry = registry
   404  		}
   405  	}
   406  	if err := c.loadModule(); err != nil {
   407  		return nil, err
   408  	}
   409  	return &c, nil
   410  }
   411  
   412  // loadModule loads the module file, resolves and downloads module
   413  // dependencies. It sets c.Module if it's empty or checks it for
   414  // consistency with the module file otherwise.
   415  //
   416  // Note that this function is a no-op if a module file does not exist,
   417  // as it is still possible to load CUE without a module.
   418  func (c *Config) loadModule() error {
   419  	// TODO: also make this work if run from outside the module?
   420  	modDir := filepath.Join(c.ModuleRoot, modDir)
   421  	modFile := filepath.Join(modDir, moduleFile)
   422  	f, cerr := c.fileSystem.openFile(modFile)
   423  	if cerr != nil {
   424  		// If we could not load cue.mod/module.cue, check whether the reason was
   425  		// a legacy cue.mod file and give the user a clear error message.
   426  		info, cerr2 := c.fileSystem.stat(modDir)
   427  		if cerr2 == nil && !info.IsDir() {
   428  			return fmt.Errorf("cue.mod files are no longer supported; use cue.mod/module.cue")
   429  		}
   430  		return nil
   431  	}
   432  	defer f.Close()
   433  	data, err := io.ReadAll(f)
   434  	if err != nil {
   435  		return err
   436  	}
   437  	parseModFile := modfile.ParseNonStrict
   438  	if c.Registry == nil {
   439  		parseModFile = modfile.ParseLegacy
   440  	} else if c.AcceptLegacyModules {
   441  		// Note: technically this does not support all legacy module
   442  		// files because some old module files might contain non-concrete
   443  		// data, but that seems like an OK restriction for now at least,
   444  		// given that no actual instances of non-concrete data in
   445  		// module files have been discovered in the wild.
   446  		parseModFile = modfile.FixLegacy
   447  	}
   448  	mf, err := parseModFile(data, modFile)
   449  	if err != nil {
   450  		return err
   451  	}
   452  	c.modFile = mf
   453  	if mf.QualifiedModule() == "" {
   454  		// Backward compatibility: allow empty module.cue file.
   455  		// TODO maybe check that the rest of the fields are empty too?
   456  		return nil
   457  	}
   458  	if c.Module != "" && c.Module != mf.Module {
   459  		return errors.Newf(token.NoPos, "inconsistent modules: got %q, want %q", mf.Module, c.Module)
   460  	}
   461  	c.Module = mf.QualifiedModule()
   462  	return nil
   463  }
   464  
   465  func (c Config) isModRoot(dir string) bool {
   466  	// Note: cue.mod used to be a file. We still allow both to match.
   467  	_, err := c.fileSystem.stat(filepath.Join(dir, modDir))
   468  	return err == nil
   469  }
   470  
   471  // findModRoot returns the module root that's ancestor
   472  // of the given absolute directory path, or "" if none was found.
   473  func (c Config) findModRoot(absDir string) string {
   474  	abs := absDir
   475  	for {
   476  		if c.isModRoot(abs) {
   477  			return abs
   478  		}
   479  		d := filepath.Dir(abs)
   480  		if filepath.Base(filepath.Dir(abs)) == modDir {
   481  			// The package was located within a "cue.mod" dir and there was
   482  			// not cue.mod found until now. So there is no root.
   483  			return ""
   484  		}
   485  		if len(d) >= len(abs) {
   486  			return "" // reached top of file system, no cue.mod
   487  		}
   488  		abs = d
   489  	}
   490  }
   491  
   492  func (c *Config) newErrInstance(err error) *build.Instance {
   493  	i := c.Context.NewInstance("", nil)
   494  	i.Root = c.ModuleRoot
   495  	i.Module = c.Module
   496  	i.Err = errors.Promote(err, "")
   497  	return i
   498  }
   499  
   500  // errorRegistry implements [modconfig.Registry] by returning err from all methods.
   501  type errorRegistry struct {
   502  	err error
   503  }
   504  
   505  func (r errorRegistry) Requirements(ctx context.Context, m module.Version) ([]module.Version, error) {
   506  	return nil, r.err
   507  }
   508  
   509  func (r errorRegistry) Fetch(ctx context.Context, m module.Version) (module.SourceLoc, error) {
   510  	return module.SourceLoc{}, r.err
   511  }
   512  
   513  func (r errorRegistry) ModuleVersions(ctx context.Context, mpath string) ([]string, error) {
   514  	return nil, r.err
   515  }