at master 3.0 kB view raw
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)