From 70a2929a12f246aa783684eca7f48cb4c8eb9d65 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Fri, 10 Feb 2023 18:07:37 -0800 Subject: [PATCH] version: make all exported funcs compile-time constant or lazy Signed-off-by: David Anderson --- build_dist.sh | 2 +- cmd/derper/depaware.txt | 1 + cmd/tailscale/depaware.txt | 1 + cmd/tailscaled/depaware.txt | 1 + version/distro/distro.go | 75 +++++++------- version/print.go | 22 +++-- version/prop.go | 119 +++++++++++----------- version/version.go | 192 +++++++++++++++++++++++------------- version/version_test.go | 10 ++ 9 files changed, 240 insertions(+), 183 deletions(-) diff --git a/build_dist.sh b/build_dist.sh index 32df3efee..831834b76 100755 --- a/build_dist.sh +++ b/build_dist.sh @@ -46,7 +46,7 @@ EOF fi tags="" -ldflags="-X tailscale.com/version.long=${LONG} -X tailscale.com/version.short=${SHORT} -X tailscale.com/version.gitCommit=${GIT_HASH}" +ldflags="-X tailscale.com/version.longStamp=${LONG} -X tailscale.com/version.shortStamp=${SHORT}" # build_dist.sh arguments must precede go build arguments. while [ "$#" -gt 1 ]; do diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index f8f2ce63e..29f2efc89 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -65,6 +65,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa tailscale.com/types/empty from tailscale.com/ipn tailscale.com/types/ipproto from tailscale.com/net/flowtrack+ tailscale.com/types/key from tailscale.com/cmd/derper+ + tailscale.com/types/lazy from tailscale.com/version+ tailscale.com/types/logger from tailscale.com/cmd/derper+ tailscale.com/types/netmap from tailscale.com/ipn tailscale.com/types/opt from tailscale.com/client/tailscale+ diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 4a3dd0e24..98c5c4b00 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -93,6 +93,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/types/empty from tailscale.com/ipn tailscale.com/types/ipproto from tailscale.com/net/flowtrack+ tailscale.com/types/key from tailscale.com/derp+ + tailscale.com/types/lazy from tailscale.com/version+ tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+ tailscale.com/types/netmap from tailscale.com/ipn tailscale.com/types/nettype from tailscale.com/net/netcheck+ diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 8f21502f0..df5526364 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -272,6 +272,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled tailscale.com/types/ipproto from tailscale.com/net/flowtrack+ tailscale.com/types/key from tailscale.com/control/controlbase+ + tailscale.com/types/lazy from tailscale.com/version+ tailscale.com/types/logger from tailscale.com/control/controlclient+ tailscale.com/types/logid from tailscale.com/logtail+ tailscale.com/types/netlogtype from tailscale.com/net/connstats+ diff --git a/version/distro/distro.go b/version/distro/distro.go index e666b6bdf..970b8c1ae 100644 --- a/version/distro/distro.go +++ b/version/distro/distro.go @@ -11,7 +11,7 @@ import ( "runtime" "strconv" - "tailscale.com/syncs" + "tailscale.com/types/lazy" "tailscale.com/util/lineread" ) @@ -31,22 +31,20 @@ const ( WDMyCloud = Distro("wdmycloud") ) -var distroAtomic syncs.AtomicValue[Distro] +var distro lazy.SyncValue[Distro] // Get returns the current distro, or the empty string if unknown. func Get() Distro { - if d, ok := distroAtomic.LoadOk(); ok { - return d - } - var d Distro - switch runtime.GOOS { - case "linux": - d = linuxDistro() - case "freebsd": - d = freebsdDistro() - } - distroAtomic.Store(d) // even if empty - return d + return distro.Get(func() Distro { + switch runtime.GOOS { + case "linux": + return linuxDistro() + case "freebsd": + return freebsdDistro() + default: + return Distro("") + } + }) } func have(file string) bool { @@ -99,7 +97,7 @@ func freebsdDistro() Distro { return "" } -var dsmVersion syncs.AtomicValue[int] +var dsmVersion lazy.SyncValue[int] // DSMVersion reports the Synology DSM major version. // @@ -108,33 +106,28 @@ func DSMVersion() int { if runtime.GOOS != "linux" { return 0 } - if Get() != Synology { - return 0 - } - if v, ok := dsmVersion.LoadOk(); ok && v != 0 { - return v - } - // This is set when running as a package: - v, _ := strconv.Atoi(os.Getenv("SYNOPKG_DSM_VERSION_MAJOR")) - if v != 0 { - dsmVersion.Store(v) - return v - } - // But when run from the command line, we have to read it from the file: - lineread.File("/etc/VERSION", func(line []byte) error { - line = bytes.TrimSpace(line) - if string(line) == `majorversion="7"` { - v = 7 - return io.EOF + return dsmVersion.Get(func() int { + if Get() != Synology { + return 0 } - if string(line) == `majorversion="6"` { - v = 6 - return io.EOF + // This is set when running as a package: + v, _ := strconv.Atoi(os.Getenv("SYNOPKG_DSM_VERSION_MAJOR")) + if v != 0 { + return v } - return nil + // But when run from the command line, we have to read it from the file: + lineread.File("/etc/VERSION", func(line []byte) error { + line = bytes.TrimSpace(line) + if string(line) == `majorversion="7"` { + v = 7 + return io.EOF + } + if string(line) == `majorversion="6"` { + v = 6 + return io.EOF + } + return nil + }) + return v }) - if v != 0 { - dsmVersion.Store(v) - } - return v } diff --git a/version/print.go b/version/print.go index ef5aad31b..7d8554279 100644 --- a/version/print.go +++ b/version/print.go @@ -7,25 +7,27 @@ import ( "fmt" "runtime" "strings" + + "tailscale.com/types/lazy" ) -func String() string { +var stringLazy = lazy.SyncFunc(func() string { var ret strings.Builder - ret.WriteString(short) + ret.WriteString(Short()) ret.WriteByte('\n') if IsUnstableBuild() { fmt.Fprintf(&ret, " track: unstable (dev); frequent updates and bugs are likely\n") } - if gitCommit != "" { - var dirty string - if gitDirty { - dirty = "-dirty" - } - fmt.Fprintf(&ret, " tailscale commit: %s%s\n", gitCommit, dirty) + if gitCommit() != "" { + fmt.Fprintf(&ret, " tailscale commit: %s%s\n", gitCommit(), dirtyString()) } - if extraGitCommit != "" { - fmt.Fprintf(&ret, " other commit: %s\n", extraGitCommit) + if extraGitCommitStamp != "" { + fmt.Fprintf(&ret, " other commit: %s\n", extraGitCommitStamp) } fmt.Fprintf(&ret, " go version: %s\n", runtime.Version()) return strings.TrimSpace(ret.String()) +}) + +func String() string { + return stringLazy() } diff --git a/version/prop.go b/version/prop.go index e4e0589b1..a6cc124c6 100644 --- a/version/prop.go +++ b/version/prop.go @@ -9,9 +9,9 @@ import ( "runtime" "strconv" "strings" - "sync" "tailscale.com/tailcfg" + "tailscale.com/types/lazy" ) // IsMobile reports whether this is a mobile client build. @@ -37,20 +37,7 @@ func OS() string { return runtime.GOOS } -var ( - macFlavorOnce sync.Once - isMacSysExt bool - isMacSandboxed bool -) - -func initMacFlavor() { - exe, err := os.Executable() - if err != nil { - return - } - isMacSysExt = filepath.Base(exe) == "io.tailscale.ipn.macsys.network-extension" - isMacSandboxed = isMacSysExt || strings.HasSuffix(exe, "/Contents/MacOS/Tailscale") || strings.HasSuffix(exe, "/Contents/MacOS/IPNExtension") -} +var isSandboxedMacOS lazy.SyncValue[bool] // IsSandboxedMacOS reports whether this process is a sandboxed macOS // process (either the app or the extension). It is true for the Mac App Store @@ -60,70 +47,76 @@ func IsSandboxedMacOS() bool { if runtime.GOOS != "darwin" { return false } - macFlavorOnce.Do(initMacFlavor) - return isMacSandboxed + return isSandboxedMacOS.Get(func() bool { + if IsMacSysExt() { + return true + } + exe, err := os.Executable() + if err != nil { + return false + } + return filepath.Base(exe) == "io.tailscale.ipn.macsys.network-extension" || strings.HasSuffix(exe, "/Contents/MacOS/Tailscale") || strings.HasSuffix(exe, "/Contents/MacOS/IPNExtension") + }) } +var isMacSysExt lazy.SyncValue[bool] + // IsMacSysExt whether this binary is from the standalone "System // Extension" (a.k.a. "macsys") version of Tailscale for macOS. func IsMacSysExt() bool { if runtime.GOOS != "darwin" { return false } - macFlavorOnce.Do(initMacFlavor) - return isMacSysExt + return isMacSysExt.Get(func() bool { + exe, err := os.Executable() + if err != nil { + return false + } + return filepath.Base(exe) == "io.tailscale.ipn.macsys.network-extension" + }) } -var ( - winFlavorOnce sync.Once - isWindowsGUI bool -) - -func initWinFlavor() { - exe, err := os.Executable() - if err != nil { - return - } - isWindowsGUI = strings.EqualFold(exe, "tailscale-ipn.exe") || strings.EqualFold(exe, "tailscale-ipn") -} +var isWindowsGUI lazy.SyncValue[bool] // IsWindowsGUI reports whether the current process is the Windows GUI. func IsWindowsGUI() bool { if runtime.GOOS != "windows" { return false } - exe, _ := os.Executable() - exe = filepath.Base(exe) - return strings.EqualFold(exe, "tailscale-ipn.exe") || strings.EqualFold(exe, "tailscale-ipn") + return isWindowsGUI.Get(func() bool { + exe, err := os.Executable() + if err != nil { + return false + } + return strings.EqualFold(exe, "tailscale-ipn.exe") || strings.EqualFold(exe, "tailscale-ipn") + }) } -var ( - isUnstableOnce sync.Once - isUnstableBuild bool -) +var isUnstableBuild lazy.SyncValue[bool] // IsUnstableBuild reports whether this is an unstable build. // That is, whether its minor version number is odd. func IsUnstableBuild() bool { - isUnstableOnce.Do(initUnstable) - return isUnstableBuild + return isUnstableBuild.Get(func() bool { + _, rest, ok := strings.Cut(Short(), ".") + if !ok { + return false + } + minorStr, _, ok := strings.Cut(rest, ".") + if !ok { + return false + } + minor, err := strconv.Atoi(minorStr) + if err != nil { + return false + } + return minor%2 == 1 + }) } -func initUnstable() { - _, rest, ok := strings.Cut(short, ".") - if !ok { - return - } - minorStr, _, ok := strings.Cut(rest, ".") - if !ok { - return - } - minor, err := strconv.Atoi(minorStr) - if err != nil { - return - } - isUnstableBuild = minor%2 == 1 -} +var isDev = lazy.SyncFunc(func() bool { + return strings.Contains(Short(), "-dev") +}) // Meta is a JSON-serializable type that contains all the version // information. @@ -183,16 +176,18 @@ type Meta struct { Cap int `json:"cap"` } +var getMeta lazy.SyncValue[Meta] + // GetMeta returns version metadata about the current build. func GetMeta() Meta { return Meta{ - MajorMinorPatch: majorMinorPatch, - Short: short, - Long: long, - GitCommit: gitCommit, - GitDirty: gitDirty, - ExtraGitCommit: extraGitCommit, - IsDev: strings.Contains(short, "-dev"), // TODO(bradfitz): could make a bool for this in init + MajorMinorPatch: majorMinorPatch(), + Short: Short(), + Long: Long(), + GitCommit: gitCommit(), + GitDirty: gitDirty(), + ExtraGitCommit: extraGitCommitStamp, + IsDev: isDev(), UnstableBranch: IsUnstableBuild(), Cap: int(tailcfg.CurrentCapabilityVersion), } diff --git a/version/version.go b/version/version.go index 84a4f757f..17a74a338 100644 --- a/version/version.go +++ b/version/version.go @@ -5,106 +5,160 @@ package version import ( + "fmt" "runtime/debug" "strings" tailscaleroot "tailscale.com" + "tailscale.com/types/lazy" ) -var long = "" +// Stamp vars can have their value set at build time by linker flags (see +// build_dist.sh for an example). When set, these stamps serve as additional +// inputs to computing the binary's version as returned by the functions in this +// package. +// +// All stamps are optional. +var ( + // longStamp is the full version identifier of the build. If set, it is + // returned verbatim by Long() and other functions that return Long()'s + // output. + longStamp string + + // shortStamp is the short version identifier of the build. If set, it + // is returned verbatim by Short() and other functions that return Short()'s + // output. + shortStamp string + + // gitCommitStamp is the git commit of the github.com/tailscale/tailscale + // repository at which Tailscale was built. Its format is the one returned + // by `git rev-parse `. If set, it is used instead of any git commit + // information embedded by the Go tool. + gitCommitStamp string + + // gitDirtyStamp is whether the git checkout from which the code was built + // was dirty. Its value is ORed with the dirty bit embedded by the Go tool. + // + // We need this because when we build binaries from another repo that + // imports tailscale.com, the Go tool doesn't stamp any dirtiness info into + // the binary. Instead, we have to inject the dirty bit ourselves here. + gitDirtyStamp bool + + // extraGitCommit, is the git commit of a "supplemental" repository at which + // Tailscale was built. Its format is the same as gitCommit. + // + // extraGitCommit is used to track the source revision when the main + // Tailscale repository is integrated into and built from another repository + // (for example, Tailscale's proprietary code, or the Android OSS + // repository). Together, gitCommit and extraGitCommit exactly describe what + // repositories and commits were used in a build. + extraGitCommitStamp string +) -var short = "" +var long lazy.SyncValue[string] -// Long is a full version number for this build, of the form -// "x.y.z-commithash" for builds stamped in the usual way (see -// build_dist.sh in the root) or, for binaries built by hand with the -// go tool, it's of the form "1.23.0-dev20220316-t29837428937{,-dirty}" -// where "1.23.0" comes from ../VERSION.txt and the part after dev -// is YYYYMMDD of the commit time, and the part after -t is the commit -// hash. The dirty suffix is whether there are uncommitted changes. +// Long returns a full version number for this build, of one of the forms: +// +// - "x.y.z-commithash-otherhash" for release builds distributed by Tailscale +// - "x.y.z-commithash" for release builds built with build_dist.sh +// - "x.y.z-changecount-commithash-otherhash" for untagged release branch +// builds by Tailscale (these are not distributed). +// - "x.y.z-changecount-commithash" for untagged release branch builds +// built with build_dist.sh +// - "x.y.z-devYYYYMMDD-commithash{,-dirty}" for builds made with plain "go +// build" or "go install" +// - "x.y.z-ERR-BuildInfo" for builds made by plain "go run" func Long() string { - return long + return long.Get(func() string { + if longStamp != "" { + return longStamp + } + bi := getEmbeddedInfo() + if !bi.valid { + return strings.TrimSpace(tailscaleroot.VersionDotTxt) + "-ERR-BuildInfo" + } + return fmt.Sprintf("%s-dev%s-t%s%s", strings.TrimSpace(tailscaleroot.VersionDotTxt), bi.commitDate, bi.commitAbbrev(), dirtyString()) + }) } -// Short is a short version number for this build, of the form -// "x.y.z" for builds stamped in the usual way (see -// build_dist.sh in the root) or, for binaries built by hand with the -// go tool, it's like Long's dev form, but ending at the date part, -// of the form "1.23.0-dev20220316". +var short lazy.SyncValue[string] + +// Short returns a short version number for this build, of the forms: +// +// - "x.y.z" for builds distributed by Tailscale or built with build_dist.sh +// - "x.y.z-devYYYYMMDD" for builds made with plain "go build" or "go install" +// - "x.y.z-ERR-BuildInfo" for builds made by plain "go run" func Short() string { - return short + return short.Get(func() string { + if shortStamp != "" { + return shortStamp + } + bi := getEmbeddedInfo() + if !bi.valid { + return strings.TrimSpace(tailscaleroot.VersionDotTxt) + "-ERR-BuildInfo" + } + return strings.TrimSpace(tailscaleroot.VersionDotTxt) + "-dev" + bi.commitDate + }) } -func init() { - defer func() { - // Must be run after Short has been initialized, easiest way to do that - // is a defer. - majorMinorPatch, _, _ = strings.Cut(short, "-") - }() +type embeddedInfo struct { + valid bool + commit string + commitDate string + dirty bool +} - if long != "" && short != "" { - // Built in the recommended way, using build_dist.sh. - return +func (i embeddedInfo) commitAbbrev() string { + if len(i.commit) >= 9 { + return i.commit[:9] } + return i.commit +} - // Otherwise, make approximate version info using Go 1.18's built-in git - // stamping. +var getEmbeddedInfo = lazy.SyncFunc(func() embeddedInfo { bi, ok := debug.ReadBuildInfo() if !ok { - long = strings.TrimSpace(tailscaleroot.VersionDotTxt) + "-ERR-BuildInfo" - short = long - return + return embeddedInfo{} } - var dirty string // "-dirty" suffix if dirty - var commitDate string + ret := embeddedInfo{valid: true} for _, s := range bi.Settings { switch s.Key { case "vcs.revision": - gitCommit = s.Value + ret.commit = s.Value case "vcs.time": if len(s.Value) >= len("yyyy-mm-dd") { - commitDate = s.Value[:len("yyyy-mm-dd")] - commitDate = strings.ReplaceAll(commitDate, "-", "") + ret.commitDate = s.Value[:len("yyyy-mm-dd")] + ret.commitDate = strings.ReplaceAll(ret.commitDate, "-", "") } case "vcs.modified": - if s.Value == "true" { - dirty = "-dirty" - gitDirty = true - } + ret.dirty = true } } - commitHashAbbrev := gitCommit - if len(commitHashAbbrev) >= 9 { - commitHashAbbrev = commitHashAbbrev[:9] - } + return ret +}) - // Backup path, using Go 1.18's built-in git stamping. - short = strings.TrimSpace(tailscaleroot.VersionDotTxt) + "-dev" + commitDate - long = short + "-t" + commitHashAbbrev + dirty +func gitCommit() string { + if gitCommitStamp != "" { + return gitCommitStamp + } + return getEmbeddedInfo().commit } -// GitCommit, if non-empty, is the git commit of the -// github.com/tailscale/tailscale repository at which Tailscale was -// built. Its format is the one returned by `git describe --always -// --exclude "*" --dirty --abbrev=200`. -var gitCommit = "" +func gitDirty() bool { + if gitDirtyStamp { + return true + } + return getEmbeddedInfo().dirty +} -// GitDirty is whether Go stamped the binary as having dirty version -// control changes in the working directory (debug.ReadBuildInfo -// setting "vcs.modified" was true). -var gitDirty bool +func dirtyString() string { + if gitDirty() { + return "-dirty" + } + return "" +} -// ExtraGitCommit, if non-empty, is the git commit of a "supplemental" -// repository at which Tailscale was built. Its format is the same as -// gitCommit. -// -// ExtraGitCommit is used to track the source revision when the main -// Tailscale repository is integrated into and built from another -// repository (for example, Tailscale's proprietary code, or the -// Android OSS repository). Together, GitCommit and ExtraGitCommit -// exactly describe what repositories and commits were used in a -// build. -var extraGitCommit = "" - -// majorMinorPatch is the major.minor.patch portion of Short. -var majorMinorPatch string +func majorMinorPatch() string { + ret, _, _ := strings.Cut(Short(), "-") + return ret +} diff --git a/version/version_test.go b/version/version_test.go index a363e1985..a51565058 100644 --- a/version/version_test.go +++ b/version/version_test.go @@ -9,6 +9,7 @@ import ( "testing" ts "tailscale.com" + "tailscale.com/version" ) func TestAlpineTag(t *testing.T) { @@ -39,3 +40,12 @@ func readAlpineTag(t *testing.T, file string) string { } return "" } + +func TestShortAllocs(t *testing.T) { + allocs := int(testing.AllocsPerRun(10000, func() { + _ = version.Short() + })) + if allocs > 0 { + t.Errorf("allocs = %v; want 0", allocs) + } +}