instantiated: parse instantiated messages

This commit is contained in:
2025-07-14 02:52:25 +09:00
parent 7a6404165e
commit 2b5a2f26db
8 changed files with 258840 additions and 0 deletions
+128
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
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
View File
File diff suppressed because it is too large Load Diff
+84832
View File
File diff suppressed because it is too large Load Diff
+108599
View File
File diff suppressed because it is too large Load Diff