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

kivy / python-for-android / 18319816173

07 Oct 2025 04:45PM UTC coverage: 59.087% (-0.04%) from 59.122%
18319816173

Pull #3180

github

web-flow
Merge 4592ee3db into 3c5b66d67
Pull Request #3180: `python`: add `3.14` support

1069 of 2411 branches covered (44.34%)

Branch coverage included in aggregate %.

91 of 129 new or added lines in 6 files covered. (70.54%)

10 existing lines in 3 files now uncovered.

4988 of 7840 relevant lines covered (63.62%)

2.53 hits per line

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

83.26
/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, isfile
4✔
7
import shutil
4✔
8

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

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

24

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

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

67
    if _p_version.major == 3 and _p_version.minor == 7:
4!
UNCOV
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:
4!
NEW
74
        patches.append('patches/py3.8.1.patch')
×
75

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

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

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

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

95
    configure_args = [
4✔
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:
4!
119
        configure_args.extend([
4✔
120
            '--with-build-python={python_host_bin}',
121
        ])
122

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

128
    MIN_NDK_API = 21
4✔
129
    '''Sets the minimal ndk api number needed to use the recipe.
2✔
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 = {
4✔
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'''
2✔
146

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

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

161
    site_packages_excluded_dir_exceptions = [
4✔
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
2✔
167
    if the full path contains any of these exceptions.'''
168

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

175
    compiled_extension = '.pyc'
4✔
176
    '''the default extension for compiled python files.
2✔
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
4✔
184
    '''python3.13 experimental free-threading build'''
2✔
185

186
    def __init__(self, *args, **kwargs):
4✔
187
        self._ctx = None
4✔
188
        super().__init__(*args, **kwargs)
4✔
189

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

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

210
    def include_root(self, arch_name):
4✔
211
        return join(self.get_build_dir(arch_name), 'Include')
4✔
212

213
    def link_root(self, arch_name):
4✔
214
        return join(self.get_build_dir(arch_name), 'android-build')
4✔
215

216
    def should_build(self, arch):
4✔
NEW
217
        return not isfile(join(self.link_root(arch.arch), self._libpython))
×
218

219
    def prebuild_arch(self, arch):
4✔
220
        super().prebuild_arch(arch)
×
221
        self.ctx.python_recipe = self
×
222

223
    def get_recipe_env(self, arch=None, with_flags_in_cc=True):
4✔
224
        env = super().get_recipe_env(arch)
4✔
225
        env['HOSTARCH'] = arch.command_prefix
4✔
226

227
        env['CC'] = arch.get_clang_exe(with_target=True)
4✔
228

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

235
        env['CFLAGS'] = ' '.join(
4✔
236
            [
237
                '-fPIC',
238
                '-DANDROID'
239
            ]
240
        )
241

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

251
        return env
4✔
252

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

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

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

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

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

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

317
        if self._p_version.minor >= 13 and self.disable_gil:
4!
NEW
318
            self.configure_args.append("--disable-gil")
×
319

320
        return env
4✔
321

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

330
        recipe_build_dir = self.get_build_dir(arch.arch)
4✔
331

332
        # Create a subdirectory to actually perform the build
333
        build_dir = join(recipe_build_dir, 'android-build')
4✔
334
        ensure_dir(build_dir)
4✔
335

336
        # TODO: Get these dynamically, like bpo-30386 does
337
        sys_prefix = '/usr/local'
4✔
338
        sys_exec_prefix = '/usr/local'
4✔
339

340
        env = self.get_recipe_env(arch)
4✔
341
        env = self.set_libs_flags(env, arch)
4✔
342

343
        android_build = sh.Command(
4✔
344
            join(recipe_build_dir,
345
                 'config.guess'))().strip()
346

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

361
            shprint(
4✔
362
                sh.make,
363
                'all',
364
                'INSTSONAME={lib_name}'.format(lib_name=self._libpython),
365
                _env=env
366
            )
367

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

372
    def compile_python_files(self, dir):
4✔
373
        '''
374
        Compile the python files (recursively) for the python files inside
375
        a given folder.
376

377
        .. note:: python2 compiles the files into extension .pyo, but in
378
            python3, and as of Python 3.5, the .pyo filename extension is no
379
            longer used...uses .pyc (https://www.python.org/dev/peps/pep-0488)
380
        '''
381
        args = [self.ctx.hostpython]
4✔
382
        args += ['-OO', '-m', 'compileall', '-b', '-f', dir]
4✔
383
        subprocess.call(args)
4✔
384

385
    def create_python_bundle(self, dirn, arch):
4✔
386
        """
387
        Create a packaged python bundle in the target directory, by
388
        copying all the modules and standard library to the right
389
        place.
390
        """
391
        # Todo: find a better way to find the build libs folder
392
        modules_build_dir = join(
4✔
393
            self.get_build_dir(arch.arch),
394
            'android-build',
395
            'build',
396
            'lib.{}{}-{}-{}'.format(
397
                # android is now supported platform
398
                "android" if self._p_version.minor >= 13 else "linux",
399
                '2' if self.version[0] == '2' else '',
400
                arch.command_prefix.split('-')[0],
401
                self.major_minor_version_string
402
                ))
403

404
        # Compile to *.pyc the python modules
405
        self.compile_python_files(modules_build_dir)
4✔
406
        # Compile to *.pyc the standard python library
407
        self.compile_python_files(join(self.get_build_dir(arch.arch), 'Lib'))
4✔
408
        # Compile to *.pyc the other python packages (site-packages)
409
        self.compile_python_files(self.ctx.get_python_install_dir(arch.arch))
4✔
410

411
        # Bundle compiled python modules to a folder
412
        modules_dir = join(dirn, 'modules')
4✔
413
        c_ext = self.compiled_extension
4✔
414
        ensure_dir(modules_dir)
4✔
415
        module_filens = (glob.glob(join(modules_build_dir, '*.so')) +
4✔
416
                         glob.glob(join(modules_build_dir, '*' + c_ext)))
417
        info("Copy {} files into the bundle".format(len(module_filens)))
4✔
418
        for filen in module_filens:
4!
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')
4✔
424
        with current_directory(join(self.get_build_dir(arch.arch), 'Lib')):
4✔
425
            stdlib_filens = list(walk_valid_filens(
4✔
426
                '.', self.stdlib_dir_blacklist, self.stdlib_filen_blacklist))
427
            if 'SOURCE_DATE_EPOCH' in environ:
4!
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)))
4✔
434
            shprint(sh.zip, '-X', stdlib_zip, *stdlib_filens)
4✔
435

436
        # copy the site-packages into place
437
        ensure_dir(join(dirn, 'site-packages'))
4✔
438
        ensure_dir(self.ctx.get_python_install_dir(arch.arch))
4✔
439
        # TODO: Improve the API around walking and copying the files
440
        with current_directory(self.ctx.get_python_install_dir(arch.arch)):
4✔
441
            filens = list(walk_valid_filens(
4✔
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)))
4✔
446
            for filen in filens:
4✔
447
                info(" - copy {}".format(filen))
4✔
448
                ensure_dir(join(dirn, 'site-packages', dirname(filen)))
4✔
449
                shutil.copy2(filen, join(dirn, 'site-packages', filen))
4✔
450

451
        # copy the python .so files into place
452
        python_build_dir = join(self.get_build_dir(arch.arch),
4✔
453
                                'android-build')
454
        python_lib_name = 'libpython' + self.link_version
4✔
455
        shprint(
4✔
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')
4✔
462
        self.reduce_object_file_names(join(dirn, 'site-packages'))
4✔
463

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

466

467
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

© 2026 Coveralls, Inc