From de1cc2c2512f76b3573862d3141a59bdfb3f511e Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Wed, 1 Jul 2026 14:30:00 +0700 Subject: [PATCH 1/2] fix: run of circular-dependencies with python3.15 Multiprocess uses dill to pickle the nested handle_module2 closure and dill's Python 3.15 support is broken (co_lnotab was removed from code objects). Review hint: use `git show -w --color-moved=dimmed-zebra` --- contrib/devtools/circular-dependencies.py | 55 ++++++++++++----------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/contrib/devtools/circular-dependencies.py b/contrib/devtools/circular-dependencies.py index e939ec6d45ba..bbbe62b1a365 100755 --- a/contrib/devtools/circular-dependencies.py +++ b/contrib/devtools/circular-dependencies.py @@ -5,7 +5,7 @@ import sys import re -from multiprocess import Pool # type: ignore[import] +import multiprocessing from typing import Dict, List, Set MAPPING = { @@ -33,10 +33,32 @@ def module_name(path): return path[:-4] return None -if __name__=="__main__": - files = dict() - deps: Dict[str, Set[str]] = dict() +files = dict() +deps: Dict[str, Set[str]] = dict() + +# Defined at module level (reading the global `deps`) so it pickles by reference +# for multiprocessing.Pool; forked workers inherit the populated `deps`. +def handle_module2(module): + # Build the transitive closure of dependencies of module + closure: Dict[str, List[str]] = dict() + for dep in deps[module]: + closure[dep] = [] + while True: + old_size = len(closure) + old_closure_keys = sorted(closure.keys()) + for src in old_closure_keys: + for dep in deps[src]: + if dep not in closure: + closure[dep] = closure[src] + [src] + if len(closure) == old_size: + break + # If module is in its own transitive closure, it's a circular dependency; check if it is the shortest + if module in closure: + return [module] + closure[module] + + return None +if __name__=="__main__": RE = re.compile("^#include <(.*)>") def handle_module(arg): @@ -47,27 +69,6 @@ def handle_module(arg): files[arg] = module deps[module] = set() - def handle_module2(module): - # Build the transitive closure of dependencies of module - closure: Dict[str, List[str]] = dict() - for dep in deps[module]: - closure[dep] = [] - while True: - old_size = len(closure) - old_closure_keys = sorted(closure.keys()) - for src in old_closure_keys: - for dep in deps[src]: - if dep not in closure: - closure[dep] = closure[src] + [src] - if len(closure) == old_size: - break - # If module is in its own transitive closure, it's a circular dependency; check if it is the shortest - if module in closure: - return [module] + closure[module] - - return None - - # Iterate over files, and create list of modules for arg in sys.argv[1:]: handle_module(arg) @@ -101,7 +102,9 @@ def shortest_c_dep(): if sorted_keys is None: sorted_keys = sorted(deps.keys()) - with Pool(8) as pool: + # Use fork so workers inherit the populated `deps` global without + # having to pickle it for every task. + with multiprocessing.get_context("fork").Pool(8) as pool: cycles = pool.map(handle_module2, sorted_keys) for cycle in cycles: From 7cb0c292f3325a619e1dc04d63237c993f74a025 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Fri, 3 Jul 2026 11:37:35 +0300 Subject: [PATCH 2/2] fix: fall back to serial map when fork is unavailable, drop multiprocess dep get_context("fork") raises ValueError on platforms without fork (Windows), so guard it and run handle_module2 serially there. Also remove the now-unused multiprocess pip package, whose only consumer was this script. Co-Authored-By: Claude Opus 4.8 --- contrib/containers/ci/ci-slim.Dockerfile | 1 - contrib/devtools/circular-dependencies.py | 10 +++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/contrib/containers/ci/ci-slim.Dockerfile b/contrib/containers/ci/ci-slim.Dockerfile index 5332f8504a57..8436fd14dfa3 100644 --- a/contrib/containers/ci/ci-slim.Dockerfile +++ b/contrib/containers/ci/ci-slim.Dockerfile @@ -81,7 +81,6 @@ RUN uv pip install --system --break-system-packages \ flake8==5.0.4 \ jinja2 \ lief==0.13.2 \ - multiprocess \ mypy==0.981 \ pyzmq==24.0.1 \ vulture==2.6 diff --git a/contrib/devtools/circular-dependencies.py b/contrib/devtools/circular-dependencies.py index bbbe62b1a365..a6cdc343d5cb 100755 --- a/contrib/devtools/circular-dependencies.py +++ b/contrib/devtools/circular-dependencies.py @@ -103,9 +103,13 @@ def shortest_c_dep(): sorted_keys = sorted(deps.keys()) # Use fork so workers inherit the populated `deps` global without - # having to pickle it for every task. - with multiprocessing.get_context("fork").Pool(8) as pool: - cycles = pool.map(handle_module2, sorted_keys) + # having to pickle it for every task. fork is unavailable on + # Windows, so fall back to a serial map there. + if "fork" in multiprocessing.get_all_start_methods(): + with multiprocessing.get_context("fork").Pool(8) as pool: + cycles = pool.map(handle_module2, sorted_keys) + else: + cycles = list(map(handle_module2, sorted_keys)) for cycle in cycles: if cycle is not None and (shortest_cycles is None or len(cycle) < len(shortest_cycles)):