11from __future__ import annotations
22
33import contextlib
4+ import functools
45import json
56import os
7+ import platform
68import shutil
79import subprocess
810import sys
@@ -33,12 +35,53 @@ def __dir__() -> list[str]:
3335 return __all__
3436
3537
36- # Make sure we don't wait forever for programs to respond
37- # CI services can be really slow under load
38- if os .environ .get ("CI" , "" ):
39- TIMEOUT = 20
40- else :
41- TIMEOUT = 10 if sys .platform .startswith ("win" ) else 5
38+ BASE_TIMEOUT = 5
39+
40+
41+ def _macos_binary_is_x86 (path : Path ) -> bool :
42+ """
43+ Returns True if the binary is x86. Only run on macOS.
44+ """
45+ try :
46+ # lipo gives clean output like: "Architectures in the fat file: ... are: x86_64 arm64"
47+ out = subprocess .check_output (["lipo" , "-info" , path ], text = True )
48+ except (FileNotFoundError , subprocess .CalledProcessError ):
49+ # Fallback to 'file' if lipo not available or fails
50+ try :
51+ out = subprocess .check_output (["file" , path ], text = True )
52+ except (FileNotFoundError , subprocess .CalledProcessError ):
53+ return False # unknown, assume not x86
54+
55+ # Ignore native or fat binaries
56+ if "arm64" in out :
57+ return False
58+
59+ return "x86_64" in out or "i386" in out
60+
61+
62+ @functools .lru_cache (None )
63+ def compute_timeout (executable : Path ) -> int :
64+ """
65+ Compute a recommended timeout. Takes the base timeout and
66+ multiplies it based on various factors:
67+
68+ * Is on CI: quadruples it
69+ * Is windows: doubles it
70+ * Runs with Rosetta: triples it
71+
72+ These do not stack.
73+ """
74+
75+ if os .environ .get ("CI" , "" ):
76+ return BASE_TIMEOUT * 4
77+
78+ if sys .platform .startswith ("win" ):
79+ return BASE_TIMEOUT * 2
80+
81+ if sys .platform == "darwin" and platform .machine () == "arm64" :
82+ return BASE_TIMEOUT * 3 if _macos_binary_is_x86 (executable ) else BASE_TIMEOUT
83+
84+ return BASE_TIMEOUT
4285
4386
4487class Program (NamedTuple ):
@@ -89,7 +132,9 @@ def get_cmake_program(cmake_path: Path) -> Program:
89132 None if it cannot be determined.
90133 """
91134 try :
92- result = Run (timeout = TIMEOUT ).capture (cmake_path , "-E" , "capabilities" )
135+ result = Run (timeout = compute_timeout (cmake_path )).capture (
136+ cmake_path , "-E" , "capabilities"
137+ )
93138 try :
94139 version = Version (
95140 json .loads (result .stdout )["version" ]["string" ].split ("-" )[0 ]
@@ -100,7 +145,9 @@ def get_cmake_program(cmake_path: Path) -> Program:
100145 logger .warning ("Could not determine CMake version, got {!r}" , result .stdout )
101146 except subprocess .CalledProcessError :
102147 try :
103- result = Run (timeout = TIMEOUT ).capture (cmake_path , "--version" )
148+ result = Run (timeout = compute_timeout (cmake_path )).capture (
149+ cmake_path , "--version"
150+ )
104151 try :
105152 version = Version (
106153 result .stdout .splitlines ()[0 ].split ()[- 1 ].split ("-" )[0 ]
@@ -144,7 +191,9 @@ def get_ninja_programs(*, module: bool = True) -> Generator[Program, None, None]
144191 """
145192 for ninja_path in _get_ninja_path (module = module ):
146193 try :
147- result = Run (timeout = TIMEOUT ).capture (ninja_path , "--version" )
194+ result = Run (timeout = compute_timeout (ninja_path )).capture (
195+ ninja_path , "--version"
196+ )
148197 except (
149198 subprocess .CalledProcessError ,
150199 PermissionError ,
0 commit comments