Skip to content

Commit 7ea4706

Browse files
laramielrickeylev
andauthored
fix(local) Add api3 targets and additional defines. (#3408)
Propagate defines and additional dll requirements for local python installs. In get_local_runtime_info.py: * detect abi3 vs. full abi libraries. * Ensure that returned libraries are unique. * Add additional dlls required by pythonXY.dll / pythonX.dll on windows. * Add default defines for Py_GIL_DISABLED when the local python is a freethreaded install. * Add defines (windows) for Py_NO_LINK_LIB to avoid #pragma comment(lib ...) macros In local_runtime_repo_setup.bzl * More closely match hermetic_runtime_repo_setup * Add abi3 header targets. In local_runtime_repo.bzl * rework linking to local repository directories to handl abi3 and extra dlls. * Update parameters passed into local_runtime_repo_setup.bzl Before these changes, some bazel builds using local Python fail to link properly. This happens due to a mismatch in the interpreter and the python GIL DISABLED mode, or (on Windows), where both freethreaded and non-freethreaded libraries may attempt to be linked at the same time. --------- Co-authored-by: Richard Levasseur <[email protected]> Co-authored-by: Richard Levasseur <[email protected]>
1 parent 23e605c commit 7ea4706

File tree

4 files changed

+258
-152
lines changed

4 files changed

+258
-152
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ END_UNRELEASED_TEMPLATE
7878
([#3085](https://github.com/bazel-contrib/rules_python/issues/3085)).
7979
* (toolchains) local toolchains now tell the `sys.abiflags` value of the
8080
underlying runtime.
81+
* (toolchains) various local toolchain fixes: add abi3 header targets,
82+
fixes to linking, Windows DLL detection, and defines for free threaded
83+
runtimes.
8184
* (toolchains) The `python_headers` target is now compatible with
8285
layering_check.
8386
* (performance) 90% reduction in py_binary/py_test analysis phase cost.

python/private/get_local_runtime_info.py

Lines changed: 112 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,27 @@
1313
# limitations under the License.
1414
"""Returns information about the local Python runtime as JSON."""
1515

16+
import glob
1617
import json
1718
import os
1819
import sys
1920
import sysconfig
21+
from typing import Any
2022

2123
_IS_WINDOWS = sys.platform == "win32"
2224
_IS_DARWIN = sys.platform == "darwin"
2325

2426

25-
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 _search_directories(get_config, base_executable) -> list[str]:
2637
"""Returns a list of library directories to search for shared libraries."""
2738
# There's several types of libraries with different names and a plethora
2839
# of settings, and many different config variables to check:
@@ -73,23 +84,31 @@ def _search_directories(get_config, base_executable):
7384
lib_dirs.append(os.path.join(os.path.dirname(exec_dir), "lib"))
7485

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

7989

80-
def _get_shlib_suffix(get_config) -> str:
81-
"""Returns the suffix for shared libraries."""
82-
if _IS_DARWIN:
83-
return ".dylib"
90+
def _default_library_names(version, abi_flags) -> tuple[str, ...]:
91+
"""Returns a list of default library files to search for shared libraries."""
8492
if _IS_WINDOWS:
85-
return ".dll"
86-
suffix = get_config("SHLIB_SUFFIX")
87-
if not suffix:
88-
suffix = ".so"
89-
return suffix
93+
return (
94+
f"python{version}{abi_flags}.dll",
95+
f"python{version}.dll",
96+
)
97+
elif _IS_DARWIN:
98+
return (
99+
f"libpython{version}{abi_flags}.dylib",
100+
f"libpython{version}.dylib",
101+
)
102+
else:
103+
return (
104+
f"libpython{version}{abi_flags}.so",
105+
f"libpython{version}.so",
106+
f"libpython{version}{abi_flags}.so.1.0",
107+
f"libpython{version}.so.1.0",
108+
)
90109

91110

92-
def _search_library_names(get_config, shlib_suffix):
111+
def _search_library_names(get_config, version, abi_flags) -> list[str]:
93112
"""Returns a list of library files to search for shared libraries."""
94113
# Quoting configure.ac in the cpython code base:
95114
# "INSTSONAME is the name of the shared library that will be use to install
@@ -112,71 +131,75 @@ def _search_library_names(get_config, shlib_suffix):
112131
)
113132
]
114133

115-
# Set the prefix and suffix to construct the library name used for linking.
116-
# The suffix and version are set here to the default values for the OS,
117-
# since they are used below to construct "default" library names.
118-
if _IS_DARWIN:
119-
prefix = "lib"
120-
elif _IS_WINDOWS:
121-
prefix = ""
122-
else:
123-
prefix = "lib"
124-
125-
version = get_config("VERSION")
126-
127-
# Ensure that the pythonXY.dll files are included in the search.
128-
lib_names.append(f"{prefix}python{version}{shlib_suffix}")
134+
# Include the default libraries for the system.
135+
lib_names.extend(_default_library_names(version, abi_flags))
129136

130-
# If there are ABIFLAGS, also add them to the python version lib search.
131-
abiflags = get_config("ABIFLAGS") or get_config("abiflags") or ""
132-
if abiflags:
133-
lib_names.append(f"{prefix}python{version}{abiflags}{shlib_suffix}")
137+
# Also include the abi3 libraries for the system.
138+
lib_names.extend(_default_library_names(sys.version_info.major, abi_flags))
134139

135-
# Add the abi-version includes to the search list.
136-
lib_names.append(f"{prefix}python{sys.version_info.major}{shlib_suffix}")
137-
138-
# Dedup and remove empty values, keeping the order.
139-
lib_names = [v for v in lib_names if v]
140-
return {k: None for k in lib_names}.keys()
140+
# Uniqify, preserving order.
141+
return list(dict.fromkeys(k for k in lib_names if k))
141142

142143

143-
def _get_python_library_info(base_executable):
144+
def _get_python_library_info(base_executable) -> dict[str, Any]:
144145
"""Returns a dictionary with the static and dynamic python libraries."""
145146
config_vars = sysconfig.get_config_vars()
146147

147148
# VERSION is X.Y in Linux/macOS and XY in Windows. This is used to
148149
# construct library paths such as python3.12, so ensure it exists.
149-
if not config_vars.get("VERSION"):
150-
if sys.platform == "win32":
151-
config_vars["VERSION"] = (
152-
f"{sys.version_info.major}{sys.version_info.minor}")
150+
version = config_vars.get("VERSION")
151+
if not version:
152+
if _IS_WINDOWS:
153+
version = f"{sys.version_info.major}{sys.version_info.minor}"
153154
else:
154-
config_vars["VERSION"] = (
155-
f"{sys.version_info.major}.{sys.version_info.minor}")
155+
version = f"{sys.version_info.major}.{sys.version_info.minor}"
156+
157+
defines = []
158+
if config_vars.get("Py_GIL_DISABLED", "0") == "1":
159+
defines.append("Py_GIL_DISABLED")
160+
161+
# Avoid automatically linking the libraries on windows via pydefine.h
162+
# pragma comment(lib ...)
163+
if _IS_WINDOWS:
164+
defines.append("Py_NO_LINK_LIB")
165+
166+
# sys.abiflags may not exist, but it still may be set in the config.
167+
abi_flags = _get_abi_flags(config_vars.get)
156168

157-
shlib_suffix = _get_shlib_suffix(config_vars.get)
158169
search_directories = _search_directories(config_vars.get, base_executable)
159-
search_libnames = _search_library_names(config_vars.get, shlib_suffix)
170+
search_libnames = _search_library_names(config_vars.get, version,
171+
abi_flags)
172+
173+
# Used to test whether the library is an abi3 library or a full api library.
174+
abi3_libraries = _default_library_names(sys.version_info.major, abi_flags)
160175

161-
interface_libraries = {}
162-
dynamic_libraries = {}
163-
static_libraries = {}
176+
# Found libraries
177+
static_libraries: dict[str, None] = {}
178+
dynamic_libraries: dict[str, None] = {}
179+
interface_libraries: dict[str, None] = {}
180+
abi_dynamic_libraries: dict[str, None] = {}
181+
abi_interface_libraries: dict[str, None] = {}
164182

165183
for root_dir in search_directories:
166184
for libname in search_libnames:
167-
# Check whether the library exists.
168185
composed_path = os.path.join(root_dir, libname)
186+
is_abi3_file = os.path.basename(composed_path) in abi3_libraries
187+
188+
# Check whether the library exists and add it to the appropriate list.
169189
if os.path.exists(composed_path) or os.path.isdir(composed_path):
170-
if libname.endswith(".a"):
190+
if is_abi3_file:
191+
if not libname.endswith(".a"):
192+
abi_dynamic_libraries[composed_path] = None
193+
elif libname.endswith(".a"):
171194
static_libraries[composed_path] = None
172195
else:
173196
dynamic_libraries[composed_path] = None
174197

175198
interface_path = None
176199
if libname.endswith(".dll"):
177-
# On windows a .lib file may be an "import library" or a static library.
178-
# The file could be inspected to determine which it is; typically python
179-
# is used as a shared library.
200+
# On windows a .lib file may be an "import library" or a static
201+
# library. The file could be inspected to determine which it is;
202+
# typically python is used as a shared library.
180203
#
181204
# On Windows, extensions should link with the pythonXY.lib interface
182205
# libraries.
@@ -190,39 +213,51 @@ def _get_python_library_info(base_executable):
190213

191214
# Check whether an interface library exists.
192215
if interface_path and os.path.exists(interface_path):
193-
interface_libraries[interface_path] = None
216+
if is_abi3_file:
217+
abi_interface_libraries[interface_path] = None
218+
else:
219+
interface_libraries[interface_path] = None
194220

195-
# Non-windows typically has abiflags.
196-
if hasattr(sys, "abiflags"):
197-
abiflags = sys.abiflags
198-
else:
199-
abiflags = ""
221+
# Additional DLLs are needed on Windows to link properly.
222+
dlls = []
223+
if _IS_WINDOWS:
224+
dlls.extend(
225+
glob.glob(os.path.join(os.path.dirname(base_executable), "*.dll")))
226+
dlls = [
227+
x for x in dlls
228+
if x not in dynamic_libraries and x not in abi_dynamic_libraries
229+
]
230+
231+
def _unique_basenames(inputs: dict[str, None]) -> list[str]:
232+
"""Returns a list of paths, keeping only the first path for each basename."""
233+
result = []
234+
seen = set()
235+
for k in inputs:
236+
b = os.path.basename(k)
237+
if b not in seen:
238+
seen.add(b)
239+
result.append(k)
240+
return result
200241

201242
# When no libraries are found it's likely that the python interpreter is not
202243
# configured to use shared or static libraries (minilinux). If this seems
203244
# suspicious try running `uv tool run find_libpython --list-all -v`
204245
return {
205-
"dynamic_libraries": list(dynamic_libraries.keys()),
206-
"static_libraries": list(static_libraries.keys()),
207-
"interface_libraries": list(interface_libraries.keys()),
208-
"shlib_suffix": "" if _IS_WINDOWS else shlib_suffix,
209-
"abi_flags": abiflags,
246+
"dynamic_libraries": _unique_basenames(dynamic_libraries),
247+
"static_libraries": _unique_basenames(static_libraries),
248+
"interface_libraries": _unique_basenames(interface_libraries),
249+
"abi_dynamic_libraries": _unique_basenames(abi_dynamic_libraries),
250+
"abi_interface_libraries": _unique_basenames(abi_interface_libraries),
251+
"abi_flags": abi_flags,
252+
"shlib_suffix": ".dylib" if _IS_DARWIN else "",
253+
"additional_dlls": dlls,
254+
"defines": defines,
210255
}
211256

212257

213-
def _get_base_executable():
258+
def _get_base_executable() -> str:
214259
"""Returns the base executable path."""
215-
try:
216-
if sys._base_executable: # pylint: disable=protected-access
217-
return sys._base_executable # pylint: disable=protected-access
218-
except AttributeError:
219-
# Bug reports indicate sys._base_executable doesn't exist in some cases,
220-
# but it's not clear why.
221-
# See https://github.com/bazel-contrib/rules_python/issues/3172
222-
pass
223-
# The normal sys.executable is the next-best guess if sys._base_executable
224-
# is missing.
225-
return sys.executable
260+
return getattr(sys, "_base_executable", None) or sys.executable
226261

227262

228263
data = {

0 commit comments

Comments
 (0)