this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

cmd/cue: more resilient mod init version

Before now, if there wasn't an available version,
`cue mod init` would create a module file with
no language version. However as the language version
field is now mandatory, this isn't a good approach.

Instead, we sanity check that the version is at least
the minimum supported version and use the fallback
version otherwise.

Technically we could compare with the fallback version
instead, because the actual version should always be at
least that, but tests often use a fixed version that's
not kept up to date, so this approach seems a bit better.

Signed-off-by: Roger Peppe <rogpeppe@gmail.com>
Change-Id: I732c94467285a2c5d502b67c4ba0616e6bb1a53b
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1193331
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>

+83 -36
+28 -9
cmd/cue/cmd/mod.go
··· 21 21 "strings" 22 22 23 23 "github.com/spf13/cobra" 24 + gomodule "golang.org/x/mod/module" 25 + "golang.org/x/mod/semver" 24 26 25 27 "cuelang.org/go/internal/cueexperiment" 26 28 "cuelang.org/go/mod/modfile" 27 29 "cuelang.org/go/mod/module" 28 - gomodule "golang.org/x/mod/module" 29 30 ) 30 31 31 32 func newModCmd(c *Command) *cobra.Command { ··· 119 120 mf := &modfile.File{ 120 121 Module: modulePath, 121 122 } 122 - if vers := versionForModFile(); vers != "" { 123 - mf.Language = &modfile.Language{ 124 - Version: vers, 125 - } 123 + vers := versionForModFile() 124 + if vers == "" { 125 + // Shouldn't happen because we should use the 126 + // fallback version if we can't the version otherwise. 127 + return fmt.Errorf("cannot determine language version for module") 128 + } 129 + mf.Language = &modfile.Language{ 130 + Version: vers, 126 131 } 127 132 128 133 err = os.Mkdir(mod, 0755) ··· 150 155 151 156 func versionForModFile() string { 152 157 version := cueVersion() 158 + earliestPossibleVersion := modfile.EarliestClosedSchemaVersion() 159 + if semver.Compare(version, earliestPossibleVersion) < 0 { 160 + // The reported version is earlier than it should be, 161 + // which can occur for some pseudo versions, or 162 + // potentially the cue command has been forked and 163 + // published under an independent version numbering. 164 + // 165 + // In this case, we use the latest known schema version 166 + // as the best guess as to a version that actually 167 + // reflects the capabilities of the module file. 168 + version = modfile.LatestKnownSchemaVersion() 169 + } 153 170 if gomodule.IsPseudoVersion(version) { 154 171 // If we have a version like v0.7.1-0.20240130142347-7855e15cb701 155 172 // we want it to turn into the base version (v0.7.0 in that example). 156 - // If there's no base version (e.g. v0.0.0-...) then PseudoVersionBase 157 - // will return the empty string, which is exactly what we want 158 - // because we don't want to put v0.0.0 in a module.cue file. 159 - version, _ = gomodule.PseudoVersionBase(version) 173 + // Subject the resulting base version to the same sanity check 174 + // as above. 175 + pv, _ := gomodule.PseudoVersionBase(version) 176 + if pv != "" && semver.Compare(pv, earliestPossibleVersion) >= 0 { 177 + version = pv 178 + } 160 179 } 161 180 return version 162 181 }
+15 -6
cmd/cue/cmd/testdata/script/modinit_without_version.txtar
··· 1 - # Check that cue mod init fails when it lacks any version 2 - # information at all. 1 + # Check that cue mod init uses the latest schema 2 + # version when it lacks any version information at all. 3 3 # A zero pseudo-version is one such case, as there are no semver numbers. 4 4 env CUE_EXPERIMENT=modules 5 5 env CUE_VERSION_OVERRIDE=v0.0.0-00010101000000-000000000000 6 - ! exec cue mod init foo.example 7 - cmp stderr want-stderr 6 + exec cue mod init foo.example 7 + cmp cue.mod/module.cue want-module 8 + 9 + 10 + # cue mod tidy should be a no-op after cue mod init 11 + env CUE_CACHE_DIR=$WORK/.tmp/cache 12 + exec cue mod tidy 13 + cmp cue.mod/module.cue want-module 8 14 9 - -- want-stderr -- 10 - cannot round-trip module file: no language version declared in module.cue 15 + -- want-module -- 16 + module: "foo.example@v0" 17 + language: { 18 + version: "v0.9.0-alpha.0" 19 + }
+30 -6
mod/modfile/modfile.go
··· 112 112 // it's almost certainly a bogus version because all versions 113 113 // we care about fail when there are unknown fields, but the 114 114 // original schema allowed all fields. 115 - return nil, fmt.Errorf("language version %v is too early for module.cue (need at least %v)", f.Language.Version, earliestClosedSchemaVersion()) 115 + return nil, fmt.Errorf("language version %v is too early for module.cue (need at least %v)", f.Language.Version, EarliestClosedSchemaVersion()) 116 116 } 117 117 return data, err 118 118 } ··· 160 160 return v.LookupPath(cue.MakePath(sels...)) 161 161 } 162 162 163 - func earliestClosedSchemaVersion() string { 164 - v, _ := moduleSchemaDo(func(ctx *cue.Context, info *schemaInfo) (string, error) { 165 - return info.EarliestClosedSchemaVersion, nil 166 - }) 167 - return v 163 + // EarliestClosedSchemaVersion returns the earliest module.cue schema version 164 + // that excludes unknown fields. Any version declared in a module.cue file 165 + // should be at least this, because that's when we added the language.version 166 + // field itself. 167 + func EarliestClosedSchemaVersion() string { 168 + return schemaVersionLimits()[0] 169 + } 170 + 171 + // LatestKnownSchemaVersion returns the language version 172 + // associated with the most recent known schema. 173 + func LatestKnownSchemaVersion() string { 174 + return schemaVersionLimits()[1] 168 175 } 176 + 177 + var schemaVersionLimits = sync.OnceValue(func() [2]string { 178 + limits, _ := moduleSchemaDo(func(_ *cue.Context, info *schemaInfo) ([2]string, error) { 179 + earliest := "" 180 + latest := "" 181 + for v := range info.Versions { 182 + if earliest == "" || semver.Compare(v, earliest) < 0 { 183 + earliest = v 184 + } 185 + if latest == "" || semver.Compare(v, latest) > 0 { 186 + latest = v 187 + } 188 + } 189 + return [2]string{earliest, latest}, nil 190 + }) 191 + return limits 192 + }) 169 193 170 194 // Parse verifies that the module file has correct syntax. 171 195 // The file name is used for error messages.
+9 -1
mod/modfile/modfile_test.go
··· 355 355 Version: "v0.4.3", 356 356 }, 357 357 }, 358 - wantError: `language version v0.4.3 is too early for module.cue \(need at least v0.8.0\)`, 358 + wantError: `cannot round-trip module file: cannot find schema suitable for reading module file with language version "v0.4.3"`, 359 359 }, { 360 360 name: "WithInvalidModuleVersion", 361 361 file: &File{ ··· 394 394 qt.Assert(t, qt.IsNil(err)) 395 395 qt.Assert(t, qt.CmpEquals(f, test.file, cmpopts.IgnoreUnexported(File{}), cmpopts.EquateEmpty())) 396 396 }) 397 + } 398 + 399 + func TestEarliestClosedSchemaVersion(t *testing.T) { 400 + qt.Assert(t, qt.Equals(EarliestClosedSchemaVersion(), "v0.8.0")) 401 + } 402 + 403 + func TestLatestKnownSchemaVersion(t *testing.T) { 404 + qt.Assert(t, qt.Equals(LatestKnownSchemaVersion(), "v0.9.0-alpha.0")) 397 405 } 398 406 399 407 func parseVersions(vs ...string) []module.Version {
+1 -14
mod/modfile/schema.cue
··· 43 43 #Strict!: _ 44 44 } 45 45 46 - versions: "v0.0.0": { 47 - // Historically all fields were allowed. 48 - #File: { 49 - module?: string 50 - ... 51 - } 52 - #Strict: #File 53 - } 54 - 55 - // earliestClosedSchemaVersion holds the earliest module.cue schema version 56 - // that excludes unknown fields. 57 - earliestClosedSchemaVersion: "v0.8.0" 58 - 59 - versions: (earliestClosedSchemaVersion): { 46 + versions: "v0.8.0": { 60 47 // Define this version in terms of the later versions 61 48 // rather than the other way around, so that 62 49 // the latest version is clearest.