1# Generic builder only used for EOL and deprecated Python 2.
2
3{
4 lib,
5 config,
6 python,
7 wrapPython,
8 unzip,
9 ensureNewerSourcesForZipFilesHook,
10 # Whether the derivation provides a Python module or not.
11 toPythonModule,
12 namePrefix,
13 update-python-libraries,
14 setuptools,
15 pipBuildHook,
16 pipInstallHook,
17 pythonCatchConflictsHook,
18 pythonImportsCheckHook,
19 pythonOutputDistHook,
20 pythonRemoveBinBytecodeHook,
21 pythonRemoveTestsDirHook,
22 setuptoolsBuildHook,
23 wheelUnpackHook,
24 eggUnpackHook,
25 eggBuildHook,
26 eggInstallHook,
27}:
28
29{
30 name ? "${attrs.pname}-${attrs.version}",
31
32 # Build-time dependencies for the package
33 nativeBuildInputs ? [ ],
34
35 # Run-time dependencies for the package
36 buildInputs ? [ ],
37
38 # Dependencies needed for running the checkPhase.
39 # These are added to buildInputs when doCheck = true.
40 checkInputs ? [ ],
41 nativeCheckInputs ? [ ],
42
43 # propagate build dependencies so in case we have A -> B -> C,
44 # C can import package A propagated by B
45 propagatedBuildInputs ? [ ],
46
47 # DEPRECATED: use propagatedBuildInputs
48 pythonPath ? [ ],
49
50 # Enabled to detect some (native)BuildInputs mistakes
51 strictDeps ? true,
52
53 outputs ? [ "out" ],
54
55 # used to disable derivation, useful for specific python versions
56 disabled ? false,
57
58 # Raise an error if two packages are installed with the same name
59 # TODO: For cross we probably need a different PYTHONPATH, or not
60 # add the runtime deps until after buildPhase.
61 catchConflicts ? (python.stdenv.hostPlatform == python.stdenv.buildPlatform),
62
63 # Additional arguments to pass to the makeWrapper function, which wraps
64 # generated binaries.
65 makeWrapperArgs ? [ ],
66
67 # Skip wrapping of python programs altogether
68 dontWrapPythonPrograms ? false,
69
70 # Don't use Pip to install a wheel
71 # Note this is actually a variable for the pipInstallPhase in pip's setupHook.
72 # It's included here to prevent an infinite recursion.
73 dontUsePipInstall ? false,
74
75 # Skip setting the PYTHONNOUSERSITE environment variable in wrapped programs
76 permitUserSite ? false,
77
78 # Remove bytecode from bin folder.
79 # When a Python script has the extension `.py`, bytecode is generated
80 # Typically, executables in bin have no extension, so no bytecode is generated.
81 # However, some packages do provide executables with extensions, and thus bytecode is generated.
82 removeBinBytecode ? true,
83
84 # Several package formats are supported.
85 # "setuptools" : Install a common setuptools/distutils based package. This builds a wheel.
86 # "wheel" : Install from a pre-compiled wheel.
87 # "pyproject": Install a package using a ``pyproject.toml`` file (PEP517). This builds a wheel.
88 # "egg": Install a package from an egg.
89 # "other" : Provide your own buildPhase and installPhase.
90 format ? "setuptools",
91
92 meta ? { },
93
94 passthru ? { },
95
96 doCheck ? true,
97
98 disabledTestPaths ? [ ],
99
100 ...
101}@attrs:
102
103let
104 inherit (python) stdenv;
105
106 withDistOutput = lib.elem format [
107 "pyproject"
108 "setuptools"
109 "wheel"
110 ];
111
112 name_ = name;
113
114 validatePythonMatches =
115 attrName:
116 let
117 isPythonModule =
118 drv:
119 # all pythonModules have the pythonModule attribute
120 (drv ? "pythonModule")
121 # Some pythonModules are turned in to a pythonApplication by setting the field to false
122 && (!builtins.isBool drv.pythonModule);
123 isMismatchedPython = drv: drv.pythonModule != python;
124
125 optionalLocation =
126 let
127 pos = builtins.unsafeGetAttrPos (if attrs ? "pname" then "pname" else "name") attrs;
128 in
129 lib.optionalString (pos != null) " at ${pos.file}:${toString pos.line}:${toString pos.column}";
130
131 leftPadName =
132 name: against:
133 let
134 len = lib.max (lib.stringLength name) (lib.stringLength against);
135 in
136 lib.strings.fixedWidthString len " " name;
137
138 throwMismatch =
139 drv:
140 let
141 myName = "'${namePrefix}${name}'";
142 theirName = "'${drv.name}'";
143 in
144 throw ''
145 Python version mismatch in ${myName}:
146
147 The Python derivation ${myName} depends on a Python derivation
148 named ${theirName}, but the two derivations use different versions
149 of Python:
150
151 ${leftPadName myName theirName} uses ${python}
152 ${leftPadName theirName myName} uses ${toString drv.pythonModule}
153
154 Possible solutions:
155
156 * If ${theirName} is a Python library, change the reference to ${theirName}
157 in the ${attrName} of ${myName} to use a ${theirName} built from the same
158 version of Python
159
160 * If ${theirName} is used as a tool during the build, move the reference to
161 ${theirName} in ${myName} from ${attrName} to nativeBuildInputs
162
163 * If ${theirName} provides executables that are called at run time, pass its
164 bin path to makeWrapperArgs:
165
166 makeWrapperArgs = [ "--prefix PATH : ''${lib.makeBinPath [ ${lib.getName drv} ] }" ];
167
168 ${optionalLocation}
169 '';
170
171 checkDrv = drv: if (isPythonModule drv) && (isMismatchedPython drv) then throwMismatch drv else drv;
172
173 in
174 inputs: builtins.map (checkDrv) inputs;
175
176 # Keep extra attributes from `attrs`, e.g., `patchPhase', etc.
177 self = toPythonModule (
178 stdenv.mkDerivation (
179 (builtins.removeAttrs attrs [
180 "disabled"
181 "checkPhase"
182 "checkInputs"
183 "nativeCheckInputs"
184 "doCheck"
185 "doInstallCheck"
186 "dontWrapPythonPrograms"
187 "catchConflicts"
188 "format"
189 "disabledTestPaths"
190 "outputs"
191 ])
192 // {
193
194 name = namePrefix + name_;
195
196 nativeBuildInputs = [
197 python
198 wrapPython
199 ensureNewerSourcesForZipFilesHook # move to wheel installer (pip) or builder (setuptools, ...)?
200 pythonRemoveTestsDirHook
201 ]
202 ++ lib.optionals catchConflicts [
203 pythonCatchConflictsHook
204 ]
205 ++ lib.optionals removeBinBytecode [
206 pythonRemoveBinBytecodeHook
207 ]
208 ++ lib.optionals (lib.hasSuffix "zip" (attrs.src.name or "")) [
209 unzip
210 ]
211 ++ lib.optionals (format == "setuptools") [
212 setuptoolsBuildHook
213 ]
214 ++ lib.optionals (format == "pyproject") [
215 (pipBuildHook)
216 ]
217 ++ lib.optionals (format == "wheel") [
218 wheelUnpackHook
219 ]
220 ++ lib.optionals (format == "egg") [
221 eggUnpackHook
222 eggBuildHook
223 eggInstallHook
224 ]
225 ++ lib.optionals (format != "other") [
226 (pipInstallHook)
227 ]
228 ++ lib.optionals (stdenv.buildPlatform == stdenv.hostPlatform) [
229 # This is a test, however, it should be ran independent of the checkPhase and checkInputs
230 pythonImportsCheckHook
231 ]
232 ++ lib.optionals withDistOutput [
233 pythonOutputDistHook
234 ]
235 ++ nativeBuildInputs;
236
237 buildInputs = validatePythonMatches "buildInputs" (buildInputs ++ pythonPath);
238
239 propagatedBuildInputs = validatePythonMatches "propagatedBuildInputs" (
240 propagatedBuildInputs
241 ++ [
242 # we propagate python even for packages transformed with 'toPythonApplication'
243 # this pollutes the PATH but avoids rebuilds
244 # see https://github.com/NixOS/nixpkgs/issues/170887 for more context
245 python
246 ]
247 );
248
249 inherit strictDeps;
250
251 LANG = "${if python.stdenv.hostPlatform.isDarwin then "en_US" else "C"}.UTF-8";
252
253 # Python packages don't have a checkPhase, only an installCheckPhase
254 doCheck = false;
255 doInstallCheck = attrs.doCheck or true;
256 nativeInstallCheckInputs = nativeCheckInputs;
257 installCheckInputs = checkInputs;
258
259 postFixup =
260 lib.optionalString (!dontWrapPythonPrograms) ''
261 wrapPythonPrograms
262 ''
263 + attrs.postFixup or "";
264
265 # Python packages built through cross-compilation are always for the host platform.
266 disallowedReferences = lib.optionals (python.stdenv.hostPlatform != python.stdenv.buildPlatform) [
267 python.pythonOnBuildForHost
268 ];
269
270 outputs = outputs ++ lib.optional withDistOutput "dist";
271
272 meta = {
273 # default to python's platforms
274 platforms = python.meta.platforms;
275 isBuildPythonPackage = python.meta.platforms;
276 }
277 // meta;
278 }
279 // lib.optionalAttrs (attrs ? checkPhase) {
280 # If given use the specified checkPhase, otherwise use the setup hook.
281 # Longer-term we should get rid of `checkPhase` and use `installCheckPhase`.
282 installCheckPhase = attrs.checkPhase;
283 }
284 // lib.optionalAttrs (disabledTestPaths != [ ]) {
285 disabledTestPaths = lib.escapeShellArgs disabledTestPaths;
286 }
287 )
288 );
289
290 passthru.updateScript =
291 let
292 filename = builtins.head (lib.splitString ":" self.meta.position);
293 in
294 attrs.passthru.updateScript or [
295 update-python-libraries
296 filename
297 ];
298in
299lib.extendDerivation (
300 disabled -> throw "${name} not supported for interpreter ${python.executable}"
301) passthru self