-
Notifications
You must be signed in to change notification settings - Fork 2.1k
ci: add module compatibility check #4723
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| main_test.go |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| .PHONY: verify | ||
| verify: generate | ||
| @GO111MODULE=on go test -v; status=$$?; \ | ||
| @$(MAKE) clean || true; \ | ||
| exit $$status | ||
|
|
||
| .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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| package main | ||
|
|
||
| //go:generate go run modulegenerator.go |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| //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") | ||
| } | ||
| if !errors.Is(err, os.ErrNotExist) { | ||
| return fmt.Errorf("failed to stat go.mod: %w", err) | ||
| } | ||
|
|
||
| // 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
+78
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leaving that for a future exercise. |
||
| }() | ||
|
|
||
| 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}} | ||
| ` | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.