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 }