internal(pystar): Copy @bazel_tools//tools/python files to rules_python (#1437)

This copies the useful pieces from @bazel_tools//tools/python into
rules_python. They're copied in relatively as-is, and not yet used.
Subsequent commits will make them usable.

These pieces are:
  * Bootstrap template (python_bootstrap_template.txt)
  * The py_runtime_pair rule (split from toolchain.bzl)
  * Autodetecting toolchain setup (split from toolchain.bzl)

Work towards #1069
diff --git a/python/private/autodetecting_toolchain.bzl b/python/private/autodetecting_toolchain.bzl
new file mode 100644
index 0000000..3f31f1f
--- /dev/null
+++ b/python/private/autodetecting_toolchain.bzl
@@ -0,0 +1,127 @@
+# Copyright 2019 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Definitions related to the Python toolchain."""
+
+load(":utils.bzl", "expand_pyversion_template")
+
+def define_autodetecting_toolchain(
+        name,
+        pywrapper_template,
+        windows_config_setting):
+    """Defines the autodetecting Python toolchain.
+
+    This includes both strict and non-strict variants.
+
+    For use only by @bazel_tools//tools/python:BUILD; see the documentation
+    comment there.
+
+    Args:
+        name: The name of the toolchain to introduce. Must have value
+            "autodetecting_toolchain". This param is present only to make the
+            BUILD file more readable.
+        pywrapper_template: The label of the pywrapper_template.txt file.
+        windows_config_setting: The label of a config_setting that matches when
+            the platform is windows, in which case the toolchain is configured
+            in a way that triggers a workaround for #7844.
+    """
+    if native.package_name() != "tools/python":
+        fail("define_autodetecting_toolchain() is private to " +
+             "@bazel_tools//tools/python")
+    if name != "autodetecting_toolchain":
+        fail("Python autodetecting toolchain must be named " +
+             "'autodetecting_toolchain'")
+
+    expand_pyversion_template(
+        name = "_generate_wrappers",
+        template = pywrapper_template,
+        out2 = ":py2wrapper.sh",
+        out3 = ":py3wrapper.sh",
+        out2_nonstrict = ":py2wrapper_nonstrict.sh",
+        out3_nonstrict = ":py3wrapper_nonstrict.sh",
+        visibility = ["//visibility:private"],
+    )
+
+    # Note that the pywrapper script is a .sh file, not a sh_binary target. If
+    # we needed to make it a proper shell target, e.g. because it needed to
+    # access runfiles and needed to depend on the runfiles library, then we'd
+    # have to use a workaround to allow it to be depended on by py_runtime. See
+    # https://github.com/bazelbuild/bazel/issues/4286#issuecomment-475661317.
+
+    # buildifier: disable=native-py
+    py_runtime(
+        name = "_autodetecting_py3_runtime",
+        interpreter = ":py3wrapper.sh",
+        python_version = "PY3",
+        stub_shebang = "#!/usr/bin/env python3",
+        visibility = ["//visibility:private"],
+    )
+
+    # buildifier: disable=native-py
+    py_runtime(
+        name = "_autodetecting_py3_runtime_nonstrict",
+        interpreter = ":py3wrapper_nonstrict.sh",
+        python_version = "PY3",
+        stub_shebang = "#!/usr/bin/env python3",
+        visibility = ["//visibility:private"],
+    )
+
+    # This is a dummy runtime whose interpreter_path triggers the native rule
+    # logic to use the legacy behavior on Windows.
+    # TODO(#7844): Remove this target.
+    # buildifier: disable=native-py
+    py_runtime(
+        name = "_magic_sentinel_runtime",
+        interpreter_path = "/_magic_pyruntime_sentinel_do_not_use",
+        python_version = "PY3",
+        visibility = ["//visibility:private"],
+    )
+
+    py_runtime_pair(
+        name = "_autodetecting_py_runtime_pair",
+        py3_runtime = select({
+            # If we're on windows, inject the sentinel to tell native rule logic
+            # that we attempted to use the autodetecting toolchain and need to
+            # switch back to legacy behavior.
+            # TODO(#7844): Remove this hack.
+            windows_config_setting: ":_magic_sentinel_runtime",
+            "//conditions:default": ":_autodetecting_py3_runtime",
+        }),
+        visibility = ["//visibility:public"],
+    )
+
+    py_runtime_pair(
+        name = "_autodetecting_py_runtime_pair_nonstrict",
+        py3_runtime = select({
+            # Same hack as above.
+            # TODO(#7844): Remove this hack.
+            windows_config_setting: ":_magic_sentinel_runtime",
+            "//conditions:default": ":_autodetecting_py3_runtime_nonstrict",
+        }),
+        visibility = ["//visibility:public"],
+    )
+
+    native.toolchain(
+        name = name,
+        toolchain = ":_autodetecting_py_runtime_pair",
+        toolchain_type = ":toolchain_type",
+        visibility = ["//visibility:public"],
+    )
+
+    native.toolchain(
+        name = name + "_nonstrict",
+        toolchain = ":_autodetecting_py_runtime_pair_nonstrict",
+        toolchain_type = ":toolchain_type",
+        visibility = ["//visibility:public"],
+    )
diff --git a/python/private/py_runtime_pair.bzl b/python/private/py_runtime_pair.bzl
new file mode 100644
index 0000000..58b5519
--- /dev/null
+++ b/python/private/py_runtime_pair.bzl
@@ -0,0 +1,140 @@
+# Copyright 2019 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Implementation of py_runtime_pair."""
+
+# TODO: move py_runtime_pair into rules_python (and the rest of @bazel_tools//python)
+# py_runtime should be loaded from rules_python, but this creates a circular dep, because py_runtime_pair is imported there.
+py_runtime = native.py_runtime
+
+def _py_runtime_pair_impl(ctx):
+    if ctx.attr.py2_runtime != None:
+        py2_runtime = ctx.attr.py2_runtime[PyRuntimeInfo]
+        if py2_runtime.python_version != "PY2":
+            fail("The Python runtime in the 'py2_runtime' attribute did not have " +
+                 "version 'PY2'")
+    else:
+        py2_runtime = None
+
+    if ctx.attr.py3_runtime != None:
+        py3_runtime = ctx.attr.py3_runtime[PyRuntimeInfo]
+        if py3_runtime.python_version != "PY3":
+            fail("The Python runtime in the 'py3_runtime' attribute did not have " +
+                 "version 'PY3'")
+    else:
+        py3_runtime = None
+
+    # TODO: Uncomment this after --incompatible_python_disable_py2 defaults to true
+    # if _is_py2_disabled(ctx) and py2_runtime != None:
+    #     fail("Using Python 2 is not supported and disabled; see " +
+    #          "https://github.com/bazelbuild/bazel/issues/15684")
+
+    return [platform_common.ToolchainInfo(
+        py2_runtime = py2_runtime,
+        py3_runtime = py3_runtime,
+    )]
+
+# buildifier: disable=unused-variable
+def _is_py2_disabled(ctx):
+    # In Google, this file isn't bundled with Bazel, so we have to conditionally
+    # check for this flag.
+    # TODO: Remove this once a build with the flag is released in Google
+    if not hasattr(ctx.fragments.py, "disable_py"):
+        return False
+    return ctx.fragments.py.disable_py2
+
+py_runtime_pair = rule(
+    implementation = _py_runtime_pair_impl,
+    attrs = {
+        # The two runtimes are used by the py_binary at runtime, and so need to
+        # be built for the target platform.
+        "py2_runtime": attr.label(
+            providers = [PyRuntimeInfo],
+            cfg = "target",
+            doc = """\
+The runtime to use for Python 2 targets. Must have `python_version` set to
+`PY2`.
+""",
+        ),
+        "py3_runtime": attr.label(
+            providers = [PyRuntimeInfo],
+            cfg = "target",
+            doc = """\
+The runtime to use for Python 3 targets. Must have `python_version` set to
+`PY3`.
+""",
+        ),
+    },
+    fragments = ["py"],
+    doc = """\
+A toolchain rule for Python.
+
+This wraps up to two Python runtimes, one for Python 2 and one for Python 3.
+The rule consuming this toolchain will choose which runtime is appropriate.
+Either runtime may be omitted, in which case the resulting toolchain will be
+unusable for building Python code using that version.
+
+Usually the wrapped runtimes are declared using the `py_runtime` rule, but any
+rule returning a `PyRuntimeInfo` provider may be used.
+
+This rule returns a `platform_common.ToolchainInfo` provider with the following
+schema:
+
+```python
+platform_common.ToolchainInfo(
+    py2_runtime = <PyRuntimeInfo or None>,
+    py3_runtime = <PyRuntimeInfo or None>,
+)
+```
+
+Example usage:
+
+```python
+# In your BUILD file...
+
+load("@rules_python//python:defs.bzl", "py_runtime_pair")
+
+py_runtime(
+    name = "my_py2_runtime",
+    interpreter_path = "/system/python2",
+    python_version = "PY2",
+)
+
+py_runtime(
+    name = "my_py3_runtime",
+    interpreter_path = "/system/python3",
+    python_version = "PY3",
+)
+
+py_runtime_pair(
+    name = "my_py_runtime_pair",
+    py2_runtime = ":my_py2_runtime",
+    py3_runtime = ":my_py3_runtime",
+)
+
+toolchain(
+    name = "my_toolchain",
+    target_compatible_with = <...>,
+    toolchain = ":my_py_runtime_pair",
+    toolchain_type = "@rules_python//python:toolchain_type",
+)
+```
+
+```python
+# In your WORKSPACE...
+
+register_toolchains("//my_pkg:my_toolchain")
+```
+""",
+)
diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt
new file mode 100644
index 0000000..92dd6b8
--- /dev/null
+++ b/python/private/python_bootstrap_template.txt
@@ -0,0 +1,559 @@
+%shebang%
+
+# This script must retain compatibility with a wide variety of Python versions
+# since it is run for every py_binary target. Currently we guarantee support
+# going back to Python 2.7, and try to support even Python 2.6 on a best-effort
+# basis. We might abandon 2.6 support once users have the ability to control the
+# above shebang string via the Python toolchain (#8685).
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import sys
+
+# The Python interpreter unconditionally prepends the directory containing this
+# script (following symlinks) to the import path. This is the cause of #9239,
+# and is a special case of #7091. We therefore explicitly delete that entry.
+# TODO(#7091): Remove this hack when no longer necessary.
+del sys.path[0]
+
+import os
+import subprocess
+
+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
+  if platform.win32_ver()[1] >= '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')
+
+PYTHON_BINARY = '%python_binary%'
+if 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."""
+  return FindBinary(module_space, PYTHON_BINARY)
+
+def PrintVerboseCoverage(*args):
+  """Print output if VERBOSE_COVERAGE is non-empty in the environment."""
+  if os.environ.get("VERBOSE_COVERAGE"):
+    print(*args, file=sys.stderr)
+
+def FindCoverageEntryPoint(module_space):
+  cov_tool = '%coverage_tool%'
+  if cov_tool:
+    PrintVerboseCoverage('Using toolchain coverage_tool %r' % cov_tool)
+  else:
+    cov_tool = os.environ.get('PYTHON_COVERAGE')
+    if cov_tool:
+      PrintVerboseCoverage('PYTHON_COVERAGE: %r' % cov_tool)
+  if cov_tool:
+    return FindBinary(module_space, cov_tool)
+  return None
+
+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 CreatePythonPathEntries(python_imports, module_space):
+  parts = python_imports.split(':')
+  return [module_space] + ['%s/%s' % (module_space, path) for path in parts]
+
+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]
+  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')
+
+# Returns repository roots to add to the import path.
+def GetRepositoriesImports(module_space, import_all):
+  if import_all:
+    repo_dirs = [os.path.join(module_space, d) for d in os.listdir(module_space)]
+    repo_dirs.sort()
+    return [d for d in repo_dirs if os.path.isdir(d)]
+  return [os.path.join(module_space, '%workspace_name%')]
+
+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 Deduplicate(items):
+  """Efficiently filter out duplicates, keeping the first element only."""
+  seen = set()
+  for it in items:
+      if it not in seen:
+          seen.add(it)
+          yield it
+
+def InstrumentedFilePaths():
+  """Yields tuples of realpath of each instrumented file with the relative path."""
+  manifest_filename = os.environ.get('COVERAGE_MANIFEST')
+  if not manifest_filename:
+    return
+  with open(manifest_filename, "r") as manifest:
+    for line in manifest:
+      filename = line.strip()
+      if not filename:
+        continue
+      try:
+        realpath = os.path.realpath(filename)
+      except OSError:
+        print(
+          "Could not find instrumented file {}".format(filename),
+          file=sys.stderr)
+        continue
+      if realpath != filename:
+        PrintVerboseCoverage("Fixing up {} -> {}".format(realpath, filename))
+        yield (realpath, filename)
+
+def UnresolveSymlinks(output_filename):
+  # type: (str) -> None
+  """Replace realpath of instrumented files with the relative path in the lcov output.
+
+  Though we are asking coveragepy to use relative file names, currently
+  ignore that for purposes of generating the lcov report (and other reports
+  which are not the XML report), so we need to go and fix up the report.
+
+  This function is a workaround for that issue. Once that issue is fixed
+  upstream and the updated version is widely in use, this should be removed.
+
+  See https://github.com/nedbat/coveragepy/issues/963.
+  """
+  substitutions = list(InstrumentedFilePaths())
+  if substitutions:
+    unfixed_file = output_filename + '.tmp'
+    os.rename(output_filename, unfixed_file)
+    with open(unfixed_file, "r") as unfixed:
+      with open(output_filename, "w") as output_file:
+        for line in unfixed:
+          if line.startswith('SF:'):
+            for (realpath, filename) in substitutions:
+              line = line.replace(realpath, filename)
+          output_file.write(line)
+    os.unlink(unfixed_file)
+
+def ExecuteFile(python_program, main_filename, args, env, module_space,
+                coverage_entrypoint, 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
+    coverage_entrypoint: (str|None) Path to the coverage tool entry point file.
+    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.
+  """
+  # 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.
+  # - For coverage targets, at least coveragepy requires running in
+  #   two invocations, which also requires control to return here.
+  #
+  if not (IsWindows() or workspace or coverage_entrypoint or delete_module_space):
+    _RunExecv(python_program, main_filename, args, env)
+
+  if coverage_entrypoint is not None:
+    ret_code = _RunForCoverage(python_program, main_filename, args, env,
+                               coverage_entrypoint, workspace)
+  else:
+    ret_code = subprocess.call(
+      [python_program, main_filename] + args,
+      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, main_filename, args, env):
+  # type: (str, str, list[str], dict[str, str]) -> ...
+  """Executes the given Python file using the various environment settings."""
+  os.environ.update(env)
+  os.execv(python_program, [python_program, main_filename] + args)
+
+def _RunForCoverage(python_program, main_filename, args, env,
+                    coverage_entrypoint, workspace):
+  # type: (str, str, list[str], dict[str, str], str, str|None) -> int
+  """Collects coverage infomration for the given Python file.
+
+  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
+    coverage_entrypoint: (str|None) Path to the coverage entry point to execute with.
+    workspace: (str|None) Name of the workspace to execute in. This is expected to be a
+        directory under the runfiles tree, and will recursively delete the
+        runfiles directory if set.
+  """
+  # We need for coveragepy to use relative paths.  This can only be configured
+  # via an rc file, so we need to make one.
+  rcfile_name = os.path.join(os.environ['COVERAGE_DIR'], '.coveragerc')
+  with open(rcfile_name, "w") as rcfile:
+    rcfile.write('''[run]
+relative_files = True
+''')
+  PrintVerboseCoverage('Coverage entrypoint:', coverage_entrypoint)
+  # First run the target Python file via coveragepy to create a .coverage
+  # database file, from which we can later export lcov.
+  ret_code = subprocess.call(
+    [
+      python_program,
+      coverage_entrypoint,
+      "run",
+      "--rcfile=" + rcfile_name,
+      "--append",
+      "--branch",
+      main_filename
+    ] + args,
+    env=env,
+    cwd=workspace
+  )
+  output_filename = os.path.join(os.environ['COVERAGE_DIR'], 'pylcov.dat')
+
+  PrintVerboseCoverage('Converting coveragepy database to lcov:', output_filename)
+  # Run coveragepy again to convert its .coverage database file into lcov.
+  ret_code = subprocess.call(
+    [
+      python_program,
+      coverage_entrypoint,
+      "lcov",
+      "--rcfile=" + rcfile_name,
+      "-o",
+      output_filename
+    ],
+    env=env,
+    cwd=workspace
+  ) or ret_code
+  try:
+    os.unlink(rcfile_name)
+  except OSError as err:
+    # It's possible that the profiled program might execute another Python
+    # binary through a wrapper that would then delete the rcfile.  Not much
+    # we can do about that, besides ignore the failure here.
+    PrintVerboseCoverage('Error removing temporary coverage rc file:', err)
+  if os.path.isfile(output_filename):
+    UnresolveSymlinks(output_filename)
+  return ret_code
+
+def Main():
+  args = sys.argv[1:]
+
+  new_env = {}
+
+  # The main Python source file.
+  # The magic string percent-main-percent is replaced with the runfiles-relative
+  # filename of the main file of the Python binary in BazelPythonSemantics.java.
+  main_rel_path = '%main%'
+  if IsWindows():
+    main_rel_path = main_rel_path.replace('/', os.sep)
+
+  if IsRunningFromZip():
+    module_space = CreateModuleSpace()
+    delete_module_space = True
+  else:
+    module_space = FindModuleSpace(main_rel_path)
+    delete_module_space = False
+
+  python_imports = '%imports%'
+  python_path_entries = CreatePythonPathEntries(python_imports, module_space)
+  python_path_entries += GetRepositoriesImports(module_space, %import_all%)
+  # Remove duplicates to avoid overly long PYTHONPATH (#10977). Preserve order,
+  # keep first occurrence only.
+  python_path_entries = [
+    GetWindowsPathWithUNCPrefix(d)
+    for d in python_path_entries
+  ]
+
+  old_python_path = os.environ.get('PYTHONPATH')
+  if old_python_path:
+    python_path_entries += old_python_path.split(os.pathsep)
+
+  python_path = os.pathsep.join(Deduplicate(python_path_entries))
+
+  if IsWindows():
+    python_path = python_path.replace('/', os.sep)
+
+  new_env['PYTHONPATH'] = python_path
+  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: ' + PYTHON_BINARY)
+
+  # COVERAGE_DIR is set if coverage is enabled and instrumentation is configured
+  # for something, though it could be another program executing this one or
+  # one executed by this one (e.g. an extension module).
+  if os.environ.get('COVERAGE_DIR'):
+    cov_tool = FindCoverageEntryPoint(module_space)
+    if cov_tool is None:
+      PrintVerboseCoverage('Coverage was enabled, but python coverage tool was not configured.')
+    else:
+      # Inhibit infinite recursion:
+      if 'PYTHON_COVERAGE' in os.environ:
+        del os.environ['PYTHON_COVERAGE']
+
+      if not os.path.exists(cov_tool):
+        raise EnvironmentError(
+          'Python coverage tool %r not found. '
+          'Try running with VERBOSE_COVERAGE=1 to collect more information.'
+          % cov_tool
+        )
+
+      # coverage library expects sys.path[0] to contain the library, and replaces
+      # it with the directory of the program it starts. Our actual sys.path[0] is
+      # the runfiles directory, which must not be replaced.
+      # CoverageScript.do_execute() undoes this sys.path[0] setting.
+      #
+      # Update sys.path such that python finds the coverage package. The coverage
+      # entry point is coverage.coverage_main, so we need to do twice the dirname.
+      python_path_entries = new_env['PYTHONPATH'].split(os.pathsep)
+      python_path_entries.append(os.path.dirname(os.path.dirname(cov_tool)))
+      new_env['PYTHONPATH'] = os.pathsep.join(Deduplicate(python_path_entries))
+  else:
+    cov_tool = None
+
+  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,
+      cov_tool, 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()