nixbuild/instantiated.go
2025-07-14 18:26:49 +09:00

131 lines
3.4 KiB
Go

package nixbuild
import (
"bufio"
"context"
"errors"
"io"
"path"
"slices"
"strings"
)
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
}
// 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)
instantiated := make([]string, 0, instantiatedInitialCap)
for scanner.Scan() {
// on error, process is killed by deferred context cancel
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()
}
// EvalInstantiated evaluates the installable and returns all derivations instantiated during the evaluation.
// This relies on 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) {
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
}
if err := cmd.Start(); err != nil {
// have finalizer take care of the pipe
return nil, err
}
instantiated, decodeErr := DecodeInstantiated(stderr)
waitErr := cmd.Wait()
return instantiated, errors.Join(decodeErr, waitErr)
}