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

kivy / python-for-android / 22275931603

22 Feb 2026 11:06AM UTC coverage: 63.456% (-0.4%) from 63.887%
22275931603

Pull #3280

github

web-flow
Merge a6b79cb23 into 6e558e430
Pull Request #3280: Add support for prebuilt wheels

1827 of 3145 branches covered (58.09%)

Branch coverage included in aggregate %.

52 of 123 new or added lines in 5 files covered. (42.28%)

2 existing lines in 1 file now uncovered.

5315 of 8110 relevant lines covered (65.54%)

5.23 hits per line

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

62.32
/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
    @property
8✔
62
    def _p_version(self):
8✔
63
        # as version is dynamic
64
        return Version(self.version)
8✔
65

66
    @property
8✔
67
    def patches(self):
8✔
NEW
68
        patches = [
×
69
            'patches/pyconfig_detection.patch',
70
            'patches/reproducible-buildinfo.diff',
71
        ]
NEW
72
        _p_version = self._p_version
×
73

NEW
74
        if _p_version.major == 3 and _p_version.minor == 7:
×
NEW
75
            patches += [
×
76
                'patches/py3.7.1_fix-ctypes-util-find-library.patch',
77
                'patches/py3.7.1_fix-zlib-version.patch',
78
            ]
79

NEW
80
        if 8 <= _p_version.minor <= 10:
×
NEW
81
            patches.append('patches/py3.8.1.patch')
×
82

NEW
83
        if _p_version.minor >= 11:
×
NEW
84
            patches.append('patches/cpython-311-ctypes-find-library.patch')
×
85

NEW
86
        if _p_version.minor >= 14:
×
NEW
87
            patches.append('patches/3.14_armv7l_fix.patch')
×
NEW
88
            patches.append('patches/3.14_fix_remote_debug.patch')
×
89

NEW
90
        if shutil.which('lld') is not None:
×
NEW
91
            if _p_version.minor == 7:
×
NEW
92
                patches.append("patches/py3.7.1_fix_cortex_a8.patch")
×
NEW
93
            elif _p_version.minor >= 8:
×
NEW
94
                patches.append("patches/py3.8.1_fix_cortex_a8.patch")
×
95

NEW
96
        return patches
×
97

98
    depends = ['hostpython3', 'sqlite3', 'openssl', 'libffi']
8✔
99
    # those optional depends allow us to build python compression modules:
100
    #   - _bz2.so
101
    #   - _lzma.so
102
    opt_depends = ['libbz2', 'liblzma']
8✔
103
    '''The optional libraries which we would like to get our python linked'''
6✔
104

105
    configure_args = [
8✔
106
        '--host={android_host}',
107
        '--build={android_build}',
108
        '--enable-shared',
109
        '--enable-ipv6',
110
        '--enable-loadable-sqlite-extensions',
111
        '--without-static-libpython',
112
        '--without-readline',
113
        '--without-ensurepip',
114

115
        # Android prefix
116
        '--prefix={prefix}',
117
        '--exec-prefix={exec_prefix}',
118
        '--enable-loadable-sqlite-extensions',
119

120
        # Special cross compile args
121
        'ac_cv_file__dev_ptmx=yes',
122
        'ac_cv_file__dev_ptc=no',
123
        'ac_cv_header_sys_eventfd_h=no',
124
        'ac_cv_little_endian_double=yes',
125
        'ac_cv_header_bzlib_h=no',
126
    ]
127

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

133
    MIN_NDK_API = 21
8✔
134
    '''Sets the minimal ndk api number needed to use the recipe.
6✔
135

136
    .. warning:: This recipe can be built only against API 21+, so it means
137
        that any class which inherits from class:`GuestPythonRecipe` will have
138
        this limitation.
139
    '''
140

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

152
    stdlib_filen_blacklist = [
8✔
153
        '*.py',
154
        '*.exe',
155
        '*.whl',
156
    ]
157
    '''The file extensions that we want to blacklist for our python bundle'''
6✔
158

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

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

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

180
    compiled_extension = '.pyc'
8✔
181
    '''the default extension for compiled python files.
6✔
182

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

188
    disable_gil = False
8✔
189
    '''python3.13 experimental free-threading build'''
6✔
190

191
    built_libraries = {"libpythonbin.so": "./android-build/"}
8✔
192

193
    def __init__(self, *args, **kwargs):
8✔
194
        self._ctx = None
8✔
195
        super().__init__(*args, **kwargs)
8✔
196

197
    @property
8✔
198
    def _libpython(self):
8✔
199
        '''return the python's library name (with extension)'''
200
        return 'libpython{link_version}.so'.format(
8✔
201
            link_version=self.link_version
202
        )
203

204
    @property
8✔
205
    def link_version(self):
8✔
206
        '''return the python's library link version e.g. 3.7m, 3.8'''
207
        major, minor = self.major_minor_version_string.split('.')
8✔
208
        flags = ''
8✔
209
        if major == '3' and int(minor) < 8:
8!
210
            flags += 'm'
×
211
        return '{major}.{minor}{flags}'.format(
8✔
212
            major=major,
213
            minor=minor,
214
            flags=flags
215
        )
216

217
    def include_root(self, arch_name):
8✔
218
        return join(self.get_build_dir(arch_name), 'Include')
8✔
219

220
    def link_root(self, arch_name):
8✔
221
        return join(self.get_build_dir(arch_name), 'android-build')
8✔
222

223
    def should_build(self, arch):
8✔
224
        return not isfile(join(self.link_root(arch.arch), self._libpython))
×
225

226
    def prebuild_arch(self, arch):
8✔
227
        super().prebuild_arch(arch)
×
228
        self.ctx.python_recipe = self
×
229

230
    def get_recipe_env(self, arch=None, with_flags_in_cc=True):
8✔
231
        env = super().get_recipe_env(arch)
8✔
232
        env['HOSTARCH'] = arch.command_prefix
8✔
233

234
        env['CC'] = arch.get_clang_exe(with_target=True)
8✔
235

236
        env['PATH'] = (
8✔
237
            '{hostpython_dir}:{old_path}').format(
238
                hostpython_dir=self.get_recipe(
239
                    'host' + self.name, self.ctx).get_path_to_python(),
240
                old_path=env['PATH'])
241

242
        env['CFLAGS'] = ' '.join(
8✔
243
            [
244
                '-fPIC',
245
                '-DANDROID'
246
            ]
247
        )
248

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

258
        return env
8✔
259

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

269
        info('Activating flags for sqlite3')
8✔
270
        recipe = Recipe.get_recipe('sqlite3', self.ctx)
8✔
271
        add_flags(' -I' + recipe.get_build_dir(arch.arch),
8✔
272
                  ' -L' + recipe.get_build_dir(arch.arch), ' -lsqlite3')
273

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

284
        info('Activating flags for openssl')
8✔
285
        recipe = Recipe.get_recipe('openssl', self.ctx)
8✔
286
        self.configure_args.append('--with-openssl=' + recipe.get_build_dir(arch.arch))
8✔
287
        add_flags(recipe.include_flags(arch),
8✔
288
                  recipe.link_dirs_flags(arch), recipe.link_libs_flags())
289

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

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

324
        if self._p_version.minor >= 11:
8!
325
            self.configure_args.append('--with-build-python={python_host_bin}')
8✔
326

327
        if self._p_version.minor >= 13 and self.disable_gil:
8!
328
            self.configure_args.append("--disable-gil")
×
329

330
        return env
8✔
331

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

474

475
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