Skip to content

Commit 1c6f216

Browse files
committed
Merge branch 'main' into landau/fix-release
2 parents db26125 + 81dfaac commit 1c6f216

File tree

5 files changed

+151
-19
lines changed

5 files changed

+151
-19
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package featureflag
2+
3+
// ResolveV2 uses the /v2/resolve endpoint when resolving packages.
4+
var ResolveV2 = disable("RESOLVE_V2")

internal/lock/resolve.go

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"go.jetpack.io/devbox/internal/debug"
1717
"go.jetpack.io/devbox/internal/devpkg/pkgtype"
1818
"go.jetpack.io/devbox/internal/nix"
19+
"go.jetpack.io/devbox/internal/redact"
1920
"go.jetpack.io/devbox/internal/searcher"
2021
"golang.org/x/sync/errgroup"
2122
)
@@ -42,6 +43,9 @@ func (f *File) FetchResolvedPackage(pkg string) (*Package, error) {
4243
Version: ref.Version,
4344
}, nil
4445
}
46+
if featureflag.ResolveV2.Enabled() {
47+
return resolveV2(context.TODO(), name, version)
48+
}
4549

4650
packageVersion, err := searcher.Client().Resolve(name, version)
4751
if err != nil {
@@ -55,9 +59,9 @@ func (f *File) FetchResolvedPackage(pkg string) (*Package, error) {
5559
return nil, err
5660
}
5761
}
58-
packageInfo, err := selectForSystem(packageVersion)
62+
packageInfo, err := selectForSystem(packageVersion.Systems)
5963
if err != nil {
60-
return nil, err
64+
return nil, fmt.Errorf("no systems found for package %q", name)
6165
}
6266

6367
if len(packageInfo.AttrPaths) == 0 {
@@ -78,17 +82,45 @@ func (f *File) FetchResolvedPackage(pkg string) (*Package, error) {
7882
}, nil
7983
}
8084

81-
func selectForSystem(pkg *searcher.PackageVersion) (searcher.PackageInfo, error) {
82-
if pi, ok := pkg.Systems[nix.System()]; ok {
83-
return pi, nil
85+
func resolveV2(ctx context.Context, name, version string) (*Package, error) {
86+
resolved, err := searcher.Client().ResolveV2(ctx, name, version)
87+
if errors.Is(err, searcher.ErrNotFound) {
88+
return nil, redact.Errorf("%s@%s: %w", name, version, nix.ErrPackageNotFound)
89+
}
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
// /v2/resolve never returns a success with no systems.
95+
sysPkg, _ := selectForSystem(resolved.Systems)
96+
pkg := &Package{
97+
LastModified: sysPkg.LastUpdated.Format(time.RFC3339),
98+
Resolved: sysPkg.FlakeInstallable.String(),
99+
Source: devboxSearchSource,
100+
Version: resolved.Version,
101+
Systems: make(map[string]*SystemInfo, len(resolved.Systems)),
84102
}
85-
if pi, ok := pkg.Systems["x86_64-linux"]; ok {
86-
return pi, nil
103+
for sys, info := range resolved.Systems {
104+
if len(info.Outputs) != 0 {
105+
pkg.Systems[sys] = &SystemInfo{
106+
StorePath: info.Outputs[0].Path,
107+
}
108+
}
109+
}
110+
return pkg, nil
111+
}
112+
113+
func selectForSystem[V any](systems map[string]V) (v V, err error) {
114+
if v, ok := systems[nix.System()]; ok {
115+
return v, nil
116+
}
117+
if v, ok := systems["x86_64-linux"]; ok {
118+
return v, nil
87119
}
88-
for _, v := range pkg.Systems {
120+
for _, v := range systems {
89121
return v, nil
90122
}
91-
return searcher.PackageInfo{}, fmt.Errorf("no systems found for package %q", pkg.Name)
123+
return v, redact.Errorf("no systems found")
92124
}
93125

94126
func buildLockSystemInfos(pkg *searcher.PackageVersion) (map[string]*SystemInfo, error) {

internal/searcher/client.go

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package searcher
55

66
import (
7+
"context"
78
"encoding/json"
89
"fmt"
910
"io"
@@ -12,6 +13,7 @@ import (
1213

1314
"github.com/pkg/errors"
1415
"go.jetpack.io/devbox/internal/envir"
16+
"go.jetpack.io/devbox/internal/redact"
1517
)
1618

1719
const searchAPIEndpoint = "https://search.devbox.sh"
@@ -39,7 +41,7 @@ func (c *client) Search(query string) (*SearchResults, error) {
3941
}
4042
searchURL := endpoint + "?q=" + url.QueryEscape(query)
4143

42-
return execGet[SearchResults](searchURL)
44+
return execGet[SearchResults](context.TODO(), searchURL)
4345
}
4446

4547
// Resolve calls the /resolve endpoint of the search service. This returns
@@ -57,22 +59,57 @@ func (c *client) Resolve(name, version string) (*PackageVersion, error) {
5759
"?name=" + url.QueryEscape(name) +
5860
"&version=" + url.QueryEscape(version)
5961

60-
return execGet[PackageVersion](searchURL)
62+
return execGet[PackageVersion](context.TODO(), searchURL)
6163
}
6264

63-
func execGet[T any](url string) (*T, error) {
64-
response, err := http.Get(url)
65+
// Resolve calls the /resolve endpoint of the search service. This returns
66+
// the latest version of the package that matches the version constraint.
67+
func (c *client) ResolveV2(ctx context.Context, name, version string) (*ResolveResponse, error) {
68+
if name == "" {
69+
return nil, redact.Errorf("name is empty")
70+
}
71+
if version == "" {
72+
return nil, redact.Errorf("version is empty")
73+
}
74+
75+
endpoint, err := url.JoinPath(c.host, "v2/resolve")
6576
if err != nil {
66-
return nil, err
77+
return nil, redact.Errorf("invalid search endpoint host %q: %w", redact.Safe(c.host), redact.Safe(err))
78+
}
79+
searchURL := endpoint +
80+
"?name=" + url.QueryEscape(name) +
81+
"&version=" + url.QueryEscape(version)
82+
83+
return execGet[ResolveResponse](ctx, searchURL)
84+
}
85+
86+
func execGet[T any](ctx context.Context, url string) (*T, error) {
87+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
88+
if err != nil {
89+
return nil, redact.Errorf("GET %s: %w", redact.Safe(url), redact.Safe(err))
90+
}
91+
response, err := http.DefaultClient.Do(req)
92+
if err != nil {
93+
return nil, redact.Errorf("GET %s: %w", redact.Safe(url), redact.Safe(err))
6794
}
6895
defer response.Body.Close()
6996
data, err := io.ReadAll(response.Body)
7097
if err != nil {
71-
return nil, err
98+
return nil, redact.Errorf("GET %s: read respoonse body: %w", redact.Safe(url), redact.Safe(err))
7299
}
73100
if response.StatusCode == http.StatusNotFound {
74101
return nil, ErrNotFound
75102
}
103+
if response.StatusCode >= 400 {
104+
return nil, redact.Errorf("GET %s: unexpected status code %s: %s",
105+
redact.Safe(url),
106+
redact.Safe(response.Status),
107+
redact.Safe(data),
108+
)
109+
}
76110
var result T
77-
return &result, json.Unmarshal(data, &result)
111+
if err := json.Unmarshal(data, &result); err != nil {
112+
return nil, redact.Errorf("GET %s: unmarshal response JSON: %w", redact.Safe(url), redact.Safe(err))
113+
}
114+
return &result, nil
78115
}

internal/searcher/model.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33

44
package searcher
55

6+
import (
7+
"time"
8+
9+
"go.jetpack.io/devbox/nix/flake"
10+
)
11+
612
type SearchResults struct {
713
NumResults int `json:"num_results"`
814
Packages []Package `json:"packages,omitempty"`
@@ -35,3 +41,56 @@ type PackageInfo struct {
3541
Version string `json:"version"`
3642
Summary string `json:"summary"`
3743
}
44+
45+
// ResolveResponse is a response from the /v2/resolve endpoint.
46+
type ResolveResponse struct {
47+
// Name is the resolved name of the package. For packages that are
48+
// identifiable by multiple names or attribute paths, this is the
49+
// "canonical" name.
50+
Name string `json:"name"`
51+
52+
// Version is the resolved package version.
53+
Version string `json:"version"`
54+
55+
// Summary is a short package description.
56+
Summary string `json:"summary,omitempty"`
57+
58+
// Systems contains information about the package that can vary across
59+
// systems. It will always have at least one system. The keys match a
60+
// Nix system identifier (aarch64-darwin, x86_64-linux, etc.).
61+
Systems map[string]struct {
62+
// FlakeInstallable is a Nix installable that specifies how to
63+
// install the resolved package version.
64+
//
65+
// [Nix installable]: https://nixos.org/manual/nix/stable/command-ref/new-cli/nix#installables
66+
FlakeInstallable flake.Installable `json:"flake_installable"`
67+
68+
// LastUpdated is the timestamp of the most recent change to the
69+
// package.
70+
LastUpdated time.Time `json:"last_updated"`
71+
72+
// Outputs provides additional information about the Nix store
73+
// paths that this package installs. This field is not available
74+
// for some (especially older) packages.
75+
Outputs []struct {
76+
// Name is the output's name. Nix appends the name to
77+
// the output's store path unless it's the default name
78+
// of "out". Output names can be anything, but
79+
// conventionally they follow the various "make install"
80+
// directories such as "bin", "lib", "src", "man", etc.
81+
Name string `json:"name,omitempty"`
82+
83+
// Path is the absolute store path (with the /nix/store/
84+
// prefix) of the output.
85+
Path string `json:"path,omitempty"`
86+
87+
// Default indicates if Nix installs this output by
88+
// default.
89+
Default bool `json:"default,omitempty"`
90+
91+
// NAR is set to the package's NAR archive URL when the
92+
// output exists in the cache.nixos.org binary cache.
93+
NAR string `json:"nar,omitempty"`
94+
} `json:"outputs,omitempty"`
95+
} `json:"systems"`
96+
}

nix/flake/flakeref.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -456,11 +456,11 @@ const (
456456
// [Nix manual]: https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix#installables
457457
type Installable struct {
458458
// Ref is the flake reference portion of the installable.
459-
Ref Ref
459+
Ref Ref `json:"ref,omitempty"`
460460

461461
// AttrPath is an attribute path of the flake, encoded as a URL
462462
// fragment.
463-
AttrPath string
463+
AttrPath string `json:"attr_path,omitempty"`
464464

465465
// Outputs is the installable's output spec, which is a comma-separated
466466
// list of package outputs to install. The outputs spec is anything
@@ -474,7 +474,7 @@ type Installable struct {
474474
// ParseInstallable cleans the list of outputs by removing empty
475475
// elements and sorting the results. Lists containing a "*" are
476476
// simplified to a single "*".
477-
Outputs string
477+
Outputs string `json:"outputs,omitempty"`
478478
}
479479

480480
// ParseInstallable parses a flake installable. The raw string must contain

0 commit comments

Comments
 (0)