Skip to content

Commit 51afe0b

Browse files
committed
link: add stamped VCS build info
Stamped rules_go binaries cannot report their source revision or dirty state through runtime/debug or go version -m. This is needed because users expect the same provenance checks supported by cmd/go-built module binaries. Map STABLE_BUILD_SCM_VCS, REVISION, TIME, and STATUS workspace values to Go's vcs.* settings. Filter those keys in a separate action so unrelated stable status changes do not relink binaries, normalize times to UTC, and use the Go 1.20 debug.BuildSetting API directly. Only stamp executable build info when the target and recorded main module both come from the main workspace. Preserve that provenance through embeds and tests so external binaries and wrappers cannot be misattributed to the caller workspace. Document the convention and cover stable refreshes, go_binary and go_test, external provenance, non-executable modes, and stamp inputs.
1 parent 87b1621 commit 51afe0b

18 files changed

Lines changed: 1365 additions & 52 deletions

docs/go/core/defines_and_stamping.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,36 @@ argument on the command line:
9292
$ bazel build --stamp --workspace_status_command=./status.sh //:cmd
9393
```
9494

95+
### VCS build info
96+
97+
When `go_binary` and `go_test` targets are linked with stamping enabled,
98+
`rules_go` also maps a small stable workspace status convention into Go build
99+
info. These settings are visible through `runtime/debug.ReadBuildInfo` and
100+
`go version -m`. They are only emitted when the linked target and its recorded
101+
main module both come from the main workspace; stamped binaries built from
102+
external repositories, targets without main module metadata, and local wrappers
103+
around external main modules omit `vcs.*` settings. Main-module provenance
104+
follows the metadata source that supplied `Main.Path`, including embedded
105+
libraries and `package_metadata` / `applicable_licenses`.
106+
107+
| Workspace status key | Go build info setting | Notes |
108+
| --- | --- | --- |
109+
| `STABLE_BUILD_SCM_VCS` | `vcs` | Required and must be non-empty before any `vcs.*` settings are emitted. |
110+
| `STABLE_BUILD_SCM_REVISION` | `vcs.revision` | Optional when non-empty. |
111+
| `STABLE_BUILD_SCM_TIME` | `vcs.time` | Optional; must be `RFC3339Nano` and is normalized to UTC. |
112+
| `STABLE_BUILD_SCM_STATUS` | `vcs.modified` | Optional; `Clean` becomes `false`, `Modified` becomes `true`. |
113+
114+
Invalid or missing optional values are ignored individually.
115+
116+
``` bash
117+
#!/usr/bin/env bash
118+
119+
echo "STABLE_BUILD_SCM_VCS git"
120+
echo "STABLE_BUILD_SCM_REVISION $(git rev-parse HEAD)"
121+
echo "STABLE_BUILD_SCM_TIME $(git show -s --format=%cI HEAD)"
122+
if test -z "$(git status --porcelain --untracked-files=normal)"; then
123+
echo "STABLE_BUILD_SCM_STATUS Clean"
124+
else
125+
echo "STABLE_BUILD_SCM_STATUS Modified"
126+
fi
127+
```

go/private/actions/archive.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ def emit_archive(go, source = None, _recompile_suffix = "", recompile_internal_d
197197
_clinkopts = tuple(source.clinkopts),
198198
_package_metadata = getattr(source, "_package_metadata", None),
199199
_main_module_package_metadata = getattr(source, "_main_module_package_metadata", None),
200+
_main_module_main_workspace = getattr(source, "_main_module_main_workspace", False),
200201

201202
# Information on dependencies
202203
_dep_labels = tuple([d.data.label for d in direct]),
@@ -240,4 +241,5 @@ def emit_archive(go, source = None, _recompile_suffix = "", recompile_internal_d
240241
_headers = headers,
241242
_package_metadata_files = package_metadata_files,
242243
_main_module_package_metadata = getattr(data, "_main_module_package_metadata", None),
244+
_main_module_main_workspace = getattr(data, "_main_module_main_workspace", False),
243245
)

go/private/actions/binary.bzl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ def emit_binary(
3737
executable = None,
3838
link_exec_group = None,
3939
buildinfo_path = None,
40-
buildinfo_module_metadata = None):
40+
buildinfo_module_metadata = None,
41+
buildinfo_module_main_workspace = False,
42+
main_workspace = True):
4143
"""See go/toolchains.rst#binary for full documentation."""
4244

4345
if name == "" and executable == None:
@@ -46,6 +48,7 @@ def emit_binary(
4648
buildinfo_path = source.importpath
4749
if buildinfo_module_metadata == None:
4850
buildinfo_module_metadata = getattr(source, "_main_module_package_metadata", None)
51+
buildinfo_module_main_workspace = getattr(source, "_main_module_main_workspace", False)
4952

5053
archive = go.archive(go, source)
5154
if not executable:
@@ -66,10 +69,12 @@ def emit_binary(
6669
test_archives = test_archives,
6770
buildinfo_path = buildinfo_path,
6871
buildinfo_module_metadata = buildinfo_module_metadata,
72+
buildinfo_module_main_workspace = buildinfo_module_main_workspace,
6973
executable = executable,
7074
gc_linkopts = gc_linkopts,
7175
version_file = version_file,
7276
info_file = info_file,
77+
main_workspace = main_workspace,
7378
exec_group = link_exec_group,
7479
)
7580
cgo_dynamic_deps = [

go/private/actions/link.bzl

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ load(
2424
)
2525
load(
2626
"//go/private:mode.bzl",
27+
"LINKMODES_EXECUTABLE",
2728
"LINKMODE_NORMAL",
2829
"LINKMODE_PLUGIN",
2930
"extld_from_cc_toolchain",
@@ -37,6 +38,26 @@ load(
3738
def _format_archive(d):
3839
return "{}={}={}".format(d.label, d.importmap, d.file.path)
3940

41+
def _should_emit_buildinfo(go):
42+
return go.mode.linkmode in LINKMODES_EXECUTABLE
43+
44+
def _emit_vcs_stamp(go, info_file, executable):
45+
out = go.actions.declare_file(executable.basename + ".vcsstamp", sibling = executable)
46+
args = go.tool_args(go)
47+
args.add("vcsstamp")
48+
args.add("-in", info_file)
49+
args.add("-out", out)
50+
go.actions.run(
51+
inputs = [info_file],
52+
outputs = [out],
53+
mnemonic = "GoVCSStamp",
54+
executable = go.toolchain._builder,
55+
arguments = [args],
56+
env = go.env,
57+
toolchain = GO_TOOLCHAIN_LABEL,
58+
)
59+
return out
60+
4061
def emit_link(
4162
go,
4263
archive = None,
@@ -47,7 +68,9 @@ def emit_link(
4768
info_file = None,
4869
exec_group = None,
4970
buildinfo_path = None,
50-
buildinfo_module_metadata = None):
71+
buildinfo_module_metadata = None,
72+
buildinfo_module_main_workspace = False,
73+
main_workspace = True):
5174
"""See go/toolchains.rst#link for full documentation."""
5275

5376
if archive == None:
@@ -58,6 +81,10 @@ def emit_link(
5881
buildinfo_path = archive.data.importpath
5982
if buildinfo_module_metadata == None:
6083
buildinfo_module_metadata = getattr(archive.data, "_main_module_package_metadata", None)
84+
buildinfo_module_main_workspace = getattr(archive.data, "_main_module_main_workspace", False)
85+
main_workspace = (main_workspace and
86+
not go.label.repo_name and
87+
not getattr(go.label, "workspace_root", ""))
6188

6289
# Exclude -lstdc++ from link options. We don't want to link against it
6390
# unless we actually have some C++ code. _cgo_codegen will include it
@@ -177,9 +204,19 @@ def emit_link(
177204
if count_group_matches(v, "{", "}") != stable_vars_count:
178205
stamp_x_defs_volatile = True
179206

207+
vcs_stamp_file = None
208+
if (go.mode.stamp and
209+
info_file and
210+
main_workspace and
211+
buildinfo_module_metadata and
212+
buildinfo_module_main_workspace and
213+
_should_emit_buildinfo(go)):
214+
vcs_stamp_file = _emit_vcs_stamp(go, info_file, executable)
215+
builder_args.add("-vcs_stamp", vcs_stamp_file)
216+
180217
# Stamping support
181218
stamp_inputs = []
182-
if stamp_x_defs_stable:
219+
if stamp_x_defs_stable and info_file:
183220
stamp_inputs.append(info_file)
184221
if stamp_x_defs_volatile:
185222
stamp_inputs.append(version_file)
@@ -191,6 +228,8 @@ def emit_link(
191228
builder_args.add("-main_package_path", buildinfo_path)
192229
if buildinfo_module_metadata:
193230
builder_args.add("-main_module_metadata", buildinfo_module_metadata)
231+
builder_args.add("-main_module_main_workspace=%s" % ("true" if buildinfo_module_main_workspace else "false"))
232+
builder_args.add("-main_workspace=%s" % ("true" if main_workspace else "false"))
194233
builder_args.add("-p", archive.data.importmap)
195234
tool_args.add_all(gc_linkopts)
196235
tool_args.add_all(go.toolchain.flags.link)
@@ -204,6 +243,8 @@ def emit_link(
204243
inputs_direct = stamp_inputs + [go.sdk.package_list]
205244
if buildinfo_module_metadata:
206245
inputs_direct.append(buildinfo_module_metadata)
246+
if vcs_stamp_file:
247+
inputs_direct.append(vcs_stamp_file)
207248
if go.coverage_enabled and go.coverdata:
208249
inputs_direct.append(go.coverdata.data.file)
209250
inputs_transitive = [

go/private/context.bzl

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,16 +272,26 @@ def _tool_args(go):
272272
args.use_param_file("-param=%s")
273273
return args
274274

275-
def package_metadata_file_from_metadata(package_metadata = (), applicable_licenses = ()):
275+
def _is_main_workspace_label(label):
276+
return not label.repo_name and not getattr(label, "workspace_root", "")
277+
278+
def _package_metadata_target_from_metadata(package_metadata = (), applicable_licenses = ()):
276279
# Bazel may surface repo-level metadata through either spelling depending on
277280
# the version and rule surface, so probe both.
278281
for metadata_group in (package_metadata, applicable_licenses):
279282
for metadata in metadata_group:
280283
if PackageMetadataInfo in metadata:
281-
return metadata[PackageMetadataInfo].metadata
282-
284+
return metadata
283285
return None
284286

287+
def package_metadata_file_from_metadata(package_metadata = (), applicable_licenses = ()):
288+
metadata = _package_metadata_target_from_metadata(package_metadata, applicable_licenses)
289+
return metadata[PackageMetadataInfo].metadata if metadata else None
290+
291+
def package_metadata_main_workspace_from_metadata(package_metadata = (), applicable_licenses = ()):
292+
metadata = _package_metadata_target_from_metadata(package_metadata, applicable_licenses)
293+
return metadata != None and _is_main_workspace_label(metadata.label)
294+
285295
def _merge_embed(source, embed):
286296
s = get_source(embed)
287297
source["srcs"] = s.srcs + source["srcs"]
@@ -297,6 +307,7 @@ def _merge_embed(source, embed):
297307
main_module_package_metadata = getattr(s, "_main_module_package_metadata", None)
298308
if not source["_main_module_package_metadata"] and main_module_package_metadata:
299309
source["_main_module_package_metadata"] = main_module_package_metadata
310+
source["_main_module_main_workspace"] = getattr(s, "_main_module_main_workspace", False)
300311

301312
if s.cgo:
302313
if source["cgo"]:
@@ -408,7 +419,10 @@ def new_go_info(
408419
getattr(attr, "applicable_licenses", ()),
409420
)
410421
package_metadata = main_module_package_metadata if include_package_metadata else None
411-
422+
main_module_main_workspace = package_metadata_main_workspace_from_metadata(
423+
getattr(attr, "package_metadata", ()),
424+
getattr(attr, "applicable_licenses", ()),
425+
)
412426
go_info = {
413427
"name": go.label.name if not name else name,
414428
"label": go.label,
@@ -435,6 +449,7 @@ def new_go_info(
435449
"pgoprofile": getattr(attr, "pgoprofile", None),
436450
"_package_metadata": package_metadata,
437451
"_main_module_package_metadata": main_module_package_metadata,
452+
"_main_module_main_workspace": main_module_main_workspace,
438453
}
439454

440455
for e in getattr(attr, "embed", []):

go/private/rules/binary.bzl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ def _go_binary_impl(ctx):
155155
importable = False,
156156
is_main = is_main,
157157
)
158+
buildinfo_module_metadata = getattr(go_info, "_main_module_package_metadata", None)
159+
buildinfo_module_main_workspace = getattr(go_info, "_main_module_main_workspace", False)
158160
name = ctx.attr.basename
159161
if not name:
160162
name = ctx.label.name
@@ -168,9 +170,12 @@ def _go_binary_impl(ctx):
168170
go,
169171
name = name,
170172
source = go_info,
173+
buildinfo_module_metadata = buildinfo_module_metadata,
174+
buildinfo_module_main_workspace = buildinfo_module_main_workspace,
171175
gc_linkopts = gc_linkopts(ctx),
172176
version_file = ctx.version_file,
173177
info_file = ctx.info_file,
178+
main_workspace = not ctx.label.repo_name and not getattr(ctx.label, "workspace_root", ""),
174179
link_exec_group = "go_link",
175180
executable = executable,
176181
)

go/private/rules/test.bzl

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,17 +184,24 @@ def _go_test_impl(ctx):
184184
generated_srcs = [main_go],
185185
coverage_instrumented = False,
186186
)
187-
buildinfo_path = internal_go_info.importpath + ".test" if internal_go_info.importpath else None
187+
buildinfo_module_metadata = getattr(internal_go_info, "_main_module_package_metadata", None)
188+
buildinfo_module_main_workspace = getattr(internal_go_info, "_main_module_main_workspace", False)
189+
buildinfo_path = None
190+
if internal_go_info.importpath:
191+
buildinfo_path = internal_go_info.importpath + ".test"
192+
188193
test_archive, executable, runfiles = go.binary(
189194
go,
190195
name = ctx.label.name,
191196
source = test_go_info,
192197
test_archives = [internal_archive.data],
193198
buildinfo_path = buildinfo_path,
194-
buildinfo_module_metadata = getattr(internal_go_info, "_main_module_package_metadata", None),
199+
buildinfo_module_metadata = buildinfo_module_metadata,
200+
buildinfo_module_main_workspace = buildinfo_module_main_workspace,
195201
gc_linkopts = test_gc_linkopts,
196202
version_file = ctx.version_file,
197203
info_file = ctx.info_file,
204+
main_workspace = not ctx.label.repo_name and not getattr(ctx.label, "workspace_root", ""),
198205
link_exec_group = "go_link",
199206
)
200207

@@ -742,6 +749,8 @@ def _recompile_external_deps(go, external_go_info, internal_archive, library_lab
742749
cxxopts = list(arc_data._cxxopts),
743750
clinkopts = list(arc_data._clinkopts),
744751
_package_metadata = package_metadata,
752+
_main_module_package_metadata = getattr(arc_data, "_main_module_package_metadata", None),
753+
_main_module_main_workspace = getattr(arc_data, "_main_module_main_workspace", False),
745754
)
746755

747756
# If this archive needs to be recompiled, use go.archive.
@@ -767,6 +776,8 @@ def _recompile_external_deps(go, external_go_info, internal_archive, library_lab
767776
direct = [package_metadata] if package_metadata else [],
768777
transitive = [getattr(a, "_package_metadata_files", depset()) for a in deps],
769778
),
779+
_main_module_package_metadata = getattr(arc_data, "_main_module_package_metadata", None),
780+
_main_module_main_workspace = getattr(arc_data, "_main_module_main_workspace", False),
770781
)
771782
label_to_archive[label] = archive
772783

go/toolchains.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,7 @@ It returns a tuple containing GoArchive_, the output executable file, and
769769
a ``runfiles`` object.
770770

771771
.. |module metadata| replace:: :param:`buildinfo_module_metadata`
772+
.. |module provenance| replace:: :param:`buildinfo_module_main_workspace`
772773

773774
+--------------------------------+-----------------------------+-----------------------------------+
774775
| **Name** | **Type** | **Default value** |
@@ -815,6 +816,16 @@ a ``runfiles`` object.
815816
| Package metadata JSON file used to populate ``BuildInfo.Main``. Defaults to the metadata |
816817
| propagated by :param:`source`. |
817818
+--------------------------------+-----------------------------+-----------------------------------+
819+
| |module provenance| | :type:`bool` | :value:`False` |
820+
+--------------------------------+-----------------------------+-----------------------------------+
821+
| Whether :param:`buildinfo_module_metadata` comes from the main workspace. Inferred when the |
822+
| metadata defaults from :param:`source`; controls whether stamped ``vcs.*`` settings are emitted. |
823+
+--------------------------------+-----------------------------+-----------------------------------+
824+
| :param:`main_workspace` | :type:`bool` | :value:`True` |
825+
+--------------------------------+-----------------------------+-----------------------------------+
826+
| Whether the binary target comes from the main workspace. Set this to ``False`` for custom rules |
827+
| that link targets in external repositories so caller workspace VCS data is not attributed to it. |
828+
+--------------------------------+-----------------------------+-----------------------------------+
818829

819830

820831
link
@@ -867,6 +878,17 @@ It does not return anything.
867878
| Package metadata JSON file used to populate ``BuildInfo.Main``. Defaults to metadata propagated |
868879
| by :param:`archive`. |
869880
+--------------------------------+-----------------------------+-----------------------------------+
881+
| |module provenance| | :type:`bool` | :value:`False` |
882+
+--------------------------------+-----------------------------+-----------------------------------+
883+
| Whether :param:`buildinfo_module_metadata` comes from the main workspace. Inferred when the |
884+
| metadata defaults from :param:`archive`; controls whether stamped ``vcs.*`` settings are |
885+
| emitted. |
886+
+--------------------------------+-----------------------------+-----------------------------------+
887+
| :param:`main_workspace` | :type:`bool` | :value:`True` |
888+
+--------------------------------+-----------------------------+-----------------------------------+
889+
| Whether the linked target comes from the main workspace. Set this to ``False`` for external |
890+
| repository targets to avoid attributing caller workspace VCS data to them. |
891+
+--------------------------------+-----------------------------+-----------------------------------+
870892

871893

872894
args

go/tools/builders/BUILD.bazel

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ go_test(
104104
],
105105
)
106106

107+
go_test(
108+
name = "stamp_test",
109+
size = "small",
110+
srcs = [
111+
"stamp.go",
112+
"stamp_test.go",
113+
],
114+
)
115+
107116
go_test(
108117
name = "nogo_version_test",
109118
size = "small",
@@ -145,8 +154,10 @@ filegroup(
145154
"nogo_validation.go",
146155
"read.go",
147156
"replicate.go",
157+
"stamp.go",
148158
"stdlib.go",
149159
"stdliblist.go",
160+
"vcsstamp.go",
150161
] + select({
151162
"@bazel_tools//src/conditions:windows": ["path_windows.go"],
152163
"//conditions:default": ["path.go"],

go/tools/builders/builder.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ func main() {
5959
action = genTestMain
6060
case "link":
6161
action = link
62+
case "vcsstamp":
63+
action = vcsStamp
6264
case "gennogomain":
6365
action = genNogoMain
6466
case "stdlib":

0 commit comments

Comments
 (0)