Skip to content

Commit c037d83

Browse files
laramielrickeylev
andauthored
fix(local): Fix local_runtime use with free-threaded python (#3399)
* Return abi_flags from get_local_runtime_info and pass it into the py3_runtime * Rework how shared-libraries are links are constructed to better meet @rules_cc cc_library.srcs requirements This improves runtime detection for macos when using a python3.14t framework runtime. --------- Co-authored-by: Richard Levasseur <[email protected]> Co-authored-by: Richard Levasseur <[email protected]>
1 parent bc196f5 commit c037d83

File tree

4 files changed

+97
-49
lines changed

4 files changed

+97
-49
lines changed

CHANGELOG.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,31 @@ A brief description of the categories of changes:
2323
<!--
2424
BEGIN_UNRELEASED_TEMPLATE
2525
26+
{#v0-0-0}
27+
## Unreleased
28+
29+
[0.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.0.0
30+
31+
{#v0-0-0-removed}
32+
### Removed
33+
* Nothing removed.
34+
35+
{#v0-0-0-changed}
36+
### Changed
37+
* Nothing changed.
38+
39+
{#v0-0-0-fixed}
40+
### Fixed
41+
* Nothing fixed.
42+
43+
{#v0-0-0-added}
44+
### Added
45+
* Nothing added.
46+
47+
END_UNRELEASED_TEMPLATE
48+
-->
49+
50+
2651
{#v0-0-0}
2752
## Unreleased
2853

@@ -46,16 +71,14 @@ BEGIN_UNRELEASED_TEMPLATE
4671
implementation assumes that it is always four levels below the runfiles
4772
directory, leading to incorrect path checks
4873
([#3085](https://github.com/bazel-contrib/rules_python/issues/3085)).
74+
* (toolchains) local toolchains now tell the `sys.abiflags` value of the
75+
underlying runtime.
4976

5077
{#v0-0-0-added}
5178
### Added
5279
* (toolchains) `3.9.25` Python toolchain from [20251031] release.
5380

5481
[20251031]: https://github.com/astral-sh/python-build-standalone/releases/tag/20251031
55-
56-
END_UNRELEASED_TEMPLATE
57-
-->
58-
5982
{#v1-7-0}
6083
## [1.7.0] - 2025-10-11
6184

python/private/get_local_runtime_info.py

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
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

1716
import json
@@ -38,7 +37,9 @@ def _search_directories(get_config, base_executable):
3837
# On MacOS, the LDLIBRARY may be a relative path under /Library/Frameworks,
3938
# such as "Python.framework/Versions/3.12/Python", not a file under the
4039
# LIBDIR/LIBPL directory, so include PYTHONFRAMEWORKPREFIX.
41-
lib_dirs = [get_config(x) for x in ("PYTHONFRAMEWORKPREFIX", "LIBPL", "LIBDIR")]
40+
lib_dirs = [
41+
get_config(x) for x in ("PYTHONFRAMEWORKPREFIX", "LIBPL", "LIBDIR")
42+
]
4243

4344
# On Debian, with multiarch enabled, prior to Python 3.10, `LIBDIR` didn't
4445
# tell the location of the libs, just the base directory. The `MULTIARCH`
@@ -55,8 +56,8 @@ def _search_directories(get_config, base_executable):
5556

5657
if not _IS_DARWIN:
5758
for exec_dir in (
58-
os.path.dirname(base_executable) if base_executable else None,
59-
get_config("BINDIR"),
59+
os.path.dirname(base_executable) if base_executable else None,
60+
get_config("BINDIR"),
6061
):
6162
if not exec_dir:
6263
continue
@@ -67,16 +68,28 @@ def _search_directories(get_config, base_executable):
6768
lib_dirs.append(os.path.join(exec_dir, "lib"))
6869
lib_dirs.append(os.path.join(exec_dir, "libs"))
6970
else:
70-
# On most systems the executable is in a bin/ directory and the libraries
71-
# are in a sibling lib/ directory.
71+
# On most non-windows systems the executable is in a bin/ directory and
72+
# the libraries are in a sibling lib/ directory.
7273
lib_dirs.append(os.path.join(os.path.dirname(exec_dir), "lib"))
7374

7475
# Dedup and remove empty values, keeping the order.
7576
lib_dirs = [v for v in lib_dirs if v]
7677
return {k: None for k in lib_dirs}.keys()
7778

7879

79-
def _search_library_names(get_config):
80+
def _get_shlib_suffix(get_config) -> str:
81+
"""Returns the suffix for shared libraries."""
82+
if _IS_DARWIN:
83+
return ".dylib"
84+
if _IS_WINDOWS:
85+
return ".dll"
86+
suffix = get_config("SHLIB_SUFFIX")
87+
if not suffix:
88+
suffix = ".so"
89+
return suffix
90+
91+
92+
def _search_library_names(get_config, shlib_suffix):
8093
"""Returns a list of library files to search for shared libraries."""
8194
# Quoting configure.ac in the cpython code base:
8295
# "INSTSONAME is the name of the shared library that will be use to install
@@ -90,8 +103,7 @@ def _search_library_names(get_config):
90103
#
91104
# A typical LIBRARY is 'libpythonX.Y.a' on Linux.
92105
lib_names = [
93-
get_config(x)
94-
for x in (
106+
get_config(x) for x in (
95107
"LDLIBRARY",
96108
"INSTSONAME",
97109
"PY3LIBRARY",
@@ -104,26 +116,24 @@ def _search_library_names(get_config):
104116
# The suffix and version are set here to the default values for the OS,
105117
# since they are used below to construct "default" library names.
106118
if _IS_DARWIN:
107-
suffix = ".dylib"
108119
prefix = "lib"
109120
elif _IS_WINDOWS:
110-
suffix = ".dll"
111121
prefix = ""
112122
else:
113-
suffix = get_config("SHLIB_SUFFIX")
114123
prefix = "lib"
115-
if not suffix:
116-
suffix = ".so"
117124

118125
version = get_config("VERSION")
119126

120127
# Ensure that the pythonXY.dll files are included in the search.
121-
lib_names.append(f"{prefix}python{version}{suffix}")
128+
lib_names.append(f"{prefix}python{version}{shlib_suffix}")
122129

123130
# If there are ABIFLAGS, also add them to the python version lib search.
124131
abiflags = get_config("ABIFLAGS") or get_config("abiflags") or ""
125132
if abiflags:
126-
lib_names.append(f"{prefix}python{version}{abiflags}{suffix}")
133+
lib_names.append(f"{prefix}python{version}{abiflags}{shlib_suffix}")
134+
135+
# Add the abi-version includes to the search list.
136+
lib_names.append(f"{prefix}python{sys.version_info.major}{shlib_suffix}")
127137

128138
# Dedup and remove empty values, keeping the order.
129139
lib_names = [v for v in lib_names if v]
@@ -138,30 +148,31 @@ def _get_python_library_info(base_executable):
138148
# construct library paths such as python3.12, so ensure it exists.
139149
if not config_vars.get("VERSION"):
140150
if sys.platform == "win32":
141-
config_vars["VERSION"] = f"{sys.version_info.major}{sys.version_info.minor}"
151+
config_vars["VERSION"] = (
152+
f"{sys.version_info.major}{sys.version_info.minor}")
142153
else:
143154
config_vars["VERSION"] = (
144-
f"{sys.version_info.major}.{sys.version_info.minor}"
145-
)
155+
f"{sys.version_info.major}.{sys.version_info.minor}")
146156

157+
shlib_suffix = _get_shlib_suffix(config_vars.get)
147158
search_directories = _search_directories(config_vars.get, base_executable)
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
159+
search_libnames = _search_library_names(config_vars.get, shlib_suffix)
153160

154161
interface_libraries = {}
155162
dynamic_libraries = {}
156163
static_libraries = {}
164+
157165
for root_dir in search_directories:
158166
for libname in search_libnames:
167+
# Check whether the library exists.
159168
composed_path = os.path.join(root_dir, libname)
160-
if libname.endswith(".a"):
161-
_add_if_exists(static_libraries, composed_path)
162-
continue
169+
if os.path.exists(composed_path) or os.path.isdir(composed_path):
170+
if libname.endswith(".a"):
171+
static_libraries[composed_path] = None
172+
else:
173+
dynamic_libraries[composed_path] = None
163174

164-
_add_if_exists(dynamic_libraries, composed_path)
175+
interface_path = None
165176
if libname.endswith(".dll"):
166177
# On windows a .lib file may be an "import library" or a static library.
167178
# The file could be inspected to determine which it is; typically python
@@ -172,14 +183,20 @@ def _add_if_exists(target, path):
172183
#
173184
# See: https://docs.python.org/3/extending/windows.html
174185
# 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-
)
186+
interface_path = os.path.join(root_dir, libname[:-3] + "lib")
178187
elif libname.endswith(".so"):
179188
# 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-
)
189+
interface_path = os.path.join(root_dir, libname[:-2] + "ifso")
190+
191+
# Check whether an interface library exists.
192+
if interface_path and os.path.exists(interface_path):
193+
interface_libraries[interface_path] = None
194+
195+
# Non-windows typically has abiflags.
196+
if hasattr(sys, "abiflags"):
197+
abiflags = sys.abiflags
198+
else:
199+
abiflags = ""
183200

184201
# When no libraries are found it's likely that the python interpreter is not
185202
# configured to use shared or static libraries (minilinux). If this seems
@@ -188,6 +205,8 @@ def _add_if_exists(target, path):
188205
"dynamic_libraries": list(dynamic_libraries.keys()),
189206
"static_libraries": list(static_libraries.keys()),
190207
"interface_libraries": list(interface_libraries.keys()),
208+
"shlib_suffix": "" if _IS_WINDOWS else shlib_suffix,
209+
"abi_flags": abiflags,
191210
}
192211

193212

python/private/local_runtime_repo.bzl

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ define_local_runtime_toolchain_impl(
3939
libraries = {libraries},
4040
implementation_name = "{implementation_name}",
4141
os = "{os}",
42+
abi_flags = "{abi_flags}",
4243
)
4344
"""
4445

@@ -49,33 +50,33 @@ def _norm_path(path):
4950
path = path[:-1]
5051
return path
5152

52-
def _symlink_first_library(rctx, logger, libraries):
53+
def _symlink_first_library(rctx, logger, libraries, shlib_suffix):
5354
"""Symlinks the shared libraries into the lib/ directory.
5455
5556
Args:
5657
rctx: A repository_ctx object
5758
logger: A repo_utils.logger object
5859
libraries: A list of static library paths to potentially symlink.
60+
shlib_suffix: A suffix only provided for shared libraries to ensure
61+
that the srcs restriction of cc_library targets are met.
5962
Returns:
6063
A single library path linked by the action.
6164
"""
62-
linked = None
6365
for target in libraries:
6466
origin = rctx.path(target)
6567
if not origin.exists:
6668
# The reported names don't always exist; it depends on the particulars
6769
# of the runtime installation.
6870
continue
69-
if target.endswith("/Python"):
70-
linked = "lib/{}.dylib".format(origin.basename)
71+
if shlib_suffix and not target.endswith(shlib_suffix):
72+
linked = "lib/{}{}".format(origin.basename, shlib_suffix)
7173
else:
7274
linked = "lib/{}".format(origin.basename)
7375
logger.debug("Symlinking {} to {}".format(origin, linked))
7476
rctx.watch(origin)
7577
rctx.symlink(origin, linked)
76-
break
77-
78-
return linked
78+
return linked
79+
return None
7980

8081
def _local_runtime_repo_impl(rctx):
8182
logger = repo_utils.logger(rctx)
@@ -152,9 +153,9 @@ def _local_runtime_repo_impl(rctx):
152153
rctx.symlink(include_path, "include")
153154

154155
rctx.report_progress("Symlinking external Python shared libraries")
155-
interface_library = _symlink_first_library(rctx, logger, info["interface_libraries"])
156-
shared_library = _symlink_first_library(rctx, logger, info["dynamic_libraries"])
157-
static_library = _symlink_first_library(rctx, logger, info["static_libraries"])
156+
interface_library = _symlink_first_library(rctx, logger, info["interface_libraries"], None)
157+
shared_library = _symlink_first_library(rctx, logger, info["dynamic_libraries"], info["shlib_suffix"])
158+
static_library = _symlink_first_library(rctx, logger, info["static_libraries"], None)
158159

159160
libraries = []
160161
if shared_library:
@@ -173,6 +174,7 @@ def _local_runtime_repo_impl(rctx):
173174
libraries = repr(libraries),
174175
implementation_name = info["implementation_name"],
175176
os = "@platforms//os:{}".format(repo_utils.get_platforms_os_name(rctx)),
177+
abi_flags = info["abi_flags"],
176178
)
177179
logger.debug(lambda: "BUILD.bazel\n{}".format(build_bazel))
178180

@@ -269,6 +271,7 @@ def _expand_incompatible_template():
269271
minor = "0",
270272
micro = "0",
271273
os = "@platforms//:incompatible",
274+
abi_flags = "",
272275
)
273276

274277
def _find_python_exe_from_target(rctx):

python/private/local_runtime_repo_setup.bzl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ def define_local_runtime_toolchain_impl(
3333
interface_library,
3434
libraries,
3535
implementation_name,
36-
os):
36+
os,
37+
abi_flags):
3738
"""Defines a toolchain implementation for a local Python runtime.
3839
3940
Generates public targets:
@@ -59,6 +60,7 @@ def define_local_runtime_toolchain_impl(
5960
`sys.implementation.name`.
6061
os: `str` A label to the OS constraint (e.g. `@platforms//os:linux`) for
6162
this runtime.
63+
abi_flags: `str` Str. Flags provided by sys.abiflags for the runtime.
6264
"""
6365
major_minor = "{}.{}".format(major, minor)
6466
major_minor_micro = "{}.{}".format(major_minor, micro)
@@ -113,6 +115,7 @@ def define_local_runtime_toolchain_impl(
113115
"minor": minor,
114116
},
115117
implementation_name = implementation_name,
118+
abi_flags = abi_flags,
116119
)
117120

118121
py_runtime_pair(

0 commit comments

Comments
 (0)