1# shellcheck shell=bash
2
3# Setup hook that modifies Python dependencies versions.
4#
5# Example usage in a derivation:
6#
7# { …, pythonPackages, … }:
8#
9# pythonPackages.buildPythonPackage {
10# …
11# nativeBuildInputs = [ pythonPackages.pythonRelaxDepsHook ];
12#
13# # This will relax the dependency restrictions
14# # e.g.: abc>1,<=2 -> abc
15# pythonRelaxDeps = [ "abc" ];
16# # This will relax all dependencies restrictions instead
17# # pythonRelaxDeps = true;
18# # This will remove the dependency
19# # e.g.: cde>1,<=2 -> <nothing>
20# pythonRemoveDeps = [ "cde" ];
21# # This will remove all dependencies from the project
22# # pythonRemoveDeps = true;
23# …
24# }
25#
26# IMPLEMENTATION NOTES:
27#
28# The "Requires-Dist" dependency specification format is described in PEP 508.
29# Examples that the regular expressions in this hook needs to support:
30#
31# Requires-Dist: foo
32# -> foo
33# Requires-Dist: foo[optional]
34# -> foo[optional]
35# Requires-Dist: foo[optional]~=1.2.3
36# -> foo[optional]
37# Requires-Dist: foo[optional, xyz] (~=1.2.3)
38# -> foo[optional, xyz]
39# Requires-Dist: foo[optional]~=1.2.3 ; os_name = "posix"
40# -> foo[optional] ; os_name = "posix"
41#
42# Currently unsupported: URL specs (foo @ https://example.com/a.zip).
43
44_pythonRelaxDeps() {
45 local -r metadata_file="$1"
46
47 if [[ -z "${pythonRelaxDeps:-}" ]] || [[ "$pythonRelaxDeps" == 0 ]]; then
48 return
49 elif [[ "$pythonRelaxDeps" == 1 ]]; then
50 sed -i "$metadata_file" -r \
51 -e 's/(Requires-Dist: [a-zA-Z0-9_.-]+\s*(\[[^]]+\])?)[^;]*(;.*)?/\1\3/'
52 else
53 for dep in $pythonRelaxDeps; do
54 sed -i "$metadata_file" -r \
55 -e "s/(Requires-Dist: $dep\s*(\[[^]]+\])?)[^;]*(;.*)?/\1\3/"
56 done
57 fi
58}
59
60_pythonRemoveDeps() {
61 local -r metadata_file="$1"
62
63 if [[ -z "${pythonRemoveDeps:-}" ]] || [[ "$pythonRemoveDeps" == 0 ]]; then
64 return
65 elif [[ "$pythonRemoveDeps" == 1 ]]; then
66 sed -i "$metadata_file" \
67 -e '/Requires-Dist:.*/d'
68 else
69 for dep in $pythonRemoveDeps; do
70 sed -i "$metadata_file" \
71 -e "/Requires-Dist: $dep/d"
72 done
73 fi
74
75}
76
77pythonRelaxDepsHook() {
78 pushd dist
79
80 # See https://peps.python.org/pep-0491/#escaping-and-unicode
81 local -r pkg_name="${pname//[^[:alnum:].]/_}"
82 local -r unpack_dir="unpacked"
83 local -r metadata_file="$unpack_dir/$pkg_name*/$pkg_name*.dist-info/METADATA"
84
85 # We generally shouldn't have multiple wheel files, but let's be safer here
86 for wheel in "$pkg_name"*".whl"; do
87 PYTHONPATH="@wheel@/@pythonSitePackages@:$PYTHONPATH" \
88 @pythonInterpreter@ -m wheel unpack --dest "$unpack_dir" "$wheel"
89 rm -rf "$wheel"
90
91 # Using no quotes on purpose since we need to expand the glob from `$metadata_file`
92 _pythonRelaxDeps $metadata_file
93 _pythonRemoveDeps $metadata_file
94
95 if (( "${NIX_DEBUG:-0}" >= 1 )); then
96 echo "pythonRelaxDepsHook: resulting METADATA for '$wheel':"
97 cat $metadata_file
98 fi
99
100 PYTHONPATH="@wheel@/@pythonSitePackages@:$PYTHONPATH" \
101 @pythonInterpreter@ -m wheel pack "$unpack_dir/$pkg_name"*
102 done
103
104 # Remove the folder since it will otherwise be in the dist output.
105 rm -rf "$unpack_dir"
106
107 popd
108}
109
110postBuild+=" pythonRelaxDepsHook"