From b904bc4e8d9b7957932cbbf5b8af2d702ab7cf05 Mon Sep 17 00:00:00 2001 From: Yonah Date: Mon, 14 Jul 2025 20:03:30 +0900 Subject: [PATCH] instantiated: embed decoder in evaluator This makes it easier to directly parse nix output via the decoder interface. --- instantiated.go | 95 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 71 insertions(+), 24 deletions(-) diff --git a/instantiated.go b/instantiated.go index 6b64b01..d739ca9 100644 --- a/instantiated.go +++ b/instantiated.go @@ -6,9 +6,12 @@ import ( "errors" "io" "iter" + "os/exec" "path" + "runtime" "slices" "strings" + "syscall" ) const ( @@ -61,7 +64,7 @@ type InstantiatedDecoder struct { 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()) } // 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() } -// EvalInstantiated evaluates the installable and returns all derivations instantiated during the evaluation. -// This interprets 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) { +// 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 its results +// are significantly more complete than `nix-store -qR` and there does not appear to be a better way. +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) - defer cancel() - - cmd := Nix(c, "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", - ) - - cmd.Stdout = Stdout - stderr, err := cmd.StderrPipe() - if err != nil { - return nil, err + e := &InstantiatedEvaluator{ + cmd: Nix(c, "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", + ), + cancel: cancel, } - 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 return nil, err } - instantiated, decodeErr := NewInstantiatedDecoder(stderr).Decode() - waitErr := cmd.Wait() - return instantiated, errors.Join(decodeErr, waitErr) + runtime.SetFinalizer(e, (*InstantiatedEvaluator).Close) + return e, nil +} + +// 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() }