@@ -81,6 +81,9 @@ the `srcs` of Python targets as required.
8181        "_py_toolchain_type" : attr .label (
8282            default  =  TARGET_TOOLCHAIN_TYPE ,
8383        ),
84+         "_python_version_flag" : attr .label (
85+             default  =  "//python/config_settings:python_version" ,
86+         ),
8487        "_windows_launcher_maker" : attr .label (
8588            default  =  "@bazel_tools//tools/launcher:launcher_maker" ,
8689            cfg  =  "exec" ,
@@ -177,13 +180,22 @@ def _create_executable(
177180    else :
178181        base_executable_name  =  executable .basename 
179182
183+     venv  =  None 
184+ 
180185    # The check for stage2_bootstrap_template is to support legacy 
181186    # BuiltinPyRuntimeInfo providers, which is likely to come from 
182187    # @bazel_tools//tools/python:autodetecting_toolchain, the toolchain used 
183188    # for workspace builds when no rules_python toolchain is configured. 
184189    if  (BootstrapImplFlag .get_value (ctx ) ==  BootstrapImplFlag .SCRIPT  and 
185190        runtime_details .effective_runtime  and 
186191        hasattr (runtime_details .effective_runtime , "stage2_bootstrap_template" )):
192+         venv  =  _create_venv (
193+             ctx ,
194+             output_prefix  =  base_executable_name ,
195+             imports  =  imports ,
196+             runtime_details  =  runtime_details ,
197+         )
198+ 
187199        stage2_bootstrap  =  _create_stage2_bootstrap (
188200            ctx ,
189201            output_prefix  =  base_executable_name ,
@@ -192,11 +204,12 @@ def _create_executable(
192204            imports  =  imports ,
193205            runtime_details  =  runtime_details ,
194206        )
195-         extra_runfiles  =  ctx .runfiles ([stage2_bootstrap ])
207+         extra_runfiles  =  ctx .runfiles ([stage2_bootstrap ]  +   venv . files_without_interpreter )
196208        zip_main  =  _create_zip_main (
197209            ctx ,
198210            stage2_bootstrap  =  stage2_bootstrap ,
199211            runtime_details  =  runtime_details ,
212+             venv  =  venv ,
200213        )
201214    else :
202215        stage2_bootstrap  =  None 
@@ -272,6 +285,7 @@ def _create_executable(
272285            zip_file  =  zip_file ,
273286            stage2_bootstrap  =  stage2_bootstrap ,
274287            runtime_details  =  runtime_details ,
288+             venv  =  venv ,
275289        )
276290    elif  bootstrap_output :
277291        _create_stage1_bootstrap (
@@ -282,6 +296,7 @@ def _create_executable(
282296            is_for_zip  =  False ,
283297            imports  =  imports ,
284298            main_py  =  main_py ,
299+             venv  =  venv ,
285300        )
286301    else :
287302        # Otherwise, this should be the Windows case of launcher + zip. 
@@ -296,13 +311,20 @@ def _create_executable(
296311                build_zip_enabled  =  build_zip_enabled ,
297312            ))
298313
314+     # The interpreter is added this late in the process so that it isn't 
315+     # added to the zipped files. 
316+     if  venv :
317+         extra_runfiles  =  extra_runfiles .merge (ctx .runfiles ([venv .interpreter ]))
299318    return  create_executable_result_struct (
300319        extra_files_to_build  =  depset (extra_files_to_build ),
301320        output_groups  =  {"python_zip_file" : depset ([zip_file ])},
302321        extra_runfiles  =  extra_runfiles ,
303322    )
304323
305- def  _create_zip_main (ctx , * , stage2_bootstrap , runtime_details ):
324+ def  _create_zip_main (ctx , * , stage2_bootstrap , runtime_details , venv ):
325+     python_binary  =  _runfiles_root_path (ctx , venv .interpreter .short_path )
326+     python_binary_actual  =  _runfiles_root_path (ctx , venv .interpreter_actual_path )
327+ 
306328    # The location of this file doesn't really matter. It's added to 
307329    # the zip file as the top-level __main__.py file and not included 
308330    # elsewhere. 
@@ -311,7 +333,8 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details):
311333        template  =  runtime_details .effective_runtime .zip_main_template ,
312334        output  =  output ,
313335        substitutions  =  {
314-             "%python_binary%" : runtime_details .executable_interpreter_path ,
336+             "%python_binary%" : python_binary ,
337+             "%python_binary_actual%" : python_binary_actual ,
315338            "%stage2_bootstrap%" : "{}/{}" .format (
316339                ctx .workspace_name ,
317340                stage2_bootstrap .short_path ,
@@ -321,6 +344,82 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details):
321344    )
322345    return  output 
323346
347+ # Create a venv the executable can use. 
348+ # For venv details and the venv startup process, see: 
349+ # * https://docs.python.org/3/library/venv.html 
350+ # * https://snarky.ca/how-virtual-environments-work/ 
351+ # * https://github.com/python/cpython/blob/main/Modules/getpath.py 
352+ # * https://github.com/python/cpython/blob/main/Lib/site.py 
353+ def  _create_venv (ctx , output_prefix , imports , runtime_details ):
354+     venv  =  "_{}.venv" .format (output_prefix .lstrip ("_" ))
355+ 
356+     # The pyvenv.cfg file must be present to trigger the venv site hooks. 
357+     # Because it's paths are expected to be absolute paths, we can't reliably 
358+     # put much in it. See https://github.com/python/cpython/issues/83650 
359+     pyvenv_cfg  =  ctx .actions .declare_file ("{}/pyvenv.cfg" .format (venv ))
360+     ctx .actions .write (pyvenv_cfg , "" )
361+ 
362+     runtime  =  runtime_details .effective_runtime 
363+     if  runtime .interpreter :
364+         py_exe_basename  =  paths .basename (runtime .interpreter .short_path )
365+ 
366+         # Even though ctx.actions.symlink() is used, using 
367+         # declare_symlink() is required to ensure that the resulting file 
368+         # in runfiles is always a symlink. An RBE implementation, for example, 
369+         # may choose to write what symlink() points to instead. 
370+         interpreter  =  ctx .actions .declare_symlink ("{}/bin/{}" .format (venv , py_exe_basename ))
371+         interpreter_actual_path  =  runtime .interpreter .short_path 
372+         parent  =  "/" .join ([".." ] *  (interpreter_actual_path .count ("/" ) +  1 ))
373+         rel_path  =  parent  +  "/"  +  interpreter_actual_path 
374+         ctx .actions .symlink (output  =  interpreter , target_path  =  rel_path )
375+     else :
376+         py_exe_basename  =  paths .basename (runtime .interpreter_path )
377+         interpreter  =  ctx .actions .declare_symlink ("{}/bin/{}" .format (venv , py_exe_basename ))
378+         ctx .actions .symlink (output  =  interpreter , target_path  =  runtime .interpreter_path )
379+         interpreter_actual_path  =  runtime .interpreter_path 
380+ 
381+     if  runtime .interpreter_version_info :
382+         version  =  "{}.{}" .format (
383+             runtime .interpreter_version_info .major ,
384+             runtime .interpreter_version_info .minor ,
385+         )
386+     else :
387+         version_flag  =  ctx .attr ._python_version_flag [config_common .FeatureFlagInfo ].value 
388+         version_flag_parts  =  version_flag .split ("." )[0 :2 ]
389+         version  =  "{}.{}" .format (* version_flag_parts )
390+ 
391+     # See site.py logic: free-threaded builds append "t" to the venv lib dir name 
392+     if  "t"  in  runtime .abi_flags :
393+         version  +=  "t" 
394+ 
395+     site_packages  =  "{}/lib/python{}/site-packages" .format (venv , version )
396+     pth  =  ctx .actions .declare_file ("{}/bazel.pth" .format (site_packages ))
397+     ctx .actions .write (pth , "import _bazel_site_init\n " )
398+ 
399+     site_init  =  ctx .actions .declare_file ("{}/_bazel_site_init.py" .format (site_packages ))
400+     computed_subs  =  ctx .actions .template_dict ()
401+     computed_subs .add_joined ("%imports%" , imports , join_with  =  ":" , map_each  =  _map_each_identity )
402+     ctx .actions .expand_template (
403+         template  =  runtime .site_init_template ,
404+         output  =  site_init ,
405+         substitutions  =  {
406+             "%import_all%" : "True"  if  ctx .fragments .bazel_py .python_import_all_repositories  else  "False" ,
407+             "%site_init_runfiles_path%" : "{}/{}" .format (ctx .workspace_name , site_init .short_path ),
408+             "%workspace_name%" : ctx .workspace_name ,
409+         },
410+         computed_substitutions  =  computed_subs ,
411+     )
412+ 
413+     return  struct (
414+         interpreter  =  interpreter ,
415+         # Runfiles-relative path or absolute path 
416+         interpreter_actual_path  =  interpreter_actual_path ,
417+         files_without_interpreter  =  [pyvenv_cfg , pth , site_init ],
418+     )
419+ 
420+ def  _map_each_identity (v ):
421+     return  v 
422+ 
324423def  _create_stage2_bootstrap (
325424        ctx ,
326425        * ,
@@ -363,6 +462,13 @@ def _create_stage2_bootstrap(
363462    )
364463    return  output 
365464
465+ def  _runfiles_root_path (ctx , path ):
466+     # The ../ comes from short_path for files in other repos. 
467+     if  path .startswith ("../" ):
468+         return  path [3 :]
469+     else :
470+         return  "{}/{}" .format (ctx .workspace_name , path )
471+ 
366472def  _create_stage1_bootstrap (
367473        ctx ,
368474        * ,
@@ -371,12 +477,24 @@ def _create_stage1_bootstrap(
371477        stage2_bootstrap  =  None ,
372478        imports  =  None ,
373479        is_for_zip ,
374-         runtime_details ):
480+         runtime_details ,
481+         venv  =  None ):
375482    runtime  =  runtime_details .effective_runtime 
376483
484+     if  venv :
485+         python_binary_path  =  _runfiles_root_path (ctx , venv .interpreter .short_path )
486+     else :
487+         python_binary_path  =  runtime_details .executable_interpreter_path 
488+ 
489+     if  is_for_zip  and  venv :
490+         python_binary_actual  =  _runfiles_root_path (ctx , venv .interpreter_actual_path )
491+     else :
492+         python_binary_actual  =  "" 
493+ 
377494    subs  =  {
378495        "%is_zipfile%" : "1"  if  is_for_zip  else  "0" ,
379-         "%python_binary%" : runtime_details .executable_interpreter_path ,
496+         "%python_binary%" : python_binary_path ,
497+         "%python_binary_actual%" : python_binary_actual ,
380498        "%target%" : str (ctx .label ),
381499        "%workspace_name%" : ctx .workspace_name ,
382500    }
@@ -447,6 +565,7 @@ def _create_windows_exe_launcher(
447565    )
448566
449567def  _create_zip_file (ctx , * , output , original_nonzip_executable , zip_main , runfiles ):
568+     """Create a Python zipapp (zip with __main__.py entry point).""" 
450569    workspace_name  =  ctx .workspace_name 
451570    legacy_external_runfiles  =  _py_builtins .get_legacy_external_runfiles (ctx )
452571
@@ -524,7 +643,14 @@ def _get_zip_runfiles_path(path, workspace_name, legacy_external_runfiles):
524643        zip_runfiles_path  =  paths .normalize ("{}/{}" .format (workspace_name , path ))
525644    return  "{}/{}" .format (_ZIP_RUNFILES_DIRECTORY_NAME , zip_runfiles_path )
526645
527- def  _create_executable_zip_file (ctx , * , output , zip_file , stage2_bootstrap , runtime_details ):
646+ def  _create_executable_zip_file (
647+         ctx ,
648+         * ,
649+         output ,
650+         zip_file ,
651+         stage2_bootstrap ,
652+         runtime_details ,
653+         venv ):
528654    prelude  =  ctx .actions .declare_file (
529655        "{}_zip_prelude.sh" .format (output .basename ),
530656        sibling  =  output ,
@@ -536,6 +662,7 @@ def _create_executable_zip_file(ctx, *, output, zip_file, stage2_bootstrap, runt
536662            stage2_bootstrap  =  stage2_bootstrap ,
537663            runtime_details  =  runtime_details ,
538664            is_for_zip  =  True ,
665+             venv  =  venv ,
539666        )
540667    else :
541668        ctx .actions .write (prelude , "#!/usr/bin/env python3\n " )
0 commit comments