• 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

47.49
/pythonforandroid/recipe.py
1
from os.path import basename, dirname, exists, isdir, isfile, join, realpath, split
4✔
2
import glob
4✔
3

4
import hashlib
4✔
5
from re import match
4✔
6

7
import sh
4✔
8
import shutil
4✔
9
import fnmatch
4✔
10
import urllib.request
4✔
11
from urllib.request import urlretrieve
4✔
12
from os import listdir, unlink, environ, curdir, walk
4✔
13
from sys import stdout
4✔
14
import time
4✔
15
try:
4✔
16
    from urlparse import urlparse
4✔
17
except ImportError:
4✔
18
    from urllib.parse import urlparse
4✔
19
from pythonforandroid.logger import (
4✔
20
    logger, info, warning, debug, shprint, info_main)
21
from pythonforandroid.util import (
4✔
22
    current_directory, ensure_dir, BuildInterruptingException, rmdir, move,
23
    touch)
24
from pythonforandroid.util import load_source as import_recipe
4✔
25

26

27
url_opener = urllib.request.build_opener()
4✔
28
url_orig_headers = url_opener.addheaders
4✔
29
urllib.request.install_opener(url_opener)
4✔
30

31

32
class RecipeMeta(type):
4✔
33
    def __new__(cls, name, bases, dct):
4✔
34
        if name != 'Recipe':
4✔
35
            if 'url' in dct:
4✔
36
                dct['_url'] = dct.pop('url')
4✔
37
            if 'version' in dct:
4✔
38
                dct['_version'] = dct.pop('version')
4✔
39

40
        return super().__new__(cls, name, bases, dct)
4✔
41

42

43
class Recipe(metaclass=RecipeMeta):
4✔
44
    _url = None
4✔
45
    '''The address from which the recipe may be downloaded. This is not
2✔
46
    essential, it may be omitted if the source is available some other
47
    way, such as via the :class:`IncludedFilesBehaviour` mixin.
48

49
    If the url includes the version, you may (and probably should)
50
    replace this with ``{version}``, which will automatically be
51
    replaced by the :attr:`version` string during download.
52

53
    .. note:: Methods marked (internal) are used internally and you
54
              probably don't need to call them, but they are available
55
              if you want.
56
    '''
57

58
    _version = None
4✔
59
    '''A string giving the version of the software the recipe describes,
2✔
60
    e.g. ``2.0.3`` or ``master``.'''
61

62
    md5sum = None
4✔
63
    '''The md5sum of the source from the :attr:`url`. Non-essential, but
2✔
64
    you should try to include this, it is used to check that the download
65
    finished correctly.
66
    '''
67

68
    sha512sum = None
4✔
69
    '''The sha512sum of the source from the :attr:`url`. Non-essential, but
2✔
70
    you should try to include this, it is used to check that the download
71
    finished correctly.
72
    '''
73

74
    blake2bsum = None
4✔
75
    '''The blake2bsum of the source from the :attr:`url`. Non-essential, but
2✔
76
    you should try to include this, it is used to check that the download
77
    finished correctly.
78
    '''
79

80
    depends = []
4✔
81
    '''A list containing the names of any recipes that this recipe depends on.
2✔
82
    '''
83

84
    conflicts = []
4✔
85
    '''A list containing the names of any recipes that are known to be
2✔
86
    incompatible with this one.'''
87

88
    opt_depends = []
4✔
89
    '''A list of optional dependencies, that must be built before this
2✔
90
    recipe if they are built at all, but whose presence is not essential.'''
91

92
    patches = []
4✔
93
    '''A list of patches to apply to the source. Values can be either a string
2✔
94
    referring to the patch file relative to the recipe dir, or a tuple of the
95
    string patch file and a callable, which will receive the kwargs `arch` and
96
    `recipe`, which should return True if the patch should be applied.'''
97

98
    python_depends = []
4✔
99
    '''A list of pure-Python packages that this package requires. These
2✔
100
    packages will NOT be available at build time, but will be added to the
101
    list of pure-Python packages to install via pip. If you need these packages
102
    at build time, you must create a recipe.'''
103

104
    archs = ['armeabi']  # Not currently implemented properly
4✔
105

106
    built_libraries = {}
4✔
107
    """Each recipe that builds a system library (e.g.:libffi, openssl, etc...)
2✔
108
    should contain a dict holding the relevant information of the library. The
109
    keys should be the generated libraries and the values the relative path of
110
    the library inside his build folder. This dict will be used to perform
111
    different operations:
112
        - copy the library into the right location, depending on if it's shared
113
          or static)
114
        - check if we have to rebuild the library
115

116
    Here an example of how it would look like for `libffi` recipe:
117

118
        - `built_libraries = {'libffi.so': '.libs'}`
119

120
    .. note:: in case that the built library resides in recipe's build
121
              directory, you can set the following values for the relative
122
              path: `'.', None or ''`
123
    """
124

125
    need_stl_shared = False
4✔
126
    '''Some libraries or python packages may need the c++_shared in APK.
2✔
127
    We can automatically do this for any recipe if we set this property to
128
    `True`'''
129

130
    stl_lib_name = 'c++_shared'
4✔
131
    '''
2✔
132
    The default STL shared lib to use: `c++_shared`.
133

134
    .. note:: Android NDK version > 17 only supports 'c++_shared', because
135
        starting from NDK r18 the `gnustl_shared` lib has been deprecated.
136
    '''
137

138
    def get_stl_library(self, arch):
4✔
139
        return join(
4✔
140
            arch.ndk_lib_dir,
141
            'lib{name}.so'.format(name=self.stl_lib_name),
142
        )
143

144
    def install_stl_lib(self, arch):
4✔
145
        if not self.ctx.has_lib(
4!
146
            arch.arch, 'lib{name}.so'.format(name=self.stl_lib_name)
147
        ):
148
            self.install_libs(arch, self.get_stl_library(arch))
4✔
149

150
    @property
4✔
151
    def version(self):
4✔
152
        key = 'VERSION_' + self.name
4✔
153
        return environ.get(key, self._version)
4✔
154

155
    @property
4✔
156
    def url(self):
4✔
157
        key = 'URL_' + self.name
4✔
158
        return environ.get(key, self._url)
4✔
159

160
    @property
4✔
161
    def versioned_url(self):
4✔
162
        '''A property returning the url of the recipe with ``{version}``
163
        replaced by the :attr:`url`. If accessing the url, you should use this
164
        property, *not* access the url directly.'''
165
        if self.url is None:
4!
166
            return None
×
167
        return self.url.format(version=self.version)
4✔
168

169
    def download_file(self, url, target, cwd=None):
4✔
170
        """
171
        (internal) Download an ``url`` to a ``target``.
172
        """
173
        if not url:
4!
174
            return
×
175
        info('Downloading {} from {}'.format(self.name, url))
4✔
176

177
        if cwd:
4!
178
            target = join(cwd, target)
×
179

180
        parsed_url = urlparse(url)
4✔
181
        if parsed_url.scheme in ('http', 'https'):
4!
182
            def report_hook(index, blksize, size):
4✔
183
                if size <= 0:
×
184
                    progression = '{0} bytes'.format(index * blksize)
×
185
                else:
186
                    progression = '{0:.2f}%'.format(
×
187
                        index * blksize * 100. / float(size))
188
                if "CI" not in environ:
×
189
                    stdout.write('- Download {}\r'.format(progression))
×
190
                    stdout.flush()
×
191

192
            if exists(target):
4!
193
                unlink(target)
×
194

195
            # Download item with multiple attempts (for bad connections):
196
            attempts = 0
4✔
197
            seconds = 1
4✔
198
            while True:
2✔
199
                try:
4✔
200
                    # jqueryui.com returns a 403 w/ the default user agent
201
                    # Mozilla/5.0 doesnt handle redirection for liblzma
202
                    url_opener.addheaders = [('User-agent', 'Wget/1.0')]
4✔
203
                    urlretrieve(url, target, report_hook)
4✔
204
                except OSError as e:
4✔
205
                    attempts += 1
4✔
206
                    if attempts >= 5:
4✔
207
                        raise
4✔
208
                    stdout.write('Download failed: {}; retrying in {} second(s)...'.format(e, seconds))
4✔
209
                    time.sleep(seconds)
4✔
210
                    seconds *= 2
4✔
211
                    continue
4✔
212
                finally:
213
                    url_opener.addheaders = url_orig_headers
4✔
214
                break
3✔
215
            return target
4✔
216
        elif parsed_url.scheme in ('git', 'git+file', 'git+ssh', 'git+http', 'git+https'):
×
217
            if not isdir(target):
×
218
                if url.startswith('git+'):
×
219
                    url = url[4:]
×
220
                # if 'version' is specified, do a shallow clone
221
                if self.version:
×
NEW
222
                    ensure_dir(target)
×
223
                    with current_directory(target):
×
224
                        shprint(sh.git, 'init')
×
225
                        shprint(sh.git, 'remote', 'add', 'origin', url)
×
226
                else:
227
                    shprint(sh.git, 'clone', '--recursive', url, target)
×
228
            with current_directory(target):
×
229
                if self.version:
×
230
                    shprint(sh.git, 'fetch', '--depth', '1', 'origin', self.version)
×
231
                    shprint(sh.git, 'checkout', self.version)
×
232
                branch = sh.git('branch', '--show-current')
×
233
                if branch:
×
234
                    shprint(sh.git, 'pull')
×
235
                    shprint(sh.git, 'pull', '--recurse-submodules')
×
236
                shprint(sh.git, 'submodule', 'update', '--recursive', '--init', '--depth', '1')
×
237
            return target
×
238

239
    def apply_patch(self, filename, arch, build_dir=None):
4✔
240
        """
241
        Apply a patch from the current recipe directory into the current
242
        build directory.
243

244
        .. versionchanged:: 0.6.0
245
            Add ability to apply patch from any dir via kwarg `build_dir`'''
246
        """
247
        info("Applying patch {}".format(filename))
4✔
248
        build_dir = build_dir if build_dir else self.get_build_dir(arch)
4✔
249
        filename = join(self.get_recipe_dir(), filename)
4✔
250
        shprint(sh.patch, "-t", "-d", build_dir, "-p1",
4✔
251
                "-i", filename, _tail=10)
252

253
    def copy_file(self, filename, dest):
4✔
254
        info("Copy {} to {}".format(filename, dest))
×
255
        filename = join(self.get_recipe_dir(), filename)
×
256
        dest = join(self.build_dir, dest)
×
257
        shutil.copy(filename, dest)
×
258

259
    def append_file(self, filename, dest):
4✔
260
        info("Append {} to {}".format(filename, dest))
×
261
        filename = join(self.get_recipe_dir(), filename)
×
262
        dest = join(self.build_dir, dest)
×
263
        with open(filename, "rb") as fd:
×
264
            data = fd.read()
×
265
        with open(dest, "ab") as fd:
×
266
            fd.write(data)
×
267

268
    @property
4✔
269
    def name(self):
4✔
270
        '''The name of the recipe, the same as the folder containing it.'''
271
        modname = self.__class__.__module__
4✔
272
        return modname.split(".", 2)[-1]
4✔
273

274
    @property
4✔
275
    def filtered_archs(self):
4✔
276
        '''Return archs of self.ctx that are valid build archs
277
        for the Recipe.'''
278
        result = []
×
279
        for arch in self.ctx.archs:
×
280
            if not self.archs or (arch.arch in self.archs):
×
281
                result.append(arch)
×
282
        return result
×
283

284
    def check_recipe_choices(self):
4✔
285
        '''Checks what recipes are being built to see which of the alternative
286
        and optional dependencies are being used,
287
        and returns a list of these.'''
288
        recipes = []
4✔
289
        built_recipes = self.ctx.recipe_build_order
4✔
290
        for recipe in self.depends:
4✔
291
            if isinstance(recipe, (tuple, list)):
4!
292
                for alternative in recipe:
×
293
                    if alternative in built_recipes:
×
294
                        recipes.append(alternative)
×
295
                        break
×
296
        for recipe in self.opt_depends:
4✔
297
            if recipe in built_recipes:
4!
298
                recipes.append(recipe)
×
299
        return sorted(recipes)
4✔
300

301
    def get_opt_depends_in_list(self, recipes):
4✔
302
        '''Given a list of recipe names, returns those that are also in
303
        self.opt_depends.
304
        '''
305
        return [recipe for recipe in recipes if recipe in self.opt_depends]
4✔
306

307
    def get_build_container_dir(self, arch):
4✔
308
        '''Given the arch name, returns the directory where it will be
309
        built.
310

311
        This returns a different directory depending on what
312
        alternative or optional dependencies are being built.
313
        '''
314
        dir_name = self.get_dir_name()
4✔
315
        return join(self.ctx.build_dir, 'other_builds',
4✔
316
                    dir_name, '{}__ndk_target_{}'.format(arch, self.ctx.ndk_api))
317

318
    def get_dir_name(self):
4✔
319
        choices = self.check_recipe_choices()
4✔
320
        dir_name = '-'.join([self.name] + choices)
4✔
321
        return dir_name
4✔
322

323
    def get_build_dir(self, arch):
4✔
324
        '''Given the arch name, returns the directory where the
325
        downloaded/copied package will be built.'''
326

327
        return join(self.get_build_container_dir(arch), self.name)
4✔
328

329
    def get_recipe_dir(self):
4✔
330
        """
331
        Returns the local recipe directory or defaults to the core recipe
332
        directory.
333
        """
334
        if self.ctx.local_recipes is not None:
4!
335
            local_recipe_dir = join(self.ctx.local_recipes, self.name)
×
336
            if exists(local_recipe_dir):
×
337
                return local_recipe_dir
×
338
        return join(self.ctx.root_dir, 'recipes', self.name)
4✔
339

340
    # Public Recipe API to be subclassed if needed
341

342
    def download_if_necessary(self):
4✔
343
        info_main('Downloading {}'.format(self.name))
4✔
344
        user_dir = environ.get('P4A_{}_DIR'.format(self.name.lower()))
4✔
345
        if user_dir is not None:
4✔
346
            info('P4A_{}_DIR is set, skipping download for {}'.format(
4✔
347
                self.name, self.name))
348
            return
4✔
349
        self.download()
4✔
350

351
    def download(self):
4✔
352
        if self.url is None:
4✔
353
            info('Skipping {} download as no URL is set'.format(self.name))
4✔
354
            return
4✔
355

356
        url = self.versioned_url
4✔
357
        expected_digests = {}
4✔
358
        for alg in set(hashlib.algorithms_guaranteed) | set(('md5', 'sha512', 'blake2b')):
4✔
359
            expected_digest = getattr(self, alg + 'sum') if hasattr(self, alg + 'sum') else None
4✔
360
            ma = match(u'^(.+)#' + alg + u'=([0-9a-f]{32,})$', url)
4✔
361
            if ma:                # fragmented URL?
4!
362
                if expected_digest:
×
363
                    raise ValueError(
×
364
                        ('Received {}sum from both the {} recipe '
365
                         'and its url').format(alg, self.name))
366
                url = ma.group(1)
×
367
                expected_digest = ma.group(2)
×
368
            if expected_digest:
4!
369
                expected_digests[alg] = expected_digest
×
370

371
        ensure_dir(join(self.ctx.packages_path, self.name))
4✔
372

373
        with current_directory(join(self.ctx.packages_path, self.name)):
4✔
374
            filename = shprint(sh.basename, url).stdout[:-1].decode('utf-8')
4✔
375

376
            do_download = True
4✔
377
            marker_filename = '.mark-{}'.format(filename)
4✔
378
            if exists(filename) and isfile(filename):
4!
379
                if not exists(marker_filename):
×
380
                    shprint(sh.rm, filename)
×
381
                else:
382
                    for alg, expected_digest in expected_digests.items():
×
383
                        current_digest = algsum(alg, filename)
×
384
                        if current_digest != expected_digest:
×
385
                            debug('* Generated {}sum: {}'.format(alg,
×
386
                                                                 current_digest))
387
                            debug('* Expected {}sum: {}'.format(alg,
×
388
                                                                expected_digest))
389
                            raise ValueError(
×
390
                                ('Generated {0}sum does not match expected {0}sum '
391
                                 'for {1} recipe').format(alg, self.name))
392
                    do_download = False
×
393

394
            # If we got this far, we will download
395
            if do_download:
4!
396
                debug('Downloading {} from {}'.format(self.name, url))
4✔
397

398
                shprint(sh.rm, '-f', marker_filename)
4✔
399
                self.download_file(self.versioned_url, filename)
4✔
400
                touch(marker_filename)
4✔
401

402
                if exists(filename) and isfile(filename):
4!
403
                    for alg, expected_digest in expected_digests.items():
×
404
                        current_digest = algsum(alg, filename)
×
405
                        if current_digest != expected_digest:
×
406
                            debug('* Generated {}sum: {}'.format(alg,
×
407
                                                                 current_digest))
408
                            debug('* Expected {}sum: {}'.format(alg,
×
409
                                                                expected_digest))
410
                            raise ValueError(
×
411
                                ('Generated {0}sum does not match expected {0}sum '
412
                                 'for {1} recipe').format(alg, self.name))
413
            else:
414
                info('{} download already cached, skipping'.format(self.name))
×
415

416
    def unpack(self, arch):
4✔
417
        info_main('Unpacking {} for {}'.format(self.name, arch))
×
418

419
        build_dir = self.get_build_container_dir(arch)
×
420

421
        user_dir = environ.get('P4A_{}_DIR'.format(self.name.lower()))
×
422
        if user_dir is not None:
×
423
            info('P4A_{}_DIR exists, symlinking instead'.format(
×
424
                self.name.lower()))
425
            if exists(self.get_build_dir(arch)):
×
426
                return
×
NEW
427
            rmdir(build_dir)
×
428
            ensure_dir(build_dir)
×
429
            shprint(sh.cp, '-a', user_dir, self.get_build_dir(arch))
×
430
            return
×
431

432
        if self.url is None:
×
433
            info('Skipping {} unpack as no URL is set'.format(self.name))
×
434
            return
×
435

436
        filename = shprint(
×
437
            sh.basename, self.versioned_url).stdout[:-1].decode('utf-8')
438
        ma = match(u'^(.+)#[a-z0-9_]{3,}=([0-9a-f]{32,})$', filename)
×
439
        if ma:                  # fragmented URL?
×
440
            filename = ma.group(1)
×
441

442
        with current_directory(build_dir):
×
443
            directory_name = self.get_build_dir(arch)
×
444

445
            if not exists(directory_name) or not isdir(directory_name):
×
446
                extraction_filename = join(
×
447
                    self.ctx.packages_path, self.name, filename)
448
                if isfile(extraction_filename):
×
449
                    if extraction_filename.endswith('.zip'):
×
450
                        try:
×
451
                            sh.unzip(extraction_filename)
×
452
                        except (sh.ErrorReturnCode_1, sh.ErrorReturnCode_2):
×
453
                            # return code 1 means unzipping had
454
                            # warnings but did complete,
455
                            # apparently happens sometimes with
456
                            # github zips
457
                            pass
×
458
                        import zipfile
×
459
                        fileh = zipfile.ZipFile(extraction_filename, 'r')
×
460
                        root_directory = fileh.filelist[0].filename.split('/')[0]
×
461
                        if root_directory != basename(directory_name):
×
NEW
462
                            move(root_directory, directory_name)
×
463
                    elif extraction_filename.endswith(
×
464
                            ('.tar.gz', '.tgz', '.tar.bz2', '.tbz2', '.tar.xz', '.txz')):
465
                        sh.tar('xf', extraction_filename)
×
466
                        root_directory = sh.tar('tf', extraction_filename).stdout.decode(
×
467
                                'utf-8').split('\n')[0].split('/')[0]
468
                        if root_directory != basename(directory_name):
×
NEW
469
                            move(root_directory, directory_name)
×
470
                    else:
471
                        raise Exception(
×
472
                            'Could not extract {} download, it must be .zip, '
473
                            '.tar.gz or .tar.bz2 or .tar.xz'.format(extraction_filename))
474
                elif isdir(extraction_filename):
×
NEW
475
                    ensure_dir(directory_name)
×
476
                    for entry in listdir(extraction_filename):
×
477
                        if entry not in ('.git',):
×
478
                            shprint(sh.cp, '-Rv',
×
479
                                    join(extraction_filename, entry),
480
                                    directory_name)
481
                else:
482
                    raise Exception(
×
483
                        'Given path is neither a file nor a directory: {}'
484
                        .format(extraction_filename))
485

486
            else:
487
                info('{} is already unpacked, skipping'.format(self.name))
×
488

489
    def get_recipe_env(self, arch=None, with_flags_in_cc=True):
4✔
490
        """Return the env specialized for the recipe
491
        """
492
        if arch is None:
4!
493
            arch = self.filtered_archs[0]
×
494
        env = arch.get_env(with_flags_in_cc=with_flags_in_cc)
4✔
495
        return env
4✔
496

497
    def prebuild_arch(self, arch):
4✔
498
        '''Run any pre-build tasks for the Recipe. By default, this checks if
499
        any prebuild_archname methods exist for the archname of the current
500
        architecture, and runs them if so.'''
501
        prebuild = "prebuild_{}".format(arch.arch.replace('-', '_'))
4✔
502
        if hasattr(self, prebuild):
4!
503
            getattr(self, prebuild)()
×
504
        else:
505
            info('{} has no {}, skipping'.format(self.name, prebuild))
4✔
506

507
    def is_patched(self, arch):
4✔
508
        build_dir = self.get_build_dir(arch.arch)
4✔
509
        return exists(join(build_dir, '.patched'))
4✔
510

511
    def apply_patches(self, arch, build_dir=None):
4✔
512
        '''Apply any patches for the Recipe.
513

514
        .. versionchanged:: 0.6.0
515
            Add ability to apply patches from any dir via kwarg `build_dir`'''
516
        if self.patches:
×
517
            info_main('Applying patches for {}[{}]'
×
518
                      .format(self.name, arch.arch))
519

520
            if self.is_patched(arch):
×
521
                info_main('{} already patched, skipping'.format(self.name))
×
522
                return
×
523

524
            build_dir = build_dir if build_dir else self.get_build_dir(arch.arch)
×
525
            for patch in self.patches:
×
526
                if isinstance(patch, (tuple, list)):
×
527
                    patch, patch_check = patch
×
528
                    if not patch_check(arch=arch, recipe=self):
×
529
                        continue
×
530

531
                self.apply_patch(
×
532
                        patch.format(version=self.version, arch=arch.arch),
533
                        arch.arch, build_dir=build_dir)
534

NEW
535
            touch(join(build_dir, '.patched'))
×
536

537
    def should_build(self, arch):
4✔
538
        '''Should perform any necessary test and return True only if it needs
539
        building again. Per default we implement a library test, in case that
540
        we detect so.
541

542
        '''
543
        if self.built_libraries:
4!
544
            return not all(
4✔
545
                exists(lib) for lib in self.get_libraries(arch.arch)
546
            )
547
        return True
×
548

549
    def build_arch(self, arch):
4✔
550
        '''Run any build tasks for the Recipe. By default, this checks if
551
        any build_archname methods exist for the archname of the current
552
        architecture, and runs them if so.'''
553
        build = "build_{}".format(arch.arch)
×
554
        if hasattr(self, build):
×
555
            getattr(self, build)()
×
556

557
    def install_libraries(self, arch):
4✔
558
        '''This method is always called after `build_arch`. In case that we
559
        detect a library recipe, defined by the class attribute
560
        `built_libraries`, we will copy all defined libraries into the
561
         right location.
562
        '''
563
        if not self.built_libraries:
4!
564
            return
×
565
        shared_libs = [
4✔
566
            lib for lib in self.get_libraries(arch) if lib.endswith(".so")
567
        ]
568
        self.install_libs(arch, *shared_libs)
4✔
569

570
    def postbuild_arch(self, arch):
4✔
571
        '''Run any post-build tasks for the Recipe. By default, this checks if
572
        any postbuild_archname methods exist for the archname of the
573
        current architecture, and runs them if so.
574
        '''
575
        postbuild = "postbuild_{}".format(arch.arch)
4✔
576
        if hasattr(self, postbuild):
4!
577
            getattr(self, postbuild)()
×
578

579
        if self.need_stl_shared:
4!
580
            self.install_stl_lib(arch)
4✔
581

582
    def prepare_build_dir(self, arch):
4✔
583
        '''Copies the recipe data into a build dir for the given arch. By
584
        default, this unpacks a downloaded recipe. You should override
585
        it (or use a Recipe subclass with different behaviour) if you
586
        want to do something else.
587
        '''
588
        self.unpack(arch)
×
589

590
    def clean_build(self, arch=None):
4✔
591
        '''Deletes all the build information of the recipe.
592

593
        If arch is not None, only this arch dir is deleted. Otherwise
594
        (the default) all builds for all archs are deleted.
595

596
        By default, this just deletes the main build dir. If the
597
        recipe has e.g. object files biglinked, or .so files stored
598
        elsewhere, you should override this method.
599

600
        This method is intended for testing purposes, it may have
601
        strange results. Rebuild everything if this seems to happen.
602

603
        '''
604
        if arch is None:
×
605
            base_dir = join(self.ctx.build_dir, 'other_builds', self.name)
×
606
        else:
607
            base_dir = self.get_build_container_dir(arch)
×
608
        dirs = glob.glob(base_dir + '-*')
×
609
        if exists(base_dir):
×
610
            dirs.append(base_dir)
×
611
        if not dirs:
×
612
            warning('Attempted to clean build for {} but found no existing '
×
613
                    'build dirs'.format(self.name))
614

615
        for directory in dirs:
×
NEW
616
            rmdir(directory)
×
617

618
        # Delete any Python distributions to ensure the recipe build
619
        # doesn't persist in site-packages
NEW
620
        rmdir(self.ctx.python_installs_dir)
×
621

622
    def install_libs(self, arch, *libs):
4✔
623
        libs_dir = self.ctx.get_libs_dir(arch.arch)
4✔
624
        if not libs:
4!
625
            warning('install_libs called with no libraries to install!')
×
626
            return
×
627
        args = libs + (libs_dir,)
4✔
628
        shprint(sh.cp, *args)
4✔
629

630
    def has_libs(self, arch, *libs):
4✔
631
        return all(map(lambda lib: self.ctx.has_lib(arch.arch, lib), libs))
×
632

633
    def get_libraries(self, arch_name, in_context=False):
4✔
634
        """Return the full path of the library depending on the architecture.
635
        Per default, the build library path it will be returned, unless
636
        `get_libraries` has been called with kwarg `in_context` set to
637
        True.
638

639
        .. note:: this method should be used for library recipes only
640
        """
641
        recipe_libs = set()
4✔
642
        if not self.built_libraries:
4!
643
            return recipe_libs
×
644
        for lib, rel_path in self.built_libraries.items():
4✔
645
            if not in_context:
4!
646
                abs_path = join(self.get_build_dir(arch_name), rel_path, lib)
4✔
647
                if rel_path in {".", "", None}:
4✔
648
                    abs_path = join(self.get_build_dir(arch_name), lib)
4✔
649
            else:
650
                abs_path = join(self.ctx.get_libs_dir(arch_name), lib)
×
651
            recipe_libs.add(abs_path)
4✔
652
        return recipe_libs
4✔
653

654
    @classmethod
4✔
655
    def recipe_dirs(cls, ctx):
4✔
656
        recipe_dirs = []
4✔
657
        if ctx.local_recipes is not None:
4✔
658
            recipe_dirs.append(realpath(ctx.local_recipes))
4✔
659
        if ctx.storage_dir:
4✔
660
            recipe_dirs.append(join(ctx.storage_dir, 'recipes'))
4✔
661
        recipe_dirs.append(join(ctx.root_dir, "recipes"))
4✔
662
        return recipe_dirs
4✔
663

664
    @classmethod
4✔
665
    def list_recipes(cls, ctx):
4✔
666
        forbidden_dirs = ('__pycache__', )
4✔
667
        for recipes_dir in cls.recipe_dirs(ctx):
4✔
668
            if recipes_dir and exists(recipes_dir):
4✔
669
                for name in listdir(recipes_dir):
4✔
670
                    if name in forbidden_dirs:
4✔
671
                        continue
4✔
672
                    fn = join(recipes_dir, name)
4✔
673
                    if isdir(fn):
4✔
674
                        yield name
4✔
675

676
    @classmethod
4✔
677
    def get_recipe(cls, name, ctx):
4✔
678
        '''Returns the Recipe with the given name, if it exists.'''
679
        name = name.lower()
4✔
680
        if not hasattr(cls, "recipes"):
4✔
681
            cls.recipes = {}
4✔
682
        if name in cls.recipes:
4✔
683
            return cls.recipes[name]
4✔
684

685
        recipe_file = None
4✔
686
        for recipes_dir in cls.recipe_dirs(ctx):
4✔
687
            if not exists(recipes_dir):
4✔
688
                continue
4✔
689
            # Find matching folder (may differ in case):
690
            for subfolder in listdir(recipes_dir):
4✔
691
                if subfolder.lower() == name:
4✔
692
                    recipe_file = join(recipes_dir, subfolder, '__init__.py')
4✔
693
                    if exists(recipe_file):
4!
694
                        name = subfolder  # adapt to actual spelling
4✔
695
                        break
4✔
696
                    recipe_file = None
×
697
            if recipe_file is not None:
4✔
698
                break
4✔
699

700
        else:
701
            raise ValueError('Recipe does not exist: {}'.format(name))
4✔
702

703
        mod = import_recipe('pythonforandroid.recipes.{}'.format(name), recipe_file)
4✔
704
        if len(logger.handlers) > 1:
4!
705
            logger.removeHandler(logger.handlers[1])
×
706
        recipe = mod.recipe
4✔
707
        recipe.ctx = ctx
4✔
708
        cls.recipes[name.lower()] = recipe
4✔
709
        return recipe
4✔
710

711

712
class IncludedFilesBehaviour(object):
4✔
713
    '''Recipe mixin class that will automatically unpack files included in
714
    the recipe directory.'''
715
    src_filename = None
4✔
716

717
    def prepare_build_dir(self, arch):
4✔
718
        if self.src_filename is None:
×
719
            raise BuildInterruptingException(
×
720
                'IncludedFilesBehaviour failed: no src_filename specified')
NEW
721
        rmdir(self.get_build_dir(arch))
×
722
        shprint(sh.cp, '-a', join(self.get_recipe_dir(), self.src_filename),
×
723
                self.get_build_dir(arch))
724

725

726
class BootstrapNDKRecipe(Recipe):
4✔
727
    '''A recipe class for recipes built in an Android project jni dir with
728
    an Android.mk. These are not cached separatly, but built in the
729
    bootstrap's own building directory.
730

731
    To build an NDK project which is not part of the bootstrap, see
732
    :class:`~pythonforandroid.recipe.NDKRecipe`.
733

734
    To link with python, call the method :meth:`get_recipe_env`
735
    with the kwarg *with_python=True*.
736
    '''
737

738
    dir_name = None  # The name of the recipe build folder in the jni dir
4✔
739

740
    def get_build_container_dir(self, arch):
4✔
741
        return self.get_jni_dir()
×
742

743
    def get_build_dir(self, arch):
4✔
744
        if self.dir_name is None:
×
745
            raise ValueError('{} recipe doesn\'t define a dir_name, but '
×
746
                             'this is necessary'.format(self.name))
747
        return join(self.get_build_container_dir(arch), self.dir_name)
×
748

749
    def get_jni_dir(self):
4✔
750
        return join(self.ctx.bootstrap.build_dir, 'jni')
4✔
751

752
    def get_recipe_env(self, arch=None, with_flags_in_cc=True, with_python=False):
4✔
753
        env = super().get_recipe_env(arch, with_flags_in_cc)
×
754
        if not with_python:
×
755
            return env
×
756

757
        env['PYTHON_INCLUDE_ROOT'] = self.ctx.python_recipe.include_root(arch.arch)
×
758
        env['PYTHON_LINK_ROOT'] = self.ctx.python_recipe.link_root(arch.arch)
×
759
        env['EXTRA_LDLIBS'] = ' -lpython{}'.format(
×
760
            self.ctx.python_recipe.link_version)
761
        return env
×
762

763

764
class NDKRecipe(Recipe):
4✔
765
    '''A recipe class for any NDK project not included in the bootstrap.'''
766

767
    generated_libraries = []
4✔
768

769
    def should_build(self, arch):
4✔
770
        lib_dir = self.get_lib_dir(arch)
×
771

772
        for lib in self.generated_libraries:
×
773
            if not exists(join(lib_dir, lib)):
×
774
                return True
×
775

776
        return False
×
777

778
    def get_lib_dir(self, arch):
4✔
779
        return join(self.get_build_dir(arch.arch), 'obj', 'local', arch.arch)
×
780

781
    def get_jni_dir(self, arch):
4✔
782
        return join(self.get_build_dir(arch.arch), 'jni')
×
783

784
    def build_arch(self, arch, *extra_args):
4✔
785
        super().build_arch(arch)
×
786

787
        env = self.get_recipe_env(arch)
×
788
        with current_directory(self.get_build_dir(arch.arch)):
×
789
            shprint(
×
790
                sh.Command(join(self.ctx.ndk_dir, "ndk-build")),
791
                'V=1',
792
                'NDK_DEBUG=' + ("1" if self.ctx.build_as_debuggable else "0"),
793
                'APP_PLATFORM=android-' + str(self.ctx.ndk_api),
794
                'APP_ABI=' + arch.arch,
795
                *extra_args, _env=env
796
            )
797

798

799
class PythonRecipe(Recipe):
4✔
800
    site_packages_name = None
4✔
801
    '''The name of the module's folder when installed in the Python
2✔
802
    site-packages (e.g. for pyjnius it is 'jnius')'''
803

804
    call_hostpython_via_targetpython = True
4✔
805
    '''If True, tries to install the module using the hostpython binary
2✔
806
    copied to the target (normally arm) python build dir. However, this
807
    will fail if the module tries to import e.g. _io.so. Set this to False
808
    to call hostpython from its own build dir, installing the module in
809
    the right place via arguments to setup.py. However, this may not set
810
    the environment correctly and so False is not the default.'''
811

812
    install_in_hostpython = False
4✔
813
    '''If True, additionally installs the module in the hostpython build
2✔
814
    dir. This will make it available to other recipes if
815
    call_hostpython_via_targetpython is False.
816
    '''
817

818
    install_in_targetpython = True
4✔
819
    '''If True, installs the module in the targetpython installation dir.
2✔
820
    This is almost always what you want to do.'''
821

822
    setup_extra_args = []
4✔
823
    '''List of extra arguments to pass to setup.py'''
2✔
824

825
    depends = ['python3']
4✔
826
    '''
2✔
827
    .. note:: it's important to keep this depends as a class attribute outside
828
              `__init__` because sometimes we only initialize the class, so the
829
              `__init__` call won't be called and the deps would be missing
830
              (which breaks the dependency graph computation)
831

832
    .. warning:: don't forget to call `super().__init__()` in any recipe's
833
                 `__init__`, or otherwise it may not be ensured that it depends
834
                 on python2 or python3 which can break the dependency graph
835
    '''
836

837
    def __init__(self, *args, **kwargs):
4✔
838
        super().__init__(*args, **kwargs)
4✔
839

840
        if 'python3' not in self.depends:
4✔
841
            # We ensure here that the recipe depends on python even it overrode
842
            # `depends`. We only do this if it doesn't already depend on any
843
            # python, since some recipes intentionally don't depend on/work
844
            # with all python variants
845
            depends = self.depends
4✔
846
            depends.append('python3')
4✔
847
            depends = list(set(depends))
4✔
848
            self.depends = depends
4✔
849

850
    def clean_build(self, arch=None):
4✔
851
        super().clean_build(arch=arch)
×
852
        name = self.folder_name
×
853
        python_install_dirs = glob.glob(join(self.ctx.python_installs_dir, '*'))
×
854
        for python_install in python_install_dirs:
×
855
            site_packages_dir = glob.glob(join(python_install, 'lib', 'python*',
×
856
                                               'site-packages'))
857
            if site_packages_dir:
×
858
                build_dir = join(site_packages_dir[0], name)
×
859
                if exists(build_dir):
×
860
                    info('Deleted {}'.format(build_dir))
×
NEW
861
                    rmdir(build_dir)
×
862

863
    @property
4✔
864
    def real_hostpython_location(self):
4✔
865
        host_name = 'host{}'.format(self.ctx.python_recipe.name)
4✔
866
        if host_name == 'hostpython3':
4!
867
            python_recipe = Recipe.get_recipe(host_name, self.ctx)
4✔
868
            return python_recipe.python_exe
4✔
869
        else:
870
            python_recipe = self.ctx.python_recipe
×
871
            return 'python{}'.format(python_recipe.version)
×
872

873
    @property
4✔
874
    def hostpython_location(self):
4✔
875
        if not self.call_hostpython_via_targetpython:
4!
876
            return self.real_hostpython_location
4✔
877
        return self.ctx.hostpython
×
878

879
    @property
4✔
880
    def folder_name(self):
4✔
881
        '''The name of the build folders containing this recipe.'''
882
        name = self.site_packages_name
×
883
        if name is None:
×
884
            name = self.name
×
885
        return name
×
886

887
    def get_recipe_env(self, arch=None, with_flags_in_cc=True):
4✔
888
        env = super().get_recipe_env(arch, with_flags_in_cc)
4✔
889

890
        env['PYTHONNOUSERSITE'] = '1'
4✔
891

892
        # Set the LANG, this isn't usually important but is a better default
893
        # as it occasionally matters how Python e.g. reads files
894
        env['LANG'] = "en_GB.UTF-8"
4✔
895

896
        if not self.call_hostpython_via_targetpython:
4!
897
            env['CFLAGS'] += ' -I{}'.format(
4✔
898
                self.ctx.python_recipe.include_root(arch.arch)
899
            )
900
            env['LDFLAGS'] += ' -L{} -lpython{}'.format(
4✔
901
                self.ctx.python_recipe.link_root(arch.arch),
902
                self.ctx.python_recipe.link_version,
903
            )
904

905
            hppath = []
4✔
906
            hppath.append(join(dirname(self.hostpython_location), 'Lib'))
4✔
907
            hppath.append(join(hppath[0], 'site-packages'))
4✔
908
            builddir = join(dirname(self.hostpython_location), 'build')
4✔
909
            if exists(builddir):
4!
910
                hppath += [join(builddir, d) for d in listdir(builddir)
×
911
                           if isdir(join(builddir, d))]
912
            if len(hppath) > 0:
4!
913
                if 'PYTHONPATH' in env:
4!
914
                    env['PYTHONPATH'] = ':'.join(hppath + [env['PYTHONPATH']])
×
915
                else:
916
                    env['PYTHONPATH'] = ':'.join(hppath)
4✔
917
        return env
4✔
918

919
    def should_build(self, arch):
4✔
920
        name = self.folder_name
×
921
        if self.ctx.has_package(name, arch):
×
922
            info('Python package already exists in site-packages')
×
923
            return False
×
924
        info('{} apparently isn\'t already in site-packages'.format(name))
×
925
        return True
×
926

927
    def build_arch(self, arch):
4✔
928
        '''Install the Python module by calling setup.py install with
929
        the target Python dir.'''
930
        super().build_arch(arch)
×
931
        self.install_python_package(arch)
×
932

933
    def install_python_package(self, arch, name=None, env=None, is_dir=True):
4✔
934
        '''Automate the installation of a Python package (or a cython
935
        package where the cython components are pre-built).'''
936
        # arch = self.filtered_archs[0]  # old kivy-ios way
937
        if name is None:
×
938
            name = self.name
×
939
        if env is None:
×
940
            env = self.get_recipe_env(arch)
×
941

942
        info('Installing {} into site-packages'.format(self.name))
×
943

944
        hostpython = sh.Command(self.hostpython_location)
×
945
        hpenv = env.copy()
×
946
        with current_directory(self.get_build_dir(arch.arch)):
×
947
            shprint(hostpython, 'setup.py', 'install', '-O2',
×
948
                    '--root={}'.format(self.ctx.get_python_install_dir(arch.arch)),
949
                    '--install-lib=.',
950
                    _env=hpenv, *self.setup_extra_args)
951

952
            # If asked, also install in the hostpython build dir
953
            if self.install_in_hostpython:
×
954
                self.install_hostpython_package(arch)
×
955

956
    def get_hostrecipe_env(self, arch):
4✔
957
        env = environ.copy()
×
958
        env['PYTHONPATH'] = join(dirname(self.real_hostpython_location), 'Lib', 'site-packages')
×
959
        return env
×
960

961
    def install_hostpython_package(self, arch):
4✔
962
        env = self.get_hostrecipe_env(arch)
×
963
        real_hostpython = sh.Command(self.real_hostpython_location)
×
964
        shprint(real_hostpython, 'setup.py', 'install', '-O2',
×
965
                '--root={}'.format(dirname(self.real_hostpython_location)),
966
                '--install-lib=Lib/site-packages',
967
                _env=env, *self.setup_extra_args)
968

969

970
class CompiledComponentsPythonRecipe(PythonRecipe):
4✔
971
    pre_build_ext = False
4✔
972

973
    build_cmd = 'build_ext'
4✔
974

975
    def build_arch(self, arch):
4✔
976
        '''Build any cython components, then install the Python module by
977
        calling setup.py install with the target Python dir.
978
        '''
979
        Recipe.build_arch(self, arch)
×
980
        self.build_compiled_components(arch)
×
981
        self.install_python_package(arch)
×
982

983
    def build_compiled_components(self, arch):
4✔
984
        info('Building compiled components in {}'.format(self.name))
×
985

986
        env = self.get_recipe_env(arch)
×
987
        hostpython = sh.Command(self.hostpython_location)
×
988
        with current_directory(self.get_build_dir(arch.arch)):
×
989
            if self.install_in_hostpython:
×
990
                shprint(hostpython, 'setup.py', 'clean', '--all', _env=env)
×
991
            shprint(hostpython, 'setup.py', self.build_cmd, '-v',
×
992
                    _env=env, *self.setup_extra_args)
993
            build_dir = glob.glob('build/lib.*')[0]
×
994
            shprint(sh.find, build_dir, '-name', '"*.o"', '-exec',
×
995
                    env['STRIP'], '{}', ';', _env=env)
996

997
    def install_hostpython_package(self, arch):
4✔
998
        env = self.get_hostrecipe_env(arch)
×
999
        self.rebuild_compiled_components(arch, env)
×
1000
        super().install_hostpython_package(arch)
×
1001

1002
    def rebuild_compiled_components(self, arch, env):
4✔
1003
        info('Rebuilding compiled components in {}'.format(self.name))
×
1004

1005
        hostpython = sh.Command(self.real_hostpython_location)
×
1006
        shprint(hostpython, 'setup.py', 'clean', '--all', _env=env)
×
1007
        shprint(hostpython, 'setup.py', self.build_cmd, '-v', _env=env,
×
1008
                *self.setup_extra_args)
1009

1010

1011
class CppCompiledComponentsPythonRecipe(CompiledComponentsPythonRecipe):
4✔
1012
    """ Extensions that require the cxx-stl """
1013
    call_hostpython_via_targetpython = False
4✔
1014
    need_stl_shared = True
4✔
1015

1016

1017
class CythonRecipe(PythonRecipe):
4✔
1018
    pre_build_ext = False
4✔
1019
    cythonize = True
4✔
1020
    cython_args = []
4✔
1021
    call_hostpython_via_targetpython = False
4✔
1022

1023
    def build_arch(self, arch):
4✔
1024
        '''Build any cython components, then install the Python module by
1025
        calling setup.py install with the target Python dir.
1026
        '''
1027
        Recipe.build_arch(self, arch)
×
1028
        self.build_cython_components(arch)
×
1029
        self.install_python_package(arch)
×
1030

1031
    def build_cython_components(self, arch):
4✔
1032
        info('Cythonizing anything necessary in {}'.format(self.name))
×
1033

1034
        env = self.get_recipe_env(arch)
×
1035

1036
        with current_directory(self.get_build_dir(arch.arch)):
×
1037
            hostpython = sh.Command(self.ctx.hostpython)
×
1038
            shprint(hostpython, '-c', 'import sys; print(sys.path)', _env=env)
×
1039
            debug('cwd is {}'.format(realpath(curdir)))
×
1040
            info('Trying first build of {} to get cython files: this is '
×
1041
                 'expected to fail'.format(self.name))
1042

1043
            manually_cythonise = False
×
1044
            try:
×
1045
                shprint(hostpython, 'setup.py', 'build_ext', '-v', _env=env,
×
1046
                        *self.setup_extra_args)
1047
            except sh.ErrorReturnCode_1:
×
1048
                print()
×
1049
                info('{} first build failed (as expected)'.format(self.name))
×
1050
                manually_cythonise = True
×
1051

1052
            if manually_cythonise:
×
1053
                self.cythonize_build(env=env)
×
1054
                shprint(hostpython, 'setup.py', 'build_ext', '-v', _env=env,
×
1055
                        _tail=20, _critical=True, *self.setup_extra_args)
1056
            else:
1057
                info('First build appeared to complete correctly, skipping manual'
×
1058
                     'cythonising.')
1059

1060
            if not self.ctx.with_debug_symbols:
×
1061
                self.strip_object_files(arch, env)
×
1062

1063
    def strip_object_files(self, arch, env, build_dir=None):
4✔
1064
        if build_dir is None:
×
1065
            build_dir = self.get_build_dir(arch.arch)
×
1066
        with current_directory(build_dir):
×
1067
            info('Stripping object files')
×
1068
            shprint(sh.find, '.', '-iname', '*.so', '-exec',
×
1069
                    '/usr/bin/echo', '{}', ';', _env=env)
1070
            shprint(sh.find, '.', '-iname', '*.so', '-exec',
×
1071
                    env['STRIP'].split(' ')[0], '--strip-unneeded',
1072
                    # '/usr/bin/strip', '--strip-unneeded',
1073
                    '{}', ';', _env=env)
1074

1075
    def cythonize_file(self, env, build_dir, filename):
4✔
1076
        short_filename = filename
×
1077
        if filename.startswith(build_dir):
×
1078
            short_filename = filename[len(build_dir) + 1:]
×
1079
        info(u"Cythonize {}".format(short_filename))
×
1080
        cyenv = env.copy()
×
1081
        if 'CYTHONPATH' in cyenv:
×
1082
            cyenv['PYTHONPATH'] = cyenv['CYTHONPATH']
×
1083
        elif 'PYTHONPATH' in cyenv:
×
1084
            del cyenv['PYTHONPATH']
×
1085
        if 'PYTHONNOUSERSITE' in cyenv:
×
1086
            cyenv.pop('PYTHONNOUSERSITE')
×
1087
        python_command = sh.Command("python{}".format(
×
1088
            self.ctx.python_recipe.major_minor_version_string.split(".")[0]
1089
        ))
1090
        shprint(python_command, "-c"
×
1091
                "import sys; from Cython.Compiler.Main import setuptools_main; sys.exit(setuptools_main());",
1092
                filename, *self.cython_args, _env=cyenv)
1093

1094
    def cythonize_build(self, env, build_dir="."):
4✔
1095
        if not self.cythonize:
×
1096
            info('Running cython cancelled per recipe setting')
×
1097
            return
×
1098
        info('Running cython where appropriate')
×
1099
        for root, dirnames, filenames in walk("."):
×
1100
            for filename in fnmatch.filter(filenames, "*.pyx"):
×
1101
                self.cythonize_file(env, build_dir, join(root, filename))
×
1102

1103
    def get_recipe_env(self, arch, with_flags_in_cc=True):
4✔
1104
        env = super().get_recipe_env(arch, with_flags_in_cc)
×
1105
        env['LDFLAGS'] = env['LDFLAGS'] + ' -L{} '.format(
×
1106
            self.ctx.get_libs_dir(arch.arch) +
1107
            ' -L{} '.format(self.ctx.libs_dir) +
1108
            ' -L{}'.format(join(self.ctx.bootstrap.build_dir, 'obj', 'local',
1109
                                arch.arch)))
1110

1111
        env['LDSHARED'] = env['CC'] + ' -shared'
×
1112
        # shprint(sh.whereis, env['LDSHARED'], _env=env)
1113
        env['LIBLINK'] = 'NOTNONE'
×
1114
        if self.ctx.copy_libs:
×
1115
            env['COPYLIBS'] = '1'
×
1116

1117
        # Every recipe uses its own liblink path, object files are
1118
        # collected and biglinked later
1119
        liblink_path = join(self.get_build_container_dir(arch.arch),
×
1120
                            'objects_{}'.format(self.name))
1121
        env['LIBLINK_PATH'] = liblink_path
×
1122
        ensure_dir(liblink_path)
×
1123

1124
        return env
×
1125

1126

1127
class TargetPythonRecipe(Recipe):
4✔
1128
    '''Class for target python recipes. Sets ctx.python_recipe to point to
1129
    itself, so as to know later what kind of Python was built or used.'''
1130

1131
    def __init__(self, *args, **kwargs):
4✔
1132
        self._ctx = None
4✔
1133
        super().__init__(*args, **kwargs)
4✔
1134

1135
    def prebuild_arch(self, arch):
4✔
1136
        super().prebuild_arch(arch)
×
1137
        self.ctx.python_recipe = self
×
1138

1139
    def include_root(self, arch):
4✔
1140
        '''The root directory from which to include headers.'''
1141
        raise NotImplementedError('Not implemented in TargetPythonRecipe')
1142

1143
    def link_root(self):
4✔
1144
        raise NotImplementedError('Not implemented in TargetPythonRecipe')
1145

1146
    @property
4✔
1147
    def major_minor_version_string(self):
4✔
1148
        from distutils.version import LooseVersion
4✔
1149
        return '.'.join([str(v) for v in LooseVersion(self.version).version[:2]])
4✔
1150

1151
    def create_python_bundle(self, dirn, arch):
4✔
1152
        """
1153
        Create a packaged python bundle in the target directory, by
1154
        copying all the modules and standard library to the right
1155
        place.
1156
        """
1157
        raise NotImplementedError('{} does not implement create_python_bundle'.format(self))
1158

1159
    def reduce_object_file_names(self, dirn):
4✔
1160
        """Recursively renames all files named XXX.cpython-...-linux-gnu.so"
1161
        to "XXX.so", i.e. removing the erroneous architecture name
1162
        coming from the local system.
1163
        """
1164
        py_so_files = shprint(sh.find, dirn, '-iname', '*.so')
4✔
1165
        filens = py_so_files.stdout.decode('utf-8').split('\n')[:-1]
4✔
1166
        for filen in filens:
4!
1167
            file_dirname, file_basename = split(filen)
×
1168
            parts = file_basename.split('.')
×
1169
            if len(parts) <= 2:
×
1170
                continue
×
NEW
1171
            move(filen, join(file_dirname, parts[0] + '.so'))
×
1172

1173

1174
def algsum(alg, filen):
4✔
1175
    '''Calculate the digest of a file.
1176
    '''
1177
    with open(filen, 'rb') as fileh:
×
1178
        digest = getattr(hashlib, alg)(fileh.read())
×
1179

1180
    return digest.hexdigest()
×
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