Skip to content

Commit 8eaafca

Browse files
committed
feat: make windows use full venvs
1 parent 639000e commit 8eaafca

10 files changed

Lines changed: 283 additions & 137 deletions

python/private/hermetic_runtime_repo_setup.bzl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@ def define_hermetic_runtime_toolchain_impl(
240240
_IS_FREETHREADED_YES: "cpython-{major}{minor}t".format(**version_dict),
241241
_IS_FREETHREADED_NO: "cpython-{major}{minor}".format(**version_dict),
242242
}),
243+
# On Windows, a symlink-style venv requires supporting .dll files.
244+
venv_bin_files = select({
245+
"@platforms//os:windows": native.glob(include=["*.dll", "*.pdb"]),
246+
"//conditions:default": [],
247+
})
243248
)
244249

245250
py_runtime_pair(

python/private/py_executable.bzl

Lines changed: 156 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ WARNING: Target: {}
475475
# The interpreter is added this late in the process so that it isn't
476476
# added to the zipped files.
477477
if venv and venv.interpreter:
478-
extra_runfiles = extra_runfiles.merge(ctx.runfiles([venv.interpreter]))
478+
extra_runfiles = extra_runfiles.merge(venv.interpreter_runfiles)
479479
return struct(
480480
# depset[File] of additional files that should be included as default
481481
# outputs.
@@ -524,25 +524,103 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details, venv):
524524
# * https://github.com/python/cpython/blob/main/Modules/getpath.py
525525
# * https://github.com/python/cpython/blob/main/Lib/site.py
526526
def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root_to_sys_path, extra_deps):
527-
venv = "_{}.venv".format(output_prefix.lstrip("_"))
528-
529-
# The pyvenv.cfg file must be present to trigger the venv site hooks.
530-
# Because it's paths are expected to be absolute paths, we can't reliably
531-
# put much in it. See https://github.com/python/cpython/issues/83650
532-
pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv))
533-
ctx.actions.write(pyvenv_cfg, "")
527+
venv_root = "_{}.venv".format(output_prefix.lstrip("_"))
528+
runtime = runtime_details.effective_runtime
529+
if runtime.interpreter:
530+
interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path)
531+
else:
532+
interpreter_actual_path = runtime.interpreter_path
534533

535-
is_bootstrap_script = BootstrapImplFlag.get_value(ctx) == BootstrapImplFlag.SCRIPT
536534
is_windows = target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints)
535+
if is_windows:
536+
venv_details = _create_venv_windows(ctx, venv_root = venv_root,
537+
interpreter_actual_path = interpreter_actual_path,
538+
runtime = runtime,
539+
)
540+
else:
541+
venv_details = _create_venv_unixy(ctx, venv_root= venv_root,
542+
interpreter_actual_path = interpreter_actual_path,
543+
runtime = runtime,
544+
)
537545

538-
create_full_venv = True
546+
site_packages = "{}/{}".format(venv_root, venv_details.venv_site_packages)
547+
548+
pth = ctx.actions.declare_file("{}/bazel.pth".format(site_packages))
549+
ctx.actions.write(pth, "import _bazel_site_init\n")
550+
551+
site_init = ctx.actions.declare_file("{}/_bazel_site_init.py".format(site_packages))
552+
computed_subs = ctx.actions.template_dict()
553+
computed_subs.add_joined("%imports%", imports, join_with = ":", map_each = _map_each_identity)
554+
ctx.actions.expand_template(
555+
template = runtime.site_init_template,
556+
output = site_init,
557+
substitutions = {
558+
"%add_runfiles_root_to_sys_path%": add_runfiles_root_to_sys_path,
559+
"%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
560+
"%import_all%": "True" if read_possibly_native_flag(ctx, "python_import_all_repositories") else "False",
561+
"%site_init_runfiles_path%": runfiles_root_path(ctx, site_init.short_path),
562+
"%workspace_name%": ctx.workspace_name,
563+
},
564+
computed_substitutions = computed_subs,
565+
)
566+
567+
venv_dir_map = {
568+
VenvSymlinkKind.BIN: venv_details.bin_dir,
569+
VenvSymlinkKind.LIB: site_packages,
570+
}
571+
venv_app_files = create_venv_app_files(
572+
ctx,
573+
deps = collect_deps(ctx, extra_deps),
574+
venv_dir_map = venv_dir_map,
575+
)
576+
577+
files_without_interpreter = [pth, site_init] + venv_app_files.venv_files
578+
if venv_details.pyvenv_cfg:
579+
files_without_interpreter.append(venv_details.pyvenv_cfg)
580+
581+
return struct(
582+
# File or None; the `bin/python3` executable in the venv.
583+
# None if a full venv isn't created.
584+
interpreter = venv_details.interpreter,
585+
# Files in the venv that need to be created for the interpreter to work
586+
interpreter_runfiles = venv_details.interpreter_runfiles,
587+
# bool; True if the venv should be recreated at runtime
588+
recreate_venv_at_runtime = venv_details.recreate_venv_at_runtime,
589+
# Runfiles root relative path or absolute path
590+
interpreter_actual_path = interpreter_actual_path,
591+
files_without_interpreter = files_without_interpreter,
592+
# string; venv-relative path to the site-packages directory.
593+
venv_site_packages = venv_details.venv_site_packages,
594+
# string; runfiles-root relative path to venv root.
595+
venv_root = runfiles_root_path(
596+
ctx,
597+
paths.join(
598+
py_internal.get_label_repo_runfiles_path(ctx.label),
599+
venv_root,
600+
),
601+
),
602+
# venv files for user library dependencies (files that are specific
603+
# to the executable bootstrap and python runtime aren't here).
604+
# `root_symlinks` should be used, otherwise, with symlinks files always go
605+
# to `_main` prefix, and binaries from non-root module become broken.
606+
lib_runfiles = ctx.runfiles(
607+
root_symlinks = venv_app_files.runfiles_symlinks,
608+
),
609+
)
539610

611+
def _create_venv_unixy(ctx, *, venv_root, runtime, interpreter_actual_path):
612+
interpreter_runfiles = builders.RunfilesBuilder()
613+
if runtime.interpreter:
614+
interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path)
615+
else:
616+
interpreter_actual_path = runtime.interpreter_path
617+
618+
is_bootstrap_script = BootstrapImplFlag.get_value(ctx) == BootstrapImplFlag.SCRIPT
619+
create_full_venv = True
540620
# The legacy build_python_zip codepath (enabled by default on windows) isn't
541621
# compatible with full venv.
542622
# TODO: Use non-build_python_zip codepath for Windows
543-
if is_windows:
544-
create_full_venv = True
545-
elif not rp_config.bazel_8_or_later and not is_bootstrap_script:
623+
if not rp_config.bazel_8_or_later and not is_bootstrap_script:
546624
# Full venv for Bazel 7 + system_python is disabled because packaging
547625
# it using build_python_zip=true or rules_pkg breaks.
548626
# * Using build_python_zip=true breaks because the legacy zipapp support
@@ -558,24 +636,18 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
558636
# The pyvenv.cfg file must be present to trigger the venv site hooks.
559637
# Because it's paths are expected to be absolute paths, we can't reliably
560638
# put much in it. See https://github.com/python/cpython/issues/83650
561-
pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv))
639+
pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv_root))
562640
ctx.actions.write(pyvenv_cfg, "")
563641
else:
564642
pyvenv_cfg = None
565-
runtime = runtime_details.effective_runtime
566643

567644
venvs_use_declare_symlink_enabled = (
568645
VenvsUseDeclareSymlinkFlag.get_value(ctx) == VenvsUseDeclareSymlinkFlag.YES
569646
)
570-
recreate_venv_at_runtime = False
571-
572-
if runtime.interpreter:
573-
interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path)
574-
else:
575-
interpreter_actual_path = runtime.interpreter_path
576647

577-
bin_dir = "{}/bin".format(venv)
648+
recreate_venv_at_runtime = False
578649

650+
bin_dir = "{}/bin".format(venv_root)
579651
if create_full_venv:
580652
# Some wrappers around the interpreter (e.g. pyenv) use the program
581653
# name to decide what to do, so preserve the name.
@@ -604,7 +676,6 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
604676
from_ = paths.dirname(runfiles_root_path(ctx, interpreter.short_path)),
605677
to = interpreter_actual_path,
606678
)
607-
608679
ctx.actions.symlink(output = interpreter, target_path = rel_path)
609680
else:
610681
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
@@ -627,67 +698,75 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
627698
version += "t"
628699

629700
venv_site_packages = "lib/python{}/site-packages".format(version)
630-
site_packages = "{}/{}".format(venv, venv_site_packages)
631-
pth = ctx.actions.declare_file("{}/bazel.pth".format(site_packages))
632-
ctx.actions.write(pth, "import _bazel_site_init\n")
633-
634-
site_init = ctx.actions.declare_file("{}/_bazel_site_init.py".format(site_packages))
635-
computed_subs = ctx.actions.template_dict()
636-
computed_subs.add_joined("%imports%", imports, join_with = ":", map_each = _map_each_identity)
637-
ctx.actions.expand_template(
638-
template = runtime.site_init_template,
639-
output = site_init,
640-
substitutions = {
641-
"%add_runfiles_root_to_sys_path%": add_runfiles_root_to_sys_path,
642-
"%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime),
643-
"%import_all%": "True" if read_possibly_native_flag(ctx, "python_import_all_repositories") else "False",
644-
"%site_init_runfiles_path%": runfiles_root_path(ctx, site_init.short_path),
645-
"%workspace_name%": ctx.workspace_name,
646-
},
647-
computed_substitutions = computed_subs,
701+
return _venv_details(
702+
pyvenv_cfg = pyvenv_cfg,
703+
venv_site_packages = venv_site_packages,
704+
bin_dir = bin_dir,
705+
recreate_venv_at_runtime = recreate_venv_at_runtime,
706+
interpreter_runfiles = interpreter_runfiles.build(ctx),
648707
)
649708

650-
venv_dir_map = {
651-
VenvSymlinkKind.BIN: bin_dir,
652-
VenvSymlinkKind.LIB: site_packages,
653-
}
654-
venv_app_files = create_venv_app_files(
655-
ctx,
656-
deps = collect_deps(ctx, extra_deps),
657-
venv_dir_map = venv_dir_map,
658-
)
709+
def _create_venv_windows(ctx, *, venv_root, runtime, interpreter_actual_path):
710+
interpreter_runfiles = builders.RunfilesBuilder()
659711

660-
files_without_interpreter = [pth, site_init] + venv_app_files.venv_files
661-
if pyvenv_cfg:
662-
files_without_interpreter.append(pyvenv_cfg)
712+
# Some wrappers around the interpreter (e.g. pyenv) use the program
713+
# name to decide what to do, so preserve the name.
714+
py_exe_basename = paths.basename(interpreter_actual_path)
715+
bin_dir = "{}/Scripts".format(venv_root)
716+
if runtime.interpreter:
717+
interpreter = ctx.actions.declare_file("{}/{}".format(bin_dir, py_exe_basename))
718+
interpreter_runfiles.add(interpreter)
719+
ctx.actions.symlink(output = interpreter, target_file = runtime.interpreter)
720+
for f in runtime.venv_bin_files:
721+
venv_path = "{}/{}".format(bin_dir, f.basename)
722+
venv_file = ctx.actions.declare_file(venv_path)
723+
ctx.actions.symlink(output = venv_file, target_file = f)
724+
interpreter_runfiles.add(venv_file)
725+
else:
726+
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
727+
interpreter_runfiles.add(interpreter)
728+
ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path)
729+
730+
# See site.py logic: Windows uses a version/build agnostic site-packages path
731+
venv_site_packages = "Lib/site-packages"
732+
733+
return _venv_details(
734+
interpreter = interpreter,
735+
pyvenv_cfg = None,
736+
venv_site_packages = venv_site_packages,
737+
bin_dir = bin_dir,
738+
recreate_venv_at_runtime = True,
739+
interpreter_runfiles = interpreter_runfiles.build(ctx),
740+
)
663741

742+
def _venv_details(*,
743+
interpreter,
744+
pyvenv_cfg,
745+
venv_site_packages,
746+
bin_dir,
747+
recreate_venv_at_runtime,
748+
interpreter_runfiles,
749+
):
750+
"""Helper to create a struct of platform-specific venv details."""
664751
return struct(
665-
# File or None; the `bin/python3` executable in the venv.
666-
# None if a full venv isn't created.
752+
# File; the `bin/python` executable (or equivalent) within the venv.
667753
interpreter = interpreter,
668-
# bool; True if the venv should be recreated at runtime
669-
recreate_venv_at_runtime = recreate_venv_at_runtime,
670-
# Runfiles root relative path or absolute path
671-
interpreter_actual_path = interpreter_actual_path,
672-
files_without_interpreter = files_without_interpreter,
673-
# string; venv-relative path to the site-packages directory.
754+
# File|None; the pyvenv.cfg file, if any. May be none, in which case,
755+
# it's expected that one will be created at runtime.
756+
pyvenv_cfg = pyvenv_cfg,
757+
# str; venv-relative path to the site-packages directory
674758
venv_site_packages = venv_site_packages,
675-
# string; runfiles-root relative path to venv root.
676-
venv_root = runfiles_root_path(
677-
ctx,
678-
paths.join(
679-
py_internal.get_label_repo_runfiles_path(ctx.label),
680-
venv,
681-
),
682-
),
683-
# venv files for user library dependencies (files that are specific
684-
# to the executable bootstrap and python runtime aren't here).
685-
# `root_symlinks` should be used, otherwise, with symlinks files always go
686-
# to `_main` prefix, and binaries from non-root module become broken.
687-
lib_runfiles = ctx.runfiles(
688-
root_symlinks = venv_app_files.runfiles_symlinks,
689-
),
759+
# str; ctx-relative path to the venv's bin directory.
760+
bin_dir = bin_dir,
761+
# bool; True if the venv needs to be recreated at runtime (because the
762+
# build-time construction isn't sufficient). False if the build-time
763+
# constructed venv is sufficient.
764+
recreate_venv_at_runtime = recreate_venv_at_runtime ,
765+
# runfiles; runfiles for interpreter-specific files in the venv.
766+
interpreter_runfiles = interpreter_runfiles,
690767
)
768+
)
769+
691770

692771
def _map_each_identity(v):
693772
return v
@@ -777,6 +856,7 @@ def _create_stage1_bootstrap(
777856
else:
778857
resolve_python_binary_at_runtime = "1"
779858

859+
780860
subs = {
781861
"%interpreter_args%": "\n".join(ctx.attr.interpreter_args),
782862
"%is_zipfile%": "1" if is_for_zip else "0",

python/private/py_runtime_info.bzl

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ def _PyRuntimeInfo_init(
6666
zip_main_template = None,
6767
abi_flags = "",
6868
site_init_template = None,
69-
supports_build_time_venv = True):
69+
supports_build_time_venv = True,
70+
venv_bin_files = None):
7071
if (interpreter_path and interpreter) or (not interpreter_path and not interpreter):
7172
fail("exactly one of interpreter or interpreter_path must be specified")
7273

@@ -120,6 +121,7 @@ def _PyRuntimeInfo_init(
120121
"stub_shebang": stub_shebang,
121122
"supports_build_time_venv": supports_build_time_venv,
122123
"zip_main_template": zip_main_template,
124+
"venv_bin_files": venv_bin_files,
123125
}
124126

125127
PyRuntimeInfo, _unused_raw_py_runtime_info_ctor = provider(
@@ -355,6 +357,14 @@ The following substitutions are made during template expansion:
355357
356358
:::{versionadded} 0.33.0
357359
:::
360+
""",
361+
"venv_bin_files": """
362+
:type: list[File]
363+
364+
Files that should be added to the venv's `bin/` (or platform-specific equivalent)
365+
directory (using the file's basename).
366+
367+
:::{versionadded} VERSION_NEXT_FEATURE
358368
""",
359369
},
360370
)

python/private/py_runtime_rule.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def _py_runtime_impl(ctx):
130130
abi_flags = abi_flags,
131131
site_init_template = ctx.file.site_init_template,
132132
supports_build_time_venv = ctx.attr.supports_build_time_venv,
133+
venv_bin_files = ctx.files.venv_bin_files,
133134
))
134135

135136
providers = [
@@ -379,6 +380,7 @@ The {obj}`PyRuntimeInfo.zip_main_template` field.
379380
"_python_version_flag": attr.label(
380381
default = labels.PYTHON_VERSION,
381382
),
383+
"venv_bin_files": attr.label_list(allow_files=True),
382384
},
383385
),
384386
)

0 commit comments

Comments
 (0)