From a24a63e8b9241e077eaebf72f654f02d08f08e18 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Tue, 11 Jun 2024 17:35:50 +0200 Subject: [PATCH] Automatically run `bazel mod tidy` in `@rules_go//go` (#3927) `bazel mod tidy` is run if supported and if relevant files change due to the command. --- .gitignore | 2 + docs/go/core/bzlmod.md | 4 +- go/private/polyfill_bazel_features.bzl | 6 +- go/tools/go_bin_runner/BUILD.bazel | 2 + go/tools/go_bin_runner/main.go | 84 ++++++++++++++++++++++++-- 5 files changed, 91 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index c3d41a3a35..c74119c9b2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ /examples/*/bazel-* /.ijwb/ /tests/bcr/.ijwb/ + +MODULE.bazel.lock diff --git a/docs/go/core/bzlmod.md b/docs/go/core/bzlmod.md index 70087dabc7..c95ca9cb7d 100644 --- a/docs/go/core/bzlmod.md +++ b/docs/go/core/bzlmod.md @@ -147,8 +147,8 @@ use_repo( ) ``` -Bazel emits a warning if the `use_repo` statement is out of date or missing entirely (requires Bazel 6.2.0 or higher). -The warning contains a `buildozer` command to automatically fix the `MODULE.bazel` file (requires buildozer 6.1.1 or higher). +When using Bazel 7.1.1 or higher, the [`@rules_go//go` target](#using-a-go-sdk) automatically updates the `use_repo` call whenever the `go.mod` file changes, using `bazel mod tidy`. +With older versions of Bazel, a warning with a fixup command will be emitted during a build if the `use_repo` call is out of date or missing. Alternatively, you can specify a module extension tag to add an individual dependency: diff --git a/go/private/polyfill_bazel_features.bzl b/go/private/polyfill_bazel_features.bzl index 5481b6b0c7..7d28a3e6d8 100644 --- a/go/private/polyfill_bazel_features.bzl +++ b/go/private/polyfill_bazel_features.bzl @@ -7,7 +7,11 @@ _POLYFILL_BAZEL_FEATURES = """bazel_features = struct( cc = struct( find_cpp_toolchain_has_mandatory_param = {find_cpp_toolchain_has_mandatory_param}, - ) + ), + external_deps = struct( + # WORKSPACE users have no use for bazel mod tidy. + bazel_mod_tidy = False, + ), ) """ diff --git a/go/tools/go_bin_runner/BUILD.bazel b/go/tools/go_bin_runner/BUILD.bazel index 7eaedbc9e8..74b92f11a7 100644 --- a/go/tools/go_bin_runner/BUILD.bazel +++ b/go/tools/go_bin_runner/BUILD.bazel @@ -1,5 +1,6 @@ # gazelle:exclude +load("@io_bazel_rules_go_bazel_features//:features.bzl", "bazel_features") load("//go:def.bzl", "go_binary", "go_library") load("//go/private:common.bzl", "RULES_GO_IS_BZLMOD_REPO") load("//go/private/rules:go_bin_for_host.bzl", "go_bin_for_host") @@ -36,6 +37,7 @@ go_binary( x_defs = { "GoBinRlocationPath": "$(rlocationpath :go_bin_for_host)", "ConfigRlocationPath": "$(rlocationpath @bazel_gazelle_go_repository_config//:config.json)" if RULES_GO_IS_BZLMOD_REPO else "WORKSPACE", + "HasBazelModTidy": str(bazel_features.external_deps.bazel_mod_tidy), }, ) diff --git a/go/tools/go_bin_runner/main.go b/go/tools/go_bin_runner/main.go index d8e5bf7420..b9d2e4de4b 100644 --- a/go/tools/go_bin_runner/main.go +++ b/go/tools/go_bin_runner/main.go @@ -1,21 +1,29 @@ package main import ( + "crypto/sha256" + "encoding/hex" "encoding/json" + "fmt" + "io" "log" "os" "os/exec" "path/filepath" + "sort" + "strings" "github.com/bazelbuild/rules_go/go/runfiles" ) var GoBinRlocationPath = "not set" var ConfigRlocationPath = "not set" +var HasBazelModTidy = "not set" // Produced by gazelle's go_deps extension. type Config struct { - GoEnv map[string]string `json:"go_env"` + GoEnv map[string]string `json:"go_env"` + DepsFiles []string `json:"dep_files"` } func main() { @@ -34,8 +42,39 @@ func main() { log.Fatal(err) } + hashesBefore, err := hashWorkspaceRelativeFiles(cfg.DepsFiles) + if err != nil { + log.Fatal(err) + } + args := append([]string{goBin}, os.Args[1:]...) - log.Fatal(runProcess(args, env, os.Getenv("BUILD_WORKING_DIRECTORY"))) + cwd := os.Getenv("BUILD_WORKING_DIRECTORY") + err = runProcess(args, env, cwd) + if err != nil { + log.Fatal(err) + } + + hashesAfter, err := hashWorkspaceRelativeFiles(cfg.DepsFiles) + if err != nil { + log.Fatal(err) + } + + diff := diffMaps(hashesBefore, hashesAfter) + if len(diff) > 0 { + if HasBazelModTidy == "True" { + bazel := os.Getenv("BAZEL") + if bazel == "" { + bazel = "bazel" + } + _, _ = fmt.Fprintf(os.Stderr, "\nrules_go: Running '%s mod tidy' since %s changed...\n", bazel, strings.Join(diff, ", ")) + err = runProcess([]string{bazel, "mod", "tidy"}, os.Environ(), cwd) + if err != nil { + log.Fatal(err) + } + } else { + _, _ = fmt.Fprintf(os.Stderr, "\nrules_go: %s changed, please apply any buildozer fixes suggested by Bazel\n", strings.Join(diff, ", ")) + } + } } func parseConfig() (Config, error) { @@ -76,6 +115,45 @@ func getGoEnv(goBin string, cfg Config) ([]string, error) { return append(env, "GOROOT="+goRoot), nil } +func hashWorkspaceRelativeFiles(relativePaths []string) (map[string]string, error) { + workspace := os.Getenv("BUILD_WORKSPACE_DIRECTORY") + + hashes := make(map[string]string) + for _, p := range relativePaths { + h, err := hashFile(filepath.Join(workspace, p)) + if err != nil { + return nil, err + } + hashes[p] = h + } + return hashes, nil +} + +// diffMaps returns the keys that have different values in a and b. +func diffMaps(a, b map[string]string) []string { + var diff []string + for k, v := range a { + if b[k] != v { + diff = append(diff, k) + } + } + sort.Strings(diff) + return diff +} + +func hashFile(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + return hex.EncodeToString(h.Sum(nil)), nil +} + func runProcess(args, env []string, dir string) error { cmd := exec.Command(args[0], args[1:]...) cmd.Dir = dir @@ -85,8 +163,6 @@ func runProcess(args, env []string, dir string) error { err := cmd.Run() if exitErr, ok := err.(*exec.ExitError); ok { os.Exit(exitErr.ExitCode()) - } else if err == nil { - os.Exit(0) } return err }