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, setuptoolsBuildHook
23, setuptoolsCheckHook
24, wheelUnpackHook
25, eggUnpackHook
26, eggBuildHook
27, eggInstallHook
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# pyproject = true <-> format = "pyproject"
85# pyproject = false <-> format = "other"
86# https://github.com/NixOS/nixpkgs/issues/253154
87, pyproject ? null
88
89# Several package formats are supported.
90# "setuptools" : Install a common setuptools/distutils based package. This builds a wheel.
91# "wheel" : Install from a pre-compiled wheel.
92# "pyproject": Install a package using a ``pyproject.toml`` file (PEP517). This builds a wheel.
93# "egg": Install a package from an egg.
94# "other" : Provide your own buildPhase and installPhase.
95, format ? null
96
97, meta ? {}
98
99, passthru ? {}
100
101, doCheck ? config.doCheckByDefault or false
102
103, disabledTestPaths ? []
104
105# Allow passing in a custom stdenv to buildPython*
106, stdenv ? python.stdenv
107
108, ... } @ attrs:
109
110assert (pyproject != null) -> (format == null);
111
112let
113 format' =
114 if pyproject != null then
115 if pyproject then
116 "pyproject"
117 else
118 "other"
119 else if format != null then
120 format
121 else
122 "setuptools";
123
124 withDistOutput = lib.elem format' ["pyproject" "setuptools" "wheel"];
125
126 name_ = name;
127
128 validatePythonMatches = attrName: let
129 isPythonModule = drv:
130 # all pythonModules have the pythonModule attribute
131 (drv ? "pythonModule")
132 # Some pythonModules are turned in to a pythonApplication by setting the field to false
133 && (!builtins.isBool drv.pythonModule);
134 isMismatchedPython = drv: drv.pythonModule != python;
135
136 optionalLocation = let
137 pos = builtins.unsafeGetAttrPos (if attrs ? "pname" then "pname" else "name") attrs;
138 in lib.optionalString (pos != null) " at ${pos.file}:${toString pos.line}:${toString pos.column}";
139
140 leftPadName = name: against: let
141 len = lib.max (lib.stringLength name) (lib.stringLength against);
142 in lib.strings.fixedWidthString len " " name;
143
144 throwMismatch = drv: let
145 myName = "'${namePrefix}${name}'";
146 theirName = "'${drv.name}'";
147 in throw ''
148 Python version mismatch in ${myName}:
149
150 The Python derivation ${myName} depends on a Python derivation
151 named ${theirName}, but the two derivations use different versions
152 of Python:
153
154 ${leftPadName myName theirName} uses ${python}
155 ${leftPadName theirName myName} uses ${toString drv.pythonModule}
156
157 Possible solutions:
158
159 * If ${theirName} is a Python library, change the reference to ${theirName}
160 in the ${attrName} of ${myName} to use a ${theirName} built from the same
161 version of Python
162
163 * If ${theirName} is used as a tool during the build, move the reference to
164 ${theirName} in ${myName} from ${attrName} to nativeBuildInputs
165
166 * If ${theirName} provides executables that are called at run time, pass its
167 bin path to makeWrapperArgs:
168
169 makeWrapperArgs = [ "--prefix PATH : ''${lib.makeBinPath [ ${lib.getName drv } ] }" ];
170
171 ${optionalLocation}
172 '';
173
174 checkDrv = drv:
175 if (isPythonModule drv) && (isMismatchedPython drv)
176 then throwMismatch drv
177 else drv;
178
179 in inputs: builtins.map (checkDrv) inputs;
180
181 isBootstrapInstallPackage = builtins.elem (attrs.pname or null) [
182 "flit-core" "installer"
183 ];
184
185 isBootstrapPackage = isBootstrapInstallPackage || builtins.elem (attrs.pname or null) ([
186 "build" "packaging" "pyproject-hooks" "wheel"
187 ] ++ lib.optionals (python.pythonOlder "3.11") [
188 "tomli"
189 ]);
190
191 isSetuptoolsDependency = builtins.elem (attrs.pname or null) [
192 "setuptools" "wheel"
193 ];
194
195 # Keep extra attributes from `attrs`, e.g., `patchPhase', etc.
196 self = toPythonModule (stdenv.mkDerivation ((builtins.removeAttrs attrs [
197 "disabled" "checkPhase" "checkInputs" "nativeCheckInputs" "doCheck" "doInstallCheck" "dontWrapPythonPrograms" "catchConflicts" "pyproject" "format"
198 "disabledTestPaths" "outputs" "stdenv"
199 ]) // {
200
201 name = namePrefix + name_;
202
203 nativeBuildInputs = [
204 python
205 wrapPython
206 ensureNewerSourcesForZipFilesHook # move to wheel installer (pip) or builder (setuptools, flit, ...)?
207 pythonRemoveTestsDirHook
208 ] ++ lib.optionals (catchConflicts && !isBootstrapPackage && !isSetuptoolsDependency) [
209 #
210 # 1. When building a package that is also part of the bootstrap chain, we
211 # must ignore conflicts after installation, because there will be one with
212 # the package in the bootstrap.
213 #
214 # 2. When a package is a dependency of setuptools, we must ignore conflicts
215 # because the hook that checks for conflicts uses setuptools.
216 #
217 pythonCatchConflictsHook
218 ] ++ lib.optionals removeBinBytecode [
219 pythonRemoveBinBytecodeHook
220 ] ++ lib.optionals (lib.hasSuffix "zip" (attrs.src.name or "")) [
221 unzip
222 ] ++ lib.optionals (format' == "setuptools") [
223 setuptoolsBuildHook
224 ] ++ lib.optionals (format' == "pyproject") [(
225 if isBootstrapPackage then
226 pypaBuildHook.override {
227 inherit (python.pythonOnBuildForHost.pkgs.bootstrap) build;
228 wheel = null;
229 }
230 else
231 pypaBuildHook
232 )] ++ lib.optionals (format' == "wheel") [
233 wheelUnpackHook
234 ] ++ lib.optionals (format' == "egg") [
235 eggUnpackHook eggBuildHook eggInstallHook
236 ] ++ lib.optionals (format' != "other") [(
237 if isBootstrapInstallPackage then
238 pypaInstallHook.override {
239 inherit (python.pythonOnBuildForHost.pkgs.bootstrap) installer;
240 }
241 else
242 pypaInstallHook
243 )] ++ lib.optionals (stdenv.buildPlatform == stdenv.hostPlatform) [
244 # This is a test, however, it should be ran independent of the checkPhase and checkInputs
245 pythonImportsCheckHook
246 ] ++ lib.optionals (python.pythonAtLeast "3.3") [
247 # Optionally enforce PEP420 for python3
248 pythonNamespacesHook
249 ] ++ lib.optionals withDistOutput [
250 pythonOutputDistHook
251 ] ++ nativeBuildInputs;
252
253 buildInputs = validatePythonMatches "buildInputs" (buildInputs ++ pythonPath);
254
255 propagatedBuildInputs = validatePythonMatches "propagatedBuildInputs" (propagatedBuildInputs ++ [
256 # we propagate python even for packages transformed with 'toPythonApplication'
257 # this pollutes the PATH but avoids rebuilds
258 # see https://github.com/NixOS/nixpkgs/issues/170887 for more context
259 python
260 ]);
261
262 inherit strictDeps;
263
264 LANG = "${if python.stdenv.isDarwin then "en_US" else "C"}.UTF-8";
265
266 # Python packages don't have a checkPhase, only an installCheckPhase
267 doCheck = false;
268 doInstallCheck = attrs.doCheck or true;
269 nativeInstallCheckInputs = [
270 ] ++ lib.optionals (format' == "setuptools") [
271 # Longer-term we should get rid of this and require
272 # users of this function to set the `installCheckPhase` or
273 # pass in a hook that sets it.
274 setuptoolsCheckHook
275 ] ++ nativeCheckInputs;
276 installCheckInputs = checkInputs;
277
278 postFixup = lib.optionalString (!dontWrapPythonPrograms) ''
279 wrapPythonPrograms
280 '' + attrs.postFixup or "";
281
282 # Python packages built through cross-compilation are always for the host platform.
283 disallowedReferences = lib.optionals (python.stdenv.hostPlatform != python.stdenv.buildPlatform) [ python.pythonOnBuildForHost ];
284
285 outputs = outputs ++ lib.optional withDistOutput "dist";
286
287 meta = {
288 # default to python's platforms
289 platforms = python.meta.platforms;
290 isBuildPythonPackage = python.meta.platforms;
291 } // meta;
292 } // lib.optionalAttrs (attrs?checkPhase) {
293 # If given use the specified checkPhase, otherwise use the setup hook.
294 # Longer-term we should get rid of `checkPhase` and use `installCheckPhase`.
295 installCheckPhase = attrs.checkPhase;
296 } // lib.optionalAttrs (disabledTestPaths != []) {
297 disabledTestPaths = lib.escapeShellArgs disabledTestPaths;
298 }));
299
300 passthru.updateScript = let
301 filename = builtins.head (lib.splitString ":" self.meta.position);
302 in attrs.passthru.updateScript or [ update-python-libraries filename ];
303in lib.extendDerivation
304 (disabled -> throw "${name} not supported for interpreter ${python.executable}")
305 passthru
306 self