diff --git a/go/sdkversion/sdkversion.go b/go/sdkversion/sdkversion.go new file mode 100644 index 0000000..46a705a --- /dev/null +++ b/go/sdkversion/sdkversion.go @@ -0,0 +1,57 @@ +// Package sdkversion says which mesh-sdk version a program was built with (novox/hq ADR 0446 clause 4: a composition +// check names its mesh-sdk version, so the controller and the assigner can be compared; ADR 0453 clause 1). +// +// The version is read from the build info, never kept by hand: a constant nobody bumps would report two different +// SDKs as one. A program that imports the SDK reports the module's pinned (pseudo-)version, or its replacement's; +// the SDK's own binaries and tests report "(devel)" with the stamped vcs.revision. +package sdkversion + +import ( + "runtime/debug" +) + +// Module is the SDK's Go module path. +const Module = "git.novox.be/novox/mesh-sdk/go" + +// Version is the mesh-sdk version this binary was built with: "unknown" when the build carries no build info. +func Version() string { + bi, ok := debug.ReadBuildInfo() + if !ok { + return "unknown" + } + return FromBuildInfo(bi) +} + +// FromBuildInfo reads the SDK's version out of a build info. A dependency replaced by another module version is that +// version; replaced by a local path it is the path, never empty. When the SDK is the main module it is "(devel)", +// followed by a space and the vcs.revision when the build stamped one. Without the SDK in the build it is "unknown". +func FromBuildInfo(bi *debug.BuildInfo) string { + if bi == nil { + return "unknown" + } + if bi.Main.Path == Module { + for _, s := range bi.Settings { + if s.Key == "vcs.revision" && s.Value != "" { + return "(devel) " + s.Value + } + } + return "(devel)" + } + for _, d := range bi.Deps { + if d == nil || d.Path != Module { + continue + } + if r := d.Replace; r != nil { + if r.Version != "" { + return r.Version + } + if r.Path != "" { + return r.Path + } + } + if d.Version != "" { + return d.Version + } + } + return "unknown" +} diff --git a/go/sdkversion/sdkversion_test.go b/go/sdkversion/sdkversion_test.go new file mode 100644 index 0000000..7a4b04d --- /dev/null +++ b/go/sdkversion/sdkversion_test.go @@ -0,0 +1,58 @@ +package sdkversion + +import ( + "runtime/debug" + "testing" +) + +const sdkPath = "git.novox.be/novox/mesh-sdk/go" + +// The build infos are constructed in the shape runtime/debug.ReadBuildInfo gives a binary that imports the SDK +// (go.mod pins like mesh-controller's v0.1.14-0.20261019091011-aa68b67389c9). +func TestAPinnedDependencyIsItsPseudoVersion(t *testing.T) { + bi := &debug.BuildInfo{Main: debug.Module{Path: "github.com/novox/mesh-controller", Version: "(devel)"}, + Deps: []*debug.Module{{Path: "example.org/other", Version: "v1.0.0"}, {Path: sdkPath, Version: "v0.1.14-0.20261019091011-aa68b67389c9"}}} + if got := FromBuildInfo(bi); got != "v0.1.14-0.20261019091011-aa68b67389c9" { + t.Fatalf("got %q", got) + } +} + +func TestAReplacedDependencyIsTheReplacement(t *testing.T) { + bi := &debug.BuildInfo{Main: debug.Module{Path: "x"}, Deps: []*debug.Module{{Path: sdkPath, Version: "v0.1.14", + Replace: &debug.Module{Path: sdkPath, Version: "v0.2.0-0.20261020000000-abcdef123456"}}}} + if got := FromBuildInfo(bi); got != "v0.2.0-0.20261020000000-abcdef123456" { + t.Fatalf("got %q", got) + } + local := &debug.BuildInfo{Main: debug.Module{Path: "x"}, Deps: []*debug.Module{{Path: sdkPath, Version: "v0.1.14", + Replace: &debug.Module{Path: "../mesh-sdk/go"}}}} + if got := FromBuildInfo(local); got != "../mesh-sdk/go" { + t.Fatalf("a local replacement: got %q", got) + } +} + +func TestTheMainModuleIsDevelWithItsRevision(t *testing.T) { + bi := &debug.BuildInfo{Main: debug.Module{Path: sdkPath, Version: "(devel)"}, + Settings: []debug.BuildSetting{{Key: "vcs", Value: "git"}, {Key: "vcs.revision", Value: "5786ba190c69bcee983223dbbc149bfb18fe01ed"}}} + if got := FromBuildInfo(bi); got != "(devel) 5786ba190c69bcee983223dbbc149bfb18fe01ed" { + t.Fatalf("got %q", got) + } + bare := &debug.BuildInfo{Main: debug.Module{Path: sdkPath, Version: "(devel)"}} + if got := FromBuildInfo(bare); got != "(devel)" { + t.Fatalf("no stamp: got %q", got) + } +} + +func TestNoBuildInfoIsUnknown(t *testing.T) { + if got := FromBuildInfo(nil); got != "unknown" { + t.Fatalf("got %q", got) + } + if got := FromBuildInfo(&debug.BuildInfo{Main: debug.Module{Path: "x"}}); got != "unknown" { + t.Fatalf("an SDK-less binary: got %q", got) + } +} + +func TestVersionIsNeverEmpty(t *testing.T) { + if Version() == "" { + t.Fatal("empty") + } +}