version.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // Package version provides the version that the binary was built at.
  4. package version
  5. import (
  6. "fmt"
  7. "runtime/debug"
  8. "strconv"
  9. "strings"
  10. "sync"
  11. tailscaleroot "tailscale.com"
  12. "tailscale.com/types/lazy"
  13. )
  14. // Stamp vars can have their value set at build time by linker flags (see
  15. // build_dist.sh for an example). When set, these stamps serve as additional
  16. // inputs to computing the binary's version as returned by the functions in this
  17. // package.
  18. //
  19. // All stamps are optional.
  20. var (
  21. // longStamp is the full version identifier of the build. If set, it is
  22. // returned verbatim by Long() and other functions that return Long()'s
  23. // output.
  24. longStamp string
  25. // shortStamp is the short version identifier of the build. If set, it
  26. // is returned verbatim by Short() and other functions that return Short()'s
  27. // output.
  28. shortStamp string
  29. // gitCommitStamp is the git commit of the github.com/tailscale/tailscale
  30. // repository at which Tailscale was built. Its format is the one returned
  31. // by `git rev-parse <commit>`. If set, it is used instead of any git commit
  32. // information embedded by the Go tool.
  33. gitCommitStamp string
  34. // gitDirtyStamp is whether the git checkout from which the code was built
  35. // was dirty. Its value is ORed with the dirty bit embedded by the Go tool.
  36. //
  37. // We need this because when we build binaries from another repo that
  38. // imports tailscale.com, the Go tool doesn't stamp any dirtiness info into
  39. // the binary. Instead, we have to inject the dirty bit ourselves here.
  40. gitDirtyStamp bool
  41. // extraGitCommit, is the git commit of a "supplemental" repository at which
  42. // Tailscale was built. Its format is the same as gitCommit.
  43. //
  44. // extraGitCommit is used to track the source revision when the main
  45. // Tailscale repository is integrated into and built from another repository
  46. // (for example, Tailscale's proprietary code, or the Android OSS
  47. // repository). Together, gitCommit and extraGitCommit exactly describe what
  48. // repositories and commits were used in a build.
  49. extraGitCommitStamp string
  50. )
  51. var long lazy.SyncValue[string]
  52. // Long returns a full version number for this build, of one of the forms:
  53. //
  54. // - "x.y.z-commithash-otherhash" for release builds distributed by Tailscale
  55. // - "x.y.z-commithash" for release builds built with build_dist.sh
  56. // - "x.y.z-changecount-commithash-otherhash" for untagged release branch
  57. // builds by Tailscale (these are not distributed).
  58. // - "x.y.z-changecount-commithash" for untagged release branch builds
  59. // built with build_dist.sh
  60. // - "x.y.z-devYYYYMMDD-commithash{,-dirty}" for builds made with plain "go
  61. // build" or "go install"
  62. // - "x.y.z-ERR-BuildInfo" for builds made by plain "go run"
  63. func Long() string {
  64. return long.Get(func() string {
  65. if longStamp != "" {
  66. return longStamp
  67. }
  68. bi := getEmbeddedInfo()
  69. if !bi.valid {
  70. return strings.TrimSpace(tailscaleroot.VersionDotTxt) + "-ERR-BuildInfo"
  71. }
  72. return fmt.Sprintf("%s-dev%s-t%s%s", strings.TrimSpace(tailscaleroot.VersionDotTxt), bi.commitDate, bi.commitAbbrev(), dirtyString())
  73. })
  74. }
  75. var short lazy.SyncValue[string]
  76. // Short returns a short version number for this build, of the forms:
  77. //
  78. // - "x.y.z" for builds distributed by Tailscale or built with build_dist.sh
  79. // - "x.y.z-devYYYYMMDD" for builds made with plain "go build" or "go install"
  80. // - "x.y.z-ERR-BuildInfo" for builds made by plain "go run"
  81. func Short() string {
  82. return short.Get(func() string {
  83. if shortStamp != "" {
  84. return shortStamp
  85. }
  86. bi := getEmbeddedInfo()
  87. if !bi.valid {
  88. return strings.TrimSpace(tailscaleroot.VersionDotTxt) + "-ERR-BuildInfo"
  89. }
  90. return strings.TrimSpace(tailscaleroot.VersionDotTxt) + "-dev" + bi.commitDate
  91. })
  92. }
  93. type embeddedInfo struct {
  94. valid bool
  95. commit string
  96. commitDate string
  97. commitTime string
  98. dirty bool
  99. }
  100. func (i embeddedInfo) commitAbbrev() string {
  101. if len(i.commit) >= 9 {
  102. return i.commit[:9]
  103. }
  104. return i.commit
  105. }
  106. var getEmbeddedInfo = sync.OnceValue(func() embeddedInfo {
  107. bi, ok := debug.ReadBuildInfo()
  108. if !ok {
  109. return embeddedInfo{}
  110. }
  111. ret := embeddedInfo{valid: true}
  112. for _, s := range bi.Settings {
  113. switch s.Key {
  114. case "vcs.revision":
  115. ret.commit = s.Value
  116. case "vcs.time":
  117. ret.commitTime = s.Value
  118. if len(s.Value) >= len("yyyy-mm-dd") {
  119. ret.commitDate = s.Value[:len("yyyy-mm-dd")]
  120. ret.commitDate = strings.ReplaceAll(ret.commitDate, "-", "")
  121. }
  122. case "vcs.modified":
  123. ret.dirty = s.Value == "true"
  124. }
  125. }
  126. if ret.commit == "" || ret.commitDate == "" {
  127. // Build info is present in the binary, but has no useful data. Act as
  128. // if it's missing.
  129. return embeddedInfo{}
  130. }
  131. return ret
  132. })
  133. func gitCommit() string {
  134. if gitCommitStamp != "" {
  135. return gitCommitStamp
  136. }
  137. return getEmbeddedInfo().commit
  138. }
  139. func gitDirty() bool {
  140. if gitDirtyStamp {
  141. return true
  142. }
  143. return getEmbeddedInfo().dirty
  144. }
  145. func dirtyString() string {
  146. if gitDirty() {
  147. return "-dirty"
  148. }
  149. return ""
  150. }
  151. func majorMinorPatch() string {
  152. ret, _, _ := strings.Cut(Short(), "-")
  153. return ret
  154. }
  155. func isValidLongWithTwoRepos(v string) bool {
  156. s := strings.Split(v, "-")
  157. if len(s) != 3 {
  158. return false
  159. }
  160. hexChunk := func(s string) bool {
  161. if len(s) < 6 {
  162. return false
  163. }
  164. for i := range len(s) {
  165. b := s[i]
  166. if (b < '0' || b > '9') && (b < 'a' || b > 'f') {
  167. return false
  168. }
  169. }
  170. return true
  171. }
  172. v, t, g := s[0], s[1], s[2]
  173. if !strings.HasPrefix(t, "t") || !strings.HasPrefix(g, "g") ||
  174. !hexChunk(t[1:]) || !hexChunk(g[1:]) {
  175. return false
  176. }
  177. nums := strings.Split(v, ".")
  178. if len(nums) != 3 {
  179. return false
  180. }
  181. for i, n := range nums {
  182. bits := 8
  183. if i == 2 {
  184. bits = 16
  185. }
  186. if _, err := strconv.ParseUint(n, 10, bits); err != nil {
  187. return false
  188. }
  189. }
  190. return true
  191. }