Add Shared libraries support for Android python libraries

- New shared_libs property to Python code for specify shared libs that should be packed into the library for modules with compiled code.
- Expand shared_lib dependencies when building the python library.
- Change python main entrypoint to extract the zip if there are any shared libraries in the executable to workaround linker limitations

Bug: 395678202
Change-Id: Id94caebf36f7eb1cfa492b2fa78f8c56623e9a43
Test: m py-grpcio
diff --git a/python/scripts/main.py b/python/scripts/main.py
index 225dbe4..35cdfc4 100644
--- a/python/scripts/main.py
+++ b/python/scripts/main.py
@@ -1,5 +1,13 @@
+
+import os
 import runpy
+import shutil
 import sys
+import tempfile
+import zipfile
+
+from pathlib import PurePath
+
 
 sys.argv[0] = __loader__.archive
 
@@ -9,4 +17,32 @@
 # when people try to use it.
 sys.executable = None
 
-runpy._run_module_as_main("ENTRY_POINT", alter_argv=False)
+# Extract the shared libraries from the zip file into a temporary directory.
+# This works around the limitations of dynamic linker.  Some Python libraries
+# reference the .so files relatively and so extracting only the .so files
+# does not work, so we extract the entire parent directory of the .so files to a
+# tempdir and then add that to sys.path.
+tempdir = None
+with zipfile.ZipFile(__loader__.archive) as z:
+  # any root so files or root directories that contain so files will be
+  # extracted to the tempdir so the linker load them, this minimizes the
+  # number of files that need to be extracted to a tempdir
+  extract_paths = {}
+  for member in z.infolist():
+    if member.filename.endswith('.so'):
+      extract_paths[PurePath(member.filename).parts[0]] = member.filename
+  if extract_paths:
+    tempdir = tempfile.mkdtemp()
+    for member in z.infolist():
+      if not PurePath(member.filename).parts[0] in extract_paths.keys():
+        continue
+      if member.is_dir():
+        os.makedirs(os.path.join(tempdir, member.filename))
+      else:
+        z.extract(member, tempdir)
+    sys.path.insert(0, tempdir)
+try:
+  runpy._run_module_as_main("ENTRY_POINT", alter_argv=False)
+finally:
+  if tempdir is not None:
+    shutil.rmtree(tempdir)