From 5c632a9ad87ccf40917fe16b2244e2aa5b788b83 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Tue, 15 Jul 2025 01:51:55 +0900 Subject: [PATCH] derivation: parse nix derivation show This time it's JSON, and actual intended behaviour. --- build.go | 15 ++------ cmd/nixbuild/main.go | 15 +++++++- derivation.go | 88 ++++++++++++++++++++++++++++++++++++++++++++ format.go | 26 +++++++++++++ 4 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 derivation.go diff --git a/build.go b/build.go index 6f3398c..26114de 100644 --- a/build.go +++ b/build.go @@ -2,9 +2,9 @@ package nixbuild import ( "context" + "errors" "io" "iter" - "strings" ) // Build builds all entries yielded by installables. @@ -32,18 +32,11 @@ func Build(ctx context.Context, installables iter.Seq[string]) error { return err } - for drv := range installables { - if strings.HasSuffix(drv, ".drv") { - // this is just what nix requires now :c - drv += "^*" - } - if _, err := iw.Write([]byte(drv + "\n")); err != nil { - return err - } + if _, err := WriteStdin(iw, installables); err != nil { + return errors.Join(err, cmd.Wait()) } - if err := iw.Close(); err != nil { - return err + return errors.Join(err, cmd.Wait()) } return cmd.Wait() diff --git a/cmd/nixbuild/main.go b/cmd/nixbuild/main.go index b9a7eeb..947f8da 100644 --- a/cmd/nixbuild/main.go +++ b/cmd/nixbuild/main.go @@ -42,7 +42,20 @@ func main() { Flag(&flagVerbose, "v", command.BoolFlag(false), "Connect nix stderr"). Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable") - c.Command("build", "Build a list of installables", func(args []string) error { + c.Command("show", command.UsageInternal, func(args []string) error { + if len(args) < 1 { + return commandHandlerError("usage requires at least 1 argument") + } + + if drv, err := nixbuild.DerivationShow(ctx, slices.Values(args)); err != nil { + return commandHandlerError(fmt.Sprintf("cannot show: %v", err)) + } else { + log.Printf("got %d derivations:\n%#v", len(drv), drv) + return nil + } + }) + + c.Command("build", command.UsageInternal, func(args []string) error { if len(args) < 1 { return commandHandlerError("build requires at least 1 argument") } diff --git a/derivation.go b/derivation.go new file mode 100644 index 0000000..324d837 --- /dev/null +++ b/derivation.go @@ -0,0 +1,88 @@ +package nixbuild + +import ( + "context" + "encoding/json" + "errors" + "io" + "iter" +) + +type ( + // DerivationMap is the output of `nix derivation show`. + DerivationMap map[string]Derivation + + // Derivation is a description of a derivation. + Derivation struct { + // Args are arguments passed to Builder. + Args []string `json:"args"` + // Builder is the store path of the program that builds the [Derivation]. + Builder string `json:"builder"` + // System is the value of pkgs.system during evaluation. + System string `json:"system"` + // Name is the user-facing name of a derivation, as seen in the nix store path suffix. + Name string `json:"name"` + + Environment json.RawMessage `json:"env"` + InputDerivations InputDerivationsMap `json:"inputDrvs"` + InputSources []string `json:"inputSrcs"` + Outputs OutputsMap `json:"outputs"` + } + + // OutputsMap is an output name to [Output] map. + OutputsMap map[string]Output + + // Output is an output of a [Derivation]. + Output struct { + Path string `json:"path"` + } + + // InputDerivationsMap is a store path to metadata map. + InputDerivationsMap map[string]InputDerivation + + // InputDerivation contains input derivation metadata. + InputDerivation struct { + DynamicOutputs json.RawMessage `json:"dynamicOutputs"` + Outputs []string `json:"outputs"` + } +) + +// DerivationShow returns a [DerivationMap] describing all entries yielded by installables. +func DerivationShow(ctx context.Context, installables iter.Seq[string]) (DerivationMap, error) { + c, cancel := context.WithCancel(ctx) + defer cancel() + + cmd := Nix(c, CommandDerivation, CommandDerivationShow, + FlagStdin) + ir, iw := io.Pipe() + cmd.Stdin = ir + or, ow := io.Pipe() + cmd.Stdout = ow + if Stderr != nil { + cmd.Stderr = Stderr + } + cmd.Cancel = func() error { _ = ow.Close(); return cmd.Process.Kill() } + + if err := cmd.Start(); err != nil { + return nil, err + } + + done := make(chan error) + var v DerivationMap + go func() { done <- json.NewDecoder(or).Decode(&v) }() + + if _, err := WriteStdin(iw, installables); err != nil { + return nil, errors.Join(err, cmd.Wait()) + } + if err := iw.Close(); err != nil { + return nil, errors.Join(err, cmd.Wait()) + } + + if err := cmd.Wait(); err != nil { + // deferred cancel closes pipe + return nil, err + } + + err := <-done + return v, err +} diff --git a/format.go b/format.go index f0c5afc..a2f60c1 100644 --- a/format.go +++ b/format.go @@ -2,12 +2,21 @@ package nixbuild import ( "context" + "io" + "iter" "os/exec" + "strings" ) const ( // CommandBuild build a derivation or fetch a store path CommandBuild = "build" + // CommandDerivation work with derivations, Nix's notion of a build plan. + CommandDerivation = "derivation" + // CommandDerivationAdd add a store derivation + CommandDerivationAdd = "add" + // CommandDerivationShow show the contents of a store derivation + CommandDerivationShow = "show" // FlagDryRun show what this command would do without doing it. FlagDryRun = "--dry-run" @@ -86,3 +95,20 @@ const ( // NixOSInstallable returns the nixos installable for a given flake and host. func NixOSInstallable(flake, host string) string { return flake + nixosSuffix0 + host + nixosSuffix1 } + +// WriteStdin writes installables for a nix process running with [FlagStdin]. +func WriteStdin(w io.Writer, installables iter.Seq[string]) (int, error) { + var count int + for drv := range installables { + if strings.HasSuffix(drv, ".drv") { + // this is just what nix requires now :c + drv += "^*" + } + n, err := w.Write([]byte(drv + "\n")) + count += n + if err != nil { + return count, err + } + } + return count, nil +}