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 <rlevasseur@google.com>
Co-authored-by: Richard Levasseur <richardlev@gmail.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9c6a6b6..8c472c0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,31 @@
{#v0-0-0-removed}
### Removed
+* Nothing removed.
+
+{#v0-0-0-changed}
+### Changed
+* Nothing changed.
+
+{#v0-0-0-fixed}
+### Fixed
+* Nothing fixed.
+
+{#v0-0-0-added}
+### Added
+* Nothing added.
+
+END_UNRELEASED_TEMPLATE
+-->
+
+
+{#v0-0-0}
+## Unreleased
+
+[0.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.0.0
+
+{#v0-0-0-removed}
+### Removed
* (toolchain) Remove all of the python 3.9 toolchain versions except for the `3.9.25`.
This version has reached EOL and will no longer receive any security fixes, please update to
`3.10` or above.
@@ -46,16 +71,14 @@
implementation assumes that it is always four levels below the runfiles
directory, leading to incorrect path checks
([#3085](https://github.com/bazel-contrib/rules_python/issues/3085)).
+* (toolchains) local toolchains now tell the `sys.abiflags` value of the
+ underlying runtime.
{#v0-0-0-added}
### Added
* (toolchains) `3.9.25` Python toolchain from [20251031] release.
[20251031]: https://github.com/astral-sh/python-build-standalone/releases/tag/20251031
-
-END_UNRELEASED_TEMPLATE
--->
-
{#v1-7-0}
## [1.7.0] - 2025-10-11
diff --git a/python/private/get_local_runtime_info.py b/python/private/get_local_runtime_info.py
index d176b1a..b20c159 100644
--- a/python/private/get_local_runtime_info.py
+++ b/python/private/get_local_runtime_info.py
@@ -11,7 +11,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-
"""Returns information about the local Python runtime as JSON."""
import json
@@ -38,7 +37,9 @@
# On MacOS, the LDLIBRARY may be a relative path under /Library/Frameworks,
# such as "Python.framework/Versions/3.12/Python", not a file under the
# LIBDIR/LIBPL directory, so include PYTHONFRAMEWORKPREFIX.
- lib_dirs = [get_config(x) for x in ("PYTHONFRAMEWORKPREFIX", "LIBPL", "LIBDIR")]
+ lib_dirs = [
+ get_config(x) for x in ("PYTHONFRAMEWORKPREFIX", "LIBPL", "LIBDIR")
+ ]
# On Debian, with multiarch enabled, prior to Python 3.10, `LIBDIR` didn't
# tell the location of the libs, just the base directory. The `MULTIARCH`
@@ -55,8 +56,8 @@
if not _IS_DARWIN:
for exec_dir in (
- os.path.dirname(base_executable) if base_executable else None,
- get_config("BINDIR"),
+ os.path.dirname(base_executable) if base_executable else None,
+ get_config("BINDIR"),
):
if not exec_dir:
continue
@@ -67,8 +68,8 @@
lib_dirs.append(os.path.join(exec_dir, "lib"))
lib_dirs.append(os.path.join(exec_dir, "libs"))
else:
- # On most systems the executable is in a bin/ directory and the libraries
- # are in a sibling lib/ directory.
+ # On most non-windows systems the executable is in a bin/ directory and
+ # the libraries are in a sibling lib/ directory.
lib_dirs.append(os.path.join(os.path.dirname(exec_dir), "lib"))
# Dedup and remove empty values, keeping the order.
@@ -76,7 +77,19 @@
return {k: None for k in lib_dirs}.keys()
-def _search_library_names(get_config):
+def _get_shlib_suffix(get_config) -> str:
+ """Returns the suffix for shared libraries."""
+ if _IS_DARWIN:
+ return ".dylib"
+ if _IS_WINDOWS:
+ return ".dll"
+ suffix = get_config("SHLIB_SUFFIX")
+ if not suffix:
+ suffix = ".so"
+ return suffix
+
+
+def _search_library_names(get_config, shlib_suffix):
"""Returns a list of library files to search for shared libraries."""
# Quoting configure.ac in the cpython code base:
# "INSTSONAME is the name of the shared library that will be use to install
@@ -90,8 +103,7 @@
#
# A typical LIBRARY is 'libpythonX.Y.a' on Linux.
lib_names = [
- get_config(x)
- for x in (
+ get_config(x) for x in (
"LDLIBRARY",
"INSTSONAME",
"PY3LIBRARY",
@@ -104,26 +116,24 @@
# The suffix and version are set here to the default values for the OS,
# since they are used below to construct "default" library names.
if _IS_DARWIN:
- suffix = ".dylib"
prefix = "lib"
elif _IS_WINDOWS:
- suffix = ".dll"
prefix = ""
else:
- suffix = get_config("SHLIB_SUFFIX")
prefix = "lib"
- if not suffix:
- suffix = ".so"
version = get_config("VERSION")
# Ensure that the pythonXY.dll files are included in the search.
- lib_names.append(f"{prefix}python{version}{suffix}")
+ lib_names.append(f"{prefix}python{version}{shlib_suffix}")
# If there are ABIFLAGS, also add them to the python version lib search.
abiflags = get_config("ABIFLAGS") or get_config("abiflags") or ""
if abiflags:
- lib_names.append(f"{prefix}python{version}{abiflags}{suffix}")
+ lib_names.append(f"{prefix}python{version}{abiflags}{shlib_suffix}")
+
+ # Add the abi-version includes to the search list.
+ lib_names.append(f"{prefix}python{sys.version_info.major}{shlib_suffix}")
# Dedup and remove empty values, keeping the order.
lib_names = [v for v in lib_names if v]
@@ -138,30 +148,31 @@
# construct library paths such as python3.12, so ensure it exists.
if not config_vars.get("VERSION"):
if sys.platform == "win32":
- config_vars["VERSION"] = f"{sys.version_info.major}{sys.version_info.minor}"
+ config_vars["VERSION"] = (
+ f"{sys.version_info.major}{sys.version_info.minor}")
else:
config_vars["VERSION"] = (
- f"{sys.version_info.major}.{sys.version_info.minor}"
- )
+ f"{sys.version_info.major}.{sys.version_info.minor}")
+ shlib_suffix = _get_shlib_suffix(config_vars.get)
search_directories = _search_directories(config_vars.get, base_executable)
- search_libnames = _search_library_names(config_vars.get)
-
- def _add_if_exists(target, path):
- if os.path.exists(path) or os.path.isdir(path):
- target[path] = None
+ search_libnames = _search_library_names(config_vars.get, shlib_suffix)
interface_libraries = {}
dynamic_libraries = {}
static_libraries = {}
+
for root_dir in search_directories:
for libname in search_libnames:
+ # Check whether the library exists.
composed_path = os.path.join(root_dir, libname)
- if libname.endswith(".a"):
- _add_if_exists(static_libraries, composed_path)
- continue
+ if os.path.exists(composed_path) or os.path.isdir(composed_path):
+ if libname.endswith(".a"):
+ static_libraries[composed_path] = None
+ else:
+ dynamic_libraries[composed_path] = None
- _add_if_exists(dynamic_libraries, composed_path)
+ interface_path = None
if libname.endswith(".dll"):
# On windows a .lib file may be an "import library" or a static library.
# The file could be inspected to determine which it is; typically python
@@ -172,14 +183,20 @@
#
# See: https://docs.python.org/3/extending/windows.html
# https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-creation
- _add_if_exists(
- interface_libraries, os.path.join(root_dir, libname[:-3] + "lib")
- )
+ interface_path = os.path.join(root_dir, libname[:-3] + "lib")
elif libname.endswith(".so"):
# It's possible, though unlikely, that interface stubs (.ifso) exist.
- _add_if_exists(
- interface_libraries, os.path.join(root_dir, libname[:-2] + "ifso")
- )
+ interface_path = os.path.join(root_dir, libname[:-2] + "ifso")
+
+ # Check whether an interface library exists.
+ if interface_path and os.path.exists(interface_path):
+ interface_libraries[interface_path] = None
+
+ # Non-windows typically has abiflags.
+ if hasattr(sys, "abiflags"):
+ abiflags = sys.abiflags
+ else:
+ abiflags = ""
# When no libraries are found it's likely that the python interpreter is not
# configured to use shared or static libraries (minilinux). If this seems
@@ -188,6 +205,8 @@
"dynamic_libraries": list(dynamic_libraries.keys()),
"static_libraries": list(static_libraries.keys()),
"interface_libraries": list(interface_libraries.keys()),
+ "shlib_suffix": "" if _IS_WINDOWS else shlib_suffix,
+ "abi_flags": abiflags,
}
diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl
index 583926b..024f7c5 100644
--- a/python/private/local_runtime_repo.bzl
+++ b/python/private/local_runtime_repo.bzl
@@ -39,6 +39,7 @@
libraries = {libraries},
implementation_name = "{implementation_name}",
os = "{os}",
+ abi_flags = "{abi_flags}",
)
"""
@@ -49,33 +50,33 @@
path = path[:-1]
return path
-def _symlink_first_library(rctx, logger, libraries):
+def _symlink_first_library(rctx, logger, libraries, shlib_suffix):
"""Symlinks the shared libraries into the lib/ directory.
Args:
rctx: A repository_ctx object
logger: A repo_utils.logger object
libraries: A list of static library paths to potentially symlink.
+ shlib_suffix: A suffix only provided for shared libraries to ensure
+ that the srcs restriction of cc_library targets are met.
Returns:
A single library path linked by the action.
"""
- linked = None
for target in libraries:
origin = rctx.path(target)
if not origin.exists:
# The reported names don't always exist; it depends on the particulars
# of the runtime installation.
continue
- if target.endswith("/Python"):
- linked = "lib/{}.dylib".format(origin.basename)
+ if shlib_suffix and not target.endswith(shlib_suffix):
+ linked = "lib/{}{}".format(origin.basename, shlib_suffix)
else:
linked = "lib/{}".format(origin.basename)
logger.debug("Symlinking {} to {}".format(origin, linked))
rctx.watch(origin)
rctx.symlink(origin, linked)
- break
-
- return linked
+ return linked
+ return None
def _local_runtime_repo_impl(rctx):
logger = repo_utils.logger(rctx)
@@ -152,9 +153,9 @@
rctx.symlink(include_path, "include")
rctx.report_progress("Symlinking external Python shared libraries")
- interface_library = _symlink_first_library(rctx, logger, info["interface_libraries"])
- shared_library = _symlink_first_library(rctx, logger, info["dynamic_libraries"])
- static_library = _symlink_first_library(rctx, logger, info["static_libraries"])
+ interface_library = _symlink_first_library(rctx, logger, info["interface_libraries"], None)
+ shared_library = _symlink_first_library(rctx, logger, info["dynamic_libraries"], info["shlib_suffix"])
+ static_library = _symlink_first_library(rctx, logger, info["static_libraries"], None)
libraries = []
if shared_library:
@@ -173,6 +174,7 @@
libraries = repr(libraries),
implementation_name = info["implementation_name"],
os = "@platforms//os:{}".format(repo_utils.get_platforms_os_name(rctx)),
+ abi_flags = info["abi_flags"],
)
logger.debug(lambda: "BUILD.bazel\n{}".format(build_bazel))
@@ -269,6 +271,7 @@
minor = "0",
micro = "0",
os = "@platforms//:incompatible",
+ abi_flags = "",
)
def _find_python_exe_from_target(rctx):
diff --git a/python/private/local_runtime_repo_setup.bzl b/python/private/local_runtime_repo_setup.bzl
index 6cff1ae..0ce1d4d 100644
--- a/python/private/local_runtime_repo_setup.bzl
+++ b/python/private/local_runtime_repo_setup.bzl
@@ -33,7 +33,8 @@
interface_library,
libraries,
implementation_name,
- os):
+ os,
+ abi_flags):
"""Defines a toolchain implementation for a local Python runtime.
Generates public targets:
@@ -59,6 +60,7 @@
`sys.implementation.name`.
os: `str` A label to the OS constraint (e.g. `@platforms//os:linux`) for
this runtime.
+ abi_flags: `str` Str. Flags provided by sys.abiflags for the runtime.
"""
major_minor = "{}.{}".format(major, minor)
major_minor_micro = "{}.{}".format(major_minor, micro)
@@ -113,6 +115,7 @@
"minor": minor,
},
implementation_name = implementation_name,
+ abi_flags = abi_flags,
)
py_runtime_pair(