Skip to content

Commit

Permalink
implement module compatibility check
Browse files Browse the repository at this point in the history
This package imports all "importable" packages, i.e., packages that:

- are not applications ("main")
- are not internal
- and that have non-test go-files

We do this to verify that our code can be consumed as a dependency
in "module mode". When using a dependency that does not have a go.mod
(i.e.; is not a "module"), go implicitly generates a go.mod. Lacking
information from the dependency itself, it assumes "go1.16" language
(see [DefaultGoModVersion]). Starting with Go1.21, go downgrades the
language version used for such dependencies, which means that any
language feature used that is not supported by go1.16 results in a
compile error;

    # github.com/docker/cli/cli/context/store
    /go/pkg/mod/github.com/docker/[email protected]+incompatible/cli/context/store/storeconfig.go:6:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    /go/pkg/mod/github.com/docker/[email protected]+incompatible/cli/context/store/store.go:74:12: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)

These errors do NOT occur when using GOPATH mode, nor do they occur
when using "pseudo module mode" (the "-mod=mod -modfile=vendor.mod"
approach used in this repository).

As a workaround for this situation, we must include "//go:build" comments
in any file that uses newer go-language features (such as the "any" type
or the "min()", "max()" builtins).

From the go toolchain docs (https://go.dev/doc/toolchain):

> The go line for each module sets the language version the compiler enforces
> when compiling packages in that module. The language version can be changed
> on a per-file basis by using a build constraint.
>
> For example, a module containing code that uses the Go 1.21 language version
> should have a go.mod file with a go line such as go 1.21 or go 1.21.3.
> If a specific source file should be compiled only when using a newer Go
> toolchain, adding //go:build go1.22 to that source file both ensures that
> only Go 1.22 and newer toolchains will compile the file and also changes
> the language version in that file to Go 1.22.

This file is a generated module that imports all packages provided in
the repository, which replicates an external consumer using our code
as a dependency in go-module mode, and verifies all files in those
packages have the correct "//go:build <go language version>" set.

To test this package:

    make shell
    make -C ./internal/gocompat/
    make: Entering directory '/go/src/github.com/docker/cli/internal/gocompat'
    GO111MODULE=off go generate .
    GO111MODULE=on go mod tidy
    GO111MODULE=on go test -v
    # github.com/docker/cli/templates
    ../../templates/templates.go:13:17: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    # github.com/docker/cli/cli/compose/template
    ../../cli/compose/template/template.go:98:45: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/template/template.go:105:27: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/template/template.go:141:28: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    # github.com/docker/cli/cli/compose/types
    ../../cli/compose/types/types.go:53:22: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:86:34: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:105:22: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:137:34: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:211:20: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:343:35: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:442:40: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:469:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:490:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:587:28: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:442:40: too many errors
    # github.com/docker/cli/cli/context/store
    ../../cli/context/store/storeconfig.go:6:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/context/store/store.go:74:12: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/context/store/store.go:75:23: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/context/store/metadatastore.go:43:58: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/context/store/metadatastore.go:48:22: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/context/store/metadatastore.go:80:30: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    # github.com/docker/cli/cli/command/idresolver
    ../../cli/command/idresolver/idresolver.go:6:2: "github.com/docker/docker/api/types" imported and not used
    ../../cli/command/idresolver/idresolver.go:7:2: "github.com/docker/docker/api/types/swarm" imported and not used
    ../../cli/command/idresolver/idresolver.go:9:2: "github.com/pkg/errors" imported and not used
    ../../cli/command/idresolver/idresolver.go:28:49: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/command/idresolver/idresolver.go:58:53: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    # github.com/docker/cli/cli/compose/schema
    ../../cli/compose/schema/schema.go:20:46: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/schema/schema.go:27:53: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/schema/schema.go:45:32: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/schema/schema.go:66:33: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    FAIL	gocompat [build failed]
    make: *** [Makefile:3: verify] Error 1
    make: Leaving directory '/go/src/github.com/docker/cli/internal/gocompat'

[DefaultGoModVersion]: https://github.com/golang/go/blob/58c28ba286dd0e98fe4cca80f5d64bbcb824a685/src/cmd/go/internal/gover/version.go#L15-L24
[2]: https://go.dev/doc/toolchain

Signed-off-by: Sebastiaan van Stijn <[email protected]>
  • Loading branch information
thaJeztah committed Dec 15, 2023
1 parent f1f4779 commit 2fc96b2
Show file tree
Hide file tree
Showing 14 changed files with 4,482 additions and 1 deletion.
4 changes: 4 additions & 0 deletions internal/gocompat/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
go.mod
go.sum
main.go
main_test.go
12 changes: 12 additions & 0 deletions internal/gocompat/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.PHONY: verify
verify: generate
GO111MODULE=on go test -v

.PHONY: generate
generate: clean
GO111MODULE=off go generate .
GO111MODULE=on go mod tidy

.PHONY: clean
clean:
@rm -f go.mod go.sum main.go main_test.go
6 changes: 6 additions & 0 deletions internal/gocompat/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package main

//go:generate go run modulegenerator.go

// make sure the modfile package is vendored.
import _ "golang.org/x/mod/modfile"
193 changes: 193 additions & 0 deletions internal/gocompat/modulegenerator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
//go:build ignore

package main

import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"strings"
"text/template"

"golang.org/x/mod/modfile"
)

func main() {
if err := generateApp(); err != nil {
log.Fatal(err)
}
if err := generateModule(); err != nil {
log.Fatal(err)
}
}

func generateApp() error {
cmd := exec.Command("go", "list", "-find", "-f", `{{- if ne .Name "main"}}{{if .GoFiles}}{{.ImportPath}}{{end}}{{end -}}`, "../../...")
out, err := cmd.CombinedOutput()
if err != nil {
return err
}

var pkgs []string
for _, p := range strings.Split(string(out), "\n") {
if strings.TrimSpace(p) == "" || strings.Contains(p, "/internal") {
continue
}
pkgs = append(pkgs, p)
}
tmpl, err := template.New("main").Parse(appTemplate)
if err != nil {
return err
}

var buf bytes.Buffer
err = tmpl.Execute(&buf, appContext{Generator: cmd.String(), Packages: pkgs})
if err != nil {
return err
}

return os.WriteFile("main_test.go", buf.Bytes(), 0o644)
}

func generateModule() error {
content, err := os.ReadFile("../../go.mod")
if err != nil {
if !os.IsNotExist(err) {
return err
}
content = []byte("module github.com/docker/cli\n")
if err := os.WriteFile("../../go.mod", content, 0o644); err != nil {
return err
}
// Let's be nice, and remove the go.mod if we created it.
// FIXME(thaJeztah): we need to clean up the go.mod after running the test, but need to know if we created it (or if it was an existing go.mod)
// defer os.Remove("../../go.mod")
} else {
log.Println("WARN: go.mod exists in the repository root!")
log.Println("WARN: Using your go.mod instead of our generated version -- this may misbehave!")
}
mod, err := modfile.Parse("../../go.mod", content, nil)
if err != nil {
return err
}
if mod.Go != nil && mod.Go.Version != "" {
return fmt.Errorf("main go.mod must not contain a go version")
}
content, err = os.ReadFile("../../vendor.mod")
if err != nil {
return err
}
mod, err = modfile.Parse("../../vendor.mod", content, nil)
if err != nil {
return err
}
if err := mod.AddModuleStmt("gocompat"); err != nil {
return err
}
if err := mod.AddReplace("github.com/docker/cli", "", "../../", ""); err != nil {
return err
}
if err := mod.AddGoStmt("1.21"); err != nil {
return err
}
out, err := mod.Format()
if err != nil {
return err
}
tmpl, err := template.New("mod").Parse(modTemplate)
if err != nil {
return err
}

gen, _ := os.Executable()

var buf bytes.Buffer
err = tmpl.Execute(&buf, appContext{Generator: gen, Dependencies: string(out)})
if err != nil {
return err
}

return os.WriteFile("go.mod", buf.Bytes(), 0o644)
}

type appContext struct {
Generator string
Packages []string
Dependencies string
}

const appTemplate = `// Code generated by "{{ .Generator }}". DO NOT EDIT.
package main_test
import (
"testing"
// Import all importable packages, i.e., packages that:
//
// - are not applications ("main")
// - are not internal
// - and that have non-test go-files
{{- range .Packages }}
_ "{{ . }}"
{{- end}}
)
// This file import all "importable" packages, i.e., packages that:
//
// - are not applications ("main")
// - are not internal
// - and that have non-test go-files
//
// We do this to verify that our code can be consumed as a dependency
// in "module mode". When using a dependency that does not have a go.mod
// (i.e.; is not a "module"), go implicitly generates a go.mod. Lacking
// information from the dependency itself, it assumes "go1.16" language
// (see [DefaultGoModVersion]). Starting with Go1.21, go downgrades the
// language version used for such dependencies, which means that any
// language feature used that is not supported by go1.16 results in a
// compile error;
//
// # github.com/docker/cli/cli/context/store
// /go/pkg/mod/github.com/docker/[email protected]+incompatible/cli/context/store/storeconfig.go:6:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
// /go/pkg/mod/github.com/docker/[email protected]+incompatible/cli/context/store/store.go:74:12: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
//
// These errors do NOT occur when using GOPATH mode, nor do they occur
// when using "pseudo module mode" (the "-mod=mod -modfile=vendor.mod"
// approach used in this repository).
//
// As a workaround for this situation, we must include "//go:build" comments
// in any file that uses newer go-language features (such as the "any" type
// or the "min()", "max()" builtins).
//
// From the go toolchain docs (https://go.dev/doc/toolchain):
//
// > The go line for each module sets the language version the compiler enforces
// > when compiling packages in that module. The language version can be changed
// > on a per-file basis by using a build constraint.
// >
// > For example, a module containing code that uses the Go 1.21 language version
// > should have a go.mod file with a go line such as go 1.21 or go 1.21.3.
// > If a specific source file should be compiled only when using a newer Go
// > toolchain, adding //go:build go1.22 to that source file both ensures that
// > only Go 1.22 and newer toolchains will compile the file and also changes
// > the language version in that file to Go 1.22.
//
// This file is a generated module that imports all packages provided in
// the repository, which replicates an external consumer using our code
// as a dependency in go-module mode, and verifies all files in those
// packages have the correct "//go:build <go language version>" set.
//
// [DefaultGoModVersion]: https://github.com/golang/go/blob/58c28ba286dd0e98fe4cca80f5d64bbcb824a685/src/cmd/go/internal/gover/version.go#L15-L24
// [2]: https://go.dev/doc/toolchain
func TestModuleCompatibllity(t *testing.T) {
t.Log("all packages have the correct go version specified through //go:build")
}
`

const modTemplate = `// Code generated by "{{ .Generator }}". DO NOT EDIT.
{{.Dependencies}}
`
4 changes: 4 additions & 0 deletions internal/tools.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package internal

// make sure the modfile package is vendored.
import _ "golang.org/x/mod/modfile"
2 changes: 1 addition & 1 deletion vendor.mod
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ require (
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a
github.com/tonistiigi/go-rosetta v0.0.0-20200727161949-f79598599c5d
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/mod v0.11.0
golang.org/x/sync v0.3.0
golang.org/x/sys v0.13.0
golang.org/x/term v0.13.0
Expand Down Expand Up @@ -80,7 +81,6 @@ require (
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/otel/trace v1.19.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.10.0 // indirect
Expand Down
78 changes: 78 additions & 0 deletions vendor/golang.org/x/mod/internal/lazyregexp/lazyre.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 2fc96b2

Please sign in to comment.