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/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,