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