1# Generic builder.
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 pypaBuildHook,
16 pypaInstallHook,
17 pythonCatchConflictsHook,
18 pythonImportsCheckHook,
19 pythonNamespacesHook,
20 pythonOutputDistHook,
21 pythonRelaxDepsHook,
22 pythonRemoveBinBytecodeHook,
23 pythonRemoveTestsDirHook,
24 pythonRuntimeDepsCheckHook,
25 setuptoolsBuildHook,
26 wheelUnpackHook,
27 eggUnpackHook,
28 eggBuildHook,
29 eggInstallHook,
30}:
31
32let
33 inherit (builtins) unsafeGetAttrPos;
34 inherit (lib)
35 elem
36 extendDerivation
37 fixedWidthString
38 flip
39 getName
40 hasSuffix
41 head
42 isBool
43 max
44 optional
45 optionalAttrs
46 optionals
47 optionalString
48 removePrefix
49 splitString
50 stringLength
51 ;
52
53 getOptionalAttrs =
54 names: attrs: lib.getAttrs (lib.intersectLists names (lib.attrNames attrs)) attrs;
55
56 leftPadName =
57 name: against:
58 let
59 len = max (stringLength name) (stringLength against);
60 in
61 fixedWidthString len " " name;
62
63 isPythonModule =
64 drv:
65 # all pythonModules have the pythonModule attribute
66 (drv ? "pythonModule")
67 # Some pythonModules are turned in to a pythonApplication by setting the field to false
68 && (!isBool drv.pythonModule);
69
70 isMismatchedPython = drv: drv.pythonModule != python;
71
72 withDistOutput' = flip elem [
73 "pyproject"
74 "setuptools"
75 "wheel"
76 ];
77
78 isBootstrapInstallPackage' = flip elem [
79 "flit-core"
80 "installer"
81 ];
82
83 isBootstrapPackage' = flip elem (
84 [
85 "build"
86 "packaging"
87 "pyproject-hooks"
88 "wheel"
89 ]
90 ++ optionals (python.pythonOlder "3.11") [
91 "tomli"
92 ]
93 );
94
95 isSetuptoolsDependency' = flip elem [
96 "setuptools"
97 "wheel"
98 ];
99
100 cleanAttrs = flip removeAttrs [
101 "disabled"
102 "checkPhase"
103 "checkInputs"
104 "nativeCheckInputs"
105 "doCheck"
106 "doInstallCheck"
107 "pyproject"
108 "format"
109 "disabledTestMarks"
110 "disabledTestPaths"
111 "disabledTests"
112 "enabledTestMarks"
113 "enabledTestPaths"
114 "enabledTests"
115 "pytestFlags"
116 "pytestFlagsArray"
117 "unittestFlags"
118 "unittestFlagsArray"
119 "outputs"
120 "stdenv"
121 "dependencies"
122 "optional-dependencies"
123 "build-system"
124 ];
125
126in
127
128{
129 # Build-time dependencies for the package
130 nativeBuildInputs ? [ ],
131
132 # Run-time dependencies for the package
133 buildInputs ? [ ],
134
135 # Dependencies needed for running the checkPhase.
136 # These are added to buildInputs when doCheck = true.
137 checkInputs ? [ ],
138 nativeCheckInputs ? [ ],
139
140 # propagate build dependencies so in case we have A -> B -> C,
141 # C can import package A propagated by B
142 propagatedBuildInputs ? [ ],
143
144 # Python module dependencies.
145 # These are named after PEP-621.
146 dependencies ? [ ],
147 optional-dependencies ? { },
148
149 # Python PEP-517 build systems.
150 build-system ? [ ],
151
152 # DEPRECATED: use propagatedBuildInputs
153 pythonPath ? [ ],
154
155 # Enabled to detect some (native)BuildInputs mistakes
156 strictDeps ? true,
157
158 outputs ? [ "out" ],
159
160 # used to disable derivation, useful for specific python versions
161 disabled ? false,
162
163 # Raise an error if two packages are installed with the same name
164 # TODO: For cross we probably need a different PYTHONPATH, or not
165 # add the runtime deps until after buildPhase.
166 catchConflicts ? (python.stdenv.hostPlatform == python.stdenv.buildPlatform),
167
168 # Additional arguments to pass to the makeWrapper function, which wraps
169 # generated binaries.
170 makeWrapperArgs ? [ ],
171
172 # Skip wrapping of python programs altogether
173 dontWrapPythonPrograms ? false,
174
175 # Don't use Pip to install a wheel
176 # Note this is actually a variable for the pipInstallPhase in pip's setupHook.
177 # It's included here to prevent an infinite recursion.
178 dontUsePipInstall ? false,
179
180 # Skip setting the PYTHONNOUSERSITE environment variable in wrapped programs
181 permitUserSite ? false,
182
183 # Remove bytecode from bin folder.
184 # When a Python script has the extension `.py`, bytecode is generated
185 # Typically, executables in bin have no extension, so no bytecode is generated.
186 # However, some packages do provide executables with extensions, and thus bytecode is generated.
187 removeBinBytecode ? true,
188
189 # pyproject = true <-> format = "pyproject"
190 # pyproject = false <-> format = "other"
191 # https://github.com/NixOS/nixpkgs/issues/253154
192 pyproject ? null,
193
194 # Several package formats are supported.
195 # "setuptools" : Install a common setuptools/distutils based package. This builds a wheel.
196 # "wheel" : Install from a pre-compiled wheel.
197 # "pyproject": Install a package using a ``pyproject.toml`` file (PEP517). This builds a wheel.
198 # "egg": Install a package from an egg.
199 # "other" : Provide your own buildPhase and installPhase.
200 format ? null,
201
202 meta ? { },
203
204 doCheck ? true,
205
206 # Allow passing in a custom stdenv to buildPython*
207 stdenv ? python.stdenv,
208
209 ...
210}@attrs:
211
212let
213 # Keep extra attributes from `attrs`, e.g., `patchPhase', etc.
214 self = stdenv.mkDerivation (
215 finalAttrs:
216 let
217 format' =
218 assert (pyproject != null) -> (format == null);
219 if pyproject != null then
220 if pyproject then "pyproject" else "other"
221 else if format != null then
222 format
223 else
224 "setuptools";
225
226 withDistOutput = withDistOutput' format';
227
228 validatePythonMatches =
229 let
230 throwMismatch =
231 attrName: drv:
232 let
233 myName = "'${finalAttrs.name}'";
234 theirName = "'${drv.name}'";
235 optionalLocation =
236 let
237 pos = unsafeGetAttrPos (if attrs ? "pname" then "pname" else "name") attrs;
238 in
239 optionalString (pos != null) " at ${pos.file}:${toString pos.line}:${toString pos.column}";
240 in
241 throw ''
242 Python version mismatch in ${myName}:
243
244 The Python derivation ${myName} depends on a Python derivation
245 named ${theirName}, but the two derivations use different versions
246 of Python:
247
248 ${leftPadName myName theirName} uses ${python}
249 ${leftPadName theirName myName} uses ${toString drv.pythonModule}
250
251 Possible solutions:
252
253 * If ${theirName} is a Python library, change the reference to ${theirName}
254 in the ${attrName} of ${myName} to use a ${theirName} built from the same
255 version of Python
256
257 * If ${theirName} is used as a tool during the build, move the reference to
258 ${theirName} in ${myName} from ${attrName} to nativeBuildInputs
259
260 * If ${theirName} provides executables that are called at run time, pass its
261 bin path to makeWrapperArgs:
262
263 makeWrapperArgs = [ "--prefix PATH : ''${lib.makeBinPath [ ${getName drv} ] }" ];
264
265 ${optionalLocation}
266 '';
267
268 checkDrv =
269 attrName: drv:
270 if (isPythonModule drv) && (isMismatchedPython drv) then throwMismatch attrName drv else drv;
271
272 in
273 attrName: inputs: map (checkDrv attrName) inputs;
274
275 isBootstrapInstallPackage = isBootstrapInstallPackage' (attrs.pname or null);
276
277 isBootstrapPackage = isBootstrapInstallPackage || isBootstrapPackage' (attrs.pname or null);
278
279 isSetuptoolsDependency = isSetuptoolsDependency' (attrs.pname or null);
280
281 in
282 (cleanAttrs attrs)
283 // {
284
285 name = namePrefix + attrs.name or "${finalAttrs.pname}-${finalAttrs.version}";
286
287 inherit catchConflicts;
288
289 nativeBuildInputs =
290 [
291 python
292 wrapPython
293 ensureNewerSourcesForZipFilesHook # move to wheel installer (pip) or builder (setuptools, flit, ...)?
294 pythonRemoveTestsDirHook
295 ]
296 ++ optionals (finalAttrs.catchConflicts && !isBootstrapPackage && !isSetuptoolsDependency) [
297 #
298 # 1. When building a package that is also part of the bootstrap chain, we
299 # must ignore conflicts after installation, because there will be one with
300 # the package in the bootstrap.
301 #
302 # 2. When a package is a dependency of setuptools, we must ignore conflicts
303 # because the hook that checks for conflicts uses setuptools.
304 #
305 pythonCatchConflictsHook
306 ]
307 ++ optionals (attrs ? pythonRelaxDeps || attrs ? pythonRemoveDeps) [
308 pythonRelaxDepsHook
309 ]
310 ++ optionals removeBinBytecode [
311 pythonRemoveBinBytecodeHook
312 ]
313 ++ optionals (hasSuffix "zip" (attrs.src.name or "")) [
314 unzip
315 ]
316 ++ optionals (format' == "setuptools") [
317 setuptoolsBuildHook
318 ]
319 ++ optionals (format' == "pyproject") [
320 (
321 if isBootstrapPackage then
322 pypaBuildHook.override {
323 inherit (python.pythonOnBuildForHost.pkgs.bootstrap) build;
324 wheel = null;
325 }
326 else
327 pypaBuildHook
328 )
329 (
330 if isBootstrapPackage then
331 pythonRuntimeDepsCheckHook.override {
332 inherit (python.pythonOnBuildForHost.pkgs.bootstrap) packaging;
333 }
334 else
335 pythonRuntimeDepsCheckHook
336 )
337 ]
338 ++ optionals (format' == "wheel") [
339 wheelUnpackHook
340 ]
341 ++ optionals (format' == "egg") [
342 eggUnpackHook
343 eggBuildHook
344 eggInstallHook
345 ]
346 ++ optionals (format' != "other") [
347 (
348 if isBootstrapInstallPackage then
349 pypaInstallHook.override {
350 inherit (python.pythonOnBuildForHost.pkgs.bootstrap) installer;
351 }
352 else
353 pypaInstallHook
354 )
355 ]
356 ++ optionals (stdenv.buildPlatform == stdenv.hostPlatform) [
357 # This is a test, however, it should be ran independent of the checkPhase and checkInputs
358 pythonImportsCheckHook
359 ]
360 ++ optionals (python.pythonAtLeast "3.3") [
361 # Optionally enforce PEP420 for python3
362 pythonNamespacesHook
363 ]
364 ++ optionals withDistOutput [
365 pythonOutputDistHook
366 ]
367 ++ nativeBuildInputs
368 ++ build-system;
369
370 buildInputs = validatePythonMatches "buildInputs" (buildInputs ++ pythonPath);
371
372 propagatedBuildInputs = validatePythonMatches "propagatedBuildInputs" (
373 propagatedBuildInputs
374 ++ dependencies
375 ++ [
376 # we propagate python even for packages transformed with 'toPythonApplication'
377 # this pollutes the PATH but avoids rebuilds
378 # see https://github.com/NixOS/nixpkgs/issues/170887 for more context
379 python
380 ]
381 );
382
383 inherit strictDeps;
384
385 LANG = "${if python.stdenv.hostPlatform.isDarwin then "en_US" else "C"}.UTF-8";
386
387 # Python packages don't have a checkPhase, only an installCheckPhase
388 doCheck = false;
389 doInstallCheck = attrs.doCheck or true;
390 nativeInstallCheckInputs = nativeCheckInputs;
391 installCheckInputs = checkInputs;
392
393 inherit dontWrapPythonPrograms;
394
395 postFixup =
396 optionalString (!finalAttrs.dontWrapPythonPrograms) ''
397 wrapPythonPrograms
398 ''
399 + attrs.postFixup or "";
400
401 # Python packages built through cross-compilation are always for the host platform.
402 disallowedReferences = optionals (python.stdenv.hostPlatform != python.stdenv.buildPlatform) [
403 python.pythonOnBuildForHost
404 ];
405
406 outputs = outputs ++ optional withDistOutput "dist";
407
408 passthru =
409 {
410 inherit disabled;
411 }
412 // {
413 updateScript =
414 let
415 filename = head (splitString ":" finalAttrs.finalPackage.meta.position);
416 in
417 [
418 update-python-libraries
419 filename
420 ];
421 }
422 // optionalAttrs (dependencies != [ ]) {
423 inherit dependencies;
424 }
425 // optionalAttrs (optional-dependencies != { }) {
426 inherit optional-dependencies;
427 }
428 // optionalAttrs (build-system != [ ]) {
429 inherit build-system;
430 }
431 // attrs.passthru or { };
432
433 meta = {
434 # default to python's platforms
435 platforms = python.meta.platforms;
436 isBuildPythonPackage = python.meta.platforms;
437 } // meta;
438 }
439 // optionalAttrs (attrs ? checkPhase) {
440 # If given use the specified checkPhase, otherwise use the setup hook.
441 # Longer-term we should get rid of `checkPhase` and use `installCheckPhase`.
442 installCheckPhase = attrs.checkPhase;
443 }
444 // optionalAttrs (attrs.doCheck or true) (
445 getOptionalAttrs [
446 "disabledTestMarks"
447 "disabledTestPaths"
448 "disabledTests"
449 "pytestFlags"
450 "pytestFlagsArray"
451 "unittestFlags"
452 "unittestFlagsArray"
453 ] attrs
454 //
455 lib.mapAttrs
456 (
457 name: value:
458 lib.throwIf (
459 attrs.${name} == [ ]
460 ) "${lib.getName finalAttrs}: ${name} must be unspecified, null or a non-empty list." attrs.${name}
461 )
462 (
463 getOptionalAttrs [
464 "enabledTestMarks"
465 "enabledTestPaths"
466 "enabledTests"
467 ] attrs
468 )
469 )
470 );
471
472 # This derivation transformation function must be independent to `attrs`
473 # for fixed-point arguments support in the future.
474 transformDrv =
475 drv:
476 extendDerivation (
477 drv.disabled
478 -> throw "${removePrefix namePrefix drv.name} not supported for interpreter ${python.executable}"
479 ) { } (toPythonModule drv);
480
481in
482transformDrv self