build: add support for filtered stamp file
The stable status file changes whenever a commit is made, which triggers
unnecessary rebuilds of targets which are only stamped with variables
which change less often. My idea for fixing this is to create a filtered
stamp file with only the variables which change less often, and then
make targets depend on this more stable stamp file instead of the stable
status file.
This change adds a Bazel rule for generating such a filtered stamp file,
and a patch for rules_go which allows using the filtered file in x_defs.
Change-Id: I1a98babeb0cc5edeac6a90c655117305c499f744
Reviewed-on: https://review.monogon.dev/c/monogon/+/4166
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
Tested-by: Jenkins CI
diff --git a/MODULE.bazel b/MODULE.bazel
index a4f20d1..4d8e1df 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -16,6 +16,7 @@
patches = [
"//third_party/rules_go:rules_go_absolute_embedsrc.patch",
"//third_party/rules_go:introduce-all-scope-for-nogo.patch",
+ "//third_party/rules_go:stamp-srcs.patch",
],
version = "0.53.0",
)
diff --git a/build/filter_stamp/BUILD.bazel b/build/filter_stamp/BUILD.bazel
new file mode 100644
index 0000000..693b12e
--- /dev/null
+++ b/build/filter_stamp/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "filter_stamp_lib",
+ srcs = ["main.go"],
+ importpath = "source.monogon.dev/build/filter_stamp",
+ visibility = ["//visibility:private"],
+)
+
+go_binary(
+ name = "filter_stamp",
+ embed = [":filter_stamp_lib"],
+ visibility = ["//visibility:public"],
+)
diff --git a/build/filter_stamp/def.bzl b/build/filter_stamp/def.bzl
new file mode 100644
index 0000000..650a025
--- /dev/null
+++ b/build/filter_stamp/def.bzl
@@ -0,0 +1,32 @@
+def _filtered_stamp_impl(ctx):
+ output = ctx.actions.declare_file(ctx.label.name + ".txt")
+ ctx.actions.run(
+ mnemonic = "FilterStamp",
+ executable = ctx.executable._filter_stamp,
+ arguments = ["-status", ctx.info_file.path, "-out", output.path, "--"] + ctx.attr.vars,
+ inputs = [ctx.info_file],
+ outputs = [output],
+ )
+ return [DefaultInfo(files = depset([output]))]
+
+filtered_stamp = rule(
+ implementation = _filtered_stamp_impl,
+ doc = """
+ Build a stamp file with a subset of
+ variables from the stable status file.
+ """,
+ attrs = {
+ "vars": attr.string_list(
+ doc = """
+ List of variables to include in the output.
+ """,
+ mandatory = True,
+ ),
+ # Tool
+ "_filter_stamp": attr.label(
+ default = Label("//build/filter_stamp"),
+ executable = True,
+ cfg = "exec",
+ ),
+ },
+)
diff --git a/build/filter_stamp/main.go b/build/filter_stamp/main.go
new file mode 100644
index 0000000..6b8d561
--- /dev/null
+++ b/build/filter_stamp/main.go
@@ -0,0 +1,52 @@
+// Copyright The Monogon Project Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "strings"
+)
+
+var (
+ statusFile = flag.String("status", "", "Path to bazel workspace status file")
+ outFile = flag.String("out", "", "Output stamp file path")
+)
+
+func main() {
+ flag.Parse()
+
+ vars := make(map[string]bool)
+ for _, variable := range flag.Args() {
+ vars[variable] = true
+ }
+
+ statusFileContent, err := os.ReadFile(*statusFile)
+ if err != nil {
+ log.Fatalf("Failed to open bazel workspace status file: %v\n", err)
+ }
+
+ var filtered []string
+ for line := range strings.SplitSeq(string(statusFileContent), "\n") {
+ if line == "" {
+ continue
+ }
+ variable, value, ok := strings.Cut(line, " ")
+ if !ok {
+ log.Fatalf("Invalid line in status file: %q\n", line)
+ }
+ variable, ok = strings.CutPrefix(variable, "STABLE_")
+ if ok && vars[variable] {
+ filtered = append(filtered, fmt.Sprintf("STABLER_%s %s\n", variable, value))
+ }
+ }
+
+ filteredContent := []byte(strings.Join(filtered, ""))
+ err = os.WriteFile(*outFile, filteredContent, 0644)
+ if err != nil {
+ log.Fatalf("Failed to write output file: %v", err)
+ }
+}
diff --git a/third_party/rules_go/stamp-srcs.patch b/third_party/rules_go/stamp-srcs.patch
new file mode 100644
index 0000000..0b3c2cd
--- /dev/null
+++ b/third_party/rules_go/stamp-srcs.patch
@@ -0,0 +1,167 @@
+commit 676d9d220a8a71f207701fc8568078028235aea6
+Author: Jan Schär <jan@monogon.tech>
+Date: Wed Apr 30 09:30:04 2025 +0000
+
+ Add stampsrcs attribute
+
+ This change adds the stampsrcs attribute, which allows providing a list
+ of files containing variable definitions, which are used in addition to
+ the stable and volatile status files and have the same format.
+
+ See https://github.com/bazel-contrib/rules_go/issues/3507
+
+diff --git a/go/private/actions/archive.bzl b/go/private/actions/archive.bzl
+index 06212139..e89cedd6 100644
+--- a/go/private/actions/archive.bzl
++++ b/go/private/actions/archive.bzl
+@@ -177,6 +177,7 @@ def emit_archive(go, source = None, _recompile_suffix = "", recompile_internal_d
+ _cover = source.cover,
+ _embedsrcs = tuple(source.embedsrcs),
+ _x_defs = tuple(source.x_defs.items()),
++ _stampsrcs = tuple(source.stampsrcs),
+ _gc_goopts = tuple(source.gc_goopts),
+ _cgo = source.cgo,
+ _cdeps = tuple(source.cdeps),
+@@ -198,8 +199,10 @@ def emit_archive(go, source = None, _recompile_suffix = "", recompile_internal_d
+ _cgo_deps = cgo_deps,
+ )
+ x_defs = dict(source.x_defs)
++ stampsrcs = source.stampsrcs
+ for a in direct:
+ x_defs.update(a.x_defs)
++ stampsrcs = stampsrcs + a.stampsrcs
+
+ # Ensure that the _cgo_export.h of the current target comes first when cgo_exports is iterated
+ # by prepending it and specifying the order explicitly. This is required as the CcInfo attached
+@@ -213,6 +216,7 @@ def emit_archive(go, source = None, _recompile_suffix = "", recompile_internal_d
+ libs = depset(direct = [out_lib], transitive = [a.libs for a in direct]),
+ transitive = depset([data], transitive = [a.transitive for a in direct]),
+ x_defs = x_defs,
++ stampsrcs = stampsrcs,
+ cgo_deps = depset(transitive = [cgo_deps] + [a.cgo_deps for a in direct]),
+ cgo_exports = cgo_exports,
+ runfiles = runfiles,
+diff --git a/go/private/actions/link.bzl b/go/private/actions/link.bzl
+index 18dd1f94..3b236f06 100644
+--- a/go/private/actions/link.bzl
++++ b/go/private/actions/link.bzl
+@@ -161,6 +161,7 @@ def emit_link(
+ stamp_inputs.append(info_file)
+ if stamp_x_defs_volatile:
+ stamp_inputs.append(version_file)
++ stamp_inputs = stamp_inputs + archive.stampsrcs
+ if stamp_inputs:
+ builder_args.add_all(stamp_inputs, before_each = "-stamp")
+
+diff --git a/go/private/context.bzl b/go/private/context.bzl
+index f5cd735d..42c5cf13 100644
+--- a/go/private/context.bzl
++++ b/go/private/context.bzl
+@@ -202,6 +202,7 @@ def _merge_embed(source, embed):
+ source["cover"] = depset(transitive = [source["cover"], s.cover])
+ source["deps"] = source["deps"] + s.deps
+ source["x_defs"].update(s.x_defs)
++ source["stampsrcs"] = source["stampsrcs"] + s.stampsrcs
+ source["gc_goopts"] = source["gc_goopts"] + s.gc_goopts
+ source["runfiles"] = source["runfiles"].merge(s.runfiles)
+
+@@ -319,6 +320,7 @@ def new_go_info(
+ "embedsrcs": embedsrcs,
+ "cover": depset(attr_srcs) if coverage_instrumented else depset(),
+ "x_defs": {},
++ "stampsrcs": [],
+ "deps": deps,
+ "gc_goopts": _expand_opts(go, "gc_goopts", getattr(attr, "gc_goopts", [])),
+ "runfiles": _collect_runfiles(go, getattr(attr, "data", []), deps),
+@@ -344,6 +346,8 @@ def new_go_info(
+ k = "{}.{}".format(importmap, k)
+ x_defs[k] = v
+ go_info["x_defs"] = x_defs
++ for t in getattr(attr, "stampsrcs", []):
++ go_info["stampsrcs"] = go_info["stampsrcs"] + t[DefaultInfo].files.to_list()
+ if not go_info["cgo"]:
+ for k in ("cdeps", "cppopts", "copts", "cxxopts", "clinkopts"):
+ if getattr(attr, k, None):
+diff --git a/go/private/rules/binary.bzl b/go/private/rules/binary.bzl
+index f3f2f07c..eac3a978 100644
+--- a/go/private/rules/binary.bzl
++++ b/go/private/rules/binary.bzl
+@@ -302,6 +302,12 @@ def _go_binary_kwargs(go_cc_aspects = []):
+ See [Defines and stamping] for examples of how to use these.
+ """,
+ ),
++ "stampsrcs": attr.label_list(
++ allow_files = True,
++ doc = """Additional files containing variables which can be referenced in `x_defs`.
++ The format of these files should be the same as the workspace status.
++ """,
++ ),
+ "basename": attr.string(
+ doc = """The basename of this binary. The binary
+ basename may also be platform-dependent: on Windows, we add an .exe extension.
+diff --git a/go/private/rules/library.bzl b/go/private/rules/library.bzl
+index 8aa020d6..fe944d3a 100644
+--- a/go/private/rules/library.bzl
++++ b/go/private/rules/library.bzl
+@@ -147,6 +147,12 @@ go_library = rule(
+ Map of defines to add to the go link command. See [Defines and stamping] for examples of how to use these.
+ """,
+ ),
++ "stampsrcs": attr.label_list(
++ allow_files = True,
++ doc = """Additional files containing variables which can be referenced in `x_defs`.
++ The format of these files should be the same as the workspace status.
++ """,
++ ),
+ "cgo": attr.bool(
+ doc = """
+ If `True`, the package may contain [cgo] code, and `srcs` may contain C, C++, Objective-C, and Objective-C++ files
+diff --git a/go/private/rules/test.bzl b/go/private/rules/test.bzl
+index 4859c53e..da46ead9 100644
+--- a/go/private/rules/test.bzl
++++ b/go/private/rules/test.bzl
+@@ -93,6 +93,7 @@ def _go_test_impl(ctx):
+ embedsrcs = [struct(files = internal_go_info.embedsrcs)],
+ deps = internal_archive.direct + [internal_archive],
+ x_defs = ctx.attr.x_defs,
++ stampsrcs = ctx.attr.stampsrcs,
+ ),
+ name = internal_go_info.name + "_test",
+ importpath = internal_go_info.importpath + "_test",
+@@ -326,6 +327,12 @@ _go_test_kwargs = {
+ See [Defines and stamping] for examples of how to use these.
+ """,
+ ),
++ "stampsrcs": attr.label_list(
++ allow_files = True,
++ doc = """Additional files containing variables which can be referenced in `x_defs`.
++ The format of these files should be the same as the workspace status.
++ """,
++ ),
+ "linkmode": attr.string(
+ default = "auto",
+ values = ["auto"] + LINKMODES,
+@@ -661,6 +668,7 @@ def _recompile_external_deps(go, external_go_info, internal_archive, library_lab
+ attrs = structs.to_dict(internal_go_info)
+ attrs["deps"] = internal_deps
+ attrs["x_defs"] = x_defs
++ attrs["stampsrcs"] = internal_go_info.stampsrcs + internal_archive.stampsrcs
+ internal_go_info = GoInfo(**attrs)
+ internal_archive = go.archive(go, internal_go_info, _recompile_suffix = ".recompileinternal", recompile_internal_deps = need_recompile_deps)
+
+@@ -698,6 +706,7 @@ def _recompile_external_deps(go, external_go_info, internal_archive, library_lab
+ cover = arc_data._cover,
+ embedsrcs = as_list(arc_data._embedsrcs),
+ x_defs = dict(arc_data._x_defs),
++ stampsrcs = as_list(arc_data._stampsrcs),
+ deps = deps,
+ gc_goopts = as_list(arc_data._gc_goopts),
+ runfiles = arc_data.runfiles,
+@@ -722,6 +731,7 @@ def _recompile_external_deps(go, external_go_info, internal_archive, library_lab
+ libs = depset(direct = [arc_data.file], transitive = [a.libs for a in deps]),
+ transitive = depset(direct = [arc_data], transitive = [a.transitive for a in deps]),
+ x_defs = go_info.x_defs,
++ stampsrcs = go_info.stampsrcs,
+ cgo_deps = depset(transitive = [arc_data._cgo_deps] + [a.cgo_deps for a in deps]),
+ cgo_exports = depset(transitive = [a.cgo_exports for a in deps]),
+ runfiles = go_info.runfiles,