Skip to content

Commit 7a3ae79

Browse files
authored
[per-OS Packages] Add platforms and excluded_platforms functionality (#1358)
## Summary **Motivation** Some users have a need to install platform-specific packages. For example, `glibcLocales` cannot be installed on macOS but may be needed on Linux. **Approach** We enable two flags in `devbox add`: ``` # install on all platforms except x86_64-darwin > devbox add glibcLocales --exclude-platform x86_64-darwin # only install on x86_64-darwin and aarch64-darwin > devbox add darwin.apple_sdk.frameworks.Security --platform x86_64-darwin > devbox add darwin.apple_sdk.frameworks.Security --platform aarch64-darwin ``` Follow up PRs: - [ ] enable multiple `--platform` flag uses - [ ] detect if `devbox add` fails due to platform-mismatch and suggest `--exclude-platform` flag ## How was it tested? Added testscript unit test Adhoc testing: ``` savil@Savil-Srivastavas-MacBook-Pro ~/c/j/d/e/d/g/hello-world (savil/config-packages-2)> rm -rf .devbox savil@Savil-Srivastavas-MacBook-Pro ~/c/j/d/e/d/g/hello-world (savil/config-packages-2)> devbox shell Ensuring packages are installed. Installing package: [email protected]. [1/1] [email protected] [1/1] [email protected]: Success Starting a devbox shell... Welcome to fish, the friendly interactive shell Type help for instructions on how to use fish ``` Install for a different platform than the one i am currently running on ``` (devbox) savil@Savil-Srivastavas-MacBook-Pro ~/c/j/d/e/d/g/hello-world (savil/config-packages-2)> devbox add hello --platform x86_64-linux (devbox) savil@Savil-Srivastavas-MacBook-Pro ~/c/j/d/e/d/g/hello-world (savil/config-packages-2)> cat devbox.json { "packages": { "go": "1.19.8", "hello": { "version": "latest", "platforms": [ "x86_64-linux" ] } }, "shell": { "init_hook": [ "export \"GOROOT=$(go env GOROOT)\"" ], "scripts": { "run_test": "go run main.go" } } } (devbox) savil@Savil-Srivastavas-MacBook-Pro ~/c/j/d/e/d/g/hello-world (savil/config-packages-2)> which hello (devbox) savil@Savil-Srivastavas-MacBook-Pro ~/c/j/d/e/d/g/hello-world (savil/config-packages-2) [1]> ``` Install on all platforms except my current platform ``` (devbox) savil@Savil-Srivastavas-MacBook-Pro ~/c/j/d/e/d/g/hello-world (savil/config-packages-2) [1]> devbox add cowsay --exclude-platform x86_64-darwin (devbox) savil@Savil-Srivastavas-MacBook-Pro ~/c/j/d/e/d/g/hello-world (savil/config-packages-2)> which cowsay (devbox) savil@Savil-Srivastavas-MacBook-Pro ~/c/j/d/e/d/g/hello-world (savil/config-packages-2) [1]> cat devbox.json { "packages": { "go": "1.19.8", "hello": { "version": "latest", "platforms": [ "x86_64-linux" ] }, "cowsay": { "version": "latest", "excluded_platforms": [ "x86_64-darwin" ] } }, "shell": { "init_hook": [ "export \"GOROOT=$(go env GOROOT)\"" ], "scripts": { "run_test": "go run main.go" } } } ``` Ensure basic adding still works ``` (devbox) savil@Savil-Srivastavas-MacBook-Pro ~/c/j/d/e/d/g/hello-world (savil/config-packages-2)> which vim /usr/bin/vim (devbox) savil@Savil-Srivastavas-MacBook-Pro ~/c/j/d/e/d/g/hello-world (savil/config-packages-2)> devbox add vim Installing package: vim@latest. [1/1] vim@latest [1/1] vim@latest: Success (devbox) savil@Savil-Srivastavas-MacBook-Pro ~/c/j/d/e/d/g/hello-world (savil/config-packages-2)> which vim /Users/savil/code/jetpack/devbox/examples/development/go/hello-world/.devbox/virtenv/.wrappers/bin/vim (devbox) savil@Savil-Srivastavas-MacBook-Pro ~/c/j/d/e/d/g/hello-world (savil/config-packages-2)> cat devbox.json { "packages": { "go": "1.19.8", "hello": { "version": "latest", "platforms": [ "x86_64-linux" ] }, "cowsay": { "version": "latest", "excluded_platforms": [ "x86_64-darwin" ] }, "vim": "latest" }, "shell": { "init_hook": [ "export \"GOROOT=$(go env GOROOT)\"" ], "scripts": { "run_test": "go run main.go" } } } ```
1 parent 0466f6a commit 7a3ae79

File tree

9 files changed

+361
-18
lines changed

9 files changed

+361
-18
lines changed

devbox.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type Devbox interface {
1919
// Add adds Nix packages to the config so that they're available in the devbox
2020
// environment. It validates that the Nix packages exist, and install them.
2121
// Adding duplicate packages is a no-op.
22-
Add(ctx context.Context, pkgs ...string) error
22+
Add(ctx context.Context, platform, excludePlatform string, pkgs ...string) error
2323
Config() *devconfig.Config
2424
ProjectDir() string
2525
// Generate creates the directory of Nix files and the Dockerfile that define

internal/boxcli/add.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import (
1818
const toSearchForPackages = "To search for packages, use the `devbox search` command"
1919

2020
type addCmdFlags struct {
21-
config configFlags
22-
allowInsecure bool
21+
config configFlags
22+
allowInsecure bool
23+
platform string
24+
excludePlatform string
2325
}
2426

2527
func addCmd() *cobra.Command {
@@ -50,7 +52,13 @@ func addCmd() *cobra.Command {
5052
flags.config.register(command)
5153
command.Flags().BoolVar(
5254
&flags.allowInsecure, "allow-insecure", false,
53-
"Allow adding packages marked as insecure.")
55+
"allow adding packages marked as insecure.")
56+
command.Flags().StringVar(
57+
&flags.platform, "platform", "",
58+
"add packages to run on only this platform.")
59+
command.Flags().StringVar(
60+
&flags.excludePlatform, "exclude-platform", "",
61+
"exclude packages from a specific platform.")
5462

5563
return command
5664
}
@@ -65,5 +73,5 @@ func addCmdFunc(cmd *cobra.Command, args []string, flags addCmdFlags) error {
6573
return errors.WithStack(err)
6674
}
6775

68-
return box.Add(cmd.Context(), args...)
76+
return box.Add(cmd.Context(), flags.platform, flags.excludePlatform, args...)
6977
}

internal/devconfig/packages.go

Lines changed: 120 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ package devconfig
22

33
import (
44
"encoding/json"
5+
"os"
6+
"strings"
57

68
"github.com/pkg/errors"
79
orderedmap "github.com/wk8/go-ordered-map/v2"
10+
"go.jetpack.io/devbox/internal/nix"
811
"go.jetpack.io/devbox/internal/searcher"
12+
"go.jetpack.io/devbox/internal/ux"
913
"golang.org/x/exp/slices"
1014
)
1115

@@ -55,6 +59,84 @@ func (pkgs *Packages) Remove(versionedName string) {
5559
})
5660
}
5761

62+
// AddPlatform adds a platform to the list of platforms for a given package
63+
func (pkgs *Packages) AddPlatform(versionedname, platform string) error {
64+
if err := nix.EnsureValidPlatform(platform); err != nil {
65+
return errors.WithStack(err)
66+
}
67+
68+
name, version := parseVersionedName(versionedname)
69+
for idx, pkg := range pkgs.Collection {
70+
if pkg.name == name && pkg.Version == version {
71+
72+
// Check if the platform is already present
73+
alreadyPresent := false
74+
for _, existing := range pkg.Platforms {
75+
if existing == platform {
76+
alreadyPresent = true
77+
break
78+
}
79+
}
80+
81+
// Add the platform if it's not already present
82+
if !alreadyPresent {
83+
pkg.Platforms = append(pkg.Platforms, platform)
84+
}
85+
86+
// Adding any platform will restrict installation to it, so
87+
// the ExcludedPlatforms are no longer needed
88+
pkg.ExcludedPlatforms = nil
89+
90+
pkgs.jsonKind = jsonMap
91+
pkg.kind = regular
92+
pkgs.Collection[idx] = pkg
93+
return nil
94+
}
95+
}
96+
return errors.Errorf("package %s not found", versionedname)
97+
}
98+
99+
// ExcludePlatform adds a platform to the list of excluded platforms for a given package
100+
func (pkgs *Packages) ExcludePlatform(versionedName, platform string) error {
101+
if err := nix.EnsureValidPlatform(platform); err != nil {
102+
return errors.WithStack(err)
103+
}
104+
105+
name, version := parseVersionedName(versionedName)
106+
for idx, pkg := range pkgs.Collection {
107+
if pkg.name == name && pkg.Version == version {
108+
109+
// Check if the platform is already present
110+
alreadyPresent := false
111+
for _, existing := range pkg.ExcludedPlatforms {
112+
if existing == platform {
113+
alreadyPresent = true
114+
break
115+
}
116+
}
117+
118+
if !alreadyPresent {
119+
pkg.ExcludedPlatforms = append(pkg.ExcludedPlatforms, platform)
120+
}
121+
if len(pkg.Platforms) > 0 {
122+
ux.Finfo(
123+
os.Stderr,
124+
"Excluding a platform for %[1]s is a bit redundant because it will only be installed on: %[2]v. "+
125+
"Consider removing the `platform` field from %[1]s's definition in your devbox."+
126+
"json if you intend for %[1]s to be installed on all platforms except %[3]s.\n",
127+
versionedName, strings.Join(pkg.Platforms, ", "), platform,
128+
)
129+
}
130+
131+
pkgs.jsonKind = jsonMap
132+
pkg.kind = regular
133+
pkgs.Collection[idx] = pkg
134+
return nil
135+
}
136+
}
137+
return errors.Errorf("package %s not found", versionedName)
138+
}
139+
58140
func (pkgs *Packages) UnmarshalJSON(data []byte) error {
59141

60142
// First, attempt to unmarshal as a list of strings (legacy format)
@@ -123,7 +205,8 @@ type Package struct {
123205
// deliberately not adding omitempty
124206
Version string `json:"version"`
125207

126-
// TODO: add other fields like platforms
208+
Platforms []string `json:"platforms,omitempty"`
209+
ExcludedPlatforms []string `json:"excluded_platforms,omitempty"`
127210
}
128211

129212
func NewVersionOnlyPackage(name, version string) Package {
@@ -143,13 +226,46 @@ func NewPackage(name string, values map[string]any) Package {
143226
version = ""
144227
}
145228

229+
var platforms []string
230+
if p, ok := values["platforms"]; ok {
231+
platforms = p.([]string)
232+
}
233+
var excludedPlatforms []string
234+
if e, ok := values["excluded_platforms"]; ok {
235+
excludedPlatforms = e.([]string)
236+
}
237+
146238
return Package{
147-
kind: regular,
148-
name: name,
149-
Version: version.(string),
239+
kind: regular,
240+
name: name,
241+
Version: version.(string),
242+
Platforms: platforms,
243+
ExcludedPlatforms: excludedPlatforms,
150244
}
151245
}
152246

247+
// enabledOnPlatform returns whether the package is enabled on the given platform.
248+
// If the package has a list of platforms, it is enabled only on those platforms.
249+
// If the package has a list of excluded platforms, it is enabled on all platforms
250+
// except those.
251+
func (p *Package) IsEnabledOnPlatform() bool {
252+
platform := nix.MustGetSystem()
253+
if len(p.Platforms) > 0 {
254+
for _, plt := range p.Platforms {
255+
if plt == platform {
256+
return true
257+
}
258+
}
259+
return false
260+
}
261+
for _, plt := range p.ExcludedPlatforms {
262+
if plt == platform {
263+
return false
264+
}
265+
}
266+
return true
267+
}
268+
153269
func (p *Package) VersionedName() string {
154270
name := p.name
155271
if p.Version != "" {
@@ -158,11 +274,6 @@ func (p *Package) VersionedName() string {
158274
return name
159275
}
160276

161-
func (p *Package) IsEnabledOnPlatform() bool {
162-
// TODO savil. Next PR will update this implementation
163-
return true
164-
}
165-
166277
func (p *Package) UnmarshalJSON(data []byte) error {
167278
// First, attempt to unmarshal as a version-only string
168279
var version string

internal/devconfig/packages_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,100 @@ func TestJsonifyConfigPackages(t *testing.T) {
7474
},
7575
},
7676
},
77+
{
78+
name: "map-with-platforms",
79+
jsonConfig: `{"packages":{"python":{"version":"latest",` +
80+
`"platforms":["x86_64-darwin","aarch64-linux"]}}}`,
81+
expected: Packages{
82+
jsonKind: jsonMap,
83+
Collection: []Package{
84+
NewPackage("python", map[string]any{
85+
"version": "latest",
86+
"platforms": []string{"x86_64-darwin", "aarch64-linux"},
87+
}),
88+
},
89+
},
90+
},
91+
{
92+
name: "map-with-excluded-platforms",
93+
jsonConfig: `{"packages":{"python":{"version":"latest",` +
94+
`"excluded_platforms":["x86_64-linux"]}}}`,
95+
expected: Packages{
96+
jsonKind: jsonMap,
97+
Collection: []Package{
98+
NewPackage("python", map[string]any{
99+
"version": "latest",
100+
"excluded_platforms": []string{"x86_64-linux"},
101+
}),
102+
},
103+
},
104+
},
105+
{
106+
name: "map-with-platforms-and-excluded-platforms",
107+
jsonConfig: `{"packages":{"python":{"version":"latest",` +
108+
`"platforms":["x86_64-darwin","aarch64-linux"],` +
109+
`"excluded_platforms":["x86_64-linux"]}}}`,
110+
expected: Packages{
111+
jsonKind: jsonMap,
112+
Collection: []Package{
113+
NewPackage("python", map[string]any{
114+
"version": "latest",
115+
"platforms": []string{"x86_64-darwin", "aarch64-linux"},
116+
"excluded_platforms": []string{"x86_64-linux"},
117+
}),
118+
},
119+
},
120+
},
121+
{
122+
name: "map-with-platforms-and-excluded-platforms-local-flake",
123+
jsonConfig: `{"packages":{"path:my-php-flake#hello":{"version":"latest",` +
124+
`"platforms":["x86_64-darwin","aarch64-linux"],` +
125+
`"excluded_platforms":["x86_64-linux"]}}}`,
126+
expected: Packages{
127+
jsonKind: jsonMap,
128+
Collection: []Package{
129+
NewPackage("path:my-php-flake#hello", map[string]any{
130+
"version": "latest",
131+
"platforms": []string{"x86_64-darwin", "aarch64-linux"},
132+
"excluded_platforms": []string{"x86_64-linux"},
133+
}),
134+
},
135+
},
136+
},
137+
{
138+
name: "map-with-platforms-and-excluded-platforms-remote-flake",
139+
jsonConfig: `{"packages":{"github:F1bonacc1/process-compose/v0.43.1":` +
140+
`{"version":"latest",` +
141+
`"platforms":["x86_64-darwin","aarch64-linux"],` +
142+
`"excluded_platforms":["x86_64-linux"]}}}`,
143+
expected: Packages{
144+
jsonKind: jsonMap,
145+
Collection: []Package{
146+
NewPackage("github:F1bonacc1/process-compose/v0.43.1", map[string]any{
147+
"version": "latest",
148+
"platforms": []string{"x86_64-darwin", "aarch64-linux"},
149+
"excluded_platforms": []string{"x86_64-linux"},
150+
}),
151+
},
152+
},
153+
},
154+
{
155+
name: "map-with-platforms-and-excluded-platforms-nixpkgs-reference",
156+
jsonConfig: `{"packages":{"github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello":` +
157+
`{"version":"latest",` +
158+
`"platforms":["x86_64-darwin","aarch64-linux"],` +
159+
`"excluded_platforms":["x86_64-linux"]}}}`,
160+
expected: Packages{
161+
jsonKind: jsonMap,
162+
Collection: []Package{
163+
NewPackage("github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello", map[string]any{
164+
"version": "latest",
165+
"platforms": []string{"x86_64-darwin", "aarch64-linux"},
166+
"excluded_platforms": []string{"x86_64-linux"},
167+
}),
168+
},
169+
},
170+
},
77171
}
78172

79173
for _, testCase := range testCases {
@@ -151,6 +245,24 @@ func TestParseVersionedName(t *testing.T) {
151245
expectedName: "emacsPackages.@",
152246
expectedVersion: "",
153247
},
248+
{
249+
name: "local-flake",
250+
input: "path:my-php-flake#hello",
251+
expectedName: "path:my-php-flake#hello",
252+
expectedVersion: "",
253+
},
254+
{
255+
name: "remote-flake",
256+
input: "github:F1bonacc1/process-compose/v0.43.1",
257+
expectedName: "github:F1bonacc1/process-compose/v0.43.1",
258+
expectedVersion: "",
259+
},
260+
{
261+
name: "nixpkgs-reference",
262+
input: "github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello",
263+
expectedName: "github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello",
264+
expectedVersion: "",
265+
},
154266
}
155267

156268
for _, testCase := range testCases {

internal/devpkg/package.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,3 +544,12 @@ func (p *Package) StoreName() (string, error) {
544544
}
545545
return name, nil
546546
}
547+
548+
func (p *Package) EnsureUninstallableIsInLockfile() error {
549+
// TODO savil: Do we need the IsDevboxPackage check here?
550+
if !p.IsInstallable() || !p.IsDevboxPackage() {
551+
return nil
552+
}
553+
_, err := p.lockfile.Resolve(p.Raw)
554+
return err
555+
}

internal/impl/packages.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ import (
3333

3434
// Add adds the `pkgs` to the config (i.e. devbox.json) and nix profile for this
3535
// devbox project
36-
func (d *Devbox) Add(ctx context.Context, pkgsNames ...string) error {
36+
// nolint:revive // warns about cognitive complexity
37+
func (d *Devbox) Add(ctx context.Context, platform, excludePlatform string, pkgsNames ...string) error {
3738
ctx, task := trace.NewTask(ctx, "devboxAdd")
3839
defer task.End()
3940

@@ -86,6 +87,19 @@ func (d *Devbox) Add(ctx context.Context, pkgsNames ...string) error {
8687
addedPackageNames = append(addedPackageNames, packageNameForConfig)
8788
}
8889

90+
for _, pkg := range addedPackageNames {
91+
if platform != "" {
92+
if err := d.cfg.Packages.AddPlatform(pkg, platform); err != nil {
93+
return err
94+
}
95+
}
96+
if excludePlatform != "" {
97+
if err := d.cfg.Packages.ExcludePlatform(pkg, excludePlatform); err != nil {
98+
return err
99+
}
100+
}
101+
}
102+
89103
// Resolving here ensures we allow insecure before running ensurePackagesAreInstalled
90104
// which will call print-dev-env. Resolving does not save the lockfile, we
91105
// save at the end when everything has succeeded.
@@ -228,6 +242,13 @@ func (d *Devbox) ensurePackagesAreInstalled(ctx context.Context, mode installMod
228242
return err
229243
}
230244

245+
// Update lockfile with new packages that are not to be installed
246+
for _, pkg := range d.configPackages() {
247+
if err := pkg.EnsureUninstallableIsInLockfile(); err != nil {
248+
return err
249+
}
250+
}
251+
231252
return d.lockfile.Save()
232253
}
233254

0 commit comments

Comments
 (0)