Skip to content

Commit

Permalink
internal/cueversion: split language and module versions
Browse files Browse the repository at this point in the history
This means that the CUE logic will not predicate language semantics on a
relatively volatile build-info-derived version.

Instead the semantics are predicated on a simple compiled-in string and
the original logic is used for version information that's displayed as
part of the `cue version` command and forms part of the User-Agent HTTP
header.

We also fix the module version logic so that it works when CUE is used
as a dependency as well as a main module.

Note that writing a local regression test for this is hard, as
dependency modules only have versions as provided by a proxy, and deps
replaced by a directory don't have module versions.

Fixes #3061

Signed-off-by: Roger Peppe <rogpeppe@gmail.com>
Change-Id: I55bc931590974a5a50b0d82d78cba0a64efbd0f6
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1194044
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>
  • Loading branch information
rogpeppe committed Apr 30, 2024
1 parent d696e44 commit 2f90f54
Show file tree
Hide file tree
Showing 15 changed files with 88 additions and 91 deletions.
11 changes: 3 additions & 8 deletions cmd/cue/cmd/modinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"golang.org/x/mod/semver"

"cuelang.org/go/internal/cueexperiment"
"cuelang.org/go/internal/cueversion"
"cuelang.org/go/mod/modfile"
"cuelang.org/go/mod/module"
)
Expand Down Expand Up @@ -95,14 +96,8 @@ func runModInit(cmd *Command, args []string) (err error) {
return err
}
}
vers := versionForModFile()
if vers == "" {
// Shouldn't happen because we should use the
// fallback version if we can't the version otherwise.
return fmt.Errorf("cannot determine language version for module")
}
mf.Language = &modfile.Language{
Version: vers,
Version: cueversion.LanguageVersion(),
}

err = os.Mkdir(mod, 0755)
Expand All @@ -129,7 +124,7 @@ func runModInit(cmd *Command, args []string) (err error) {
}

func versionForModFile() string {
version := cueVersion()
version := cueversion.LanguageVersion()
earliestPossibleVersion := modfile.EarliestClosedSchemaVersion()
if semver.Compare(version, earliestPossibleVersion) < 0 {
// The reported version is earlier than it should be,
Expand Down
3 changes: 2 additions & 1 deletion cmd/cue/cmd/modtidy.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/spf13/cobra"

"cuelang.org/go/internal/cueversion"
"cuelang.org/go/internal/mod/modload"
)

Expand Down Expand Up @@ -71,7 +72,7 @@ func runModTidy(cmd *Command, args []string) error {
if flagCheck.Bool(cmd) {
return modload.CheckTidy(ctx, os.DirFS(modRoot), ".", reg)
}
mf, err := modload.Tidy(ctx, os.DirFS(modRoot), ".", reg, versionForModFile())
mf, err := modload.Tidy(ctx, os.DirFS(modRoot), ".", reg, cueversion.LanguageVersion())
if err != nil {
return err
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/cue/cmd/script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import (
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/parser"
"cuelang.org/go/internal/cuetest"
"cuelang.org/go/internal/cueversion"
"cuelang.org/go/internal/registrytest"
)

Expand Down Expand Up @@ -232,6 +233,7 @@ func TestScript(t *testing.T) {
e.Vars = append(e.Vars,
"GOPROXY="+srv.URL,
"GONOSUMDB=*", // GOPROXY is a private proxy
"CUE_LANGUAGE_VERSION="+cueversion.LanguageVersion(),
)
entries, err := os.ReadDir(e.WorkDir)
if err != nil {
Expand Down
7 changes: 4 additions & 3 deletions cmd/cue/cmd/testdata/script/modinit_majorversion.txtar
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
env CUE_VERSION_OVERRIDE=v0.8.100
env-fill want-module.cue
env-fill want-module-experiment.cue

# Without the experiment, the major version is allowed,
# even though it's not particularly useful.
Expand All @@ -19,10 +20,10 @@ exists cue.mod/pkg
-- want-module.cue --
module: "foo.com/bar@v1"
language: {
version: "v0.8.100"
version: "$CUE_LANGUAGE_VERSION"
}
-- want-module-experiment.cue --
module: "foo.com/bar@v1"
language: {
version: "v0.8.100"
version: "$CUE_LANGUAGE_VERSION"
}
4 changes: 2 additions & 2 deletions cmd/cue/cmd/testdata/script/modinit_noargs.txtar
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# TODO we might want to deprecate or remove the ability to create
# module file with an empty module directive.
env CUE_VERSION_OVERRIDE=v0.8.100
exec cue mod init
env-fill want-module.cue
cmp cue.mod/module.cue want-module.cue
exists cue.mod/usr
exists cue.mod/pkg

-- want-module.cue --
module: ""
language: {
version: "v0.8.100"
version: "$CUE_LANGUAGE_VERSION"
}
7 changes: 4 additions & 3 deletions cmd/cue/cmd/testdata/script/modinit_nomajorversion.txtar
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
env CUE_VERSION_OVERRIDE=v0.8.0
env-fill want-module.cue
env-fill want-module-experiment.cue

# Without the experiment, we use the module path as-is.
exec cue mod init foo.com/bar
Expand All @@ -17,10 +18,10 @@ exists cue.mod/pkg
-- want-module.cue --
module: "foo.com/bar"
language: {
version: "v0.8.0"
version: "$CUE_LANGUAGE_VERSION"
}
-- want-module-experiment.cue --
module: "foo.com/bar@v0"
language: {
version: "v0.8.0"
version: "$CUE_LANGUAGE_VERSION"
}
28 changes: 0 additions & 28 deletions cmd/cue/cmd/testdata/script/modinit_with_pseudoversion.txtar

This file was deleted.

7 changes: 3 additions & 4 deletions cmd/cue/cmd/testdata/script/modinit_without_version.txtar
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# Check that cue mod init uses the latest schema
# version when it lacks any version information at all.
# A zero pseudo-version is one such case, as there are no semver numbers.
# Check that cue mod init is independent of the module version.
env CUE_EXPERIMENT=modules
env CUE_VERSION_OVERRIDE=v0.0.0-00010101000000-000000000000
env-fill want-module
exec cue mod init foo.example
cmp cue.mod/module.cue want-module

Expand All @@ -15,5 +14,5 @@ cmp cue.mod/module.cue want-module
-- want-module --
module: "foo.example@v0"
language: {
version: "v0.9.0-alpha.0"
version: "$CUE_LANGUAGE_VERSION"
}
7 changes: 4 additions & 3 deletions cmd/cue/cmd/testdata/script/modinit_withsource.txtar
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Test that we can use the --source flag to cue mod init.

env CUE_VERSION_OVERRIDE=v0.9.0-alpha.2
env-fill $WORK/want-module.cue-0
env-fill $WORK/want-module.cue-1

mkdir $WORK/test0
cd $WORK/test0
Expand All @@ -20,15 +21,15 @@ cmp stderr $WORK/want-stderr
-- want-module.cue-0 --
module: "test.example"
language: {
version: "v0.9.0-alpha.2"
version: "$CUE_LANGUAGE_VERSION"
}
source: {
kind: "self"
}
-- want-module.cue-1 --
module: "test.example"
language: {
version: "v0.9.0-alpha.2"
version: "$CUE_LANGUAGE_VERSION"
}
source: {
kind: "git"
Expand Down
2 changes: 1 addition & 1 deletion cmd/cue/cmd/testdata/script/modtidy_logging.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ env CUE_DEBUG=http
# Smoke test that the logging looks reasonable. The actual log messages
# are tested more fully in the httplog package itself.
exec cue mod tidy
stderr '{"time":".*","level":"INFO","msg":"http client->","info":{"id":[0-9]+,"method":"GET","url":"http://[^/]+/v2/example.com/tags/list\?n=\d+","contentLength":0,"header":{"User-Agent":\["Cue/.* \(cmd/cue\) Go/[^ ]+ \(.*\)"\]}}}'
stderr '{"time":".*","level":"INFO","msg":"http client->","info":{"id":[0-9]+,"method":"GET","url":"http://[^/]+/v2/example.com/tags/list\?n=\d+","contentLength":0,"header":{"User-Agent":\["Cue/[^ ]+ \(cmd/cue; .*\) Go/[^ ]+ \(.*\)"\]}}}'

# Check that the resulting module evaluates as expected.
exec cue export .
Expand Down
5 changes: 3 additions & 2 deletions cmd/cue/cmd/testdata/script/version.txtar
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# Version output without VCS information.
exec cue version
stdout -count=1 '^cue version v0.[1-9]'
stdout -count=1 '^cue version .+'
stdout -count=1 '^go version (devel )?go1.'

# Version output with VCS information.
env CUE_VERSION_TEST_CFG='[{"Key":"vcs","Value":"git"},{"Key":"vcs.revision","Value":"47b7032385cb490fab7d47b89fca36835cf13d39"},{"Key":"vcs.time","Value":"2022-05-10T04:58:46Z"},{"Key":"vcs.modified","Value":"true"}]'
exec cue version
stdout -count=1 '^cue version v0.[1-9]'
stdout -count=1 '^cue version .+'
stdout -count=1 '^go version (devel )?go1.'
stdout -count=1 'vcs git'
stdout -count=1 'vcs\.revision 47b7032385cb490fab7d47b89fca36835cf13d39'
stdout -count=1 'vcs\.time 2022-05-10T04:58:46Z'
stdout -count=1 'vcs\.modified true'
stdout -count=1 'cue\.lang\.version '${CUE_LANGUAGE_VERSION@R}'$'
15 changes: 10 additions & 5 deletions cmd/cue/cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@ func runVersion(cmd *Command, args []string) error {
// shouldn't happen
return errors.New("unknown error reading build-info")
}
fmt.Fprintf(w, "cue version %s\n\n", cueVersion())
fmt.Fprintf(w, "cue version %s\n\n", moduleVersion())
fmt.Fprintf(w, "go version %s\n", runtime.Version())
bi.Settings = append(bi.Settings, debug.BuildSetting{
Key: "cue.lang.version",
Value: cueversion.LanguageVersion(),
})
for _, s := range bi.Settings {
if s.Value == "" {
// skip empty build settings
Expand All @@ -68,16 +72,17 @@ func runVersion(cmd *Command, args []string) error {
// veryverylong.key value
// short.key some-other-value
//
// Empirically, 16 is enough; the longest key seen is "vcs.revision".
// Empirically, 16 is enough; the longest key seen outside our own "cue.lang.version"
// is "vcs.revision".
fmt.Fprintf(w, "%16s %s\n", s.Key, s.Value)
}
return nil
}

// cueVersion returns the version of the CUE module as much
// moduleVersion returns the version of the main module as much
// as can reasonably be determined. If no version can be
// determined, it returns the empty string.
func cueVersion() string {
func moduleVersion() string {
if testing.Testing() {
if v := os.Getenv("CUE_VERSION_OVERRIDE"); v != "" {
return v
Expand All @@ -87,7 +92,7 @@ func cueVersion() string {
// The global version variable has been configured via ldflags.
return v
}
return cueversion.Version()
return cueversion.ModuleVersion()
}

func readBuildInfo() (*debug.BuildInfo, bool) {
Expand Down
66 changes: 43 additions & 23 deletions internal/cueversion/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,61 @@ import (
"sync"
)

// fallbackVersion is used when there isn't a recorded main module version,
// for example when building via `go install ./cmd/cue`.
// It should reflect the last release in the current branch.
//
// TODO: remove once Go stamps local builds with a main module version
// derived from the local VCS information per https://go.dev/issue/50603.
const fallbackVersion = "v0.9.0-alpha.3"

// Version returns the version of the cuelang.org/go module as best as can
// reasonably be determined. The result is always a valid Go semver version.
func Version() string {
return versionOnce()
// LanguageVersion returns the CUE language version.
// This determines the latest version of CUE that
// is accepted by the module.
func LanguageVersion() string {
return "v0.9.0"
}

// ModuleVersion returns the version of the cuelang.org/go module as best as can
// reasonably be determined. This is provided for informational
// and debugging purposes and should not be used to predicate
// version-specific behavior.
func ModuleVersion() string {
return moduleVersionOnce()
}

var versionOnce = sync.OnceValue(func() string {
const cueModule = "cuelang.org/go"

var moduleVersionOnce = sync.OnceValue(func() string {
bi, ok := debug.ReadBuildInfo()
if !ok {
return fallbackVersion
// This might happen if the binary was not built with module support
// or with an alternative toolchain.
return "(no-build-info)"
}
switch bi.Main.Version {
case "": // missing version
case "(devel)": // local build
case "v0.0.0-00010101000000-000000000000": // build via a directory replace directive
default:
return bi.Main.Version
cueMod := findCUEModule(bi)
if cueMod == nil {
// Could happen if someone has forked CUE under a different
// module name; it also happens when running the cue tests.
return "(no-cue-module)"
}
return fallbackVersion
return cueMod.Version
})

func findCUEModule(bi *debug.BuildInfo) *debug.Module {
if bi.Main.Path == cueModule {
return &bi.Main
}
for _, m := range bi.Deps {
if m.Replace != nil && m.Replace.Path == cueModule {
return m.Replace
}
if m.Path == cueModule {
return m
}
}
return nil
}

// UserAgent returns a string suitable for adding as the User-Agent
// header in an HTTP agent. The clientType argument specifies
// how CUE is being used: if this is empty it defaults to "cuelang.org/go".
//
// Example:
//
// Cue/v0.8.0 (cuelang.org/go) Go/go1.22.0 (linux/amd64)
// Cue/v0.8.0 (cuelang.org/go; vxXXX) Go/go1.22.0 (linux/amd64)
func UserAgent(clientType string) string {
if clientType == "" {
clientType = "cuelang.org/go"
Expand All @@ -55,5 +74,6 @@ func UserAgent(clientType string) string {
// As the runtime version won't contain underscores itself, this
// is reversible.
goVersion := strings.ReplaceAll(runtime.Version(), " ", "_")
return fmt.Sprintf("Cue/%s (%s) Go/%s (%s/%s)", Version(), clientType, goVersion, runtime.GOOS, runtime.GOARCH)

return fmt.Sprintf("Cue/%s (%s; lang %s) Go/%s (%s/%s)", ModuleVersion(), clientType, LanguageVersion(), goVersion, runtime.GOOS, runtime.GOARCH)
}
11 changes: 5 additions & 6 deletions internal/cueversion/version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,23 @@ import (
"testing"

"github.com/go-quicktest/qt"
"golang.org/x/mod/semver"
)

func TestVersion(t *testing.T) {
func TestModuleVersion(t *testing.T) {
// This is just a smoke test to make sure that things
// are wired up OK. It would be possible to unit
// test the logic inside Version, but it's simple
// enough that that would amount to creating invariants
// that just match the code, not providing any more
// assurance of correctness.
vers := Version()
qt.Assert(t, qt.Satisfies(vers, semver.IsValid))
vers := ModuleVersion()
qt.Assert(t, qt.Not(qt.Equals(vers, "")))
}

func TestUserAgent(t *testing.T) {
agent := UserAgent("custom")
qt.Assert(t, qt.Matches(agent,
`Cue/v[^ ]+ \(custom\) Go/[^ ]+ \([^/]+/[^/]+\)`,
`Cue/[^ ]+ \(custom; lang v[^)]+\) Go/[^ ]+ \([^/]+/[^/]+\)`,
))
}

Expand All @@ -41,5 +40,5 @@ func TestTransport(t *testing.T) {
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.Matches(string(data), `Cue/v[^ ]+ \(foo\) Go/[^ ]+ \([^/]+/[^/]+\)`))
qt.Assert(t, qt.Matches(string(data), `Cue/[^ ]+ \(foo; lang v[^)]+\) Go/[^ ]+ \([^/]+/[^/]+\)`))
}

0 comments on commit 2f90f54

Please sign in to comment.