@@ -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