From a25e43e6d7089d4655f945c9874bd6756fbb5c90 Mon Sep 17 00:00:00 2001 From: Ben Wolsieffer Date: Sat, 20 Apr 2024 14:47:00 -0400 Subject: [PATCH] pythonCatchConflictsHook: prevent exponential worst-case The hook performs a depth first search on the graph defined by propagatedBuildInputs. This traverses all paths through the graph, except for any cycles. In the worst case with a highly connected graph, this search can take exponential time. In practice, this means that in cases with long dependency chains and multiple packages depending on the same package, the hook can take several hours to run. Avoid this problem by keeping track of already visited paths and only visiting each path once. This makes the search complete in linear time. The visible effect of this change is that, if a conflict is found, only one dependency chain that leads to the conflicting package is printed, rather than all the possible dependency chains. --- .../python/catch_conflicts/catch_conflicts.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkgs/development/interpreters/python/catch_conflicts/catch_conflicts.py b/pkgs/development/interpreters/python/catch_conflicts/catch_conflicts.py index ad679d9f9f99..648fec015903 100644 --- a/pkgs/development/interpreters/python/catch_conflicts/catch_conflicts.py +++ b/pkgs/development/interpreters/python/catch_conflicts/catch_conflicts.py @@ -3,9 +3,10 @@ from pathlib import Path import collections import sys import os -from typing import Dict, List, Tuple +from typing import Dict, List, Set, Tuple do_abort: bool = False packages: Dict[str, Dict[str, List[Dict[str, List[str]]]]] = collections.defaultdict(list) +found_paths: Set[Path] = set() out_path: Path = Path(os.getenv("out")) version: Tuple[int, int] = sys.version_info site_packages_path: str = f'lib/python{version[0]}.{version[1]}/site-packages' @@ -46,6 +47,12 @@ def find_packages(store_path: Path, site_packages_path: str, parents: List[str]) site_packages: Path = (store_path / site_packages_path) propagated_build_inputs: Path = (store_path / "nix-support/propagated-build-inputs") + # only visit each path once, to avoid exponential complexity with highly + # connected dependency graphs + if store_path in found_paths: + return + found_paths.add(store_path) + # add the current package to the list if site_packages.exists(): for dist_info in site_packages.glob("*.dist-info"):