version: make all exported funcs compile-time constant or lazy

Signed-off-by: David Anderson <danderson@tailscale.com>
pull/7258/head
David Anderson 1 year ago committed by Dave Anderson
parent 8b2ae47c31
commit 70a2929a12

@ -46,7 +46,7 @@ EOF
fi fi
tags="" 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. # build_dist.sh arguments must precede go build arguments.
while [ "$#" -gt 1 ]; do while [ "$#" -gt 1 ]; do

@ -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/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+ tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
tailscale.com/types/key from tailscale.com/cmd/derper+ 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/logger from tailscale.com/cmd/derper+
tailscale.com/types/netmap from tailscale.com/ipn tailscale.com/types/netmap from tailscale.com/ipn
tailscale.com/types/opt from tailscale.com/client/tailscale+ tailscale.com/types/opt from tailscale.com/client/tailscale+

@ -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/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+ tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
tailscale.com/types/key from tailscale.com/derp+ 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/logger from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/netmap from tailscale.com/ipn tailscale.com/types/netmap from tailscale.com/ipn
tailscale.com/types/nettype from tailscale.com/net/netcheck+ tailscale.com/types/nettype from tailscale.com/net/netcheck+

@ -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/flagtype from tailscale.com/cmd/tailscaled
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+ tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
tailscale.com/types/key from tailscale.com/control/controlbase+ 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/logger from tailscale.com/control/controlclient+
tailscale.com/types/logid from tailscale.com/logtail+ tailscale.com/types/logid from tailscale.com/logtail+
tailscale.com/types/netlogtype from tailscale.com/net/connstats+ tailscale.com/types/netlogtype from tailscale.com/net/connstats+

@ -11,7 +11,7 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"tailscale.com/syncs" "tailscale.com/types/lazy"
"tailscale.com/util/lineread" "tailscale.com/util/lineread"
) )
@ -31,22 +31,20 @@ const (
WDMyCloud = Distro("wdmycloud") WDMyCloud = Distro("wdmycloud")
) )
var distroAtomic syncs.AtomicValue[Distro] var distro lazy.SyncValue[Distro]
// Get returns the current distro, or the empty string if unknown. // Get returns the current distro, or the empty string if unknown.
func Get() Distro { func Get() Distro {
if d, ok := distroAtomic.LoadOk(); ok { return distro.Get(func() Distro {
return d switch runtime.GOOS {
} case "linux":
var d Distro return linuxDistro()
switch runtime.GOOS { case "freebsd":
case "linux": return freebsdDistro()
d = linuxDistro() default:
case "freebsd": return Distro("")
d = freebsdDistro() }
} })
distroAtomic.Store(d) // even if empty
return d
} }
func have(file string) bool { func have(file string) bool {
@ -99,7 +97,7 @@ func freebsdDistro() Distro {
return "" return ""
} }
var dsmVersion syncs.AtomicValue[int] var dsmVersion lazy.SyncValue[int]
// DSMVersion reports the Synology DSM major version. // DSMVersion reports the Synology DSM major version.
// //
@ -108,33 +106,28 @@ func DSMVersion() int {
if runtime.GOOS != "linux" { if runtime.GOOS != "linux" {
return 0 return 0
} }
if Get() != Synology { return dsmVersion.Get(func() int {
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
} }
if string(line) == `majorversion="6"` { // This is set when running as a package:
v = 6 v, _ := strconv.Atoi(os.Getenv("SYNOPKG_DSM_VERSION_MAJOR"))
return io.EOF 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
} }

@ -7,25 +7,27 @@ import (
"fmt" "fmt"
"runtime" "runtime"
"strings" "strings"
"tailscale.com/types/lazy"
) )
func String() string { var stringLazy = lazy.SyncFunc(func() string {
var ret strings.Builder var ret strings.Builder
ret.WriteString(short) ret.WriteString(Short())
ret.WriteByte('\n') ret.WriteByte('\n')
if IsUnstableBuild() { if IsUnstableBuild() {
fmt.Fprintf(&ret, " track: unstable (dev); frequent updates and bugs are likely\n") fmt.Fprintf(&ret, " track: unstable (dev); frequent updates and bugs are likely\n")
} }
if gitCommit != "" { if gitCommit() != "" {
var dirty string fmt.Fprintf(&ret, " tailscale commit: %s%s\n", gitCommit(), dirtyString())
if gitDirty {
dirty = "-dirty"
}
fmt.Fprintf(&ret, " tailscale commit: %s%s\n", gitCommit, dirty)
} }
if extraGitCommit != "" { if extraGitCommitStamp != "" {
fmt.Fprintf(&ret, " other commit: %s\n", extraGitCommit) fmt.Fprintf(&ret, " other commit: %s\n", extraGitCommitStamp)
} }
fmt.Fprintf(&ret, " go version: %s\n", runtime.Version()) fmt.Fprintf(&ret, " go version: %s\n", runtime.Version())
return strings.TrimSpace(ret.String()) return strings.TrimSpace(ret.String())
})
func String() string {
return stringLazy()
} }

@ -9,9 +9,9 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/lazy"
) )
// IsMobile reports whether this is a mobile client build. // IsMobile reports whether this is a mobile client build.
@ -37,20 +37,7 @@ func OS() string {
return runtime.GOOS return runtime.GOOS
} }
var ( var isSandboxedMacOS lazy.SyncValue[bool]
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")
}
// IsSandboxedMacOS reports whether this process is a sandboxed macOS // IsSandboxedMacOS reports whether this process is a sandboxed macOS
// process (either the app or the extension). It is true for the Mac App Store // 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" { if runtime.GOOS != "darwin" {
return false return false
} }
macFlavorOnce.Do(initMacFlavor) return isSandboxedMacOS.Get(func() bool {
return isMacSandboxed 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 // IsMacSysExt whether this binary is from the standalone "System
// Extension" (a.k.a. "macsys") version of Tailscale for macOS. // Extension" (a.k.a. "macsys") version of Tailscale for macOS.
func IsMacSysExt() bool { func IsMacSysExt() bool {
if runtime.GOOS != "darwin" { if runtime.GOOS != "darwin" {
return false return false
} }
macFlavorOnce.Do(initMacFlavor) return isMacSysExt.Get(func() bool {
return isMacSysExt exe, err := os.Executable()
if err != nil {
return false
}
return filepath.Base(exe) == "io.tailscale.ipn.macsys.network-extension"
})
} }
var ( var isWindowsGUI lazy.SyncValue[bool]
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")
}
// IsWindowsGUI reports whether the current process is the Windows GUI. // IsWindowsGUI reports whether the current process is the Windows GUI.
func IsWindowsGUI() bool { func IsWindowsGUI() bool {
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
return false return false
} }
exe, _ := os.Executable() return isWindowsGUI.Get(func() bool {
exe = filepath.Base(exe) exe, err := os.Executable()
return strings.EqualFold(exe, "tailscale-ipn.exe") || strings.EqualFold(exe, "tailscale-ipn") if err != nil {
return false
}
return strings.EqualFold(exe, "tailscale-ipn.exe") || strings.EqualFold(exe, "tailscale-ipn")
})
} }
var ( var isUnstableBuild lazy.SyncValue[bool]
isUnstableOnce sync.Once
isUnstableBuild bool
)
// IsUnstableBuild reports whether this is an unstable build. // IsUnstableBuild reports whether this is an unstable build.
// That is, whether its minor version number is odd. // That is, whether its minor version number is odd.
func IsUnstableBuild() bool { func IsUnstableBuild() bool {
isUnstableOnce.Do(initUnstable) return isUnstableBuild.Get(func() bool {
return isUnstableBuild _, 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() { var isDev = lazy.SyncFunc(func() bool {
_, rest, ok := strings.Cut(short, ".") return strings.Contains(Short(), "-dev")
if !ok { })
return
}
minorStr, _, ok := strings.Cut(rest, ".")
if !ok {
return
}
minor, err := strconv.Atoi(minorStr)
if err != nil {
return
}
isUnstableBuild = minor%2 == 1
}
// Meta is a JSON-serializable type that contains all the version // Meta is a JSON-serializable type that contains all the version
// information. // information.
@ -183,16 +176,18 @@ type Meta struct {
Cap int `json:"cap"` Cap int `json:"cap"`
} }
var getMeta lazy.SyncValue[Meta]
// GetMeta returns version metadata about the current build. // GetMeta returns version metadata about the current build.
func GetMeta() Meta { func GetMeta() Meta {
return Meta{ return Meta{
MajorMinorPatch: majorMinorPatch, MajorMinorPatch: majorMinorPatch(),
Short: short, Short: Short(),
Long: long, Long: Long(),
GitCommit: gitCommit, GitCommit: gitCommit(),
GitDirty: gitDirty, GitDirty: gitDirty(),
ExtraGitCommit: extraGitCommit, ExtraGitCommit: extraGitCommitStamp,
IsDev: strings.Contains(short, "-dev"), // TODO(bradfitz): could make a bool for this in init IsDev: isDev(),
UnstableBranch: IsUnstableBuild(), UnstableBranch: IsUnstableBuild(),
Cap: int(tailcfg.CurrentCapabilityVersion), Cap: int(tailcfg.CurrentCapabilityVersion),
} }

@ -5,106 +5,160 @@
package version package version
import ( import (
"fmt"
"runtime/debug" "runtime/debug"
"strings" "strings"
tailscaleroot "tailscale.com" 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 <commit>`. 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 // Long returns a full version number for this build, of one of the forms:
// "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 // - "x.y.z-commithash-otherhash" for release builds distributed by Tailscale
// go tool, it's of the form "1.23.0-dev20220316-t29837428937{,-dirty}" // - "x.y.z-commithash" for release builds built with build_dist.sh
// where "1.23.0" comes from ../VERSION.txt and the part after dev // - "x.y.z-changecount-commithash-otherhash" for untagged release branch
// is YYYYMMDD of the commit time, and the part after -t is the commit // builds by Tailscale (these are not distributed).
// hash. The dirty suffix is whether there are uncommitted changes. // - "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 { 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 var short lazy.SyncValue[string]
// "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 // Short returns a short version number for this build, of the forms:
// go tool, it's like Long's dev form, but ending at the date part, //
// of the form "1.23.0-dev20220316". // - "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 { 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() { type embeddedInfo struct {
defer func() { valid bool
// Must be run after Short has been initialized, easiest way to do that commit string
// is a defer. commitDate string
majorMinorPatch, _, _ = strings.Cut(short, "-") dirty bool
}() }
if long != "" && short != "" { func (i embeddedInfo) commitAbbrev() string {
// Built in the recommended way, using build_dist.sh. if len(i.commit) >= 9 {
return return i.commit[:9]
} }
return i.commit
}
// Otherwise, make approximate version info using Go 1.18's built-in git var getEmbeddedInfo = lazy.SyncFunc(func() embeddedInfo {
// stamping.
bi, ok := debug.ReadBuildInfo() bi, ok := debug.ReadBuildInfo()
if !ok { if !ok {
long = strings.TrimSpace(tailscaleroot.VersionDotTxt) + "-ERR-BuildInfo" return embeddedInfo{}
short = long
return
} }
var dirty string // "-dirty" suffix if dirty ret := embeddedInfo{valid: true}
var commitDate string
for _, s := range bi.Settings { for _, s := range bi.Settings {
switch s.Key { switch s.Key {
case "vcs.revision": case "vcs.revision":
gitCommit = s.Value ret.commit = s.Value
case "vcs.time": case "vcs.time":
if len(s.Value) >= len("yyyy-mm-dd") { if len(s.Value) >= len("yyyy-mm-dd") {
commitDate = s.Value[:len("yyyy-mm-dd")] ret.commitDate = s.Value[:len("yyyy-mm-dd")]
commitDate = strings.ReplaceAll(commitDate, "-", "") ret.commitDate = strings.ReplaceAll(ret.commitDate, "-", "")
} }
case "vcs.modified": case "vcs.modified":
if s.Value == "true" { ret.dirty = true
dirty = "-dirty"
gitDirty = true
}
} }
} }
commitHashAbbrev := gitCommit return ret
if len(commitHashAbbrev) >= 9 { })
commitHashAbbrev = commitHashAbbrev[:9]
}
// Backup path, using Go 1.18's built-in git stamping. func gitCommit() string {
short = strings.TrimSpace(tailscaleroot.VersionDotTxt) + "-dev" + commitDate if gitCommitStamp != "" {
long = short + "-t" + commitHashAbbrev + dirty return gitCommitStamp
}
return getEmbeddedInfo().commit
} }
// GitCommit, if non-empty, is the git commit of the func gitDirty() bool {
// github.com/tailscale/tailscale repository at which Tailscale was if gitDirtyStamp {
// built. Its format is the one returned by `git describe --always return true
// --exclude "*" --dirty --abbrev=200`. }
var gitCommit = "" return getEmbeddedInfo().dirty
}
// GitDirty is whether Go stamped the binary as having dirty version func dirtyString() string {
// control changes in the working directory (debug.ReadBuildInfo if gitDirty() {
// setting "vcs.modified" was true). return "-dirty"
var gitDirty bool }
return ""
}
// ExtraGitCommit, if non-empty, is the git commit of a "supplemental" func majorMinorPatch() string {
// repository at which Tailscale was built. Its format is the same as ret, _, _ := strings.Cut(Short(), "-")
// gitCommit. return ret
// }
// 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

@ -9,6 +9,7 @@ import (
"testing" "testing"
ts "tailscale.com" ts "tailscale.com"
"tailscale.com/version"
) )
func TestAlpineTag(t *testing.T) { func TestAlpineTag(t *testing.T) {
@ -39,3 +40,12 @@ func readAlpineTag(t *testing.T, file string) string {
} }
return "" return ""
} }
func TestShortAllocs(t *testing.T) {
allocs := int(testing.AllocsPerRun(10000, func() {
_ = version.Short()
}))
if allocs > 0 {
t.Errorf("allocs = %v; want 0", allocs)
}
}

Loading…
Cancel
Save