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