instantiated: parse instantiated messages

This commit is contained in:
Yonah 2025-07-14 02:52:25 +09:00
parent db93b6be6c
commit 1f01e82714
Signed by: yonah
SSH Key Fingerprint: SHA256:vnQvK8+XXH9Tbni2AV1a/8qdVK/zPcXw52GM0ruQvwA
8 changed files with 258840 additions and 0 deletions

128
instantiated.go Normal file
View File

@ -0,0 +1,128 @@
package nixbuild
import (
"bufio"
"context"
"errors"
"io"
"os/exec"
"path"
"slices"
"strings"
)
const (
instantiatedPrefix = "instantiated '"
instantiatedSeparator = " -> "
instantiatedFields = 2
// a reasonable starting buffer capacity: pretty close to average of test cases
instantiatedInitialCap = 1 << 13
)
const (
InstantiatedBadFields = iota
InstantiatedUnexpectedQuotes
InstantiatedNotAbsolute
)
type MalformedInstantiatedError struct {
Message string
Type int
}
func (m *MalformedInstantiatedError) Error() string {
switch m.Type {
case InstantiatedBadFields:
return "incorrect amount of fields in instantiated message"
case InstantiatedUnexpectedQuotes:
return "unexpected quotes in final instantiated field"
case InstantiatedNotAbsolute:
return "instantiated derivation path is not absolute"
default:
panic("unreachable")
}
}
func (m *MalformedInstantiatedError) Is(err error) bool {
var e *MalformedInstantiatedError
if !errors.As(err, &e) {
return false
}
if e == nil || m == nil {
return e == m
}
return e.Type == m.Type
}
// DecodeInstantiated interprets the verbose output of `nix build` and returns a slice of all instantiated derivations.
func DecodeInstantiated(stderr io.Reader) ([]string, error) {
scanner := bufio.NewScanner(stderr)
instantiated := make([]string, 0, instantiatedInitialCap)
for scanner.Scan() {
// on error, process is killed by deferred context cancel
if len(instantiated) == cap(instantiated) {
// grow the buffer exponentially to minimise copies
instantiated = slices.Grow(instantiated, cap(instantiated)<<1)
}
v := scanner.Text()
if !strings.HasPrefix(v, instantiatedPrefix) {
continue
}
f := strings.SplitN(v, instantiatedSeparator, instantiatedFields)
if len(f) != instantiatedFields {
return nil, &MalformedInstantiatedError{v, InstantiatedBadFields}
}
// very basic validation here: the output format is not fully understood
if len(f[1]) < 3 || f[1][0] != '\'' || f[1][len(f[1])-1] != '\'' {
return nil, &MalformedInstantiatedError{v, InstantiatedUnexpectedQuotes}
}
drv := f[1][1 : len(f[1])-1]
if !path.IsAbs(drv) {
return nil, &MalformedInstantiatedError{v, InstantiatedNotAbsolute}
}
instantiated = append(instantiated, drv)
}
slices.Sort(instantiated)
return slices.Compact(instantiated), scanner.Err()
}
// EvalInstantiated evaluates the installable and returns all derivations instantiated during the evaluation.
// This relies on verbose output of `nix build` and is certainly not the correct way to do it, but so far it works
// significantly better than `nix-store -qR` and there does not appear to be a better way.
func EvalInstantiated(ctx context.Context, installable string) ([]string, error) {
c, cancel := context.WithCancel(ctx)
defer cancel()
cmd := exec.CommandContext(c,
"nix", "build", installable,
// 'instantiated' messages are only emitted when actually evaluating something
"--option", "eval-cache", "false",
// do not actually build anything
"--dry-run",
// increase verbosity so nix outputs 'instantiated' messages
"-Lvvv",
)
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
if err := cmd.Start(); err != nil {
// have finalizer take care of the pipe
return nil, err
}
instantiated, decodeErr := DecodeInstantiated(stderr)
waitErr := cmd.Wait()
return instantiated, errors.Join(decodeErr, waitErr)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

121
instantiated_test.go Normal file
View File

@ -0,0 +1,121 @@
package nixbuild_test
import (
"errors"
"runtime"
"slices"
"strings"
"testing"
"git.gensokyo.uk/yonah/nixbuild"
)
func TestDecodeInstantiated(t *testing.T) {
testCases := []struct {
name string
out string
want []string
wantErr error
}{
{"bad fields", segmentPrefix + `instantiated 'config.sub-948ae97' ` + segmentSuffix, nil,
&nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedBadFields}},
{"unexpected quotes left", segmentPrefix + `instantiated 'config.sub-948ae97' -> /n'` + segmentSuffix, nil,
&nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedUnexpectedQuotes}},
{"unexpected quotes right", segmentPrefix + `instantiated 'config.sub-948ae97' -> '/n` + segmentSuffix, nil,
&nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedUnexpectedQuotes}},
{"unexpected quotes short", segmentPrefix + `instantiated 'config.sub-948ae97' -> ''` + segmentSuffix, nil,
&nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedUnexpectedQuotes}},
{"not absolute", segmentPrefix + `instantiated 'config.sub-948ae97' -> ' '` + segmentSuffix, nil,
&nixbuild.MalformedInstantiatedError{Type: nixbuild.InstantiatedNotAbsolute}},
{"good segment", segmentPrefix + segmentBody + segmentSuffix, []string{
"/nix/store/3zilrlmq7r6rpzfd94mwss32b62yinj5-bootstrap-stage0-stdenv-linux.drv",
"/nix/store/7yfwy95p6lcdpljdajs5aw10h6q0sfx0-update-autotools-gnu-config-scripts-hook.drv",
"/nix/store/bamwxswxacs3cjdcydv0z7bj22d7g2kc-config.guess-948ae97.drv",
"/nix/store/gyks6vvl7x0gq214ldjhi3w4rg37nh8i-zlib-1.3.1.tar.gz.drv",
"/nix/store/nbsdqpfzh1jlpmh95s69b3iivfcvv3lh-config.sub-948ae97.drv",
"/nix/store/ysp83x9nrks28zkblqmnc1s1kb68dr69-gnu-config-2024-01-01.drv",
}, nil},
{"getchoo atlas", getchooAtlasOut, getchooAtlas, nil},
{"getchoo glados", getchooGladosOut, getchooGlados, nil},
{"pluiedev pappardelle", pluiedevPappardelleOut, pluiedevPappardelle, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
stderr := strings.NewReader(tc.out)
got, err := nixbuild.DecodeInstantiated(stderr)
if !errors.Is(err, tc.wantErr) {
t.Fatalf("DecodeInstantiated: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
t.Logf("DecodeInstantiated: error = %v", err)
return
}
if !slices.Equal(got, tc.want) {
t.Errorf("DecodeInstantiated: %#v, want %#v", got, tc.want)
}
})
}
t.Run("errors", func(t *testing.T) {
t.Run("bad type", func(t *testing.T) {
badErr := errors.New("")
if errors.Is(new(nixbuild.MalformedInstantiatedError), badErr) {
t.Error("unexpected MalformedInstantiatedError equivalence")
}
})
t.Run("nil", func(t *testing.T) {
if errors.Is(new(nixbuild.MalformedInstantiatedError), (*nixbuild.MalformedInstantiatedError)(nil)) {
t.Error("unexpected MalformedInstantiatedError equivalence")
}
})
})
}
const (
segmentPrefix = `evaluating file '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/development/libraries/zlib/default.nix'
evaluating file '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/fetchurl/boot.nix'
performing daemon worker op: 7
instantiated 'zlib-1.3.1.tar.gz' -> '/nix/store/gyks6vvl7x0gq214ldjhi3w4rg37nh8i-zlib-1.3.1.tar.gz.drv'
source path '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/setup-hooks/update-autotools-gnu-config-scripts.sh' is uncacheable
copying '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/setup-hooks/update-autotools-gnu-config-scripts.sh' to the store...
performing daemon worker op: 7
copied '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/setup-hooks/update-autotools-gnu-config-scripts.sh' to '/nix/store/96rvfw5vlv1hwwm9sdxhdkkpjyym6p2x-update-autotools-gnu-config-scripts.sh'
copied source '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/setup-hooks/update-autotools-gnu-config-scripts.sh' -> '/nix/store/96rvfw5vlv1hwwm9sdxhdkkpjyym6p2x-update-autotools-gnu-config-scripts.sh'
evaluating file '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/by-name/gn/gnu-config/package.nix'
performing daemon worker op: 7
instantiated 'config.guess-948ae97' -> '/nix/store/bamwxswxacs3cjdcydv0z7bj22d7g2kc-config.guess-948ae97.drv'
performing daemon worker op: 7
`
segmentBody = `instantiated 'config.sub-948ae97' -> '/nix/store/nbsdqpfzh1jlpmh95s69b3iivfcvv3lh-config.sub-948ae97.drv'`
segmentSuffix = `
performing daemon worker op: 7
instantiated 'gnu-config-2024-01-01' -> '/nix/store/ysp83x9nrks28zkblqmnc1s1kb68dr69-gnu-config-2024-01-01.drv'
performing daemon worker op: 7
instantiated 'bootstrap-stage0-stdenv-linux' -> '/nix/store/3zilrlmq7r6rpzfd94mwss32b62yinj5-bootstrap-stage0-stdenv-linux.drv'
performing daemon worker op: 7
instantiated 'update-autotools-gnu-config-scripts-hook' -> '/nix/store/7yfwy95p6lcdpljdajs5aw10h6q0sfx0-update-autotools-gnu-config-scripts-hook.drv'
source path '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/bintools-wrapper/ld-wrapper.sh' is uncacheable
copying '/nix/store/vdzlppvrdkz9rv14q4j02g9kpjbww2ww-source/pkgs/build-support/bintools-wrapper/ld-wrapper.sh' to the store...`
)
func BenchmarkDecodeInstantiated(b *testing.B) {
stderr := strings.NewReader(pluiedevPappardelleOut)
var v []string
for i := 0; i < b.N; i++ {
v, _ = nixbuild.DecodeInstantiated(stderr)
runtime.KeepAlive(v)
}
}
func BenchmarkDecodeInstantiatedCopy(b *testing.B) {
stderr := strings.NewReader(getchooAtlasOut)
var v []string
for i := 0; i < b.N; i++ {
v, _ = nixbuild.DecodeInstantiated(stderr)
runtime.KeepAlive(v)
}
}

37432
testdata/getchoo_atlas vendored Normal file

File diff suppressed because it is too large Load Diff

84832
testdata/getchoo_glados vendored Normal file

File diff suppressed because it is too large Load Diff

108599
testdata/pluiedev_pappardelle vendored Normal file

File diff suppressed because it is too large Load Diff