Skip to content

Commit

Permalink
[nix] Fix StorePathsAreInStore (#2104)
Browse files Browse the repository at this point in the history
## Summary

Addresses issue introduced in 

#2076
and
#2098

Basically we have 2 different uses cases when looking up store paths.
Sometimes we want all store paths (whether installed or not) and in
other cases we only want paths that are in local store. This change
fixes that and adds some testing.

## How was it tested?

unit tests, manual integration test by installing a package that was not
in local store.
  • Loading branch information
mikeland73 committed May 30, 2024
1 parent 01c0417 commit b95f4cd
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 35 deletions.
47 changes: 21 additions & 26 deletions internal/nix/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ func StorePathsFromInstallable(ctx context.Context, installable string, allowIns
return nil, err
}

validPaths, err := parseStorePathFromInstallableOutput(resultBytes)
paths, err := parseStorePathFromInstallableOutput(resultBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse path-info for %s: %w", installable, err)
}

return maps.Keys(validPaths), nil
return maps.Keys(paths), nil
}

// StorePathsAreInStore a map of store paths to whether they are in the store.
Expand All @@ -71,44 +71,39 @@ func StorePathsAreInStore(ctx context.Context, storePaths []string) (map[string]
return nil, err
}

validPaths, err := parseStorePathFromInstallableOutput(output)
if err != nil {
return nil, err
}

result := map[string]bool{}
for _, storePath := range storePaths {
_, ok := validPaths[storePath]
result[storePath] = ok
}

return result, nil
return parseStorePathFromInstallableOutput(output)
}

// Older nix versions (like 2.17) are an array of objects that contain path and valid fields
type pathInfoLegacy struct {
Path string `json:"path"`
type LegacyPathInfo struct {
Path string `json:"path"`
Valid bool `json:"valid"` // this means path is in store
}

// parseStorePathFromInstallableOutput parses the output of `nix store path-from-installable --json`
// into a map of store paths to whether they are in the store.
// This function is decomposed out of StorePathFromInstallable to make it testable.
func parseStorePathFromInstallableOutput(output []byte) (map[string]any, error) {
func parseStorePathFromInstallableOutput(output []byte) (map[string]bool, error) {
result := map[string]bool{}

// Newer nix versions (like 2.20) have output of the form
// {"<store-path>": {}}
// Note that values will be null if paths are not in store.
var out1 map[string]any
if err := json.Unmarshal(output, &out1); err == nil {
return out1, nil
var modernPathInfo map[string]any
if err := json.Unmarshal(output, &modernPathInfo); err == nil {
for path, val := range modernPathInfo {
result[path] = val != nil
}
return result, nil
}

var out2 []pathInfoLegacy
var legacyPathInfos []LegacyPathInfo

if err := json.Unmarshal(output, &out2); err == nil {
res := map[string]any{}
for _, outValue := range out2 {
res[outValue.Path] = true
if err := json.Unmarshal(output, &legacyPathInfos); err == nil {
for _, outValue := range legacyPathInfos {
result[outValue.Path] = outValue.Valid
}
return res, nil
return result, nil
}

return nil, fmt.Errorf("failed to parse path-info output: %s", output)
Expand Down
36 changes: 27 additions & 9 deletions internal/nix/store_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package nix

import (
"slices"
"testing"

"golang.org/x/exp/maps"
Expand All @@ -11,18 +10,37 @@ func TestParseStorePathFromInstallableOutput(t *testing.T) {
testCases := []struct {
name string
input string
expected []string
expected map[string]bool
}{
{
name: "go-basic-nix-2-20-1",
// snipped the actual output for brevity. We mainly care about the first key in the JSON.
input: `{"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0":{"deriver":"/nix/store/clr3bm8njqysvyw4r4x4xmldhz4knrff-go-1.22.0.drv"}}`,
expected: []string{"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0"},
input: `{"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0":{"deriver":"/nix/store/clr3bm8njqysvyw4r4x4xmldhz4knrff-go-1.22.0.drv"}}`,
expected: map[string]bool{
"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0": true,
},
},
{
name: "go-basic-nix-2-17-0",
input: `[{"path":"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0"}]`,
expected: []string{"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0"},
name: "go-basic-nix-2-20-1",
// snipped the actual output for brevity. We mainly care about the first key in the JSON.
input: `{"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0":null}`,
expected: map[string]bool{
"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0": false,
},
},
{
name: "go-basic-nix-2-17-0",
input: `[{"path":"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0"}]`,
expected: map[string]bool{
"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0": false,
},
},
{
name: "go-basic-nix-2-17-0",
input: `[{"path":"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0", "valid": true}]`,
expected: map[string]bool{
"/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0": true,
},
},
}

Expand All @@ -32,8 +50,8 @@ func TestParseStorePathFromInstallableOutput(t *testing.T) {
if err != nil {
t.Errorf("Expected no error but got error: %s", err)
}
if !slices.Equal(tc.expected, maps.Keys(actual)) {
t.Errorf("Expected store path %s but got %s", tc.expected, actual)
if !maps.Equal(tc.expected, actual) {
t.Errorf("Expected store path %v but got %v", tc.expected, actual)
}
})
}
Expand Down

0 comments on commit b95f4cd

Please sign in to comment.