{ lib, pkgs, stdenvNoCC, }: let inherit (pkgs) buildEnv; testingThrow = expr: { expr = (builtins.tryEval (builtins.seq expr "didn't throw")); expected = { success = false; value = false; }; }; tests-name = { testNameFromNameArg = { expr = (buildEnv { name = "test-env"; paths = [ ]; }).name; expected = "test-env"; }; testNameFromPnameVersion = { expr = (buildEnv { pname = "test-env"; version = "1.0"; paths = [ ]; }).name; expected = "test-env-1.0"; }; }; tests-passthru-paths = { testPathsInPassthru = { expr = let env = buildEnv { name = "test-env"; paths = [ pkgs.hello ]; }; in builtins.length env.paths > 0; expected = true; }; testPassthruPathsOverridable = { expr = let env = buildEnv { name = "test-env"; paths = [ pkgs.hello ]; }; overridden = env.overrideAttrs { passthru.paths = [ pkgs.figlet ]; }; in builtins.length overridden.paths == 1; expected = true; }; }; tests-finalAttrs = { testFinalAttrsSelfReference = { expr = let env = buildEnv (finalAttrs: { name = "test-env"; paths = [ ]; passthru.description = "An env named ${finalAttrs.name}"; }); in env.description; expected = "An env named test-env"; }; }; tests-overrideAttrs = let base = buildEnv { name = "test-env"; paths = [ pkgs.hello ]; passthru.custom = "original"; }; overridden = base.overrideAttrs ( finalAttrs: prev: { passthru = prev.passthru // { custom = "modified"; }; } ); in { testOverrideAttrsChangesPassthru = { expr = overridden.custom; expected = "modified"; }; testOverrideAttrsPreservesName = { expr = overridden.name; expected = "test-env"; }; testOverrideAttrsAffectsDrv = { expr = let withPostBuild = base.overrideAttrs { postBuild = "echo overridden"; }; in base.drvPath != withPostBuild.drvPath; expected = true; }; }; tests-passthru-merging = let env = buildEnv { name = "test-env"; paths = [ pkgs.hello ]; derivationArgs.passthru.fromDerivationArgs = "a"; passthru.fromPassthru = "b"; }; in { testPassthruMergingAutoPathsPresent = { expr = env ? paths; expected = true; }; testPassthruMergingDerivationArgs = { expr = env.fromDerivationArgs; expected = "a"; }; testPassthruMergingDirectPassthru = { expr = env.fromPassthru; expected = "b"; }; # Direct passthru takes precedence over derivationArgs.passthru testPassthruMergingPrecedence = { expr = let env' = buildEnv { name = "test-env"; paths = [ ]; derivationArgs.passthru.key = "from-derivationArgs"; passthru.key = "from-passthru"; }; in env'.key; expected = "from-passthru"; }; }; tests-derivationArgs = let env = buildEnv { name = "test-env"; paths = [ ]; derivationArgs.allowSubstitutes = true; }; in { # derivationArgs.allowSubstitutes overrides the default (false) testDerivationArgsForwarded = { expr = env.allowSubstitutes; expected = true; }; # Backward compat: top-level nativeBuildInputs still works testCompatNativeBuildInputs = { expr = let env' = buildEnv { name = "test-env"; paths = [ ]; nativeBuildInputs = [ pkgs.hello ]; }; in builtins.length env'.nativeBuildInputs > 0; expected = true; }; }; # Build tests: derivations that build a buildEnv and verify its output. # These are exposed via passthru.buildTests and checked in buildCommand. buildTests = { basic-symlinking = pkgs.runCommand "test-buildenv-basic-symlinking" { testEnv = buildEnv { name = "test-env"; paths = [ pkgs.hello ]; }; } '' # With a single package, buildEnv symlinks the directory itself test -L "$testEnv/bin" || { echo "FAIL: $testEnv/bin is not a symlink"; exit 1; } # The symlink should point into the store target=$(readlink "$testEnv/bin") case "$target" in /nix/store/*) ;; *) echo "FAIL: symlink target '$target' is not a store path"; exit 1 ;; esac # The binary should be accessible and executable through the symlink test -x "$testEnv/bin/hello" || { echo "FAIL: hello binary not executable"; exit 1; } "$testEnv/bin/hello" > /dev/null || { echo "FAIL: hello binary did not run"; exit 1; } touch $out ''; pathsToLink = pkgs.runCommand "test-buildenv-pathsToLink" { testEnv = buildEnv { name = "test-env"; paths = [ pkgs.hello ]; pathsToLink = [ "/bin" ]; }; } '' # /bin should exist test -d "$testEnv/bin" || { echo "FAIL: $testEnv/bin missing"; exit 1; } # Other directories from hello (like /share) should NOT exist test ! -e "$testEnv/share" || { echo "FAIL: $testEnv/share should not exist with pathsToLink = [\"/bin\"]"; exit 1; } touch $out ''; extraPrefix = pkgs.runCommand "test-buildenv-extraPrefix" { testEnv = buildEnv { name = "test-env"; paths = [ pkgs.hello ]; extraPrefix = "/myprefix"; }; } '' # Content should be under the extra prefix test -e "$testEnv/myprefix/bin/hello" || { echo "FAIL: $testEnv/myprefix/bin/hello missing"; exit 1; } test -x "$testEnv/myprefix/bin/hello" || { echo "FAIL: $testEnv/myprefix/bin/hello not executable"; exit 1; } # Content should NOT be at the top level test ! -e "$testEnv/bin" || { echo "FAIL: $testEnv/bin should not exist at top level with extraPrefix"; exit 1; } touch $out ''; postBuild = pkgs.runCommand "test-buildenv-postBuild" { testEnv = buildEnv { name = "test-env"; paths = [ ]; postBuild = '' echo "postBuild was here" > $out/marker ''; }; } '' # postBuild should have created the marker file test -f "$testEnv/marker" || { echo "FAIL: $testEnv/marker missing; postBuild did not run"; exit 1; } content=$(cat "$testEnv/marker") test "$content" = "postBuild was here" || { echo "FAIL: marker content wrong: $content"; exit 1; } touch $out ''; # buildEnv explicitly sets __structuredAttrs = true because builder.pl # reads all inputs from `$NIX_ATTRS_JSON_FILE`. # Verify the build succeeds even when derivationArgs tries to disable structuredAttrs. structuredAttrs-overridden = pkgs.runCommand "test-buildenv-structuredAttrs-overridden" { testEnv = buildEnv { name = "test-env-structuredAttrs"; paths = [ pkgs.hello ]; derivationArgs.__structuredAttrs = false; }; } '' test -x "$testEnv/bin/hello" || { echo "FAIL: hello not present after structuredAttrs override"; exit 1; } touch $out ''; ignoreCollisions = pkgs.runCommand "test-buildenv-ignoreCollisions" { # Two copies of hello with different priorities that collide testEnv = buildEnv { name = "test-env-ignore"; paths = [ pkgs.hello (lib.meta.setPrio 1 pkgs.hello) ]; ignoreCollisions = true; }; } '' # Should succeed because ignoreCollisions = true test -x "$testEnv/bin/hello" || { echo "FAIL: hello not present with ignoreCollisions"; exit 1; } touch $out ''; }; # buildEnv's builder.pl reads all inputs from `$NIX_ATTRS_JSON_FILE`, # which requires __structuredAttrs = true. # buildEnv explicitly forces __structuredAttrs = true. tests-structuredAttrs = { testStructuredAttrsExplicitlyFalse = { expr = (buildEnv { name = "test-env"; paths = [ ]; }).__structuredAttrs; expected = true; }; testStructuredAttrsCantBeOverriddenViaDerivationArgs = { expr = (buildEnv { name = "test-env"; paths = [ ]; derivationArgs.__structuredAttrs = false; }).__structuredAttrs; expected = true; }; }; tests = tests-name // tests-passthru-paths // tests-finalAttrs // tests-overrideAttrs // tests-passthru-merging // tests-derivationArgs // tests-structuredAttrs; in stdenvNoCC.mkDerivation (finalAttrs: { __structuredAttrs = true; name = "test-buildenv"; passthru = { inherit tests buildTests; failures = lib.runTests finalAttrs.passthru.tests; }; testResults = lib.mapAttrs (_: test: test.expr == test.expected) finalAttrs.passthru.tests; buildCommand = '' touch $out for testName in "''${!testResults[@]}"; do if [[ -n "''${testResults[$testName]}" ]]; then echo "$testName success" else echo "$testName fail" fi done '' + lib.optionalString (lib.any (v: !v) (lib.attrValues finalAttrs.testResults)) '' { echo "ERROR: tests.buildenv: Encountering failed tests." for testName in "''${!testResults[@]}"; do if [[ -z "''${testResults[$testName]}" ]]; then echo "- $testName" fi done echo "To inspect the expected and actual result, " echo ' evaluate `tests.buildenv.tests.''${testName}`.' } >&2 exit 1 ''; })