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
1717import json
1818import os
1919import sys
2020import 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
226268data = {
0 commit comments