summary refs log tree commit diff
path: root/pkgs/development/interpreters/python/mk-python-derivation.nix
blob: c14c6bc096fd3ddf1ae50e22d55eaf288cd26036 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# Generic builder.

{ lib
, config
, python
, wrapPython
, unzip
, ensureNewerSourcesForZipFilesHook
# Whether the derivation provides a Python module or not.
, toPythonModule
, namePrefix
, update-python-libraries
, setuptools
, pypaBuildHook
, pypaInstallHook
, pythonCatchConflictsHook
, pythonImportsCheckHook
, pythonNamespacesHook
, pythonOutputDistHook
, pythonRemoveBinBytecodeHook
, pythonRemoveTestsDirHook
, setuptoolsBuildHook
, setuptoolsCheckHook
, wheelUnpackHook
, eggUnpackHook
, eggBuildHook
, eggInstallHook
}:

{ name ? "${attrs.pname}-${attrs.version}"

# Build-time dependencies for the package
, nativeBuildInputs ? []

# Run-time dependencies for the package
, buildInputs ? []

# Dependencies needed for running the checkPhase.
# These are added to buildInputs when doCheck = true.
, checkInputs ? []
, nativeCheckInputs ? []

# propagate build dependencies so in case we have A -> B -> C,
# C can import package A propagated by B
, propagatedBuildInputs ? []

# DEPRECATED: use propagatedBuildInputs
, pythonPath ? []

# Enabled to detect some (native)BuildInputs mistakes
, strictDeps ? true

, outputs ? [ "out" ]

# used to disable derivation, useful for specific python versions
, disabled ? false

# Raise an error if two packages are installed with the same name
# TODO: For cross we probably need a different PYTHONPATH, or not
# add the runtime deps until after buildPhase.
, catchConflicts ? (python.stdenv.hostPlatform == python.stdenv.buildPlatform)

# Additional arguments to pass to the makeWrapper function, which wraps
# generated binaries.
, makeWrapperArgs ? []

# Skip wrapping of python programs altogether
, dontWrapPythonPrograms ? false

# Don't use Pip to install a wheel
# Note this is actually a variable for the pipInstallPhase in pip's setupHook.
# It's included here to prevent an infinite recursion.
, dontUsePipInstall ? false

# Skip setting the PYTHONNOUSERSITE environment variable in wrapped programs
, permitUserSite ? false

# Remove bytecode from bin folder.
# When a Python script has the extension `.py`, bytecode is generated
# Typically, executables in bin have no extension, so no bytecode is generated.
# However, some packages do provide executables with extensions, and thus bytecode is generated.
, removeBinBytecode ? true

# pyproject = true <-> format = "pyproject"
# pyproject = false <-> format = "other"
# https://github.com/NixOS/nixpkgs/issues/253154
, pyproject ? null

# Several package formats are supported.
# "setuptools" : Install a common setuptools/distutils based package. This builds a wheel.
# "wheel" : Install from a pre-compiled wheel.
# "pyproject": Install a package using a ``pyproject.toml`` file (PEP517). This builds a wheel.
# "egg": Install a package from an egg.
# "other" : Provide your own buildPhase and installPhase.
, format ? null

, meta ? {}

, passthru ? {}

, doCheck ? config.doCheckByDefault or false

, disabledTestPaths ? []

# Allow passing in a custom stdenv to buildPython*
, stdenv ? python.stdenv

, ... } @ attrs:

assert (pyproject != null) -> (format == null);

let
  format' =
    if pyproject != null then
      if pyproject then
        "pyproject"
      else
        "other"
    else if format != null then
      format
    else
      "setuptools";

  withDistOutput = lib.elem format' ["pyproject" "setuptools" "wheel"];

  name_ = name;

  validatePythonMatches = attrName: let
    isPythonModule = drv:
      # all pythonModules have the pythonModule attribute
      (drv ? "pythonModule")
      # Some pythonModules are turned in to a pythonApplication by setting the field to false
      && (!builtins.isBool drv.pythonModule);
    isMismatchedPython = drv: drv.pythonModule != python;

    optionalLocation = let
        pos = builtins.unsafeGetAttrPos (if attrs ? "pname" then "pname" else "name") attrs;
      in lib.optionalString (pos != null) " at ${pos.file}:${toString pos.line}:${toString pos.column}";

    leftPadName = name: against: let
        len = lib.max (lib.stringLength name) (lib.stringLength against);
      in lib.strings.fixedWidthString len " " name;

    throwMismatch = drv: let
      myName = "'${namePrefix}${name}'";
      theirName = "'${drv.name}'";
    in throw ''
      Python version mismatch in ${myName}:

      The Python derivation ${myName} depends on a Python derivation
      named ${theirName}, but the two derivations use different versions
      of Python:

          ${leftPadName myName theirName} uses ${python}
          ${leftPadName theirName myName} uses ${toString drv.pythonModule}

      Possible solutions:

        * If ${theirName} is a Python library, change the reference to ${theirName}
          in the ${attrName} of ${myName} to use a ${theirName} built from the same
          version of Python

        * If ${theirName} is used as a tool during the build, move the reference to
          ${theirName} in ${myName} from ${attrName} to nativeBuildInputs

        * If ${theirName} provides executables that are called at run time, pass its
          bin path to makeWrapperArgs:

              makeWrapperArgs = [ "--prefix PATH : ''${lib.makeBinPath [ ${lib.getName drv } ] }" ];

      ${optionalLocation}
    '';

    checkDrv = drv:
      if (isPythonModule drv) && (isMismatchedPython drv)
      then throwMismatch drv
      else drv;

    in inputs: builtins.map (checkDrv) inputs;

  isBootstrapInstallPackage = builtins.elem (attrs.pname or null) [
    "flit-core" "installer"
  ];

  isBootstrapPackage = isBootstrapInstallPackage || builtins.elem (attrs.pname or null) ([
    "build" "packaging" "pyproject-hooks" "wheel"
  ] ++ lib.optionals (python.pythonOlder "3.11") [
    "tomli"
  ]);

  isSetuptoolsDependency = builtins.elem (attrs.pname or null) [
    "setuptools" "wheel"
  ];

  # Keep extra attributes from `attrs`, e.g., `patchPhase', etc.
  self = toPythonModule (stdenv.mkDerivation ((builtins.removeAttrs attrs [
    "disabled" "checkPhase" "checkInputs" "nativeCheckInputs" "doCheck" "doInstallCheck" "dontWrapPythonPrograms" "catchConflicts" "pyproject" "format"
    "disabledTestPaths" "outputs" "stdenv"
  ]) // {

    name = namePrefix + name_;

    nativeBuildInputs = [
      python
      wrapPython
      ensureNewerSourcesForZipFilesHook  # move to wheel installer (pip) or builder (setuptools, flit, ...)?
      pythonRemoveTestsDirHook
    ] ++ lib.optionals (catchConflicts && !isBootstrapPackage && !isSetuptoolsDependency) [
      #
      # 1. When building a package that is also part of the bootstrap chain, we
      #    must ignore conflicts after installation, because there will be one with
      #    the package in the bootstrap.
      #
      # 2. When a package is a dependency of setuptools, we must ignore conflicts
      #    because the hook that checks for conflicts uses setuptools.
      #
      pythonCatchConflictsHook
    ] ++ lib.optionals removeBinBytecode [
      pythonRemoveBinBytecodeHook
    ] ++ lib.optionals (lib.hasSuffix "zip" (attrs.src.name or "")) [
      unzip
    ] ++ lib.optionals (format' == "setuptools") [
      setuptoolsBuildHook
    ] ++ lib.optionals (format' == "pyproject") [(
      if isBootstrapPackage then
        pypaBuildHook.override {
          inherit (python.pythonOnBuildForHost.pkgs.bootstrap) build;
          wheel = null;
        }
      else
        pypaBuildHook
    )] ++ lib.optionals (format' == "wheel") [
      wheelUnpackHook
    ] ++ lib.optionals (format' == "egg") [
      eggUnpackHook eggBuildHook eggInstallHook
    ] ++ lib.optionals (format' != "other") [(
      if isBootstrapInstallPackage then
        pypaInstallHook.override {
          inherit (python.pythonOnBuildForHost.pkgs.bootstrap) installer;
        }
      else
        pypaInstallHook
    )] ++ lib.optionals (stdenv.buildPlatform == stdenv.hostPlatform) [
      # This is a test, however, it should be ran independent of the checkPhase and checkInputs
      pythonImportsCheckHook
    ] ++ lib.optionals (python.pythonAtLeast "3.3") [
      # Optionally enforce PEP420 for python3
      pythonNamespacesHook
    ] ++ lib.optionals withDistOutput [
      pythonOutputDistHook
    ] ++ nativeBuildInputs;

    buildInputs = validatePythonMatches "buildInputs" (buildInputs ++ pythonPath);

    propagatedBuildInputs = validatePythonMatches "propagatedBuildInputs" (propagatedBuildInputs ++ [
      # we propagate python even for packages transformed with 'toPythonApplication'
      # this pollutes the PATH but avoids rebuilds
      # see https://github.com/NixOS/nixpkgs/issues/170887 for more context
      python
    ]);

    inherit strictDeps;

    LANG = "${if python.stdenv.isDarwin then "en_US" else "C"}.UTF-8";

    # Python packages don't have a checkPhase, only an installCheckPhase
    doCheck = false;
    doInstallCheck = attrs.doCheck or true;
    nativeInstallCheckInputs = [
    ] ++ lib.optionals (format' == "setuptools") [
      # Longer-term we should get rid of this and require
      # users of this function to set the `installCheckPhase` or
      # pass in a hook that sets it.
      setuptoolsCheckHook
    ] ++ nativeCheckInputs;
    installCheckInputs = checkInputs;

    postFixup = lib.optionalString (!dontWrapPythonPrograms) ''
      wrapPythonPrograms
    '' + attrs.postFixup or "";

    # Python packages built through cross-compilation are always for the host platform.
    disallowedReferences = lib.optionals (python.stdenv.hostPlatform != python.stdenv.buildPlatform) [ python.pythonOnBuildForHost ];

    outputs = outputs ++ lib.optional withDistOutput "dist";

    meta = {
      # default to python's platforms
      platforms = python.meta.platforms;
      isBuildPythonPackage = python.meta.platforms;
    } // meta;
  } // lib.optionalAttrs (attrs?checkPhase) {
    # If given use the specified checkPhase, otherwise use the setup hook.
    # Longer-term we should get rid of `checkPhase` and use `installCheckPhase`.
    installCheckPhase = attrs.checkPhase;
  } //  lib.optionalAttrs (disabledTestPaths != []) {
      disabledTestPaths = lib.escapeShellArgs disabledTestPaths;
  }));

  passthru.updateScript = let
      filename = builtins.head (lib.splitString ":" self.meta.position);
    in attrs.passthru.updateScript or [ update-python-libraries filename ];
in lib.extendDerivation
  (disabled -> throw "${name} not supported for interpreter ${python.executable}")
  passthru
  self