Skip to content

Usage Guide

Understanding the JUnit Output

Junix maps Nix build activities to JUnit test cases:

Nix build result JUnit element
Build succeeded <testcase> with <system-out> containing build logs
Build failed <testcase> with <failure> containing build logs
Eval failed <testcase> with <failure> and Nix error trace in <system-out>
Build incomplete (process crashed) <testcase> with <error>
Cached or substituted (no build needed) <testcase> with <skipped>
Nix error messages <system-err> on the first failing testcase

Example Report

Here is a real JUnit report from junix check on a flake with one passing and one failing check:

<?xml version="1.0" encoding="utf-8" ?>
<testsuites>
    <testsuite
        name="nix-eval.x86_64-linux"
        tests="2"
        failures="0"
        errors="0"
        skipped="0"
        time="0"
    >
        <testcase
            name="nix eval .#checks.x86_64-linux.passing"
            classname="/nix/store/xxx-passing-check.drv"
            time="0"
        >
            <system-out>/nix/store/xxx-passing-check.drv</system-out>
        </testcase>
        <testcase
            name="nix eval .#checks.x86_64-linux.failing"
            classname="/nix/store/yyy-failing-check.drv"
            time="0"
        >
            <system-out>/nix/store/yyy-failing-check.drv</system-out>
        </testcase>
    </testsuite>
    <testsuite
        name="nix-build.x86_64-linux"
        tests="2"
        failures="1"
        errors="0"
        skipped="0"
        time="0"
    >
        <testcase
            name="nix build .#checks.x86_64-linux.passing"
            classname="/nix/store/xxx-passing-check.drv"
            time="0"
        >
            <system-out>This check passes</system-out>
        </testcase>
        <testcase
            name="nix build .#checks.x86_64-linux.failing"
            classname="/nix/store/yyy-failing-check.drv"
            time="0"
        >
            <failure message="Build failed" type="failure" />
            <system-out>This check fails</system-out>
            <system-err>
                error: builder for '/nix/store/yyy-failing-check.drv' failed with exit
                code 1
            </system-err>
        </testcase>
    </testsuite>
</testsuites>

If a check fails to evaluate (builtins.tryEval returns success: false), the <system-out> of the failing eval testcase contains the Nix error trace for that check, captured by a follow-up nix eval --show-trace call. For example, a check defined as pkgs.runCommand "eval-fail-check" {} (throw "boom") produces:

<testcase name="nix eval .#checks.x86_64-linux.eval-fail" ...>
    <failure message="Build failed" type="failure" />
    <system-out>error:
        while evaluating the 'drvPath' of derivation 'eval-fail-check'
         at /nix/store/.../lib/customisation.nix:415:11:
          414|         // {
          415|           drvPath =
             |           ^
          416|             assert condition;

        while calling the 'throw' builtin
         at ./flake.nix:23:9:
           22|       eval-fail = pkgs.runCommand "eval-fail-check" {} (
           23|         throw "This check fails to evaluate"
             |         ^

       error: This check fails to evaluate
</system-out>
</testcase>

The report contains one <testsuite> per architecture per phase:

  • Eval suites (nix-eval.{arch}) — one testcase per check, with the derivation path in <system-out> on success, or <failure> and the Nix error trace in <system-out> if the check failed to evaluate.
  • Build suites (nix-build.{arch}) — one testcase per check, with build logs in <system-out> on success, <failure> on build failure, or <skipped> when the build was cached. A check whose eval already failed also appears here as <failure> (since the build can never succeed without a derivation) carrying the same eval error trace in <system-out>.

The <testcase> name in every JUnit suite is the exact command you would run to reproduce the work: nix eval <path>#checks.<arch>.<name> for eval suites, nix build <path>#checks.<arch>.<name> for build suites, and nix build <attr> for the build subcommand. This makes failures in CI reproducible by copy-paste.

Colored terminal summary

See the --color global switch in the CLI reference for the full description of the three modes (auto, yes, no) and the JUNIX_COLOR precedence rule (CLI > env > auto). When colors are enabled, the summary uses the following scheme:

  • blue for informational headers (e.g. eval {arch} ... N checks, ⬇ N path(s) downloaded)
  • green for passed checks
  • bold red for failed checks and the final FAILED line
  • red for the cached count line
  • dim green for per-check cached entries

Example output from a mixed run:

  ⬇ 3 path(s) downloaded
  ~ 1 build(s) cached
  ✓ checks.x86_64-linux.passing
  ✗ checks.x86_64-linux.failing

  FAILED (1/2):
    - checks.x86_64-linux.failing

translate: Parsing Existing Build Output

Use translate when you already have Nix build output and want to convert it to JUnit XML. This is useful when:

  • You want to capture output from a complex build script
  • You need to reprocess previously saved logs
  • You're integrating Junix into an existing pipeline
# Stream a live build into junix
nix build --log-format internal-json -v --print-build-logs --no-link .#pkg1 2>&1 | junix translate -o report.xml

# Or capture to a file and translate later
nix build --log-format internal-json -v --print-build-logs --no-link .#pkg1 2> build.log
cat build.log | junix translate -o report.xml

junix translate reads @nix JSON lines from stdin and writes the JUnit XML report to stdout (or to -o). While it reads, it immediately forwards non-@nix lines and Nix log messages that match the current verbosity to its own stderr, formatted in human-readable form. This lets you watch a long build live while still getting the report at the end.

Tip

Make sure to use --log-format internal-json when running Nix, otherwise Junix won't be able to parse the output.

build: Build and Report

Use build when you want Junix to drive the Nix build directly. This is the simplest way to get a JUnit report.

Each <testcase> in the JUnit report is named after the exact nix build command that would reproduce it (e.g. nix build nixpkgs#hello). A colorful summary is printed to stderr showing downloaded, cached, passed and failed builds, ending with PASSED (T evals, T builds) or FAILED (F/T evals, F/T builds) (F = failed, T = total).

# Build one or more packages
junix build .#pkg1 .#pkg2 -o report.xml

# Build with a remote store
junix build .#pkg1 --store ssh-ng://eu.nixbuild.net -o report.xml

# Pass extra flags to nix build after --
junix build nixpkgs#hello -- --no-link --rebuild

The --store option sets a remote build store (e.g. nixbuild.net).

check: Run All Flake Checks

Use check to run all checks defined in your flake. This is ideal for CI pipelines where you want to validate the entire project.

# Run all checks (eval all arches, build local only)
junix check -o report.xml

# Evaluate only specific architectures
junix check --eval-arch x86_64-linux --eval-arch aarch64-linux -o report.xml

# Build specific architectures (default: local only)
junix check --build-arch x86_64-linux -o report.xml

# Pass extra flags to nix build
junix check -- --no-link --rebuild

How Check Discovery Works

  1. Junix runs nix eval --json "path#checks" --apply with a Nix expression that evaluates each check using builtins.tryEval, so one eval failure does not abort the entire discovery.
  2. The expression returns a JSON list of objects with arch, name, drvPath, and error fields.
  3. Checks whose eval succeeded (drvPath is not null) are sent to nix build in the build phase.
  4. Checks whose eval failed are never sent to nix build — they are reported as build failures with the eval error message. builtins.tryEval swallows the Nix error trace and only returns success: false, so for each failing check junix runs a second nix eval --show-trace "path#checks.<arch>.<name>.drvPath" to capture the real trace. That trace becomes the error field in the JUnit <system-out> of the failing testcase. Only failing checks pay the extra round trip; healthy flakes run one eval.
  5. By default, only the local system checks are built. Use -b to override.

Path handling

Junix passes the PATH argument to check directly to nix eval and nix build, without rewriting it. Like Nix itself, relative paths must start with ./ or be absolute. The default is .. If you pass a bare relative path (e.g. tests/sample-flake), Nix rejects it with its usual error message — Junix surfaces that error in the JUnit report as a build failure.

Summary output

The colorful summary on stderr has two sections: one for the eval phase (per architecture) and one for the build phase (per architecture). The final line uses separate counts for evals and builds:

eval x86_64-linux ... 2 checks
  ✓ nix eval .#checks.x86_64-linux.passing
  ✗ nix eval .#checks.x86_64-linux.failing
build x86_64-linux ... 2 checks
  ✗ nix build .#checks.x86_64-linux.failing
  ✓ nix build .#checks.x86_64-linux.passing (cached)

FAILED (1/2 evals, 1/2 builds)

Architecture flags

The -e/--eval-arch and -b/--build-arch flags are independent:

  • -e controls which architectures are evaluated (default: all)
  • -b controls which architectures are built (default: local only)

Passing -e aarch64-linux does NOT change the default of -b (still local). Passing -b aarch64-linux does NOT change the default of -e (still all).

Memory-constrained flakes

The default junix check discovery runs one nix eval [...magic...] flake#checks call that evaluates every check × arch in a single Nix process. That's efficient for small flakes, but can OOM on large ones, like those building full NixOS or derivations with hundreds of dependencies accross some arches. The symptom is a CI job that dies without junix ever printing a line similar to eval x86_64-linux ... 2 checks.

The -1/--eval-individually flag trades that for per-check isolation: junix runs just one nix eval to find all check names, and then evals each check individually, so the RAM peak is bounded by the closure of the largest single check — not the sum of all checks. The wall-clock cost is one Nix process per check (hundreds of milliseconds each), so a flake with hundreds of checks pays noticeable overhead. Use this only when the bulk path OOMs:

junix check -1o report.xml

--eval-individually is orthogonal to -e and -b: it does not change which arches are evaluated or built, only how each check is evaluated. The JUnit structure is identical to the bulk path (per-arch nix-eval.<arch> and nix-build.<arch> suites, same reproducer testcase names, same drv path in <system-out>). Failing checks automatically get the real Nix error trace in their <system-out> — no second nix eval --show-trace round trip is needed, since each per-check call surfaces the trace directly.

Controlling Log Verbosity

Nix has multiple log levels. Use -v flags to control what appears in the report:

# Errors only (default)
junix build .#pkg1 -o report.xml

# Info messages included
junix build .#pkg1 -v -o report.xml

# Chatty output (build phases, progress)
junix build .#pkg1 -vv -o report.xml

# Everything (debug-level detail)
junix build .#pkg1 -vvv -o report.xml

Verbosity maps to Nix log levels as follows:

-v flag Nix level Description
(none) 0 (Error) Errors only
-v 3 (Info) Informational
-vv 5 (Chatty) Build phases
-vvv 7 (Vomit) Everything

CI Integration

GitLab CI

test:
    script:
        - junix check -o report.xml
    artifacts:
        reports:
            junit: report.xml

Jenkins Pipeline

stage('Nix Build') {
  steps {
    sh 'junix build .#pkg1 -o report.xml'
  }
  post {
    always {
      junit 'report.xml'
    }
  }
}

GitHub Actions

- name: Run Nix checks
  run: junix check -o report.xml

- name: Upload JUnit report
  uses: dorny/test-reporter@v1
  if: success() || failure()
  with:
      name: Nix Build Results
      path: report.xml
      reporter: java-junit