Skip to content

Commit 1398df1

Browse files
committed
[Config Packages] Add platforms and excluded_platforms functionality
1 parent 819e76f commit 1398df1

File tree

7 files changed

+235
-11
lines changed

7 files changed

+235
-11
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: 117 additions & 4 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

@@ -28,6 +32,11 @@ type Packages struct {
2832
Collection []Package `json:"-,omitempty"`
2933
}
3034

35+
// TODO savil: rm
36+
func (pkgs *Packages) Kind() int {
37+
return int(pkgs.jsonKind)
38+
}
39+
3140
// VersionedNames returns a list of package names with versions.
3241
// NOTE: if the package is unversioned, the version will be omitted (doesn't default to @latest).
3342
//
@@ -55,6 +64,84 @@ func (pkgs *Packages) Remove(versionedName string) {
5564
})
5665
}
5766

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

60147
// First, attempt to unmarshal as a list of strings (legacy format)
@@ -123,7 +210,8 @@ type Package struct {
123210
// deliberately not adding omitempty
124211
Version string `json:"version"`
125212

126-
// TODO: add other fields like platforms
213+
Platforms []string `json:"platforms,omitempty"`
214+
ExcludedPlatforms []string `json:"excluded_platforms,omitempty"`
127215
}
128216

129217
func NewVersionOnlyPackage(name, version string) Package {
@@ -143,13 +231,34 @@ func NewPackage(name string, values map[string]any) Package {
143231
version = ""
144232
}
145233

234+
var platforms []string
235+
if p, ok := values["platforms"]; ok {
236+
platforms = p.([]string)
237+
}
238+
var excludedPlatforms []string
239+
if e, ok := values["excluded_platforms"]; ok {
240+
excludedPlatforms = e.([]string)
241+
}
242+
146243
return Package{
147-
kind: regular,
148-
name: name,
149-
Version: version.(string),
244+
kind: regular,
245+
name: name,
246+
Version: version.(string),
247+
Platforms: platforms,
248+
ExcludedPlatforms: excludedPlatforms,
150249
}
151250
}
152251

252+
// TODO savil: rm
253+
func (p *Package) Name() string {
254+
return p.name
255+
}
256+
257+
// TODO savil: rm
258+
func (p *Package) Kind() int {
259+
return int(p.kind)
260+
}
261+
153262
func (p *Package) VersionedName() string {
154263
name := p.name
155264
if p.Version != "" {
@@ -181,6 +290,10 @@ func (p *Package) UnmarshalJSON(data []byte) error {
181290

182291
*p = Package(*alias)
183292
p.kind = regular
293+
294+
// TODO savil. needed?
295+
//p.Platforms = alias.Platforms
296+
//p.ExcludedPlatforms = alias.ExcludedPlatforms
184297
return nil
185298
}
186299

internal/devconfig/packages_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,50 @@ 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+
},
77121
}
78122

79123
for _, testCase := range testCases {
@@ -165,3 +209,24 @@ func TestParseVersionedName(t *testing.T) {
165209
})
166210
}
167211
}
212+
213+
func TestConvertToKind(t *testing.T) {
214+
testCase := Packages{
215+
jsonKind: jsonList,
216+
Collection: packagesFromLegacyList([]string{"python", "hello@latest", "[email protected]"}),
217+
}
218+
219+
expected := Packages{
220+
jsonKind: jsonMap,
221+
Collection: []Package{
222+
NewVersionOnlyPackage("python", "" /*version*/),
223+
NewVersionOnlyPackage("hello", "latest"),
224+
NewVersionOnlyPackage("go", "1.20"),
225+
},
226+
}
227+
228+
testCase.convertToKind(jsonMap)
229+
if !reflect.DeepEqual(testCase, expected) {
230+
t.Errorf("expected: %+v, got: %+v", expected, testCase)
231+
}
232+
}

internal/impl/packages.go

Lines changed: 16 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

@@ -47,6 +48,7 @@ func (d *Devbox) Add(ctx context.Context, pkgsNames ...string) error {
4748
addedPackageNames := []string{}
4849
existingPackageNames := d.ConfigPackageNames()
4950
for _, pkg := range pkgs {
51+
5052
// If exact versioned package is already in the config, skip.
5153
if slices.Contains(existingPackageNames, pkg.Versioned()) {
5254
addedPackageNames = append(addedPackageNames, pkg.Versioned())
@@ -87,6 +89,19 @@ func (d *Devbox) Add(ctx context.Context, pkgsNames ...string) error {
8789
addedPackageNames = append(addedPackageNames, packageNameForConfig)
8890
}
8991

92+
for _, pkg := range addedPackageNames {
93+
if platform != "" {
94+
if err := d.cfg.Packages.AddPlatform(pkg, platform); err != nil {
95+
return err
96+
}
97+
}
98+
if excludePlatform != "" {
99+
if err := d.cfg.Packages.ExcludePlatform(pkg, excludePlatform); err != nil {
100+
return err
101+
}
102+
}
103+
}
104+
90105
// Resolving here ensures we allow insecure before running ensurePackagesAreInstalled
91106
// which will call print-dev-env. Resolving does not save the lockfile, we
92107
// save at the end when everything has succeeded.

internal/impl/update.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func (d *Devbox) Update(ctx context.Context, pkgs ...string) error {
3434
// Calling Add function with the original package names, since
3535
// Add will automatically append @latest if search is able to handle that.
3636
// If not, it will fallback to the nixpkg format.
37-
if err := d.Add(ctx, pkg.Raw); err != nil {
37+
if err := d.Add(ctx, "" /*platform*/, "" /*excludePlatform*/, pkg.Raw); err != nil {
3838
return err
3939
}
4040
} else {

internal/nix/nix.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ func ExperimentalFlags() []string {
120120

121121
var cachedSystem string
122122

123+
// TODO: rename to Platform?
123124
func System() (string, error) {
124125
// For Savil to debug "remove nixpkgs" feature. The Search api lacks x86-darwin info.
125126
// So, I need to fake that I am x86-linux and inspect the output in generated devbox.lock
@@ -169,6 +170,28 @@ func Version() (string, error) {
169170
return version, nil
170171
}
171172

173+
var nixPlatforms = []string{
174+
"aarch64-darwin",
175+
"aarch64-linux",
176+
"i686-linux",
177+
"x86_64-darwin",
178+
"x86_64-linux",
179+
// not technically supported, but should work?
180+
// ref.
181+
"armv7l-linux",
182+
}
183+
184+
// EnsureValidPlatform returns an error if the platform is not supported by nix.
185+
// https://nixos.org/manual/nix/stable/installation/supported-platforms.html
186+
func EnsureValidPlatform(platform string) error {
187+
for _, p := range nixPlatforms {
188+
if p == platform {
189+
return nil
190+
}
191+
}
192+
return usererr.New("Unsupported platform: %s. Valid platforms are: %v", platform, nixPlatforms)
193+
}
194+
172195
// Warning: be careful using the bins in default/bin, they won't always match bins
173196
// produced by the flakes.nix. Use devbox.NixBins() instead.
174197
func ProfileBinPath(projectDir string) string {

0 commit comments

Comments
 (0)