github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/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  	"io"
    19  	"os"
    20  	pathpkg "path"
    21  	"path/filepath"
    22  	"strings"
    23  
    24  	"github.com/joomcode/cue/cue/ast"
    25  	"github.com/joomcode/cue/cue/build"
    26  	"github.com/joomcode/cue/cue/errors"
    27  	"github.com/joomcode/cue/cue/parser"
    28  	"github.com/joomcode/cue/cue/token"
    29  	"github.com/joomcode/cue/internal"
    30  	"github.com/joomcode/cue/internal/core/compile"
    31  	"github.com/joomcode/cue/internal/core/eval"
    32  	"github.com/joomcode/cue/internal/core/runtime"
    33  )
    34  
    35  const (
    36  	cueSuffix  = ".cue"
    37  	modDir     = "cue.mod"
    38  	configFile = "module.cue"
    39  	pkgDir     = "pkg"
    40  )
    41  
    42  // FromArgsUsage is a partial usage message that applications calling
    43  // FromArgs may wish to include in their -help output.
    44  //
    45  // Some of the aspects of this documentation, like flags and handling '--' need
    46  // to be implemented by the tools.
    47  const FromArgsUsage = `
    48  <args> is a list of arguments denoting a set of instances of the form:
    49  
    50     <package>* <file_args>*
    51  
    52  1. A list of source files
    53  
    54     CUE files are parsed, loaded and unified into a single instance. All files
    55     must have the same package name.
    56  
    57     Data files, like YAML or JSON, are handled in one of two ways:
    58  
    59     a. Explicitly mapped into a single CUE namespace, using the --path, --files
    60        and --list flags. In this case these are unified into a single instance
    61        along with any other CUE files.
    62  
    63     b. Treated as a stream of data elements that each is optionally unified with
    64        a single instance, which either consists of the other CUE files specified
    65         on the command line or a single package.
    66  
    67     By default, the format of files is derived from the file extension.
    68     This behavior may be modified with file arguments of the form <qualifiers>:
    69     For instance,
    70  
    71        cue eval foo.cue json: bar.data
    72  
    73     indicates that the bar.data file should be interpreted as a JSON file.
    74     A qualifier applies to all files following it until the next qualifier.
    75  
    76     The following qualifiers are available:
    77  
    78        encodings
    79        cue           CUE definitions and data
    80        json          JSON data, one value only
    81        jsonl         newline-separated JSON values
    82        yaml          a YAML file, may contain a stream
    83        proto         Protobuf definitions
    84  
    85        interpretations
    86        jsonschema   data encoding describes JSON Schema
    87        openapi      data encoding describes Open API
    88  
    89        formats
    90        data         output as -- or only accept -- data
    91        graph        data allowing references or anchors
    92        schema       output as schema; defaults JSON files to JSON Schema
    93        def          full definitions, including documentation
    94  
    95  2. A list of relative directories to denote a package instance.
    96  
    97     Each directory matching the pattern is loaded as a separate instance.
    98     The instance contains all files in this directory and ancestor directories,
    99     up to the module root, with the same package name. The package name must
   100     be either uniquely determined by the files in the given directory, or
   101     explicitly defined using a package name qualifier. For instance, ./...:foo
   102     selects all packages named foo in the any subdirectory of the current
   103     working directory.
   104  
   105     3. An import path referring to a directory within the current module
   106  
   107     All CUE files in that directory, and all the ancestor directories up to the
   108     module root (if applicable), with a package name corresponding to the base
   109     name of the directory or the optional explicit package name are loaded into
   110     a single instance.
   111  
   112     Examples, assume a module name of acme.org/root:
   113        example.com/foo   package in cue.mod
   114        ./foo             package corresponding to foo directory
   115        .:bar             package in current directory with package name bar
   116  `
   117  
   118  // GenPath reports the directory in which to store generated
   119  // files.
   120  func GenPath(root string) string {
   121  	return internal.GenPath(root)
   122  }
   123  
   124  // A Config configures load behavior.
   125  type Config struct {
   126  	// TODO: allow passing a cuecontext to be able to lookup and verify builtin
   127  	// packages at loading time.
   128  
   129  	// Context specifies the context for the load operation.
   130  	// If the context is cancelled, the loader may stop early
   131  	// and return an ErrCancelled error.
   132  	// If Context is nil, the load cannot be cancelled.
   133  	Context *build.Context
   134  
   135  	loader *loader
   136  
   137  	// A Module is a collection of packages and instances that are within the
   138  	// directory hierarchy rooted at the module root. The module root can be
   139  	// marked with a cue.mod file.
   140  	ModuleRoot string
   141  
   142  	// Module specifies the module prefix. If not empty, this value must match
   143  	// the module field of an existing cue.mod file.
   144  	Module string
   145  
   146  	// Package defines the name of the package to be loaded. If this is not set,
   147  	// the package must be uniquely defined from its context. Special values:
   148  	//    _    load files without a package
   149  	//    *    load all packages. Files without packages are loaded
   150  	//         in the _ package.
   151  	Package string
   152  
   153  	// Dir is the directory in which to run the build system's query tool
   154  	// that provides information about the packages.
   155  	// If Dir is empty, the tool is run in the current directory.
   156  	Dir string
   157  
   158  	// Tags defines boolean tags or key-value pairs to select files to build
   159  	// or be injected as values in fields.
   160  	//
   161  	// Each string is of the form
   162  	//
   163  	//     key [ "=" value ]
   164  	//
   165  	// where key is a valid CUE identifier and value valid CUE scalar.
   166  	//
   167  	// The Tags values are used to both select which files get included in a
   168  	// build and to inject values into the AST.
   169  	//
   170  	//
   171  	// File selection
   172  	//
   173  	// Files with an attribute of the form @if(expr) before a package clause
   174  	// are conditionally included if expr resolves to true, where expr refers to
   175  	// boolean values in Tags.
   176  	//
   177  	// It is an error for a file to have more than one @if attribute or to
   178  	// have a @if attribute without or after a package clause.
   179  	//
   180  	//
   181  	// Value injection
   182  	//
   183  	// The Tags values are also used to inject values into fields with a
   184  	// @tag attribute.
   185  	//
   186  	// For any field of the form
   187  	//
   188  	//    field: x @tag(key)
   189  	//
   190  	// and Tags value for which the name matches key, the field will be
   191  	// modified to
   192  	//
   193  	//   field: x & "value"
   194  	//
   195  	// By default, the injected value is treated as a string. Alternatively, a
   196  	// "type" option of the @tag attribute allows a value to be interpreted as
   197  	// an int, number, or bool. For instance, for a field
   198  	//
   199  	//    field: x @tag(key,type=int)
   200  	//
   201  	// an entry "key=2" modifies the field to
   202  	//
   203  	//    field: x & 2
   204  	//
   205  	// Valid values for type are "int", "number", "bool", and "string".
   206  	//
   207  	// A @tag attribute can also define shorthand values, which can be injected
   208  	// into the fields without having to specify the key. For instance, for
   209  	//
   210  	//    environment: string @tag(env,short=prod|staging)
   211  	//
   212  	// the Tags entry "prod" sets the environment field to the value "prod".
   213  	// This is equivalent to a Tags entry of "env=prod".
   214  	//
   215  	// The use of @tag does not preclude using any of the usual CUE constraints
   216  	// to limit the possible values of a field. For instance
   217  	//
   218  	//    environment: "prod" | "staging" @tag(env,short=prod|staging)
   219  	//
   220  	// ensures the user may only specify "prod" or "staging".
   221  	Tags []string
   222  
   223  	// TagVars defines a set of key value pair the values of which may be
   224  	// referenced by tags.
   225  	//
   226  	// Use DefaultTagVars to get a pre-loaded map with supported values.
   227  	TagVars map[string]TagVar
   228  
   229  	// Include all files, regardless of tags.
   230  	AllCUEFiles bool
   231  
   232  	// Deprecated: use Tags
   233  	BuildTags   []string
   234  	releaseTags []string
   235  
   236  	// If Tests is set, the loader includes not just the packages
   237  	// matching a particular pattern but also any related test packages.
   238  	Tests bool
   239  
   240  	// If Tools is set, the loader includes tool files associated with
   241  	// a package.
   242  	Tools bool
   243  
   244  	// filesMode indicates that files are specified
   245  	// explicitly on the command line.
   246  	filesMode bool
   247  
   248  	// If DataFiles is set, the loader includes entries for directories that
   249  	// have no CUE files, but have recognized data files that could be converted
   250  	// to CUE.
   251  	DataFiles bool
   252  
   253  	// StdRoot specifies an alternative directory for standard libaries.
   254  	// This is mostly used for bootstrapping.
   255  	StdRoot string
   256  
   257  	// ParseFile is called to read and parse each file when preparing a
   258  	// package's syntax tree. It must be safe to call ParseFile simultaneously
   259  	// from multiple goroutines. If ParseFile is nil, the loader will uses
   260  	// parser.ParseFile.
   261  	//
   262  	// ParseFile should parse the source from src and use filename only for
   263  	// recording position information.
   264  	//
   265  	// An application may supply a custom implementation of ParseFile to change
   266  	// the effective file contents or the behavior of the parser, or to modify
   267  	// the syntax tree.
   268  	ParseFile func(name string, src interface{}) (*ast.File, error)
   269  
   270  	// Overlay provides a mapping of absolute file paths to file contents.  If
   271  	// the file with the given path already exists, the parser will use the
   272  	// alternative file contents provided by the map.
   273  	Overlay map[string]Source
   274  
   275  	// Stdin defines an alternative for os.Stdin for the file "-". When used,
   276  	// the corresponding build.File will be associated with the full buffer.
   277  	Stdin io.Reader
   278  
   279  	fileSystem
   280  
   281  	loadFunc build.LoadFunc
   282  
   283  	// Path to starlark function registry
   284  	StarlarkCodePath string
   285  }
   286  
   287  func (c *Config) stdin() io.Reader {
   288  	if c.Stdin == nil {
   289  		return os.Stdin
   290  	}
   291  	return c.Stdin
   292  }
   293  
   294  func (c *Config) newInstance(pos token.Pos, p importPath) *build.Instance {
   295  	dir, name, err := c.absDirFromImportPath(pos, p)
   296  	i := c.Context.NewInstance(dir, c.loadFunc)
   297  	i.Dir = dir
   298  	i.PkgName = name
   299  	i.DisplayPath = string(p)
   300  	i.ImportPath = string(p)
   301  	i.Root = c.ModuleRoot
   302  	i.Module = c.Module
   303  	i.Err = errors.Append(i.Err, err)
   304  
   305  	return i
   306  }
   307  
   308  func (c *Config) newRelInstance(pos token.Pos, path, pkgName string) *build.Instance {
   309  	fs := c.fileSystem
   310  
   311  	var err errors.Error
   312  	dir := path
   313  
   314  	p := c.Context.NewInstance(path, c.loadFunc)
   315  	p.PkgName = pkgName
   316  	p.DisplayPath = filepath.ToSlash(path)
   317  	// p.ImportPath = string(dir) // compute unique ID.
   318  	p.Root = c.ModuleRoot
   319  	p.Module = c.Module
   320  
   321  	if isLocalImport(path) {
   322  		if c.Dir == "" {
   323  			err = errors.Append(err, errors.Newf(pos, "cwd unknown"))
   324  		}
   325  		dir = filepath.Join(c.Dir, filepath.FromSlash(path))
   326  	}
   327  
   328  	if path == "" {
   329  		err = errors.Append(err, errors.Newf(pos,
   330  			"import %q: invalid import path", path))
   331  	} else if path != cleanImport(path) {
   332  		err = errors.Append(err, c.loader.errPkgf(nil,
   333  			"non-canonical import path: %q should be %q", path, pathpkg.Clean(path)))
   334  	}
   335  
   336  	if importPath, e := c.importPathFromAbsDir(fsPath(dir), path); e != nil {
   337  		// Detect later to keep error messages consistent.
   338  	} else {
   339  		p.ImportPath = string(importPath)
   340  	}
   341  
   342  	p.Dir = dir
   343  
   344  	if fs.isAbsPath(path) || strings.HasPrefix(path, "/") {
   345  		err = errors.Append(err, errors.Newf(pos,
   346  			"absolute import path %q not allowed", path))
   347  	}
   348  	if err != nil {
   349  		p.Err = errors.Append(p.Err, err)
   350  		p.Incomplete = true
   351  	}
   352  
   353  	return p
   354  }
   355  
   356  func (c Config) newErrInstance(pos token.Pos, path importPath, err error) *build.Instance {
   357  	i := c.newInstance(pos, path)
   358  	i.Err = errors.Promote(err, "instance")
   359  	return i
   360  }
   361  
   362  func toImportPath(dir string) importPath {
   363  	return importPath(filepath.ToSlash(dir))
   364  }
   365  
   366  type importPath string
   367  
   368  type fsPath string
   369  
   370  func (c *Config) importPathFromAbsDir(absDir fsPath, key string) (importPath, errors.Error) {
   371  	if c.ModuleRoot == "" {
   372  		return "", errors.Newf(token.NoPos,
   373  			"cannot determine import path for %q (root undefined)", key)
   374  	}
   375  
   376  	dir := filepath.Clean(string(absDir))
   377  	if !strings.HasPrefix(dir, c.ModuleRoot) {
   378  		return "", errors.Newf(token.NoPos,
   379  			"cannot determine import path for %q (dir outside of root)", key)
   380  	}
   381  
   382  	pkg := filepath.ToSlash(dir[len(c.ModuleRoot):])
   383  	switch {
   384  	case strings.HasPrefix(pkg, "/cue.mod/"):
   385  		pkg = pkg[len("/cue.mod/"):]
   386  		if pkg == "" {
   387  			return "", errors.Newf(token.NoPos,
   388  				"invalid package %q (root of %s)", key, modDir)
   389  		}
   390  
   391  		// TODO(legacy): remove.
   392  	case strings.HasPrefix(pkg, "/pkg/"):
   393  		pkg = pkg[len("/pkg/"):]
   394  		if pkg == "" {
   395  			return "", errors.Newf(token.NoPos,
   396  				"invalid package %q (root of %s)", key, pkgDir)
   397  		}
   398  
   399  	case c.Module == "":
   400  		return "", errors.Newf(token.NoPos,
   401  			"cannot determine import path for %q (no module)", key)
   402  	default:
   403  		pkg = c.Module + pkg
   404  	}
   405  
   406  	name := c.Package
   407  	switch name {
   408  	case "_", "*":
   409  		name = ""
   410  	}
   411  
   412  	return addImportQualifier(importPath(pkg), name)
   413  }
   414  
   415  func addImportQualifier(pkg importPath, name string) (importPath, errors.Error) {
   416  	if name != "" {
   417  		s := string(pkg)
   418  		if i := strings.LastIndexByte(s, '/'); i >= 0 {
   419  			s = s[i+1:]
   420  		}
   421  		if i := strings.LastIndexByte(s, ':'); i >= 0 {
   422  			// should never happen, but just in case.
   423  			s = s[i+1:]
   424  			if s != name {
   425  				return "", errors.Newf(token.NoPos,
   426  					"non-matching package names (%s != %s)", s, name)
   427  			}
   428  		} else if s != name {
   429  			pkg += importPath(":" + name)
   430  		}
   431  	}
   432  
   433  	return pkg, nil
   434  }
   435  
   436  // absDirFromImportPath converts a giving import path to an absolute directory
   437  // and a package name. The root directory must be set.
   438  //
   439  // The returned directory may not exist.
   440  func (c *Config) absDirFromImportPath(pos token.Pos, p importPath) (absDir, name string, err errors.Error) {
   441  	if c.ModuleRoot == "" {
   442  		return "", "", errors.Newf(pos, "cannot import %q (root undefined)", p)
   443  	}
   444  
   445  	// Extract the package name.
   446  
   447  	name = string(p)
   448  	switch i := strings.LastIndexAny(name, "/:"); {
   449  	case i < 0:
   450  	case p[i] == ':':
   451  		name = string(p[i+1:])
   452  		p = p[:i]
   453  
   454  	default: // p[i] == '/'
   455  		name = string(p[i+1:])
   456  	}
   457  
   458  	// TODO: fully test that name is a valid identifier.
   459  	if name == "" {
   460  		err = errors.Newf(pos, "empty package name in import path %q", p)
   461  	} else if strings.IndexByte(name, '.') >= 0 {
   462  		err = errors.Newf(pos,
   463  			"cannot determine package name for %q (set explicitly with ':')", p)
   464  	}
   465  
   466  	// Determine the directory.
   467  
   468  	sub := filepath.FromSlash(string(p))
   469  	switch hasPrefix := strings.HasPrefix(string(p), c.Module); {
   470  	case hasPrefix && len(sub) == len(c.Module):
   471  		absDir = c.ModuleRoot
   472  
   473  	case hasPrefix && p[len(c.Module)] == '/':
   474  		absDir = filepath.Join(c.ModuleRoot, sub[len(c.Module)+1:])
   475  
   476  	default:
   477  		absDir = filepath.Join(GenPath(c.ModuleRoot), sub)
   478  	}
   479  
   480  	return absDir, name, err
   481  }
   482  
   483  // Complete updates the configuration information. After calling complete,
   484  // the following invariants hold:
   485  //  - c.ModuleRoot != ""
   486  //  - c.Module is set to the module import prefix if there is a cue.mod file
   487  //    with the module property.
   488  //  - c.loader != nil
   489  //  - c.cache != ""
   490  func (c Config) complete() (cfg *Config, err error) {
   491  	// Each major CUE release should add a tag here.
   492  	// Old tags should not be removed. That is, the cue1.x tag is present
   493  	// in all releases >= CUE 1.x. Code that requires CUE 1.x or later should
   494  	// say "+build cue1.x", and code that should only be built before CUE 1.x
   495  	// (perhaps it is the stub to use in that case) should say "+build !cue1.x".
   496  	c.releaseTags = []string{"cue0.1"}
   497  
   498  	if c.Dir == "" {
   499  		c.Dir, err = os.Getwd()
   500  		if err != nil {
   501  			return nil, err
   502  		}
   503  	} else if c.Dir, err = filepath.Abs(c.Dir); err != nil {
   504  		return nil, err
   505  	}
   506  
   507  	// TODO: we could populate this already with absolute file paths,
   508  	// but relative paths cannot be added. Consider what is reasonable.
   509  	if err := c.fileSystem.init(&c); err != nil {
   510  		return nil, err
   511  	}
   512  
   513  	// TODO: determine root on a package basis. Maybe we even need a
   514  	// pkgname.cue.mod
   515  	// Look to see if there is a cue.mod.
   516  	if c.ModuleRoot == "" {
   517  		// Only consider the current directory by default
   518  		c.ModuleRoot = c.Dir
   519  		if root := c.findRoot(c.Dir); root != "" {
   520  			c.ModuleRoot = root
   521  		}
   522  	}
   523  
   524  	c.loader = &loader{
   525  		cfg:       &c,
   526  		buildTags: make(map[string]bool),
   527  	}
   528  
   529  	// TODO: also make this work if run from outside the module?
   530  	switch {
   531  	case true:
   532  		mod := filepath.Join(c.ModuleRoot, modDir)
   533  		info, cerr := c.fileSystem.stat(mod)
   534  		if cerr != nil {
   535  			break
   536  		}
   537  		if info.IsDir() {
   538  			mod = filepath.Join(mod, configFile)
   539  		}
   540  		f, cerr := c.fileSystem.openFile(mod)
   541  		if cerr != nil {
   542  			break
   543  		}
   544  
   545  		// TODO: move to full build again
   546  		file, err := parser.ParseFile("load", f)
   547  		if err != nil {
   548  			return nil, errors.Wrapf(err, token.NoPos, "invalid cue.mod file")
   549  		}
   550  
   551  		r := runtime.New()
   552  		v, err := compile.Files(nil, r, "_", file)
   553  		if err != nil {
   554  			return nil, errors.Wrapf(err, token.NoPos, "invalid cue.mod file")
   555  		}
   556  		ctx := eval.NewContext(r, v)
   557  		v.Finalize(ctx)
   558  		prefix := v.Lookup(ctx.StringLabel("module"))
   559  		if prefix != nil {
   560  			name := ctx.StringValue(prefix.Value())
   561  			if err := ctx.Err(); err != nil {
   562  				return &c, err.Err
   563  			}
   564  			pos := token.NoPos
   565  			src := prefix.Value().Source()
   566  			if src != nil {
   567  				pos = src.Pos()
   568  			}
   569  			if c.Module != "" && c.Module != name {
   570  				return &c, errors.Newf(pos, "inconsistent modules: got %q, want %q", name, c.Module)
   571  			}
   572  			c.Module = name
   573  		}
   574  	}
   575  
   576  	c.loadFunc = c.loader.loadFunc()
   577  
   578  	if c.Context == nil {
   579  		c.Context = build.NewContext(
   580  			build.Loader(c.loadFunc),
   581  			build.ParseFile(c.loader.cfg.ParseFile),
   582  		)
   583  	}
   584  
   585  	return &c, nil
   586  }
   587  
   588  func (c Config) isRoot(dir string) bool {
   589  	fs := &c.fileSystem
   590  	// Note: cue.mod used to be a file. We still allow both to match.
   591  	_, err := fs.stat(filepath.Join(dir, modDir))
   592  	return err == nil
   593  }
   594  
   595  // findRoot returns the module root or "" if none was found.
   596  func (c Config) findRoot(dir string) string {
   597  	fs := &c.fileSystem
   598  
   599  	absDir, err := filepath.Abs(dir)
   600  	if err != nil {
   601  		return ""
   602  	}
   603  	abs := absDir
   604  	for {
   605  		if c.isRoot(abs) {
   606  			return abs
   607  		}
   608  		d := filepath.Dir(abs)
   609  		if filepath.Base(filepath.Dir(abs)) == modDir {
   610  			// The package was located within a "cue.mod" dir and there was
   611  			// not cue.mod found until now. So there is no root.
   612  			return ""
   613  		}
   614  		if len(d) >= len(abs) {
   615  			break // reached top of file system, no cue.mod
   616  		}
   617  		abs = d
   618  	}
   619  	abs = absDir
   620  
   621  	// TODO(legacy): remove this capability at some point.
   622  	for {
   623  		info, err := fs.stat(filepath.Join(abs, pkgDir))
   624  		if err == nil && info.IsDir() {
   625  			return abs
   626  		}
   627  		d := filepath.Dir(abs)
   628  		if len(d) >= len(abs) {
   629  			return "" // reached top of file system, no pkg dir.
   630  		}
   631  		abs = d
   632  	}
   633  }