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