2626# We just put them in one place so its easy to tell which are used.
2727
2828# Runfiles-relative path to the main Python source file.
29- MAIN = "%main%"
29+ # Empty if MAIN_MODULE is used
30+ MAIN_PATH = "%main%"
31+
32+ # Module name to execute. Empty if MAIN is used.
33+ MAIN_MODULE = "%main_module%"
3034
3135# ===== Template substitutions end =====
3236
@@ -249,7 +253,7 @@ def unresolve_symlinks(output_filename):
249253 os .unlink (unfixed_file )
250254
251255
252- def _run_py (main_filename , * , args , cwd = None ):
256+ def _run_py_path (main_filename , * , args , cwd = None ):
253257 # type: (str, str, list[str], dict[str, str]) -> ...
254258 """Executes the given Python file using the various environment settings."""
255259
@@ -269,6 +273,11 @@ def _run_py(main_filename, *, args, cwd=None):
269273 sys .argv = orig_argv
270274
271275
276+ def _run_py_module (module_name ):
277+ # Match `python -m` behavior, so modify sys.argv and the run name
278+ runpy .run_module (module_name , alter_sys = True , run_name = "__main__" )
279+
280+
272281@contextlib .contextmanager
273282def _maybe_collect_coverage (enable ):
274283 print_verbose_coverage ("enabled:" , enable )
@@ -356,64 +365,79 @@ def main():
356365 print_verbose ("initial environ:" , mapping = os .environ )
357366 print_verbose ("initial sys.path:" , values = sys .path )
358367
359- main_rel_path = MAIN
360- if is_windows ():
361- main_rel_path = main_rel_path .replace ("/" , os .sep )
362-
363- module_space = find_runfiles_root (main_rel_path )
364- print_verbose ("runfiles root:" , module_space )
365-
366- # Recreate the "add main's dir to sys.path[0]" behavior to match the
367- # system-python bootstrap / typical Python behavior.
368- #
369- # Without safe path enabled, when `python foo/bar.py` is run, python will
370- # resolve the foo/bar.py symlink to its real path, then add the directory
371- # of that path to sys.path. But, the resolved directory for the symlink
372- # depends on if the file is generated or not.
373- #
374- # When foo/bar.py is a source file, then it's a symlink pointing
375- # back to the client source directory. This means anything from that source
376- # directory becomes importable, i.e. most code is importable.
377- #
378- # When foo/bar.py is a generated file, then it's a symlink pointing to
379- # somewhere under bazel-out/.../bin, i.e. where generated files are. This
380- # means only other generated files are importable (not source files).
381- #
382- # To replicate this behavior, we add main's directory within the runfiles
383- # when safe path isn't enabled.
384- if not getattr (sys .flags , "safe_path" , False ):
385- prepend_path_entries = [
386- os .path .join (module_space , os .path .dirname (main_rel_path ))
387- ]
368+ main_rel_path = None
369+ # todo: things happen to work because find_runfiles_root
370+ # ends up using stage2_bootstrap, and ends up computing the proper
371+ # runfiles root
372+ if MAIN_PATH :
373+ main_rel_path = MAIN_PATH
374+ if is_windows ():
375+ main_rel_path = main_rel_path .replace ("/" , os .sep )
376+
377+ runfiles_root = find_runfiles_root (main_rel_path )
388378 else :
389- prepend_path_entries = []
379+ runfiles_root = find_runfiles_root ("" )
380+
381+ print_verbose ("runfiles root:" , runfiles_root )
390382
391- runfiles_envkey , runfiles_envvalue = runfiles_envvar (module_space )
383+ runfiles_envkey , runfiles_envvalue = runfiles_envvar (runfiles_root )
392384 if runfiles_envkey :
393385 os .environ [runfiles_envkey ] = runfiles_envvalue
394386
395- main_filename = os .path .join (module_space , main_rel_path )
396- main_filename = get_windows_path_with_unc_prefix (main_filename )
397- assert os .path .exists (main_filename ), (
398- "Cannot exec() %r: file not found." % main_filename
399- )
400- assert os .access (main_filename , os .R_OK ), (
401- "Cannot exec() %r: file not readable." % main_filename
402- )
387+ if MAIN_PATH :
388+ # Recreate the "add main's dir to sys.path[0]" behavior to match the
389+ # system-python bootstrap / typical Python behavior.
390+ #
391+ # Without safe path enabled, when `python foo/bar.py` is run, python will
392+ # resolve the foo/bar.py symlink to its real path, then add the directory
393+ # of that path to sys.path. But, the resolved directory for the symlink
394+ # depends on if the file is generated or not.
395+ #
396+ # When foo/bar.py is a source file, then it's a symlink pointing
397+ # back to the client source directory. This means anything from that source
398+ # directory becomes importable, i.e. most code is importable.
399+ #
400+ # When foo/bar.py is a generated file, then it's a symlink pointing to
401+ # somewhere under bazel-out/.../bin, i.e. where generated files are. This
402+ # means only other generated files are importable (not source files).
403+ #
404+ # To replicate this behavior, we add main's directory within the runfiles
405+ # when safe path isn't enabled.
406+ if not getattr (sys .flags , "safe_path" , False ):
407+ prepend_path_entries = [
408+ os .path .join (runfiles_root , os .path .dirname (main_rel_path ))
409+ ]
410+ else :
411+ prepend_path_entries = []
412+
413+ main_filename = os .path .join (runfiles_root , main_rel_path )
414+ main_filename = get_windows_path_with_unc_prefix (main_filename )
415+ assert os .path .exists (main_filename ), (
416+ "Cannot exec() %r: file not found." % main_filename
417+ )
418+ assert os .access (main_filename , os .R_OK ), (
419+ "Cannot exec() %r: file not readable." % main_filename
420+ )
403421
404- sys .stdout .flush ()
422+ sys .stdout .flush ()
405423
406- sys .path [0 :0 ] = prepend_path_entries
424+ sys .path [0 :0 ] = prepend_path_entries
425+ else :
426+ main_filename = None
407427
408428 if os .environ .get ("COVERAGE_DIR" ):
409429 import _bazel_site_init
430+
410431 coverage_enabled = _bazel_site_init .COVERAGE_SETUP
411432 else :
412433 coverage_enabled = False
413434
414435 with _maybe_collect_coverage (enable = coverage_enabled ):
415- # The first arg is this bootstrap, so drop that for the re-invocation.
416- _run_py (main_filename , args = sys .argv [1 :])
436+ if MAIN_PATH :
437+ # The first arg is this bootstrap, so drop that for the re-invocation.
438+ _run_py_path (main_filename , args = sys .argv [1 :])
439+ else :
440+ _run_py_module (MAIN_MODULE )
417441 sys .exit (0 )
418442
419443
0 commit comments