fix(local_runtime): Search for libs in sys._base_executable when available. (#3178)

Search directory for libraries should look in the same directory as
sys._base_executable. Since sys._base_executable may be unset, fallback
to sys.executable

Found this when trying to build using a venv for
[tensorstore](https://github.com/google/tensorstore) on Windows:
* Github CI uses nuget to download Python.
* Build sets up a Python venv.

The venv does not include all the lib directories required to link an
extension.

Fixes https://github.com/bazel-contrib/rules_python/issues/3172

---------

Co-authored-by: Richard Levasseur <richardlev@gmail.com>
diff --git a/python/private/get_local_runtime_info.py b/python/private/get_local_runtime_info.py
index ff3b0ae..d176b1a 100644
--- a/python/private/get_local_runtime_info.py
+++ b/python/private/get_local_runtime_info.py
@@ -23,7 +23,7 @@
 _IS_DARWIN = sys.platform == "darwin"
 
 
-def _search_directories(get_config):
+def _search_directories(get_config, base_executable):
     """Returns a list of library directories to search for shared libraries."""
     # There's several types of libraries with different names and a plethora
     # of settings, and many different config variables to check:
@@ -53,19 +53,23 @@
             if config_value and not config_value.endswith(multiarch):
                 lib_dirs.append(os.path.join(config_value, multiarch))
 
-    if _IS_WINDOWS:
-        # On Windows DLLs go in the same directory as the executable, while .lib
-        # files live in the lib/ or libs/ subdirectory.
-        lib_dirs.append(get_config("BINDIR"))
-        lib_dirs.append(os.path.join(os.path.dirname(sys.executable)))
-        lib_dirs.append(os.path.join(os.path.dirname(sys.executable), "lib"))
-        lib_dirs.append(os.path.join(os.path.dirname(sys.executable), "libs"))
-    elif not _IS_DARWIN:
-        # On most systems the executable is in a bin/ directory and the libraries
-        # are in a sibling lib/ directory.
-        lib_dirs.append(
-            os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "lib")
-        )
+    if not _IS_DARWIN:
+        for exec_dir in (
+            os.path.dirname(base_executable) if base_executable else None,
+            get_config("BINDIR"),
+        ):
+            if not exec_dir:
+                continue
+            if _IS_WINDOWS:
+                # On Windows DLLs go in the same directory as the executable, while .lib
+                # files live in the lib/ or libs/ subdirectory.
+                lib_dirs.append(exec_dir)
+                lib_dirs.append(os.path.join(exec_dir, "lib"))
+                lib_dirs.append(os.path.join(exec_dir, "libs"))
+            else:
+                # On most systems the executable is in a bin/ directory and the libraries
+                # are in a sibling lib/ directory.
+                lib_dirs.append(os.path.join(os.path.dirname(exec_dir), "lib"))
 
     # Dedup and remove empty values, keeping the order.
     lib_dirs = [v for v in lib_dirs if v]
@@ -126,7 +130,7 @@
     return {k: None for k in lib_names}.keys()
 
 
-def _get_python_library_info():
+def _get_python_library_info(base_executable):
     """Returns a dictionary with the static and dynamic python libraries."""
     config_vars = sysconfig.get_config_vars()
 
@@ -140,7 +144,7 @@
                 f"{sys.version_info.major}.{sys.version_info.minor}"
             )
 
-    search_directories = _search_directories(config_vars.get)
+    search_directories = _search_directories(config_vars.get, base_executable)
     search_libnames = _search_library_names(config_vars.get)
 
     def _add_if_exists(target, path):
@@ -187,13 +191,28 @@
     }
 
 
+def _get_base_executable():
+    """Returns the base executable path."""
+    try:
+        if sys._base_executable:  # pylint: disable=protected-access
+            return sys._base_executable  # pylint: disable=protected-access
+    except AttributeError:
+        # Bug reports indicate sys._base_executable  doesn't exist in some cases,
+        # but it's not clear why.
+        # See https://github.com/bazel-contrib/rules_python/issues/3172
+        pass
+    # The normal sys.executable is the next-best guess if sys._base_executable
+    # is missing.
+    return sys.executable
+
+
 data = {
     "major": sys.version_info.major,
     "minor": sys.version_info.minor,
     "micro": sys.version_info.micro,
     "include": sysconfig.get_path("include"),
     "implementation_name": sys.implementation.name,
-    "base_executable": sys._base_executable,
+    "base_executable": _get_base_executable(),
 }
-data.update(_get_python_library_info())
+data.update(_get_python_library_info(_get_base_executable()))
 print(json.dumps(data))