From 9199444d115078b90b18a2b42689cd74ad0b6bf7 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Wed, 14 Oct 2026 00:00:35 +0200 Subject: [PATCH] Take the version from the git tag, not a constant (closes #80) `mfer version`, `mfer --version` and the banner print the revision the build stamps into main.Gitrev or, when it stamped none (as after `go install` of a tag), the main module's version from runtime/debug.ReadBuildInfo. The banner no longer shows a release date. mfer.Version and mfer.ReleaseDate go, with main.Version and the version field and parameter in internal/cli that nothing read. A program using the library reads the module version from its own build information. The README says where the version comes from. Judgement call: a binary with no build information, or none holding a module version, prints "unknown", the word script/build already uses when git describe gives nothing. Model: opus-5-5 --- README.md | 9 +++++++++ cmd/mfer/main.go | 10 ++++------ cmd/mfer/main_test.go | 7 +++---- internal/cli/entry.go | 11 ++++------ internal/cli/entry_test.go | 41 ++++++++++++++++++++++++++++++++------ internal/cli/mfer.go | 25 ++++++++++++++++------- mfer/constants.go | 6 ------ 7 files changed, 73 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index b8482ee..7635a59 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,15 @@ The current version is pre-1.0 and while the repo was published in 2022, there has not yet been any versioned release. [SemVer](https://semver.org) will be used for releases. +A version is a git tag, and no file in the tree records a version number. +`mfer version` prints the revision the build stamped into the binary, which for +`make build` and the Docker image is the output of `git describe` on the +checkout they build. A binary built without that stamp, as +`go install sneak.berlin/go/mfer/cmd/mfer@` builds it, prints the module +version Go records in it instead. A program that imports the `mfer` package +finds the version it was built against in its own build information +(`runtime/debug.ReadBuildInfo`). + This project was started by [@sneak](https://sneak.berlin) to scratch an itch in 2022 and is currently a one-person effort, though the goal is for this to emerge as a de-facto standard and be incorporated into other software. A compatible diff --git a/cmd/mfer/main.go b/cmd/mfer/main.go index 6c8fb04..94630cf 100644 --- a/cmd/mfer/main.go +++ b/cmd/mfer/main.go @@ -10,14 +10,12 @@ import ( // Appname is the name of this program. const Appname = "mfer" -// Version and Gitrev are injected at build time via -ldflags. +// Gitrev is the revision `mfer version` prints, injected at build time via +// -ldflags. // //nolint:gochecknoglobals // set via ldflags at build time -var ( - Version string - Gitrev string -) +var Gitrev string func main() { - os.Exit(cli.Run(Appname, Version, Gitrev)) + os.Exit(cli.Run(Appname, Gitrev)) } diff --git a/cmd/mfer/main_test.go b/cmd/mfer/main_test.go index c5d3d99..368c3d3 100644 --- a/cmd/mfer/main_test.go +++ b/cmd/mfer/main_test.go @@ -15,11 +15,10 @@ func TestAppname(t *testing.T) { assert.Equal(t, "mfer", Appname) } -// TestVersionDefaults documents that Version and Gitrev are empty unless -// injected at build time via -ldflags. -func TestVersionDefaults(t *testing.T) { +// TestGitrevDefault documents that Gitrev is empty unless injected at build +// time via -ldflags. +func TestGitrevDefault(t *testing.T) { t.Parallel() - assert.Empty(t, Version) assert.Empty(t, Gitrev) } diff --git a/internal/cli/entry.go b/internal/cli/entry.go index 19d2681..3e4aa73 100644 --- a/internal/cli/entry.go +++ b/internal/cli/entry.go @@ -26,8 +26,7 @@ func noColorEnvSet() bool { // Use DefaultRunOptions for standard CLI execution, or construct manually for testing. type RunOptions struct { Appname string // Application name displayed in help and version output - Version string // Version string (typically set at build time) - Gitrev string // Git revision hash (typically set at build time) + Gitrev string // Revision stamped in at build time, or empty Args []string // Command-line arguments (typically os.Args) Stdin io.Reader // Standard input stream Stdout io.Writer // Standard output stream @@ -36,10 +35,9 @@ type RunOptions struct { } // DefaultRunOptions returns RunOptions configured for normal CLI execution. -func DefaultRunOptions(appname, version, gitrev string) *RunOptions { +func DefaultRunOptions(appname, gitrev string) *RunOptions { return &RunOptions{ Appname: appname, - Version: version, Gitrev: gitrev, Args: os.Args, Stdin: os.Stdin, @@ -50,15 +48,14 @@ func DefaultRunOptions(appname, version, gitrev string) *RunOptions { } // Run creates and runs the CLI application with default options. -func Run(appname, version, gitrev string) int { - return RunWithOptions(DefaultRunOptions(appname, version, gitrev)) +func Run(appname, gitrev string) int { + return RunWithOptions(DefaultRunOptions(appname, gitrev)) } // RunWithOptions creates and runs the CLI application with the given options. func RunWithOptions(opts *RunOptions) int { m := &CLIApp{ appname: opts.Appname, - version: opts.Version, gitrev: opts.Gitrev, exitCode: 0, maxManifestSize: mfer.MaxManifestSize, diff --git a/internal/cli/entry_test.go b/internal/cli/entry_test.go index 2d35d92..705d4d2 100644 --- a/internal/cli/entry_test.go +++ b/internal/cli/entry_test.go @@ -11,6 +11,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "runtime/debug" "slices" "strings" "sync" @@ -27,6 +28,7 @@ import ( const ( testApp = "mfer" + testGitrev = "abc123" testDir = "/testdir" testFile1 = "/testdir/file1.txt" testMF = "/testdir/test.mf" @@ -94,8 +96,7 @@ func TestBuild(t *testing.T) { func testOpts(args []string, fs afero.Fs) *RunOptions { return &RunOptions{ Appname: testApp, - Version: "1.0.0", - Gitrev: "abc123", + Gitrev: testGitrev, Args: args, Stdin: &bytes.Buffer{}, Stdout: &bytes.Buffer{}, @@ -137,10 +138,35 @@ func TestVersionCommand(t *testing.T) { exitCode := runCLI(opts) assert.Equal(t, 0, exitCode) + assert.Equal(t, "mfer version "+testGitrev+"\n", testStdout(t, opts)) +} - stdout := testStdout(t, opts) - assert.Contains(t, stdout, mfer.Version) - assert.Contains(t, stdout, "abc123") +// TestVersionFromBuildInfo covers a binary whose build stamped no Gitrev, +// as `go install` builds it: mfer version prints the main module's version +// from the build information instead. +func TestVersionFromBuildInfo(t *testing.T) { + t.Parallel() + + info, ok := debug.ReadBuildInfo() + require.True(t, ok) + require.NotEmpty(t, info.Main.Version) + + opts := testOpts([]string{testApp, cmdVersion}, afero.NewMemMapFs()) + opts.Gitrev = "" + + require.Equal(t, 0, runCLI(opts)) + assert.Equal(t, "mfer version "+info.Main.Version+"\n", testStdout(t, opts)) +} + +// TestModuleVersionEmpty covers build information that holds no module +// version, as in a build with GO111MODULE=off, and a binary with no build +// information at all. Both report "unknown": an empty version would print +// nothing after "mfer version" and drop the --version flag. +func TestModuleVersionEmpty(t *testing.T) { + t.Parallel() + + assert.Equal(t, "unknown", moduleVersion(&debug.BuildInfo{}, true)) + assert.Equal(t, "unknown", moduleVersion(nil, false)) } // TestVFlagCollision covers the -v/--verbose vs --version flag interaction @@ -166,7 +192,7 @@ func TestVFlagCollision(t *testing.T) { exitCode := runCLI(opts) assert.Equal(t, 0, exitCode, "stderr: %s", testStderr(t, opts)) - assert.Contains(t, testStdout(t, opts), mfer.Version) + assert.Contains(t, testStdout(t, opts), testGitrev) assert.NotContains(t, testStderr(t, opts), "two forms of the same flag") }) } @@ -746,6 +772,9 @@ func TestBannerOutput(t *testing.T) { stdout := testStdout(t, opts) assert.Contains(t, stdout, "___") assert.Contains(t, stdout, "\\") + + // The banner shows the version mfer version prints, and nothing after it. + assert.Contains(t, stdout, "mfer by @sneak: "+testGitrev+"\n") } func TestUnknownCommand(t *testing.T) { diff --git a/internal/cli/mfer.go b/internal/cli/mfer.go index b3fe903..d609710 100644 --- a/internal/cli/mfer.go +++ b/internal/cli/mfer.go @@ -6,12 +6,12 @@ import ( "fmt" "io" "os" + "runtime/debug" "time" "github.com/spf13/afero" "github.com/urfave/cli/v3" "sneak.berlin/go/mfer/internal/log" - "sneak.berlin/go/mfer/mfer" ) // Command and flag names shared across command definitions and tests. @@ -58,7 +58,6 @@ var errUnexpectedArgument = errors.New("unexpected argument") //nolint:revive // established name used throughout the codebase and tests type CLIApp struct { appname string - version string gitrev string startupTime time.Time exitCode int @@ -87,13 +86,26 @@ const banner = ` \ \:\ \ \:\ \ \::/ \ \:\ \__\/ \__\/ \__\/ \__\/` -// VersionString returns the version and git revision formatted for display. +// VersionString returns the version mfer reports: the revision the build +// stamped into main.Gitrev or, when it stamped none, as with `go install`, +// the main module's version from the binary's build information. func (mfa *CLIApp) VersionString() string { if mfa.gitrev != "" { - return fmt.Sprintf("%s (%s)", mfer.Version, mfa.gitrev) + return mfa.gitrev } - return mfer.Version + return moduleVersion(debug.ReadBuildInfo()) +} + +// moduleVersion returns the main module's version from the binary's build +// information, or "unknown" when the binary has none or it holds no version, +// as in a build with GO111MODULE=off. +func moduleVersion(info *debug.BuildInfo, ok bool) string { + if !ok || info.Main.Version == "" { + return "unknown" + } + + return info.Main.Version } // printVersion writes the version line shared by the --version flag and the @@ -106,8 +118,7 @@ func (mfa *CLIApp) printBanner() { if log.GetLevel() <= log.InfoLevel { _, _ = fmt.Fprintln(mfa.Stdout, banner) _, _ = fmt.Fprintf(mfa.Stdout, - " mfer by @sneak: v%s released %s\n", - mfer.Version, mfer.ReleaseDate) + " mfer by @sneak: %s\n", mfa.VersionString()) _, _ = fmt.Fprintln(mfa.Stdout, " https://sneak.berlin/go/mfer") } } diff --git a/mfer/constants.go b/mfer/constants.go index f5005ef..ef632d5 100644 --- a/mfer/constants.go +++ b/mfer/constants.go @@ -1,12 +1,6 @@ package mfer const ( - // Version is the current mfer release version. - Version = "0.1.0" - - // ReleaseDate is the date on which Version was released. - ReleaseDate = "2025-12-17" - // MaxDecompressedSize is the maximum allowed size of decompressed manifest // data (256 MiB). This prevents decompression bombs from consuming excessive // memory.