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