1{ pythonOnBuildForHost, runCommand, writeShellScript, coreutils, gnugrep }: let
2
3 pythonPkgs = pythonOnBuildForHost.pkgs;
4
5 ### UTILITIES
6
7 # customize a package so that its store paths differs
8 customize = pkg: pkg.overrideAttrs { some_modification = true; };
9
10 # generates minimal pyproject.toml
11 pyprojectToml = pname: builtins.toFile "pyproject.toml" ''
12 [project]
13 name = "${pname}"
14 version = "1.0.0"
15 '';
16
17 # generates source for a python project
18 projectSource = pname: runCommand "my-project-source" {} ''
19 mkdir -p $out/src
20 cp ${pyprojectToml pname} $out/pyproject.toml
21 touch $out/src/__init__.py
22 '';
23
24 # helper to reduce boilerplate
25 generatePythonPackage = args: pythonPkgs.buildPythonPackage (
26 {
27 version = "1.0.0";
28 src = runCommand "my-project-source" {} ''
29 mkdir -p $out/src
30 cp ${pyprojectToml args.pname} $out/pyproject.toml
31 touch $out/src/__init__.py
32 '';
33 pyproject = true;
34 catchConflicts = true;
35 buildInputs = [ pythonPkgs.setuptools ];
36 }
37 // args
38 );
39
40 # in order to test for a failing build, wrap it in a shell script
41 expectFailure = build: errorMsg: build.overrideDerivation (old: {
42 builder = writeShellScript "test-for-failure" ''
43 export PATH=${coreutils}/bin:${gnugrep}/bin:$PATH
44 ${old.builder} "$@" > ./log 2>&1
45 status=$?
46 cat ./log
47 if [ $status -eq 0 ] || ! grep -q "${errorMsg}" ./log; then
48 echo "The build should have failed with '${errorMsg}', but it didn't"
49 exit 1
50 else
51 echo "The build failed as expected with: ${errorMsg}"
52 mkdir -p $out
53 fi
54 '';
55 });
56in {
57
58 ### TEST CASES
59
60 # Test case which must not trigger any conflicts.
61 # This derivation has runtime dependencies on custom versions of multiple build tools.
62 # This scenario is relevant for lang2nix tools which do not override the nixpkgs fix-point.
63 # see https://github.com/NixOS/nixpkgs/issues/283695
64 ignores-build-time-deps =
65 generatePythonPackage {
66 pname = "ignores-build-time-deps";
67 buildInputs = [
68 pythonPkgs.build
69 pythonPkgs.packaging
70 pythonPkgs.setuptools
71 pythonPkgs.wheel
72 ];
73 propagatedBuildInputs = [
74 # Add customized versions of build tools as runtime deps
75 (customize pythonPkgs.packaging)
76 (customize pythonPkgs.setuptools)
77 (customize pythonPkgs.wheel)
78 ];
79 };
80
81 # multi-output derivation with dependency on itself must not crash
82 cyclic-dependencies =
83 generatePythonPackage {
84 pname = "cyclic-dependencies";
85 preFixup = ''
86 propagatedBuildInputs+=("$out")
87 '';
88 };
89
90 # Simplest test case that should trigger a conflict
91 catches-simple-conflict = let
92 # this build must fail due to conflicts
93 package = pythonPkgs.buildPythonPackage rec {
94 pname = "catches-simple-conflict";
95 version = "0.0.0";
96 src = projectSource pname;
97 pyproject = true;
98 catchConflicts = true;
99 buildInputs = [
100 pythonPkgs.setuptools
101 ];
102 # depend on two different versions of packaging
103 # (an actual runtime dependency conflict)
104 propagatedBuildInputs = [
105 pythonPkgs.packaging
106 (customize pythonPkgs.packaging)
107 ];
108 };
109 in
110 expectFailure package "Found duplicated packages in closure for dependency 'packaging'";
111
112
113 /*
114 More complex test case with a transitive conflict
115
116 Test sets up this dependency tree:
117
118 toplevel
119 ├── dep1
120 │ └── leaf
121 └── dep2
122 └── leaf (customized version -> conflicting)
123 */
124 catches-transitive-conflict = let
125 # package depending on both dependency1 and dependency2
126 toplevel = generatePythonPackage {
127 pname = "catches-transitive-conflict";
128 propagatedBuildInputs = [ dep1 dep2 ];
129 };
130 # dep1 package depending on leaf
131 dep1 = generatePythonPackage {
132 pname = "dependency1";
133 propagatedBuildInputs = [ leaf ];
134 };
135 # dep2 package depending on conflicting version of leaf
136 dep2 = generatePythonPackage {
137 pname = "dependency2";
138 propagatedBuildInputs = [ (customize leaf) ];
139 };
140 # some leaf package
141 leaf = generatePythonPackage {
142 pname = "leaf";
143 };
144 in
145 expectFailure toplevel "Found duplicated packages in closure for dependency 'leaf'";
146
147 /*
148 Transitive conflict with multiple dependency chains leading to the
149 conflicting package.
150
151 Test sets up this dependency tree:
152
153 toplevel
154 ├── dep1
155 │ └── leaf
156 ├── dep2
157 │ └── leaf
158 └── dep3
159 └── leaf (customized version -> conflicting)
160 */
161 catches-conflict-multiple-chains = let
162 # package depending on dependency1, dependency2 and dependency3
163 toplevel = generatePythonPackage {
164 pname = "catches-conflict-multiple-chains";
165 propagatedBuildInputs = [ dep1 dep2 dep3 ];
166 };
167 # dep1 package depending on leaf
168 dep1 = generatePythonPackage {
169 pname = "dependency1";
170 propagatedBuildInputs = [ leaf ];
171 };
172 # dep2 package depending on leaf
173 dep2 = generatePythonPackage {
174 pname = "dependency2";
175 propagatedBuildInputs = [ leaf ];
176 };
177 # dep3 package depending on conflicting version of leaf
178 dep3 = generatePythonPackage {
179 pname = "dependency3";
180 propagatedBuildInputs = [ (customize leaf) ];
181 };
182 # some leaf package
183 leaf = generatePythonPackage {
184 pname = "leaf";
185 };
186 in
187 expectFailure toplevel "Found duplicated packages in closure for dependency 'leaf'";
188}