Skip to content

Commit 3ade15c

Browse files
laramielrickeylev
andauthored
fix(local_runtime): Improve local_runtime usability in macos / windows (#3148)
local_runtime fails to handle many variations of python install on Windows and MacOS, such as: * LDLIBRARY on MacOS may refer to a file under PYTHONFRAMEWORKPREFIX, not LIBDIR * LDLIBRARY on Windows refers to pythonXY.dll, not the linkable pythonXY.lib * LIBDIR may not be correctly set on Windows. * On windows, interpreter_path needs to be normalized. Other paths also require this. * SHLIB_SUFFIX does not indicate the correct suffix. For examples, see: https://docs.python.org/3/extending/windows.html In order to resolve this the shared library resolution has been moved into get_local_runtime_info.py, which now does the following: * Constructs a list of paths to search based on LIBDIR, LIBPL, PYTHONFRAMEWORKPREFIX, and the executable directory. * Constructs a list of libraries to search based on INSTSONAME, LDLIBRARY, pythonXY.lib, etc. * Checks to see which files exist, partitioning the result into a list of "dynamic_libraries" and "static_libraries" On Windows and macOS, since SHLIB_SUFFIX does not always indicate the filenames needed searching, this has been removed from local_runtime_repo_setup and replaced with an explicit file. On Windows the interpreter_path and other search paths are now normalized (`\` converted to `/`). Additional logging added to local_runtime_repo. Fixes #3055 Work towards #824 --------- Co-authored-by: Richard Levasseur <[email protected]>
1 parent 9843447 commit 3ade15c

File tree

14 files changed

+559
-100
lines changed

14 files changed

+559
-100
lines changed

.bazelci/presubmit.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ buildifier:
5757
- "--enable_workspace"
5858
- "--build_tag_filters=-integration-test"
5959
bazel: 7.x
60+
# NOTE: The Mac and Windows bazelinbazel jobs override parts of this config.
6061
.common_bazelinbazel_config: &common_bazelinbazel_config
6162
build_flags:
6263
- "--build_tag_filters=integration-test"
@@ -503,6 +504,24 @@ tasks:
503504
<<: *common_bazelinbazel_config
504505
name: "tests/integration bazel-in-bazel: Debian"
505506
platform: debian11
507+
# The bazelinbazel tests were disabled on Mac to save CI jobs slots, and
508+
# have bitrotted a bit. For now, just run a subset of what we're most
509+
# interested in.
510+
integration_test_bazelinbazel_macos:
511+
<<: *common_bazelinbazel_config
512+
name: "tests/integration bazel-in-bazel: macOS (subset)"
513+
platform: macos
514+
build_targets: ["//tests/integration:local_toolchains_test_bazel_self"]
515+
test_targets: ["//tests/integration:local_toolchains_test_bazel_self"]
516+
# The bazelinbazel tests were disabled on Windows to save CI jobs slots, and
517+
# have bitrotted a bit. For now, just run a subset of what we're most
518+
# interested in.
519+
integration_test_bazelinbazel_windows:
520+
<<: *common_bazelinbazel_config
521+
name: "tests/integration bazel-in-bazel: Windows (subset)"
522+
platform: windows
523+
build_targets: ["//tests/integration:local_toolchains_test_bazel_self"]
524+
test_targets: ["//tests/integration:local_toolchains_test_bazel_self"]
506525

507526
integration_test_compile_pip_requirements_ubuntu:
508527
<<: *reusable_build_test_all

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
/bazel-genfiles
3838
/bazel-out
3939
/bazel-testlogs
40+
**/bazel-*
41+
4042
user.bazelrc
4143

4244
# vim swap files

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ END_UNRELEASED_TEMPLATE
106106
* (toolchains) use "command -v" to find interpreter in `$PATH`
107107
([#3150](https://github.com/bazel-contrib/rules_python/pull/3150)).
108108
* (pypi) `bazel vendor` now works in `bzlmod` ({gh-issue}`3079`).
109+
* (toolchains) `local_runtime_repo` now works on Windows
110+
([#3055](https://github.com/bazel-contrib/rules_python/issues/3055)).
111+
* (toolchains) `local_runtime_repo` supports more types of Python
112+
installations (Mac frameworks, missing dynamic libraries, and other
113+
esoteric cases, see
114+
[#3148](https://github.com/bazel-contrib/rules_python/pull/3148) for details).
109115

110116
{#v0-0-0-added}
111117
### Added

python/private/get_local_runtime_info.py

Lines changed: 170 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,47 +12,188 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
"""Returns information about the local Python runtime as JSON."""
16+
1517
import json
18+
import os
1619
import sys
1720
import sysconfig
1821

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+
2725

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."""
3428
# 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.
3635
# https://stackoverflow.com/questions/47423246/get-pythons-lib-path
37-
# For now, it seems LIBDIR has what is needed, so just use that.
3836
# 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+
4043
# On Debian, with multiarch enabled, prior to Python 3.10, `LIBDIR` didn't
4144
# tell the location of the libs, just the base directory. The `MULTIARCH`
4245
# sysconfig variable tells the subdirectory within it with the libs.
4346
# See:
4447
# https://wiki.debian.org/Python/MultiArch
4548
# 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())
58199
print(json.dumps(data))

0 commit comments

Comments
 (0)