From 9b5ce048bda7dc9c5f5475a8138d4545ab46e48d Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Tue, 13 Oct 2020 21:44:25 +0100 Subject: [PATCH] Support dependencies which depend on external native executables (refs #5657): Doesn't work on API level 29 or higher. --- demo/app/build.gradle | 3 +- .../src/main/python/java/android/importer.py | 23 ++++++++++++- .../test/python/chaquopy/test/test_android.py | 33 +++++++++++++++++-- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/demo/app/build.gradle b/demo/app/build.gradle index c8a84ca3ce..2f6528c98b 100644 --- a/demo/app/build.gradle +++ b/demo/app/build.gradle @@ -52,7 +52,8 @@ android { // Python unit tests pip { - install "murmurhash==0.28.0" // Requires chaquopy-libcxx + install "chaquopy-flac==1.3.3" + install "murmurhash==0.28.0" } staticProxy("chaquopy.test.static_proxy.basic", "chaquopy.test.static_proxy.header", "chaquopy.test.static_proxy.method") diff --git a/product/runtime/src/main/python/java/android/importer.py b/product/runtime/src/main/python/java/android/importer.py index 6d95976b91..a6dada1a9b 100644 --- a/product/runtime/src/main/python/java/android/importer.py +++ b/product/runtime/src/main/python/java/android/importer.py @@ -22,7 +22,7 @@ from zipimport import zipimporter import java.chaquopy -from java._vendor.elftools.elf.elffile import ELFFile +from java._vendor.elftools.elf.elffile import ELFError, ELFFile from java.chaquopy_android import AssetFile from android.os import Build @@ -71,12 +71,33 @@ def hook(path): not any(finder.exists(f"{name}/__init__{suffix}") for suffix in LOADERS): finder.extract_dir(name) + # Extract executables. + if finder.isdir("chaquopy/bin"): + for name in finder.listdir("chaquopy/bin"): + # If chaquopy/ is a Python package, it won't have been extracted above. + filename = finder.extract_if_changed(f"chaquopy/bin/{name}") + os.chmod(filename, 0o755) + try: + finder.extract_needed(filename) + except ELFError: + pass # Maybe it's a shell script or something. + + prepend_path("PATH", f"{finder.path}/chaquopy/bin") + prepend_path("LD_LIBRARY_PATH", f"{finder.path}/chaquopy/lib") + # We do this here instead of in AssetFinder.__init__ because code in the .pth files may # require the finder to be fully available to the system, which isn't the case until # get_importer returns. site.addsitedir(finder.extract_root) +def prepend_path(variable, path): + old_path = os.environ.get(variable) + if old_path: + path += ":" + old_path + os.environ[variable] = path + + def initialize_ctypes(): import ctypes.util import sysconfig diff --git a/product/runtime/src/test/python/chaquopy/test/test_android.py b/product/runtime/src/test/python/chaquopy/test/test_android.py index a73189affe..887224193b 100644 --- a/product/runtime/src/test/python/chaquopy/test/test_android.py +++ b/product/runtime/src/test/python/chaquopy/test/test_android.py @@ -24,7 +24,8 @@ # Flags from PEP 3149. ABI_FLAGS = "" -REQUIREMENTS = ["chaquopy-libcxx", "murmurhash", "Pygments"] +REQUIREMENTS = ["chaquopy-libcxx", "chaquopy-flac", "chaquopy-libogg", + "murmurhash", "Pygments"] try: from android.os import Build @@ -164,6 +165,24 @@ def test_so(self): # Library extraction caused by importing a Python module linked against it. self.check_extract_if_changed(mod, LIBCXX_FILENAME) + def test_bin(self): + chaquopy_dir = asset_path(REQS_ABI_ZIP, "chaquopy") + self.assertCountEqual(["bin", "lib"], os.listdir(chaquopy_dir)) + + # Execution of flac is covered by test_subprocess. + self.assertCountEqual(["flac", "metaflac"], os.listdir(f"{chaquopy_dir}/bin")) + + # Library extraction caused by executables in chaquopy/bin. Notes: + # * libFLAC and libogg are needed by the flac executable. + # * libFLAC++ isn't needed by any executable, so it shouldn't exist at this point. + # However, its presence in the APK is checked by test_ctypes. + # * libc++_shared may exist if the tests have been run before. + actual = os.listdir(f"{chaquopy_dir}/lib") + expected = ["libFLAC.so", "libogg.so"] + if len(actual) > len(expected): + expected.append("libc++_shared.so") + self.assertCountEqual(expected, actual) + def test_non_package_data(self): for dir_name, dir_description in [("", "root"), ("non_package_data", "directory"), ("non_package_data/subdir", "subdirectory")]: @@ -774,11 +793,14 @@ def test_ctypes(self): # Library extraction caused by find_library. os.remove(LIBCXX_FILENAME) - find_library_result = find_library("c++_shared") + find_library_result = find_library("FLAC++") self.assertIsInstance(find_library_result, str) if platform.architecture()[0] == "32bit" or Build.VERSION.SDK_INT >= 23: self.assertRegex(find_library_result, r"^/") + libflacxx_filename = asset_path(REQS_ABI_ZIP, "chaquopy/lib/libFLAC++.so") + self.assertPredicate(exists, libflacxx_filename) self.assertPredicate(exists, LIBCXX_FILENAME) + os.remove(libflacxx_filename) # Clean up for test_bin. # Work around double-underscore mangling of __android_log_write. def assertHasSymbol(dll, name): @@ -915,9 +937,14 @@ def test_ssl(self): self.assertRegex(resp.info()["Content-type"], r"^text/html") def test_subprocess(self): - # An executable on the PATH. + # A system executable. subprocess.run(["ls", os.environ["HOME"]]) + # An executable in the requirements. + self.assertEqual("flac 1.3.3\n", + subprocess.run(["flac", "--version"], capture_output=True, text=True) + .stdout) + # A nonexistent executable. for name in ["nonexistent", # PATH search "/system/bin/nonexistent"]: # Absolute filename