返回 DeepSeek-Reasonix
buildinfo.go
根目录 / internal / cli / buildinfo.go
1 package cli
2
3 import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "runtime"
8 "runtime/debug"
9 "strings"
10 )
11
12 // BuildInfo is the machine- and human-readable build identity for
13 // `reasonix version` / `reasonix --version`. Release builds may only fill
14 // Version; source and CI builds inject the rest via -ldflags so the binary is
15 // traceable without shelling out to git.
16 //
17 // Plan contract (Integration D/E):
18 // - `reasonix --version` / `-v` stay single-line: "reasonix <version>"
19 // - `reasonix version --verbose` prints structured fields
20 // - `reasonix version --json` prints a JSON object
21 // - Fields are limited to version, commit, build time, target (and go runtime);
22 // no config paths and no fixed CST conversion.
23 type BuildInfo struct {
24 Version string
25 GitCommit string
26 BuildTimeUTC string
27 BuildTarget string
28 }
29
30 // versionCommand handles `reasonix version [flags]`. When allowFlags is false
31 // (top-level --version / -v), output is always the single-line form.
32 func versionCommand(args []string, info BuildInfo, allowFlags bool) int {
33 info = info.withDefaults()
34 if !allowFlags {
35 fmt.Println(info.singleLine())
36 return 0
37 }
38 verbose := false
39 jsonOut := false
40 for _, arg := range args {
41 switch arg {
42 case "--verbose", "-verbose":
43 verbose = true
44 case "--json":
45 jsonOut = true
46 case "--help", "-h":
47 versionUsage(os.Stdout)
48 return 0
49 default:
50 fmt.Fprintf(os.Stderr, "unknown version flag %q\n", arg)
51 versionUsage(os.Stderr)
52 return 2
53 }
54 }
55 if jsonOut && verbose {
56 fmt.Fprintln(os.Stderr, "error: --json and --verbose are mutually exclusive")
57 return 2
58 }
59 if jsonOut {
60 enc := json.NewEncoder(os.Stdout)
61 enc.SetIndent("", " ")
62 if err := enc.Encode(info.jsonPayload()); err != nil {
63 fmt.Fprintln(os.Stderr, "error:", err)
64 return 1
65 }
66 return 0
67 }
68 if verbose {
69 fmt.Print(info.verboseText())
70 return 0
71 }
72 fmt.Println(info.singleLine())
73 return 0
74 }
75
76 func versionUsage(w *os.File) {
77 fmt.Fprintln(w, `Usage:
78 reasonix version
79 reasonix version --verbose
80 reasonix version --json
81 reasonix --version
82 reasonix -v
83
84 --version / -v always print a single line (reasonix <version>).
85 version --verbose prints build metadata; version --json prints the same as JSON.`)
86 }
87
88 func (b BuildInfo) singleLine() string {
89 return "reasonix " + strings.TrimSpace(b.withDefaults().Version)
90 }
91
92 func (b BuildInfo) verboseText() string {
93 b = b.withDefaults()
94 var lines []string
95 appendField := func(k, v string) {
96 v = strings.TrimSpace(v)
97 if v != "" {
98 lines = append(lines, k+": "+v)
99 }
100 }
101 lines = append(lines, b.singleLine())
102 appendField("git_commit", b.GitCommit)
103 appendField("build_time_utc", b.BuildTimeUTC)
104 appendField("build_target", b.BuildTarget)
105 appendField("go", fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH))
106 return strings.Join(lines, "\n") + "\n"
107 }
108
109 type versionJSON struct {
110 Version string `json:"version"`
111 GitCommit string `json:"git_commit,omitempty"`
112 BuildTimeUTC string `json:"build_time_utc,omitempty"`
113 BuildTarget string `json:"build_target,omitempty"`
114 Go string `json:"go"`
115 }
116
117 func (b BuildInfo) jsonPayload() versionJSON {
118 b = b.withDefaults()
119 return versionJSON{
120 Version: b.Version,
121 GitCommit: omitUnknown(b.GitCommit),
122 BuildTimeUTC: omitUnknown(b.BuildTimeUTC),
123 BuildTarget: b.BuildTarget,
124 Go: fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH),
125 }
126 }
127
128 func omitUnknown(v string) string {
129 v = strings.TrimSpace(v)
130 if v == "" || v == "unknown" {
131 return ""
132 }
133 return v
134 }
135
136 func (b BuildInfo) withDefaults() BuildInfo {
137 if strings.TrimSpace(b.Version) == "" {
138 b.Version = "dev"
139 }
140 if strings.TrimSpace(b.BuildTarget) == "" {
141 b.BuildTarget = defaultBuildTarget()
142 }
143 // Commit may fall back to the embedded VCS revision from `go build` when
144 // ldflags were not set (local `go run` / untagged builds). Build time must
145 // NOT use vcs.time — that is commit timestamp, not the binary's build clock.
146 // Official release/npm/desktop/Makefile paths inject buildTimeUTC explicitly.
147 if strings.TrimSpace(b.GitCommit) == "" {
148 if rev, ok := buildSetting("vcs.revision"); ok {
149 b.GitCommit = shortGitRevision(rev)
150 }
151 }
152 if strings.TrimSpace(b.GitCommit) == "" {
153 b.GitCommit = "unknown"
154 }
155 if strings.TrimSpace(b.BuildTimeUTC) == "" {
156 b.BuildTimeUTC = "unknown"
157 }
158 return b
159 }
160
161 func buildSetting(key string) (string, bool) {
162 info, ok := debug.ReadBuildInfo()
163 if !ok {
164 return "", false
165 }
166 for _, setting := range info.Settings {
167 if setting.Key == key && strings.TrimSpace(setting.Value) != "" {
168 return setting.Value, true
169 }
170 }
171 return "", false
172 }
173
174 func shortGitRevision(revision string) string {
175 revision = strings.TrimSpace(revision)
176 if len(revision) > 12 {
177 return revision[:12]
178 }
179 return revision
180 }
181
182 func defaultBuildTarget() string {
183 if runtime.GOOS == "" || runtime.GOARCH == "" {
184 return ""
185 }
186 return runtime.GOOS + "/" + runtime.GOARCH
187 }
188
188 lines GO