instantiated: embed decoder in evaluator

This makes it easier to directly parse nix output via the decoder interface.
This commit is contained in:
Ophestra 2025-07-14 20:03:30 +09:00
parent c1cdd0b559
commit d470e770ff
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q

View File

@ -6,9 +6,12 @@ import (
"errors" "errors"
"io" "io"
"iter" "iter"
"os/exec"
"path" "path"
"runtime"
"slices" "slices"
"strings" "strings"
"syscall"
) )
const ( const (
@ -61,7 +64,7 @@ type InstantiatedDecoder struct {
scanner *bufio.Scanner scanner *bufio.Scanner
} }
// Err returns the first error encountered by the InstantiatedDecoder. // Err returns the first error encountered by the [InstantiatedDecoder].
func (d *InstantiatedDecoder) Err() error { return errors.Join(d.err, d.scanner.Err()) } func (d *InstantiatedDecoder) Err() error { return errors.Join(d.err, d.scanner.Err()) }
// NewInstantiatedDecoder returns a [InstantiatedDecoder] struct for collecting derivations from r. // NewInstantiatedDecoder returns a [InstantiatedDecoder] struct for collecting derivations from r.
@ -123,34 +126,78 @@ func (d *InstantiatedDecoder) Decode() ([]string, error) {
return slices.Compact(instantiated), d.Err() return slices.Compact(instantiated), d.Err()
} }
// EvalInstantiated evaluates the installable and returns all derivations instantiated during the evaluation. // InstantiatedEvaluator evaluates a nix installable and collects its instantiated derivations via [InstantiatedDecoder].
// This interprets verbose output of `nix build` and is certainly not the correct way to do it, but so far it works // This interprets verbose output of `nix build` and is certainly not the correct way to do it, but so far its results
// significantly better than `nix-store -qR` and there does not appear to be a better way. // are significantly more complete than `nix-store -qR` and there does not appear to be a better way.
func EvalInstantiated(ctx context.Context, installable string) ([]string, error) { type InstantiatedEvaluator struct {
// underlying nix program
cmd *exec.Cmd
// kills the nix command
cancel context.CancelFunc
// populated by Close
waitErr error
*InstantiatedDecoder
}
// Err returns the first error encountered by the [InstantiatedEvaluator].
func (e *InstantiatedEvaluator) Err() error {
return errors.Join(e.waitErr, e.InstantiatedDecoder.Err())
}
// NewInstantiatedEvaluator initialises an [InstantiatedEvaluator] struct and its underlying nix process.
func NewInstantiatedEvaluator(ctx context.Context, installable string) (*InstantiatedEvaluator, error) {
c, cancel := context.WithCancel(ctx) c, cancel := context.WithCancel(ctx)
defer cancel() e := &InstantiatedEvaluator{
cmd: Nix(c, "build", installable,
cmd := Nix(c, "build", installable, // 'instantiated' messages are only emitted when actually evaluating something
// 'instantiated' messages are only emitted when actually evaluating something "--option", "eval-cache", "false",
"--option", "eval-cache", "false", // do not actually build anything
// do not actually build anything "--dry-run",
"--dry-run", // increase verbosity so nix outputs 'instantiated' messages
// increase verbosity so nix outputs 'instantiated' messages "-Lvvv",
"-Lvvv", ),
) cancel: cancel,
cmd.Stdout = Stdout
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
} }
if err := cmd.Start(); err != nil { e.cmd.Stdout = Stdout
// verbose output ends up on stderr in the current nix implementation
if stderr, err := e.cmd.StderrPipe(); err != nil {
cancel()
return nil, err
} else {
e.InstantiatedDecoder = NewInstantiatedDecoder(stderr)
}
if err := e.cmd.Start(); err != nil {
cancel()
// have finalizer take care of the pipe // have finalizer take care of the pipe
return nil, err return nil, err
} }
instantiated, decodeErr := NewInstantiatedDecoder(stderr).Decode() runtime.SetFinalizer(e, (*InstantiatedEvaluator).Close)
waitErr := cmd.Wait() return e, nil
return instantiated, errors.Join(decodeErr, waitErr) }
// Close releases system resources held by [InstantiatedEvaluator].
func (e *InstantiatedEvaluator) Close() error {
if e.cmd.ProcessState != nil {
return syscall.EBADE
}
e.cancel()
e.waitErr = e.cmd.Wait()
runtime.SetFinalizer(e, nil)
return e.Err()
}
// EvalInstantiated calls the underlying [InstantiatedDecoder.Decode] of a new [InstantiatedEvaluator].
func EvalInstantiated(ctx context.Context, installable string) ([]string, error) {
evaluator, err := NewInstantiatedEvaluator(ctx, installable)
if err != nil {
return nil, err
}
instantiated, _ := evaluator.Decode() // error joined by Close
return instantiated, evaluator.Close()
} }