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

kivy / python-for-android / 5976260269

25 Aug 2023 01:05PM UTC coverage: 58.986% (+0.3%) from 58.676%
5976260269

push

github

web-flow
Standardise `ensure_dir` and `rmdir` (#2871)

* Standardise ensure_dir and rmdir

* Standardise ensure_dir and rmdir

* Add libmysqlclient to broken list

* Libtorrent failing to be rebuilt

* Add boost to broken recipes list

940 of 2241 branches covered (0.0%)

Branch coverage included in aggregate %.

73 of 113 new or added lines in 21 files covered. (64.6%)

3 existing lines in 3 files now uncovered.

4715 of 7346 relevant lines covered (64.18%)

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 (logger, info, warning, debug, shprint, info_main)
4✔
20
from pythonforandroid.util import (current_directory, ensure_dir,
4✔
21
                                   BuildInterruptingException, rmdir)
22
from pythonforandroid.util import load_source as import_recipe
4✔
23

24

25
url_opener = urllib.request.build_opener()
4✔
26
url_orig_headers = url_opener.addheaders
4✔
27
urllib.request.install_opener(url_opener)
4✔
28

29

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

38
        return super().__new__(cls, name, bases, dct)
4✔
39

40

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

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

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

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

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

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

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

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

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

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

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

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

102
    archs = ['armeabi']  # Not currently implemented properly
4✔
103

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

114
    Here an example of how it would look like for `libffi` recipe:
115

116
        - `built_libraries = {'libffi.so': '.libs'}`
117

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

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

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

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

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

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

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

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

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

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

175
        if cwd:
4!
176
            target = join(cwd, target)
×
177

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

190
            if exists(target):
4!
191
                unlink(target)
×
192

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

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

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

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

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

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

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

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

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

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

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

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

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

325
        return join(self.get_build_container_dir(arch), self.name)
4✔
326

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

338
    # Public Recipe API to be subclassed if needed
339

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

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

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

369
        ensure_dir(join(self.ctx.packages_path, self.name))
4✔
370

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

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

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

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

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

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

417
        build_dir = self.get_build_container_dir(arch)
×
418

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

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

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

440
        with current_directory(build_dir):
×
441
            directory_name = self.get_build_dir(arch)
×
442

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

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

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

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

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

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

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

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

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

529
                self.apply_patch(
×
530
                        patch.format(version=self.version, arch=arch.arch),
531
                        arch.arch, build_dir=build_dir)
532

533
            shprint(sh.touch, join(build_dir, '.patched'))
×
534

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

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

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

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

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

577
        if self.need_stl_shared:
4!
578
            self.install_stl_lib(arch)
4✔
579

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

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

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

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

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

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

613
        for directory in dirs:
×
NEW
614
            rmdir(directory)
×
615

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

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

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

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

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

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

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

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

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

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

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

709

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

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

723

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

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

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

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

738
    def get_build_container_dir(self, arch):
4✔
739
        return self.get_jni_dir()
×
740

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

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

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

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

761

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

765
    generated_libraries = []
4✔
766

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

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

774
        return False
×
775

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

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

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

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

796

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

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

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

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

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

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

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

835
    def __init__(self, *args, **kwargs):
4✔
836
        super().__init__(*args, **kwargs)
4✔
837

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

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

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

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

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

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

888
        env['PYTHONNOUSERSITE'] = '1'
4✔
889

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

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

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

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

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

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

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

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

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

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

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

967

968
class CompiledComponentsPythonRecipe(PythonRecipe):
4✔
969
    pre_build_ext = False
4✔
970

971
    build_cmd = 'build_ext'
4✔
972

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

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

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

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

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

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

1008

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

1014

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

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

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

1032
        env = self.get_recipe_env(arch)
×
1033

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

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

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

1058
            if not self.ctx.with_debug_symbols:
×
1059
                self.strip_object_files(arch, env)
×
1060

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

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

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

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

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

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

1122
        return env
×
1123

1124

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

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

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

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

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

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

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

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

1171

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

1178
    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