Skip to content

Commit 6f26953

Browse files
committed
have a manifest of symlinks to work around windows+declare_symlink bugs
1 parent d771b76 commit 6f26953

7 files changed

Lines changed: 195 additions & 42 deletions

File tree

python/private/hermetic_runtime_repo_setup.bzl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,6 @@ def define_hermetic_runtime_toolchain_impl(
245245
"@platforms//os:windows": native.glob(
246246
include = [
247247
"*.dll",
248-
# The pdb files just provide debugging information
249-
"*.pdb",
250248
],
251249
# This must be true because glob empty-ness is checked
252250
# during loading phase, before select() filters it out.

python/private/py_executable.bzl

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,20 @@ _EXTERNAL_PATH_PREFIX = "external"
7373
_ZIP_RUNFILES_DIRECTORY_NAME = "runfiles"
7474
_INIT_PY = "__init__.py"
7575

76+
ExplicitSymlink = provider(
77+
doc = """
78+
A runfile that should be created as a symlink pointing to a specific location.
79+
80+
This is only needed on Windows, where Bazel doesn't preserve declare_symlink
81+
with relative paths.
82+
""",
83+
fields = {
84+
"rf_path": "runfile-root-relative path for the link",
85+
"venv_rel_path": "venv-root-relative path for the link",
86+
"link_to": "Path the symlink should point to"
87+
}
88+
)
89+
7690
# Non-Google-specific attributes for executables
7791
# These attributes are for rules that accept Python sources.
7892
EXECUTABLE_ATTRS = dicts.add(
@@ -496,6 +510,8 @@ WARNING: Target: {}
496510
venv_python_exe = venv.interpreter if venv else None,
497511
# runfiles|None; runfiles in the venv for the interpreter
498512
venv_interpreter_runfiles = venv.interpreter_runfiles if venv else None,
513+
# depset[ExplicitSymlink]|None; symlinks that should be created
514+
venv_interpreter_symlinks = venv.interpreter_symlinks if venv else None,
499515
)
500516

501517
def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details, venv):
@@ -528,7 +544,7 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details, venv):
528544
# * https://github.com/python/cpython/blob/main/Modules/getpath.py
529545
# * https://github.com/python/cpython/blob/main/Lib/site.py
530546
def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root_to_sys_path, extra_deps):
531-
venv_root = "_{}.venv".format(output_prefix.lstrip("_"))
547+
venv_ctx_rel_root = "_{}.venv".format(output_prefix.lstrip("_"))
532548
runtime = runtime_details.effective_runtime
533549
if runtime.interpreter:
534550
interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path)
@@ -539,19 +555,19 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
539555
if is_windows:
540556
venv_details = _create_venv_windows(
541557
ctx,
542-
venv_root = venv_root,
558+
venv_ctx_rel_root = venv_ctx_rel_root,
543559
interpreter_actual_path = interpreter_actual_path,
544560
runtime = runtime,
545561
)
546562
else:
547563
venv_details = _create_venv_unixy(
548564
ctx,
549-
venv_root = venv_root,
565+
venv_ctx_rel_root = venv_ctx_rel_root,
550566
interpreter_actual_path = interpreter_actual_path,
551567
runtime = runtime,
552568
)
553569

554-
site_packages = "{}/{}".format(venv_root, venv_details.venv_site_packages)
570+
site_packages = "{}/{}".format(venv_ctx_rel_root, venv_details.venv_site_packages)
555571

556572
pth = ctx.actions.declare_file("{}/bazel.pth".format(site_packages))
557573
ctx.actions.write(pth, "import _bazel_site_init\n")
@@ -592,6 +608,10 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
592608
interpreter = venv_details.interpreter,
593609
# Files in the venv that need to be created for the interpreter to work
594610
interpreter_runfiles = venv_details.interpreter_runfiles,
611+
# depset[ExplicitSymlink] of symlinks to create.
612+
# This is only used when declare_symlink() can't be used to represent
613+
# creating such a link (i.e Windows)
614+
interpreter_symlinks = venv_details.interpreter_symlinks,
595615
# bool; True if the venv should be recreated at runtime
596616
recreate_venv_at_runtime = venv_details.recreate_venv_at_runtime,
597617
# Runfiles root relative path or absolute path
@@ -604,7 +624,7 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
604624
ctx,
605625
paths.join(
606626
py_internal.get_label_repo_runfiles_path(ctx.label),
607-
venv_root,
627+
venv_ctx_rel_root,
608628
),
609629
),
610630
# venv files for user library dependencies (files that are specific
@@ -616,7 +636,7 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
616636
),
617637
)
618638

619-
def _create_venv_unixy(ctx, *, venv_root, runtime, interpreter_actual_path):
639+
def _create_venv_unixy(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_path):
620640
interpreter_runfiles = builders.RunfilesBuilder()
621641
is_bootstrap_script = BootstrapImplFlag.get_value(ctx) == BootstrapImplFlag.SCRIPT
622642
create_full_venv = True
@@ -640,7 +660,7 @@ def _create_venv_unixy(ctx, *, venv_root, runtime, interpreter_actual_path):
640660
# The pyvenv.cfg file must be present to trigger the venv site hooks.
641661
# Because it's paths are expected to be absolute paths, we can't reliably
642662
# put much in it. See https://github.com/python/cpython/issues/83650
643-
pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv_root))
663+
pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv_ctx_rel_root))
644664
ctx.actions.write(pyvenv_cfg, "")
645665
else:
646666
pyvenv_cfg = None
@@ -651,7 +671,7 @@ def _create_venv_unixy(ctx, *, venv_root, runtime, interpreter_actual_path):
651671

652672
recreate_venv_at_runtime = False
653673

654-
bin_dir = "{}/bin".format(venv_root)
674+
bin_dir = "{}/bin".format(venv_ctx_rel_root)
655675
if create_full_venv:
656676
# Some wrappers around the interpreter (e.g. pyenv) use the program
657677
# name to decide what to do, so preserve the name.
@@ -710,31 +730,60 @@ def _create_venv_unixy(ctx, *, venv_root, runtime, interpreter_actual_path):
710730
bin_dir = bin_dir,
711731
recreate_venv_at_runtime = recreate_venv_at_runtime,
712732
interpreter_runfiles = interpreter_runfiles.build(ctx),
733+
interpreter_symlinks = depset(),
713734
)
714735

715-
def _create_venv_windows(ctx, *, venv_root, runtime, interpreter_actual_path):
736+
def _create_venv_windows(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_path):
716737
interpreter_runfiles = builders.RunfilesBuilder()
738+
interpreter_symlinks = []
717739

718740
# Some wrappers around the interpreter (e.g. pyenv) use the program
719741
# name to decide what to do, so preserve the name.
720742
py_exe_basename = paths.basename(interpreter_actual_path)
721-
bin_dir = "{}/Scripts".format(venv_root)
743+
venv_bin_rel_path = "Scripts"
744+
venv_bin_ctx_rel_path = "{}/{}".format(venv_ctx_rel_root, venv_bin_rel_path)
722745
if runtime.interpreter:
723-
interpreter = ctx.actions.declare_file("{}/{}".format(bin_dir, py_exe_basename))
746+
venv_rel_path = paths.join(venv_bin_rel_path, py_exe_basename)
747+
venv_ctx_rel_path = paths.join(venv_ctx_rel_root, venv_rel_path)
748+
interpreter = ctx.actions.declare_file(venv_ctx_rel_path)
724749
interpreter_runfiles.add(interpreter)
725750
ctx.actions.symlink(output = interpreter, target_file = runtime.interpreter)
751+
752+
rf_path = runfiles_root_path(ctx, interpreter.short_path)
753+
interpreter_symlinks.append(ExplicitSymlink(
754+
rf_path = rf_path,
755+
venv_rel_path = venv_rel_path,
756+
link_to = relative_path(
757+
# dirname is necessary because a relative symlink is relative to
758+
# the directory the symlink resides within.
759+
from_ = paths.dirname(rf_path),
760+
to = interpreter_actual_path,
761+
)
762+
))
726763
else:
727-
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
764+
interpreter = ctx.actions.declare_symlink("{}/{}".format(venv_bin_ctx_rel_path, py_exe_basename))
728765
interpreter_runfiles.add(interpreter)
729766
ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path)
730767

731768
# NOTE: The .dll files must exist, however, they may not be known at build time
732769
# if the interpreter is resolved at runtime.
733770
for f in runtime.venv_bin_files:
734-
venv_path = "{}/{}".format(bin_dir, f.basename)
735-
venv_file = ctx.actions.declare_file(venv_path)
771+
venv_rel_path = paths.join(venv_bin_rel_path, f.basename)
772+
venv_ctx_rel_path = paths.join(venv_ctx_rel_root, venv_rel_path)
773+
venv_file = ctx.actions.declare_file(venv_ctx_rel_path)
736774
ctx.actions.symlink(output = venv_file, target_file = f)
737775
interpreter_runfiles.add(venv_file)
776+
rf_path = runfiles_root_path(ctx, venv_file.short_path)
777+
interpreter_symlinks.append(ExplicitSymlink(
778+
rf_path = rf_path,
779+
venv_rel_path = venv_rel_path,
780+
link_to = relative_path(
781+
# dirname is necessary because a relative symlink is relative to
782+
# the directory the symlink resides within.
783+
from_ = paths.dirname(rf_path),
784+
to = runfiles_root_path(ctx, f.short_path),
785+
)
786+
))
738787

739788
# See site.py logic: Windows uses a version/build agnostic site-packages path
740789
venv_site_packages = "Lib/site-packages"
@@ -743,9 +792,10 @@ def _create_venv_windows(ctx, *, venv_root, runtime, interpreter_actual_path):
743792
interpreter = interpreter,
744793
pyvenv_cfg = None,
745794
venv_site_packages = venv_site_packages,
746-
bin_dir = bin_dir,
795+
bin_dir = venv_bin_ctx_rel_path,
747796
recreate_venv_at_runtime = True,
748797
interpreter_runfiles = interpreter_runfiles.build(ctx),
798+
interpreter_symlinks = depset(interpreter_symlinks),
749799
)
750800

751801
def _venv_details(
@@ -755,7 +805,9 @@ def _venv_details(
755805
venv_site_packages,
756806
bin_dir,
757807
recreate_venv_at_runtime,
758-
interpreter_runfiles):
808+
interpreter_runfiles,
809+
interpreter_symlinks,
810+
):
759811
"""Helper to create a struct of platform-specific venv details."""
760812
return struct(
761813
# File; the `bin/python` executable (or equivalent) within the venv.
@@ -773,6 +825,9 @@ def _venv_details(
773825
recreate_venv_at_runtime = recreate_venv_at_runtime,
774826
# runfiles; runfiles for interpreter-specific files in the venv.
775827
interpreter_runfiles = interpreter_runfiles,
828+
# depset[ExplicitSymlink] of symlinks specific
829+
# to the interpreter. Only used for Windows.
830+
interpreter_symlinks = interpreter_symlinks,
776831
)
777832

778833
def _map_each_identity(v):
@@ -874,6 +929,10 @@ def _create_stage1_bootstrap(
874929
"%venv_rel_site_packages%": venv.venv_site_packages if venv else "",
875930
"%workspace_name%": ctx.workspace_name,
876931
}
932+
computed_subs = ctx.actions.template_dict()
933+
if venv:
934+
computed_subs.add_joined("%runtime_venv_symlinks%",
935+
venv.interpreter_symlinks, join_with = "\n", map_each = _map_runtime_venv_symlink)
877936

878937
if stage2_bootstrap:
879938
subs["%stage2_bootstrap%"] = runfiles_root_path(ctx, stage2_bootstrap.short_path)
@@ -904,9 +963,13 @@ def _create_stage1_bootstrap(
904963
template = template,
905964
output = output,
906965
substitutions = subs,
966+
computed_substitutions = computed_subs,
907967
is_executable = True,
908968
)
909969

970+
def _map_runtime_venv_symlink(entry):
971+
return entry.venv_rel_path + "|" + entry.link_to
972+
910973
def _create_zip_file(ctx, *, output, zip_main, runfiles):
911974
"""Create a Python zipapp (zip with __main__.py entry point)."""
912975
workspace_name = ctx.workspace_name
@@ -1199,6 +1262,7 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
11991262
app_runfiles = app_runfiles,
12001263
venv_python_exe = exec_result.venv_python_exe,
12011264
venv_interpreter_runfiles = exec_result.venv_interpreter_runfiles,
1265+
venv_interpreter_symlinks = exec_result.venv_interpreter_symlinks,
12021266
interpreter_args = ctx.attr.interpreter_args,
12031267
)
12041268

@@ -1746,6 +1810,7 @@ def _create_providers(
17461810
app_runfiles,
17471811
venv_python_exe,
17481812
venv_interpreter_runfiles,
1813+
venv_interpreter_symlinks,
17491814
interpreter_args):
17501815
"""Creates the providers an executable should return.
17511816
@@ -1780,6 +1845,8 @@ def _create_providers(
17801845
venv_python_exe: File; the python executable in the venv.
17811846
venv_interpreter_runfiles: runfiles; runfiles specific to the interpreter
17821847
for the venv.
1848+
venv_interpreter_symlinks: depset[ExplicitSymlink]; interpreter-specific symlinks
1849+
to create for the venv.
17831850
interpreter_args: list of strings; arguments to pass to the interpreter.
17841851
17851852
Returns:
@@ -1809,6 +1876,7 @@ def _create_providers(
18091876
runfiles_without_exe = runfiles_details.runfiles_without_exe,
18101877
stage2_bootstrap = stage2_bootstrap,
18111878
venv_interpreter_runfiles = venv_interpreter_runfiles,
1879+
venv_interpreter_symlinks = venv_interpreter_symlinks,
18121880
venv_python_exe = venv_python_exe,
18131881
),
18141882
]

python/private/py_executable_info.bzl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,17 @@ implementation isn't being used.
8383
8484
Runfiles that are specific to the interpreter within the venv.
8585
86+
:::{versionadded} VERSION_NEXT_FEATURE
87+
:::
88+
""",
89+
"venv_interpreter_symlinks": """
90+
:type: depset[ExplicitSymlink] | None
91+
92+
Symlinks that are specific to the interpreter within the venv.
93+
94+
Only used with windows. These take precedence over entries in
95+
`venv_interpreter_runfiles`
96+
8697
:::{versionadded} VERSION_NEXT_FEATURE
8798
:::
8899
""",

python/private/python_bootstrap_template.txt

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ from __future__ import print_function
77

88
# Generated file from @rules_python//python/private:python_bootstrap_template.txt
99

10-
from os.path import dirname, join, basename
10+
from os.path import dirname, join, basename, normpath
1111
import os
1212
import shutil
1313
import subprocess
@@ -74,6 +74,14 @@ if _INTERPRETER_ARGS_RAW == _INTERPRETER_ARGS_SENTINEL:
7474
else:
7575
INTERPRETER_ARGS = [arg for arg in _INTERPRETER_ARGS_RAW.split("\n") if arg]
7676

77+
# Symlinks to create at runtime in the venv.
78+
# Line delimited entries
79+
# Each entry is "venv_relative_path|path_to_symlink_to"
80+
RUNTIME_VENV_SYMLINKS = """
81+
%runtime_venv_symlinks%
82+
""".strip().split("\n")
83+
RUNTIME_VENV_SYMLINKS = dict(line.split("|") for line in RUNTIME_VENV_SYMLINKS)
84+
7785
ADDITIONAL_INTERPRETER_ARGS = os.environ.get("RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS", "")
7886
EXTRACT_ROOT = os.environ.get("RULES_PYTHON_EXTRACT_ROOT")
7987

@@ -88,6 +96,20 @@ IS_WINDOWS = os.name == "nt"
8896

8997
BIN_DIR_NAME = "bin" if not IS_WINDOWS else "Scripts"
9098

99+
# Windows APIs can be picky about slashes depending on the context,
100+
# so convert to backslashes to avoid any issues.
101+
if IS_WINDOWS:
102+
def norm_slashes(s):
103+
return s.replace("/", "\\")
104+
105+
STAGE2_BOOTSTRAP = norm_slashes(STAGE2_BOOTSTRAP)
106+
PYTHON_BINARY = norm_slashes(PYTHON_BINARY)
107+
PYTHON_BINARY_ACTUAL = norm_slashes(PYTHON_BINARY_ACTUAL)
108+
RUNTIME_VENV_SYMLINKS = {
109+
norm_slashes(k): norm_slashes(v)
110+
for k, v in RUNTIME_VENV_SYMLINKS.items()
111+
}
112+
91113
def get_windows_path_with_unc_prefix(path):
92114
"""Adds UNC prefix after getting a normalized absolute Windows path.
93115
@@ -349,12 +371,17 @@ print(site.getsitepackages(["{venv_src}"])[-1])
349371
target = join(python_home, f)
350372
_symlink_exist_ok(from_=venv_path, to=target)
351373

374+
for venv_rel_path, link_to in RUNTIME_VENV_SYMLINKS.items():
375+
venv_abs_path = join(venv, venv_rel_path)
376+
link_to = normpath(join(runfiles_venv, dirname(venv_rel_path), link_to))
377+
_symlink_exist_ok(from_=venv_abs_path, to=link_to)
378+
352379
runfiles_venv_bin = join(runfiles_venv, BIN_DIR_NAME)
353-
# The runfiles bin directory may not exist if
354-
# supports_build_time_venv=False and the interpreter is resolved at runtime.
380+
# The runfiles bin directory may not exist if it's empty, e.g.
381+
# supports_build_time_venv=False and the interpreter is resolved at
382+
# runtime.
355383
if os.path.exists(runfiles_venv_bin):
356-
# Re-add all the entries under bin/, which includes supporting
357-
# .dll files when under windows.
384+
# Add any missing build-time entries under bin/
358385
for f_basename in os.listdir(runfiles_venv_bin):
359386
venv_path = join(venv, BIN_DIR_NAME, f_basename)
360387
target = join(runfiles_venv_bin, f_basename)
@@ -495,7 +522,6 @@ def _symlink_exist_ok(*, from_, to):
495522
pass
496523

497524

498-
499525
def main():
500526
print_verbose("sys.version:", sys.version)
501527
print_verbose("initial argv:", values=sys.argv)
@@ -507,6 +533,7 @@ def main():
507533
print_verbose("PYTHON_BINARY_ACTUAL:", PYTHON_BINARY_ACTUAL)
508534
print_verbose("RECREATE_VENV_AT_RUNTIME:", RECREATE_VENV_AT_RUNTIME)
509535
print_verbose("RESOLVE_PYTHON_BINARY_AT_RUNTIME:", RESOLVE_PYTHON_BINARY_AT_RUNTIME)
536+
print_verbose("RUNTIME_VENV_SYMLINKS size:", len(RUNTIME_VENV_SYMLINKS))
510537
print_verbose("STAGE2_BOOTSTRAP:", STAGE2_BOOTSTRAP)
511538
print_verbose("VENV_REL_SITE_PACKAGES:", VENV_REL_SITE_PACKAGES)
512539
print_verbose("WORKSPACE_NAME:", WORKSPACE_NAME )

0 commit comments

Comments
 (0)