This essentially does the same thing underneath the hood but the API is less painful to use, and it makes more sense in this use case.
190 lines
5.4 KiB
Go
190 lines
5.4 KiB
Go
package nixbuild
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"iter"
|
|
"os/exec"
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
// 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)}
|
|
}
|
|
|
|
// Instantiated returns a non-reusable iterator over instantiated derivations collected from [io.Reader].
|
|
func (d *InstantiatedDecoder) Instantiated() 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Decode collects and deduplicates derivations emitted by [InstantiatedDecoder].
|
|
func (d *InstantiatedDecoder) Decode() ([]string, error) {
|
|
instantiated := make([]string, 0, instantiatedInitialCap)
|
|
for drv := range d.Instantiated() {
|
|
if len(instantiated) == cap(instantiated) {
|
|
// grow the buffer exponentially to minimise copies
|
|
instantiated = slices.Grow(instantiated, cap(instantiated)<<1)
|
|
}
|
|
instantiated = append(instantiated, drv)
|
|
}
|
|
|
|
slices.Sort(instantiated)
|
|
return slices.Compact(instantiated), d.Err()
|
|
}
|
|
|
|
// 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
|
|
// populated by Close
|
|
waitErr error
|
|
// synchronises access to waitErr
|
|
waitMu sync.RWMutex
|
|
|
|
*InstantiatedDecoder
|
|
}
|
|
|
|
// Err blocks until process exit and returns the first error encountered by the [InstantiatedEvaluator].
|
|
func (e *InstantiatedEvaluator) Err() error {
|
|
e.waitMu.RLock()
|
|
defer e.waitMu.RUnlock()
|
|
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)
|
|
e := &InstantiatedEvaluator{
|
|
cmd: Nix(c, CommandBuild, installable,
|
|
// 'instantiated' messages are only emitted when actually evaluating something
|
|
FlagOption, OptionEvalCache, ValueFalse,
|
|
// do not actually build anything
|
|
FlagDryRun,
|
|
// increase verbosity so nix outputs 'instantiated' messages
|
|
FlagPrintBuildLogs, FlagDebug,
|
|
),
|
|
}
|
|
|
|
e.cmd.Stdout = Stdout
|
|
|
|
// verbose output ends up on stderr in the current nix implementation
|
|
er, ew := io.Pipe()
|
|
e.cmd.Stderr = ew
|
|
e.InstantiatedDecoder = NewInstantiatedDecoder(er)
|
|
|
|
if err := e.cmd.Start(); err != nil {
|
|
cancel()
|
|
// have finalizer take care of the pipe
|
|
return nil, err
|
|
}
|
|
e.waitMu.Lock()
|
|
go func() { e.waitErr = e.cmd.Wait(); cancel(); _ = ew.Close(); e.waitMu.Unlock() }()
|
|
|
|
return e, nil
|
|
}
|
|
|
|
// 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.Err()
|
|
}
|