Skip to content

Commit c6d8142

Browse files
authored
[nix] Create bin wrappers. Eliminate need to refresh (#803)
## Summary This creates bin wrappers for all binaries and services. It sources the devbox environment in each wrapper and in service wrappers it sets environment variables if not already set. Benefits: * No longer need to call "refresh" after installing a package. * Fix bug where env variables get overwritten on refresh * Can pass limited flags to wrappers (basically only at the end) Downsides (fixable): * minor performance hit. Roughly 5ms are added per wrapper. We might be able to fix this in the future by turning wrappers into sym links if nothing needs to be sourced. Future benefits: * Plugin env variables can be removed from devbox env ## How was it tested? ``` devbox shell devbox add apacheHttpd HTTPD_PORT=8085 devbox services start apache curl localhost:8085 ```
1 parent 29eb63a commit c6d8142

File tree

17 files changed

+282
-85
lines changed

17 files changed

+282
-85
lines changed

devbox.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type Devbox interface {
1818
// Add adds a Nix package to the config so that it's available in the devbox
1919
// environment. It validates that the Nix package exists, but doesn't install
2020
// it. Adding a duplicate package is a no-op.
21-
Add(pkgs ...string) error
21+
Add(ctx context.Context, pkgs ...string) error
2222
AddGlobal(pkgs ...string) error
2323
Config() *impl.Config
2424
ProjectDir() string
@@ -35,12 +35,12 @@ type Devbox interface {
3535
PullGlobal(path string) error
3636
// Remove removes Nix packages from the config so that it no longer exists in
3737
// the devbox environment.
38-
Remove(pkgs ...string) error
38+
Remove(ctx context.Context, pkgs ...string) error
3939
RemoveGlobal(pkgs ...string) error
4040
RunScript(scriptName string, scriptArgs []string) error
4141
Services() (plugin.Services, error)
4242
// Shell generates the devbox environment and launches nix-shell as a child process.
43-
Shell() error
43+
Shell(ctx context.Context) error
4444
// ShellPlan creates a plan of the actions that devbox will take to generate its
4545
// shell environment.
4646
ShellPlan() (*plansdk.ShellPlan, error)

internal/boxcli/add.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,5 @@ func addCmdFunc(cmd *cobra.Command, args []string, flags addCmdFlags) error {
5454
return errors.WithStack(err)
5555
}
5656

57-
return box.Add(args...)
57+
return box.Add(cmd.Context(), args...)
5858
}

internal/boxcli/install.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package boxcli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
func InstallCmd() *cobra.Command {
10+
flags := runCmdFlags{}
11+
command := &cobra.Command{
12+
Use: "install",
13+
Short: "Installs all packages mentioned in devbox.json",
14+
Long: "Starts a new devbox shell and installs all packages mentioned in devbox.json in current directory or" +
15+
"a directory specified via --config. \n\n Then exits the shell when packages are done installing.\n\n ",
16+
Args: cobra.MaximumNArgs(0),
17+
PreRunE: ensureNixInstalled,
18+
RunE: func(cmd *cobra.Command, args []string) error {
19+
// the colon ':' character in standard shell means noop.
20+
// So essentially, this command is running devbox run noop
21+
err := runScriptCmd(cmd, []string{":"}, flags)
22+
if err != nil {
23+
return err
24+
}
25+
fmt.Fprintln(cmd.ErrOrStderr(), "Finished installing packages.")
26+
return nil
27+
},
28+
}
29+
30+
flags.config.register(command)
31+
32+
return command
33+
}

internal/boxcli/rm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,5 @@ func runRemoveCmd(cmd *cobra.Command, args []string, flags removeCmdFlags) error
3535
return errors.WithStack(err)
3636
}
3737

38-
return box.Remove(args...)
38+
return box.Remove(cmd.Context(), args...)
3939
}

internal/boxcli/run.go

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
package boxcli
55

66
import (
7-
"fmt"
8-
97
"github.com/pkg/errors"
108
"github.com/spf13/cobra"
119
"go.jetpack.io/devbox"
@@ -43,32 +41,6 @@ func RunCmd() *cobra.Command {
4341
return command
4442
}
4543

46-
func InstallCmd() *cobra.Command {
47-
flags := runCmdFlags{}
48-
command := &cobra.Command{
49-
Use: "install",
50-
Short: "Installs all packages mentioned in devbox.json",
51-
Long: "Starts a new devbox shell and installs all packages mentioned in devbox.json in current directory or" +
52-
"a directory specified via --config. \n\n Then exits the shell when packages are done installing.\n\n ",
53-
Args: cobra.MaximumNArgs(0),
54-
PreRunE: ensureNixInstalled,
55-
RunE: func(cmd *cobra.Command, args []string) error {
56-
// the colon ':' character in standard shell means noop.
57-
// So essentially, this command is running devbox run noop
58-
err := runScriptCmd(cmd, []string{":"}, flags)
59-
if err != nil {
60-
return err
61-
}
62-
fmt.Fprintln(cmd.ErrOrStderr(), "Finished installing packages.")
63-
return nil
64-
},
65-
}
66-
67-
flags.config.register(command)
68-
69-
return command
70-
}
71-
7244
func listScripts(cmd *cobra.Command, flags runCmdFlags) []string {
7345
path, err := configPathFromUser([]string{}, &flags.config)
7446
if err != nil {

internal/boxcli/shell.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func runShellCmd(cmd *cobra.Command, args []string, flags shellCmdFlags) error {
6666
return shellInceptionErrorMsg("devbox shell")
6767
}
6868

69-
return box.Shell()
69+
return box.Shell(cmd.Context())
7070
}
7171

7272
func parseShellArgs(cmd *cobra.Command, args []string, flags shellCmdFlags) (string, []string, error) {

internal/boxcli/shellenv.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ func shellEnvCmd() *cobra.Command {
2727
return err
2828
}
2929
fmt.Fprintln(cmd.OutOrStdout(), s)
30-
fmt.Fprintln(cmd.OutOrStdout(), ";hash -r")
3130
return nil
3231
},
3332
}

internal/impl/config.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"go.jetpack.io/devbox/internal/debug"
2020
"go.jetpack.io/devbox/internal/impl/shellcmd"
2121
"go.jetpack.io/devbox/internal/planner/plansdk"
22-
"go.jetpack.io/devbox/internal/ux"
2322
)
2423

2524
// Config defines a devbox environment as JSON.
@@ -56,7 +55,6 @@ type Stage struct {
5655
func (c *Config) Packages(w io.Writer) []string {
5756
dataPath, err := GlobalDataPath()
5857
if err != nil {
59-
ux.Ferror(w, "unable to get devbox global data path: %s\n", err)
6058
return c.RawPackages
6159
}
6260
global, err := readConfig(filepath.Join(dataPath, "devbox.json"))

internal/impl/devbox.go

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"go.jetpack.io/devbox/internal/services"
3636
"go.jetpack.io/devbox/internal/telemetry"
3737
"go.jetpack.io/devbox/internal/ux"
38+
"go.jetpack.io/devbox/internal/wrapnix"
3839
)
3940

4041
const (
@@ -148,8 +149,8 @@ func (d *Devbox) Generate() error {
148149
return errors.WithStack(d.generateShellFiles())
149150
}
150151

151-
func (d *Devbox) Shell() error {
152-
ctx, task := trace.NewTask(context.Background(), "devboxShell")
152+
func (d *Devbox) Shell(ctx context.Context) error {
153+
ctx, task := trace.NewTask(ctx, "devboxShell")
153154
defer task.End()
154155

155156
if err := d.ensurePackagesAreInstalled(ctx, ensure); err != nil {
@@ -167,11 +168,15 @@ func (d *Devbox) Shell() error {
167168
return err
168169
}
169170

170-
env, err := d.computeNixEnv(ctx)
171+
env, err := d.cachedComputeNixEnv(ctx)
171172
if err != nil {
172173
return err
173174
}
174175

176+
if err := wrapnix.CreateWrappers(ctx, d); err != nil {
177+
return err
178+
}
179+
175180
shellStartTime := os.Getenv("DEVBOX_SHELL_START_TIME")
176181
if shellStartTime == "" {
177182
shellStartTime = telemetry.UnixTimestampFromTime(telemetry.CommandStartTime())
@@ -207,11 +212,15 @@ func (d *Devbox) RunScript(cmdName string, cmdArgs []string) error {
207212
return err
208213
}
209214

210-
env, err := d.computeNixEnv(ctx)
215+
env, err := d.cachedComputeNixEnv(ctx)
211216
if err != nil {
212217
return err
213218
}
214219

220+
if err = wrapnix.CreateWrappers(ctx, d); err != nil {
221+
return err
222+
}
223+
215224
var cmdWithArgs []string
216225
if _, ok := d.cfg.Shell.Scripts[cmdName]; ok {
217226
// it's a script, so replace the command with the script file's path.
@@ -247,7 +256,7 @@ func (d *Devbox) PrintEnv() (string, error) {
247256
ctx, task := trace.NewTask(context.Background(), "devboxPrintEnv")
248257
defer task.End()
249258

250-
envs, err := d.computeNixEnv(ctx)
259+
envs, err := d.cachedComputeNixEnv(ctx)
251260
if err != nil {
252261
return "", err
253262
}
@@ -544,21 +553,26 @@ func (d *Devbox) computeNixEnv(ctx context.Context) (map[string]string, error) {
544553
debug.Log("nix environment PATH is: %s", env)
545554

546555
// Add any vars defined in plugins.
556+
// TODO: Now that we have bin wrappers, this may can eventually be removed.
557+
// We still need to be able to add env variables to non-service binaries
558+
// (e.g. ruby). This would involve understanding what binaries are associated
559+
// to a given plugin.
547560
pluginEnv, err := plugin.Env(d.mergedPackages(), d.projectDir, env)
548561
if err != nil {
549562
return nil, err
550563
}
551564

552-
for k, v := range pluginEnv {
553-
env[k] = v
554-
}
565+
addEnvIfNotPreviouslySetByDevbox(env, pluginEnv)
566+
567+
// Prepend virtenv bin path first so user can override it if needed. Virtenv
568+
// is where the bin wrappers live
569+
env["PATH"] = JoinPathLists(d.virtenvBinPath(), env["PATH"])
555570

556571
// Include env variables in devbox.json
557-
if featureflag.EnvConfig.Enabled() {
558-
for k, v := range d.configEnvs(env) {
559-
env[k] = v
560-
}
561-
}
572+
configEnv := d.configEnvs(env)
573+
addEnvIfNotPreviouslySetByDevbox(env, configEnv)
574+
575+
markEnvsAsSetByDevbox(pluginEnv, configEnv)
562576

563577
nixEnvPath := env["PATH"]
564578
debug.Log("PATH after plugins and config is: %s", nixEnvPath)
@@ -571,6 +585,17 @@ func (d *Devbox) computeNixEnv(ctx context.Context) (map[string]string, error) {
571585
return env, nil
572586
}
573587

588+
var nixEnvCache map[string]string
589+
590+
// cachedComputeNixEnv is a wrapper around computeNixEnv that caches the result.
591+
func (d *Devbox) cachedComputeNixEnv(ctx context.Context) (map[string]string, error) {
592+
var err error
593+
if nixEnvCache == nil {
594+
nixEnvCache, err = d.computeNixEnv(ctx)
595+
}
596+
return nixEnvCache, err
597+
}
598+
574599
// writeScriptsToFiles writes scripts defined in devbox.json into files inside .devbox/gen/scripts.
575600
// Scripts (and hooks) are persisted so that we can easily call them from devbox run (inside or outside shell).
576601
func (d *Devbox) writeScriptsToFiles() error {
@@ -768,3 +793,37 @@ func (d *Devbox) setCommonHelperEnvVars(env map[string]string) {
768793
env["LD_LIBRARY_PATH"] = filepath.Join(d.projectDir, nix.ProfilePath, "lib") + ":" + env["LD_LIBRARY_PATH"]
769794
env["LIBRARY_PATH"] = filepath.Join(d.projectDir, nix.ProfilePath, "lib") + ":" + env["LIBRARY_PATH"]
770795
}
796+
797+
func (d *Devbox) virtenvBinPath() string {
798+
return filepath.Join(d.projectDir, plugin.VirtenvBinPath)
799+
}
800+
801+
// nix bins returns the paths to all the nix binaries that are installed by
802+
// the flake. If there are conflicts, it returns the first one it finds of a
803+
// give name. This matches how nix flakes behaves if there are conflicts in
804+
// buildInputs
805+
func (d *Devbox) NixBins(ctx context.Context) ([]string, error) {
806+
env, err := d.cachedComputeNixEnv(ctx)
807+
808+
if err != nil {
809+
return nil, err
810+
}
811+
dirs := strings.Split(env["buildInputs"], " ")
812+
bins := map[string]string{}
813+
for _, dir := range dirs {
814+
binPath := filepath.Join(dir, "bin")
815+
if _, err = os.Stat(binPath); os.IsNotExist(err) {
816+
continue
817+
}
818+
files, err := os.ReadDir(binPath)
819+
if err != nil {
820+
return nil, errors.WithStack(err)
821+
}
822+
for _, file := range files {
823+
if _, alreadySet := bins[file.Name()]; !alreadySet {
824+
bins[file.Name()] = filepath.Join(binPath, file.Name())
825+
}
826+
}
827+
}
828+
return lo.Values(bins), nil
829+
}

internal/impl/envvars.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"strings"
77
)
88

9+
const devboxSetPrefix = "__DEVBOX_SET_"
10+
911
func mapToPairs(m map[string]string) []string {
1012
pairs := []string{}
1113
for k, v := range m {
@@ -53,3 +55,25 @@ func exportify(vars map[string]string) string {
5355
}
5456
return strings.TrimSpace(strb.String())
5557
}
58+
59+
// addEnvIfNotPreviouslySetByDevbox adds the key-value pairs from new to existing,
60+
// but only if the key was not previously set by devbox
61+
// Caveat, this won't mark the values as set by devbox automatically. Instead,
62+
// you need to call markEnvAsSetByDevbox when you are done setting variables.
63+
// This is so you can add variables from multiple sources (e.g. plugin, devbox.json)
64+
// that may build on each other (e.g. PATH=$PATH:...)
65+
func addEnvIfNotPreviouslySetByDevbox(existing, new map[string]string) {
66+
for k, v := range new {
67+
if _, alreadySet := existing[devboxSetPrefix+k]; !alreadySet {
68+
existing[k] = v
69+
}
70+
}
71+
}
72+
73+
func markEnvsAsSetByDevbox(envs ...map[string]string) {
74+
for _, env := range envs {
75+
for key := range env {
76+
env[devboxSetPrefix+key] = "1"
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)