From 6eb223c7ba667b4dbf1d5a7139c7e283eefdf856 Mon Sep 17 00:00:00 2001 From: Jonas Thiem Date: Mon, 25 Mar 2019 15:20:28 +0100 Subject: [PATCH] Properly search native lib dir in ctypes, fixes #1770 --- .../src/android/_ctypes_library_finder.py | 63 +++++++++ .../fix-ctypes-util-find-library.patch | 14 +- tests/test_androidmodule_ctypes_finder.py | 124 ++++++++++++++++++ 3 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 pythonforandroid/recipes/android/src/android/_ctypes_library_finder.py create mode 100644 tests/test_androidmodule_ctypes_finder.py diff --git a/pythonforandroid/recipes/android/src/android/_ctypes_library_finder.py b/pythonforandroid/recipes/android/src/android/_ctypes_library_finder.py new file mode 100644 index 0000000000..8783c617f7 --- /dev/null +++ b/pythonforandroid/recipes/android/src/android/_ctypes_library_finder.py @@ -0,0 +1,63 @@ + +import os + + +def get_activity_lib_dir(activity_name): + from jnius import autoclass + + # Get the actual activity instance: + activity_class = autoclass(activity_name) + if activity_class is None: + return None + activity = None + if hasattr(activity_class, "mActivity") and \ + activity_class.mActivity is not None: + activity = activity_class.mActivity + elif hasattr(activity_class, "mService") and \ + activity_class.mService is not None: + activity = activity_class.mService + if activity is None: + return None + + # Extract the native lib dir from the activity instance: + package_name = activity.getApplicationContext().getPackageName() + manager = activity.getApplicationContext().getPackageManager() + manager_class = autoclass("android.content.pm.PackageManager") + native_lib_dir = manager.getApplicationInfo( + package_name, manager_class.GET_SHARED_LIBRARY_FILES + ).nativeLibraryDir + return native_lib_dir + + +def does_libname_match_filename(search_name, file_path): + # Filter file names so given search_name="mymodule" we match one of: + # mymodule.so (direct name + .so) + # libmymodule.so (added lib prefix) + # mymodule.arm64.so (added dot-separated middle parts) + # mymodule.so.1.3.4 (added dot-separated version tail) + # and all above (all possible combinations) + import re + file_name = os.path.basename(file_path) + return (re.match(r"^(lib)?" + re.escape(search_name) + + r"\.(.*\.)?so(\.[0-9]+)*$", file_name) is not None) + + +def find_library(name): + # Obtain all places for native libraries: + lib_search_dirs = ["/system/lib"] + lib_dir_1 = get_activity_lib_dir("org.kivy.android.PythonActivity") + if lib_dir_1 is not None: + lib_search_dirs.insert(0, lib_dir_1) + lib_dir_2 = get_activity_lib_dir("org.kivy.android.PythonService") + if lib_dir_2 is not None and lib_dir_2 not in lib_search_dirs: + lib_search_dirs.insert(0, lib_dir_2) + + # Now scan the lib dirs: + for lib_dir in [l for l in lib_search_dirs if os.path.exists(l)]: + filelist = [ + f for f in os.listdir(lib_dir) + if does_libname_match_filename(name, f) + ] + if len(filelist) > 0: + return os.path.join(lib_dir, filelist[0]) + return None diff --git a/pythonforandroid/recipes/python3/patches/fix-ctypes-util-find-library.patch b/pythonforandroid/recipes/python3/patches/fix-ctypes-util-find-library.patch index ac75c83919..494270d2c7 100644 --- a/pythonforandroid/recipes/python3/patches/fix-ctypes-util-find-library.patch +++ b/pythonforandroid/recipes/python3/patches/fix-ctypes-util-find-library.patch @@ -1,23 +1,15 @@ diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py -@@ -67,4 +67,19 @@ +@@ -67,4 +67,11 @@ return fname return None +# This patch overrides the find_library to look in the right places on +# Android +if True: ++ from android._ctypes_library_finder import find_library as _find_lib + def find_library(name): -+ # Check the user app libs and system libraries directory: -+ app_root = os.path.normpath(os.path.abspath('../../')) -+ lib_search_dirs = [os.path.join(app_root, 'lib'), "/system/lib"] -+ for lib_dir in lib_search_dirs: -+ for filename in os.listdir(lib_dir): -+ if filename.endswith('.so') and ( -+ filename.startswith("lib" + name + ".") or -+ filename.startswith(name + ".")): -+ return os.path.join(lib_dir, filename) -+ return None ++ return _find_lib(name) + elif os.name == "posix" and sys.platform == "darwin": diff --git a/tests/test_androidmodule_ctypes_finder.py b/tests/test_androidmodule_ctypes_finder.py new file mode 100644 index 0000000000..7d4526888d --- /dev/null +++ b/tests/test_androidmodule_ctypes_finder.py @@ -0,0 +1,124 @@ + +import mock +from mock import MagicMock +import os +import shutil +import sys +import tempfile + + +# Import the tested android._ctypes_library_finder module, +# making sure android._android won't crash us! +# (since android._android is android-only / not compilable on desktop) +android_module_folder = os.path.abspath(os.path.join( + os.path.dirname(__file__), + "..", "pythonforandroid", "recipes", "android", "src" +)) +sys.path.insert(0, android_module_folder) +sys.modules['android._android'] = MagicMock() +import android._ctypes_library_finder +sys.path.remove(android_module_folder) + + +@mock.patch.dict('sys.modules', jnius=MagicMock()) +def test_get_activity_lib_dir(): + import jnius # should get us our fake module + + # Short test that it works when activity doesn't exist: + jnius.autoclass = MagicMock() + jnius.autoclass.return_value = None + assert android._ctypes_library_finder.get_activity_lib_dir( + "JavaClass" + ) is None + assert mock.call("JavaClass") in jnius.autoclass.call_args_list + + # Comprehensive test that verifies getApplicationInfo() call: + activity = MagicMock() + app_context = activity.getApplicationContext() + app_context.getPackageName.return_value = "test.package" + app_info = app_context.getPackageManager().getApplicationInfo() + app_info.nativeLibraryDir = '/testpath' + + def pick_class(name): + cls = MagicMock() + if name == "JavaClass": + cls.mActivity = activity + elif name == "android.content.pm.PackageManager": + # Manager class: + cls.GET_SHARED_LIBRARY_FILES = 1024 + return cls + + jnius.autoclass = MagicMock(side_effect=pick_class) + assert android._ctypes_library_finder.get_activity_lib_dir( + "JavaClass" + ) == "/testpath" + assert mock.call("JavaClass") in jnius.autoclass.call_args_list + assert mock.call("test.package", 1024) in ( + app_context.getPackageManager().getApplicationInfo.call_args_list + ) + + +@mock.patch.dict('sys.modules', jnius=MagicMock()) +def test_find_library(): + test_d = tempfile.mkdtemp(prefix="p4a-android-ctypes-test-libdir-") + try: + with open(os.path.join(test_d, "mymadeuplib.so.5"), "w"): + pass + import jnius # should get us our fake module + + # Test with mActivity returned: + jnius.autoclass = MagicMock() + jnius.autoclass().mService = None + app_context = jnius.autoclass().mActivity.getApplicationContext() + app_info = app_context.getPackageManager().getApplicationInfo() + app_info.nativeLibraryDir = '/doesnt-exist-testpath' + assert android._ctypes_library_finder.find_library( + "mymadeuplib" + ) is None + assert mock.call("org.kivy.android.PythonActivity") in ( + jnius.autoclass.call_args_list + ) + app_info.nativeLibraryDir = test_d + assert os.path.normpath(android._ctypes_library_finder.find_library( + "mymadeuplib" + )) == os.path.normpath(os.path.join(test_d, "mymadeuplib.so.5")) + + # Test with mService returned: + jnius.autoclass = MagicMock() + jnius.autoclass().mActivity = None + app_context = jnius.autoclass().mService.getApplicationContext() + app_info = app_context.getPackageManager().getApplicationInfo() + app_info.nativeLibraryDir = '/doesnt-exist-testpath' + assert android._ctypes_library_finder.find_library( + "mymadeuplib" + ) is None + app_info.nativeLibraryDir = test_d + assert os.path.normpath(android._ctypes_library_finder.find_library( + "mymadeuplib" + )) == os.path.normpath(os.path.join(test_d, "mymadeuplib.so.5")) + finally: + shutil.rmtree(test_d) + + +def test_does_libname_match_filename(): + assert android._ctypes_library_finder.does_libname_match_filename( + "mylib", "mylib.so" + ) + assert not android._ctypes_library_finder.does_libname_match_filename( + "mylib", "amylib.so" + ) + assert not android._ctypes_library_finder.does_libname_match_filename( + "mylib", "mylib.txt" + ) + assert not android._ctypes_library_finder.does_libname_match_filename( + "mylib", "mylib" + ) + assert android._ctypes_library_finder.does_libname_match_filename( + "mylib", "libmylib.test.so.1.2.3" + ) + assert not android._ctypes_library_finder.does_libname_match_filename( + "mylib", "libtest.mylib.so" + ) + assert android._ctypes_library_finder.does_libname_match_filename( + "mylib", "mylib.so.5" + )