@@ -81,6 +81,9 @@ the `srcs` of Python targets as required.
81
81
"_py_toolchain_type" : attr .label (
82
82
default = TARGET_TOOLCHAIN_TYPE ,
83
83
),
84
+ "_python_version_flag" : attr .label (
85
+ default = "//python/config_settings:python_version" ,
86
+ ),
84
87
"_windows_launcher_maker" : attr .label (
85
88
default = "@bazel_tools//tools/launcher:launcher_maker" ,
86
89
cfg = "exec" ,
@@ -177,13 +180,22 @@ def _create_executable(
177
180
else :
178
181
base_executable_name = executable .basename
179
182
183
+ venv = None
184
+
180
185
# The check for stage2_bootstrap_template is to support legacy
181
186
# BuiltinPyRuntimeInfo providers, which is likely to come from
182
187
# @bazel_tools//tools/python:autodetecting_toolchain, the toolchain used
183
188
# for workspace builds when no rules_python toolchain is configured.
184
189
if (BootstrapImplFlag .get_value (ctx ) == BootstrapImplFlag .SCRIPT and
185
190
runtime_details .effective_runtime and
186
191
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
+
187
199
stage2_bootstrap = _create_stage2_bootstrap (
188
200
ctx ,
189
201
output_prefix = base_executable_name ,
@@ -192,11 +204,12 @@ def _create_executable(
192
204
imports = imports ,
193
205
runtime_details = runtime_details ,
194
206
)
195
- extra_runfiles = ctx .runfiles ([stage2_bootstrap ])
207
+ extra_runfiles = ctx .runfiles ([stage2_bootstrap ] + venv . files_without_interpreter )
196
208
zip_main = _create_zip_main (
197
209
ctx ,
198
210
stage2_bootstrap = stage2_bootstrap ,
199
211
runtime_details = runtime_details ,
212
+ venv = venv ,
200
213
)
201
214
else :
202
215
stage2_bootstrap = None
@@ -272,6 +285,7 @@ def _create_executable(
272
285
zip_file = zip_file ,
273
286
stage2_bootstrap = stage2_bootstrap ,
274
287
runtime_details = runtime_details ,
288
+ venv = venv ,
275
289
)
276
290
elif bootstrap_output :
277
291
_create_stage1_bootstrap (
@@ -282,6 +296,7 @@ def _create_executable(
282
296
is_for_zip = False ,
283
297
imports = imports ,
284
298
main_py = main_py ,
299
+ venv = venv ,
285
300
)
286
301
else :
287
302
# Otherwise, this should be the Windows case of launcher + zip.
@@ -296,13 +311,20 @@ def _create_executable(
296
311
build_zip_enabled = build_zip_enabled ,
297
312
))
298
313
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 ]))
299
318
return create_executable_result_struct (
300
319
extra_files_to_build = depset (extra_files_to_build ),
301
320
output_groups = {"python_zip_file" : depset ([zip_file ])},
302
321
extra_runfiles = extra_runfiles ,
303
322
)
304
323
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
+
306
328
# The location of this file doesn't really matter. It's added to
307
329
# the zip file as the top-level __main__.py file and not included
308
330
# elsewhere.
@@ -311,7 +333,8 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details):
311
333
template = runtime_details .effective_runtime .zip_main_template ,
312
334
output = output ,
313
335
substitutions = {
314
- "%python_binary%" : runtime_details .executable_interpreter_path ,
336
+ "%python_binary%" : python_binary ,
337
+ "%python_binary_actual%" : python_binary_actual ,
315
338
"%stage2_bootstrap%" : "{}/{}" .format (
316
339
ctx .workspace_name ,
317
340
stage2_bootstrap .short_path ,
@@ -321,6 +344,82 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details):
321
344
)
322
345
return output
323
346
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
+
324
423
def _create_stage2_bootstrap (
325
424
ctx ,
326
425
* ,
@@ -363,6 +462,13 @@ def _create_stage2_bootstrap(
363
462
)
364
463
return output
365
464
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
+
366
472
def _create_stage1_bootstrap (
367
473
ctx ,
368
474
* ,
@@ -371,12 +477,24 @@ def _create_stage1_bootstrap(
371
477
stage2_bootstrap = None ,
372
478
imports = None ,
373
479
is_for_zip ,
374
- runtime_details ):
480
+ runtime_details ,
481
+ venv = None ):
375
482
runtime = runtime_details .effective_runtime
376
483
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
+
377
494
subs = {
378
495
"%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 ,
380
498
"%target%" : str (ctx .label ),
381
499
"%workspace_name%" : ctx .workspace_name ,
382
500
}
@@ -447,6 +565,7 @@ def _create_windows_exe_launcher(
447
565
)
448
566
449
567
def _create_zip_file (ctx , * , output , original_nonzip_executable , zip_main , runfiles ):
568
+ """Create a Python zipapp (zip with __main__.py entry point)."""
450
569
workspace_name = ctx .workspace_name
451
570
legacy_external_runfiles = _py_builtins .get_legacy_external_runfiles (ctx )
452
571
@@ -524,7 +643,14 @@ def _get_zip_runfiles_path(path, workspace_name, legacy_external_runfiles):
524
643
zip_runfiles_path = paths .normalize ("{}/{}" .format (workspace_name , path ))
525
644
return "{}/{}" .format (_ZIP_RUNFILES_DIRECTORY_NAME , zip_runfiles_path )
526
645
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 ):
528
654
prelude = ctx .actions .declare_file (
529
655
"{}_zip_prelude.sh" .format (output .basename ),
530
656
sibling = output ,
@@ -536,6 +662,7 @@ def _create_executable_zip_file(ctx, *, output, zip_file, stage2_bootstrap, runt
536
662
stage2_bootstrap = stage2_bootstrap ,
537
663
runtime_details = runtime_details ,
538
664
is_for_zip = True ,
665
+ venv = venv ,
539
666
)
540
667
else :
541
668
ctx .actions .write (prelude , "#!/usr/bin/env python3\n " )
0 commit comments