├── .envrc ├── tests ├── fixtures │ ├── patch.patch │ ├── python │ │ ├── foo.py │ │ ├── tests │ │ │ └── test_1.py │ │ └── setup.py │ ├── qmake │ │ ├── foo │ │ └── qmake.pro │ ├── make │ │ ├── foo.in │ │ └── Makefile │ ├── meson │ │ └── meson.build │ └── cmake │ │ └── CMakeLists.txt ├── license-missing │ ├── no-meta.nix │ ├── no-license.nix │ ├── have-license.nix │ ├── empty-license.nix │ └── default.nix ├── maintainers-missing │ ├── no-meta.nix │ ├── no-maintainers.nix │ ├── empty-maintainers.nix │ ├── have-maintainers.nix │ └── default.nix ├── name-and-version │ ├── negative.nix │ ├── positive.nix │ ├── default.nix │ └── everything.nix ├── unused-argument │ ├── unused-pattern.nix │ ├── used-in-string1.nix │ ├── used-in-string2.nix │ ├── unused-single.nix │ ├── used-single.nix │ ├── used-in-defaults.nix │ ├── used-pattern.nix │ ├── unused-pattern-var-in-let-binding.nix │ ├── unused-pattern-var-as-key.nix │ └── default.nix ├── no-flags-array │ ├── default.nix │ ├── make.nix │ └── make-finalAttrs.nix ├── attribute-typo │ ├── deletion.nix │ ├── insertion.nix │ ├── unknown-short.nix │ ├── casing.nix │ ├── transposition.nix │ └── default.nix ├── no-flags-spaces │ ├── nonstring.nix │ ├── default.nix │ ├── okay.nix │ └── bad.nix ├── python-include-tests │ ├── explicit-check-phase.nix │ ├── no-tests-no-import-checks.nix │ ├── has-imports-check.nix │ ├── pytest-check-hook.nix │ ├── tests-disabled-no-import-checks.nix │ └── default.nix ├── stale-substitute │ ├── default.nix │ ├── live.nix │ └── stale.nix ├── unclear-gpl │ ├── gpl2.nix │ ├── gpl3.nix │ ├── lgpl2.nix │ ├── lgpl21.nix │ ├── lgpl3.nix │ ├── lgpl3-python.nix │ ├── single-nonmatching-license.nix │ └── default.nix ├── no-uri-literals │ ├── string.nix │ ├── default.nix │ └── uri-literal.nix ├── EvalError │ ├── no-exception.nix │ ├── default.nix │ └── exception.nix ├── fixup-phase │ └── default.nix ├── patch-phase │ └── default.nix ├── missing-phase-hooks │ ├── build-both.nix │ ├── check-both.nix │ ├── install-both.nix │ ├── build-post.nix │ ├── build-pre.nix │ ├── check-post.nix │ ├── check-pre.nix │ ├── configure-both.nix │ ├── install-post.nix │ ├── install-pre.nix │ ├── configure-post.nix │ ├── configure-pre.nix │ └── default.nix ├── python-explicit-check-phase │ ├── redundant-pytest.nix │ ├── nonredundant-pytest.nix │ └── default.nix ├── python-imports-check-typo │ ├── pythonImportCheck.nix │ ├── pythonImportTests.nix │ ├── pythonImportsTest.nix │ ├── pythonCheckImports.nix │ ├── pythonImportsCheck.nix │ └── default.nix ├── explicit-phases │ ├── build.nix │ ├── check.nix │ ├── default.nix │ ├── configure.nix │ └── install.nix ├── missing-patch-comment │ ├── missing-comment.nix │ ├── general-comment.nix │ ├── comment-after-newline.nix │ ├── comment-inline.nix │ ├── comment-above.nix │ ├── comment-within.nix │ ├── complex-structure1.nix │ ├── ignore-nested-lists1.nix │ ├── ignore-nested-lists2.nix │ └── default.nix ├── attribute-ordering │ ├── inherited.nix │ ├── default.nix │ ├── out-of-order.nix │ └── properly-ordered.nix ├── build-tools-in-build-inputs │ ├── cmake.nix │ ├── pkg-config.nix │ ├── sphinx.nix │ ├── default.nix │ ├── meson.nix │ └── ninja.nix ├── environment-variables-go-to-env │ ├── env-var.nix │ ├── no-env-vars.nix │ ├── env-vars-just-in-env.nix │ ├── pkg-config-var.nix │ └── default.nix ├── duplicate-check-inputs │ └── default.nix ├── python-inconsistent-interpreters │ ├── normal.nix │ ├── mixed-checkInputs.nix │ ├── default.nix │ └── mixed-propagatedBuildInputs.nix ├── unnecessary-parallel-building │ ├── default.nix │ ├── cmake.nix │ ├── qmake.nix │ ├── qt-derivation.nix │ └── meson.nix ├── meson-cmake │ └── default.nix ├── default.nix └── hammering.rs ├── .gitignore ├── rust-checks ├── src │ ├── lib.rs │ ├── checks │ │ ├── mod.rs │ │ ├── no_uri_literals.rs │ │ ├── stale_substitute.rs │ │ ├── missing_patch_comment.rs │ │ └── unused_argument.rs │ ├── common_structs.rs │ ├── tree_utils.rs │ ├── comment_finders.rs │ └── analysis.rs ├── Cargo.toml └── Cargo.lock ├── lib ├── environment-variables.nix ├── derivation-attributes-unordered.nix ├── derivation-attributes.nix ├── default.nix └── levenshtein.nix ├── .github ├── dependabot.yml └── workflows │ └── main.yaml ├── explanations ├── attribute-typo.md ├── maintainers-missing.md ├── duplicate-check-inputs.md ├── stale-substitute.md ├── license-missing.md ├── no-uri-literals.md ├── no-python-tests.md ├── unnecessary-parallel-building.md ├── python-imports-check-typo.md ├── name-and-version.md ├── fixup-phase.md ├── python-inconsistent-interpreters.md ├── python-include-tests.md ├── patch-phase.md ├── meson-cmake.md ├── python-explicit-check-phase.md ├── build-tools-in-build-inputs.md ├── no-flags-spaces.md ├── missing-patch-comment.md ├── environment-variables-go-to-env.md ├── no-flags-array.md ├── unclear-gpl.md ├── missing-phase-hooks.md ├── explicit-phases.md └── attribute-ordering.md ├── default.nix ├── overlays ├── fixup-phase.nix ├── patch-phase.nix ├── name-and-version.nix ├── python-explicit-check-phase.nix ├── explicit-phases.nix ├── meson-cmake.nix ├── python-imports-check-typo.nix ├── unclear-gpl.nix ├── duplicate-check-inputs.nix ├── environment-variables-go-to-env.nix ├── python-include-tests.nix ├── license-missing.nix ├── no-flags-spaces.nix ├── no-flags-array.nix ├── maintainers-missing.nix ├── unnecessary-parallel-building.nix ├── missing-phase-hooks.nix ├── python-inconsistent-interpreters.nix ├── attribute-ordering.nix ├── attribute-typo.nix └── build-tools-in-build-inputs.nix ├── shell.nix ├── Cargo.toml ├── LICENSE.md ├── src ├── utils.rs ├── main.rs ├── model.rs └── lib.rs ├── flake.lock ├── README.md ├── flake.nix └── Cargo.lock /.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /tests/fixtures/patch.patch: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/python/foo.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/qmake/foo: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /tests/fixtures/make/foo.in: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.direnv 2 | /result* 3 | /target 4 | -------------------------------------------------------------------------------- /tests/fixtures/python/tests/test_1.py: -------------------------------------------------------------------------------- 1 | def test_1(): 2 | assert True 3 | -------------------------------------------------------------------------------- /tests/fixtures/python/setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup(name="foo", py_modules="foo") 4 | -------------------------------------------------------------------------------- /tests/fixtures/qmake/qmake.pro: -------------------------------------------------------------------------------- 1 | TARGET = foo 2 | INSTALLS += foo 3 | foo.path = $$(out) 4 | foo.files = foo 5 | -------------------------------------------------------------------------------- /tests/fixtures/meson/meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'meson-project', 3 | [], 4 | ) 5 | 6 | install_data('meson.build') 7 | -------------------------------------------------------------------------------- /rust-checks/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod analysis; 2 | pub mod checks; 3 | pub mod comment_finders; 4 | pub mod common_structs; 5 | pub mod tree_utils; 6 | -------------------------------------------------------------------------------- /tests/fixtures/cmake/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.1) 2 | project(cmake-project LANGUAGES NONE) 3 | 4 | install(FILES "CMakeLists.txt" TYPE DATA) 5 | -------------------------------------------------------------------------------- /tests/license-missing/no-meta.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "no-meta"; 7 | 8 | src = ../fixtures/make; 9 | } 10 | -------------------------------------------------------------------------------- /tests/maintainers-missing/no-meta.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "no-meta"; 7 | 8 | src = ../fixtures/make; 9 | } 10 | -------------------------------------------------------------------------------- /tests/name-and-version/negative.nix: -------------------------------------------------------------------------------- 1 | { stdenv, lib }: 2 | 3 | stdenv.mkDerivation { 4 | pname = "name-and-version"; 5 | version = "1.0"; 6 | 7 | src = ../fixtures/make; 8 | } 9 | -------------------------------------------------------------------------------- /tests/name-and-version/positive.nix: -------------------------------------------------------------------------------- 1 | { stdenv, lib }: 2 | 3 | stdenv.mkDerivation { 4 | name = "name-and-version"; 5 | version = "1.0"; 6 | 7 | src = ../fixtures/make; 8 | } 9 | -------------------------------------------------------------------------------- /tests/unused-argument/unused-pattern.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , unused 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "unused-pattern"; 7 | 8 | src = ../fixtures/make; 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/make/Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= $(out) 2 | 3 | INSTALL ?= install 4 | 5 | build: foo 6 | 7 | foo: foo.in 8 | cp $< $@ 9 | 10 | install: build 11 | $(INSTALL) -Dt $(PREFIX) foo 12 | -------------------------------------------------------------------------------- /tests/no-flags-array/default.nix: -------------------------------------------------------------------------------- 1 | { callPackage 2 | }: 3 | 4 | { 5 | # positive cases 6 | make = callPackage ./make.nix { }; 7 | make-finalAttrs = callPackage ./make-finalAttrs.nix { }; 8 | } 9 | -------------------------------------------------------------------------------- /tests/unused-argument/used-in-string1.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , foo 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "used-in-string"; 7 | 8 | src = ../fixtures/make; 9 | key = "${foo}"; 10 | } 11 | -------------------------------------------------------------------------------- /tests/attribute-typo/deletion.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation rec { 5 | name = "attribute-typo-deletion"; 6 | 7 | src = ../fixtures/make; 8 | 9 | configurFlags = []; 10 | } 11 | -------------------------------------------------------------------------------- /tests/attribute-typo/insertion.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation rec { 5 | name = "attribute-typo-insertion"; 6 | 7 | src = ../fixtures/make; 8 | 9 | passthrough = []; 10 | } 11 | -------------------------------------------------------------------------------- /tests/attribute-typo/unknown-short.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation rec { 5 | name = "attribute-typo-unknown-short"; 6 | 7 | src = ../fixtures/make; 8 | 9 | wrc = true; 10 | } 11 | -------------------------------------------------------------------------------- /tests/license-missing/no-license.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "no-license"; 7 | 8 | src = ../fixtures/make; 9 | 10 | meta = with lib; { 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /tests/no-flags-spaces/nonstring.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "nonstring"; 6 | 7 | src = ../fixtures/make; 8 | 9 | configureFlags = [ "--foo" [ "--bar" ] ]; 10 | } 11 | -------------------------------------------------------------------------------- /tests/python-include-tests/explicit-check-phase.nix: -------------------------------------------------------------------------------- 1 | { buildPythonPackage 2 | }: 3 | 4 | buildPythonPackage { 5 | name = "package"; 6 | 7 | src = ../fixtures/make; 8 | 9 | checkPhase = ""; 10 | } 11 | -------------------------------------------------------------------------------- /tests/unused-argument/used-in-string2.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , foo 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "used-in-string2"; 7 | 8 | src = ../fixtures/make; 9 | key = '' '${foo}' ''; 10 | } 11 | -------------------------------------------------------------------------------- /tests/stale-substitute/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | 3 | { 4 | # positive cases 5 | stale = pkgs.callPackage ./stale.nix { }; 6 | 7 | # negative cases 8 | live = pkgs.callPackage ./live.nix { }; 9 | } 10 | -------------------------------------------------------------------------------- /tests/unclear-gpl/gpl2.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "unclear-gpl-gpl2"; 7 | 8 | src = ../fixtures/make; 9 | 10 | meta.license = [ lib.licenses.gpl2 ]; 11 | } 12 | -------------------------------------------------------------------------------- /tests/unclear-gpl/gpl3.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "unclear-gpl-gpl3"; 7 | 8 | src = ../fixtures/make; 9 | 10 | meta.license = [ lib.licenses.gpl3 ]; 11 | } 12 | -------------------------------------------------------------------------------- /tests/attribute-typo/casing.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation rec { 5 | name = "attribute-typo-casing"; 6 | 7 | src = ../fixtures/make; 8 | 9 | postfixup = '' 10 | blah 11 | ''; 12 | } 13 | -------------------------------------------------------------------------------- /tests/name-and-version/default.nix: -------------------------------------------------------------------------------- 1 | { callPackage }: 2 | 3 | { 4 | positive = callPackage ./positive.nix { }; 5 | 6 | negative = callPackage ./negative.nix { }; 7 | everything = callPackage ./everything.nix { }; 8 | } 9 | -------------------------------------------------------------------------------- /tests/no-uri-literals/string.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "no-uri-literals-string"; 6 | 7 | src = ../fixtures/make; 8 | 9 | meta.homepage = "https://www.example.com"; 10 | } 11 | -------------------------------------------------------------------------------- /tests/python-include-tests/no-tests-no-import-checks.nix: -------------------------------------------------------------------------------- 1 | { buildPythonPackage 2 | }: 3 | 4 | buildPythonPackage { 5 | name = "package"; 6 | 7 | format = "pyproject"; 8 | 9 | src = ../fixtures/make; 10 | } 11 | -------------------------------------------------------------------------------- /tests/unclear-gpl/lgpl2.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "unclear-gpl-lgpl2"; 7 | 8 | src = ../fixtures/make; 9 | 10 | meta.license = [ lib.licenses.lgpl2 ]; 11 | } 12 | -------------------------------------------------------------------------------- /tests/unclear-gpl/lgpl21.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "unclear-gpl-lgpl21"; 7 | 8 | src = ../fixtures/make; 9 | 10 | meta.license = [ lib.licenses.lgpl21 ]; 11 | } 12 | -------------------------------------------------------------------------------- /tests/unclear-gpl/lgpl3.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "unclear-gpl-lgpl3"; 7 | 8 | src = ../fixtures/make; 9 | 10 | meta.license = [ lib.licenses.lgpl3 ]; 11 | } 12 | -------------------------------------------------------------------------------- /tests/unused-argument/unused-single.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | let 5 | function = unused: 1; 6 | in 7 | stdenv.mkDerivation { 8 | name = "unused-single"; 9 | 10 | src = ../fixtures/make; 11 | } 12 | -------------------------------------------------------------------------------- /tests/EvalError/no-exception.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation rec { 5 | pname = "no-exception"; 6 | version = "0"; 7 | 8 | src = ../fixtures/make; 9 | 10 | propagatedBuildInputs = [ ]; 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixup-phase/default.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "fixup-phase"; 6 | 7 | src = ../fixtures/make; 8 | 9 | fixupPhase = '' 10 | sed -i 's/o/e/g' $out/foo 11 | ''; 12 | } 13 | -------------------------------------------------------------------------------- /tests/license-missing/have-license.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "have-license"; 7 | 8 | src = ../fixtures/make; 9 | 10 | meta.license = [ lib.licenses.mit ]; 11 | } 12 | -------------------------------------------------------------------------------- /tests/maintainers-missing/no-maintainers.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "no-maintainers"; 7 | 8 | src = ../fixtures/make; 9 | 10 | meta = with lib; { 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /tests/patch-phase/default.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "patch-phase"; 6 | 7 | src = ../fixtures/make; 8 | 9 | patchPhase = '' 10 | sed -i 's/o/e/g' foo.in 11 | ''; 12 | } 13 | -------------------------------------------------------------------------------- /tests/python-include-tests/has-imports-check.nix: -------------------------------------------------------------------------------- 1 | { buildPythonPackage 2 | }: 3 | 4 | buildPythonPackage { 5 | name = "package"; 6 | 7 | src = ../fixtures/make; 8 | 9 | pythonImportsCheck = [ 10 | ]; 11 | } 12 | -------------------------------------------------------------------------------- /tests/attribute-typo/transposition.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation rec { 5 | name = "attribute-typo-transposition"; 6 | 7 | src = ../fixtures/make; 8 | 9 | propgaatedNativeBuildInptus = []; 10 | } 11 | -------------------------------------------------------------------------------- /tests/missing-phase-hooks/build-both.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "phase-hooks-build-both"; 6 | 7 | src = ../fixtures/make; 8 | 9 | buildPhase = '' 10 | make 11 | ''; 12 | } 13 | -------------------------------------------------------------------------------- /tests/no-uri-literals/default.nix: -------------------------------------------------------------------------------- 1 | { callPackage 2 | }: 3 | 4 | { 5 | # positive cases 6 | uri-literal = callPackage ./uri-literal.nix { }; 7 | 8 | # negative cases 9 | string = callPackage ./string.nix { }; 10 | } 11 | -------------------------------------------------------------------------------- /tests/no-uri-literals/uri-literal.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "no-uri-literals-uri-literal"; 6 | 7 | src = ../fixtures/make; 8 | 9 | meta.homepage = https://www.example.com; 10 | } 11 | -------------------------------------------------------------------------------- /tests/python-explicit-check-phase/redundant-pytest.nix: -------------------------------------------------------------------------------- 1 | { buildPythonPackage 2 | }: 3 | 4 | buildPythonPackage { 5 | name = "redundant-pytest"; 6 | 7 | src = ../fixtures/make; 8 | 9 | checkPhase = " pytest "; 10 | } 11 | -------------------------------------------------------------------------------- /tests/python-imports-check-typo/pythonImportCheck.nix: -------------------------------------------------------------------------------- 1 | { buildPythonPackage 2 | }: 3 | 4 | buildPythonPackage { 5 | name = "pythonImportCheck"; 6 | 7 | src = ../fixtures/make; 8 | 9 | pythonImportCheck = []; 10 | } 11 | -------------------------------------------------------------------------------- /tests/python-imports-check-typo/pythonImportTests.nix: -------------------------------------------------------------------------------- 1 | { buildPythonPackage 2 | }: 3 | 4 | buildPythonPackage { 5 | name = "pythonImportTests"; 6 | 7 | src = ../fixtures/make; 8 | 9 | pythonImportTests = []; 10 | } 11 | -------------------------------------------------------------------------------- /tests/python-imports-check-typo/pythonImportsTest.nix: -------------------------------------------------------------------------------- 1 | { buildPythonPackage 2 | }: 3 | 4 | buildPythonPackage { 5 | name = "pythonImportsTest"; 6 | 7 | src = ../fixtures/make; 8 | 9 | pythonImportsTest = []; 10 | } 11 | -------------------------------------------------------------------------------- /lib/environment-variables.nix: -------------------------------------------------------------------------------- 1 | [ 2 | "CFLAGS" 3 | "CPPFLAGS" 4 | "CXXFLAGS" 5 | "FCFLAGS" 6 | "FONTCONFIG_FILE" 7 | "NIX_CFLAGS_COMPILE" 8 | "NIX_CFLAGS_LINK" 9 | "NIX_ENFORCE_NO_NATIVE" 10 | "NIX_LDFLAGS" 11 | ] 12 | -------------------------------------------------------------------------------- /tests/EvalError/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | }: 3 | 4 | { 5 | # positive cases 6 | exception = pkgs.callPackage ./exception.nix { }; 7 | 8 | # negative cases 9 | no-exception = pkgs.callPackage ./no-exception.nix { }; 10 | } 11 | -------------------------------------------------------------------------------- /tests/missing-phase-hooks/check-both.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "phase-hooks-check-both"; 6 | 7 | src = ../fixtures/make; 8 | 9 | checkPhase = '' 10 | make check 11 | ''; 12 | } 13 | -------------------------------------------------------------------------------- /tests/python-imports-check-typo/pythonCheckImports.nix: -------------------------------------------------------------------------------- 1 | { buildPythonPackage 2 | }: 3 | 4 | buildPythonPackage { 5 | name = "pythonCheckImports"; 6 | 7 | src = ../fixtures/make; 8 | 9 | pythonCheckImports = []; 10 | } 11 | -------------------------------------------------------------------------------- /tests/python-imports-check-typo/pythonImportsCheck.nix: -------------------------------------------------------------------------------- 1 | { buildPythonPackage 2 | }: 3 | 4 | buildPythonPackage { 5 | name = "pythonImportsCheck"; 6 | 7 | src = ../fixtures/make; 8 | 9 | pythonImportsCheck = []; 10 | } 11 | -------------------------------------------------------------------------------- /tests/license-missing/empty-license.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "empty-license"; 7 | 8 | src = ../fixtures/make; 9 | 10 | meta = with lib; { 11 | license = []; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /tests/missing-phase-hooks/install-both.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "phase-hooks-install-both"; 6 | 7 | src = ../fixtures/make; 8 | 9 | installPhase = '' 10 | make install 11 | ''; 12 | } 13 | -------------------------------------------------------------------------------- /tests/unused-argument/used-single.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "unused-single"; 6 | 7 | src = ../fixtures/make; 8 | 9 | passthru = { 10 | function = used: used + used; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /tests/explicit-phases/build.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "explicit-phases-build"; 6 | 7 | src = ../fixtures/make; 8 | 9 | buildPhase = '' 10 | make 11 | runHook postBuild 12 | ''; 13 | } 14 | -------------------------------------------------------------------------------- /tests/stale-substitute/live.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "live"; 6 | 7 | src = ../fixtures/make; 8 | 9 | patchPhase = '' 10 | substituteInPlace foo.in --replace-warn foo bar 11 | ''; 12 | } 13 | -------------------------------------------------------------------------------- /tests/unclear-gpl/lgpl3-python.nix: -------------------------------------------------------------------------------- 1 | { lib 2 | , buildPythonPackage 3 | }: 4 | 5 | buildPythonPackage { 6 | name = "unclear-gpl-lgpl3-python"; 7 | 8 | src = ../fixtures/make; 9 | 10 | meta.license = [ lib.licenses.lgpl3 ]; 11 | } 12 | -------------------------------------------------------------------------------- /tests/explicit-phases/check.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "explicit-phases-check"; 6 | 7 | src = ../fixtures/make; 8 | 9 | checkPhase = '' 10 | make check 11 | runHook postCheck 12 | ''; 13 | } 14 | -------------------------------------------------------------------------------- /tests/missing-patch-comment/missing-comment.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "missing-comment"; 6 | 7 | src = ../fixtures/make; 8 | 9 | patches = [ 10 | "a" 11 | "b" 12 | "c" 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /tests/EvalError/exception.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation rec { 5 | pname = "exception"; 6 | version = "0"; 7 | 8 | src = ../fixtures/make; 9 | 10 | propagatedBuildInputs = [ (builtins.throw "Throw an exception") ]; 11 | } 12 | -------------------------------------------------------------------------------- /tests/attribute-ordering/inherited.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | let 5 | pname = "attribute-ordering-inherited"; 6 | version = "0.0.0"; 7 | src = ../fixtures/make; 8 | in 9 | stdenv.mkDerivation rec { 10 | inherit pname version src; 11 | } 12 | -------------------------------------------------------------------------------- /tests/build-tools-in-build-inputs/cmake.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , cmake 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "build-tools-in-build-inputs-cmake"; 7 | 8 | src = ../fixtures/cmake; 9 | 10 | buildInputs = [ 11 | cmake 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /tests/environment-variables-go-to-env/env-var.nix: -------------------------------------------------------------------------------- 1 | { 2 | stdenv, 3 | }: 4 | 5 | stdenv.mkDerivation rec { 6 | name = "environment-variables-go-to-env--env-var"; 7 | 8 | src = ../fixtures/make; 9 | 10 | NIX_CFLAGS_COMPILE = "-lm"; 11 | } 12 | -------------------------------------------------------------------------------- /tests/maintainers-missing/empty-maintainers.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "empty-maintainers"; 7 | 8 | src = ../fixtures/make; 9 | 10 | meta = with lib; { 11 | maintainers = []; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /tests/missing-phase-hooks/build-post.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "phase-hooks-build-post"; 6 | 7 | src = ../fixtures/make; 8 | 9 | buildPhase = '' 10 | runHook preBuild 11 | make 12 | ''; 13 | } 14 | -------------------------------------------------------------------------------- /tests/missing-phase-hooks/build-pre.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "phase-hooks-build-pre"; 6 | 7 | src = ../fixtures/make; 8 | 9 | buildPhase = '' 10 | make 11 | runHook postBuild 12 | ''; 13 | } 14 | -------------------------------------------------------------------------------- /tests/unclear-gpl/single-nonmatching-license.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "unclear-gpl-single-nonmatching-license"; 7 | 8 | src = ../fixtures/make; 9 | 10 | meta.license = lib.licenses.asl20; 11 | } 12 | -------------------------------------------------------------------------------- /tests/environment-variables-go-to-env/no-env-vars.nix: -------------------------------------------------------------------------------- 1 | { 2 | stdenv, 3 | }: 4 | 5 | stdenv.mkDerivation rec { 6 | name = "environment-variables-go-to-env--no-env-vars"; 7 | 8 | src = ../fixtures/make; 9 | 10 | nativeBuildInputs = []; 11 | } 12 | -------------------------------------------------------------------------------- /tests/maintainers-missing/have-maintainers.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "have-maintainers"; 7 | 8 | src = ../fixtures/make; 9 | 10 | meta = with lib; { 11 | maintainers = [ "foo" ]; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /tests/missing-phase-hooks/check-post.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "phase-hooks-check-post"; 6 | 7 | src = ../fixtures/make; 8 | 9 | checkPhase = '' 10 | runHook preCheck 11 | make check 12 | ''; 13 | } 14 | -------------------------------------------------------------------------------- /tests/missing-phase-hooks/check-pre.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "phase-hooks-check-pre"; 6 | 7 | src = ../fixtures/make; 8 | 9 | checkPhase = '' 10 | make check 11 | runHook postCheck 12 | ''; 13 | } 14 | -------------------------------------------------------------------------------- /tests/python-explicit-check-phase/nonredundant-pytest.nix: -------------------------------------------------------------------------------- 1 | { buildPythonPackage 2 | }: 3 | 4 | buildPythonPackage { 5 | name = "redundant-pytest"; 6 | 7 | src = ../fixtures/make; 8 | 9 | checkPhase = " pytest -some-nonstandard-flag "; 10 | } 11 | -------------------------------------------------------------------------------- /tests/duplicate-check-inputs/default.nix: -------------------------------------------------------------------------------- 1 | { buildPythonPackage 2 | , numpy 3 | }: 4 | 5 | buildPythonPackage { 6 | name = "patch-phase"; 7 | 8 | src = ../fixtures/make; 9 | 10 | propagatedBuildInputs = [ numpy ]; 11 | checkInputs = [ numpy ]; 12 | } 13 | -------------------------------------------------------------------------------- /tests/missing-phase-hooks/configure-both.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "phase-hooks-configure-both"; 6 | 7 | src = ../fixtures/make; 8 | 9 | configurePhase = '' 10 | echo ./configure # just pretend 11 | ''; 12 | } 13 | -------------------------------------------------------------------------------- /tests/no-flags-spaces/default.nix: -------------------------------------------------------------------------------- 1 | { callPackage 2 | }: 3 | 4 | { 5 | # positive cases 6 | bad = callPackage ./bad.nix { }; 7 | 8 | # negative cases 9 | okay = callPackage ./okay.nix { }; 10 | nonstring = callPackage ./nonstring.nix { }; 11 | } 12 | -------------------------------------------------------------------------------- /tests/python-inconsistent-interpreters/normal.nix: -------------------------------------------------------------------------------- 1 | { 2 | buildPythonPackage, 3 | numpy, 4 | scipy, 5 | }: 6 | 7 | buildPythonPackage rec { 8 | name = "mixed-normal"; 9 | 10 | propagatedBuildInputs = [ 11 | numpy 12 | scipy 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: cargo 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /tests/missing-patch-comment/general-comment.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "general-comment"; 6 | 7 | src = ../fixtures/make; 8 | 9 | # global comment 10 | patches = [ 11 | "a" 12 | "b" 13 | "c" 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /tests/missing-phase-hooks/install-post.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "phase-hooks-install-post"; 6 | 7 | src = ../fixtures/make; 8 | 9 | installPhase = '' 10 | runHook preInstall 11 | make install 12 | ''; 13 | } 14 | -------------------------------------------------------------------------------- /tests/missing-phase-hooks/install-pre.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "phase-hooks-install-pre"; 6 | 7 | src = ../fixtures/make; 8 | 9 | installPhase = '' 10 | make install 11 | runHook postInstall 12 | ''; 13 | } 14 | -------------------------------------------------------------------------------- /tests/python-include-tests/pytest-check-hook.nix: -------------------------------------------------------------------------------- 1 | { buildPythonPackage 2 | , pytestCheckHook 3 | }: 4 | 5 | buildPythonPackage { 6 | name = "package"; 7 | 8 | src = ../fixtures/make; 9 | 10 | nativeCheckInputs = [ 11 | pytestCheckHook 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /tests/stale-substitute/stale.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "stale"; 6 | 7 | src = ../fixtures/make; 8 | 9 | patchPhase = '' 10 | substituteInPlace foo.in --replace-warn string-that-does-not-exist bar 11 | ''; 12 | } 13 | -------------------------------------------------------------------------------- /tests/build-tools-in-build-inputs/pkg-config.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , pkg-config 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "build-tools-in-build-inputs-pkg-config"; 7 | 8 | src = ../fixtures/make; 9 | 10 | buildInputs = [ 11 | pkg-config 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /tests/build-tools-in-build-inputs/sphinx.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , python3 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "build-tools-in-build-inputs-sphinx"; 7 | 8 | src = ../fixtures/make; 9 | 10 | buildInputs = [ 11 | python3.pkgs.sphinx 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /tests/explicit-phases/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | }: 3 | 4 | { 5 | configure = pkgs.callPackage ./configure.nix { }; 6 | build = pkgs.callPackage ./build.nix { }; 7 | check = pkgs.callPackage ./check.nix { }; 8 | install = pkgs.callPackage ./install.nix { }; 9 | } 10 | -------------------------------------------------------------------------------- /tests/name-and-version/everything.nix: -------------------------------------------------------------------------------- 1 | { stdenv, lib }: 2 | 3 | stdenv.mkDerivation rec { 4 | name = "${pname}-${variant}-${version}"; 5 | pname = "name-and-version"; 6 | variant = "frobnicated"; 7 | version = "1.0"; 8 | 9 | src = ../fixtures/make; 10 | } 11 | -------------------------------------------------------------------------------- /tests/missing-patch-comment/comment-after-newline.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "comment-after-newline"; 6 | 7 | src = ../fixtures/make; 8 | 9 | patches = [ 10 | "a" 11 | # comment after newline doesn't count 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /tests/no-flags-spaces/okay.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "no-flags-spaces-okay"; 6 | 7 | src = ../fixtures/make; 8 | 9 | makeFlags = [ 10 | "PREFIX=${placeholder "out"}" 11 | "LIBDIR=${placeholder "out"}/lib" 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /tests/no-flags-array/make.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "no-flags-array-make"; 6 | 7 | src = ../fixtures/make; 8 | 9 | makeFlagsArray = [ 10 | "PREFIX=${placeholder "out"}" 11 | "LIBDIR=${placeholder "out"}/lib" 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /tests/unused-argument/used-in-defaults.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , pkgs # might look unused, but is not 3 | , foobarbazqux ? pkgs.hello 4 | }: 5 | 6 | stdenv.mkDerivation { 7 | name = "used-pattern"; 8 | 9 | src = ../fixtures/make; 10 | buildInputs = [ foobarbazqux ]; 11 | } 12 | -------------------------------------------------------------------------------- /tests/unused-argument/used-pattern.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , var1 3 | , var2 4 | }: 5 | 6 | stdenv.mkDerivation { 7 | name = "used-pattern"; 8 | 9 | src = ../fixtures/make; 10 | 11 | passthru = { 12 | argument = { 13 | inherit var1 var2; 14 | }; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /tests/explicit-phases/configure.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "explicit-phases-configure"; 6 | 7 | src = ../fixtures/make; 8 | 9 | configurePhase = '' 10 | echo ./configure # just pretend 11 | runHook postConfigure 12 | ''; 13 | } 14 | -------------------------------------------------------------------------------- /tests/explicit-phases/install.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "explicit-phases-install"; 6 | 7 | src = ../fixtures/make; 8 | 9 | installPhase = '' 10 | runHook preInstall 11 | make install 12 | runHook postInstall 13 | ''; 14 | } 15 | -------------------------------------------------------------------------------- /explanations/attribute-typo.md: -------------------------------------------------------------------------------- 1 | nixpkgs-hammering contains a list of [known attributes](../lib/derivation-attributes.nix). When an attribute whose name is not listed there is passed to `stdenv.mkDerivation` but we know about a similarly named atribute, the check will suggest it as a likely typo. 2 | -------------------------------------------------------------------------------- /tests/missing-patch-comment/comment-inline.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "comment-inline"; 6 | 7 | src = ../fixtures/make; 8 | 9 | patches = [ 10 | "a" # comment for a 11 | "b" # comment for b 12 | "c" # comment for c 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /tests/missing-phase-hooks/configure-post.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "phase-hooks-configure-post"; 6 | 7 | src = ../fixtures/make; 8 | 9 | configurePhase = '' 10 | runHook preConfigure 11 | echo ./configure # just pretend 12 | ''; 13 | } 14 | -------------------------------------------------------------------------------- /tests/missing-phase-hooks/configure-pre.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "phase-hooks-configure-pre"; 6 | 7 | src = ../fixtures/make; 8 | 9 | configurePhase = '' 10 | echo ./configure # just pretend 11 | runHook postConfigure 12 | ''; 13 | } 14 | -------------------------------------------------------------------------------- /tests/python-inconsistent-interpreters/mixed-checkInputs.nix: -------------------------------------------------------------------------------- 1 | { 2 | buildPythonPackage, 3 | python3Full, 4 | }: 5 | 6 | buildPythonPackage rec { 7 | name = "python-inconsistent-interpreters--mixed-checkInputs"; 8 | 9 | checkInputs = [ 10 | python3Full.pkgs.numpy 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /tests/unnecessary-parallel-building/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | }: 3 | 4 | { 5 | cmake = pkgs.callPackage ./cmake.nix { }; 6 | meson = pkgs.callPackage ./meson.nix { }; 7 | qmake = pkgs.callPackage ./qmake.nix { }; 8 | qt-derivation = pkgs.qt5.callPackage ./qt-derivation.nix { }; 9 | } 10 | -------------------------------------------------------------------------------- /tests/environment-variables-go-to-env/env-vars-just-in-env.nix: -------------------------------------------------------------------------------- 1 | { 2 | stdenv, 3 | }: 4 | 5 | stdenv.mkDerivation rec { 6 | name = "environment-variables-go-to-env--env-vars-just-in-env"; 7 | 8 | src = ../fixtures/make; 9 | 10 | env = { 11 | NIX_CFLAGS_COMPILE = "-lm"; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /tests/no-flags-array/make-finalAttrs.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation (finalAttrs: { 5 | name = "no-flags-array-make"; 6 | 7 | src = ../fixtures/make; 8 | 9 | makeFlagsArray = [ 10 | "PREFIX=${placeholder "out"}" 11 | "LIBDIR=${placeholder "out"}/lib" 12 | ]; 13 | }) 14 | -------------------------------------------------------------------------------- /tests/no-flags-spaces/bad.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "no-flags-spaces-bad"; 6 | 7 | src = ../fixtures/make; 8 | 9 | makeFlags = [ 10 | "PREFIX=${placeholder "out"}" 11 | "LIBDIR=${placeholder "out"}/lib" 12 | "RELEASE=August 2020" 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /tests/python-explicit-check-phase/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | python3, 3 | }: 4 | 5 | { 6 | # positive cases 7 | redundant-pytest = python3.pkgs.callPackage ./redundant-pytest.nix { }; 8 | 9 | # negative cases 10 | nonredundant-pytest = python3.pkgs.callPackage ./nonredundant-pytest.nix { }; 11 | } 12 | -------------------------------------------------------------------------------- /tests/python-include-tests/tests-disabled-no-import-checks.nix: -------------------------------------------------------------------------------- 1 | { buildPythonPackage 2 | , pytestCheckHook 3 | }: 4 | 5 | buildPythonPackage { 6 | name = "package"; 7 | 8 | src = ../fixtures/make; 9 | 10 | checkInputs = [ 11 | pytestCheckHook 12 | ]; 13 | 14 | doCheck = false; 15 | } 16 | -------------------------------------------------------------------------------- /tests/unnecessary-parallel-building/cmake.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , cmake 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "unnecessary-parallel-building-cmake"; 7 | 8 | src = ../fixtures/cmake; 9 | 10 | nativeBuildInputs = [ 11 | cmake 12 | ]; 13 | 14 | enableParallelBuilding = true; 15 | } 16 | -------------------------------------------------------------------------------- /tests/unnecessary-parallel-building/qmake.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , qt5 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "unnecessary-parallel-building-qmake"; 7 | 8 | src = ../fixtures/qmake; 9 | 10 | nativeBuildInputs = [ 11 | qt5.qmake 12 | ]; 13 | 14 | enableParallelBuilding = true; 15 | } 16 | -------------------------------------------------------------------------------- /tests/attribute-ordering/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | }: 3 | 4 | { 5 | # positive cases 6 | out-of-order = pkgs.callPackage ./out-of-order.nix { }; 7 | 8 | # negative cases 9 | inherited = pkgs.callPackage ./inherited.nix { }; 10 | properly-ordered = pkgs.callPackage ./properly-ordered.nix { }; 11 | } 12 | -------------------------------------------------------------------------------- /explanations/maintainers-missing.md: -------------------------------------------------------------------------------- 1 | All packages ought to have a maintainer. 2 | 3 | This can be the update submitter or a community member that accepts to take maintainership of the package. See [Section 7.1 of the Nixpkgs manual](https://nixos.org/manual/nixpkgs/unstable/#sec-standard-meta-attributes) for more details. 4 | -------------------------------------------------------------------------------- /tests/meson-cmake/default.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , meson 3 | , cmake 4 | , ninja 5 | }: 6 | 7 | stdenv.mkDerivation { 8 | name = "meson-cmake"; 9 | 10 | src = ../fixtures/meson; 11 | 12 | nativeBuildInputs = [ 13 | meson 14 | cmake 15 | ninja # Meson does not support make backend. 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /tests/missing-patch-comment/comment-above.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | }: 3 | 4 | stdenv.mkDerivation { 5 | name = "comment-above"; 6 | 7 | src = ../fixtures/make; 8 | 9 | patches = [ 10 | # comment for a 11 | "a" 12 | # comment for b 13 | "b" 14 | # comment for c 15 | "c" 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /tests/environment-variables-go-to-env/pkg-config-var.nix: -------------------------------------------------------------------------------- 1 | { 2 | stdenv, 3 | }: 4 | 5 | stdenv.mkDerivation rec { 6 | name = "environment-variables-go-to-env--pkg-config-var"; 7 | 8 | src = ../fixtures/make; 9 | 10 | PKG_CONFIG_SYSTEMD_SYSTEMDSYSTEMUNITDIR = "${placeholder "out"}/lib/systemd/system"; 11 | } 12 | -------------------------------------------------------------------------------- /tests/unnecessary-parallel-building/qt-derivation.nix: -------------------------------------------------------------------------------- 1 | { mkDerivation 2 | , qmake 3 | }: 4 | 5 | mkDerivation { 6 | name = "unnecessary-parallel-building-qt-derivation"; 7 | 8 | src = ../fixtures/qmake; 9 | 10 | nativeBuildInputs = [ 11 | qmake 12 | ]; 13 | 14 | enableParallelBuilding = true; 15 | } 16 | -------------------------------------------------------------------------------- /tests/build-tools-in-build-inputs/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | }: 3 | 4 | { 5 | cmake = pkgs.callPackage ./cmake.nix { }; 6 | meson = pkgs.callPackage ./meson.nix { }; 7 | ninja = pkgs.callPackage ./ninja.nix { }; 8 | pkg-config = pkgs.callPackage ./pkg-config.nix { }; 9 | sphinx = pkgs.callPackage ./sphinx.nix { }; 10 | } 11 | -------------------------------------------------------------------------------- /tests/license-missing/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | }: 3 | 4 | { 5 | # positive cases 6 | no-license = pkgs.callPackage ./no-license.nix { }; 7 | empty-license = pkgs.callPackage ./empty-license.nix { }; 8 | no-meta = pkgs.callPackage ./no-meta.nix { }; 9 | 10 | # negative cases 11 | have-license = pkgs.callPackage ./have-license.nix {}; 12 | } 13 | -------------------------------------------------------------------------------- /tests/build-tools-in-build-inputs/meson.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , meson 3 | , ninja 4 | }: 5 | 6 | stdenv.mkDerivation { 7 | name = "build-tools-in-build-inputs-meson"; 8 | 9 | src = ../fixtures/meson; 10 | 11 | nativeBuildInputs = [ 12 | ninja # Meson does not support make backend. 13 | ]; 14 | 15 | buildInputs = [ 16 | meson 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /tests/unnecessary-parallel-building/meson.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , meson 3 | , ninja 4 | }: 5 | 6 | stdenv.mkDerivation { 7 | name = "unnecessary-parallel-building-meson"; 8 | 9 | src = ../fixtures/meson; 10 | 11 | nativeBuildInputs = [ 12 | meson 13 | ninja # Meson does not support make backend. 14 | ]; 15 | 16 | enableParallelBuilding = true; 17 | } 18 | -------------------------------------------------------------------------------- /rust-checks/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-checks" 3 | version = "0.0.0" 4 | authors = ["Robert T. McGibbon "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | codespan = "0.12" 9 | regex = "1" 10 | rnix = "0.12" 11 | rowan = { version = "0.15", features = [ "serde1" ] } 12 | serde = { workspace = true } 13 | serde_json = { workspace = true } 14 | -------------------------------------------------------------------------------- /tests/build-tools-in-build-inputs/ninja.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , meson 3 | , ninja 4 | }: 5 | 6 | stdenv.mkDerivation { 7 | name = "build-tools-in-build-inputs-ninja"; 8 | 9 | src = ../fixtures/meson; # Writing ninja rules by hand is intentionally painful. 10 | 11 | nativeBuildInputs = [ 12 | meson 13 | ]; 14 | 15 | buildInputs = [ 16 | ninja 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /tests/maintainers-missing/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | }: 3 | 4 | { 5 | # positive cases 6 | no-maintainers = pkgs.callPackage ./no-maintainers.nix { }; 7 | empty-maintainers = pkgs.callPackage ./empty-maintainers.nix { }; 8 | no-meta = pkgs.callPackage ./no-meta.nix { }; 9 | 10 | # negative cases 11 | have-maintainers = pkgs.callPackage ./have-maintainers.nix { }; 12 | } 13 | -------------------------------------------------------------------------------- /tests/python-inconsistent-interpreters/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | python3, 3 | }: 4 | 5 | { 6 | # positive cases 7 | mixed-checkInputs = python3.pkgs.callPackage ./mixed-checkInputs.nix { }; 8 | mixed-propagatedBuildInputs = python3.pkgs.callPackage ./mixed-propagatedBuildInputs.nix { }; 9 | 10 | # negative cases 11 | normal = python3.pkgs.callPackage ./normal.nix { }; 12 | } 13 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 3 | flake-compat = fetchTarball { 4 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 5 | sha256 = lock.nodes.flake-compat.locked.narHash; 6 | }; 7 | self = import flake-compat { 8 | src = ./.; 9 | }; 10 | in 11 | self.defaultNix 12 | -------------------------------------------------------------------------------- /tests/python-inconsistent-interpreters/mixed-propagatedBuildInputs.nix: -------------------------------------------------------------------------------- 1 | { 2 | buildPythonPackage, 3 | python311Packages, 4 | python39Packages, 5 | }: 6 | 7 | buildPythonPackage rec { 8 | name = "python-inconsistent-interpreters--mixed-propagatedBuildInputs"; 9 | 10 | propagatedBuildInputs = [ 11 | python311Packages.numpy 12 | python39Packages.scipy 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /explanations/duplicate-check-inputs.md: -------------------------------------------------------------------------------- 1 | In Python package expressions, `propagatedBuildInputs` is used to list dependencies that are required to be available at runtime. `checkInputs` lists dependencies that are required specifically during the `checkPhase`. 2 | 3 | All dependencies from `propagatedBuildInputs` are available during `checkPhase`, so there is no need to duplicate them in the `checkInputs`. 4 | -------------------------------------------------------------------------------- /tests/environment-variables-go-to-env/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | }: 4 | 5 | { 6 | # positive cases 7 | env-var = pkgs.callPackage ./env-var.nix { }; 8 | pkg-config-var = pkgs.callPackage ./pkg-config-var.nix { }; 9 | 10 | # negative cases 11 | env-vars-just-in-env = pkgs.callPackage ./env-vars-just-in-env.nix { }; 12 | no-env-vars = pkgs.callPackage ./no-env-vars.nix { }; 13 | } 14 | -------------------------------------------------------------------------------- /tests/unused-argument/unused-pattern-var-in-let-binding.nix: -------------------------------------------------------------------------------- 1 | { stdenv, unused }: 2 | 3 | stdenv.mkDerivation { 4 | name = "unused-pattern"; 5 | 6 | # although we have the identifier 'unused' in the function body 7 | # this does not count as usage for purposes of 'unused-argument' 8 | # because of it's position as a key in an attrset 9 | src = let unused = "foo"; in ../fixtures/make; 10 | } 11 | -------------------------------------------------------------------------------- /tests/unused-argument/unused-pattern-var-as-key.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , unused 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "unused-pattern"; 7 | 8 | src = ../fixtures/make; 9 | 10 | # although we have the identifier 'unused' in the function body 11 | # this does not count as usage for purposes of 'unused-argument' 12 | # because of it's position as a key in an attrset 13 | unused = 1; 14 | } 15 | -------------------------------------------------------------------------------- /explanations/stale-substitute.md: -------------------------------------------------------------------------------- 1 | The build log of this package indicates that a nix `substitute` function, such as `substituteInPlace`, was called but failed to match its pattern. 2 | 3 | There may be a typo in the pattern, or it may be the case that the pattern being substituted has been changed in the upstream source such that it does not match anymore. 4 | 5 | Please check the offending substituteInPlace for typos or changes in source. 6 | -------------------------------------------------------------------------------- /explanations/license-missing.md: -------------------------------------------------------------------------------- 1 | All packages should have a license, otherwise they are legally considered unfree (all rights reserved). 2 | 3 | The `meta.license` attribute should preferrably contain a value from `lib.licenses` defined in [`nixpkgs/lib/licenses.nix`](https://github.com/NixOS/nixpkgs/blob/master/lib/licenses.nix). See the [Licenses section of Nixpkgs manual](https://nixos.org/manual/nixpkgs/unstable/#sec-meta-license) for more details. 4 | -------------------------------------------------------------------------------- /tests/missing-patch-comment/comment-within.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , fetchpatch 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "comment-within"; 7 | 8 | src = ../fixtures/make; 9 | 10 | patches = [ 11 | (fetchpatch { 12 | # comment within 13 | url = builtins.unsafeDiscardStringContext "file://${../fixtures/patch.patch}"; 14 | sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; 15 | }) 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v5 13 | with: 14 | # Nix fails to load the flake using flake-compat otherwise. 15 | fetch-depth: 0 16 | 17 | - name: Install Nix 18 | uses: cachix/install-nix-action@v31 19 | 20 | - name: Run tests 21 | run: nix develop -c cargo test 22 | -------------------------------------------------------------------------------- /tests/attribute-ordering/out-of-order.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | , fetchurl 4 | }: 5 | 6 | stdenv.mkDerivation rec { 7 | meta = with lib; { 8 | description = ""; 9 | license = licenses.mit; 10 | platforms = platforms.unix; 11 | maintainers = []; 12 | }; 13 | 14 | passthru = { }; 15 | 16 | nativeBuildInputs = []; 17 | 18 | patches = []; 19 | 20 | src = ../fixtures/make; 21 | 22 | version = "0.0.0"; 23 | 24 | pname = "out-of-order"; 25 | } 26 | -------------------------------------------------------------------------------- /tests/attribute-typo/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | }: 3 | 4 | { 5 | # positive cases 6 | casing = pkgs.callPackage ./casing.nix { }; 7 | deletion = pkgs.callPackage ./deletion.nix { }; 8 | insertion = pkgs.callPackage ./insertion.nix { }; 9 | transposition = pkgs.callPackage ./transposition.nix { }; 10 | 11 | # negative cases 12 | properly-ordered = pkgs.callPackage ../attribute-ordering/properly-ordered.nix { }; 13 | unknown-short = pkgs.callPackage ./unknown-short.nix { }; 14 | } 15 | -------------------------------------------------------------------------------- /explanations/no-uri-literals.md: -------------------------------------------------------------------------------- 1 | URI literal syntax has been deprecated by [RFC 45](https://github.com/NixOS/rfcs/pull/45) and should not be used any more. URIs should be quoted as regular strings. 2 | 3 | ## Examples 4 | 5 | ### Before 6 | 7 | ```nix 8 | stdenv.mkDerivation { 9 | … 10 | 11 | meta.homepage = https://www.example.com; 12 | } 13 | ``` 14 | 15 | ### After 16 | 17 | ```nix 18 | stdenv.mkDerivation { 19 | … 20 | 21 | meta.homepage = "https://www.example.com"; 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /tests/unclear-gpl/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | }: 3 | 4 | { 5 | # positive cases 6 | gpl2 = pkgs.callPackage ./gpl2.nix { }; 7 | gpl3 = pkgs.callPackage ./gpl3.nix { }; 8 | lgpl2 = pkgs.callPackage ./lgpl2.nix { }; 9 | lgpl21 = pkgs.callPackage ./lgpl21.nix { }; 10 | lgpl3 = pkgs.callPackage ./lgpl3.nix { }; 11 | lgpl3-python = pkgs.python3.pkgs.callPackage ./lgpl3-python.nix { }; 12 | 13 | # negative cases 14 | single-nonmatching-license = pkgs.callPackage ./single-nonmatching-license.nix { }; 15 | } 16 | -------------------------------------------------------------------------------- /tests/missing-patch-comment/complex-structure1.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | , unrarSupport 4 | }: 5 | 6 | # From https://github.com/NixOS/nixpkgs/pull/113149 7 | # https://github.com/jtojnar/nixpkgs-hammering/issues/23 8 | 9 | stdenv.mkDerivation { 10 | name = "comments-in-complex-structure-1"; 11 | 12 | src = ../fixtures/make; 13 | 14 | patches = [ 15 | # Plugin installation (very insecure) disabled (from Debian) 16 | ../fixtures/patch.patch 17 | ] 18 | ++ lib.optional (!unrarSupport) ./foobar.patch; 19 | } 20 | -------------------------------------------------------------------------------- /tests/python-imports-check-typo/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | python3, 3 | }: 4 | 5 | { 6 | # positive cases 7 | pythonImportTests = python3.pkgs.callPackage ./pythonImportTests.nix { }; 8 | pythonImportCheck = python3.pkgs.callPackage ./pythonImportCheck.nix { }; 9 | pythonImportsTest = python3.pkgs.callPackage ./pythonImportsTest.nix { }; 10 | pythonCheckImports = python3.pkgs.callPackage ./pythonCheckImports.nix { }; 11 | 12 | # negative cases 13 | pythonImportsCheck = python3.pkgs.callPackage ./pythonImportsCheck.nix { }; 14 | } 15 | -------------------------------------------------------------------------------- /tests/missing-patch-comment/ignore-nested-lists1.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , fetchpatch 3 | }: 4 | 5 | stdenv.mkDerivation { 6 | name = "ignore-nested-lists"; 7 | 8 | src = ../fixtures/make; 9 | 10 | patches = [ 11 | # comment here. no comment is required next to "foo" and "bar" 12 | (fetchpatch { 13 | url = builtins.unsafeDiscardStringContext "file://${../fixtures/patch.patch}"; 14 | sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; 15 | excludes = ["foo" "bar"]; 16 | }) 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /overlays/fixup-phase.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkMkDerivationFor; 5 | 6 | checkDerivation = drvArgs: drv: 7 | lib.singleton { 8 | name = "fixup-phase"; 9 | cond = drvArgs ? fixupPhase; 10 | msg = '' 11 | `fixupPhase` should not be overridden, use `postFixup` instead. 12 | ''; 13 | locations = [ 14 | (builtins.unsafeGetAttrPos "fixupPhase" drvArgs) 15 | ]; 16 | }; 17 | 18 | in 19 | checkMkDerivationFor checkDerivation final prev 20 | -------------------------------------------------------------------------------- /overlays/patch-phase.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkMkDerivationFor; 5 | 6 | checkDerivation = drvArgs: drv: 7 | lib.singleton { 8 | name = "patch-phase"; 9 | cond = drvArgs ? patchPhase; 10 | msg = '' 11 | `patchPhase` should not be overridden, use `postPatch` instead. 12 | ''; 13 | locations = [ 14 | (builtins.unsafeGetAttrPos "patchPhase" drvArgs) 15 | ]; 16 | }; 17 | 18 | in 19 | checkMkDerivationFor checkDerivation final prev 20 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 3 | flake-compat = fetchTarball { 4 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 5 | sha256 = lock.nodes.flake-compat.locked.narHash; 6 | }; 7 | self = import flake-compat { 8 | src = ./.; 9 | }; 10 | devShells = self.shellNix.outputs.devShells.${builtins.currentSystem}; 11 | in 12 | # Add rest of the devShells for the current system so that we can easily access them with nix-shell. 13 | devShells.default // devShells 14 | -------------------------------------------------------------------------------- /tests/missing-patch-comment/ignore-nested-lists2.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , fetchpatch 3 | , lib 4 | }: 5 | 6 | stdenv.mkDerivation { 7 | name = "ignore-nested-lists2"; 8 | 9 | src = ../fixtures/make; 10 | 11 | patches = lib.optionals (true) [ 12 | # comment here. no comment is required next to "foo" and "bar" 13 | (fetchpatch { 14 | url = builtins.unsafeDiscardStringContext "file://${../fixtures/patch.patch}"; 15 | sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; 16 | excludes = ["foo" "bar"]; 17 | }) 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /tests/python-include-tests/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | python3, 3 | }: 4 | 5 | { 6 | # positive cases 7 | no-tests-no-import-checks = python3.pkgs.callPackage ./no-tests-no-import-checks.nix { }; 8 | tests-disabled-no-import-checks = python3.pkgs.callPackage ./tests-disabled-no-import-checks.nix { }; 9 | 10 | # negative cases 11 | pytest-check-hook = python3.pkgs.callPackage ./pytest-check-hook.nix { }; 12 | explicit-check-phase = python3.pkgs.callPackage ./explicit-check-phase.nix { }; 13 | has-imports-check = python3.pkgs.callPackage ./has-imports-check.nix { }; 14 | } 15 | -------------------------------------------------------------------------------- /explanations/no-python-tests.md: -------------------------------------------------------------------------------- 1 | The build log of the package contained the string “Ran 0 tests in 0.000s”. 2 | 3 | This generally indicates that the test runner was unable to find the tests associated with this project. 4 | 5 | Some possible ways to solve this problem are: 6 | 7 | - Tell `pytest` or `pytestCheckHook` where to find the tests included in the package. 8 | - Check if the GitHub repo contains tests but they are not shipped with PyPi. If so, please switch to `fetchFromGitHub`. 9 | - If the packages does not contain any tests add `doCheck = false;` and a `pythonImportsCheck`. 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nixpkgs-hammering" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Jan Tojnar "] 6 | 7 | [[bin]] 8 | name = "nixpkgs-hammer" 9 | path = "src/main.rs" 10 | 11 | [dependencies] 12 | clap = { version = "4.5.36", features = ["derive"] } 13 | indoc = "2.0.5" 14 | serde = { workspace = true } 15 | serde_json = { workspace = true } 16 | rust-checks = { path = "./rust-checks" } 17 | 18 | [workspace] 19 | members = ["rust-checks"] 20 | 21 | [workspace.dependencies] 22 | serde = { version = "1.0.203", features = ["serde_derive"] } 23 | serde_json = "1.0.143" 24 | -------------------------------------------------------------------------------- /explanations/unnecessary-parallel-building.md: -------------------------------------------------------------------------------- 1 | [Meson](https://github.com/NixOS/nixpkgs/blob/99d379c45c793c078af4bb5d6c85459f72b1f30b/pkgs/development/tools/build-managers/meson/setup-hook.sh#L27), [CMake](https://github.com/NixOS/nixpkgs/blob/99d379c45c793c078af4bb5d6c85459f72b1f30b/pkgs/development/tools/build-managers/cmake/setup-hook.sh#L128) and [qmake](https://github.com/NixOS/nixpkgs/blob/99d379c45c793c078af4bb5d6c85459f72b1f30b/pkgs/development/libraries/qt-5/hooks/qmake-hook.sh#L27) already set `enableParallelBuilding = true` by default so adding it to package expressions using one of those build systems is not necessary. 2 | -------------------------------------------------------------------------------- /overlays/name-and-version.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkMkDerivationFor; 5 | 6 | checkDerivation = drvArgs: drv: 7 | lib.singleton { 8 | name = "name-and-version"; 9 | cond = drvArgs ? name 10 | && drvArgs ? version 11 | && !(drvArgs ? pname); 12 | msg = '' 13 | Did you mean to pass `pname` instead of `name` to `mkDerivation`? 14 | ''; 15 | locations = [ 16 | (builtins.unsafeGetAttrPos "name" drvArgs) 17 | (builtins.unsafeGetAttrPos "version" drvArgs) 18 | ]; 19 | }; 20 | 21 | in 22 | checkMkDerivationFor checkDerivation final prev 23 | -------------------------------------------------------------------------------- /explanations/python-imports-check-typo.md: -------------------------------------------------------------------------------- 1 | When a Python package not provide tests, or making them pass in Nixpkgs is too difficult, expression should contain at least [`pythonImportsCheck`](https://nixos.org/manual/nixpkgs/unstable/#using-pythonimportscheck) to check that each of the modules declared by the package can be `import`ed. This effectively tests for many packaging errors. 2 | 3 | `pythonImportsCheck` can be used like this: 4 | 5 | ```nix 6 | buildPythonPackage { 7 | pname = "mymodule"; 8 | version = "1.0.0"; 9 | 10 | src = …; 11 | 12 | pythonImportsCheck = [ 13 | "mymodule" 14 | ]; 15 | } 16 | ``` 17 | 18 | Note: it is easy to make a typo in the key `pythonImportsCheck` here, so watch out for that. 19 | -------------------------------------------------------------------------------- /overlays/python-explicit-check-phase.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkBuildPythonPackageFor; 5 | regex = "[[:space:]]*pytest[[:space:]]*"; 6 | 7 | checkDerivation = drvArgs: drv: 8 | lib.singleton { 9 | name = "python-explicit-check-phase"; 10 | cond = (drvArgs ? checkPhase) && (builtins.match regex drvArgs.checkPhase) != null; 11 | msg = '' 12 | Consider using `pytestCheckHook` in `checkInputs` rather than `checkPhase = "pytest";`. 13 | ''; 14 | locations = [ 15 | (builtins.unsafeGetAttrPos "checkPhase" drvArgs) 16 | ]; 17 | }; 18 | in 19 | checkBuildPythonPackageFor checkDerivation final prev 20 | -------------------------------------------------------------------------------- /overlays/explicit-phases.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkMkDerivationFor; 5 | 6 | phases = [ 7 | "configure" 8 | "build" 9 | "check" 10 | "install" 11 | ]; 12 | 13 | checkDerivation = drvArgs: drv: 14 | (map 15 | (phase: { 16 | name = "explicit-phases"; 17 | cond = drvArgs ? "${phase}Phase"; 18 | msg = '' 19 | It is a good idea to avoid overriding `${phase}Phase` when possible. 20 | ''; 21 | locations = [ 22 | (builtins.unsafeGetAttrPos "${phase}Phase" drvArgs) 23 | ]; 24 | }) 25 | phases 26 | ); 27 | 28 | in 29 | checkMkDerivationFor checkDerivation final prev 30 | -------------------------------------------------------------------------------- /overlays/meson-cmake.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkMkDerivationFor; 5 | 6 | checkDerivation = drvArgs: drv: 7 | lib.singleton { 8 | name = "meson-cmake"; 9 | cond = lib.elem prev.meson (drvArgs.nativeBuildInputs or [ ]) && lib.elem prev.cmake (drvArgs.nativeBuildInputs or [ ]); 10 | msg = '' 11 | Meson uses CMake as a fallback dependency resolution method and it likely is not necessary here. The message about cmake not being found is purely informational. 12 | ''; 13 | locations = [ 14 | (builtins.unsafeGetAttrPos "nativeBuildInputs" drvArgs) 15 | ]; 16 | }; 17 | 18 | in 19 | checkMkDerivationFor checkDerivation final prev 20 | -------------------------------------------------------------------------------- /rust-checks/src/checks/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::common_structs::CheckedAttr; 2 | use std::error::Error; 3 | 4 | mod missing_patch_comment; 5 | mod no_uri_literals; 6 | mod stale_substitute; 7 | mod unused_argument; 8 | 9 | pub use missing_patch_comment::run as missing_patch_comment; 10 | pub use no_uri_literals::run as no_uri_literals; 11 | pub use stale_substitute::run as stale_substitute; 12 | pub use unused_argument::run as unused_argument; 13 | 14 | pub type Check = fn(Vec) -> Result>; 15 | 16 | pub const ALL: [(&'static str, Check); 4] = [ 17 | ("missing-patch-comment", missing_patch_comment), 18 | ("no-uri-literals", no_uri_literals), 19 | ("stale-substitute", stale_substitute), 20 | ("unused-argument", unused_argument), 21 | ]; 22 | -------------------------------------------------------------------------------- /tests/missing-phase-hooks/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | }: 3 | 4 | { 5 | configure-pre = pkgs.callPackage ./configure-pre.nix { }; 6 | configure-post = pkgs.callPackage ./configure-post.nix { }; 7 | configure-both = pkgs.callPackage ./configure-both.nix { }; 8 | build-pre = pkgs.callPackage ./build-pre.nix { }; 9 | build-post = pkgs.callPackage ./build-post.nix { }; 10 | build-both = pkgs.callPackage ./build-both.nix { }; 11 | check-pre = pkgs.callPackage ./check-pre.nix { }; 12 | check-post = pkgs.callPackage ./check-post.nix { }; 13 | check-both = pkgs.callPackage ./check-both.nix { }; 14 | install-pre = pkgs.callPackage ./install-pre.nix { }; 15 | install-post = pkgs.callPackage ./install-post.nix { }; 16 | install-both = pkgs.callPackage ./install-both.nix { }; 17 | } 18 | -------------------------------------------------------------------------------- /tests/missing-patch-comment/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | }: 3 | 4 | { 5 | # positive cases 6 | missing-comment = pkgs.callPackage ./missing-comment.nix { }; 7 | comment-after-newline = pkgs.callPackage ./comment-after-newline.nix { }; 8 | 9 | # negative cases 10 | general-comment = pkgs.callPackage ./general-comment.nix { }; 11 | comment-above = pkgs.callPackage ./comment-above.nix { }; 12 | comment-within = pkgs.callPackage ./comment-within.nix { }; 13 | comment-inline = pkgs.callPackage ./comment-inline.nix { }; 14 | complex-structure1 = pkgs.callPackage ./complex-structure1.nix { 15 | unrarSupport = true; 16 | }; 17 | ignore-nested-lists1 = pkgs.callPackage ./ignore-nested-lists1.nix { }; 18 | ignore-nested-lists2 = pkgs.callPackage ./ignore-nested-lists2.nix { }; 19 | } 20 | -------------------------------------------------------------------------------- /tests/unused-argument/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | 3 | { 4 | # positive cases 5 | unused-pattern = pkgs.callPackage ./unused-pattern.nix { }; 6 | unused-pattern-var-as-key = pkgs.callPackage ./unused-pattern-var-as-key.nix { }; 7 | unused-pattern-var-in-let-binding = pkgs.callPackage ./unused-pattern-var-in-let-binding.nix { }; 8 | 9 | # negative cases 10 | used-pattern = pkgs.callPackage ./used-pattern.nix { 11 | var1 = 1; 12 | var2 = 2; 13 | }; 14 | used-single = pkgs.callPackage ./used-single.nix { }; 15 | unused-single = pkgs.callPackage ./unused-single.nix { }; 16 | used-in-string1 = pkgs.callPackage ./used-in-string1.nix { foo = "bar"; }; 17 | used-in-string2 = pkgs.callPackage ./used-in-string2.nix { foo = "bar"; }; 18 | used-in-defaults = pkgs.callPackage ./used-in-defaults.nix { }; 19 | } 20 | -------------------------------------------------------------------------------- /explanations/name-and-version.md: -------------------------------------------------------------------------------- 1 | Passing both `name` and `version` arguments is probably a mistake. 2 | 3 | `stdenv.mkDerivation` can create the derivation `name` automatically by joining its `pname` and `version` arguments as `${pname}-${version}`. This does not happen if you pass `name` argument explicitly – in that case, you will be responsible for appending version string to the derivation name. 4 | 5 | In most cases you can just pass separate *project name* (`pname`) and `version` arguments and forget about the derivation name. 6 | 7 | `mkDerivation` only started supporting that relatively recently so you may find older expressions that explicitly construct `name`. 8 | 9 | ## Possible exceptions 10 | 11 | When you want to distinguish multiple variants of a project in the *derivation name* but want to keep the `pname` argument the same. 12 | -------------------------------------------------------------------------------- /explanations/fixup-phase.md: -------------------------------------------------------------------------------- 1 | You should not override `fixupPhase`. 2 | 3 | `fixupPhase` is responsible for [various support functions](https://github.com/NixOS/nixpkgs/blob/d71a03ad695407dd482ead32d3eddff50092a1c3/pkgs/stdenv/generic/setup.sh#L1101-L1178) so if you override it without carefully replicating the original behaviour, you will likely break something. 4 | 5 | Add your commands to `postFixup` or `preFixup` hook instead. 6 | 7 | If you need to disable some action that happens during fixup, use relevant variable to disable it in a targetted way (e.g. [`dontStrip`](https://github.com/NixOS/nixpkgs/blob/d71a03ad695407dd482ead32d3eddff50092a1c3/pkgs/build-support/setup-hooks/strip.sh#L22-L24) for for `strip.sh`, [`dontPatchELF`](https://github.com/NixOS/nixpkgs/blob/d71a03ad695407dd482ead32d3eddff50092a1c3/pkgs/development/tools/misc/patchelf/setup-hook.sh#L5) for `patchelf`…). 8 | -------------------------------------------------------------------------------- /explanations/python-inconsistent-interpreters.md: -------------------------------------------------------------------------------- 1 | Each Python package expression in nixpkgs should take its inputs in such a way that it can be properly built for multiple different Python interpreters. 2 | 3 | For example, the following is incorrect: 4 | 5 | ```nix 6 | { lib 7 | , buildPythonPackage 8 | , python3Packages 9 | }: 10 | 11 | buildPythonPackage rec { 12 | pname = "mypackage"; 13 | … 14 | propagatedBuildInputs = [ 15 | python3Packages.numpy 16 | ]; 17 | } 18 | ``` 19 | 20 | Instead, it should be: 21 | 22 | ```nix 23 | { lib 24 | , buildPythonPackage 25 | , numpy 26 | }: 27 | 28 | buildPythonPackage rec { 29 | pname = "mypackage"; 30 | … 31 | propagatedBuildInputs = [ 32 | numpy 33 | ]; 34 | } 35 | ``` 36 | 37 | This design allows nixpkgs to ensure that the version of Python pulled in by `numpy` is the same as the version that the package uses. 38 | -------------------------------------------------------------------------------- /overlays/python-imports-check-typo.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkBuildPythonPackageFor; 5 | 6 | incorrectSpellings = [ 7 | "pythonImportTests" 8 | "pythonImportCheck" 9 | "pythonImportsTest" 10 | "pythonCheckImports" 11 | ]; 12 | 13 | checkDerivation = drvArgs: drv: 14 | (map 15 | (incorrectSpelling: { 16 | name = "python-imports-check-typo"; 17 | cond = builtins.hasAttr incorrectSpelling drvArgs; 18 | msg = '' 19 | A likely typo in the `${incorrectSpelling}` argument was found. Did you mean `pythonImportsCheck`? 20 | ''; 21 | locations = [ 22 | (builtins.unsafeGetAttrPos incorrectSpelling drvArgs) 23 | ]; 24 | }) 25 | incorrectSpellings 26 | ); 27 | in 28 | checkBuildPythonPackageFor checkDerivation final prev 29 | -------------------------------------------------------------------------------- /overlays/unclear-gpl.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkFor; 5 | 6 | licenses = lib.filter 7 | (licenseName: lib.licenses ? ${licenseName}) 8 | [ 9 | "gpl2" 10 | "gpl3" 11 | "lgpl2" 12 | "lgpl21" 13 | "lgpl3" 14 | ]; 15 | 16 | checkDerivation = drvArgs: drv: 17 | (map 18 | (license: { 19 | name = "unclear-gpl"; 20 | cond = lib.elem lib.licenses.${license} (lib.toList (drvArgs.meta.license or [ ])); 21 | msg = '' 22 | `${license}` is a deprecated license, please check if project uses `${license}Plus` or `${license}Only` and change `meta.license` accordingly. 23 | ''; 24 | locations = [ 25 | (builtins.unsafeGetAttrPos "license" drvArgs.meta) 26 | ]; 27 | }) 28 | licenses 29 | ); 30 | 31 | in 32 | checkFor checkDerivation final prev 33 | -------------------------------------------------------------------------------- /explanations/python-include-tests.md: -------------------------------------------------------------------------------- 1 | Some level of testing should be done for each Python package expression. 2 | 3 | If the `checkPhase` reports there are no tests, it might be necessary to: 4 | 5 | - If the tests are in a different directory than `tests/`, tell [`pytest`](https://nixos.org/manual/nixpkgs/unstable/#using-pytest) or [`pytestCheckHook`](https://nixos.org/manual/nixpkgs/unstable/#using-pytestcheckhook) where to find them. 6 | - If the upstream source code repository contains tests but they are not shipped with package on PyPI, switch to fetching from the repository (e.g. using `fetchFromGitHub`) and open an issue upstream asking to ship tests in the PyPI packages ([example](https://github.com/gnome-keysign/babel-glade/issues/5)). 7 | - If the package does not contain any tests, add `doCheck = false;` with a comment that there are no tests, and a [`pythonImportsCheck`](https://nixos.org/manual/nixpkgs/unstable/#using-pythonimportscheck) as a smoke test. 8 | -------------------------------------------------------------------------------- /explanations/patch-phase.md: -------------------------------------------------------------------------------- 1 | You should not override `patchPhase`. 2 | 3 | `patchPhase` is responsible for [applying `patches`](https://github.com/NixOS/nixpkgs/blob/d71a03ad695407dd482ead32d3eddff50092a1c3/pkgs/stdenv/generic/setup.sh#L918-L944) so if you override it without carefully replicating the original behaviour, you will likely confuse someone trying to add a patch to `patches` in the future. 4 | 5 | Add your commands to `postPatch` hook instead. 6 | 7 | Avoid using `prePatch` too. It might change the source code so that the patches in `patches` no longer apply and when that happens, it is often easier to handle the conflict by tweaking a command in `postPatch` than modifying a patch. 8 | 9 | If you need to pass some arguments to `patch` call, for example, to handle non-standard paths using `-p0`, using [`fetchpatch`](https://github.com/NixOS/nixpkgs/blob/d71a03ad695407dd482ead32d3eddff50092a1c3/pkgs/build-support/fetchpatch/default.nix#L12) to modify the patch might be nicer. 10 | -------------------------------------------------------------------------------- /overlays/duplicate-check-inputs.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkFor; 5 | 6 | checkDerivation = drvArgs: drv: 7 | let 8 | list1 = drvArgs.propagatedBuildInputs or []; 9 | list2 = drvArgs.checkInputs or []; 10 | intersection = lib.intersectLists list1 list2; 11 | in 12 | lib.singleton { 13 | name = "duplicate-check-inputs"; 14 | cond = builtins.length intersection > 0; 15 | msg = '' 16 | Dependencies listed in `propagatedBuildInputs` should not be repeated in `checkInputs`. 17 | 18 | Detected duplicates: 19 | 20 | ${lib.concatMapStringsSep "\n" (n: "- ${n}") (map (d: d.name) intersection)} 21 | ''; 22 | locations = [ 23 | (builtins.unsafeGetAttrPos "checkInputs" drvArgs) 24 | (builtins.unsafeGetAttrPos "propagatedBuildInputs" drvArgs) 25 | ]; 26 | }; 27 | in 28 | checkFor checkDerivation final prev 29 | -------------------------------------------------------------------------------- /rust-checks/src/checks/no_uri_literals.rs: -------------------------------------------------------------------------------- 1 | use crate::{analysis::*, common_structs::*, tree_utils::walk_kind}; 2 | use codespan::{FileId, Files}; 3 | use rnix::SyntaxKind::*; 4 | use std::error::Error; 5 | 6 | pub fn run(attrs: Vec) -> Result> { 7 | analyze_nix_files(attrs, analyze_single_file) 8 | } 9 | 10 | fn analyze_single_file(files: &Files, file_id: FileId) -> Result> { 11 | let root = find_root(files, file_id)?; 12 | let mut report: Report = vec![]; 13 | 14 | // Find all URI literals and report them. 15 | for uri in walk_kind(&root, TOKEN_URI) { 16 | let start: u32 = uri.text_range().start().into(); 17 | 18 | report.push(NixpkgsHammerMessage { 19 | msg: "URI literals are deprecated.".to_string(), 20 | name: "no-uri-literals", 21 | locations: vec![SourceLocation::from_byte_index(files, file_id, start)?], 22 | link: true, 23 | }); 24 | } 25 | 26 | Ok(report) 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2020 Jan Tojnar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /overlays/environment-variables-go-to-env.nix: -------------------------------------------------------------------------------- 1 | final: 2 | prev: 3 | 4 | let 5 | inherit (prev) lib; 6 | inherit (import ../lib { inherit lib; }) checkMkDerivationFor; 7 | 8 | knownEnvironmentVariables = import ../lib/environment-variables.nix; 9 | 10 | isEnvironmentVariable = 11 | name: 12 | builtins.elem name knownEnvironmentVariables 13 | || lib.hasPrefix "PKG_CONFIG_" name; 14 | 15 | checkDerivation = drvArgs: drv: 16 | let 17 | environmentVariables = builtins.filter isEnvironmentVariable (builtins.attrNames drvArgs); 18 | in 19 | builtins.map 20 | (var: 21 | { 22 | name = "environment-variables-go-to-env"; 23 | cond = builtins.length environmentVariables > 0; 24 | msg = '' 25 | Environment variable `${var}` should be moved to `env` attribute rather than being passed directly to ‘stdenv.mkDerivation’. 26 | ''; 27 | locations = [ 28 | (builtins.unsafeGetAttrPos var drvArgs) 29 | ]; 30 | } 31 | ) 32 | environmentVariables; 33 | in 34 | checkMkDerivationFor checkDerivation final prev 35 | -------------------------------------------------------------------------------- /overlays/python-include-tests.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkBuildPythonPackageFor getLocation; 5 | 6 | checkDerivation = drvArgs: drv: 7 | lib.singleton { 8 | name = "python-include-tests"; 9 | cond = 10 | let 11 | isTestHook = name: builtins.elem name [ 12 | "pytest-check-hook" 13 | ]; 14 | hasCheckPhase = drvArgs ? checkPhase || drvArgs ? installCheckPhase; 15 | hasDoCheckFalse = (drv ? doInstallCheck) && !drv.doInstallCheck; 16 | hasCheckHook = lib.any (n: isTestHook (n.name or "")) drv.nativeBuildInputs; 17 | hasPythonImportsCheck = drvArgs ? pythonImportsCheck; 18 | 19 | hasActiveCheckPhase = (hasCheckPhase || hasCheckHook) && (!hasDoCheckFalse); 20 | in 21 | !(hasActiveCheckPhase || hasPythonImportsCheck); 22 | msg = '' 23 | Consider adding a `checkPhase` for tests, or if not feasible, `pythonImportsCheck`. 24 | ''; 25 | locations = [ (getLocation drv) ]; 26 | }; 27 | in 28 | checkBuildPythonPackageFor checkDerivation final prev 29 | -------------------------------------------------------------------------------- /overlays/license-missing.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkFor; 5 | 6 | checkDerivation = drvArgs: drv: 7 | lib.singleton { 8 | name = "license-missing"; 9 | cond = lib.length (lib.toList (drvArgs.meta.license or [ ])) == 0; 10 | msg = '' 11 | Package is missing a license. 12 | ''; 13 | locations = [ 14 | (builtins.head (builtins.filter (l: l != null) [ 15 | # If the package has a meta.license but it's empty, that's where the location of the 16 | # error is 17 | (builtins.unsafeGetAttrPos "license" drvArgs.meta or {}) 18 | # Otherwise, it probably has a meta field, so that's where we'll locate the error 19 | (builtins.unsafeGetAttrPos "meta" drvArgs) 20 | # Otherwise, the package contains no meta field, so I'm not sure where we can 21 | # say the error is, but we'll just pick another attribute it does have? 22 | (builtins.unsafeGetAttrPos (builtins.head (builtins.attrNames drvArgs)) drvArgs) 23 | ])) 24 | ]; 25 | }; 26 | 27 | in 28 | checkFor checkDerivation final prev 29 | -------------------------------------------------------------------------------- /overlays/no-flags-spaces.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkMkDerivationFor; 5 | 6 | flagAttrs = [ 7 | "cmakeFlags" 8 | "mesonFlags" 9 | "configureFlags" 10 | "qmakeFlags" 11 | "makeFlags" 12 | "ninjaFlags" 13 | "buildFlags" 14 | "checkFlags" 15 | "installFlags" 16 | "installCheckFlags" 17 | "distFlags" 18 | ]; 19 | 20 | containsSpace = str: builtins.match ".*[[:space:]].*" (builtins.toString str) != null; 21 | 22 | checkDerivation = drvArgs: drv: 23 | (map 24 | (flagAttr: { 25 | name = "no-flags-spaces"; 26 | severity = "error"; 27 | cond = 28 | let 29 | attr = drvArgs.${flagAttr} or []; 30 | in 31 | builtins.isList attr && builtins.any containsSpace attr; 32 | msg = '' 33 | `${flagAttr}` cannot contain spaces, please use `${flagAttr}Array` in Bash. 34 | ''; 35 | locations = [ 36 | (builtins.unsafeGetAttrPos flagAttr drvArgs) 37 | ]; 38 | }) 39 | flagAttrs 40 | ); 41 | in 42 | checkMkDerivationFor checkDerivation final prev 43 | -------------------------------------------------------------------------------- /rust-checks/src/checks/stale_substitute.rs: -------------------------------------------------------------------------------- 1 | use crate::{analysis::*, common_structs::*}; 2 | use regex::Regex; 3 | use std::{ 4 | error::Error, 5 | io::{BufRead, BufReader}, 6 | process::ChildStdout, 7 | }; 8 | 9 | pub fn run(attrs: Vec) -> Result> { 10 | analyze_log_files(attrs, analyze_single_file) 11 | } 12 | 13 | fn analyze_single_file( 14 | log: BufReader, 15 | attr: &CheckedAttr, 16 | ) -> Result> { 17 | let re = Regex::new( 18 | r"substituteStream\(\) in derivation .+: WARNING: pattern (.*?) doesn't match anything in file '(.*?)'", 19 | ) 20 | .unwrap(); 21 | 22 | let report = log 23 | .lines() 24 | .filter_map(|line| line.ok()) 25 | .filter_map(|s| re.find(&s).and_then(|m| Some(m.as_str().to_string()))) 26 | .map(|m| NixpkgsHammerMessage { 27 | msg: format!("Stale substituteInPlace detected.\n{}", m), 28 | name: "stale-substitute", 29 | locations: attr.location.iter().map(|x| x.clone()).collect(), 30 | link: true, 31 | }) 32 | .collect(); 33 | 34 | Ok(report) 35 | } 36 | -------------------------------------------------------------------------------- /overlays/no-flags-array.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkMkDerivationFor; 5 | 6 | arrayAttrs = [ 7 | # Do not include makeWrapperArgs, it is type aware: 8 | # https://github.com/NixOS/nixpkgs/blob/fcac694fe5ca5204e276e14ea0481428172280b2/pkgs/development/interpreters/python/wrap.sh#L87 9 | "cmakeFlagsArray" 10 | "mesonFlagsArray" 11 | "configureFlagsArray" 12 | "qmakeFlagsArray" 13 | "makeFlagsArray" 14 | "ninjaFlagsArray" 15 | "buildFlagsArray" 16 | "checkFlagsArray" 17 | "installFlagsArray" 18 | "installCheckFlagsArray" 19 | "distFlagsArray" 20 | ]; 21 | 22 | checkDerivation = drvArgs: drv: 23 | (map 24 | (arrayAttr: { 25 | name = "no-flags-array"; 26 | severity = "error"; 27 | cond = builtins.hasAttr arrayAttr drvArgs; 28 | msg = '' 29 | `${arrayAttr}` is only intended to be used in Bash, not as a Nix attribute. 30 | ''; 31 | locations = [ 32 | (builtins.unsafeGetAttrPos arrayAttr drvArgs) 33 | ]; 34 | }) 35 | arrayAttrs 36 | ); 37 | in 38 | checkMkDerivationFor checkDerivation final prev 39 | -------------------------------------------------------------------------------- /overlays/maintainers-missing.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkFor; 5 | 6 | checkDerivation = drvArgs: drv: 7 | lib.singleton { 8 | name = "maintainers-missing"; 9 | cond = lib.length (lib.toList (drvArgs.meta.maintainers or [ ])) == 0; 10 | msg = '' 11 | Package does not have a maintainer. Consider adding yourself? 12 | ''; 13 | locations = [ 14 | (builtins.head (builtins.filter (l: l != null) [ 15 | # If the package has a meta.maintainer but it's empty, that's where the location of the 16 | # error is 17 | (builtins.unsafeGetAttrPos "maintainers" drvArgs.meta or {}) 18 | # Otherwise, it probably has a meta field, so that's where we'll locate the error 19 | (builtins.unsafeGetAttrPos "meta" drvArgs) 20 | # Otherwise, the package contains no meta field, so I'm not sure where we can 21 | # say the error is, but we'll just pick another attribute it does have? 22 | (builtins.unsafeGetAttrPos (builtins.head (builtins.attrNames drvArgs)) drvArgs) 23 | ])) 24 | ]; 25 | }; 26 | 27 | in 28 | checkFor checkDerivation final prev 29 | -------------------------------------------------------------------------------- /overlays/unnecessary-parallel-building.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkMkDerivationFor; 5 | 6 | checkDerivation = drvArgs: drv: 7 | lib.singleton { 8 | name = "unnecessary-parallel-building"; 9 | cond = 10 | let 11 | inputs = drvArgs.nativeBuildInputs or [ ] ++ drvArgs.propagatedBuildInputs or [ ] ++ drvArgs.nativeBuildInputs or [ ]; 12 | toolConfigureHookNotDisabled = 13 | (lib.elem prev.meson inputs && !drvArgs.dontUseMesonConfigure or false) || 14 | (lib.elem prev.cmake inputs && !drvArgs.dontUseCmakeConfigure or false) || 15 | (lib.elem prev.qt5.qmake inputs && !drvArgs.dontUseQmakeConfigure or false); 16 | hasEnableParallelBuilding = drvArgs.enableParallelBuilding or false; 17 | hasDefaultConfigurePhase = ! (drvArgs ? configurePhase); 18 | in 19 | toolConfigureHookNotDisabled && hasEnableParallelBuilding && hasDefaultConfigurePhase; 20 | msg = '' 21 | Meson, CMake and qmake already set `enableParallelBuilding = true` by default so it is not necessary. 22 | ''; 23 | locations = [ 24 | (builtins.unsafeGetAttrPos "enableParallelBuilding" drvArgs) 25 | ]; 26 | }; 27 | 28 | in 29 | checkMkDerivationFor checkDerivation final prev 30 | -------------------------------------------------------------------------------- /explanations/meson-cmake.md: -------------------------------------------------------------------------------- 1 | Meson supports CMake as an alternative method for finding dependencies and will print the following message when checking dependencies: 2 | 3 | ``` 4 | Did not find CMake 'cmake' 5 | Found CMake: NO? 6 | ``` 7 | 8 | That is just an informational message you do not need to pay much heed. `pkg-config` is the preferred method for finding dependencies on UNIX systems and will be sufficient most of the time. 9 | 10 | ## Exceptions 11 | 12 | You need both tools in the following cases: 13 | 14 | - a project builds subprojects using a different build system and it is non-trivial to separate the subprojects into separate Nix packages 15 | - a CMake project builds a Meson subproject 16 | - a Meson project builds a CMake subproject using the [experimental support](https://mesonbuild.com/CMake-module.html#cmake-subprojects) 17 | - a project using another build system builds subprojects using CMake and Meson 18 | - a Meson project depends on a package that can only be located through a [CMake `find_package` system](https://mesonbuild.com/Release-notes-for-0-49-0.html#cmake-find_package-dependency-backend) and the dependency does not want to add `pkg-config` support upstream 19 | 20 | Please add a comment with explanation if one of these applies. 21 | 22 | ## What to do? 23 | 24 | Try removing `cmake` from dependencies and see if the package still builds. 25 | -------------------------------------------------------------------------------- /overlays/missing-phase-hooks.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) capitalize checkMkDerivationFor; 5 | 6 | phases = [ 7 | "configure" 8 | "build" 9 | "check" 10 | "install" 11 | ]; 12 | 13 | checkDerivation = drvArgs: drv: 14 | (map 15 | (phase: 16 | let 17 | preMissing = builtins.match ".*runHook pre${capitalize phase}.*" drvArgs."${phase}Phase" == null; 18 | postMissing = builtins.match ".*runHook post${capitalize phase}.*" drvArgs."${phase}Phase" == null; 19 | in { 20 | name = "missing-phase-hooks"; 21 | cond = ( 22 | drvArgs ? "${phase}Phase" && 23 | drvArgs."${phase}Phase" != null && 24 | builtins.isString drvArgs."${phase}Phase" && 25 | (preMissing || postMissing) 26 | ); 27 | msg = '' 28 | `${phase}Phase` should probably contain ${lib.optionalString preMissing "`runHook pre${capitalize phase}`"}${lib.optionalString (preMissing && postMissing) " and "}${lib.optionalString postMissing "`runHook post${capitalize phase}`"}. 29 | ''; 30 | locations = [ 31 | (builtins.unsafeGetAttrPos "${phase}Phase" drvArgs) 32 | ]; 33 | } 34 | ) 35 | phases 36 | ); 37 | 38 | in 39 | checkMkDerivationFor checkDerivation final prev 40 | -------------------------------------------------------------------------------- /explanations/python-explicit-check-phase.md: -------------------------------------------------------------------------------- 1 | Nixpkgs contains a [`pytestCheckHook`](https://nixos.org/manual/nixpkgs/unstable/#using-pytestcheckhook) that can automatically run tests using pytest. 2 | 3 | For example, rather than 4 | 5 | ```nix 6 | buildPythonPackage { 7 | checkInputs = [ 8 | pytest 9 | … 10 | ]; 11 | 12 | checkPhase = '' 13 | pytest 14 | ''; 15 | } 16 | ``` 17 | 18 | consider using 19 | 20 | ```nix 21 | buildPythonPackage { 22 | checkInputs = [ 23 | pytestCheckHook 24 | … 25 | ]; 26 | } 27 | ``` 28 | 29 | Also note that many flags that you might want to pass to `pytest` can be passed automatically through `pytestCheckHook`. 30 | 31 | `buildPythonPackage` accepts: 32 | 33 | - `disabledTests` to disable particular pytest tests. 34 | - `disabledTestPaths` to disable particular pytest files or entire directories. 35 | - `pytestFlagsArray` to pass flags to pytest. This should not be used to disable tests or test files. 36 | 37 | Here is a complete example using `pytestCheckHook`, `pytestFlagsArray`, and `disabledTests` and `disabledTestPaths`: 38 | 39 | ```nix 40 | buildPythonPackage { 41 | … 42 | 43 | checkInputs = [ 44 | pytestCheckHook 45 | ]; 46 | 47 | pytestFlagsArray = [ 48 | "--timeout=30" 49 | ]; 50 | 51 | disabledTests = [ 52 | "test_function_1" 53 | ]; 54 | 55 | disabledTestPaths = [ 56 | "tests/some_file.py" 57 | "tests/reproducible" 58 | ]; 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /rust-checks/src/common_structs.rs: -------------------------------------------------------------------------------- 1 | use codespan::{ByteIndex, FileId, Files}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::error::Error; 4 | 5 | #[derive(Serialize, Deserialize, Debug, Clone)] 6 | pub struct CheckedAttr { 7 | pub name: String, 8 | pub location: Option, 9 | pub drv: Option, 10 | pub output: Option, 11 | } 12 | 13 | #[derive(Serialize, Deserialize, Debug, Clone)] 14 | pub struct SourceLocation { 15 | pub file: String, 16 | pub line: usize, 17 | pub column: Option, 18 | } 19 | 20 | impl SourceLocation { 21 | pub fn from_byte_index( 22 | files: &Files, 23 | file_id: FileId, 24 | byte_index: impl Into, 25 | ) -> Result> { 26 | let loc = files.location(file_id, byte_index)?; 27 | 28 | Ok(SourceLocation { 29 | file: files 30 | .name(file_id) 31 | .to_str() 32 | .ok_or("encoding error")? 33 | .to_string(), 34 | // Convert 0-based indexing to 1-based. 35 | column: Some(loc.column.to_usize() + 1), 36 | line: loc.line.to_usize() + 1, 37 | }) 38 | } 39 | } 40 | 41 | #[derive(Serialize, Deserialize, Debug)] 42 | pub struct NixpkgsHammerMessage { 43 | pub locations: Vec, 44 | pub msg: String, 45 | pub name: &'static str, 46 | pub link: bool, 47 | } 48 | -------------------------------------------------------------------------------- /explanations/build-tools-in-build-inputs.md: -------------------------------------------------------------------------------- 1 | The following packages are tools primarily used during build so they likely go to `nativeBuildInputs`, not `buildInputs`: 2 | 3 | - `cmake` 4 | - `meson` 5 | - `ninja` 6 | - `pkg-config` 7 | - and [more](https://github.com/jtojnar/nixpkgs-hammering/blob/master/overlays/build-tools-in-build-inputs.nix)… 8 | 9 | The distinction primarily matters when [cross-compiling](https://nixos.org/nixpkgs/manual/#chap-cross) a package for a different architecture than the one the build runs on – if you want to run a program during a package’s build, its _host_ (runtime) architecture needs to match the package’s _build_ architecture, or the operating system might not be able to execute it. 10 | 11 | Dependencies listed in `nativeBuildInputs` will ensure precisely that, while those in `buildInputs` will have the same _host_ platform as the built package. 12 | 13 | Note: In most derivations, when not cross-compiling, the programs listed in `buildInputs` will end up in [`PATH`](https://github.com/NixOS/nixpkgs/blob/390d38400aa7a425abef6e20c6773e2717b2fda1/pkgs/stdenv/generic/setup.sh#L486-L488) environment variable for build too. But you can force the distinction by setting `strictDeps = true;`; and [`python3.pkgs.buildPythonPackage`](https://github.com/NixOS/nixpkgs/blob/01b0290516db18ebfda5eee3b830fe5c0cebcf4b/pkgs/development/interpreters/python/mk-python-derivation.nix#L50-L51) and `rustPlatform.buildRustPackage` already enable that option by default. 14 | 15 | ## Exceptions 16 | 17 | - When building an environment for an IDE, you will likely want to be able to run the build tools at runtime so they should go to `buildInputs` (possibly to both). 18 | -------------------------------------------------------------------------------- /tests/attribute-ordering/properly-ordered.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | , fetchurl 4 | }: 5 | 6 | stdenv.mkDerivation rec { 7 | pname = "properly-ordered"; 8 | version = "0.0.0"; 9 | 10 | outputs = [ "out" ]; 11 | 12 | src = ../fixtures/make; 13 | 14 | patches = [ 15 | ]; 16 | 17 | nativeBuildInputs = [ 18 | ]; 19 | 20 | buildInputs = [ 21 | ]; 22 | 23 | propagatedNativeBuildInputs = [ 24 | ]; 25 | 26 | propagatedBuildInputs = [ 27 | ]; 28 | 29 | nativeCheckInputs = [ 30 | ]; 31 | 32 | checkInputs = [ 33 | ]; 34 | 35 | installCheckInputs = [ 36 | ]; 37 | 38 | configureFlags = [ 39 | ]; 40 | 41 | makeFlags = [ 42 | ]; 43 | 44 | buildFlags = [ 45 | ]; 46 | 47 | installFlags = [ 48 | ]; 49 | 50 | env = { 51 | FONTCONFIG_FILE = ""; 52 | NIX_CFLAGS_COMPILE = ""; 53 | }; 54 | 55 | doCheck = true; 56 | doInstallCheck = true; 57 | 58 | dontBuild = true; 59 | 60 | preUnpack = ""; 61 | unpackPhase = null; 62 | postUnpack = ""; 63 | prePatch = ""; 64 | patchPhase = null; 65 | postPatch = ""; 66 | preConfigure = ""; 67 | configurePhase = null; 68 | postConfigure = ""; 69 | preBuild = ""; 70 | buildPhase = null; 71 | postBuild = ""; 72 | preCheck = ""; 73 | checkPhase = null; 74 | postCheck = ""; 75 | preInstall = ""; 76 | installPhase = null; 77 | postInstall = ""; 78 | preFixup = ""; 79 | fixupOutput = ""; 80 | fixupPhase = null; 81 | postFixup = ""; 82 | preInstallCheck = ""; 83 | installCheckPhase = null; 84 | postInstallCheck = ""; 85 | 86 | passthru = { }; 87 | 88 | meta = with lib; { 89 | description = ""; 90 | license = licenses.mit; 91 | platforms = platforms.unix; 92 | maintainers = []; 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /explanations/no-flags-spaces.md: -------------------------------------------------------------------------------- 1 | `buildFlags` and other similar attributes are passed around unquoted in stdenv. This means Bash will split them along [`$IFS`](https://tldp.org/LDP/abs/html/internalvariables.html#IFSREF) when given as arguments to a program or when iterated over in a loop. The downside is that you cannot pass flags containing spaces to stdenv using these variables. 2 | 3 | Bash also does not really interpret the values of variables passed to derivations so it does not matter if you include quotes, apostrophes, backslashes, dollar signs or any other special characters – you will not be able to sidestep this splitting behaviour. 4 | 5 | To fix this, add the flags to `buildFlagsArray` in one of the Bash hooks (e.g. in `preBuild`). Unfortunately, `*FlagsArray` [cannot be used](./no-flags-array.md) as a `mkDerivation` argument in Nix. 6 | 7 | ## Examples 8 | 9 | ### Before 10 | 11 | ```nix 12 | stdenv.mkDerivation { 13 | … 14 | 15 | makeFlags = [ 16 | "RELEASE=August 2020" 17 | ]; 18 | } 19 | ``` 20 | 21 | ### After 22 | 23 | ```nix 24 | stdenv.mkDerivation { 25 | … 26 | 27 | preBuild = '' 28 | makeFlagsArray+=( 29 | "RELEASE=August 2020" 30 | ) 31 | ''; 32 | } 33 | ``` 34 | 35 | ## Exceptions 36 | 37 | If the list item in question are actually multiple arguments: 38 | 39 | ```nix 40 | stdenv.mkDerivation { 41 | … 42 | 43 | configureFlags = [ 44 | "--with-foo bar" 45 | ]; 46 | } 47 | ``` 48 | 49 | you should make the division explicit: 50 | 51 | ```nix 52 | configureFlags = [ 53 | "--with-foo" "bar" 54 | ]; 55 | ``` 56 | 57 | or, if they are a key and a value and the script supports it, join them using `=`: 58 | 59 | ```nix 60 | configureFlags = [ 61 | "--with-foo=bar" 62 | ]; 63 | ``` 64 | -------------------------------------------------------------------------------- /overlays/python-inconsistent-interpreters.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkBuildPythonPackageFor; 5 | 6 | checkDerivation = drvArgs: drv: 7 | let 8 | propagatedBuildInputs = drvArgs.propagatedBuildInputs or []; 9 | checkInputs = drvArgs.checkInputs or []; 10 | runtimeInputs = checkInputs ++ propagatedBuildInputs; 11 | inputPythonInterpreters = map (p: p.pythonModule) (builtins.filter (p: p ? pythonModule) runtimeInputs); 12 | allPythonInterpreters = lib.unique (inputPythonInterpreters ++ [ drv.pythonModule ]); 13 | in 14 | lib.singleton { 15 | name = "python-inconsistent-interpreters"; 16 | cond = builtins.length allPythonInterpreters > 1; 17 | msg = 18 | let 19 | allPythonNames = map (p: p.name) allPythonInterpreters; 20 | sources = [ 21 | "buildPythonPackage" 22 | ] ++ lib.optionals (builtins.any (p: p.pythonModule or drv.pythonModule != drv.pythonModule) propagatedBuildInputs) [ 23 | "propagatedBuildInputs" 24 | ] ++ lib.optionals (builtins.any (p: p.pythonModule or drv.pythonModule != drv.pythonModule) checkInputs) [ 25 | "checkInputs" 26 | ]; 27 | in '' 28 | Between ${lib.concatStringsSep " and " sources}, this derivation seems 29 | to be simultaneously trying to use packages from multiple different Python package sets. 30 | Mixing package sets like this is not supported. 31 | 32 | Detected Python packages: 33 | 34 | ${lib.concatMapStringsSep "\n" (p: "- ${p}") allPythonNames} 35 | ''; 36 | }; 37 | in 38 | checkBuildPythonPackageFor checkDerivation final prev 39 | -------------------------------------------------------------------------------- /explanations/missing-patch-comment.md: -------------------------------------------------------------------------------- 1 | Each patch in nixpkgs applied to the upstream source should be documented. Out-of-tree patches, fetched using `fetchpatch`, are preferred. 2 | 3 | In each patch comment, please explain the purpose of the patch and link to the relevant upstream issue if possible. If the patch has been merged upstream but is not yet part of the released version, please note the version number or date in the comment such that a future maintainer updating the nix expression will know whether the patch has been incorporated upstream and can thus be removed from nixpkgs. 4 | 5 | Furthermore, please use a _stable_ URL for the patch. Rather than, for example, linking to a GitHub pull request of the form `https://github.com/owner/repo/pull/pr_number.patch`, which would change every time a commit is added or the PR is force-pushed, link to a specific commit patch in the form `https://github.com/owner/repo/commit/sha.patch`. 6 | 7 | Here are two good examples of patch comments: 8 | 9 | ```nix 10 | mkDerivation { 11 | … 12 | 13 | patches = [ 14 | # Ensure RStudio compiles against R 4.0.0. 15 | # Should be removed next 1.2.X RStudio update or possibly 1.3.X. 16 | (fetchpatch { 17 | url = "https://github.com/rstudio/rstudio/commit/3fb2397c2f208bb8ace0bbaf269481ccb96b5b20.patch"; 18 | sha256 = "0qpgjy6aash0fc0xbns42cwpj3nsw49nkbzwyq8az01xwg81g0f3"; 19 | }) 20 | ]; 21 | } 22 | ``` 23 | 24 | ```nix 25 | mkDerivation { 26 | … 27 | 28 | patches = [ 29 | # Allow building with bison 3.7 30 | # PR at https://github.com/GoldenCheetah/GoldenCheetah/pull/3590 31 | (fetchpatch { 32 | url = "https://github.com/GoldenCheetah/GoldenCheetah/commit/e1f42f8b3340eb4695ad73be764332e75b7bce90.patch"; 33 | sha256 = "1h0y9vfji5jngqcpzxna5nnawxs77i1lrj44w8a72j0ah0sznivb"; 34 | }) 35 | ]; 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /explanations/environment-variables-go-to-env.md: -------------------------------------------------------------------------------- 1 | Environment variables should go inside `env` attribute instead of being passed directly to `stdenv.mkDerivation`. 2 | 3 | Note that an environment variable can only contain a string. While attributes containing lists passed to `mkDerivation` would end up being concatenated, it should not be relied on as it can lead to confusion (e.g. [expecting certain tokenization](no-flags-spaces.md)). To avoid this, `mkDerivation` will only allow values that can be safely stringified in the `env` attribute (i.e. no lists). 4 | 5 | ## Example 6 | 7 | ### Before 8 | 9 | ```nix 10 | stdenv.mkDerivation { 11 | … 12 | 13 | NIX_CFLAGS_COMPILE = "-lm -ld"; 14 | } 15 | ``` 16 | 17 | ### After 18 | 19 | ```nix 20 | stdenv.mkDerivation { 21 | … 22 | 23 | env = { 24 | NIX_CFLAGS_COMPILE = "-lm -ld"; 25 | }; 26 | } 27 | ``` 28 | 29 | ## Why this change? 30 | 31 | By default, the attributes in the attribute set passed to [`derivation` function](https://nixos.org/manual/nix/stable/language/derivations.html) are turned into environment variables. That is still the primary method of passing data to the builder today but since environment variables can only contain strings, this limits the usable data types. Additionally, since environment variables tend to be inherited by child processes, the environment of various build tools would be contaminated by control variables of the builder. 32 | 33 | To allow passing data other than strings to the builder [`__structuredAttrs`](https://nixos.mayflower.consulting/blog/2020/01/20/structured-attrs/) feature was introduced to Nix. It serializes the attributes passed to `derivation` into a JSON file and places it into the build directory. If a builder wants to allow customizing the environment in derivations with `__structuredAttrs`, it needs to designate a facility for that. The generic builder of Nixpkgs’s `stdenv.mkDerivation` allows that through the `env` attribute. 34 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::Write as _, 3 | process::{Command, Stdio}, 4 | }; 5 | 6 | pub(crate) fn optional_env(name: &str) -> Result, String> { 7 | match std::env::var(name) { 8 | Ok(value) => return Ok(Some(value)), 9 | Err(std::env::VarError::NotPresent) => Ok(None), 10 | Err(err) => { 11 | return Err(format!( 12 | "Unable to decode ‘{name}’ environment variable: {err}" 13 | )) 14 | } 15 | } 16 | } 17 | 18 | pub(crate) fn run_command_with_input( 19 | program: &str, 20 | args: &[&str], 21 | input: &str, 22 | ) -> Result { 23 | let mut child = Command::new(program) 24 | .args(args) 25 | .stdin(Stdio::piped()) 26 | .stdout(Stdio::piped()) 27 | .spawn() 28 | .map_err(|err| format!("Unable to spawn program ‘{program}’: {}", err.to_string()))?; 29 | 30 | { 31 | let Some(mut child_stdin) = child.stdin.take() else { 32 | return Err(format!("Unable to acquire stdin handle of ‘{program}’")); 33 | }; 34 | 35 | child_stdin.write_all(input.as_bytes()).map_err(|err| { 36 | format!( 37 | "Unable to write to stdin of ‘{program}’: {}", 38 | err.to_string() 39 | ) 40 | })?; 41 | } 42 | 43 | let output = child 44 | .wait_with_output() 45 | .map_err(|err| format!("Unable to wait on ‘{program}’: {}", err.to_string()))?; 46 | 47 | let stdout = String::from_utf8(output.stdout).map_err(|err| { 48 | format!( 49 | "Unable to parse output of ‘{program}’ as UTF-8: {}", 50 | err.to_string() 51 | ) 52 | })?; 53 | 54 | Ok(stdout) 55 | } 56 | 57 | pub(crate) fn indent(text: String, steps: usize) -> String { 58 | text.split("\n") 59 | .map(|line| " ".repeat(4 * steps * (!line.is_empty() as usize)) + line) 60 | .collect::>() 61 | .join("\n") 62 | } 63 | -------------------------------------------------------------------------------- /explanations/no-flags-array.md: -------------------------------------------------------------------------------- 1 | Passing `buildFlagsArray` or other similar attributes as an argument to `stdenv.mkDerivation` does not do what you would expect it to. 2 | 3 | If you pass an attribute containing a list of strings to `mkDerivation`, Nix will intercalate the elements with spaces and pass the whole list to the builder as a single string. The builder will then only be able to extract a single item from `"${buildFlagsArray[@]}"`, containing all elements of the list concatenated together. 4 | 5 | Bash also does not really interpret the values of variables passed to derivations so it does not matter if you include quotes, apostrophes, backslashes, dollar signs or any other special characters – they will be passed to the Bash `buildFlagsArray` variable literally. 6 | 7 | It does sort of work as expected if you only pass it a single flag (or multiple list items that will be concatenated together contrary to the misleading quoting) but you should not rely on that since another person might want to add more flags and then will end up confused why it does not work. 8 | 9 | I recommend using `buildFlags` in Nix if you are certain your list items do not contain spaces. If any of them may contain spaces, you will have to resort to setting `buildFlagsArray` in Bash (e.g. in `preBuild`). Unfortunately, we cannot really do any better until we switch to [`__structuredAttrs`](https://github.com/NixOS/nixpkgs/pull/72074). 10 | 11 | ## Example 12 | 13 | ### Before 14 | 15 | ```nix 16 | stdenv.mkDerivation { 17 | … 18 | 19 | makeFlagsArray = [ 20 | "PREFIX=${placeholder "out"}" 21 | "LIBDIR=${placeholder "out"}/lib" 22 | ]; 23 | } 24 | ``` 25 | 26 | This will result in a the contents of the array being passed to make as a single argument: `make "PREFIX=$out LIBDIR=$out/lib"`. 27 | 28 | ### After 29 | 30 | ```nix 31 | stdenv.mkDerivation { 32 | … 33 | 34 | preBuild = '' 35 | makeFlagsArray+=( 36 | "PREFIX=${placeholder "out"}" 37 | "LIBDIR=${placeholder "out"}/lib" 38 | ) 39 | ''; 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /explanations/unclear-gpl.md: -------------------------------------------------------------------------------- 1 | The `lib.licenses.gpl3` attribute refers to GPL 3.0 only. Unfortunately, it is not clear from the attribute name so people often use that without realizing `lib.licenses.gpl3Plus` might be more precise. 2 | 3 | To avoid this confusion, the unqualified GNU licenses were deprecated, in line with the [GNU recommendations](https://gnu.org/licenses/identify-licenses-clearly.html). 4 | 5 | ## What to do? 6 | 7 | Replace the deprecated unqualified in `meta.license` as follows: 8 | 9 | - `gpl2` by either `gpl2Plus` or `gpl2Only` 10 | - `gpl3` by either `gpl3Plus` or `gpl3Only` 11 | - `lgpl2` by either `lgpl2Plus` or `lgpl2Only` 12 | - `lgpl21` by either `lgpl21Plus` or `lgpl21Only` 13 | - `lgpl3` by either `lgpl3Plus` or `lgpl3Only` 14 | 15 | ## How to determine license 16 | 17 | Projects might mention license terms in the `README` file or on their homepage. 18 | 19 | If that is not the case, check few source files. They might contain a blurb in the comment at the top of a file, similar to the following: 20 | 21 | > This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, **or (at your option) any later version**. 22 | 23 | You can also try to grep (or search on GitHub) the repository for `license`. 24 | 25 | **Do not** rely on the contents of `COPYING` file or the license shown by the GitHub/GitLab interface (which is determined from the `COPYING` file) – the file only contains the text of GNU ?GPL itself, as mandated by the license. The extra terms allowing to use later versions of the license would be stored in the source code/documentation. 26 | 27 | If no statement about license terms is found, you should ask the project maintainers to clarify. 28 | 29 | ## See also 30 | 31 | - [Pull request that deprecated the unqualified license](https://github.com/NixOS/nixpkgs/pull/92348) 32 | - [Relevant Discourse thread](https://discourse.nixos.org/t/lib-licenses-gpl3-co-are-now-deprecated/8206) 33 | -------------------------------------------------------------------------------- /explanations/missing-phase-hooks.md: -------------------------------------------------------------------------------- 1 | If you are overriding `configurePhase`, `buildPhase`, `checkPhase`, `installPhase` or any other phase, you should not forget about explicitly running pre and post-phase hooks like [their](https://github.com/NixOS/nixpkgs/blob/d71a03ad695407dd482ead32d3eddff50092a1c3/pkgs/stdenv/generic/setup.sh#L953) [original](https://github.com/NixOS/nixpkgs/blob/d71a03ad695407dd482ead32d3eddff50092a1c3/pkgs/stdenv/generic/setup.sh#L1008) [definitions](https://github.com/NixOS/nixpkgs/blob/d71a03ad695407dd482ead32d3eddff50092a1c3/pkgs/stdenv/generic/setup.sh#L1037) [do](https://github.com/NixOS/nixpkgs/blob/d71a03ad695407dd482ead32d3eddff50092a1c3/pkgs/stdenv/generic/setup.sh#L1078). 2 | 3 | It is generally expected that an appropriate pre-phase hook (e.g. `preBuild`) hook will run at the beginning of a phase (e.g. `buildPhase`) and post-phase hook (e.g. `postBuild`) will run at the end. Hooks are normally ran as a part of a phase so if you override a phase as a whole, you will need to add `runHook hookName` calls (e.g. `runHook preBuild`) manually. 4 | 5 | Having phases run pre/post-phase hooks is important because many setup hooks insert their own code into them – omitting a hook might therefore prevent some setup hooks required for proper functionality of a package from running. Additionally, hooks are often inserted by developers into the package expression and by users when overriding a package using `overrideAttrs`. Not running them can thus cause confusion why their code is not executed. 6 | 7 | ## Examples 8 | 9 | ### Before 10 | 11 | ```nix 12 | installPhase = '' 13 | your commands 14 | ''; 15 | ``` 16 | 17 | ### After 18 | 19 | ```nix 20 | installPhase = '' 21 | runHook preInstall 22 | 23 | your commands 24 | 25 | runHook postInstall 26 | ''; 27 | ``` 28 | 29 | ## Alternatives 30 | 31 | And if you just want to add a flag to `make` call, you might not even need to override the phases, see [`explicit-phases`](explicit-phases.md) rule. 32 | 33 | ## See also 34 | 35 | - [Relevant Nixpkgs issue](https://github.com/NixOS/nixpkgs/issues/175605) 36 | -------------------------------------------------------------------------------- /rust-checks/src/tree_utils.rs: -------------------------------------------------------------------------------- 1 | use rnix::{ast::*, SyntaxElement, SyntaxKind, SyntaxKind::*, SyntaxNode, WalkEvent}; 2 | use rowan::ast::AstNode as _; 3 | use std::iter::{once, successors}; 4 | 5 | pub fn walk_kind(node: &SyntaxNode, kind: SyntaxKind) -> impl Iterator { 6 | // Iterates over all AST nodes under `node` with the specified `kind`. 7 | node.preorder_with_tokens() 8 | .filter_map(move |event| match event { 9 | WalkEvent::Enter(element) => Some(element).filter(|it| it.kind() == kind), 10 | WalkEvent::Leave(_) => None, 11 | }) 12 | } 13 | 14 | pub fn walk_attrpath_values_filter_attrpath<'a>( 15 | node: &SyntaxNode, 16 | key: &'a str, 17 | ) -> impl Iterator + 'a { 18 | // Iterates over all AST nodes under `node` that are of kind `NODE_KEY_VALUE`, 19 | // (which corresponds to a key-value pair inside a nix attrset) and yields 20 | // only the nodes where the key is equal to `key`. 21 | walk_kind(node, NODE_ATTRPATH_VALUE) 22 | .filter_map(|element| element.into_node()) 23 | .filter_map(|node| AttrpathValue::cast(node)) 24 | .filter(move |pair| { 25 | pair.attrpath() 26 | .map_or(false, |e| e.syntax().to_string() == key.to_string()) 27 | }) 28 | } 29 | 30 | pub fn next_siblings(element: &SyntaxElement) -> impl Iterator { 31 | // Iterates over both `element` and its sibling elements that come after it in the AST. 32 | once(element.clone()).chain(successors(element.next_sibling_or_token(), |it| { 33 | it.next_sibling_or_token() 34 | })) 35 | } 36 | 37 | pub fn prev_siblings(element: &SyntaxElement) -> impl Iterator { 38 | // Iterate over both `element` and its sibling elements that come before it in the AST. 39 | once(element.clone()).chain(successors(element.prev_sibling_or_token(), |it| { 40 | it.prev_sibling_or_token() 41 | })) 42 | } 43 | 44 | pub fn parents(node: SyntaxNode) -> impl Iterator { 45 | std::iter::successors(Some(node), |node| node.parent()) 46 | } 47 | -------------------------------------------------------------------------------- /explanations/explicit-phases.md: -------------------------------------------------------------------------------- 1 | If you just want to override some parameters of the build process, you might not need to override the phases. The default [generic builder](https://github.com/NixOS/nixpkgs/blob/b7ce309e6c6fbad584df85d9fd62c5185153e8f9/pkgs/stdenv/generic/setup.sh#L952-L1098) is quite flexible and allows you to control many aspects of the build using [variables](https://nixos.org/nixpkgs/manual/#ssec-controlling-phases). 2 | 3 | ## Why? 4 | 5 | - It is an idiom in Nixpkgs. 6 | - The default phases do lot of stuff behind the scenes and you would need to do that too, if you do not want for things to break. 7 | - It saves you typing. 8 | 9 | ## Why not? 10 | 11 | - It is an abstraction so general arguments against abstractions hold. 12 | 13 | ## When to use 14 | 15 | Whenever the project you are packaging is using Autotools/make-based build tool. Or even other build tools – packages for Meson, CMake and many other build systems include setup hooks that extend the generic builders with their specific build steps. 16 | 17 | ## Examples 18 | 19 | ### Before 20 | 21 | ```nix 22 | configurePhase = '' 23 | NOCONFIGURE=1 ./autogen.sh 24 | ./configure --prefix=$out --with-udevdir=$out/lib/udev --sysconfdir=/etc 25 | ''; 26 | 27 | buildPhase = '' 28 | make all docs INTROSPECTION_GIRDIR=$out/share/gir-1.0 29 | ''; 30 | 31 | installPhase = '' 32 | make install install-docs sysconfdir=$out/etc INTROSPECTION_GIRDIR=$out/share/gir-1.0 33 | ''; 34 | ``` 35 | 36 | ### After 37 | 38 | ```nix 39 | configureFlags = [ 40 | # --prefix is set by default: https://github.com/NixOS/nixpkgs/blob/b7ce309e6c6fbad584df85d9fd62c5185153e8f9/pkgs/stdenv/generic/setup.sh#L972 41 | "--with-udevdir=${placeholder "out"}/lib/udev" 42 | "--sysconfdir=/etc" 43 | ]; 44 | 45 | makeFlags = [ 46 | "INTROSPECTION_GIRDIR=${placeholder "out"}/share/gir-1.0" 47 | ]; 48 | 49 | buildFlags = [ "all" "docs" ]; 50 | 51 | installTargets = [ "install" "install-docs" ]; 52 | 53 | installFlags = [ 54 | "sysconfdir=${placeholder "out"}/etc" 55 | ]; 56 | 57 | preConfigure = '' 58 | NOCONFIGURE=1 ./autogen.sh 59 | ''; 60 | ``` 61 | -------------------------------------------------------------------------------- /rust-checks/src/comment_finders.rs: -------------------------------------------------------------------------------- 1 | use crate::tree_utils::{next_siblings, prev_siblings, walk_kind}; 2 | use rnix::{NodeOrToken, SyntaxElement, SyntaxKind::*, SyntaxNode}; 3 | 4 | pub fn find_comment_within(node: &SyntaxNode) -> Option { 5 | // Find the first `TOKEN_COMMENT` within. 6 | let tok = walk_kind(node, TOKEN_COMMENT).next()?; 7 | // Iterate over sibling comments and concatenate them. 8 | let node_iter = next_siblings(&tok); 9 | let doc = collect_comment_text(node_iter, true); 10 | Some(doc).filter(|it| !it.is_empty()) 11 | } 12 | 13 | pub fn find_comment_above(node: &SyntaxNode) -> Option { 14 | // Note: The `prev_siblings` iterator includes self, which 15 | // is not a `TOKEN_COMMENT`, so we need to skip one. 16 | let node_iter = prev_siblings(&NodeOrToken::Node(node.clone())).skip(1); 17 | let doc = collect_comment_text(node_iter, true); 18 | Some(doc).filter(|it| !it.is_empty()) 19 | } 20 | 21 | pub fn find_comment_after(node: &SyntaxNode) -> Option { 22 | let node_iter = next_siblings(&NodeOrToken::Node(node.clone())).skip(1); 23 | let doc = collect_comment_text(node_iter, false); 24 | Some(doc).filter(|it| !it.is_empty()) 25 | } 26 | 27 | fn collect_comment_text( 28 | node_iter: impl Iterator, 29 | allow_newline: bool, 30 | ) -> String { 31 | // Take text of all immediately subsequent `TOKEN_COMMENT`s, 32 | // skipping over whitespace-only tokens. 33 | // Note this would be more clearly written using `map_while`, but that 34 | // does not seem to be in Rust stable yet. 35 | // If allow_newline = false, we skip over only non-newline containing 36 | // whitespace tokens 37 | node_iter 38 | .filter_map(|node| node.into_token()) 39 | .take_while(|tok| { 40 | tok.kind() == TOKEN_COMMENT 41 | || (tok.kind().is_trivia() && (allow_newline || (!tok.text().contains("\n")))) 42 | }) 43 | .map(|tok| tok.text().to_string()) 44 | .map(|s| s.trim_start_matches('#').trim().to_string()) 45 | .filter(|s| !s.is_empty()) 46 | .collect::>() 47 | .join("\n") 48 | } 49 | -------------------------------------------------------------------------------- /lib/derivation-attributes-unordered.nix: -------------------------------------------------------------------------------- 1 | [ 2 | # TODO: move some of these into their proper place in derivation-attributes.nix 3 | 4 | # Qt 5 | "qtInputs" 6 | 7 | # Undocumented stdenv 8 | "preHook" 9 | "strictDeps" 10 | "hardeningDisable" 11 | "hardeningEnable" 12 | "dontAutoPatchelf" 13 | 14 | # Taken from stdenv.xml 15 | "depsBuildBuild" 16 | "depsBuildTarget" 17 | "depsHostHost" 18 | "depsTargetTarget" 19 | "depsBuildBuildPropagated" 20 | "depsBuildTargetPropagated" 21 | "depsHostHostPropagated" 22 | "depsTargetTargetPropagated" 23 | "NIX_DEBUG" 24 | "phases" 25 | "prePhases" 26 | "preConfigurePhases" 27 | "preBuildPhases" 28 | "preInstallPhases" 29 | "preFixupPhases" 30 | "preDistPhases" 31 | "postPhases" 32 | "sourceRoot" 33 | "setSourceRoot" 34 | "dontMakeSourcesWritable" 35 | "unpackCmd" 36 | "patchFlags" 37 | "configureScript" 38 | "configureFlagsArray" 39 | "dontAddPrefix" 40 | "prefix" 41 | "prefixKey" 42 | "dontAddDisableDepTrack" 43 | "dontFixLibtool" 44 | "dontDisableStatic" 45 | "configurePlatforms" 46 | "makefile" 47 | "makeFlagsArray" 48 | "installTargets" 49 | "dontStrip" 50 | "dontStripHost" 51 | "dontStripTarget" 52 | "dontMoveSbin" 53 | "stripAllList" 54 | "stripAllFlags" 55 | "stripDebugList" 56 | "stripDebugFlags" 57 | "dontPatchELF" 58 | "dontPatchShebangs" 59 | "dontPruneLibtoolFiles" 60 | "forceShare" 61 | "setupHook" 62 | "installCheckTarget" 63 | "distTarget" 64 | "distFlags" 65 | "tarballs" 66 | "dontCopyDist" 67 | "preDist" 68 | "postDist" 69 | 70 | # Multiple outputs 71 | "outputDev" 72 | "outputBin" 73 | "outputLib" 74 | "outputDoc" 75 | "outputDevdoc" 76 | "outputMan" 77 | "outputDevman" 78 | "outputInfo" 79 | 80 | # Nix derivation 81 | # https://nixos.org/manual/nix/unstable/expressions/advanced-attributes.html 82 | "allowedReferences" 83 | "allowedRequisites" 84 | "disallowedReferences" 85 | "disallowedRequisites" 86 | "exportReferencesGraph" 87 | "impureEnvVars" 88 | "outputHash" 89 | "outputHashAlgo" 90 | "outputHashMode" 91 | "passAsFile" 92 | "preferLocalBuild" 93 | "allowSubstitutes" 94 | ] 95 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use nixpkgs_hammering::{hammer, Config}; 3 | use std::{collections::HashSet, path::PathBuf}; 4 | 5 | /// Check package expressions for common mistakes. 6 | #[derive(Parser, Debug)] 7 | struct Args { 8 | /// Evaluate attributes in given path. 9 | /// 10 | /// The path needs to be importable by Nix and the imported value 11 | /// has to accept attribute set with overlays attribute as an argument. 12 | /// 13 | /// Defaults to current working directory. 14 | #[arg(long, short = 'f', value_name = "FILE")] 15 | file: Option, 16 | 17 | /// Show trace when error occurs. 18 | #[arg(long, default_value_t = false)] 19 | show_trace: bool, 20 | 21 | /// Avoid catching evaluation errors (for debugging). 22 | #[arg(long, default_value_t = false)] 23 | do_not_catch_errors: bool, 24 | 25 | /// Output results as JSON. 26 | #[arg(long, default_value_t = false)] 27 | json: bool, 28 | 29 | /// Rule to exclude (can be passed repeatedly). 30 | #[arg(long, short = 'e', value_name = "rule")] 31 | excluded: Vec, 32 | 33 | /// Attribute path of package to check. 34 | #[arg(value_name = "attr-path", required = true)] 35 | attr_paths: Vec, 36 | } 37 | 38 | fn main() -> Result<(), String> { 39 | let Args { 40 | file, 41 | show_trace, 42 | do_not_catch_errors, 43 | json, 44 | excluded, 45 | attr_paths, 46 | } = Args::parse(); 47 | 48 | // Absolutize so we can refer to it from Nix. 49 | let nix_file = if let Some(file) = file { 50 | file.canonicalize().map_err(|err| { 51 | format!( 52 | "Unable to resolve path to Nix expression: {}", 53 | err.to_string() 54 | ) 55 | })? 56 | } else { 57 | std::env::current_dir().map_err(|err| { 58 | format!( 59 | "Unable to determine current working directory: {}", 60 | err.to_string() 61 | ) 62 | })? 63 | }; 64 | 65 | hammer(Config { 66 | nix_file, 67 | show_trace, 68 | do_not_catch_errors, 69 | json, 70 | excluded_rules: HashSet::from_iter(excluded), 71 | attr_paths, 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1733328505, 7 | "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "nixpkgs": { 20 | "locked": { 21 | "lastModified": 1741678040, 22 | "narHash": "sha256-rmBsz7BBcDwfvDkxnKHmolKceGJrr0nyz5PQYZg0kMk=", 23 | "owner": "NixOS", 24 | "repo": "nixpkgs", 25 | "rev": "3ee8818da146871cd570b164fc4f438f78479a50", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "NixOS", 30 | "ref": "nixpkgs-unstable", 31 | "repo": "nixpkgs", 32 | "type": "github" 33 | } 34 | }, 35 | "root": { 36 | "inputs": { 37 | "flake-compat": "flake-compat", 38 | "nixpkgs": "nixpkgs", 39 | "utils": "utils" 40 | } 41 | }, 42 | "systems": { 43 | "locked": { 44 | "lastModified": 1681028828, 45 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 46 | "owner": "nix-systems", 47 | "repo": "default", 48 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "nix-systems", 53 | "repo": "default", 54 | "type": "github" 55 | } 56 | }, 57 | "utils": { 58 | "inputs": { 59 | "systems": "systems" 60 | }, 61 | "locked": { 62 | "lastModified": 1731533236, 63 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 64 | "owner": "numtide", 65 | "repo": "flake-utils", 66 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "numtide", 71 | "repo": "flake-utils", 72 | "type": "github" 73 | } 74 | } 75 | }, 76 | "root": "root", 77 | "version": 7 78 | } 79 | -------------------------------------------------------------------------------- /overlays/attribute-ordering.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkFor; 5 | 6 | preferredOrdering = 7 | let 8 | flattenGroup = item: 9 | if builtins.isAttrs item then 10 | map (subitem: { group = item.name; attr = subitem; }) item.values 11 | else 12 | [ { attr = item; } ]; 13 | addOrder = n: i: i // { order = n; }; 14 | in 15 | lib.pipe (import ../lib/derivation-attributes.nix) [ 16 | (builtins.concatMap flattenGroup) 17 | (lib.imap0 addOrder) 18 | (builtins.map ({ attr, order, group ? null }@attrs: { 19 | name = attr; 20 | value = { inherit order group; }; 21 | })) 22 | builtins.listToAttrs 23 | ]; 24 | 25 | checkDerivation = drvArgs: drv: 26 | let 27 | getAttrPos = attr: (builtins.unsafeGetAttrPos attr drvArgs); 28 | 29 | drvAttrs = lib.pipe drvArgs [ 30 | builtins.attrNames 31 | # Certain functions like mapAttrs can produce attributes without associated position. 32 | (builtins.filter (attr: getAttrPos attr != null)) 33 | (builtins.sort (aName: bName: let a = getAttrPos aName; b = getAttrPos bName; in a.line < b.line || (a.line == b.line && a.column < b.column))) 34 | ]; 35 | 36 | knownDrvAttrs = builtins.filter (attr: preferredOrdering ? "${attr}") drvAttrs; 37 | 38 | sideBySide = lib.zipLists knownDrvAttrs (builtins.tail knownDrvAttrs); 39 | in 40 | lib.forEach sideBySide ({ fst, snd }: 41 | let 42 | fstInfo = preferredOrdering.${fst}; 43 | sndInfo = preferredOrdering.${snd}; 44 | sameGroup = fstInfo.group == sndInfo.group; 45 | in { 46 | name = "attribute-ordering"; 47 | cond = fstInfo.order > sndInfo.order; 48 | msg = '' 49 | The ${lib.optionalString (sndInfo.group != null && !sameGroup) "${sndInfo.group}, including the "}attribute “${snd}” should preferably come before ${lib.optionalString (fstInfo.group != null && !sameGroup) "${fstInfo.group}’ "}“${fst}” attribute ${lib.optionalString (sndInfo.group != null && sameGroup) "among ${sndInfo.group} "}in the expression. 50 | ''; 51 | locations = [ 52 | (builtins.unsafeGetAttrPos fst drvArgs) 53 | (builtins.unsafeGetAttrPos snd drvArgs) 54 | ]; 55 | } 56 | ); 57 | 58 | in 59 | checkFor checkDerivation final prev 60 | -------------------------------------------------------------------------------- /tests/default.nix: -------------------------------------------------------------------------------- 1 | { overlays ? [] 2 | , pkgs ? import (import ../default.nix).inputs.nixpkgs.outPath { inherit overlays; } 3 | }: 4 | 5 | { 6 | attribute-ordering = pkgs.recurseIntoAttrs (pkgs.callPackage ./attribute-ordering { }); 7 | attribute-typo = pkgs.recurseIntoAttrs (pkgs.callPackage ./attribute-typo { }); 8 | build-tools-in-build-inputs = pkgs.recurseIntoAttrs (pkgs.callPackage ./build-tools-in-build-inputs { }); 9 | duplicate-check-inputs = pkgs.python3.pkgs.callPackage ./duplicate-check-inputs { }; 10 | environment-variables-go-to-env = pkgs.recurseIntoAttrs (pkgs.callPackage ./environment-variables-go-to-env { }); 11 | EvalError = pkgs.recurseIntoAttrs (pkgs.callPackage ./EvalError { }); 12 | explicit-phases = pkgs.recurseIntoAttrs (pkgs.callPackage ./explicit-phases { }); 13 | fixup-phase = pkgs.callPackage ./fixup-phase { }; 14 | license-missing = pkgs.recurseIntoAttrs (pkgs.callPackage ./license-missing { }); 15 | maintainers-missing = pkgs.recurseIntoAttrs (pkgs.callPackage ./maintainers-missing { }); 16 | meson-cmake = pkgs.callPackage ./meson-cmake { }; 17 | missing-patch-comment = pkgs.recurseIntoAttrs (pkgs.callPackage ./missing-patch-comment { }); 18 | missing-phase-hooks = pkgs.recurseIntoAttrs (pkgs.callPackage ./missing-phase-hooks { }); 19 | name-and-version = pkgs.recurseIntoAttrs (pkgs.callPackage ./name-and-version { }); 20 | no-flags-array = pkgs.recurseIntoAttrs (pkgs.callPackage ./no-flags-array { }); 21 | no-flags-spaces = pkgs.recurseIntoAttrs (pkgs.callPackage ./no-flags-spaces { }); 22 | no-uri-literals = pkgs.recurseIntoAttrs (pkgs.callPackage ./no-uri-literals { }); 23 | patch-phase = pkgs.callPackage ./patch-phase { }; 24 | python-explicit-check-phase = pkgs.recurseIntoAttrs (pkgs.callPackage ./python-explicit-check-phase { }); 25 | python-imports-check-typo = pkgs.recurseIntoAttrs (pkgs.callPackage ./python-imports-check-typo { }); 26 | python-include-tests = pkgs.recurseIntoAttrs (pkgs.callPackage ./python-include-tests { }); 27 | python-inconsistent-interpreters = pkgs.recurseIntoAttrs (pkgs.callPackage ./python-inconsistent-interpreters { }); 28 | stale-substitute = pkgs.recurseIntoAttrs (pkgs.callPackage ./stale-substitute { }); 29 | unclear-gpl = pkgs.recurseIntoAttrs (pkgs.callPackage ./unclear-gpl { }); 30 | unnecessary-parallel-building = pkgs.recurseIntoAttrs (pkgs.callPackage ./unnecessary-parallel-building { }); 31 | unused-argument = pkgs.recurseIntoAttrs (pkgs.callPackage ./unused-argument {}); 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nixpkgs-hammering 2 | 3 | There are many idioms in nixpkgs which beginner packagers might not be aware of. This is a set of nit-picky rules that aim to point out and explain common mistakes in nixpkgs package pull requests. 4 | 5 | This repository contains a bunch of [overlays](https://nixos.org/nixpkgs/manual/#chap-overlays) that add extra checks to `stdenv.mkDerivation` and other similar nixpkgs tools. The `nixpkgs-hammer` command will try to evaluate the specified attributes with the overlays, making sure the warnings only touch the attributes you care about, not their dependencies. 6 | 7 | *Note that while these rules almost always apply, there are some exceptions. Please read the explanations before taking an action.* 8 | 9 | ## How do I use this? 10 | 11 | Run the following command in your nixpkgs directory when you use classic Nix: 12 | 13 | ``` 14 | nix-shell https://github.com/jtojnar/nixpkgs-hammering/archive/main.tar.gz -A nixpkgs-hammering --run 'nixpkgs-hammer ...' 15 | ``` 16 | 17 | or with Flakes-enabled Nix: 18 | 19 | ``` 20 | nix run github:jtojnar/nixpkgs-hammering ... 21 | ``` 22 | 23 | or when you use unstable Nix but do not have Flakes enabled: 24 | 25 | ``` 26 | nix shell -f https://github.com/jtojnar/nixpkgs-hammering/archive/main.tar.gz -c nixpkgs-hammer ... 27 | ``` 28 | 29 | ## How does this work? 30 | 31 | There are two kinds of checks. 32 | 33 | The first kind can be expressed directly in Nix and they are implemented as Nixpkgs overlays that replace `stdenv.mkDerivation` and other similar functions and attach an attribute containing a report to the resulting derivation’s attribute set. The tool then evaluates the attribute. 34 | 35 | The second are more involved and the tool will execute them individually, passing each of them the attribute paths to check, the corresponding file paths, and (if available) build logs. They are implemented in Rust so they can perform arbitrary validation tasks – for example, parse the code into [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree), read build logs, inspect the build output or even access network. 36 | 37 | ## How do I develop this? 38 | 39 | Run tests: 40 | 41 | ``` 42 | cargo test 43 | ``` 44 | 45 | > **Note** 46 | > The overlays of new checks must be added to the Git index in order for nixpkgs-hammering to discover them. 47 | 48 | ## License 49 | 50 | The code is licensed under [MIT](LICENSE.md), the explanations are licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). 51 | -------------------------------------------------------------------------------- /lib/derivation-attributes.nix: -------------------------------------------------------------------------------- 1 | [ 2 | "name" 3 | "pname" 4 | "version" 5 | 6 | "outputs" 7 | 8 | "src" 9 | "srcs" 10 | "patches" 11 | 12 | { 13 | name = "lists of dependencies"; 14 | values = [ 15 | "nativeBuildInputs" 16 | "buildInputs" 17 | "propagatedNativeBuildInputs" 18 | "propagatedBuildInputs" 19 | "nativeCheckInputs" 20 | "checkInputs" 21 | "installCheckInputs" 22 | ]; 23 | } 24 | 25 | { 26 | name = "build system configuration flags"; 27 | values = [ 28 | "cmakeFlags" 29 | "mesonFlags" 30 | "configureFlags" 31 | "qmakeFlags" 32 | ]; 33 | } 34 | 35 | { 36 | name = "build tool flags"; 37 | values = [ 38 | "makeFlags" 39 | "ninjaFlags" 40 | "buildFlags" 41 | "checkTarget" 42 | "checkFlags" 43 | "installFlags" 44 | "installCheckFlags" 45 | "enableParallelBuilding" 46 | "enableParallelChecking" 47 | ]; 48 | } 49 | 50 | "env" 51 | 52 | { 53 | name = "attributes for enabling off-by-default phases"; 54 | values = [ 55 | "doCheck" 56 | "doInstallCheck" 57 | "doDist" 58 | ]; 59 | } 60 | 61 | { 62 | name = "attributes for disabling on-by-default phases"; 63 | values = [ 64 | "dontUnpack" 65 | "dontPatch" 66 | "dontConfigure" 67 | "dontBuild" 68 | "dontUseNinjaBuild" 69 | "dontInstall" 70 | "dontUseNinjaInstall" 71 | "dontFixup" 72 | "dontUseNinjaCheck" 73 | "dontNpmInstall" 74 | ]; 75 | } 76 | 77 | { 78 | name = "attributes for overriding default phases"; 79 | values = [ 80 | "preUnpack" 81 | "unpackPhase" 82 | "postUnpack" 83 | "prePatch" 84 | "patchPhase" 85 | "postPatch" 86 | # "preConfigurePhases" 87 | "preConfigure" 88 | "configurePhase" 89 | "postConfigure" 90 | # "preBuildPhases" 91 | "preBuild" 92 | "buildPhase" 93 | "postBuild" 94 | "preCheck" 95 | "checkPhase" 96 | "postCheck" 97 | # "preInstallPhases" 98 | "preInstall" 99 | "installPhase" 100 | "postInstall" 101 | # "preFixupPhases" 102 | "preFixup" 103 | "fixupOutput" 104 | "fixupPhase" 105 | "postFixup" 106 | "preInstallCheck" 107 | "installCheckPhase" 108 | "postInstallCheck" 109 | # "preDistPhases" 110 | "distPhase" 111 | # "postPhases" 112 | ]; 113 | } 114 | 115 | "passthru" 116 | "meta" 117 | ] 118 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Tool for pointing out issues in Nixpkgs packages"; 3 | 4 | inputs = { 5 | flake-compat = { 6 | url = "github:edolstra/flake-compat"; 7 | flake = false; 8 | }; 9 | 10 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 11 | 12 | utils.url = "github:numtide/flake-utils"; 13 | }; 14 | 15 | outputs = { self, flake-compat, nixpkgs, utils }: { 16 | overlays = { 17 | default = 18 | final: 19 | prev: 20 | 21 | { 22 | nixpkgs-hammering = 23 | prev.rustPlatform.buildRustPackage { 24 | pname = "nixpkgs-hammering"; 25 | version = (prev.lib.importTOML ./Cargo.toml).package.version; 26 | src = ./.; 27 | 28 | cargoLock.lockFile = ./Cargo.lock; 29 | 30 | nativeBuildInputs = with prev; [ 31 | makeWrapper 32 | ]; 33 | 34 | passthru = { 35 | exePath = "/bin/nixpkgs-hammer"; 36 | }; 37 | 38 | # Running tests is slow and requires properly placed overlays. 39 | # We are doing that in CI outside of the derivation. 40 | doCheck = false; 41 | 42 | postInstall = '' 43 | datadir="$out/share/nixpkgs-hammering" 44 | mkdir -p "$datadir" 45 | 46 | wrapProgram "$out/bin/nixpkgs-hammer" \ 47 | --prefix PATH ":" ${prev.lib.makeBinPath [ 48 | prev.nix 49 | ]} \ 50 | --set OVERLAYS_DIR "$datadir/overlays" 51 | cp -r ${./overlays} "$datadir/overlays" 52 | cp -r ${./lib} "$datadir/lib" 53 | ''; 54 | }; 55 | }; 56 | }; 57 | } // utils.lib.eachDefaultSystem (system: let 58 | pkgs = import nixpkgs { inherit system; }; 59 | in { 60 | packages = rec { 61 | default = nixpkgs-hammering; 62 | 63 | nixpkgs-hammering = (self.overlays.default pkgs pkgs).nixpkgs-hammering; 64 | }; 65 | 66 | apps = rec { 67 | nixpkgs-hammer = utils.lib.mkApp { 68 | drv = self.packages.${system}.nixpkgs-hammering; 69 | }; 70 | 71 | default = nixpkgs-hammer; 72 | }; 73 | 74 | devShells = { 75 | default = 76 | pkgs.mkShell { 77 | buildInputs = with pkgs; [ 78 | rustc 79 | cargo 80 | rust-analyzer 81 | rustfmt 82 | ]; 83 | }; 84 | 85 | # For legacy shell.nix 86 | nixpkgs-hammering = 87 | pkgs.mkShell { 88 | buildInputs = with pkgs; [ 89 | self.packages.${system}.nixpkgs-hammering 90 | ]; 91 | }; 92 | }; 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /rust-checks/src/analysis.rs: -------------------------------------------------------------------------------- 1 | use crate::common_structs::{CheckedAttr, NixpkgsHammerMessage}; 2 | use codespan::{FileId, Files}; 3 | use rnix::{Root, SyntaxNode}; 4 | use rowan::ast::AstNode as _; 5 | use std::{ 6 | collections::HashMap, 7 | error::Error, 8 | fs, 9 | io::BufReader, 10 | path::Path, 11 | process::{ChildStdout, Command, Stdio}, 12 | }; 13 | 14 | pub type Report = Vec; 15 | pub type NixFileAnalyzer = fn(&Files, FileId) -> Result>; 16 | pub type LogFileAnalyzer = 17 | fn(BufReader, &CheckedAttr) -> Result>; 18 | 19 | /// Runs given analyzer the nix file for each attr 20 | pub fn analyze_nix_files( 21 | attrs: Vec, 22 | analyzer: NixFileAnalyzer, 23 | ) -> Result> { 24 | let mut report: HashMap = HashMap::new(); 25 | 26 | let mut files = Files::new(); 27 | for attr in attrs.iter().filter(|a| a.location.is_some()) { 28 | let filename = attr.location.as_ref().unwrap().file.clone(); 29 | let file_id = files.add(filename.clone(), fs::read_to_string(&filename)?); 30 | report.insert(attr.name.clone(), analyzer(&files, file_id)?); 31 | } 32 | 33 | Ok(serde_json::to_string(&report)?) 34 | } 35 | 36 | /// Runs given analyzer on log file for each attr, if exists 37 | pub fn analyze_log_files( 38 | attrs: Vec, 39 | analyzer: LogFileAnalyzer, 40 | ) -> Result> { 41 | let mut report: HashMap = HashMap::new(); 42 | 43 | for attr in attrs 44 | .iter() 45 | .filter(|a| a.output.is_some()) 46 | .filter(|a| Path::new(&a.output.as_ref().unwrap()).exists()) 47 | { 48 | let mut program = Command::new("nix") 49 | .args(&[ 50 | "--experimental-features", 51 | "nix-command", 52 | "log", 53 | &attr.output.as_ref().unwrap(), 54 | ]) 55 | .stdout(Stdio::piped()) 56 | .spawn()?; 57 | 58 | let logreader = BufReader::new(program.stdout.take().unwrap()); 59 | let checkresult = analyzer(logreader, &attr)?; 60 | report.insert(attr.name.clone(), checkresult); 61 | } 62 | 63 | Ok(serde_json::to_string(&report)?) 64 | } 65 | 66 | /// Parses a Nix file and returns a root AST node. 67 | pub fn find_root(files: &Files, file_id: FileId) -> Result { 68 | let root = Root::parse(files.source(file_id)).ok().map_err(|_| { 69 | format!( 70 | "Unable to parse {} as a nix file", 71 | files 72 | .name(file_id) 73 | .to_str() 74 | .unwrap_or("unprintable file, encoding error") 75 | ) 76 | })?; 77 | 78 | let expr = root.expr().ok_or(format!( 79 | "No elements in the AST in path {}", 80 | files 81 | .name(file_id) 82 | .to_str() 83 | .unwrap_or("unprintable file, encoding error") 84 | ))?; 85 | 86 | Ok(expr.syntax().clone()) 87 | } 88 | -------------------------------------------------------------------------------- /explanations/attribute-ordering.md: -------------------------------------------------------------------------------- 1 | Many nixpkgs maintainers like having consistent ordering so we can quickly locate things we need to change. This is nothing but a personal preference but [growing stronger](https://discourse.nixos.org/t/document-attribute-ordering-in-package-expressions/4887). 2 | 3 | We start with general package information and sources that change every update, then list dependencies, set up build, potentially override the default builder phases and finally provide metadata. This is sort of high level to low level ordering but not exactly. 4 | 5 | ## Examples 6 | 7 | ```nix 8 | { stdenv 9 | , lib 10 | , fetchurl 11 | }: 12 | 13 | stdenv.mkDerivation rec { 14 | pname = ""; 15 | version = ""; 16 | 17 | # Outputs are also part of store paths. 18 | outputs = [ ]; 19 | 20 | src = fetchurl { 21 | url = ""; 22 | sha256 = ""; 23 | }; 24 | 25 | # Patches often depend on source code and need to be kept in sync. 26 | patches = [ 27 | ]; 28 | 29 | # Dependencies. Build-time dependencies (native) first, then regular, then propagated variants of the two, then dependencies for tests. 30 | nativeBuildInputs = [ 31 | ]; 32 | 33 | buildInputs = [ 34 | ]; 35 | 36 | propagatedNativeBuildInputs = [ 37 | ]; 38 | 39 | propagatedBuildInputs = [ 40 | ]; 41 | 42 | nativeCheckInputs = [ 43 | ]; 44 | 45 | checkInputs = [ 46 | ]; 47 | 48 | installCheckInputs = [ 49 | ]; 50 | 51 | # Build systemd configuration flags. 52 | configureFlags = [ 53 | ]; 54 | # or cmakeFlags, mesonFlags 55 | 56 | # Build tool flags common for all phases. 57 | makeFlags = [ 58 | ]; 59 | # or ninjaFlags 60 | 61 | # 62 | buildFlags = [ 63 | ]; 64 | 65 | installFlags = [ 66 | ]; 67 | 68 | # Environment variables. 69 | env = { 70 | FONTCONFIG_FILE = ""; 71 | NIX_CFLAGS_COMPILE = ""; 72 | }; 73 | 74 | # Enabling off-by-default phases. 75 | doCheck = true; 76 | doInstallCheck = true; 77 | 78 | # Disabling on-by-default phases. 79 | dontBuild = true; 80 | 81 | # Attributes for overriding default phases, and their hooks should be ordered exactly as they are executed in setup.sh (https://github.com/NixOS/nixpkgs/blob/18f47ecbac1066b388e11dfa12617b557abeaf66/pkgs/stdenv/generic/setup.sh#L1261). 82 | # preUnpack 83 | # unpackPhase 84 | # postUnpack 85 | # prePatch 86 | # patchPhase 87 | # postPatch 88 | # preConfigure 89 | # configurePhase 90 | # postConfigure 91 | # preBuild 92 | # buildPhase 93 | # postBuild 94 | # preCheck 95 | # checkPhase 96 | # postCheck 97 | # preInstall 98 | # installPhase 99 | # postInstall 100 | # preFixup 101 | # fixupOutput 102 | # fixupPhase 103 | # postFixup 104 | # preInstallCheck 105 | # installCheckPhase 106 | # postInstallCheck 107 | 108 | # Metadata 109 | passthru = { 110 | }; 111 | 112 | meta = with lib; { 113 | description = ""; 114 | longDescription = '' 115 | ''; 116 | homepage = ""; 117 | license = licenses.mit; 118 | platforms = platforms.unix; 119 | maintainers = with maintainers; [ 120 | somebody 121 | ]; 122 | }; 123 | } 124 | ``` 125 | -------------------------------------------------------------------------------- /src/model.rs: -------------------------------------------------------------------------------- 1 | use rust_checks::common_structs; 2 | use serde::{Deserialize, Serialize}; 3 | use std::{fmt::Display, path::PathBuf}; 4 | 5 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub enum Severity { 8 | Error, 9 | Warning, 10 | Notice, 11 | } 12 | impl Display for Severity { 13 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 14 | match self { 15 | Severity::Error => write!(f, "error"), 16 | Severity::Warning => write!(f, "warning"), 17 | Severity::Notice => write!(f, "notice"), 18 | } 19 | } 20 | } 21 | 22 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] 23 | pub struct SourceLocation { 24 | pub file: PathBuf, 25 | pub line: usize, 26 | pub column: Option, 27 | } 28 | 29 | fn pb_to_string(buf: PathBuf) -> Result { 30 | Ok(buf 31 | .to_str() 32 | .ok_or_else(|| format!("File path ‘{}’ not a valid UTF-8 string", buf.display()))? 33 | .to_owned()) 34 | } 35 | 36 | impl TryInto for SourceLocation { 37 | type Error = String; 38 | 39 | fn try_into(self) -> Result { 40 | let SourceLocation { file, line, column } = self; 41 | Ok(common_structs::SourceLocation { 42 | file: pb_to_string(file)?, 43 | line, 44 | column, 45 | }) 46 | } 47 | } 48 | 49 | fn default_link() -> bool { 50 | true 51 | } 52 | 53 | fn default_severity() -> Severity { 54 | Severity::Warning 55 | } 56 | 57 | #[derive(Debug, Serialize, Deserialize)] 58 | pub struct Report { 59 | pub name: String, 60 | pub msg: String, 61 | #[serde(default)] 62 | pub locations: Vec, 63 | #[serde(default = "default_link")] 64 | pub link: bool, 65 | #[serde(default = "default_severity")] 66 | pub severity: Severity, 67 | } 68 | 69 | #[derive(Debug, Clone, PartialEq, Serialize, Eq, Hash)] 70 | pub struct CheckedAttr { 71 | /// name of attr, e.g. python38Packages.numpy 72 | pub name: String, 73 | /// location in which the attr is defined, if exist 74 | pub location: Option, 75 | /// path to the .drv file, if exists 76 | pub drv: Option, 77 | /// path the the output of the drv in the nix store, if exists 78 | pub output: Option, 79 | } 80 | 81 | impl TryInto for CheckedAttr { 82 | type Error = String; 83 | 84 | fn try_into(self) -> Result { 85 | let CheckedAttr { 86 | name, 87 | location, 88 | drv, 89 | output, 90 | } = self; 91 | 92 | let location = location.map(|l| l.try_into()).transpose()?; 93 | let drv = drv.map(pb_to_string).transpose()?; 94 | let output = output.map(pb_to_string).transpose()?; 95 | 96 | Ok(common_structs::CheckedAttr { 97 | name, 98 | location, 99 | drv, 100 | output, 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /overlays/attribute-typo.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkMkDerivationFor; 5 | inherit (import ../lib/levenshtein.nix { inherit lib; }) levenshtein levenshteinAtMost; 6 | 7 | knowAttributeNames = 8 | let 9 | flattenGroup = item: 10 | if builtins.isAttrs item then 11 | item.values 12 | else 13 | [ item ]; 14 | in 15 | builtins.concatMap flattenGroup (import ../lib/derivation-attributes.nix) ++ import ../lib/derivation-attributes-unordered.nix ++ import ../lib/environment-variables.nix; 16 | 17 | knownAttributeNamesLowerMapping = builtins.listToAttrs (map (item: { name = lib.toLower item; value = item; }) knowAttributeNames); 18 | knownAttributeNamesLower = builtins.attrNames knownAttributeNamesLowerMapping; 19 | 20 | isUnknownAttr = name: !builtins.elem name knowAttributeNames; 21 | 22 | # Get a list of suggested argument names for a given unknown one 23 | # Based on infinisil’s code from https://github.com/NixOS/nixpkgs/pull/79877 24 | getSuggestions = argName: 25 | let 26 | argNameLower = lib.toLower argName; 27 | argLength = builtins.stringLength argName; 28 | maxDistance = 29 | if argLength <= 7 then 30 | 1 31 | else if argLength <= 9 then 32 | 2 33 | else 34 | 4; 35 | in 36 | if builtins.hasAttr argNameLower knownAttributeNamesLowerMapping then 37 | # Casing mismatch 38 | lib.singleton (builtins.getAttr argNameLower knownAttributeNamesLowerMapping) 39 | else if argLength <= 5 then 40 | # Too short, only casing typos are checked. 41 | [] 42 | else 43 | # Look for close matches 44 | lib.pipe knownAttributeNamesLower [ 45 | # Only use ones that are at most maxDistance edits away. 46 | # levenshteinAtMost is only fast for 2 or less so this is suboptimal but it will have to do for now. 47 | (builtins.filter (levenshteinAtMost maxDistance argNameLower)) 48 | # Put strings with shorter distance first. 49 | (lib.sort (a: b: levenshtein a argNameLower < levenshtein b argNameLower)) 50 | # Only take the first couple results. 51 | (lib.take 3) 52 | # Quote all entries. 53 | (map (n: knownAttributeNamesLowerMapping.${n})) 54 | ]; 55 | 56 | prettySuggestions = unquotedSuggestions: 57 | let 58 | suggestions = map (s: "`${s}`") unquotedSuggestions; 59 | in 60 | if suggestions == [] then 61 | "." 62 | else if lib.length suggestions == 1 then 63 | ", did you mean ${lib.elemAt suggestions 0}?" 64 | else 65 | ", did you mean ${lib.concatStringsSep ", " (lib.init suggestions)} or ${lib.last suggestions}?"; 66 | 67 | checkDerivation = drvArgs: drv: 68 | let 69 | unknownAttributeNames = builtins.filter isUnknownAttr (builtins.attrNames drvArgs); 70 | in 71 | map 72 | (argName: 73 | let 74 | suggestions = getSuggestions argName; 75 | in { 76 | name = "attribute-typo"; 77 | # Comment out the following line to see all unknown attributes (when extending our lexicon). 78 | cond = builtins.length suggestions > 0; 79 | msg = '' 80 | A likely typo in the `${argName}` argument was found${prettySuggestions suggestions} 81 | ''; 82 | locations = [ 83 | (builtins.unsafeGetAttrPos argName drvArgs) 84 | ]; 85 | } 86 | ) 87 | unknownAttributeNames; 88 | in 89 | checkMkDerivationFor checkDerivation final prev 90 | -------------------------------------------------------------------------------- /overlays/build-tools-in-build-inputs.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | inherit (import ../lib { inherit lib; }) checkMkDerivationFor; 5 | 6 | buildToolAttrs = [ 7 | "antlr" 8 | "asciidoc" 9 | "asciidoctor" 10 | "autoconf" 11 | "autogen" 12 | "automake" 13 | "autoreconfHook" 14 | "bazel" 15 | "bc" 16 | "bdf2psf" 17 | "bison" 18 | "bmake" 19 | "breakpointHook" 20 | "bzip2" 21 | "bundler" 22 | "cmake" 23 | "docutils" 24 | "dos2unix" 25 | "desktop-file-utils" 26 | "docbook2x" 27 | "docbook_xml_dtd_412" 28 | "docbook_xml_dtd_42" 29 | "docbook_xml_dtd_43" 30 | "docbook_xml_dtd_44" 31 | "docbook_xml_dtd_45" 32 | "docbook-xsl-nons" 33 | "docbook-xsl-ns" 34 | "doxygen" 35 | "ensureNewerSourcesForZipFilesHook" 36 | "extra-cmake-modules" 37 | "flex" 38 | "gawk" 39 | "gcc" 40 | "getopt" 41 | "git" 42 | "gnum4" 43 | "gnumake" 44 | "gnused" 45 | "gnustep-make" 46 | "gogUnpackHook" 47 | "go-md2man" 48 | "gtk-doc" 49 | "gzip" 50 | "help2man" 51 | "intltool" 52 | "libtool" 53 | "lit" 54 | "lld" 55 | "llvm" 56 | "makeWrapper" 57 | "man" 58 | "maven" 59 | "meson" 60 | "ninja" 61 | "node-gyp" 62 | "node-pre-gyp" 63 | "nodePackages.node-gyp-build" 64 | "pandoc" 65 | "patch" 66 | "patchelf" 67 | "pkg-config" 68 | "poetry" 69 | "qt5.wrapQtAppsHook" 70 | "ronn" 71 | "rpcsvc-proto" 72 | "rpm" 73 | "rpmextract" 74 | "rsync" 75 | "python3.pkgs.setuptools-git" 76 | "python3.pkgs.setuptools-scm" 77 | "python3.pkgs.sphinx" 78 | "python3.pkgs.wrapPython" 79 | "squashfsTools" 80 | "sudo" 81 | "subversion" 82 | "swig" 83 | "unzip" 84 | "updateAutotoolsGnuConfigScriptsHook" 85 | "util-linux" 86 | "vala" 87 | "wafHook" 88 | "which" 89 | "wrapGAppsHook3" 90 | "wrapGAppsHook4" 91 | "wrapGAppsNoGuiHook" 92 | "xcbuildHook" 93 | "xcodebuild" 94 | "xmlto" 95 | "xvfb-run" 96 | "yarn" 97 | "zip" 98 | ] ++ lib.optionals (prev.stdenv.isDarwin) [ 99 | "darwin.cctools" 100 | ]; 101 | 102 | developmentMode = builtins.getEnv "NIXPKGS_HAMMERING_FATAL_EVAL_ISSUES" != ""; 103 | 104 | mkTool = 105 | attrPath: 106 | let 107 | package = lib.attrByPath (lib.splitString "." attrPath) null prev; 108 | in 109 | if !(builtins.tryEval package).success then 110 | if developmentMode then throw "‘${attrPath}’ is a ‘throw \"...\"’ in Nixpkgs." else null 111 | else if package == null then 112 | if developmentMode then throw "‘${attrPath}’ does not exist in Nixpkgs." else null 113 | else if package.meta.broken or false then 114 | if developmentMode then throw "‘${attrPath}’ is broken in Nixpkgs." else null 115 | else 116 | { 117 | name = attrPath; 118 | inherit package; 119 | }; 120 | 121 | buildTools = builtins.filter (tool: tool != null) (builtins.map mkTool buildToolAttrs); 122 | 123 | checkDerivation = drvArgs: drv: 124 | (map 125 | (tool: { 126 | name = "build-tools-in-build-inputs"; 127 | cond = lib.elem tool.package (drvArgs.buildInputs or [ ]); 128 | msg = '' 129 | ${tool.name} is a build tool so it likely goes to `nativeBuildInputs`, not `buildInputs`. 130 | ''; 131 | locations = [ 132 | (builtins.unsafeGetAttrPos "buildInputs" drvArgs) 133 | ]; 134 | }) 135 | buildTools 136 | ); 137 | 138 | in 139 | checkMkDerivationFor checkDerivation final prev 140 | -------------------------------------------------------------------------------- /lib/default.nix: -------------------------------------------------------------------------------- 1 | { lib 2 | }: 3 | 4 | rec { 5 | attrByPathString = attrPath: lib.getAttrFromPath (lib.splitString "." attrPath); 6 | 7 | # Removes reports with `cond == false` and then strips the `cond` attribute. 8 | filterReports = reports: 9 | map (report: builtins.removeAttrs report [ "cond" ]) (lib.filter ({ cond, ... }: cond) reports); 10 | 11 | # Get the location of a drv. Return either an attset containing the fields 'file' and 'line' 12 | # or null. 13 | getLocation = drv: 14 | let 15 | position = drv.meta.position or null; 16 | posSplit = builtins.split ":" position; 17 | in 18 | if position == null then 19 | null 20 | else { 21 | file = builtins.elemAt posSplit 0; 22 | line = lib.toInt (builtins.elemAt posSplit 2); 23 | }; 24 | 25 | capitalize = str: 26 | if builtins.stringLength str == 0 then 27 | str 28 | else 29 | let 30 | head = builtins.substring 0 1 str; 31 | tail = builtins.substring 1 (builtins.stringLength str - 1) str; 32 | in 33 | lib.toUpper head + tail; 34 | 35 | # Attaches reports to a package’s attribute set. 36 | addReports = 37 | originalDrv: 38 | reports: 39 | 40 | lib.recursiveUpdate originalDrv { 41 | __nixpkgs-hammering-state = { 42 | reports = lib.unique (originalDrv.__nixpkgs-hammering-state.reports or [] ++ filterReports reports); 43 | }; 44 | }; 45 | 46 | # Identity element for overlays. 47 | idOverlay = final: prev: {}; 48 | 49 | # Creates a function based on the original one, that runs a check 50 | # on the arguments passed to it and then returns the original result 51 | # enriched with the reports. 52 | wrapFunctionWithChecks = 53 | originalFunction: 54 | check: 55 | 56 | args: 57 | 58 | let 59 | originalDrv = originalFunction args; 60 | namePosition = originalDrv.meta.position or null; 61 | resolvedArgs = 62 | if builtins.isFunction args then 63 | # Resolve recursive attributes in mkDerivation 64 | # https://nixos.org/manual/nixpkgs/stable/#mkderivation-recursive-attributes 65 | lib.fix (finalAttrs: args finalAttrs // { finalPackage = originalDrv; }) 66 | else 67 | args; 68 | in 69 | addReports originalDrv (check resolvedArgs originalDrv); 70 | 71 | # Creates an overlay that replaces stdenv.mkDerivation with a function that 72 | # checks the attribute set passed as argument to mkDerivation. 73 | checkMkDerivationFor = 74 | check: 75 | 76 | final: 77 | prev: 78 | 79 | { 80 | stdenv = prev.stdenv // { 81 | mkDerivation = wrapFunctionWithChecks prev.stdenv.mkDerivation check; 82 | }; 83 | }; 84 | 85 | # Creates an overlay that replaces buildPythonPackage with a function that 86 | # checks the attribute set passed as argument to it. 87 | checkBuildPythonPackageFor = 88 | check: 89 | 90 | final: 91 | prev: 92 | 93 | let 94 | # Keep in sync with Nixpkgs’s all-packages.nix. Look for `inherit (pythonInterpreters)`. 95 | # No need to include e.g. `python3`, since that is derived from one of the listed. 96 | pythonPackageSetNames = [ 97 | "python2" 98 | "python38" 99 | "python39" 100 | "python310" 101 | "python311" 102 | "python312" 103 | "python3Minimal" 104 | "pypy27" 105 | "pypy39" 106 | "pypy38" 107 | "pypy37" 108 | "rustpython" 109 | ]; 110 | 111 | in 112 | lib.genAttrs pythonPackageSetNames (pythonName: 113 | prev.${pythonName}.override (oldOverrides: { 114 | packageOverrides = lib.composeExtensions (oldOverrides.packageOverrides or idOverlay) (final: prev: { 115 | buildPythonPackage = wrapFunctionWithChecks prev.buildPythonPackage check; 116 | }); 117 | }) 118 | ); 119 | 120 | # Creates an overlay that replaces all supported functions for creating derivations 121 | # with a function that checks the attribute set passed as argument to it. 122 | checkFor = check: 123 | let 124 | o1 = (checkMkDerivationFor check); 125 | o2 = (checkBuildPythonPackageFor check); 126 | in lib.composeExtensions o1 o2; 127 | } 128 | -------------------------------------------------------------------------------- /rust-checks/src/checks/missing_patch_comment.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | analysis::*, 3 | comment_finders::{find_comment_above, find_comment_after, find_comment_within}, 4 | common_structs::{CheckedAttr, NixpkgsHammerMessage, SourceLocation}, 5 | tree_utils::{parents, walk_attrpath_values_filter_attrpath, walk_kind}, 6 | }; 7 | use codespan::{FileId, Files}; 8 | use rnix::{ast::*, SyntaxKind::*}; 9 | use rowan::ast::AstNode as _; 10 | use std::error::Error; 11 | 12 | pub fn run(attrs: Vec) -> Result> { 13 | analyze_nix_files(attrs, analyze_single_file) 14 | } 15 | 16 | fn analyze_single_file(files: &Files, file_id: FileId) -> Result> { 17 | let root = find_root(files, file_id)?; 18 | let mut report: Report = vec![]; 19 | 20 | // Find all “patches” attrset attributes, that 21 | // *do not* have a comment directly above them. 22 | let patches_without_top_comment = walk_attrpath_values_filter_attrpath(&root, "patches") 23 | .filter(|pair| { 24 | // Look for a comment directly above the `patches = …` line 25 | // (Interpreting that as a comment which applies to all patches.) 26 | find_comment_above(pair.syntax()).is_none() 27 | }); 28 | 29 | // For each list of patches without a top comment, 30 | // produce a report for each patch that is missing a per-patch comment. 31 | for pair in patches_without_top_comment { 32 | let value = pair 33 | .value() 34 | .ok_or("Internal logic error: Unable to extract value from key/value pair")?; 35 | let contained_nonnested_lists = walk_kind(&value.syntax(), NODE_LIST).filter(|elem| { 36 | // As we’re walking down the tree looking for list nodes under `pair`, we don’t 37 | // want to walk too deep. We’re looking for lists, but we don’t want to find 38 | // any lists that are inside other lists, since those are likely not patches 39 | // (they’re probably arguments to `fetchpatch` like `excludes`) 40 | 41 | // So we’ll filter the lists returned by walk_kind (which is doing a full 42 | // tree traversal) by counting the number of lists that occur between this 43 | // node `elem` and `pair`, and checking for there to be only 1. 44 | 45 | // N.b.: there’s a more efficient way to do this, but it wouldn’t allow us 46 | // to re-use walk_kind and implementing our own tree traversal in rust is not 47 | // worth it -- speed is not an issue 48 | parents(elem.clone().into_node().unwrap()) 49 | .take_while(|elem| elem.text_range() != pair.syntax().text_range()) 50 | .filter(|elem| elem.kind() == NODE_LIST) 51 | .count() 52 | == 1 53 | }); 54 | for elem in contained_nonnested_lists { 55 | let patch_list = List::cast(elem.into_node().ok_or( 56 | "Internal logic error: Unable to cast element with kind NODE_LIST to into a Node", 57 | )?) 58 | .ok_or( 59 | "Internal logic error: Unable to cast from a node with kind NODE_LIST to a List", 60 | )?; 61 | report.extend(process_patch_list(patch_list, files, file_id)?); 62 | } 63 | } 64 | 65 | Ok(report) 66 | } 67 | 68 | fn process_patch_list( 69 | patchlist: List, 70 | files: &Files, 71 | file_id: FileId, 72 | ) -> Result> { 73 | let mut report: Report = vec![]; 74 | 75 | // For each element in the list of patches, look for 76 | // a comment directly above the element or, if we don’t see 77 | // one of those, look for a comment within AST of the element. 78 | for item in patchlist.items() { 79 | let item = item.syntax(); 80 | let has_comment_above = find_comment_above(&item).is_some(); 81 | let has_comment_within = find_comment_within(&item).is_some(); 82 | let has_comment_after = find_comment_after(&item).is_some(); 83 | let has_comment = has_comment_above || has_comment_within || has_comment_after; 84 | 85 | if !has_comment { 86 | let start: u32 = item.text_range().start().into(); 87 | 88 | report.push(NixpkgsHammerMessage { 89 | msg: 90 | "Consider adding a comment explaining the purpose of this patch on the line preceeding." 91 | .to_string(), 92 | name: "missing-patch-comment", 93 | locations: vec![SourceLocation::from_byte_index(files, file_id, start)?], 94 | link: true, 95 | }); 96 | } 97 | } 98 | 99 | Ok(report) 100 | } 101 | -------------------------------------------------------------------------------- /rust-checks/src/checks/unused_argument.rs: -------------------------------------------------------------------------------- 1 | use crate::{analysis::*, common_structs::*, tree_utils::walk_kind}; 2 | use codespan::{FileId, Files}; 3 | use rnix::{ast::*, SyntaxKind::*, SyntaxNode}; 4 | use rowan::ast::AstNode as _; 5 | use std::error::Error; 6 | 7 | pub fn run(attrs: Vec) -> Result> { 8 | analyze_nix_files(attrs, analyze_single_file) 9 | } 10 | 11 | fn analyze_single_file(files: &Files, file_id: FileId) -> Result> { 12 | let root = find_root(files, file_id)?; 13 | let mut report: Report = vec![]; 14 | 15 | // This check looks for unused variables by walking the AST. It has a very 16 | // simple implementation: for each function that takes its arguments using 17 | // a pattern contract, we record the names of the variables. Then, we walk through 18 | // the body of the function and look for those names being used as identifiers. If 19 | // we don't see them, we call it an unused variable. 20 | 21 | // This design is simple and I don't believe that it produces any false positives, 22 | // which would be highly undesirable. It does, however, produce some false negatives. 23 | // Because we're not tracking all the intermediate scopes between the declaration of the 24 | // formal parameter and its use in the function body, it's possible for inner functions, 25 | // with statements, or let blocks that cause variables in the outer scope to be shadowed 26 | // to make this check _think_ that a variable is used when it really wasn't. 27 | 28 | // Fixing this behavior could be done by following the pseudocode in 29 | // https://github.com/jtojnar/nixpkgs-hammering/pull/32#issuecomment-776936922 30 | // but hasn't been done yet because we don't think rare false negatives are a high 31 | // priority. 32 | 33 | for lambda in walk_kind(&root, NODE_LAMBDA) 34 | .filter_map(|elem| elem.into_node().and_then(Lambda::cast)) 35 | .filter(|lambda| { 36 | lambda 37 | .param() 38 | .and_then(|param| Pattern::cast(param.syntax().clone())) 39 | .is_some() 40 | }) 41 | { 42 | let body = lambda 43 | .clone() 44 | .body() 45 | .ok_or("Unable to extract function body")?; 46 | 47 | // Extract the formal parameters from pattern-type functions, and don't 48 | // extract anything from single-argument functions because they often 49 | // need to have unused arguments for overlay-type constructs. 50 | let pattern = lambda 51 | .param() 52 | .and_then(|param| Pattern::cast(param.syntax().clone())) 53 | .unwrap(); 54 | 55 | let identifiers_in_body: Vec = walk_kind(&body.syntax(), NODE_IDENT) 56 | .filter_map(|elem| elem.into_node()) 57 | .filter_map(Ident::cast) 58 | // Filter out identifiers that are acting as keys in an attrset 59 | // i.e a construct like ```{ 60 | // x = 1; 61 | // }``` 62 | // does not mean that `x` is acting as a usage of the identifier 63 | // `x` for purposes of detecting unused variables 64 | .filter(|ident| !ident_is_attrset_key(ident.syntax())) 65 | .chain( 66 | // Also collect any identifiers that appear in the default 67 | // definitions of arguments to the function, such as `pkgs` in 68 | // 69 | // { pkgs # might look unused, but is not 70 | // , foobarbazqux ? pkgs.hello 71 | // }: … 72 | pattern 73 | .pat_entries() 74 | .filter_map(|entry| entry.default()) 75 | .flat_map(|e| { 76 | walk_kind(&e.syntax(), NODE_IDENT) 77 | .filter_map(|el| el.into_node()) 78 | .filter_map(Ident::cast) 79 | }), 80 | ) 81 | .collect(); 82 | 83 | let unused_formal_parameters = pattern 84 | .pat_entries() 85 | .filter_map(|entry| entry.ident()) 86 | // Filter out formal parameters that are used as identifiers 87 | // in the function body 88 | .filter(|formal| { 89 | let formal = formal.syntax(); 90 | !identifiers_in_body 91 | .iter() 92 | .any(|ident| ident.syntax().text() == formal.text()) 93 | }); 94 | 95 | for unused in unused_formal_parameters { 96 | let start: u32 = unused.syntax().text_range().start().into(); 97 | report.push(NixpkgsHammerMessage { 98 | msg: format!("Unused argument: `{}`.", unused.syntax()), 99 | name: "unused-argument", 100 | locations: vec![SourceLocation::from_byte_index(files, file_id, start)?], 101 | link: false, 102 | }); 103 | } 104 | } 105 | 106 | Ok(report) 107 | } 108 | 109 | fn ident_is_attrset_key(node: &SyntaxNode) -> bool { 110 | match node.parent() { 111 | Some(p) if p.kind() == NODE_ATTRPATH => true, 112 | _ => false, 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/levenshtein.nix: -------------------------------------------------------------------------------- 1 | { lib 2 | }: 3 | 4 | /* 5 | Created by infinisil for https://github.com/NixOS/nixpkgs/pull/79877 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | let 10 | inherit (lib) stringLength substring; 11 | in 12 | 13 | rec { 14 | /* Computes the Levenshtein distance between two strings. 15 | Complexity O(n*m) where n and m are the lengths of the strings. 16 | Algorithm adjusted from https://stackoverflow.com/a/9750974/6605742 17 | Type: levenshtein :: string -> string -> int 18 | Example: 19 | levenshtein "foo" "foo" 20 | => 0 21 | levenshtein "book" "hook" 22 | => 1 23 | levenshtein "hello" "Heyo" 24 | => 3 25 | */ 26 | levenshtein = a: b: let 27 | # Two dimensional array with dimensions (stringLength a + 1, stringLength b + 1) 28 | arr = lib.genList (i: 29 | lib.genList (j: 30 | dist i j 31 | ) (stringLength b + 1) 32 | ) (stringLength a + 1); 33 | d = x: y: lib.elemAt (lib.elemAt arr x) y; 34 | dist = i: j: 35 | let c = if substring (i - 1) 1 a == substring (j - 1) 1 b 36 | then 0 else 1; 37 | in 38 | if j == 0 then i 39 | else if i == 0 then j 40 | else lib.min 41 | ( lib.min (d (i - 1) j + 1) (d i (j - 1) + 1)) 42 | ( d (i - 1) (j - 1) + c ); 43 | in d (stringLength a) (stringLength b); 44 | 45 | /* Returns the length of the prefix common to both strings. 46 | */ 47 | commonPrefixLength = a: b: 48 | let 49 | m = lib.min (stringLength a) (stringLength b); 50 | go = i: if i >= m then m else if substring i 1 a == substring i 1 b then go (i + 1) else i; 51 | in go 0; 52 | 53 | /* Returns the length of the suffix common to both strings. 54 | */ 55 | commonSuffixLength = a: b: 56 | let 57 | m = lib.min (stringLength a) (stringLength b); 58 | go = i: if i >= m then m else if substring (stringLength a - i - 1) 1 a == substring (stringLength b - i - 1) 1 b then go (i + 1) else i; 59 | in go 0; 60 | 61 | /* Returns whether the levenshtein distance between two strings is at most some value 62 | Complexity is O(min(n,m)) for k <= 2 and O(n*m) otherwise 63 | Type: levenshteinAtMost :: int -> string -> string -> bool 64 | Example: 65 | levenshteinAtMost 0 "foo" "foo" 66 | => true 67 | levenshteinAtMost 1 "foo" "boa" 68 | => false 69 | levenshteinAtMost 2 "foo" "boa" 70 | => true 71 | levenshteinAtMost 2 "This is a sentence" "this is a sentense." 72 | => false 73 | levenshteinAtMost 3 "This is a sentence" "this is a sentense." 74 | => true 75 | */ 76 | levenshteinAtMost = let 77 | infixDifferAtMost1 = x: y: stringLength x <= 1 && stringLength y <= 1; 78 | 79 | # This function takes two strings stripped by their common pre and suffix, 80 | # and returns whether they differ by at most two by Levenshtein distance. 81 | # Because of this stripping, if they do indeed differ by at most two edits, 82 | # we know that those edits were (if at all) done at the start or the end, 83 | # while the middle has to have stayed the same. This fact is used in the 84 | # implementation. 85 | infixDifferAtMost2 = x: y: 86 | let 87 | xlen = stringLength x; 88 | ylen = stringLength y; 89 | # This function is only called with |x| >= |y| and |x| - |y| <= 2, so 90 | # diff is one of 0, 1 or 2 91 | diff = xlen - ylen; 92 | 93 | # Infix of x and y, stripped by the left and right most character 94 | xinfix = substring 1 (xlen - 2) x; 95 | yinfix = substring 1 (ylen - 2) y; 96 | 97 | # x and y but a character deleted at the left or right 98 | xdelr = substring 0 (xlen - 1) x; 99 | xdell = substring 1 (xlen - 1) x; 100 | ydelr = substring 0 (ylen - 1) y; 101 | ydell = substring 1 (ylen - 1) y; 102 | in 103 | # A length difference of 2 can only be gotten with 2 delete edits, 104 | # which have to have happened at the start and end of x 105 | # Example: "abcdef" -> "bcde" 106 | if diff == 2 then xinfix == y 107 | # A length difference of 1 can only be gotten with a deletion on the 108 | # right and a replacement on the left or vice versa. 109 | # Example: "abcdef" -> "bcdez" or "zbcde" 110 | else if diff == 1 then xinfix == ydelr || xinfix == ydell 111 | # No length difference can either happen through replacements on both 112 | # sides, or a deletion on the left and an insertion on the right or 113 | # vice versa 114 | # Example: "abcdef" -> "zbcdez" or "bcdefz" or "zabcde" 115 | else xinfix == yinfix || xdelr == ydell || xdell == ydelr; 116 | 117 | in k: if k <= 0 then a: b: a == b else 118 | let f = a: b: 119 | let 120 | alen = stringLength a; 121 | blen = stringLength b; 122 | prelen = commonPrefixLength a b; 123 | suflen = commonSuffixLength a b; 124 | presuflen = prelen + suflen; 125 | ainfix = substring prelen (alen - presuflen) a; 126 | binfix = substring prelen (blen - presuflen) b; 127 | in 128 | # Make a be the bigger string 129 | if alen < blen then f b a 130 | # If a has over k more characters than b, even with k deletes on a, b can't be reached 131 | else if alen - blen > k then false 132 | else if k == 1 then infixDifferAtMost1 ainfix binfix 133 | else if k == 2 then infixDifferAtMost2 ainfix binfix 134 | else levenshtein ainfix binfix <= k; 135 | in f; 136 | } 137 | -------------------------------------------------------------------------------- /tests/hammering.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use std::collections::HashMap; 3 | use std::ffi::OsStr; 4 | use std::process::{Command, Output, Stdio}; 5 | 6 | fn run_command(program: S, args: I, envs: HashMap<&str, &str>) -> Result 7 | where 8 | S: AsRef, 9 | I: IntoIterator, 10 | { 11 | let child = Command::new(&program) 12 | .args(args) 13 | .stdout(Stdio::piped()) 14 | .envs(envs) 15 | .spawn() 16 | // todo: use display method once stabilized in 1.87.0 17 | // https://github.com/rust-lang/rust/issues/120048 18 | .map_err(|err| format!("Unable to spawn program ‘{:?}’: {err}", program.as_ref()))?; 19 | 20 | child 21 | .wait_with_output() 22 | .map_err(|err| format!("Unable to wait on ‘{:?}’: {err}", program.as_ref())) 23 | } 24 | 25 | fn make_test_variant( 26 | rule: String, 27 | variant: Option, 28 | should_match: bool, 29 | prebuild: bool, 30 | ) -> Result<(), String> { 31 | let attr_path = match variant { 32 | Some(variant) => format!("{rule}.{variant}"), 33 | None => rule.clone(), 34 | }; 35 | 36 | if prebuild { 37 | run_command( 38 | "nix-build", 39 | ["--no-out-link", "./tests", "-A", &attr_path], 40 | HashMap::default(), 41 | )?; 42 | } 43 | 44 | let envs = HashMap::from([("NIXPKGS_HAMMERING_FATAL_EVAL_ISSUES", "1")]); 45 | 46 | let nixpkgs_hammer_path = env!("CARGO_BIN_EXE_nixpkgs-hammer"); 47 | let test_build_output = run_command( 48 | nixpkgs_hammer_path, 49 | ["-f", "./tests", "--json", &attr_path], 50 | envs, 51 | )?; 52 | 53 | if !test_build_output.status.success() { 54 | return Err(format!( 55 | "error building the test: {}", 56 | String::from_utf8_lossy(&test_build_output.stdout) 57 | )); 58 | } 59 | 60 | let report: serde_json::Value = 61 | serde_json::from_slice(&test_build_output.stdout).map_err(|err| err.to_string())?; 62 | let matches = report 63 | .get(&attr_path) 64 | .and_then(Value::as_array) 65 | .map(|checks| checks.iter().any(|check| check["name"] == rule)) 66 | .unwrap_or_default(); 67 | 68 | if should_match && !matches { 69 | return Err("error matching the rule".to_string()); 70 | } else if !should_match && matches { 71 | return Err("rule should not match".to_string()); 72 | } 73 | 74 | Ok(()) 75 | } 76 | 77 | macro_rules! make_test_rule { 78 | ( 79 | $name:ident 80 | ) => { 81 | #[test] 82 | #[allow(non_snake_case)] 83 | fn $name() -> Result<(), String> { 84 | make_test_variant( 85 | stringify!($name).replace("_", "-"), 86 | None, 87 | true, 88 | false, 89 | ) 90 | } 91 | }; 92 | 93 | ( 94 | $name:ident, 95 | [$($matching_variant:ident),* $(,)?] $(,)? 96 | $([$($nonmatching_variant:ident),* $(,)?] $(,)?)? 97 | $(prebuild = $prebuild:literal $(,)?)? 98 | ) => { 99 | #[allow(non_snake_case)] 100 | mod $name { 101 | use super::make_test_variant; 102 | 103 | const PREBUILD: bool = make_test_rule!(@prebuild, $($prebuild)?); 104 | 105 | $( 106 | #[test] 107 | #[allow(non_snake_case)] 108 | fn $matching_variant() -> Result<(), String> { 109 | make_test_variant( 110 | stringify!($name).replace("_", "-"), 111 | Some(stringify!($matching_variant).replace("_", "-")), 112 | true, 113 | PREBUILD, 114 | ) 115 | } 116 | )* 117 | $($( 118 | #[test] 119 | #[allow(non_snake_case)] 120 | fn $nonmatching_variant() -> Result<(), String> { 121 | make_test_variant( 122 | stringify!($name).replace("_", "-"), 123 | Some(stringify!($nonmatching_variant).replace("_", "-")), 124 | false, 125 | PREBUILD, 126 | ) 127 | } 128 | )*)? 129 | } 130 | }; 131 | 132 | 133 | (@prebuild, $prebuild:literal) => { 134 | $prebuild 135 | }; 136 | 137 | (@prebuild, ) => { 138 | false 139 | }; 140 | } 141 | 142 | make_test_rule!(AttrPathNotFound, [foobarbaz]); 143 | 144 | make_test_rule!( 145 | attribute_ordering, 146 | [out_of_order], 147 | [inherited, properly_ordered], 148 | ); 149 | 150 | make_test_rule!( 151 | attribute_typo, 152 | [casing, deletion, insertion, transposition], 153 | [properly_ordered, unknown_short], 154 | ); 155 | 156 | make_test_rule!( 157 | build_tools_in_build_inputs, 158 | [cmake, meson, ninja, pkg_config, sphinx], 159 | ); 160 | 161 | // make_test_rule!(duplicate_check_inputs); 162 | 163 | make_test_rule!( 164 | environment_variables_go_to_env, 165 | [env_var, pkg_config_var], 166 | [env_vars_just_in_env, no_env_vars], 167 | ); 168 | 169 | make_test_rule!(EvalError, [exception], [no_exception]); 170 | 171 | make_test_rule!(explicit_phases, [configure, build, check, install]); 172 | 173 | make_test_rule!(fixup_phase); 174 | 175 | make_test_rule!( 176 | license_missing, 177 | [no_license, empty_license, no_meta], 178 | [have_license], 179 | ); 180 | 181 | make_test_rule!( 182 | maintainers_missing, 183 | [no_maintainers, empty_maintainers, no_meta], 184 | [have_maintainers], 185 | ); 186 | 187 | make_test_rule!(meson_cmake); 188 | 189 | make_test_rule!( 190 | missing_patch_comment, 191 | [missing_comment, comment_after_newline], 192 | [ 193 | general_comment, 194 | comment_above, 195 | comment_inline, 196 | comment_within, 197 | complex_structure1, 198 | ignore_nested_lists1, 199 | ignore_nested_lists2, 200 | ], 201 | ); 202 | 203 | make_test_rule!( 204 | missing_phase_hooks, 205 | [ 206 | configure_pre, 207 | configure_post, 208 | configure_both, 209 | build_pre, 210 | build_post, 211 | build_both, 212 | check_pre, 213 | check_post, 214 | check_both, 215 | install_pre, 216 | install_post, 217 | install_both, 218 | ], 219 | ); 220 | 221 | make_test_rule!(name_and_version, [positive], [negative, everything]); 222 | 223 | make_test_rule!(no_flags_array, [make, make_finalAttrs]); 224 | 225 | make_test_rule!(no_flags_spaces, [bad], [okay, nonstring]); 226 | 227 | make_test_rule!(no_uri_literals, [uri_literal], [string]); 228 | 229 | make_test_rule!(patch_phase); 230 | 231 | make_test_rule!( 232 | python_explicit_check_phase, 233 | [ 234 | // redundant_pytest, 235 | ], 236 | [nonredundant_pytest], 237 | ); 238 | 239 | make_test_rule!( 240 | python_imports_check_typo, 241 | [ 242 | // pythonImportTests, 243 | // pythonImportCheck, 244 | // pythonImportsTest, 245 | // pythonCheckImports, 246 | ], 247 | [pythonImportsCheck], 248 | ); 249 | 250 | make_test_rule!( 251 | python_include_tests, 252 | [ 253 | // no_tests_no_import_checks, 254 | // tests_disabled_no_import_checks, 255 | ], 256 | [pytest_check_hook, explicit_check_phase, has_imports_check], 257 | ); 258 | 259 | make_test_rule!( 260 | python_inconsistent_interpreters, 261 | [ 262 | // mixed_checkInputs, 263 | // mixed_propagatedBuildInputs, 264 | ], 265 | [normal], 266 | ); 267 | 268 | make_test_rule!(stale_substitute, [stale], [live], prebuild = true); 269 | 270 | make_test_rule!( 271 | unnecessary_parallel_building, 272 | [cmake, meson, qmake, qt_derivation], 273 | ); 274 | 275 | make_test_rule!( 276 | unclear_gpl, 277 | [ 278 | gpl2, gpl3, lgpl2, lgpl21, lgpl3, 279 | // lgpl3_python, 280 | ], 281 | [single_nonmatching_license], 282 | ); 283 | 284 | make_test_rule!( 285 | unused_argument, 286 | [ 287 | unused_pattern, 288 | unused_pattern_var_as_key, 289 | unused_pattern_var_in_let_binding, 290 | ], 291 | [ 292 | used_pattern, 293 | used_single, 294 | unused_single, 295 | used_in_string1, 296 | used_in_string2, 297 | used_in_defaults, 298 | ], 299 | ); 300 | -------------------------------------------------------------------------------- /rust-checks/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "codespan" 16 | version = "0.11.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "3362992a0d9f1dd7c3d0e89e0ab2bb540b7a95fea8cd798090e758fda2899b5e" 19 | dependencies = [ 20 | "codespan-reporting", 21 | ] 22 | 23 | [[package]] 24 | name = "codespan-reporting" 25 | version = "0.11.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 28 | dependencies = [ 29 | "termcolor", 30 | "unicode-width", 31 | ] 32 | 33 | [[package]] 34 | name = "countme" 35 | version = "3.0.1" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" 38 | 39 | [[package]] 40 | name = "hashbrown" 41 | version = "0.14.5" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 44 | 45 | [[package]] 46 | name = "itoa" 47 | version = "1.0.15" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 50 | 51 | [[package]] 52 | name = "memchr" 53 | version = "2.7.4" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 56 | 57 | [[package]] 58 | name = "proc-macro2" 59 | version = "1.0.94" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 62 | dependencies = [ 63 | "unicode-ident", 64 | ] 65 | 66 | [[package]] 67 | name = "quote" 68 | version = "1.0.39" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" 71 | dependencies = [ 72 | "proc-macro2", 73 | ] 74 | 75 | [[package]] 76 | name = "regex" 77 | version = "1.11.1" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 80 | dependencies = [ 81 | "aho-corasick", 82 | "memchr", 83 | "regex-automata", 84 | "regex-syntax", 85 | ] 86 | 87 | [[package]] 88 | name = "regex-automata" 89 | version = "0.4.9" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 92 | dependencies = [ 93 | "aho-corasick", 94 | "memchr", 95 | "regex-syntax", 96 | ] 97 | 98 | [[package]] 99 | name = "regex-syntax" 100 | version = "0.8.5" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 103 | 104 | [[package]] 105 | name = "rnix" 106 | version = "0.11.0" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "bb35cedbeb70e0ccabef2a31bcff0aebd114f19566086300b8f42c725fc2cb5f" 109 | dependencies = [ 110 | "rowan", 111 | ] 112 | 113 | [[package]] 114 | name = "rowan" 115 | version = "0.15.16" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "0a542b0253fa46e632d27a1dc5cf7b930de4df8659dc6e720b647fc72147ae3d" 118 | dependencies = [ 119 | "countme", 120 | "hashbrown", 121 | "rustc-hash", 122 | "serde", 123 | "text-size", 124 | ] 125 | 126 | [[package]] 127 | name = "rust-checks" 128 | version = "0.0.0" 129 | dependencies = [ 130 | "codespan", 131 | "regex", 132 | "rnix", 133 | "rowan", 134 | "serde", 135 | "serde_json", 136 | ] 137 | 138 | [[package]] 139 | name = "rustc-hash" 140 | version = "1.1.0" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 143 | 144 | [[package]] 145 | name = "ryu" 146 | version = "1.0.20" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 149 | 150 | [[package]] 151 | name = "serde" 152 | version = "1.0.219" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 155 | dependencies = [ 156 | "serde_derive", 157 | ] 158 | 159 | [[package]] 160 | name = "serde_derive" 161 | version = "1.0.219" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 164 | dependencies = [ 165 | "proc-macro2", 166 | "quote", 167 | "syn", 168 | ] 169 | 170 | [[package]] 171 | name = "serde_json" 172 | version = "1.0.140" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 175 | dependencies = [ 176 | "itoa", 177 | "memchr", 178 | "ryu", 179 | "serde", 180 | ] 181 | 182 | [[package]] 183 | name = "syn" 184 | version = "2.0.100" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 187 | dependencies = [ 188 | "proc-macro2", 189 | "quote", 190 | "unicode-ident", 191 | ] 192 | 193 | [[package]] 194 | name = "termcolor" 195 | version = "1.4.1" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 198 | dependencies = [ 199 | "winapi-util", 200 | ] 201 | 202 | [[package]] 203 | name = "text-size" 204 | version = "1.1.1" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" 207 | dependencies = [ 208 | "serde", 209 | ] 210 | 211 | [[package]] 212 | name = "unicode-ident" 213 | version = "1.0.18" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 216 | 217 | [[package]] 218 | name = "unicode-width" 219 | version = "0.1.14" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 222 | 223 | [[package]] 224 | name = "winapi-util" 225 | version = "0.1.9" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 228 | dependencies = [ 229 | "windows-sys", 230 | ] 231 | 232 | [[package]] 233 | name = "windows-sys" 234 | version = "0.59.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 237 | dependencies = [ 238 | "windows-targets", 239 | ] 240 | 241 | [[package]] 242 | name = "windows-targets" 243 | version = "0.52.6" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 246 | dependencies = [ 247 | "windows_aarch64_gnullvm", 248 | "windows_aarch64_msvc", 249 | "windows_i686_gnu", 250 | "windows_i686_gnullvm", 251 | "windows_i686_msvc", 252 | "windows_x86_64_gnu", 253 | "windows_x86_64_gnullvm", 254 | "windows_x86_64_msvc", 255 | ] 256 | 257 | [[package]] 258 | name = "windows_aarch64_gnullvm" 259 | version = "0.52.6" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 262 | 263 | [[package]] 264 | name = "windows_aarch64_msvc" 265 | version = "0.52.6" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 268 | 269 | [[package]] 270 | name = "windows_i686_gnu" 271 | version = "0.52.6" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 274 | 275 | [[package]] 276 | name = "windows_i686_gnullvm" 277 | version = "0.52.6" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 280 | 281 | [[package]] 282 | name = "windows_i686_msvc" 283 | version = "0.52.6" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 286 | 287 | [[package]] 288 | name = "windows_x86_64_gnu" 289 | version = "0.52.6" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 292 | 293 | [[package]] 294 | name = "windows_x86_64_gnullvm" 295 | version = "0.52.6" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 298 | 299 | [[package]] 300 | name = "windows_x86_64_msvc" 301 | version = "0.52.6" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 304 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.18" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.10" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 49 | dependencies = [ 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.7" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell", 61 | "windows-sys", 62 | ] 63 | 64 | [[package]] 65 | name = "clap" 66 | version = "4.5.47" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" 69 | dependencies = [ 70 | "clap_builder", 71 | "clap_derive", 72 | ] 73 | 74 | [[package]] 75 | name = "clap_builder" 76 | version = "4.5.47" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" 79 | dependencies = [ 80 | "anstream", 81 | "anstyle", 82 | "clap_lex", 83 | "strsim", 84 | ] 85 | 86 | [[package]] 87 | name = "clap_derive" 88 | version = "4.5.47" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" 91 | dependencies = [ 92 | "heck", 93 | "proc-macro2", 94 | "quote", 95 | "syn", 96 | ] 97 | 98 | [[package]] 99 | name = "clap_lex" 100 | version = "0.7.4" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 103 | 104 | [[package]] 105 | name = "codespan" 106 | version = "0.12.0" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "3e4b418d52c9206820a56fc1aa28db73d67e346ba8ba6aa90987e8d6becef7e4" 109 | dependencies = [ 110 | "codespan-reporting", 111 | "serde", 112 | ] 113 | 114 | [[package]] 115 | name = "codespan-reporting" 116 | version = "0.12.0" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" 119 | dependencies = [ 120 | "serde", 121 | "termcolor", 122 | "unicode-width", 123 | ] 124 | 125 | [[package]] 126 | name = "colorchoice" 127 | version = "1.0.3" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 130 | 131 | [[package]] 132 | name = "countme" 133 | version = "3.0.1" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" 136 | 137 | [[package]] 138 | name = "hashbrown" 139 | version = "0.14.5" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 142 | 143 | [[package]] 144 | name = "heck" 145 | version = "0.5.0" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 148 | 149 | [[package]] 150 | name = "indoc" 151 | version = "2.0.6" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 154 | 155 | [[package]] 156 | name = "is_terminal_polyfill" 157 | version = "1.70.1" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 160 | 161 | [[package]] 162 | name = "itoa" 163 | version = "1.0.15" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 166 | 167 | [[package]] 168 | name = "memchr" 169 | version = "2.7.4" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 172 | 173 | [[package]] 174 | name = "nixpkgs-hammering" 175 | version = "0.1.0" 176 | dependencies = [ 177 | "clap", 178 | "indoc", 179 | "rust-checks", 180 | "serde", 181 | "serde_json", 182 | ] 183 | 184 | [[package]] 185 | name = "once_cell" 186 | version = "1.21.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" 189 | 190 | [[package]] 191 | name = "proc-macro2" 192 | version = "1.0.94" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 195 | dependencies = [ 196 | "unicode-ident", 197 | ] 198 | 199 | [[package]] 200 | name = "quote" 201 | version = "1.0.39" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" 204 | dependencies = [ 205 | "proc-macro2", 206 | ] 207 | 208 | [[package]] 209 | name = "regex" 210 | version = "1.11.2" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" 213 | dependencies = [ 214 | "aho-corasick", 215 | "memchr", 216 | "regex-automata", 217 | "regex-syntax", 218 | ] 219 | 220 | [[package]] 221 | name = "regex-automata" 222 | version = "0.4.9" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 225 | dependencies = [ 226 | "aho-corasick", 227 | "memchr", 228 | "regex-syntax", 229 | ] 230 | 231 | [[package]] 232 | name = "regex-syntax" 233 | version = "0.8.5" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 236 | 237 | [[package]] 238 | name = "rnix" 239 | version = "0.12.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "6f15e00b0ab43abd70d50b6f8cd021290028f9b7fdd7cdfa6c35997173bc1ba9" 242 | dependencies = [ 243 | "rowan", 244 | ] 245 | 246 | [[package]] 247 | name = "rowan" 248 | version = "0.15.16" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "0a542b0253fa46e632d27a1dc5cf7b930de4df8659dc6e720b647fc72147ae3d" 251 | dependencies = [ 252 | "countme", 253 | "hashbrown", 254 | "rustc-hash", 255 | "serde", 256 | "text-size", 257 | ] 258 | 259 | [[package]] 260 | name = "rust-checks" 261 | version = "0.0.0" 262 | dependencies = [ 263 | "codespan", 264 | "regex", 265 | "rnix", 266 | "rowan", 267 | "serde", 268 | "serde_json", 269 | ] 270 | 271 | [[package]] 272 | name = "rustc-hash" 273 | version = "1.1.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 276 | 277 | [[package]] 278 | name = "ryu" 279 | version = "1.0.20" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 282 | 283 | [[package]] 284 | name = "serde" 285 | version = "1.0.219" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 288 | dependencies = [ 289 | "serde_derive", 290 | ] 291 | 292 | [[package]] 293 | name = "serde_derive" 294 | version = "1.0.219" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 297 | dependencies = [ 298 | "proc-macro2", 299 | "quote", 300 | "syn", 301 | ] 302 | 303 | [[package]] 304 | name = "serde_json" 305 | version = "1.0.143" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" 308 | dependencies = [ 309 | "itoa", 310 | "memchr", 311 | "ryu", 312 | "serde", 313 | ] 314 | 315 | [[package]] 316 | name = "strsim" 317 | version = "0.11.1" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 320 | 321 | [[package]] 322 | name = "syn" 323 | version = "2.0.100" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 326 | dependencies = [ 327 | "proc-macro2", 328 | "quote", 329 | "unicode-ident", 330 | ] 331 | 332 | [[package]] 333 | name = "termcolor" 334 | version = "1.4.1" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 337 | dependencies = [ 338 | "winapi-util", 339 | ] 340 | 341 | [[package]] 342 | name = "text-size" 343 | version = "1.1.1" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" 346 | dependencies = [ 347 | "serde", 348 | ] 349 | 350 | [[package]] 351 | name = "unicode-ident" 352 | version = "1.0.18" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 355 | 356 | [[package]] 357 | name = "unicode-width" 358 | version = "0.1.14" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 361 | 362 | [[package]] 363 | name = "utf8parse" 364 | version = "0.2.2" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 367 | 368 | [[package]] 369 | name = "winapi-util" 370 | version = "0.1.9" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 373 | dependencies = [ 374 | "windows-sys", 375 | ] 376 | 377 | [[package]] 378 | name = "windows-sys" 379 | version = "0.59.0" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 382 | dependencies = [ 383 | "windows-targets", 384 | ] 385 | 386 | [[package]] 387 | name = "windows-targets" 388 | version = "0.52.6" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 391 | dependencies = [ 392 | "windows_aarch64_gnullvm", 393 | "windows_aarch64_msvc", 394 | "windows_i686_gnu", 395 | "windows_i686_gnullvm", 396 | "windows_i686_msvc", 397 | "windows_x86_64_gnu", 398 | "windows_x86_64_gnullvm", 399 | "windows_x86_64_msvc", 400 | ] 401 | 402 | [[package]] 403 | name = "windows_aarch64_gnullvm" 404 | version = "0.52.6" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 407 | 408 | [[package]] 409 | name = "windows_aarch64_msvc" 410 | version = "0.52.6" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 413 | 414 | [[package]] 415 | name = "windows_i686_gnu" 416 | version = "0.52.6" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 419 | 420 | [[package]] 421 | name = "windows_i686_gnullvm" 422 | version = "0.52.6" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 425 | 426 | [[package]] 427 | name = "windows_i686_msvc" 428 | version = "0.52.6" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 431 | 432 | [[package]] 433 | name = "windows_x86_64_gnu" 434 | version = "0.52.6" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 437 | 438 | [[package]] 439 | name = "windows_x86_64_gnullvm" 440 | version = "0.52.6" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 443 | 444 | [[package]] 445 | name = "windows_x86_64_msvc" 446 | version = "0.52.6" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 449 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use indoc::formatdoc; 2 | use model::{CheckedAttr, Report, Severity, SourceLocation}; 3 | use rust_checks::checks::{self, Check}; 4 | use serde::{Deserialize, Serialize}; 5 | use std::{ 6 | collections::{HashMap, HashSet}, 7 | fs::read_dir, 8 | path::PathBuf, 9 | }; 10 | use utils::{indent, optional_env, run_command_with_input}; 11 | 12 | pub mod model; 13 | pub mod utils; 14 | 15 | #[derive(Debug)] 16 | pub struct Config { 17 | pub nix_file: PathBuf, 18 | pub show_trace: bool, 19 | pub do_not_catch_errors: bool, 20 | pub json: bool, 21 | pub excluded_rules: HashSet, 22 | pub attr_paths: Vec, 23 | } 24 | 25 | fn get_overlays_dir() -> Result { 26 | // Provided by wrapper. 27 | if let Some(dir) = optional_env("OVERLAYS_DIR")? { 28 | return Ok(PathBuf::from(dir)); 29 | } 30 | 31 | // Running using `cargo run`. 32 | if let Some(dir) = optional_env("CARGO_MANIFEST_DIR")? { 33 | return Ok(PathBuf::from(&dir).join("overlays")); 34 | } 35 | 36 | Err("Either ‘OVERLAYS_DIR’ or ‘CARGO_MANIFEST_DIR’ environment variable expected.".to_owned()) 37 | } 38 | 39 | fn get_checks(excluded_rules: &HashSet) -> HashMap { 40 | checks::ALL 41 | .into_iter() 42 | .map(|(name, check)| (name.to_string(), check)) 43 | .filter(|(name, _)| !excluded_rules.contains(name)) 44 | .collect() 45 | } 46 | 47 | fn run_checks( 48 | attrs: &[CheckedAttr], 49 | excluded_rules: &HashSet, 50 | ) -> Result>, String> { 51 | let rules = get_checks(excluded_rules); 52 | if rules.is_empty() { 53 | return Ok(HashMap::new()); 54 | } 55 | 56 | let encoded_attrs = serde_json::to_string(attrs) 57 | .map_err(|err| format!("Unable to serialize attrs: {}", err.to_string()))?; 58 | let mut result = HashMap::new(); 59 | 60 | if !excluded_rules.contains("no-build-output") { 61 | let names_without_outputs = attrs 62 | .into_iter() 63 | .filter(|a| a.output.as_ref().is_some_and(|o| o.exists())) 64 | .map(|a| a.name.clone()); 65 | for name in names_without_outputs { 66 | result 67 | .entry(name.clone()) 68 | .or_insert_with(|| Vec::new()) 69 | .push(Report { 70 | name: "no-build-output".to_owned(), 71 | msg: format!( 72 | "‘{name}’ has not yet been built. \ 73 | Checks that rely on the presence of build logs are skipped." 74 | ), 75 | link: false, 76 | locations: vec![], 77 | severity: Severity::Notice, 78 | }) 79 | } 80 | } 81 | 82 | let rc_attrs = attrs 83 | .iter() 84 | .map(|attr| attr.clone().try_into()) 85 | .collect::, _>>()?; 86 | for (name, check) in rules { 87 | let json_text = check(rc_attrs.clone()).map_err(|err| { 88 | eprintln!( 89 | "{}", 90 | red(&format!( 91 | "Rule ‘{name}’ failed with input ‘{encoded_attrs}’.\ 92 | This is a bug. Please file a ticket on GitHub." 93 | )) 94 | ); 95 | 96 | format!("Unable to execute rule ‘{name}’: {}", err.to_string()) 97 | })?; 98 | 99 | if !json_text.is_empty() { 100 | let results: HashMap> = 101 | serde_json::from_str(&json_text).map_err(|err| { 102 | format!( 103 | "Unable to parse result of rule ‘{name}’: {}", 104 | err.to_string() 105 | ) 106 | })?; 107 | for (attr_name, reports) in results.into_iter() { 108 | result 109 | .entry(attr_name) 110 | .or_insert_with(|| Vec::new()) 111 | .extend(reports) 112 | } 113 | } 114 | } 115 | 116 | Ok(result) 117 | } 118 | 119 | fn concatenate_messages( 120 | report_sources: &mut [HashMap>], 121 | ) -> HashMap> { 122 | let mut result = HashMap::new(); 123 | for m in report_sources.into_iter() { 124 | for (attr_name, mut report) in m.drain() { 125 | result 126 | .entry(attr_name.clone()) 127 | .or_insert_with(|| Vec::new()) 128 | .append(&mut report); 129 | } 130 | } 131 | result 132 | } 133 | 134 | fn escape_attr(attr: &str) -> String { 135 | attr.split(".") 136 | .map(|section| format!("\"{section}\"")) 137 | .collect::>() 138 | .join(".") 139 | } 140 | 141 | #[derive(Debug, Serialize, Deserialize)] 142 | #[serde(rename_all = "camelCase")] 143 | pub struct OverlayCheckResult { 144 | /// Location in which the attr is defined, if exist 145 | location: Option, 146 | /// Path to the .drv file, if exists 147 | drv_path: Option, 148 | /// Path the the output of the drv in the nix store, if exists 149 | output_path: Option, 150 | report: Vec, 151 | } 152 | 153 | fn nix_eval_json( 154 | expr: String, 155 | show_trace: bool, 156 | ) -> Result>, String> { 157 | let mut args = vec!["--strict", "--json", "--eval", "-"]; 158 | 159 | if show_trace { 160 | args.push("--show-trace"); 161 | } 162 | 163 | let json_text = run_command_with_input("nix-instantiate", &args, &expr)?; 164 | 165 | let mut result = HashMap::new(); 166 | let results: HashMap = 167 | serde_json::from_str(&json_text).map_err(|err| { 168 | format!( 169 | "Unable to parse result of overlay checks: {} {json_text}", 170 | err.to_string() 171 | ) 172 | })?; 173 | for ( 174 | name, 175 | OverlayCheckResult { 176 | location, 177 | drv_path, 178 | output_path, 179 | mut report, 180 | }, 181 | ) in results.into_iter() 182 | { 183 | let attr = CheckedAttr { 184 | name, 185 | output: output_path, 186 | drv: drv_path, 187 | location, 188 | }; 189 | result 190 | .entry(attr) 191 | .or_insert_with(|| Vec::new()) 192 | .append(&mut report); 193 | } 194 | 195 | Ok(result) 196 | } 197 | 198 | fn bold(msg: &str) -> String { 199 | format!("{msg}") 200 | } 201 | 202 | fn yellow(msg: &str) -> String { 203 | format!("{msg}") 204 | } 205 | 206 | fn red(msg: &str) -> String { 207 | format!("{msg}") 208 | } 209 | 210 | fn green(msg: &str) -> String { 211 | format!("{msg}") 212 | } 213 | 214 | fn render_code_location_ansi(loc: SourceLocation) -> String { 215 | let Ok(opened_file) = std::fs::read_to_string(&loc.file) else { 216 | return format!("Unable to load contents of file ‘{}’", loc.file.display()); 217 | }; 218 | let all_lines: Vec<_> = opened_file.split("\n").collect(); 219 | let line_contents = all_lines[loc.line - 1]; 220 | let line_no = loc.line.to_string(); 221 | let line_spaces = " ".repeat(line_no.len()); 222 | let column = loc.column.unwrap_or(1); 223 | let pointer = " ".repeat(column - 1) + "^"; 224 | let mut rendered_loc_parts = vec![loc.file.to_string_lossy().to_string(), line_no.to_string()]; 225 | if !loc.column.is_none() { 226 | let column_no = loc.line.to_string(); 227 | rendered_loc_parts.push(column_no); 228 | } 229 | 230 | formatdoc!( 231 | "Near {}: 232 | {line_spaces} | 233 | {line_no} | {line_contents} 234 | {line_spaces} | {pointer}", 235 | rendered_loc_parts.join(":") 236 | ) 237 | } 238 | 239 | fn render_report_ansi(report: Report) -> String { 240 | let color = if report.severity == Severity::Error { 241 | red 242 | } else { 243 | yellow 244 | }; 245 | 246 | let mut message_lines = vec![ 247 | color(&format!("{}: {}", report.severity, report.name)), 248 | report.msg, 249 | ]; 250 | message_lines.extend(report.locations.into_iter().map(render_code_location_ansi)); 251 | 252 | if report.link { 253 | message_lines.push(format!( 254 | "See: https://github.com/jtojnar/nixpkgs-hammering/blob/master/explanations/{}.md", 255 | report.name, 256 | )); 257 | } 258 | 259 | return message_lines.join("\n"); 260 | } 261 | 262 | pub fn hammer(config: Config) -> Result<(), String> { 263 | let overlay_generators_path = get_overlays_dir()?; 264 | 265 | let try_eval = if config.do_not_catch_errors { 266 | "(value: { inherit value; success = true; })" 267 | } else { 268 | "builtins.tryEval" 269 | }; 270 | 271 | let mut attr_messages = vec![]; 272 | 273 | for attr in config.attr_paths { 274 | attr_messages.push(formatdoc!( 275 | r#" 276 | "{attr}" = 277 | let 278 | result = {try_eval} pkgs.{} or null; 279 | maybeReport = {try_eval} (result.value.__nixpkgs-hammering-state.reports or []); 280 | in 281 | if !(result.success && maybeReport.success) then 282 | {{ 283 | report = [ {{ 284 | name = "EvalError"; 285 | msg = "Cannot evaluate attribute ‘{attr}’ in ‘${{packageSet}}’."; 286 | severity = "warning"; 287 | link = false; 288 | }} ]; 289 | location = null; 290 | outputPath = null; 291 | drvPath = null; 292 | }} 293 | else if result.value == null then 294 | {{ 295 | report = [ {{ 296 | name = "AttrPathNotFound"; 297 | msg = "Packages in ‘${{packageSet}}’ do not contain ‘{attr}’ attribute."; 298 | severity = "error"; 299 | link = false; 300 | }} ]; 301 | location = null; 302 | outputPath = null; 303 | drvPath = null; 304 | }} 305 | else 306 | {{ 307 | report = if maybeReport.success then maybeReport.value else []; 308 | location = 309 | let 310 | position = result.value.meta.position or null; 311 | posSplit = builtins.split ":" result.value.meta.position; 312 | in 313 | if position == null then 314 | null 315 | else {{ 316 | file = builtins.elemAt posSplit 0; 317 | line = builtins.fromJSON (builtins.elemAt posSplit 2); 318 | }}; 319 | outputPath = result.value.outPath; 320 | drvPath = result.value.drvPath; 321 | }};"#, 322 | escape_attr(&attr) 323 | )); 324 | } 325 | 326 | // Our overlays need to know the built attributes so that they can check only them. 327 | // We do it by using functions that return overlays so we need to instantiate them. 328 | let mut overlay_expressions = vec![]; 329 | let generators = read_dir(&overlay_generators_path).map_err(|err| { 330 | format!( 331 | "Unable to list contents of overlays directory ‘{}’: {}", 332 | overlay_generators_path.display(), 333 | err.to_string() 334 | ) 335 | })?; 336 | for overlay_generator in generators { 337 | let overlay_generator = overlay_generator.map_err(|err| { 338 | format!( 339 | "Error reading overlays directory contents: {}", 340 | err.to_string() 341 | ) 342 | })?; 343 | if overlay_generator 344 | .path() 345 | .file_stem() 346 | .map(|stem| { 347 | config 348 | .excluded_rules 349 | .contains(&stem.to_string_lossy().to_string()) 350 | }) 351 | .unwrap_or(false) 352 | { 353 | continue; 354 | } 355 | 356 | let overlay = overlay_generator.path(); 357 | let overlay = overlay.to_str().ok_or_else(|| { 358 | format!( 359 | "Overlay path ‘{}’ not valid UTF-8 string.", 360 | overlay.display() 361 | ) 362 | })?; 363 | overlay_expressions.push(format!("(import {overlay})")); 364 | } 365 | 366 | let attr_messages_nix = if !attr_messages.is_empty() { 367 | "{\n".to_owned() + &indent(attr_messages.join("\n\n"), 1) + "\n}" 368 | } else { 369 | "{ }".to_owned() 370 | }; 371 | let overlays_nix = if !overlay_expressions.is_empty() { 372 | "[\n".to_owned() + &indent(overlay_expressions.join("\n"), 1) + "\n]" 373 | } else { 374 | "[ ]".to_owned() 375 | }; 376 | let nix_file = config.nix_file.to_str().ok_or_else(|| { 377 | format!( 378 | "Nix expression file path ‘{}’ not valid UTF-8 string.", 379 | config.nix_file.display(), 380 | ) 381 | })?; 382 | let all_messages_nix = formatdoc!( 383 | " 384 | let 385 | packageSet = {nix_file}; 386 | cleanPkgs = import packageSet {{ }}; 387 | 388 | pkgs = import packageSet {{ 389 | overlays = {}; 390 | }}; 391 | in {} 392 | ", 393 | indent(overlays_nix, 2).trim(), 394 | indent(attr_messages_nix, 0).trim() 395 | ); 396 | 397 | if config.show_trace { 398 | eprintln!("Nix expression:\n{}", all_messages_nix); 399 | } 400 | 401 | let overlay_data = nix_eval_json(all_messages_nix, config.show_trace)?; 402 | let reports = run_checks( 403 | &overlay_data.keys().map(|k| k.clone()).collect::>(), 404 | &HashSet::from_iter(config.excluded_rules), 405 | )?; 406 | let overlay_reports = HashMap::from_iter( 407 | overlay_data 408 | .into_iter() 409 | .map(|(attr, reports)| (attr.name, reports)), 410 | ); 411 | let all_messages = concatenate_messages(&mut [overlay_reports, reports]); 412 | 413 | if config.json { 414 | let encoded_messages = serde_json::to_string(&all_messages) 415 | .map_err(|err| format!("Unable to serialize result messages: {}", err.to_string()))?; 416 | println!("{}", encoded_messages); 417 | } else { 418 | for (attr_name, reports) in all_messages.into_iter() { 419 | eprintln!( 420 | "{}", 421 | bold(&format!("When evaluating attribute ‘{attr_name}’:")) 422 | ); 423 | if !reports.is_empty() { 424 | eprintln!( 425 | "{}", 426 | reports 427 | .into_iter() 428 | .map(render_report_ansi) 429 | .collect::>() 430 | .join("\n") 431 | ); 432 | } else { 433 | eprintln!("{}", green("No issues found.")); 434 | } 435 | eprintln!(""); 436 | } 437 | } 438 | 439 | Ok(()) 440 | } 441 | --------------------------------------------------------------------------------