about summary refs log tree commit diff
path: root/nixpkgs/pkgs/test/nixpkgs-check-by-name
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/pkgs/test/nixpkgs-check-by-name')
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/.envrc7
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/.gitignore2
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/Cargo.lock606
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/Cargo.toml21
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/README.md102
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/default.nix82
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/README.md38
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/pinned-tool.json4
-rwxr-xr-xnixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/run-local.sh82
-rwxr-xr-xnixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/update-pinned-tool.sh40
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/shell.nix6
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.nix116
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.rs474
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/src/main.rs266
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/src/nix_file.rs510
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/src/nixpkgs_problem.rs273
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/src/ratchet.rs173
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/src/references.rs174
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/src/structure.rs182
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/src/utils.rs95
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/src/validation.rs111
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/aliases.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/all-packages.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/pkgs/by-name/fo/foo/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/broken-autocall/default.nix4
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/broken-autocall/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/broken-autocall/pkgs/by-name/fo/foo/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/callPackage-syntax/all-packages.nix7
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/callPackage-syntax/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/callPackage-syntax/pkgs/by-name/README.md0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/empty-base/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/empty-base/pkgs/by-name/README.md0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/incorrect-shard/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/incorrect-shard/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/incorrect-shard/pkgs/by-name/aa/FOO/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/all-packages.nix4
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/foo.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/pkgs/by-name/README.md0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/pkgs/by-name/fo/foo/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-package-name/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-package-name/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-package-name/pkgs/by-name/fo/fo@/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/pkgs/by-name/A/A/.git-keep0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/pkgs/by-name/A/A/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/all-packages.nix10
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/all-packages.nix9
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/pkgs/by-name/README.md0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/some-pkg.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/expected2
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/pkgs/by-name/no/noEval/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/pkgs/by-name/on/onlyMove/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/missing-package-nix/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/missing-package-nix/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/missing-package-nix/pkgs/by-name/fo/foo/.git-keep0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/mock-nixpkgs.nix101
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/all-packages.nix10
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo1/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo2/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo3/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo4/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/expected4
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/pkgs/by-name/README.md0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/with-config.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/without-config.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/expected13
l---------nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/A/fo@/foo1
l---------nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/A/fo@/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/aa0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/ba/bar0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/ba/baz/package.nix/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/ba/foo/package.nix6
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/fo/foo/.git-keep0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/someDrv.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/all-packages.nix11
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/base/all-packages.nix5
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/base/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/base/pkgs/by-name/README.md0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/expected4
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/pkgs/by-name/README.md0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/with-config.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/without-config.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-by-name/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-by-name/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/all-packages.nix5
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/base/all-packages.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/base/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/base/pkgs/by-name/README.md0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/pkgs/by-name/README.md0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-attrs/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-attrs/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-attrs/pkgs/by-name/no/nonDerivation/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-derivation/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-derivation/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-derivation/pkgs/by-name/no/nonDerivation/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/all-packages.nix6
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/pkgs/by-name/fo/foo/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/one-letter/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/one-letter/pkgs/by-name/a/a/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/only-callPackage-derivations/all-packages.nix16
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/only-callPackage-derivations/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/only-callPackage-derivations/pkgs/by-name/README.md0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/all-packages.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/pkgs/by-name/no/nonDerivation/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/someDrv.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/all-packages.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/base/all-packages.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/base/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/base/pkgs/by-name/no/nonDerivation/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/expected0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/pkgs/by-name/no/nonDerivation/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/all-packages.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/base/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/base/pkgs/by-name/README.md0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/pkgs/by-name/no/nonDerivation/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/all-packages.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/pkgs/by-name/no/nonDerivation/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/all-packages.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/pkgs/by-name/no/nonDerivation/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-success/all-packages.nix5
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-success/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-success/pkgs/by-name/fo/foo/package.nix8
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-dir-is-file/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-dir-is-file/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-dir-is-file/pkgs/by-name/fo/foo0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-nix-dir/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-nix-dir/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-nix-dir/pkgs/by-name/fo/foo/package.nix/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/all-packages.nix5
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/base/all-packages.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/base/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/base/pkgs/by-name/fo/foo/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/pkgs/by-name/fo/foo/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-absolute/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-absolute/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-absolute/pkgs/by-name/aa/aa/package.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-escape/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-escape/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-escape/pkgs/by-name/aa/aa/package.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-nix-path/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-nix-path/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-nix-path/pkgs/by-name/aa/aa/package.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-path-subexpr/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-path-subexpr/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-path-subexpr/pkgs/by-name/aa/aa/package.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/dir/default.nix2
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/file0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/file.nix2
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/package.nix5
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/shard-file/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/shard-file/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/shard-file/pkgs/by-name/fo0
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/all-packages.nix6
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/expected4
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/pkgs/by-name/a/a/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/pkgs/by-name/c/c/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/success/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/success/pkgs/by-name/fo/foo/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/expected1
l---------nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/pkgs/by-name/fo/foo/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/someDrv.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/expected1
l---------nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/pkgs/by-name/fo/foo/foo1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/pkgs/by-name/fo/foo/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/someDrv.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/all-packages.nix3
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/expected1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/pkgs/by-name/fo/foo/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/uppercase/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/uppercase/pkgs/by-name/fo/FOO/package.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/with-readme/default.nix1
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/with-readme/pkgs/by-name/README.md0
201 files changed, 3782 insertions, 0 deletions
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/.envrc b/nixpkgs/pkgs/test/nixpkgs-check-by-name/.envrc
new file mode 100644
index 000000000000..4555ccf0921f
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/.envrc
@@ -0,0 +1,7 @@
+if has nix_direnv_watch_file; then
+  nix_direnv_watch_file default.nix shell.nix
+else
+  watch_file default.nix shell.nix
+fi
+
+use nix
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/.gitignore b/nixpkgs/pkgs/test/nixpkgs-check-by-name/.gitignore
new file mode 100644
index 000000000000..75e92a908987
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/.gitignore
@@ -0,0 +1,2 @@
+target
+.direnv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/Cargo.lock b/nixpkgs/pkgs/test/nixpkgs-check-by-name/Cargo.lock
new file mode 100644
index 000000000000..904a9cff0e78
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/Cargo.lock
@@ -0,0 +1,606 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
+
+[[package]]
+name = "cc"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d5f1946157a96594eb2d2c10eb7ad9a2b27518cb3000209dec700c35df9197d"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+ "once_cell",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78116e32a042dd73c2901f0dc30790d20ff3447f3e3472fad359e8c3d282bcd6"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "colored"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6"
+dependencies = [
+ "is-terminal",
+ "lazy_static",
+ "windows-sys",
+]
+
+[[package]]
+name = "countme"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
+
+[[package]]
+name = "either"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+
+[[package]]
+name = "errno"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
+
+[[package]]
+name = "indoc"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
+
+[[package]]
+name = "is-terminal"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
+dependencies = [
+ "hermit-abi",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "itertools"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
+
+[[package]]
+name = "lock_api"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "memoffset"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "nixpkgs-check-by-name"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "colored",
+ "indoc",
+ "itertools",
+ "lazy_static",
+ "regex",
+ "rnix",
+ "rowan",
+ "serde",
+ "serde_json",
+ "temp-env",
+ "tempfile",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "regex"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
+
+[[package]]
+name = "rnix"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb35cedbeb70e0ccabef2a31bcff0aebd114f19566086300b8f42c725fc2cb5f"
+dependencies = [
+ "rowan",
+]
+
+[[package]]
+name = "rowan"
+version = "0.15.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64449cfef9483a475ed56ae30e2da5ee96448789fb2aa240a04beb6a055078bf"
+dependencies = [
+ "countme",
+ "hashbrown",
+ "memoffset",
+ "rustc-hash",
+ "text-size",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustix"
+version = "0.38.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f"
+dependencies = [
+ "bitflags 2.4.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "2.0.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "temp-env"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e010429b1f3ea1311190c658c7570100f03c1dab05c16cfab774181c648d656a"
+dependencies = [
+ "parking_lot",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "redox_syscall",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "text-size"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/Cargo.toml b/nixpkgs/pkgs/test/nixpkgs-check-by-name/Cargo.toml
new file mode 100644
index 000000000000..5240cd69f996
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "nixpkgs-check-by-name"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+rnix = "0.11.0"
+regex = "1.9.3"
+clap = { version = "4.3.23", features = ["derive"] }
+serde_json = "1.0.105"
+tempfile = "3.8.0"
+serde = { version = "1.0.185", features = ["derive"] }
+anyhow = "1.0"
+lazy_static = "1.4.0"
+colored = "2.0.4"
+itertools = "0.11.0"
+rowan = "0.15.11"
+
+[dev-dependencies]
+temp-env = "0.3.5"
+indoc = "2.0.4"
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/README.md b/nixpkgs/pkgs/test/nixpkgs-check-by-name/README.md
new file mode 100644
index 000000000000..1aa256978416
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/README.md
@@ -0,0 +1,102 @@
+# Nixpkgs pkgs/by-name checker
+
+This directory implements a program to check the [validity](#validity-checks) of the `pkgs/by-name` Nixpkgs directory.
+This is part of the implementation of [RFC 140](https://github.com/NixOS/rfcs/pull/140).
+
+A [pinned version](./scripts/pinned-tool.json) of this tool is used by [this GitHub Actions workflow](../../../.github/workflows/check-by-name.yml).
+See [./scripts](./scripts/README.md#update-pinned-toolsh) for how to update the pinned version.
+
+The source of the tool being right inside Nixpkgs allows any Nixpkgs committer to make updates to it.
+
+## Interface
+
+The interface of the tool is shown with `--help`:
+```
+cargo run -- --help
+```
+
+The interface may be changed over time only if the CI workflow making use of it is adjusted to deal with the change appropriately.
+
+## Validity checks
+
+These checks are performed by this tool:
+
+### File structure checks
+- `pkgs/by-name` must only contain subdirectories of the form `${shard}/${name}`, called _package directories_.
+- The `name`'s of package directories must be unique when lowercased.
+- `name` is a string only consisting of the ASCII characters `a-z`, `A-Z`, `0-9`, `-` or `_`.
+- `shard` is the lowercased first two letters of `name`, expressed in Nix: `shard = toLower (substring 0 2 name)`.
+- Each package directory must contain a `package.nix` file and may contain arbitrary other files.
+
+### Nix parser checks
+- Each package directory must not refer to files outside itself using symlinks or Nix path expressions.
+
+### Nix evaluation checks
+
+Evaluate Nixpkgs with `system` set to `x86_64-linux` and check that:
+- For each package directory, the `pkgs.${name}` attribute must be defined as `callPackage pkgs/by-name/${shard}/${name}/package.nix args` for some `args`.
+- For each package directory, `pkgs.lib.isDerivation pkgs.${name}` must be `true`.
+
+### Ratchet checks
+
+Furthermore, this tool implements certain [ratchet](https://qntm.org/ratchet) checks.
+This allows gradually phasing out deprecated patterns without breaking the base branch or having to migrate it all at once.
+It works by not allowing new instances of the pattern to be introduced, but allowing already existing instances.
+The existing instances are coming from `<BASE_NIXPKGS>`, which is then checked against `<NIXPKGS>` for new instances.
+Ratchets should be removed eventually once the pattern is not used anymore.
+
+The current ratchets are:
+
+- New manual definitions of `pkgs.${name}` (e.g. in `pkgs/top-level/all-packages.nix`) with `args = { }`
+  (see [nix evaluation checks](#nix-evaluation-checks)) must not be introduced.
+- New top-level packages defined using `pkgs.callPackage` must be defined with a package directory.
+  - Once a top-level package uses `pkgs/by-name`, it also can't be moved back out of it.
+
+## Development
+
+Enter the development environment in this directory either automatically with `direnv` or with
+```
+nix-shell
+```
+
+Then use `cargo`:
+```
+cargo build
+cargo test
+cargo fmt
+cargo clippy
+```
+
+## Tests
+
+Tests are declared in [`./tests`](./tests) as subdirectories imitating Nixpkgs with these files:
+- `default.nix`:
+  Always contains
+  ```nix
+  import <test-nixpkgs> { root = ./.; }
+  ```
+  which makes
+  ```
+  nix-instantiate <subdir> --eval -A <attr> --arg overlays <overlays>
+  ```
+  work very similarly to the real Nixpkgs, just enough for the program to be able to test it.
+- `pkgs/by-name`:
+  The `pkgs/by-name` directory to check.
+
+- `all-packages.nix` (optional):
+  Contains an overlay of the form
+  ```nix
+  self: super: {
+    # ...
+  }
+  ```
+  allowing the simulation of package overrides to the real [`pkgs/top-level/all-packages.nix`](../../top-level/all-packages.nix`).
+  The default is an empty overlay.
+
+- `base` (optional):
+  Contains another subdirectory imitating Nixpkgs with potentially any of the above structures.
+  This is used for [ratchet checks](#ratchet-checks).
+
+- `expected` (optional):
+  A file containing the expected standard output.
+  The default is expecting an empty standard output.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/default.nix
new file mode 100644
index 000000000000..8836da1f403f
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/default.nix
@@ -0,0 +1,82 @@
+{
+  lib,
+  rustPlatform,
+  nix,
+  rustfmt,
+  clippy,
+  mkShell,
+  makeWrapper,
+  runCommand,
+}:
+let
+  runtimeExprPath = ./src/eval.nix;
+  nixpkgsLibPath = ../../../lib;
+  testNixpkgsPath = ./tests/mock-nixpkgs.nix;
+
+  # Needed to make Nix evaluation work inside nix builds
+  initNix = ''
+    export TEST_ROOT=$(pwd)/test-tmp
+    export NIX_CONF_DIR=$TEST_ROOT/etc
+    export NIX_LOCALSTATE_DIR=$TEST_ROOT/var
+    export NIX_LOG_DIR=$TEST_ROOT/var/log/nix
+    export NIX_STATE_DIR=$TEST_ROOT/var/nix
+    export NIX_STORE_DIR=$TEST_ROOT/store
+
+    # Ensure that even if tests run in parallel, we don't get an error
+    # We'd run into https://github.com/NixOS/nix/issues/2706 unless the store is initialised first
+    nix-store --init
+  '';
+
+  fs = lib.fileset;
+
+  package =
+    rustPlatform.buildRustPackage {
+      name = "nixpkgs-check-by-name";
+      src = fs.toSource {
+        root = ./.;
+        fileset = fs.unions [
+          ./Cargo.lock
+          ./Cargo.toml
+          ./src
+          ./tests
+        ];
+      };
+      cargoLock.lockFile = ./Cargo.lock;
+      nativeBuildInputs = [
+        nix
+        rustfmt
+        clippy
+        makeWrapper
+      ];
+      env.NIX_CHECK_BY_NAME_EXPR_PATH = "${runtimeExprPath}";
+      env.NIX_PATH = "test-nixpkgs=${testNixpkgsPath}:test-nixpkgs/lib=${nixpkgsLibPath}";
+      preCheck = initNix;
+      postCheck = ''
+        cargo fmt --check
+        cargo clippy -- -D warnings
+      '';
+      postInstall = ''
+        wrapProgram $out/bin/nixpkgs-check-by-name \
+          --set NIX_CHECK_BY_NAME_EXPR_PATH "$NIX_CHECK_BY_NAME_EXPR_PATH"
+      '';
+      passthru.shell = mkShell {
+        env.NIX_CHECK_BY_NAME_EXPR_PATH = toString runtimeExprPath;
+        env.NIX_PATH = "test-nixpkgs=${toString testNixpkgsPath}:test-nixpkgs/lib=${toString nixpkgsLibPath}";
+        inputsFrom = [ package ];
+      };
+
+      # Tests the tool on the current Nixpkgs tree, this is a good sanity check
+      passthru.tests.nixpkgs = runCommand "test-nixpkgs-check-by-name" {
+        nativeBuildInputs = [
+          package
+          nix
+        ];
+        nixpkgsPath = lib.cleanSource ../../..;
+      } ''
+        ${initNix}
+        nixpkgs-check-by-name --base "$nixpkgsPath" "$nixpkgsPath"
+        touch $out
+      '';
+    };
+in
+package
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/README.md b/nixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/README.md
new file mode 100644
index 000000000000..ccd4108ea288
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/README.md
@@ -0,0 +1,38 @@
+# CI-related Scripts
+
+This directory contains scripts and files used and related to the CI running the `pkgs/by-name` checks in Nixpkgs.
+See also the [CI GitHub Action](../../../../.github/workflows/check-by-name.yml).
+
+## `./run-local.sh BASE_BRANCH [REPOSITORY]`
+
+Runs the `pkgs/by-name` check on the HEAD commit, closely matching what CI does.
+
+Note that this can't do exactly the same as CI,
+because CI needs to rely on GitHub's server-side Git history to compute the mergeability of PRs before the check can be started.
+In turn when running locally, we don't want to have to push commits to test them,
+and we can also rely on the local Git history to do the mergeability check.
+
+Arguments:
+- `BASE_BRANCH`: The base branch to use, e.g. master or release-23.11
+- `REPOSITORY`: The repository to fetch the base branch from, defaults to https://github.com/NixOS/nixpkgs.git
+
+## `./update-pinned-tool.sh`
+
+Updates the pinned CI tool in [`./pinned-tool.json`](./pinned-tool.json) to the
+[latest version from the `nixos-unstable` channel](https://hydra.nixos.org/job/nixos/trunk-combined/nixpkgs.tests.nixpkgs-check-by-name.x86_64-linux).
+
+This script needs to be called manually when the CI tooling needs to be updated.
+
+The `pinned-tool.json` file gets populated with both:
+- The `/nix/store` path for `x86_64-linux`, such that CI doesn't have to evaluate Nixpkgs and can directly fetch it from the cache instead.
+- The Nixpkgs revision, such that the `./run-local.sh` script can be used to run the checks locally on any system.
+
+To ensure that the tool is always pre-built for `x86_64-linux` in the `nixos-unstable` channel,
+it's included in the `tested` jobset description in [`nixos/release-combined.nix`](../../../nixos/release-combined.nix).
+
+Why not just build the tooling right from the PRs Nixpkgs version?
+- Because it allows CI to check all PRs, even if they would break the CI tooling.
+- Because it makes the CI check very fast, since no Nix builds need to be done, even for mass rebuilds.
+- Because it improves security, since we don't have to build potentially untrusted code from PRs.
+  The tool only needs a very minimal Nix evaluation at runtime, which can work with [readonly-mode](https://nixos.org/manual/nix/stable/command-ref/opt-common.html#opt-readonly-mode) and [restrict-eval](https://nixos.org/manual/nix/stable/command-ref/conf-file.html#conf-restrict-eval).
+
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/pinned-tool.json b/nixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/pinned-tool.json
new file mode 100644
index 000000000000..4ecb86ff6dcf
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/pinned-tool.json
@@ -0,0 +1,4 @@
+{
+  "rev": "f8e2ebd66d097614d51a56a755450d4ae1632df1",
+  "ci-path": "/nix/store/4kv4fyb6x5ivn0qncg7d9i5zhqhzy7bi-nixpkgs-check-by-name"
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/run-local.sh b/nixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/run-local.sh
new file mode 100755
index 000000000000..46cbd5e858e2
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/run-local.sh
@@ -0,0 +1,82 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -i bash -p jq
+
+set -o pipefail -o errexit -o nounset
+
+trace() { echo >&2 "$@"; }
+
+tmp=$(mktemp -d)
+cleanup() {
+    # Don't exit early if anything fails to cleanup
+    set +o errexit
+
+    trace -n "Cleaning up.. "
+
+    [[ -e "$tmp/base" ]] && git worktree remove --force "$tmp/base"
+    [[ -e "$tmp/merged" ]] && git worktree remove --force "$tmp/merged"
+    [[ -e "$tmp/tool-nixpkgs" ]] && git worktree remove --force "$tmp/tool-nixpkgs"
+
+    rm -rf "$tmp"
+
+    trace "Done"
+}
+trap cleanup exit
+
+
+repo=https://github.com/NixOS/nixpkgs.git
+
+if (( $# != 0 )); then
+    baseBranch=$1
+    shift
+else
+    trace "Usage: $0 BASE_BRANCH [REPOSITORY]"
+    trace "BASE_BRANCH: The base branch to use, e.g. master or release-23.11"
+    trace "REPOSITORY: The repository to fetch the base branch from, defaults to $repo"
+    exit 1
+fi
+
+if (( $# != 0 )); then
+    repo=$1
+    shift
+fi
+
+if [[ -n "$(git status --porcelain)" ]]; then
+    trace -e "\e[33mWarning: Dirty tree, uncommitted changes won't be taken into account\e[0m"
+fi
+headSha=$(git rev-parse HEAD)
+trace -e "Using HEAD commit \e[34m$headSha\e[0m"
+
+trace -n "Creating Git worktree for the HEAD commit in $tmp/merged.. "
+git worktree add --detach -q "$tmp/merged" HEAD
+trace "Done"
+
+trace -n "Fetching base branch $baseBranch to compare against.. "
+git fetch -q "$repo" refs/heads/"$baseBranch"
+baseSha=$(git rev-parse FETCH_HEAD)
+trace -e "\e[34m$baseSha\e[0m"
+
+trace -n "Creating Git worktree for the base branch in $tmp/base.. "
+git worktree add -q "$tmp/base" "$baseSha"
+trace "Done"
+
+trace -n "Merging base branch into the HEAD commit in $tmp/merged.. "
+git -C "$tmp/merged" merge -q --no-edit "$baseSha"
+trace -e "\e[34m$(git -C "$tmp/merged" rev-parse HEAD)\e[0m"
+
+trace -n "Reading pinned nixpkgs-check-by-name revision from pinned-tool.json.. "
+toolSha=$(jq -r .rev "$tmp/merged/pkgs/test/nixpkgs-check-by-name/scripts/pinned-tool.json")
+trace -e "\e[34m$toolSha\e[0m"
+
+trace -n "Creating Git worktree for the nixpkgs-check-by-name revision in $tmp/tool-nixpkgs.. "
+git worktree add -q "$tmp/tool-nixpkgs" "$toolSha"
+trace "Done"
+
+trace "Building/fetching nixpkgs-check-by-name.."
+nix-build -o "$tmp/tool" "$tmp/tool-nixpkgs" \
+    -A tests.nixpkgs-check-by-name \
+    --arg config '{}' \
+    --arg overlays '[]' \
+    -j 0
+
+trace "Running nixpkgs-check-by-name.."
+"$tmp/tool/bin/nixpkgs-check-by-name" --base "$tmp/base" "$tmp/merged"
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/update-pinned-tool.sh b/nixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/update-pinned-tool.sh
new file mode 100755
index 000000000000..dbc6e91df08a
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/scripts/update-pinned-tool.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -i bash -p jq
+
+set -o pipefail -o errexit -o nounset
+
+trace() { echo >&2 "$@"; }
+
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+
+# Determined by `runs-on: ubuntu-latest` in .github/workflows/check-by-name.yml
+CI_SYSTEM=x86_64-linux
+
+channel=nixos-unstable
+pin_file=$SCRIPT_DIR/pinned-tool.json
+
+trace -n "Fetching latest version of channel $channel.. "
+# This is probably the easiest way to get Nix to output the path to a downloaded channel!
+nixpkgs=$(nix-instantiate --find-file nixpkgs -I nixpkgs=channel:"$channel")
+trace "$nixpkgs"
+
+# This file only exists in channels
+rev=$(<"$nixpkgs/.git-revision")
+trace -e "Git revision of channel $channel is \e[34m$rev\e[0m"
+
+trace -n "Fetching the prebuilt version of nixpkgs-check-by-name for $CI_SYSTEM.. "
+# This is the architecture used by CI, we want to prefetch the exact path to avoid having to evaluate Nixpkgs
+ci_path=$(nix-build --no-out-link "$nixpkgs" \
+    -A tests.nixpkgs-check-by-name \
+    --arg config '{}' \
+    --argstr system "$CI_SYSTEM" \
+    --arg overlays '[]' \
+    -j 0 \
+    | tee /dev/stderr)
+
+trace "Updating $pin_file"
+jq -n \
+    --arg rev "$rev" \
+    --arg ci-path "$ci_path" \
+    '$ARGS.named' \
+    > "$pin_file"
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/shell.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/shell.nix
new file mode 100644
index 000000000000..33bcf45b8d05
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/shell.nix
@@ -0,0 +1,6 @@
+let
+  pkgs = import ../../.. {
+    config = {};
+    overlays = [];
+  };
+in pkgs.tests.nixpkgs-check-by-name.shell
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.nix
new file mode 100644
index 000000000000..ab1c41e0b145
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.nix
@@ -0,0 +1,116 @@
+# Takes a path to nixpkgs and a path to the json-encoded list of `pkgs/by-name` attributes.
+# Returns a value containing information on all Nixpkgs attributes
+# which is decoded on the Rust side.
+# See ./eval.rs for the meaning of the returned values
+{
+  attrsPath,
+  nixpkgsPath,
+}:
+let
+  attrs = builtins.fromJSON (builtins.readFile attrsPath);
+
+  # We need to check whether attributes are defined manually e.g. in
+  # `all-packages.nix`, automatically by the `pkgs/by-name` overlay, or
+  # neither. The only way to do so is to override `callPackage` and
+  # `_internalCallByNamePackageFile` with our own version that adds this
+  # information to the result, and then try to access it.
+  overlay = final: prev: {
+
+    # Adds information to each attribute about whether it's manually defined using `callPackage`
+    callPackage = fn: args:
+      addVariantInfo (prev.callPackage fn args) {
+        # This is a manual definition of the attribute, and it's a callPackage, specifically a semantic callPackage
+        ManualDefinition.is_semantic_call_package = true;
+      };
+
+    # Adds information to each attribute about whether it's automatically
+    # defined by the `pkgs/by-name` overlay. This internal attribute is only
+    # used by that overlay.
+    # This overrides the above `callPackage` information (we don't need that
+    # one, since `pkgs/by-name` always uses `callPackage` underneath.
+    _internalCallByNamePackageFile = file:
+      addVariantInfo (prev._internalCallByNamePackageFile file) {
+        AutoDefinition = null;
+      };
+
+  };
+
+  # We can't just replace attribute values with their info in the overlay,
+  # because attributes can depend on other attributes, so this would break evaluation.
+  addVariantInfo = value: variant:
+    if builtins.isAttrs value then
+      value // {
+        _callPackageVariant = variant;
+      }
+    else
+      # It's very rare that callPackage doesn't return an attribute set, but it can occur.
+      # In such a case we can't really return anything sensible that would include the info,
+      # so just don't return the value directly and treat it as if it wasn't a callPackage.
+      value;
+
+  pkgs = import nixpkgsPath {
+    # Don't let the users home directory influence this result
+    config = { };
+    overlays = [ overlay ];
+    # We check evaluation and callPackage only for x86_64-linux.
+    # Not ideal, but hard to fix
+    system = "x86_64-linux";
+  };
+
+  # See AttributeInfo in ./eval.rs for the meaning of this
+  attrInfo = name: value: {
+    location = builtins.unsafeGetAttrPos name pkgs;
+    attribute_variant =
+      if ! builtins.isAttrs value then
+        { NonAttributeSet = null; }
+      else
+        {
+          AttributeSet = {
+            is_derivation = pkgs.lib.isDerivation value;
+            definition_variant =
+              if ! value ? _callPackageVariant then
+                { ManualDefinition.is_semantic_call_package = false; }
+              else
+                value._callPackageVariant;
+          };
+        };
+  };
+
+  # Information on all attributes that are in pkgs/by-name.
+  byNameAttrs = builtins.listToAttrs (map (name: {
+    inherit name;
+    value.ByName =
+      if ! pkgs ? ${name} then
+        { Missing = null; }
+      else
+        # Evaluation failures are not allowed, so don't try to catch them
+        { Existing = attrInfo name pkgs.${name}; };
+  }) attrs);
+
+  # Information on all attributes that exist but are not in pkgs/by-name.
+  # We need this to enforce pkgs/by-name for new packages
+  nonByNameAttrs = builtins.mapAttrs (name: value:
+    let
+      # Packages outside `pkgs/by-name` often fail evaluation,
+      # so we need to handle that
+      output = attrInfo name value;
+      result = builtins.tryEval (builtins.deepSeq output null);
+    in
+    {
+      NonByName =
+        if result.success then
+          { EvalSuccess = output; }
+        else
+          { EvalFailure = null; };
+    }
+  ) (builtins.removeAttrs pkgs attrs);
+
+  # All attributes
+  attributes = byNameAttrs // nonByNameAttrs;
+in
+# We output them in the form [ [ <name> <value> ] ]` such that the Rust side
+# doesn't need to sort them again to get deterministic behavior (good for testing)
+map (name: [
+  name
+  attributes.${name}
+]) (builtins.attrNames attributes)
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.rs
new file mode 100644
index 000000000000..e90a95533144
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.rs
@@ -0,0 +1,474 @@
+use crate::nixpkgs_problem::NixpkgsProblem;
+use crate::ratchet;
+use crate::structure;
+use crate::utils;
+use crate::validation::ResultIteratorExt as _;
+use crate::validation::{self, Validation::Success};
+use crate::NixFileStore;
+use std::path::Path;
+
+use anyhow::Context;
+use serde::Deserialize;
+use std::path::PathBuf;
+use std::process;
+use tempfile::NamedTempFile;
+
+/// Attribute set of this structure is returned by eval.nix
+#[derive(Deserialize)]
+enum Attribute {
+    /// An attribute that should be defined via pkgs/by-name
+    ByName(ByNameAttribute),
+    /// An attribute not defined via pkgs/by-name
+    NonByName(NonByNameAttribute),
+}
+
+#[derive(Deserialize)]
+enum NonByNameAttribute {
+    /// The attribute doesn't evaluate
+    EvalFailure,
+    EvalSuccess(AttributeInfo),
+}
+
+#[derive(Deserialize)]
+enum ByNameAttribute {
+    /// The attribute doesn't exist at all
+    Missing,
+    Existing(AttributeInfo),
+}
+
+#[derive(Deserialize)]
+struct AttributeInfo {
+    /// The location of the attribute as returned by `builtins.unsafeGetAttrPos`
+    location: Option<Location>,
+    attribute_variant: AttributeVariant,
+}
+
+/// The structure returned by a successful `builtins.unsafeGetAttrPos`
+#[derive(Deserialize, Clone, Debug)]
+struct Location {
+    pub file: PathBuf,
+    pub line: usize,
+    pub column: usize,
+}
+
+#[derive(Deserialize)]
+pub enum AttributeVariant {
+    /// The attribute is not an attribute set, we're limited in the amount of information we can get
+    /// from it (though it's obviously not a derivation)
+    NonAttributeSet,
+    AttributeSet {
+        /// Whether the attribute is a derivation (`lib.isDerivation`)
+        is_derivation: bool,
+        /// The type of callPackage
+        definition_variant: DefinitionVariant,
+    },
+}
+
+#[derive(Deserialize)]
+pub enum DefinitionVariant {
+    /// An automatic definition by the `pkgs/by-name` overlay
+    /// Though it's detected using the internal _internalCallByNamePackageFile attribute,
+    /// which can in theory also be used by other code
+    AutoDefinition,
+    /// A manual definition of the attribute, typically in `all-packages.nix`
+    ManualDefinition {
+        /// Whether the attribute is defined as `pkgs.callPackage ...` or something else.
+        is_semantic_call_package: bool,
+    },
+}
+
+/// Check that the Nixpkgs attribute values corresponding to the packages in pkgs/by-name are
+/// of the form `callPackage <package_file> { ... }`.
+/// See the `eval.nix` file for how this is achieved on the Nix side
+pub fn check_values(
+    nixpkgs_path: &Path,
+    nix_file_store: &mut NixFileStore,
+    package_names: Vec<String>,
+    keep_nix_path: bool,
+) -> validation::Result<ratchet::Nixpkgs> {
+    // Write the list of packages we need to check into a temporary JSON file.
+    // This can then get read by the Nix evaluation.
+    let attrs_file = NamedTempFile::new().with_context(|| "Failed to create a temporary file")?;
+    // We need to canonicalise this path because if it's a symlink (which can be the case on
+    // Darwin), Nix would need to read both the symlink and the target path, therefore need 2
+    // NIX_PATH entries for restrict-eval. But if we resolve the symlinks then only one predictable
+    // entry is needed.
+    let attrs_file_path = attrs_file.path().canonicalize()?;
+
+    serde_json::to_writer(&attrs_file, &package_names).with_context(|| {
+        format!(
+            "Failed to serialise the package names to the temporary path {}",
+            attrs_file_path.display()
+        )
+    })?;
+
+    let expr_path = std::env::var("NIX_CHECK_BY_NAME_EXPR_PATH")
+        .with_context(|| "Could not get environment variable NIX_CHECK_BY_NAME_EXPR_PATH")?;
+    // With restrict-eval, only paths in NIX_PATH can be accessed, so we explicitly specify the
+    // ones needed needed
+    let mut command = process::Command::new("nix-instantiate");
+    command
+        // Inherit stderr so that error messages always get shown
+        .stderr(process::Stdio::inherit())
+        .args([
+            "--eval",
+            "--json",
+            "--strict",
+            "--readonly-mode",
+            "--restrict-eval",
+            "--show-trace",
+        ])
+        // Pass the path to the attrs_file as an argument and add it to the NIX_PATH so it can be
+        // accessed in restrict-eval mode
+        .args(["--arg", "attrsPath"])
+        .arg(&attrs_file_path)
+        .arg("-I")
+        .arg(&attrs_file_path)
+        // Same for the nixpkgs to test
+        .args(["--arg", "nixpkgsPath"])
+        .arg(nixpkgs_path)
+        .arg("-I")
+        .arg(nixpkgs_path);
+
+    // Clear NIX_PATH to be sure it doesn't influence the result
+    // But not when requested to keep it, used so that the tests can pass extra Nix files
+    if !keep_nix_path {
+        command.env_remove("NIX_PATH");
+    }
+
+    command.args(["-I", &expr_path]);
+    command.arg(expr_path);
+
+    let result = command
+        .output()
+        .with_context(|| format!("Failed to run command {command:?}"))?;
+
+    if !result.status.success() {
+        anyhow::bail!("Failed to run command {command:?}");
+    }
+    // Parse the resulting JSON value
+    let attributes: Vec<(String, Attribute)> = serde_json::from_slice(&result.stdout)
+        .with_context(|| {
+            format!(
+                "Failed to deserialise {}",
+                String::from_utf8_lossy(&result.stdout)
+            )
+        })?;
+
+    let check_result = validation::sequence(
+        attributes
+            .into_iter()
+            .map(|(attribute_name, attribute_value)| {
+                let check_result = match attribute_value {
+                    Attribute::NonByName(non_by_name_attribute) => handle_non_by_name_attribute(
+                        nixpkgs_path,
+                        nix_file_store,
+                        non_by_name_attribute,
+                    )?,
+                    Attribute::ByName(by_name_attribute) => by_name(
+                        nix_file_store,
+                        nixpkgs_path,
+                        &attribute_name,
+                        by_name_attribute,
+                    )?,
+                };
+                Ok::<_, anyhow::Error>(check_result.map(|value| (attribute_name.clone(), value)))
+            })
+            .collect_vec()?,
+    );
+
+    Ok(check_result.map(|elems| ratchet::Nixpkgs {
+        package_names: elems.iter().map(|(name, _)| name.to_owned()).collect(),
+        package_map: elems.into_iter().collect(),
+    }))
+}
+
+/// Handles the evaluation result for an attribute in `pkgs/by-name`,
+/// turning it into a validation result.
+fn by_name(
+    nix_file_store: &mut NixFileStore,
+    nixpkgs_path: &Path,
+    attribute_name: &str,
+    by_name_attribute: ByNameAttribute,
+) -> validation::Result<ratchet::Package> {
+    use ratchet::RatchetState::*;
+    use ByNameAttribute::*;
+
+    let relative_package_file = structure::relative_file_for_package(attribute_name);
+    let absolute_package_file = nixpkgs_path.join(&relative_package_file);
+
+    // At this point we know that `pkgs/by-name/fo/foo/package.nix` has to exists.
+    // This match decides whether the attribute `foo` is defined accordingly
+    // and whether a legacy manual definition could be removed
+    let manual_definition_result = match by_name_attribute {
+        // The attribute is missing
+        Missing => {
+            // This indicates a bug in the `pkgs/by-name` overlay, because it's supposed to
+            // automatically defined attributes in `pkgs/by-name`
+            NixpkgsProblem::UndefinedAttr {
+                relative_package_file: relative_package_file.to_owned(),
+                package_name: attribute_name.to_owned(),
+            }
+            .into()
+        }
+        // The attribute exists
+        Existing(AttributeInfo {
+            // But it's not an attribute set, which limits the amount of information we can get
+            // about this attribute (see ./eval.nix)
+            attribute_variant: AttributeVariant::NonAttributeSet,
+            location: _location,
+        }) => {
+            // The only thing we know is that it's definitely not a derivation, since those are
+            // always attribute sets.
+            //
+            // We can't know whether the attribute is automatically or manually defined for sure,
+            // and while we could check the location, the error seems clear enough as is.
+            NixpkgsProblem::NonDerivation {
+                relative_package_file: relative_package_file.to_owned(),
+                package_name: attribute_name.to_owned(),
+            }
+            .into()
+        }
+        // The attribute exists
+        Existing(AttributeInfo {
+            // And it's an attribute set, which allows us to get more information about it
+            attribute_variant:
+                AttributeVariant::AttributeSet {
+                    is_derivation,
+                    definition_variant,
+                },
+            location,
+        }) => {
+            // Only derivations are allowed in `pkgs/by-name`
+            let is_derivation_result = if is_derivation {
+                Success(())
+            } else {
+                NixpkgsProblem::NonDerivation {
+                    relative_package_file: relative_package_file.to_owned(),
+                    package_name: attribute_name.to_owned(),
+                }
+                .into()
+            };
+
+            // If the definition looks correct
+            let variant_result = match definition_variant {
+                // An automatic `callPackage` by the `pkgs/by-name` overlay.
+                // Though this gets detected by checking whether the internal
+                // `_internalCallByNamePackageFile` was used
+                DefinitionVariant::AutoDefinition => {
+                    if let Some(_location) = location {
+                        // Such an automatic definition should definitely not have a location
+                        // Having one indicates that somebody is using `_internalCallByNamePackageFile`,
+                        NixpkgsProblem::InternalCallPackageUsed {
+                            attr_name: attribute_name.to_owned(),
+                        }
+                        .into()
+                    } else {
+                        Success(Tight)
+                    }
+                }
+                // The attribute is manually defined, e.g. in `all-packages.nix`.
+                // This means we need to enforce it to look like this:
+                //   callPackage ../pkgs/by-name/fo/foo/package.nix { ... }
+                DefinitionVariant::ManualDefinition {
+                    is_semantic_call_package,
+                } => {
+                    // We should expect manual definitions to have a location, otherwise we can't
+                    // enforce the expected format
+                    if let Some(location) = location {
+                        // Parse the Nix file in the location and figure out whether it's an
+                        // attribute definition of the form `= callPackage <arg1> <arg2>`,
+                        // returning the arguments if so.
+                        let optional_syntactic_call_package = nix_file_store
+                            .get(&location.file)?
+                            .call_package_argument_info_at(
+                            location.line,
+                            location.column,
+                            // We're passing `pkgs/by-name/fo/foo/package.nix` here, which causes
+                            // the function to verify that `<arg1>` is the same path,
+                            // making `syntactic_call_package.relative_path` end up as `""`
+                            // TODO: This is confusing and should be improved
+                            &absolute_package_file,
+                        )?;
+
+                        // At this point, we completed two different checks for whether it's a
+                        // `callPackage`
+                        match (is_semantic_call_package, optional_syntactic_call_package) {
+                            // Something like `<attr> = { ... }`
+                            // or a `pkgs.callPackage` but with the wrong file
+                            (false, None)
+                            // Something like `<attr> = pythonPackages.callPackage ./pkgs/by-name/...`
+                            | (false, Some(_))
+                            // Something like `<attr> = bar` where `bar = pkgs.callPackage ...`
+                            // or a `callPackage` but with the wrong file
+                            | (true, None) => {
+                                // All of these are not of the expected form, so error out
+                                // TODO: Make error more specific, don't lump everything together
+                                NixpkgsProblem::WrongCallPackage {
+                                      relative_package_file: relative_package_file.to_owned(),
+                                      package_name: attribute_name.to_owned(),
+                                }.into()
+                            }
+                            // Something like `<attr> = pkgs.callPackage ./pkgs/by-name/...`,
+                            // with the correct file
+                            (true, Some(syntactic_call_package)) => {
+                                Success(
+                                    // Manual definitions with empty arguments are not allowed
+                                    // anymore
+                                    if syntactic_call_package.empty_arg {
+                                        Loose(())
+                                    } else {
+                                        Tight
+                                    }
+                                )
+                            }
+                        }
+                    } else {
+                        // If manual definitions don't have a location, it's likely `mapAttrs`'d
+                        // over, e.g. if it's defined in aliases.nix.
+                        // We can't verify whether its of the expected `callPackage`, so error out
+                        NixpkgsProblem::CannotDetermineAttributeLocation {
+                            attr_name: attribute_name.to_owned(),
+                        }
+                        .into()
+                    }
+                }
+            };
+
+            // Independently report problems about whether it's a derivation and the callPackage variant
+            is_derivation_result.and(variant_result)
+        }
+    };
+    Ok(
+        // Packages being checked in this function are _always_ already defined in `pkgs/by-name`,
+        // so instead of repeating ourselves all the time to define `uses_by_name`, just set it
+        // once at the end with a map
+        manual_definition_result.map(|manual_definition| ratchet::Package {
+            manual_definition,
+            uses_by_name: Tight,
+        }),
+    )
+}
+
+/// Handles the evaluation result for an attribute _not_ in `pkgs/by-name`,
+/// turning it into a validation result.
+fn handle_non_by_name_attribute(
+    nixpkgs_path: &Path,
+    nix_file_store: &mut NixFileStore,
+    non_by_name_attribute: NonByNameAttribute,
+) -> validation::Result<ratchet::Package> {
+    use ratchet::RatchetState::*;
+    use NonByNameAttribute::*;
+
+    // The ratchet state whether this attribute uses `pkgs/by-name`.
+    // This is never `Tight`, because we only either:
+    // - Know that the attribute _could_ be migrated to `pkgs/by-name`, which is `Loose`
+    // - Or we're unsure, in which case we use NonApplicable
+    let uses_by_name =
+        // This is a big ol' match on various properties of the attribute
+
+        // First, it needs to succeed evaluation. We can't know whether an attribute could be
+        // migrated to `pkgs/by-name` if it doesn't evaluate, since we need to check that it's a
+        // derivation.
+        //
+        // This only has the minor negative effect that if a PR that breaks evaluation
+        // gets merged, fixing those failures won't force anything into `pkgs/by-name`.
+        //
+        // For now this isn't our problem, but in the future we
+        // might have another check to enforce that evaluation must not be broken.
+        //
+        // The alternative of assuming that failing attributes would have been fit for `pkgs/by-name`
+        // has the problem that if a package evaluation gets broken temporarily,
+        // fixing it requires a move to pkgs/by-name, which could happen more
+        // often and isn't really justified.
+        if let EvalSuccess(AttributeInfo {
+            // We're only interested in attributes that are attribute sets (which includes
+            // derivations). Anything else can't be in `pkgs/by-name`.
+            attribute_variant: AttributeVariant::AttributeSet {
+                // Indeed, we only care about derivations, non-derivation attribute sets can't be
+                // in `pkgs/by-name`
+                is_derivation: true,
+                // Of the two definition variants, really only the manual one makes sense here.
+                // Special cases are:
+                // - Manual aliases to auto-called packages are not treated as manual definitions,
+                //   due to limitations in the semantic callPackage detection. So those should be
+                //   ignored.
+                // - Manual definitions using the internal _internalCallByNamePackageFile are
+                //   not treated as manual definitions, since _internalCallByNamePackageFile is
+                //   used to detect automatic ones. We can't distinguish from the above case, so we
+                //   just need to ignore this one too, even if that internal attribute should never
+                //   be called manually.
+                definition_variant: DefinitionVariant::ManualDefinition { is_semantic_call_package }
+            },
+            // We need the location of the manual definition, because otherwise
+            // we can't figure out whether it's a syntactic callPackage
+            location: Some(location),
+        }) = non_by_name_attribute {
+
+        // Parse the Nix file in the location and figure out whether it's an
+        // attribute definition of the form `= callPackage <arg1> <arg2>`,
+        // returning the arguments if so.
+        let optional_syntactic_call_package = nix_file_store
+            .get(&location.file)?
+            .call_package_argument_info_at(
+                location.line,
+                location.column,
+                // Passing the Nixpkgs path here both checks that the <arg1> is within Nixpkgs, and
+                // strips the absolute Nixpkgs path from it, such that
+                // syntactic_call_package.relative_path is relative to Nixpkgs
+                nixpkgs_path
+            )?;
+
+        // At this point, we completed two different checks for whether it's a
+        // `callPackage`
+        match (is_semantic_call_package, optional_syntactic_call_package) {
+            // Something like `<attr> = { }`
+            (false, None)
+            // Something like `<attr> = pythonPackages.callPackage ...`
+            | (false, Some(_))
+            // Something like `<attr> = bar` where `bar = pkgs.callPackage ...`
+            | (true, None) => {
+                // In all of these cases, it's not possible to migrate the package to `pkgs/by-name`
+                NonApplicable
+            }
+            // Something like `<attr> = pkgs.callPackage ...`
+            (true, Some(syntactic_call_package)) => {
+                // It's only possible to migrate such a definitions if..
+                match syntactic_call_package.relative_path {
+                    Some(ref rel_path) if rel_path.starts_with(utils::BASE_SUBPATH) => {
+                        // ..the path is not already within `pkgs/by-name` like
+                        //
+                        //   foo-variant = callPackage ../by-name/fo/foo/package.nix {
+                        //     someFlag = true;
+                        //   }
+                        //
+                        // While such definitions could be moved to `pkgs/by-name` by using
+                        // `.override { someFlag = true; }` instead, this changes the semantics in
+                        // relation with overlays, so migration is generally not possible.
+                        //
+                        // See also "package variants" in RFC 140:
+                        // https://github.com/NixOS/rfcs/blob/master/rfcs/0140-simple-package-paths.md#package-variants
+                        NonApplicable
+                    }
+                    _ => {
+                        // Otherwise, the path is outside `pkgs/by-name`, which means it can be
+                        // migrated
+                        Loose(syntactic_call_package)
+                    }
+                }
+            }
+        }
+    } else {
+        // This catches all the cases not matched by the above `if let`, falling back to not being
+        // able to migrate such attributes
+        NonApplicable
+    };
+    Ok(Success(ratchet::Package {
+        // Packages being checked in this function _always_ need a manual definition, because
+        // they're not using `pkgs/by-name` which would allow avoiding it.
+        // so instead of repeating ourselves all the time to define `manual_definition`,
+        // just set it once at the end here
+        manual_definition: Tight,
+        uses_by_name,
+    }))
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/main.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/main.rs
new file mode 100644
index 000000000000..0d0ddcd7e632
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/main.rs
@@ -0,0 +1,266 @@
+use crate::nix_file::NixFileStore;
+mod eval;
+mod nix_file;
+mod nixpkgs_problem;
+mod ratchet;
+mod references;
+mod structure;
+mod utils;
+mod validation;
+
+use crate::structure::check_structure;
+use crate::validation::Validation::Failure;
+use crate::validation::Validation::Success;
+use anyhow::Context;
+use clap::Parser;
+use colored::Colorize;
+use std::io;
+use std::path::{Path, PathBuf};
+use std::process::ExitCode;
+
+/// Program to check the validity of pkgs/by-name
+///
+/// This CLI interface may be changed over time if the CI workflow making use of
+/// it is adjusted to deal with the change appropriately.
+///
+/// Exit code:
+/// - `0`: If the validation is successful
+/// - `1`: If the validation is not successful
+/// - `2`: If an unexpected I/O error occurs
+///
+/// Standard error:
+/// - Informative messages
+/// - Detected problems if validation is not successful
+#[derive(Parser, Debug)]
+#[command(about, verbatim_doc_comment)]
+pub struct Args {
+    /// Path to the main Nixpkgs to check.
+    /// For PRs, this should be set to a checkout of the PR branch.
+    nixpkgs: PathBuf,
+
+    /// Path to the base Nixpkgs to run ratchet checks against.
+    /// For PRs, this should be set to a checkout of the PRs base branch.
+    #[arg(long)]
+    base: PathBuf,
+}
+
+fn main() -> ExitCode {
+    let args = Args::parse();
+    match process(&args.base, &args.nixpkgs, false, &mut io::stderr()) {
+        Ok(true) => {
+            eprintln!("{}", "Validated successfully".green());
+            ExitCode::SUCCESS
+        }
+        Ok(false) => {
+            eprintln!("{}", "Validation failed, see above errors".yellow());
+            ExitCode::from(1)
+        }
+        Err(e) => {
+            eprintln!("{} {:#}", "I/O error: ".yellow(), e);
+            ExitCode::from(2)
+        }
+    }
+}
+
+/// Does the actual work. This is the abstraction used both by `main` and the tests.
+///
+/// # Arguments
+/// - `base_nixpkgs`: Path to the base Nixpkgs to run ratchet checks against.
+/// - `main_nixpkgs`: Path to the main Nixpkgs to check.
+/// - `keep_nix_path`: Whether the value of the NIX_PATH environment variable should be kept for
+/// the evaluation stage, allowing its contents to be accessed.
+///   This is used to allow the tests to access e.g. the mock-nixpkgs.nix file
+/// - `error_writer`: An `io::Write` value to write validation errors to, if any.
+///
+/// # Return value
+/// - `Err(e)` if an I/O-related error `e` occurred.
+/// - `Ok(false)` if there are problems, all of which will be written to `error_writer`.
+/// - `Ok(true)` if there are no problems
+pub fn process<W: io::Write>(
+    base_nixpkgs: &Path,
+    main_nixpkgs: &Path,
+    keep_nix_path: bool,
+    error_writer: &mut W,
+) -> anyhow::Result<bool> {
+    // Check the main Nixpkgs first
+    let main_result = check_nixpkgs(main_nixpkgs, keep_nix_path, error_writer)?;
+    let check_result = main_result.result_map(|nixpkgs_version| {
+        // If the main Nixpkgs doesn't have any problems, run the ratchet checks against the base
+        // Nixpkgs
+        check_nixpkgs(base_nixpkgs, keep_nix_path, error_writer)?.result_map(
+            |base_nixpkgs_version| {
+                Ok(ratchet::Nixpkgs::compare(
+                    base_nixpkgs_version,
+                    nixpkgs_version,
+                ))
+            },
+        )
+    })?;
+
+    match check_result {
+        Failure(errors) => {
+            for error in errors {
+                writeln!(error_writer, "{}", error.to_string().red())?
+            }
+            Ok(false)
+        }
+        Success(()) => Ok(true),
+    }
+}
+
+/// Checks whether the pkgs/by-name structure in Nixpkgs is valid.
+///
+/// This does not include ratchet checks, see ../README.md#ratchet-checks
+/// Instead a `ratchet::Nixpkgs` value is returned, whose `compare` method allows performing the
+/// ratchet check against another result.
+pub fn check_nixpkgs<W: io::Write>(
+    nixpkgs_path: &Path,
+    keep_nix_path: bool,
+    error_writer: &mut W,
+) -> validation::Result<ratchet::Nixpkgs> {
+    let mut nix_file_store = NixFileStore::default();
+
+    Ok({
+        let nixpkgs_path = nixpkgs_path.canonicalize().with_context(|| {
+            format!(
+                "Nixpkgs path {} could not be resolved",
+                nixpkgs_path.display()
+            )
+        })?;
+
+        if !nixpkgs_path.join(utils::BASE_SUBPATH).exists() {
+            writeln!(
+                error_writer,
+                "Given Nixpkgs path does not contain a {} subdirectory, no check necessary.",
+                utils::BASE_SUBPATH
+            )?;
+            Success(ratchet::Nixpkgs::default())
+        } else {
+            check_structure(&nixpkgs_path, &mut nix_file_store)?.result_map(|package_names|
+                // Only if we could successfully parse the structure, we do the evaluation checks
+                eval::check_values(&nixpkgs_path, &mut nix_file_store, package_names, keep_nix_path))?
+        }
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::process;
+    use crate::utils;
+    use anyhow::Context;
+    use std::fs;
+    use std::path::Path;
+    use tempfile::{tempdir_in, TempDir};
+
+    #[test]
+    fn tests_dir() -> anyhow::Result<()> {
+        for entry in Path::new("tests").read_dir()? {
+            let entry = entry?;
+            let path = entry.path();
+            let name = entry.file_name().to_string_lossy().into_owned();
+
+            if !path.is_dir() {
+                continue;
+            }
+
+            let expected_errors =
+                fs::read_to_string(path.join("expected")).unwrap_or(String::new());
+
+            test_nixpkgs(&name, &path, &expected_errors)?;
+        }
+        Ok(())
+    }
+
+    // tempfile::tempdir needs to be wrapped in temp_env lock
+    // because it accesses TMPDIR environment variable.
+    pub fn tempdir() -> anyhow::Result<TempDir> {
+        let empty_list: [(&str, Option<&str>); 0] = [];
+        Ok(temp_env::with_vars(empty_list, tempfile::tempdir)?)
+    }
+
+    // We cannot check case-conflicting files into Nixpkgs (the channel would fail to
+    // build), so we generate the case-conflicting file instead.
+    #[test]
+    fn test_case_sensitive() -> anyhow::Result<()> {
+        let temp_nixpkgs = tempdir()?;
+        let path = temp_nixpkgs.path();
+
+        if is_case_insensitive_fs(&path)? {
+            eprintln!("We're on a case-insensitive filesystem, skipping case-sensitivity test");
+            return Ok(());
+        }
+
+        let base = path.join(utils::BASE_SUBPATH);
+
+        fs::create_dir_all(base.join("fo/foo"))?;
+        fs::write(base.join("fo/foo/package.nix"), "{ someDrv }: someDrv")?;
+
+        fs::create_dir_all(base.join("fo/foO"))?;
+        fs::write(base.join("fo/foO/package.nix"), "{ someDrv }: someDrv")?;
+
+        test_nixpkgs(
+            "case_sensitive",
+            &path,
+            "pkgs/by-name/fo: Duplicate case-sensitive package directories \"foO\" and \"foo\".\n",
+        )?;
+
+        Ok(())
+    }
+
+    /// Tests symlinked temporary directories.
+    /// This is needed because on darwin, `/tmp` is a symlink to `/private/tmp`, and Nix's
+    /// restrict-eval doesn't also allow access to the canonical path when you allow the
+    /// non-canonical one.
+    ///
+    /// The error if we didn't do this would look like this:
+    /// error: access to canonical path '/private/var/folders/[...]/.tmpFbcNO0' is forbidden in restricted mode
+    #[test]
+    fn test_symlinked_tmpdir() -> anyhow::Result<()> {
+        // Create a directory with two entries:
+        // - actual (dir)
+        // - symlinked -> actual (symlink)
+        let temp_root = tempdir()?;
+        fs::create_dir(temp_root.path().join("actual"))?;
+        std::os::unix::fs::symlink("actual", temp_root.path().join("symlinked"))?;
+        let tmpdir = temp_root.path().join("symlinked");
+
+        temp_env::with_var("TMPDIR", Some(&tmpdir), || {
+            test_nixpkgs("symlinked_tmpdir", Path::new("tests/success"), "")
+        })
+    }
+
+    fn test_nixpkgs(name: &str, path: &Path, expected_errors: &str) -> anyhow::Result<()> {
+        let base_path = path.join("base");
+        let base_nixpkgs = if base_path.exists() {
+            base_path.as_path()
+        } else {
+            Path::new("tests/empty-base")
+        };
+
+        // We don't want coloring to mess up the tests
+        let writer = temp_env::with_var("NO_COLOR", Some("1"), || -> anyhow::Result<_> {
+            let mut writer = vec![];
+            process(base_nixpkgs, &path, true, &mut writer)
+                .with_context(|| format!("Failed test case {name}"))?;
+            Ok(writer)
+        })?;
+
+        let actual_errors = String::from_utf8_lossy(&writer);
+
+        if actual_errors != expected_errors {
+            panic!(
+                "Failed test case {name}, expected these errors:\n\n{}\n\nbut got these:\n\n{}",
+                expected_errors, actual_errors
+            );
+        }
+        Ok(())
+    }
+
+    /// Check whether a path is in a case-insensitive filesystem
+    fn is_case_insensitive_fs(path: &Path) -> anyhow::Result<bool> {
+        let dir = tempdir_in(path)?;
+        let base = dir.path();
+        fs::write(base.join("aaa"), "")?;
+        Ok(base.join("AAA").exists())
+    }
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/nix_file.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/nix_file.rs
new file mode 100644
index 000000000000..836c5e2dcdda
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/nix_file.rs
@@ -0,0 +1,510 @@
+//! This is a utility module for interacting with the syntax of Nix files
+
+use crate::utils::LineIndex;
+use anyhow::Context;
+use rnix::ast;
+use rnix::ast::Expr;
+use rnix::ast::HasEntry;
+use rnix::SyntaxKind;
+use rowan::ast::AstNode;
+use rowan::TextSize;
+use rowan::TokenAtOffset;
+use std::collections::hash_map::Entry;
+use std::collections::HashMap;
+use std::fs::read_to_string;
+use std::path::Path;
+use std::path::PathBuf;
+
+/// A structure to store parse results of Nix files in memory,
+/// making sure that the same file never has to be parsed twice
+#[derive(Default)]
+pub struct NixFileStore {
+    entries: HashMap<PathBuf, NixFile>,
+}
+
+impl NixFileStore {
+    /// Get the store entry for a Nix file if it exists, otherwise parse the file, insert it into
+    /// the store, and return the value
+    ///
+    /// Note that this function only gives an anyhow::Result::Err for I/O errors.
+    /// A parse error is anyhow::Result::Ok(Result::Err(error))
+    pub fn get(&mut self, path: &Path) -> anyhow::Result<&NixFile> {
+        match self.entries.entry(path.to_owned()) {
+            Entry::Occupied(entry) => Ok(entry.into_mut()),
+            Entry::Vacant(entry) => Ok(entry.insert(NixFile::new(path)?)),
+        }
+    }
+}
+
+/// A structure for storing a successfully parsed Nix file
+pub struct NixFile {
+    /// The parent directory of the Nix file, for more convenient error handling
+    pub parent_dir: PathBuf,
+    /// The path to the file itself, for errors
+    pub path: PathBuf,
+    pub syntax_root: rnix::Root,
+    pub line_index: LineIndex,
+}
+
+impl NixFile {
+    /// Creates a new NixFile, failing for I/O or parse errors
+    fn new(path: impl AsRef<Path>) -> anyhow::Result<NixFile> {
+        let Some(parent_dir) = path.as_ref().parent() else {
+            anyhow::bail!("Could not get parent of path {}", path.as_ref().display())
+        };
+
+        let contents = read_to_string(&path)
+            .with_context(|| format!("Could not read file {}", path.as_ref().display()))?;
+        let line_index = LineIndex::new(&contents);
+
+        // NOTE: There's now another Nixpkgs CI check to make sure all changed Nix files parse
+        // correctly, though that uses mainline Nix instead of rnix, so it doesn't give the same
+        // errors. In the future we should unify these two checks, ideally moving the other CI
+        // check into this tool as well and checking for both mainline Nix and rnix.
+        rnix::Root::parse(&contents)
+            // rnix's ::ok returns Result<_, _> , so no error is thrown away like it would be with
+            // std::result's ::ok
+            .ok()
+            .map(|syntax_root| NixFile {
+                parent_dir: parent_dir.to_path_buf(),
+                path: path.as_ref().to_owned(),
+                syntax_root,
+                line_index,
+            })
+            .with_context(|| format!("Could not parse file {} with rnix", path.as_ref().display()))
+    }
+}
+
+/// Information about callPackage arguments
+#[derive(Debug, PartialEq)]
+pub struct CallPackageArgumentInfo {
+    /// The relative path of the first argument, or `None` if it's not a path.
+    pub relative_path: Option<PathBuf>,
+    /// Whether the second argument is an empty attribute set
+    pub empty_arg: bool,
+}
+
+impl NixFile {
+    /// Returns information about callPackage arguments for an attribute at a specific line/column
+    /// index.
+    /// If the location is not of the form `<attr> = callPackage <arg1> <arg2>;`, `None` is
+    /// returned.
+    /// This function only returns `Err` for problems that can't be caused by the Nix contents,
+    /// but rather problems in this programs code itself.
+    ///
+    /// This is meant to be used with the location returned from `builtins.unsafeGetAttrPos`, e.g.:
+    /// - Create file `default.nix` with contents
+    ///   ```nix
+    ///   self: {
+    ///     foo = self.callPackage ./default.nix { };
+    ///   }
+    ///   ```
+    /// - Evaluate
+    ///   ```nix
+    ///   builtins.unsafeGetAttrPos "foo" (import ./default.nix { })
+    ///   ```
+    ///   results in `{ file = ./default.nix; line = 2; column = 3; }`
+    /// - Get the NixFile for `.file` from a `NixFileStore`
+    /// - Call this function with `.line`, `.column` and `relative_to` as the (absolute) current directory
+    ///
+    /// You'll get back
+    /// ```rust
+    /// Some(CallPackageArgumentInfo { path = Some("default.nix"), empty_arg: true })
+    /// ```
+    ///
+    /// Note that this also returns the same for `pythonPackages.callPackage`. It doesn't make an
+    /// attempt at distinguishing this.
+    pub fn call_package_argument_info_at(
+        &self,
+        line: usize,
+        column: usize,
+        relative_to: &Path,
+    ) -> anyhow::Result<Option<CallPackageArgumentInfo>> {
+        let Some(attrpath_value) = self.attrpath_value_at(line, column)? else {
+            return Ok(None);
+        };
+        self.attrpath_value_call_package_argument_info(attrpath_value, relative_to)
+    }
+
+    // Internal function mainly to make it independently testable
+    fn attrpath_value_at(
+        &self,
+        line: usize,
+        column: usize,
+    ) -> anyhow::Result<Option<ast::AttrpathValue>> {
+        let index = self.line_index.fromlinecolumn(line, column);
+
+        let token_at_offset = self
+            .syntax_root
+            .syntax()
+            .token_at_offset(TextSize::from(index as u32));
+
+        // The token_at_offset function takes indices to mean a location _between_ characters,
+        // which in this case is some spacing followed by the attribute name:
+        //
+        //   foo = 10;
+        //  /\
+        //  This is the token offset, we get both the (newline + indentation) on the left side,
+        //  and the attribute name on the right side.
+        let TokenAtOffset::Between(_space, token) = token_at_offset else {
+            anyhow::bail!("Line {line} column {column} in {} is not the start of a token, but rather {token_at_offset:?}", self.path.display())
+        };
+
+        // token looks like "foo"
+        let Some(node) = token.parent() else {
+            anyhow::bail!(
+                "Token on line {line} column {column} in {} does not have a parent node: {token:?}",
+                self.path.display()
+            )
+        };
+
+        // node looks like "foo"
+        let Some(attrpath_node) = node.parent() else {
+            anyhow::bail!(
+                "Node in {} does not have a parent node: {node:?}",
+                self.path.display()
+            )
+        };
+
+        if attrpath_node.kind() != SyntaxKind::NODE_ATTRPATH {
+            // This can happen for e.g. `inherit foo`, so definitely not a syntactic `callPackage`
+            return Ok(None);
+        }
+        // attrpath_node looks like "foo.bar"
+        let Some(attrpath_value_node) = attrpath_node.parent() else {
+            anyhow::bail!(
+                "Attribute path node in {} does not have a parent node: {attrpath_node:?}",
+                self.path.display()
+            )
+        };
+
+        if !ast::AttrpathValue::can_cast(attrpath_value_node.kind()) {
+            anyhow::bail!(
+                "Node in {} is not an attribute path value node: {attrpath_value_node:?}",
+                self.path.display()
+            )
+        }
+        // attrpath_value_node looks like "foo.bar = 10;"
+
+        // unwrap is fine because we confirmed that we can cast with the above check.
+        // We could avoid this `unwrap` for a `clone`, since `cast` consumes the argument,
+        // but we still need it for the error message when the cast fails.
+        Ok(Some(ast::AttrpathValue::cast(attrpath_value_node).unwrap()))
+    }
+
+    // Internal function mainly to make attrpath_value_at independently testable
+    fn attrpath_value_call_package_argument_info(
+        &self,
+        attrpath_value: ast::AttrpathValue,
+        relative_to: &Path,
+    ) -> anyhow::Result<Option<CallPackageArgumentInfo>> {
+        let Some(attrpath) = attrpath_value.attrpath() else {
+            anyhow::bail!("attrpath value node doesn't have an attrpath: {attrpath_value:?}")
+        };
+
+        // At this point we know it's something like `foo...bar = ...`
+
+        if attrpath.attrs().count() > 1 {
+            // If the attribute path has multiple entries, the left-most entry is an attribute and
+            // can't be a `callPackage`.
+            //
+            // FIXME: `builtins.unsafeGetAttrPos` will return the same position for all attribute
+            // paths and we can't really know which one it is. We could have a case like
+            // `foo.bar = callPackage ... { }` and trying to determine if `bar` is a `callPackage`,
+            // where this is not correct.
+            // However, this case typically doesn't occur anyways,
+            // because top-level packages wouldn't be nested under an attribute set.
+            return Ok(None);
+        }
+        let Some(value) = attrpath_value.value() else {
+            anyhow::bail!("attrpath value node doesn't have a value: {attrpath_value:?}")
+        };
+
+        // At this point we know it's something like `foo = ...`
+
+        let Expr::Apply(apply1) = value else {
+            // Not even a function call, instead something like `foo = null`
+            return Ok(None);
+        };
+        let Some(function1) = apply1.lambda() else {
+            anyhow::bail!("apply node doesn't have a lambda: {apply1:?}")
+        };
+        let Some(arg1) = apply1.argument() else {
+            anyhow::bail!("apply node doesn't have an argument: {apply1:?}")
+        };
+
+        // At this point we know it's something like `foo = <fun> <arg>`.
+        // For a callPackage, `<fun>` would be `callPackage ./file` and `<arg>` would be `{ }`
+
+        let empty_arg = if let Expr::AttrSet(attrset) = arg1 {
+            // We can only statically determine whether the argument is empty if it's an attribute
+            // set _expression_, even though other kind of expressions could evaluate to an attribute
+            // set _value_. But this is what we want anyways
+            attrset.entries().next().is_none()
+        } else {
+            false
+        };
+
+        // Because callPackage takes two curried arguments, the first function needs to be a
+        // function call itself
+        let Expr::Apply(apply2) = function1 else {
+            // Not a callPackage, instead something like `foo = import ./foo`
+            return Ok(None);
+        };
+        let Some(function2) = apply2.lambda() else {
+            anyhow::bail!("apply node doesn't have a lambda: {apply2:?}")
+        };
+        let Some(arg2) = apply2.argument() else {
+            anyhow::bail!("apply node doesn't have an argument: {apply2:?}")
+        };
+
+        // At this point we know it's something like `foo = <fun2> <arg2> <arg1>`.
+        // For a callPackage, `<fun2>` would be `callPackage`, `<arg2>` would be `./file`
+
+        // Check that <arg2> is a path expression
+        let path = if let Expr::Path(actual_path) = arg2 {
+            // Try to statically resolve the path and turn it into a nixpkgs-relative path
+            if let ResolvedPath::Within(p) = self.static_resolve_path(actual_path, relative_to) {
+                Some(p)
+            } else {
+                // We can't statically know an existing path inside Nixpkgs used as <arg2>
+                None
+            }
+        } else {
+            // <arg2> is not a path, but rather e.g. an inline expression
+            None
+        };
+
+        // Check that <fun2> is an identifier, or an attribute path with an identifier at the end
+        let ident = match function2 {
+            Expr::Ident(ident) => {
+                // This means it's something like `foo = callPackage <arg2> <arg1>`
+                ident
+            }
+            Expr::Select(select) => {
+                // This means it's something like `foo = self.callPackage <arg2> <arg1>`.
+                // We also end up here for e.g. `pythonPackages.callPackage`, but the
+                // callPackage-mocking method will take care of not triggering for this case.
+
+                if select.default_expr().is_some() {
+                    // Very odd case, but this would be `foo = self.callPackage or true ./test.nix {}
+                    // (yes this is valid Nix code)
+                    return Ok(None);
+                }
+                let Some(attrpath) = select.attrpath() else {
+                    anyhow::bail!("select node doesn't have an attrpath: {select:?}")
+                };
+                let Some(last) = attrpath.attrs().last() else {
+                    // This case shouldn't be possible, it would be `foo = self. ./test.nix {}`,
+                    // which shouldn't parse
+                    anyhow::bail!("select node has an empty attrpath: {select:?}")
+                };
+                if let ast::Attr::Ident(ident) = last {
+                    ident
+                } else {
+                    // Here it's something like `foo = self."callPackage" /test.nix {}`
+                    // which we're not gonna bother with
+                    return Ok(None);
+                }
+            }
+            // Any other expression we're not gonna treat as callPackage
+            _ => return Ok(None),
+        };
+
+        let Some(token) = ident.ident_token() else {
+            anyhow::bail!("ident node doesn't have a token: {ident:?}")
+        };
+
+        if token.text() == "callPackage" {
+            Ok(Some(CallPackageArgumentInfo {
+                relative_path: path,
+                empty_arg,
+            }))
+        } else {
+            Ok(None)
+        }
+    }
+}
+
+/// The result of trying to statically resolve a Nix path expression
+pub enum ResolvedPath {
+    /// Something like `./foo/${bar}/baz`, can't be known statically
+    Interpolated,
+    /// Something like `<nixpkgs>`, can't be known statically
+    SearchPath,
+    /// Path couldn't be resolved due to an IO error,
+    /// e.g. if the path doesn't exist or you don't have the right permissions
+    Unresolvable(std::io::Error),
+    /// The path is outside the given absolute path
+    Outside,
+    /// The path is within the given absolute path.
+    /// The `PathBuf` is the relative path under the given absolute path.
+    Within(PathBuf),
+}
+
+impl NixFile {
+    /// Statically resolves a Nix path expression and checks that it's within an absolute path
+    ///
+    /// E.g. for the path expression `./bar.nix` in `./foo.nix` and an absolute path of the
+    /// current directory, the function returns `ResolvedPath::Within(./bar.nix)`
+    pub fn static_resolve_path(&self, node: ast::Path, relative_to: &Path) -> ResolvedPath {
+        if node.parts().count() != 1 {
+            // If there's more than 1 interpolated part, it's of the form `./foo/${bar}/baz`.
+            return ResolvedPath::Interpolated;
+        }
+
+        let text = node.to_string();
+
+        if text.starts_with('<') {
+            // A search path like `<nixpkgs>`. There doesn't appear to be better way to detect
+            // these in rnix
+            return ResolvedPath::SearchPath;
+        }
+
+        // Join the file's parent directory and the path expression, then resolve it
+        // FIXME: Expressions like `../../../../foo/bar/baz/qux` or absolute paths
+        // may resolve close to the original file, but may have left the relative_to.
+        // That should be checked more strictly
+        match self.parent_dir.join(Path::new(&text)).canonicalize() {
+            Err(resolution_error) => ResolvedPath::Unresolvable(resolution_error),
+            Ok(resolved) => {
+                // Check if it's within relative_to
+                match resolved.strip_prefix(relative_to) {
+                    Err(_prefix_error) => ResolvedPath::Outside,
+                    Ok(suffix) => ResolvedPath::Within(suffix.to_path_buf()),
+                }
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::tests;
+    use indoc::indoc;
+
+    #[test]
+    fn detects_attributes() -> anyhow::Result<()> {
+        let temp_dir = tests::tempdir()?;
+        let file = temp_dir.path().join("file.nix");
+        let contents = indoc! {r#"
+            toInherit: {
+              foo = 1;
+              "bar" = 2;
+              ${"baz"} = 3;
+              "${"qux"}" = 4;
+
+              # A
+              quux
+              # B
+              =
+              # C
+              5
+              # D
+              ;
+              # E
+
+              /**/quuux/**/=/**/5/**/;/*E*/
+
+              inherit toInherit;
+            }
+        "#};
+
+        std::fs::write(&file, contents)?;
+
+        let nix_file = NixFile::new(&file)?;
+
+        // These are builtins.unsafeGetAttrPos locations for the attributes
+        let cases = [
+            (2, 3, Some("foo = 1;")),
+            (3, 3, Some(r#""bar" = 2;"#)),
+            (4, 3, Some(r#"${"baz"} = 3;"#)),
+            (5, 3, Some(r#""${"qux"}" = 4;"#)),
+            (8, 3, Some("quux\n  # B\n  =\n  # C\n  5\n  # D\n  ;")),
+            (17, 7, Some("quuux/**/=/**/5/**/;")),
+            (19, 10, None),
+        ];
+
+        for (line, column, expected_result) in cases {
+            let actual_result = nix_file
+                .attrpath_value_at(line, column)?
+                .map(|node| node.to_string());
+            assert_eq!(actual_result.as_deref(), expected_result);
+        }
+
+        Ok(())
+    }
+
+    #[test]
+    fn detects_call_package() -> anyhow::Result<()> {
+        let temp_dir = tests::tempdir()?;
+        let file = temp_dir.path().join("file.nix");
+        let contents = indoc! {r#"
+            self: with self; {
+              a.sub = null;
+              b = null;
+              c = import ./file.nix;
+              d = import ./file.nix { };
+              e = pythonPackages.callPackage ./file.nix { };
+              f = callPackage ./file.nix { };
+              g = callPackage ({ }: { }) { };
+              h = callPackage ./file.nix { x = 0; };
+              i = callPackage ({ }: { }) (let in { });
+            }
+        "#};
+
+        std::fs::write(&file, contents)?;
+
+        let nix_file = NixFile::new(&file)?;
+
+        let cases = [
+            (2, None),
+            (3, None),
+            (4, None),
+            (5, None),
+            (
+                6,
+                Some(CallPackageArgumentInfo {
+                    relative_path: Some(PathBuf::from("file.nix")),
+                    empty_arg: true,
+                }),
+            ),
+            (
+                7,
+                Some(CallPackageArgumentInfo {
+                    relative_path: Some(PathBuf::from("file.nix")),
+                    empty_arg: true,
+                }),
+            ),
+            (
+                8,
+                Some(CallPackageArgumentInfo {
+                    relative_path: None,
+                    empty_arg: true,
+                }),
+            ),
+            (
+                9,
+                Some(CallPackageArgumentInfo {
+                    relative_path: Some(PathBuf::from("file.nix")),
+                    empty_arg: false,
+                }),
+            ),
+            (
+                10,
+                Some(CallPackageArgumentInfo {
+                    relative_path: None,
+                    empty_arg: false,
+                }),
+            ),
+        ];
+
+        for (line, expected_result) in cases {
+            let actual_result = nix_file.call_package_argument_info_at(line, 3, temp_dir.path())?;
+            assert_eq!(actual_result, expected_result);
+        }
+
+        Ok(())
+    }
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/nixpkgs_problem.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/nixpkgs_problem.rs
new file mode 100644
index 000000000000..e13869adaa41
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/nixpkgs_problem.rs
@@ -0,0 +1,273 @@
+use crate::structure;
+use crate::utils::PACKAGE_NIX_FILENAME;
+use std::ffi::OsString;
+use std::fmt;
+use std::io;
+use std::path::PathBuf;
+
+/// Any problem that can occur when checking Nixpkgs
+pub enum NixpkgsProblem {
+    ShardNonDir {
+        relative_shard_path: PathBuf,
+    },
+    InvalidShardName {
+        relative_shard_path: PathBuf,
+        shard_name: String,
+    },
+    PackageNonDir {
+        relative_package_dir: PathBuf,
+    },
+    CaseSensitiveDuplicate {
+        relative_shard_path: PathBuf,
+        first: OsString,
+        second: OsString,
+    },
+    InvalidPackageName {
+        relative_package_dir: PathBuf,
+        package_name: String,
+    },
+    IncorrectShard {
+        relative_package_dir: PathBuf,
+        correct_relative_package_dir: PathBuf,
+    },
+    PackageNixNonExistent {
+        relative_package_dir: PathBuf,
+    },
+    PackageNixDir {
+        relative_package_dir: PathBuf,
+    },
+    UndefinedAttr {
+        relative_package_file: PathBuf,
+        package_name: String,
+    },
+    WrongCallPackage {
+        relative_package_file: PathBuf,
+        package_name: String,
+    },
+    NonDerivation {
+        relative_package_file: PathBuf,
+        package_name: String,
+    },
+    OutsideSymlink {
+        relative_package_dir: PathBuf,
+        subpath: PathBuf,
+    },
+    UnresolvableSymlink {
+        relative_package_dir: PathBuf,
+        subpath: PathBuf,
+        io_error: io::Error,
+    },
+    PathInterpolation {
+        relative_package_dir: PathBuf,
+        subpath: PathBuf,
+        line: usize,
+        text: String,
+    },
+    SearchPath {
+        relative_package_dir: PathBuf,
+        subpath: PathBuf,
+        line: usize,
+        text: String,
+    },
+    OutsidePathReference {
+        relative_package_dir: PathBuf,
+        subpath: PathBuf,
+        line: usize,
+        text: String,
+    },
+    UnresolvablePathReference {
+        relative_package_dir: PathBuf,
+        subpath: PathBuf,
+        line: usize,
+        text: String,
+        io_error: io::Error,
+    },
+    MovedOutOfByName {
+        package_name: String,
+        call_package_path: Option<PathBuf>,
+        empty_arg: bool,
+    },
+    NewPackageNotUsingByName {
+        package_name: String,
+        call_package_path: Option<PathBuf>,
+        empty_arg: bool,
+    },
+    InternalCallPackageUsed {
+        attr_name: String,
+    },
+    CannotDetermineAttributeLocation {
+        attr_name: String,
+    },
+}
+
+impl fmt::Display for NixpkgsProblem {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            NixpkgsProblem::ShardNonDir { relative_shard_path } =>
+                write!(
+                    f,
+                    "{}: This is a file, but it should be a directory.",
+                    relative_shard_path.display(),
+                ),
+            NixpkgsProblem::InvalidShardName { relative_shard_path, shard_name } =>
+                write!(
+                    f,
+                    "{}: Invalid directory name \"{shard_name}\", must be at most 2 ASCII characters consisting of a-z, 0-9, \"-\" or \"_\".",
+                    relative_shard_path.display()
+                ),
+            NixpkgsProblem::PackageNonDir { relative_package_dir } =>
+                write!(
+                    f,
+                    "{}: This path is a file, but it should be a directory.",
+                    relative_package_dir.display(),
+                ),
+            NixpkgsProblem::CaseSensitiveDuplicate { relative_shard_path, first, second } =>
+                write!(
+                    f,
+                    "{}: Duplicate case-sensitive package directories {first:?} and {second:?}.",
+                    relative_shard_path.display(),
+                ),
+            NixpkgsProblem::InvalidPackageName { relative_package_dir, package_name } =>
+                write!(
+                    f,
+                    "{}: Invalid package directory name \"{package_name}\", must be ASCII characters consisting of a-z, A-Z, 0-9, \"-\" or \"_\".",
+                    relative_package_dir.display(),
+                ),
+            NixpkgsProblem::IncorrectShard { relative_package_dir, correct_relative_package_dir } =>
+                write!(
+                    f,
+                    "{}: Incorrect directory location, should be {} instead.",
+                    relative_package_dir.display(),
+                    correct_relative_package_dir.display(),
+                ),
+            NixpkgsProblem::PackageNixNonExistent { relative_package_dir } =>
+                write!(
+                    f,
+                    "{}: Missing required \"{PACKAGE_NIX_FILENAME}\" file.",
+                    relative_package_dir.display(),
+                ),
+            NixpkgsProblem::PackageNixDir { relative_package_dir } =>
+                write!(
+                    f,
+                    "{}: \"{PACKAGE_NIX_FILENAME}\" must be a file.",
+                    relative_package_dir.display(),
+                ),
+            NixpkgsProblem::UndefinedAttr { relative_package_file, package_name } =>
+                write!(
+                    f,
+                    "pkgs.{package_name}: This attribute is not defined but it should be defined automatically as {}",
+                    relative_package_file.display()
+                ),
+            NixpkgsProblem::WrongCallPackage { relative_package_file, package_name } =>
+                write!(
+                    f,
+                    "pkgs.{package_name}: This attribute is manually defined (most likely in pkgs/top-level/all-packages.nix), which is only allowed if the definition is of the form `pkgs.callPackage {} {{ ... }}` with a non-empty second argument.",
+                    relative_package_file.display()
+                ),
+            NixpkgsProblem::NonDerivation { relative_package_file, package_name } =>
+                write!(
+                    f,
+                    "pkgs.{package_name}: This attribute defined by {} is not a derivation",
+                    relative_package_file.display()
+                ),
+            NixpkgsProblem::OutsideSymlink { relative_package_dir, subpath } =>
+                write!(
+                    f,
+                    "{}: Path {} is a symlink pointing to a path outside the directory of that package.",
+                    relative_package_dir.display(),
+                    subpath.display(),
+                ),
+            NixpkgsProblem::UnresolvableSymlink { relative_package_dir, subpath, io_error } =>
+                write!(
+                    f,
+                    "{}: Path {} is a symlink which cannot be resolved: {io_error}.",
+                    relative_package_dir.display(),
+                    subpath.display(),
+                ),
+            NixpkgsProblem::PathInterpolation { relative_package_dir, subpath, line, text } =>
+                write!(
+                    f,
+                    "{}: File {} at line {line} contains the path expression \"{}\", which is not yet supported and may point outside the directory of that package.",
+                    relative_package_dir.display(),
+                    subpath.display(),
+                    text
+                ),
+            NixpkgsProblem::SearchPath { relative_package_dir, subpath, line, text } =>
+                write!(
+                    f,
+                    "{}: File {} at line {line} contains the nix search path expression \"{}\" which may point outside the directory of that package.",
+                    relative_package_dir.display(),
+                    subpath.display(),
+                    text
+                ),
+            NixpkgsProblem::OutsidePathReference { relative_package_dir, subpath, line, text } =>
+                write!(
+                    f,
+                    "{}: File {} at line {line} contains the path expression \"{}\" which may point outside the directory of that package.",
+                    relative_package_dir.display(),
+                    subpath.display(),
+                    text,
+                ),
+            NixpkgsProblem::UnresolvablePathReference { relative_package_dir, subpath, line, text, io_error } =>
+                write!(
+                    f,
+                    "{}: File {} at line {line} contains the path expression \"{}\" which cannot be resolved: {io_error}.",
+                    relative_package_dir.display(),
+                    subpath.display(),
+                    text,
+                ),
+            NixpkgsProblem::MovedOutOfByName { package_name, call_package_path, empty_arg } => {
+                let call_package_arg =
+                    if let Some(path) = &call_package_path {
+                        format!("./{}", path.display())
+                    } else {
+                        "...".into()
+                    };
+                if *empty_arg {
+                    write!(
+                        f,
+                        "pkgs.{package_name}: This top-level package was previously defined in {}, but is now manually defined as `callPackage {call_package_arg} {{ }}` (e.g. in `pkgs/top-level/all-packages.nix`). Please move the package back and remove the manual `callPackage`.",
+                        structure::relative_file_for_package(package_name).display(),
+                        )
+                } else {
+                    // This can happen if users mistakenly assume that for custom arguments,
+                    // pkgs/by-name can't be used.
+                    write!(
+                        f,
+                        "pkgs.{package_name}: This top-level package was previously defined in {}, but is now manually defined as `callPackage {call_package_arg} {{ ... }}` (e.g. in `pkgs/top-level/all-packages.nix`). While the manual `callPackage` is still needed, it's not necessary to move the package files.",
+                        structure::relative_file_for_package(package_name).display(),
+                        )
+                }
+            },
+            NixpkgsProblem::NewPackageNotUsingByName { package_name, call_package_path, empty_arg } => {
+                let call_package_arg =
+                    if let Some(path) = &call_package_path {
+                        format!("./{}", path.display())
+                    } else {
+                        "...".into()
+                    };
+                let extra =
+                    if *empty_arg {
+                        "Since the second `callPackage` argument is `{ }`, no manual `callPackage` (e.g. in `pkgs/top-level/all-packages.nix`) is needed anymore."
+                    } else {
+                        "Since the second `callPackage` argument is not `{ }`, the manual `callPackage` (e.g. in `pkgs/top-level/all-packages.nix`) is still needed."
+                    };
+                write!(
+                    f,
+                    "pkgs.{package_name}: This is a new top-level package of the form `callPackage {call_package_arg} {{ }}`. Please define it in {} instead. See `pkgs/by-name/README.md` for more details. {extra}",
+                    structure::relative_file_for_package(package_name).display(),
+                )
+            },
+            NixpkgsProblem::InternalCallPackageUsed { attr_name } =>
+                write!(
+                    f,
+                    "pkgs.{attr_name}: This attribute is defined using `_internalCallByNamePackageFile`, which is an internal function not intended for manual use.",
+                ),
+            NixpkgsProblem::CannotDetermineAttributeLocation { attr_name } =>
+                write!(
+                    f,
+                    "pkgs.{attr_name}: Cannot determine the location of this attribute using `builtins.unsafeGetAttrPos`.",
+                ),
+       }
+    }
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/ratchet.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/ratchet.rs
new file mode 100644
index 000000000000..200bf92c516a
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/ratchet.rs
@@ -0,0 +1,173 @@
+//! This module implements the ratchet checks, see ../README.md#ratchet-checks
+//!
+//! Each type has a `compare` method that validates the ratchet checks for that item.
+
+use crate::nix_file::CallPackageArgumentInfo;
+use crate::nixpkgs_problem::NixpkgsProblem;
+use crate::structure;
+use crate::validation::{self, Validation, Validation::Success};
+use std::collections::HashMap;
+
+/// The ratchet value for the entirety of Nixpkgs.
+#[derive(Default)]
+pub struct Nixpkgs {
+    /// Sorted list of packages in package_map
+    pub package_names: Vec<String>,
+    /// The ratchet values for all packages
+    pub package_map: HashMap<String, Package>,
+}
+
+impl Nixpkgs {
+    /// Validates the ratchet checks for Nixpkgs
+    pub fn compare(from: Self, to: Self) -> Validation<()> {
+        validation::sequence_(
+            // We only loop over the current attributes,
+            // we don't need to check ones that were removed
+            to.package_names.into_iter().map(|name| {
+                Package::compare(&name, from.package_map.get(&name), &to.package_map[&name])
+            }),
+        )
+    }
+}
+
+/// The ratchet value for a top-level package
+pub struct Package {
+    /// The ratchet value for the check for non-auto-called empty arguments
+    pub manual_definition: RatchetState<ManualDefinition>,
+
+    /// The ratchet value for the check for new packages using pkgs/by-name
+    pub uses_by_name: RatchetState<UsesByName>,
+}
+
+impl Package {
+    /// Validates the ratchet checks for a top-level package
+    pub fn compare(name: &str, optional_from: Option<&Self>, to: &Self) -> Validation<()> {
+        validation::sequence_([
+            RatchetState::<ManualDefinition>::compare(
+                name,
+                optional_from.map(|x| &x.manual_definition),
+                &to.manual_definition,
+            ),
+            RatchetState::<UsesByName>::compare(
+                name,
+                optional_from.map(|x| &x.uses_by_name),
+                &to.uses_by_name,
+            ),
+        ])
+    }
+}
+
+/// The ratchet state of a generic ratchet check.
+pub enum RatchetState<Ratchet: ToNixpkgsProblem> {
+    /// The ratchet is loose, it can be tightened more.
+    /// In other words, this is the legacy state we're trying to move away from.
+    /// Introducing new instances is not allowed but previous instances will continue to be allowed.
+    /// The `Context` is context for error messages in case a new instance of this state is
+    /// introduced
+    Loose(Ratchet::ToContext),
+    /// The ratchet is tight, it can't be tightened any further.
+    /// This is either because we already use the latest state, or because the ratchet isn't
+    /// relevant.
+    Tight,
+    /// This ratchet can't be applied.
+    /// State transitions from/to NonApplicable are always allowed
+    NonApplicable,
+}
+
+/// A trait that can convert an attribute-specific error context into a NixpkgsProblem
+pub trait ToNixpkgsProblem {
+    /// Context relating to the Nixpkgs that is being transitioned _to_
+    type ToContext;
+
+    /// How to convert an attribute-specific error context into a NixpkgsProblem
+    fn to_nixpkgs_problem(
+        name: &str,
+        optional_from: Option<()>,
+        to: &Self::ToContext,
+    ) -> NixpkgsProblem;
+}
+
+impl<Context: ToNixpkgsProblem> RatchetState<Context> {
+    /// Compare the previous ratchet state of an attribute to the new state.
+    /// The previous state may be `None` in case the attribute is new.
+    fn compare(name: &str, optional_from: Option<&Self>, to: &Self) -> Validation<()> {
+        match (optional_from, to) {
+            // Loosening a ratchet is now allowed
+            (Some(RatchetState::Tight), RatchetState::Loose(loose_context)) => {
+                Context::to_nixpkgs_problem(name, Some(()), loose_context).into()
+            }
+
+            // Introducing a loose ratchet is also not allowed
+            (None, RatchetState::Loose(loose_context)) => {
+                Context::to_nixpkgs_problem(name, None, loose_context).into()
+            }
+
+            // Everything else is allowed, including:
+            // - Loose -> Loose (grandfathering policy for a loose ratchet)
+            // - -> Tight (always okay to keep or make the ratchet tight)
+            // - Anything involving NotApplicable, where we can't really make any good calls
+            _ => Success(()),
+        }
+    }
+}
+
+/// The ratchet to check whether a top-level attribute has/needs
+/// a manual definition, e.g. in all-packages.nix.
+///
+/// This ratchet is only tight for attributes that:
+/// - Are not defined in `pkgs/by-name`, and rely on a manual definition
+/// - Are defined in `pkgs/by-name` without any manual definition,
+///   (no custom argument overrides)
+/// - Are defined with `pkgs/by-name` with a manual definition that can't be removed
+///   because it provides custom argument overrides
+///
+/// In comparison, this ratchet is loose for attributes that:
+/// - Are defined in `pkgs/by-name` with a manual definition
+///   that doesn't have any custom argument overrides
+pub enum ManualDefinition {}
+
+impl ToNixpkgsProblem for ManualDefinition {
+    type ToContext = ();
+
+    fn to_nixpkgs_problem(
+        name: &str,
+        _optional_from: Option<()>,
+        _to: &Self::ToContext,
+    ) -> NixpkgsProblem {
+        NixpkgsProblem::WrongCallPackage {
+            relative_package_file: structure::relative_file_for_package(name),
+            package_name: name.to_owned(),
+        }
+    }
+}
+
+/// The ratchet value of an attribute
+/// for the check that new packages use pkgs/by-name
+///
+/// This checks that all new package defined using callPackage must be defined via pkgs/by-name
+/// It also checks that once a package uses pkgs/by-name, it can't switch back to all-packages.nix
+pub enum UsesByName {}
+
+impl ToNixpkgsProblem for UsesByName {
+    type ToContext = CallPackageArgumentInfo;
+
+    fn to_nixpkgs_problem(
+        name: &str,
+        optional_from: Option<()>,
+        to: &Self::ToContext,
+    ) -> NixpkgsProblem {
+        if let Some(()) = optional_from {
+            NixpkgsProblem::MovedOutOfByName {
+                package_name: name.to_owned(),
+                call_package_path: to.relative_path.clone(),
+                empty_arg: to.empty_arg,
+            }
+        } else {
+            NixpkgsProblem::NewPackageNotUsingByName {
+                package_name: name.to_owned(),
+                call_package_path: to.relative_path.clone(),
+                empty_arg: to.empty_arg,
+            }
+        }
+    }
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/references.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/references.rs
new file mode 100644
index 000000000000..169e996300ba
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/references.rs
@@ -0,0 +1,174 @@
+use crate::nixpkgs_problem::NixpkgsProblem;
+use crate::utils;
+use crate::validation::{self, ResultIteratorExt, Validation::Success};
+use crate::NixFileStore;
+
+use anyhow::Context;
+use rowan::ast::AstNode;
+use std::ffi::OsStr;
+use std::path::Path;
+
+/// Check that every package directory in pkgs/by-name doesn't link to outside that directory.
+/// Both symlinks and Nix path expressions are checked.
+pub fn check_references(
+    nix_file_store: &mut NixFileStore,
+    relative_package_dir: &Path,
+    absolute_package_dir: &Path,
+) -> validation::Result<()> {
+    // The first subpath to check is the package directory itself, which we can represent as an
+    // empty path, since the absolute package directory gets prepended to this.
+    // We don't use `./.` to keep the error messages cleaner
+    // (there's no canonicalisation going on underneath)
+    let subpath = Path::new("");
+    check_path(
+        nix_file_store,
+        relative_package_dir,
+        absolute_package_dir,
+        subpath,
+    )
+    .with_context(|| {
+        format!(
+            "While checking the references in package directory {}",
+            relative_package_dir.display()
+        )
+    })
+}
+
+/// Checks for a specific path to not have references outside
+///
+/// The subpath is the relative path within the package directory we're currently checking.
+/// A relative path so that the error messages don't get absolute paths (which are messy in CI).
+/// The absolute package directory gets prepended before doing anything with it though.
+fn check_path(
+    nix_file_store: &mut NixFileStore,
+    relative_package_dir: &Path,
+    absolute_package_dir: &Path,
+    subpath: &Path,
+) -> validation::Result<()> {
+    let path = absolute_package_dir.join(subpath);
+
+    Ok(if path.is_symlink() {
+        // Check whether the symlink resolves to outside the package directory
+        match path.canonicalize() {
+            Ok(target) => {
+                // No need to handle the case of it being inside the directory, since we scan through the
+                // entire directory recursively anyways
+                if let Err(_prefix_error) = target.strip_prefix(absolute_package_dir) {
+                    NixpkgsProblem::OutsideSymlink {
+                        relative_package_dir: relative_package_dir.to_path_buf(),
+                        subpath: subpath.to_path_buf(),
+                    }
+                    .into()
+                } else {
+                    Success(())
+                }
+            }
+            Err(io_error) => NixpkgsProblem::UnresolvableSymlink {
+                relative_package_dir: relative_package_dir.to_path_buf(),
+                subpath: subpath.to_path_buf(),
+                io_error,
+            }
+            .into(),
+        }
+    } else if path.is_dir() {
+        // Recursively check each entry
+        validation::sequence_(
+            utils::read_dir_sorted(&path)?
+                .into_iter()
+                .map(|entry| {
+                    check_path(
+                        nix_file_store,
+                        relative_package_dir,
+                        absolute_package_dir,
+                        &subpath.join(entry.file_name()),
+                    )
+                })
+                .collect_vec()
+                .with_context(|| format!("Error while recursing into {}", subpath.display()))?,
+        )
+    } else if path.is_file() {
+        // Only check Nix files
+        if let Some(ext) = path.extension() {
+            if ext == OsStr::new("nix") {
+                check_nix_file(
+                    nix_file_store,
+                    relative_package_dir,
+                    absolute_package_dir,
+                    subpath,
+                )
+                .with_context(|| format!("Error while checking Nix file {}", subpath.display()))?
+            } else {
+                Success(())
+            }
+        } else {
+            Success(())
+        }
+    } else {
+        // This should never happen, git doesn't support other file types
+        anyhow::bail!("Unsupported file type for path {}", subpath.display());
+    })
+}
+
+/// Check whether a nix file contains path expression references pointing outside the package
+/// directory
+fn check_nix_file(
+    nix_file_store: &mut NixFileStore,
+    relative_package_dir: &Path,
+    absolute_package_dir: &Path,
+    subpath: &Path,
+) -> validation::Result<()> {
+    let path = absolute_package_dir.join(subpath);
+
+    let nix_file = nix_file_store.get(&path)?;
+
+    Ok(validation::sequence_(
+        nix_file.syntax_root.syntax().descendants().map(|node| {
+            let text = node.text().to_string();
+            let line = nix_file.line_index.line(node.text_range().start().into());
+
+            // We're only interested in Path expressions
+            let Some(path) = rnix::ast::Path::cast(node) else {
+                return Success(());
+            };
+
+            use crate::nix_file::ResolvedPath;
+
+            match nix_file.static_resolve_path(path, absolute_package_dir) {
+                ResolvedPath::Interpolated => NixpkgsProblem::PathInterpolation {
+                    relative_package_dir: relative_package_dir.to_path_buf(),
+                    subpath: subpath.to_path_buf(),
+                    line,
+                    text,
+                }
+                .into(),
+                ResolvedPath::SearchPath => NixpkgsProblem::SearchPath {
+                    relative_package_dir: relative_package_dir.to_path_buf(),
+                    subpath: subpath.to_path_buf(),
+                    line,
+                    text,
+                }
+                .into(),
+                ResolvedPath::Outside => NixpkgsProblem::OutsidePathReference {
+                    relative_package_dir: relative_package_dir.to_path_buf(),
+                    subpath: subpath.to_path_buf(),
+                    line,
+                    text,
+                }
+                .into(),
+                ResolvedPath::Unresolvable(e) => NixpkgsProblem::UnresolvablePathReference {
+                    relative_package_dir: relative_package_dir.to_path_buf(),
+                    subpath: subpath.to_path_buf(),
+                    line,
+                    text,
+                    io_error: e,
+                }
+                .into(),
+                ResolvedPath::Within(..) => {
+                    // No need to handle the case of it being inside the directory, since we scan through the
+                    // entire directory recursively anyways
+                    Success(())
+                }
+            }
+        }),
+    ))
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/structure.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/structure.rs
new file mode 100644
index 000000000000..9b615dd9969a
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/structure.rs
@@ -0,0 +1,182 @@
+use crate::nixpkgs_problem::NixpkgsProblem;
+use crate::references;
+use crate::utils;
+use crate::utils::{BASE_SUBPATH, PACKAGE_NIX_FILENAME};
+use crate::validation::{self, ResultIteratorExt, Validation::Success};
+use crate::NixFileStore;
+use itertools::concat;
+use lazy_static::lazy_static;
+use regex::Regex;
+use std::fs::DirEntry;
+use std::path::{Path, PathBuf};
+
+lazy_static! {
+    static ref SHARD_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_-]{1,2}$").unwrap();
+    static ref PACKAGE_NAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap();
+}
+
+// Some utility functions for the basic structure
+
+pub fn shard_for_package(package_name: &str) -> String {
+    package_name.to_lowercase().chars().take(2).collect()
+}
+
+pub fn relative_dir_for_shard(shard_name: &str) -> PathBuf {
+    PathBuf::from(format!("{BASE_SUBPATH}/{shard_name}"))
+}
+
+pub fn relative_dir_for_package(package_name: &str) -> PathBuf {
+    relative_dir_for_shard(&shard_for_package(package_name)).join(package_name)
+}
+
+pub fn relative_file_for_package(package_name: &str) -> PathBuf {
+    relative_dir_for_package(package_name).join(PACKAGE_NIX_FILENAME)
+}
+
+/// Check the structure of Nixpkgs, returning the attribute names that are defined in
+/// `pkgs/by-name`
+pub fn check_structure(
+    path: &Path,
+    nix_file_store: &mut NixFileStore,
+) -> validation::Result<Vec<String>> {
+    let base_dir = path.join(BASE_SUBPATH);
+
+    let shard_results = utils::read_dir_sorted(&base_dir)?
+        .into_iter()
+        .map(|shard_entry| -> validation::Result<_> {
+            let shard_path = shard_entry.path();
+            let shard_name = shard_entry.file_name().to_string_lossy().into_owned();
+            let relative_shard_path = relative_dir_for_shard(&shard_name);
+
+            Ok(if shard_name == "README.md" {
+                // README.md is allowed to be a file and not checked
+
+                Success(vec![])
+            } else if !shard_path.is_dir() {
+                NixpkgsProblem::ShardNonDir {
+                    relative_shard_path: relative_shard_path.clone(),
+                }
+                .into()
+                // we can't check for any other errors if it's a file, since there's no subdirectories to check
+            } else {
+                let shard_name_valid = SHARD_NAME_REGEX.is_match(&shard_name);
+                let result = if !shard_name_valid {
+                    NixpkgsProblem::InvalidShardName {
+                        relative_shard_path: relative_shard_path.clone(),
+                        shard_name: shard_name.clone(),
+                    }
+                    .into()
+                } else {
+                    Success(())
+                };
+
+                let entries = utils::read_dir_sorted(&shard_path)?;
+
+                let duplicate_results = entries
+                    .iter()
+                    .zip(entries.iter().skip(1))
+                    .filter(|(l, r)| {
+                        l.file_name().to_ascii_lowercase() == r.file_name().to_ascii_lowercase()
+                    })
+                    .map(|(l, r)| {
+                        NixpkgsProblem::CaseSensitiveDuplicate {
+                            relative_shard_path: relative_shard_path.clone(),
+                            first: l.file_name(),
+                            second: r.file_name(),
+                        }
+                        .into()
+                    });
+
+                let result = result.and(validation::sequence_(duplicate_results));
+
+                let package_results = entries
+                    .into_iter()
+                    .map(|package_entry| {
+                        check_package(
+                            nix_file_store,
+                            path,
+                            &shard_name,
+                            shard_name_valid,
+                            package_entry,
+                        )
+                    })
+                    .collect_vec()?;
+
+                result.and(validation::sequence(package_results))
+            })
+        })
+        .collect_vec()?;
+
+    // Combine the package names conatained within each shard into a longer list
+    Ok(validation::sequence(shard_results).map(concat))
+}
+
+fn check_package(
+    nix_file_store: &mut NixFileStore,
+    path: &Path,
+    shard_name: &str,
+    shard_name_valid: bool,
+    package_entry: DirEntry,
+) -> validation::Result<String> {
+    let package_path = package_entry.path();
+    let package_name = package_entry.file_name().to_string_lossy().into_owned();
+    let relative_package_dir = PathBuf::from(format!("{BASE_SUBPATH}/{shard_name}/{package_name}"));
+
+    Ok(if !package_path.is_dir() {
+        NixpkgsProblem::PackageNonDir {
+            relative_package_dir: relative_package_dir.clone(),
+        }
+        .into()
+    } else {
+        let package_name_valid = PACKAGE_NAME_REGEX.is_match(&package_name);
+        let result = if !package_name_valid {
+            NixpkgsProblem::InvalidPackageName {
+                relative_package_dir: relative_package_dir.clone(),
+                package_name: package_name.clone(),
+            }
+            .into()
+        } else {
+            Success(())
+        };
+
+        let correct_relative_package_dir = relative_dir_for_package(&package_name);
+        let result = result.and(if relative_package_dir != correct_relative_package_dir {
+            // Only show this error if we have a valid shard and package name
+            // Because if one of those is wrong, you should fix that first
+            if shard_name_valid && package_name_valid {
+                NixpkgsProblem::IncorrectShard {
+                    relative_package_dir: relative_package_dir.clone(),
+                    correct_relative_package_dir: correct_relative_package_dir.clone(),
+                }
+                .into()
+            } else {
+                Success(())
+            }
+        } else {
+            Success(())
+        });
+
+        let package_nix_path = package_path.join(PACKAGE_NIX_FILENAME);
+        let result = result.and(if !package_nix_path.exists() {
+            NixpkgsProblem::PackageNixNonExistent {
+                relative_package_dir: relative_package_dir.clone(),
+            }
+            .into()
+        } else if package_nix_path.is_dir() {
+            NixpkgsProblem::PackageNixDir {
+                relative_package_dir: relative_package_dir.clone(),
+            }
+            .into()
+        } else {
+            Success(())
+        });
+
+        let result = result.and(references::check_references(
+            nix_file_store,
+            &relative_package_dir,
+            &path.join(&relative_package_dir),
+        )?);
+
+        result.map(|_| package_name.clone())
+    })
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/utils.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/utils.rs
new file mode 100644
index 000000000000..9a5d12748918
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/utils.rs
@@ -0,0 +1,95 @@
+use anyhow::Context;
+use std::fs;
+use std::io;
+use std::path::Path;
+
+pub const BASE_SUBPATH: &str = "pkgs/by-name";
+pub const PACKAGE_NIX_FILENAME: &str = "package.nix";
+
+/// Deterministic file listing so that tests are reproducible
+pub fn read_dir_sorted(base_dir: &Path) -> anyhow::Result<Vec<fs::DirEntry>> {
+    let listing = base_dir
+        .read_dir()
+        .with_context(|| format!("Could not list directory {}", base_dir.display()))?;
+    let mut shard_entries = listing
+        .collect::<io::Result<Vec<_>>>()
+        .with_context(|| format!("Could not list directory {}", base_dir.display()))?;
+    shard_entries.sort_by_key(|entry| entry.file_name());
+    Ok(shard_entries)
+}
+
+/// A simple utility for calculating the line for a string offset.
+/// This doesn't do any Unicode handling, though that probably doesn't matter
+/// because newlines can't split up Unicode characters. Also this is only used
+/// for error reporting
+pub struct LineIndex {
+    /// Stores the indices of newlines
+    newlines: Vec<usize>,
+}
+
+impl LineIndex {
+    pub fn new(s: &str) -> LineIndex {
+        let mut newlines = vec![];
+        let mut index = 0;
+        // Iterates over all newline-split parts of the string, adding the index of the newline to
+        // the vec
+        for split in s.split_inclusive('\n') {
+            index += split.len();
+            newlines.push(index - 1);
+        }
+        LineIndex { newlines }
+    }
+
+    /// Returns the line number for a string index.
+    /// If the index points to a newline, returns the line number before the newline
+    pub fn line(&self, index: usize) -> usize {
+        match self.newlines.binary_search(&index) {
+            // +1 because lines are 1-indexed
+            Ok(x) => x + 1,
+            Err(x) => x + 1,
+        }
+    }
+
+    /// Returns the string index for a line and column.
+    pub fn fromlinecolumn(&self, line: usize, column: usize) -> usize {
+        // If it's the 1th line, the column is the index
+        if line == 1 {
+            // But columns are 1-indexed
+            column - 1
+        } else {
+            // For the nth line, we add the index of the (n-1)st newline to the column,
+            // and remove one more from the index since arrays are 0-indexed.
+            // Then add the 1-indexed column to get not the newline index itself,
+            // but rather the index of the position on the next line
+            self.newlines[line - 2] + column
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn line_index() {
+        let line_index = LineIndex::new("a\nbc\n\ndef\n");
+
+        let pairs = [
+            (0, 1, 1),
+            (1, 1, 2),
+            (2, 2, 1),
+            (3, 2, 2),
+            (4, 2, 3),
+            (5, 3, 1),
+            (6, 4, 1),
+            (7, 4, 2),
+            (8, 4, 3),
+            (9, 4, 4),
+        ];
+
+        for (index, line, column) in pairs {
+            assert_eq!(line_index.line(index), line);
+            assert_eq!(line_index.fromlinecolumn(line, column), index);
+        }
+    }
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/validation.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/validation.rs
new file mode 100644
index 000000000000..b14bfb92eb2e
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/validation.rs
@@ -0,0 +1,111 @@
+use crate::nixpkgs_problem::NixpkgsProblem;
+use itertools::concat;
+use itertools::{
+    Either::{Left, Right},
+    Itertools,
+};
+use Validation::*;
+
+/// The validation result of a check.
+/// Instead of exiting at the first failure,
+/// this type can accumulate multiple failures.
+/// This can be achieved using the functions `and`, `sequence` and `sequence_`
+///
+/// This leans on https://hackage.haskell.org/package/validation
+pub enum Validation<A> {
+    Failure(Vec<NixpkgsProblem>),
+    Success(A),
+}
+
+impl<A> From<NixpkgsProblem> for Validation<A> {
+    /// Create a `Validation<A>` from a single check problem
+    fn from(value: NixpkgsProblem) -> Self {
+        Failure(vec![value])
+    }
+}
+
+/// A type alias representing the result of a check, either:
+/// - Err(anyhow::Error): A fatal failure, typically I/O errors.
+///   Such failures are not caused by the files in Nixpkgs.
+///   This hints at a bug in the code or a problem with the deployment.
+/// - Ok(Failure(Vec<NixpkgsProblem>)): A non-fatal validation problem with the Nixpkgs files.
+///   Further checks can be run even with this result type.
+///   Such problems can be fixed by changing the Nixpkgs files.
+/// - Ok(Success(A)): A successful (potentially intermediate) result with an arbitrary value.
+///   No fatal errors have occurred and no validation problems have been found with Nixpkgs.
+pub type Result<A> = anyhow::Result<Validation<A>>;
+
+pub trait ResultIteratorExt<A, E>: Sized + Iterator<Item = std::result::Result<A, E>> {
+    fn collect_vec(self) -> std::result::Result<Vec<A>, E>;
+}
+
+impl<I, A, E> ResultIteratorExt<A, E> for I
+where
+    I: Sized + Iterator<Item = std::result::Result<A, E>>,
+{
+    /// A convenience version of `collect` specialised to a vector
+    fn collect_vec(self) -> std::result::Result<Vec<A>, E> {
+        self.collect()
+    }
+}
+
+impl<A> Validation<A> {
+    /// Map a `Validation<A>` to a `Validation<B>` by applying a function to the
+    /// potentially contained value in case of success.
+    pub fn map<B>(self, f: impl FnOnce(A) -> B) -> Validation<B> {
+        match self {
+            Failure(err) => Failure(err),
+            Success(value) => Success(f(value)),
+        }
+    }
+
+    /// Map a `Validation<A>` to a `Result<B>` by applying a function `A -> Result<B>`
+    /// only if there is a `Success` value
+    pub fn result_map<B>(self, f: impl FnOnce(A) -> Result<B>) -> Result<B> {
+        match self {
+            Failure(err) => Ok(Failure(err)),
+            Success(value) => f(value),
+        }
+    }
+}
+
+impl Validation<()> {
+    /// Combine two validations, both of which need to be successful for the return value to be successful.
+    /// The `NixpkgsProblem`s of both sides are returned concatenated.
+    pub fn and<A>(self, other: Validation<A>) -> Validation<A> {
+        match (self, other) {
+            (Success(_), Success(right_value)) => Success(right_value),
+            (Failure(errors), Success(_)) => Failure(errors),
+            (Success(_), Failure(errors)) => Failure(errors),
+            (Failure(errors_l), Failure(errors_r)) => Failure(concat([errors_l, errors_r])),
+        }
+    }
+}
+
+/// Combine many validations into a single one.
+/// All given validations need to be successful in order for the returned validation to be successful,
+/// in which case the returned validation value contains a `Vec` of each individual value.
+/// Otherwise the `NixpkgsProblem`s of all validations are returned concatenated.
+pub fn sequence<A>(check_results: impl IntoIterator<Item = Validation<A>>) -> Validation<Vec<A>> {
+    let (errors, values): (Vec<Vec<NixpkgsProblem>>, Vec<A>) = check_results
+        .into_iter()
+        .partition_map(|validation| match validation {
+            Failure(err) => Left(err),
+            Success(value) => Right(value),
+        });
+
+    // To combine the errors from the results we flatten all the error Vec's into a new Vec
+    // This is not very efficient, but doesn't matter because generally we should have no errors
+    let flattened_errors = errors.into_iter().concat();
+
+    if flattened_errors.is_empty() {
+        Success(values)
+    } else {
+        Failure(flattened_errors)
+    }
+}
+
+/// Like `sequence`, but without any containing value, for convenience
+pub fn sequence_(validations: impl IntoIterator<Item = Validation<()>>) -> Validation<()> {
+    sequence(validations).map(|_| ())
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/aliases.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/aliases.nix
new file mode 100644
index 000000000000..c70ec23478ea
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/aliases.nix
@@ -0,0 +1,3 @@
+self: super: {
+  baz = self.foo;
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/all-packages.nix
new file mode 100644
index 000000000000..2cdfcef08fa6
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/all-packages.nix
@@ -0,0 +1,3 @@
+self: super: {
+  bar = self.foo;
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/pkgs/by-name/fo/foo/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/pkgs/by-name/fo/foo/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/aliases/pkgs/by-name/fo/foo/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/broken-autocall/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/broken-autocall/default.nix
new file mode 100644
index 000000000000..bd4825f8bad8
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/broken-autocall/default.nix
@@ -0,0 +1,4 @@
+args:
+builtins.removeAttrs
+  (import <test-nixpkgs> { root = ./.; } args)
+  [ "foo" ]
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/broken-autocall/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/broken-autocall/expected
new file mode 100644
index 000000000000..fff17c6c7cd5
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/broken-autocall/expected
@@ -0,0 +1 @@
+pkgs.foo: This attribute is not defined but it should be defined automatically as pkgs/by-name/fo/foo/package.nix
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/broken-autocall/pkgs/by-name/fo/foo/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/broken-autocall/pkgs/by-name/fo/foo/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/broken-autocall/pkgs/by-name/fo/foo/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/callPackage-syntax/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/callPackage-syntax/all-packages.nix
new file mode 100644
index 000000000000..306d719c9e9d
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/callPackage-syntax/all-packages.nix
@@ -0,0 +1,7 @@
+self: super: {
+  set = self.callPackages ({ callPackage }: {
+    foo = callPackage ({ someDrv }: someDrv) { };
+  }) { };
+
+  inherit (self.set) foo;
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/callPackage-syntax/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/callPackage-syntax/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/callPackage-syntax/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/callPackage-syntax/pkgs/by-name/README.md b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/callPackage-syntax/pkgs/by-name/README.md
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/callPackage-syntax/pkgs/by-name/README.md
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/empty-base/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/empty-base/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/empty-base/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/empty-base/pkgs/by-name/README.md b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/empty-base/pkgs/by-name/README.md
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/empty-base/pkgs/by-name/README.md
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/incorrect-shard/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/incorrect-shard/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/incorrect-shard/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/incorrect-shard/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/incorrect-shard/expected
new file mode 100644
index 000000000000..3627368c0ef0
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/incorrect-shard/expected
@@ -0,0 +1 @@
+pkgs/by-name/aa/FOO: Incorrect directory location, should be pkgs/by-name/fo/FOO instead.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/incorrect-shard/pkgs/by-name/aa/FOO/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/incorrect-shard/pkgs/by-name/aa/FOO/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/incorrect-shard/pkgs/by-name/aa/FOO/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/all-packages.nix
new file mode 100644
index 000000000000..3fbe2d5e51dc
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/all-packages.nix
@@ -0,0 +1,4 @@
+self: super: {
+  foo = self._internalCallByNamePackageFile ./foo.nix;
+  bar = self._internalCallByNamePackageFile ./foo.nix;
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/expected
new file mode 100644
index 000000000000..404795ee5c79
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/expected
@@ -0,0 +1 @@
+pkgs.foo: This attribute is defined using `_internalCallByNamePackageFile`, which is an internal function not intended for manual use.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/foo.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/foo.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/foo.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/pkgs/by-name/README.md b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/pkgs/by-name/README.md
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/pkgs/by-name/README.md
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/pkgs/by-name/fo/foo/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/pkgs/by-name/fo/foo/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/internalCallPackage/pkgs/by-name/fo/foo/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-package-name/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-package-name/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-package-name/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-package-name/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-package-name/expected
new file mode 100644
index 000000000000..8c8eafdcb3d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-package-name/expected
@@ -0,0 +1 @@
+pkgs/by-name/fo/fo@: Invalid package directory name "fo@", must be ASCII characters consisting of a-z, A-Z, 0-9, "-" or "_".
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-package-name/pkgs/by-name/fo/fo@/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-package-name/pkgs/by-name/fo/fo@/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-package-name/pkgs/by-name/fo/fo@/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/expected
new file mode 100644
index 000000000000..248aa8877966
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/expected
@@ -0,0 +1 @@
+pkgs/by-name/A: Invalid directory name "A", must be at most 2 ASCII characters consisting of a-z, 0-9, "-" or "_".
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/pkgs/by-name/A/A/.git-keep b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/pkgs/by-name/A/A/.git-keep
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/pkgs/by-name/A/A/.git-keep
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/pkgs/by-name/A/A/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/pkgs/by-name/A/A/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/invalid-shard-name/pkgs/by-name/A/A/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/all-packages.nix
new file mode 100644
index 000000000000..07b2caaab4e7
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/all-packages.nix
@@ -0,0 +1,10 @@
+self: super: {
+  nonAttributeSet = self.callPackage ({ someDrv }: someDrv) { };
+  nonCallPackage = self.callPackage ({ someDrv }: someDrv) { };
+  internalCallByName = self.callPackage ({ someDrv }: someDrv) { };
+  nonDerivation = self.callPackage ({ someDrv }: someDrv) { };
+
+  onlyMove = self.callPackage ./pkgs/by-name/on/onlyMove/package.nix { };
+
+  noEval = self.callPackage ./pkgs/by-name/no/noEval/package.nix { };
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/all-packages.nix
new file mode 100644
index 000000000000..75efb5952e7a
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/all-packages.nix
@@ -0,0 +1,9 @@
+self: super: {
+  nonAttributeSet = null;
+  nonCallPackage = self.someDrv;
+  internalCallByName = self._internalCallByNamePackageFile ./some-pkg.nix;
+  nonDerivation = self.callPackage ({ }: { }) { };
+
+  onlyMove = self.callPackage ({ someDrv }: someDrv) { };
+  noEval = throw "foo";
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/pkgs/by-name/README.md b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/pkgs/by-name/README.md
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/pkgs/by-name/README.md
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/some-pkg.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/some-pkg.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/base/some-pkg.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/expected
new file mode 100644
index 000000000000..29d33f7dbdc0
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/expected
@@ -0,0 +1,2 @@
+pkgs.noEval: This attribute is manually defined (most likely in pkgs/top-level/all-packages.nix), which is only allowed if the definition is of the form `pkgs.callPackage pkgs/by-name/no/noEval/package.nix { ... }` with a non-empty second argument.
+pkgs.onlyMove: This attribute is manually defined (most likely in pkgs/top-level/all-packages.nix), which is only allowed if the definition is of the form `pkgs.callPackage pkgs/by-name/on/onlyMove/package.nix { ... }` with a non-empty second argument.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/pkgs/by-name/no/noEval/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/pkgs/by-name/no/noEval/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/pkgs/by-name/no/noEval/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/pkgs/by-name/on/onlyMove/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/pkgs/by-name/on/onlyMove/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/manual-definition/pkgs/by-name/on/onlyMove/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/missing-package-nix/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/missing-package-nix/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/missing-package-nix/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/missing-package-nix/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/missing-package-nix/expected
new file mode 100644
index 000000000000..ce1afcbf2d34
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/missing-package-nix/expected
@@ -0,0 +1 @@
+pkgs/by-name/fo/foo: Missing required "package.nix" file.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/missing-package-nix/pkgs/by-name/fo/foo/.git-keep b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/missing-package-nix/pkgs/by-name/fo/foo/.git-keep
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/missing-package-nix/pkgs/by-name/fo/foo/.git-keep
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/mock-nixpkgs.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/mock-nixpkgs.nix
new file mode 100644
index 000000000000..81a9c916ac2d
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/mock-nixpkgs.nix
@@ -0,0 +1,101 @@
+/*
+This file returns a mocked version of Nixpkgs' default.nix for testing purposes.
+It does not depend on Nixpkgs itself for the sake of simplicity.
+
+It takes one attribute as an argument:
+- `root`: The root of Nixpkgs to read other files from, including:
+  - `./pkgs/by-name`: The `pkgs/by-name` directory to test
+  - `./all-packages.nix`: A file containing an overlay to mirror the real `pkgs/top-level/all-packages.nix`.
+    This allows adding overrides on top of the auto-called packages in `pkgs/by-name`.
+
+It returns a Nixpkgs-like function that can be auto-called and evaluates to an attribute set.
+*/
+{
+  root,
+}:
+# The arguments for the Nixpkgs function
+{
+  # Passed by the checker to modify `callPackage`
+  overlays ? [],
+  # Passed by the checker to make sure a real Nixpkgs isn't influenced by impurities
+  config ? {},
+  # Passed by the checker to make sure a real Nixpkgs isn't influenced by impurities
+  system ? null,
+}:
+let
+
+  # Simplified versions of lib functions
+  lib = import <test-nixpkgs/lib>;
+
+  # The base fixed-point function to populate the resulting attribute set
+  pkgsFun = self: {
+    inherit lib;
+    newScope = extra: lib.callPackageWith (self // extra);
+    callPackage = self.newScope { };
+    callPackages = lib.callPackagesWith self;
+    someDrv = { type = "derivation"; };
+  };
+
+  baseDirectory = root + "/pkgs/by-name";
+
+  # Generates { <name> = <file>; } entries mapping package names to their `package.nix` files in `pkgs/by-name`.
+  # Could be more efficient, but this is only for testing.
+  autoCalledPackageFiles =
+    let
+      entries = builtins.readDir baseDirectory;
+
+      namesForShard = shard:
+        if entries.${shard} != "directory" then
+          # Only README.md is allowed to be a file, but it's not this code's job to check for that
+          { }
+        else
+          builtins.mapAttrs
+            (name: _: baseDirectory + "/${shard}/${name}/package.nix")
+            (builtins.readDir (baseDirectory + "/${shard}"));
+
+    in
+    builtins.foldl'
+      (acc: el: acc // el)
+      { }
+      (map namesForShard (builtins.attrNames entries));
+
+  # Turns autoCalledPackageFiles into an overlay that `callPackage`'s all of them
+  autoCalledPackages = self: super:
+    {
+      # Needed to be able to detect empty arguments in all-packages.nix
+      # See a more detailed description in pkgs/top-level/by-name-overlay.nix
+      _internalCallByNamePackageFile = file: self.callPackage file { };
+    }
+    // builtins.mapAttrs
+      (name: self._internalCallByNamePackageFile)
+      autoCalledPackageFiles;
+
+  # A list optionally containing the `all-packages.nix` file from the test case as an overlay
+  optionalAllPackagesOverlay =
+    if builtins.pathExists (root + "/all-packages.nix") then
+      [ (import (root + "/all-packages.nix")) ]
+    else
+      [ ];
+
+  # A list optionally containing the `aliases.nix` file from the test case as an overlay
+  # But only if config.allowAliases is not false
+  optionalAliasesOverlay =
+    if (config.allowAliases or true) && builtins.pathExists (root + "/aliases.nix") then
+      [ (import (root + "/aliases.nix")) ]
+    else
+      [ ];
+
+  # All the overlays in the right order, including the user-supplied ones
+  allOverlays =
+    [
+      autoCalledPackages
+    ]
+    ++ optionalAllPackagesOverlay
+    ++ optionalAliasesOverlay
+    ++ overlays;
+
+  # Apply all the overlays in order to the base fixed-point function pkgsFun
+  f = builtins.foldl' (f: overlay: lib.extends overlay f) pkgsFun allOverlays;
+in
+# Evaluate the fixed-point
+lib.fix f
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/all-packages.nix
new file mode 100644
index 000000000000..16834c4f7856
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/all-packages.nix
@@ -0,0 +1,10 @@
+self: super: {
+  foo1 = self.callPackage ({ someDrv }: someDrv) { };
+  foo2 = self.callPackage ./without-config.nix { };
+  foo3 = self.callPackage ({ someDrv, enableFoo }: someDrv) {
+    enableFoo = null;
+  };
+  foo4 = self.callPackage ./with-config.nix {
+    enableFoo = null;
+  };
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo1/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo1/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo1/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo2/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo2/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo2/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo3/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo3/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo3/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo4/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo4/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/base/pkgs/by-name/fo/foo4/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/expected
new file mode 100644
index 000000000000..96da50b52491
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/expected
@@ -0,0 +1,4 @@
+pkgs.foo1: This top-level package was previously defined in pkgs/by-name/fo/foo1/package.nix, but is now manually defined as `callPackage ... { }` (e.g. in `pkgs/top-level/all-packages.nix`). Please move the package back and remove the manual `callPackage`.
+pkgs.foo2: This top-level package was previously defined in pkgs/by-name/fo/foo2/package.nix, but is now manually defined as `callPackage ./without-config.nix { }` (e.g. in `pkgs/top-level/all-packages.nix`). Please move the package back and remove the manual `callPackage`.
+pkgs.foo3: This top-level package was previously defined in pkgs/by-name/fo/foo3/package.nix, but is now manually defined as `callPackage ... { ... }` (e.g. in `pkgs/top-level/all-packages.nix`). While the manual `callPackage` is still needed, it's not necessary to move the package files.
+pkgs.foo4: This top-level package was previously defined in pkgs/by-name/fo/foo4/package.nix, but is now manually defined as `callPackage ./with-config.nix { ... }` (e.g. in `pkgs/top-level/all-packages.nix`). While the manual `callPackage` is still needed, it's not necessary to move the package files.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/pkgs/by-name/README.md b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/pkgs/by-name/README.md
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/pkgs/by-name/README.md
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/with-config.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/with-config.nix
new file mode 100644
index 000000000000..7cca53882ea5
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/with-config.nix
@@ -0,0 +1 @@
+{ someDrv, enableFoo }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/without-config.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/without-config.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/move-to-non-by-name/without-config.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/expected
new file mode 100644
index 000000000000..ff5d18556ef0
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/expected
@@ -0,0 +1,13 @@
+pkgs/by-name/A: Invalid directory name "A", must be at most 2 ASCII characters consisting of a-z, 0-9, "-" or "_".
+pkgs/by-name/A/fo@: Invalid package directory name "fo@", must be ASCII characters consisting of a-z, A-Z, 0-9, "-" or "_".
+pkgs/by-name/A/fo@: Path foo is a symlink which cannot be resolved: No such file or directory (os error 2).
+pkgs/by-name/A/fo@: Path package.nix is a symlink pointing to a path outside the directory of that package.
+pkgs/by-name/aa: This is a file, but it should be a directory.
+pkgs/by-name/ba/bar: This path is a file, but it should be a directory.
+pkgs/by-name/ba/baz: "package.nix" must be a file.
+pkgs/by-name/ba/foo: Incorrect directory location, should be pkgs/by-name/fo/foo instead.
+pkgs/by-name/ba/foo: File package.nix at line 2 contains the path expression "/bar" which cannot be resolved: No such file or directory (os error 2).
+pkgs/by-name/ba/foo: File package.nix at line 3 contains the path expression "../." which may point outside the directory of that package.
+pkgs/by-name/ba/foo: File package.nix at line 4 contains the nix search path expression "<nixpkgs>" which may point outside the directory of that package.
+pkgs/by-name/ba/foo: File package.nix at line 5 contains the path expression "./${"test"}", which is not yet supported and may point outside the directory of that package.
+pkgs/by-name/fo/foo: Missing required "package.nix" file.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/A/fo@/foo b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/A/fo@/foo
new file mode 120000
index 000000000000..c86c3f3551ec
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/A/fo@/foo
@@ -0,0 +1 @@
+none
\ No newline at end of file
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/A/fo@/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/A/fo@/package.nix
new file mode 120000
index 000000000000..f079163d158a
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/A/fo@/package.nix
@@ -0,0 +1 @@
+../../../../someDrv.nix
\ No newline at end of file
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/aa b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/aa
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/aa
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/ba/bar b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/ba/bar
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/ba/bar
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/ba/baz/package.nix/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/ba/baz/package.nix/default.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/ba/baz/package.nix/default.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/ba/foo/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/ba/foo/package.nix
new file mode 100644
index 000000000000..31b4742f5914
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/ba/foo/package.nix
@@ -0,0 +1,6 @@
+{ someDrv }: someDrv // {
+  escapeAbsolute = /bar;
+  escapeRelative = ../.;
+  nixPath = <nixpkgs>;
+  pathWithSubexpr = ./${"test"};
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/fo/foo/.git-keep b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/fo/foo/.git-keep
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/pkgs/by-name/fo/foo/.git-keep
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/someDrv.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/someDrv.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/multiple-failures/someDrv.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/all-packages.nix
new file mode 100644
index 000000000000..069119ad4c7b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/all-packages.nix
@@ -0,0 +1,11 @@
+self: super: {
+  before = self.callPackage ({ someDrv }: someDrv) { };
+  new1 = self.callPackage ({ someDrv }: someDrv) { };
+  new2 = self.callPackage ./without-config.nix { };
+  new3 = self.callPackage ({ someDrv, enableNew }: someDrv) {
+    enableNew = null;
+  };
+  new4 = self.callPackage ./with-config.nix {
+    enableNew = null;
+  };
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/base/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/base/all-packages.nix
new file mode 100644
index 000000000000..c2665d04d5f2
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/base/all-packages.nix
@@ -0,0 +1,5 @@
+self: super: {
+
+  before = self.callPackage ({ someDrv }: someDrv) { };
+
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/base/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/base/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/base/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/base/pkgs/by-name/README.md b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/base/pkgs/by-name/README.md
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/base/pkgs/by-name/README.md
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/expected
new file mode 100644
index 000000000000..3f294f26dfd8
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/expected
@@ -0,0 +1,4 @@
+pkgs.new1: This is a new top-level package of the form `callPackage ... { }`. Please define it in pkgs/by-name/ne/new1/package.nix instead. See `pkgs/by-name/README.md` for more details. Since the second `callPackage` argument is `{ }`, no manual `callPackage` (e.g. in `pkgs/top-level/all-packages.nix`) is needed anymore.
+pkgs.new2: This is a new top-level package of the form `callPackage ./without-config.nix { }`. Please define it in pkgs/by-name/ne/new2/package.nix instead. See `pkgs/by-name/README.md` for more details. Since the second `callPackage` argument is `{ }`, no manual `callPackage` (e.g. in `pkgs/top-level/all-packages.nix`) is needed anymore.
+pkgs.new3: This is a new top-level package of the form `callPackage ... { }`. Please define it in pkgs/by-name/ne/new3/package.nix instead. See `pkgs/by-name/README.md` for more details. Since the second `callPackage` argument is not `{ }`, the manual `callPackage` (e.g. in `pkgs/top-level/all-packages.nix`) is still needed.
+pkgs.new4: This is a new top-level package of the form `callPackage ./with-config.nix { }`. Please define it in pkgs/by-name/ne/new4/package.nix instead. See `pkgs/by-name/README.md` for more details. Since the second `callPackage` argument is not `{ }`, the manual `callPackage` (e.g. in `pkgs/top-level/all-packages.nix`) is still needed.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/pkgs/by-name/README.md b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/pkgs/by-name/README.md
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/pkgs/by-name/README.md
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/with-config.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/with-config.nix
new file mode 100644
index 000000000000..65bcbf9928a2
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/with-config.nix
@@ -0,0 +1 @@
+{ someDrv, enableNew }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/without-config.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/without-config.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/new-package-non-by-name/without-config.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-by-name/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-by-name/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-by-name/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-by-name/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-by-name/expected
new file mode 100644
index 000000000000..ddcb2df46e5f
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-by-name/expected
@@ -0,0 +1 @@
+Given Nixpkgs path does not contain a pkgs/by-name subdirectory, no check necessary.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/all-packages.nix
new file mode 100644
index 000000000000..38762c6de1cc
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/all-packages.nix
@@ -0,0 +1,5 @@
+self: super: {
+  iDontEval = throw "I don't eval";
+
+  futureEval = self.callPackage ({ someDrv }: someDrv) { };
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/base/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/base/all-packages.nix
new file mode 100644
index 000000000000..ac763b454eb0
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/base/all-packages.nix
@@ -0,0 +1,3 @@
+self: super: {
+  futureEval = throw "foo";
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/base/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/base/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/base/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/base/pkgs/by-name/README.md b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/base/pkgs/by-name/README.md
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/base/pkgs/by-name/README.md
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/pkgs/by-name/README.md b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/pkgs/by-name/README.md
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/no-eval/pkgs/by-name/README.md
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-attrs/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-attrs/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-attrs/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-attrs/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-attrs/expected
new file mode 100644
index 000000000000..e6c923790102
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-attrs/expected
@@ -0,0 +1 @@
+pkgs.nonDerivation: This attribute defined by pkgs/by-name/no/nonDerivation/package.nix is not a derivation
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-attrs/pkgs/by-name/no/nonDerivation/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-attrs/pkgs/by-name/no/nonDerivation/package.nix
new file mode 100644
index 000000000000..bd68dba1ded5
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-attrs/pkgs/by-name/no/nonDerivation/package.nix
@@ -0,0 +1 @@
+{ }: null
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-derivation/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-derivation/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-derivation/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-derivation/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-derivation/expected
new file mode 100644
index 000000000000..e6c923790102
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-derivation/expected
@@ -0,0 +1 @@
+pkgs.nonDerivation: This attribute defined by pkgs/by-name/no/nonDerivation/package.nix is not a derivation
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-derivation/pkgs/by-name/no/nonDerivation/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-derivation/pkgs/by-name/no/nonDerivation/package.nix
new file mode 100644
index 000000000000..b021e28c2145
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-derivation/pkgs/by-name/no/nonDerivation/package.nix
@@ -0,0 +1 @@
+{ }: { }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/all-packages.nix
new file mode 100644
index 000000000000..3e0ea20c2281
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/all-packages.nix
@@ -0,0 +1,6 @@
+self: super: {
+
+  bar = (x: x) self.callPackage ./pkgs/by-name/fo/foo/package.nix { someFlag = true; };
+  foo = self.bar;
+
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/expected
new file mode 100644
index 000000000000..9df788191ece
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/expected
@@ -0,0 +1 @@
+pkgs.foo: This attribute is manually defined (most likely in pkgs/top-level/all-packages.nix), which is only allowed if the definition is of the form `pkgs.callPackage pkgs/by-name/fo/foo/package.nix { ... }` with a non-empty second argument.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/pkgs/by-name/fo/foo/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/pkgs/by-name/fo/foo/package.nix
new file mode 100644
index 000000000000..5ad6ea5e24d6
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/non-syntactical-callPackage-by-name/pkgs/by-name/fo/foo/package.nix
@@ -0,0 +1 @@
+{ someDrv, someFlag }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/one-letter/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/one-letter/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/one-letter/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/one-letter/pkgs/by-name/a/a/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/one-letter/pkgs/by-name/a/a/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/one-letter/pkgs/by-name/a/a/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/only-callPackage-derivations/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/only-callPackage-derivations/all-packages.nix
new file mode 100644
index 000000000000..5b1ed9d2ccda
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/only-callPackage-derivations/all-packages.nix
@@ -0,0 +1,16 @@
+self: super: {
+  alternateCallPackage = self.myScope.callPackage ({ myScopeValue, someDrv }:
+    assert myScopeValue;
+    someDrv
+  ) { };
+
+  myScope = self.lib.makeScope self.newScope (self: {
+    myScopeValue = true;
+  });
+
+  myPackages = self.callPackages ({ someDrv }: {
+    a = someDrv;
+    b = someDrv;
+  }) { };
+  inherit (self.myPackages) a b;
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/only-callPackage-derivations/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/only-callPackage-derivations/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/only-callPackage-derivations/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/only-callPackage-derivations/pkgs/by-name/README.md b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/only-callPackage-derivations/pkgs/by-name/README.md
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/only-callPackage-derivations/pkgs/by-name/README.md
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/all-packages.nix
new file mode 100644
index 000000000000..8bedb90d89a7
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/all-packages.nix
@@ -0,0 +1,3 @@
+self: super: {
+  nonDerivation = self.callPackage ./someDrv.nix { };
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/expected
new file mode 100644
index 000000000000..51479e22d26f
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/expected
@@ -0,0 +1 @@
+pkgs.nonDerivation: This attribute is manually defined (most likely in pkgs/top-level/all-packages.nix), which is only allowed if the definition is of the form `pkgs.callPackage pkgs/by-name/no/nonDerivation/package.nix { ... }` with a non-empty second argument.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/pkgs/by-name/no/nonDerivation/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/pkgs/by-name/no/nonDerivation/package.nix
new file mode 100644
index 000000000000..bd68dba1ded5
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/pkgs/by-name/no/nonDerivation/package.nix
@@ -0,0 +1 @@
+{ }: null
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/someDrv.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/someDrv.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-different-file/someDrv.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/all-packages.nix
new file mode 100644
index 000000000000..d369dd7228dc
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/all-packages.nix
@@ -0,0 +1,3 @@
+self: super: {
+  nonDerivation = self.callPackage ./pkgs/by-name/no/nonDerivation/package.nix { };
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/base/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/base/all-packages.nix
new file mode 100644
index 000000000000..d369dd7228dc
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/base/all-packages.nix
@@ -0,0 +1,3 @@
+self: super: {
+  nonDerivation = self.callPackage ./pkgs/by-name/no/nonDerivation/package.nix { };
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/base/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/base/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/base/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/base/pkgs/by-name/no/nonDerivation/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/base/pkgs/by-name/no/nonDerivation/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/base/pkgs/by-name/no/nonDerivation/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/expected
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/expected
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/pkgs/by-name/no/nonDerivation/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/pkgs/by-name/no/nonDerivation/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg-gradual/pkgs/by-name/no/nonDerivation/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/all-packages.nix
new file mode 100644
index 000000000000..d369dd7228dc
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/all-packages.nix
@@ -0,0 +1,3 @@
+self: super: {
+  nonDerivation = self.callPackage ./pkgs/by-name/no/nonDerivation/package.nix { };
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/base/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/base/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/base/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/base/pkgs/by-name/README.md b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/base/pkgs/by-name/README.md
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/base/pkgs/by-name/README.md
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/expected
new file mode 100644
index 000000000000..51479e22d26f
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/expected
@@ -0,0 +1 @@
+pkgs.nonDerivation: This attribute is manually defined (most likely in pkgs/top-level/all-packages.nix), which is only allowed if the definition is of the form `pkgs.callPackage pkgs/by-name/no/nonDerivation/package.nix { ... }` with a non-empty second argument.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/pkgs/by-name/no/nonDerivation/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/pkgs/by-name/no/nonDerivation/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-empty-arg/pkgs/by-name/no/nonDerivation/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/all-packages.nix
new file mode 100644
index 000000000000..853c3a87db56
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/all-packages.nix
@@ -0,0 +1,3 @@
+self: super: {
+  nonDerivation = self.someDrv;
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/expected
new file mode 100644
index 000000000000..51479e22d26f
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/expected
@@ -0,0 +1 @@
+pkgs.nonDerivation: This attribute is manually defined (most likely in pkgs/top-level/all-packages.nix), which is only allowed if the definition is of the form `pkgs.callPackage pkgs/by-name/no/nonDerivation/package.nix { ... }` with a non-empty second argument.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/pkgs/by-name/no/nonDerivation/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/pkgs/by-name/no/nonDerivation/package.nix
new file mode 100644
index 000000000000..bd68dba1ded5
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-call-package/pkgs/by-name/no/nonDerivation/package.nix
@@ -0,0 +1 @@
+{ }: null
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/all-packages.nix
new file mode 100644
index 000000000000..dc07f69b40ee
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/all-packages.nix
@@ -0,0 +1,3 @@
+self: super: {
+  nonDerivation = self.callPackage ({ someDrv }: someDrv) { };
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/expected
new file mode 100644
index 000000000000..51479e22d26f
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/expected
@@ -0,0 +1 @@
+pkgs.nonDerivation: This attribute is manually defined (most likely in pkgs/top-level/all-packages.nix), which is only allowed if the definition is of the form `pkgs.callPackage pkgs/by-name/no/nonDerivation/package.nix { ... }` with a non-empty second argument.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/pkgs/by-name/no/nonDerivation/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/pkgs/by-name/no/nonDerivation/package.nix
new file mode 100644
index 000000000000..bd68dba1ded5
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-no-file/pkgs/by-name/no/nonDerivation/package.nix
@@ -0,0 +1 @@
+{ }: null
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-success/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-success/all-packages.nix
new file mode 100644
index 000000000000..6b323756ae41
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-success/all-packages.nix
@@ -0,0 +1,5 @@
+self: super: {
+  foo = self.callPackage ./pkgs/by-name/fo/foo/package.nix {
+    enableBar = true;
+  };
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-success/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-success/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-success/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-success/pkgs/by-name/fo/foo/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-success/pkgs/by-name/fo/foo/package.nix
new file mode 100644
index 000000000000..c811a7215a28
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/override-success/pkgs/by-name/fo/foo/package.nix
@@ -0,0 +1,8 @@
+{
+  someDrv,
+  enableBar ? false,
+}:
+if enableBar then
+  someDrv
+else
+  {}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-dir-is-file/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-dir-is-file/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-dir-is-file/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-dir-is-file/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-dir-is-file/expected
new file mode 100644
index 000000000000..3ad4b8f820f5
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-dir-is-file/expected
@@ -0,0 +1 @@
+pkgs/by-name/fo/foo: This path is a file, but it should be a directory.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-dir-is-file/pkgs/by-name/fo/foo b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-dir-is-file/pkgs/by-name/fo/foo
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-dir-is-file/pkgs/by-name/fo/foo
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-nix-dir/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-nix-dir/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-nix-dir/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-nix-dir/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-nix-dir/expected
new file mode 100644
index 000000000000..67a0c69fe29e
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-nix-dir/expected
@@ -0,0 +1 @@
+pkgs/by-name/fo/foo: "package.nix" must be a file.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-nix-dir/pkgs/by-name/fo/foo/package.nix/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-nix-dir/pkgs/by-name/fo/foo/package.nix/default.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-nix-dir/pkgs/by-name/fo/foo/package.nix/default.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/all-packages.nix
new file mode 100644
index 000000000000..85f8c6138c5c
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/all-packages.nix
@@ -0,0 +1,5 @@
+self: super: {
+  foo-variant-unvarianted = self.callPackage ./package.nix { };
+
+  foo-variant-new = self.callPackage ./pkgs/by-name/fo/foo/package.nix { };
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/base/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/base/all-packages.nix
new file mode 100644
index 000000000000..734604360073
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/base/all-packages.nix
@@ -0,0 +1,3 @@
+self: super: {
+  foo-variant-unvarianted = self.callPackage ./pkgs/by-name/fo/foo/package.nix { };
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/base/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/base/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/base/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/base/pkgs/by-name/fo/foo/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/base/pkgs/by-name/fo/foo/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/base/pkgs/by-name/fo/foo/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/pkgs/by-name/fo/foo/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/pkgs/by-name/fo/foo/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/package-variants/pkgs/by-name/fo/foo/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-absolute/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-absolute/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-absolute/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-absolute/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-absolute/expected
new file mode 100644
index 000000000000..7d20c32aad68
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-absolute/expected
@@ -0,0 +1 @@
+pkgs/by-name/aa/aa: File package.nix at line 2 contains the path expression "/foo" which cannot be resolved: No such file or directory (os error 2).
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-absolute/pkgs/by-name/aa/aa/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-absolute/pkgs/by-name/aa/aa/package.nix
new file mode 100644
index 000000000000..7a51ba1ec719
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-absolute/pkgs/by-name/aa/aa/package.nix
@@ -0,0 +1,3 @@
+{ someDrv }: someDrv // {
+  escape = /foo;
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-escape/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-escape/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-escape/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-escape/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-escape/expected
new file mode 100644
index 000000000000..3d7fb64e80a3
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-escape/expected
@@ -0,0 +1 @@
+pkgs/by-name/aa/aa: File package.nix at line 2 contains the path expression "../." which may point outside the directory of that package.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-escape/pkgs/by-name/aa/aa/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-escape/pkgs/by-name/aa/aa/package.nix
new file mode 100644
index 000000000000..5989f52eb899
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-escape/pkgs/by-name/aa/aa/package.nix
@@ -0,0 +1,3 @@
+{ someDrv }: someDrv // {
+  escape = ../.;
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-nix-path/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-nix-path/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-nix-path/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-nix-path/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-nix-path/expected
new file mode 100644
index 000000000000..b0cdff4a4778
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-nix-path/expected
@@ -0,0 +1 @@
+pkgs/by-name/aa/aa: File package.nix at line 2 contains the nix search path expression "<nixpkgs>" which may point outside the directory of that package.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-nix-path/pkgs/by-name/aa/aa/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-nix-path/pkgs/by-name/aa/aa/package.nix
new file mode 100644
index 000000000000..864fdce13319
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-nix-path/pkgs/by-name/aa/aa/package.nix
@@ -0,0 +1,3 @@
+{ someDrv }: someDrv // {
+  nixPath = <nixpkgs>;
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-path-subexpr/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-path-subexpr/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-path-subexpr/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-path-subexpr/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-path-subexpr/expected
new file mode 100644
index 000000000000..ad662af27a86
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-path-subexpr/expected
@@ -0,0 +1 @@
+pkgs/by-name/aa/aa: File package.nix at line 2 contains the path expression "./${"test"}", which is not yet supported and may point outside the directory of that package.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-path-subexpr/pkgs/by-name/aa/aa/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-path-subexpr/pkgs/by-name/aa/aa/package.nix
new file mode 100644
index 000000000000..a94ba7541263
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-path-subexpr/pkgs/by-name/aa/aa/package.nix
@@ -0,0 +1,3 @@
+{ someDrv }: someDrv // {
+  pathWithSubexpr = ./${"test"};
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/dir/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/dir/default.nix
new file mode 100644
index 000000000000..7e4a7548fec7
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/dir/default.nix
@@ -0,0 +1,2 @@
+# Recursive
+../package.nix
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/file b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/file
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/file
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/file.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/file.nix
new file mode 100644
index 000000000000..bd55e601bf64
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/file.nix
@@ -0,0 +1,2 @@
+# Recursive test
+import ./file.nix
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/package.nix
new file mode 100644
index 000000000000..474db5b0ebfc
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/ref-success/pkgs/by-name/aa/aa/package.nix
@@ -0,0 +1,5 @@
+{ someDrv }: someDrv // {
+  nixFile = ./file.nix;
+  nonNixFile = ./file;
+  directory = ./dir;
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/shard-file/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/shard-file/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/shard-file/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/shard-file/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/shard-file/expected
new file mode 100644
index 000000000000..447b38e6b6c1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/shard-file/expected
@@ -0,0 +1 @@
+pkgs/by-name/fo: This is a file, but it should be a directory.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/shard-file/pkgs/by-name/fo b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/shard-file/pkgs/by-name/fo
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/shard-file/pkgs/by-name/fo
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/all-packages.nix
new file mode 100644
index 000000000000..688f52b9358f
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/all-packages.nix
@@ -0,0 +1,6 @@
+self: super: {
+  a = self.callPackage ./pkgs/by-name/a/a/package.nix { };
+  b = self.callPackage ({ someDrv }: someDrv) { };
+  c = self.callPackage ./pkgs/by-name/c/c/package.nix { };
+  d = self.callPackage ({ someDrv }: someDrv) { };
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/expected
new file mode 100644
index 000000000000..349e9ad47c41
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/expected
@@ -0,0 +1,4 @@
+pkgs.a: This attribute is manually defined (most likely in pkgs/top-level/all-packages.nix), which is only allowed if the definition is of the form `pkgs.callPackage pkgs/by-name/a/a/package.nix { ... }` with a non-empty second argument.
+pkgs.b: This is a new top-level package of the form `callPackage ... { }`. Please define it in pkgs/by-name/b/b/package.nix instead. See `pkgs/by-name/README.md` for more details. Since the second `callPackage` argument is `{ }`, no manual `callPackage` (e.g. in `pkgs/top-level/all-packages.nix`) is needed anymore.
+pkgs.c: This attribute is manually defined (most likely in pkgs/top-level/all-packages.nix), which is only allowed if the definition is of the form `pkgs.callPackage pkgs/by-name/c/c/package.nix { ... }` with a non-empty second argument.
+pkgs.d: This is a new top-level package of the form `callPackage ... { }`. Please define it in pkgs/by-name/d/d/package.nix instead. See `pkgs/by-name/README.md` for more details. Since the second `callPackage` argument is `{ }`, no manual `callPackage` (e.g. in `pkgs/top-level/all-packages.nix`) is needed anymore.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/pkgs/by-name/a/a/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/pkgs/by-name/a/a/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/pkgs/by-name/a/a/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/pkgs/by-name/c/c/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/pkgs/by-name/c/c/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/sorted-order/pkgs/by-name/c/c/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/success/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/success/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/success/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/success/pkgs/by-name/fo/foo/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/success/pkgs/by-name/fo/foo/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/success/pkgs/by-name/fo/foo/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/expected
new file mode 100644
index 000000000000..335c5d6b6e5d
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/expected
@@ -0,0 +1 @@
+pkgs/by-name/fo/foo: Path package.nix is a symlink pointing to a path outside the directory of that package.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/pkgs/by-name/fo/foo/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/pkgs/by-name/fo/foo/package.nix
new file mode 120000
index 000000000000..f079163d158a
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/pkgs/by-name/fo/foo/package.nix
@@ -0,0 +1 @@
+../../../../someDrv.nix
\ No newline at end of file
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/someDrv.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/someDrv.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-escape/someDrv.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/expected
new file mode 100644
index 000000000000..c1e7a28205a7
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/expected
@@ -0,0 +1 @@
+pkgs/by-name/fo/foo: Path foo is a symlink which cannot be resolved: No such file or directory (os error 2).
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/pkgs/by-name/fo/foo/foo b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/pkgs/by-name/fo/foo/foo
new file mode 120000
index 000000000000..c86c3f3551ec
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/pkgs/by-name/fo/foo/foo
@@ -0,0 +1 @@
+none
\ No newline at end of file
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/pkgs/by-name/fo/foo/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/pkgs/by-name/fo/foo/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/pkgs/by-name/fo/foo/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/someDrv.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/someDrv.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/someDrv.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/all-packages.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/all-packages.nix
new file mode 100644
index 000000000000..3398e974cb6b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/all-packages.nix
@@ -0,0 +1,3 @@
+self: super: builtins.mapAttrs (name: value: value) {
+  foo = self.someDrv;
+}
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/expected b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/expected
new file mode 100644
index 000000000000..2a248c23ab50
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/expected
@@ -0,0 +1 @@
+pkgs.foo: Cannot determine the location of this attribute using `builtins.unsafeGetAttrPos`.
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/pkgs/by-name/fo/foo/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/pkgs/by-name/fo/foo/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/unknown-location/pkgs/by-name/fo/foo/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/uppercase/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/uppercase/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/uppercase/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/uppercase/pkgs/by-name/fo/FOO/package.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/uppercase/pkgs/by-name/fo/FOO/package.nix
new file mode 100644
index 000000000000..a1b92efbbadb
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/uppercase/pkgs/by-name/fo/FOO/package.nix
@@ -0,0 +1 @@
+{ someDrv }: someDrv
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/with-readme/default.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/with-readme/default.nix
new file mode 100644
index 000000000000..861260cdca4b
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/with-readme/default.nix
@@ -0,0 +1 @@
+import <test-nixpkgs> { root = ./.; }
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/with-readme/pkgs/by-name/README.md b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/with-readme/pkgs/by-name/README.md
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/tests/with-readme/pkgs/by-name/README.md