Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 --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
# Trunk cache under project for sandbox-friendly lint
Expand All @@ -16,7 +17,7 @@ all: build

## build: Build the binary
build:
@echo "Building $(BINARY_NAME) v$(VERSION)..."
@echo "Building $(BINARY_NAME) v$(VERSION)$(if $(DESCRIBE), ($(DESCRIBE)))..."
@go build $(LDFLAGS) -o $(BINARY_NAME) $(MAIN_PATH)

## build-debug: Build with debug symbols
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 21 additions & 1 deletion cmd/lmm/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)")
Expand Down
62 changes: 62 additions & 0 deletions cmd/lmm/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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)",
},
{
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 {
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)
}
})
}
}
Loading