cuelang.org/go@v0.13.0/mod/modfile/modfile.go (about) 1 // Copyright 2023 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 modfile provides functionality for reading and parsing 16 // the CUE module file, cue.mod/module.cue. 17 // 18 // WARNING: THIS PACKAGE IS EXPERIMENTAL. 19 // ITS API MAY CHANGE AT ANY TIME. 20 package modfile 21 22 import ( 23 _ "embed" 24 "fmt" 25 "path" 26 "slices" 27 "strings" 28 "sync" 29 30 "cuelang.org/go/internal/mod/semver" 31 32 "cuelang.org/go/cue" 33 "cuelang.org/go/cue/ast" 34 "cuelang.org/go/cue/build" 35 "cuelang.org/go/cue/cuecontext" 36 "cuelang.org/go/cue/errors" 37 "cuelang.org/go/cue/format" 38 "cuelang.org/go/cue/token" 39 "cuelang.org/go/internal/cueversion" 40 "cuelang.org/go/internal/encoding" 41 "cuelang.org/go/internal/filetypes" 42 "cuelang.org/go/mod/module" 43 ) 44 45 //go:embed schema.cue 46 var moduleSchemaData string 47 48 const schemaFile = "cuelang.org/go/mod/modfile/schema.cue" 49 50 // File represents the contents of a cue.mod/module.cue file. 51 type File struct { 52 // Module holds the module path, which may 53 // not contain a major version suffix. 54 // Use the [File.QualifiedModule] method to obtain a module 55 // path that's always qualified. See also the 56 // [File.ModulePath] and [File.MajorVersion] methods. 57 Module string `json:"module"` 58 Language *Language `json:"language,omitempty"` 59 Source *Source `json:"source,omitempty"` 60 Deps map[string]*Dep `json:"deps,omitempty"` 61 Custom map[string]map[string]any `json:"custom,omitempty"` 62 versions []module.Version 63 versionByModule map[string]module.Version 64 // defaultMajorVersions maps from module base path to the 65 // major version default for that path. 66 defaultMajorVersions map[string]string 67 // actualSchemaVersion holds the actual schema version 68 // that was used to validate the file. This will be one of the 69 // entries in the versions field in schema.cue and 70 // is set by the Parse functions. 71 actualSchemaVersion string 72 } 73 74 // Module returns the fully qualified module path 75 // if is one. It returns the empty string when [ParseLegacy] 76 // is used and the module field is empty. 77 // 78 // Note that when the module field does not contain a major 79 // version suffix, "@v0" is assumed. 80 func (f *File) QualifiedModule() string { 81 if strings.Contains(f.Module, "@") { 82 return f.Module 83 } 84 if f.Module == "" { 85 return "" 86 } 87 return f.Module + "@v0" 88 } 89 90 // ModulePath returns the path part of the module without 91 // its major version suffix. 92 func (f *File) ModulePath() string { 93 path, _, _ := ast.SplitPackageVersion(f.QualifiedModule()) 94 return path 95 } 96 97 // MajorVersion returns the major version of the module, 98 // not including the "@". 99 // If there is no module (which can happen when [ParseLegacy] 100 // is used or if Module is explicitly set to an empty string), 101 // it returns the empty string. 102 func (f *File) MajorVersion() string { 103 _, vers, _ := ast.SplitPackageVersion(f.QualifiedModule()) 104 return vers 105 } 106 107 // baseFileVersion is used to decode the language version 108 // to decide how to decode the rest of the file. 109 type baseFileVersion struct { 110 Language struct { 111 Version string `json:"version"` 112 } `json:"language"` 113 } 114 115 // Source represents how to transform from a module's 116 // source to its actual contents. 117 type Source struct { 118 Kind string `json:"kind"` 119 } 120 121 // Validate checks that src is well formed. 122 func (src *Source) Validate() error { 123 switch src.Kind { 124 case "git", "self": 125 return nil 126 } 127 return fmt.Errorf("unrecognized source kind %q", src.Kind) 128 } 129 130 // Format returns a formatted representation of f 131 // in CUE syntax. 132 func (f *File) Format() ([]byte, error) { 133 if len(f.Deps) == 0 && f.Deps != nil { 134 // There's no way to get the CUE encoder to omit an empty 135 // but non-nil slice (despite the current doc comment on 136 // [cue.Context.Encode], so make a copy of f to allow us 137 // to do that. 138 f1 := *f 139 f1.Deps = nil 140 f = &f1 141 } 142 // TODO this could be better: 143 // - it should omit the outer braces 144 v := cuecontext.New().Encode(f) 145 if err := v.Validate(cue.Concrete(true)); err != nil { 146 return nil, err 147 } 148 n := v.Syntax(cue.Concrete(true)).(*ast.StructLit) 149 150 data, err := format.Node(&ast.File{ 151 Decls: n.Elts, 152 }) 153 if err != nil { 154 return nil, fmt.Errorf("cannot format: %v", err) 155 } 156 // Sanity check that it can be parsed. 157 // TODO this could be more efficient by checking all the file fields 158 // before formatting the output. 159 f1, err := ParseNonStrict(data, "-") 160 if err != nil { 161 return nil, fmt.Errorf("cannot parse result: %v", strings.TrimSuffix(errors.Details(err, nil), "\n")) 162 } 163 if f.Language != nil && f1.actualSchemaVersion == "v0.0.0" { 164 // It's not a legacy module file (because the language field is present) 165 // but we've used the legacy schema to parse it, which means that 166 // it's almost certainly a bogus version because all versions 167 // we care about fail when there are unknown fields, but the 168 // original schema allowed all fields. 169 return nil, fmt.Errorf("language version %v is too early for module.cue (need at least %v)", f.Language.Version, EarliestClosedSchemaVersion()) 170 } 171 return data, err 172 } 173 174 type Language struct { 175 Version string `json:"version,omitempty"` 176 } 177 178 type Dep struct { 179 Version string `json:"v"` 180 Default bool `json:"default,omitempty"` 181 } 182 183 type noDepsFile struct { 184 Module string `json:"module"` 185 } 186 187 var ( 188 moduleSchemaOnce sync.Once // guards the creation of _moduleSchema 189 // TODO remove this mutex when https://cuelang.org/issue/2733 is fixed. 190 moduleSchemaMutex sync.Mutex // guards any use of _moduleSchema 191 _schemas schemaInfo 192 ) 193 194 type schemaInfo struct { 195 Versions map[string]cue.Value `json:"versions"` 196 EarliestClosedSchemaVersion string `json:"earliestClosedSchemaVersion"` 197 } 198 199 // moduleSchemaDo runs f with information about all the schema versions 200 // present in schema.cue. It does this within a mutex because it is 201 // not currently allowed to use cue.Value concurrently. 202 // TODO remove the mutex when https://cuelang.org/issue/2733 is fixed. 203 func moduleSchemaDo[T any](f func(*schemaInfo) (T, error)) (T, error) { 204 moduleSchemaOnce.Do(func() { 205 // It is important that this cue.Context not be used for building any other cue.Value, 206 // such as in [Parse] or [ParseLegacy]. 207 // A value holds memory as long as the context it was built with is kept alive for, 208 // and this context is alive forever via the _schemas global. 209 // 210 // TODO(mvdan): this violates the documented API rules in the cue package: 211 // 212 // Only values created from the same Context can be involved in the same operation. 213 // 214 // However, this appears to work in practice, and all alternatives right now would be 215 // either too costly or awkward. We want to lift that API restriction, and this works OK, 216 // so leave it as-is for the time being. 217 ctx := cuecontext.New() 218 schemav := ctx.CompileString(moduleSchemaData, cue.Filename(schemaFile)) 219 if err := schemav.Decode(&_schemas); err != nil { 220 panic(fmt.Errorf("internal error: invalid CUE module.cue schema: %v", errors.Details(err, nil))) 221 } 222 }) 223 moduleSchemaMutex.Lock() 224 defer moduleSchemaMutex.Unlock() 225 return f(&_schemas) 226 } 227 228 func lookup(v cue.Value, sels ...cue.Selector) cue.Value { 229 return v.LookupPath(cue.MakePath(sels...)) 230 } 231 232 // EarliestClosedSchemaVersion returns the earliest module.cue schema version 233 // that excludes unknown fields. Any version declared in a module.cue file 234 // should be at least this, because that's when we added the language.version 235 // field itself. 236 func EarliestClosedSchemaVersion() string { 237 return earliestClosedSchemaVersion() 238 } 239 240 var earliestClosedSchemaVersion = sync.OnceValue(func() string { 241 earliest, _ := moduleSchemaDo(func(info *schemaInfo) (string, error) { 242 earliest := "" 243 for v := range info.Versions { 244 if earliest == "" || semver.Compare(v, earliest) < 0 { 245 earliest = v 246 } 247 } 248 return earliest, nil 249 }) 250 return earliest 251 }) 252 253 // Parse verifies that the module file has correct syntax 254 // and follows the schema following the required language.version field. 255 // The file name is used for error messages. 256 // All dependencies must be specified correctly: with major 257 // versions in the module paths and canonical dependency versions. 258 func Parse(modfile []byte, filename string) (*File, error) { 259 return parse(modfile, filename, true) 260 } 261 262 // ParseLegacy parses the legacy version of the module file 263 // that only supports the single field "module" and ignores all other 264 // fields. 265 func ParseLegacy(modfile []byte, filename string) (*File, error) { 266 ctx := cuecontext.New() 267 file, err := parseDataOnlyCUE(ctx, modfile, filename) 268 if err != nil { 269 return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file syntax") 270 } 271 // Unfortunately we need a new context. See the note inside [moduleSchemaDo]. 272 v := ctx.BuildFile(file) 273 if err := v.Err(); err != nil { 274 return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file") 275 } 276 var f noDepsFile 277 if err := v.Decode(&f); err != nil { 278 return nil, newCUEError(err, filename) 279 } 280 return &File{ 281 Module: f.Module, 282 actualSchemaVersion: "v0.0.0", 283 }, nil 284 } 285 286 // ParseNonStrict is like Parse but allows some laxity in the parsing: 287 // - if a module path lacks a version, it's taken from the version. 288 // - if a non-canonical version is used, it will be canonicalized. 289 // 290 // The file name is used for error messages. 291 func ParseNonStrict(modfile []byte, filename string) (*File, error) { 292 return parse(modfile, filename, false) 293 } 294 295 // FixLegacy converts a legacy module.cue file as parsed by [ParseLegacy] 296 // into a format suitable for parsing with [Parse]. It adds a language.version 297 // field and moves all unrecognized fields into custom.legacy. 298 // 299 // If there is no module field or it is empty, it is set to "test.example". 300 // 301 // If the file already parses OK with [ParseNonStrict], it returns the 302 // result of that. 303 func FixLegacy(modfile []byte, filename string) (*File, error) { 304 f, err := ParseNonStrict(modfile, filename) 305 if err == nil { 306 // It parses OK so it doesn't need fixing. 307 return f, nil 308 } 309 ctx := cuecontext.New() 310 file, err := parseDataOnlyCUE(ctx, modfile, filename) 311 if err != nil { 312 return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file syntax") 313 } 314 v := ctx.BuildFile(file) 315 if err := v.Validate(cue.Concrete(true)); err != nil { 316 return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file value") 317 } 318 var allFields map[string]any 319 if err := v.Decode(&allFields); err != nil { 320 return nil, err 321 } 322 mpath := "test.example" 323 if m, ok := allFields["module"]; ok { 324 if mpath1, ok := m.(string); ok && mpath1 != "" { 325 mpath = mpath1 326 } else if !ok { 327 return nil, fmt.Errorf("module field has unexpected type %T", m) 328 } 329 // TODO decide what to do if the module path isn't OK according to the new rules. 330 } 331 customLegacy := make(map[string]any) 332 for k, v := range allFields { 333 if k != "module" { 334 customLegacy[k] = v 335 } 336 } 337 var custom map[string]map[string]any 338 if len(customLegacy) > 0 { 339 custom = map[string]map[string]any{ 340 "legacy": customLegacy, 341 } 342 } 343 f = &File{ 344 Module: mpath, 345 Language: &Language{ 346 // If there's a legacy module file, the CUE code 347 // is unlikely to be using new language features, 348 // so keep the language version fixed rather than 349 // using [cueversion.LanguageVersion]. 350 // See https://cuelang.org/issue/3222. 351 Version: "v0.9.0", 352 }, 353 Custom: custom, 354 } 355 // Round-trip through [Parse] so that we get exactly the same 356 // result as a later parse of the same data will. This also 357 // adds a major version to the module path if needed. 358 data, err := f.Format() 359 if err != nil { 360 return nil, fmt.Errorf("cannot format fixed file: %v", err) 361 } 362 f, err = ParseNonStrict(data, "fixed-"+filename) 363 if err != nil { 364 return nil, fmt.Errorf("cannot parse resulting module file %q: %v", data, err) 365 } 366 return f, nil 367 } 368 369 func parse(modfile []byte, filename string, strict bool) (*File, error) { 370 // Unfortunately we need a new context. See the note inside [moduleSchemaDo]. 371 ctx := cuecontext.New() 372 file, err := parseDataOnlyCUE(ctx, modfile, filename) 373 if err != nil { 374 return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file syntax") 375 } 376 377 v := ctx.BuildFile(file) 378 if err := v.Validate(cue.Concrete(true)); err != nil { 379 return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file value") 380 } 381 // First determine the declared version of the module file. 382 var base baseFileVersion 383 if err := v.Decode(&base); err != nil { 384 return nil, errors.Wrapf(err, token.NoPos, "cannot determine language version") 385 } 386 if base.Language.Version == "" { 387 return nil, ErrNoLanguageVersion 388 } 389 if !semver.IsValid(base.Language.Version) { 390 return nil, fmt.Errorf("language version %q in module.cue is not valid semantic version", base.Language.Version) 391 } 392 if mv, lv := base.Language.Version, cueversion.LanguageVersion(); semver.Compare(mv, lv) > 0 { 393 return nil, fmt.Errorf("language version %q declared in module.cue is too new for current language version %q", mv, lv) 394 } 395 396 mf, err := moduleSchemaDo(func(schemas *schemaInfo) (*File, error) { 397 // Now that we're happy we're within bounds, find the latest 398 // schema that applies to the declared version. 399 latest := "" 400 var latestSchema cue.Value 401 for vers, schema := range schemas.Versions { 402 if semver.Compare(vers, base.Language.Version) > 0 { 403 continue 404 } 405 if latest == "" || semver.Compare(vers, latest) > 0 { 406 latest = vers 407 latestSchema = schema 408 } 409 } 410 if latest == "" { 411 // Should never happen, because there should always 412 // be some applicable schema. 413 return nil, fmt.Errorf("cannot find schema suitable for reading module file with language version %q", base.Language.Version) 414 } 415 schema := latestSchema 416 v = v.Unify(lookup(schema, cue.Def("#File"))) 417 if err := v.Validate(); err != nil { 418 return nil, newCUEError(err, filename) 419 } 420 if latest == "v0.0.0" { 421 // The chosen schema is the earliest schema which allowed 422 // all fields. We don't actually want a module.cue file with 423 // an old version to treat those fields as special, so don't try 424 // to decode into *File because that will do so. 425 // This mirrors the behavior of [ParseLegacy]. 426 var f noDepsFile 427 if err := v.Decode(&f); err != nil { 428 return nil, newCUEError(err, filename) 429 } 430 return &File{ 431 Module: f.Module, 432 actualSchemaVersion: "v0.0.0", 433 }, nil 434 } 435 var mf File 436 if err := v.Decode(&mf); err != nil { 437 return nil, errors.Wrapf(err, token.NoPos, "internal error: cannot decode into modFile struct") 438 } 439 mf.actualSchemaVersion = latest 440 return &mf, nil 441 }) 442 if err != nil { 443 return nil, err 444 } 445 mainPath, mainMajor, ok := ast.SplitPackageVersion(mf.Module) 446 if ok { 447 if semver.Major(mainMajor) != mainMajor { 448 return nil, fmt.Errorf("module path %s in %q should contain the major version only", mf.Module, filename) 449 } 450 } else if mainPath != "" { 451 if err := module.CheckPathWithoutVersion(mainPath); err != nil { 452 return nil, fmt.Errorf("module path %q in %q is not valid: %v", mainPath, filename, err) 453 } 454 // There's no main module major version: default to v0. 455 mainMajor = "v0" 456 } else { 457 return nil, fmt.Errorf("empty module path in %q", filename) 458 } 459 if mf.Language != nil { 460 vers := mf.Language.Version 461 if !semver.IsValid(vers) { 462 return nil, fmt.Errorf("language version %q in %s is not well formed", vers, filename) 463 } 464 if semver.Canonical(vers) != vers { 465 return nil, fmt.Errorf("language version %v in %s is not canonical", vers, filename) 466 } 467 } 468 mf.versionByModule = make(map[string]module.Version) 469 var versions []module.Version 470 defaultMajorVersions := make(map[string]string) 471 if mainPath != "" { 472 // The main module is always the default for its own major version. 473 defaultMajorVersions[mainPath] = mainMajor 474 } 475 // Check that major versions match dependency versions. 476 for m, dep := range mf.Deps { 477 vers, err := module.NewVersion(m, dep.Version) 478 if err != nil { 479 return nil, fmt.Errorf("invalid module.cue file %s: cannot make version from module %q, version %q: %v", filename, m, dep.Version, err) 480 } 481 versions = append(versions, vers) 482 if strict && vers.Path() != m { 483 return nil, fmt.Errorf("invalid module.cue file %s: no major version in %q", filename, m) 484 } 485 if dep.Default { 486 mp := vers.BasePath() 487 if _, ok := defaultMajorVersions[mp]; ok { 488 return nil, fmt.Errorf("multiple default major versions found for %v", mp) 489 } 490 defaultMajorVersions[mp] = semver.Major(vers.Version()) 491 } 492 mf.versionByModule[vers.Path()] = vers 493 } 494 if mainPath != "" { 495 // We don't necessarily have a full version for the main module. 496 mainWithMajor := mainPath + "@" + mainMajor 497 mainVersion, err := module.NewVersion(mainWithMajor, "") 498 if err != nil { 499 return nil, err 500 } 501 mf.versionByModule[mainWithMajor] = mainVersion 502 } 503 if len(defaultMajorVersions) > 0 { 504 mf.defaultMajorVersions = defaultMajorVersions 505 } 506 mf.versions = versions[:len(versions):len(versions)] 507 slices.SortFunc(mf.versions, module.Version.Compare) 508 return mf, nil 509 } 510 511 // ErrNoLanguageVersion is returned by [Parse] and [ParseNonStrict] 512 // when a cue.mod/module.cue file lacks the `language.version` field. 513 var ErrNoLanguageVersion = fmt.Errorf("no language version declared in module.cue") 514 515 func parseDataOnlyCUE(ctx *cue.Context, cueData []byte, filename string) (*ast.File, error) { 516 dec := encoding.NewDecoder(ctx, &build.File{ 517 Filename: filename, 518 Encoding: build.CUE, 519 Interpretation: build.Auto, 520 Form: build.Data, 521 Source: cueData, 522 }, &encoding.Config{ 523 Mode: filetypes.Export, 524 AllErrors: true, 525 }) 526 if err := dec.Err(); err != nil { 527 return nil, err 528 } 529 return dec.File(), nil 530 } 531 532 func newCUEError(err error, filename string) error { 533 ps := errors.Positions(err) 534 for _, p := range ps { 535 if errStr := findErrorComment(p); errStr != "" { 536 return fmt.Errorf("invalid module.cue file: %s", errStr) 537 } 538 } 539 // TODO we have more potential to improve error messages here. 540 return err 541 } 542 543 // findErrorComment finds an error comment in the form 544 // 545 // //error: ... 546 // 547 // before the given position. 548 // This works as a kind of poor-man's error primitive 549 // so we can customize the error strings when verification 550 // fails. 551 func findErrorComment(p token.Pos) string { 552 if p.Filename() != schemaFile { 553 return "" 554 } 555 off := p.Offset() 556 source := moduleSchemaData 557 if off > len(source) { 558 return "" 559 } 560 source, _, ok := cutLast(source[:off], "\n") 561 if !ok { 562 return "" 563 } 564 _, errorLine, ok := cutLast(source, "\n") 565 if !ok { 566 return "" 567 } 568 errStr, ok := strings.CutPrefix(errorLine, "//error: ") 569 if !ok { 570 return "" 571 } 572 return errStr 573 } 574 575 func cutLast(s, sep string) (before, after string, found bool) { 576 if i := strings.LastIndex(s, sep); i >= 0 { 577 return s[:i], s[i+len(sep):], true 578 } 579 return "", s, false 580 } 581 582 // DepVersions returns the versions of all the modules depended on by the 583 // file. The caller should not modify the returned slice. 584 // 585 // This always returns the same value, even if the contents 586 // of f are changed. If f was not created with [Parse], it returns nil. 587 func (f *File) DepVersions() []module.Version { 588 return slices.Clip(f.versions) 589 } 590 591 // DefaultMajorVersions returns a map from module base path 592 // to the major version that's specified as the default for that module. 593 // The caller should not modify the returned map. 594 func (f *File) DefaultMajorVersions() map[string]string { 595 return f.defaultMajorVersions 596 } 597 598 // ModuleForImportPath returns the module that should contain the given 599 // import path and reports whether the module was found. 600 // It does not check to see if the import path actually exists within the module. 601 // 602 // It works entirely from information in f, meaning that it does 603 // not consult a registry to resolve a package whose module is not 604 // mentioned in the file, which means it will not work in general unless 605 // the module is tidy (as with `cue mod tidy`). 606 func (f *File) ModuleForImportPath(importPath string) (module.Version, bool) { 607 ip := ast.ParseImportPath(importPath) 608 for prefix := ip.Path; prefix != "."; prefix = path.Dir(prefix) { 609 pkgVersion := ip.Version 610 if pkgVersion == "" { 611 if pkgVersion = f.defaultMajorVersions[prefix]; pkgVersion == "" { 612 continue 613 } 614 } 615 if mv, ok := f.versionByModule[prefix+"@"+pkgVersion]; ok { 616 return mv, true 617 } 618 } 619 return module.Version{}, false 620 }