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

kivy / python-for-android / 18248007425

04 Oct 2025 06:30PM UTC coverage: 59.09% (-0.03%) from 59.122%
18248007425

Pull #3180

github

web-flow
Merge 7473c1e8e into 5aa97321e
Pull Request #3180: `python`: add `3.13` support

1068 of 2409 branches covered (44.33%)

Branch coverage included in aggregate %.

90 of 127 new or added lines in 6 files covered. (70.87%)

10 existing lines in 3 files now uncovered.

4987 of 7838 relevant lines covered (63.63%)

2.53 hits per line

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

83.83
/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 shutil.which('lld') is not None:
4✔
80
        if _p_version.minor == 7:
4!
NEW
81
            patches.append("patches/py3.7.1_fix_cortex_a8.patch")
×
82
        elif _p_version.minor >= 8:
4!
83
            patches.append("patches/py3.8.1_fix_cortex_a8.patch")
4✔
84

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

92
    configure_args = [
4✔
93
        '--host={android_host}',
94
        '--build={android_build}',
95
        '--enable-shared',
96
        '--enable-ipv6',
97
        '--enable-loadable-sqlite-extensions',
98
        '--without-static-libpython',
99
        '--without-readline',
100
        '--without-ensurepip',
101

102
        # Android prefix
103
        '--prefix={prefix}',
104
        '--exec-prefix={exec_prefix}',
105
        '--enable-loadable-sqlite-extensions',
106

107
        # Special cross compile args
108
        'ac_cv_file__dev_ptmx=yes',
109
        'ac_cv_file__dev_ptc=no',
110
        'ac_cv_header_sys_eventfd_h=no',
111
        'ac_cv_little_endian_double=yes',
112
        'ac_cv_header_bzlib_h=no',
113
    ]
114

115
    if _p_version.minor >= 11:
4!
116
        configure_args.extend([
4✔
117
            '--with-build-python={python_host_bin}',
118
        ])
119

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

125
    MIN_NDK_API = 21
4✔
126
    '''Sets the minimal ndk api number needed to use the recipe.
2✔
127

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

133
    stdlib_dir_blacklist = {
4✔
134
        '__pycache__',
135
        'test',
136
        'tests',
137
        'lib2to3',
138
        'ensurepip',
139
        'idlelib',
140
        'tkinter',
141
    }
142
    '''The directories that we want to omit for our python bundle'''
2✔
143

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

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

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

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

172
    compiled_extension = '.pyc'
4✔
173
    '''the default extension for compiled python files.
2✔
174

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

180
    disable_gil = False
4✔
181
    '''python3.13 experimental free-threading build'''
2✔
182

183
    def __init__(self, *args, **kwargs):
4✔
184
        self._ctx = None
4✔
185
        super().__init__(*args, **kwargs)
4✔
186

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

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

207
    def include_root(self, arch_name):
4✔
208
        return join(self.get_build_dir(arch_name), 'Include')
4✔
209

210
    def link_root(self, arch_name):
4✔
211
        return join(self.get_build_dir(arch_name), 'android-build')
4✔
212

213
    def should_build(self, arch):
4✔
NEW
214
        return not isfile(join(self.link_root(arch.arch), self._libpython))
×
215

216
    def prebuild_arch(self, arch):
4✔
217
        super().prebuild_arch(arch)
×
218
        self.ctx.python_recipe = self
×
219

220
    def get_recipe_env(self, arch=None, with_flags_in_cc=True):
4✔
221
        env = super().get_recipe_env(arch)
4✔
222
        env['HOSTARCH'] = arch.command_prefix
4✔
223

224
        env['CC'] = arch.get_clang_exe(with_target=True)
4✔
225

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

232
        env['CFLAGS'] = ' '.join(
4✔
233
            [
234
                '-fPIC',
235
                '-DANDROID'
236
            ]
237
        )
238

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

248
        return env
4✔
249

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

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

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

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

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

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

314
        if self._p_version.minor >= 13 and self.disable_gil:
4!
NEW
315
            self.configure_args.append("--disable-gil")
×
316

317
        return env
4✔
318

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

327
        recipe_build_dir = self.get_build_dir(arch.arch)
4✔
328

329
        # Create a subdirectory to actually perform the build
330
        build_dir = join(recipe_build_dir, 'android-build')
4✔
331
        ensure_dir(build_dir)
4✔
332

333
        # TODO: Get these dynamically, like bpo-30386 does
334
        sys_prefix = '/usr/local'
4✔
335
        sys_exec_prefix = '/usr/local'
4✔
336

337
        env = self.get_recipe_env(arch)
4✔
338
        env = self.set_libs_flags(env, arch)
4✔
339

340
        android_build = sh.Command(
4✔
341
            join(recipe_build_dir,
342
                 'config.guess'))().strip()
343

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

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

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

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

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

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

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

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

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

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

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

458
        info('Renaming .so files to reflect cross-compile')
4✔
459
        self.reduce_object_file_names(join(dirn, 'site-packages'))
4✔
460

461
        return join(dirn, 'site-packages')
4✔
462

463

464
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