Files
beads/build-docs.md

8.9 KiB

Noridoc: Build and Version Infrastructure

Path: @/ (root level - Makefile, .goreleaser.yml)

Overview

The beads project uses a coordinated build and version reporting system that ensures all installation methods (direct go install, make install, GitHub releases, Homebrew, npm) produce binaries with complete version information including git commit hash and branch name.

This infrastructure is critical for debugging, auditing, and user support - it allows anyone to identify exactly what code their binary was built from.

How it fits into the larger codebase

  • Build Entry Points: The Makefile and .goreleaser.yml are the authoritative build configurations that users and CI/CD systems interact with. They control how version information flows into binaries.

  • Version Pipeline: These files work with @/cmd/bd/version.go to establish the complete version reporting chain:

    • Build time: Extract git info via shell commands (Makefile) or goreleaser templates
    • Compilation: Pass info to Go compiler via -X ldflags
    • Runtime: Resolve functions in version.go retrieve and display the info
  • Installation Methods: The build configuration enables multiple installation paths while maintaining version consistency:

    • make install - Used by developers building from source
    • go install ./cmd/bd - Direct Go installation with embedded ldflag injection
    • GitHub releases - Goreleaser-built binaries for all platforms
    • Homebrew - Pre-built binaries installed via brew install bd
    • npm - Node.js package that downloads pre-built binaries via postinstall hook
    • ./scripts/install.sh - User-friendly build-from-source helper
  • Release Automation: Goreleaser configuration integrates with GitHub Actions and the release process documented in @/RELEASING.md, ensuring released binaries have full version info.

Core Implementation

Makefile (@/Makefile, lines 37-41 - install target):

The install target is the primary development build mechanism:

install:
	@echo "Installing bd to $$(go env GOPATH)/bin..."
	@bash -c 'commit=$$(git rev-parse HEAD 2>/dev/null || echo ""); \
		branch=$$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo ""); \
		go install -ldflags="-X main.Commit=$$commit -X main.Branch=$$branch" ./cmd/bd'

How it works:

  1. Uses bash subshell to extract git information at install time
  2. git rev-parse HEAD gets the full commit hash
  3. git rev-parse --abbrev-ref HEAD gets the current branch name
  4. Passes both as ldflags to go install using the -X flag (variable assignment)
  5. The ldflags set main.Commit and main.Branch package variables
  6. These variables are then retrieved by functions in @/cmd/bd/version.go at runtime

Key implementation detail: The original target depended on build, but this was removed because go install is sufficient and handles compilation itself.

Goreleaser Configuration (@/.goreleaser.yml, lines 11-95):

Goreleaser builds binaries for all platforms and sets version information via ldflags. Each of the 5 build configurations uses identical ldflag patterns:

ldflags:
  - -s -w
  - -X main.Version={{.Version}}
  - -X main.Build={{.ShortCommit}}
  - -X main.Commit={{.Commit}}
  - -X main.Branch={{.Branch}}

Platform configurations:

  1. bd-linux-amd64 (lines 12-26): Linux 64-bit Intel
  2. bd-linux-arm64 (lines 28-44): Linux 64-bit ARM (Apple Silicon support)
  3. bd-darwin-amd64 (lines 46-60): macOS 64-bit Intel
  4. bd-darwin-arm64 (lines 62-76): macOS 64-bit ARM (M1/M2/M3)
  5. bd-windows-amd64 (lines 78-95): Windows 64-bit Intel with additional -buildmode=exe flag

The ldflags explained:

  • -s -w: Strip debug symbols to reduce binary size
  • -X main.Version: Semantic version (e.g., "0.29.0") from git tag
  • -X main.Build: Short commit hash for quick reference
  • -X main.Commit: Full commit hash for precise identification
  • -X main.Branch: Branch name for build context

Goreleaser template variables:

  • {{.Version}}: The release version from git tag
  • {{.ShortCommit}}: First 7 characters of commit (used for Build variable)
  • {{.Commit}}: Full commit hash
  • {{.Branch}}: Current git branch

Installation Script (@/scripts/install.sh):

Provides a user-friendly way to build from source with full version info:

  • Extracts git commit and branch
  • Calls go install with the same ldflags pattern as Makefile
  • Immediately verifies installation by running bd version

Version Resolution Chain (@/cmd/bd/version.go, lines 116-163):

The version.go file implements functions that retrieve the injected information:

  1. resolveCommitHash() (lines 116-130):

    • First checks if Commit package variable was set via ldflag (most reliable)
    • Falls back to runtime/debug.ReadBuildInfo() to extract VCS info automatically embedded by go build
    • Returns empty string if neither source has data
  2. resolveBranch() (lines 139-163):

    • First checks if Branch package variable was set via ldflag (most reliable)
    • Falls back to runtime/debug.ReadBuildInfo() to extract VCS branch info
    • Falls back to git symbolic-ref --short HEAD for runtime detection
    • Returns empty string if none available
  3. Output Formatting (lines 52-58):

    • Displays commit and branch in human-readable format: bd version 0.29.0 (dev: main@7e70940)

Things to Know

Critical Design Decision - Why Explicit Ldflags:

The Go toolchain (as of 1.18+) can automatically embed VCS information when compiling with go build, but this does NOT happen with go install. This creates an asymmetry:

  • go build ./cmd/bd → automatically embeds vcs.revision and vcs.branch
  • go install ./cmd/bd → does NOT embed VCS info automatically

The solution is to explicitly pass git information as ldflags in all build configurations. This ensures:

  • Users who run make install get full version info
  • Users who run go install ./cmd/bd need to explicitly set ldflags (via Makefile or script)
  • Released binaries from goreleaser have full version info (handled by goreleaser templates)
  • The version command is consistent regardless of installation method

Issue #503 Root Cause:

The original system relied on Go's automatic VCS embedding which only works with go build. When released binaries (built via goreleaser) or installed binaries (via go install) came without explicit ldflags, the bd version command couldn't report commit and branch information.

The fix adds explicit ldflag injection at all build points, creating a reliable pipeline independent of Go's automatic VCS embedding feature.

Ldflag Variable Names:

The variables in @/cmd/bd/version.go (lines 15-23) must match the ldflag paths in build configurations:

  • main.Version → Version variable
  • main.Build → Build variable
  • main.Commit → Commit variable
  • main.Branch → Branch variable

These are fully qualified with the package name (main) because the ldflag syntax is: -X package.Variable=value

Build-Time vs Runtime Information:

  • Build-Time (injected via ldflags): Git commit and branch at the moment of compilation

    • Most reliable and consistent
    • Does not change after binary is created
    • Reflects the exact code in the binary
  • Runtime (fallback via git commands): Current branch of the source directory

    • Used only if build-time info is not available
    • Can differ from build-time info (e.g., if working directory changes)
    • Useful for development but less reliable

Release Process Integration:

The build configuration integrates with @/RELEASING.md:

  1. Version tag is pushed to GitHub (e.g., v0.29.0)
  2. GitHub Actions/goreleaser automatically builds binaries for all platforms
  3. Goreleaser uses git metadata to populate {{.Commit}} and {{.Branch}} template variables
  4. Released binaries include full version info
  5. Users installing via any channel get consistent version reporting

Testing Version Information:

The test file @/cmd/bd/version_test.go includes:

  • TestResolveCommitHash: Verifies ldflag values are prioritized
  • TestResolveBranch: Verifies ldflag values are prioritized
  • TestVersionOutputWithCommitAndBranch: Verifies output formatting with real values

These tests simulate build-time injection by directly setting the package variables, ensuring the resolution chain works correctly.

Multi-Platform Consistency:

All 5 goreleaser build configurations use identical ldflag patterns. This ensures:

  • macOS (Intel and ARM) binaries have full version info
  • Linux (Intel and ARM) binaries have full version info
  • Windows binaries have full version info
  • Users on any platform have equal access to version information

Dependency Requirements:

  • Makefile: Requires git and bash to be available at install time
  • Goreleaser: Requires git tags and repository metadata (automatic in CI/CD)
  • Scripts: Require git and go in PATH

All are standard tools in development and CI/CD environments.

Created and maintained by Nori.