Skip to content
Open
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
23 changes: 23 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,26 @@ jobs:
shell: 'script --return --quiet --command "bash {0}"'
run: |
make -f docker.Makefile ${{ matrix.target }}

validate-gocompat:
runs-on: ubuntu-24.04
env:
GOPATH: ${{ github.workspace }}
GO111MODULE: off
steps:
-
name: Checkout
uses: actions/checkout@v6
with:
path: src/github.com/docker/cli
-
name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.25.8"
-
name: Run gocompat check
shell: 'script --return --quiet --command "bash {0}"'
working-directory: ${{ github.workspace }}/src/github.com/docker/cli
run: |
make -C ./internal/gocompat verify clean
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
14 changes: 14 additions & 0 deletions internal/gocompat/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.PHONY: verify
verify: generate
GO111MODULE=on go test -v; status=$$?; \
rm -f go.mod go.sum main.go main_test.go ../../go.mod; \
exit $$status
Comment on lines +3 to +5

.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 ../../go.mod
3 changes: 3 additions & 0 deletions internal/gocompat/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package main

//go:generate go run modulegenerator.go
196 changes: 196 additions & 0 deletions internal/gocompat/modulegenerator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
//go:build ignore
// +build ignore

package main

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

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

func main() {
if err := generateApp(); err != nil {
log.Fatal(err)
}
rootDir := "../../"
if err := generateModule(rootDir); 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 fmt.Errorf("go list failed: %w\nOutput:\n%s", err, string(out))
}

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(rootDir string) (retErr error) {
modFile := filepath.Join(rootDir, "go.mod")
_, err := os.Stat(modFile)
if err == nil {
return errors.New("go.mod exists in the repository root")
}

// create an empty go.mod without go version.
//
// this go.mod must exist when running the test.
err = os.WriteFile(modFile, []byte("module github.com/docker/cli\n"), 0o644)
if err != nil {
return fmt.Errorf("failed to write go.mod: %w", err)
}
defer func() {
if retErr != nil {
_ = os.Remove(modFile)
}
Comment on lines +59 to +75
}()

content, err := os.ReadFile(filepath.Join(rootDir, "vendor.mod"))
if err != nil {
return err
}
mod, err := modfile.Parse(filepath.Join(rootDir, "vendor.mod"), content, nil)
if err != nil {
return err
}
if err := mod.AddModuleStmt("example.com/gocompat"); err != nil {
return err
}
if err := mod.AddReplace("github.com/docker/cli", "", rootDir, ""); err != nil {
return err
}
if err := mod.AddGoStmt("1.24"); 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, err := os.Executable()
if err != nil {
return err
}

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 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/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 TestModuleCompatibility(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}}
`
3 changes: 3 additions & 0 deletions vendor.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ module github.com/docker/cli

go 1.24.0

tool golang.org/x/mod/modfile // for module compatibility check

require (
dario.cat/mergo v1.0.2
github.com/containerd/errdefs v1.0.0
Expand Down Expand Up @@ -98,6 +100,7 @@ require (
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
Expand Down
2 changes: 2 additions & 0 deletions vendor.sum
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,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.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
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.

Loading
Loading