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