Repository: kivy/python-for-android

Issue:
My despair at trying to simply import the opencv face class (python-for-android) 
I've been trying this for weeks, I was told it would be straightforward and that if I wanted to import the face class I could simply "add that option to the recipe".  But this is somehow starting to seem more difficult than I first thought. Has anyone ever managed to compile any of the extra modules (contrib) through p4a by changing the opencv recipe? If this is indeed too difficult, could I use pyjnius, as only 10 lines of code in my entire app is dedicated to detecting facial landmarks.

Discussion:

Patch:
diff --git a/pythonforandroid/bootstraps/common/build/build.py b/pythonforandroid/bootstraps/common/build/build.py
index 8d3bee6e..a91ac71e 100644
--- a/pythonforandroid/bootstraps/common/build/build.py
+++ b/pythonforandroid/bootstraps/common/build/build.py
@@ -680,6 +680,9 @@ tools directory of the Android SDK.
                     const='release', default='debug',
                     help='Build your app as a non-debug release build. '
                          '(Disables gdb debugging among other things)')
+    ap.add_argument('--with-debug-symbols', dest='with_debug_symbols',
+                    action='store_const', const=True, default=False,
+                    help='Will keep debug symbols from `.so` files.')
     ap.add_argument('--add-jar', dest='add_jar', action='append',
                     help=('Add a Java .jar to the libs, so you can access its '
                           'classes with pyjnius. You can specify this '
diff --git a/pythonforandroid/bootstraps/sdl2/__init__.py b/pythonforandroid/bootstraps/sdl2/__init__.py
index 3476abb6..5f7c9cee 100644
--- a/pythonforandroid/bootstraps/sdl2/__init__.py
+++ b/pythonforandroid/bootstraps/sdl2/__init__.py
@@ -47,7 +47,7 @@ class SDL2GradleBootstrap(Bootstrap):
                 with open('blacklist.txt', 'a') as fileh:
                     fileh.write('\nsqlite3/*\nlib-dynload/_sqlite3.so\n')
 
-        if not self.ctx.build_as_debuggable:
+        if not self.ctx.with_debug_symbols:
             self.strip_libraries(arch)
         self.fry_eggs(site_packages_dir)
         super().assemble_distribution()
diff --git a/pythonforandroid/bootstraps/service_library/build/templates/AndroidManifest.tmpl.xml b/pythonforandroid/bootstraps/service_library/build/templates/AndroidManifest.tmpl.xml
index f9b5afe3..f6676517 100644
--- a/pythonforandroid/bootstraps/service_library/build/templates/AndroidManifest.tmpl.xml
+++ b/pythonforandroid/bootstraps/service_library/build/templates/AndroidManifest.tmpl.xml
@@ -7,7 +7,7 @@
     <!-- Android 2.3.3 -->
     <uses-sdk android:minSdkVersion="{{ args.min_sdk_version }}" android:targetSdkVersion="{{ android_api }}" />
 
-    <application>
+    <application {% if debug %}android:debuggable="true"{% endif %} >
         {% for name in service_names %}
         <service android:name="{{ args.package }}.Service{{ name|capitalize }}"
                  android:process=":service_{{ name }}"
diff --git a/pythonforandroid/bootstraps/service_only/__init__.py b/pythonforandroid/bootstraps/service_only/__init__.py
index 806794ad..41016d73 100644
--- a/pythonforandroid/bootstraps/service_only/__init__.py
+++ b/pythonforandroid/bootstraps/service_only/__init__.py
@@ -46,7 +46,7 @@ class ServiceOnlyBootstrap(Bootstrap):
                 with open('blacklist.txt', 'a') as fileh:
                     fileh.write('\nsqlite3/*\nlib-dynload/_sqlite3.so\n')
 
-        if not self.ctx.build_as_debuggable:
+        if not self.ctx.with_debug_symbols:
             self.strip_libraries(arch)
         self.fry_eggs(site_packages_dir)
         super().assemble_distribution()
diff --git a/pythonforandroid/bootstraps/service_only/build/templates/AndroidManifest.tmpl.xml b/pythonforandroid/bootstraps/service_only/build/templates/AndroidManifest.tmpl.xml
index f3eb3bef..95a5ed1e 100644
--- a/pythonforandroid/bootstraps/service_only/build/templates/AndroidManifest.tmpl.xml
+++ b/pythonforandroid/bootstraps/service_only/build/templates/AndroidManifest.tmpl.xml
@@ -45,6 +45,7 @@
          An example Java class can be found in README-android.txt
     -->
     <application android:label="@string/app_name"
+                 {% if debug %}android:debuggable="true"{% endif %}
                  android:icon="@drawable/icon"
                  android:allowBackup="true"
                  android:theme="{{args.android_apptheme}}{% if not args.window %}.Fullscreen{% endif %}"
diff --git a/pythonforandroid/bootstraps/webview/__init__.py b/pythonforandroid/bootstraps/webview/__init__.py
index b90f3319..24b4cf4b 100644
--- a/pythonforandroid/bootstraps/webview/__init__.py
+++ b/pythonforandroid/bootstraps/webview/__init__.py
@@ -43,7 +43,7 @@ class WebViewBootstrap(Bootstrap):
                 with open('blacklist.txt', 'a') as fileh:
                     fileh.write('\nsqlite3/*\nlib-dynload/_sqlite3.so\n')
 
-        if not self.ctx.build_as_debuggable:
+        if not self.ctx.with_debug_symbols:
             self.strip_libraries(arch)
         self.fry_eggs(site_packages_dir)
         super().assemble_distribution()
diff --git a/pythonforandroid/bootstraps/webview/build/templates/AndroidManifest.tmpl.xml b/pythonforandroid/bootstraps/webview/build/templates/AndroidManifest.tmpl.xml
index 8b6b4ca9..ae28b8b2 100644
--- a/pythonforandroid/bootstraps/webview/build/templates/AndroidManifest.tmpl.xml
+++ b/pythonforandroid/bootstraps/webview/build/templates/AndroidManifest.tmpl.xml
@@ -51,6 +51,7 @@
                  android:theme="{{args.android_apptheme}}{% if not args.window %}.Fullscreen{% endif %}"
                  android:hardwareAccelerated="true"
                  android:usesCleartextTraffic="true"
+                 {% if debug %}android:debuggable="true"{% endif %}
                  >
         {% for l in args.android_used_libs %}
         <uses-library android:name="{{ l }}" />
diff --git a/pythonforandroid/build.py b/pythonforandroid/build.py
index fba23ce0..9bed914d 100644
--- a/pythonforandroid/build.py
+++ b/pythonforandroid/build.py
@@ -82,9 +82,12 @@ class Context:
     '''A build context. If anything will be built, an instance this class
     will be instantiated and used to hold all the build state.'''
 
-    # Whether to build with debugging symbols
+    # Whether to make a debug or release build
     build_as_debuggable = False
 
+    # Whether to strip debug symbols in `.so` files
+    with_debug_symbols = False
+
     env = environ.copy()
     # the filepath of toolchain.py
     root_dir = None
@@ -831,7 +834,7 @@ def run_pymodules_install(ctx, modules, project_dir=None,
                 )
 
         # Strip object files after potential Cython or native code builds:
-        if not ctx.build_as_debuggable:
+        if not ctx.with_debug_symbols:
             standard_recipe.strip_object_files(
                 ctx.archs[0], env, build_dir=ctx.build_dir
             )
diff --git a/pythonforandroid/recipe.py b/pythonforandroid/recipe.py
index 6128074e..3933dd78 100644
--- a/pythonforandroid/recipe.py
+++ b/pythonforandroid/recipe.py
@@ -1072,7 +1072,7 @@ class CythonRecipe(PythonRecipe):
                 info('First build appeared to complete correctly, skipping manual'
                      'cythonising.')
 
-            if not self.ctx.build_as_debuggable:
+            if not self.ctx.with_debug_symbols:
                 self.strip_object_files(arch, env)
 
     def strip_object_files(self, arch, env, build_dir=None):
diff --git a/pythonforandroid/recipes/opencv/__init__.py b/pythonforandroid/recipes/opencv/__init__.py
index a5708992..a90ddda9 100644
--- a/pythonforandroid/recipes/opencv/__init__.py
+++ b/pythonforandroid/recipes/opencv/__init__.py
@@ -31,7 +31,7 @@ class OpenCVRecipe(NDKRecipe):
         'libopencv_video.so',
         'libopencv_dnn.so',
         'libopencv_imgcodecs.so',
-        'libopencv_photo.so'
+        'libopencv_photo.so',
     ]
 
     def get_lib_dir(self, arch):
@@ -46,6 +46,16 @@ class OpenCVRecipe(NDKRecipe):
     def build_arch(self, arch):
         build_dir = join(self.get_build_dir(arch.arch), 'build')
         shprint(sh.mkdir, '-p', build_dir)
+
+        opencv_extras = []
+        if 'opencv_extras' in self.ctx.recipe_build_order:
+            opencv_extras_dir = self.get_recipe(
+                'opencv_extras', self.ctx).get_build_dir(arch.arch)
+            opencv_extras = [
+                f'-DOPENCV_EXTRA_MODULES_PATH={opencv_extras_dir}/modules',
+                '-DBUILD_opencv_legacy=OFF',
+            ]
+
         with current_directory(build_dir):
             env = self.get_recipe_env(arch)
 
@@ -120,6 +130,8 @@ class OpenCVRecipe(NDKRecipe):
                     '-DPYTHON{major}_PACKAGES_PATH={site_packages}'.format(
                         major=python_major, site_packages=python_site_packages),
 
+                    *opencv_extras,
+
                     self.get_build_dir(arch.arch),
                     _env=env)
             shprint(sh.make, '-j' + str(cpu_count()), 'opencv_python' + python_major)
diff --git a/pythonforandroid/recipes/opencv_extras/__init__.py b/pythonforandroid/recipes/opencv_extras/__init__.py
new file mode 100644
index 00000000..c63d8da5
--- /dev/null
+++ b/pythonforandroid/recipes/opencv_extras/__init__.py
@@ -0,0 +1,23 @@
+from pythonforandroid.recipe import Recipe
+
+
+class OpenCVExtrasRecipe(Recipe):
+    """
+    OpenCV extras recipe allows us to build extra modules from the
+    `opencv_contrib` repository. It depends on opencv recipe and all the build
+    of the modules will be performed inside opencv recipe build directory.
+
+    .. note:: the version of this recipe should be the same than opencv recipe.
+
+    .. warning:: Be aware that these modules are experimental, some of them
+        maybe included in opencv future releases and removed from extras.
+
+    .. seealso:: https://github.com/opencv/opencv_contrib
+
+    """
+    version = '4.0.1'
+    url = 'https://github.com/opencv/opencv_contrib/archive/{version}.zip'
+    depends = ['opencv']
+
+
+recipe = OpenCVExtrasRecipe()
diff --git a/pythonforandroid/toolchain.py b/pythonforandroid/toolchain.py
index 3ba4f5b8..19b35242 100644
--- a/pythonforandroid/toolchain.py
+++ b/pythonforandroid/toolchain.py
@@ -196,6 +196,14 @@ def build_dist_from_args(ctx, dist, args):
         ctx.recipe_build_order))
     info('Dist will also contain modules ({}) installed from pip'.format(
         ', '.join(ctx.python_modules)))
+    info(
+        'Dist will be build in mode {build_mode}{with_debug_symbols}'.format(
+            build_mode='debug' if ctx.build_as_debuggable else 'release',
+            with_debug_symbols=' (with debug symbols)'
+            if ctx.with_debug_symbols
+            else '',
+        )
+    )
 
     ctx.distribution = dist
     ctx.prepare_bootstrap(bs)
@@ -518,6 +526,10 @@ class ToolchainCL:
             const='release', default='debug',
             help='Build your app as a non-debug release build. '
                  '(Disables gdb debugging among other things)')
+        parser_packaging.add_argument(
+            '--with-debug-symbols', dest='with_debug_symbols',
+            action='store_const', const=True, default=False,
+            help='Will keep debug symbols from `.so` files.')
         parser_packaging.add_argument(
             '--keystore', dest='keystore', action='store', default=None,
             help=('Keystore for JAR signing key, will use jarsigner '
@@ -593,6 +605,8 @@ class ToolchainCL:
             args.unknown_args += ["--private", args.private]
         if hasattr(args, "build_mode") and args.build_mode == "release":
             args.unknown_args += ["--release"]
+        if hasattr(args, "with_debug_symbols") and args.with_debug_symbols:
+            args.unknown_args += ["--with-debug-symbols"]
         if hasattr(args, "ignore_setup_py") and args.ignore_setup_py:
             args.use_setup_py = False
 
@@ -612,6 +626,9 @@ class ToolchainCL:
         self.ctx.build_as_debuggable = getattr(
             args, "build_mode", "debug"
         ) == "debug"
+        self.ctx.with_debug_symbols = getattr(
+            args, "with_debug_symbols", False
+        )
 
         have_setup_py_or_similar = False
         if getattr(args, "private", None) is not None:


Test patch:
diff --git a/tests/test_build.py b/tests/test_build.py
index f76a1dab..727205bd 100644
--- a/tests/test_build.py
+++ b/tests/test_build.py
@@ -19,7 +19,7 @@ class TestBuildBasic(unittest.TestCase):
         assert m_info.call_args_list[-1] == mock.call(
             'No Python modules and no setup.py to process, skipping')
 
-    def test_strip_if_debuggable(self):
+    def test_strip_if_with_debug_symbols(self):
         ctx = mock.Mock()
         ctx.python_recipe.major_minor_version_string = "python3.6"
         ctx.get_site_packages_dir.return_value = "test-doesntexist"
@@ -38,12 +38,13 @@ class TestBuildBasic(unittest.TestCase):
                 mock.patch('pythonforandroid.build.run_setuppy_install'):
             m_project_has_setup_py.return_value = False
 
-            # Make sure it is NOT called when debug build:
-            ctx.build_as_debuggable = True
+            # Make sure it is NOT called when `with_debug_symbols` is true:
+            ctx.with_debug_symbols = True
             assert run_pymodules_install(ctx, modules, project_dir) is None
             assert m_CythonRecipe().strip_object_files.called is False
 
-            # Make sure strip object files IS called when release build:
-            ctx.build_as_debuggable = False
+            # Make sure strip object files IS called when
+            # `with_debug_symbols` is fasle:
+            ctx.with_debug_symbols = False
             assert run_pymodules_install(ctx, modules, project_dir) is None
             assert m_CythonRecipe().strip_object_files.called is True