Skip to content

Commit 7ba3661

Browse files
authored
[version] part 1: update command for devbox and launcher updates (#965)
## Summary **Motivation:** We want to disable auto-update for devbox CLIs. This will minimize unexpected interruptions for our users. To do so, we are going to do the following: 1. Update `launch.sh` file in `axiom` repo to write new versions to `{XDG_CACHE_HOME}/devbox/available-version`. jetify-com/axiom#3150 2. Change `devbox version update` to update the launcher (if needed) and else, update the CLI binary. (this PR). 3. Change the `vercheck.CheckVersion` that runs in `boxcli/root.go` on every command to check if the launcher or CLI binary versions are out-of-date, and to print a notice if so. (reviewed in #966, but merged into this PR) We will cherry-pick ~these two PRs~ this PR for the next release so auto-update stops for users. **Implementation:** This PR enables `jetpack version update` to: 1. Update the launcher: We detect if the launcher version is outdated, and update it, if needed. This also updates the devbox CLI binary as a side-effect. This requires `sudo` so we only do it, if necessary. 2. Update just the devbox CLI binary: If the launcher is up-to-date, then we update just the devbox CLI binary. 3. Remove dependency on new code like `env` package to minimize cherry-picking merge conflicts. TODO: - [x] confirm that we can switch the `devbox version` operation after an update to `StdOut`. I feel this is better so user is informed about the new version. cc @mikeland86 for the above question. Will only land this after the launch.sh file is updated in jetify-com/axiom#3150 ## How was it tested? Did some basic testing: hardcoded `true` to ensure `selfUpdateLauncher` runs, and did `devbox version update`: <img width="742" alt="Screenshot 2023-05-01 at 8 57 46 PM" src="https://user-images.githubusercontent.com/676452/235576315-03069599-a469-4f3a-90c2-cc6a58ec648a.png"> regular `devbox version update` <img width="332" alt="Screenshot 2023-05-01 at 8 48 20 PM" src="https://user-images.githubusercontent.com/676452/235575369-f2a70299-4a3a-4b41-80ec-ecedb730ad0b.png">
1 parent cbeb593 commit 7ba3661

File tree

4 files changed

+308
-35
lines changed

4 files changed

+308
-35
lines changed

internal/boxcli/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func RootCmd() *cobra.Command {
3333
Use: "devbox",
3434
Short: "Instant, easy, predictable development environments",
3535
PersistentPreRun: func(cmd *cobra.Command, args []string) {
36-
vercheck.CheckLauncherVersion(cmd.ErrOrStderr())
36+
vercheck.CheckVersion(cmd.ErrOrStderr())
3737
if flags.quiet {
3838
cmd.SetErr(io.Discard)
3939
}

internal/boxcli/version.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package boxcli
55

66
import (
77
"fmt"
8+
"os"
89
"runtime"
910

1011
"github.com/spf13/cobra"
@@ -56,19 +57,21 @@ func versionCmdFunc(cmd *cobra.Command, _ []string, flags versionFlags) error {
5657
fmt.Fprintf(w, "Commit: %v\n", v.Commit)
5758
fmt.Fprintf(w, "Commit Time: %v\n", v.CommitDate)
5859
fmt.Fprintf(w, "Go Version: %v\n", v.GoVersion)
60+
fmt.Fprintf(w, "Launcher: %v\n", v.LauncherVersion)
5961
} else {
6062
fmt.Fprintf(w, "%v\n", v.Version)
6163
}
6264
return nil
6365
}
6466

6567
type versionInfo struct {
66-
Version string
67-
IsPrerelease bool
68-
Platform string
69-
Commit string
70-
CommitDate string
71-
GoVersion string
68+
Version string
69+
IsPrerelease bool
70+
Platform string
71+
Commit string
72+
CommitDate string
73+
GoVersion string
74+
LauncherVersion string
7275
}
7376

7477
func getVersionInfo() *versionInfo {
@@ -78,6 +81,8 @@ func getVersionInfo() *versionInfo {
7881
Commit: build.Commit,
7982
CommitDate: build.CommitDate,
8083
GoVersion: runtime.Version(),
84+
// Change to env.LauncherVersion. Not doing so to minimize merge conflicts.
85+
LauncherVersion: os.Getenv("LAUNCHER_VERSION"),
8186
}
8287

8388
return v

internal/vercheck/vercheck.go

Lines changed: 218 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,75 @@
44
package vercheck
55

66
import (
7+
"bytes"
78
"fmt"
89
"io"
10+
"io/fs"
911
"os"
1012
"os/exec"
1113
"path/filepath"
14+
"strings"
1215

16+
"github.com/fatih/color"
1317
"github.com/pkg/errors"
18+
"go.jetpack.io/devbox/internal/build"
1419
"golang.org/x/mod/semver"
1520

1621
"go.jetpack.io/devbox/internal/boxcli/usererr"
17-
"go.jetpack.io/devbox/internal/envir"
18-
"go.jetpack.io/devbox/internal/ux"
1922
"go.jetpack.io/devbox/internal/xdg"
2023
)
2124

2225
// 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"
2528

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") != "" {
2943
return
3044
}
3145

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)
4158
}
4259
}
4360

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.
4667
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 {
4776
installScript := ""
4877
if _, err := exec.LookPath("curl"); err == nil {
4978
installScript = "curl -fsSL https://get.jetpack.io/devbox | bash"
@@ -53,26 +82,187 @@ func SelfUpdate(stdOut, stdErr io.Writer) error {
5382
return usererr.New("curl or wget is required to update devbox. Please install either and try again.")
5483
}
5584

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+
}
6090

91+
// Fetch the new launcher.
6192
cmd := exec.Command("sh", "-c", installScript)
6293
cmd.Stdout = stdOut
6394
cmd.Stderr = stdErr
6495
if err := cmd.Run(); err != nil {
6596
return errors.WithStack(err)
6697
}
6798

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)
70120
if err != nil {
71121
return errors.WithStack(err)
72122
}
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)
76148
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
78268
}

0 commit comments

Comments
 (0)