Refactoring

- Extract utility functions to ./lib.nix
- Extract buildPythonPackage to ./mk-poetry-dep.nix
- Replace some functions with existing builtin functions
- Use lib.partition for python package splitting
- Drop package filtering on the top-level and leave it to pep425.nix
This commit is contained in:
Tobias Pflug 2019-12-13 16:56:51 +01:00 committed by Andreas Rammhold
parent 29c93b1dc2
commit 84f27ee31d
No known key found for this signature in database
GPG key ID: E432E410B5E48C86
7 changed files with 198 additions and 191 deletions

View file

@ -1,12 +1,18 @@
{ pkgs ? import <nixpkgs> {}
, lib ? pkgs.lib
, poetry ? null
,
, poetryLib ? import ./lib.nix { inherit lib pkgs; }
}:
let
inherit (poetryLib) isCompatible readTOML;
importTOML = path: builtins.fromTOML (builtins.readFile path);
defaultPoetryOverrides = import ./overrides.nix { inherit pkgs; };
mkEvalPep508 = import ./pep508.nix {
inherit lib;
stdenv = pkgs.stdenv;
};
getAttrDefault = attribute: set: default: (
if builtins.hasAttr attribute set
@ -14,94 +20,6 @@ let
else default
);
# Fetch the artifacts from the PyPI index. Since we get all
# info we need from the lock file we don't use nixpkgs' fetchPyPi
# as it modifies casing while not providing anything we don't already
# have.
#
# Args:
# pname: package name
# file: filename including extension
# hash: SRI hash
# kind: Language implementation and version tag https://www.python.org/dev/peps/pep-0427/#file-name-convention
fetchFromPypi = lib.makeOverridable (
{ pname, file, hash, kind }:
pkgs.fetchurl {
url = "https://files.pythonhosted.org/packages/${kind}/${lib.toLower (builtins.substring 0 1 file)}/${pname}/${file}";
inherit hash;
}
);
getAttrPath = attrPath: set: (
builtins.foldl'
(acc: v: if builtins.typeOf acc == "set" && builtins.hasAttr v acc then acc."${v}" else null)
set (lib.splitString "." attrPath)
);
satisfiesSemver = (import ./semver.nix { inherit lib; }).satisfies;
# Check Python version is compatible with package
isCompatible = pythonVersion: pythonVersions: let
operators = {
"||" = cond1: cond2: cond1 || cond2;
"," = cond1: cond2: cond1 && cond2; # , means &&
};
tokens = builtins.filter (x: x != "") (builtins.split "(,|\\|\\|)" pythonVersions);
in
(
builtins.foldl' (
acc: v: let
isOperator = builtins.typeOf v == "list";
operator = if isOperator then (builtins.elemAt v 0) else acc.operator;
in
if isOperator then (acc // { inherit operator; }) else {
inherit operator;
state = operators."${operator}" acc.state (satisfiesSemver pythonVersion v);
}
)
{
operator = ",";
state = true;
}
tokens
).state;
extensions = pkgs.lib.importJSON ./extensions.json;
getExtension = filename: builtins.elemAt
(builtins.filter (ext: builtins.match "^.*\.${ext}" filename != null) extensions)
0;
supportedRe = ("^.*?(" + builtins.concatStringsSep "|" extensions + ")");
fileSupported = fname: builtins.match supportedRe fname != null;
defaultPoetryOverrides = import ./overrides.nix { inherit pkgs; };
isBdist = f: builtins.match "^.*?whl$" f.file != null;
isSdist = f: ! isBdist f;
mkEvalPep508 = import ./pep508.nix {
inherit lib;
stdenv = pkgs.stdenv;
};
selectWhl = (
import ./pep425.nix {
inherit lib;
inherit (pkgs) stdenv;
python = pkgs.python3;
}
).select;
#
# Returns the appropriate manylinux dependencies and string representation for the file specified
#
getManyLinuxDeps = f:
let
ml = pkgs.pythonManylinuxPackages;
in
if lib.strings.hasInfix "manylinux1" f then { pkg = [ ml.manylinux1 ]; str = "1"; }
else if lib.strings.hasInfix "manylinux2010" f then { pkg = [ ml.manylinux2010 ]; str = "2010"; }
else if lib.strings.hasInfix "manylinux2014" f then { pkg = [ ml.manylinux2014 ]; str = "2014"; }
else { pkg = []; str = null; };
mkPoetryPackage =
{ src
@ -112,10 +30,9 @@ let
, python ? pkgs.python3
, ...
}@attrs: let
pyProject = importTOML pyproject;
poetryLock = importTOML poetrylock;
files = getAttrDefault "files" (getAttrDefault "metadata" poetryLock {}) {};
pyProject = readTOML pyproject;
poetryLock = readTOML poetrylock;
lockFiles = lib.getAttrFromPath [ "metadata" "files" ] poetryLock;
specialAttrs = [ "pyproject" "poetrylock" "overrides" ];
passedAttrs = builtins.removeAttrs attrs specialAttrs;
@ -130,93 +47,38 @@ let
# closure as python can only ever have one version of a dependency
py = let
packageOverrides = self: super: let
getDep = depName: if builtins.hasAttr depName self then self."${depName}" else null;
mkPoetryDep = pkgMeta: let
pkgFiles = let
all = getAttrDefault pkgMeta.name files [];
in
builtins.filter (f: fileSupported f.file) all;
filterFile = fname: builtins.match ("^.*" + builtins.replaceStrings [ "." ] [ "\\." ] pkgMeta.version + ".*$") fname != null;
filteredFiles = builtins.filter (f: filterFile f.file) pkgFiles;
binaryDist = selectWhl filteredFiles;
sourceDist = builtins.filter isSdist filteredFiles;
file = if (builtins.length sourceDist) > 0 then builtins.head sourceDist else builtins.head binaryDist;
format =
if isBdist file
then "wheel"
else "setuptools";
in
self.buildPythonPackage {
pname = pkgMeta.name;
version = pkgMeta.version;
doCheck = false; # We never get development deps
dontStrip = true;
inherit format;
nativeBuildInputs = [ pkgs.autoPatchelfHook ];
buildInputs = (getManyLinuxDeps file.file).pkg;
NIX_PYTHON_MANYLINUX = (getManyLinuxDeps file.file).str;
propagatedBuildInputs = let
depAttrs = getAttrDefault "dependencies" pkgMeta {};
# Some dependencies like django gets the attribute name django
# but dependencies try to access Django
dependencies = builtins.map (d: lib.toLower d) (builtins.attrNames depAttrs);
in
builtins.map getDep dependencies;
meta = {
broken = ! isCompatible python.version pkgMeta.python-versions;
license = [];
};
src = fetchFromPypi {
pname = pkgMeta.name;
inherit (file) file hash;
# We need to retrieve kind from the interpreter and the filename of the package
# Interpreters should declare what wheel types they're compatible with (python type + ABI)
# Here we can then choose a file based on that info.
kind = if format == "setuptools" then "source" else (builtins.elemAt (lib.strings.splitString "-" file.file) 2);
};
};
getDep = depName: if builtins.hasAttr depName self then self."${depName}" else throw "foo";
# Filter packages by their PEP508 markers
pkgsWithFilter = builtins.map (
pkgMeta: let
f = if builtins.hasAttr "marker" pkgMeta then (!evalPep508 pkgMeta.marker) else false;
in
pkgMeta // { p2nixFiltered = f; }
) poetryLock.package;
partitions = let
supportsPythonVersion = pkgMeta: if pkgMeta ? marker then (evalPep508 pkgMeta.marker) else true;
in
lib.partition supportsPythonVersion poetryLock.package;
lockPkgs = builtins.map (
pkgMeta: {
name = pkgMeta.name;
value = let
drv = mkPoetryDep pkgMeta;
override = getAttrDefault pkgMeta.name overrides (_: _: drv: drv);
in
if drv != null then (override self super drv) else null;
}
) (builtins.filter (pkgMeta: !pkgMeta.p2nixFiltered) pkgsWithFilter);
compatible = partitions.right;
incompatible = partitions.wrong;
# Null out any filtered packages, we don't want python.pkgs from nixpkgs
nulledPkgs = (
builtins.listToAttrs
(
builtins.map (x: { name = x.name; value = null; })
(builtins.filter (pkgMeta: pkgMeta.p2nixFiltered) pkgsWithFilter)
)
lockPkgs = builtins.listToAttrs (
builtins.map (
pkgMeta: rec {
name = pkgMeta.name;
value = let
drv = self.mkPoetryDep (pkgMeta // { files = lockFiles.${name}; });
override = getAttrDefault pkgMeta.name overrides (_: _: drv: drv);
in
override self super drv;
}
) compatible
);
# Null out any filtered packages, we don't want python.pkgs from nixpkgs
nulledPkgs = builtins.listToAttrs (builtins.map (x: { name = x.name; value = null; }) incompatible);
in
nulledPkgs // builtins.listToAttrs lockPkgs;
{
mkPoetryDep = self.callPackage ./mk-poetry-dep.nix {
inherit pkgs lib python poetryLib;
};
} // nulledPkgs // lockPkgs;
in
python.override { inherit packageOverrides; self = py; };
pythonPackages = py.pkgs;
@ -237,8 +99,7 @@ let
};
getBuildSystemPkgs = let
buildSystem = getAttrPath
"build-system.build-backend" pyProject;
buildSystem = lib.getAttrFromPath [ "build-system" "build-backend" ] pyProject;
in
knownBuildSystems.${buildSystem} or (throw "unsupported build system ${buildSystem}");
in

67
lib.nix Normal file
View file

@ -0,0 +1,67 @@
{ lib, pkgs }:
let
inherit (import ./semver.nix { inherit lib; }) satisfiesSemver;
# Returns true if pythonVersion matches with the expression in pythonVersions
isCompatible = pythonVersion: pythonVersions:
let
operators = {
"||" = cond1: cond2: cond1 || cond2;
"," = cond1: cond2: cond1 && cond2; # , means &&
};
# split string at "," and "||"
tokens = builtins.filter (x: x != "") (builtins.split "(,|\\|\\|)" pythonVersions);
combine = acc: v:
let
isOperator = builtins.typeOf v == "list";
operator = if isOperator then (builtins.elemAt v 0) else acc.operator;
in
if isOperator then (acc // { inherit operator; }) else {
inherit operator;
state = operators."${operator}" acc.state (satisfiesSemver pythonVersion v);
};
initial = { operator = ","; state = true; };
in
(builtins.foldl' combine initial tokens).state;
readTOML = path: builtins.fromTOML (builtins.readFile path);
#
# Returns the appropriate manylinux dependencies and string representation for the file specified
#
getManyLinuxDeps = f:
let
ml = pkgs.pythonManylinuxPackages;
in
if lib.strings.hasInfix "manylinux1" f then { pkg = [ ml.manylinux1 ]; str = "1"; }
else if lib.strings.hasInfix "manylinux2010" f then { pkg = [ ml.manylinux2010 ]; str = "2010"; }
else if lib.strings.hasInfix "manylinux2014" f then { pkg = [ ml.manylinux2014 ]; str = "2014"; }
else { pkg = []; str = null; };
# Fetch the artifacts from the PyPI index. Since we get all
# info we need from the lock file we don't use nixpkgs' fetchPyPi
# as it modifies casing while not providing anything we don't already
# have.
#
# Args:
# pname: package name
# file: filename including extension
# hash: SRI hash
# kind: Language implementation and version tag https://www.python.org/dev/peps/pep-0427/#file-name-convention
fetchFromPypi = lib.makeOverridable (
{ pname, file, hash, kind }:
pkgs.fetchurl {
url = "https://files.pythonhosted.org/packages/${kind}/${lib.toLower (builtins.substring 0 1 file)}/${pname}/${file}";
inherit hash;
}
);
in
{
inherit
fetchFromPypi
getManyLinuxDeps
isCompatible
readTOML
;
}

79
mk-poetry-dep.nix Normal file
View file

@ -0,0 +1,79 @@
{ autoPatchelfHook
, pkgs
, lib
, python
, buildPythonPackage
, pythonPackages
, poetryLib
}:
{ name
, version
, files
, dependencies ? {}
, python-versions
, supportedExtensions ? lib.importJSON ./extensions.json
, ...
}: let
inherit (poetryLib) isCompatible getManyLinuxDeps fetchFromPypi;
inherit (import ./pep425.nix {
inherit lib python;
inherit (pkgs) stdenv;
}) selectWheel;
fileCandidates = let
supportedRegex = ("^.*?(" + builtins.concatStringsSep "|" supportedExtensions + ")");
matchesVersion = fname: builtins.match ("^.*" + builtins.replaceStrings [ "." ] [ "\\." ] version + ".*$") fname != null;
hasSupportedExtension = fname: builtins.match supportedRegex fname != null;
in
builtins.filter (f: matchesVersion f.file && hasSupportedExtension f.file) files;
fileInfo = let
isBdist = f: lib.strings.hasSuffix "whl" f.file;
isSdist = f: ! isBdist f;
binaryDist = selectWheel fileCandidates;
sourceDist = builtins.filter isSdist fileCandidates;
lockFileEntry = if (builtins.length sourceDist) > 0 then builtins.head sourceDist else builtins.head binaryDist;
in
rec {
inherit (lockFileEntry) file hash;
name = file;
format = if lib.strings.hasSuffix ".whl" name then "wheel" else "setuptools";
kind = if format == "setuptools" then "source" else (builtins.elemAt (lib.strings.splitString "-" name) 2);
};
in
buildPythonPackage {
pname = name;
version = version;
doCheck = false; # We never get development deps
dontStrip = true;
format = fileInfo.format;
nativeBuildInputs = if (getManyLinuxDeps fileInfo.name).str != null then [ autoPatchelfHook ] else [];
buildInputs = (getManyLinuxDeps fileInfo.name).pkg;
NIX_PYTHON_MANYLINUX = (getManyLinuxDeps fileInfo.name).str;
propagatedBuildInputs =
let
# Some dependencies like django gets the attribute name django
# but dependencies try to access Django
deps = builtins.map (d: lib.toLower d) (builtins.attrNames dependencies);
in
builtins.map (n: pythonPackages.${n}) deps;
meta = {
broken = ! isCompatible python.version python-versions;
license = [];
};
# We need to retrieve kind from the interpreter and the filename of the package
# Interpreters should declare what wheel types they're compatible with (python type + ABI)
# Here we can then choose a file based on that info.
src = fetchFromPypi {
pname = name;
inherit (fileInfo) file hash kind;
};
}

View file

@ -57,7 +57,7 @@ let
#
# Selects the best matching wheel file from a list of files
#
select = files:
selectWheel = files:
let
filesWithoutSources = (builtins.filter (x: hasSuffix ".whl" x.file) files);
@ -97,5 +97,5 @@ let
in
{
inherit select;
inherit selectWheel;
}

View file

@ -71,10 +71,10 @@ let
else throw "Constraint \"${constraintStr}\" could not be parsed"
);
satisfies = version: constraint: let
satisfiesSemver = version: constraint: let
inherit (parseConstraint constraint) op v;
in
if constraint == "*" then true else operators."${op}" version v;
in
{ inherit satisfies; }
{ inherit satisfiesSemver; }

View file

@ -7,7 +7,7 @@ let
poetryLock = ./poetry.lock;
src = lib.cleanSource ./.;
};
p = pkg.py.withPackages (ps: [ ps.numpy ps.opencv-python ]);
p = pkg.python.withPackages (ps: [ ps.numpy ps.opencv-python ]);
in
runCommand "test" {} ''
${p}/bin/python -c "import cv2"

View file

@ -10,7 +10,7 @@ lib.debug.runTests {
];
in
{
expr = (pep425.select cs);
expr = (pep425.selectWheel cs);
expected = [ { file = "grpcio-1.25.0-cp27-cp27m-manylinux2010_x86_64.whl"; } ];
};
@ -22,7 +22,7 @@ lib.debug.runTests {
];
in
{
expr = (pep425OSX.select cs);
expr = (pep425OSX.selectWheel cs);
expected = [ { file = "grpcio-1.25.0-cp27-cp27m-macosx_10_10_x86_64.whl"; } ];
};
@ -38,7 +38,7 @@ lib.debug.runTests {
];
in
{
expr = (pep425Python37.select cs);
expr = (pep425Python37.selectWheel cs);
expected = [ { file = "grpcio-1.25.0-cp37-cp37m-manylinux2010_x86_64.whl"; } ];
};
@ -50,7 +50,7 @@ lib.debug.runTests {
];
in
{
expr = (pep425OSX.select cs);
expr = (pep425OSX.selectWheel cs);
expected = [ { file = "grpcio-1.25.0-cp27-cp27m-macosx_10_12_x86_64.whl"; } ];
};
@ -62,7 +62,7 @@ lib.debug.runTests {
];
in
{
expr = (pep425OSX.select cs);
expr = (pep425OSX.selectWheel cs);
expected = [];
};
@ -74,7 +74,7 @@ lib.debug.runTests {
];
in
{
expr = (pep425.select cs);
expr = (pep425.selectWheel cs);
expected = [ { file = "grpcio-1.25.0-cp27-cp27m-manylinux1_x86_64.whl"; } ];
};
@ -86,17 +86,17 @@ lib.debug.runTests {
];
in
{
expr = (pep425.select cs);
expr = (pep425.selectWheel cs);
expected = [];
};
testLinuxEmptyList = {
expr = pep425.select [];
expr = pep425.selectWheel [];
expected = [];
};
testOSXEmptyList = {
expr = pep425OSX.select [];
expr = pep425OSX.selectWheel [];
expected = [];
};
@ -139,7 +139,7 @@ lib.debug.runTests {
];
in
{
expr = pep425.select cs;
expr = pep425.selectWheel cs;
expected = [ { file = "cffi-1.13.2-cp27-cp27m-manylinux1_x86_64.whl"; } ];
};