Skip to content

Conversation

@jsharpe
Copy link
Member

@jsharpe jsharpe commented Nov 7, 2025

Implements Go 1.18+ runtime/debug.BuildInfo embedding in Bazel-built binaries with accurate dependency versions collected from Gazelle-generated package_info targets.

This change introduces a buildinfo aspect that traverses the dependency graph and collects version metadata via the package_metadata common attribute (set by go_repository's default_package_metadata in REPO.bazel files). The aspect follows the bazel-contrib/supply-chain gather_metadata pattern.

Features:

  • Collects real module versions (e.g., v1.2.3) from package_info metadata
  • Replaces (devel) sentinels with actual versions from package_info
  • Supports both package_metadata (Bazel 5.4+) and applicable_licenses (legacy)
  • Filters out internal monorepo packages to avoid invalid version strings
  • Embeds build settings (GOOS, GOARCH, CGO_ENABLED, etc.) in BuildInfo
  • Makes dependency information accessible via runtime/debug.ReadBuildInfo()

Implementation details:

  • New buildinfo_aspect.bzl traverses all dependencies using attr_aspects=["*"]
  • Version map file aggregates importpath→version tuples and passes to linker
  • Link builder merges version map with buildinfo dependency list
  • Follows supply-chain memory optimization pattern for depset handling
  • Includes modinfo.go and buildinfo.go from Go stdlib for metadata generation

Limitations:

  • Module checksums are not included as they are not available in package_metadata

This enables tools like go version -m and OpenTelemetry instrumentation to properly identify dependencies in Bazel-built binaries.

Fixes #3090
Fixes #4159

🤖 Generated with Claude Code

Implements Go 1.18+ runtime/debug.BuildInfo embedding in Bazel-built binaries
with accurate dependency versions collected from Gazelle-generated package_info
targets.

This change introduces a buildinfo aspect that traverses the dependency graph
and collects version metadata via the package_metadata common attribute
(set by go_repository's default_package_metadata in REPO.bazel files).
The aspect follows the bazel-contrib/supply-chain gather_metadata pattern.

Features:
- Collects real module versions (e.g., v1.2.3) from package_info metadata
- Replaces (devel) sentinels with actual versions from package_info
- Supports both package_metadata (Bazel 5.4+) and applicable_licenses (legacy)
- Filters out internal monorepo packages to avoid invalid version strings
- Embeds build settings (GOOS, GOARCH, CGO_ENABLED, etc.) in BuildInfo
- Makes dependency information accessible via runtime/debug.ReadBuildInfo()

Implementation details:
- New buildinfo_aspect.bzl traverses all dependencies using attr_aspects=["*"]
- Version map file aggregates importpath→version tuples and passes to linker
- Link builder merges version map with buildinfo dependency list
- Follows supply-chain memory optimization pattern for depset handling
- Includes modinfo.go and buildinfo.go from Go stdlib for metadata generation

Limitations:
- Module checksums are not included as they are not available in package_metadata

This enables tools like `go version -m` and OpenTelemetry instrumentation
to properly identify dependencies in Bazel-built binaries.

Fixes bazel-contrib#3090
Fixes bazel-contrib#4159

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
# Add transitive dependencies
for dep_path, dep_version in getattr(d.data, "_buildinfo_deps", ()):
if dep_path not in dep_buildinfo_set:
dep_buildinfo.append((dep_path, dep_version))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this result in potentially quadratic total memory usage for deeper dependency trees? Would it be possible to instead track separate depsets of importpaths and depsets of package metadata providers and doing the matching at execution time in top-level rules only?

buildinfo_content += "dep\t{}\t{}\n".format(dep_path, dep_version)

go.actions.write(
output = buildinfo_file,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we track info as depsets (see comment above), we could materialize that into a file at execution time using TemplateDict or Args. I can take over that part if you want.

def _buildinfo_aspect_impl(target, ctx):
"""Collects package_info metadata from dependencies.
Following bazel-contrib/supply-chain gather_metadata pattern, this checks:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Yannic I haven't followed this repo lately, are there any bits that rulesets can reuse rather than adapt?

"""
direct_versions = []

# Approach 1: Check package_metadata common attribute (Bazel 5.4+)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only support Bazel 6 and even that will be dropped soon. We could skip the fallback.

# Collect metadata from transitive dependencies (supply-chain pattern)
transitive_depsets = []

# Traverse all attributes (supply-chain uses attr_aspects = ["*"])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we only want to collect info on non-exec Go deps, we should limit collection to deps and embed (everything that accepts GoInfo). Restricting in this way would greatly simplify the logic below.

# No metadata at all, return empty provider
return [BuildInfoMetadata(version_map = depset([]))]

if not direct_versions and len(transitive_depsets) == 1:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This optimization is unnecessary, the depset constructor already does this (and more)

attr_aspects = ["*"], # Traverse all attributes (supply-chain pattern)
provides = [BuildInfoMetadata],
# Apply to generated targets including package_info
apply_to_generating_rules = True,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think that we don't want this (e.g. don't traverse code generators that produce .go files in sources), but I may be missing a use case.

version_dict[importpath] = version

# Write tab-separated file
lines = ["{}\t{}".format(imp, ver) for imp, ver in sorted(version_dict.items())]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also do this lazily.

buildSettingAppend("-compiler", cfg.compiler)
buildSettingAppend("-buildmode", cfg.buildMode)
if cfg.pgoProfilePath != "" {
buildSettingAppend("-pgo", cfg.pgoProfilePath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to check that these paths aren't absolute. It would be very easy to accidentally destroy hermeticity that way. This also applies to other flags below.

)

func ModInfoData(info string) []byte {
return []byte(string(infoStart) + info + string(infoEnd))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of back and forth conversion here and at the call site. Are the magic numbers UTF-8 strings? We should either choose []byte and append or string and + and convert at most once.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

how to get the dependencies and their versions for go executables built with bazel Go 1.18 BuildInfo support

2 participants