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() }