| %shebang% |
| # vim: syntax=python |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import sys |
| |
| import os |
| import subprocess |
| import uuid |
| |
| # runfiles-relative path |
| STAGE2_BOOTSTRAP="%stage2_bootstrap%" |
| |
| # runfiles-relative path to venv's python interpreter |
| # Empty string if a venv is not setup. |
| PYTHON_BINARY = '%python_binary%' |
| |
| # The path to the actual interpreter that is used. |
| # Typically PYTHON_BINARY is a symlink pointing to this. |
| # runfiles-relative path, absolute path, or single word. |
| # Used to create a venv at runtime, or when a venv isn't setup. |
| PYTHON_BINARY_ACTUAL = "%python_binary_actual%" |
| |
| # 0 or 1. |
| # 1 if this bootstrap was created for placement within a zipfile. 0 otherwise. |
| IS_ZIPFILE = "%is_zipfile%" == "1" |
| # 0 or 1. |
| # If 1, then a venv will be created at runtime that replicates what would have |
| # been the build-time structure. |
| RECREATE_VENV_AT_RUNTIME="%recreate_venv_at_runtime%" |
| |
| WORKSPACE_NAME = "%workspace_name%" |
| |
| # Target-specific interpreter args. |
| INTERPRETER_ARGS = [ |
| %interpreter_args% |
| ] |
| |
| ADDITIONAL_INTERPRETER_ARGS = os.environ.get("RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS", "") |
| |
| def IsRunningFromZip(): |
| return IS_ZIPFILE |
| |
| if IsRunningFromZip(): |
| import shutil |
| import tempfile |
| import zipfile |
| else: |
| import re |
| |
| # Return True if running on Windows |
| def IsWindows(): |
| return os.name == 'nt' |
| |
| def GetWindowsPathWithUNCPrefix(path): |
| """Adds UNC prefix after getting a normalized absolute Windows path. |
| |
| No-op for non-Windows platforms or if running under python2. |
| """ |
| path = path.strip() |
| |
| # No need to add prefix for non-Windows platforms. |
| # And \\?\ doesn't work in python 2 or on mingw |
| if not IsWindows() or sys.version_info[0] < 3: |
| return path |
| |
| # Starting in Windows 10, version 1607(OS build 14393), MAX_PATH limitations have been |
| # removed from common Win32 file and directory functions. |
| # Related doc: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later |
| import platform |
| win32_version = None |
| # Windows 2022 with Python 3.12.8 gives flakey errors, so try a couple times. |
| for _ in range(3): |
| try: |
| win32_version = platform.win32_ver()[1] |
| break |
| except (ValueError, KeyError): |
| pass |
| if win32_version and win32_version >= '10.0.14393': |
| return path |
| |
| # import sysconfig only now to maintain python 2.6 compatibility |
| import sysconfig |
| if sysconfig.get_platform() == 'mingw': |
| return path |
| |
| # Lets start the unicode fun |
| unicode_prefix = '\\\\?\\' |
| if path.startswith(unicode_prefix): |
| return path |
| |
| # os.path.abspath returns a normalized absolute path |
| return unicode_prefix + os.path.abspath(path) |
| |
| def HasWindowsExecutableExtension(path): |
| return path.endswith('.exe') or path.endswith('.com') or path.endswith('.bat') |
| |
| if PYTHON_BINARY and IsWindows() and not HasWindowsExecutableExtension(PYTHON_BINARY): |
| PYTHON_BINARY = PYTHON_BINARY + '.exe' |
| |
| def SearchPath(name): |
| """Finds a file in a given search path.""" |
| search_path = os.getenv('PATH', os.defpath).split(os.pathsep) |
| for directory in search_path: |
| if directory: |
| path = os.path.join(directory, name) |
| if os.path.isfile(path) and os.access(path, os.X_OK): |
| return path |
| return None |
| |
| def FindPythonBinary(module_space): |
| """Finds the real Python binary if it's not a normal absolute path.""" |
| if PYTHON_BINARY: |
| return FindBinary(module_space, PYTHON_BINARY) |
| else: |
| return FindBinary(module_space, PYTHON_BINARY_ACTUAL) |
| |
| |
| def print_verbose(*args, mapping=None, values=None): |
| if os.environ.get("RULES_PYTHON_BOOTSTRAP_VERBOSE"): |
| if mapping is not None: |
| for key, value in sorted((mapping or {}).items()): |
| print( |
| "bootstrap: stage 1: ", |
| *(list(args) + ["{}={}".format(key, repr(value))]), |
| file=sys.stderr, |
| flush=True |
| ) |
| elif values is not None: |
| for i, v in enumerate(values): |
| print( |
| "bootstrap: stage 1:", |
| *(list(args) + ["[{}] {}".format(i, repr(v))]), |
| file=sys.stderr, |
| flush=True |
| ) |
| else: |
| print("bootstrap: stage 1:", *args, file=sys.stderr, flush=True) |
| |
| def FindBinary(module_space, bin_name): |
| """Finds the real binary if it's not a normal absolute path.""" |
| if not bin_name: |
| return None |
| if bin_name.startswith("//"): |
| # Case 1: Path is a label. Not supported yet. |
| raise AssertionError( |
| "Bazel does not support execution of Python interpreters via labels yet" |
| ) |
| elif os.path.isabs(bin_name): |
| # Case 2: Absolute path. |
| return bin_name |
| # Use normpath() to convert slashes to os.sep on Windows. |
| elif os.sep in os.path.normpath(bin_name): |
| # Case 3: Path is relative to the repo root. |
| return os.path.join(module_space, bin_name) |
| else: |
| # Case 4: Path has to be looked up in the search path. |
| return SearchPath(bin_name) |
| |
| def FindModuleSpace(main_rel_path): |
| """Finds the runfiles tree.""" |
| # When the calling process used the runfiles manifest to resolve the |
| # location of this stub script, the path may be expanded. This means |
| # argv[0] may no longer point to a location inside the runfiles |
| # directory. We should therefore respect RUNFILES_DIR and |
| # RUNFILES_MANIFEST_FILE set by the caller. |
| runfiles_dir = os.environ.get('RUNFILES_DIR', None) |
| if not runfiles_dir: |
| runfiles_manifest_file = os.environ.get('RUNFILES_MANIFEST_FILE', '') |
| if (runfiles_manifest_file.endswith('.runfiles_manifest') or |
| runfiles_manifest_file.endswith('.runfiles/MANIFEST')): |
| runfiles_dir = runfiles_manifest_file[:-9] |
| # Be defensive: the runfiles dir should contain our main entry point. If |
| # it doesn't, then it must not be our runfiles directory. |
| if runfiles_dir and os.path.exists(os.path.join(runfiles_dir, main_rel_path)): |
| return runfiles_dir |
| |
| stub_filename = sys.argv[0] |
| # On Windows, the path may contain both forward and backslashes. |
| # Normalize to the OS separator because the regex used later assumes |
| # the OS-specific separator. |
| if IsWindows: |
| stub_filename = stub_filename.replace("/", os.sep) |
| |
| if not os.path.isabs(stub_filename): |
| stub_filename = os.path.join(os.getcwd(), stub_filename) |
| |
| while True: |
| module_space = stub_filename + ('.exe' if IsWindows() else '') + '.runfiles' |
| if os.path.isdir(module_space): |
| return module_space |
| |
| runfiles_pattern = r'(.*\.runfiles)' + (r'\\' if IsWindows() else '/') + '.*' |
| matchobj = re.match(runfiles_pattern, stub_filename) |
| if matchobj: |
| return matchobj.group(1) |
| |
| if not os.path.islink(stub_filename): |
| break |
| target = os.readlink(stub_filename) |
| if os.path.isabs(target): |
| stub_filename = target |
| else: |
| stub_filename = os.path.join(os.path.dirname(stub_filename), target) |
| |
| raise AssertionError('Cannot find .runfiles directory for %s' % sys.argv[0]) |
| |
| def ExtractZip(zip_path, dest_dir): |
| """Extracts the contents of a zip file, preserving the unix file mode bits. |
| |
| These include the permission bits, and in particular, the executable bit. |
| |
| Ideally the zipfile module should set these bits, but it doesn't. See: |
| https://bugs.python.org/issue15795. |
| |
| Args: |
| zip_path: The path to the zip file to extract |
| dest_dir: The path to the destination directory |
| """ |
| zip_path = GetWindowsPathWithUNCPrefix(zip_path) |
| dest_dir = GetWindowsPathWithUNCPrefix(dest_dir) |
| with zipfile.ZipFile(zip_path) as zf: |
| for info in zf.infolist(): |
| zf.extract(info, dest_dir) |
| # UNC-prefixed paths must be absolute/normalized. See |
| # https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file#maximum-path-length-limitation |
| file_path = os.path.abspath(os.path.join(dest_dir, info.filename)) |
| # The Unix st_mode bits (see "man 7 inode") are stored in the upper 16 |
| # bits of external_attr. Of those, we set the lower 12 bits, which are the |
| # file mode bits (since the file type bits can't be set by chmod anyway). |
| attrs = info.external_attr >> 16 |
| if attrs != 0: # Rumor has it these can be 0 for zips created on Windows. |
| os.chmod(file_path, attrs & 0o7777) |
| |
| # Create the runfiles tree by extracting the zip file |
| def CreateModuleSpace(): |
| temp_dir = tempfile.mkdtemp('', 'Bazel.runfiles_') |
| ExtractZip(os.path.dirname(__file__), temp_dir) |
| # IMPORTANT: Later code does `rm -fr` on dirname(module_space) -- it's |
| # important that deletion code be in sync with this directory structure |
| return os.path.join(temp_dir, 'runfiles') |
| |
| def RunfilesEnvvar(module_space): |
| """Finds the runfiles manifest or the runfiles directory. |
| |
| Returns: |
| A tuple of (var_name, var_value) where var_name is either 'RUNFILES_DIR' or |
| 'RUNFILES_MANIFEST_FILE' and var_value is the path to that directory or |
| file, or (None, None) if runfiles couldn't be found. |
| """ |
| # If this binary is the data-dependency of another one, the other sets |
| # RUNFILES_MANIFEST_FILE or RUNFILES_DIR for our sake. |
| runfiles = os.environ.get('RUNFILES_MANIFEST_FILE', None) |
| if runfiles: |
| return ('RUNFILES_MANIFEST_FILE', runfiles) |
| |
| runfiles = os.environ.get('RUNFILES_DIR', None) |
| if runfiles: |
| return ('RUNFILES_DIR', runfiles) |
| |
| # If running from a zip, there's no manifest file. |
| if IsRunningFromZip(): |
| return ('RUNFILES_DIR', module_space) |
| |
| # Look for the runfiles "output" manifest, argv[0] + ".runfiles_manifest" |
| runfiles = module_space + '_manifest' |
| if os.path.exists(runfiles): |
| return ('RUNFILES_MANIFEST_FILE', runfiles) |
| |
| # Look for the runfiles "input" manifest, argv[0] + ".runfiles/MANIFEST" |
| # Normally .runfiles_manifest and MANIFEST are both present, but the |
| # former will be missing for zip-based builds or if someone copies the |
| # runfiles tree elsewhere. |
| runfiles = os.path.join(module_space, 'MANIFEST') |
| if os.path.exists(runfiles): |
| return ('RUNFILES_MANIFEST_FILE', runfiles) |
| |
| # If running in a sandbox and no environment variables are set, then |
| # Look for the runfiles next to the binary. |
| if module_space.endswith('.runfiles') and os.path.isdir(module_space): |
| return ('RUNFILES_DIR', module_space) |
| |
| return (None, None) |
| |
| def ExecuteFile(python_program, main_filename, args, env, module_space, |
| workspace, delete_module_space): |
| # type: (str, str, list[str], dict[str, str], str, str|None, str|None) -> ... |
| """Executes the given Python file using the various environment settings. |
| |
| This will not return, and acts much like os.execv, except is much |
| more restricted, and handles Bazel-related edge cases. |
| |
| Args: |
| python_program: (str) Path to the Python binary to use for execution |
| main_filename: (str) The Python file to execute |
| args: (list[str]) Additional args to pass to the Python file |
| env: (dict[str, str]) A dict of environment variables to set for the execution |
| module_space: (str) Path to the module space/runfiles tree directory |
| workspace: (str|None) Name of the workspace to execute in. This is expected to be a |
| directory under the runfiles tree. |
| delete_module_space: (bool), True if the module space should be deleted |
| after a successful (exit code zero) program run, False if not. |
| """ |
| argv = [python_program] |
| argv.extend(INTERPRETER_ARGS) |
| additional_interpreter_args = os.environ.pop("RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS", "") |
| if additional_interpreter_args: |
| import shlex |
| argv.extend(shlex.split(additional_interpreter_args)) |
| argv.append(main_filename) |
| argv.extend(args) |
| # We want to use os.execv instead of subprocess.call, which causes |
| # problems with signal passing (making it difficult to kill |
| # Bazel). However, these conditions force us to run via |
| # subprocess.call instead: |
| # |
| # - On Windows, os.execv doesn't handle arguments with spaces |
| # correctly, and it actually starts a subprocess just like |
| # subprocess.call. |
| # - When running in a workspace or zip file, we need to clean up the |
| # workspace after the process finishes so control must return here. |
| # - If we may need to emit a host config warning after execution, we |
| # can't execv because we need control to return here. This only |
| # happens for targets built in the host config. |
| # |
| if not (IsWindows() or workspace or delete_module_space): |
| _RunExecv(python_program, argv, env) |
| |
| ret_code = subprocess.call( |
| argv, |
| env=env, |
| cwd=workspace |
| ) |
| |
| if delete_module_space: |
| # NOTE: dirname() is called because CreateModuleSpace() creates a |
| # sub-directory within a temporary directory, and we want to remove the |
| # whole temporary directory. |
| shutil.rmtree(os.path.dirname(module_space), True) |
| sys.exit(ret_code) |
| |
| def _RunExecv(python_program, argv, env): |
| # type: (str, list[str], dict[str, str]) -> ... |
| """Executes the given Python file using the various environment settings.""" |
| os.environ.update(env) |
| print_verbose("RunExecv: environ:", mapping=os.environ) |
| print_verbose("RunExecv: python:", python_program) |
| print_verbose("RunExecv: argv:", values=argv) |
| os.execv(python_program, argv) |
| |
| def Main(): |
| print_verbose("initial argv:", values=sys.argv) |
| print_verbose("initial cwd:", os.getcwd()) |
| print_verbose("initial environ:", mapping=os.environ) |
| print_verbose("initial sys.path:", values=sys.path) |
| args = sys.argv[1:] |
| |
| new_env = {} |
| |
| # NOTE: We call normpath for two reasons: |
| # 1. Transform Bazel `foo/bar` to Windows `foo\bar` |
| # 2. Transform `_main/../foo/main.py` to simply `foo/main.py`, which |
| # matters if `_main` doesn't exist (which can occur if a binary |
| # is packaged and needs no artifacts from the main repo) |
| main_rel_path = os.path.normpath(STAGE2_BOOTSTRAP) |
| |
| if IsRunningFromZip(): |
| module_space = CreateModuleSpace() |
| delete_module_space = True |
| else: |
| module_space = FindModuleSpace(main_rel_path) |
| delete_module_space = False |
| |
| if os.environ.get("RULES_PYTHON_TESTING_TELL_MODULE_SPACE"): |
| new_env["RULES_PYTHON_TESTING_MODULE_SPACE"] = module_space |
| |
| runfiles_envkey, runfiles_envvalue = RunfilesEnvvar(module_space) |
| if runfiles_envkey: |
| new_env[runfiles_envkey] = runfiles_envvalue |
| |
| # Don't prepend a potentially unsafe path to sys.path |
| # See: https://docs.python.org/3.11/using/cmdline.html#envvar-PYTHONSAFEPATH |
| new_env['PYTHONSAFEPATH'] = '1' |
| |
| main_filename = os.path.join(module_space, main_rel_path) |
| main_filename = GetWindowsPathWithUNCPrefix(main_filename) |
| assert os.path.exists(main_filename), \ |
| 'Cannot exec() %r: file not found.' % main_filename |
| assert os.access(main_filename, os.R_OK), \ |
| 'Cannot exec() %r: file not readable.' % main_filename |
| |
| program = python_program = FindPythonBinary(module_space) |
| if python_program is None: |
| raise AssertionError('Could not find python binary: ' + repr(PYTHON_BINARY)) |
| |
| # Some older Python versions on macOS (namely Python 3.7) may unintentionally |
| # leave this environment variable set after starting the interpreter, which |
| # causes problems with Python subprocesses correctly locating sys.executable, |
| # which subsequently causes failure to launch on Python 3.11 and later. |
| if '__PYVENV_LAUNCHER__' in os.environ: |
| del os.environ['__PYVENV_LAUNCHER__'] |
| |
| new_env.update((key, val) for key, val in os.environ.items() if key not in new_env) |
| |
| workspace = None |
| if IsRunningFromZip(): |
| # If RUN_UNDER_RUNFILES equals 1, it means we need to |
| # change directory to the right runfiles directory. |
| # (So that the data files are accessible) |
| if os.environ.get('RUN_UNDER_RUNFILES') == '1': |
| workspace = os.path.join(module_space, WORKSPACE_NAME) |
| |
| try: |
| sys.stdout.flush() |
| # NOTE: ExecuteFile may call execve() and lines after this will never run. |
| ExecuteFile( |
| python_program, main_filename, args, new_env, module_space, |
| workspace, |
| delete_module_space = delete_module_space, |
| ) |
| |
| except EnvironmentError: |
| # This works from Python 2.4 all the way to 3.x. |
| e = sys.exc_info()[1] |
| # This exception occurs when os.execv() fails for some reason. |
| if not getattr(e, 'filename', None): |
| e.filename = program # Add info to error message |
| raise |
| |
| if __name__ == '__main__': |
| Main() |