@@ -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.
7892EXECUTABLE_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
501517def _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
530546def _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
751801def _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
778833def _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+
910973def _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 ]
0 commit comments