@@ -18,10 +18,23 @@ package cmd
1818
1919import (
2020 "fmt"
21+ "os"
22+ "os/exec"
23+ "path/filepath"
24+ "runtime"
2125 "runtime/debug"
26+ "strconv"
27+ "strings"
28+ "time"
29+
30+ "golang.org/x/mod/semver"
2231)
2332
24- const unknown = "unknown"
33+ const (
34+ unknown = "unknown"
35+ develVersion = "(devel)"
36+ pseudoVersionTimestampLayout = "20060102150405"
37+ )
2538
2639// var needs to be used instead of const as ldflags is used to fill this
2740// information in the release process
@@ -47,11 +60,7 @@ type version struct {
4760
4861// versionString returns the Full CLI version
4962func versionString () string {
50- if kubeBuilderVersion == unknown {
51- if info , ok := debug .ReadBuildInfo (); ok && info .Main .Version != "" {
52- kubeBuilderVersion = info .Main .Version
53- }
54- }
63+ kubeBuilderVersion = getKubebuilderVersion ()
5564
5665 return fmt .Sprintf ("Version: %#v" , version {
5766 kubeBuilderVersion ,
@@ -65,10 +74,187 @@ func versionString() string {
6574
6675// getKubebuilderVersion returns only the CLI version string
6776func getKubebuilderVersion () string {
68- if kubeBuilderVersion == unknown {
69- if info , ok := debug .ReadBuildInfo (); ok && info .Main .Version != "" {
70- kubeBuilderVersion = info .Main .Version
71- }
77+ if strings .Contains (kubeBuilderVersion , "dirty" ) {
78+ return develVersion
79+ }
80+ if shouldResolveVersion (kubeBuilderVersion ) {
81+ kubeBuilderVersion = resolveKubebuilderVersion ()
7282 }
7383 return kubeBuilderVersion
7484}
85+
86+ func shouldResolveVersion (v string ) bool {
87+ return v == "" || v == unknown || v == develVersion
88+ }
89+
90+ func resolveKubebuilderVersion () string {
91+ if info , ok := debug .ReadBuildInfo (); ok {
92+ if info .Main .Sum == "" {
93+ return develVersion
94+ }
95+ mainVersion := strings .TrimSpace (info .Main .Version )
96+ if mainVersion != "" && mainVersion != develVersion {
97+ return mainVersion
98+ }
99+
100+ if v := pseudoVersionFromGit (info .Main .Path ); v != "" {
101+ return v
102+ }
103+ }
104+
105+ if v := pseudoVersionFromGit ("" ); v != "" {
106+ return v
107+ }
108+
109+ return unknown
110+ }
111+
112+ func pseudoVersionFromGit (modulePath string ) string {
113+ repoRoot , err := findRepoRoot ()
114+ if err != nil {
115+ return ""
116+ }
117+ return pseudoVersionFromGitDir (modulePath , repoRoot )
118+ }
119+
120+ func pseudoVersionFromGitDir (modulePath , repoRoot string ) string {
121+ dirty , err := repoDirty (repoRoot )
122+ if err != nil {
123+ return ""
124+ }
125+ if dirty {
126+ return develVersion
127+ }
128+
129+ commitHash , err := runGitCommand (repoRoot , "rev-parse" , "--short=12" , "HEAD" )
130+ if err != nil || commitHash == "" {
131+ return ""
132+ }
133+
134+ commitTimestamp , err := runGitCommand (repoRoot , "show" , "-s" , "--format=%ct" , "HEAD" )
135+ if err != nil || commitTimestamp == "" {
136+ return ""
137+ }
138+ seconds , err := strconv .ParseInt (commitTimestamp , 10 , 64 )
139+ if err != nil {
140+ return ""
141+ }
142+ timestamp := time .Unix (seconds , 0 ).UTC ().Format (pseudoVersionTimestampLayout )
143+
144+ if tag , err := runGitCommand (repoRoot , "describe" , "--tags" , "--exact-match" ); err == nil {
145+ tag = strings .TrimSpace (tag )
146+ if tag != "" {
147+ return tag
148+ }
149+ }
150+
151+ if baseTag , err := runGitCommand (repoRoot , "describe" , "--tags" , "--abbrev=0" ); err == nil {
152+ baseTag = strings .TrimSpace (baseTag )
153+ if semver .IsValid (baseTag ) {
154+ if next := incrementPatch (baseTag ); next != "" {
155+ return fmt .Sprintf ("%s-0.%s-%s" , next , timestamp , commitHash )
156+ }
157+ }
158+ if baseTag != "" {
159+ return baseTag
160+ }
161+ }
162+
163+ major := moduleMajorVersion (modulePath )
164+ return buildDefaultPseudoVersion (major , timestamp , commitHash )
165+ }
166+
167+ func repoDirty (repoRoot string ) (bool , error ) {
168+ status , err := runGitCommand (repoRoot , "status" , "--porcelain" , "--untracked-files=no" )
169+ if err != nil {
170+ return false , err
171+ }
172+ return status != "" , nil
173+ }
174+
175+ func incrementPatch (tag string ) string {
176+ trimmed := strings .TrimPrefix (tag , "v" )
177+ trimmed = strings .SplitN (trimmed , "-" , 2 )[0 ]
178+ parts := strings .Split (trimmed , "." )
179+ if len (parts ) < 3 {
180+ return ""
181+ }
182+ major , err := strconv .Atoi (parts [0 ])
183+ if err != nil {
184+ return ""
185+ }
186+ minor , err := strconv .Atoi (parts [1 ])
187+ if err != nil {
188+ return ""
189+ }
190+ patch , err := strconv .Atoi (parts [2 ])
191+ if err != nil {
192+ return ""
193+ }
194+ patch ++
195+ return fmt .Sprintf ("v%d.%d.%d" , major , minor , patch )
196+ }
197+
198+ func buildDefaultPseudoVersion (major int , timestamp , commitHash string ) string {
199+ if major < 0 {
200+ major = 0
201+ }
202+ return fmt .Sprintf ("v%d.0.0-%s-%s" , major , timestamp , commitHash )
203+ }
204+
205+ func moduleMajorVersion (modulePath string ) int {
206+ if modulePath == "" {
207+ return 0
208+ }
209+ lastSlash := strings .LastIndex (modulePath , "/v" )
210+ if lastSlash == - 1 || lastSlash == len (modulePath )- 2 {
211+ return 0
212+ }
213+ majorStr := modulePath [lastSlash + 2 :]
214+ if strings .Contains (majorStr , "/" ) {
215+ majorStr = majorStr [:strings .Index (majorStr , "/" )]
216+ }
217+ major , err := strconv .Atoi (majorStr )
218+ if err != nil {
219+ return 0
220+ }
221+ return major
222+ }
223+
224+ func findRepoRoot () (string , error ) {
225+ _ , currentFile , _ , ok := runtime .Caller (0 )
226+ if ! ok {
227+ return "" , fmt .Errorf ("failed to determine caller" )
228+ }
229+
230+ if ! filepath .IsAbs (currentFile ) {
231+ abs , err := filepath .Abs (currentFile )
232+ if err != nil {
233+ return "" , fmt .Errorf ("getting absolute path: %w" , err )
234+ }
235+ currentFile = abs
236+ }
237+
238+ dir := filepath .Dir (currentFile )
239+ for {
240+ if dir == "" || dir == filepath .Dir (dir ) {
241+ return "" , fmt .Errorf ("git repository root not found from %s" , currentFile )
242+ }
243+
244+ if _ , err := os .Stat (filepath .Join (dir , ".git" )); err == nil {
245+ return dir , nil
246+ }
247+ dir = filepath .Dir (dir )
248+ }
249+ }
250+
251+ func runGitCommand (dir string , args ... string ) (string , error ) {
252+ cmd := exec .Command ("git" , args ... )
253+ cmd .Dir = dir
254+ cmd .Env = append (os .Environ (), "LC_ALL=C" , "LANG=C" )
255+ output , err := cmd .CombinedOutput ()
256+ if err != nil {
257+ return "" , fmt .Errorf ("running git %v: %w" , args , err )
258+ }
259+ return strings .TrimSpace (string (output )), nil
260+ }
0 commit comments