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

kivy / python-for-android / 22285832031

22 Feb 2026 09:35PM UTC coverage: 63.683% (-0.2%) from 63.887%
22285832031

Pull #3283

github

web-flow
Merge 89bb823ed into 0635bc1e5
Pull Request #3283: Fixed Python version support.

1808 of 3104 branches covered (58.25%)

Branch coverage included in aggregate %.

6 of 23 new or added lines in 1 file covered. (26.09%)

1 existing line in 1 file now uncovered.

5278 of 8023 relevant lines covered (65.79%)

5.25 hits per line

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

61.34
/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.2'
8✔
58
    url = 'https://github.com/python/cpython/archive/refs/tags/v{version}.tar.gz'
8✔
59
    name = 'python3'
8✔
60

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

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

73
    configure_args = [
8✔
74
        '--host={android_host}',
75
        '--build={android_build}',
76
        '--enable-shared',
77
        '--enable-ipv6',
78
        '--enable-loadable-sqlite-extensions',
79
        '--without-static-libpython',
80
        '--without-readline',
81
        '--without-ensurepip',
82

83
        # Android prefix
84
        '--prefix={prefix}',
85
        '--exec-prefix={exec_prefix}',
86
        '--enable-loadable-sqlite-extensions',
87

88
        # Special cross compile args
89
        'ac_cv_file__dev_ptmx=yes',
90
        'ac_cv_file__dev_ptc=no',
91
        'ac_cv_header_sys_eventfd_h=no',
92
        'ac_cv_little_endian_double=yes',
93
        'ac_cv_header_bzlib_h=no',
94
    ]
95

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

101
    MIN_NDK_API = 21
8✔
102
    '''Sets the minimal ndk api number needed to use the recipe.
6✔
103

104
    .. warning:: This recipe can be built only against API 21+, so it means
105
        that any class which inherits from class:`GuestPythonRecipe` will have
106
        this limitation.
107
    '''
108

109
    stdlib_dir_blacklist = {
8✔
110
        '__pycache__',
111
        'test',
112
        'tests',
113
        'lib2to3',
114
        'ensurepip',
115
        'idlelib',
116
        'tkinter',
117
    }
118
    '''The directories that we want to omit for our python bundle'''
6✔
119

120
    stdlib_filen_blacklist = [
8✔
121
        '*.py',
122
        '*.exe',
123
        '*.whl',
124
    ]
125
    '''The file extensions that we want to blacklist for our python bundle'''
6✔
126

127
    site_packages_dir_blacklist = {
8✔
128
        '__pycache__',
129
        'tests'
130
    }
131
    '''The directories from site packages dir that we don't want to be included
6✔
132
    in our python bundle.'''
133

134
    site_packages_excluded_dir_exceptions = [
8✔
135
        # 'numpy' is excluded here because importing with `import numpy as np`
136
        # can fail if the `tests` directory inside the numpy package is excluded.
137
        'numpy',
138
    ]
139
    '''Directories from `site_packages_dir_blacklist` will not be excluded
6✔
140
    if the full path contains any of these exceptions.'''
141

142
    site_packages_filen_blacklist = [
8✔
143
        '*.py'
144
    ]
145
    '''The file extensions from site packages dir that we don't want to be
6✔
146
    included in our python bundle.'''
147

148
    compiled_extension = '.pyc'
8✔
149
    '''the default extension for compiled python files.
6✔
150

151
    .. note:: the default extension for compiled python files has been .pyo for
152
        python 2.x-3.4 but as of Python 3.5, the .pyo filename extension is no
153
        longer used and has been removed in favour of extension .pyc
154
    '''
155

156
    disable_gil = False
8✔
157
    '''python3.13 experimental free-threading build'''
6✔
158

159
    built_libraries = {"libpythonbin.so": "./android-build/"}
8✔
160

161
    def __init__(self, *args, **kwargs):
8✔
162
        self._ctx = None
8✔
163
        super().__init__(*args, **kwargs)
8✔
164

165
    @property
8✔
166
    def _libpython(self):
8✔
167
        '''return the python's library name (with extension)'''
168
        return 'libpython{link_version}.so'.format(
8✔
169
            link_version=self.link_version
170
        )
171

172
    @property
8✔
173
    def link_version(self):
8✔
174
        '''return the python's library link version e.g. 3.7m, 3.8'''
175
        major, minor = self.major_minor_version_string.split('.')
8✔
176
        flags = ''
8✔
177
        if major == '3' and int(minor) < 8:
8!
178
            flags += 'm'
×
179
        return '{major}.{minor}{flags}'.format(
8✔
180
            major=major,
181
            minor=minor,
182
            flags=flags
183
        )
184

185
    def apply_patches(self, arch, build_dir=None):
8✔
186

NEW
187
        _p_version = Version(self.version)
×
NEW
188
        if _p_version.major == 3 and _p_version.minor == 7:
×
NEW
189
            self.patches += [
×
190
                'patches/py3.7.1_fix-ctypes-util-find-library.patch',
191
                'patches/py3.7.1_fix-zlib-version.patch',
192
            ]
193

NEW
194
        if 8 <= _p_version.minor <= 10:
×
NEW
195
            self.patches.append('patches/py3.8.1.patch')
×
196

NEW
197
        if _p_version.minor >= 11:
×
NEW
198
            self.patches.append('patches/cpython-311-ctypes-find-library.patch')
×
199

NEW
200
        if _p_version.minor >= 14:
×
NEW
201
            self.patches.append('patches/3.14_armv7l_fix.patch')
×
NEW
202
            self.patches.append('patches/3.14_fix_remote_debug.patch')
×
203

NEW
204
        if shutil.which('lld') is not None:
×
NEW
205
            if _p_version.minor == 7:
×
NEW
206
                self.patches.append("patches/py3.7.1_fix_cortex_a8.patch")
×
NEW
207
            elif _p_version.minor >= 8:
×
NEW
208
                self.patches.append("patches/py3.8.1_fix_cortex_a8.patch")
×
209

NEW
210
        self.patches = list(set(self.patches))
×
NEW
211
        super().apply_patches(arch, build_dir)
×
212

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

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

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

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

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

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

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

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

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

254
        return env
8✔
255

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

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

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

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

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

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

320
        _p_version = Version(self.version)
8✔
321
        if _p_version.minor >= 11:
8!
322
            self.configure_args.append('--with-build-python={python_host_bin}')
8✔
323

324
        if _p_version.minor >= 13 and self.disable_gil:
8!
UNCOV
325
            self.configure_args.append("--disable-gil")
×
326

327
        self.configure_args = list(set(self.configure_args))
8✔
328

329
        return env
8✔
330

331
    def build_arch(self, arch):
8✔
332
        if self.ctx.ndk_api < self.MIN_NDK_API:
8✔
333
            raise BuildInterruptingException(
8✔
334
                NDK_API_LOWER_THAN_SUPPORTED_MESSAGE.format(
335
                    ndk_api=self.ctx.ndk_api, min_ndk_api=self.MIN_NDK_API
336
                ),
337
            )
338

339
        recipe_build_dir = self.get_build_dir(arch.arch)
8✔
340

341
        # Create a subdirectory to actually perform the build
342
        build_dir = join(recipe_build_dir, 'android-build')
8✔
343
        ensure_dir(build_dir)
8✔
344

345
        # TODO: Get these dynamically, like bpo-30386 does
346
        sys_prefix = '/usr/local'
8✔
347
        sys_exec_prefix = '/usr/local'
8✔
348

349
        env = self.get_recipe_env(arch)
8✔
350
        env = self.set_libs_flags(env, arch)
8✔
351

352
        android_build = sh.Command(
8✔
353
            join(recipe_build_dir,
354
                 'config.guess'))().strip()
355

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

370
            shprint(
8✔
371
                sh.make,
372
                'all',
373
                'INSTSONAME={lib_name}'.format(lib_name=self._libpython),
374
                _env=env
375
            )
376
            # rename executable
377
            if isfile("python"):
8!
378
                sh.cp('python', 'libpythonbin.so')
×
379
            elif isfile("python.exe"):  # for macos
8!
380
                sh.cp('python.exe', 'libpythonbin.so')
×
381

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

386
    def compile_python_files(self, dir):
8✔
387
        '''
388
        Compile the python files (recursively) for the python files inside
389
        a given folder.
390

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

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

418
        # Bundle compiled python modules to a folder
419
        modules_dir = join(dirn, 'modules')
×
420
        c_ext = self.compiled_extension
×
421
        ensure_dir(modules_dir)
×
422
        module_filens = (glob.glob(join(modules_build_dir, '*.so')) +
×
423
                         glob.glob(join(modules_build_dir, '*' + c_ext)))
424
        info("Copy {} files into the bundle".format(len(module_filens)))
×
425
        for filen in module_filens:
×
426
            info(" - copy {}".format(filen))
×
427
            shutil.copy2(filen, modules_dir)
×
428

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

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

458
        # copy the python .so files into place
459
        python_build_dir = join(self.get_build_dir(arch.arch),
×
460
                                'android-build')
461
        python_lib_name = 'libpython' + self.link_version
×
462
        shprint(
×
463
            sh.cp,
464
            join(python_build_dir, python_lib_name + '.so'),
465
            join(self.ctx.bootstrap.dist_dir, 'libs', arch.arch)
466
        )
467

468
        info('Renaming .so files to reflect cross-compile')
×
469
        self.reduce_object_file_names(join(dirn, 'site-packages'))
×
470

471
        return join(dirn, 'site-packages')
×
472

473

474
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