about summary refs log tree commit diff
path: root/nixpkgs/pkgs/build-support/trivial-builders
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/pkgs/build-support/trivial-builders')
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/default.nix899
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/test-overriding.nix110
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/test/concat-test.nix12
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/test/default.nix36
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/test/link-farm.nix45
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/test/references/default.nix135
-rwxr-xr-xnixpkgs/pkgs/build-support/trivial-builders/test/references/references-test.sh68
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/test/references/samples.nix30
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/test/write-shell-script.nix14
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/test/write-text-file.nix71
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/test/writeCBin.nix43
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/test/writeClosure-union.nix23
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/test/writeScriptBin.nix39
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/test/writeShellApplication.nix141
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/test/writeShellScriptBin.nix39
-rw-r--r--nixpkgs/pkgs/build-support/trivial-builders/test/writeStringReferencesToFile.nix17
16 files changed, 1722 insertions, 0 deletions
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/default.nix b/nixpkgs/pkgs/build-support/trivial-builders/default.nix
new file mode 100644
index 000000000000..1625b0c96719
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/default.nix
@@ -0,0 +1,899 @@
+{ lib, config, stdenv, stdenvNoCC, jq, lndir, runtimeShell, shellcheck-minimal }:
+
+let
+  inherit (lib)
+    optionalAttrs
+    warn
+    ;
+in
+
+rec {
+
+  # Docs in doc/build-helpers/trivial-build-helpers.chapter.md
+  # See https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-runCommand
+  runCommand = name: env: runCommandWith {
+    stdenv = stdenvNoCC;
+    runLocal = false;
+    inherit name;
+    derivationArgs = env;
+  };
+  # Docs in doc/build-helpers/trivial-build-helpers.chapter.md
+  # See https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-runCommandLocal
+  runCommandLocal = name: env: runCommandWith {
+    stdenv = stdenvNoCC;
+    runLocal = true;
+    inherit name;
+    derivationArgs = env;
+  };
+  # Docs in doc/build-helpers/trivial-build-helpers.chapter.md
+  # See https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-runCommandCC
+  runCommandCC = name: env: runCommandWith {
+    stdenv = stdenv;
+    runLocal = false;
+    inherit name;
+    derivationArgs = env;
+  };
+  # `runCommandCCLocal` left out on purpose.
+  # We shouldn’t force the user to have a cc in scope.
+
+  # TODO: Move documentation for runCommandWith to the Nixpkgs manual
+  /*
+    Generalized version of the `runCommand`-variants
+    which does customized behavior via a single
+    attribute set passed as the first argument
+    instead of having a lot of variants like
+    `runCommand*`. Additionally it allows changing
+    the used `stdenv` freely and has a more explicit
+    approach to changing the arguments passed to
+    `stdenv.mkDerivation`.
+   */
+  runCommandWith =
+    let
+      # prevent infinite recursion for the default stdenv value
+      defaultStdenv = stdenv;
+    in
+    {
+      # which stdenv to use, defaults to a stdenv with a C compiler, pkgs.stdenv
+      stdenv ? defaultStdenv
+      # whether to build this derivation locally instead of substituting
+    , runLocal ? false
+      # extra arguments to pass to stdenv.mkDerivation
+    , derivationArgs ? { }
+      # name of the resulting derivation
+    , name
+      # TODO(@Artturin): enable strictDeps always
+    }: buildCommand:
+      stdenv.mkDerivation ({
+        enableParallelBuilding = true;
+        inherit buildCommand name;
+        passAsFile = [ "buildCommand" ]
+          ++ (derivationArgs.passAsFile or [ ]);
+      }
+      // lib.optionalAttrs (! derivationArgs?meta) {
+        pos = let args = builtins.attrNames derivationArgs; in
+          if builtins.length args > 0
+          then builtins.unsafeGetAttrPos (builtins.head args) derivationArgs
+          else null;
+      }
+      // (lib.optionalAttrs runLocal {
+        preferLocalBuild = true;
+        allowSubstitutes = false;
+      })
+      // builtins.removeAttrs derivationArgs [ "passAsFile" ]);
+
+
+  # Docs in doc/build-helpers/trivial-build-helpers.chapter.md
+  # See https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-writeTextFile
+  writeTextFile =
+    { name
+    , text
+    , executable ? false
+    , destination ? ""
+    , checkPhase ? ""
+    , meta ? { }
+    , allowSubstitutes ? false
+    , preferLocalBuild ? true
+    , derivationArgs ? { }
+    }:
+    let
+      matches = builtins.match "/bin/([^/]+)" destination;
+    in
+    runCommand name
+      ({
+        inherit text executable checkPhase allowSubstitutes preferLocalBuild;
+        passAsFile = [ "text" ]
+          ++ derivationArgs.passAsFile or [ ];
+        meta = lib.optionalAttrs (executable && matches != null)
+          {
+            mainProgram = lib.head matches;
+          } // meta // derivationArgs.meta or {};
+      } // removeAttrs derivationArgs [ "passAsFile" "meta" ])
+      ''
+        target=$out${lib.escapeShellArg destination}
+        mkdir -p "$(dirname "$target")"
+
+        if [ -e "$textPath" ]; then
+          mv "$textPath" "$target"
+        else
+          echo -n "$text" > "$target"
+        fi
+
+        if [ -n "$executable" ]; then
+          chmod +x "$target"
+        fi
+
+        eval "$checkPhase"
+      '';
+
+  # See doc/build-helpers/trivial-build-helpers.chapter.md
+  # or https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-text-writing
+  writeText = name: text: writeTextFile { inherit name text; };
+
+  # See doc/build-helpers/trivial-build-helpers.chapter.md
+  # or https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-text-writing
+  writeTextDir = path: text: writeTextFile {
+    inherit text;
+    name = builtins.baseNameOf path;
+    destination = "/${path}";
+  };
+
+  # See doc/build-helpers/trivial-build-helpers.chapter.md
+  # or https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-text-writing
+  writeScript = name: text: writeTextFile { inherit name text; executable = true; };
+
+  # See doc/build-helpers/trivial-build-helpers.chapter.md
+  # or https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-text-writing
+  writeScriptBin = name: text: writeTextFile {
+    inherit name text;
+    executable = true;
+    destination = "/bin/${name}";
+  };
+
+  # See doc/build-helpers/trivial-build-helpers.chapter.md
+  # or https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-text-writing
+  writeShellScript = name: text:
+    writeTextFile {
+      inherit name;
+      executable = true;
+      text = ''
+        #!${runtimeShell}
+        ${text}
+      '';
+      checkPhase = ''
+        ${stdenv.shellDryRun} "$target"
+      '';
+    };
+
+  # See doc/build-helpers/trivial-build-helpers.chapter.md
+  # or https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-text-writing
+  writeShellScriptBin = name: text:
+    writeTextFile {
+      inherit name;
+      executable = true;
+      destination = "/bin/${name}";
+      text = ''
+        #!${runtimeShell}
+        ${text}
+      '';
+      checkPhase = ''
+        ${stdenv.shellDryRun} "$target"
+      '';
+      meta.mainProgram = name;
+    };
+
+  # TODO: move parameter documentation to the Nixpkgs manual
+  # See doc/build-helpers/trivial-build-helpers.chapter.md
+  # or https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-writeShellApplication
+  writeShellApplication =
+    {
+      /*
+         The name of the script to write.
+
+         Type: String
+       */
+      name,
+      /*
+         The shell script's text, not including a shebang.
+
+         Type: String
+       */
+      text,
+      /*
+         Inputs to add to the shell script's `$PATH` at runtime.
+
+         Type: [String|Derivation]
+       */
+      runtimeInputs ? [ ],
+      /*
+         Extra environment variables to set at runtime.
+
+         Type: AttrSet
+       */
+      runtimeEnv ? null,
+      /*
+         `stdenv.mkDerivation`'s `meta` argument.
+
+         Type: AttrSet
+       */
+      meta ? { },
+      /*
+         The `checkPhase` to run. Defaults to `shellcheck` on supported
+         platforms and `bash -n`.
+
+         The script path will be given as `$target` in the `checkPhase`.
+
+         Type: String
+       */
+      checkPhase ? null,
+      /*
+         Checks to exclude when running `shellcheck`, e.g. `[ "SC2016" ]`.
+
+         See <https://www.shellcheck.net/wiki/> for a list of checks.
+
+         Type: [String]
+       */
+      excludeShellChecks ? [ ],
+      /*
+         Extra command-line flags to pass to ShellCheck.
+
+         Type: [String]
+       */
+      extraShellCheckFlags ? [ ],
+      /*
+         Bash options to activate with `set -o` at the start of the script.
+
+         Defaults to `[ "errexit" "nounset" "pipefail" ]`.
+
+         Type: [String]
+       */
+      bashOptions ? [ "errexit" "nounset" "pipefail" ],
+      /* Extra arguments to pass to `stdenv.mkDerivation`.
+
+         :::{.caution}
+         Certain derivation attributes are used internally,
+         overriding those could cause problems.
+         :::
+
+         Type: AttrSet
+       */
+      derivationArgs ? { },
+    }:
+    writeTextFile {
+      inherit name meta derivationArgs;
+      executable = true;
+      destination = "/bin/${name}";
+      allowSubstitutes = true;
+      preferLocalBuild = false;
+      text = ''
+        #!${runtimeShell}
+        ${lib.concatMapStringsSep "\n" (option: "set -o ${option}") bashOptions}
+      '' + lib.optionalString (runtimeEnv != null)
+        (lib.concatStrings
+          (lib.mapAttrsToList
+            (name: value: ''
+              ${lib.toShellVar name value}
+              export ${name}
+            '')
+            runtimeEnv))
+      + lib.optionalString (runtimeInputs != [ ]) ''
+
+        export PATH="${lib.makeBinPath runtimeInputs}:$PATH"
+      '' + ''
+
+        ${text}
+      '';
+
+      checkPhase =
+        # GHC (=> shellcheck) isn't supported on some platforms (such as risc-v)
+        # but we still want to use writeShellApplication on those platforms
+        let
+          shellcheckSupported = lib.meta.availableOn stdenv.buildPlatform shellcheck-minimal.compiler;
+          excludeFlags = lib.optionals (excludeShellChecks != [ ]) [ "--exclude" (lib.concatStringsSep "," excludeShellChecks) ];
+          shellcheckCommand = lib.optionalString shellcheckSupported ''
+            # use shellcheck which does not include docs
+            # pandoc takes long to build and documentation isn't needed for just running the cli
+            ${lib.getExe shellcheck-minimal} ${lib.escapeShellArgs (excludeFlags ++ extraShellCheckFlags)} "$target"
+          '';
+        in
+        if checkPhase == null then ''
+          runHook preCheck
+          ${stdenv.shellDryRun} "$target"
+          ${shellcheckCommand}
+          runHook postCheck
+        ''
+        else checkPhase;
+    };
+
+  # Create a C binary
+  # TODO: add to writers? pkgs/build-support/writers
+  writeCBin = pname: code:
+    runCommandCC pname
+      {
+        inherit pname code;
+        executable = true;
+        passAsFile = [ "code" ];
+        # Pointless to do this on a remote machine.
+        preferLocalBuild = true;
+        allowSubstitutes = false;
+        meta = {
+          mainProgram = pname;
+        };
+      }
+      ''
+        n=$out/bin/${pname}
+        mkdir -p "$(dirname "$n")"
+        mv "$codePath" code.c
+        $CC -x c code.c -o "$n"
+      '';
+
+  # TODO: deduplicate with documentation in doc/build-helpers/trivial-build-helpers.chapter.md
+  #       see also https://github.com/NixOS/nixpkgs/pull/249721
+  # See https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-concatText
+  /* concat a list of files to the nix store.
+    The contents of files are added to the file in the store.
+
+    Example:
+
+
+    # Writes my-file to /nix/store/<store path>
+    concatTextFile {
+      name = "my-file";
+      files = [ drv1 "${drv2}/path/to/file" ];
+    }
+
+
+    See also the `concatText` helper function below.
+
+
+    # Writes executable my-file to /nix/store/<store path>/bin/my-file
+    concatTextFile {
+      name = "my-file";
+      files = [ drv1 "${drv2}/path/to/file" ];
+      executable = true;
+      destination = "/bin/my-file";
+    }
+
+
+   */
+  concatTextFile =
+    { name # the name of the derivation
+    , files
+    , executable ? false # run chmod +x ?
+    , destination ? ""   # relative path appended to $out eg "/bin/foo"
+    , checkPhase ? ""    # syntax checks, e.g. for scripts
+    , meta ? { }
+    }:
+    runCommandLocal name
+      { inherit files executable checkPhase meta destination; }
+      ''
+        file=$out$destination
+        mkdir -p "$(dirname "$file")"
+        cat $files > "$file"
+
+        if [ -n "$executable" ]; then
+          chmod +x "$file"
+        fi
+
+        eval "$checkPhase"
+      '';
+
+  # TODO: deduplicate with documentation in doc/build-helpers/trivial-build-helpers.chapter.md
+  #       see also https://github.com/NixOS/nixpkgs/pull/249721
+  # See https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-concatText
+  /*
+    Writes a text file to nix store with no optional parameters available.
+
+    Example:
+
+
+    # Writes contents of files to /nix/store/<store path>
+    concatText "my-file" [ file1 file2 ]
+
+
+  */
+  concatText = name: files: concatTextFile { inherit name files; };
+
+  # TODO: deduplicate with documentation in doc/build-helpers/trivial-build-helpers.chapter.md
+  #       see also https://github.com/NixOS/nixpkgs/pull/249721
+  # See https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-concatText
+  /*
+    Writes a text file to nix store with and mark it as executable.
+
+    Example:
+    # Writes contents of files to /nix/store/<store path>
+    concatScript "my-file" [ file1 file2 ]
+
+  */
+  concatScript = name: files: concatTextFile { inherit name files; executable = true; };
+
+
+  /*
+    TODO: Deduplicate this documentation.
+    More docs in doc/build-helpers/trivial-build-helpers.chapter.md
+    See https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-symlinkJoin
+
+    Create a forest of symlinks to the files in `paths`.
+
+    This creates a single derivation that replicates the directory structure
+    of all the input paths.
+
+    BEWARE: it may not "work right" when the passed paths contain symlinks to directories.
+
+    Example:
+
+
+    # adds symlinks of hello to current build.
+    symlinkJoin { name = "myhello"; paths = [ pkgs.hello ]; }
+
+
+
+
+    # adds symlinks of hello and stack to current build and prints "links added"
+    symlinkJoin { name = "myexample"; paths = [ pkgs.hello pkgs.stack ]; postBuild = "echo links added"; }
+
+
+    This creates a derivation with a directory structure like the following:
+
+
+    /nix/store/sglsr5g079a5235hy29da3mq3hv8sjmm-myexample
+    |-- bin
+    |   |-- hello -> /nix/store/qy93dp4a3rqyn2mz63fbxjg228hffwyw-hello-2.10/bin/hello
+    |   `-- stack -> /nix/store/6lzdpxshx78281vy056lbk553ijsdr44-stack-2.1.3.1/bin/stack
+    `-- share
+        |-- bash-completion
+        |   `-- completions
+        |       `-- stack -> /nix/store/6lzdpxshx78281vy056lbk553ijsdr44-stack-2.1.3.1/share/bash-completion/completions/stack
+        |-- fish
+        |   `-- vendor_completions.d
+        |       `-- stack.fish -> /nix/store/6lzdpxshx78281vy056lbk553ijsdr44-stack-2.1.3.1/share/fish/vendor_completions.d/stack.fish
+    ...
+
+
+    symlinkJoin and linkFarm are similar functions, but they output
+    derivations with different structure.
+
+    symlinkJoin is used to create a derivation with a familiar directory
+    structure (top-level bin/, share/, etc), but with all actual files being symlinks to
+    the files in the input derivations.
+
+    symlinkJoin is used many places in nixpkgs to create a single derivation
+    that appears to contain binaries, libraries, documentation, etc from
+    multiple input derivations.
+
+    linkFarm is instead used to create a simple derivation with symlinks to
+    other derivations.  A derivation created with linkFarm is often used in CI
+    as a easy way to build multiple derivations at once.
+   */
+  symlinkJoin =
+    args_@{ name
+    , paths
+    , preferLocalBuild ? true
+    , allowSubstitutes ? false
+    , postBuild ? ""
+    , ...
+    }:
+    let
+      args = removeAttrs args_ [ "name" "postBuild" ]
+        // {
+        inherit preferLocalBuild allowSubstitutes;
+        passAsFile = [ "paths" ];
+      }; # pass the defaults
+    in
+    runCommand name args
+      ''
+        mkdir -p $out
+        for i in $(cat $pathsPath); do
+          ${lndir}/bin/lndir -silent $i $out
+        done
+        ${postBuild}
+      '';
+
+  # TODO: move linkFarm docs to the Nixpkgs manual
+  /*
+    Quickly create a set of symlinks to derivations.
+
+    This creates a simple derivation with symlinks to all inputs.
+
+    entries can be a list of attribute sets like
+
+    [ { name = "name" ; path = "/nix/store/..."; } ]
+
+
+    or an attribute set name -> path like:
+
+    { name = "/nix/store/..."; other = "/nix/store/..."; }
+
+
+    Example:
+
+    # Symlinks hello and stack paths in store to current $out/hello-test and
+    # $out/foobar.
+    linkFarm "myexample" [ { name = "hello-test"; path = pkgs.hello; } { name = "foobar"; path = pkgs.stack; } ]
+
+    This creates a derivation with a directory structure like the following:
+
+    /nix/store/qc5728m4sa344mbks99r3q05mymwm4rw-myexample
+    |-- foobar -> /nix/store/6lzdpxshx78281vy056lbk553ijsdr44-stack-2.1.3.1
+    `-- hello-test -> /nix/store/qy93dp4a3rqyn2mz63fbxjg228hffwyw-hello-2.10
+
+
+    See the note on symlinkJoin for the difference between linkFarm and symlinkJoin.
+   */
+  linkFarm = name: entries:
+    let
+      entries' =
+        if (lib.isAttrs entries) then entries
+        # We do this foldl to have last-wins semantics in case of repeated entries
+        else if (lib.isList entries) then lib.foldl (a: b: a // { "${b.name}" = b.path; }) { } entries
+        else throw "linkFarm entries must be either attrs or a list!";
+
+      linkCommands = lib.mapAttrsToList
+        (name: path: ''
+          mkdir -p "$(dirname ${lib.escapeShellArg "${name}"})"
+          ln -s ${lib.escapeShellArg "${path}"} ${lib.escapeShellArg "${name}"}
+        '')
+        entries';
+    in
+    runCommand name
+      {
+        preferLocalBuild = true;
+        allowSubstitutes = false;
+        passthru.entries = entries';
+      } ''
+      mkdir -p $out
+      cd $out
+      ${lib.concatStrings linkCommands}
+    '';
+
+  # TODO: move linkFarmFromDrvs docs to the Nixpkgs manual
+  /*
+    Easily create a linkFarm from a set of derivations.
+
+    This calls linkFarm with a list of entries created from the list of input
+    derivations.  It turns each input derivation into an attribute set
+    like { name = drv.name ; path = drv }, and passes this to linkFarm.
+
+    Example:
+
+    # Symlinks the hello, gcc, and ghc derivations in $out
+    linkFarmFromDrvs "myexample" [ pkgs.hello pkgs.gcc pkgs.ghc ]
+
+    This creates a derivation with a directory structure like the following:
+
+
+    /nix/store/m3s6wkjy9c3wy830201bqsb91nk2yj8c-myexample
+    |-- gcc-wrapper-9.2.0 -> /nix/store/fqhjxf9ii4w4gqcsx59fyw2vvj91486a-gcc-wrapper-9.2.0
+    |-- ghc-8.6.5 -> /nix/store/gnf3s07bglhbbk4y6m76sbh42siym0s6-ghc-8.6.5
+    `-- hello-2.10 -> /nix/store/k0ll91c4npk4lg8lqhx00glg2m735g74-hello-2.10
+
+  */
+  linkFarmFromDrvs = name: drvs:
+    let mkEntryFromDrv = drv: { name = drv.name; path = drv; };
+    in linkFarm name (map mkEntryFromDrv drvs);
+
+  # TODO: move onlyBin docs to the Nixpkgs manual
+  /*
+    Produce a derivation that links to the target derivation's `/bin`,
+    and *only* `/bin`.
+
+    This is useful when your favourite package doesn't have a separate
+    bin output and other contents of the package's output (e.g. setup
+    hooks) cause trouble when used in your environment.
+  */
+  onlyBin = drv: runCommand "${drv.name}-only-bin" { } ''
+    mkdir -p $out
+    ln -s ${lib.getBin drv}/bin $out/bin
+  '';
+
+
+  # Docs in doc/builders/special/makesetuphook.section.md
+  # See https://nixos.org/manual/nixpkgs/unstable/#sec-pkgs.makeSetupHook
+  makeSetupHook =
+    { name ? lib.warn "calling makeSetupHook without passing a name is deprecated." "hook"
+    , deps ? [ ]
+      # hooks go in nativeBuildInput so these will be nativeBuildInput
+    , propagatedBuildInputs ? [ ]
+      # these will be buildInputs
+    , depsTargetTargetPropagated ? [ ]
+    , meta ? { }
+    , passthru ? { }
+    , substitutions ? { }
+    }:
+    script:
+    runCommand name
+      (substitutions // {
+        # TODO(@Artturin:) substitutions should be inside the env attrset
+        # but users are likely passing non-substitution arguments through substitutions
+        # turn off __structuredAttrs to unbreak substituteAll
+        __structuredAttrs = false;
+        inherit meta;
+        inherit depsTargetTargetPropagated;
+        propagatedBuildInputs =
+          # remove list conditionals before 23.11
+          lib.warnIf (!lib.isList deps) "'deps' argument to makeSetupHook must be a list. content of deps: ${toString deps}"
+            (lib.warnIf (deps != [ ]) "'deps' argument to makeSetupHook is deprecated and will be removed in release 23.11., Please use propagatedBuildInputs instead. content of deps: ${toString deps}"
+              propagatedBuildInputs ++ (if lib.isList deps then deps else [ deps ]));
+        strictDeps = true;
+        # TODO 2023-01, no backport: simplify to inherit passthru;
+        passthru = passthru
+          // optionalAttrs (substitutions?passthru)
+          (warn "makeSetupHook (name = ${lib.strings.escapeNixString name}): `substitutions.passthru` is deprecated. Please set `passthru` directly."
+            substitutions.passthru);
+      })
+      (''
+        mkdir -p $out/nix-support
+        cp ${script} $out/nix-support/setup-hook
+        recordPropagatedDependencies
+      '' + lib.optionalString (substitutions != { }) ''
+        substituteAll ${script} $out/nix-support/setup-hook
+      '');
+
+
+  # Docs in doc/build-helpers/trivial-build-helpers.chapter.md
+  # See https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-writeReferencesToFile
+  # TODO: Convert to throw after Nixpkgs 24.05 branch-off.
+  writeReferencesToFile = (if config.allowAliases then lib.warn else throw)
+    "writeReferencesToFile is deprecated in favour of writeClosure"
+    (path: writeClosure [ path ]);
+
+  # Docs in doc/build-helpers/trivial-build-helpers.chapter.md
+  # See https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-writeClosure
+  writeClosure = paths: runCommand "runtime-deps"
+    {
+      # Get the cleaner exportReferencesGraph interface
+      __structuredAttrs = true;
+      exportReferencesGraph.graph = paths;
+      nativeBuildInputs = [ jq ];
+    }
+    ''
+      jq -r ".graph | map(.path) | sort | .[]" "$NIX_ATTRS_JSON_FILE" > "$out"
+    '';
+
+  # Docs in doc/build-helpers/trivial-build-helpers.chapter.md
+  # See https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-writeDirectReferencesToFile
+  writeDirectReferencesToFile = path: runCommand "runtime-references"
+    {
+      exportReferencesGraph = [ "graph" path ];
+      inherit path;
+    }
+    ''
+      touch ./references
+      while read p; do
+        read dummy
+        read nrRefs
+        if [[ $p == $path ]]; then
+          for ((i = 0; i < nrRefs; i++)); do
+            read ref;
+            echo $ref >>./references
+          done
+        else
+          for ((i = 0; i < nrRefs; i++)); do
+            read ref;
+          done
+        fi
+      done < graph
+      sort ./references >$out
+    '';
+
+  # TODO: move writeStringReferencesToFile docs to the Nixpkgs manual
+  /*
+    Extract a string's references to derivations and paths (its
+    context) and write them to a text file, removing the input string
+    itself from the dependency graph. This is useful when you want to
+    make a derivation depend on the string's references, but not its
+    contents (to avoid unnecessary rebuilds, for example).
+
+    Note that this only works as intended on Nix >= 2.3.
+   */
+  writeStringReferencesToFile = string:
+    /*
+       The basic operation this performs is to copy the string context
+       from `string` to a second string and wrap that string in a
+       derivation. However, that alone is not enough, since nothing in the
+       string refers to the output paths of the derivations/paths in its
+       context, meaning they'll be considered build-time dependencies and
+       removed from the wrapper derivation's closure. Putting the
+       necessary output paths in the new string is however not very
+       straightforward - the attrset returned by `getContext` contains
+       only references to derivations' .drv-paths, not their output
+       paths. In order to "convert" them, we try to extract the
+       corresponding paths from the original string using regex.
+    */
+    let
+      # Taken from https://github.com/NixOS/nix/blob/130284b8508dad3c70e8160b15f3d62042fc730a/src/libutil/hash.cc#L84
+      nixHashChars = "0123456789abcdfghijklmnpqrsvwxyz";
+      context = builtins.getContext string;
+      derivations = lib.filterAttrs (n: v: v ? outputs) context;
+      # Objects copied from outside of the store, such as paths and
+      # `builtins.fetch*`ed ones
+      sources = lib.attrNames (lib.filterAttrs (n: v: v ? path) context);
+      packages =
+        lib.mapAttrs'
+          (name: value:
+            {
+              inherit value;
+              name = lib.head (builtins.match "${builtins.storeDir}/[${nixHashChars}]+-(.*)\.drv" name);
+            })
+          derivations;
+      # The syntax of output paths differs between outputs named `out`
+      # and other, explicitly named ones. For explicitly named ones,
+      # the output name is suffixed as `-name`, but `out` outputs
+      # aren't suffixed at all, and thus aren't easily distinguished
+      # from named output paths. Therefore, we find all the named ones
+      # first so we can use them to remove false matches when looking
+      # for `out` outputs (see the definition of `outputPaths`).
+      namedOutputPaths =
+        lib.flatten
+          (lib.mapAttrsToList
+            (name: value:
+              (map
+                (output:
+                  lib.filter
+                    lib.isList
+                    (builtins.split "(${builtins.storeDir}/[${nixHashChars}]+-${name}-${output})" string))
+                (lib.remove "out" value.outputs)))
+            packages);
+      # Only `out` outputs
+      outputPaths =
+        lib.flatten
+          (lib.mapAttrsToList
+            (name: value:
+              if lib.elem "out" value.outputs then
+                lib.filter
+                  (x: lib.isList x &&
+                    # If the matched path is in `namedOutputPaths`,
+                    # it's a partial match of an output path where
+                    # the output name isn't `out`
+                    lib.all (o: !lib.hasPrefix (lib.head x) o) namedOutputPaths)
+                  (builtins.split "(${builtins.storeDir}/[${nixHashChars}]+-${name})" string)
+              else
+                [ ])
+            packages);
+      allPaths = lib.concatStringsSep "\n" (lib.unique (sources ++ namedOutputPaths ++ outputPaths));
+      allPathsWithContext = builtins.appendContext allPaths context;
+    in
+    if builtins ? getContext then
+      writeText "string-references" allPathsWithContext
+    else
+      writeDirectReferencesToFile (writeText "string-file" string);
+
+
+  # Docs in doc/build-helpers/fetchers.chapter.md
+  # See https://nixos.org/manual/nixpkgs/unstable/#requirefile
+  requireFile =
+    { name ? null
+    , sha256 ? null
+    , sha1 ? null
+    , hash ? null
+    , url ? null
+    , message ? null
+    , hashMode ? "flat"
+    }:
+      assert (message != null) || (url != null);
+      assert (sha256 != null) || (sha1 != null) || (hash != null);
+      assert (name != null) || (url != null);
+      let
+        msg =
+          if message != null then message
+          else ''
+            Unfortunately, we cannot download file ${name_} automatically.
+            Please go to ${url} to download it yourself, and add it to the Nix store
+            using either
+              nix-store --add-fixed ${hashAlgo} ${name_}
+            or
+              nix-prefetch-url --type ${hashAlgo} file:///path/to/${name_}
+          '';
+        hashAlgo =
+          if hash != null then (builtins.head (lib.strings.splitString "-" hash))
+          else if sha256 != null then "sha256"
+          else "sha1";
+        hashAlgo_ = if hash != null then "" else hashAlgo;
+        hash_ =
+          if hash != null then hash
+          else if sha256 != null then sha256
+          else sha1;
+        name_ = if name == null then baseNameOf (toString url) else name;
+      in
+      stdenvNoCC.mkDerivation {
+        name = name_;
+        outputHashMode = hashMode;
+        outputHashAlgo = hashAlgo_;
+        outputHash = hash_;
+        preferLocalBuild = true;
+        allowSubstitutes = false;
+        builder = writeScript "restrict-message" ''
+          source ${stdenvNoCC}/setup
+          cat <<_EOF_
+
+          ***
+          ${msg}
+          ***
+
+          _EOF_
+          exit 1
+        '';
+      };
+
+
+  # TODO: move copyPathToStore docs to the Nixpkgs manual
+  /*
+    Copy a path to the Nix store.
+    Nix automatically copies files to the store before stringifying paths.
+    If you need the store path of a file, ${copyPathToStore <path>} can be
+    shortened to ${<path>}.
+  */
+  copyPathToStore = builtins.filterSource (p: t: true);
+
+
+  # TODO: move copyPathsToStore docs to the Nixpkgs manual
+  /*
+    Copy a list of paths to the Nix store.
+  */
+  copyPathsToStore = builtins.map copyPathToStore;
+
+  # TODO: move applyPatches docs to the Nixpkgs manual
+  /* Applies a list of patches to a source directory.
+
+    Example:
+
+    # Patching nixpkgs:
+
+    applyPatches {
+      src = pkgs.path;
+      patches = [
+        (pkgs.fetchpatch {
+          url = "https://github.com/NixOS/nixpkgs/commit/1f770d20550a413e508e081ddc08464e9d08ba3d.patch";
+          sha256 = "1nlzx171y3r3jbk0qhvnl711kmdk57jlq4na8f8bs8wz2pbffymr";
+        })
+      ];
+    }
+
+   */
+  applyPatches =
+    { src
+    , name ? (if builtins.typeOf src == "path"
+      then builtins.baseNameOf src
+      else
+        if builtins.isAttrs src && builtins.hasAttr "name" src
+        then src.name
+        else throw "applyPatches: please supply a `name` argument because a default name can only be computed when the `src` is a path or is an attribute set with a `name` attribute."
+      ) + "-patched"
+    , patches ? [ ]
+    , prePatch ? ""
+    , postPatch ? ""
+    , ...
+    }@args:
+    if patches == [ ] && prePatch == "" && postPatch == ""
+    then src # nothing to do, so use original src to avoid additional drv
+    else stdenvNoCC.mkDerivation
+      ({
+        inherit name src patches prePatch postPatch;
+        preferLocalBuild = true;
+        allowSubstitutes = false;
+        phases = "unpackPhase patchPhase installPhase";
+        installPhase = "cp -R ./ $out";
+      }
+      # Carry `meta` information from the underlying `src` if present.
+      // (optionalAttrs (src?meta) { inherit (src) meta; })
+      // (removeAttrs args [ "src" "name" "patches" "prePatch" "postPatch" ]));
+
+  # TODO: move docs to Nixpkgs manual
+  /* An immutable file in the store with a length of 0 bytes. */
+  emptyFile = runCommand "empty-file"
+    {
+      outputHashAlgo = "sha256";
+      outputHashMode = "recursive";
+      outputHash = "0ip26j2h11n1kgkz36rl4akv694yz65hr72q4kv4b3lxcbi65b3p";
+      preferLocalBuild = true;
+    } "touch $out";
+
+  # TODO: move docs to Nixpkgs manual
+  /* An immutable empty directory in the store. */
+  emptyDirectory = runCommand "empty-directory"
+    {
+      outputHashAlgo = "sha256";
+      outputHashMode = "recursive";
+      outputHash = "0sjjj9z1dhilhpc8pq4154czrb79z9cm044jvn75kxcjv6v5l2m5";
+      preferLocalBuild = true;
+    } "mkdir $out";
+}
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test-overriding.nix b/nixpkgs/pkgs/build-support/trivial-builders/test-overriding.nix
new file mode 100644
index 000000000000..a16bbbee1b1b
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test-overriding.nix
@@ -0,0 +1,110 @@
+# Check that overriding works for trivial-builders like
+# `writeShellScript` via `overrideAttrs`. This is useful
+# to override the `checkPhase`, e. g. if you want
+# to disable extglob in `writeShellScript`.
+#
+# Run using `nix-build -A tests.trivial-builders.overriding`.
+{ lib
+, stdenv
+, runtimeShell
+, runCommand
+, callPackage
+, writeShellScript
+, writeTextFile
+, writeShellScriptBin
+}:
+
+let
+  extglobScript = ''
+    shopt -s extglob
+    touch success
+    echo @(success|failure)
+    rm success
+  '';
+
+  simpleCase = case:
+    writeShellScript "test-trivial-overriding-${case}" extglobScript;
+
+  callPackageCase = case: callPackage (
+    { writeShellScript }:
+    writeShellScript "test-trivial-callpackage-overriding-${case}" extglobScript
+  ) { };
+
+  binCase = case:
+    writeShellScriptBin "test-trivial-overriding-bin-${case}" extglobScript;
+
+  # building this derivation would fail without overriding
+  textFileCase = writeTextFile {
+    name = "test-trivial-overriding-text-file";
+    checkPhase = "false";
+    text = ''
+      #!${runtimeShell}
+      echo success
+    '';
+    executable = true;
+  };
+
+    disallowExtglob = x: x.overrideAttrs (_: {
+      checkPhase = ''
+        ${stdenv.shell} -n "$target"
+      '';
+    });
+
+    # Run old checkPhase, but only succeed if it fails.
+    # This HACK is required because we can't introspect build failures
+    # in nix: With `assertFail` we want to make sure that the default
+    # `checkPhase` would fail if extglob was used in the script.
+    assertFail = x: x.overrideAttrs (old: {
+      checkPhase = ''
+        if
+          ${old.checkPhase}
+        then exit 1; fi
+      '';
+    });
+
+  mkCase = case: outcome: isBin:
+    let
+      drv = lib.pipe outcome ([ case ] ++ lib.optionals (outcome == "fail") [ disallowExtglob assertFail ]);
+    in if isBin then "${drv}/bin/${drv.name}" else drv;
+
+  writeTextOverrides = {
+    # Make sure extglob works by default
+    simpleSucc = mkCase simpleCase "succ" false;
+    # Ensure it's possible to fail; in this case extglob is not enabled
+    simpleFail = mkCase simpleCase "fail" false;
+    # Do the same checks after wrapping with callPackage
+    # to make sure callPackage doesn't mess with the override
+    callpSucc = mkCase callPackageCase "succ" false;
+    callpFail = mkCase callPackageCase "fail" false;
+    # Do the same check using `writeShellScriptBin`
+    binSucc = mkCase binCase "succ" true;
+    binFail = mkCase binCase "fail" true;
+    # Check that we can also override plain writeTextFile
+    textFileSuccess = textFileCase.overrideAttrs (_: {
+      checkPhase = "true";
+    });
+  };
+
+  # `runTest` forces nix to build the script of our test case and
+  # run its `checkPhase` which is our main interest. Additionally
+  # it executes the script and thus makes sure that extglob also
+  # works at run time.
+  runTest = script:
+    let
+      name = script.name or (builtins.baseNameOf script);
+    in writeShellScript "run-${name}" ''
+      if [ "$(${script})" != "success" ]; then
+        echo "Failed in ${name}"
+        exit 1
+      fi
+    '';
+in
+
+runCommand "test-writeShellScript-overriding" {
+  passthru = { inherit writeTextOverrides; };
+} ''
+  ${lib.concatMapStrings (test: ''
+      ${runTest test}
+    '') (lib.attrValues writeTextOverrides)}
+  touch "$out"
+''
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test/concat-test.nix b/nixpkgs/pkgs/build-support/trivial-builders/test/concat-test.nix
new file mode 100644
index 000000000000..5ce435619069
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test/concat-test.nix
@@ -0,0 +1,12 @@
+{ callPackage, lib, pkgs, runCommand, concatText, writeText, hello, emptyFile }:
+let
+  stri = writeText "pathToTest";
+  txt1 = stri "abc";
+  txt2 = stri hello;
+  res = concatText "textToTest" [ txt1 txt2 ];
+in
+runCommand "test-concatPaths" { } ''
+  diff -U3 <(cat ${txt1} ${txt2}) ${res}
+  diff -U3 ${concatText "void" []} ${emptyFile}
+  touch $out
+''
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test/default.nix b/nixpkgs/pkgs/build-support/trivial-builders/test/default.nix
new file mode 100644
index 000000000000..e1ed0be72bf3
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test/default.nix
@@ -0,0 +1,36 @@
+/*
+  Run all tests with:
+
+      cd nixpkgs
+      nix-build -A tests.trivial-builders
+
+  or run a specific test with:
+
+      cd nixpkgs
+      nix-build -A tests.trivial-builders.foo
+
+*/
+
+{ callPackage, lib, stdenv }:
+let
+  inherit (lib) recurseIntoAttrs;
+  references = callPackage ./references {};
+in
+recurseIntoAttrs {
+  concat = callPackage ./concat-test.nix {};
+  linkFarm = callPackage ./link-farm.nix {};
+  overriding = callPackage ../test-overriding.nix {};
+  inherit references;
+  writeCBin = callPackage ./writeCBin.nix {};
+  writeClosure-union = callPackage ./writeClosure-union.nix {
+    inherit (references) samples;
+  };
+  writeShellApplication = callPackage ./writeShellApplication.nix {};
+  writeScriptBin = callPackage ./writeScriptBin.nix {};
+  writeShellScript = callPackage ./write-shell-script.nix {};
+  writeShellScriptBin = callPackage ./writeShellScriptBin.nix {};
+  writeStringReferencesToFile = callPackage ./writeStringReferencesToFile.nix {
+    inherit (references) samples;
+  };
+  writeTextFile = callPackage ./write-text-file.nix {};
+}
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test/link-farm.nix b/nixpkgs/pkgs/build-support/trivial-builders/test/link-farm.nix
new file mode 100644
index 000000000000..1ebfc707632f
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test/link-farm.nix
@@ -0,0 +1,45 @@
+{ linkFarm, hello, writeTextFile, runCommand }:
+let
+  foo = writeTextFile {
+    name = "foo";
+    text = "foo";
+  };
+
+  linkFarmFromList = linkFarm "linkFarmFromList" [
+    { name = "foo"; path = foo; }
+    { name = "hello"; path = hello; }
+  ];
+
+  linkFarmWithRepeats = linkFarm "linkFarmWithRepeats" [
+    { name = "foo"; path = foo; }
+    { name = "hello"; path = hello; }
+    { name = "foo"; path = hello; }
+  ];
+
+  linkFarmFromAttrs = linkFarm "linkFarmFromAttrs" {
+    inherit foo hello;
+  };
+in
+runCommand "test-linkFarm" { } ''
+  function assertPathEquals() {
+    local a b;
+    a="$(realpath "$1")"
+    b="$(realpath "$2")"
+    if [ "$a" != "$b" ]; then
+      echo "path mismatch!"
+      echo "a: $1 -> $a"
+      echo "b: $2 -> $b"
+      exit 1
+    fi
+  }
+
+  assertPathEquals "${linkFarmFromList}/foo" "${foo}"
+  assertPathEquals "${linkFarmFromList}/hello" "${hello}"
+
+  assertPathEquals "${linkFarmWithRepeats}/foo" "${hello}"
+  assertPathEquals "${linkFarmWithRepeats}/hello" "${hello}"
+
+  assertPathEquals "${linkFarmFromAttrs}/foo" "${foo}"
+  assertPathEquals "${linkFarmFromAttrs}/hello" "${hello}"
+  touch $out
+''
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test/references/default.nix b/nixpkgs/pkgs/build-support/trivial-builders/test/references/default.nix
new file mode 100644
index 000000000000..85df57ea210c
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test/references/default.nix
@@ -0,0 +1,135 @@
+{ lib
+, stdenvNoCC
+, testers
+, callPackage
+, writeText
+  # nativeBuildInputs
+, shellcheck-minimal
+  # Samples
+, samples ? cleanSamples (callPackage ./samples.nix { })
+  # Filter out the non-string-like attributes such as <pkg>.override added by
+  # callPackage.
+, cleanSamples ? lib.filterAttrs (n: lib.isStringLike)
+  # Test targets
+, writeDirectReferencesToFile
+, writeClosure
+}:
+
+# -------------------------------------------------------------------------- #
+#
+#                         trivial-builders test
+#
+# -------------------------------------------------------------------------- #
+#
+# Execute this build script directly (quick):
+#
+# * Classic
+#   $ NIX_PATH="nixpkgs=$PWD" nix-shell -p tests.trivial-builders.references.testScriptBin --run references-test
+#
+# * Flake-based
+#   $ nix run .#tests.trivial-builders.references.testScriptBin
+#
+# or in the build sandbox with a ~20s VM overhead:
+#
+# * Classic
+#   $ nix-build --no-out-link -A tests.trivial-builders.references
+#
+# * Flake-based
+#   $ nix build -L --no-link .#tests.trivial-builders.references
+#
+# -------------------------------------------------------------------------- #
+
+let
+  # Map each attribute to an element specification of Bash associative arrary
+  # and concatenate them with white spaces, to be used to define a
+  # one-line Bash associative array.
+  samplesToString = attrs:
+    lib.concatMapStringsSep " " (name: "[${name}]=${lib.escapeShellArg "${attrs.${name}}"}") (builtins.attrNames attrs);
+
+  closures = lib.mapAttrs (n: v: writeClosure [ v ]) samples;
+  directReferences = lib.mapAttrs (n: v: writeDirectReferencesToFile v) samples;
+  collectiveClosure = writeClosure (lib.attrValues samples);
+
+  testScriptBin = stdenvNoCC.mkDerivation (finalAttrs: {
+    name = "references-test";
+
+    src = ./references-test.sh;
+    dontUnpack = true;
+    dontBuild = true;
+
+    installPhase = ''
+      runHook preInstall
+      mkdir -p "$out/bin"
+      substitute "$src" "$out/bin/${finalAttrs.meta.mainProgram}" \
+        --replace "@SAMPLES@" ${lib.escapeShellArg (samplesToString samples)} \
+        --replace "@CLOSURES@" ${lib.escapeShellArg (samplesToString closures)} \
+        --replace "@DIRECT_REFS@" ${lib.escapeShellArg (samplesToString directReferences)} \
+        --replace "@COLLECTIVE_CLOSURE@" ${lib.escapeShellArg collectiveClosure}
+      runHook postInstall
+      chmod +x "$out/bin/${finalAttrs.meta.mainProgram}"
+    '';
+
+    doInstallCheck = true;
+    nativeInstallCheckInputs = [
+      shellcheck-minimal
+    ];
+    installCheckPhase = ''
+      runHook preInstallCheck
+      shellcheck "$out/bin/${finalAttrs.meta.mainProgram}"
+      runHook postInstallCheck
+    '';
+
+    passthru = {
+      inherit
+        collectiveClosure
+        directReferences
+        closures
+        samples
+        ;
+    };
+
+    meta = with lib; {
+      mainProgram = "references-test";
+    };
+  });
+in
+testers.runNixOSTest ({ config, lib, ... }:
+let
+  # Use the testScriptBin from guest pkgs.
+  # The attribute path to access the guest version of testScriptBin is
+  # tests.trivial-builders.references.config.node.pkgs.tests.trivial-builders.references.testScriptBin
+  # which is why passthru.guestTestScriptBin is provided.
+  guestTestScriptBin = config.node.pkgs.tests.trivial-builders.references.testScriptBin;
+in
+{
+  name = "nixpkgs-trivial-builders-references";
+  nodes.machine = { config, lib, pkgs, ... }: {
+    virtualisation.writableStore = true;
+
+    # Test runs without network, so we don't substitute and prepare our deps
+    nix.settings.substituters = lib.mkForce [ ];
+    system.extraDependencies = [ guestTestScriptBin ];
+  };
+  testScript =
+    ''
+      machine.succeed("""
+        ${lib.getExe guestTestScriptBin} 2>/dev/console
+      """)
+    '';
+  passthru = {
+    inherit
+      collectiveClosure
+      directReferences
+      closures
+      samples
+      testScriptBin
+      ;
+    inherit guestTestScriptBin;
+  };
+  meta = {
+    maintainers = with lib.maintainers; [
+      roberth
+      ShamrockLee
+    ];
+  };
+})
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test/references/references-test.sh b/nixpkgs/pkgs/build-support/trivial-builders/test/references/references-test.sh
new file mode 100755
index 000000000000..92e4467287ed
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test/references/references-test.sh
@@ -0,0 +1,68 @@
+#!/usr/bin/env bash
+
+# -------------------------------------------------------------------------- #
+#
+#                         trivial-builders test
+#
+# -------------------------------------------------------------------------- #
+#
+# Execute this build script directly (quick):
+#
+# * Classic
+#   $ NIX_PATH="nixpkgs=$PWD" nix-shell -p tests.trivial-builders.references.testScriptBin --run references-test
+#
+# * Flake-based
+#   $ nix run .#tests.trivial-builders.references.testScriptBin
+#
+# or in the build sandbox with a ~20s VM overhead:
+#
+# * Classic
+#   $ nix-build --no-out-link -A tests.trivial-builders.references
+#
+# * Flake-based
+#   $ nix build -L --no-link .#tests.trivial-builders.references
+#
+# -------------------------------------------------------------------------- #
+
+# strict bash
+set -euo pipefail
+
+# debug
+# set -x
+# PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
+
+cd "$(dirname "${BASH_SOURCE[0]}")"  # nixpkgs root
+
+  # Inject the path to compare from the Nix expression
+
+  # Associative Arrays
+  declare -A samples=( @SAMPLES@ )
+  declare -A directRefs=( @DIRECT_REFS@ )
+  declare -A closures=( @CLOSURES@ )
+
+  # Path string
+  collectiveClosure=@COLLECTIVE_CLOSURE@
+
+echo >&2 Testing direct closures...
+for i in "${!samples[@]}"; do
+  echo >&2 Checking "$i" "${samples[$i]}" "${directRefs[$i]}"
+  diff -U3 \
+    <(sort <"${directRefs[$i]}") \
+    <(nix-store -q --references "${samples[$i]}" | sort)
+done
+
+echo >&2 Testing closure...
+for i in "${!samples[@]}"; do
+  echo >&2 Checking "$i" "${samples[$i]}" "${closures[$i]}"
+  diff -U3 \
+    <(sort <"${closures[$i]}") \
+    <(nix-store -q --requisites "${samples[$i]}" | sort)
+done
+
+echo >&2 Testing mixed closures...
+echo >&2 Checking all samples "(${samples[*]})" "$collectiveClosure"
+diff -U3 \
+  <(sort <"$collectiveClosure") \
+  <(nix-store -q --requisites "${samples[@]}" | sort)
+
+echo 'OK!'
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test/references/samples.nix b/nixpkgs/pkgs/build-support/trivial-builders/test/references/samples.nix
new file mode 100644
index 000000000000..3afb970c08ae
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test/references/samples.nix
@@ -0,0 +1,30 @@
+{ lib
+, runCommand
+, writeText
+, emptyFile
+, emptyDirectory
+, figlet
+, hello
+, zlib
+}:
+{
+  inherit
+    figlet
+    hello
+    zlib
+    ;
+  zlib-dev = zlib.dev;
+  norefs = writeText "hi" "hello";
+  norefsDup = writeText "hi" "hello";
+  helloRef = writeText "hi" "hello ${hello}";
+  helloRefDup = writeText "hi" "hello ${hello}";
+  path = ./samples.nix;
+  pathLike.outPath = ./samples.nix;
+  helloFigletRef = writeText "hi" "hello ${hello} ${figlet}";
+  selfRef = runCommand "self-ref-1" { } "echo $out >$out";
+  selfRef2 = runCommand "self-ref-2" { } ''echo "${figlet}, $out" >$out'';
+  inherit
+    emptyFile
+    emptyDirectory
+    ;
+}
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test/write-shell-script.nix b/nixpkgs/pkgs/build-support/trivial-builders/test/write-shell-script.nix
new file mode 100644
index 000000000000..a5c9f1fae42f
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test/write-shell-script.nix
@@ -0,0 +1,14 @@
+{ lib, writeShellScript }: let
+  output = "hello";
+in (writeShellScript "test-script" ''
+  echo ${lib.escapeShellArg output}
+'').overrideAttrs (old: {
+  checkPhase = old.checkPhase or "" + ''
+    expected=${lib.escapeShellArg output}
+    got=$("$target")
+    if [[ "$got" != "$expected" ]]; then
+      echo "wrong output: expected $expected, got $got"
+      exit 1
+    fi
+  '';
+})
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test/write-text-file.nix b/nixpkgs/pkgs/build-support/trivial-builders/test/write-text-file.nix
new file mode 100644
index 000000000000..2e6685c1980b
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test/write-text-file.nix
@@ -0,0 +1,71 @@
+/*
+  To run:
+
+      cd nixpkgs
+      nix-build -A tests.trivial-builders.writeTextFile
+
+  or to run an individual test case
+
+      cd nixpkgs
+      nix-build -A tests.trivial-builders.writeTextFile.foo
+*/
+{ lib, runCommand, runtimeShell, writeTextFile }:
+let
+  veryWeirdName = ''here's a name with some "bad" characters, like spaces and quotes'';
+in
+lib.recurseIntoAttrs {
+
+  different-exe-name =
+    let
+      pkg = writeTextFile {
+        name = "bar";
+        destination = "/bin/foo";
+        executable = true;
+        text = ''
+          #!${runtimeShell}
+          echo hi
+        '';
+      };
+    in
+      assert pkg.meta.mainProgram == "foo";
+      assert baseNameOf (lib.getExe pkg) == "foo";
+      assert pkg.name == "bar";
+      runCommand "test-writeTextFile-different-exe-name" {} ''
+        PATH="${lib.makeBinPath [ pkg ]}:$PATH"
+        x=$(foo)
+        [[ "$x" == hi ]]
+        touch $out
+      '';
+
+  weird-name = writeTextFile {
+    name = "weird-names";
+    destination = "/etc/${veryWeirdName}";
+    text = ''passed!'';
+    checkPhase = ''
+      # intentionally hardcode everything here, to make sure
+      # Nix does not mess with file paths
+
+      name="here's a name with some \"bad\" characters, like spaces and quotes"
+      fullPath="$out/etc/$name"
+
+      if [ -f "$fullPath" ]; then
+        echo "[PASS] File exists!"
+      else
+        echo "[FAIL] File was not created at expected path!"
+        exit 1
+      fi
+
+      content=$(<"$fullPath")
+      expected="passed!"
+
+      if [ "$content" = "$expected" ]; then
+        echo "[PASS] Contents match!"
+      else
+        echo "[FAIL] File contents don't match!"
+        echo "       Expected: $expected"
+        echo "       Got:      $content"
+        exit 2
+      fi
+    '';
+  };
+}
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test/writeCBin.nix b/nixpkgs/pkgs/build-support/trivial-builders/test/writeCBin.nix
new file mode 100644
index 000000000000..56cab45b3801
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test/writeCBin.nix
@@ -0,0 +1,43 @@
+/*
+  Run with:
+
+      cd nixpkgs
+      nix-build -A tests.trivial-builders.writeCBin
+*/
+
+{ lib, writeCBin, runCommand }:
+let
+  output = "hello";
+  pkg = writeCBin "test-script" ''
+    #include <stdio.h>
+    int main () {
+      printf("hello\n");
+      return 0;
+    }
+  '';
+in
+  assert pkg.meta.mainProgram == "test-script";
+  runCommand "test-writeCBin" { } ''
+
+    echo Testing with getExe...
+
+    target=${lib.getExe pkg}
+    expected=${lib.escapeShellArg output}
+    got=$("$target")
+    if [[ "$got" != "$expected" ]]; then
+      echo "wrong output: expected $expected, got $got"
+      exit 1
+    fi
+
+    echo Testing with makeBinPath...
+
+    PATH="${lib.makeBinPath [ pkg ]}:$PATH"
+    got=$(test-script)
+    if [[ "$got" != "$expected" ]]; then
+      echo "wrong output: expected $expected, got $got"
+      exit 1
+    fi
+
+    touch $out
+  ''
+
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test/writeClosure-union.nix b/nixpkgs/pkgs/build-support/trivial-builders/test/writeClosure-union.nix
new file mode 100644
index 000000000000..92a2bf9f0988
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test/writeClosure-union.nix
@@ -0,0 +1,23 @@
+{ lib
+, runCommandLocal
+  # Test targets
+, writeClosure
+, samples
+}:
+runCommandLocal "test-trivial-builders-writeClosure-union" {
+  __structuredAttrs = true;
+  closures = lib.mapAttrs (n: v: writeClosure [ v ]) samples;
+  collectiveClosure = writeClosure (lib.attrValues samples);
+  inherit samples;
+  meta.maintainers = with lib.maintainers; [
+    ShamrockLee
+  ];
+} ''
+  set -eu -o pipefail
+  echo >&2 Testing mixed closures...
+  echo >&2 Checking all samples "(''${samples[*]})" "$collectiveClosure"
+  diff -U3 \
+    <(sort <"$collectiveClosure") \
+    <(cat "''${closures[@]}" | sort | uniq)
+  touch "$out"
+''
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test/writeScriptBin.nix b/nixpkgs/pkgs/build-support/trivial-builders/test/writeScriptBin.nix
new file mode 100644
index 000000000000..1487443130da
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test/writeScriptBin.nix
@@ -0,0 +1,39 @@
+/*
+  Run with:
+
+      cd nixpkgs
+      nix-build -A tests.trivial-builders.writeShellScriptBin
+*/
+
+{ lib, writeScriptBin, runCommand }:
+let
+  output = "hello";
+  pkg = writeScriptBin "test-script" ''
+    echo ${lib.escapeShellArg output}
+  '';
+in
+  assert pkg.meta.mainProgram == "test-script";
+  runCommand "test-writeScriptBin" { } ''
+
+    echo Testing with getExe...
+
+    target=${lib.getExe pkg}
+    expected=${lib.escapeShellArg output}
+    got=$("$target")
+    if [[ "$got" != "$expected" ]]; then
+      echo "wrong output: expected $expected, got $got"
+      exit 1
+    fi
+
+    echo Testing with makeBinPath...
+
+    PATH="${lib.makeBinPath [ pkg ]}:$PATH"
+    got=$(test-script)
+    if [[ "$got" != "$expected" ]]; then
+      echo "wrong output: expected $expected, got $got"
+      exit 1
+    fi
+
+    touch $out
+  ''
+
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test/writeShellApplication.nix b/nixpkgs/pkgs/build-support/trivial-builders/test/writeShellApplication.nix
new file mode 100644
index 000000000000..c50f5a4d283f
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test/writeShellApplication.nix
@@ -0,0 +1,141 @@
+# Run with:
+# nix-build -A tests.trivial-builders.writeShellApplication
+{ writeShellApplication
+, writeTextFile
+, runCommand
+, lib
+, linkFarm
+, diffutils
+, hello
+}:
+let
+  checkShellApplication = args@{name, expected, ...}:
+    let
+      writeShellApplicationArgs = builtins.removeAttrs args ["expected"];
+      script = writeShellApplication writeShellApplicationArgs;
+      executable = lib.getExe script;
+      expected' = writeTextFile {
+        name = "${name}-expected";
+        text = expected;
+      };
+      actual = "${name}-actual";
+    in
+    runCommand name { } ''
+      echo "Running test executable ${name}"
+      ${executable} > ${actual}
+      echo "Got output from test executable:"
+      cat ${actual}
+      echo "Checking test output against expected output:"
+      ${diffutils}/bin/diff --color --unified ${expected'} ${actual}
+      touch $out
+    '';
+in
+linkFarm "writeShellApplication-tests" {
+  test-meta =
+    let
+      script = writeShellApplication {
+        name = "test-meta";
+        text = "";
+        meta.description = "A test for the `writeShellApplication` `meta` argument.";
+      };
+    in
+    assert script.meta.mainProgram == "test-meta";
+    assert script.meta.description == "A test for the `writeShellApplication` `meta` argument.";
+    script;
+
+  test-runtime-inputs =
+    checkShellApplication {
+      name = "test-runtime-inputs";
+      text = ''
+        hello
+      '';
+      runtimeInputs = [ hello ];
+      expected = "Hello, world!\n";
+    };
+
+  test-runtime-env =
+    checkShellApplication {
+      name = "test-runtime-env";
+      runtimeEnv = {
+        MY_COOL_ENV_VAR = "my-cool-env-value";
+        MY_OTHER_COOL_ENV_VAR = "my-other-cool-env-value";
+        # Check that we can serialize a bunch of different types:
+        BOOL = true;
+        INT = 1;
+        LIST = [1 2 3];
+        MAP = {
+          a = "a";
+          b = "b";
+        };
+      };
+      text = ''
+        echo "$MY_COOL_ENV_VAR"
+        echo "$MY_OTHER_COOL_ENV_VAR"
+      '';
+      expected = ''
+        my-cool-env-value
+        my-other-cool-env-value
+      '';
+    };
+
+  test-check-phase =
+    checkShellApplication {
+      name = "test-check-phase";
+      text = "";
+      checkPhase = ''
+        echo "echo -n hello" > $target
+      '';
+      expected = "hello";
+    };
+
+  test-argument-forwarding =
+    checkShellApplication {
+      name = "test-argument-forwarding";
+      text = "";
+      derivationArgs.MY_BUILD_TIME_VARIABLE = "puppy";
+      derivationArgs.postCheck = ''
+        if [[ "$MY_BUILD_TIME_VARIABLE" != puppy ]]; then
+          echo "\$MY_BUILD_TIME_VARIABLE is not set to 'puppy'!"
+          exit 1
+        fi
+      '';
+      meta.description = "A test checking that `writeShellApplication` forwards extra arguments to `stdenv.mkDerivation`.";
+      expected = "";
+    };
+
+  test-exclude-shell-checks = writeShellApplication {
+    name = "test-exclude-shell-checks";
+    excludeShellChecks = [ "SC2016" ];
+    text = ''
+      # Triggers SC2016: Expressions don't expand in single quotes, use double
+      # quotes for that.
+      echo '$SHELL'
+    '';
+  };
+
+  test-bash-options-pipefail = checkShellApplication {
+    name = "test-bash-options-pipefail";
+    text = ''
+      touch my-test-file
+      echo puppy | grep doggy | sed 's/doggy/puppy/g'
+      #            ^^^^^^^^^^ This will fail.
+      true
+    '';
+    # Don't use `pipefail`:
+    bashOptions = ["errexit" "nounset"];
+    expected = "";
+  };
+
+  test-bash-options-nounset = checkShellApplication {
+    name = "test-bash-options-nounset";
+    text = ''
+      echo -n "$someUndefinedVariable"
+    '';
+    # Don't use `nounset`:
+    bashOptions = [];
+    # Don't warn about the undefined variable at build time:
+    excludeShellChecks = [ "SC2154" ];
+    expected = "";
+  };
+
+}
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test/writeShellScriptBin.nix b/nixpkgs/pkgs/build-support/trivial-builders/test/writeShellScriptBin.nix
new file mode 100644
index 000000000000..e93410e25bcb
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test/writeShellScriptBin.nix
@@ -0,0 +1,39 @@
+/*
+  Run with:
+
+      cd nixpkgs
+      nix-build -A tests.trivial-builders.writeShellScriptBin
+*/
+
+{ lib, writeShellScriptBin, runCommand }:
+let
+  output = "hello";
+  pkg = writeShellScriptBin "test-script" ''
+    echo ${lib.escapeShellArg output}
+  '';
+in
+  assert pkg.meta.mainProgram == "test-script";
+  runCommand "test-writeShellScriptBin" { } ''
+
+    echo Testing with getExe...
+
+    target=${lib.getExe pkg}
+    expected=${lib.escapeShellArg output}
+    got=$("$target")
+    if [[ "$got" != "$expected" ]]; then
+      echo "wrong output: expected $expected, got $got"
+      exit 1
+    fi
+
+    echo Testing with makeBinPath...
+
+    PATH="${lib.makeBinPath [ pkg ]}:$PATH"
+    got=$(test-script)
+    if [[ "$got" != "$expected" ]]; then
+      echo "wrong output: expected $expected, got $got"
+      exit 1
+    fi
+
+    touch $out
+  ''
+
diff --git a/nixpkgs/pkgs/build-support/trivial-builders/test/writeStringReferencesToFile.nix b/nixpkgs/pkgs/build-support/trivial-builders/test/writeStringReferencesToFile.nix
new file mode 100644
index 000000000000..dedd7e183a17
--- /dev/null
+++ b/nixpkgs/pkgs/build-support/trivial-builders/test/writeStringReferencesToFile.nix
@@ -0,0 +1,17 @@
+{ callPackage, lib, pkgs, runCommand, samples, writeText, writeStringReferencesToFile }:
+let
+  samplePaths = lib.unique (lib.attrValues samples);
+  stri = x: "${x}";
+  sampleText = writeText "sample-text" (lib.concatStringsSep "\n" (lib.unique (map stri samplePaths)));
+  stringReferencesText =
+    writeStringReferencesToFile
+      ((lib.concatMapStringsSep "fillertext"
+        stri
+        (lib.attrValues samples)) + ''
+        STORE=${builtins.storeDir};\nsystemctl start bar-foo.service
+      '');
+in
+runCommand "test-writeStringReferencesToFile" { } ''
+  diff -U3 <(sort ${stringReferencesText}) <(sort ${sampleText})
+  touch $out
+''