Skip to content

Commit a3135c6

Browse files
authored
[tyson] Add tyson support experiment (#1656)
## Summary Minimal viable implementation of tyson support. This allows user to opt-in via feature flag and a `devbox.tson` config. It has no effect for non-opt Proof of concept of how this can be used: https://github.com/jetpack-io/frontend/pull/504 (warning private repo) To simplify this implementation, it is not possible to use any devbox command that writes to `devbox.json`. It will fail with an error. We will also show an error if both configs are present. Tyson allows more programatic logic in our configs. Specifically: * Allows importing other configs which adds composability. * Allows importing of other package manager files (e.g. `package.json`, `composer.json`, etc which allows you to mimic and enhance their functionality) * Allows you to customize hooks, packages, and scripts using host environment variables. ([Proof of concept](jetify-com/axiom#3532)) ## How was it tested? * Tested in the proof of concept link above. * CICD
1 parent 6e93416 commit a3135c6

File tree

10 files changed

+108
-18
lines changed

10 files changed

+108
-18
lines changed

internal/boxcli/featureflag/feature.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strconv"
99
"testing"
1010

11+
"go.jetpack.io/devbox/internal/build"
1112
"go.jetpack.io/devbox/internal/debug"
1213
"go.jetpack.io/devbox/internal/envir"
1314
)
@@ -61,6 +62,13 @@ func (f *feature) Enabled() bool {
6162
return f.enabled
6263
}
6364

65+
func (f *feature) EnableOnDev() *feature {
66+
if build.IsDev {
67+
f.enabled = true
68+
}
69+
return f
70+
}
71+
6472
func (f *feature) EnableForTest(t *testing.T) {
6573
t.Setenv(envir.DevboxFeaturePrefix+f.name, "1")
6674
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package featureflag
2+
3+
var TySON = disable("TYSON").EnableOnDev()

internal/boxcli/multi/multi.go

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

77
"go.jetpack.io/devbox"
88
"go.jetpack.io/devbox/internal/debug"
9+
"go.jetpack.io/devbox/internal/devconfig"
910
"go.jetpack.io/devbox/internal/impl/devopt"
1011
)
1112

@@ -20,7 +21,7 @@ func Open(opts *devopt.Opts) ([]devbox.Devbox, error) {
2021
return err
2122
}
2223

23-
if !dirEntry.IsDir() && filepath.Base(path) == "devbox.json" {
24+
if !dirEntry.IsDir() && devconfig.IsConfigName(filepath.Base(path)) {
2425
optsCopy := *opts
2526
optsCopy.Dir = path
2627
box, err := devbox.Open(&optsCopy)

internal/devconfig/config.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,21 @@ import (
1616

1717
"github.com/pkg/errors"
1818
"github.com/tailscale/hujson"
19+
"go.jetpack.io/devbox/internal/boxcli/featureflag"
1920
"go.jetpack.io/devbox/internal/boxcli/usererr"
2021
"go.jetpack.io/devbox/internal/cuecfg"
2122
"go.jetpack.io/devbox/internal/impl/shellcmd"
2223
)
2324

24-
const DefaultName = "devbox.json"
25+
const (
26+
defaultName = "devbox.json"
27+
defaultTySONName = "devbox.tson"
28+
)
29+
30+
const (
31+
jsonFormat = iota
32+
tsonFormat
33+
)
2534

2635
// Config defines a devbox environment as JSON.
2736
type Config struct {
@@ -48,7 +57,8 @@ type Config struct {
4857
// This is a similar format to nix inputs
4958
Include []string `json:"include,omitempty"`
5059

51-
ast *configAST
60+
ast *configAST
61+
format int
5262
}
5363

5464
type shellConfig struct {
@@ -128,7 +138,10 @@ func (c *Config) InitHook() *shellcmd.Commands {
128138

129139
// SaveTo writes the config to a file.
130140
func (c *Config) SaveTo(path string) error {
131-
return os.WriteFile(filepath.Join(path, DefaultName), c.Bytes(), 0o644)
141+
if c.format != jsonFormat {
142+
return errors.New("cannot save config to non-json format")
143+
}
144+
return os.WriteFile(filepath.Join(path, defaultName), c.Bytes(), 0o644)
132145
}
133146

134147
// Load reads a devbox config file, and validates it.
@@ -224,3 +237,15 @@ func ValidateNixpkg(cfg *Config) error {
224237
}
225238
return nil
226239
}
240+
241+
func IsConfigName(name string) bool {
242+
return slices.Contains(ValidConfigNames(), name)
243+
}
244+
245+
func ValidConfigNames() []string {
246+
names := []string{defaultName}
247+
if featureflag.TySON.Enabled() {
248+
names = append(names, defaultTySONName)
249+
}
250+
return names
251+
}

internal/devconfig/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ func TestDefault(t *testing.T) {
602602
if err != nil {
603603
t.Fatal("got save error:", err)
604604
}
605-
out, err := Load(filepath.Join(path, "devbox.json"))
605+
out, err := Load(filepath.Join(path, defaultName))
606606
if err != nil {
607607
t.Fatal("got load error:", err)
608608
}

internal/devconfig/init.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,25 @@
44
package devconfig
55

66
import (
7+
"context"
78
"errors"
89
"fmt"
910
"io"
1011
"os"
12+
"os/exec"
1113
"path/filepath"
1214
"strings"
1315

1416
"github.com/fatih/color"
1517

18+
"go.jetpack.io/devbox/internal/boxcli/featureflag"
19+
"go.jetpack.io/devbox/internal/devpkg/pkgtype"
20+
"go.jetpack.io/devbox/internal/fileutil"
1621
"go.jetpack.io/devbox/internal/initrec"
1722
)
1823

1924
func Init(dir string, writer io.Writer) (created bool, err error) {
20-
created, err = initConfigFile(filepath.Join(dir, DefaultName))
25+
created, err = initConfigFile(filepath.Join(dir, defaultName))
2126
if err != nil || !created {
2227
return created, err
2328
}
@@ -62,3 +67,43 @@ func initConfigFile(path string) (created bool, err error) {
6267
}
6368
return true, nil
6469
}
70+
71+
func Open(projectDir string) (*Config, error) {
72+
cfgPath := filepath.Join(projectDir, defaultName)
73+
74+
if !featureflag.TySON.Enabled() {
75+
return Load(cfgPath)
76+
}
77+
78+
tysonCfgPath := filepath.Join(projectDir, defaultTySONName)
79+
80+
// If tyson config exists use it. Otherwise fallback to json config.
81+
// In the future we may error out if both configs exist, but for now support
82+
// both while we experiment with tyson support.
83+
if fileutil.Exists(tysonCfgPath) {
84+
paths, err := pkgtype.RunXClient().Install(context.TODO(), "jetpack-io/tyson")
85+
if err != nil {
86+
return nil, err
87+
}
88+
binPath := filepath.Join(paths[0], "tyson")
89+
tmpFile, err := os.CreateTemp("", "*.json")
90+
if err != nil {
91+
return nil, err
92+
}
93+
cmd := exec.Command(binPath, "eval", tysonCfgPath)
94+
cmd.Stderr = os.Stderr
95+
cmd.Stdout = tmpFile
96+
if err = cmd.Run(); err != nil {
97+
return nil, err
98+
}
99+
cfgPath = tmpFile.Name()
100+
config, err := Load(cfgPath)
101+
if err != nil {
102+
return nil, err
103+
}
104+
config.format = tsonFormat
105+
return config, nil
106+
}
107+
108+
return Load(cfgPath)
109+
}

internal/impl/devbox.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,8 @@ func Open(opts *devopt.Opts) (*Devbox, error) {
7878
if err != nil {
7979
return nil, err
8080
}
81-
cfgPath := filepath.Join(projectDir, devconfig.DefaultName)
8281

83-
cfg, err := devconfig.Load(cfgPath)
82+
cfg, err := devconfig.Open(projectDir)
8483
if err != nil {
8584
return nil, errors.WithStack(err)
8685
}

internal/impl/dir.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func findProjectDirAtPath(absPath string) (string, error) {
4343

4444
switch mode := fi.Mode(); {
4545
case mode.IsDir():
46-
if !fileutil.Exists(filepath.Join(absPath, devconfig.DefaultName)) {
46+
if !configExistsIn(absPath) {
4747
return "", missingConfigError(absPath, false /*didCheckParents*/)
4848
}
4949
return absPath, nil
@@ -63,13 +63,13 @@ func findProjectDirFromParentDirSearch(
6363
cur := absPath
6464
// Search parent directories for a devbox.json
6565
for cur != root {
66-
debug.Log("finding %s in dir: %s\n", devconfig.DefaultName, cur)
67-
if fileutil.Exists(filepath.Join(cur, devconfig.DefaultName)) {
66+
debug.Log("finding devbox config in dir: %s\n", cur)
67+
if configExistsIn(cur) {
6868
return cur, nil
6969
}
7070
cur = filepath.Dir(cur)
7171
}
72-
if fileutil.Exists(filepath.Join(cur, devconfig.DefaultName)) {
72+
if configExistsIn(cur) {
7373
return cur, nil
7474
}
7575
return "", missingConfigError(absPath, true /*didCheckParents*/)
@@ -107,3 +107,12 @@ func missingConfigError(path string, didCheckParents bool) error {
107107
parentDirCheckAddendum,
108108
)
109109
}
110+
111+
func configExistsIn(path string) bool {
112+
for _, name := range devconfig.ValidConfigNames() {
113+
if fileutil.Exists(filepath.Join(path, name)) {
114+
return true
115+
}
116+
}
117+
return false
118+
}

internal/impl/dir_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func TestFindProjectDirFromParentDirSearch(t *testing.T) {
5353
err = os.MkdirAll(filepath.Join(root, testCase.allDirs), 0o777)
5454
assert.NoError(err)
5555

56-
absProjectPath, err := filepath.Abs(filepath.Join(root, testCase.projectDir, devconfig.DefaultName))
56+
absProjectPath, err := filepath.Abs(filepath.Join(root, testCase.projectDir, devconfig.ValidConfigNames()[0]))
5757
assert.NoError(err)
5858
err = os.WriteFile(absProjectPath, []byte("{}"), 0o666)
5959
assert.NoError(err)
@@ -97,14 +97,14 @@ func TestFindParentDirAtPath(t *testing.T) {
9797
name: "flag_path_is_file_has_config",
9898
allDirs: "a/b/c",
9999
projectDir: "a/b",
100-
flagPath: "a/b/" + devconfig.DefaultName,
100+
flagPath: "a/b/" + devconfig.ValidConfigNames()[0],
101101
expectError: false,
102102
},
103103
{
104104
name: "flag_path_is_file_missing_config",
105105
allDirs: "a/b/c",
106106
projectDir: "", // missing config
107-
flagPath: "a/b/" + devconfig.DefaultName,
107+
flagPath: "a/b/" + devconfig.ValidConfigNames()[0],
108108
expectError: true,
109109
},
110110
}
@@ -121,7 +121,7 @@ func TestFindParentDirAtPath(t *testing.T) {
121121

122122
var absProjectPath string
123123
if testCase.projectDir != "" {
124-
absProjectPath, err = filepath.Abs(filepath.Join(root, testCase.projectDir, devconfig.DefaultName))
124+
absProjectPath, err = filepath.Abs(filepath.Join(root, testCase.projectDir, devconfig.ValidConfigNames()[0]))
125125
assert.NoError(err)
126126
err = os.WriteFile(absProjectPath, []byte("{}"), 0o666)
127127
assert.NoError(err)

internal/pullbox/files.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func profileIsNotEmpty(path string) (bool, error) {
6262
return false, errors.WithStack(err)
6363
}
6464
for _, entry := range entries {
65-
if entry.Name() != devconfig.DefaultName ||
65+
if !devconfig.IsConfigName(entry.Name()) ||
6666
isModifiedConfig(filepath.Join(path, entry.Name())) {
6767
return true, nil
6868
}
@@ -71,7 +71,7 @@ func profileIsNotEmpty(path string) (bool, error) {
7171
}
7272

7373
func isModifiedConfig(path string) bool {
74-
if filepath.Base(path) == devconfig.DefaultName {
74+
if devconfig.IsConfigName(filepath.Base(path)) {
7575
cfg, err := devconfig.Load(path)
7676
if err != nil {
7777
return false

0 commit comments

Comments
 (0)