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