Skip to content

Commit b7e3a23

Browse files
committed
build: Add hacks function for 2-way Nixpkgs Python infrastructure
This adds a `hacks.toNixpkgs` that generates a Nixpkgs overlay from pyproject.nix build (uv2nix) packages. It is conceptually similar to `hacks.nixpkgsPrebuilt`, just in reverse.
1 parent 02e9418 commit b7e3a23

File tree

7 files changed

+396
-3
lines changed

7 files changed

+396
-3
lines changed

build/checks/build-systems.nix

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,74 @@ let
688688
setuptools = [ ];
689689
};
690690
};
691+
692+
hatch-vcs =
693+
{
694+
stdenv,
695+
python3Packages,
696+
pyprojectHook,
697+
resolveBuildSystem,
698+
}:
699+
stdenv.mkDerivation {
700+
inherit (python3Packages.hatch-vcs)
701+
pname
702+
version
703+
src
704+
meta
705+
;
706+
707+
nativeBuildInputs = [
708+
pyprojectHook
709+
]
710+
++ resolveBuildSystem {
711+
hatchling = [ ];
712+
};
713+
};
714+
715+
# Package urllib3 for testing hacks.toNixpkgs.
716+
# This is a simple dependency that we can test both dependencies & optional-dependencies with.
717+
urllib3 =
718+
{
719+
stdenv,
720+
python3Packages,
721+
pyprojectHook,
722+
resolveBuildSystem,
723+
}:
724+
stdenv.mkDerivation {
725+
inherit (python3Packages.urllib3)
726+
pname
727+
version
728+
src
729+
meta
730+
;
731+
732+
passthru = {
733+
optional-dependencies = {
734+
brotli = {
735+
brotli = [ ];
736+
};
737+
zstd = {
738+
zstandard = [ ];
739+
};
740+
socks = {
741+
PySocks = [ ];
742+
};
743+
h2 = {
744+
h2 = [ ];
745+
};
746+
};
747+
};
748+
749+
nativeBuildInputs = [
750+
pyprojectHook
751+
]
752+
++ resolveBuildSystem {
753+
hatchling = [ ];
754+
hatch-vcs = [ ];
755+
setuptools-scm = [ ];
756+
};
757+
};
758+
691759
};
692760

693761
crossOverlay = lib.composeExtensions (_final: prev: {

build/checks/default.nix

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ let
5252
in
5353

5454
{
55+
setuptools-scm = pythonSet.setuptools-scm.overrideAttrs (old: {
56+
passthru = old.passthru // {
57+
inherit pythonSet;
58+
};
59+
});
60+
5561
make-venv =
5662
pkgs.runCommand "venv-run-build-test"
5763
{

build/hacks/checks.nix

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,30 @@ in
102102
ln -s ${venv} $out
103103
'';
104104

105+
toNixpkgs =
106+
let
107+
overlay = hacks.toNixpkgs {
108+
inherit pythonSet;
109+
packages = [
110+
"pip" # Testing dependencies
111+
"urllib3" # Testing optional-dependencies
112+
];
113+
};
114+
115+
python = pkgs.python3.override {
116+
packageOverrides = overlay;
117+
self = python;
118+
};
119+
120+
pythonEnv = python.withPackages (ps: [
121+
ps.urllib3
122+
ps.pip
123+
]);
124+
in
125+
assert pkgs.python3.pkgs.urllib3 != python.pkgs.urllib3;
126+
assert pkgs.python3.pkgs.pip != python.pkgs.pip;
127+
pkgs.runCommand "toNixpkgs-check" { } ''
128+
${pythonEnv}/bin/python -c 'import urllib3'
129+
${pythonEnv}/bin/pip --version > $out
130+
'';
105131
}

build/hacks/default.nix

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22

33
let
44
inherit (pkgs) stdenv;
5-
inherit (lib) isDerivation isAttrs;
5+
inherit (lib) isDerivation isAttrs listToAttrs;
6+
inherit (builtins)
7+
concatMap
8+
elem
9+
attrNames
10+
mapAttrs
11+
isFunction
12+
isList
13+
typeOf
14+
filter
15+
;
616

717
in
818
{
@@ -192,4 +202,158 @@ in
192202
rustc
193203
];
194204
});
205+
206+
/**
207+
Create a nixpkgs Python (buildPythonPackage) compatible package from a pyproject.nix build package.
208+
209+
Adapts a package by:
210+
- Activating a wheel output, if not already enabled
211+
- Create a package using generated wheel as input
212+
213+
# Example
214+
215+
```nix
216+
toNixpkgs {
217+
inherit pythonSet;
218+
packages = [ "requests" ];
219+
}
220+
=>
221+
«lambda @ /nix/store/f05hjk9fh1m5py5j1ixzly07p4lla56x-source/build/hacks/default.nix:263:5»
222+
```
223+
224+
# Type
225+
226+
```
227+
nixpkgsPrebuilt :: AttrSet -> derivation
228+
```
229+
230+
# Arguments
231+
232+
pythonSet
233+
: Pyproject.nix build Python package set
234+
235+
packages
236+
: List/predicate of overlay member packages
237+
*/
238+
toNixpkgs =
239+
let
240+
# Always filter out when generating set
241+
wellKnown = [
242+
"python"
243+
"pkgs"
244+
"stdenv"
245+
"pythonPkgsBuildHost"
246+
"resolveBuildSystem"
247+
"resolveVirtualEnv"
248+
"mkVirtualEnv"
249+
"hooks"
250+
];
251+
in
252+
{
253+
pythonSet,
254+
packages ? null,
255+
}:
256+
let
257+
packages' =
258+
if (packages == null || isFunction packages) then
259+
(
260+
let
261+
hookNames = attrNames pythonSet.hooks;
262+
predicate = if packages == null then (_: true) else packages;
263+
in
264+
filter (name: !elem name wellKnown && !elem name hookNames && predicate name) (attrNames pythonSet)
265+
)
266+
else if isList packages then
267+
packages
268+
else
269+
throw "Unhandled packages type: ${typeOf packages}";
270+
271+
# Ensure wheel artifacts are created for all packages we are generating from
272+
pythonSet' = pythonSet.overrideScope (
273+
_final: prev:
274+
listToAttrs (
275+
map (
276+
name:
277+
let
278+
drv = prev.${name};
279+
in
280+
{
281+
inherit name;
282+
value =
283+
if elem "dist" (drv.outputs or [ ]) then
284+
drv
285+
else
286+
drv.overrideAttrs (old: {
287+
outputs = (old.outputs or [ "out" ]) ++ [ "dist" ];
288+
});
289+
}
290+
) packages'
291+
)
292+
);
293+
in
294+
pythonPackagesFinal: _pythonPackagesPrev:
295+
let
296+
inherit (pythonPackagesFinal) buildPythonPackage pkgs;
297+
inherit (pkgs) autoPatchelfHook;
298+
in
299+
listToAttrs (
300+
map (
301+
name:
302+
let
303+
from = pythonSet'.${name};
304+
dependencies = from.passthru.dependencies or { };
305+
optional-dependencies = from.passthru.optional-dependencies or { };
306+
in
307+
{
308+
inherit name;
309+
value = buildPythonPackage {
310+
inherit (from) pname version;
311+
src = from.dist;
312+
313+
format = "wheel";
314+
dontBuild = true;
315+
316+
# Default wheelUnpackPhase assumes we are passing a single wheel, but we are passing a dist dir
317+
unpackPhase = ''
318+
runHook preUnpack
319+
mkdir dist
320+
cp ${from.dist}/* dist/
321+
# runHook postUnpack # Calls find...?
322+
'';
323+
324+
# Include any buildInputs from build for autoPatchelfHook
325+
buildInputs = from.buildInputs or [ ];
326+
327+
nativeBuildInputs = lib.optional stdenv.isLinux [
328+
autoPatchelfHook
329+
];
330+
331+
propagatedBuildInputs = concatMap (
332+
name:
333+
let
334+
pkg = pythonPackagesFinal.${name};
335+
extras = dependencies.${name};
336+
in
337+
[ pkg ] ++ concatMap (extra: pkg.optional-dependencies.${extra}) extras
338+
) (attrNames dependencies);
339+
340+
passthru = {
341+
optional-dependencies = mapAttrs (
342+
name: dependencies:
343+
concatMap (
344+
name:
345+
let
346+
pkg = pythonPackagesFinal.${name};
347+
extras = dependencies.${name};
348+
in
349+
[ pkg ] ++ concatMap (extra: pkg.optional-dependencies.${extra}) extras
350+
) (attrNames dependencies)
351+
) optional-dependencies;
352+
};
353+
354+
# Note: PEP-735 dependency groups are dropped as nixpkgs lacks support.
355+
};
356+
}
357+
) packages'
358+
);
195359
}

build/hacks/tests.nix

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
{
2+
pkgs,
3+
lib,
4+
pyproject-nix,
5+
}:
6+
let
7+
hacks = pkgs.callPackages pyproject-nix.build.hacks { };
8+
9+
python = pkgs.python3;
10+
11+
buildSystems = import ../checks/build-systems.nix {
12+
inherit lib;
13+
};
14+
15+
pythonSet =
16+
(pkgs.callPackage pyproject-nix.build.packages {
17+
inherit python;
18+
}).overrideScope
19+
buildSystems;
20+
21+
in
22+
{
23+
toNixpkgs = {
24+
testList =
25+
let
26+
overlay = hacks.toNixpkgs {
27+
inherit pythonSet;
28+
packages = [
29+
"pip" # Testing dependencies
30+
"urllib3" # Testing optional-dependencies
31+
];
32+
};
33+
python = pkgs.python3.override {
34+
packageOverrides = overlay;
35+
self = python;
36+
};
37+
38+
in
39+
{
40+
expr = {
41+
urllib3 = python.pkgs.urllib3.version;
42+
pip = python.pkgs.pip.version;
43+
};
44+
expected = {
45+
urllib3 = "2.4.0";
46+
pip = "25.0.1";
47+
};
48+
};
49+
50+
testPredicate =
51+
let
52+
overlay = hacks.toNixpkgs {
53+
inherit pythonSet;
54+
packages = lib.flip lib.elem [
55+
"pip"
56+
"urllib3"
57+
];
58+
};
59+
python = pkgs.python3.override {
60+
packageOverrides = overlay;
61+
self = python;
62+
};
63+
in
64+
{
65+
expr = {
66+
urllib3 = python.pkgs.urllib3.version;
67+
pip = python.pkgs.pip.version;
68+
};
69+
expected = {
70+
urllib3 = "2.4.0";
71+
pip = "25.0.1";
72+
};
73+
};
74+
75+
testNull =
76+
let
77+
overlay = hacks.toNixpkgs {
78+
inherit pythonSet;
79+
};
80+
python = pkgs.python3.override {
81+
packageOverrides = overlay;
82+
self = python;
83+
};
84+
85+
in
86+
{
87+
expr = {
88+
urllib3 = python.pkgs.urllib3.version;
89+
pip = python.pkgs.pip.version;
90+
};
91+
expected = {
92+
urllib3 = "2.4.0";
93+
pip = "25.0.1";
94+
};
95+
};
96+
};
97+
}

0 commit comments

Comments
 (0)