204 lines
5.6 KiB
Go
204 lines
5.6 KiB
Go
package nixbuild
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"iter"
|
|
"os/exec"
|
|
"path"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
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)}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Decode collects and deduplicates derivations emitted by [InstantiatedDecoder].
|
|
func (d *InstantiatedDecoder) Decode() ([]string, error) {
|
|
instantiated := make([]string, 0, instantiatedInitialCap)
|
|
for drv := range d.Drv() {
|
|
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
|
|
// 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)
|
|
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,
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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()
|
|
}
|