• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kivy / python-for-android / 40578488838

27 Oct 2026 08:29PM UTC coverage: 63.297% (+0.02%) from 63.278%
40578488838

Pull #3400

github

web-flow
Merge ebd38e747 into 26b6ef072
Pull Request #3400: Fix android.broadcast.BroadcastReceiver on Android 14+: pass RECEIVER_EXPORTED / RECEIVER_NOT_EXPORTED

1859 of 3196 branches covered (58.17%)

Branch coverage included in aggregate %.

5448 of 8348 relevant lines covered (65.26%)

3.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

62.14
/pythonforandroid/recipes/python3/__init__.py
1
import glob
6 ✔
2
import sh
6 ✔
3
import subprocess
6 ✔
4

5
from os import environ, utime
6 ✔
6
from os.path import dirname, exists, join, isfile
6 ✔
7
import shutil
6 ✔
8

9
from packaging.version import Version
6 ✔
10
from pythonforandroid.logger import info, shprint, warning
6 ✔
11
from pythonforandroid.recipe import Recipe, TargetPythonRecipe
6 ✔
12
from pythonforandroid.util import (
6 ✔
13
    current_directory,
14
    ensure_dir,
15
    walk_valid_filens,
16
    BuildInterruptingException,
17
)
18

19
NDK_API_LOWER_THAN_SUPPORTED_MESSAGE = (
6 ✔
20
    'Target ndk-api is {ndk_api}, '
21
    'but the python3 recipe supports only {min_ndk_api}+'
22
)
23

24

25
class Python3Recipe(TargetPythonRecipe):
6 ✔
26
    '''
27
    The python3's recipe
28
    ^^^^^^^^^^^^^^^^^^^^
29

30
    The python 3 recipe can be built with some extra python modules, but to do
31
    so, we need some libraries. By default, we ship the python3 recipe with
32
    some common libraries, defined in ``depends``. We also support some optional
33
    libraries, which are less common that the ones defined in ``depends``, so
34
    we added them as optional dependencies (``opt_depends``).
35

36
    Below you have a relationship between the python modules and the recipe
37
    libraries::
38

39
        - _ctypes: you must add the recipe for ``libffi``.
40
        - _sqlite3: you must add the recipe for ``sqlite3``.
41
        - _ssl: you must add the recipe for ``openssl``.
42
        - _bz2: you must add the recipe for ``libbz2`` (optional).
43
        - _lzma: you must add the recipe for ``liblzma`` (optional).
44

45
    .. note:: This recipe can be built only against API 21+.
46

47
    .. versionchanged:: 2019.10.06.post0
48
        - Refactored from deleted class ``python.GuestPythonRecipe`` into here
49
        - Added optional dependencies: :mod:`~pythonforandroid.recipes.libbz2`
50
          and :mod:`~pythonforandroid.recipes.liblzma`
51

52
    .. versionchanged:: 0.6.0
53
        Refactored into class
54
        :class:`~pythonforandroid.python.GuestPythonRecipe`
55
    '''
56

57
    version = '3.14.2'
6 ✔
58
    url = 'https://github.com/python/cpython/archive/refs/tags/v{version}.tar.gz'
6 ✔
59
    name = 'python3'
6 ✔
60

61
    patches = [
6 ✔
62
        'patches/pyconfig_detection.patch',
63
        'patches/reproducible-buildinfo.diff',
64
    ]
65

66
    depends = ['hostpython3', 'sqlite3', 'openssl', 'libffi']
6 ✔
67
    # those optional depends allow us to build python compression modules:
68
    #   - _bz2.so
69
    #   - _lzma.so
70
    opt_depends = ['libbz2', 'liblzma']
6 ✔
71
    '''The optional libraries which we would like to get our python linked'''
6 ✔
72

73
    configure_args = [
6 ✔
74
        '--host={android_host}',
75
        '--build={android_build}',
76
        '--enable-ipv6',
77
        '--enable-loadable-sqlite-extensions',
78
        '--enable-shared',
79

80
        # Attempt on making the builds lighter
81
        '--disable-test-modules',
82
        '--without-doc-strings',
83
        '--without-ensurepip',
84
        '--without-readline',
85
        '--without-static-libpython',
86

87
        # Android prefix
88
        '--prefix={prefix}',
89

90
        # Special cross compile args
91
        'ac_cv_file__dev_ptc=no',
92
        'ac_cv_file__dev_ptmx=yes',
93
        'ac_cv_header_bzlib_h=no',
94
        'ac_cv_header_sys_eventfd_h=no',
95
        'py_cv_module__curses=n/a',
96
        'py_cv_module__curses_panel=n/a',
97
        'py_cv_module__tkinter=n/a'
98
    ]
99

100
    '''The configure arguments needed to build the python recipe. Those are
6 ✔
101
    used in method :meth:`build_arch` (if not overwritten like python3's
102
    recipe does).
103
    '''
104

105
    MIN_NDK_API = 21
6 ✔
106
    '''Sets the minimal ndk api number needed to use the recipe.
6 ✔
107

108
    .. warning:: This recipe can be built only against API 21+, so it means
109
        that any class which inherits from class:`GuestPythonRecipe` will have
110
        this limitation.
111
    '''
112

113
    stdlib_dir_blacklist = {
6 ✔
114
        '__pycache__',
115
        'curses',
116
        'ensurepip',
117
        'idlelib',
118
        'lib2to3',
119
        'pydoc_data',
120
        'test',
121
        'tests',
122
        'tkinter',
123
        'turtledemo',
124
        'venv'
125
    }
126
    '''The directories that we want to omit for our python bundle'''
6 ✔
127

128
    stdlib_filen_blacklist = [
6 ✔
129
        '*.exe',
130
        '*.py',
131
        '*.whl',
132
        'turtle.pyc'
133
    ]
134
    '''The file extensions that we want to blacklist for our python bundle'''
6 ✔
135

136
    site_packages_dir_blacklist = {
6 ✔
137
        '__pycache__',
138
        'tests'
139
    }
140
    '''The directories from site packages dir that we don't want to be included
6 ✔
141
    in our python bundle.'''
142

143
    site_packages_excluded_dir_exceptions = [
6 ✔
144
        # 'numpy' is excluded here because importing with `import numpy as np`
145
        # can fail if the `tests` directory inside the numpy package is excluded.
146
        'numpy',
147
    ]
148
    '''Directories from `site_packages_dir_blacklist` will not be excluded
6 ✔
149
    if the full path contains any of these exceptions.'''
150

151
    site_packages_filen_blacklist = [
6 ✔
152
        '*.py',
153
        '*.pyx'
154
    ]
155
    '''The file extensions from site packages dir that we don't want to be
6 ✔
156
    included in our python bundle.'''
157

158
    compiled_extension = '.pyc'
6 ✔
159
    '''the default extension for compiled python files.
6 ✔
160

161
    .. note:: the default extension for compiled python files has been .pyo for
162
        python 2.x-3.4 but as of Python 3.5, the .pyo filename extension is no
163
        longer used and has been removed in favour of extension .pyc
164
    '''
165

166
    disable_gil = False
6 ✔
167
    '''python3.13 experimental free-threading build'''
6 ✔
168

169
    built_libraries = {"libpythonbin.so": "./android-build/"}
6 ✔
170

171
    def __init__(self, *args, **kwargs):
6 ✔
172
        self._ctx = None
6 ✔
173
        super().__init__(*args, **kwargs)
6 ✔
174

175
    @property
6 ✔
176
    def _libpython(self):
6 ✔
177
        '''return the python's library name (with extension)'''
178
        return 'libpython{link_version}.so'.format(
6 ✔
179
            link_version=self.link_version
180
        )
181

182
    @property
6 ✔
183
    def link_version(self):
6 ✔
184
        '''return the python's library link version e.g. 3.7m, 3.8'''
185
        major, minor = self.major_minor_version_string.split('.')
6 ✔
186
        flags = ''
6 ✔
187
        if major == '3' and int(minor) < 8:
6 !
188
            flags += 'm'
×
189
        return '{major}.{minor}{flags}'.format(
6 ✔
190
            major=major,
191
            minor=minor,
192
            flags=flags
193
        )
194

195
    def apply_patches(self, arch, build_dir=None):
6 ✔
196

197
        _p_version = Version(self.version)
×
198
        if _p_version.major == 3 and _p_version.minor == 7:
×
199
            self.patches += [
×
200
                'patches/py3.7.1_fix-ctypes-util-find-library.patch',
201
                'patches/py3.7.1_fix-zlib-version.patch',
202
            ]
203

204
        if 8 <= _p_version.minor <= 10:
×
205
            self.patches.append('patches/py3.8.1.patch')
×
206

207
        if _p_version.minor >= 11:
×
208
            self.patches.append('patches/cpython-311-ctypes-find-library.patch')
×
209

210
        if _p_version.minor >= 14:
×
211
            self.patches.append('patches/3.14_armv7l_fix.patch')
×
212
            self.patches.append('patches/3.14_fix_remote_debug.patch')
×
213

214
        if shutil.which('lld') is not None:
×
215
            if _p_version.minor == 7:
×
216
                self.patches.append("patches/py3.7.1_fix_cortex_a8.patch")
×
217
            elif _p_version.minor >= 8:
×
218
                self.patches.append("patches/py3.8.1_fix_cortex_a8.patch")
×
219

220
        self.patches = list(dict.fromkeys(self.patches))  # preserve order for reproducibility
×
221
        super().apply_patches(arch, build_dir)
×
222

223
    def include_root(self, arch_name):
6 ✔
224
        _p_version = Version(self.version)
6 ✔
225
        return join(
6 ✔
226
            self.get_build_dir(arch_name), 'android-build', 'android-root',
227
            'include', f'python{_p_version.major}.{_p_version.minor}'
228
        )
229

230
    def link_root(self, arch_name):
6 ✔
231
        return join(self.get_build_dir(arch_name), 'android-build')
6 ✔
232

233
    def get_python_root(self, arch):
6 ✔
234
        return join(self.get_build_dir(arch.arch), 'android-build', 'android-root')
×
235

236
    def get_android_python_exe(self, arch):
6 ✔
237
        return join(self.get_python_root(arch), 'bin', self.name)
×
238

239
    def should_build(self, arch):
6 ✔
240
        return not isfile(join(self.link_root(arch.arch), self._libpython))
×
241

242
    def prebuild_arch(self, arch):
6 ✔
243
        super().prebuild_arch(arch)
×
244
        self.ctx.python_recipe = self
×
245

246
    def get_recipe_env(self, arch=None, with_flags_in_cc=True):
6 ✔
247
        env = super().get_recipe_env(arch)
6 ✔
248
        env['HOSTARCH'] = arch.command_prefix
6 ✔
249
        env['CC'] = arch.get_clang_exe(with_target=True)
6 ✔
250
        env['PATH'] = '{hostpython_dir}:{old_path}'.format(
6 ✔
251
            hostpython_dir=self.get_recipe(
252
                'host' + self.name, self.ctx
253
            ).get_path_to_python(),
254
            old_path=env['PATH']
255
        )
256
        env['CFLAGS'] = ' '.join(
6 ✔
257
            [
258
                '-ffunction-sections',
259
                '-fdata-sections',
260
                '-fPIC'
261
            ]
262
        )
263

264
        env['LDFLAGS'] = env.get('LDFLAGS', '')
6 ✔
265
        if shutil.which('lld') is not None:
6 !
266
            # Note: The -L. is to fix a bug in python 3.7.
267
            # https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234409
268
            env['LDFLAGS'] += ' -L. -fuse-ld=lld'
6 ✔
269
        else:
270
            warning('lld not found, linking without it. '
×
271
                    'Consider installing lld if linker errors occur.')
272

273
        return env
6 ✔
274

275
    def set_libs_flags(self, env, arch):
6 ✔
276
        '''Takes care to properly link libraries with python depending on our
277
        requirements and the attribute :attr:`opt_depends`.
278
        '''
279
        def add_flags(include_flags, link_dirs, link_libs):
6 ✔
280
            env['CPPFLAGS'] = env.get('CPPFLAGS', '') + include_flags
6 ✔
281
            env['LDFLAGS'] = env.get('LDFLAGS', '') + link_dirs
6 ✔
282
            env['LIBS'] = env.get('LIBS', '') + link_libs
6 ✔
283

284
        info('Activating flags for sqlite3')
6 ✔
285
        recipe = Recipe.get_recipe('sqlite3', self.ctx)
6 ✔
286
        add_flags(' -I' + recipe.get_build_dir(arch.arch),
6 ✔
287
                  ' -L' + recipe.get_build_dir(arch.arch), ' -lsqlite3')
288

289
        info('Activating flags for libffi')
6 ✔
290
        recipe = Recipe.get_recipe('libffi', self.ctx)
6 ✔
291
        # In order to force the correct linkage for our libffi library, we
292
        # set the following variable to point where is our libffi.pc file,
293
        # because the python build system uses pkg-config to configure it.
294
        env['PKG_CONFIG_LIBDIR'] = recipe.get_build_dir(arch.arch)
6 ✔
295
        add_flags(' -I' + ' -I'.join(recipe.get_include_dirs(arch)),
6 ✔
296
                  ' -L' + join(recipe.get_build_dir(arch.arch), '.libs'),
297
                  ' -lffi')
298

299
        info('Activating flags for openssl')
6 ✔
300
        recipe = Recipe.get_recipe('openssl', self.ctx)
6 ✔
301
        self.configure_args.append('--with-openssl=' + recipe.get_build_dir(arch.arch))
6 ✔
302
        add_flags(recipe.include_flags(arch),
6 ✔
303
                  recipe.link_dirs_flags(arch), recipe.link_libs_flags())
304

305
        for library_name in {'libbz2', 'liblzma'}:
6 ✔
306
            if library_name in self.ctx.recipe_build_order:
6 !
307
                info(f'Activating flags for {library_name}')
×
308
                recipe = Recipe.get_recipe(library_name, self.ctx)
×
309
                add_flags(recipe.get_library_includes(arch),
×
310
                          recipe.get_library_ldflags(arch),
311
                          recipe.get_library_libs_flag())
312

313
        # python build system contains hardcoded zlib version which prevents
314
        # the build of zlib module, here we search for android's zlib version
315
        # and sets the right flags, so python can be build with android's zlib
316
        info("Activating flags for android's zlib")
6 ✔
317
        zlib_lib_path = arch.ndk_lib_dir_versioned
6 ✔
318
        zlib_includes = self.ctx.ndk.sysroot_include_dir
6 ✔
319
        zlib_h = join(zlib_includes, 'zlib.h')
6 ✔
320
        try:
6 ✔
321
            with open(zlib_h) as fileh:
6 ✔
322
                zlib_data = fileh.read()
6 ✔
323
        except IOError:
×
324
            raise BuildInterruptingException(
×
325
                "Could not determine android's zlib version, no zlib.h ({}) in"
326
                " the NDK dir includes".format(zlib_h)
327
            )
328
        for line in zlib_data.split('\n'):
6 !
329
            if line.startswith('#define ZLIB_VERSION '):
6 !
330
                break
6 ✔
331
        else:
332
            raise BuildInterruptingException(
×
333
                'Could not parse zlib.h...so we cannot find zlib version,'
334
                'required by python build,'
335
            )
336
        env['ZLIB_VERSION'] = line.replace('#define ZLIB_VERSION ', '')
6 ✔
337
        add_flags(' -I' + zlib_includes, ' -L' + zlib_lib_path, ' -lz')
6 ✔
338

339
        _p_version = Version(self.version)
6 ✔
340
        if _p_version.minor >= 11:
6 !
341
            self.configure_args.append('--with-build-python={python_host_bin}')
6 ✔
342

343
        if _p_version.minor >= 13 and self.disable_gil:
6 !
344
            self.configure_args.append("--disable-gil")
×
345

346
        self.configure_args = list(dict.fromkeys(self.configure_args))  # preserve order for reproducibility
6 ✔
347

348
        return env
6 ✔
349

350
    def build_arch(self, arch):
6 ✔
351
        if self.ctx.ndk_api < self.MIN_NDK_API:
6 ✔
352
            raise BuildInterruptingException(
6 ✔
353
                NDK_API_LOWER_THAN_SUPPORTED_MESSAGE.format(
354
                    ndk_api=self.ctx.ndk_api, min_ndk_api=self.MIN_NDK_API
355
                ),
356
            )
357

358
        recipe_build_dir = self.get_build_dir(arch.arch)
6 ✔
359

360
        # Create a subdirectory to actually perform the build
361
        build_dir = join(recipe_build_dir, 'android-build')
6 ✔
362
        ensure_dir(build_dir)
6 ✔
363

364
        sys_prefix = join(build_dir, "android-root")
6 ✔
365
        ensure_dir(sys_prefix)
6 ✔
366

367
        env = self.get_recipe_env(arch)
6 ✔
368
        env = self.set_libs_flags(env, arch)
6 ✔
369

370
        android_build = sh.Command(
6 ✔
371
            join(recipe_build_dir,
372
                 'config.guess'))().strip()
373

374
        with current_directory(build_dir):
6 ✔
375
            if not exists('config.status'):
6 !
376
                shprint(
6 ✔
377
                    sh.Command(join(recipe_build_dir, 'configure')),
378
                    *(' '.join(self.configure_args).format(
379
                                    android_host=env['HOSTARCH'],
380
                                    android_build=android_build,
381
                                    python_host_bin=self.get_recipe(
382
                                        'host' + self.name, self.ctx
383
                                    ).python_exe,
384
                                    prefix=sys_prefix).split(' ')),
385
                    _env=env)
386

387
            shprint(
6 ✔
388
                sh.make,
389
                'all',
390
                'INSTSONAME={lib_name}'.format(lib_name=self._libpython),
391
                _env=env
392
            )
393
            shprint(sh.make, 'install', _env=env)
6 ✔
394

395
            # rename executable
396
            if isfile("python"):
6 !
397
                sh.cp('python', 'libpythonbin.so')
×
398
            elif isfile("python.exe"):  # for macos
6 !
399
                sh.cp('python.exe', 'libpythonbin.so')
×
400

401
            # TODO: Look into passing the path to pyconfig.h in a
402
            # better way, although this is probably acceptable
403
            sh.cp('pyconfig.h', join(recipe_build_dir, 'Include'))
6 ✔
404

405
    def compile_python_files(self, dir):
6 ✔
406
        '''
407
        Compile the python files (recursively) for the python files inside
408
        a given folder.
409

410
        .. note:: python2 compiles the files into extension .pyo, but in
411
            python3, and as of Python 3.5, the .pyo filename extension is no
412
            longer used...uses .pyc (https://www.python.org/dev/peps/pep-0488)
413
        '''
414
        args = [self.ctx.hostpython]
6 ✔
415
        if self.ctx.optimize_python:
6 ✔
416
            # -OO = strip docstrings and asserts
417
            args.append('-OO')
6 ✔
418
        args += ['-m', 'compileall', '-b', '-f', '-q', dir]
6 ✔
419
        subprocess.call(args)
6 ✔
420

421
    def create_python_bundle(self, dirn, arch):
6 ✔
422
        """
423
        Create a packaged python bundle in the target directory, by
424
        copying all the modules and standard library to the right
425
        place.
426
        """
427
        modules_build_dir = glob.glob(join(
×
428
            self.get_build_dir(arch.arch),
429
            'android-build',
430
            'build',
431
            'lib.*'
432
        ))[0]
433
        # Compile to *.pyc the python modules
434
        self.compile_python_files(modules_build_dir)
×
435
        # Compile to *.pyc the standard python library
436
        self.compile_python_files(join(self.get_build_dir(arch.arch), 'Lib'))
×
437
        # Compile to *.pyc the other python packages (site-packages)
438
        self.compile_python_files(self.ctx.get_python_install_dir(arch.arch))
×
439

440
        # Bundle compiled python modules to a folder
441
        modules_dir = join(dirn, 'modules')
×
442
        c_ext = self.compiled_extension
×
443
        ensure_dir(modules_dir)
×
444
        module_filens = (glob.glob(join(modules_build_dir, '*.so')) +
×
445
                         glob.glob(join(modules_build_dir, '*' + c_ext)))
446
        info("Copy {} files into the bundle".format(len(module_filens)))
×
447
        for filen in module_filens:
×
448
            info(" - copy {}".format(filen))
×
449
            shutil.copy2(filen, modules_dir)
×
450

451
        # zip up the standard library
452
        stdlib_zip = join(dirn, 'stdlib.zip')
×
453
        with current_directory(join(self.get_build_dir(arch.arch), 'Lib')):
×
454
            stdlib_filens = list(walk_valid_filens(
×
455
                '.', self.stdlib_dir_blacklist, self.stdlib_filen_blacklist))
456
            if 'SOURCE_DATE_EPOCH' in environ:
×
457
                # for reproducible builds
458
                stdlib_filens.sort()
×
459
                timestamp = int(environ['SOURCE_DATE_EPOCH'])
×
460
                for filen in stdlib_filens:
×
461
                    utime(filen, (timestamp, timestamp))
×
462
            info("Zip {} files into the bundle".format(len(stdlib_filens)))
×
463
            shprint(sh.zip, '-X', stdlib_zip, *stdlib_filens)
×
464

465
        # copy the site-packages into place
466
        ensure_dir(join(dirn, 'site-packages'))
×
467
        ensure_dir(self.ctx.get_python_install_dir(arch.arch))
×
468
        # TODO: Improve the API around walking and copying the files
469
        with current_directory(self.ctx.get_python_install_dir(arch.arch)):
×
470
            filens = list(walk_valid_filens(
×
471
                '.', self.site_packages_dir_blacklist,
472
                self.site_packages_filen_blacklist,
473
                excluded_dir_exceptions=self.site_packages_excluded_dir_exceptions))
474
            info("Copy {} files into the site-packages".format(len(filens)))
×
475
            for filen in filens:
×
476
                info(" - copy {}".format(filen))
×
477
                ensure_dir(join(dirn, 'site-packages', dirname(filen)))
×
478
                shutil.copy2(filen, join(dirn, 'site-packages', filen))
×
479

480
        # copy the python .so files into place
481
        python_build_dir = join(self.get_build_dir(arch.arch),
×
482
                                'android-build')
483
        python_lib_name = 'libpython' + self.link_version
×
484
        shprint(
×
485
            sh.cp,
486
            join(python_build_dir, python_lib_name + '.so'),
487
            join(self.ctx.bootstrap.dist_dir, 'libs', arch.arch)
488
        )
489

490
        info('Renaming .so files to reflect cross-compile')
×
491
        self.reduce_object_file_names(join(dirn, 'site-packages'))
×
492

493
        return join(dirn, 'site-packages')
×
494

495

496
recipe = Python3Recipe()
6 ✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc