blob: 3890df40cb7421161719cfb5b9151824f6158fb3 (
plain) (
blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
|
{ 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)
];
};
# multi-output derivation with dependency on itself must not crash
cyclic-dependencies =
generatePythonPackage {
pname = "cyclic-dependencies";
preFixup = ''
propagatedBuildInputs+=("$out")
'';
};
# 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'";
/*
Transitive conflict with multiple dependency chains leading to the
conflicting package.
Test sets up this dependency tree:
toplevel
├── dep1
│ └── leaf
├── dep2
│ └── leaf
└── dep3
└── leaf (customized version -> conflicting)
*/
catches-conflict-multiple-chains = let
# package depending on dependency1, dependency2 and dependency3
toplevel = generatePythonPackage {
pname = "catches-conflict-multiple-chains";
propagatedBuildInputs = [ dep1 dep2 dep3 ];
};
# dep1 package depending on leaf
dep1 = generatePythonPackage {
pname = "dependency1";
propagatedBuildInputs = [ leaf ];
};
# dep2 package depending on leaf
dep2 = generatePythonPackage {
pname = "dependency2";
propagatedBuildInputs = [ leaf ];
};
# dep3 package depending on conflicting version of leaf
dep3 = generatePythonPackage {
pname = "dependency3";
propagatedBuildInputs = [ (customize leaf) ];
};
# some leaf package
leaf = generatePythonPackage {
pname = "leaf";
};
in
expectFailure toplevel "Found duplicated packages in closure for dependency 'leaf'";
}
|