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

kivy / python-for-android / 6215290912

17 Sep 2023 06:49PM UTC coverage: 59.095% (+1.4%) from 57.68%
6215290912

push

github

web-flow
Merge pull request #2891 from misl6/release-2023.09.16

* Update `cffi` recipe for Python 3.10 (#2800)

* Update __init__.py

version bump to 1.15.1

* Update disable-pkg-config.patch

adjust patch for 1.15.1

* Use build rather than pep517 for building (#2784)

pep517 has been renamed to pyproject-hooks, and as a consequence all of
the deprecated functionality has been removed. build now provides the
functionality required, and since we are only interested in the
metadata, we can leverage a helper function for that. I've also removed
all of the subprocess machinery for calling the wrapping function, since
it appears to not be as noisy as pep517.

* Bump actions/setup-python and actions/checkout versions, as old ones are deprecated (#2827)

* Removes `mysqldb` recipe as does not support Python 3 (#2828)

* Removes `Babel` recipe as it's not needed anymore. (#2826)

* Remove dateutil recipe, as it's not needed anymore (#2829)

* Optimize CI runs, by avoiding unnecessary rebuilds (#2833)

* Remove `pytz` recipe, as it's not needed anymore (#2830)

* `freetype` recipe: Changed the url to use https as http doesn't work (#2846)

* Fix `vlc` recipe build (#2841)

* Correct sys_platform (#2852)

On Window, sys.platform = "win32".

I think "nt" is a reference to os.name.

* Fix code string - quickstart.rst

* Bump `kivy` version to `2.2.1` (#2855)

* Use a pinned version of `Cython` for now, as most of the recipes are incompatible with `Cython==3.x.x` (#2862)

* Automatically generate required pre-requisites (#2858)

`get_required_prerequisites()` maintains a list of Prerequisites required by each platform.

But that same information is already stored in each Prerequisite class.

Rather than rather than maintaining two lists which might become inconsistent, auto-generate one.

* Use `platform.uname` instead of `os.uname` (#2857)

Advantages:

- Works cross platform, not just Unix.
- Is a namedtuple, ... (continued)

944 of 2241 branches covered (0.0%)

Branch coverage included in aggregate %.

174 of 272 new or added lines in 32 files covered. (63.97%)

9 existing lines in 5 files now uncovered.

4725 of 7352 relevant lines covered (64.27%)

2.56 hits per line

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

39.83
/pythonforandroid/build.py
1
from contextlib import suppress
4✔
2
import copy
4✔
3
import glob
4✔
4
import os
4✔
5
from os import environ
4✔
6
from os.path import (
4✔
7
    abspath, join, realpath, dirname, expanduser, exists
8
)
9
import re
4✔
10
import shutil
4✔
11
import subprocess
4✔
12

13
import sh
4✔
14

15
from pythonforandroid.androidndk import AndroidNDK
4✔
16
from pythonforandroid.archs import ArchARM, ArchARMv7_a, ArchAarch_64, Archx86, Archx86_64
4✔
17
from pythonforandroid.logger import (info, warning, info_notify, info_main, shprint)
4✔
18
from pythonforandroid.pythonpackage import get_package_name
4✔
19
from pythonforandroid.recipe import CythonRecipe, Recipe
4✔
20
from pythonforandroid.recommendations import (
4✔
21
    check_ndk_version, check_target_api, check_ndk_api,
22
    RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API)
23
from pythonforandroid.util import (
4✔
24
    current_directory, ensure_dir,
25
    BuildInterruptingException, rmdir
26
)
27

28

29
def get_targets(sdk_dir):
4✔
30
    if exists(join(sdk_dir, 'cmdline-tools', 'latest', 'bin', 'avdmanager')):
×
31
        avdmanager = sh.Command(join(sdk_dir, 'cmdline-tools', 'latest', 'bin', 'avdmanager'))
×
32
        targets = avdmanager('list', 'target').stdout.decode('utf-8').split('\n')
×
33

34
    elif exists(join(sdk_dir, 'tools', 'bin', 'avdmanager')):
×
35
        avdmanager = sh.Command(join(sdk_dir, 'tools', 'bin', 'avdmanager'))
×
36
        targets = avdmanager('list', 'target').stdout.decode('utf-8').split('\n')
×
37
    elif exists(join(sdk_dir, 'tools', 'android')):
×
38
        android = sh.Command(join(sdk_dir, 'tools', 'android'))
×
39
        targets = android('list').stdout.decode('utf-8').split('\n')
×
40
    else:
41
        raise BuildInterruptingException(
×
42
            'Could not find `android` or `sdkmanager` binaries in Android SDK',
43
            instructions='Make sure the path to the Android SDK is correct')
44
    return targets
×
45

46

47
def get_available_apis(sdk_dir):
4✔
48
    targets = get_targets(sdk_dir)
×
49
    apis = [s for s in targets if re.match(r'^ *API level: ', s)]
×
50
    apis = [re.findall(r'[0-9]+', s) for s in apis]
×
51
    apis = [int(s[0]) for s in apis if s]
×
52
    return apis
×
53

54

55
class Context:
4✔
56
    '''A build context. If anything will be built, an instance this class
57
    will be instantiated and used to hold all the build state.'''
58

59
    # Whether to make a debug or release build
60
    build_as_debuggable = False
4✔
61

62
    # Whether to strip debug symbols in `.so` files
63
    with_debug_symbols = False
4✔
64

65
    env = environ.copy()
4✔
66
    # the filepath of toolchain.py
67
    root_dir = None
4✔
68
    # the root dir where builds and dists will be stored
69
    storage_dir = None
4✔
70

71
    # in which bootstraps are copied for building
72
    # and recipes are built
73
    build_dir = None
4✔
74

75
    distribution = None
4✔
76
    """The Distribution object representing the current build target location."""
2✔
77

78
    # the Android project folder where everything ends up
79
    dist_dir = None
4✔
80

81
    # Whether setup.py or similar should be used if present:
82
    use_setup_py = False
4✔
83

84
    ccache = None  # whether to use ccache
4✔
85

86
    ndk = None
4✔
87

88
    bootstrap = None
4✔
89
    bootstrap_build_dir = None
4✔
90

91
    recipe_build_order = None  # Will hold the list of all built recipes
4✔
92

93
    symlink_bootstrap_files = False  # If True, will symlink instead of copying during build
4✔
94

95
    java_build_tool = 'auto'
4✔
96

97
    @property
4✔
98
    def packages_path(self):
4✔
99
        '''Where packages are downloaded before being unpacked'''
100
        return join(self.storage_dir, 'packages')
4✔
101

102
    @property
4✔
103
    def templates_dir(self):
4✔
104
        return join(self.root_dir, 'templates')
×
105

106
    @property
4✔
107
    def libs_dir(self):
4✔
108
        """
109
        where Android libs are cached after build
110
        but before being placed in dists
111
        """
112
        # Was previously hardcoded as self.build_dir/libs
113
        directory = join(self.build_dir, 'libs_collections',
4✔
114
                         self.bootstrap.distribution.name)
115
        ensure_dir(directory)
4✔
116
        return directory
4✔
117

118
    @property
4✔
119
    def javaclass_dir(self):
4✔
120
        # Was previously hardcoded as self.build_dir/java
121
        directory = join(self.build_dir, 'javaclasses',
4✔
122
                         self.bootstrap.distribution.name)
123
        ensure_dir(directory)
4✔
124
        return directory
4✔
125

126
    @property
4✔
127
    def aars_dir(self):
4✔
128
        directory = join(self.build_dir, 'aars', self.bootstrap.distribution.name)
4✔
129
        ensure_dir(directory)
4✔
130
        return directory
4✔
131

132
    @property
4✔
133
    def python_installs_dir(self):
4✔
134
        directory = join(self.build_dir, 'python-installs')
4✔
135
        ensure_dir(directory)
4✔
136
        return directory
4✔
137

138
    def get_python_install_dir(self, arch):
4✔
139
        return join(self.python_installs_dir, self.bootstrap.distribution.name, arch)
4✔
140

141
    def setup_dirs(self, storage_dir):
4✔
142
        '''Calculates all the storage and build dirs, and makes sure
143
        the directories exist where necessary.'''
144
        self.storage_dir = expanduser(storage_dir)
4✔
145
        if ' ' in self.storage_dir:
4!
146
            raise ValueError('storage dir path cannot contain spaces, please '
×
147
                             'specify a path with --storage-dir')
148
        self.build_dir = join(self.storage_dir, 'build')
4✔
149
        self.dist_dir = join(self.storage_dir, 'dists')
4✔
150

151
    def ensure_dirs(self):
4✔
152
        ensure_dir(self.storage_dir)
4✔
153
        ensure_dir(self.build_dir)
4✔
154
        ensure_dir(self.dist_dir)
4✔
155
        ensure_dir(join(self.build_dir, 'bootstrap_builds'))
4✔
156
        ensure_dir(join(self.build_dir, 'other_builds'))
4✔
157

158
    @property
4✔
159
    def android_api(self):
4✔
160
        '''The Android API being targeted.'''
161
        if self._android_api is None:
4!
162
            raise ValueError('Tried to access android_api but it has not '
×
163
                             'been set - this should not happen, something '
164
                             'went wrong!')
165
        return self._android_api
4✔
166

167
    @android_api.setter
4✔
168
    def android_api(self, value):
4✔
169
        self._android_api = value
4✔
170

171
    @property
4✔
172
    def ndk_api(self):
4✔
173
        '''The API number compile against'''
174
        if self._ndk_api is None:
4!
175
            raise ValueError('Tried to access ndk_api but it has not '
×
176
                             'been set - this should not happen, something '
177
                             'went wrong!')
178
        return self._ndk_api
4✔
179

180
    @ndk_api.setter
4✔
181
    def ndk_api(self, value):
4✔
182
        self._ndk_api = value
4✔
183

184
    @property
4✔
185
    def sdk_dir(self):
4✔
186
        '''The path to the Android SDK.'''
187
        if self._sdk_dir is None:
4!
188
            raise ValueError('Tried to access sdk_dir but it has not '
×
189
                             'been set - this should not happen, something '
190
                             'went wrong!')
191
        return self._sdk_dir
4✔
192

193
    @sdk_dir.setter
4✔
194
    def sdk_dir(self, value):
4✔
195
        self._sdk_dir = value
4✔
196

197
    @property
4✔
198
    def ndk_dir(self):
4✔
199
        '''The path to the Android NDK.'''
200
        if self._ndk_dir is None:
4!
201
            raise ValueError('Tried to access ndk_dir but it has not '
×
202
                             'been set - this should not happen, something '
203
                             'went wrong!')
204
        return self._ndk_dir
4✔
205

206
    @ndk_dir.setter
4✔
207
    def ndk_dir(self, value):
4✔
208
        self._ndk_dir = value
4✔
209

210
    def prepare_build_environment(self,
4✔
211
                                  user_sdk_dir,
212
                                  user_ndk_dir,
213
                                  user_android_api,
214
                                  user_ndk_api):
215
        '''Checks that build dependencies exist and sets internal variables
216
        for the Android SDK etc.
217

218
        ..warning:: This *must* be called before trying any build stuff
219

220
        '''
221

222
        self.ensure_dirs()
4✔
223

224
        if self._build_env_prepared:
4!
225
            return
×
226

227
        # Work out where the Android SDK is
228
        sdk_dir = None
4✔
229
        if user_sdk_dir:
4✔
230
            sdk_dir = user_sdk_dir
4✔
231
        # This is the old P4A-specific var
232
        if sdk_dir is None:
4✔
233
            sdk_dir = environ.get('ANDROIDSDK', None)
4✔
234
        # This seems used more conventionally
235
        if sdk_dir is None:
4✔
236
            sdk_dir = environ.get('ANDROID_HOME', None)
4✔
237
        # Checks in the buildozer SDK dir, useful for debug tests of p4a
238
        if sdk_dir is None:
4✔
239
            possible_dirs = glob.glob(expanduser(join(
4✔
240
                '~', '.buildozer', 'android', 'platform', 'android-sdk-*')))
241
            possible_dirs = [d for d in possible_dirs if not
4✔
242
                             d.endswith(('.bz2', '.gz'))]
243
            if possible_dirs:
4!
244
                info('Found possible SDK dirs in buildozer dir: {}'.format(
×
245
                    ', '.join(d.split(os.sep)[-1] for d in possible_dirs)))
246
                info('Will attempt to use SDK at {}'.format(possible_dirs[0]))
×
247
                warning('This SDK lookup is intended for debug only, if you '
×
248
                        'use python-for-android much you should probably '
249
                        'maintain your own SDK download.')
250
                sdk_dir = possible_dirs[0]
×
251
        if sdk_dir is None:
4✔
252
            raise BuildInterruptingException('Android SDK dir was not specified, exiting.')
4✔
253
        self.sdk_dir = realpath(sdk_dir)
4✔
254

255
        # Check what Android API we're using
256
        android_api = None
4✔
257
        if user_android_api:
4!
258
            android_api = user_android_api
×
259
            info('Getting Android API version from user argument: {}'.format(android_api))
×
260
        elif 'ANDROIDAPI' in environ:
4!
261
            android_api = environ['ANDROIDAPI']
×
262
            info('Found Android API target in $ANDROIDAPI: {}'.format(android_api))
×
263
        else:
264
            info('Android API target was not set manually, using '
4✔
265
                 'the default of {}'.format(RECOMMENDED_TARGET_API))
266
            android_api = RECOMMENDED_TARGET_API
4✔
267
        android_api = int(android_api)
4✔
268
        self.android_api = android_api
4✔
269

270
        for arch in self.archs:
4✔
271
            # Maybe We could remove this one in a near future (ARMv5 is definitely old)
272
            check_target_api(android_api, arch)
4✔
273
        apis = get_available_apis(self.sdk_dir)
4✔
274
        info('Available Android APIs are ({})'.format(
4✔
275
            ', '.join(map(str, apis))))
276
        if android_api in apis:
4!
277
            info(('Requested API target {} is available, '
4✔
278
                  'continuing.').format(android_api))
279
        else:
280
            raise BuildInterruptingException(
×
281
                ('Requested API target {} is not available, install '
282
                 'it with the SDK android tool.').format(android_api))
283

284
        # Find the Android NDK
285
        # Could also use ANDROID_NDK, but doesn't look like many tools use this
286
        ndk_dir = None
4✔
287
        if user_ndk_dir:
4!
288
            ndk_dir = user_ndk_dir
4✔
289
            info('Getting NDK dir from from user argument')
4✔
290
        if ndk_dir is None:  # The old P4A-specific dir
4!
291
            ndk_dir = environ.get('ANDROIDNDK', None)
×
292
            if ndk_dir is not None:
×
293
                info('Found NDK dir in $ANDROIDNDK: {}'.format(ndk_dir))
×
294
        if ndk_dir is None:  # Apparently the most common convention
4!
295
            ndk_dir = environ.get('NDK_HOME', None)
×
296
            if ndk_dir is not None:
×
297
                info('Found NDK dir in $NDK_HOME: {}'.format(ndk_dir))
×
298
        if ndk_dir is None:  # Another convention (with maven?)
4!
299
            ndk_dir = environ.get('ANDROID_NDK_HOME', None)
×
300
            if ndk_dir is not None:
×
301
                info('Found NDK dir in $ANDROID_NDK_HOME: {}'.format(ndk_dir))
×
302
        if ndk_dir is None:  # Checks in the buildozer NDK dir, useful
4!
303
            #                # for debug tests of p4a
304
            possible_dirs = glob.glob(expanduser(join(
×
305
                '~', '.buildozer', 'android', 'platform', 'android-ndk-r*')))
306
            if possible_dirs:
×
307
                info('Found possible NDK dirs in buildozer dir: {}'.format(
×
308
                    ', '.join(d.split(os.sep)[-1] for d in possible_dirs)))
309
                info('Will attempt to use NDK at {}'.format(possible_dirs[0]))
×
310
                warning('This NDK lookup is intended for debug only, if you '
×
311
                        'use python-for-android much you should probably '
312
                        'maintain your own NDK download.')
313
                ndk_dir = possible_dirs[0]
×
314
        if ndk_dir is None:
4!
315
            raise BuildInterruptingException('Android NDK dir was not specified')
×
316
        self.ndk_dir = realpath(ndk_dir)
4✔
317
        check_ndk_version(ndk_dir)
4✔
318

319
        ndk_api = None
4✔
320
        if user_ndk_api:
4!
321
            ndk_api = user_ndk_api
×
322
            info('Getting NDK API version (i.e. minimum supported API) from user argument')
×
323
        elif 'NDKAPI' in environ:
4!
324
            ndk_api = environ.get('NDKAPI', None)
×
325
            info('Found Android API target in $NDKAPI')
×
326
        else:
327
            ndk_api = min(self.android_api, RECOMMENDED_NDK_API)
4✔
328
            warning('NDK API target was not set manually, using '
4✔
329
                    'the default of {} = min(android-api={}, default ndk-api={})'.format(
330
                        ndk_api, self.android_api, RECOMMENDED_NDK_API))
331
        ndk_api = int(ndk_api)
4✔
332
        self.ndk_api = ndk_api
4✔
333

334
        check_ndk_api(ndk_api, self.android_api)
4✔
335

336
        self.ndk = AndroidNDK(self.ndk_dir)
4✔
337

338
        # path to some tools
339
        self.ccache = shutil.which("ccache")
4✔
340
        if not self.ccache:
4!
341
            info('ccache is missing, the build will not be optimized in the '
4✔
342
                 'future.')
343
        try:
4✔
344
            subprocess.check_output([
4✔
345
                "python3", "-m", "cython", "--help",
346
            ])
347
        except subprocess.CalledProcessError:
4✔
348
            warning('Cython for python3 missing. If you are building for '
4✔
349
                    ' a python 3 target (which is the default)'
350
                    ' then THINGS WILL BREAK.')
351

352
        self.env["PATH"] = ":".join(
4✔
353
            [
354
                self.ndk.llvm_bin_dir,
355
                self.ndk_dir,
356
                f"{self.sdk_dir}/tools",
357
                environ.get("PATH"),
358
            ]
359
        )
360

361
    def __init__(self):
4✔
362
        self.include_dirs = []
4✔
363

364
        self._build_env_prepared = False
4✔
365

366
        self._sdk_dir = None
4✔
367
        self._ndk_dir = None
4✔
368
        self._android_api = None
4✔
369
        self._ndk_api = None
4✔
370
        self.ndk = None
4✔
371

372
        self.local_recipes = None
4✔
373
        self.copy_libs = False
4✔
374

375
        self.activity_class_name = u'org.kivy.android.PythonActivity'
4✔
376
        self.service_class_name = u'org.kivy.android.PythonService'
4✔
377

378
        # this list should contain all Archs, it is pruned later
379
        self.archs = (
4✔
380
            ArchARM(self),
381
            ArchARMv7_a(self),
382
            Archx86(self),
383
            Archx86_64(self),
384
            ArchAarch_64(self),
385
            )
386

387
        self.root_dir = realpath(dirname(__file__))
4✔
388

389
        # remove the most obvious flags that can break the compilation
390
        self.env.pop("LDFLAGS", None)
4✔
391
        self.env.pop("ARCHFLAGS", None)
4✔
392
        self.env.pop("CFLAGS", None)
4✔
393

394
        self.python_recipe = None  # Set by TargetPythonRecipe
4✔
395

396
    def set_archs(self, arch_names):
4✔
397
        all_archs = self.archs
4✔
398
        new_archs = set()
4✔
399
        for name in arch_names:
4✔
400
            matching = [arch for arch in all_archs if arch.arch == name]
4✔
401
            for match in matching:
4✔
402
                new_archs.add(match)
4✔
403
        self.archs = list(new_archs)
4✔
404
        if not self.archs:
4!
405
            raise BuildInterruptingException('Asked to compile for no Archs, so failing.')
×
406
        info('Will compile for the following archs: {}'.format(
4✔
407
            ', '.join(arch.arch for arch in self.archs)))
408

409
    def prepare_bootstrap(self, bootstrap):
4✔
410
        if not bootstrap:
4!
411
            raise TypeError("None is not allowed for bootstrap")
×
412
        bootstrap.ctx = self
4✔
413
        self.bootstrap = bootstrap
4✔
414
        self.bootstrap.prepare_build_dir()
4✔
415
        self.bootstrap_build_dir = self.bootstrap.build_dir
4✔
416

417
    def prepare_dist(self):
4✔
418
        self.bootstrap.prepare_dist_dir()
4✔
419

420
    def get_site_packages_dir(self, arch):
4✔
421
        '''Returns the location of site-packages in the python-install build
422
        dir.
423
        '''
424
        return self.get_python_install_dir(arch.arch)
×
425

426
    def get_libs_dir(self, arch):
4✔
427
        '''The libs dir for a given arch.'''
428
        ensure_dir(join(self.libs_dir, arch))
4✔
429
        return join(self.libs_dir, arch)
4✔
430

431
    def has_lib(self, arch, lib):
4✔
432
        return exists(join(self.get_libs_dir(arch), lib))
4✔
433

434
    def has_package(self, name, arch=None):
4✔
435
        # If this is a file path, it'll need special handling:
436
        if (name.find("/") >= 0 or name.find("\\") >= 0) and \
×
437
                name.find("://") < 0:  # (:// would indicate an url)
438
            if not os.path.exists(name):
×
439
                # Non-existing dir, cannot look this up.
440
                return False
×
441
            try:
×
442
                name = get_package_name(os.path.abspath(name))
×
443
            except ValueError:
×
444
                # Failed to look up any meaningful name.
445
                return False
×
446

447
        # Try to look up recipe by name:
448
        try:
×
449
            recipe = Recipe.get_recipe(name, self)
×
450
        except ValueError:
×
451
            pass
×
452
        else:
453
            name = getattr(recipe, 'site_packages_name', None) or name
×
454
        name = name.replace('.', '/')
×
455
        site_packages_dir = self.get_site_packages_dir(arch)
×
456
        return (exists(join(site_packages_dir, name)) or
×
457
                exists(join(site_packages_dir, name + '.py')) or
458
                exists(join(site_packages_dir, name + '.pyc')) or
459
                exists(join(site_packages_dir, name + '.so')) or
460
                glob.glob(join(site_packages_dir, name + '-*.egg')))
461

462
    def not_has_package(self, name, arch=None):
4✔
463
        return not self.has_package(name, arch)
×
464

465

466
def build_recipes(build_order, python_modules, ctx, project_dir,
4✔
467
                  ignore_project_setup_py=False
468
                 ):
469
    # Put recipes in correct build order
470
    info_notify("Recipe build order is {}".format(build_order))
×
471
    if python_modules:
×
472
        python_modules = sorted(set(python_modules))
×
473
        info_notify(
×
474
            ('The requirements ({}) were not found as recipes, they will be '
475
             'installed with pip.').format(', '.join(python_modules)))
476

477
    recipes = [Recipe.get_recipe(name, ctx) for name in build_order]
×
478

479
    # download is arch independent
480
    info_main('# Downloading recipes ')
×
481
    for recipe in recipes:
×
482
        recipe.download_if_necessary()
×
483

484
    for arch in ctx.archs:
×
485
        info_main('# Building all recipes for arch {}'.format(arch.arch))
×
486

487
        info_main('# Unpacking recipes')
×
488
        for recipe in recipes:
×
489
            ensure_dir(recipe.get_build_container_dir(arch.arch))
×
490
            recipe.prepare_build_dir(arch.arch)
×
491

492
        info_main('# Prebuilding recipes')
×
493
        # 2) prebuild packages
494
        for recipe in recipes:
×
495
            info_main('Prebuilding {} for {}'.format(recipe.name, arch.arch))
×
496
            recipe.prebuild_arch(arch)
×
497
            recipe.apply_patches(arch)
×
498

499
        # 3) build packages
500
        info_main('# Building recipes')
×
501
        for recipe in recipes:
×
502
            info_main('Building {} for {}'.format(recipe.name, arch.arch))
×
503
            if recipe.should_build(arch):
×
504
                recipe.build_arch(arch)
×
505
            else:
506
                info('{} said it is already built, skipping'
×
507
                     .format(recipe.name))
508
            recipe.install_libraries(arch)
×
509

510
        # 4) biglink everything
511
        info_main('# Biglinking object files')
×
512
        if not ctx.python_recipe:
×
513
            biglink(ctx, arch)
×
514
        else:
515
            warning(
×
516
                "Context's python recipe found, "
517
                "skipping biglink (will this work?)"
518
            )
519

520
        # 5) postbuild packages
521
        info_main('# Postbuilding recipes')
×
522
        for recipe in recipes:
×
523
            info_main('Postbuilding {} for {}'.format(recipe.name, arch.arch))
×
524
            recipe.postbuild_arch(arch)
×
525

526
    info_main('# Installing pure Python modules')
×
527
    for arch in ctx.archs:
×
528
        run_pymodules_install(
×
529
            ctx, arch, python_modules, project_dir,
530
            ignore_setup_py=ignore_project_setup_py
531
        )
532

533

534
def project_has_setup_py(project_dir):
4✔
535
    return (project_dir is not None and
×
536
            (exists(join(project_dir, "setup.py")) or
537
             exists(join(project_dir, "pyproject.toml"))
538
            ))
539

540

541
def run_setuppy_install(ctx, project_dir, env=None, arch=None):
4✔
542
    env = env or {}
×
543

544
    with current_directory(project_dir):
×
545
        info('got setup.py or similar, running project install. ' +
×
546
             '(disable this behavior with --ignore-setup-py)')
547

548
        # Compute & output the constraints we will use:
549
        info('Contents that will be used for constraints.txt:')
×
550
        constraints = subprocess.check_output([
×
551
            join(
552
                ctx.build_dir, "venv", "bin", "pip"
553
            ),
554
            "freeze"
555
        ], env=copy.copy(env))
556
        with suppress(AttributeError):
×
557
            constraints = constraints.decode("utf-8", "replace")
×
558
        info(constraints)
×
559

560
        # Make sure all packages found are fixed in version
561
        # by writing a constraint file, to avoid recipes being
562
        # upgraded & reinstalled:
563
        with open('._tmp_p4a_recipe_constraints.txt', 'wb') as fileh:
×
564
            fileh.write(constraints.encode("utf-8", "replace"))
×
565
        try:
×
566

567
            info('Populating venv\'s site-packages with '
×
568
                 'ctx.get_site_packages_dir()...')
569

570
            # Copy dist contents into site-packages for discovery.
571
            # Why this is needed:
572
            # --target is somewhat evil and messes with discovery of
573
            # packages in PYTHONPATH if that also includes the target
574
            # folder. So we need to use the regular virtualenv
575
            # site-packages folder instead.
576
            # Reference:
577
            # https://github.com/pypa/pip/issues/6223
578
            ctx_site_packages_dir = os.path.normpath(
×
579
                os.path.abspath(ctx.get_site_packages_dir(arch))
580
            )
581
            venv_site_packages_dir = os.path.normpath(os.path.join(
×
582
                ctx.build_dir, "venv", "lib", [
583
                    f for f in os.listdir(os.path.join(
584
                        ctx.build_dir, "venv", "lib"
585
                    )) if f.startswith("python")
586
                ][0], "site-packages"
587
            ))
588
            copied_over_contents = []
×
589
            for f in os.listdir(ctx_site_packages_dir):
×
590
                full_path = os.path.join(ctx_site_packages_dir, f)
×
591
                if not os.path.exists(os.path.join(
×
592
                            venv_site_packages_dir, f
593
                        )):
594
                    if os.path.isdir(full_path):
×
595
                        shutil.copytree(full_path, os.path.join(
×
596
                            venv_site_packages_dir, f
597
                        ))
598
                    else:
599
                        shutil.copy2(full_path, os.path.join(
×
600
                            venv_site_packages_dir, f
601
                        ))
602
                    copied_over_contents.append(f)
×
603

604
            # Get listing of virtualenv's site-packages, to see the
605
            # newly added things afterwards & copy them back into
606
            # the distribution folder / build context site-packages:
607
            previous_venv_contents = os.listdir(
×
608
                venv_site_packages_dir
609
            )
610

611
            # Actually run setup.py:
612
            info('Launching package install...')
×
613
            shprint(sh.bash, '-c', (
×
614
                "'" + join(
615
                    ctx.build_dir, "venv", "bin", "pip"
616
                ).replace("'", "'\"'\"'") + "' " +
617
                "install -c ._tmp_p4a_recipe_constraints.txt -v ."
618
            ).format(ctx.get_site_packages_dir(arch).
619
                     replace("'", "'\"'\"'")),
620
                    _env=copy.copy(env))
621

622
            # Go over all new additions and copy them back:
623
            info('Copying additions resulting from setup.py back '
×
624
                 'into ctx.get_site_packages_dir()...')
625
            new_venv_additions = []
×
626
            for f in (set(os.listdir(venv_site_packages_dir)) -
×
627
                      set(previous_venv_contents)):
628
                new_venv_additions.append(f)
×
629
                full_path = os.path.join(venv_site_packages_dir, f)
×
630
                if os.path.isdir(full_path):
×
631
                    shutil.copytree(full_path, os.path.join(
×
632
                        ctx_site_packages_dir, f
633
                    ))
634
                else:
635
                    shutil.copy2(full_path, os.path.join(
×
636
                        ctx_site_packages_dir, f
637
                    ))
638

639
            # Undo all the changes we did to the venv-site packages:
640
            info('Reverting additions to '
×
641
                 'virtualenv\'s site-packages...')
642
            for f in set(copied_over_contents + new_venv_additions):
×
643
                full_path = os.path.join(venv_site_packages_dir, f)
×
644
                if os.path.isdir(full_path):
×
NEW
645
                    rmdir(full_path)
×
646
                else:
647
                    os.remove(full_path)
×
648
        finally:
649
            os.remove("._tmp_p4a_recipe_constraints.txt")
×
650

651

652
def run_pymodules_install(ctx, arch, modules, project_dir=None,
4✔
653
                          ignore_setup_py=False):
654
    """ This function will take care of all non-recipe things, by:
655

656
        1. Processing them from --requirements (the modules argument)
657
           and installing them
658

659
        2. Installing the user project/app itself via setup.py if
660
           ignore_setup_py=True
661

662
    """
663

664
    info('*** PYTHON PACKAGE / PROJECT INSTALL STAGE FOR ARCH: {} ***'.format(arch))
4✔
665

666
    modules = [m for m in modules if ctx.not_has_package(m, arch)]
4✔
667

668
    # We change current working directory later, so this has to be an absolute
669
    # path or `None` in case that we didn't supply the `project_dir` via kwargs
670
    project_dir = abspath(project_dir) if project_dir else None
4✔
671

672
    # Bail out if no python deps and no setup.py to process:
673
    if not modules and (
4✔
674
            ignore_setup_py or
675
            project_dir is None or
676
            not project_has_setup_py(project_dir)
677
            ):
678
        info('No Python modules and no setup.py to process, skipping')
4✔
679
        return
4✔
680

681
    # Output messages about what we're going to do:
682
    if modules:
4!
683
        info(
4✔
684
            "The requirements ({}) don\'t have recipes, attempting to "
685
            "install them with pip".format(', '.join(modules))
686
        )
687
        info(
4✔
688
            "If this fails, it may mean that the module has compiled "
689
            "components and needs a recipe."
690
        )
691
    if project_dir is not None and \
4!
692
            project_has_setup_py(project_dir) and not ignore_setup_py:
693
        info(
×
694
            "Will process project install, if it fails then the "
695
            "project may not be compatible for Android install."
696
        )
697

698
    # Use our hostpython to create the virtualenv
699
    host_python = sh.Command(ctx.hostpython)
4✔
700
    with current_directory(join(ctx.build_dir)):
4✔
701
        shprint(host_python, '-m', 'venv', 'venv')
4✔
702

703
        # Prepare base environment and upgrade pip:
704
        base_env = dict(copy.copy(os.environ))
4✔
705
        base_env["PYTHONPATH"] = ctx.get_site_packages_dir(arch)
4✔
706
        info('Upgrade pip to latest version')
4✔
707
        shprint(sh.bash, '-c', (
4✔
708
            "source venv/bin/activate && pip install -U pip"
709
        ), _env=copy.copy(base_env))
710

711
        # Install Cython in case modules need it to build:
712
        info('Install Cython in case one of the modules needs it to build')
4✔
713
        shprint(sh.bash, '-c', (
4✔
714
            "venv/bin/pip install Cython"
715
        ), _env=copy.copy(base_env))
716

717
        # Get environment variables for build (with CC/compiler set):
718
        standard_recipe = CythonRecipe()
4✔
719
        standard_recipe.ctx = ctx
4✔
720
        # (note: following line enables explicit -lpython... linker options)
721
        standard_recipe.call_hostpython_via_targetpython = False
4✔
722
        recipe_env = standard_recipe.get_recipe_env(ctx.archs[0])
4✔
723
        env = copy.copy(base_env)
4✔
724
        env.update(recipe_env)
4✔
725

726
        # Make sure our build package dir is available, and the virtualenv
727
        # site packages come FIRST (so the proper pip version is used):
728
        env["PYTHONPATH"] += ":" + ctx.get_site_packages_dir(arch)
4✔
729
        env["PYTHONPATH"] = os.path.abspath(join(
4✔
730
            ctx.build_dir, "venv", "lib",
731
            "python" + ctx.python_recipe.major_minor_version_string,
732
            "site-packages")) + ":" + env["PYTHONPATH"]
733

734
        # Install the manually specified requirements first:
735
        if not modules:
4!
736
            info('There are no Python modules to install, skipping')
×
737
        else:
738
            info('Creating a requirements.txt file for the Python modules')
4✔
739
            with open('requirements.txt', 'w') as fileh:
4✔
740
                for module in modules:
4✔
741
                    key = 'VERSION_' + module
4✔
742
                    if key in environ:
4!
743
                        line = '{}=={}\n'.format(module, environ[key])
×
744
                    else:
745
                        line = '{}\n'.format(module)
4✔
746
                    fileh.write(line)
4✔
747

748
            info('Installing Python modules with pip')
4✔
749
            info(
4✔
750
                "IF THIS FAILS, THE MODULES MAY NEED A RECIPE. "
751
                "A reason for this is often modules compiling "
752
                "native code that is unaware of Android cross-compilation "
753
                "and does not work without additional "
754
                "changes / workarounds."
755
            )
756

757
            shprint(sh.bash, '-c', (
4✔
758
                "venv/bin/pip " +
759
                "install -v --target '{0}' --no-deps -r requirements.txt"
760
            ).format(ctx.get_site_packages_dir(arch).replace("'", "'\"'\"'")),
761
                    _env=copy.copy(env))
762

763
        # Afterwards, run setup.py if present:
764
        if project_dir is not None and (
4!
765
                project_has_setup_py(project_dir) and not ignore_setup_py
766
                ):
767
            run_setuppy_install(ctx, project_dir, env, arch.arch)
×
768
        elif not ignore_setup_py:
4!
769
            info("No setup.py found in project directory: " + str(project_dir))
4✔
770

771
        # Strip object files after potential Cython or native code builds:
772
        if not ctx.with_debug_symbols:
4✔
773
            standard_recipe.strip_object_files(
4✔
774
                arch, env, build_dir=ctx.build_dir
775
            )
776

777

778
def biglink(ctx, arch):
4✔
779
    # First, collate object files from each recipe
780
    info('Collating object files from each recipe')
×
781
    obj_dir = join(ctx.bootstrap.build_dir, 'collated_objects')
×
782
    ensure_dir(obj_dir)
×
783
    recipes = [Recipe.get_recipe(name, ctx) for name in ctx.recipe_build_order]
×
784
    for recipe in recipes:
×
785
        recipe_obj_dir = join(recipe.get_build_container_dir(arch.arch),
×
786
                              'objects_{}'.format(recipe.name))
787
        if not exists(recipe_obj_dir):
×
788
            info('{} recipe has no biglinkable files dir, skipping'
×
789
                 .format(recipe.name))
790
            continue
×
791
        files = glob.glob(join(recipe_obj_dir, '*'))
×
792
        if not len(files):
×
793
            info('{} recipe has no biglinkable files, skipping'
×
794
                 .format(recipe.name))
795
            continue
×
796
        info('{} recipe has object files, copying'.format(recipe.name))
×
797
        files.append(obj_dir)
×
798
        shprint(sh.cp, '-r', *files)
×
799

800
    env = arch.get_env()
×
801
    env['LDFLAGS'] = env['LDFLAGS'] + ' -L{}'.format(
×
802
        join(ctx.bootstrap.build_dir, 'obj', 'local', arch.arch))
803

804
    if not len(glob.glob(join(obj_dir, '*'))):
×
805
        info('There seem to be no libraries to biglink, skipping.')
×
806
        return
×
807
    info('Biglinking')
×
808
    info('target {}'.format(join(ctx.get_libs_dir(arch.arch),
×
809
                                 'libpymodules.so')))
810
    do_biglink = copylibs_function if ctx.copy_libs else biglink_function
×
811

812
    # Move to the directory containing crtstart_so.o and crtend_so.o
813
    # This is necessary with newer NDKs? A gcc bug?
814
    with current_directory(arch.ndk_lib_dir):
×
815
        do_biglink(
×
816
            join(ctx.get_libs_dir(arch.arch), 'libpymodules.so'),
817
            obj_dir.split(' '),
818
            extra_link_dirs=[join(ctx.bootstrap.build_dir,
819
                                  'obj', 'local', arch.arch),
820
                             os.path.abspath('.')],
821
            env=env)
822

823

824
def biglink_function(soname, objs_paths, extra_link_dirs=None, env=None):
4✔
825
    if extra_link_dirs is None:
×
826
        extra_link_dirs = []
×
827
    print('objs_paths are', objs_paths)
×
828
    sofiles = []
×
829

830
    for directory in objs_paths:
×
831
        for fn in os.listdir(directory):
×
832
            fn = os.path.join(directory, fn)
×
833

834
            if not fn.endswith(".so.o"):
×
835
                continue
×
836
            if not os.path.exists(fn[:-2] + ".libs"):
×
837
                continue
×
838

839
            sofiles.append(fn[:-2])
×
840

841
    # The raw argument list.
842
    args = []
×
843

844
    for fn in sofiles:
×
845
        afn = fn + ".o"
×
846
        libsfn = fn + ".libs"
×
847

848
        args.append(afn)
×
849
        with open(libsfn) as fd:
×
850
            data = fd.read()
×
851
            args.extend(data.split(" "))
×
852

853
    unique_args = []
×
854
    while args:
×
855
        a = args.pop()
×
856
        if a in ('-L', ):
×
857
            continue
×
858
        if a not in unique_args:
×
859
            unique_args.insert(0, a)
×
860

861
    for dir in extra_link_dirs:
×
862
        link = '-L{}'.format(dir)
×
863
        if link not in unique_args:
×
864
            unique_args.append(link)
×
865

866
    cc_name = env['CC']
×
867
    cc = sh.Command(cc_name.split()[0])
×
868
    cc = cc.bake(*cc_name.split()[1:])
×
869

870
    shprint(cc, '-shared', '-O3', '-o', soname, *unique_args, _env=env)
×
871

872

873
def copylibs_function(soname, objs_paths, extra_link_dirs=None, env=None):
4✔
874
    if extra_link_dirs is None:
×
875
        extra_link_dirs = []
×
876
    print('objs_paths are', objs_paths)
×
877

878
    re_needso = re.compile(r'^.*\(NEEDED\)\s+Shared library: \[lib(.*)\.so\]\s*$')
×
879
    blacklist_libs = (
×
880
        'c',
881
        'stdc++',
882
        'dl',
883
        'python2.7',
884
        'sdl',
885
        'sdl_image',
886
        'sdl_ttf',
887
        'z',
888
        'm',
889
        'GLESv2',
890
        'jpeg',
891
        'png',
892
        'log',
893

894
        # bootstrap takes care of sdl2 libs (if applicable)
895
        'SDL2',
896
        'SDL2_ttf',
897
        'SDL2_image',
898
        'SDL2_mixer',
899
    )
900
    found_libs = []
×
901
    sofiles = []
×
902
    if env and 'READELF' in env:
×
903
        readelf = env['READELF']
×
904
    elif 'READELF' in os.environ:
×
905
        readelf = os.environ['READELF']
×
906
    else:
907
        readelf = shutil.which('readelf').strip()
×
908
    readelf = sh.Command(readelf).bake('-d')
×
909

910
    dest = dirname(soname)
×
911

912
    for directory in objs_paths:
×
913
        for fn in os.listdir(directory):
×
914
            fn = join(directory, fn)
×
915

916
            if not fn.endswith('.libs'):
×
917
                continue
×
918

919
            dirfn = fn[:-1] + 'dirs'
×
920
            if not exists(dirfn):
×
921
                continue
×
922

923
            with open(fn) as f:
×
924
                libs = f.read().strip().split(' ')
×
925
                needed_libs = [lib for lib in libs
×
926
                               if lib and
927
                               lib not in blacklist_libs and
928
                               lib not in found_libs]
929

930
            while needed_libs:
×
931
                print('need libs:\n\t' + '\n\t'.join(needed_libs))
×
932

933
                start_needed_libs = needed_libs[:]
×
934
                found_sofiles = []
×
935

936
                with open(dirfn) as f:
×
937
                    libdirs = f.read().split()
×
938
                    for libdir in libdirs:
×
939
                        if not needed_libs:
×
940
                            break
×
941

942
                        if libdir == dest:
×
943
                            # don't need to copy from dest to dest!
944
                            continue
×
945

946
                        libdir = libdir.strip()
×
947
                        print('scanning', libdir)
×
948
                        for lib in needed_libs[:]:
×
949
                            if lib in found_libs:
×
950
                                continue
×
951

952
                            if lib.endswith('.a'):
×
953
                                needed_libs.remove(lib)
×
954
                                found_libs.append(lib)
×
955
                                continue
×
956

957
                            lib_a = 'lib' + lib + '.a'
×
958
                            libpath_a = join(libdir, lib_a)
×
959
                            lib_so = 'lib' + lib + '.so'
×
960
                            libpath_so = join(libdir, lib_so)
×
961
                            plain_so = lib + '.so'
×
962
                            plainpath_so = join(libdir, plain_so)
×
963

964
                            sopath = None
×
965
                            if exists(libpath_so):
×
966
                                sopath = libpath_so
×
967
                            elif exists(plainpath_so):
×
968
                                sopath = plainpath_so
×
969

970
                            if sopath:
×
971
                                print('found', lib, 'in', libdir)
×
972
                                found_sofiles.append(sopath)
×
973
                                needed_libs.remove(lib)
×
974
                                found_libs.append(lib)
×
975
                                continue
×
976

977
                            if exists(libpath_a):
×
978
                                print('found', lib, '(static) in', libdir)
×
979
                                needed_libs.remove(lib)
×
980
                                found_libs.append(lib)
×
981
                                continue
×
982

983
                for sofile in found_sofiles:
×
984
                    print('scanning dependencies for', sofile)
×
985
                    out = readelf(sofile)
×
986
                    for line in out.splitlines():
×
987
                        needso = re_needso.match(line)
×
988
                        if needso:
×
989
                            lib = needso.group(1)
×
990
                            if (lib not in needed_libs
×
991
                                    and lib not in found_libs
992
                                    and lib not in blacklist_libs):
993
                                needed_libs.append(needso.group(1))
×
994

995
                sofiles += found_sofiles
×
996

997
                if needed_libs == start_needed_libs:
×
998
                    raise RuntimeError(
×
999
                            'Failed to locate needed libraries!\n\t' +
1000
                            '\n\t'.join(needed_libs))
1001

1002
    print('Copying libraries')
×
1003
    shprint(sh.cp, *sofiles, dest)
×
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

© 2025 Coveralls, Inc