instantiated: implement decoding iterator

This commit is contained in:
Ophestra 2025-07-14 19:00:11 +09:00
parent edac902c7f
commit 93f447163d
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
2 changed files with 81 additions and 29 deletions

View File

@ -5,6 +5,7 @@ import (
"context"
"errors"
"io"
"iter"
"path"
"slices"
"strings"
@ -54,46 +55,74 @@ func (m *MalformedInstantiatedError) Is(err error) bool {
return e.Type == m.Type
}
// InstantiatedDecoder interprets the verbose output of `nix build` and collects instantiated derivations.
type InstantiatedDecoder struct {
err error
scanner *bufio.Scanner
}
// Err returns the first error encountered by the InstantiatedDecoder.
func (d *InstantiatedDecoder) Err() error { return errors.Join(d.err, d.scanner.Err()) }
// NewInstantiatedDecoder returns a [InstantiatedDecoder] struct for collecting derivations from r.
func NewInstantiatedDecoder(r io.Reader) *InstantiatedDecoder {
return &InstantiatedDecoder{scanner: bufio.NewScanner(r)}
}
// Drv returns a non-reusable iterator over instantiated derivations collected from [io.Reader].
func (d *InstantiatedDecoder) Drv() iter.Seq[string] {
return func(yield func(string) bool) {
for d.scanner.Scan() {
v := d.scanner.Text()
if !strings.HasPrefix(v, instantiatedPrefix) {
// write skipped lines to stderr
if Stderr != nil {
_, _ = Stderr.Write([]byte(v + "\n"))
}
continue
}
fields := strings.SplitN(v, instantiatedSeparator, instantiatedFields)
if len(fields) != instantiatedFields {
// no more than a single -> is expected
d.err = &MalformedInstantiatedError{v, InstantiatedBadFields}
break
}
// very basic validation here: the output format is not fully understood
if len(fields[1]) < 3 || fields[1][0] != '\'' || fields[1][len(fields[1])-1] != '\'' {
d.err = &MalformedInstantiatedError{v, InstantiatedUnexpectedQuotes}
break
}
drv := fields[1][1 : len(fields[1])-1]
if !path.IsAbs(drv) {
d.err = &MalformedInstantiatedError{v, InstantiatedNotAbsolute}
break
}
if !yield(drv) {
break
}
}
}
}
// 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)
func DecodeInstantiated(r io.Reader) ([]string, error) {
decoder := NewInstantiatedDecoder(r)
instantiated := make([]string, 0, instantiatedInitialCap)
for scanner.Scan() {
// on error, process is killed by deferred context cancel
for drv := range decoder.Drv() {
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) {
if Stderr != nil {
_, _ = Stderr.Write([]byte(v + "\n"))
}
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()
return slices.Compact(instantiated), decoder.Err()
}

View File

@ -64,6 +64,29 @@ func TestDecodeInstantiated(t *testing.T) {
})
}
t.Run("stop early", func(t *testing.T) {
want := []string{
"/nix/store/gyks6vvl7x0gq214ldjhi3w4rg37nh8i-zlib-1.3.1.tar.gz.drv",
"/nix/store/bamwxswxacs3cjdcydv0z7bj22d7g2kc-config.guess-948ae97.drv",
"/nix/store/nbsdqpfzh1jlpmh95s69b3iivfcvv3lh-config.sub-948ae97.drv",
}
decoder := nixbuild.NewInstantiatedDecoder(strings.NewReader(segmentPrefix + segmentBody + segmentSuffix))
counter := 3
got := make([]string, 0, counter)
for drv := range decoder.Drv() {
got = append(got, drv)
counter--
if counter == 0 {
break
}
}
if !slices.Equal(got, want) {
t.Errorf("Drv: %#v, want %#v", got, want)
}
})
t.Run("errors", func(t *testing.T) {
t.Run("bad type", func(t *testing.T) {
badErr := errors.New("")