From f4380518e66bef21955ba7b9f6befa983afadb7f Mon Sep 17 00:00:00 2001 From: "Donovan C. Young" Date: Sun, 2 Aug 2026 12:02:50 -0400 Subject: [PATCH 1/2] feat: inject git describe into dev builds for --version (#208) make build now stamps `git describe --tags --dirty` into the binary via -ldflags -X, so a develop-branch build self-identifies (e.g. "1.28.0 (dev: v1.28.0-2-g140e3c6-dirty)") instead of showing the same plain version as a release build. A clean build of the release tag, or a plain go build/go test without the Makefile's ldflags, is unaffected. The static version var (and man pages/genman) stay untouched by construction. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 1 + Makefile | 5 ++-- README.md | 6 +++++ cmd/lmm/root.go | 22 +++++++++++++++- cmd/lmm/version_test.go | 56 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 cmd/lmm/version_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 41085aff..09b7dee1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `lmm game list` — a table of every configured game (ID, name, install path, mod path, deploy mode, and a compact `source:id` rendering of its sources), marking the default game (see `lmm game show-default`) and pointing at `lmm game add`/`lmm game detect` when nothing is configured yet. Supports `--json` like `list`/`search`/`source list` (#205) +- `make build` (and `make`) now stamp `git describe --tags --dirty` into the binary via `-ldflags -X`, so `lmm --version` on a dev build self-identifies (e.g. `1.28.0 (dev: v1.28.0-2-g140e3c6-dirty)`) instead of showing the same plain `1.28.0` as a release build — the confusion this fixed came up during the v1.28.0 cycle. A build exactly on the release tag (clean) still shows the plain version; a plain `go build`/`go test` (no ldflags) is unaffected. The static `version` var and man pages are untouched by construction (#208) ### Changed diff --git a/Makefile b/Makefile index 185c5a26..35c76ee2 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,8 @@ BINARY_NAME := lmm BUILD_DIR := ./build MAIN_PATH := ./cmd/lmm VERSION := $(shell grep 'version = ' cmd/lmm/root.go | cut -d'"' -f2) -LDFLAGS := -ldflags "-s -w" +DESCRIBE := $(shell git describe --tags --dirty 2>/dev/null) +LDFLAGS := -ldflags "-s -w -X main.buildDescribe=$(DESCRIBE)" # Project-local Go cache so tests run in sandboxed environments (e.g. CI, Cursor) GOCACHE_LOCAL := $(CURDIR)/.go-mod/cache # Trunk cache under project for sandbox-friendly lint @@ -16,7 +17,7 @@ all: build ## build: Build the binary build: - @echo "Building $(BINARY_NAME) v$(VERSION)..." + @echo "Building $(BINARY_NAME) v$(VERSION) ($(DESCRIBE))..." @go build $(LDFLAGS) -o $(BINARY_NAME) $(MAIN_PATH) ## build-debug: Build with debug symbols diff --git a/README.md b/README.md index f11ea5a8..049d6548 100644 --- a/README.md +++ b/README.md @@ -1122,6 +1122,12 @@ go vet ./... go build -o lmm ./cmd/lmm ``` +`make build` (or `make`) is the preferred way to build a working binary: it +stamps `git describe --tags --dirty` into the binary, so `lmm --version` on +a dev build self-identifies as e.g. `1.28.0 (dev: v1.28.0-2-g140e3c6-dirty)` +instead of silently claiming the last released version. A plain +`go build`/`go test` (no ldflags) behaves exactly like a clean release build. + ## License MIT License - See [LICENSE](LICENSE) for details. diff --git a/cmd/lmm/root.go b/cmd/lmm/root.go index 12bfcac9..0c6fda30 100644 --- a/cmd/lmm/root.go +++ b/cmd/lmm/root.go @@ -41,6 +41,12 @@ var ErrReported = errors.New("already reported") var ( version = "1.28.0" + // buildDescribe is injected at build time via -ldflags -X (see the + // Makefile's `build` target) with `git describe --tags --dirty`. + // Empty for builds made without that ldflags (plain `go build`/`go + // test`), which display identically to a clean release build. + buildDescribe = "" + // Global flags configDir string dataDir string @@ -78,11 +84,25 @@ FILES cache/ (downloaded and extracted mod files), and downloads/ (staging area for in-flight downloads and archive extraction). Override with --data.`, - Version: version, + Version: computeDisplayVersion(version, buildDescribe), SilenceUsage: true, // Runtime errors should not print usage SilenceErrors: true, // We handle error output in Execute() } +// computeDisplayVersion returns the version string shown to the user for +// `--version`: ver unadorned when buildDescribe is empty (no ldflags, e.g. +// `go build`/`go test`) or matches "v"+ver exactly (a clean build of the +// release tag itself), otherwise ver with buildDescribe's git-describe +// provenance appended - a "-dirty" suffix from `git describe --dirty` +// passes through inside that unchanged. JSON surfaces must keep emitting +// the static `version` var, not this - only human-facing display uses it. +func computeDisplayVersion(ver, describe string) string { + if describe == "" || describe == "v"+ver { + return ver + } + return fmt.Sprintf("%s (dev: %s)", ver, describe) +} + func init() { // Persistent flags available to all commands rootCmd.PersistentFlags().StringVar(&configDir, "config", "", "config directory (default: ~/.config/lmm)") diff --git a/cmd/lmm/version_test.go b/cmd/lmm/version_test.go new file mode 100644 index 00000000..b0e6aaf8 --- /dev/null +++ b/cmd/lmm/version_test.go @@ -0,0 +1,56 @@ +package main + +import "testing" + +// TestDisplayVersion pins the dev-provenance display logic (#208): a build +// exactly on the release tag (or one made without the Makefile's ldflags, +// where buildDescribe is empty) shows the plain static version; anything +// else appends the git-describe provenance, dirty suffix included verbatim. +func TestDisplayVersion(t *testing.T) { + tests := []struct { + name string + version string + buildDescribe string + want string + }{ + { + name: "empty buildDescribe (go build/go test without ldflags)", + version: "1.28.0", + buildDescribe: "", + want: "1.28.0", + }, + { + name: "clean tag build matches v+version exactly", + version: "1.28.0", + buildDescribe: "v1.28.0", + want: "1.28.0", + }, + { + name: "ahead of the tag", + version: "1.28.0", + buildDescribe: "v1.28.0-2-g140e3c6", + want: "1.28.0 (dev: v1.28.0-2-g140e3c6)", + }, + { + name: "dirty worktree on the tag itself", + version: "1.28.0", + buildDescribe: "v1.28.0-dirty", + want: "1.28.0 (dev: v1.28.0-dirty)", + }, + { + name: "ahead of the tag and dirty", + version: "1.28.0", + buildDescribe: "v1.28.0-2-g140e3c6-dirty", + want: "1.28.0 (dev: v1.28.0-2-g140e3c6-dirty)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := computeDisplayVersion(tt.version, tt.buildDescribe) + if got != tt.want { + t.Errorf("computeDisplayVersion(%q, %q) = %q, want %q", tt.version, tt.buildDescribe, got, tt.want) + } + }) + } +} From 726d99f847aee570ce32ecdd846bf66bf750130e Mon Sep 17 00:00:00 2001 From: "Donovan C. Young" Date: Sun, 2 Aug 2026 12:16:03 -0400 Subject: [PATCH 2/2] fix: handle tag-less clones and empty DESCRIBE in Makefile Add --always to git describe so shallow or tag-less clones stamp a commit hash instead of silently matching a release build. Also skip the empty "()" in the Building echo when DESCRIBE is unset via $(if ...). computeDisplayVersion already renders a bare hash sensibly as "dev: "; added a test case to pin it. Co-Authored-By: Claude Sonnet 5 --- Makefile | 4 ++-- cmd/lmm/version_test.go | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 35c76ee2..9c66b1af 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BINARY_NAME := lmm BUILD_DIR := ./build MAIN_PATH := ./cmd/lmm VERSION := $(shell grep 'version = ' cmd/lmm/root.go | cut -d'"' -f2) -DESCRIBE := $(shell git describe --tags --dirty 2>/dev/null) +DESCRIBE := $(shell git describe --tags --always --dirty 2>/dev/null) LDFLAGS := -ldflags "-s -w -X main.buildDescribe=$(DESCRIBE)" # Project-local Go cache so tests run in sandboxed environments (e.g. CI, Cursor) GOCACHE_LOCAL := $(CURDIR)/.go-mod/cache @@ -17,7 +17,7 @@ all: build ## build: Build the binary build: - @echo "Building $(BINARY_NAME) v$(VERSION) ($(DESCRIBE))..." + @echo "Building $(BINARY_NAME) v$(VERSION)$(if $(DESCRIBE), ($(DESCRIBE)))..." @go build $(LDFLAGS) -o $(BINARY_NAME) $(MAIN_PATH) ## build-debug: Build with debug symbols diff --git a/cmd/lmm/version_test.go b/cmd/lmm/version_test.go index b0e6aaf8..52845f69 100644 --- a/cmd/lmm/version_test.go +++ b/cmd/lmm/version_test.go @@ -43,6 +43,12 @@ func TestDisplayVersion(t *testing.T) { buildDescribe: "v1.28.0-2-g140e3c6-dirty", want: "1.28.0 (dev: v1.28.0-2-g140e3c6-dirty)", }, + { + name: "tag-less clone falls back to a bare hash (git describe --always)", + version: "1.28.0", + buildDescribe: "140e3c6", + want: "1.28.0 (dev: 140e3c6)", + }, } for _, tt := range tests {