diff --git a/app/app.go b/app/app.go index 99a40de..48e3886 100644 --- a/app/app.go +++ b/app/app.go @@ -23,6 +23,7 @@ import ( // App is the application. type App struct { + name string ctx *actx.Context cli *cli.CLI args []string @@ -35,13 +36,17 @@ type App struct { // the directory where application data will be stored, though this can be // overriden with the DISCO_DATA_DIR environment variable, and --data-dir CLI // flag. -func New(dataDir string, opts ...Option) (*App, error) { +func New(name, dataDir string, opts ...Option) (*App, error) { + version, err := actx.GetVersion() + if err != nil { + return nil, err + } defaultCtx := &actx.Context{ Ctx: context.Background(), Logger: slog.Default(), Version: version, } - app := &App{ctx: defaultCtx} + app := &App{name: name, ctx: defaultCtx} for _, opt := range opts { opt(app) @@ -54,7 +59,8 @@ func New(dataDir string, opts ...Option) (*App, error) { } app.ctx.UUIDGen = uuidgen - app.cli, err = cli.New(dataDir, app.ctx.Version) + ver := fmt.Sprintf("%s %s", app.name, app.ctx.Version.String()) + app.cli, err = cli.New(dataDir, ver) if err != nil { return nil, err } diff --git a/app/cli/init.go b/app/cli/init.go index bf11bc0..ee25940 100644 --- a/app/cli/init.go +++ b/app/cli/init.go @@ -36,12 +36,12 @@ func (c *Init) Run(appCtx *actx.Context) error { return fmt.Errorf("failed generating the server TLS certificate: %w", err) } - appCtx.User, err = appCtx.DB.Init(appCtx.Version, tlsCert, tlsPrivKey, rndSAN, appCtx.Logger) + appCtx.User, err = appCtx.DB.Init(appCtx.Version.Semantic, tlsCert, tlsPrivKey, rndSAN, appCtx.Logger) if err != nil { return aerrors.NewRuntimeError("failed initializing database", err, "") } - if err = appCtx.Store.Init(appCtx.Version, appCtx.Logger); err != nil { + if err = appCtx.Store.Init(appCtx.Version.Semantic, appCtx.Logger); err != nil { return aerrors.NewRuntimeError("failed initializing store", err, "") } diff --git a/app/context/context.go b/app/context/context.go index 4bfb50e..20c51c6 100644 --- a/app/context/context.go +++ b/app/context/context.go @@ -42,8 +42,7 @@ type Context struct { User *models.User // current app user // Metadata - Version string // short semantic app version - VersionFull string // app version including Go runtime information + Version *VersionInfo VersionInit string // app version the DB was initialized with } diff --git a/app/context/version.go b/app/context/version.go new file mode 100644 index 0000000..8256616 --- /dev/null +++ b/app/context/version.go @@ -0,0 +1,127 @@ +package context + +import ( + "fmt" + "regexp" + "runtime" + "runtime/debug" + "strconv" + "strings" +) + +// The semantic version of the application. +// This can be overriden by vcsVersion. +const version = "0.0.0" + +var ( + vcsVersion string // version from VCS set at build time + // Simplified semver regex. A more complete one can be found on https://semver.org/ + versionRx = regexp.MustCompile(`^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*).*`) + shaRx = regexp.MustCompile(`^g[0-9a-f]{6,}`) +) + +// VersionInfo stores app version information. +type VersionInfo struct { + Commit string + Semantic string + TagDistance int // number of commits since latest tag + Dirty bool + goInfo string +} + +// GetVersion returns the app version including VCS and Go runtime information. +// It first reads the VCS version set at build time, and falls back to the VCS +// information provided by the Go runtime. +// The information in the Go runtime is not as extensive as the one extracted +// from Git, and we use the Go runtime information in case the binary is built +// without setting -ldflags (e.g. Homebrew), so we still need both. +// See https://github.com/golang/go/issues/50603 +func GetVersion() (*VersionInfo, error) { + vi := &VersionInfo{} + vi.goInfo = fmt.Sprintf("%s, %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH) + + if vcsVersion != "" { + if err := vi.UnmarshalText([]byte(vcsVersion)); err != nil { + return nil, fmt.Errorf("failed reading VCS version '%s': %w", vcsVersion, err) + } + } + + vi.extractVersionFromRuntime() + + if vi.Semantic == "" { + vi.Semantic = version + } + + return vi, nil +} + +// String returns the full version information. +func (vi *VersionInfo) String() string { + var distance string + if vi.TagDistance > 0 { + distance = fmt.Sprintf("-%d", vi.TagDistance) + } + + var dirty string + if vi.Dirty { + dirty = "-dirty" + } + + return fmt.Sprintf("v%s (commit/%s%s%s, %s)", + vi.Semantic, vi.Commit, distance, dirty, vi.goInfo) +} + +// UnmarshalText parses the output of `git describe`. +func (vi *VersionInfo) UnmarshalText(data []byte) error { + parts := strings.Split(string(data), "-") + verParts := []string{} + + for _, part := range parts { + if shaRx.Match([]byte(part)) { + vi.Commit = strings.TrimPrefix(part, "g") + continue + } + if part == "dirty" { + vi.Dirty = true + continue + } + if distance, err := strconv.Atoi(part); err == nil { + vi.TagDistance = distance + continue + } + verParts = append(verParts, part) + } + + ver := strings.Join(verParts, "-") + if versionRx.Match([]byte(ver)) { + vi.Semantic = strings.TrimPrefix(ver, "v") + } else if vi.Commit == "" { + vi.Commit = ver + } + + return nil +} + +func (vi *VersionInfo) extractVersionFromRuntime() { + buildInfo, ok := debug.ReadBuildInfo() + if !ok { + return + } + + for _, s := range buildInfo.Settings { + switch s.Key { + case "vcs.revision": + commitLen := 10 + if len(s.Value) < commitLen { + commitLen = len(s.Value) + } + if vi.Commit == "" { + vi.Commit = s.Value[:commitLen] + } + case "vcs.modified": + if s.Value == "true" { + vi.Dirty = true + } + } + } +} diff --git a/app/utils_test.go b/app/utils_test.go index 1684e54..e70994c 100644 --- a/app/utils_test.go +++ b/app/utils_test.go @@ -74,7 +74,7 @@ func newTestApp(ctx context.Context, options ...Option) (*testApp, error) { WithUser(localUser), } opts = append(opts, options...) - app, err := New("/disco", opts...) + app, err := New("Disco", "/disco", opts...) if err != nil { return nil, err } diff --git a/app/version.go b/app/version.go deleted file mode 100644 index 4a2f82e..0000000 --- a/app/version.go +++ /dev/null @@ -1,4 +0,0 @@ -package app - -// The app version is extracted from Git and set by the Go linker at build time. -var version string diff --git a/cmd/disco/main.go b/cmd/disco/main.go index 94b483f..f3b67dc 100644 --- a/cmd/disco/main.go +++ b/cmd/disco/main.go @@ -17,7 +17,7 @@ import ( func main() { // NOTE: The order of the passed options is significant, as some options // depend on the values set by previous ones. - a, err := app.New(filepath.Join(xdg.DataHome, "disco"), + a, err := app.New("Disco", filepath.Join(xdg.DataHome, "disco"), app.WithFDs( os.Stdin, colorable.NewColorable(os.Stdout),