diff --git a/default.nix b/default.nix index 8c24149..e219cbc 100644 --- a/default.nix +++ b/default.nix @@ -1,7 +1,6 @@ { pkgs ? import { }, lib ? pkgs.lib, - python ? pkgs.python3, }: let @@ -16,7 +15,7 @@ let satisfiesSemver = (import ./semver.nix {inherit lib;}).satisfies; # Check Python version is compatible with package - isCompatible = pythonVersions: let + isCompatible = pythonVersion: pythonVersions: let operators = { "||" = cond1: cond2: cond1 || cond2; "," = cond1: cond2: cond1 && cond2; # , means && @@ -27,7 +26,7 @@ let 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 python.version v); + state = operators."${operator}" acc.state (satisfiesSemver pythonVersion v); }) { operator = ","; @@ -47,12 +46,18 @@ let isBdist = f: builtins.match "^.*?whl$" f.file != null; isSdist = f: ! isBdist f; + mkEvalPep508 = import ./pep508.nix { + inherit lib; + stdenv = pkgs.stdenv; + }; + mkPoetryPackage = { src, pyproject ? src + "/pyproject.toml", poetrylock ? src + "/poetry.lock", overrides ? defaultPoetryOverrides, meta ? {}, + python ? pkgs.python3, ... }@attrs: let pyProject = importTOML pyproject; @@ -63,6 +68,8 @@ let specialAttrs = [ "pyproject" "poetrylock" "overrides" ]; passedAttrs = builtins.removeAttrs attrs specialAttrs; + evalPep508 = mkEvalPep508 python; + # Create an overriden version of pythonPackages # # We need to avoid mixing multiple versions of pythonPackages in the same @@ -85,7 +92,10 @@ let then "wheel" else "setuptools"; - in self.buildPythonPackage { + # Filter derivations by their PEP508 markers + markerFiltered = if builtins.hasAttr "marker" pkgMeta then (! evalPep508 pkgMeta.marker) else false; + + in if markerFiltered then null else self.buildPythonPackage { pname = pkgMeta.name; version = pkgMeta.version; @@ -101,7 +111,7 @@ let in builtins.map (dep: self."${dep}") dependencies; meta = { - broken = ! isCompatible pkgMeta.python-versions; + broken = ! isCompatible python.version pkgMeta.python-versions; }; src = @@ -126,7 +136,7 @@ let value = let drv = mkPoetryDep pkgMeta; override = getAttrDefault pkgMeta.name overrides (_: _: drv: drv); - in override self super drv; + in if drv != null then (override self super drv) else null; }) poetryLock.package; in { @@ -134,13 +144,7 @@ let pytest_xdist = super.pytest_xdist.overrideAttrs(old: { doInstallCheck = false; }); - } // builtins.listToAttrs lockPkgs // { - # Temporary hacks (Missing support for markers) - enum34 = null; - functools32 = null; - typing = null; - futures = null; - }; + } // builtins.listToAttrs lockPkgs; in python.override { inherit packageOverrides; self = py; }; pythonPackages = py.pkgs; @@ -160,9 +164,9 @@ let # TODO: Only conditionally include poetry based on build-system # buildInputs = mkInput "buildInputs" ([ pythonPackages.poetry ]); - buildInputs = mkInput "buildInputs" ([ pythonPackages.setuptools ]); + buildInputs = mkInput "buildInputs" ([ pythonPackages.poetry ]); - propagatedBuildInputs = mkInput "propagatedBuildInputs" (getDeps "dependencies"); + propagatedBuildInputs = mkInput "propagatedBuildInputs" (getDeps "dependencies") ++ ([ pythonPackages.setuptools ]); checkInputs = mkInput "checkInputs" (getDeps "dev-dependencies"); passthru = { diff --git a/pep508.nix b/pep508.nix new file mode 100644 index 0000000..7c18e43 --- /dev/null +++ b/pep508.nix @@ -0,0 +1,185 @@ +{ lib, stdenv }: python: + +let + + # Like builtins.substring but with stop being offset instead of length + substr = start: stop: s: builtins.substring start (stop - start) s; + + # Strip leading/trailing whitespace from string + stripStr = s: lib.elemAt (builtins.split "^ *| *$" s) 2; + + findSubExpressionsFun = acc: c: ( + if c == "(" then ( + let + posNew = acc.pos + 1; + isOpen = acc.openP == 0; + startPos = if isOpen then posNew else acc.startPos; + exprs = if isOpen then acc.exprs else acc.exprs ++ [ (substr acc.exprPos (acc.pos - 1) acc.expr) ]; + in acc // { + inherit exprs startPos; + pos = posNew; + openP = acc.openP + 1; + } + ) else if c == ")" then ( + let + openP = acc.openP - 1; + exprs = findSubExpressions (substr acc.startPos acc.pos acc.expr); + in acc // { + inherit openP; + pos = acc.pos + 1; + exprs = if openP == 0 then acc.exprs ++ [ exprs ] else acc.exprs; + exprPos = if openP == 0 then acc.pos + 1 else acc.exprPos; + } + ) else acc // {pos = acc.pos + 1;} + ); + + # Make a tree out of expression groups (parens) + findSubExpressions = expr: let + acc = builtins.foldl' findSubExpressionsFun { + exprs = []; + expr = expr; + pos = 0; + openP = 0; + exprPos = 0; + startPos = 0; + } (lib.stringToCharacters expr); + in acc.exprs ++ [ (substr acc.exprPos acc.pos expr) ]; + + parseExpressions = exprs: let + splitCond = (s: builtins.map + (x: if builtins.typeOf x == "list" then (builtins.elemAt x 0) else x) + (builtins.split " (and|or) " s)); + + mapfn = expr: ( + if (builtins.match "^ ?$" expr != null) then null # Filter empty + else if (builtins.elem expr [ "and" "or" ]) then { + type = "bool"; + value = expr; + } + else { + type = "expr"; + value = expr; + }); + + parsed = builtins.filter (x: x != null) (builtins.map mapfn (splitCond exprs)); + + in if builtins.typeOf exprs == "string" then parsed else builtins.map parseExpressions exprs; + + # Transform individual expressions to structured expressions + # This function also performs variable substitution, replacing environment markers with their explicit values + transformExpressions = exprs: let + variables = { + os_name = "posix"; # TODO: Check other platforms + sys_platform = ( + if stdenv.isLinux then "linux" + else if stdenv.isDarwin then "darwin" + else throw "Unsupported platform" + ); + platform_machine = stdenv.platform.kernelArch; + platform_python_implementation = "CPython"; # Only CPython supported for now + platform_release = ""; # Field not reproducible + platform_system = ( + if stdenv.isLinux then "Linux" + else if stdenv.isDarwin then "Darwin" + else throw "Unsupported platform" + ); + platform_version = ""; # Field not reproducible + python_version = python.passthru.pythonVersion; + python_full_version = python.version; + implementation_name = "cpython"; # Only cpython supported for now + implementation_version = python.version; + extra = ""; + }; + + substituteVar = value: if builtins.hasAttr value variables then (builtins.toJSON variables."${value}") else value; + + processVar = value: builtins.foldl' (acc: v: v acc) value [ + stripStr + substituteVar + ]; + + in if builtins.typeOf exprs == "set" then ( + if exprs.type == "expr" then (let + mVal = ''[a-zA-Z0-9\'"_\. ]+''; + mOp = "in|[!=<>]+"; + e = stripStr exprs.value; + m = builtins.map stripStr (builtins.match ''^(${mVal}) *(${mOp}) *(${mVal})$'' e); + in { + type = "expr"; + value = { + op = builtins.elemAt m 1; + values = [ + (processVar (builtins.elemAt m 0)) + (processVar (builtins.elemAt m 2)) + ]; + }; + }) else exprs + ) else builtins.map transformExpressions exprs; + + # Recursively eval all expressions + evalExpressions = exprs: let + unmarshal = v: ( + # TODO: Handle single quoted values + if v == "True" then true + else if v == "False" then false + else builtins.fromJSON v + ); + hasElem = needle: haystack: builtins.elem needle (builtins.filter (x: builtins.typeOf x == "string") (builtins.split " " haystack)); + # TODO: Implement all operators + op = { + "<=" = x: y: (unmarshal x) <= (unmarshal y); + "<" = x: y: (unmarshal x) < (unmarshal y); + "!=" = x: y: x != y; + "==" = x: y: x == y; + ">=" = x: y: (unmarshal x) >= (unmarshal y); + ">" = x: y: (unmarshal x) > (unmarshal y); + "~=" = null; + "===" = null; + "in" = x: y: let + values = builtins.filter (x: builtins.typeOf x == "string") (builtins.split " " (unmarshal y)); + in builtins.elem (unmarshal x) values; + }; + in if builtins.typeOf exprs == "set" then ( + if exprs.type == "expr" then (let + expr = exprs; + result = (op."${expr.value.op}") (builtins.elemAt expr.value.values 0) (builtins.elemAt expr.value.values 1); + in { + type = "value"; + value = result; + }) else exprs + ) else builtins.map evalExpressions exprs; + + # Now that we have performed an eval all that's left to do is to concat the graph into a single bool + reduceExpressions = exprs: let + cond = { + "and" = x: y: x && y; + "or" = x: y: x || y; + }; + + reduceExpressionsFun = acc: v: ( + if builtins.typeOf v == "set" then ( + if v.type == "value" then ( + acc // { + value = cond."${acc.cond}" acc.value v.value; + } + ) else if v.type == "bool" then ( + acc // { + cond = v.value; + } + ) else throw "Unsupported type" + ) else if builtins.typeOf v == "list" then ( + builtins.foldl' reduceExpressionsFun acc v + ) else throw "Unsupported type" + ); + in (builtins.foldl' reduceExpressionsFun { + value = true; + cond = "and"; + } exprs).value; + +in e: builtins.foldl' (acc: v: v acc) e [ + findSubExpressions + parseExpressions + transformExpressions + evalExpressions + reduceExpressions +]