Skip to content

Commit 8dbe685

Browse files
authored
Refactor local runtime info functions and improve structure
1 parent ba87607 commit 8dbe685

File tree

1 file changed

+117
-75
lines changed

1 file changed

+117
-75
lines changed

bazel/repo_rules/get_local_runtime_info.py

Lines changed: 117 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,41 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
1514
"""Returns information about the local Python runtime as JSON."""
1615

16+
import glob
1717
import json
1818
import os
1919
import sys
2020
import sysconfig
21+
from typing import Any
2122

2223
_IS_WINDOWS = sys.platform == "win32"
2324
_IS_DARWIN = sys.platform == "darwin"
2425

2526

26-
def _search_directories(get_config, base_executable):
27+
def _get_abi_flags(get_config) -> str:
28+
"""Returns the ABI flags for the Python runtime."""
29+
# sys.abiflags may not exist, but it still may be set in the config.
30+
abi_flags = getattr(sys, "abiflags", None)
31+
if abi_flags is None:
32+
abi_flags = get_config("ABIFLAGS") or get_config("abiflags") or ""
33+
return abi_flags
34+
35+
36+
def _get_shlib_suffix(get_config) -> str:
37+
"""Returns the shared library suffix for the platform."""
38+
if _IS_DARWIN:
39+
return ".dylib"
40+
if _IS_WINDOWS:
41+
return ".dll"
42+
suffix = get_config("SHLIB_SUFFIX")
43+
if not suffix:
44+
suffix = ".so"
45+
return suffix
46+
47+
48+
def _search_directories(get_config, base_executable) -> list[str]:
2749
"""Returns a list of library directories to search for shared libraries."""
2850
# There's several types of libraries with different names and a plethora
2951
# of settings, and many different config variables to check:
@@ -74,23 +96,12 @@ def _search_directories(get_config, base_executable):
7496
lib_dirs.append(os.path.join(os.path.dirname(exec_dir), "lib"))
7597

7698
# Dedup and remove empty values, keeping the order.
77-
lib_dirs = [v for v in lib_dirs if v]
78-
return {k: None for k in lib_dirs}.keys()
99+
return list(dict.fromkeys(d for d in lib_dirs if d))
79100

80101

81-
def _get_shlib_suffix(get_config) -> str:
82-
"""Returns the suffix for shared libraries."""
83-
if _IS_DARWIN:
84-
return ".dylib"
85-
if _IS_WINDOWS:
86-
return ".dll"
87-
suffix = get_config("SHLIB_SUFFIX")
88-
if not suffix:
89-
suffix = ".so"
90-
return suffix
91-
92-
93-
def _search_library_names(get_config, shlib_suffix):
102+
def _search_library_names(
103+
get_config, version, abi_flags, shlib_suffix
104+
) -> list[str]:
94105
"""Returns a list of library files to search for shared libraries."""
95106
# Quoting configure.ac in the cpython code base:
96107
# "INSTSONAME is the name of the shared library that will be use to install
@@ -117,57 +128,32 @@ def _search_library_names(get_config, shlib_suffix):
117128
# Set the prefix and suffix to construct the library name used for linking.
118129
# The suffix and version are set here to the default values for the OS,
119130
# since they are used below to construct "default" library names.
120-
if _IS_DARWIN:
121-
prefix = "lib"
122-
elif _IS_WINDOWS:
131+
if _IS_WINDOWS:
123132
prefix = ""
124133
else:
125134
prefix = "lib"
126135

127-
version = get_config("VERSION")
128-
129136
# Ensure that the pythonXY.dll files are included in the search.
130137
lib_names.append(f"{prefix}python{version}{shlib_suffix}")
131138

132139
# If there are ABIFLAGS, also add them to the python version lib search.
133-
abiflags = get_config("ABIFLAGS") or get_config("abiflags") or ""
134-
if abiflags:
135-
lib_names.append(f"{prefix}python{version}{abiflags}{shlib_suffix}")
140+
if abi_flags:
141+
lib_names.append(f"{prefix}python{version}{abi_flags}{shlib_suffix}")
136142

137-
# Add the abi-version includes to the search list.
138-
lib_names.append(f"{prefix}python{sys.version_info.major}{shlib_suffix}")
143+
return list(dict.fromkeys(k for k in lib_names if k))
139144

140-
# Dedup and remove empty values, keeping the order.
141-
lib_names = [v for v in lib_names if v]
142-
return {k: None for k in lib_names}.keys()
143-
144-
145-
def _get_python_library_info(base_executable):
146-
"""Returns a dictionary with the static and dynamic python libraries."""
147-
config_vars = sysconfig.get_config_vars()
148-
149-
# VERSION is X.Y in Linux/macOS and XY in Windows. This is used to
150-
# construct library paths such as python3.12, so ensure it exists.
151-
if not config_vars.get("VERSION"):
152-
if sys.platform == "win32":
153-
config_vars["VERSION"] = (
154-
f"{sys.version_info.major}{sys.version_info.minor}"
155-
)
156-
else:
157-
config_vars["VERSION"] = (
158-
f"{sys.version_info.major}.{sys.version_info.minor}"
159-
)
160-
161-
shlib_suffix = _get_shlib_suffix(config_vars.get)
162-
search_directories = _search_directories(config_vars.get, base_executable)
163-
search_libnames = _search_library_names(config_vars.get, shlib_suffix)
164-
165-
interface_libraries = {}
166-
dynamic_libraries = {}
167-
static_libraries = {}
168145

146+
def _do_library_search(
147+
*,
148+
search_directories: list[str],
149+
libnames: list[str],
150+
) -> tuple[dict[str, None], dict[str, None], dict[str, None]]:
151+
"""Finds existing libnames in search_directories, returning the found libraries."""
152+
static_libraries: dict[str, None] = {}
153+
dynamic_libraries: dict[str, None] = {}
154+
interface_libraries: dict[str, None] = {}
169155
for root_dir in search_directories:
170-
for libname in search_libnames:
156+
for libname in libnames:
171157
# Check whether the library exists.
172158
composed_path = os.path.join(root_dir, libname)
173159
if os.path.exists(composed_path) or os.path.isdir(composed_path):
@@ -178,9 +164,9 @@ def _get_python_library_info(base_executable):
178164

179165
interface_path = None
180166
if libname.endswith(".dll"):
181-
# On windows a .lib file may be an "import library" or a static library.
182-
# The file could be inspected to determine which it is; typically python
183-
# is used as a shared library.
167+
# On windows a .lib file may be an "import library" or a static
168+
# library. The file could be inspected to determine which it is;
169+
# typically python is used as a shared library.
184170
#
185171
# On Windows, extensions should link with the pythonXY.lib interface
186172
# libraries.
@@ -195,32 +181,88 @@ def _get_python_library_info(base_executable):
195181
# Check whether an interface library exists.
196182
if interface_path and os.path.exists(interface_path):
197183
interface_libraries[interface_path] = None
184+
return static_libraries, dynamic_libraries, interface_libraries
198185

199-
if hasattr(sys, "abiflags"):
200-
abiflags = sys.abiflags
201-
else:
202-
abiflags = ""
186+
187+
def _get_python_library_info(base_executable) -> dict[str, Any]:
188+
"""Returns a dictionary with the static and dynamic python libraries."""
189+
config_vars = sysconfig.get_config_vars()
190+
191+
# VERSION is X.Y in Linux/macOS and XY in Windows. This is used to
192+
# construct library paths such as python3.12, so ensure it exists.
193+
version = config_vars.get("VERSION")
194+
if not version:
195+
if _IS_WINDOWS:
196+
version = f"{sys.version_info.major}{sys.version_info.minor}"
197+
else:
198+
version = f"{sys.version_info.major}.{sys.version_info.minor}"
199+
200+
# sys.abiflags may not exist, but it still may be set in the config.
201+
abi_flags = _get_abi_flags(config_vars.get)
202+
shlib_suffix = _get_shlib_suffix(config_vars.get)
203+
search_directories = _search_directories(config_vars.get, base_executable)
204+
search_libnames = _search_library_names(
205+
config_vars.get, version, abi_flags, shlib_suffix
206+
)
207+
208+
# Search for the python libraries for the current python interpreter.
209+
static_libraries, dynamic_libraries, interface_libraries = _do_library_search(
210+
search_directories=search_directories,
211+
libnames=search_libnames,
212+
)
213+
214+
# Search for major version abi libraries which must be dynamic.
215+
abi_search_libnames = [
216+
s.replace(version, f"{sys.version_info.major}")
217+
for s in search_libnames
218+
if version in s
219+
]
220+
_, abi_dynamic_libraries, abi_interface_libraries = _do_library_search(
221+
search_directories=search_directories,
222+
libnames=abi_search_libnames,
223+
)
224+
225+
# Additional DLLs are needed on Windows to link properly.
226+
dlls = []
227+
if _IS_WINDOWS:
228+
dlls.extend(
229+
glob.glob(os.path.join(os.path.dirname(base_executable), "*.dll"))
230+
)
231+
dlls = [
232+
x
233+
for x in dlls
234+
if x not in dynamic_libraries and x not in abi_dynamic_libraries
235+
]
236+
237+
def _unique_basenames(inputs: dict[str, None]) -> list[str]:
238+
"""Returns a list of paths, keeping only the first path for each basename."""
239+
result = []
240+
seen = set()
241+
for k in inputs:
242+
b = os.path.basename(k)
243+
if b not in seen:
244+
seen.add(b)
245+
result.append(k)
246+
return result
203247

204248
# When no libraries are found it's likely that the python interpreter is not
205249
# configured to use shared or static libraries (minilinux). If this seems
206250
# suspicious try running `uv tool run find_libpython --list-all -v`
207251
return {
208-
"dynamic_libraries": list(dynamic_libraries.keys()),
209-
"static_libraries": list(static_libraries.keys()),
210-
"interface_libraries": list(interface_libraries.keys()),
211-
"shlib_suffix": "" if _IS_WINDOWS else shlib_suffix,
212-
"abi_flags": abiflags,
252+
"dynamic_libraries": _unique_basenames(dynamic_libraries),
253+
"static_libraries": _unique_basenames(static_libraries),
254+
"interface_libraries": _unique_basenames(interface_libraries),
255+
"abi_dynamic_libraries": _unique_basenames(abi_dynamic_libraries),
256+
"abi_interface_libraries": _unique_basenames(abi_interface_libraries),
257+
"abi_flags": abi_flags,
258+
"shlib_suffix": shlib_suffix if _IS_DARWIN else "",
259+
"additional_dlls": dlls,
213260
}
214261

215262

216-
def _get_base_executable():
263+
def _get_base_executable() -> str:
217264
"""Returns the base executable path."""
218-
try:
219-
if sys._base_executable: # pylint: disable=protected-access
220-
return sys._base_executable # pylint: disable=protected-access
221-
except AttributeError:
222-
pass
223-
return sys.executable
265+
return getattr(sys, "_base_executable", None) or sys.executable
224266

225267

226268
data = {

0 commit comments

Comments
 (0)