Skip to content

Commit b24890b

Browse files
authored
[Bug fix] Ensure bin-wrappers use latest devbox binary to prevent false update notifications (#1324)
## Summary **Motivation** In #1260, the bin-wrappers were changed to invoke the devbox-binary, instead of the devbox-launcher. However, when devbox has been updated, these bin-wrappers have the older devbox-binary's path hardcoded. So, they call the older devbox-binary leading to two issues: 1. runs older functionality of the older devbox binary 2. re-prints the notice about "New devbox available. Run .... to update", despite the user already having updated their devbox. **Implementation** 1. Create a symlink that always points to the "current" devbox binary. - I use "current" instead of "latest" to be consistent with the Launcher's usage of these terms. 2. In the bin-wrappers, we prepend the directory containing this devbox-symlink to PATH. The bin-wrappers revert back to directly invoking devbox, as in `devbox shellenv`. - This is done for robustness: if the symlink is missing, then the bin-wrappers do still invoke `devbox` via the launcher. This is problematic for users who have installed `coreutils` via devbox, but for most users this fallback would work. 3. We ensure the bin-wrappers are created in `ensurePackagesAreInstalled` that runs in `devbox.PrintEnv` during `devbox shellenv`. - This is needed because we need existing users that have the bin-wrappers with the hardcoded DevboxExecutablePath to refresh their bin-wrappers. When they open a new terminal, `devbox global shellenv` would run, refreshing these bin-wrappers. 4. Finally, we create the Devbox symlink during `devbox version update`, so that it points to the _new_ Devbox binary. **Drawbacks** Switching between devbox versions (for testing and development) may lead to subtle unexpected behavior: The devbox-symlink will point to the version that created the bin-wrappers, rather than respecting the devbox of the PATH. We need to ensure we run `devbox install` or similar to re-create the bin-wrappers. ## How was it tested? 1. Sanity check to do `devbox shell` in `examples/development/go/hello-world`. 2. Sanity check to do `devbox run build` using the devbox binary from this PR. 3. Deleted devbox-symlink, and ran `devbox version -v` to ensure that it was re-created.
1 parent 80c12e0 commit b24890b

File tree

7 files changed

+99
-27
lines changed

7 files changed

+99
-27
lines changed

internal/boxcli/version.go

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ import (
99
"runtime"
1010

1111
"github.com/spf13/cobra"
12+
"go.jetpack.io/devbox/internal/wrapnix"
1213

1314
"go.jetpack.io/devbox/internal/build"
1415
"go.jetpack.io/devbox/internal/envir"
1516
"go.jetpack.io/devbox/internal/vercheck"
1617
)
1718

1819
type versionFlags struct {
19-
verbose bool
20+
verbose bool
21+
updateDevboxSymlink bool
2022
}
2123

2224
func versionCmd() *cobra.Command {
@@ -33,6 +35,14 @@ func versionCmd() *cobra.Command {
3335
command.Flags().BoolVarP(&flags.verbose, "verbose", "v", false, // value
3436
"displays additional version information",
3537
)
38+
// Make this flag hidden because:
39+
// This functionality doesn't strictly belong in this command, but we add it here
40+
// since `devbox version update` calls `devbox version -v` to trigger an update.
41+
command.Flags().BoolVarP(&flags.updateDevboxSymlink, "update-devbox-symlink", "u", false, // value
42+
"update the devbox symlink to point to the current binary",
43+
)
44+
_ = command.Flags().MarkHidden("update-devbox-symlink")
45+
3646
command.AddCommand(selfUpdateCmd())
3747
return command
3848
}
@@ -52,16 +62,24 @@ func selfUpdateCmd() *cobra.Command {
5262

5363
func versionCmdFunc(cmd *cobra.Command, _ []string, flags versionFlags) error {
5464
w := cmd.OutOrStdout()
55-
v := getVersionInfo()
65+
info := getVersionInfo()
5666
if flags.verbose {
57-
fmt.Fprintf(w, "Version: %v\n", v.Version)
58-
fmt.Fprintf(w, "Platform: %v\n", v.Platform)
59-
fmt.Fprintf(w, "Commit: %v\n", v.Commit)
60-
fmt.Fprintf(w, "Commit Time: %v\n", v.CommitDate)
61-
fmt.Fprintf(w, "Go Version: %v\n", v.GoVersion)
62-
fmt.Fprintf(w, "Launcher: %v\n", v.LauncherVersion)
67+
fmt.Fprintf(w, "Version: %v\n", info.Version)
68+
fmt.Fprintf(w, "Platform: %v\n", info.Platform)
69+
fmt.Fprintf(w, "Commit: %v\n", info.Commit)
70+
fmt.Fprintf(w, "Commit Time: %v\n", info.CommitDate)
71+
fmt.Fprintf(w, "Go Version: %v\n", info.GoVersion)
72+
fmt.Fprintf(w, "Launcher: %v\n", info.LauncherVersion)
73+
74+
// TODO: in a subsequent PR, we should do this when flags.updateDevboxSymlink is true.
75+
// Not doing for now, since users who have Devbox binary prior to this edit
76+
// (before Devbox v0.5.9) will not invoke this flag in `devbox version update`.
77+
// But we still want this to run for them.
78+
if _, err := wrapnix.CreateDevboxSymlink(); err != nil {
79+
return err
80+
}
6381
} else {
64-
fmt.Fprintf(w, "%v\n", v.Version)
82+
fmt.Fprintf(w, "%v\n", info.Version)
6583
}
6684
return nil
6785
}

internal/cloud/openssh/config.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"text/template"
1616

1717
"github.com/pkg/errors"
18+
"go.jetpack.io/devbox/internal/fileutil"
1819
)
1920

2021
// These must match what's in sshConfigTmpl. We should eventually make the hosts
@@ -211,17 +212,8 @@ func containsDevboxInclude(r io.Reader) bool {
211212
return false
212213
}
213214

214-
// move to a file utility
215215
func EnsureDirExists(path string, perm fs.FileMode, chmod bool) error {
216-
if err := os.Mkdir(path, perm); err != nil && !errors.Is(err, fs.ErrExist) {
217-
return errors.WithStack(err)
218-
}
219-
if chmod {
220-
if err := os.Chmod(path, perm); err != nil {
221-
return errors.WithStack(err)
222-
}
223-
}
224-
return nil
216+
return fileutil.EnsureDirExists(path, perm, chmod)
225217
}
226218

227219
// returns path to ~/.config/devbox/ssh

internal/fileutil/fileutil.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
package fileutil
55

66
import (
7+
"io/fs"
78
"os"
89
"strings"
10+
11+
"github.com/pkg/errors"
912
)
1013

1114
// TODO: publish as it's own shared package that other binaries can use.
@@ -55,3 +58,15 @@ func FileContains(path string, substring string) (bool, error) {
5558
}
5659
return strings.Contains(string(data), substring), nil
5760
}
61+
62+
func EnsureDirExists(path string, perm fs.FileMode, chmod bool) error {
63+
if err := os.MkdirAll(path, perm); err != nil && !errors.Is(err, fs.ErrExist) {
64+
return errors.WithStack(err)
65+
}
66+
if chmod {
67+
if err := os.Chmod(path, perm); err != nil {
68+
return errors.WithStack(err)
69+
}
70+
}
71+
return nil
72+
}

internal/impl/packages.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ func (d *Devbox) ensurePackagesAreInstalled(ctx context.Context, mode installMod
215215
// Ensure we clean out packages that are no longer needed.
216216
d.lockfile.Tidy()
217217

218+
if err := wrapnix.CreateWrappers(ctx, d); err != nil {
219+
return err
220+
}
221+
218222
return d.lockfile.Save()
219223
}
220224

internal/vercheck/vercheck.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ func triggerUpdate(stdErr io.Writer) (*updatedVersions, error) {
175175
}
176176

177177
// TODO savil. Add a --json flag to devbox version and parse the output as JSON
178-
cmd := exec.Command(exePath, "version", "-v")
178+
cmd := exec.Command(exePath, "version", "-v", "--update-devbox-symlink")
179179

180180
buf := new(bytes.Buffer)
181181
cmd.Stdout = io.MultiWriter(stdErr, buf)

internal/wrapnix/wrapper.go

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import (
1414

1515
"github.com/pkg/errors"
1616
"go.jetpack.io/devbox/internal/cmdutil"
17+
"go.jetpack.io/devbox/internal/fileutil"
1718
"go.jetpack.io/devbox/internal/nix"
1819
"go.jetpack.io/devbox/internal/plugin"
20+
"go.jetpack.io/devbox/internal/xdg"
1921
)
2022

2123
type devboxer interface {
@@ -49,9 +51,7 @@ func CreateWrappers(ctx context.Context, devbox devboxer) error {
4951
if err != nil {
5052
return err
5153
}
52-
// get absolute path of devbox binary that the launcher script invokes
53-
// to avoid causing an infinite loop when coreutils gets installed
54-
executablePath, err := os.Executable()
54+
devboxSymlinkPath, err := CreateDevboxSymlink()
5555
if err != nil {
5656
return err
5757
}
@@ -62,7 +62,7 @@ func CreateWrappers(ctx context.Context, devbox devboxer) error {
6262
BashPath: bashPath,
6363
Command: bin,
6464
ShellEnvHash: shellEnvHash,
65-
DevboxBinaryPath: executablePath,
65+
DevboxSymlinkDir: filepath.Dir(devboxSymlinkPath),
6666
destPath: filepath.Join(destPath, filepath.Base(bin)),
6767
}); err != nil {
6868
return errors.WithStack(err)
@@ -72,12 +72,53 @@ func CreateWrappers(ctx context.Context, devbox devboxer) error {
7272
return createSymlinksForSupportDirs(devbox.ProjectDir())
7373
}
7474

75+
// CreateDevboxSymlink creates a symlink to the devbox binary
76+
//
77+
// Needed because:
78+
//
79+
// 1. The bin-wrappers cannot invoke devbox via the Launcher. The Launcher script
80+
// invokes some coreutils commands that may themselves be installed by devbox
81+
// and so be bin-wrappers. This causes an infinite loop.
82+
//
83+
// So, the bin-wrappers need to directly invoke the devbox binary.
84+
//
85+
// 2. The devbox binary's path will change when devbox is updated. Hence
86+
// using absolute paths to the devbox binaries in the bin-wrappers
87+
// will result in bin-wrappers invoking older devbox binaries.
88+
//
89+
// So, the bin-wrappers need to use a symlink to the latest devbox binary. This
90+
// symlink is updated when devbox is updated.
91+
func CreateDevboxSymlink() (string, error) {
92+
curDir := xdg.CacheSubpath(filepath.Join("devbox", "bin", "current"))
93+
if err := fileutil.EnsureDirExists(curDir, 0755, false /*chmod*/); err != nil {
94+
return "", err
95+
}
96+
currentDevboxSymlinkPath := filepath.Join(curDir, "devbox")
97+
98+
devboxBinaryPath, err := os.Executable()
99+
if err != nil {
100+
return "", errors.WithStack(err)
101+
}
102+
103+
// We will always re-create this symlink to ensure correctness.
104+
if err := os.Remove(currentDevboxSymlinkPath); err != nil && !errors.Is(err, os.ErrNotExist) {
105+
return "", errors.WithStack(err)
106+
}
107+
108+
// Don't return error if error is os.ErrExist to protect against race conditions.
109+
if err := os.Symlink(devboxBinaryPath, currentDevboxSymlinkPath); err != nil && !errors.Is(err, os.ErrExist) {
110+
return "", errors.WithStack(err)
111+
}
112+
113+
return currentDevboxSymlinkPath, nil
114+
}
115+
75116
type createWrapperArgs struct {
76117
devboxer
77118
BashPath string
78119
Command string
79120
ShellEnvHash string
80-
DevboxBinaryPath string
121+
DevboxSymlinkDir string
81122
destPath string
82123
}
83124

internal/wrapnix/wrapper.sh.tmpl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#!{{ .BashPath }}
22

3+
export PATH="{{ .DevboxSymlinkDir }}:$PATH"
4+
35
{{/*
46
# If env variable has never been set by devbox we set it, but also
57
# default to env value set by user. This means plugin env variables behave a bit
@@ -19,7 +21,7 @@ DO_NOT_TRACK=1 can be removed once we optimize segment to queue events.
1921

2022
if [[ "${{ .ShellEnvHashKey }}" != "{{ .ShellEnvHash }}" ]] && [[ -z "${{ .ShellEnvHashKey }}_GUARD" ]]; then
2123
export {{ .ShellEnvHashKey }}_GUARD=true
22-
eval "$(DO_NOT_TRACK=1 {{ .DevboxBinaryPath }} shellenv -c {{ .ProjectDir }})"
24+
eval "$(DO_NOT_TRACK=1 devbox shellenv -c {{ .ProjectDir }})"
2325
fi
2426

2527
{{/*
@@ -29,6 +31,6 @@ should be in PATH.
2931

3032
DO_NOT_TRACK=1 can be removed once we optimize segment to queue events.
3133
*/ -}}
32-
eval "$(DO_NOT_TRACK=1 {{ .DevboxBinaryPath }} shellenv only-path-without-wrappers)"
34+
eval "$(DO_NOT_TRACK=1 devbox shellenv only-path-without-wrappers)"
3335

3436
exec {{ .Command }} "$@"

0 commit comments

Comments
 (0)