Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,31 @@ A brief description of the categories of changes:
<!--
BEGIN_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
* 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

Expand All @@ -46,16 +71,14 @@ BEGIN_UNRELEASED_TEMPLATE
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

Expand Down
87 changes: 53 additions & 34 deletions python/private/get_local_runtime_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,7 +37,9 @@ def _search_directories(get_config, base_executable):
# 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`
Expand All @@ -55,8 +56,8 @@ def _search_directories(get_config, base_executable):

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
Expand All @@ -67,16 +68,28 @@ def _search_directories(get_config, base_executable):
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.
lib_dirs = [v for v in lib_dirs if v]
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
Expand All @@ -90,8 +103,7 @@ def _search_library_names(get_config):
#
# 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",
Expand All @@ -104,26 +116,24 @@ def _search_library_names(get_config):
# 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]
Expand All @@ -138,30 +148,31 @@ def _get_python_library_info(base_executable):
# 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
Expand All @@ -172,14 +183,20 @@ def _add_if_exists(target, path):
#
# 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
Expand All @@ -188,6 +205,8 @@ def _add_if_exists(target, path):
"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,
}


Expand Down
23 changes: 13 additions & 10 deletions python/private/local_runtime_repo.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ define_local_runtime_toolchain_impl(
libraries = {libraries},
implementation_name = "{implementation_name}",
os = "{os}",
abi_flags = "{abi_flags}",
)
"""

Expand All @@ -49,33 +50,33 @@ def _norm_path(path):
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)
Expand Down Expand Up @@ -152,9 +153,9 @@ def _local_runtime_repo_impl(rctx):
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:
Expand All @@ -173,6 +174,7 @@ def _local_runtime_repo_impl(rctx):
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))

Expand Down Expand Up @@ -269,6 +271,7 @@ def _expand_incompatible_template():
minor = "0",
micro = "0",
os = "@platforms//:incompatible",
abi_flags = "",
)

def _find_python_exe_from_target(rctx):
Expand Down
5 changes: 4 additions & 1 deletion python/private/local_runtime_repo_setup.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ def define_local_runtime_toolchain_impl(
interface_library,
libraries,
implementation_name,
os):
os,
abi_flags):
"""Defines a toolchain implementation for a local Python runtime.

Generates public targets:
Expand All @@ -59,6 +60,7 @@ def define_local_runtime_toolchain_impl(
`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)
Expand Down Expand Up @@ -113,6 +115,7 @@ def define_local_runtime_toolchain_impl(
"minor": minor,
},
implementation_name = implementation_name,
abi_flags = abi_flags,
)

py_runtime_pair(
Expand Down