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

kivy / python-for-android / 22259302242

21 Feb 2026 03:24PM UTC coverage: 63.887% (+4.7%) from 59.214%
22259302242

Pull #3198

github

web-flow
Merge 758a52847 into 1fc026943
Pull Request #3198: Bump SDL3 (`3.4.2`) and SDL3_image (`3.4.0`) to the latest stable releases.

1823 of 3111 branches covered (58.6%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 2 files covered. (100.0%)

788 existing lines in 24 files now uncovered.

5287 of 8018 relevant lines covered (65.94%)

5.26 hits per line

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

69.74
/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
    _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!
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:
8!
UNCOV
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
        patches.append('patches/3.14_fix_remote_debug.patch')
8✔
82

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

200
    @property
8✔
201
    def link_version(self):
8✔
202
        '''return the python's library link version e.g. 3.7m, 3.8'''
203
        major, minor = self.major_minor_version_string.split('.')
8✔
204
        flags = ''
8✔
205
        if major == '3' and int(minor) < 8:
8!
206
            flags += 'm'
×
207
        return '{major}.{minor}{flags}'.format(
8✔
208
            major=major,
209
            minor=minor,
210
            flags=flags
211
        )
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✔
UNCOV
220
        return not isfile(join(self.link_root(arch.arch), self._libpython))
×
221

222
    def prebuild_arch(self, arch):
8✔
UNCOV
223
        super().prebuild_arch(arch)
×
UNCOV
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:
UNCOV
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!
UNCOV
288
                info(f'Activating flags for {library_name}')
×
UNCOV
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✔
UNCOV
304
        except IOError:
×
UNCOV
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:
UNCOV
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
        if self._p_version.minor >= 13 and self.disable_gil:
8!
UNCOV
321
            self.configure_args.append("--disable-gil")
×
322

323
        return env
8✔
324

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

467

468
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