From 8057f1b3a3b4ce661f8c488c51953d649e755d4e Mon Sep 17 00:00:00 2001 From: Paul Myjavec Date: Mon, 24 Mar 2025 00:59:33 +0000 Subject: [PATCH 1/2] fix: Do not blindly append devbox.json, saving When saving a file, do not blindly append devbox.json to the file path. --- internal/devconfig/configfile/file.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/devconfig/configfile/file.go b/internal/devconfig/configfile/file.go index de88ed952a5..edad451cf0a 100644 --- a/internal/devconfig/configfile/file.go +++ b/internal/devconfig/configfile/file.go @@ -110,6 +110,13 @@ func (c *ConfigFile) InitHook() *shellcmd.Commands { // SaveTo writes the config to a file. func (c *ConfigFile) SaveTo(path string) error { return os.WriteFile(filepath.Join(path, DefaultName), c.Bytes(), 0o644) + finalPath := path + if filepath.Base(path) != DefaultName { + finalPath = filepath.Join(path, DefaultName) + } + + //return os.WriteFile(filepath.Join(path, DefaultName), c.Bytes(), 0o644) + return os.WriteFile(filepath.Join(finalPath), c.Bytes(), 0o644) } // TODO: Can we remove SaveTo and just use Save()? From 1dedd8f89af51d1520e5440a81ddab39a01997af Mon Sep 17 00:00:00 2001 From: Paul Myjavec Date: Mon, 24 Mar 2025 01:16:27 +0000 Subject: [PATCH 2/2] feat: Ability to lock devbox version per project Let's users pin the devbox version so there are no issues with people running various devbox versions while working in team setting. * Implements #1371 --- devbox.json | 1 + docs/app/docs/configuration.md | 6 ++ go.mod | 2 +- internal/devconfig/config.go | 1 + internal/devconfig/configfile/file.go | 37 ++++++++- internal/templates/template.go | 107 ++++++++++++++++++-------- 6 files changed, 119 insertions(+), 35 deletions(-) diff --git a/devbox.json b/devbox.json index f346011420f..2beff4dd67c 100644 --- a/devbox.json +++ b/devbox.json @@ -1,6 +1,7 @@ { "name": "devbox", "description": "Instant, easy, and predictable development environments", + "devbox_version": "0.0.0-dev", "packages": { "fd": "latest", "git": "latest", diff --git a/docs/app/docs/configuration.md b/docs/app/docs/configuration.md index ce5f9a64efd..3e9fa08d833 100644 --- a/docs/app/docs/configuration.md +++ b/docs/app/docs/configuration.md @@ -7,6 +7,7 @@ Your devbox configuration is stored in a `devbox.json` file, located in your pro ```json { + "devbox_version": "", "packages": [] | {}, "env": {}, "shell": { @@ -17,6 +18,10 @@ Your devbox configuration is stored in a `devbox.json` file, located in your pro } ``` +## Devbox Version + +The devbox_version field locks your project to a specific Devbox version, safeguarding against unexpected changes when collaborators update their environments. + ### Packages This is a list or map of Nix packages that should be installed in your Devbox shell and containers. These packages will only be installed and available within your shell, and will have precedence over any packages installed in your local machine. You can search for Nix packages using [Nix Package Search](https://search.nixos.org/packages). @@ -297,6 +302,7 @@ An example of a devbox configuration for a Rust project called `hello_world` mig ```json { + "devbox_version": "v1.0.0", "packages": [ "rustup@latest", "libiconv@latest" diff --git a/go.mod b/go.mod index 41dbba4bcc7..293ad036ad2 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/hashicorp/go-envparse v0.1.0 + github.com/hashicorp/go-version v1.7.0 github.com/joho/godotenv v1.5.1 github.com/mattn/go-isatty v0.0.20 github.com/mholt/archives v0.1.0 @@ -167,7 +168,6 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect diff --git a/internal/devconfig/config.go b/internal/devconfig/config.go index 445585dd04a..73b1a16c558 100644 --- a/internal/devconfig/config.go +++ b/internal/devconfig/config.go @@ -49,6 +49,7 @@ const defaultInitHook = "echo 'Welcome to devbox!' > /dev/null" func DefaultConfig() *Config { cfg, err := loadBytes([]byte(fmt.Sprintf(`{ "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/%s/.schema/devbox.schema.json", + "devbox_version": "%s", "packages": [], "shell": { "init_hook": [ diff --git a/internal/devconfig/configfile/file.go b/internal/devconfig/configfile/file.go index edad451cf0a..76a3b3bb01a 100644 --- a/internal/devconfig/configfile/file.go +++ b/internal/devconfig/configfile/file.go @@ -17,6 +17,8 @@ import ( "go.jetify.com/devbox/internal/boxcli/usererr" "go.jetify.com/devbox/internal/cachehash" "go.jetify.com/devbox/internal/devbox/shellcmd" + "go.jetify.com/devbox/internal/build" + "github.com/hashicorp/go-version" ) const ( @@ -32,6 +34,9 @@ type ConfigFile struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` + // Let's users specify the version of devbox. + DevboxVersion string `json:"devbox_version,omitempty"` + // PackagesMutator is the slice of Nix packages that devbox makes available in // its environment. Deliberately do not omitempty. PackagesMutator PackagesMutator `json:"packages"` @@ -109,7 +114,6 @@ func (c *ConfigFile) InitHook() *shellcmd.Commands { // SaveTo writes the config to a file. func (c *ConfigFile) SaveTo(path string) error { - return os.WriteFile(filepath.Join(path, DefaultName), c.Bytes(), 0o644) finalPath := path if filepath.Base(path) != DefaultName { finalPath = filepath.Join(path, DefaultName) @@ -164,6 +168,7 @@ func validateConfig(cfg *ConfigFile) error { fns := []func(cfg *ConfigFile) error{ ValidateNixpkg, validateScripts, + ValidateDevboxVersion, } for _, fn := range fns { @@ -210,3 +215,33 @@ func ValidateNixpkg(cfg *ConfigFile) error { } return nil } + +func ValidateDevboxVersion(cfg *ConfigFile) error { + if cfg.DevboxVersion == "" { + return usererr.New("Missing devbox_version field in config, suggested value: \"~%s\",", build.Version) + } + + // Use hashicorp/go-version for version constraint checking + constraints, err := version.NewConstraint(cfg.DevboxVersion) + if err != nil { + return usererr.New("Invalid devbox_version constraint in config: %s", cfg.DevboxVersion) + } + + currentVersion, err := version.NewVersion(build.Version) + if err != nil { + return usererr.New("Invalid current devbox version: %s", build.Version) + } + + if !constraints.Check(currentVersion) { + return usererr.New("Devbox version mismatch: project requires version %s but your running version is %s", + cfg.DevboxVersion, build.Version) + } + + return nil +} + +// SetDevboxVersion sets the devbox_version field in the config +func (c *ConfigFile) SetDevboxVersion(version string) { + c.DevboxVersion = version + c.SetStringField("DevboxVersion", version) +} diff --git a/internal/templates/template.go b/internal/templates/template.go index 7dbfa18863b..570352875c8 100644 --- a/internal/templates/template.go +++ b/internal/templates/template.go @@ -18,6 +18,9 @@ import ( "go.jetify.com/devbox/internal/boxcli/usererr" "go.jetify.com/devbox/internal/build" + "go.jetify.com/devbox/internal/devconfig" + + "github.com/hashicorp/go-version" ) func InitFromName(w io.Writer, template, target string) error { @@ -29,41 +32,46 @@ func InitFromName(w io.Writer, template, target string) error { } func InitFromRepo(w io.Writer, repo, subdir, target string) error { - if err := createDirAndEnsureEmpty(target); err != nil { - return err - } - parsedRepoURL, err := ParseRepoURL(repo) - if err != nil { - return errors.WithStack(err) - } + if err := createDirAndEnsureEmpty(target); err != nil { + return err + } + parsedRepoURL, err := ParseRepoURL(repo) + if err != nil { + return errors.WithStack(err) + } - tmp, err := os.MkdirTemp("", "devbox-template") - if err != nil { - return errors.WithStack(err) - } - cmd := exec.Command( - "git", "clone", parsedRepoURL, - // Clone and checkout a specific ref - "-b", lo.Ternary(build.IsDev, "main", build.Version), - // Create shallow clone with depth of 1 - "--depth", "1", - tmp, - ) - fmt.Fprintf(w, "%s\n", cmd) - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - if err = cmd.Run(); err != nil { - return errors.WithStack(err) - } + tmp, err := os.MkdirTemp("", "devbox-template") + if err != nil { + return errors.WithStack(err) + } + cmd := exec.Command( + "git", "clone", parsedRepoURL, + // Clone and checkout a specific ref + "-b", lo.Ternary(build.IsDev, "main", build.Version), + // Create shallow clone with depth of 1 + "--depth", "1", + tmp, + ) + fmt.Fprintf(w, "%s\n", cmd) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + if err = cmd.Run(); err != nil { + return errors.WithStack(err) + } + + cmd = exec.Command( + "sh", "-c", + fmt.Sprintf("cp -r %s %s", filepath.Join(tmp, subdir, "*"), target), + ) + fmt.Fprintf(w, "%s\n", cmd) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + if err = cmd.Run(); err != nil { + return errors.WithStack(err) + } - cmd = exec.Command( - "sh", "-c", - fmt.Sprintf("cp -r %s %s", filepath.Join(tmp, subdir, "*"), target), - ) - fmt.Fprintf(w, "%s\n", cmd) - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - return errors.WithStack(cmd.Run()) + // Set the devbox version after initializing the template + return SetCurrentDevboxVersion(w, target) } func List(w io.Writer, showAll bool) { @@ -105,3 +113,36 @@ func ParseRepoURL(repo string) (string, error) { // like: https://github.com/jetify-com/devbox.git return strings.TrimSuffix(repo, ".git"), nil } + +// SetCurrentDevboxVersion sets the current version as the required version in the config +func SetCurrentDevboxVersion(w io.Writer, projectDir string) error { + if strings.HasSuffix(projectDir, "devbox.json") { + projectDir = filepath.Dir(projectDir) + } + + fmt.Println(projectDir) + + cfg, err := devconfig.Open(projectDir) + if err != nil { + return errors.WithStack(err) + } + fmt.Printf("%v", cfg) + + // Create a constraint like "~1.2.0" (compatible with 1.2.x) + currentVersion, err := version.NewVersion(build.Version) + if err != nil { + return errors.WithStack(err) + } + + segments := currentVersion.Segments() + if len(segments) < 2 { + return errors.New("invalid version format") + } + + // Create a constraint for the current major.minor version + versionConstraint := fmt.Sprintf("~%d.%d.0", segments[0], segments[1]) + + fmt.Fprintf(w, "Setting project devbox version constraint: %s\n", versionConstraint) + cfg.Root.SetDevboxVersion(versionConstraint) + return cfg.Root.SaveTo(cfg.Root.AbsRootPath) +}