Skip to content

Commit f7bb72b

Browse files
committed
devpkg: add --patch and deprecate --patch-glibc
Add a `devbox add --patch <mode>` flag that replaces `--patch-glibc` and a corresponding `patch` JSON field that replaces `patch_glibc`. The new name reflects the new patching behavior, which affects more than just glibc. The new patch flag/field is a string instead of a bool. Valid values are `auto`, `always` and `never`. The default is `auto`. devbox add --patch <auto/always/never> - `auto` - let Devbox decide if a package should be patched. Currently only enables patching for Python or if `patch_glibc` is true in the config. - `always` - always attempt to patch a package. Corresponds to the `--patch-glibc=true` behavior. - `never` - never patch a package, even if `auto` would. If a config has an existing package with the `"patch_glibc": true` field, it's interpreted as `"patch": "always"` but the config itself isn't modified. However, if the user runs a command that does write to the config, then `patch_glibc` will be migrated to `patch`. Example `devbox.json`: ```json5 { "packages": { "ruby": { "version": "latest", "patch": "always" } "python": { "version": "latest" // No patch field implies "auto". // "patch": "auto" } } } ```
1 parent 6881453 commit f7bb72b

File tree

7 files changed

+177
-34
lines changed

7 files changed

+177
-34
lines changed

.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ linters-settings:
7070
- m map[string]int
7171
- n int
7272
- ns string
73+
- ok bool
7374
- r *http.Request
7475
- r io.Reader
7576
- r *os.File

internal/boxcli/add.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type addCmdFlags struct {
2424
platforms []string
2525
excludePlatforms []string
2626
patchGlibc bool
27+
patch string
2728
outputs []string
2829
}
2930

@@ -68,10 +69,16 @@ func addCmd() *cobra.Command {
6869
command.Flags().BoolVar(
6970
&flags.patchGlibc, "patch-glibc", false,
7071
"patch any ELF binaries to use the latest glibc version in nixpkgs")
72+
command.Flags().StringVar(
73+
&flags.patch, "patch", "auto",
74+
"allow Devbox to patch the package to fix known issues (auto, always, never)")
7175
command.Flags().StringSliceVarP(
7276
&flags.outputs, "outputs", "o", []string{},
7377
"specify the outputs to select for the nix package")
7478

79+
_ = command.Flags().MarkDeprecated("patch-glibc", `use --patch=always instead`)
80+
command.MarkFlagsMutuallyExclusive("patch", "patch-glibc")
81+
7582
return command
7683
}
7784

@@ -85,12 +92,17 @@ func addCmdFunc(cmd *cobra.Command, args []string, flags addCmdFlags) error {
8592
return errors.WithStack(err)
8693
}
8794

88-
return box.Add(cmd.Context(), args, devopt.AddOpts{
95+
opts := devopt.AddOpts{
8996
AllowInsecure: flags.allowInsecure,
9097
DisablePlugin: flags.disablePlugin,
9198
Platforms: flags.platforms,
9299
ExcludePlatforms: flags.excludePlatforms,
93-
PatchGlibc: flags.patchGlibc,
100+
Patch: flags.patch,
94101
Outputs: flags.outputs,
95-
})
102+
}
103+
if flags.patchGlibc {
104+
// Backwards compatibility so --patch-glibc still works.
105+
opts.Patch = "always"
106+
}
107+
return box.Add(cmd.Context(), args, opts)
96108
}

internal/devbox/devopt/devboxopts.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ type AddOpts struct {
5151
Platforms []string
5252
ExcludePlatforms []string
5353
DisablePlugin bool
54-
PatchGlibc bool
54+
Patch string
5555
Outputs []string
5656
}
5757

internal/devbox/packages.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ func (d *Devbox) setPackageOptions(pkgs []string, opts devopt.AddOpts) error {
142142
pkg, opts.DisablePlugin); err != nil {
143143
return err
144144
}
145-
if err := d.cfg.PackageMutator().SetPatchGLibc(
146-
pkg, opts.PatchGlibc); err != nil {
145+
if err := d.cfg.PackageMutator().SetPatch(
146+
pkg, configfile.PatchMode(opts.Patch)); err != nil {
147147
return err
148148
}
149149
if err := d.cfg.PackageMutator().SetOutputs(

internal/devconfig/configfile/ast.go

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ func (c *configAST) removePackageElement(arr *hujson.Array, name string) {
173173

174174
// setPackageBool sets a bool field on a package.
175175
func (c *configAST) setPackageBool(name, fieldName string, val bool) {
176-
pkgObject := c.FindPkgObject(name)
176+
pkgObject := c.findPkgObject(name)
177177
if pkgObject == nil {
178178
return
179179
}
@@ -216,7 +216,72 @@ func (c *configAST) appendAllowInsecure(name, fieldName string, whitelist []stri
216216
c.appendStringSliceField(name, fieldName, whitelist)
217217
}
218218

219-
func (c *configAST) FindPkgObject(name string) *hujson.Object {
219+
// removePatch removes the patch field from the named package.
220+
func (c *configAST) removePatch(name string) {
221+
pkgs := c.packagesField(false)
222+
obj, ok := pkgs.Value.Value.(*hujson.Object)
223+
if !ok {
224+
// Packages field is an array.
225+
return
226+
}
227+
i := c.memberIndex(obj, name)
228+
if i == -1 {
229+
// Package not found.
230+
return
231+
}
232+
233+
obj, ok = obj.Members[i].Value.Value.(*hujson.Object)
234+
if !ok {
235+
// Package is a string, not an object.
236+
return
237+
}
238+
i = c.memberIndex(obj, "patch")
239+
if i == -1 {
240+
// Patch field doesn't exist.
241+
return
242+
}
243+
244+
obj.Members = slices.Delete(obj.Members, i, i+1)
245+
c.root.Format()
246+
}
247+
248+
// setPatch sets the patch field of the named package.
249+
func (c *configAST) setPatch(name string, mode PatchMode) {
250+
pkgObject := c.findPkgObject(name)
251+
if pkgObject == nil {
252+
return
253+
}
254+
255+
glibcIndex := c.memberIndex(pkgObject, "patch_glibc") // deprecated
256+
patchIndex := c.memberIndex(pkgObject, "patch")
257+
switch {
258+
// Neither patch_glibc or patch exist - append a new field.
259+
case patchIndex == -1 && glibcIndex == -1:
260+
pkgObject.Members = append(pkgObject.Members, hujson.ObjectMember{
261+
Name: hujson.Value{
262+
BeforeExtra: []byte{'\n'},
263+
},
264+
})
265+
patchIndex = len(pkgObject.Members) - 1
266+
defer c.root.Format()
267+
// patch_glibc exists and patch doesn't - rename patch_glibc to
268+
// preserve formatting/comments.
269+
case patchIndex == -1 && glibcIndex != -1:
270+
patchIndex = glibcIndex
271+
// Both patch_glibc and patch exist - delete patch_glibc.
272+
case patchIndex != -1 && glibcIndex != -1:
273+
pkgObject.Members = slices.Delete(pkgObject.Members, glibcIndex, glibcIndex+1)
274+
if patchIndex > glibcIndex {
275+
patchIndex--
276+
}
277+
defer c.root.Format()
278+
}
279+
280+
pkgObject.Members[patchIndex].Name.Value = hujson.String("patch")
281+
pkgObject.Members[patchIndex].Value.Value = hujson.String(string(mode))
282+
}
283+
284+
func (c *configAST) findPkgObject(name string) *hujson.Object {
220285
pkgs := c.packagesField(true).Value.Value.(*hujson.Object)
221286
i := c.memberIndex(pkgs, name)
222287
if i == -1 {
@@ -301,7 +366,7 @@ func joinNameVersion(name, version string) string {
301366
}
302367

303368
func (c *configAST) appendStringSliceField(name, fieldName string, fieldValues []string) {
304-
pkgObject := c.FindPkgObject(name)
369+
pkgObject := c.findPkgObject(name)
305370
if pkgObject == nil {
306371
return
307372
}

internal/devconfig/configfile/packages.go

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package configfile
33

44
import (
55
"encoding/json"
6+
"fmt"
67
"io"
78
"slices"
89
"strings"
@@ -154,15 +155,24 @@ func (pkgs *PackagesMutator) UnmarshalJSON(data []byte) error {
154155
return nil
155156
}
156157

157-
func (pkgs *PackagesMutator) SetPatchGLibc(versionedName string, v bool) error {
158+
func (pkgs *PackagesMutator) SetPatch(versionedName string, mode PatchMode) error {
159+
if err := mode.validate(); err != nil {
160+
return fmt.Errorf("set patch field for %s: %v", versionedName, err)
161+
}
162+
158163
name, version := parseVersionedName(versionedName)
159164
i := pkgs.index(name, version)
160165
if i == -1 {
161166
return errors.Errorf("package %s not found", versionedName)
162167
}
163-
if pkgs.collection[i].PatchGlibc != v {
164-
pkgs.collection[i].PatchGlibc = v
165-
pkgs.ast.setPackageBool(name, "patch_glibc", v)
168+
169+
pkgs.collection[i].PatchGlibc = false
170+
pkgs.collection[i].Patch = mode
171+
if mode == PatchAuto {
172+
// PatchAuto is the default behavior, so just remove the field.
173+
pkgs.ast.removePatch(name)
174+
} else {
175+
pkgs.ast.setPatch(name, mode)
166176
}
167177
return nil
168178
}
@@ -231,6 +241,34 @@ func (pkgs *PackagesMutator) index(name, version string) int {
231241
})
232242
}
233243

244+
// PatchMode specifies when to patch packages.
245+
type PatchMode string
246+
247+
const (
248+
// PatchAuto automatically applies patches to fix known issues with
249+
// certain packages. It is the default behavior when the config doesn't
250+
// specify a patching mode.
251+
PatchAuto PatchMode = "auto"
252+
253+
// PatchAlways always applies patches to a package, overriding the
254+
// default behavior of PatchAuto. It might cause problems with untested
255+
// packages.
256+
PatchAlways PatchMode = "always"
257+
258+
// PatchNever disables all patching for a package.
259+
PatchNever PatchMode = "never"
260+
)
261+
262+
func (p PatchMode) validate() error {
263+
switch p {
264+
case PatchAuto, PatchAlways, PatchNever:
265+
return nil
266+
default:
267+
return fmt.Errorf("invalid patch mode %q (must be %s, %s or %s)",
268+
p, PatchAuto, PatchAlways, PatchNever)
269+
}
270+
}
271+
234272
type Package struct {
235273
Name string
236274
Version string `json:"version,omitempty"`
@@ -241,8 +279,14 @@ type Package struct {
241279

242280
// PatchGlibc applies a function to the package's derivation that
243281
// patches any ELF binaries to use the latest version of nixpkgs#glibc.
282+
//
283+
// Deprecated: Use Patch instead, which also patches glibc.
244284
PatchGlibc bool `json:"patch_glibc,omitempty"`
245285

286+
// Patch controls when to patch the package. If empty, it defaults to
287+
// [PatchAuto].
288+
Patch PatchMode `json:"patch,omitempty"`
289+
246290
// Outputs is the list of outputs to use for this package, assuming
247291
// it is a nix package. If empty, the default output is used.
248292
Outputs []string `json:"outputs,omitempty"`
@@ -291,21 +335,28 @@ func (p *Package) VersionedName() string {
291335

292336
func (p *Package) UnmarshalJSON(data []byte) error {
293337
// First, attempt to unmarshal as a version-only string
294-
var version string
295-
if err := json.Unmarshal(data, &version); err == nil {
296-
p.Version = version
297-
return nil
298-
}
299-
300-
// Second, attempt to unmarshal as a Package struct
301-
type packageAlias Package // Use an alias-type to avoid infinite recursion
302-
alias := &packageAlias{}
303-
if err := json.Unmarshal(data, alias); err != nil {
304-
return errors.WithStack(err)
338+
if err := json.Unmarshal(data, &p.Version); err != nil {
339+
// Second, attempt to unmarshal as a Package struct
340+
type packageAlias Package // Use an alias-type to avoid infinite recursion
341+
alias := &packageAlias{}
342+
if err := json.Unmarshal(data, alias); err != nil {
343+
return errors.WithStack(err)
344+
}
345+
*p = Package(*alias)
346+
}
347+
348+
if p.Patch == "" {
349+
if p.PatchGlibc {
350+
// Force patching if the user has an old config with the deprecated
351+
// patch_glibc field set to true.
352+
p.Patch = PatchAlways
353+
} else {
354+
// Default to PatchAuto if the field is missing, null,
355+
// or empty.
356+
p.Patch = PatchAuto
357+
}
305358
}
306-
307-
*p = Package(*alias)
308-
return nil
359+
return p.Patch.validate()
309360
}
310361

311362
// parseVersionedName parses the name and version from package@version representation

internal/devpkg/package.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ type Package struct {
8181
// Outputs is a list of outputs to build from the package's derivation.
8282
outputs outputs
8383

84-
// patchGlibc applies a function to the package's derivation that
84+
// patch applies a function to the package's derivation that
8585
// patches any ELF binaries to use the latest version of nixpkgs#glibc.
8686
// It's a function to allow deferring nix System call until it's needed.
87-
patchGlibc func() bool
87+
patch func() bool
8888

8989
// AllowInsecure are a list of nix packages that are whitelisted to be
9090
// installed even if they are marked as insecure.
@@ -110,9 +110,7 @@ func PackagesFromConfig(packages []configfile.Package, l lock.Locker) []*Package
110110
for _, cfgPkg := range packages {
111111
pkg := newPackage(cfgPkg.VersionedName(), cfgPkg.IsEnabledOnPlatform, l)
112112
pkg.DisablePlugin = cfgPkg.DisablePlugin
113-
pkg.patchGlibc = sync.OnceValue(func() bool {
114-
return cfgPkg.PatchGlibc && nix.SystemIsLinux()
115-
})
113+
pkg.patch = patchGlibcFunc(pkg.CanonicalName(), cfgPkg.Patch)
116114
pkg.outputs.selectedNames = lo.Uniq(append(pkg.outputs.selectedNames, cfgPkg.Outputs...))
117115
pkg.AllowInsecure = cfgPkg.AllowInsecure
118116
result = append(result, pkg)
@@ -127,7 +125,7 @@ func PackageFromStringWithDefaults(raw string, locker lock.Locker) *Package {
127125
func PackageFromStringWithOptions(raw string, locker lock.Locker, opts devopt.AddOpts) *Package {
128126
pkg := PackageFromStringWithDefaults(raw, locker)
129127
pkg.DisablePlugin = opts.DisablePlugin
130-
pkg.patchGlibc = sync.OnceValue(func() bool { return opts.PatchGlibc })
128+
pkg.patch = patchGlibcFunc(pkg.CanonicalName(), configfile.PatchMode(opts.Patch))
131129
pkg.outputs.selectedNames = lo.Uniq(append(pkg.outputs.selectedNames, opts.Outputs...))
132130
pkg.AllowInsecure = opts.AllowInsecure
133131
return pkg
@@ -179,6 +177,22 @@ func resolve(pkg *Package) error {
179177
return nil
180178
}
181179

180+
func patchGlibcFunc(canonicalName string, mode configfile.PatchMode) func() bool {
181+
return sync.OnceValue(func() (patch bool) {
182+
switch mode {
183+
case configfile.PatchAuto:
184+
patch = canonicalName == "python"
185+
case configfile.PatchAlways:
186+
patch = true
187+
case configfile.PatchNever:
188+
patch = false
189+
}
190+
191+
// Check nix.SystemIsLinux() last because it's slow.
192+
return patch && nix.SystemIsLinux()
193+
})
194+
}
195+
182196
func (p *Package) setInstallable(i flake.Installable, projectDir string) {
183197
if i.Ref.Type == flake.TypePath && !filepath.IsAbs(i.Ref.Path) {
184198
i.Ref.Path = filepath.Join(projectDir, i.Ref.Path)
@@ -238,7 +252,7 @@ func (p *Package) IsInstallable() bool {
238252
}
239253

240254
func (p *Package) PatchGlibc() bool {
241-
return p.patchGlibc != nil && p.patchGlibc()
255+
return p.patch != nil && p.patch()
242256
}
243257

244258
// Installables for this package. Installables is a nix concept defined here:

0 commit comments

Comments
 (0)