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