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 3 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"],
)
35 changes: 35 additions & 0 deletions examples/local_python/WORKSPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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",
on_failure = "fail",
interpreter_path = "python3"
# 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()
14 changes: 11 additions & 3 deletions python/private/get_local_runtime_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,17 @@
# 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",
# On MacOS, the LDLIBRARY may be a relative path rooted under /Library/Frameworks,
# such as "Python.framework/Versions/3.12/Python", not a file under the LIBDIR directory.
"PYTHONFRAMEWORKPREFIX",
]
data.update(zip(config_vars, sysconfig.get_config_vars(*config_vars)))

# On windows, the SHLIB_SUFFIX is .lib, and the toolchain needs to link with pythonXY.lib.
# See: https://docs.python.org/3/extending/windows.html
if sys.platform == "win32":
data["LDLIBRARY"] = "python{}{}.lib".format(
sys.version_info.major, sys.version_info.minor
)

print(json.dumps(data))
117 changes: 81 additions & 36 deletions python/private/local_runtime_repo.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,76 @@ load("@rules_python//python/private:local_runtime_repo_setup.bzl", "define_local

define_local_runtime_toolchain_impl(
name = "local_runtime",
lib_ext = "{lib_ext}",
major = "{major}",
minor = "{minor}",
micro = "{micro}",
interpreter_path = "{interpreter_path}",
library_srcs = {library_srcs},
implementation_name = "{implementation_name}",
os = "{os}",
)
"""

def _norm_path(path):
"""Returns a path using '/' separators and no trailing slash."""
path = path.replace("\\", "/")
if path[-1] == "/":
path = path[:-1]
return path

def _symlink_libs(rctx, logger, shared_lib_dirs, shared_lib_names):
"""Symlinks the shared libraries into the lib/ directory.

Args:
rctx: A repository_ctx object
logger: A repo_utils.logger object
shared_lib_dirs: The search directories for shared libraries
shared_lib_names: The individual shared libraries to attempt to link in the directories.

Returns:
A list of src libraries linked by the action.

The specific files are symlinked instead of the whole directory because
shared_lib_dirs contains multiple search paths for the shared libraries,
and the python files may be missing from any of those directories, and
any of those directories may include non-python runtime libraries,
as would be the case if LIBDIR were, for example, /usr/lib.
"""
found = []
for shared_lib_dir in shared_lib_dirs:
for name in shared_lib_names:
origin = rctx.path("{}/{}".format(shared_lib_dir, name))
if not origin.exists:
# The reported names don't always exist; it depends on the particulars
# of the runtime installation.
continue

found.append("lib/{}".format(origin.basename))
logger.debug("Symlinking {} to lib/{}".format(origin, origin.basename))
repo_utils.watch(rctx, origin)
rctx.symlink(origin, "lib/{}".format(origin.basename))

# Libraries will only be linked from the same directory.
if found:
break

return found

def _local_runtime_repo_impl(rctx):
logger = repo_utils.logger(rctx)
on_failure = rctx.attr.on_failure

result = _resolve_interpreter_path(rctx)
if not result.resolved_path:
def _emit_log(msg):
if on_failure == "fail":
fail("interpreter not found: {}".format(result.describe_failure()))
logger.fail(msg)
elif on_failure == "warn":
logger.warn(msg)
else:
logger.debug(msg)

if on_failure == "warn":
logger.warn(lambda: "interpreter not found: {}".format(result.describe_failure()))
result = _resolve_interpreter_path(rctx)
if not result.resolved_path:
_emit_log(lambda: "interpreter not found: {}".format(result.describe_failure()))

# else, on_failure must be skip
rctx.file("BUILD.bazel", _expand_incompatible_template())
Expand All @@ -72,10 +121,7 @@ def _local_runtime_repo_impl(rctx):
logger = logger,
)
if exec_result.return_code != 0:
if on_failure == "fail":
fail("GetPythonInfo failed: {}".format(exec_result.describe_failure()))
if on_failure == "warn":
logger.warn(lambda: "GetPythonInfo failed: {}".format(exec_result.describe_failure()))
_emit_log(lambda: "GetPythonInfo failed: {}".format(exec_result.describe_failure()))

# else, on_failure must be skip
rctx.file("BUILD.bazel", _expand_incompatible_template())
Expand Down Expand Up @@ -120,45 +166,44 @@ def _local_runtime_repo_impl(rctx):
info["INSTSONAME"],
]

# In some cases, the value may be empty. Not clear why.
# Not all config fields exist; nor are they necessarily distinct.
# Dedup and remove empty values.
shared_lib_names = [v for v in shared_lib_names if v]

# In some cases, the same value is returned for multiple keys. Not clear why.
shared_lib_names = {v: None for v in shared_lib_names}.keys()
shared_lib_dir = info["LIBDIR"]
multiarch = info["MULTIARCH"]

shared_lib_dirs = []
if info["LIBDIR"]:
libdir = _norm_path(info["LIBDIR"])
shared_lib_dirs.append(libdir)
if info["MULTIARCH"]:
shared_lib_dirs.append("{}/{}".format(libdir, info["MULTIARCH"]))
if info["PYTHONFRAMEWORKPREFIX"]:
shared_lib_dirs.append(info["PYTHONFRAMEWORKPREFIX"])

# The specific files are symlinked instead of the whole directory
# because it can point to a directory that has more than just
# the Python runtime shared libraries, e.g. /usr/lib, or a Python
# specific directory with pip-installed shared libraries.
rctx.report_progress("Symlinking external Python shared libraries")
for name in shared_lib_names:
origin = rctx.path("{}/{}".format(shared_lib_dir, name))
library_srcs = _symlink_libs(rctx, logger, shared_lib_dirs, shared_lib_names)
if not library_srcs:
logger.info("No external python libraries found in {}".format(shared_lib_dirs))

# If the origin doesn't exist, try the multiarch location, in case
# it's an older Python / Debian release.
if not origin.exists and multiarch:
origin = rctx.path("{}/{}/{}".format(shared_lib_dir, multiarch, name))

# The reported names don't always exist; it depends on the particulars
# of the runtime installation.
if origin.exists:
repo_utils.watch(rctx, origin)
rctx.symlink(origin, "lib/" + name)

rctx.file("WORKSPACE", "")
rctx.file("MODULE.bazel", "")
rctx.file("REPO.bazel", "")
rctx.file("BUILD.bazel", _TOOLCHAIN_IMPL_TEMPLATE.format(
build_bazel = _TOOLCHAIN_IMPL_TEMPLATE.format(
major = info["major"],
minor = info["minor"],
micro = info["micro"],
interpreter_path = interpreter_path,
lib_ext = info["SHLIB_SUFFIX"],
interpreter_path = _norm_path(interpreter_path),
library_srcs = repr(library_srcs),
implementation_name = info["implementation_name"],
os = "@platforms//os:{}".format(repo_utils.get_platforms_os_name(rctx)),
))
)
logger.debug("BUILD.bazel\n{}".format(build_bazel))

rctx.file("WORKSPACE", "")
rctx.file("MODULE.bazel", "")
rctx.file("REPO.bazel", "")
rctx.file("BUILD.bazel", build_bazel)

local_runtime_repo = repository_rule(
implementation = _local_runtime_repo_impl,
Expand Down Expand Up @@ -218,7 +263,7 @@ def _expand_incompatible_template():
return _TOOLCHAIN_IMPL_TEMPLATE.format(
interpreter_path = "/incompatible",
implementation_name = "incompatible",
lib_ext = "incompatible",
library_srcs = "[]"
major = "0",
minor = "0",
micro = "0",
Expand Down
11 changes: 2 additions & 9 deletions python/private/local_runtime_repo_setup.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ _PYTHON_VERSION_FLAG = Label("@rules_python//python/config_settings:python_versi

def define_local_runtime_toolchain_impl(
name,
lib_ext,
major,
minor,
micro,
interpreter_path,
library_srcs,
implementation_name,
os):
"""Defines a toolchain implementation for a local Python runtime.
Expand Down Expand Up @@ -73,14 +73,7 @@ def define_local_runtime_toolchain_impl(
name = "_libpython",
# Don't use a recursive glob because the lib/ directory usually contains
# a subdirectory of the stdlib -- lots of unrelated files
srcs = native.glob(
[
"lib/*{}".format(lib_ext), # Match libpython*.so
"lib/*{}*".format(lib_ext), # Also match libpython*.so.1.0
],
# A Python install may not have shared libraries.
allow_empty = True,
),
srcs = library_srcs,
hdrs = [":_python_headers"],
)

Expand Down