11package vercheck
22
33import (
4+ "bytes"
45 "fmt"
56 "io"
7+ "io/fs"
68 "os"
79 "os/exec"
810 "path/filepath"
11+ "strings"
912
13+ "github.com/fatih/color"
1014 "github.com/pkg/errors"
15+ "go.jetpack.io/devbox/internal/build"
16+ "golang.org/x/mod/semver"
17+
1118 "go.jetpack.io/devbox/internal/boxcli/usererr"
12- "go.jetpack.io/devbox/internal/cloud/envir"
13- "go.jetpack.io/devbox/internal/ux"
1419 "go.jetpack.io/devbox/internal/xdg"
15- "golang.org/x/mod/semver"
1620)
1721
1822// 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"
2125
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" ) != "" {
2540 return
2641 }
2742
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 )
3755 }
3856}
3957
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.
4264func 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 {
4373 installScript := ""
4474 if _ , err := exec .LookPath ("curl" ); err == nil {
4575 installScript = "curl -fsSL https://get.jetpack.io/devbox | bash"
@@ -49,26 +79,187 @@ func SelfUpdate(stdOut, stdErr io.Writer) error {
4979 return usererr .New ("curl or wget is required to update devbox. Please install either and try again." )
5080 }
5181
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+ }
5687
88+ // Fetch the new launcher.
5789 cmd := exec .Command ("sh" , "-c" , installScript )
5890 cmd .Stdout = stdOut
5991 cmd .Stderr = stdErr
6092 if err := cmd .Run (); err != nil {
6193 return errors .WithStack (err )
6294 }
6395
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 )
6698 if err != nil {
6799 return errors .WithStack (err )
68100 }
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 )
72145 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
74265}
0 commit comments