Skip to content

Commit e1d264d

Browse files
authored
[bin wrappers] add --omit-wrappers-from-path to shellenv to prevent a binary from invoking a wrapped-binary. (#1151)
## Summary **Motivation** In elixir projects, `iex -S mix <script>` has the `iex` interpreter invoking the `mix` build tool. However, the `mix` build tool has been wrapped by devbox in a bash script. And this leads to a failure as `iex` is unable to interpret a bash script. **Fix** Binaries should be able to directly invoke other installed binaries. We implemented the binary-wrappers so that the shell-environment is correctly configured prior to the installed binary being invoked. In this case, the `iex` binary wrapper will run thereby correctly configuring the environment. Any subsequent binary invoked in the same sub-shell will inherit this up-to-date environment. So, the other binaries can be directly invoked (instead of the binary-wrapper scripts). **Implementation** In this PR, we change the `PATH` that is set within a binary-wrapper. Specifically, the `PATH` has the form: ``` <project-dir>/.devbox/virtenv/.wrappers/bin : <project-dir>/.devbox/nix/profile/default/bin ``` Within the binary-wrapper, we now omit the `.wrappers/bin` directory from PATH by using a new hidden flag `--omit-wrappers-from-path` on `devbox shellenv`. **perf analysis** I expect the perf impact to be minimal. 1. Speed up: Binaries invoking other binaries will be a bit faster since they no longer first execute the binary-wrapper. 2. Slowdown: we remove one layer of caching. Previously, we'd check the shellenv-hash in the binary wrapper, and skip invoking `devbox shellenv` if the hash was up-to-date. Now, on every invocation of a binary-wrapper we'll call `devbox shellenv`. This should still be fast due to the internal caching we do within `devbox shellenv`. Fixes #1142 ## How was it tested? BEFORE: doing `devbox run -- iex -S mix` would result in the error in the linked issue. AFTER: ``` > devbox run -- iex -S mix * creating .nix-mix/archives/hex-2.0.6 * creating .nix-mix/elixir/1-14/rebar3 Resolving Hex dependencies... Resolution completed in 0.015s Unchanged: cowboy 2.9.0 cowlib 2.11.0 ranch 1.8.0 All dependencies are up to date Erlang/OTP 25 [erts-13.2.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] Hello World! Interactive Elixir (1.14.4) - press Ctrl+C to exit (type h() ENTER for help) iex(1)> Goodbye World 15:14:23.204 [notice] Application elixir_hello exited: shutdown BREAK: (a)bort (A)bort with dump (c)ontinue (p)roc info (i)nfo (l)oaded (v)ersion (k)ill (D)b-tables (d)istribution ``` Also - did test plan of #1093 to verify `devbox update` continues working. A sanity check since we dropped ShellEnvHash from the wrappers and I wanted to ensure this works as expected.
1 parent 706ca1a commit e1d264d

File tree

7 files changed

+76
-40
lines changed

7 files changed

+76
-40
lines changed

devbox.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type Devbox interface {
3333
Install(ctx context.Context) error
3434
IsEnvEnabled() bool
3535
ListScripts() []string
36-
PrintEnv(ctx context.Context, includeHooks bool) (string, error)
36+
PrintEnv(opts *devopt.PrintEnv) (string, error)
3737
PrintGlobalList() error
3838
Pull(ctx context.Context, overwrite bool, path string) error
3939
Push(url string) error

internal/boxcli/shell.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@ func runShellCmd(cmd *cobra.Command, flags shellCmdFlags) error {
5959
if flags.printEnv {
6060
// false for includeHooks is because init hooks is not compatible with .envrc files generated
6161
// by versions older than 0.4.6
62-
script, err := box.PrintEnv(cmd.Context(), false /*includeHooks*/)
62+
opts := &devopt.PrintEnv{
63+
Ctx: cmd.Context(),
64+
IncludeHooks: false,
65+
OmitWrappersFromPath: false,
66+
}
67+
script, err := box.PrintEnv(opts)
6368
if err != nil {
6469
return err
6570
}

internal/boxcli/shellenv.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import (
1313

1414
type shellEnvCmdFlags struct {
1515
config configFlags
16-
runInitHook bool
1716
install bool
18-
useCachedPrintDevEnv bool
17+
omitWrappersFromPath bool
1918
pure bool
19+
runInitHook bool
20+
useCachedPrintDevEnv bool
2021
}
2122

2223
func shellEnvCmd() *cobra.Command {
@@ -45,6 +46,12 @@ func shellEnvCmd() *cobra.Command {
4546
command.Flags().BoolVar(
4647
&flags.pure, "pure", false, "If this flag is specified, devbox creates an isolated environment inheriting almost no variables from the current environment. A few variables, in particular HOME, USER and DISPLAY, are retained.")
4748

49+
// This flag is to be used by our generated bin-wrappers shell script.
50+
command.Flags().BoolVar(
51+
&flags.omitWrappersFromPath, "omit-wrappers-from-path", false, "If this flag is specified, "+
52+
"the PATH from shellenv will not include the binary wrappers")
53+
command.Flag("omit-wrappers-from-path").Hidden = true
54+
4855
// This is no longer used. Remove after 0.4.8 is released.
4956
command.Flags().BoolVar(
5057
&flags.useCachedPrintDevEnv,
@@ -74,7 +81,12 @@ func shellEnvFunc(cmd *cobra.Command, flags shellEnvCmdFlags) (string, error) {
7481
}
7582
}
7683

77-
envStr, err := box.PrintEnv(cmd.Context(), flags.runInitHook)
84+
opts := &devopt.PrintEnv{
85+
Ctx: cmd.Context(),
86+
IncludeHooks: flags.runInitHook,
87+
OmitWrappersFromPath: flags.omitWrappersFromPath,
88+
}
89+
envStr, err := box.PrintEnv(opts)
7890
if err != nil {
7991
return "", err
8092
}

internal/impl/devbox.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,12 @@ func (d *Devbox) RunScript(ctx context.Context, cmdName string, cmdArgs []string
293293
// creates all wrappers, but does not run init hooks. It is used to power
294294
// devbox install cli command.
295295
func (d *Devbox) Install(ctx context.Context) error {
296-
if _, err := d.PrintEnv(ctx, false /* run init hooks */); err != nil {
296+
opts := &devopt.PrintEnv{
297+
Ctx: ctx,
298+
IncludeHooks: false,
299+
OmitWrappersFromPath: false,
300+
}
301+
if _, err := d.PrintEnv(opts); err != nil {
297302
return err
298303
}
299304
return wrapnix.CreateWrappers(ctx, d)
@@ -309,8 +314,8 @@ func (d *Devbox) ListScripts() []string {
309314
return keys
310315
}
311316

312-
func (d *Devbox) PrintEnv(ctx context.Context, includeHooks bool) (string, error) {
313-
ctx, task := trace.NewTask(ctx, "devboxPrintEnv")
317+
func (d *Devbox) PrintEnv(opts *devopt.PrintEnv) (string, error) {
318+
ctx, task := trace.NewTask(opts.Ctx, "devboxPrintEnv")
314319
defer task.End()
315320

316321
if err := d.ensurePackagesAreInstalled(ctx, ensure); err != nil {
@@ -322,9 +327,19 @@ func (d *Devbox) PrintEnv(ctx context.Context, includeHooks bool) (string, error
322327
return "", err
323328
}
324329

330+
if opts.OmitWrappersFromPath {
331+
path := []string{}
332+
for _, p := range strings.Split(envs["PATH"], string(filepath.ListSeparator)) {
333+
if !strings.Contains(p, plugin.WrapperPath) {
334+
path = append(path, p)
335+
}
336+
}
337+
envs["PATH"] = strings.Join(path, string(filepath.ListSeparator))
338+
}
339+
325340
envStr := exportify(envs)
326341

327-
if includeHooks {
342+
if opts.IncludeHooks {
328343
hooksStr := ". " + d.scriptPath(hooksFilename)
329344
envStr = fmt.Sprintf("%s\n%s;\n", envStr, hooksStr)
330345
}

internal/impl/devopt/devboxopts.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
package devopt
22

3-
import "io"
3+
import (
4+
"context"
5+
"io"
6+
)
47

58
type Opts struct {
69
Dir string
710
Pure bool
811
IgnoreWarnings bool
912
Writer io.Writer
1013
}
14+
15+
type PrintEnv struct {
16+
Ctx context.Context
17+
IncludeHooks bool
18+
OmitWrappersFromPath bool
19+
}

internal/wrapnix/wrapper.go

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,6 @@ var wrapperTemplate = template.Must(template.New("wrapper").Parse(wrapper))
3434

3535
// CreateWrappers creates wrappers for all the executables in nix paths
3636
func CreateWrappers(ctx context.Context, devbox devboxer) error {
37-
shellEnvHash, err := devbox.ShellEnvHash(ctx)
38-
if err != nil {
39-
return err
40-
}
41-
4237
services, err := devbox.Services()
4338
if err != nil {
4439
return err
@@ -54,22 +49,20 @@ func CreateWrappers(ctx context.Context, devbox devboxer) error {
5449
bashPath := cmdutil.GetPathOrDefault("bash", "/bin/bash")
5550
for _, service := range services {
5651
if err = createWrapper(&createWrapperArgs{
57-
devboxer: devbox,
58-
BashPath: bashPath,
59-
Command: service.Start,
60-
Env: service.Env,
61-
ShellEnvHash: shellEnvHash,
62-
destPath: filepath.Join(destPath, service.StartName()),
52+
devboxer: devbox,
53+
BashPath: bashPath,
54+
Command: service.Start,
55+
Env: service.Env,
56+
destPath: filepath.Join(destPath, service.StartName()),
6357
}); err != nil {
6458
return err
6559
}
6660
if err = createWrapper(&createWrapperArgs{
67-
devboxer: devbox,
68-
BashPath: bashPath,
69-
Command: service.Stop,
70-
Env: service.Env,
71-
ShellEnvHash: shellEnvHash,
72-
destPath: filepath.Join(destPath, service.StopName()),
61+
devboxer: devbox,
62+
BashPath: bashPath,
63+
Command: service.Stop,
64+
Env: service.Env,
65+
destPath: filepath.Join(destPath, service.StopName()),
7366
}); err != nil {
7467
return err
7568
}
@@ -82,11 +75,10 @@ func CreateWrappers(ctx context.Context, devbox devboxer) error {
8275

8376
for _, bin := range bins {
8477
if err = createWrapper(&createWrapperArgs{
85-
devboxer: devbox,
86-
BashPath: bashPath,
87-
Command: bin,
88-
ShellEnvHash: shellEnvHash,
89-
destPath: filepath.Join(destPath, filepath.Base(bin)),
78+
devboxer: devbox,
79+
BashPath: bashPath,
80+
Command: bin,
81+
destPath: filepath.Join(destPath, filepath.Base(bin)),
9082
}); err != nil {
9183
return errors.WithStack(err)
9284
}
@@ -100,10 +92,9 @@ func CreateWrappers(ctx context.Context, devbox devboxer) error {
10092

10193
type createWrapperArgs struct {
10294
devboxer
103-
BashPath string
104-
Command string
105-
Env map[string]string
106-
ShellEnvHash string
95+
BashPath string
96+
Command string
97+
Env map[string]string
10798

10899
destPath string
109100
}

internal/wrapnix/wrapper.sh.tmpl

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,23 @@ fi
1414
{{- end }}
1515

1616
{{/*
17-
We use ShellEnvHashKey to prevent doing shellenv if the correct environment is
18-
already set. (perf optimization)
1917

2018
We use the guard to prevent infinite loop if something in shellenv causes
2119
another wrapped binary to be called. The guard is specific to this project so shellenv
2220
could still cause another project's shellenv to be called.
21+
Note, this guard can likely be removed since we use --omit-wrappers-from-path=true below,
22+
but leaving in to minimize change.
2323

2424
DO_NOT_TRACK=1 can be removed once we optimize segment to queue events.
25+
26+
--omit-wrappers-from-path so that we do not invoke other bin-wrappers from
27+
this bin-wrapper. Instead, we directly invoke the binary from the nix store, which
28+
should be in PATH.
2529
*/ -}}
2630

27-
if [[ "${{ .ShellEnvHashKey }}" != "{{ .ShellEnvHash }}" ]] && [[ -z "${{ .ShellEnvHashKey }}_GUARD" ]]; then
31+
if [[ -z "${{ .ShellEnvHashKey }}_GUARD" ]]; then
2832
export {{ .ShellEnvHashKey }}_GUARD=true
29-
eval "$(DO_NOT_TRACK=1 devbox shellenv -c {{ .ProjectDir }})"
33+
eval "$(DO_NOT_TRACK=1 devbox shellenv --omit-wrappers-from-path=true -c {{ .ProjectDir }})"
3034
fi
3135

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

0 commit comments

Comments
 (0)