Skip to content

Commit b2a8835

Browse files
authored
[runx] Proof of concept for runx integration (#1524)
## Summary Inspired by golangci-lint being broken in nixpkgs, I wanted to see if I could get a quick and dirty implementation so we can replace golangci-lint from nixpkgs with runx. The current PR implements this using `runx:<path>` syntax. Since we already use `github:<path>` for nix flakes, I can't use that without breaking backwards compatibility. We could overload it. What this PR implements: * Add and rm of runx, with and without versions. * runx installed packages are added to PATH for run, shell, etc Things that are not implemented by this PR: * Search and validation. Currently trying to install a non-existing package will fail with a semi-cryptic error. * lockfile support. ## How was it tested? <img width="563" alt="image" src="https://github.com/jetpack-io/devbox/assets/544948/1c0a5521-a6f9-420f-9178-4fc23a1bc059">
1 parent 0d0de12 commit b2a8835

File tree

12 files changed

+164
-45
lines changed

12 files changed

+164
-45
lines changed

.github/workflows/cli-tests.yaml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,11 @@ jobs:
6868
go-version-file: ./go.mod
6969
cache: false
7070

71-
- name: Install devbox
72-
uses: jetpack-io/[email protected]
73-
with:
74-
enable-cache: true
71+
# This can be reanabled once released version supports runx
72+
# - name: Install devbox
73+
# uses: jetpack-io/[email protected]
74+
# with:
75+
# enable-cache: true
7576

7677
- name: Mount golang cache
7778
uses: actions/cache@v3
@@ -82,7 +83,8 @@ jobs:
8283
~/go/pkg
8384
key: go-${{ runner.os }}-${{ hashFiles('go.sum') }}
8485

85-
- run: devbox run fmt
86+
# Use main devbox for now to ensure it supports runx
87+
- run: go run ./cmd/devbox run fmt
8688

8789
- name: golangci-lint
8890
uses: golangci/[email protected]

devbox.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"packages": [
3-
4-
"go@latest"
3+
"go@latest",
4+
"runx:golangci/golangci-lint@latest"
55
],
66
"env": {
77
"GOENV": "off",
@@ -20,8 +20,8 @@
2020
"GOOS=linux GOARCH=arm64 go build -o dist/devbox-linux-arm64 ./cmd/devbox"
2121
],
2222
"code": "code .",
23-
"lint": "golangci-lint run",
2423
"fmt": "scripts/gofumpt.sh",
24+
"lint": "golangci-lint run",
2525
"test": "go test -race -cover ./...",
2626
"update-examples": "devbox run build && go run testscripts/testrunner/updater/main.go"
2727
}

devbox.lock

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,8 @@
2121
}
2222
}
2323
},
24-
25-
"last_modified": "2023-05-01T16:53:22Z",
26-
"resolved": "github:NixOS/nixpkgs/8670e496ffd093b60e74e7fa53526aa5920d09eb#golangci-lint",
27-
"version": "1.52.2",
28-
"systems": {
29-
"aarch64-darwin": {
30-
"store_path": "/nix/store/x2l9ljvl5fzffc7vggjk5yksrka9yra4-golangci-lint-1.52.2"
31-
},
32-
"aarch64-linux": {
33-
"store_path": "/nix/store/k905aab7fj7dvf2hhsbb3mmizk052ad7-golangci-lint-1.52.2"
34-
},
35-
"x86_64-darwin": {
36-
"store_path": "/nix/store/imxj7162whh9d56zpk1lzs1b3iw6wzp3-golangci-lint-1.52.2"
37-
},
38-
"x86_64-linux": {
39-
"store_path": "/nix/store/nfhrynpdjd5yamz5snv2c377v1j9jdmx-golangci-lint-1.52.2"
40-
}
41-
}
24+
"runx:golangci/golangci-lint@latest": {
25+
"resolved": "runx:golangci/golangci-lint@latest"
4226
}
4327
}
4428
}

internal/devpkg/package.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"go.jetpack.io/devbox/internal/boxcli/usererr"
2020
"go.jetpack.io/devbox/internal/cuecfg"
2121
"go.jetpack.io/devbox/internal/devconfig"
22+
"go.jetpack.io/devbox/internal/devpkg/pkgtype"
2223
"go.jetpack.io/devbox/internal/lock"
2324
"go.jetpack.io/devbox/internal/nix"
2425
"go.jetpack.io/devbox/plugins"
@@ -93,6 +94,9 @@ func newPackage(raw string, isInstallable bool, locker lock.Locker) *Package {
9394
normalizedURL += "#" + pkgURL.Fragment
9495
}
9596
pkgURL, _ = url.Parse(normalizedURL)
97+
} else if pkgURL.Scheme == pkgtype.RunXScheme {
98+
// THIS IS A HACK. These are not URLs and should not be treated as such
99+
pkgURL.Path = pkgURL.Opaque
96100
}
97101

98102
return &Package{URL: *pkgURL, lockfile: locker, Raw: raw, isInstallable: isInstallable}
@@ -107,11 +111,13 @@ func (p *Package) isLocal() bool {
107111
}
108112

109113
// IsDevboxPackage specifies whether this package is a devbox package. Devbox
110-
// packages have the format `canonicalName@version`and can be resolved by devbox
111-
// search. This also returns true for legacy packages which are just an
112-
// attribute path. An explicit flake reference is _not_ a devbox package.
114+
// packages have the format `canonicalName@version`and can be resolved by
115+
// lockfile.Resolve (including runx packages)
116+
// This also returns true for legacy packages which are just
117+
// an attribute path. An explicit flake reference is _not_ a devbox package.
118+
// TODO: Consider renaming to IsResolvable
113119
func (p *Package) IsDevboxPackage() bool {
114-
return p.Scheme == ""
120+
return p.Scheme == "" || p.IsRunX()
115121
}
116122

117123
// isGithub specifies whether this Package is referenced by a remote flake
@@ -401,7 +407,7 @@ func (p *Package) CanonicalName() string {
401407
if !p.IsDevboxPackage() {
402408
return ""
403409
}
404-
name, _, _ := strings.Cut(p.Path, "@")
410+
name, _, _ := strings.Cut(p.Raw, "@")
405411
return name
406412
}
407413

@@ -519,3 +525,23 @@ func (p *Package) EnsureUninstallableIsInLockfile() error {
519525
_, err := p.lockfile.Resolve(p.Raw)
520526
return err
521527
}
528+
529+
func (p *Package) IsRunX() bool {
530+
return pkgtype.IsRunX(p.Raw)
531+
}
532+
533+
func (p *Package) IsNix() bool {
534+
return IsNix(p, 0)
535+
}
536+
537+
func (p *Package) RunXPath() string {
538+
return strings.TrimPrefix(p.Raw, pkgtype.RunXPrefix)
539+
}
540+
541+
func IsNix(p *Package, _ int) bool {
542+
return !p.IsRunX()
543+
}
544+
545+
func IsRunX(p *Package, _ int) bool {
546+
return p.IsRunX()
547+
}

internal/devpkg/package_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,29 @@ func TestStorePathParts(t *testing.T) {
226226
})
227227
}
228228
}
229+
230+
func TestCanonicalName(t *testing.T) {
231+
tests := []struct {
232+
pkgName string
233+
expectedName string
234+
}{
235+
{"go", "go"},
236+
{"go@latest", "go"},
237+
{"[email protected]", "go"},
238+
{"runx:golangci/golangci-lint@latest", "runx:golangci/golangci-lint"},
239+
{"runx:golangci/[email protected]", "runx:golangci/golangci-lint"},
240+
{"runx:golangci/golangci-lint", "runx:golangci/golangci-lint"},
241+
{"github:NixOS/nixpkgs/12345", ""},
242+
{"path:/to/my/file", ""},
243+
}
244+
245+
for _, tt := range tests {
246+
t.Run(tt.pkgName, func(t *testing.T) {
247+
pkg := PackageFromString(tt.pkgName, nil)
248+
got := pkg.CanonicalName()
249+
if got != tt.expectedName {
250+
t.Errorf("Expected canonical name %q, but got %q", tt.expectedName, got)
251+
}
252+
})
253+
}
254+
}

internal/devpkg/pkgtype/runx.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package pkgtype
2+
3+
import "strings"
4+
5+
const (
6+
RunXScheme = "runx"
7+
RunXPrefix = RunXScheme + ":"
8+
)
9+
10+
func IsRunX(s string) bool {
11+
return strings.HasPrefix(s, RunXPrefix)
12+
}

internal/devpkg/validation.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import (
88
)
99

1010
func (p *Package) ValidateExists() (bool, error) {
11+
if p.IsRunX() {
12+
// TODO implement runx validation
13+
return true, nil
14+
}
1115
if p.isVersioned() && p.version() == "" {
1216
return false, usererr.New("No version specified for %q.", p.Path)
1317
}

internal/impl/devbox.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"go.jetpack.io/devbox/internal/searcher"
2828
"go.jetpack.io/devbox/internal/shellgen"
2929
"go.jetpack.io/devbox/internal/telemetry"
30+
"go.jetpack.io/pkg/sandbox/runx"
3031

3132
"go.jetpack.io/devbox/internal/boxcli/usererr"
3233
"go.jetpack.io/devbox/internal/cmdutil"
@@ -747,6 +748,7 @@ func (d *Devbox) StartProcessManager(
747748
// Note that the shellrc.tmpl template (which sources this environment) does
748749
// some additional processing. The computeNixEnv environment won't necessarily
749750
// represent the final "devbox run" or "devbox shell" environments.
751+
// TODO: Rename to computeDevboxEnv?
750752
func (d *Devbox) computeNixEnv(ctx context.Context, usePrintDevEnvCache bool) (map[string]string, error) {
751753
defer trace.StartRegion(ctx, "computeNixEnv").End()
752754

@@ -874,6 +876,12 @@ func (d *Devbox) computeNixEnv(ctx context.Context, usePrintDevEnvCache bool) (m
874876
})
875877
debug.Log("PATH after filtering with buildInputs (%v) is: %s", buildInputs, nixEnvPath)
876878

879+
runXPaths, err := d.RunXPaths()
880+
if err != nil {
881+
return nil, err
882+
}
883+
nixEnvPath = envpath.JoinPathLists(nixEnvPath, runXPaths)
884+
877885
pathStack := envpath.Stack(env, originalEnv)
878886
pathStack.Push(env, d.projectDirHash(), nixEnvPath, d.preservePathStack)
879887
env["PATH"] = pathStack.Path(env)
@@ -978,6 +986,9 @@ func (d *Devbox) HasDeprecatedPackages() bool {
978986
}
979987

980988
func (d *Devbox) findPackageByName(name string) (*devpkg.Package, error) {
989+
if name == "" {
990+
return nil, errors.New("package name cannot be empty")
991+
}
981992
results := map[*devpkg.Package]bool{}
982993
for _, pkg := range d.configPackages() {
983994
if pkg.Raw == name || pkg.CanonicalName() == name {
@@ -1194,3 +1205,16 @@ func (d *Devbox) PluginManager() *plugin.Manager {
11941205
func (d *Devbox) Lockfile() *lock.File {
11951206
return d.lockfile
11961207
}
1208+
1209+
func (d *Devbox) RunXPaths() (string, error) {
1210+
packages := lo.Filter(d.InstallablePackages(), devpkg.IsRunX)
1211+
paths := []string{}
1212+
for _, pkg := range packages {
1213+
p, err := runx.Install(pkg.RunXPath())
1214+
if err != nil {
1215+
return "", err
1216+
}
1217+
paths = append(paths, p...)
1218+
}
1219+
return envpath.JoinPathLists(paths...), nil
1220+
}

internal/impl/packages.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"go.jetpack.io/devbox/internal/devpkg"
1919
"go.jetpack.io/devbox/internal/nix/nixprofile"
2020
"go.jetpack.io/devbox/internal/shellgen"
21+
"go.jetpack.io/pkg/sandbox/runx"
2122

2223
"go.jetpack.io/devbox/internal/boxcli/usererr"
2324
"go.jetpack.io/devbox/internal/debug"
@@ -66,6 +67,7 @@ func (d *Devbox) Add(ctx context.Context, platforms, excludePlatforms []string,
6667
// match.
6768
found, _ := d.findPackageByName(pkg.CanonicalName())
6869
if found != nil {
70+
ux.Finfo(d.stderr, "Replacing package %q in devbox.json\n", found.Raw)
6971
if err := d.Remove(ctx, found.Raw); err != nil {
7072
return err
7173
}
@@ -234,6 +236,10 @@ func (d *Devbox) ensurePackagesAreInstalled(ctx context.Context, mode installMod
234236
return err
235237
}
236238

239+
if err := d.InstallRunXPackages(); err != nil {
240+
return err
241+
}
242+
237243
if err := shellgen.GenerateForPrintEnv(ctx, d); err != nil {
238244
return err
239245
}
@@ -307,6 +313,9 @@ func (d *Devbox) syncPackagesToProfile(ctx context.Context, mode installMode) er
307313
return err
308314
}
309315

316+
// Remove non-nix packages from the list
317+
packages = lo.Filter(packages, devpkg.IsNix)
318+
310319
if err := devpkg.FillNarInfoCache(ctx, packages...); err != nil {
311320
return err
312321
}
@@ -407,7 +416,7 @@ func (d *Devbox) addPackagesToProfile(ctx context.Context, pkgs []*devpkg.Packag
407416

408417
profileDir, err := d.profilePath()
409418
if err != nil {
410-
return err
419+
return fmt.Errorf("error getting profile path: %w", err)
411420
}
412421

413422
total := len(pkgs)
@@ -416,14 +425,14 @@ func (d *Devbox) addPackagesToProfile(ctx context.Context, pkgs []*devpkg.Packag
416425

417426
stepMsg := fmt.Sprintf("[%d/%d] %s", stepNum, total, pkg)
418427

419-
if err := nixprofile.ProfileInstall(ctx, &nixprofile.ProfileInstallArgs{
428+
if err = nixprofile.ProfileInstall(ctx, &nixprofile.ProfileInstallArgs{
420429
CustomStepMessage: stepMsg,
421430
Lockfile: d.lockfile,
422431
Package: pkg.Raw,
423432
ProfilePath: profileDir,
424433
Writer: d.stderr,
425434
}); err != nil {
426-
return err
435+
return fmt.Errorf("error installing package %s: %w", pkg, err)
427436
}
428437
}
429438

@@ -463,3 +472,20 @@ func resetProfileDirForFlakes(profileDir string) (err error) {
463472

464473
return errors.WithStack(os.Remove(profileDir))
465474
}
475+
476+
func (d *Devbox) InstallRunXPackages() error {
477+
for _, pkg := range d.InstallablePackages() {
478+
if pkg.IsRunX() {
479+
// TODO: Once resolve is implemented, we use whatever version is in the lockfile.
480+
if _, err := d.lockfile.Resolve(pkg.Raw); err != nil {
481+
return err
482+
}
483+
_, err := runx.Install(pkg.RunXPath())
484+
if err != nil {
485+
return fmt.Errorf("error installing runx package %s: %w", pkg, err)
486+
}
487+
488+
}
489+
}
490+
return nil
491+
}

internal/lock/lockfile.go

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

1212
"github.com/pkg/errors"
1313
"github.com/samber/lo"
14+
"go.jetpack.io/devbox/internal/devpkg/pkgtype"
1415
"go.jetpack.io/devbox/internal/searcher"
1516

1617
"go.jetpack.io/devbox/internal/cuecfg"
@@ -69,7 +70,12 @@ func (f *File) Resolve(pkg string) (*Package, error) {
6970
if !hasEntry || entry.Resolved == "" {
7071
locked := &Package{}
7172
var err error
72-
if _, _, versioned := searcher.ParseVersionedPackage(pkg); versioned {
73+
if pkgtype.IsRunX(pkg) {
74+
// TODO implement runx resolution. This can be done by reading the releases.json file
75+
locked = &Package{
76+
Resolved: pkg,
77+
}
78+
} else if _, _, versioned := searcher.ParseVersionedPackage(pkg); versioned {
7379
locked, err = f.FetchResolvedPackage(pkg)
7480
if err != nil {
7581
return nil, err

internal/nix/search.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import (
55
"fmt"
66
"os"
77
"os/exec"
8+
"strings"
89

910
"github.com/pkg/errors"
10-
"go.jetpack.io/devbox/internal/boxcli/usererr"
1111
"go.jetpack.io/devbox/internal/debug"
1212
)
1313

@@ -30,6 +30,10 @@ func (i *Info) String() string {
3030
}
3131

3232
func Search(url string) (map[string]*Info, error) {
33+
if strings.HasPrefix(url, "runx:") {
34+
// TODO implement runx search
35+
return map[string]*Info{}, nil
36+
}
3337
return searchSystem(url, "")
3438
}
3539

@@ -98,7 +102,7 @@ func searchSystem(url, system string) (map[string]*Info, error) {
98102
out, err := cmd.Output()
99103
if err != nil {
100104
// for now, assume all errors are invalid packages.
101-
return nil, usererr.NewExecError(err)
105+
return nil, fmt.Errorf("error searching for pkg %s: %w", url, err)
102106
}
103107
return parseSearchResults(out), nil
104108
}

0 commit comments

Comments
 (0)