Skip to content

Commit 3b39535

Browse files
committed
fix(local) 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. 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
1 parent 179e2cb commit 3b39535

File tree

3 files changed

+255
-153
lines changed

3 files changed

+255
-153
lines changed

python/private/get_local_runtime_info.py

Lines changed: 111 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,74 @@ 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+
return list(dict.fromkeys(k for k in lib_names if k))
141141

142142

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

147147
# VERSION is X.Y in Linux/macOS and XY in Windows. This is used to
148148
# 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}")
149+
version = config_vars.get("VERSION")
150+
if not version:
151+
if _IS_WINDOWS:
152+
version = f"{sys.version_info.major}{sys.version_info.minor}"
153153
else:
154-
config_vars["VERSION"] = (
155-
f"{sys.version_info.major}.{sys.version_info.minor}")
154+
version = f"{sys.version_info.major}.{sys.version_info.minor}"
155+
156+
defines = []
157+
if config_vars.get("Py_GIL_DISABLED", "0") == "1":
158+
defines.append("Py_GIL_DISABLED")
159+
160+
# Avoid automatically linking the libraries on windows via pydefine.h
161+
# pragma comment(lib ...)
162+
if _IS_WINDOWS:
163+
defines.append("Py_NO_LINK_LIB")
164+
165+
# sys.abiflags may not exist, but it still may be set in the config.
166+
abi_flags = _get_abi_flags(config_vars.get)
156167

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

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

165182
for root_dir in search_directories:
166183
for libname in search_libnames:
167-
# Check whether the library exists.
168184
composed_path = os.path.join(root_dir, libname)
185+
is_abi3_file = os.path.basename(composed_path) in abi3_libraries
186+
187+
# Check whether the library exists and add it to the appropriate list.
169188
if os.path.exists(composed_path) or os.path.isdir(composed_path):
170-
if libname.endswith(".a"):
189+
if is_abi3_file:
190+
if not libname.endswith(".a"):
191+
abi_dynamic_libraries[composed_path] = None
192+
elif libname.endswith(".a"):
171193
static_libraries[composed_path] = None
172194
else:
173195
dynamic_libraries[composed_path] = None
174196

175197
interface_path = None
176198
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.
199+
# On windows a .lib file may be an "import library" or a static
200+
# library. The file could be inspected to determine which it is;
201+
# typically python is used as a shared library.
180202
#
181203
# On Windows, extensions should link with the pythonXY.lib interface
182204
# libraries.
@@ -190,39 +212,51 @@ def _get_python_library_info(base_executable):
190212

191213
# Check whether an interface library exists.
192214
if interface_path and os.path.exists(interface_path):
193-
interface_libraries[interface_path] = None
215+
if is_abi3_file:
216+
abi_interface_libraries[interface_path] = None
217+
else:
218+
interface_libraries[interface_path] = None
194219

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

201241
# When no libraries are found it's likely that the python interpreter is not
202242
# configured to use shared or static libraries (minilinux). If this seems
203243
# suspicious try running `uv tool run find_libpython --list-all -v`
204244
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,
245+
"dynamic_libraries": _unique_basenames(dynamic_libraries),
246+
"static_libraries": _unique_basenames(static_libraries),
247+
"interface_libraries": _unique_basenames(interface_libraries),
248+
"abi_dynamic_libraries": _unique_basenames(abi_dynamic_libraries),
249+
"abi_interface_libraries": _unique_basenames(abi_interface_libraries),
250+
"abi_flags": abi_flags,
251+
"shlib_suffix": ".dylib" if _IS_DARWIN else "",
252+
"additional_dlls": dlls,
253+
"defines": defines,
210254
}
211255

212256

213-
def _get_base_executable():
257+
def _get_base_executable() -> str:
214258
"""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
259+
return getattr(sys, "_base_executable", None) or sys.executable
226260

227261

228262
data = {

0 commit comments

Comments
 (0)