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