Files
nixpkgs/pkgs/development/interpreters/python/mk-python-derivation.nix

570 lines
18 KiB
Nix

# Generic builder.
{
lib,
config,
python,
# Allow passing in a custom stdenv to buildPython*.override
stdenv,
wrapPython,
unzip,
ensureNewerSourcesForZipFilesHook,
# Whether the derivation provides a Python module or not.
toPythonModule,
namePrefix,
nix-update-script,
setuptools,
pypaBuildHook,
pypaInstallHook,
pythonCatchConflictsHook,
pythonImportsCheckHook,
pythonMetadataCheckHook,
pythonNamespacesHook,
pythonOutputDistHook,
pythonRelaxDepsHook,
pythonRemoveBinBytecodeHook,
pythonRemoveTestsDirHook,
pythonRuntimeDepsCheckHook,
setuptoolsBuildHook,
wheelUnpackHook,
eggUnpackHook,
eggBuildHook,
eggInstallHook,
}:
let
inherit (builtins) unsafeGetAttrPos;
inherit (lib)
concatMapStrings
elem
extendDerivation
fixedWidthString
flip
getName
hasSuffix
isBool
max
optional
optionalAttrs
optionals
optionalString
removePrefix
stringLength
all
seq
;
leftPadName =
name: against:
let
len = max (stringLength name) (stringLength against);
in
fixedWidthString len " " name;
hasZipSuffix = hasSuffix "zip";
isPythonModule =
drv:
# all pythonModules have the pythonModule attribute
(drv ? "pythonModule")
# Some pythonModules are turned in to a pythonApplication by setting the field to false
&& (!isBool drv.pythonModule);
isMismatchedPython = drv: drv.pythonModule != python;
withDistOutput' = flip elem [
"pyproject"
"setuptools"
"wheel"
];
isBootstrapInstallPackage' = flip elem [
"flit-core"
"installer"
];
isBootstrapPackage' = flip elem [
"build"
"packaging"
"pyproject-hooks"
"wheel"
];
isSetuptoolsDependency' = flip elem [
"setuptools"
"wheel"
];
bootstrappedPypaBuildHook = pypaBuildHook.override {
inherit (python.pythonOnBuildForHost.pkgs.bootstrap) build;
wheel = null;
};
bootstrappedPypaInstallHook = pypaInstallHook.override {
inherit (python.pythonOnBuildForHost.pkgs.bootstrap) installer;
};
bootstrappedRuntimeDepsCheckHook = pythonRuntimeDepsCheckHook.override {
inherit (python.pythonOnBuildForHost.pkgs.bootstrap) packaging;
};
in
lib.extendMkDerivation {
constructDrv = stdenv.mkDerivation;
excludeDrvArgNames = [
"disabled"
"checkPhase"
"checkInputs"
"nativeCheckInputs"
"doCheck"
"doInstallCheck"
"pyproject"
"format"
"dependencies"
"optional-dependencies"
"build-system"
];
extendDrvArgs =
finalAttrs:
{
# 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 ? [ ],
# Python module dependencies.
# These are named after PEP-621.
dependencies ? [ ],
optional-dependencies ? { },
# Python PEP-517 build systems.
build-system ? [ ],
# 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 ? { },
doCheck ? true,
__structuredAttrs ? true,
...
}@attrs:
let
getFinalPassthru =
let
pos = unsafeGetAttrPos "passthru" finalAttrs;
in
attrName:
finalAttrs.passthru.${attrName} or (throw (
''
${finalAttrs.name}: passthru.${attrName} missing after overrideAttrs overriding.
''
+ optionalString (pos != null) ''
Last overridden at ${pos.file}:${toString pos.line}
''
));
format' =
assert (getFinalPassthru "pyproject" != null) -> (format == null);
if getFinalPassthru "pyproject" != null then
if getFinalPassthru "pyproject" then "pyproject" else "other"
else if format != null then
format
else
throw "${name} does not configure a `format`. To build with setuptools as before, set `pyproject = true` and `build-system = [ setuptools ]`.";
withDistOutput = withDistOutput' format';
validatePythonMatches =
let
throwMismatch =
attrName: drv:
let
myName = "'${finalAttrs.name}'";
theirName = "'${drv.name}'";
optionalLocation =
let
pos = unsafeGetAttrPos (if attrs ? "pname" then "pname" else "name") attrs;
in
optionalString (pos != null) " at ${pos.file}:${toString pos.line}:${toString pos.column}";
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 [ ${getName drv} ]) ];
${optionalLocation}
'';
checkDrv =
attrName: drv:
if isPythonModule drv && isMismatchedPython drv then throwMismatch attrName drv else true;
in
attrName: inputs: seq (all (checkDrv attrName) inputs) inputs;
isBootstrapInstallPackage = isBootstrapInstallPackage' (finalAttrs.pname or null);
isBootstrapPackage = isBootstrapInstallPackage || isBootstrapPackage' (finalAttrs.pname or null);
isSetuptoolsDependency = isSetuptoolsDependency' (finalAttrs.pname or null);
name = namePrefix + attrs.name or "${finalAttrs.pname}-${finalAttrs.version}";
runtimeDepsCheckHook =
if isBootstrapPackage then bootstrappedRuntimeDepsCheckHook else pythonRuntimeDepsCheckHook;
in
{
inherit name;
inherit catchConflicts;
nativeBuildInputs = [
python
wrapPython
ensureNewerSourcesForZipFilesHook # move to wheel installer (pip) or builder (setuptools, flit, ...)?
pythonRemoveTestsDirHook
]
++ optionals (finalAttrs.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
]
++
optionals (finalAttrs.pythonRelaxDeps or [ ] != [ ] || finalAttrs.pythonRemoveDeps or [ ] != [ ])
[
pythonRelaxDepsHook
]
++ optionals removeBinBytecode [
pythonRemoveBinBytecodeHook
]
++ optionals (attrs ? src.name && hasZipSuffix attrs.src.name) [
unzip
]
++ optionals (format' == "setuptools") [
setuptoolsBuildHook
]
++ optionals (format' == "pyproject") [
(if isBootstrapPackage then bootstrappedPypaBuildHook else pypaBuildHook)
runtimeDepsCheckHook
]
++ optionals (format' == "wheel") [
wheelUnpackHook
runtimeDepsCheckHook
]
++ optionals (format' == "egg") [
eggUnpackHook
eggBuildHook
eggInstallHook
]
++ optionals (format' != "other") [
(if isBootstrapInstallPackage then bootstrappedPypaInstallHook else pypaInstallHook)
]
++ optionals (stdenv.buildPlatform == stdenv.hostPlatform) [
# This is a test, however, it should be ran independent of the checkPhase and checkInputs
pythonImportsCheckHook
]
++
optionals
(
finalAttrs ? "pname"
&& finalAttrs ? "version"
# We don't care about the METADATA of Python applications.
&& isPythonModule finalAttrs.passthru
# METADATA is unlikely to be correct if pyproject is false or null.
&& pyproject == true
&& !isBootstrapPackage
)
[
pythonMetadataCheckHook
]
++ optionals (python.pythonAtLeast "3.3") [
# Optionally enforce PEP420 for python3
pythonNamespacesHook
]
++ optionals withDistOutput [
pythonOutputDistHook
]
++ nativeBuildInputs
++ getFinalPassthru "build-system";
buildInputs = validatePythonMatches "buildInputs" (buildInputs ++ pythonPath);
propagatedBuildInputs =
validatePythonMatches "propagatedBuildInputs" (
propagatedBuildInputs ++ getFinalPassthru "dependencies"
)
++ [
# 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 __structuredAttrs strictDeps;
env = {
LANG = "${if python.stdenv.hostPlatform.isDarwin then "en_US" else "C"}.UTF-8";
}
// (attrs.env or { });
# Python packages don't have a checkPhase, only an installCheckPhase
doCheck = false;
doInstallCheck = attrs.doCheck or true;
nativeInstallCheckInputs = nativeCheckInputs ++ attrs.nativeInstallCheckInputs or [ ];
installCheckInputs = checkInputs ++ attrs.installCheckInputs or [ ];
# If given use the specified checkPhase, otherwise use the setup hook.
# Longer-term we should get rid of `checkPhase` and use `installCheckPhase`.
${if attrs ? checkPhase then "installCheckPhase" else null} = attrs.checkPhase;
inherit dontWrapPythonPrograms;
postFixup =
optionalString (!finalAttrs.dontWrapPythonPrograms) ''
wrapPythonPrograms
''
+ attrs.postFixup or "";
# Python packages built through cross-compilation are always for the host platform.
disallowedReferences = optionals (python.stdenv.hostPlatform != python.stdenv.buildPlatform) [
python.pythonOnBuildForHost
];
outputs = outputs ++ optional withDistOutput "dist";
passthru = {
inherit
pyproject
build-system
dependencies
optional-dependencies
;
disabled = finalAttrs ? meta.problems.unsupportedPython;
updateScript = nix-update-script { };
}
// attrs.passthru or { };
meta = {
# default to python's platforms
platforms = python.meta.platforms;
isBuildPythonPackage = python.meta.platforms;
}
// meta
// {
problems = meta.problems or { } // {
## Public meta.problems interface for all Python packages
# Unsupported Python interpreter
${if meta ? problems.unsupportedPython then "unsupportedPython" else null} =
meta.problems.unsupportedPython or { }
// {
kind = "broken";
message =
meta.problems.unsupportedPython.message
or "${removePrefix namePrefix finalAttrs.name} not supported for interpreter ${python.executable}";
};
## Implementation details, subject to changes (`buildPythonPackage-internal-*`)
# Deprecated <pkg>.overridePythonPackage argument disabled (internal)
# makeOverridablePythonPackage will specify this problem as { } when <pkg>.overridePythonAttrs overrides argument disabled;
# fill its content here.
${
if meta ? problems.buildPythonPackage-internal-disabled-overridePythonAttrs then
"buildPythonPackage-internal-disabled-overridePythonAttrs"
else
null
} =
{
kind = "broken";
message =
let
pos = unsafeGetAttrPos "disabled" attrs;
posString = lib.optionalString (pos != null) "At ${pos.file}:${toString pos.line}\n";
in
''
${finalAttrs.name}: Overriding argument `disabled` with `<pkg>.overridePythonAttrs` is deprecated.
Add/remove `meta.problems.unsupportedPython` instead.
''
+ posString;
};
# Any of enabledTest* is an empty list (internal)
${
if
# `enabledTestPaths` is the most common,
# and then `enabledTests`,
# `enabledTestMarks` being the least common.
finalAttrs ? enabledTestPaths && finalAttrs.enabledTestPaths == [ ]
|| finalAttrs ? enabledTests && finalAttrs.enabledTests == [ ]
|| finalAttrs ? enabledTestMarks && finalAttrs.enabledTestMarks == [ ]
then
"buildPythonPackage-internal-enabledTests-empty"
else
null
} =
{
kind = "broken";
message = ''
${finalAttrs.name}: Unexpected empty list found in enabledTest* attributes:
''
+
# Cold path, use more compact expression.
concatMapStrings
(
n:
let
pos = if attrs.${n} or null != [ ] then unsafeGetAttrPos n attrs else null;
posString = optionalString (pos != null) " at ${pos.file}:${toString pos.line}";
in
optionalString (finalAttrs.${n} or null == [ ]) "- ${n}${posString}\n"
)
[
"enabledTestMark"
"enabledTestPath"
"enabledTests"
]
+ ''
Defining these attributes as empty lists is ambiguous.
To opt out from whitelisting, omit them, or set them as `null` when overriding.
'';
};
# Deprecated pytestFlagsArray (internal)
${
if
finalAttrs ? pytestFlagsArray
&& finalAttrs.pytestFlagsArray != null
&& finalAttrs.pytestFlagsArray != [ ]
then
"buildPythonPackage-internal-pytestFlagsArray"
else
null
} =
{
kind = "broken";
message =
let
pos =
if attrs ? pytestFlagsArray && attrs.pytestFlagsArray != null && attrs.pytestFlagsArray != [ ] then
unsafeGetAttrPos "pytestFlagsArray" attrs
else
null;
posString = optionalString (pos != null) " at ${pos.file}:${toString pos.line}";
in
''
${finalAttrs.name}: Deprecated argument pytestFlagsArray found${posString}
Use pytestFlags or (enabled|disabled)(TestPaths|Tests|TestMarks) instead.
'';
};
# Deprecated argument stdenv (internal)
${if attrs ? stdenv then "buildPythonPackage-internal-stdenv" else null} = {
kind = "broken";
message =
let
pos = unsafeGetAttrPos "stdenv" attrs;
posString = lib.optionalString (pos != null) " at ${pos.file}:${toString pos.line}";
in
''
${finalAttrs.name}: Deprecated argument `stdenv` found${posString}
Override `stdenv` with `buildPythonPackage.override` or `buildPythonApplication.override` instead.
'';
};
};
};
};
# This derivation transformation function must be independent to `attrs`
# for fixed-point arguments support in the future.
transformDrv = toPythonModule;
}