diff --git a/.goreleaser.yml b/.goreleaser.yml index dc2378a9..7bc2171c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -23,10 +23,6 @@ builds: - linux_arm64 flags: - -mod=readonly - ldflags: - - -s -w - - -X github.com/juanfont/headscale/hscontrol/types.Version={{ .Version }} - - -X github.com/juanfont/headscale/hscontrol/types.GitCommitHash={{ .Commit }} tags: - ts2019 diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go index f3a16018..420cf363 100644 --- a/cmd/headscale/cli/root.go +++ b/cmd/headscale/cli/root.go @@ -71,19 +71,20 @@ func initConfig() { disableUpdateCheck := viper.GetBool("disable_check_updates") if !disableUpdateCheck && !machineOutput { + versionInfo := types.GetVersionInfo() if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && - types.Version != "dev" { + !versionInfo.Dirty { githubTag := &latest.GithubTag{ Owner: "juanfont", Repository: "headscale", } - res, err := latest.Check(githubTag, types.Version) + res, err := latest.Check(githubTag, versionInfo.Version) if err == nil && res.Outdated { //nolint log.Warn().Msgf( "An updated version of Headscale has been found (%s vs. your current %s). Check it out https://github.com/juanfont/headscale/releases\n", res.Current, - types.Version, + versionInfo.Version, ) } } diff --git a/cmd/headscale/cli/version.go b/cmd/headscale/cli/version.go index b007d05c..df8a0be4 100644 --- a/cmd/headscale/cli/version.go +++ b/cmd/headscale/cli/version.go @@ -7,6 +7,7 @@ import ( func init() { rootCmd.AddCommand(versionCmd) + versionCmd.Flags().StringP("output", "o", "", "Output format. Empty for human-readable, 'json', 'json-line' or 'yaml'") } var versionCmd = &cobra.Command{ @@ -15,9 +16,9 @@ var versionCmd = &cobra.Command{ Long: "The version of headscale.", Run: func(cmd *cobra.Command, args []string) { output, _ := cmd.Flags().GetString("output") - SuccessOutput(map[string]string{ - "version": types.Version, - "commit": types.GitCommitHash, - }, types.Version, output) + + info := types.GetVersionInfo() + + SuccessOutput(info, info.String(), output) }, } diff --git a/hscontrol/app.go b/hscontrol/app.go index 885066a0..6880c6be 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -511,7 +511,8 @@ func (h *Headscale) Serve() error { spew.Dump(h.cfg) } - log.Info().Str("version", types.Version).Str("commit", types.GitCommitHash).Msg("Starting Headscale") + versionInfo := types.GetVersionInfo() + log.Info().Str("version", versionInfo.Version).Str("commit", versionInfo.Commit).Msg("Starting Headscale") log.Info(). Str("minimum_version", capver.TailscaleVersion(capver.MinSupportedCapabilityVersion)). Msg("Clients with a lower minimum version will be rejected") diff --git a/hscontrol/types/version.go b/hscontrol/types/version.go index 7fe23250..6676c92f 100644 --- a/hscontrol/types/version.go +++ b/hscontrol/types/version.go @@ -1,6 +1,81 @@ package types -var ( - Version = "dev" - GitCommitHash = "dev" +import ( + "fmt" + "runtime" + "runtime/debug" + "strings" + "sync" ) + +type GoInfo struct { + Version string `json:"version"` + OS string `json:"os"` + Arch string `json:"arch"` +} + +type VersionInfo struct { + Version string `json:"version"` + Commit string `json:"commit"` + BuildTime string `json:"buildTime"` + Go GoInfo `json:"go"` + Dirty bool `json:"dirty"` +} + +func (v *VersionInfo) String() string { + var sb strings.Builder + + version := v.Version + if v.Dirty && !strings.Contains(version, "dirty") { + version += "-dirty" + } + + sb.WriteString(fmt.Sprintf("headscale version %s\n", version)) + sb.WriteString(fmt.Sprintf("commit: %s\n", v.Commit)) + sb.WriteString(fmt.Sprintf("build time: %s\n", v.BuildTime)) + sb.WriteString(fmt.Sprintf("built with: %s %s/%s\n", v.Go.Version, v.Go.OS, v.Go.Arch)) + + return sb.String() +} + +var buildInfo = sync.OnceValues(func() (*debug.BuildInfo, bool) { + return debug.ReadBuildInfo() +}) + +var GetVersionInfo = sync.OnceValue(func() *VersionInfo { + info := &VersionInfo{ + Version: "dev", + Commit: "unknown", + BuildTime: "unknown", + Go: GoInfo{ + Version: runtime.Version(), + OS: runtime.GOOS, + Arch: runtime.GOARCH, + }, + Dirty: false, + } + + buildInfo, ok := buildInfo() + if !ok { + return info + } + + // Extract version from module path or main version + if buildInfo.Main.Version != "" && buildInfo.Main.Version != "(devel)" { + info.Version = buildInfo.Main.Version + } + + // Extract build settings + for _, setting := range buildInfo.Settings { + switch setting.Key { + case "vcs.revision": + info.Commit = setting.Value + case "vcs.modified": + info.Dirty = setting.Value == "true" + case "vcs.time": + info.BuildTime = setting.Value + } + } + + return info +})