@@ -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 files that zipping processes.
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,75 @@ 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+ interpreter = ctx .actions .declare_file ("{}/bin/{}" .format (venv , py_exe_basename ))
366+ ctx .actions .symlink (output = interpreter , target_file = runtime .interpreter )
367+ interpreter_actual_path = runtime .interpreter .short_path
368+ else :
369+ py_exe_basename = paths .basename (runtime .interpreter_path )
370+ interpreter = ctx .actions .declare_symlink ("{}/bin/{}" .format (venv , py_exe_basename ))
371+ ctx .actions .symlink (output = interpreter , target_path = runtime .interpreter_path )
372+ interpreter_actual_path = runtime .interpreter_path
373+
374+ if runtime .interpreter_version_info :
375+ version = "{}.{}" .format (
376+ runtime .interpreter_version_info .major ,
377+ runtime .interpreter_version_info .minor ,
378+ )
379+ else :
380+ version_flag = ctx .attr ._python_version_flag [config_common .FeatureFlagInfo ].value
381+ version_flag_parts = version_flag .split ("." )[0 :2 ]
382+ version = "{}.{}" .format (* version_flag_parts )
383+
384+ # See site.py logic: free-threaded builds append "t" to the venv lib dir name
385+ if "t" in runtime .abi_flags :
386+ version += "t"
387+
388+ site_packages = "{}/lib/python{}/site-packages" .format (venv , version )
389+ pth = ctx .actions .declare_file ("{}/bazel.pth" .format (site_packages ))
390+ ctx .actions .write (pth , "import _bazel_site_init\n " )
391+
392+ site_init = ctx .actions .declare_file ("{}/_bazel_site_init.py" .format (site_packages ))
393+ computed_subs = ctx .actions .template_dict ()
394+ computed_subs .add_joined ("%imports%" , imports , join_with = ":" , map_each = _map_each_identity )
395+ ctx .actions .expand_template (
396+ template = runtime .site_init_template ,
397+ output = site_init ,
398+ substitutions = {
399+ "%import_all%" : "True" if ctx .fragments .bazel_py .python_import_all_repositories else "False" ,
400+ "%site_init_runfiles_path%" : "{}/{}" .format (ctx .workspace_name , site_init .short_path ),
401+ "%workspace_name%" : ctx .workspace_name ,
402+ },
403+ computed_substitutions = computed_subs ,
404+ )
405+
406+ return struct (
407+ interpreter = interpreter ,
408+ # Runfiles-relative path or absolute path
409+ interpreter_actual_path = interpreter_actual_path ,
410+ files_without_interpreter = [pyvenv_cfg , pth , site_init ],
411+ )
412+
413+ def _map_each_identity (v ):
414+ return v
415+
324416def _create_stage2_bootstrap (
325417 ctx ,
326418 * ,
@@ -363,6 +455,13 @@ def _create_stage2_bootstrap(
363455 )
364456 return output
365457
458+ def _runfiles_root_path (ctx , path ):
459+ # The ../ comes from short_path for files in other repos.
460+ if path .startswith ("../" ):
461+ return path [3 :]
462+ else :
463+ return "{}/{}" .format (ctx .workspace_name , path )
464+
366465def _create_stage1_bootstrap (
367466 ctx ,
368467 * ,
@@ -371,12 +470,24 @@ def _create_stage1_bootstrap(
371470 stage2_bootstrap = None ,
372471 imports = None ,
373472 is_for_zip ,
374- runtime_details ):
473+ runtime_details ,
474+ venv = None ):
375475 runtime = runtime_details .effective_runtime
376476
477+ if venv :
478+ python_binary_path = _runfiles_root_path (ctx , venv .interpreter .short_path )
479+ else :
480+ python_binary_path = runtime_details .executable_interpreter_path
481+
482+ if is_for_zip and venv :
483+ python_binary_actual = _runfiles_root_path (ctx , venv .interpreter_actual_path )
484+ else :
485+ python_binary_actual = ""
486+
377487 subs = {
378488 "%is_zipfile%" : "1" if is_for_zip else "0" ,
379- "%python_binary%" : runtime_details .executable_interpreter_path ,
489+ "%python_binary%" : python_binary_path ,
490+ "%python_binary_actual%" : python_binary_actual ,
380491 "%target%" : str (ctx .label ),
381492 "%workspace_name%" : ctx .workspace_name ,
382493 }
@@ -447,6 +558,7 @@ def _create_windows_exe_launcher(
447558 )
448559
449560def _create_zip_file (ctx , * , output , original_nonzip_executable , zip_main , runfiles ):
561+ """Create a Python zipapp (zip with __main__.py entry point)."""
450562 workspace_name = ctx .workspace_name
451563 legacy_external_runfiles = _py_builtins .get_legacy_external_runfiles (ctx )
452564
@@ -524,7 +636,14 @@ def _get_zip_runfiles_path(path, workspace_name, legacy_external_runfiles):
524636 zip_runfiles_path = paths .normalize ("{}/{}" .format (workspace_name , path ))
525637 return "{}/{}" .format (_ZIP_RUNFILES_DIRECTORY_NAME , zip_runfiles_path )
526638
527- def _create_executable_zip_file (ctx , * , output , zip_file , stage2_bootstrap , runtime_details ):
639+ def _create_executable_zip_file (
640+ ctx ,
641+ * ,
642+ output ,
643+ zip_file ,
644+ stage2_bootstrap ,
645+ runtime_details ,
646+ venv ):
528647 prelude = ctx .actions .declare_file (
529648 "{}_zip_prelude.sh" .format (output .basename ),
530649 sibling = output ,
@@ -536,6 +655,7 @@ def _create_executable_zip_file(ctx, *, output, zip_file, stage2_bootstrap, runt
536655 stage2_bootstrap = stage2_bootstrap ,
537656 runtime_details = runtime_details ,
538657 is_for_zip = True ,
658+ venv = venv ,
539659 )
540660 else :
541661 ctx .actions .write (prelude , "#!/usr/bin/env python3\n " )
0 commit comments