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

kivy / python-for-android / 21117072110

18 Jan 2026 07:07PM UTC coverage: 63.828%. First build
21117072110

Pull #3276

github

web-flow
Merge 11fdc481e into 6494ac165
Pull Request #3276: `ffmpeg`, `python3`: include executable

1788 of 3060 branches covered (58.43%)

Branch coverage included in aggregate %.

12 of 19 new or added lines in 3 files covered. (63.16%)

5221 of 7921 relevant lines covered (65.91%)

5.26 hits per line

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

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

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

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

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

24

25
class Python3Recipe(TargetPythonRecipe):
8✔
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.0'
8✔
58
    _p_version = Version(version)
8✔
59
    url = 'https://github.com/python/cpython/archive/refs/tags/v{version}.tar.gz'
8✔
60
    name = 'python3'
8✔
61

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

67
    if _p_version.major == 3 and _p_version.minor == 7:
8!
68
        patches += [
×
69
            'patches/py3.7.1_fix-ctypes-util-find-library.patch',
70
            'patches/py3.7.1_fix-zlib-version.patch',
71
        ]
72

73
    if 8 <= _p_version.minor <= 10:
8!
74
        patches.append('patches/py3.8.1.patch')
×
75

76
    if _p_version.minor >= 11:
8!
77
        patches.append('patches/cpython-311-ctypes-find-library.patch')
8✔
78

79
    if _p_version.minor >= 14:
8!
80
        patches.append('patches/3.14_armv7l_fix.patch')
8✔
81

82
    if shutil.which('lld') is not None:
8✔
83
        if _p_version.minor == 7:
8!
84
            patches.append("patches/py3.7.1_fix_cortex_a8.patch")
×
85
        elif _p_version.minor >= 8:
8!
86
            patches.append("patches/py3.8.1_fix_cortex_a8.patch")
8✔
87

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

95
    configure_args = [
8✔
96
        '--host={android_host}',
97
        '--build={android_build}',
98
        '--enable-shared',
99
        '--enable-ipv6',
100
        '--enable-loadable-sqlite-extensions',
101
        '--without-static-libpython',
102
        '--without-readline',
103
        '--without-ensurepip',
104

105
        # Android prefix
106
        '--prefix={prefix}',
107
        '--exec-prefix={exec_prefix}',
108
        '--enable-loadable-sqlite-extensions',
109

110
        # Special cross compile args
111
        'ac_cv_file__dev_ptmx=yes',
112
        'ac_cv_file__dev_ptc=no',
113
        'ac_cv_header_sys_eventfd_h=no',
114
        'ac_cv_little_endian_double=yes',
115
        'ac_cv_header_bzlib_h=no',
116
    ]
117

118
    if _p_version.minor >= 11:
8!
119
        configure_args.extend([
8✔
120
            '--with-build-python={python_host_bin}',
121
        ])
122

123
    '''The configure arguments needed to build the python recipe. Those are
6✔
124
    used in method :meth:`build_arch` (if not overwritten like python3's
125
    recipe does).
126
    '''
127

128
    MIN_NDK_API = 21
8✔
129
    '''Sets the minimal ndk api number needed to use the recipe.
6✔
130

131
    .. warning:: This recipe can be built only against API 21+, so it means
132
        that any class which inherits from class:`GuestPythonRecipe` will have
133
        this limitation.
134
    '''
135

136
    stdlib_dir_blacklist = {
8✔
137
        '__pycache__',
138
        'test',
139
        'tests',
140
        'lib2to3',
141
        'ensurepip',
142
        'idlelib',
143
        'tkinter',
144
    }
145
    '''The directories that we want to omit for our python bundle'''
6✔
146

147
    stdlib_filen_blacklist = [
8✔
148
        '*.py',
149
        '*.exe',
150
        '*.whl',
151
    ]
152
    '''The file extensions that we want to blacklist for our python bundle'''
6✔
153

154
    site_packages_dir_blacklist = {
8✔
155
        '__pycache__',
156
        'tests'
157
    }
158
    '''The directories from site packages dir that we don't want to be included
6✔
159
    in our python bundle.'''
160

161
    site_packages_excluded_dir_exceptions = [
8✔
162
        # 'numpy' is excluded here because importing with `import numpy as np`
163
        # can fail if the `tests` directory inside the numpy package is excluded.
164
        'numpy',
165
    ]
166
    '''Directories from `site_packages_dir_blacklist` will not be excluded
6✔
167
    if the full path contains any of these exceptions.'''
168

169
    site_packages_filen_blacklist = [
8✔
170
        '*.py'
171
    ]
172
    '''The file extensions from site packages dir that we don't want to be
6✔
173
    included in our python bundle.'''
174

175
    compiled_extension = '.pyc'
8✔
176
    '''the default extension for compiled python files.
6✔
177

178
    .. note:: the default extension for compiled python files has been .pyo for
179
        python 2.x-3.4 but as of Python 3.5, the .pyo filename extension is no
180
        longer used and has been removed in favour of extension .pyc
181
    '''
182

183
    disable_gil = False
8✔
184
    '''python3.13 experimental free-threading build'''
6✔
185

186
    built_libraries = {"libpythonbin.so": "./android-build/"}
8✔
187

188
    def __init__(self, *args, **kwargs):
8✔
189
        self._ctx = None
8✔
190
        super().__init__(*args, **kwargs)
8✔
191

192
    @property
8✔
193
    def _libpython(self):
8✔
194
        '''return the python's library name (with extension)'''
195
        return 'libpython{link_version}.so'.format(
8✔
196
            link_version=self.link_version
197
        )
198

199
    @property
8✔
200
    def link_version(self):
8✔
201
        '''return the python's library link version e.g. 3.7m, 3.8'''
202
        major, minor = self.major_minor_version_string.split('.')
8✔
203
        flags = ''
8✔
204
        if major == '3' and int(minor) < 8:
8!
205
            flags += 'm'
×
206
        return '{major}.{minor}{flags}'.format(
8✔
207
            major=major,
208
            minor=minor,
209
            flags=flags
210
        )
211

212
    def include_root(self, arch_name):
8✔
213
        return join(self.get_build_dir(arch_name), 'Include')
8✔
214

215
    def link_root(self, arch_name):
8✔
216
        return join(self.get_build_dir(arch_name), 'android-build')
8✔
217

218
    def should_build(self, arch):
8✔
219
        return not isfile(join(self.link_root(arch.arch), self._libpython))
×
220

221
    def prebuild_arch(self, arch):
8✔
222
        super().prebuild_arch(arch)
×
223
        self.ctx.python_recipe = self
×
224

225
    def get_recipe_env(self, arch=None, with_flags_in_cc=True):
8✔
226
        env = super().get_recipe_env(arch)
8✔
227
        env['HOSTARCH'] = arch.command_prefix
8✔
228

229
        env['CC'] = arch.get_clang_exe(with_target=True)
8✔
230

231
        env['PATH'] = (
8✔
232
            '{hostpython_dir}:{old_path}').format(
233
                hostpython_dir=self.get_recipe(
234
                    'host' + self.name, self.ctx).get_path_to_python(),
235
                old_path=env['PATH'])
236

237
        env['CFLAGS'] = ' '.join(
8✔
238
            [
239
                '-fPIC',
240
                '-DANDROID'
241
            ]
242
        )
243

244
        env['LDFLAGS'] = env.get('LDFLAGS', '')
8✔
245
        if shutil.which('lld') is not None:
8!
246
            # Note: The -L. is to fix a bug in python 3.7.
247
            # https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234409
248
            env['LDFLAGS'] += ' -L. -fuse-ld=lld'
8✔
249
        else:
250
            warning('lld not found, linking without it. '
×
251
                    'Consider installing lld if linker errors occur.')
252

253
        return env
8✔
254

255
    def set_libs_flags(self, env, arch):
8✔
256
        '''Takes care to properly link libraries with python depending on our
257
        requirements and the attribute :attr:`opt_depends`.
258
        '''
259
        def add_flags(include_flags, link_dirs, link_libs):
8✔
260
            env['CPPFLAGS'] = env.get('CPPFLAGS', '') + include_flags
8✔
261
            env['LDFLAGS'] = env.get('LDFLAGS', '') + link_dirs
8✔
262
            env['LIBS'] = env.get('LIBS', '') + link_libs
8✔
263

264
        info('Activating flags for sqlite3')
8✔
265
        recipe = Recipe.get_recipe('sqlite3', self.ctx)
8✔
266
        add_flags(' -I' + recipe.get_build_dir(arch.arch),
8✔
267
                  ' -L' + recipe.get_build_dir(arch.arch), ' -lsqlite3')
268

269
        info('Activating flags for libffi')
8✔
270
        recipe = Recipe.get_recipe('libffi', self.ctx)
8✔
271
        # In order to force the correct linkage for our libffi library, we
272
        # set the following variable to point where is our libffi.pc file,
273
        # because the python build system uses pkg-config to configure it.
274
        env['PKG_CONFIG_LIBDIR'] = recipe.get_build_dir(arch.arch)
8✔
275
        add_flags(' -I' + ' -I'.join(recipe.get_include_dirs(arch)),
8✔
276
                  ' -L' + join(recipe.get_build_dir(arch.arch), '.libs'),
277
                  ' -lffi')
278

279
        info('Activating flags for openssl')
8✔
280
        recipe = Recipe.get_recipe('openssl', self.ctx)
8✔
281
        self.configure_args.append('--with-openssl=' + recipe.get_build_dir(arch.arch))
8✔
282
        add_flags(recipe.include_flags(arch),
8✔
283
                  recipe.link_dirs_flags(arch), recipe.link_libs_flags())
284

285
        for library_name in {'libbz2', 'liblzma'}:
8✔
286
            if library_name in self.ctx.recipe_build_order:
8!
287
                info(f'Activating flags for {library_name}')
×
288
                recipe = Recipe.get_recipe(library_name, self.ctx)
×
289
                add_flags(recipe.get_library_includes(arch),
×
290
                          recipe.get_library_ldflags(arch),
291
                          recipe.get_library_libs_flag())
292

293
        # python build system contains hardcoded zlib version which prevents
294
        # the build of zlib module, here we search for android's zlib version
295
        # and sets the right flags, so python can be build with android's zlib
296
        info("Activating flags for android's zlib")
8✔
297
        zlib_lib_path = arch.ndk_lib_dir_versioned
8✔
298
        zlib_includes = self.ctx.ndk.sysroot_include_dir
8✔
299
        zlib_h = join(zlib_includes, 'zlib.h')
8✔
300
        try:
8✔
301
            with open(zlib_h) as fileh:
8✔
302
                zlib_data = fileh.read()
8✔
303
        except IOError:
×
304
            raise BuildInterruptingException(
×
305
                "Could not determine android's zlib version, no zlib.h ({}) in"
306
                " the NDK dir includes".format(zlib_h)
307
            )
308
        for line in zlib_data.split('\n'):
8!
309
            if line.startswith('#define ZLIB_VERSION '):
8!
310
                break
8✔
311
        else:
312
            raise BuildInterruptingException(
×
313
                'Could not parse zlib.h...so we cannot find zlib version,'
314
                'required by python build,'
315
            )
316
        env['ZLIB_VERSION'] = line.replace('#define ZLIB_VERSION ', '')
8✔
317
        add_flags(' -I' + zlib_includes, ' -L' + zlib_lib_path, ' -lz')
8✔
318

319
        if self._p_version.minor >= 13 and self.disable_gil:
8!
320
            self.configure_args.append("--disable-gil")
×
321

322
        return env
8✔
323

324
    def build_arch(self, arch):
8✔
325
        if self.ctx.ndk_api < self.MIN_NDK_API:
8✔
326
            raise BuildInterruptingException(
8✔
327
                NDK_API_LOWER_THAN_SUPPORTED_MESSAGE.format(
328
                    ndk_api=self.ctx.ndk_api, min_ndk_api=self.MIN_NDK_API
329
                ),
330
            )
331

332
        recipe_build_dir = self.get_build_dir(arch.arch)
8✔
333

334
        # Create a subdirectory to actually perform the build
335
        build_dir = join(recipe_build_dir, 'android-build')
8✔
336
        ensure_dir(build_dir)
8✔
337

338
        # TODO: Get these dynamically, like bpo-30386 does
339
        sys_prefix = '/usr/local'
8✔
340
        sys_exec_prefix = '/usr/local'
8✔
341

342
        env = self.get_recipe_env(arch)
8✔
343
        env = self.set_libs_flags(env, arch)
8✔
344

345
        android_build = sh.Command(
8✔
346
            join(recipe_build_dir,
347
                 'config.guess'))().strip()
348

349
        with current_directory(build_dir):
8✔
350
            if not exists('config.status'):
8!
351
                shprint(
8✔
352
                    sh.Command(join(recipe_build_dir, 'configure')),
353
                    *(' '.join(self.configure_args).format(
354
                                    android_host=env['HOSTARCH'],
355
                                    android_build=android_build,
356
                                    python_host_bin=join(self.get_recipe(
357
                                        'host' + self.name, self.ctx
358
                                    ).get_path_to_python(), "python3"),
359
                                    prefix=sys_prefix,
360
                                    exec_prefix=sys_exec_prefix)).split(' '),
361
                    _env=env)
362

363
            shprint(
8✔
364
                sh.make,
365
                'all',
366
                'INSTSONAME={lib_name}'.format(lib_name=self._libpython),
367
                _env=env
368
            )
369
            # rename executable
370
            if isfile("python"):
8!
NEW
371
                sh.cp('python', 'libpythonbin.so')
×
372
            elif isfile("python.exe"):  # for macos
8!
NEW
373
                sh.cp('python.exe', 'libpythonbin.so')
×
374

375
            # TODO: Look into passing the path to pyconfig.h in a
376
            # better way, although this is probably acceptable
377
            sh.cp('pyconfig.h', join(recipe_build_dir, 'Include'))
8✔
378

379
    def compile_python_files(self, dir):
8✔
380
        '''
381
        Compile the python files (recursively) for the python files inside
382
        a given folder.
383

384
        .. note:: python2 compiles the files into extension .pyo, but in
385
            python3, and as of Python 3.5, the .pyo filename extension is no
386
            longer used...uses .pyc (https://www.python.org/dev/peps/pep-0488)
387
        '''
388
        args = [self.ctx.hostpython]
8✔
389
        args += ['-OO', '-m', 'compileall', '-b', '-f', dir]
8✔
390
        subprocess.call(args)
8✔
391

392
    def create_python_bundle(self, dirn, arch):
8✔
393
        """
394
        Create a packaged python bundle in the target directory, by
395
        copying all the modules and standard library to the right
396
        place.
397
        """
398
        modules_build_dir = glob.glob(join(
×
399
            self.get_build_dir(arch.arch),
400
            'android-build',
401
            'build',
402
            'lib.*'
403
        ))[0]
404
        # Compile to *.pyc the python modules
405
        self.compile_python_files(modules_build_dir)
×
406
        # Compile to *.pyc the standard python library
407
        self.compile_python_files(join(self.get_build_dir(arch.arch), 'Lib'))
×
408
        # Compile to *.pyc the other python packages (site-packages)
409
        self.compile_python_files(self.ctx.get_python_install_dir(arch.arch))
×
410

411
        # Bundle compiled python modules to a folder
412
        modules_dir = join(dirn, 'modules')
×
413
        c_ext = self.compiled_extension
×
414
        ensure_dir(modules_dir)
×
415
        module_filens = (glob.glob(join(modules_build_dir, '*.so')) +
×
416
                         glob.glob(join(modules_build_dir, '*' + c_ext)))
417
        info("Copy {} files into the bundle".format(len(module_filens)))
×
418
        for filen in module_filens:
×
419
            info(" - copy {}".format(filen))
×
420
            shutil.copy2(filen, modules_dir)
×
421

422
        # zip up the standard library
423
        stdlib_zip = join(dirn, 'stdlib.zip')
×
424
        with current_directory(join(self.get_build_dir(arch.arch), 'Lib')):
×
425
            stdlib_filens = list(walk_valid_filens(
×
426
                '.', self.stdlib_dir_blacklist, self.stdlib_filen_blacklist))
427
            if 'SOURCE_DATE_EPOCH' in environ:
×
428
                # for reproducible builds
429
                stdlib_filens.sort()
×
430
                timestamp = int(environ['SOURCE_DATE_EPOCH'])
×
431
                for filen in stdlib_filens:
×
432
                    utime(filen, (timestamp, timestamp))
×
433
            info("Zip {} files into the bundle".format(len(stdlib_filens)))
×
434
            shprint(sh.zip, '-X', stdlib_zip, *stdlib_filens)
×
435

436
        # copy the site-packages into place
437
        ensure_dir(join(dirn, 'site-packages'))
×
438
        ensure_dir(self.ctx.get_python_install_dir(arch.arch))
×
439
        # TODO: Improve the API around walking and copying the files
440
        with current_directory(self.ctx.get_python_install_dir(arch.arch)):
×
441
            filens = list(walk_valid_filens(
×
442
                '.', self.site_packages_dir_blacklist,
443
                self.site_packages_filen_blacklist,
444
                excluded_dir_exceptions=self.site_packages_excluded_dir_exceptions))
445
            info("Copy {} files into the site-packages".format(len(filens)))
×
446
            for filen in filens:
×
447
                info(" - copy {}".format(filen))
×
448
                ensure_dir(join(dirn, 'site-packages', dirname(filen)))
×
449
                shutil.copy2(filen, join(dirn, 'site-packages', filen))
×
450

451
        # copy the python .so files into place
452
        python_build_dir = join(self.get_build_dir(arch.arch),
×
453
                                'android-build')
454
        python_lib_name = 'libpython' + self.link_version
×
455
        shprint(
×
456
            sh.cp,
457
            join(python_build_dir, python_lib_name + '.so'),
458
            join(self.ctx.bootstrap.dist_dir, 'libs', arch.arch)
459
        )
460

461
        info('Renaming .so files to reflect cross-compile')
×
462
        self.reduce_object_file_names(join(dirn, 'site-packages'))
×
463

464
        return join(dirn, 'site-packages')
×
465

466

467
recipe = Python3Recipe()
8✔
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

© 2026 Coveralls, Inc