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

kivy / python-for-android / 17641397155

11 Sep 2025 10:15AM UTC coverage: 59.214% (+0.04%) from 59.171%
17641397155

Pull #3168

github

web-flow
Merge 42336ff0c into 5330267b5
Pull Request #3168: Fix broadcast receiver

1060 of 2385 branches covered (44.44%)

Branch coverage included in aggregate %.

4965 of 7790 relevant lines covered (63.74%)

2.54 hits per line

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

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

5
from os import environ, utime
4✔
6
from os.path import dirname, exists, join
4✔
7
from pathlib import Path
4✔
8
import shutil
4✔
9

10
from pythonforandroid.logger import info, warning, shprint
4✔
11
from pythonforandroid.patching import version_starts_with
4✔
12
from pythonforandroid.recipe import Recipe, TargetPythonRecipe
4✔
13
from pythonforandroid.util import (
4✔
14
    current_directory,
15
    ensure_dir,
16
    walk_valid_filens,
17
    BuildInterruptingException,
18
)
19

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

25

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

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

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

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

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

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

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

58
    version = '3.11.5'
4✔
59
    url = 'https://www.python.org/ftp/python/{version}/Python-{version}.tgz'
4✔
60
    name = 'python3'
4✔
61

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

66
        # Python 3.7.1
67
        ('patches/py3.7.1_fix-ctypes-util-find-library.patch', version_starts_with("3.7")),
68
        ('patches/py3.7.1_fix-zlib-version.patch', version_starts_with("3.7")),
69

70
        # Python 3.8.1 & 3.9.X
71
        ('patches/py3.8.1.patch', version_starts_with("3.8")),
72
        ('patches/py3.8.1.patch', version_starts_with("3.9")),
73
        ('patches/py3.8.1.patch', version_starts_with("3.10")),
74
        ('patches/cpython-311-ctypes-find-library.patch', version_starts_with("3.11")),
75
    ]
76

77
    if shutil.which('lld') is not None:
4✔
78
        patches += [
4✔
79
            ("patches/py3.7.1_fix_cortex_a8.patch", version_starts_with("3.7")),
80
            ("patches/py3.8.1_fix_cortex_a8.patch", version_starts_with("3.8")),
81
            ("patches/py3.8.1_fix_cortex_a8.patch", version_starts_with("3.9")),
82
            ("patches/py3.8.1_fix_cortex_a8.patch", version_starts_with("3.10")),
83
            ("patches/py3.8.1_fix_cortex_a8.patch", version_starts_with("3.11")),
84
        ]
85

86
    depends = ['hostpython3', 'sqlite3', 'openssl', 'libffi']
4✔
87
    # those optional depends allow us to build python compression modules:
88
    #   - _bz2.so
89
    #   - _lzma.so
90
    opt_depends = ['libbz2', 'liblzma']
4✔
91
    '''The optional libraries which we would like to get our python linked'''
2✔
92

93
    configure_args = (
4✔
94
        '--host={android_host}',
95
        '--build={android_build}',
96
        '--enable-shared',
97
        '--enable-ipv6',
98
        'ac_cv_file__dev_ptmx=yes',
99
        'ac_cv_file__dev_ptc=no',
100
        '--without-ensurepip',
101
        'ac_cv_little_endian_double=yes',
102
        'ac_cv_header_sys_eventfd_h=no',
103
        '--prefix={prefix}',
104
        '--exec-prefix={exec_prefix}',
105
        '--enable-loadable-sqlite-extensions'
106
    )
107

108
    if version_starts_with("3.11"):
4!
109
        configure_args += ('--with-build-python={python_host_bin}',)
4✔
110

111
    '''The configure arguments needed to build the python recipe. Those are
2✔
112
    used in method :meth:`build_arch` (if not overwritten like python3's
113
    recipe does).
114
    '''
115

116
    MIN_NDK_API = 21
4✔
117
    '''Sets the minimal ndk api number needed to use the recipe.
2✔
118

119
    .. warning:: This recipe can be built only against API 21+, so it means
120
        that any class which inherits from class:`GuestPythonRecipe` will have
121
        this limitation.
122
    '''
123

124
    stdlib_dir_blacklist = {
4✔
125
        '__pycache__',
126
        'test',
127
        'tests',
128
        'lib2to3',
129
        'ensurepip',
130
        'idlelib',
131
        'tkinter',
132
    }
133
    '''The directories that we want to omit for our python bundle'''
2✔
134

135
    stdlib_filen_blacklist = [
4✔
136
        '*.py',
137
        '*.exe',
138
        '*.whl',
139
    ]
140
    '''The file extensions that we want to blacklist for our python bundle'''
2✔
141

142
    site_packages_dir_blacklist = {
4✔
143
        '__pycache__',
144
        'tests'
145
    }
146
    '''The directories from site packages dir that we don't want to be included
2✔
147
    in our python bundle.'''
148

149
    site_packages_excluded_dir_exceptions = [
4✔
150
        # 'numpy' is excluded here because importing with `import numpy as np`
151
        # can fail if the `tests` directory inside the numpy package is excluded.
152
        'numpy',
153
    ]
154
    '''Directories from `site_packages_dir_blacklist` will not be excluded
2✔
155
    if the full path contains any of these exceptions.'''
156

157
    site_packages_filen_blacklist = [
4✔
158
        '*.py'
159
    ]
160
    '''The file extensions from site packages dir that we don't want to be
2✔
161
    included in our python bundle.'''
162

163
    compiled_extension = '.pyc'
4✔
164
    '''the default extension for compiled python files.
2✔
165

166
    .. note:: the default extension for compiled python files has been .pyo for
167
        python 2.x-3.4 but as of Python 3.5, the .pyo filename extension is no
168
        longer used and has been removed in favour of extension .pyc
169
    '''
170

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

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

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

195
    def include_root(self, arch_name):
4✔
196
        return join(self.get_build_dir(arch_name), 'Include')
4✔
197

198
    def link_root(self, arch_name):
4✔
199
        return join(self.get_build_dir(arch_name), 'android-build')
4✔
200

201
    def should_build(self, arch):
4✔
202
        return not Path(self.link_root(arch.arch), self._libpython).is_file()
4✔
203

204
    def prebuild_arch(self, arch):
4✔
205
        super().prebuild_arch(arch)
×
206
        self.ctx.python_recipe = self
×
207

208
    def get_recipe_env(self, arch=None, with_flags_in_cc=True):
4✔
209
        env = super().get_recipe_env(arch)
4✔
210
        env['HOSTARCH'] = arch.command_prefix
4✔
211

212
        env['CC'] = arch.get_clang_exe(with_target=True)
4✔
213

214
        env['PATH'] = (
4✔
215
            '{hostpython_dir}:{old_path}').format(
216
                hostpython_dir=self.get_recipe(
217
                    'host' + self.name, self.ctx).get_path_to_python(),
218
                old_path=env['PATH'])
219

220
        env['CFLAGS'] = ' '.join(
4✔
221
            [
222
                '-fPIC',
223
                '-DANDROID'
224
            ]
225
        )
226

227
        env['LDFLAGS'] = env.get('LDFLAGS', '')
4✔
228
        if shutil.which('lld') is not None:
4!
229
            # Note: The -L. is to fix a bug in python 3.7.
230
            # https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234409
231
            env['LDFLAGS'] += ' -L. -fuse-ld=lld'
4✔
232
        else:
233
            warning('lld not found, linking without it. '
×
234
                    'Consider installing lld if linker errors occur.')
235

236
        return env
4✔
237

238
    def set_libs_flags(self, env, arch):
4✔
239
        '''Takes care to properly link libraries with python depending on our
240
        requirements and the attribute :attr:`opt_depends`.
241
        '''
242
        def add_flags(include_flags, link_dirs, link_libs):
4✔
243
            env['CPPFLAGS'] = env.get('CPPFLAGS', '') + include_flags
4✔
244
            env['LDFLAGS'] = env.get('LDFLAGS', '') + link_dirs
4✔
245
            env['LIBS'] = env.get('LIBS', '') + link_libs
4✔
246

247
        if 'sqlite3' in self.ctx.recipe_build_order:
4!
248
            info('Activating flags for sqlite3')
×
249
            recipe = Recipe.get_recipe('sqlite3', self.ctx)
×
250
            add_flags(' -I' + recipe.get_build_dir(arch.arch),
×
251
                      ' -L' + recipe.get_lib_dir(arch), ' -lsqlite3')
252

253
        if 'libffi' in self.ctx.recipe_build_order:
4!
254
            info('Activating flags for libffi')
×
255
            recipe = Recipe.get_recipe('libffi', self.ctx)
×
256
            # In order to force the correct linkage for our libffi library, we
257
            # set the following variable to point where is our libffi.pc file,
258
            # because the python build system uses pkg-config to configure it.
259
            env['PKG_CONFIG_PATH'] = recipe.get_build_dir(arch.arch)
×
260
            add_flags(' -I' + ' -I'.join(recipe.get_include_dirs(arch)),
×
261
                      ' -L' + join(recipe.get_build_dir(arch.arch), '.libs'),
262
                      ' -lffi')
263

264
        if 'openssl' in self.ctx.recipe_build_order:
4!
265
            info('Activating flags for openssl')
×
266
            recipe = Recipe.get_recipe('openssl', self.ctx)
×
267
            self.configure_args += \
×
268
                ('--with-openssl=' + recipe.get_build_dir(arch.arch),)
269
            add_flags(recipe.include_flags(arch),
×
270
                      recipe.link_dirs_flags(arch), recipe.link_libs_flags())
271

272
        for library_name in {'libbz2', 'liblzma'}:
4✔
273
            if library_name in self.ctx.recipe_build_order:
4!
274
                info(f'Activating flags for {library_name}')
×
275
                recipe = Recipe.get_recipe(library_name, self.ctx)
×
276
                add_flags(recipe.get_library_includes(arch),
×
277
                          recipe.get_library_ldflags(arch),
278
                          recipe.get_library_libs_flag())
279

280
        # python build system contains hardcoded zlib version which prevents
281
        # the build of zlib module, here we search for android's zlib version
282
        # and sets the right flags, so python can be build with android's zlib
283
        info("Activating flags for android's zlib")
4✔
284
        zlib_lib_path = arch.ndk_lib_dir_versioned
4✔
285
        zlib_includes = self.ctx.ndk.sysroot_include_dir
4✔
286
        zlib_h = join(zlib_includes, 'zlib.h')
4✔
287
        try:
4✔
288
            with open(zlib_h) as fileh:
4✔
289
                zlib_data = fileh.read()
4✔
290
        except IOError:
×
291
            raise BuildInterruptingException(
×
292
                "Could not determine android's zlib version, no zlib.h ({}) in"
293
                " the NDK dir includes".format(zlib_h)
294
            )
295
        for line in zlib_data.split('\n'):
4!
296
            if line.startswith('#define ZLIB_VERSION '):
4!
297
                break
4✔
298
        else:
299
            raise BuildInterruptingException(
×
300
                'Could not parse zlib.h...so we cannot find zlib version,'
301
                'required by python build,'
302
            )
303
        env['ZLIB_VERSION'] = line.replace('#define ZLIB_VERSION ', '')
4✔
304
        add_flags(' -I' + zlib_includes, ' -L' + zlib_lib_path, ' -lz')
4✔
305

306
        return env
4✔
307

308
    def build_arch(self, arch):
4✔
309
        if self.ctx.ndk_api < self.MIN_NDK_API:
4✔
310
            raise BuildInterruptingException(
4✔
311
                NDK_API_LOWER_THAN_SUPPORTED_MESSAGE.format(
312
                    ndk_api=self.ctx.ndk_api, min_ndk_api=self.MIN_NDK_API
313
                ),
314
            )
315

316
        recipe_build_dir = self.get_build_dir(arch.arch)
4✔
317

318
        # Create a subdirectory to actually perform the build
319
        build_dir = join(recipe_build_dir, 'android-build')
4✔
320
        ensure_dir(build_dir)
4✔
321

322
        # TODO: Get these dynamically, like bpo-30386 does
323
        sys_prefix = '/usr/local'
4✔
324
        sys_exec_prefix = '/usr/local'
4✔
325

326
        env = self.get_recipe_env(arch)
4✔
327
        env = self.set_libs_flags(env, arch)
4✔
328

329
        android_build = sh.Command(
4✔
330
            join(recipe_build_dir,
331
                 'config.guess'))().strip()
332

333
        with current_directory(build_dir):
4✔
334
            if not exists('config.status'):
4!
335
                shprint(
4✔
336
                    sh.Command(join(recipe_build_dir, 'configure')),
337
                    *(' '.join(self.configure_args).format(
338
                                    android_host=env['HOSTARCH'],
339
                                    android_build=android_build,
340
                                    python_host_bin=join(self.get_recipe(
341
                                        'host' + self.name, self.ctx
342
                                    ).get_path_to_python(), "python3"),
343
                                    prefix=sys_prefix,
344
                                    exec_prefix=sys_exec_prefix)).split(' '),
345
                    _env=env)
346

347
            # Python build does not seem to play well with make -j option from Python 3.11 and onwards
348
            # Before losing some time, please check issue
349
            # https://github.com/python/cpython/issues/101295 , as the root cause looks similar
350
            shprint(
4✔
351
                sh.make,
352
                'all',
353
                'INSTSONAME={lib_name}'.format(lib_name=self._libpython),
354
                _env=env
355
            )
356

357
            # TODO: Look into passing the path to pyconfig.h in a
358
            # better way, although this is probably acceptable
359
            sh.cp('pyconfig.h', join(recipe_build_dir, 'Include'))
4✔
360

361
    def compile_python_files(self, dir):
4✔
362
        '''
363
        Compile the python files (recursively) for the python files inside
364
        a given folder.
365

366
        .. note:: python2 compiles the files into extension .pyo, but in
367
            python3, and as of Python 3.5, the .pyo filename extension is no
368
            longer used...uses .pyc (https://www.python.org/dev/peps/pep-0488)
369
        '''
370
        args = [self.ctx.hostpython]
4✔
371
        args += ['-OO', '-m', 'compileall', '-b', '-f', dir]
4✔
372
        subprocess.call(args)
4✔
373

374
    def create_python_bundle(self, dirn, arch):
4✔
375
        """
376
        Create a packaged python bundle in the target directory, by
377
        copying all the modules and standard library to the right
378
        place.
379
        """
380
        # Todo: find a better way to find the build libs folder
381
        modules_build_dir = join(
4✔
382
            self.get_build_dir(arch.arch),
383
            'android-build',
384
            'build',
385
            'lib.linux{}-{}-{}'.format(
386
                '2' if self.version[0] == '2' else '',
387
                arch.command_prefix.split('-')[0],
388
                self.major_minor_version_string
389
            ))
390

391
        # Compile to *.pyc the python modules
392
        self.compile_python_files(modules_build_dir)
4✔
393
        # Compile to *.pyc the standard python library
394
        self.compile_python_files(join(self.get_build_dir(arch.arch), 'Lib'))
4✔
395
        # Compile to *.pyc the other python packages (site-packages)
396
        self.compile_python_files(self.ctx.get_python_install_dir(arch.arch))
4✔
397

398
        # Bundle compiled python modules to a folder
399
        modules_dir = join(dirn, 'modules')
4✔
400
        c_ext = self.compiled_extension
4✔
401
        ensure_dir(modules_dir)
4✔
402
        module_filens = (glob.glob(join(modules_build_dir, '*.so')) +
4✔
403
                         glob.glob(join(modules_build_dir, '*' + c_ext)))
404
        info("Copy {} files into the bundle".format(len(module_filens)))
4✔
405
        for filen in module_filens:
4!
406
            info(" - copy {}".format(filen))
×
407
            shutil.copy2(filen, modules_dir)
×
408

409
        # zip up the standard library
410
        stdlib_zip = join(dirn, 'stdlib.zip')
4✔
411
        with current_directory(join(self.get_build_dir(arch.arch), 'Lib')):
4✔
412
            stdlib_filens = list(walk_valid_filens(
4✔
413
                '.', self.stdlib_dir_blacklist, self.stdlib_filen_blacklist))
414
            if 'SOURCE_DATE_EPOCH' in environ:
4!
415
                # for reproducible builds
416
                stdlib_filens.sort()
×
417
                timestamp = int(environ['SOURCE_DATE_EPOCH'])
×
418
                for filen in stdlib_filens:
×
419
                    utime(filen, (timestamp, timestamp))
×
420
            info("Zip {} files into the bundle".format(len(stdlib_filens)))
4✔
421
            shprint(sh.zip, '-X', stdlib_zip, *stdlib_filens)
4✔
422

423
        # copy the site-packages into place
424
        ensure_dir(join(dirn, 'site-packages'))
4✔
425
        ensure_dir(self.ctx.get_python_install_dir(arch.arch))
4✔
426
        # TODO: Improve the API around walking and copying the files
427
        with current_directory(self.ctx.get_python_install_dir(arch.arch)):
4✔
428
            filens = list(walk_valid_filens(
4✔
429
                '.', self.site_packages_dir_blacklist,
430
                self.site_packages_filen_blacklist,
431
                excluded_dir_exceptions=self.site_packages_excluded_dir_exceptions))
432
            info("Copy {} files into the site-packages".format(len(filens)))
4✔
433
            for filen in filens:
4✔
434
                info(" - copy {}".format(filen))
4✔
435
                ensure_dir(join(dirn, 'site-packages', dirname(filen)))
4✔
436
                shutil.copy2(filen, join(dirn, 'site-packages', filen))
4✔
437

438
        # copy the python .so files into place
439
        python_build_dir = join(self.get_build_dir(arch.arch),
4✔
440
                                'android-build')
441
        python_lib_name = 'libpython' + self.link_version
4✔
442
        shprint(
4✔
443
            sh.cp,
444
            join(python_build_dir, python_lib_name + '.so'),
445
            join(self.ctx.bootstrap.dist_dir, 'libs', arch.arch)
446
        )
447

448
        info('Renaming .so files to reflect cross-compile')
4✔
449
        self.reduce_object_file_names(join(dirn, 'site-packages'))
4✔
450

451
        return join(dirn, 'site-packages')
4✔
452

453

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

© 2025 Coveralls, Inc