1from importlib.metadata import PathDistribution
2from pathlib import Path
3import collections
4import sys
5import os
6from typing import Dict, List, Set, Tuple
7do_abort: bool = False
8packages: Dict[str, Dict[str, Dict[str, List[str]]]] = collections.defaultdict(dict)
9found_paths: Set[Path] = set()
10out_path: Path = Path(os.getenv("out"))
11version: Tuple[int, int] = sys.version_info
12site_packages_path: str = f'lib/python{version[0]}.{version[1]}/site-packages'
13
14
15def get_name(dist: PathDistribution) -> str:
16 return dist.metadata['name'].lower().replace('-', '_')
17
18
19# pretty print a package
20def describe_package(dist: PathDistribution) -> str:
21 return f"{get_name(dist)} {dist.version} ({dist._path})"
22
23
24# pretty print a list of parents (dependency chain)
25def describe_parents(parents: List[str]) -> str:
26 if not parents:
27 return ""
28 return \
29 f" dependency chain:\n " \
30 + str(f"\n ...depending on: ".join(parents))
31
32
33# inserts an entry into 'packages'
34def add_entry(name: str, version: str, store_path: str, parents: List[str]) -> None:
35 packages[name][store_path] = dict(
36 version=version,
37 parents=parents,
38 )
39
40
41# transitively discover python dependencies and store them in 'packages'
42def find_packages(store_path: Path, site_packages_path: str, parents: List[str]) -> None:
43 site_packages: Path = (store_path / site_packages_path)
44 propagated_build_inputs: Path = (store_path / "nix-support/propagated-build-inputs")
45
46 # only visit each path once, to avoid exponential complexity with highly
47 # connected dependency graphs
48 if store_path in found_paths:
49 return
50 found_paths.add(store_path)
51
52 # add the current package to the list
53 if site_packages.exists():
54 for dist_info in site_packages.glob("*.dist-info"):
55 dist: PathDistribution = PathDistribution(dist_info)
56 add_entry(get_name(dist), dist.version, store_path, parents)
57
58 # recursively add dependencies
59 if propagated_build_inputs.exists():
60 with open(propagated_build_inputs, "r") as f:
61 build_inputs: List[str] = f.read().split()
62 for build_input in build_inputs:
63 find_packages(Path(build_input), site_packages_path, parents + [build_input])
64
65
66find_packages(out_path, site_packages_path, [f"this derivation: {out_path}"])
67
68# print all duplicates
69for name, store_paths in packages.items():
70 if len(store_paths) > 1:
71 do_abort = True
72 print("Found duplicated packages in closure for dependency '{}': ".format(name))
73 for store_path, candidate in store_paths.items():
74 print(f" {name} {candidate['version']} ({store_path})")
75 print(describe_parents(candidate['parents']))
76
77# fail if duplicates were found
78if do_abort:
79 print("")
80 print(
81 "Package duplicates found in closure, see above. Usually this "
82 "happens if two packages depend on different version "
83 "of the same dependency."
84 )
85 sys.exit(1)