1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
42 var moduleSchemaData []byte
43
44
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
51
52 defaultMajorVersions map[string]string
53 }
54
55
56
57 func (f *File) Format() ([]byte, error) {
58 if len(f.Deps) == 0 && f.Deps != nil {
59
60
61
62
63 f1 := *f
64 f1.Deps = nil
65 f = &f1
66 }
67
68
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
82
83
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
105
106 moduleSchemaMutex sync.Mutex
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
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
131
132
133
134
135 func Parse(modfile []byte, filename string) (*File, error) {
136 return parse(modfile, filename, true)
137 }
138
139
140
141
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
159
160
161
162
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
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
205 mainMajor = "v0"
206
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
220 defaultMajorVersions := map[string]string{
221 mainPath: mainMajor,
222 }
223
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
252 return err
253 }
254
255
256
257
258
259
260 func (f *File) DepVersions() []module.Version {
261 return slices.Clip(f.versions)
262 }
263
264
265
266
267 func (f *File) DefaultMajorVersions() map[string]string {
268 return f.defaultMajorVersions
269 }
270
View as plain text