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