{ pythonOnBuildForHost, runCommand, writeShellScript, coreutils, gnugrep }: let pythonPkgs = pythonOnBuildForHost.pkgs; ### UTILITIES # customize a package so that its store paths differs customize = pkg: pkg.overrideAttrs { some_modification = true; }; # generates minimal pyproject.toml pyprojectToml = pname: builtins.toFile "pyproject.toml" '' [project] name = "${pname}" version = "1.0.0" ''; # generates source for a python project projectSource = pname: runCommand "my-project-source" {} '' mkdir -p $out/src cp ${pyprojectToml pname} $out/pyproject.toml touch $out/src/__init__.py ''; # helper to reduce boilerplate generatePythonPackage = args: pythonPkgs.buildPythonPackage ( { version = "1.0.0"; src = runCommand "my-project-source" {} '' mkdir -p $out/src cp ${pyprojectToml args.pname} $out/pyproject.toml touch $out/src/__init__.py ''; pyproject = true; catchConflicts = true; buildInputs = [ pythonPkgs.setuptools ]; } // args ); # in order to test for a failing build, wrap it in a shell script expectFailure = build: errorMsg: build.overrideDerivation (old: { builder = writeShellScript "test-for-failure" '' export PATH=${coreutils}/bin:${gnugrep}/bin:$PATH ${old.builder} "$@" > ./log 2>&1 status=$? cat ./log if [ $status -eq 0 ] || ! grep -q "${errorMsg}" ./log; then echo "The build should have failed with '${errorMsg}', but it didn't" exit 1 else echo "The build failed as expected with: ${errorMsg}" mkdir -p $out fi ''; }); in { ### TEST CASES # Test case which must not trigger any conflicts. # This derivation has runtime dependencies on custom versions of multiple build tools. # This scenario is relevant for lang2nix tools which do not override the nixpkgs fix-point. # see https://github.com/NixOS/nixpkgs/issues/283695 ignores-build-time-deps = generatePythonPackage { pname = "ignores-build-time-deps"; buildInputs = [ pythonPkgs.build pythonPkgs.packaging pythonPkgs.setuptools pythonPkgs.wheel ]; propagatedBuildInputs = [ # Add customized versions of build tools as runtime deps (customize pythonPkgs.packaging) (customize pythonPkgs.setuptools) (customize pythonPkgs.wheel) ]; }; # Simplest test case that should trigger a conflict catches-simple-conflict = let # this build must fail due to conflicts package = pythonPkgs.buildPythonPackage rec { pname = "catches-simple-conflict"; version = "0.0.0"; src = projectSource pname; pyproject = true; catchConflicts = true; buildInputs = [ pythonPkgs.setuptools ]; # depend on two different versions of packaging # (an actual runtime dependency conflict) propagatedBuildInputs = [ pythonPkgs.packaging (customize pythonPkgs.packaging) ]; }; in expectFailure package "Found duplicated packages in closure for dependency 'packaging'"; /* More complex test case with a transitive conflict Test sets up this dependency tree: toplevel ├── dep1 │ └── leaf └── dep2 └── leaf (customized version -> conflicting) */ catches-transitive-conflict = let # package depending on both dependency1 and dependency2 toplevel = generatePythonPackage { pname = "catches-transitive-conflict"; propagatedBuildInputs = [ dep1 dep2 ]; }; # dep1 package depending on leaf dep1 = generatePythonPackage { pname = "dependency1"; propagatedBuildInputs = [ leaf ]; }; # dep2 package depending on conflicting version of leaf dep2 = generatePythonPackage { pname = "dependency2"; propagatedBuildInputs = [ (customize leaf) ]; }; # some leaf package leaf = generatePythonPackage { pname = "leaf"; }; in expectFailure toplevel "Found duplicated packages in closure for dependency 'leaf'"; }