From 866b5e1c37a9098be7d52c90fcae74e2787fcf40 Mon Sep 17 00:00:00 2001 From: adisbladis Date: Mon, 17 Jun 2019 16:27:16 +0100 Subject: [PATCH] Initial commit --- README.org | 2 + default.nix | 105 ++++++++++ example_project/.envrc | 1 + example_project/README.org | 1 + example_project/default.nix | 5 + example_project/example_project/__init__.py | 5 + example_project/poetry.lock | 200 ++++++++++++++++++++ example_project/pyproject.toml | 17 ++ example_project/shell.nix | 15 ++ overrides.nix | 51 +++++ 10 files changed, 402 insertions(+) create mode 100644 README.org create mode 100644 default.nix create mode 100644 example_project/.envrc create mode 100644 example_project/README.org create mode 100644 example_project/default.nix create mode 100644 example_project/example_project/__init__.py create mode 100644 example_project/poetry.lock create mode 100644 example_project/pyproject.toml create mode 100644 example_project/shell.nix create mode 100644 overrides.nix diff --git a/README.org b/README.org new file mode 100644 index 0000000..2bebbc8 --- /dev/null +++ b/README.org @@ -0,0 +1,2 @@ +* Convert poetry projects to nix automagically +This is under _really_ early development diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..3d4b229 --- /dev/null +++ b/default.nix @@ -0,0 +1,105 @@ +{ + pkgs ? import { }, + python ? pkgs.python3, + pythonPackages ? python.pkgs, +}: + +let + + importTOML = path: builtins.fromTOML (builtins.readFile path); + + # TODO: Because pip (and by extension poetry) supports wheels hashes are a list + # This list has determistic but non-distinguishable origins + # (we dont know what url the hashes corresponds to) + # + # Just grabbing the first possible hash only works ~50% of the time + getSha256 = pname: poetryLock: builtins.elemAt poetryLock.metadata.hashes."${pname}" 0; + + # Note: Makes a derivation tree + mkPoetryDerivation = { + pname, + depPkgs, + pyProject, + poetryLock, + overrides, + }: let + pkgMeta = depPkgs."${pname}"; + version = pkgMeta.version; + dependencies = + if (builtins.hasAttr "dependencies" pkgMeta) + then (builtins.attrNames pkgMeta.dependencies) + else [ ]; + + drv = pythonPackages.buildPythonPackage { + inherit pname version; + + # TODO: It's probably better to model derivations as an attrset that we pick deps from + # That way we avoid instantiating the same derivation multiple times + propagatedBuildInputs = builtins.map (pname: (mkPoetryDerivation { + inherit pname depPkgs pyProject poetryLock overrides; + })) dependencies; + + doCheck = false; # We never get development deps + + src = pythonPackages.fetchPypi { + inherit pname version; + sha256 = getSha256 pname poetryLock; + }; + }; + + override = + if (builtins.hasAttr pname overrides) + then overrides."${pname}" + else (drv: drv); + + in override drv; + + mkPoetryPackage = { + src, + pyproject ? src + "/pyproject.toml", + poetrylock ? src + "/poetry.lock", + overrides ? import ./overrides.nix { + inherit pkgs python pythonPackages; + }, + }: let + pyProject = importTOML pyproject; + poetryLock = importTOML poetrylock; + + # Turn list of packages from lock-file into attrset for easy lookup + depPkgs = builtins.listToAttrs (builtins.map (pkgMeta: { + name = pkgMeta.name; + value=pkgMeta; + }) poetryLock.package); + + # Turn an attrset of name/version pairs into a list of derivations + getDeps = deps: let + depNames = builtins.filter (depName: depName != "python") (builtins.attrNames deps); + in builtins.map (pname: mkPoetryDerivation { + inherit pname depPkgs pyProject poetryLock overrides; + }) depNames; + + in pythonPackages.buildPythonApplication { + pname = pyProject.tool.poetry.name; + version = pyProject.tool.poetry.version; + + inherit src; + + format = "pyproject"; + + buildInputs = [ + pythonPackages.poetry + ]; + + propagatedBuildInputs = getDeps pyProject.tool.poetry.dependencies; + checkInputs = getDeps pyProject.tool.poetry.dev-dependencies; + + meta = { + description = pyProject.tool.poetry.description; + licenses = [ pyProject.tool.poetry.license ]; + }; + + }; + +in { + inherit mkPoetryPackage; +} diff --git a/example_project/.envrc b/example_project/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/example_project/.envrc @@ -0,0 +1 @@ +use nix diff --git a/example_project/README.org b/example_project/README.org new file mode 100644 index 0000000..c7d6088 --- /dev/null +++ b/example_project/README.org @@ -0,0 +1 @@ +Note that this example project contains a hand-patched lock-file to work around https://github.com/sdispater/poetry/issues/1172 diff --git a/example_project/default.nix b/example_project/default.nix new file mode 100644 index 0000000..890b13e --- /dev/null +++ b/example_project/default.nix @@ -0,0 +1,5 @@ +with import ../. { }; + +mkPoetryPackage { + src = ./.; +} diff --git a/example_project/example_project/__init__.py b/example_project/example_project/__init__.py new file mode 100644 index 0000000..3f1b9ca --- /dev/null +++ b/example_project/example_project/__init__.py @@ -0,0 +1,5 @@ +import requests + + +if __name__ == '__main__': + print(requests.get('https://example.com').text) diff --git a/example_project/poetry.lock b/example_project/poetry.lock new file mode 100644 index 0000000..cf93cbd --- /dev/null +++ b/example_project/poetry.lock @@ -0,0 +1,200 @@ +[[package]] +category = "dev" +description = "Atomic file writes." +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.0" + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.1.0" + +[[package]] +category = "main" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2019.6.16" + +[[package]] +category = "main" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "dev" +description = "Cross-platform colored terminal text." +marker = "sys_platform == \"win32\"" +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.4.1" + +[[package]] +category = "main" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8" + +[[package]] +category = "dev" +description = "Read metadata from Python packages" +name = "importlib-metadata" +optional = false +python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" +version = "0.18" + +[package.dependencies] +zipp = ">=0.5" + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +marker = "python_version > \"2.7\"" +name = "more-itertools" +optional = false +python-versions = ">=3.4" +version = "7.0.0" + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.0" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.12.0" + +[package.dependencies] +importlib-metadata = ">=0.12" + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.0" + +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.0" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "4.6.3" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +importlib-metadata = ">=0.12" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +six = ">=1.10.0" +wcwidth = "*" + +[package.dependencies.more-itertools] +python = ">=2.8" +version = ">=4.0.0" + +[[package]] +category = "main" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.22.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<3.1.0" +idna = ">=2.5,<2.9" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[[package]] +category = "dev" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "1.12.0" + +[[package]] +category = "main" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" +version = "1.25.3" + +[[package]] +category = "dev" +description = "Measures number of Terminal column cells of wide-character codes" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.1.7" + +[[package]] +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +name = "zipp" +optional = false +python-versions = ">=2.7" +version = "0.5.1" + +[metadata] +content-hash = "81f5e454b0b11af37c64b11a6288cf92768a311f72008b4ff858b804c22d4f6c" +python-versions = "^3.7" + +[metadata.hashes] +atomicwrites = ["75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6", "03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"] +attrs = ["f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399", "69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79"] +certifi = ["945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695", "046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939"] +chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] +colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] +idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] +importlib-metadata = ["cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db", "6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7"] +more-itertools = ["c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a", "2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7"] +packaging = ["0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", "9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"] +pluggy = ["0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", "b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c"] +py = ["dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53", "64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa"] +pyparsing = ["1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", "9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"] +pytest = ["4a784f1d4f2ef198fe9b7aef793e9fa1a3b2f84e822d9b3a64a181293a572d45", "926855726d8ae8371803f7b2e6ec0a69953d9c6311fa7c3b6c1b929ff92d27da"] +requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"] +six = ["d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73", "3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c"] +urllib3 = ["dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232", "b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1"] +wcwidth = ["3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"] +zipp = ["ca943a7e809cc12257001ccfb99e3563da9af99d52f261725e96dfe0f9275bc3", "8c1019c6aad13642199fbe458275ad6a84907634cc9f0989877ccc4a2840139d"] diff --git a/example_project/pyproject.toml b/example_project/pyproject.toml new file mode 100644 index 0000000..b47967c --- /dev/null +++ b/example_project/pyproject.toml @@ -0,0 +1,17 @@ +[tool.poetry] +name = "example_project" +version = "0.1.0" +description = "An example project to test poetry2nix" +authors = ["adisbladis "] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.7" +requests = "^2.22" + +[tool.poetry.dev-dependencies] +pytest = "^4.6" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/example_project/shell.nix b/example_project/shell.nix new file mode 100644 index 0000000..b57c726 --- /dev/null +++ b/example_project/shell.nix @@ -0,0 +1,15 @@ +with import {}; + +let + # TODO: Stop propagating PYTHONPATH + pythonEnv = (python3.withPackages(ps: [ + ps.poetry + ])).override (args: { + ignoreCollisions = true; + }); + +in mkShell { + buildInputs = [ + pythonEnv + ]; +} diff --git a/overrides.nix b/overrides.nix new file mode 100644 index 0000000..b7a8fc8 --- /dev/null +++ b/overrides.nix @@ -0,0 +1,51 @@ +{ + pkgs, + python, + pythonPackages, +}: + +{ + + pytest = (drv: drv.overrideAttrs(old: { + buildInputs = old.buildInputs ++ [ + pythonPackages.setuptools_scm + ]; + })); + + six = (drv: drv.overrideAttrs(old: { + buildInputs = old.buildInputs ++ [ + pythonPackages.setuptools_scm + ]; + })); + + py = (drv: drv.overrideAttrs(old: { + buildInputs = old.buildInputs ++ [ + pythonPackages.setuptools_scm + ]; + })); + + zipp = (drv: drv.overrideAttrs(old: { + buildInputs = old.buildInputs ++ [ + pythonPackages.setuptools_scm + ]; + })); + + importlib-metadata = (drv: drv.overrideAttrs(old: { + src = pythonPackages.fetchPypi { + pname = "importlib_metadata"; + version = old.version; + sha256 = old.src.outputHash; + }; + + buildInputs = old.buildInputs ++ [ + pythonPackages.setuptools_scm + ]; + })); + + pluggy = (drv: drv.overrideAttrs(old: { + buildInputs = old.buildInputs ++ [ + pythonPackages.setuptools_scm + ]; + })); + +}