4
4
package vercheck
5
5
6
6
import (
7
+ "bytes"
7
8
"fmt"
8
9
"io"
10
+ "io/fs"
9
11
"os"
10
12
"os/exec"
11
13
"path/filepath"
14
+ "strings"
12
15
16
+ "github.com/fatih/color"
13
17
"github.com/pkg/errors"
18
+ "go.jetpack.io/devbox/internal/build"
14
19
"golang.org/x/mod/semver"
15
20
16
21
"go.jetpack.io/devbox/internal/boxcli/usererr"
17
- "go.jetpack.io/devbox/internal/envir"
18
- "go.jetpack.io/devbox/internal/ux"
19
22
"go.jetpack.io/devbox/internal/xdg"
20
23
)
21
24
22
25
// Keep this in-sync with latest version in launch.sh. If this version is newer
23
- // Than the version in launch.sh, we'll print a warning .
24
- const expectedLauncherVersion = "v0.1 .0"
26
+ // than the version in launch.sh, we'll print a notice .
27
+ const expectedLauncherVersion = "v0.2 .0"
25
28
26
- func CheckLauncherVersion (w io.Writer ) {
27
- launcherVersion := os .Getenv (envir .LauncherVersion )
28
- if launcherVersion == "" || envir .IsDevboxCloud () {
29
+ // currentDevboxVersion is the version of the devbox CLI binary that is currently running.
30
+ // We use this variable so we can mock it in tests.
31
+ var currentDevboxVersion = build .Version
32
+
33
+ // envDevboxLatestVersion is the latest version available of the devbox CLI binary.
34
+ // Change to env.DevboxLatestVersion. Not doing so to minimize merge conflicts.
35
+ var envDevboxLatestVersion = "DEVBOX_LATEST_VERSION"
36
+
37
+ // CheckVersion checks the launcher and binary versions and prints a notice if
38
+ // they are out of date.
39
+ func CheckVersion (w io.Writer ) {
40
+
41
+ // Replace with envir.IsDevboxCloud(). Not doing so to minimize merge conflicts.
42
+ if os .Getenv ("DEVBOX_REGION" ) != "" {
29
43
return
30
44
}
31
45
32
- // If launcherVersion is invalid, this will return 0 and we won't print a warning
33
- if semver .Compare ("v" + launcherVersion , expectedLauncherVersion ) < 0 {
34
- ux .Fwarning (
35
- w ,
36
- "newer launcher version %s is available (current = v%s), please update " +
37
- "using `devbox version update`\n " ,
38
- expectedLauncherVersion ,
39
- launcherVersion ,
40
- )
46
+ launcherNotice := launcherVersionNotice ()
47
+ if launcherNotice != "" {
48
+ // TODO: use ux.FNotice
49
+ color .New (color .FgYellow ).Fprintf (w , launcherNotice )
50
+
51
+ // fallthrough to alert the user about a new Devbox CLI binary being possibly available
52
+ }
53
+
54
+ devboxNotice := devboxVersionNotice ()
55
+ if devboxNotice != "" {
56
+ // TODO: use ux.FNotice
57
+ color .New (color .FgYellow ).Fprintf (w , devboxNotice )
41
58
}
42
59
}
43
60
44
- // SelfUpdate updates the devbox launcher and binary. It ignores and deletes the
45
- // version cache
61
+ // SelfUpdate updates the devbox launcher and devbox CLI binary.
62
+ // It ignores and deletes the version cache.
63
+ //
64
+ // The launcher is a wrapper bash script introduced to manage the auto-update process
65
+ // for devbox. The production devbox application is actually this launcher script
66
+ // that acts as "devbox" and delegates commands to the devbox CLI binary.
46
67
func SelfUpdate (stdOut , stdErr io.Writer ) error {
68
+ if isNewLauncherAvailable () {
69
+ return selfUpdateLauncher (stdOut , stdErr )
70
+ }
71
+
72
+ return selfUpdateDevbox (stdErr )
73
+ }
74
+
75
+ func selfUpdateLauncher (stdOut , stdErr io.Writer ) error {
47
76
installScript := ""
48
77
if _ , err := exec .LookPath ("curl" ); err == nil {
49
78
installScript = "curl -fsSL https://get.jetpack.io/devbox | bash"
@@ -53,26 +82,187 @@ func SelfUpdate(stdOut, stdErr io.Writer) error {
53
82
return usererr .New ("curl or wget is required to update devbox. Please install either and try again." )
54
83
}
55
84
56
- // Delete version cache. Keep this in-sync with whatever logic is in launch.sh
57
- cacheDir := xdg .CacheSubpath ("devbox" )
58
- versionCacheFile := filepath .Join (cacheDir , "latest-version" )
59
- _ = os .Remove (versionCacheFile )
85
+ // Delete current version file. This will trigger an update when invoking any devbox command;
86
+ // in this case, inside triggerUpdate function.
87
+ if err := removeCurrentVersionFile (); err != nil {
88
+ return err
89
+ }
60
90
91
+ // Fetch the new launcher.
61
92
cmd := exec .Command ("sh" , "-c" , installScript )
62
93
cmd .Stdout = stdOut
63
94
cmd .Stderr = stdErr
64
95
if err := cmd .Run (); err != nil {
65
96
return errors .WithStack (err )
66
97
}
67
98
68
- fmt .Fprint (stdErr , "Latest version: " )
69
- exe , err := os .Executable ()
99
+ // Invoke a devbox command to trigger an update of the devbox CLI binary.
100
+ updated , err := triggerUpdate (stdErr )
101
+ if err != nil {
102
+ return errors .WithStack (err )
103
+ }
104
+
105
+ printSuccessMessage (stdErr , "Launcher" , currentLauncherVersion (), updated .launcherVersion )
106
+ printSuccessMessage (stdErr , "Devbox" , currentDevboxVersion , updated .devboxVersion )
107
+
108
+ return nil
109
+ }
110
+
111
+ // selfUpdateDevbox will update the devbox CLI binary to the latest version.
112
+ func selfUpdateDevbox (stdErr io.Writer ) error {
113
+ // Delete current version file. This will trigger an update when the next devbox command is run;
114
+ // in this case, inside triggerUpdate function.
115
+ if err := removeCurrentVersionFile (); err != nil {
116
+ return err
117
+ }
118
+
119
+ updated , err := triggerUpdate (stdErr )
70
120
if err != nil {
71
121
return errors .WithStack (err )
72
122
}
73
- cmd = exec .Command (exe , "version" )
74
- // The output of version is incidental, so just send it all to stdErr
75
- cmd .Stdout = stdErr
123
+
124
+ printSuccessMessage (stdErr , "Devbox" , currentDevboxVersion , updated .devboxVersion )
125
+
126
+ return nil
127
+ }
128
+
129
+ type updatedVersions struct {
130
+ devboxVersion string
131
+ launcherVersion string
132
+ }
133
+
134
+ // triggerUpdate runs `devbox version -v` and triggers an update since a new
135
+ // version is available. It parses the output to get the new launcher and
136
+ // devbox versions.
137
+ func triggerUpdate (stdErr io.Writer ) (* updatedVersions , error ) {
138
+
139
+ exe , err := os .Executable ()
140
+ if err != nil {
141
+ return nil , errors .WithStack (err )
142
+ }
143
+ // TODO savil. Add a --json flag to devbox version and parse the output as JSON
144
+ cmd := exec .Command (exe , "version" , "-v" )
145
+
146
+ buf := new (bytes.Buffer )
147
+ cmd .Stdout = io .MultiWriter (stdErr , buf )
76
148
cmd .Stderr = stdErr
77
- return errors .WithStack (cmd .Run ())
149
+ if err := cmd .Run (); err != nil {
150
+ return nil , errors .WithStack (err )
151
+ }
152
+
153
+ // Parse the output to ascertain the new devbox and launcher versions
154
+ updated := & updatedVersions {}
155
+ for _ , line := range strings .Split (buf .String (), "\n " ) {
156
+ if strings .HasPrefix (line , "Version:" ) {
157
+ updated .devboxVersion = strings .TrimSpace (strings .TrimPrefix (line , "Version:" ))
158
+ }
159
+
160
+ if strings .HasPrefix (line , "Launcher:" ) {
161
+ updated .launcherVersion = strings .TrimSpace (strings .TrimPrefix (line , "Launcher:" ))
162
+ }
163
+ }
164
+ return updated , nil
165
+ }
166
+
167
+ func printSuccessMessage (w io.Writer , toolName , oldVersion , newVersion string ) {
168
+ var msg string
169
+ if semverCompare (oldVersion , newVersion ) == 0 {
170
+ msg = fmt .Sprintf ("already at %s version %s" , toolName , newVersion )
171
+ } else {
172
+ msg = fmt .Sprintf ("updated to %s version %s" , toolName , newVersion )
173
+ }
174
+
175
+ // Prints a <green>Success:</green> message to the writer.
176
+ // Move to ux.Success. Not doing so to minimize merge-conflicts.
177
+ fmt .Fprintf (w , "%s%s\n " , color .New (color .FgGreen ).Sprint ("Success: " ), msg )
178
+ }
179
+
180
+ func launcherVersionNotice () string {
181
+ if ! isNewLauncherAvailable () {
182
+ return ""
183
+ }
184
+
185
+ return fmt .Sprintf (
186
+ "New launcher available: %s -> %s. Please run `devbox version update`.\n " ,
187
+ currentLauncherVersion (),
188
+ expectedLauncherVersion ,
189
+ )
190
+ }
191
+
192
+ func devboxVersionNotice () string {
193
+ if ! isNewDevboxAvailable () {
194
+ return ""
195
+ }
196
+
197
+ return fmt .Sprintf (
198
+ "New devbox available: %s -> %s. Please run `devbox version update`.\n " ,
199
+ currentDevboxVersion ,
200
+ latestVersion (),
201
+ )
202
+ }
203
+
204
+ // isNewLauncherAvailable returns true if a new launcher version is available.
205
+ func isNewLauncherAvailable () bool {
206
+ launcherVersion := currentLauncherVersion ()
207
+ if launcherVersion == "" {
208
+ return false
209
+ }
210
+ return semverCompare (launcherVersion , expectedLauncherVersion ) < 0
211
+ }
212
+
213
+ // isNewDevboxAvailable returns true if a new devbox CLI binary version is available.
214
+ func isNewDevboxAvailable () bool {
215
+ latest := latestVersion ()
216
+ if latest == "" {
217
+ return false
218
+ }
219
+ return semverCompare (currentDevboxVersion , latest ) < 0
220
+ }
221
+
222
+ // currentLauncherAvailable returns launcher's version if it is
223
+ // available, or empty string if it is not.
224
+ func currentLauncherVersion () string {
225
+ // Change to envir.LauncherVersion. Not doing so to minimize merge-conflicts.
226
+ launcherVersion := os .Getenv ("LAUNCHER_VERSION" )
227
+ if launcherVersion == "" {
228
+ return ""
229
+ }
230
+ return "v" + launcherVersion
231
+ }
232
+
233
+ func removeCurrentVersionFile () error {
234
+ // currentVersionFilePath is the path to the file that contains the cached
235
+ // version. The launcher checks this file to see if a new version is available.
236
+ // If the version is newer, then the launcher updates.
237
+ //
238
+ // Note: keep this in sync with launch.sh code
239
+ currentVersionFilePath := filepath .Join (xdg .CacheSubpath ("devbox" ), "current-version" )
240
+
241
+ if err := os .Remove (currentVersionFilePath ); err != nil && ! errors .Is (err , fs .ErrNotExist ) {
242
+ return usererr .WithLoggedUserMessage (
243
+ err ,
244
+ "Failed to delete version-cache at %s. Please manually delete it and try again." ,
245
+ currentVersionFilePath ,
246
+ )
247
+ }
248
+ return nil
249
+ }
250
+
251
+ func semverCompare (ver1 , ver2 string ) int {
252
+ if ! strings .HasPrefix (ver1 , "v" ) {
253
+ ver1 = "v" + ver1
254
+ }
255
+ if ! strings .HasPrefix (ver2 , "v" ) {
256
+ ver2 = "v" + ver2
257
+ }
258
+ return semver .Compare (ver1 , ver2 )
259
+ }
260
+
261
+ // latestVersion returns the latest version available for the binary.
262
+ func latestVersion () string {
263
+ version := os .Getenv (envDevboxLatestVersion )
264
+ if version == "" {
265
+ return ""
266
+ }
267
+ return "v" + version
78
268
}
0 commit comments