NixOS / nix

Nix, the purely functional package manager
https://nixos.org/
GNU Lesser General Public License v2.1
12.49k stars 1.5k forks source link

Capture full output from recursive Nix build #7276

Open lukego opened 1 year ago

lukego commented 1 year ago

I would like to be able to run a recursive Nix build and to capture all the output logged to stdout/stderr. However I am not getting all of the output that I expect (see below.)

Context

My use case is "try-catch." The inner nix build attempts to build a derivation - which may fail either directly or via dependency - and the outer nix build produces the logs from this build attempt as its output.

(My higher-level use case is to use Nix to build a report about which derivations do and don't build successfully. I think it would be much neater to do this with recursive Nix than e.g. as an extension of Hydra.)

Example

Here is a recursive Nix expression:

let
  # master branch 2022-11-08
  nixpkgs = fetchTarball {
    url = "https://github.com/nixos/nixpkgs/archive/f6072a8d7b1520194453e27be1fd59a28c751a7f.tar.gz";
    sha256 = "sha256:1zr2jm5gjbsyca9bcq7f3h354qf8d5bd6b0h4qjr0i03dvrvl10s";
  };
in

with import nixpkgs {};

runCommand "outer"
  {
    requiredSystemFeatures = [ "recursive-nix" ];
    nativeBuildInputs = [ nix ];
    expr = writeText "inner.nix"
      ''
        with import ${nixpkgs} {};
        runCommand "inner" {} "echo building inner.. | tee $out"
      '';
  }
  ''
    nix --experimental-features "recursive-nix nix-command" \
      build -L -f $expr 2>&1 | tee $out
  ''

that when built produces this output in the shell:

[luke@snowy:~/git/lispnix]$ nix --experimental-features "nix-command recursive-nix" build -L -f rec.nix
outer> warning: you don't have Internet access; disabling some network-dependent features
outer> this derivation will be built:
outer>   /nix/store/3i3c36fymmwjk1kfrfc2cyc1kbbkygs7-inner.drv
inner> building inner..

and produces this log as its result:

[luke@snowy:~/git/lispnix]$ cat result
warning: you don't have Internet access; disabling some network-dependent features
this derivation will be built:
  /nix/store/3i3c36fymmwjk1kfrfc2cyc1kbbkygs7-inner.drv

The problem is that this log is incomplete: what I need to see in the log is the output from the inner build, building inner..

Is there a way that I can change this code so that the log-output of the inner build is captured as the build-output of the outer build?

(Aside: Is there also a neat way to avoid embedding the inner expression in a string while still ensuring that it's not built in the outer process? I'd imagined taking a drvPath and passing that to nix build but that didn't seem to be accepted.)

lukego commented 1 year ago

Maybe I have an adequate kludge: Overriding the builder scripts to produce stdout/stderr logs instead of the normal outputs.

This addresses the problem more directly because the information that I want to capture is all included in the build outputs. I don't really need to capture the nix build output because the information will be available in the result file in plain text.

I have a little proof of concept (below) and it seems to reliably produce the output of the inner nix build like so:

[luke@snowy:~/git/lispnix]$ nix --experimental-features "nix-command recursive-nix" build -L -f rec.nix

[luke@snowy:~/git/lispnix]$ cat result
START: building /nix/store/nar8fpywn5lfbsrp8sh3g7agi8knpkqv-outer
warning: you don't have Internet access; disabling some network-dependent features
this derivation will be built:
  /nix/store/fw35kdc1dn99v9ssa81mp15pgps614nz-inner.drv
START: building /nix/store/wxghg65467rfwhf21vy9764f2azq73ma-inner
building inner..
FINISH: exit status 0 for /nix/store/wxghg65467rfwhf21vy9764f2azq73ma-inner
FINISH: exit status 0 for /nix/store/nar8fpywn5lfbsrp8sh3g7agi8knpkqv-outer

This string building inner is the information that has been alluding me.

This also seems to handle error cases plausibly. If the inner build fails then the log is produced with useful information. Likewise if the inner derivation fails to evaluate.

Error example for the case of an inner derivation that fails to evaluate due to syntax error:

[luke@snowy:~/git/lispnix]$ nix --experimental-features "nix-command recursive-nix" build -L -f rec.nix

[luke@snowy:~/git/lispnix]$ cat result
START: building /nix/store/56x4kwxfwciafmgahlghsc5rqnlqj20d-outer
warning: you don't have Internet access; disabling some network-dependent features
error: syntax error, unexpected ';'

       at /nix/store/j29wqqy775lczrrds4ir8zppw1jrr2qr-inner.nix:3:78:

            2| runCommand "inner"
            3|   { builder = /nix/store/h2vh7i7yqiflk36frsbw7cgs890hvs56-logging-builder.sh;;;;; }
             |                                                                              ^
            4|   "echo building  inner.."
FINISH: exit status 1 for /nix/store/56x4kwxfwciafmgahlghsc5rqnlqj20d-outer

Good enough?

Here is the contents of rec.nix:

let
  # master branch 2022-11-08
  nixpkgs = fetchTarball {
    url = "https://github.com/nixos/nixpkgs/archive/f6072a8d7b1520194453e27be1fd59a28c751a7f.tar.gz";
    sha256 = "sha256:1zr2jm5gjbsyca9bcq7f3h354qf8d5bd6b0h4qjr0i03dvrvl10s";
  };
in

with import nixpkgs {};

# Custom builder for mkDerivation to produce build logs instead of normal output.
#
# Output will be a text file containing the output on stdout/stderr during build.
# One-liner header and trailer metadata is also included.
let logging-builder = writeScript "logging-builder.sh"
  ''
    # Run the real builder as mkDerivation normally would
    echo "START: building $out" | ${coreutils}/bin/tee .logging-builder.log

    set +e
    set -o pipefail
    ${stdenv.shell} -c 'source $stdenv/setup; genericBuild' 2>&1 \
      | ${coreutils}/bin/tee -a .logging-builder.log
    status=$?

    echo "FINISH: exit status $status for $out" >> .logging-builder.log
    [ -e "$out" ] && ${coreutils}/bin/rm -rf $out
    ${coreutils}/bin/cp .logging-builder.log $out
  '';
in

runCommand "outer"
  {
    requiredSystemFeatures = [ "recursive-nix" ];
    nativeBuildInputs = [ nix ];
    expr = writeText "inner.nix"
      ''
        with import ${nixpkgs} {};
        runCommand "inner"
          { builder = ${logging-builder}; }
          "echo building  inner.."
      '';
    builder = logging-builder;
  }
  ''
    # Run nix build and log output
    nix --experimental-features "recursive-nix nix-command" \
      build -L -f $expr
    status=$?
    [ -e result ] && cat result
  ''