turbo-editors/turbo-corepublic Fork 0
v1.0.1
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

version.go · 245 lines · 8.2 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 20h ago1// Package version tells the editor what build of itself it is.
2//
3// The number comes from whichever of three sources knows it, in this order:
4// the linker, the Go build system, and a constant of last resort. Nothing here
5// reads a file or runs a command, so it costs nothing to ask.
6package version
7
8import (
9 "runtime/debug"
10 "strings"
11 "time"
12)
13
14// The build stamps these through the linker:
15//
📦 Turbo Core f3ade8d k33g 12h ago16// go build -ldflags "-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0'" .
🛟 Updated. 28d5985 k33g 20h ago17//
18// The Makefile and scripts/install.sh both do it, filling stamp from
19// `git describe --tags --dirty`. They are unexported because nothing but the
20// linker should ever write them, and package-level strings are what -X can set.
21var (
22 stamp string
23 commit string
24 built string
25)
26
27// unknownNumber is what a build reports when no source could name it. It is
28// deliberately not a version number: a stale one printed as fact is the defect
29// this package exists to remove.
30const unknownNumber = "unknown"
31
32// develVersion is what the Go build system calls a binary built from a working
33// tree when it can read no version at all.
34const develVersion = "(devel)"
35
36// pseudoTailLength is the width of the tail every Go pseudo-version ends with:
37// a dash, a 14-digit UTC timestamp, a dash, and twelve hex characters.
38const pseudoTailLength = 1 + 14 + 1 + 12
39
40// shortCommitLength is how much of a revision hash is worth showing. Seven
41// characters is what git itself abbreviates to, and what a reader will paste
42// back into `git show`.
43const shortCommitLength = 7
44
45// Info is what a binary knows about its own build.
46//
47// Commit and Built are empty when nothing recorded them, which is normal: a
48// binary from `go install …@v0.2.0` knows its version and nothing else. A
49// caller shows the fields it has and says nothing about the rest.
50type Info struct {
51 // Number is the version, without a leading "v": "0.2.0" for a release,
52 // "0.1.0-14-g88a4c38" for a build between two of them.
53 Number string
54 // Commit is the abbreviated revision, when it is known separately from
55 // Number.
56 Commit string
57 // Built is when the binary was linked, in RFC 3339, when it is known.
58 Built string
59}
60
61// Current returns what this binary knows about itself.
62//
63// info := version.Current()
64// fmt.Println(info.Number) // "0.1.0-14-g88a4c38"
65func Current() Info {
66 return resolve(stamp, commit, built, readBuildInfo())
67}
68
69// String renders the whole of what is known on one line, for `-version`.
70//
71// One line rather than several because the installer prints it beside a path,
72// and because a caller that wants the parts has the fields.
73//
74// version.Info{Number: "0.2.0", Commit: "88a4c38"}.String()
75// // "0.2.0 (88a4c38)"
76func (i Info) String() string {
77 switch {
78 case i.Commit != "" && i.Built != "":
79 return i.Number + " (" + i.Commit + ", built " + i.Built + ")"
80 case i.Commit != "":
81 return i.Number + " (" + i.Commit + ")"
82 case i.Built != "":
83 return i.Number + " (built " + i.Built + ")"
84 }
85 return i.Number
86}
87
88// resolve turns the raw build facts into an Info.
89//
90// It is separate from Current so that every combination can be tested without
91// linker flags: the interesting cases are precisely the ones a test binary
92// cannot be built into.
93func resolve(stamp, commit, built string, info *debug.BuildInfo) Info {
94 if stamp != "" {
95 return Info{Number: trimV(stamp), Commit: shorten(commit), Built: built}
96 }
97 if info == nil {
98 return Info{Number: unknownNumber}
99 }
100 return fromBuildInfo(info)
101}
102
103// fromBuildInfo reads what the Go build system recorded.
104//
105// Two quite different builds arrive here. `go install <module>@v0.2.0` records
106// the module version and **no** VCS information at all, because there was no
107// repository to read. A plain `go build .` in a checkout records the reverse:
108// the version is "(devel)" and the revision is known. Neither can name a tag —
109// the build system does not read them — which is why a build that wants to
110// show one has to be stamped.
111func fromBuildInfo(info *debug.BuildInfo) Info {
112 settings := settingsOf(info)
113 out := Info{
114 Number: trimV(info.Main.Version),
115 Commit: shorten(settings["vcs.revision"]),
116 }
117
118 // A pseudo-version is the Go tool naming a commit that no tag names —
119 // "0.1.1-0.20260831165958-88a4c3859bf3+dirty". It is unreadable in a
120 // dialog, and its "0.1.1" is a patch release that does not exist, so it is
121 // reported for what it means rather than for what it says.
122 if out.Number == "" || out.Number == develVersion || isPseudoVersion(out.Number) {
123 out.Number = develNumber(settings)
124 }
125 // vcs.time is when the *commit* was made, not when this binary was linked,
126 // so it is not a build date and is not reported as one.
127 return out
128}
129
130// develNumber names a build made from a working tree, where no tag is
131// available: "devel", and "devel-dirty" when the tree had uncommitted changes.
132func develNumber(settings map[string]string) string {
133 if settings["vcs.modified"] == "true" {
134 return "devel-dirty"
135 }
136 if _, built := settings["vcs.revision"]; built {
137 return "devel"
138 }
139 return unknownNumber
140}
141
142// isPseudoVersion reports whether a module version is one the Go tool invented
143// for a commit that no tag names.
144//
145// Every form of one ends the same way, whatever base version precedes it, and
146// build metadata such as "+dirty" is not part of that tail.
147//
148// isPseudoVersion("0.1.1-0.20260831165958-88a4c3859bf3+dirty") // true
149// isPseudoVersion("0.2.0") // false
150func isPseudoVersion(number string) bool {
151 number = withoutBuildMetadata(number)
152 if len(number) <= pseudoTailLength {
153 return false // nothing left for the base version it was derived from
154 }
155 return hasPseudoTail(number[len(number)-pseudoTailLength:])
156}
157
158// hasPseudoTail reports whether a tail of exactly pseudoTailLength characters
159// is the "-yyyymmddhhmmss-abcdefabcdef" a pseudo-version ends with.
160func hasPseudoTail(tail string) bool {
161 if !isAll(tail[1:15], isDigit) || !isAll(tail[16:], isHex) {
162 return false
163 }
164 // The character before the timestamp is a dash when no tag precedes the
165 // commit ("v0.0.0-<ts>-<hash>") and a dot when one does, because the base
166 // then ends in "-0." or "-pre.0.".
167 separatesTimestamp := tail[0] == '-' || tail[0] == '.'
168 return separatesTimestamp && tail[15] == '-'
169}
170
171// withoutBuildMetadata drops the "+dirty" or "+incompatible" a version may
172// carry, which is not part of the pseudo-version tail.
173func withoutBuildMetadata(number string) string {
174 if plus := strings.IndexByte(number, '+'); plus >= 0 {
175 return number[:plus]
176 }
177 return number
178}
179
180// isAll reports whether every byte of s satisfies want.
181func isAll(s string, want func(byte) bool) bool {
182 for i := 0; i < len(s); i++ {
183 if !want(s[i]) {
184 return false
185 }
186 }
187 return true
188}
189
190func isDigit(c byte) bool { return c >= '0' && c <= '9' }
191
192func isHex(c byte) bool { return isDigit(c) || (c >= 'a' && c <= 'f') }
193
194// settingsOf turns a build's settings into a map, which is how every caller
195// here wants them.
196func settingsOf(info *debug.BuildInfo) map[string]string {
197 out := make(map[string]string, len(info.Settings))
198 for _, setting := range info.Settings {
199 out[setting.Key] = setting.Value
200 }
201 return out
202}
203
204// trimV drops the leading "v" a git tag carries. The tag is `v0.2.0`; what a
205// person reads in an About box is `0.2.0`.
206func trimV(number string) string {
207 if len(number) > 1 && number[0] == 'v' && number[1] >= '0' && number[1] <= '9' {
208 return number[1:]
209 }
210 return number
211}
212
213// shorten abbreviates a revision hash the way git does.
214func shorten(revision string) string {
215 if len(revision) <= shortCommitLength {
216 return revision
217 }
218 return revision[:shortCommitLength]
219}
220
221// BuiltAt renders Built for a person: "2026-08-31 18:04 UTC".
222//
223// A stamp that does not parse is shown as it is rather than dropped — a build
224// whose date is malformed should say so, not appear to have none.
225//
226// version.Info{Built: "2026-08-31T18:04:05Z"}.BuiltAt() // "2026-08-31 18:04 UTC"
227func (i Info) BuiltAt() string {
228 if i.Built == "" {
229 return ""
230 }
231 moment, err := time.Parse(time.RFC3339, i.Built)
232 if err != nil {
233 return i.Built
234 }
235 return moment.UTC().Format("2006-01-02 15:04") + " UTC"
236}
237
238// readBuildInfo is debug.ReadBuildInfo, with the "ok" folded into a nil.
239func readBuildInfo() *debug.BuildInfo {
240 info, ok := debug.ReadBuildInfo()
241 if !ok {
242 return nil
243 }
244 return info
245}