Files
beads/build-docs.md

189 lines
8.9 KiB
Markdown

# 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:
```makefile
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:
```yaml
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.