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

kivy / python-for-android / 22278642113

22 Feb 2026 02:05PM UTC coverage: 63.685% (-0.2%) from 63.887%
22278642113

Pull #3283

github

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

1808 of 3104 branches covered (58.25%)

Branch coverage included in aggregate %.

5 of 21 new or added lines in 1 file covered. (23.81%)

1 existing line in 1 file now uncovered.

5277 of 8021 relevant lines covered (65.79%)

5.25 hits per line

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

61.42
/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")
×
NEW
209
        super().apply_patches(arch, build_dir)
×
210

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

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

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

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

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

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

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

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

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

252
        return env
8✔
253

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

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

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

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

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

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

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

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

325
        return env
8✔
326

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

335
        recipe_build_dir = self.get_build_dir(arch.arch)
8✔
336

337
        # Create a subdirectory to actually perform the build
338
        build_dir = join(recipe_build_dir, 'android-build')
8✔
339
        ensure_dir(build_dir)
8✔
340

341
        # TODO: Get these dynamically, like bpo-30386 does
342
        sys_prefix = '/usr/local'
8✔
343
        sys_exec_prefix = '/usr/local'
8✔
344

345
        env = self.get_recipe_env(arch)
8✔
346
        env = self.set_libs_flags(env, arch)
8✔
347

348
        android_build = sh.Command(
8✔
349
            join(recipe_build_dir,
350
                 'config.guess'))().strip()
351

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

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

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

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

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

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

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

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

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

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

464
        info('Renaming .so files to reflect cross-compile')
×
465
        self.reduce_object_file_names(join(dirn, 'site-packages'))
×
466

467
        return join(dirn, 'site-packages')
×
468

469

470
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