Skip to content

Commit 0ac0b2d

Browse files
authored
[planner->plugin] Turn php planner into a plugin (#1000)
## Summary This PR turns our PHP planner into a plugin. Our plugins are actually almost able to do this with some minimal changes. Re-used existing functionality: * Create `flake.nix` file in the php plugin * packages (inputs) can be local flakes Changes: * Pass `Packages`, `System`, and `URLForInput` to plugin file templates. This allows plugins to build and customize some interesting flakes. * Add `packages` field to plugins. We wanted to do this for a while. In this case it simply points to the flake created by the plugin. These packages are added to the devbox environment. Semi-related changes: * Use `--recreate-lock-file` every time we `print-dev-env`. The reason this is needed is because input flakes might change se we need to frequently update. It doesn't hurt performance much because we cache print-dev-env. This means any unlocked package will update frequently. I think that's fine because this lockfile is not shared so there's no expectation of stability. cc: @Lagoja @savil ## How was it tested? ```bash devbox add php phpExtensions.ds devbox run 'php -m | grep ds' ```
1 parent 2563283 commit 0ac0b2d

File tree

17 files changed

+254
-189
lines changed

17 files changed

+254
-189
lines changed

internal/cuecfg/hash.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import (
1313

1414
func FileHash(path string) (string, error) {
1515
data, err := os.ReadFile(path)
16-
if err != nil && !errors.Is(err, fs.ErrNotExist) {
16+
if errors.Is(err, fs.ErrNotExist) {
17+
return "", nil
18+
}
19+
if err != nil {
1720
return "", err
1821
}
1922
hash := sha256.Sum256(data)

internal/impl/devbox.go

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ func Open(path string, writer io.Writer) (*Devbox, error) {
125125
if err != nil {
126126
return nil, err
127127
}
128-
box.pluginManager.ApplyOptions(plugin.WithLockfile(lock))
128+
box.pluginManager.ApplyOptions(
129+
plugin.WithDevbox(box),
130+
plugin.WithLockfile(lock),
131+
)
129132
box.lockfile = lock
130133
return box, nil
131134
}
@@ -152,7 +155,26 @@ func (d *Devbox) NixPkgsCommitHash() string {
152155
}
153156

154157
func (d *Devbox) ShellPlan() (*plansdk.ShellPlan, error) {
155-
shellPlan := planner.GetShellPlan(d.projectDir, d.packages())
158+
// Create plugin directories first because inputs might depend on them
159+
for _, pkg := range d.packagesAsInputs() {
160+
if err := d.pluginManager.Create(d.writer, pkg); err != nil {
161+
return nil, err
162+
}
163+
}
164+
165+
for _, included := range d.cfg.Include {
166+
// This is a slightly weird place to put this, but since includes can't be
167+
// added via command and we need them to be added before we call
168+
// plugin manager.Include
169+
if err := d.lockfile.Add(included); err != nil {
170+
return nil, err
171+
}
172+
if err := d.pluginManager.Include(d.writer, included); err != nil {
173+
return nil, err
174+
}
175+
}
176+
177+
shellPlan := planner.GetShellPlan(d.projectDir, d.Packages())
156178
var err error
157179
shellPlan.FlakeInputs, err = d.flakeInputs()
158180
if err != nil {
@@ -369,7 +391,7 @@ func (d *Devbox) GenerateDevcontainer(force bool) error {
369391
redact.Safe(filepath.Base(devContainerPath)), err)
370392
}
371393
// generate devcontainer.json
372-
err = generate.CreateDevcontainer(devContainerPath, d.packages())
394+
err = generate.CreateDevcontainer(devContainerPath, d.Packages())
373395
if err != nil {
374396
return redact.Errorf("error generating devcontainer.json in <project>/%s: %w",
375397
redact.Safe(filepath.Base(devContainerPath)), err)
@@ -452,7 +474,6 @@ func (d *Devbox) Services() (services.Services, error) {
452474
pluginSvcs, err := d.pluginManager.GetServices(
453475
d.packagesAsInputs(),
454476
d.cfg.Include,
455-
d.projectDir,
456477
)
457478
if err != nil {
458479
return nil, err
@@ -787,8 +808,7 @@ func (d *Devbox) computeNixEnv(ctx context.Context, usePrintDevEnvCache bool) (m
787808
// We still need to be able to add env variables to non-service binaries
788809
// (e.g. ruby). This would involve understanding what binaries are associated
789810
// to a given plugin.
790-
pluginEnv, err := d.pluginManager.Env(
791-
d.packagesAsInputs(), d.cfg.Include, d.projectDir, env)
811+
pluginEnv, err := d.pluginManager.Env(d.packagesAsInputs(), d.cfg.Include, env)
792812
if err != nil {
793813
return nil, err
794814
}
@@ -955,13 +975,13 @@ func (d *Devbox) nixFlakesFilePath() string {
955975
return filepath.Join(d.projectDir, ".devbox/gen/flake/flake.nix")
956976
}
957977

958-
// packages returns the list of packages to be installed in the nix shell.
959-
func (d *Devbox) packages() []string {
978+
// Packages returns the list of Packages to be installed in the nix shell.
979+
func (d *Devbox) Packages() []string {
960980
return d.cfg.Packages
961981
}
962982

963983
func (d *Devbox) packagesAsInputs() []*nix.Input {
964-
return nix.InputsFromStrings(d.packages(), d.lockfile)
984+
return nix.InputsFromStrings(d.Packages(), d.lockfile)
965985
}
966986

967987
func (d *Devbox) findPackageByName(name string) (string, error) {

internal/impl/devbox_test.go

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/bmatcuk/doublestar/v4"
1616
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
1718

1819
"go.jetpack.io/devbox/internal/envir"
1920
"go.jetpack.io/devbox/internal/fileutil"
@@ -25,7 +26,7 @@ func TestDevbox(t *testing.T) {
2526
t.Setenv("TMPDIR", "/tmp")
2627
t.Setenv(envir.DevboxDoNotUpgradeConfig, "1")
2728
testPaths, err := doublestar.FilepathGlob("../../examples/**/devbox.json")
28-
assert.NoError(t, err, "Reading testdata/ should not fail")
29+
require.NoError(t, err, "Reading testdata/ should not fail")
2930

3031
assert.Greater(t, len(testPaths), 0, "testdata/ and examples/ should contain at least 1 test")
3132

@@ -78,25 +79,28 @@ func (n *testNix) PrintDevEnv(ctx context.Context, args *nix.PrintDevEnvArgs) (*
7879
}
7980

8081
func TestComputeNixEnv(t *testing.T) {
81-
d := &Devbox{
82-
cfg: &Config{},
83-
nix: &testNix{},
84-
}
82+
path := t.TempDir()
83+
_, err := InitConfig(path, os.Stdout)
84+
require.NoError(t, err, "InitConfig should not fail")
85+
d, err := Open(path, os.Stdout)
86+
require.NoError(t, err, "Open should not fail")
87+
d.nix = &testNix{}
8588
ctx := context.Background()
8689
env, err := d.computeNixEnv(ctx, false /*use cache*/)
87-
assert.NoError(t, err, "computeNixEnv should not fail")
90+
require.NoError(t, err, "computeNixEnv should not fail")
8891
assert.NotNil(t, env, "computeNixEnv should return a valid env")
8992
}
9093

9194
func TestComputeNixPathIsIdempotent(t *testing.T) {
92-
devbox := &Devbox{
93-
cfg: &Config{},
94-
nix: &testNix{"/tmp/my/path"},
95-
projectDir: "/tmp/TestComputeNixPathIsIdempotent",
96-
}
95+
dir := t.TempDir()
96+
_, err := InitConfig(dir, os.Stdout)
97+
require.NoError(t, err, "InitConfig should not fail")
98+
devbox, err := Open(dir, os.Stdout)
99+
require.NoError(t, err, "Open should not fail")
100+
devbox.nix = &testNix{"/tmp/my/path"}
97101
ctx := context.Background()
98102
env, err := devbox.computeNixEnv(ctx, false /*use cache*/)
99-
assert.NoError(t, err, "computeNixEnv should not fail")
103+
require.NoError(t, err, "computeNixEnv should not fail")
100104
path := env["PATH"]
101105
assert.NotEmpty(t, path, "path should not be nil")
102106

@@ -107,21 +111,22 @@ func TestComputeNixPathIsIdempotent(t *testing.T) {
107111
)
108112

109113
env, err = devbox.computeNixEnv(ctx, false /*use cache*/)
110-
assert.NoError(t, err, "computeNixEnv should not fail")
114+
require.NoError(t, err, "computeNixEnv should not fail")
111115
path2 := env["PATH"]
112116

113117
assert.Equal(t, path, path2, "path should be the same")
114118
}
115119

116120
func TestComputeNixPathWhenRemoving(t *testing.T) {
117-
devbox := &Devbox{
118-
cfg: &Config{},
119-
nix: &testNix{"/tmp/my/path"},
120-
projectDir: "/tmp/TestComputeNixPathWhenRemoving",
121-
}
121+
dir := t.TempDir()
122+
_, err := InitConfig(dir, os.Stdout)
123+
require.NoError(t, err, "InitConfig should not fail")
124+
devbox, err := Open(dir, os.Stdout)
125+
require.NoError(t, err, "Open should not fail")
126+
devbox.nix = &testNix{"/tmp/my/path"}
122127
ctx := context.Background()
123128
env, err := devbox.computeNixEnv(ctx, false /*use cache*/)
124-
assert.NoError(t, err, "computeNixEnv should not fail")
129+
require.NoError(t, err, "computeNixEnv should not fail")
125130
path := env["PATH"]
126131
assert.NotEmpty(t, path, "path should not be nil")
127132
assert.Contains(t, path, "/tmp/my/path", "path should contain /tmp/my/path")
@@ -134,7 +139,7 @@ func TestComputeNixPathWhenRemoving(t *testing.T) {
134139

135140
devbox.nix.(*testNix).path = ""
136141
env, err = devbox.computeNixEnv(ctx, false /*use cache*/)
137-
assert.NoError(t, err, "computeNixEnv should not fail")
142+
require.NoError(t, err, "computeNixEnv should not fail")
138143
path2 := env["PATH"]
139144
assert.NotContains(t, path2, "/tmp/my/path", "path should not contain /tmp/my/path")
140145

internal/impl/flakes.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,37 @@ package impl
66
import (
77
"github.com/samber/lo"
88

9-
"go.jetpack.io/devbox/internal/nix"
109
"go.jetpack.io/devbox/internal/planner/plansdk"
1110
)
1211

1312
// flakeInputs returns a list of flake inputs for the top level flake.nix
1413
// created by devbox. We map packages to the correct flake and attribute path
1514
// and group flakes by URL to avoid duplication. All inputs should be locked
1615
// i.e. have a commit hash and always resolve to the same package/version.
16+
// Note: inputs returned by this function include plugin packages. (php only for now)
17+
// It's not entirely clear we always want to add plugin packages to the top level
1718
func (d *Devbox) flakeInputs() ([]*plansdk.FlakeInput, error) {
1819
inputs := map[string]*plansdk.FlakeInput{}
19-
for _, p := range d.packages() {
20-
pkg := nix.InputFromString(p, d.lockfile)
20+
21+
userPackages := d.packagesAsInputs()
22+
pluginPackages, err := d.pluginManager.PluginPackages(userPackages)
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
order := []string{}
28+
// We prioritize plugin packages so that the php plugin works. Not sure
29+
// if this is behavior we want for user plugins. We may need to add an optional
30+
// priority field to the config.
31+
for _, pkg := range append(pluginPackages, userPackages...) {
2132
AttributePath, err := pkg.PackageAttributePath()
2233
if err != nil {
2334
return nil, err
2435
}
2536
if input, ok := inputs[pkg.URLForInput()]; !ok {
37+
order = append(order, pkg.URLForInput())
2638
inputs[pkg.URLForInput()] = &plansdk.FlakeInput{
27-
Name: pkg.Name(),
39+
Name: pkg.InputName(),
2840
URL: pkg.URLForInput(),
2941
Packages: []string{AttributePath},
3042
}
@@ -35,5 +47,14 @@ func (d *Devbox) flakeInputs() ([]*plansdk.FlakeInput, error) {
3547
}
3648
}
3749

38-
return lo.Values(inputs), nil
50+
return PickByKeysSorted(inputs, order), nil
51+
}
52+
53+
// TODO: move this to a util package
54+
func PickByKeysSorted[K comparable, V any](in map[K]V, keys []K) []V {
55+
out := make([]V, len(keys))
56+
for i, key := range keys {
57+
out[i] = in[key]
58+
}
59+
return out
3960
}

internal/impl/generate.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,6 @@ func (d *Devbox) generateShellFiles() error {
6060
}
6161
}
6262

63-
for _, pkg := range d.packagesAsInputs() {
64-
if err := d.pluginManager.Create(d.writer, pkg, d.projectDir); err != nil {
65-
return err
66-
}
67-
}
68-
69-
for _, included := range d.cfg.Include {
70-
if err := d.lockfile.Add(included); err != nil {
71-
return err
72-
}
73-
if err := d.pluginManager.Include(d.writer, included, d.projectDir); err != nil {
74-
return err
75-
}
76-
}
77-
7863
return d.writeScriptsToFiles()
7964
}
8065

internal/impl/packages.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ func (d *Devbox) pendingPackagesForInstallation(ctx context.Context) ([]string,
314314
if err != nil {
315315
return nil, err
316316
}
317-
for _, pkg := range d.packages() {
317+
for _, pkg := range d.Packages() {
318318
_, err := nix.ProfileListIndex(&nix.ProfileListIndexArgs{
319319
List: list,
320320
Lockfile: d.lockfile,

internal/impl/update.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func (d *Devbox) Update(ctx context.Context, pkgs ...string) error {
1717
pkgsToUpdate = append(pkgsToUpdate, found)
1818
}
1919
if len(pkgsToUpdate) == 0 {
20-
pkgsToUpdate = d.packages()
20+
pkgsToUpdate = d.Packages()
2121
}
2222

2323
for _, pkg := range pkgsToUpdate {

internal/nix/input.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"go.jetpack.io/devbox/internal/boxcli/featureflag"
1818
"go.jetpack.io/devbox/internal/boxcli/usererr"
19+
"go.jetpack.io/devbox/internal/cuecfg"
1920
"go.jetpack.io/devbox/internal/lock"
2021
"go.jetpack.io/devbox/internal/searcher"
2122
)
@@ -60,7 +61,7 @@ func (i *Input) IsGithub() bool {
6061

6162
var inputNameRegex = regexp.MustCompile("[^a-zA-Z0-9-]+")
6263

63-
func (i *Input) Name() string {
64+
func (i *Input) InputName() string {
6465
result := ""
6566
if i.IsLocal() {
6667
result = filepath.Base(i.Path) + "-" + i.hash()
@@ -175,6 +176,14 @@ func (i *Input) urlWithoutFragment() string {
175176
}
176177

177178
func (i *Input) hash() string {
179+
// For local flakes, use content hash of the flake.nix file to ensure
180+
// user always gets newest input.
181+
if i.IsLocal() {
182+
fileHash, _ := cuecfg.FileHash(filepath.Join(i.Path, "flake.nix"))
183+
if fileHash != "" {
184+
return fileHash[:6]
185+
}
186+
}
178187
hasher := md5.New()
179188
hasher.Write([]byte(i.String()))
180189
hash := hasher.Sum(nil)

internal/nix/input_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func TestInput(t *testing.T) {
8686

8787
for _, testCase := range cases {
8888
i := testInputFromString(testCase.pkg, projectDir)
89-
if name := i.Name(); testCase.name != name {
89+
if name := i.InputName(); testCase.name != name {
9090
t.Errorf("Name() = %v, want %v", name, testCase.name)
9191
}
9292
if urlWithoutFragment := i.urlWithoutFragment(); testCase.urlWithoutFragment != urlWithoutFragment {

internal/nix/nix.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ func (*Nix) PrintDevEnv(ctx context.Context, args *PrintDevEnvArgs) (*PrintDevEn
5858
}
5959

6060
if len(data) == 0 {
61-
cmd := exec.CommandContext(ctx, "nix", "print-dev-env", args.FlakesFilePath)
61+
cmd := exec.CommandContext(
62+
ctx,
63+
"nix", "print-dev-env",
64+
args.FlakesFilePath,
65+
)
6266
cmd.Args = append(cmd.Args, ExperimentalFlags()...)
6367
cmd.Args = append(cmd.Args, "--json")
6468
debug.Log("Running print-dev-env cmd: %s\n", cmd)
@@ -104,6 +108,23 @@ func ExperimentalFlags() []string {
104108
}
105109
}
106110

111+
var cachedSystem string
112+
113+
func System() (string, error) {
114+
if cachedSystem == "" {
115+
cmd := exec.Command(
116+
"nix", "eval", "--impure", "--raw", "--expr", "builtins.currentSystem",
117+
)
118+
cmd.Args = append(cmd.Args, ExperimentalFlags()...)
119+
out, err := cmd.Output()
120+
if err != nil {
121+
return "", errors.WithStack(err)
122+
}
123+
cachedSystem = string(out)
124+
}
125+
return cachedSystem, nil
126+
}
127+
107128
// Warning: be careful using the bins in default/bin, they won't always match bins
108129
// produced by the flakes.nix. Use devbox.NixBins() instead.
109130
func ProfileBinPath(projectDir string) string {

0 commit comments

Comments
 (0)