|
12 | 12 | # See the License for the specific language governing permissions and
|
13 | 13 | # limitations under the License.
|
14 | 14 |
|
| 15 | +"""Returns information about the local Python runtime as JSON.""" |
| 16 | + |
15 | 17 | import json
|
| 18 | +import os |
16 | 19 | import sys
|
17 | 20 | import sysconfig
|
18 | 21 |
|
19 |
| -data = { |
20 |
| - "major": sys.version_info.major, |
21 |
| - "minor": sys.version_info.minor, |
22 |
| - "micro": sys.version_info.micro, |
23 |
| - "include": sysconfig.get_path("include"), |
24 |
| - "implementation_name": sys.implementation.name, |
25 |
| - "base_executable": sys._base_executable, |
26 |
| -} |
| 22 | +_IS_WINDOWS = sys.platform == "win32" |
| 23 | +_IS_DARWIN = sys.platform == "darwin" |
| 24 | + |
27 | 25 |
|
28 |
| -config_vars = [ |
29 |
| - # The libpythonX.Y.so file. Usually? |
30 |
| - # It might be a static archive (.a) file instead. |
31 |
| - "LDLIBRARY", |
32 |
| - # The directory with library files. Supposedly. |
33 |
| - # It's not entirely clear how to get the directory with libraries. |
| 26 | +def _search_directories(get_config): |
| 27 | + """Returns a list of library directories to search for shared libraries.""" |
34 | 28 | # There's several types of libraries with different names and a plethora
|
35 |
| - # of settings. |
| 29 | + # of settings, and many different config variables to check: |
| 30 | + # |
| 31 | + # LIBPL is used in python-config when shared library is not enabled: |
| 32 | + # https://github.com/python/cpython/blob/v3.12.0/Misc/python-config.in#L63 |
| 33 | + # |
| 34 | + # LIBDIR may also be the python directory with library files. |
36 | 35 | # https://stackoverflow.com/questions/47423246/get-pythons-lib-path
|
37 |
| - # For now, it seems LIBDIR has what is needed, so just use that. |
38 | 36 | # See also: MULTIARCH
|
39 |
| - "LIBDIR", |
| 37 | + # |
| 38 | + # On MacOS, the LDLIBRARY may be a relative path under /Library/Frameworks, |
| 39 | + # such as "Python.framework/Versions/3.12/Python", not a file under the |
| 40 | + # LIBDIR/LIBPL directory, so include PYTHONFRAMEWORKPREFIX. |
| 41 | + lib_dirs = [get_config(x) for x in ("PYTHONFRAMEWORKPREFIX", "LIBPL", "LIBDIR")] |
| 42 | + |
40 | 43 | # On Debian, with multiarch enabled, prior to Python 3.10, `LIBDIR` didn't
|
41 | 44 | # tell the location of the libs, just the base directory. The `MULTIARCH`
|
42 | 45 | # sysconfig variable tells the subdirectory within it with the libs.
|
43 | 46 | # See:
|
44 | 47 | # https://wiki.debian.org/Python/MultiArch
|
45 | 48 | # https://git.launchpad.net/ubuntu/+source/python3.12/tree/debian/changelog#n842
|
46 |
| - "MULTIARCH", |
47 |
| - # The versioned libpythonX.Y.so.N file. Usually? |
48 |
| - # It might be a static archive (.a) file instead. |
49 |
| - "INSTSONAME", |
50 |
| - # The libpythonX.so file. Usually? |
51 |
| - # It might be a static archive (a.) file instead. |
52 |
| - "PY3LIBRARY", |
53 |
| - # The platform-specific filename suffix for library files. |
54 |
| - # Includes the dot, e.g. `.so` |
55 |
| - "SHLIB_SUFFIX", |
56 |
| -] |
57 |
| -data.update(zip(config_vars, sysconfig.get_config_vars(*config_vars))) |
| 49 | + multiarch = get_config("MULTIARCH") |
| 50 | + if multiarch: |
| 51 | + for x in ("LIBPL", "LIBDIR"): |
| 52 | + config_value = get_config(x) |
| 53 | + if config_value and not config_value.endswith(multiarch): |
| 54 | + lib_dirs.append(os.path.join(config_value, multiarch)) |
| 55 | + |
| 56 | + if _IS_WINDOWS: |
| 57 | + # On Windows DLLs go in the same directory as the executable, while .lib |
| 58 | + # files live in the lib/ or libs/ subdirectory. |
| 59 | + lib_dirs.append(get_config("BINDIR")) |
| 60 | + lib_dirs.append(os.path.join(os.path.dirname(sys.executable))) |
| 61 | + lib_dirs.append(os.path.join(os.path.dirname(sys.executable), "lib")) |
| 62 | + lib_dirs.append(os.path.join(os.path.dirname(sys.executable), "libs")) |
| 63 | + elif not _IS_DARWIN: |
| 64 | + # On most systems the executable is in a bin/ directory and the libraries |
| 65 | + # are in a sibling lib/ directory. |
| 66 | + lib_dirs.append( |
| 67 | + os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "lib") |
| 68 | + ) |
| 69 | + |
| 70 | + # Dedup and remove empty values, keeping the order. |
| 71 | + lib_dirs = [v for v in lib_dirs if v] |
| 72 | + return {k: None for k in lib_dirs}.keys() |
| 73 | + |
| 74 | + |
| 75 | +def _search_library_names(get_config): |
| 76 | + """Returns a list of library files to search for shared libraries.""" |
| 77 | + # Quoting configure.ac in the cpython code base: |
| 78 | + # "INSTSONAME is the name of the shared library that will be use to install |
| 79 | + # on the system - some systems like version suffix, others don't."" |
| 80 | + # |
| 81 | + # A typical INSTSONAME is 'libpython3.8.so.1.0' on Linux, or |
| 82 | + # 'Python.framework/Versions/3.9/Python' on MacOS. |
| 83 | + # |
| 84 | + # A typical LDLIBRARY is 'libpythonX.Y.so' on Linux, or 'pythonXY.dll' on |
| 85 | + # Windows, or 'Python.framework/Versions/3.9/Python' on MacOS. |
| 86 | + # |
| 87 | + # A typical LIBRARY is 'libpythonX.Y.a' on Linux. |
| 88 | + lib_names = [ |
| 89 | + get_config(x) |
| 90 | + for x in ( |
| 91 | + "LDLIBRARY", |
| 92 | + "INSTSONAME", |
| 93 | + "PY3LIBRARY", |
| 94 | + "LIBRARY", |
| 95 | + "DLLLIBRARY", |
| 96 | + ) |
| 97 | + ] |
| 98 | + |
| 99 | + # Set the prefix and suffix to construct the library name used for linking. |
| 100 | + # The suffix and version are set here to the default values for the OS, |
| 101 | + # since they are used below to construct "default" library names. |
| 102 | + if _IS_DARWIN: |
| 103 | + suffix = ".dylib" |
| 104 | + prefix = "lib" |
| 105 | + elif _IS_WINDOWS: |
| 106 | + suffix = ".dll" |
| 107 | + prefix = "" |
| 108 | + else: |
| 109 | + suffix = get_config("SHLIB_SUFFIX") |
| 110 | + prefix = "lib" |
| 111 | + if not suffix: |
| 112 | + suffix = ".so" |
| 113 | + |
| 114 | + version = get_config("VERSION") |
| 115 | + |
| 116 | + # Ensure that the pythonXY.dll files are included in the search. |
| 117 | + lib_names.append(f"{prefix}python{version}{suffix}") |
| 118 | + |
| 119 | + # If there are ABIFLAGS, also add them to the python version lib search. |
| 120 | + abiflags = get_config("ABIFLAGS") or get_config("abiflags") or "" |
| 121 | + if abiflags: |
| 122 | + lib_names.append(f"{prefix}python{version}{abiflags}{suffix}") |
| 123 | + |
| 124 | + # Dedup and remove empty values, keeping the order. |
| 125 | + lib_names = [v for v in lib_names if v] |
| 126 | + return {k: None for k in lib_names}.keys() |
| 127 | + |
| 128 | + |
| 129 | +def _get_python_library_info(): |
| 130 | + """Returns a dictionary with the static and dynamic python libraries.""" |
| 131 | + config_vars = sysconfig.get_config_vars() |
| 132 | + |
| 133 | + # VERSION is X.Y in Linux/macOS and XY in Windows. This is used to |
| 134 | + # construct library paths such as python3.12, so ensure it exists. |
| 135 | + if not config_vars.get("VERSION"): |
| 136 | + if sys.platform == "win32": |
| 137 | + config_vars["VERSION"] = f"{sys.version_info.major}{sys.version_info.minor}" |
| 138 | + else: |
| 139 | + config_vars["VERSION"] = ( |
| 140 | + f"{sys.version_info.major}.{sys.version_info.minor}" |
| 141 | + ) |
| 142 | + |
| 143 | + search_directories = _search_directories(config_vars.get) |
| 144 | + search_libnames = _search_library_names(config_vars.get) |
| 145 | + |
| 146 | + def _add_if_exists(target, path): |
| 147 | + if os.path.exists(path) or os.path.isdir(path): |
| 148 | + target[path] = None |
| 149 | + |
| 150 | + interface_libraries = {} |
| 151 | + dynamic_libraries = {} |
| 152 | + static_libraries = {} |
| 153 | + for root_dir in search_directories: |
| 154 | + for libname in search_libnames: |
| 155 | + composed_path = os.path.join(root_dir, libname) |
| 156 | + if libname.endswith(".a"): |
| 157 | + _add_if_exists(static_libraries, composed_path) |
| 158 | + continue |
| 159 | + |
| 160 | + _add_if_exists(dynamic_libraries, composed_path) |
| 161 | + if libname.endswith(".dll"): |
| 162 | + # On windows a .lib file may be an "import library" or a static library. |
| 163 | + # The file could be inspected to determine which it is; typically python |
| 164 | + # is used as a shared library. |
| 165 | + # |
| 166 | + # On Windows, extensions should link with the pythonXY.lib interface |
| 167 | + # libraries. |
| 168 | + # |
| 169 | + # See: https://docs.python.org/3/extending/windows.html |
| 170 | + # https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-creation |
| 171 | + _add_if_exists( |
| 172 | + interface_libraries, os.path.join(root_dir, libname[:-3] + "lib") |
| 173 | + ) |
| 174 | + elif libname.endswith(".so"): |
| 175 | + # It's possible, though unlikely, that interface stubs (.ifso) exist. |
| 176 | + _add_if_exists( |
| 177 | + interface_libraries, os.path.join(root_dir, libname[:-2] + "ifso") |
| 178 | + ) |
| 179 | + |
| 180 | + # When no libraries are found it's likely that the python interpreter is not |
| 181 | + # configured to use shared or static libraries (minilinux). If this seems |
| 182 | + # suspicious try running `uv tool run find_libpython --list-all -v` |
| 183 | + return { |
| 184 | + "dynamic_libraries": list(dynamic_libraries.keys()), |
| 185 | + "static_libraries": list(static_libraries.keys()), |
| 186 | + "interface_libraries": list(interface_libraries.keys()), |
| 187 | + } |
| 188 | + |
| 189 | + |
| 190 | +data = { |
| 191 | + "major": sys.version_info.major, |
| 192 | + "minor": sys.version_info.minor, |
| 193 | + "micro": sys.version_info.micro, |
| 194 | + "include": sysconfig.get_path("include"), |
| 195 | + "implementation_name": sys.implementation.name, |
| 196 | + "base_executable": sys._base_executable, |
| 197 | +} |
| 198 | +data.update(_get_python_library_info()) |
58 | 199 | print(json.dumps(data))
|
0 commit comments