Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/cli@v25.0.0-beta.2+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/cli@v25.0.0-beta.2+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"
1 change: 1 addition & 0 deletions vendor.mod
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/mod v0.31.0
golang.org/x/sync v0.18.0
golang.org/x/sys v0.38.0
golang.org/x/term v0.37.0
Expand Down
2 changes: 2 additions & 0 deletions vendor.sum
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
Expand Down
27 changes: 27 additions & 0 deletions vendor/golang.org/x/mod/LICENSE

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

22 changes: 22 additions & 0 deletions vendor/golang.org/x/mod/PATENTS

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

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
Loading