Skip to content

fix(local_runtime): Improve local_runtime usability in macos / windows #3148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
76926ac
fix(local_runtime): Improve local_runtime usability in macos / windows
laramiel Aug 6, 2025
b9cbc67
Correct _expand_incompatible_template
laramiel Aug 6, 2025
965a6a9
Update changelog
laramiel Aug 6, 2025
8b61915
Formatting + minor cleanups
laramiel Aug 6, 2025
6ac66c9
format WORKSPACE
laramiel Aug 6, 2025
f4c0fed
Move the complex logic to resolve the python shared libraries from bz…
laramiel Aug 6, 2025
9283a3f
Fix formatting and method docs
laramiel Aug 6, 2025
e8997c1
Better handle missing interface_library values. This can happen on m…
laramiel Aug 7, 2025
655ea0f
Format local_runtime_repo_setup.bzl
laramiel Aug 7, 2025
e4eea87
reformat get_local_runtime_info.py and main.py
laramiel Aug 7, 2025
87d5391
Update CHANGELOG and minor comments.
laramiel Aug 7, 2025
09bfee9
Minor tweaks to library name genration
laramiel Aug 7, 2025
dda37d6
expand changelog description
rickeylev Aug 8, 2025
d7b2888
Merge branch 'main' into main
rickeylev Aug 8, 2025
0df9bf1
name loop var, add non-uv libs finding comment
rickeylev Aug 8, 2025
5874d5c
Distinguish between interface library and dynamic library file paths.
laramiel Aug 10, 2025
7bafc72
Refine comments, a few other minor changes
laramiel Aug 10, 2025
7c3428e
Update comment.
laramiel Aug 10, 2025
454c923
Merge branch 'main' into main
rickeylev Aug 11, 2025
230f5cb
Merge branch 'main' into main
rickeylev Aug 11, 2025
499ac15
fix incorrect merge
rickeylev Aug 11, 2025
66a16f9
Add workspace to tests/integration/local_toolchains
rickeylev Aug 11, 2025
9454daa
Delete examples/local_python directory
rickeylev Aug 11, 2025
a9f1e07
Fallback to shared library if there is no static library
laramiel Aug 11, 2025
5757bcd
add workspace test for local_toolchains
rickeylev Aug 11, 2025
931a1b6
add mac, windows bazel-in-bazel jobs
rickeylev Aug 11, 2025
db3de32
buildifier WORKSPACE
laramiel Aug 11, 2025
f6f31ff
Add a test of building an extension for local_runtime
laramiel Aug 11, 2025
70509e1
Complete extension rename: echo -> echo_ext
laramiel Aug 12, 2025
59c5812
run pre-commit
laramiel Aug 12, 2025
379506f
Minor improvements to py_extension test; add comments, rename, etc.
laramiel Aug 12, 2025
d966723
have mac/win only run local toolchains bazel in bazel integration test
rickeylev Aug 12, 2025
15d8e0d
Add -fvisibility=hidden to py_extension
laramiel Aug 12, 2025
4dbf51d
Simplify py_extension slightly
laramiel Aug 12, 2025
530ec06
more minor updates to py_extension
laramiel Aug 12, 2025
f32a551
comment formatting
laramiel Aug 12, 2025
81f80e3
replace if with else
laramiel Aug 12, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ END_UNRELEASED_TEMPLATE
`# gazelle:python_resolve_sibling_imports true`
* (pypi) Show overridden index URL of packages when downloading metadata have failed.
([#2985](https://github.com/bazel-contrib/rules_python/issues/2985)).
* (toolchains) `local_runtime_repo` better handles variants in MacOS and Windows.

{#v0-0-0-added}
### Added
Expand Down
4 changes: 4 additions & 0 deletions examples/local_python/.bazelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The equivalent bzlmod behavior is covered by examples/bzlmod/py_proto_library
common --noenable_bzlmod
common --enable_workspace
common --incompatible_python_disallow_native_rules
4 changes: 4 additions & 0 deletions examples/local_python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# git ignore patterns

/bazel-*
user.bazelrc
6 changes: 6 additions & 0 deletions examples/local_python/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
load("@rules_python//python:py_binary.bzl", "py_binary")

py_binary(
name = "main",
srcs = ["main.py"],
)
31 changes: 31 additions & 0 deletions examples/local_python/WORKSPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
workspace(
name = "local_python_example",
)

local_repository(
name = "rules_python",
path = "../..",
)

load("@rules_python//python:repositories.bzl", "py_repositories")

py_repositories()

load("@rules_python//python/local_toolchains:repos.bzl", "local_runtime_repo", "local_runtime_toolchains_repo")

# Step 1: Define the python runtime.
local_runtime_repo(
name = "local_python3",
interpreter_path = "python3",
on_failure = "fail",
# or interpreter_path = "C:\\path\\to\\python.exe"
)

# Step 2: Create toolchains for the runtimes
local_runtime_toolchains_repo(
name = "local_toolchains",
runtimes = ["local_python3"],
)

# Step 3: Register the toolchains
register_toolchains("@local_toolchains//:all")
22 changes: 22 additions & 0 deletions examples/local_python/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2025 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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.



def main():
print(42)


if __name__ == "__main__":
main()
181 changes: 150 additions & 31 deletions python/private/get_local_runtime_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,158 @@
# limitations under the License.

import json
import os
import sys
import sysconfig

_IS_WINDOWS = sys.platform == "win32"


def _search_directories(get_config):
"""Returns a list of library directories to search for shared libraries."""
# There's several types of libraries with different names and a plethora
# of settings, and many different config variables to check:
#
# LIBPL is used in python-config when shared library is not enabled:
# https://github.com/python/cpython/blob/v3.12.0/Misc/python-config.in#L63
#
# LIBDIR may also be the python directory with library files.
# https://stackoverflow.com/questions/47423246/get-pythons-lib-path
# See also: MULTIARCH
#
# 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 ("LIBPL", "LIBDIR", "PYTHONFRAMEWORKPREFIX")
]

# 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`
# sysconfig variable tells the subdirectory within it with the libs.
# See:
# https://wiki.debian.org/Python/MultiArch
# https://git.launchpad.net/ubuntu/+source/python3.12/tree/debian/changelog#n842
multiarch = get_config("MULTIARCH")
if multiarch:
for x in ["LIBPL", "LIBDIR"]:
config_value = get_config(x)
if config_value and not config_value.endswith(multiarch):
lib_dirs.append(os.path.join(config_value, multiarch))

if _IS_WINDOWS:
# On Windows DLLs go in the same directory as the executable, while .lib
# files live in the lib/ or libs/ subdirectory.
lib_dirs.append(get_config("BINDIR"))
lib_dirs.append(os.path.join(os.path.dirname(sys.executable)))
lib_dirs.append(os.path.join(os.path.dirname(sys.executable), "lib"))
lib_dirs.append(os.path.join(os.path.dirname(sys.executable), "libs"))
else:
# On most 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(os.path.dirname(sys.executable)), "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):
"""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
# on the system - some systems like version suffix, others don't.""
#
# A typical INSTSONAME is 'libpython3.8.so.1.0' on Linux, or
# 'Python.framework/Versions/3.9/Python' on MacOS. Due to the possible
# version suffix we have to find the suffix within the filename.
#
# A typical LDLIBRARY is 'libpythonX.Y.so' on Linux, or 'pythonXY.dll' on
# Windows.
#
# A typical LIBRARY is 'libpythonX.Y.a' on Linux.
lib_names = [
get_config(x)
for x in (
"INSTSONAME",
"LDLIBRARY",
"PY3LIBRARY",
"LIBRARY",
"DLLLIBRARY",
)
]

if _IS_WINDOWS:
so_prefix = ""
else:
so_prefix = "lib"

# Get/override the SHLIB_SUFFIX, which is typically ".so" on Linux and
# ".dylib" on macOS.
so_suffix = get_config("SHLIB_SUFFIX")
if sys.platform == "darwin":
so_suffix = ".dylib"
elif _IS_WINDOWS:
so_suffix = ".lib"
elif not so_suffix:
so_suffix = ".so"

version = get_config("VERSION")
abiflags = get_config("ABIFLAGS") or get_config("abiflags") or ""

# On Windows, extensions should link with the pythonXY.lib files.
# See: https://docs.python.org/3/extending/windows.html
# So ensure that the pythonXY.lib files are included in the search.
if abiflags:
lib_names.append(f"{so_prefix}python{version}{abiflags}{so_suffix}")
lib_names.append(f"{so_prefix}python{version}{so_suffix}")

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


def _get_python_library_info():
"""Returns a dictionary with the static and dynamic python libraries."""
config_vars = sysconfig.get_config_vars()

# VERSION is X.Y in Linux/macOS and XY in Windows.
if not config_vars.get("VERSION"):
if sys.platform == "win32":
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}"
)

search_directories = _search_directories(config_vars.get)
search_libnames = _search_library_names(config_vars.get)

dynamic_libraries = {}
static_libraries = {}
for root_dir in search_directories:
for libname in search_libnames:
composed_path = os.path.join(root_dir, libname)
if not os.path.exists(composed_path) or os.path.isdir(composed_path):
continue
if composed_path.endswith(".lib") or composed_path.endswith(".a"):
static_libraries[composed_path] = None
else:
dynamic_libraries[composed_path] = None

# NOTE: It's possible that including the dynamic libraries currently loaded
# by the running python interpreter would be a useful addition.

return {
"dynamic_libraries": list(dynamic_libraries.keys()),
"static_libraries": list(static_libraries.keys()),
}


data = {
"major": sys.version_info.major,
"minor": sys.version_info.minor,
Expand All @@ -24,35 +173,5 @@
"implementation_name": sys.implementation.name,
"base_executable": sys._base_executable,
}

config_vars = [
# The libpythonX.Y.so file. Usually?
# It might be a static archive (.a) file instead.
"LDLIBRARY",
# The directory with library files. Supposedly.
# It's not entirely clear how to get the directory with libraries.
# There's several types of libraries with different names and a plethora
# of settings.
# https://stackoverflow.com/questions/47423246/get-pythons-lib-path
# For now, it seems LIBDIR has what is needed, so just use that.
# See also: MULTIARCH
"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`
# sysconfig variable tells the subdirectory within it with the libs.
# See:
# https://wiki.debian.org/Python/MultiArch
# https://git.launchpad.net/ubuntu/+source/python3.12/tree/debian/changelog#n842
"MULTIARCH",
# The versioned libpythonX.Y.so.N file. Usually?
# It might be a static archive (.a) file instead.
"INSTSONAME",
# The libpythonX.so file. Usually?
# It might be a static archive (a.) file instead.
"PY3LIBRARY",
# The platform-specific filename suffix for library files.
# Includes the dot, e.g. `.so`
"SHLIB_SUFFIX",
]
data.update(zip(config_vars, sysconfig.get_config_vars(*config_vars)))
data.update(_get_python_library_info())
print(json.dumps(data))
Loading