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