Skip to content

Commit 1652e31

Browse files
authored
Dev (#63)
* fix abi tag for python 3.8 * handle circular deps; print info ResolutionImpossible; add tests * fix readme * version 2.2.1
1 parent a206910 commit 1652e31

14 files changed

Lines changed: 147 additions & 33 deletions

Changelog.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
# 2.2.1 (11 Aug 2020)
2+
Handle circular dependencies, fix python 3.8 wheels, improve error message
3+
4+
### Features
5+
- Print more detailed info when the resolver raises a ResolutionImpossible error.
6+
- Warm on circular dependencies and fix them automatically.
7+
8+
### Fixes
9+
- Fix crash on circular dependencies.
10+
- Python 3.8 wheels have abi tag cp38, not cp38m. This was not considered before which prevented finding suitable manylinux wheels for python 3.8
11+
12+
### Development
13+
- Added integration tests under [./tests/](/tests/)
14+
115
# 2.2.0 (09 Aug 2020)
216
Improved success rate, MacOS support, bugfixes, optimizations
317

Readme.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ Table of Contents
5555
You can either install mach-nix via pip or by using nix in case you already have the nix package manager installed.
5656
#### Installing via pip
5757
```shell
58-
pip install git+git://github.com/DavHau/mach-nix@2.2.0
58+
pip install git+git://github.com/DavHau/mach-nix@2.2.1
5959
```
6060
#### Installing via nix
6161
```shell
62-
nix-env -if https://github.com/DavHau/mach-nix/tarball/2.2.0 -A mach-nix
62+
nix-env -if https://github.com/DavHau/mach-nix/tarball/2.2.1 -A mach-nix
6363
```
6464

6565
---
@@ -91,7 +91,7 @@ You can call mach-nix directly from a nix expression
9191
let
9292
mach-nix = import (builtins.fetchGit {
9393
url = "https://github.com/DavHau/mach-nix/";
94-
ref = "2.2.0";
94+
ref = "2.2.1";
9595
});
9696
in
9797
mach-nix.mkPython {
@@ -158,7 +158,7 @@ Providers can be disabled/enabled/preferred like in the following examples:
158158

159159
Mach-nix will always satisfy your **requirements.txt** fully with the configured providers or fail with a **ResolutionImpossible** error.
160160

161-
If a mach-nix build fails, Most of the times it can be resolved by just switching the provider of a package, which is simple and doesn't require writing a lot of nix code. For some more complex scenarios, checkout the following examples.
161+
If a mach-nix build fails, Most of the times it can be resolved by just switching the provider of a package, which is simple and doesn't require writing a lot of nix code. For some more complex scenarios, checkout the [./examples.md](/examples.md).
162162

163163
## Why nix?
164164
Usually people rely on multiple layers of different package management tools for building their software environments. These tools are often not well integrated with each other and don't offer strong reproducibility. Example: You are on debian/ubuntu and use APT (layer 1) to install python. Then you use venv (layer 2) to overcome some of your layer 1 limitations (not being able to have multiple versions of the same package installed) and afterwards you are using pip (layer 3) to install python packages. You notice that even after pinning all your requirements, your environment behaves differently on your server or your colleagues machine because their underlying system differs from yours. You start using docker (layer 4) to overcome this problem which adds extra complexity to the whole process and gives you some nasty limitations during development. You need to configure your IDE's docker integration and so on. Despite all the effort you put in, still the problem is not fully solved and from time to time your build pipeline just breaks and you need to fix it manually.

examples.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ build a python environment from a list of requirements
2424
let
2525
mach-nix = import (builtins.fetchGit {
2626
url = "https://github.com/DavHau/mach-nix/";
27-
ref = "2.2.0";
27+
ref = "2.2.1";
2828
});
2929
in mach-nix.mkPython {
3030
requirements = builtins.readFile ./requirements.txt;
@@ -37,7 +37,7 @@ Build a python package from its source code and a list of requirements
3737
let
3838
mach-nix = import (builtins.fetchGit {
3939
url = "https://github.com/DavHau/mach-nix/";
40-
ref = "2.2.0";
40+
ref = "2.2.1";
4141
});
4242
in mach-nix.buildPythonPackage {
4343
pname = "my-package";
@@ -53,7 +53,7 @@ Build a python package from its source code and a list of requirements
5353
let
5454
mach-nix = import (builtins.fetchGit {
5555
url = "https://github.com/DavHau/mach-nix/";
56-
ref = "2.2.0";
56+
ref = "2.2.1";
5757
});
5858
in mach-nix.buildPythonPackage rec {
5959
pname = "projectname";
@@ -77,7 +77,7 @@ I have a complex set of requirements including tensorflow. I'd like to have tens
7777
let
7878
mach-nix = import (builtins.fetchGit {
7979
url = "https://github.com/DavHau/mach-nix/";
80-
ref = "2.2.0";
80+
ref = "2.2.1";
8181
});
8282
in mach-nix.mkPython {
8383
@@ -100,7 +100,7 @@ I'd like to install a more recent version of tensorflow which is not available f
100100
let
101101
mach-nix = import (builtins.fetchGit {
102102
url = "https://github.com/DavHau/mach-nix/";
103-
ref = "2.2.0";
103+
ref = "2.2.1";
104104
});
105105
in mach-nix.mkPython {
106106
@@ -129,7 +129,7 @@ I'd like to use a recent version of Pytorch from wheel, but I'd like to build th
129129
let
130130
mach-nix = import (builtins.fetchGit {
131131
url = "https://github.com/DavHau/mach-nix/";
132-
ref = "2.2.0";
132+
ref = "2.2.1";
133133
});
134134
overlays = []; # some very useful overlays
135135
in mach-nix.mkPython rec {
@@ -164,7 +164,7 @@ I have a complex requirements.txt which includes `imagecodecs`. It is available
164164
let
165165
mach-nix = import (builtins.fetchGit {
166166
url = "https://github.com/DavHau/mach-nix/";
167-
ref = "2.2.0";
167+
ref = "2.2.1";
168168
});
169169
in mach-nix.mkPython rec {
170170

mach_nix/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.2.0
1+
2.2.1

mach_nix/data/providers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ def __init__(self, data_dir: str, *args, **kwargs):
260260
maj = self.py_ver_digits[0] # major version
261261
min = self.py_ver_digits[1] # minor version
262262
if self.system == "linux":
263-
cp_abi = f"cp{maj}{min}mu" if int(maj) == 2 else f"cp{maj}{min}m"
263+
cp_abi = f"cp{maj}{min}mu" if int(maj) == 2 else f"cp{maj}{min}m?"
264264
self.preferred_wheels = (
265265
re.compile(rf"(py{maj}|cp{maj}){min}?-({cp_abi}|abi3|none)-manylinux2014_{self.platform}"),
266266
re.compile(rf"(py{maj}|cp{maj}){min}?-({cp_abi}|abi3|none)-manylinux2010_{self.platform}"),
Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from operator import itemgetter
2-
from typing import Iterable
2+
from typing import Iterable, List
3+
34
from tree_format import format_tree
45

56
from mach_nix.data.nixpkgs import NixpkgsIndex
@@ -15,23 +16,45 @@ def __init__(self, pkg: ResolvedPkg, name, parent: 'Node' = None):
1516
if parent:
1617
self.parent.children.append(self)
1718

19+
def all_parents(self) -> List['Node']:
20+
if self.parent is None:
21+
return []
22+
return [self.parent] + self.parent.all_parents()
23+
1824

1925
def make_name(pkg: ResolvedPkg, nixpkgs: NixpkgsIndex):
2026
pi = pkg.provider_info
21-
name = f"{pkg.name} - {pkg.ver} - {pi.provider}"
27+
extras = f"[{' '.join(pkg.extras_selected)}]" if pkg.extras_selected else ''
28+
name = f"{pkg.name}{extras} - {pkg.ver} - {pi.provider}"
2229
if pi.provider == 'wheel':
2330
name += f" - {'-'.join(pi.wheel_fname.split('-')[-3:])[:-4]}"
2431
if pi.provider == 'nixpkgs':
2532
name += f" (attrs: {' '.join(c.nix_key for c in nixpkgs.get_all_candidates(pkg.name))})"
2633
return name
2734

2835

29-
def build_tree(pkgs: dict, root: Node, nixpkgs: NixpkgsIndex):
36+
def build_tree(pkgs: dict, root: Node, nixpkgs: NixpkgsIndex) -> List[str]:
37+
"""
38+
Recursively adds children to given root node.
39+
Removes cycles from original graph while processing.
40+
Returns list of warnings.
41+
"""
3042
root_pkg = pkgs[root.pkg.name]
43+
warnings = []
3144
for name in sorted(root_pkg.build_inputs + root_pkg.prop_build_inputs):
3245
child_pkg: ResolvedPkg = pkgs[name]
46+
# detect circles
47+
if child_pkg in [node.pkg for node in root.all_parents()]:
48+
warnings.append(
49+
f"WARNING: Circular dependency detected and removed:"
50+
f" {root.pkg.name}:{root.pkg.ver} -> {child_pkg.name}:{child_pkg.ver}")
51+
root.pkg.build_inputs = [bi for bi in root.pkg.build_inputs if bi != child_pkg.name]
52+
root.pkg.prop_build_inputs = [bi for bi in root.pkg.prop_build_inputs if bi != child_pkg.name]
53+
root.pkg.removed_circular_deps.append(child_pkg.name)
54+
continue
3355
child_node = Node(child_pkg, make_name(child_pkg, nixpkgs), root)
34-
build_tree(pkgs, child_node, nixpkgs)
56+
warnings += build_tree(pkgs, child_node, nixpkgs)
57+
return warnings
3558

3659

3760
def tree_to_dict(root_node: Node):
@@ -44,11 +67,13 @@ def print_tree(root_node):
4467
d, format_node=itemgetter(0), get_children=itemgetter(1)))
4568

4669

47-
def print_deps(pkgs: Iterable[ResolvedPkg], nixpkgs: NixpkgsIndex):
70+
def remove_circles_and_print(pkgs: Iterable[ResolvedPkg], nixpkgs: NixpkgsIndex):
4871
print("\n### Resolved Dependencies ###\n")
4972
indexed_pkgs = {p.name: p for p in sorted(pkgs, key=lambda p: p.name)}
5073
roots: Iterable[ResolvedPkg] = (p for p in pkgs if p.is_root)
5174
for root in roots:
5275
root_node = Node(root, make_name(root, nixpkgs))
53-
build_tree(indexed_pkgs, root_node, nixpkgs)
76+
warnings = build_tree(indexed_pkgs, root_node, nixpkgs)
5477
print_tree(root_node)
78+
if warnings:
79+
print(''.join(warnings) + '\n')

mach_nix/generate.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
import json
12
import os
23
import sys
4+
from os.path import dirname
5+
from pprint import pformat
6+
from typing import List
37

8+
from resolvelib import ResolutionImpossible
9+
from resolvelib.resolvers import RequirementInformation
10+
11+
import mach_nix
412
from mach_nix.data.nixpkgs import NixpkgsIndex
513
from mach_nix.data.providers import CombinedDependencyProvider, ProviderSettings
614
from mach_nix.generators.overides_generator import OverridesGenerator
@@ -18,10 +26,12 @@ def load_env(name, *args, **kwargs):
1826

1927

2028
def main():
29+
providers_json = load_env('providers')
30+
2131
disable_checks = load_env('disable_checks')
2232
nixpkgs_json = load_env('nixpkgs_json')
2333
out_file = load_env('out_file')
24-
provider_settings = ProviderSettings(load_env('providers'))
34+
provider_settings = ProviderSettings(providers_json)
2535
py_ver_str = load_env('py_ver_str')
2636
pypi_deps_db_src = load_env('pypi_deps_db_src')
2737
pypi_fetcher_commit = load_env('pypi_fetcher_commit')
@@ -49,9 +59,35 @@ def main():
4959
ResolvelibResolver(nixpkgs, deps_provider),
5060
)
5161
reqs = filter_reqs_by_eval_marker(parse_reqs(requirements), context(py_ver, platform, system))
52-
expr = generator.generate(reqs)
53-
with open(out_file, 'w') as f:
54-
f.write(expr)
62+
try:
63+
expr = generator.generate(reqs)
64+
except ResolutionImpossible as e:
65+
handle_resolution_impossible(e, requirements, providers_json, py_ver_str)
66+
exit(1)
67+
else:
68+
with open(out_file, 'w') as f:
69+
f.write(expr)
70+
71+
72+
def handle_resolution_impossible(exc: ResolutionImpossible, reqs_str, providers_json, py_ver_str):
73+
causes: List[RequirementInformation] = exc.causes
74+
causes_str = ''
75+
for ri in causes:
76+
causes_str += f"\n {ri.requirement}"
77+
if ri.parent:
78+
causes_str += \
79+
f" - parent: {ri.parent.name}{ri.parent.extras if ri.parent.extras else None}:{ri.parent.version}"
80+
nl = '\n'
81+
print(
82+
f"\nSome requirements could not be resolved.\n"
83+
f"Top level requirements: \n {' '.join(l for l in reqs_str.splitlines())}\n"
84+
f"Providers:\n {f'{nl} '.join(pformat(json.loads(providers_json)).splitlines())}\n"
85+
f"Mach-nix version: {open(dirname(mach_nix.__file__) + '/VERSION').read().strip()}\n"
86+
f"Python: {py_ver_str}\n"
87+
f"Cause: {exc.__context__}\n"
88+
f"The requirements which caused the error:"
89+
f"{causes_str}\n",
90+
file=sys.stderr)
5591

5692

5793
if __name__ == "__main__":

mach_nix/generators/overides_generator.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,17 @@ def _gen_prop_build_inputs(self, prop_build_inputs_local, prop_build_inputs_nixp
6868
f"{b}" for b in sorted(prop_build_inputs_local | prop_build_inputs_nixpkgs))
6969
return prop_build_inputs_str
7070

71-
def _gen_overrideAttrs(self, name, ver, nix_name, build_inputs_str, prop_build_inputs_str, keep_src=False):
71+
def _gen_overrideAttrs(self, name, ver, circular_deps, nix_name, build_inputs_str, prop_build_inputs_str, keep_src=False):
7272
out = f"""
7373
{nix_name} = python-super.{nix_name}.overridePythonAttrs ( oldAttrs: {{
7474
pname = "{name}";
7575
version = "{ver}";"""
7676
if not keep_src:
7777
out += f"""
7878
src = fetchPypi "{name}" "{ver}";"""
79+
if circular_deps:
80+
out += f"""
81+
pipInstallFlags = "--no-dependencies";"""
7982
if build_inputs_str:
8083
out += f"""
8184
buildInputs = with python-self; (filter_deps oldAttrs "buildInputs") ++ [ {build_inputs_str} ];"""
@@ -90,12 +93,15 @@ def _gen_overrideAttrs(self, name, ver, nix_name, build_inputs_str, prop_build_i
9093
});\n"""
9194
return unindent(out, 8)
9295

93-
def _gen_builPythonPackage(self, name, ver, nix_name, build_inputs_str, prop_build_inputs_str):
96+
def _gen_builPythonPackage(self, name, ver, circular_deps, nix_name, build_inputs_str, prop_build_inputs_str):
9497
out = f"""
9598
{nix_name} = python-self.buildPythonPackage {{
9699
pname = "{name}";
97100
version = "{ver}";
98101
src = fetchPypi "{name}" "{ver}";"""
102+
if circular_deps:
103+
out += f"""
104+
pipInstallFlags = "--no-dependencies";"""
99105
if build_inputs_str.strip():
100106
out += f"""
101107
buildInputs = with python-self; [ {build_inputs_str} ];"""
@@ -110,7 +116,7 @@ def _gen_builPythonPackage(self, name, ver, nix_name, build_inputs_str, prop_bui
110116
};\n"""
111117
return unindent(out, 8)
112118

113-
def _gen_wheel_buildPythonPackage(self, name, ver, prop_build_inputs_str, fname):
119+
def _gen_wheel_buildPythonPackage(self, name, ver, circular_deps, prop_build_inputs_str, fname):
114120
manylinux = "manylinux1 ++ " if 'manylinux' in fname else ''
115121

116122
# dontStrip added due to this bug - https://github.com/pypa/manylinux/issues/119
@@ -123,6 +129,9 @@ def _gen_wheel_buildPythonPackage(self, name, ver, prop_build_inputs_str, fname)
123129
doCheck = false;
124130
doInstallCheck = false;
125131
dontStrip = true;"""
132+
if circular_deps:
133+
out += f"""
134+
pipInstallFlags = "--no-dependencies";"""
126135
if manylinux:
127136
out += f"""
128137
nativeBuildInputs = [ autoPatchelfHook ];
@@ -174,19 +183,19 @@ def _gen_overrides(self, pkgs: Dict[str, ResolvedPkg], overlay_keys, pkgs_names:
174183
# or by creating it from scratch using `buildPythonPackage`
175184
nix_name = self._get_ref_name(pkg.name, pkg.ver)
176185
if self.nixpkgs.exists(pkg.name):
177-
out += self._gen_overrideAttrs(pkg.name, pkg.ver, nix_name, build_inputs_str, prop_build_inputs_str)
186+
out += self._gen_overrideAttrs(pkg.name, pkg.ver, pkg.removed_circular_deps, nix_name, build_inputs_str, prop_build_inputs_str)
178187
out += self._unify_nixpkgs_keys(pkg.name, main_key=nix_name)
179188
else:
180-
out += self._gen_builPythonPackage(pkg.name, pkg.ver, nix_name, build_inputs_str, prop_build_inputs_str)
189+
out += self._gen_builPythonPackage(pkg.name, pkg.ver, pkg.removed_circular_deps, nix_name, build_inputs_str, prop_build_inputs_str)
181190
elif pkg.provider_info.provider == WheelDependencyProvider.name:
182-
out += self._gen_wheel_buildPythonPackage(pkg.name, pkg.ver, prop_build_inputs_str,
191+
out += self._gen_wheel_buildPythonPackage(pkg.name, pkg.ver, pkg.removed_circular_deps, prop_build_inputs_str,
183192
pkg.provider_info.wheel_fname)
184193
if self.nixpkgs.exists(pkg.name):
185194
out += self._unify_nixpkgs_keys(pkg.name)
186195
elif pkg.provider_info.provider == NixpkgsDependencyProvider.name:
187196
nix_name = self.nixpkgs.find_best_nixpkgs_candidate(pkg.name, pkg.ver)
188197
out += self._gen_overrideAttrs(
189-
pkg.name, pkg.ver, nix_name, build_inputs_str, prop_build_inputs_str,
198+
pkg.name, pkg.ver, pkg.removed_circular_deps, nix_name, build_inputs_str, prop_build_inputs_str,
190199
keep_src=True)
191200
out += self._unify_nixpkgs_keys(pkg.name, main_key=nix_name)
192201
end_overlay_section = f"""

mach_nix/resolver/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from abc import ABC, abstractmethod
2-
from dataclasses import dataclass
2+
from dataclasses import dataclass, field
33
from typing import List, Iterable, Optional
44

55
from packaging.version import Version
@@ -17,6 +17,7 @@ class ResolvedPkg:
1717
is_root: bool
1818
provider_info: ProviderInfo
1919
extras_selected: List[str]
20+
removed_circular_deps: List[str] = field(default_factory=list)
2021

2122

2223
class Resolver(ABC):

mach_nix/resolver/resolvelib_resolver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from mach_nix.requirements import Requirement
1010
from mach_nix.resolver import Resolver, ResolvedPkg
1111
from mach_nix.versions import filter_versions
12-
from mach_nix.visualize import print_deps
12+
from mach_nix.deptree import remove_circles_and_print
1313

1414

1515
@dataclass
@@ -81,5 +81,5 @@ def resolve(self, reqs: Iterable[Requirement]) -> List[ResolvedPkg]:
8181
provider_info=provider_info,
8282
extras_selected=list(result.mapping[name].extras)
8383
))
84-
print_deps(nix_py_pkgs, self.nixpkgs)
84+
remove_circles_and_print(nix_py_pkgs, self.nixpkgs)
8585
return nix_py_pkgs

0 commit comments

Comments
 (0)