You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

104 lines
5.4 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

---
draft: false
title: "Stamping Builds with Bazel"
aliases: ["Stamping Builds with Bazel"]
series: ["building-with-bazel"]
series_order: 3
date: "2024-05-15"
author: "Nick Dumas"
cover: ""
keywords: ["", ""]
description: "Versioning is a critical part of delivering software to users. With bazel, you can derive per-build values and inject them anywhere in your build process."
showFullContent: false
tags:
- bazel
- golang
---
## What am I Doing?
In my [last post](/2024/09/the-joy-of-versioning/) I spent some time talking about how more rigorous versioning helped reduce wasted time debugging and upgrading code as it started getting broken into lots of little pieces.
That post was pretty light on direct `bazel` usage but I promise, it'll pay off. Here, we're going to cover how to use these tags *in* your builds to tag docker images or inject build information into compiled binaries.
I'm assuming that you've read the [first bazel post](/2023/08/beautiful-builds-with-bazel/) in this series, or that you've already got your bazel + bzlmod setup going.
## Stamping and you
Bazel includes functionality that it calls "stamping". Bazel has to separate this into its own conceptual space because one of the core design principles is build reproducibility: for bazel's caching to work, inputs have to be deterministic and ideally change infrequently between runs.
Bazel's [documentation](https://bazel.build/docs/user-manual#workspace-status) covers the bare essentials, with a small caveat. Stamping requires a script , the "workplace status" script, that emits space-separated key-value pairs, e.g. `STABLE_KEY_NAME VALUE`. An example script is included below.
```bash {title="tools/workspace_status.sh"}
#! /usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
echo "STABLE_STAMP_VERSION $(git describe --tags --dirty=-dev)"
echo "STABLE_STAMP_COMMIT $(git rev-parse HEAD)"
echo "STABLE_STAMP_BRANCH $(git rev-parse --abbrev-ref HEAD)"
```
One important detail that the documentation doesn't cover is that your workspace status script **cannot** live in the root of your bazel project. You have two options:
- Place the status script somewhere in your `$PATH`
- Place the status script in a subdirectory
Still not sure why, but if you simply do `bazel --workplace_status_command=status.sh`, `bazel` will *only* look for it in your `$PATH`.
## Build Injection
Using the variables created by your workspace status script ends up being incredibly simple, if you're using `rules_go`. the `x_defs` parameter lets you override values at compile-time, exactly for cases like this.
```
x_defs = {
       "Version": "{STABLE_STAMP_VERSION}",
       "Build": "{STABLE_STAMP_COMMIT}",
   },
```
This is equivalent to the Go build flag `-ldflags "-X PACKAGENAME.Version=whatever -X PACKAGENAME.BUILD=whatever"`. It's important to note that the raw Go flags require a fully qualified package name be specified. Bazel is smart enough to derive the necessary package name on its own, all you have to do is tell it which variable needs to be overriden with what value.
## Putting it all together
The final, full invocation for stamping your builds should look something like this.
```
wikilink-obsidian-resolver on  main [⇡] via 🐹 v1.22.2
bazel run --stamp --workspace_status_command=tools/workspace_status.sh //cmd/version
INFO: Analyzed target //cmd/version:version (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //cmd/version:version up-to-date:
dist/bin/cmd/version/version_/version
INFO: Elapsed time: 1.262s, Critical Path: 1.10s
INFO: 2 processes: 1 internal, 1 darwin-sandbox.
INFO: Build completed successfully, 2 total actions
INFO: Running command line: dist/bin/cmd/version/version_/version
Version: v0.1.4-1-g5792d62
Build: 5792d623fc9fc1852aeb09dd008eabb640cb6711
```
Bazel runs your binary, injects variables generated by your workspace_status script, and it all finally comes together.
The stamping also works for builds:
```
wikilink-obsidian-resolver on  main [⇡] via 🐹 v1.22.2
bazel build --stamp --workspace_status_command=tools/workspace_status.sh //cmd/version
INFO: Analyzed target //cmd/version:version (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //cmd/version:version up-to-date:
dist/bin/cmd/version/version_/version
INFO: Elapsed time: 0.246s, Critical Path: 0.06s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
wikilink-obsidian-resolver on  main [⇡] via 🐹 v1.22.2
./dist/bin/cmd/version/version_/version
Version: v0.1.4-1-g5792d62
Build: 5792d623fc9fc1852aeb09dd008eabb640cb6711
```
## What next?
Stamping is not going to accomplish any of my goals on its own, but it was an important part of preparing my toolkit. If I'm going to release software for other people to consume, I need versioning. Injecting a git tag into a Go binary is a trivial proof of concept; I'll be using this to automatically tag and push OCI images and probably other stuff I haven't come up with yet.
## Notes, Warnings, Caveats
- Important: the `workspace-status-script` *cannot* live in the root of the bazel project. It has to be in a subdirectory for some reason.
- If you're defining `x_defs` on a `rules_go` `go_library`, you cannot fully qualify the variable names.
- To find where bazel places artifacts, use `bazel cquery --output=files`