Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
639000e
force full venv
rickeylev Mar 21, 2026
8eaafca
feat: make windows use full venvs
rickeylev Apr 4, 2026
27732f6
fix syntax error, sys_path order test
rickeylev Apr 4, 2026
e89f1c9
allow empty dll glob
rickeylev Apr 4, 2026
5b07725
Merge branch 'main' of https://github.com/bazel-contrib/rules_python …
rickeylev Apr 4, 2026
1f47ab5
fix is_windows bug, address gemini comments
rickeylev Apr 4, 2026
04fd4ee
format code
rickeylev Apr 4, 2026
34c7b37
fix missing arg, test_basic_windows analysis test
rickeylev Apr 4, 2026
5344aa0
fix test_debugger
rickeylev Apr 4, 2026
6fc07d7
fix/handle missing interpreter in bin
rickeylev Apr 4, 2026
a77e542
remove windows build_python_zip_true test
rickeylev Apr 4, 2026
d08770b
format
rickeylev Apr 4, 2026
815cacb
udno deleted packages change
rickeylev Apr 4, 2026
fb51a46
make zippapp + windows + venv work
rickeylev Apr 4, 2026
23783ff
Merge branch 'sys.py.win.venv' of https://github.com/rickeylev/rules_…
rickeylev Apr 4, 2026
8495123
remove extraneous zip_main logic
rickeylev Apr 5, 2026
e3ba016
Merge branch 'main' of https://github.com/bazel-contrib/rules_python …
rickeylev Apr 5, 2026
4ac1c81
Merge branch 'main' of https://github.com/bazel-contrib/rules_python …
rickeylev Apr 5, 2026
98aa09d
remove chmod+w, cleanup print_verbose
rickeylev Apr 5, 2026
27088fe
normpath, fix venv.interpreter_runfiles NPE
rickeylev Apr 5, 2026
1c069b9
special test configs
rickeylev Apr 5, 2026
fc800e8
fix zipapp bootstrap for windows
rickeylev Apr 6, 2026
d087713
cleanup python_bootstrap_template.txt a bit
rickeylev Apr 6, 2026
e0722d0
fix repl test
rickeylev Apr 6, 2026
78c62b2
doc missing arg
rickeylev Apr 6, 2026
bc9d121
format code
rickeylev Apr 6, 2026
887ebfb
fix some windows 10 settings for local_toolchains
rickeylev Apr 6, 2026
5a73954
Merge branch 'sys.py.win.venv' of https://github.com/rickeylev/rules_…
rickeylev Apr 6, 2026
085bc24
fix local toolchain
rickeylev Apr 6, 2026
132174a
fix glob for windows+linux
rickeylev Apr 6, 2026
5c51f06
format
rickeylev Apr 6, 2026
399a462
remove defunct comment
rickeylev Apr 6, 2026
96c5929
Merge branch 'sys.py.win.venv' of https://github.com/rickeylev/rules_…
rickeylev Apr 6, 2026
d771b76
fix dll include again
rickeylev Apr 6, 2026
6f26953
have a manifest of symlinks to work around windows+declare_symlink bugs
rickeylev Apr 8, 2026
711f758
format
rickeylev Apr 8, 2026
4a8260c
fix empty runtime venv symlinks handling
rickeylev Apr 8, 2026
454d3a6
fix buildifier warning
rickeylev Apr 8, 2026
b6fd07b
update zipper test
rickeylev Apr 8, 2026
4451571
re-enable zip cleanup
rickeylev Apr 8, 2026
c4d0643
have explicit symlinks add files to runfiles to ensure target exists
rickeylev Apr 8, 2026
5b4ebbb
format
rickeylev Apr 8, 2026
5093729
update zipper test
rickeylev Apr 8, 2026
9f77a5e
Merge branch 'main' of https://github.com/bazel-contrib/rules_python …
rickeylev Apr 8, 2026
2576dd1
format
rickeylev Apr 8, 2026
58307f5
fix relpath bug in zipper
rickeylev Apr 8, 2026
754790c
fix zipper test
rickeylev Apr 8, 2026
e301c14
add test to verify symlink is readable after extraction
rickeylev Apr 8, 2026
ff00bd3
fix zipapp build_manifest bad arg
rickeylev Apr 8, 2026
78eff68
fix zip_main hash calculation, was omitting inputs
rickeylev Apr 8, 2026
6110990
update zipper test
rickeylev Apr 8, 2026
c49bb5d
remove debug print
rickeylev Apr 8, 2026
8a03d79
handle symlink manifest entries in zip_main_maker
rickeylev Apr 8, 2026
23c4d87
fix zipper test for windows
rickeylev Apr 8, 2026
f5ef9d3
format code
rickeylev Apr 8, 2026
77d087d
rename specialized config
rickeylev Apr 9, 2026
93be441
add note that the file is generated
rickeylev Apr 9, 2026
49aa579
cleanup, expand nodeps test
rickeylev Apr 9, 2026
0c42698
cleanup var name
rickeylev Apr 9, 2026
311c166
Merge branch 'sys.py.win.venv' of https://github.com/rickeylev/rules_…
rickeylev Apr 9, 2026
a0f96de
make build_zip_enabled a one liner
rickeylev Apr 9, 2026
22a5d61
update changelog
rickeylev Apr 9, 2026
7696cae
copyedit changelog
rickeylev Apr 9, 2026
7deb7e2
format
rickeylev Apr 9, 2026
62b0dc9
remove defunct comment
rickeylev Apr 9, 2026
ace63ba
Merge branch 'sys.py.win.venv' of https://github.com/rickeylev/rules_…
rickeylev Apr 9, 2026
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 .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ common --incompatible_no_implicit_file_export

build --lockfile_mode=update

try-import user.bazelrc
8 changes: 8 additions & 0 deletions python/private/hermetic_runtime_repo_setup.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,14 @@ def define_hermetic_runtime_toolchain_impl(
_IS_FREETHREADED_YES: "cpython-{major}{minor}t".format(**version_dict),
_IS_FREETHREADED_NO: "cpython-{major}{minor}".format(**version_dict),
}),
# On Windows, a symlink-style venv requires supporting .dll files.
venv_bin_files = select({
"@platforms//os:windows": native.glob(
include=["*.dll", "*.pdb"],
allow_empty=True,
),
"//conditions:default": [],
})
)

py_runtime_pair(
Expand Down
231 changes: 155 additions & 76 deletions python/private/py_executable.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ def _create_executable(

# NOTE: --build_python_zip defaults to true on Windows
build_zip_enabled = read_possibly_native_flag(ctx, "build_python_zip")
Comment thread
rickeylev marked this conversation as resolved.
Outdated
build_zip_enabled = False
Comment thread
rickeylev marked this conversation as resolved.
Outdated

# When --build_python_zip is enabled, then the zip file becomes
# one of the default outputs.
Expand Down Expand Up @@ -474,7 +475,7 @@ WARNING: Target: {}
# The interpreter is added this late in the process so that it isn't
# added to the zipped files.
if venv and venv.interpreter:
extra_runfiles = extra_runfiles.merge(ctx.runfiles([venv.interpreter]))
extra_runfiles = extra_runfiles.merge(venv.interpreter_runfiles)
return struct(
# depset[File] of additional files that should be included as default
# outputs.
Expand Down Expand Up @@ -523,25 +524,103 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details, venv):
# * https://github.com/python/cpython/blob/main/Modules/getpath.py
# * https://github.com/python/cpython/blob/main/Lib/site.py
def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root_to_sys_path, extra_deps):
venv = "_{}.venv".format(output_prefix.lstrip("_"))

# The pyvenv.cfg file must be present to trigger the venv site hooks.
# Because it's paths are expected to be absolute paths, we can't reliably
# put much in it. See https://github.com/python/cpython/issues/83650
pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv))
ctx.actions.write(pyvenv_cfg, "")
venv_root = "_{}.venv".format(output_prefix.lstrip("_"))
runtime = runtime_details.effective_runtime
if runtime.interpreter:
interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path)
else:
interpreter_actual_path = runtime.interpreter_path

is_bootstrap_script = BootstrapImplFlag.get_value(ctx) == BootstrapImplFlag.SCRIPT
is_windows = target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints)
if is_windows:
venv_details = _create_venv_windows(ctx, venv_root = venv_root,
interpreter_actual_path = interpreter_actual_path,
runtime = runtime,
)
else:
venv_details = _create_venv_unixy(ctx, venv_root= venv_root,
interpreter_actual_path = interpreter_actual_path,
runtime = runtime,
)

create_full_venv = True
site_packages = "{}/{}".format(venv_root, venv_details.venv_site_packages)

pth = ctx.actions.declare_file("{}/bazel.pth".format(site_packages))
ctx.actions.write(pth, "import _bazel_site_init\n")

site_init = ctx.actions.declare_file("{}/_bazel_site_init.py".format(site_packages))
computed_subs = ctx.actions.template_dict()
computed_subs.add_joined("%imports%", imports, join_with = ":", map_each = _map_each_identity)
ctx.actions.expand_template(
template = runtime.site_init_template,
output = site_init,
substitutions = {
"%add_runfiles_root_to_sys_path%": add_runfiles_root_to_sys_path,
"%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
"%import_all%": "True" if read_possibly_native_flag(ctx, "python_import_all_repositories") else "False",
"%site_init_runfiles_path%": runfiles_root_path(ctx, site_init.short_path),
"%workspace_name%": ctx.workspace_name,
},
computed_substitutions = computed_subs,
)

venv_dir_map = {
VenvSymlinkKind.BIN: venv_details.bin_dir,
VenvSymlinkKind.LIB: site_packages,
}
venv_app_files = create_venv_app_files(
ctx,
deps = collect_deps(ctx, extra_deps),
venv_dir_map = venv_dir_map,
)

files_without_interpreter = [pth, site_init] + venv_app_files.venv_files
if venv_details.pyvenv_cfg:
files_without_interpreter.append(venv_details.pyvenv_cfg)

return struct(
# File or None; the `bin/python3` executable in the venv.
# None if a full venv isn't created.
interpreter = venv_details.interpreter,
# Files in the venv that need to be created for the interpreter to work
interpreter_runfiles = venv_details.interpreter_runfiles,
# bool; True if the venv should be recreated at runtime
recreate_venv_at_runtime = venv_details.recreate_venv_at_runtime,
# Runfiles root relative path or absolute path
interpreter_actual_path = interpreter_actual_path,
files_without_interpreter = files_without_interpreter,
# string; venv-relative path to the site-packages directory.
venv_site_packages = venv_details.venv_site_packages,
# string; runfiles-root relative path to venv root.
venv_root = runfiles_root_path(
ctx,
paths.join(
py_internal.get_label_repo_runfiles_path(ctx.label),
venv_root,
),
),
# venv files for user library dependencies (files that are specific
# to the executable bootstrap and python runtime aren't here).
# `root_symlinks` should be used, otherwise, with symlinks files always go
# to `_main` prefix, and binaries from non-root module become broken.
lib_runfiles = ctx.runfiles(
root_symlinks = venv_app_files.runfiles_symlinks,
),
)

def _create_venv_unixy(ctx, *, venv_root, runtime, interpreter_actual_path):
interpreter_runfiles = builders.RunfilesBuilder()
if runtime.interpreter:
interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path)
else:
interpreter_actual_path = runtime.interpreter_path
Comment thread
rickeylev marked this conversation as resolved.
Outdated

is_bootstrap_script = BootstrapImplFlag.get_value(ctx) == BootstrapImplFlag.SCRIPT
create_full_venv = True
# The legacy build_python_zip codepath (enabled by default on windows) isn't
# compatible with full venv.
# TODO: Use non-build_python_zip codepath for Windows
if is_windows:
create_full_venv = False
elif not rp_config.bazel_8_or_later and not is_bootstrap_script:
if not rp_config.bazel_8_or_later and not is_bootstrap_script:
# Full venv for Bazel 7 + system_python is disabled because packaging
# it using build_python_zip=true or rules_pkg breaks.
# * Using build_python_zip=true breaks because the legacy zipapp support
Expand All @@ -557,24 +636,18 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
# The pyvenv.cfg file must be present to trigger the venv site hooks.
# Because it's paths are expected to be absolute paths, we can't reliably
# put much in it. See https://github.com/python/cpython/issues/83650
pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv))
pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv_root))
ctx.actions.write(pyvenv_cfg, "")
else:
pyvenv_cfg = None
runtime = runtime_details.effective_runtime

venvs_use_declare_symlink_enabled = (
VenvsUseDeclareSymlinkFlag.get_value(ctx) == VenvsUseDeclareSymlinkFlag.YES
)
recreate_venv_at_runtime = False

if runtime.interpreter:
interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path)
else:
interpreter_actual_path = runtime.interpreter_path

bin_dir = "{}/bin".format(venv)
recreate_venv_at_runtime = False

bin_dir = "{}/bin".format(venv_root)
if create_full_venv:
# Some wrappers around the interpreter (e.g. pyenv) use the program
# name to decide what to do, so preserve the name.
Expand Down Expand Up @@ -603,7 +676,6 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
from_ = paths.dirname(runfiles_root_path(ctx, interpreter.short_path)),
to = interpreter_actual_path,
)

ctx.actions.symlink(output = interpreter, target_path = rel_path)
else:
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
Expand All @@ -626,66 +698,72 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
version += "t"

venv_site_packages = "lib/python{}/site-packages".format(version)
site_packages = "{}/{}".format(venv, venv_site_packages)
pth = ctx.actions.declare_file("{}/bazel.pth".format(site_packages))
ctx.actions.write(pth, "import _bazel_site_init\n")

site_init = ctx.actions.declare_file("{}/_bazel_site_init.py".format(site_packages))
computed_subs = ctx.actions.template_dict()
computed_subs.add_joined("%imports%", imports, join_with = ":", map_each = _map_each_identity)
ctx.actions.expand_template(
template = runtime.site_init_template,
output = site_init,
substitutions = {
"%add_runfiles_root_to_sys_path%": add_runfiles_root_to_sys_path,
"%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
"%import_all%": "True" if read_possibly_native_flag(ctx, "python_import_all_repositories") else "False",
"%site_init_runfiles_path%": runfiles_root_path(ctx, site_init.short_path),
"%workspace_name%": ctx.workspace_name,
},
computed_substitutions = computed_subs,
return _venv_details(
pyvenv_cfg = pyvenv_cfg,
venv_site_packages = venv_site_packages,
bin_dir = bin_dir,
recreate_venv_at_runtime = recreate_venv_at_runtime,
interpreter_runfiles = interpreter_runfiles.build(ctx),
)

venv_dir_map = {
VenvSymlinkKind.BIN: bin_dir,
VenvSymlinkKind.LIB: site_packages,
}
venv_app_files = create_venv_app_files(
ctx,
deps = collect_deps(ctx, extra_deps),
venv_dir_map = venv_dir_map,
)
def _create_venv_windows(ctx, *, venv_root, runtime, interpreter_actual_path):
interpreter_runfiles = builders.RunfilesBuilder()

files_without_interpreter = [pth, site_init] + venv_app_files.venv_files
if pyvenv_cfg:
files_without_interpreter.append(pyvenv_cfg)
# Some wrappers around the interpreter (e.g. pyenv) use the program
# name to decide what to do, so preserve the name.
py_exe_basename = paths.basename(interpreter_actual_path)
bin_dir = "{}/Scripts".format(venv_root)
if runtime.interpreter:
interpreter = ctx.actions.declare_file("{}/{}".format(bin_dir, py_exe_basename))
interpreter_runfiles.add(interpreter)
ctx.actions.symlink(output = interpreter, target_file = runtime.interpreter)
for f in runtime.venv_bin_files:
venv_path = "{}/{}".format(bin_dir, f.basename)
venv_file = ctx.actions.declare_file(venv_path)
ctx.actions.symlink(output = venv_file, target_file = f)
interpreter_runfiles.add(venv_file)
else:
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
interpreter_runfiles.add(interpreter)
ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path)

# See site.py logic: Windows uses a version/build agnostic site-packages path
venv_site_packages = "Lib/site-packages"

return _venv_details(
interpreter = interpreter,
pyvenv_cfg = None,
venv_site_packages = venv_site_packages,
bin_dir = bin_dir,
recreate_venv_at_runtime = True,
interpreter_runfiles = interpreter_runfiles.build(ctx),
)

def _venv_details(*,
interpreter,
pyvenv_cfg,
venv_site_packages,
bin_dir,
recreate_venv_at_runtime,
interpreter_runfiles,
):
"""Helper to create a struct of platform-specific venv details."""
return struct(
# File or None; the `bin/python3` executable in the venv.
# None if a full venv isn't created.
# File; the `bin/python` executable (or equivalent) within the venv.
interpreter = interpreter,
# bool; True if the venv should be recreated at runtime
recreate_venv_at_runtime = recreate_venv_at_runtime,
# Runfiles root relative path or absolute path
interpreter_actual_path = interpreter_actual_path,
files_without_interpreter = files_without_interpreter,
# string; venv-relative path to the site-packages directory.
# File|None; the pyvenv.cfg file, if any. May be none, in which case,
# it's expected that one will be created at runtime.
pyvenv_cfg = pyvenv_cfg,
# str; venv-relative path to the site-packages directory
venv_site_packages = venv_site_packages,
# string; runfiles-root relative path to venv root.
venv_root = runfiles_root_path(
ctx,
paths.join(
py_internal.get_label_repo_runfiles_path(ctx.label),
venv,
),
),
# venv files for user library dependencies (files that are specific
# to the executable bootstrap and python runtime aren't here).
# `root_symlinks` should be used, otherwise, with symlinks files always go
# to `_main` prefix, and binaries from non-root module become broken.
lib_runfiles = ctx.runfiles(
root_symlinks = venv_app_files.runfiles_symlinks,
),
# str; ctx-relative path to the venv's bin directory.
bin_dir = bin_dir,
# bool; True if the venv needs to be recreated at runtime (because the
# build-time construction isn't sufficient). False if the build-time
# constructed venv is sufficient.
recreate_venv_at_runtime = recreate_venv_at_runtime ,
Comment thread
rickeylev marked this conversation as resolved.
Outdated
# runfiles; runfiles for interpreter-specific files in the venv.
interpreter_runfiles = interpreter_runfiles,
)

def _map_each_identity(v):
Expand Down Expand Up @@ -776,6 +854,7 @@ def _create_stage1_bootstrap(
else:
resolve_python_binary_at_runtime = "1"


subs = {
"%interpreter_args%": "\n".join(ctx.attr.interpreter_args),
"%is_zipfile%": "1" if is_for_zip else "0",
Expand Down
12 changes: 11 additions & 1 deletion python/private/py_runtime_info.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ def _PyRuntimeInfo_init(
zip_main_template = None,
abi_flags = "",
site_init_template = None,
supports_build_time_venv = True):
supports_build_time_venv = True,
venv_bin_files = None):
if (interpreter_path and interpreter) or (not interpreter_path and not interpreter):
fail("exactly one of interpreter or interpreter_path must be specified")

Expand Down Expand Up @@ -120,6 +121,7 @@ def _PyRuntimeInfo_init(
"stub_shebang": stub_shebang,
"supports_build_time_venv": supports_build_time_venv,
"zip_main_template": zip_main_template,
"venv_bin_files": venv_bin_files,
}

PyRuntimeInfo, _unused_raw_py_runtime_info_ctor = provider(
Expand Down Expand Up @@ -355,6 +357,14 @@ The following substitutions are made during template expansion:

:::{versionadded} 0.33.0
:::
""",
"venv_bin_files": """
:type: list[File]

Files that should be added to the venv's `bin/` (or platform-specific equivalent)
directory (using the file's basename).

:::{versionadded} VERSION_NEXT_FEATURE
Comment thread
rickeylev marked this conversation as resolved.
Outdated
""",
},
)
2 changes: 2 additions & 0 deletions python/private/py_runtime_rule.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ def _py_runtime_impl(ctx):
abi_flags = abi_flags,
site_init_template = ctx.file.site_init_template,
supports_build_time_venv = ctx.attr.supports_build_time_venv,
venv_bin_files = ctx.files.venv_bin_files,
))

providers = [
Expand Down Expand Up @@ -379,6 +380,7 @@ The {obj}`PyRuntimeInfo.zip_main_template` field.
"_python_version_flag": attr.label(
default = labels.PYTHON_VERSION,
),
"venv_bin_files": attr.label_list(allow_files=True),
},
),
)
Expand Down
Loading