...

Source file src/cuelang.org/go/mod/modfile/modfile.go

Documentation: cuelang.org/go/mod/modfile

     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/cuecontext"
    34  	"cuelang.org/go/cue/errors"
    35  	"cuelang.org/go/cue/format"
    36  	"cuelang.org/go/cue/parser"
    37  	"cuelang.org/go/cue/token"
    38  	"cuelang.org/go/mod/module"
    39  )
    40  
    41  //go:embed schema.cue
    42  var moduleSchemaData []byte
    43  
    44  // File represents the contents of a cue.mod/module.cue file.
    45  type File struct {
    46  	Module   string          `json:"module"`
    47  	Language *Language       `json:"language,omitempty"`
    48  	Deps     map[string]*Dep `json:"deps,omitempty"`
    49  	versions []module.Version
    50  	// defaultMajorVersions maps from module base path to the
    51  	// major version default for that path.
    52  	defaultMajorVersions map[string]string
    53  }
    54  
    55  // Format returns a formatted representation of f
    56  // in CUE syntax.
    57  func (f *File) Format() ([]byte, error) {
    58  	if len(f.Deps) == 0 && f.Deps != nil {
    59  		// There's no way to get the CUE encoder to omit an empty
    60  		// but non-nil slice (despite the current doc comment on
    61  		// [cue.Context.Encode], so make a copy of f to allow us
    62  		// to do that.
    63  		f1 := *f
    64  		f1.Deps = nil
    65  		f = &f1
    66  	}
    67  	// TODO this could be better:
    68  	// - it should omit the outer braces
    69  	v := cuecontext.New().Encode(f)
    70  	if err := v.Validate(cue.Concrete(true)); err != nil {
    71  		return nil, err
    72  	}
    73  	n := v.Syntax(cue.Concrete(true)).(*ast.StructLit)
    74  
    75  	data, err := format.Node(&ast.File{
    76  		Decls: n.Elts,
    77  	})
    78  	if err != nil {
    79  		return nil, fmt.Errorf("cannot format: %v", err)
    80  	}
    81  	// Sanity check that it can be parsed.
    82  	// TODO this could be more efficient by checking all the file fields
    83  	// before formatting the output.
    84  	if _, err := ParseNonStrict(data, "-"); err != nil {
    85  		return nil, fmt.Errorf("cannot round-trip module file: %v", strings.TrimSuffix(errors.Details(err, nil), "\n"))
    86  	}
    87  	return data, err
    88  }
    89  
    90  type Language struct {
    91  	Version string `json:"version,omitempty"`
    92  }
    93  
    94  type Dep struct {
    95  	Version string `json:"v"`
    96  	Default bool   `json:"default,omitempty"`
    97  }
    98  
    99  type noDepsFile struct {
   100  	Module string `json:"module"`
   101  }
   102  
   103  var (
   104  	moduleSchemaOnce sync.Once // guards the creation of _moduleSchema
   105  	// TODO remove this mutex when https://cuelang.org/issue/2733 is fixed.
   106  	moduleSchemaMutex sync.Mutex // guards any use of _moduleSchema
   107  	_moduleSchema     cue.Value
   108  )
   109  
   110  func moduleSchemaDo[T any](f func(moduleSchema cue.Value) (T, error)) (T, error) {
   111  	moduleSchemaOnce.Do(func() {
   112  		ctx := cuecontext.New()
   113  		schemav := ctx.CompileBytes(moduleSchemaData, cue.Filename("cuelang.org/go/mod/modfile/schema.cue"))
   114  		schemav = lookup(schemav, cue.Def("#File"))
   115  		//schemav = schemav.Unify(lookup(schemav, cue.Hid("#Strict", "_")))
   116  		if err := schemav.Validate(); err != nil {
   117  			panic(fmt.Errorf("internal error: invalid CUE module.cue schema: %v", errors.Details(err, nil)))
   118  		}
   119  		_moduleSchema = schemav
   120  	})
   121  	moduleSchemaMutex.Lock()
   122  	defer moduleSchemaMutex.Unlock()
   123  	return f(_moduleSchema)
   124  }
   125  
   126  func lookup(v cue.Value, sels ...cue.Selector) cue.Value {
   127  	return v.LookupPath(cue.MakePath(sels...))
   128  }
   129  
   130  // Parse verifies that the module file has correct syntax.
   131  // The file name is used for error messages.
   132  // All dependencies must be specified correctly: with major
   133  // versions in the module paths and canonical dependency
   134  // versions.
   135  func Parse(modfile []byte, filename string) (*File, error) {
   136  	return parse(modfile, filename, true)
   137  }
   138  
   139  // ParseLegacy parses the legacy version of the module file
   140  // that only supports the single field "module" and ignores all other
   141  // fields.
   142  func ParseLegacy(modfile []byte, filename string) (*File, error) {
   143  	return moduleSchemaDo(func(schema cue.Value) (*File, error) {
   144  		v := schema.Context().CompileBytes(modfile, cue.Filename(filename))
   145  		if err := v.Err(); err != nil {
   146  			return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file")
   147  		}
   148  		var f noDepsFile
   149  		if err := v.Decode(&f); err != nil {
   150  			return nil, newCUEError(err, filename)
   151  		}
   152  		return &File{
   153  			Module: f.Module,
   154  		}, nil
   155  	})
   156  }
   157  
   158  // ParseNonStrict is like Parse but allows some laxity in the parsing:
   159  //   - if a module path lacks a version, it's taken from the version.
   160  //   - if a non-canonical version is used, it will be canonicalized.
   161  //
   162  // The file name is used for error messages.
   163  func ParseNonStrict(modfile []byte, filename string) (*File, error) {
   164  	return parse(modfile, filename, false)
   165  }
   166  
   167  func parse(modfile []byte, filename string, strict bool) (*File, error) {
   168  	file, err := parser.ParseFile(filename, modfile)
   169  	if err != nil {
   170  		return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file syntax")
   171  	}
   172  	// TODO disallow non-data-mode CUE.
   173  
   174  	mf, err := moduleSchemaDo(func(schema cue.Value) (*File, error) {
   175  		v := schema.Context().BuildFile(file)
   176  		if err := v.Validate(cue.Concrete(true)); err != nil {
   177  			return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file value")
   178  		}
   179  		v = v.Unify(schema)
   180  		if err := v.Validate(); err != nil {
   181  			return nil, newCUEError(err, filename)
   182  		}
   183  		var mf File
   184  		if err := v.Decode(&mf); err != nil {
   185  			return nil, errors.Wrapf(err, token.NoPos, "internal error: cannot decode into modFile struct")
   186  		}
   187  		return &mf, nil
   188  	})
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  	mainPath, mainMajor, ok := module.SplitPathVersion(mf.Module)
   193  	if strict && !ok {
   194  		return nil, fmt.Errorf("module path %q in %s does not contain major version", mf.Module, filename)
   195  	}
   196  	if ok {
   197  		if semver.Major(mainMajor) != mainMajor {
   198  			return nil, fmt.Errorf("module path %s in %q should contain the major version only", mf.Module, filename)
   199  		}
   200  	} else if mainPath = mf.Module; mainPath != "" {
   201  		if err := module.CheckPathWithoutVersion(mainPath); err != nil {
   202  			return nil, fmt.Errorf("module path %q in %q is not valid: %v", mainPath, filename, err)
   203  		}
   204  		// There's no main module major version: default to v0.
   205  		mainMajor = "v0"
   206  		// TODO perhaps we'd be better preserving the original?
   207  		mf.Module += "@v0"
   208  	}
   209  	if mf.Language != nil {
   210  		vers := mf.Language.Version
   211  		if !semver.IsValid(vers) {
   212  			return nil, fmt.Errorf("language version %q in %s is not well formed", vers, filename)
   213  		}
   214  		if semver.Canonical(vers) != vers {
   215  			return nil, fmt.Errorf("language version %v in %s is not canonical", vers, filename)
   216  		}
   217  	}
   218  	var versions []module.Version
   219  	// The main module is always the default for its own major version.
   220  	defaultMajorVersions := map[string]string{
   221  		mainPath: mainMajor,
   222  	}
   223  	// Check that major versions match dependency versions.
   224  	for m, dep := range mf.Deps {
   225  		vers, err := module.NewVersion(m, dep.Version)
   226  		if err != nil {
   227  			return nil, fmt.Errorf("invalid module.cue file %s: cannot make version from module %q, version %q: %v", filename, m, dep.Version, err)
   228  		}
   229  		versions = append(versions, vers)
   230  		if strict && vers.Path() != m {
   231  			return nil, fmt.Errorf("invalid module.cue file %s: no major version in %q", filename, m)
   232  		}
   233  		if dep.Default {
   234  			mp := vers.BasePath()
   235  			if _, ok := defaultMajorVersions[mp]; ok {
   236  				return nil, fmt.Errorf("multiple default major versions found for %v", mp)
   237  			}
   238  			defaultMajorVersions[mp] = semver.Major(vers.Version())
   239  		}
   240  	}
   241  
   242  	if len(defaultMajorVersions) > 0 {
   243  		mf.defaultMajorVersions = defaultMajorVersions
   244  	}
   245  	mf.versions = versions[:len(versions):len(versions)]
   246  	module.Sort(mf.versions)
   247  	return mf, nil
   248  }
   249  
   250  func newCUEError(err error, filename string) error {
   251  	// TODO we have some potential to improve error messages here.
   252  	return err
   253  }
   254  
   255  // DepVersions returns the versions of all the modules depended on by the
   256  // file. The caller should not modify the returned slice.
   257  //
   258  // This always returns the same value, even if the contents
   259  // of f are changed. If f was not created with [Parse], it returns nil.
   260  func (f *File) DepVersions() []module.Version {
   261  	return slices.Clip(f.versions)
   262  }
   263  
   264  // DefaultMajorVersions returns a map from module base path
   265  // to the major version that's specified as the default for that module.
   266  // The caller should not modify the returned map.
   267  func (f *File) DefaultMajorVersions() map[string]string {
   268  	return f.defaultMajorVersions
   269  }
   270  

View as plain text