{
  callPackage,
  lib,
  jq,
  runCommand,
  writeText,
  python3,
  stdenvNoCC,
  makeWrapper,
  codeowners,
}:
let
  python = python3.withPackages (ps: [
    ps.numpy
    ps.pandas
    ps.scipy
    ps.tabulate
  ]);

  cmp-stats = stdenvNoCC.mkDerivation {
    pname = "cmp-stats";
    version = lib.trivial.release;

    dontUnpack = true;

    nativeBuildInputs = [ makeWrapper ];

    installPhase = ''
      runHook preInstall

      mkdir -p $out/share/cmp-stats

      cp ${./cmp-stats.py} "$out/share/cmp-stats/cmp-stats.py"

      makeWrapper ${python.interpreter} "$out/bin/cmp-stats" \
          --add-flags "$out/share/cmp-stats/cmp-stats.py"

      runHook postInstall
    '';

    meta = {
      description = "Performance comparison of Nix evaluation statistics";
      license = lib.licenses.mit;
      mainProgram = "cmp-stats";
      maintainers = with lib.maintainers; [ philiptaron ];
    };
  };
in
{
  combinedDir,
  touchedFilesJson,
  baseBranch,
  ownersFile ? ../../OWNERS,
}:
let
  # Usually we expect a derivation, but when evaluating in multiple separate steps, we pass
  # nix store paths around. These need to be turned into (fake) derivations again to track
  # dependencies properly.
  # We use two steps for evaluation, because we compare results from two different checkouts.
  # CI additionalls spreads evaluation across multiple workers.
  combined = if lib.isDerivation combinedDir then combinedDir else lib.toDerivation combinedDir;

  /*
    Derivation that computes which packages are affected (added, changed or removed) between two revisions of nixpkgs.
    Note: "platforms" are "x86_64-linux", "aarch64-darwin", ...

    ---
    Inputs:
    - beforeDir, afterDir: The evaluation result from before and after the change.
      They can be obtained by running `nix-build -A ci.eval.full` on both revisions.

    ---
    Outputs:
      - changed-paths.json: Various information about the changes:
        {
          attrdiff: {
            added: ["package1"],
            changed: ["package2", "package3", "package4"],
            removed: ["package4"],
          },
          attrdiffByKernel: {
            darwin: {
              added: [],
              changed: ["package2", "package4"],
              removed: ["package4"],
            },
            linux: {
              added: ["package1"],
              changed: ["package3", "package4"],
              removed: [],
            },
          },
          attrdiffByPlatform: {
            aarch64-darwin: {
              added: [],
              changed: ["package2"],
              removed: ["package4"],
            },
            aarch64-linux: {
              added: ["package1"],
              changed: ["package3"],
              removed: [],
            },
            x86_64-linux: {
              added: [],
              changed: ["package4"],
              removed: [],
            },
          },
          labels: {
            "10.rebuild-darwin: 1-10": true,
            "10.rebuild-linux: 1-10": true
          },
          rebuildsByKernel: {
            darwin: ["package1", "package2"],
            linux: ["package1", "package2", "package3"]
          },
          rebuildCountByKernel: {
            darwin: 2,
            linux: 3,
          },
          rebuildsByPlatform: {
            aarch64-darwin: ["package1", "package2"],
            aarch64-linux: ["package1", "package2"],
            x86_64-linux: ["package1", "package2", "package3"],
            x86_64-darwin: ["package1"],
          },
        }
      - step-summary.md: A markdown render of the changes

    ---
    Implementation details:

    Helper functions can be found in ./utils.nix.
    Two main "types" are important:

    - `packagePlatformPath`: A string of the form "<PACKAGE_PATH>.<PLATFORM>"
      Example: "python312Packages.numpy.x86_64-linux"

    - `packagePlatformAttr`: An attrs representation of a packagePlatformPath:
      Example: { name = "python312Packages.numpy"; platform = "x86_64-linux"; }
  */
  inherit (import ./utils.nix { inherit lib; })
    groupByKernel
    convertToPackagePlatformAttrs
    groupAttrdiffByKernel
    groupAttrdiffByPlatform
    groupByPlatform
    extractPackageNames
    getLabels
    ;

  # Attrs
  # - keys: "added", "changed", "removed" and "rebuilds"
  # - values: lists of `packagePlatformPath`s
  diffAttrs = builtins.fromJSON (builtins.readFile "${combined}/combined-diff.json");

  rebuildsPackagePlatformAttrs = convertToPackagePlatformAttrs diffAttrs.rebuilds;

  changed-paths =
    let
      attrdiff = lib.mapAttrs (_: extractPackageNames) {
        inherit (diffAttrs) added changed removed;
      };
      attrdiffByPlatform = groupAttrdiffByPlatform {
        inherit (diffAttrs) added changed removed;
      };
      attrdiffByKernel = groupAttrdiffByKernel {
        inherit (diffAttrs) added changed removed;
      };
      rebuildsByPlatform = groupByPlatform rebuildsPackagePlatformAttrs;
      rebuildsByKernel = groupByKernel rebuildsPackagePlatformAttrs;
      rebuildCountByKernel = lib.mapAttrs (
        kernel: kernelRebuilds: lib.length kernelRebuilds
      ) rebuildsByKernel;
      rebuildNames = extractPackageNames diffAttrs.rebuilds;
    in
    writeText "changed-paths.json" (
      builtins.toJSON {
        inherit attrdiff attrdiffByKernel attrdiffByPlatform;
        inherit
          rebuildsByPlatform
          rebuildsByKernel
          rebuildCountByKernel
          ;
        labels =
          getLabels rebuildCountByKernel
          # Sets "10.rebuild-*-stdenv" label to whether the "stdenv" attribute was changed.
          // lib.mapAttrs' (
            kernel: rebuilds: lib.nameValuePair "10.rebuild-${kernel}-stdenv" (lib.elem "stdenv" rebuilds)
          ) rebuildsByKernel
          // {
            "10.rebuild-nixos-tests" =
              lib.elem "nixosTests.simple-container" rebuildNames || lib.elem "nixosTests.simple-vm" rebuildNames;
          };
      }
    );

  getMaintainers = callPackage ./maintainers.nix { };

  inherit
    (getMaintainers {
      affectedAttrPaths = map (a: a.packagePath) (
        convertToPackagePlatformAttrs (diffAttrs.changed ++ diffAttrs.removed)
      );
      changedFiles = lib.importJSON touchedFilesJson;
    })
    users
    teams
    packages
    ;
in
runCommand "compare"
  {
    # Don't depend on -dev outputs to reduce closure size for CI.
    nativeBuildInputs = map lib.getBin [
      jq
      cmp-stats
      codeowners
    ];
    users = builtins.toJSON users;
    teams = builtins.toJSON teams;
    packages = builtins.toJSON (lib.map (lib.concatStringsSep ".") packages);
    passAsFile = [
      "users"
      "teams"
      "packages"
    ];
  }
  ''
    mkdir $out

    cp ${changed-paths} $out/changed-paths.json

    {
      echo
      echo "# Packages"
      echo
      jq -r -f ${./generate-step-summary.jq} < ${changed-paths}
    } >> $out/step-summary.md

    {
      echo
      echo "# Performance comparison"
      echo
      echo "This compares the performance of this branch against the \`${baseBranch}\` branch."
      echo
    } >> $out/step-summary.md

    # cmp-stats only compares the stats chunks present in both revisions, so the
    # comparison is still produced when packages were added/removed. The paired
    # chunks may cover different attrs in that case, so caveat the figures.
    if ! jq -e '(.attrdiff.added | length == 0) and (.attrdiff.removed | length == 0)' "${changed-paths}" > /dev/null; then
      {
        echo "> [!NOTE]"
        echo "> The package sets differ between the two revisions. This comparison only"
        echo "> covers packages evaluated in both, so treat the figures as approximate."
        echo
      } >> $out/step-summary.md
    fi

    {
      echo "For further help please refer to: [ci/README.md](https://github.com/NixOS/nixpkgs/blob/master/ci/README.md)"
      echo
    } >> $out/step-summary.md

    cmp-stats --explain ${combined}/before/stats ${combined}/after/stats >> $out/step-summary.md

    jq -r '.[]' "${touchedFilesJson}" > ./touched-files
    readarray -t touchedFiles < ./touched-files
    echo "This PR touches ''${#touchedFiles[@]} files"

    # TODO: Move ci/OWNERS to Nix and produce owners.json instead of owners.txt.
    touch "$out/owners.txt"
    for file in "''${touchedFiles[@]}"; do
        result=$(codeowners --file "${ownersFile}" "$file")

        # Remove the file prefix and trim the surrounding spaces
        read -r owners <<< "''${result#"$file"}"
        if [[ "$owners" == "(unowned)" ]]; then
            echo "File $file is unowned"
            continue
        fi
        echo "File $file is owned by $owners"

        # Split up multiple owners, separated by arbitrary amounts of spaces
        IFS=" " read -r -a entries <<< "$owners"

        for entry in "''${entries[@]}"; do
            # GitHub technically also supports Emails as code owners,
            # but we can't easily support that, so let's not
            if [[ ! "$entry" =~ @(.*) ]]; then
                echo -e "\e[33mCodeowner \"$entry\" for file $file is not valid: Must start with \"@\"\e[0m"
                # Don't fail, because the PR for which this script runs can't fix it,
                # it has to be fixed in the base branch
                continue
            fi
            # The first regex match is everything after the @
            entry=''${BASH_REMATCH[1]}

            echo "$entry" >> "$out/owners.txt"
        done

    done

    cp "$usersPath" "$out/maintainers.json"
    cp "$teamsPath" "$out/teams.json"
    cp "$packagesPath" "$out/packages.json"
  ''
