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

kivy / python-for-android / 3770697291

pending completion
3770697291

push

github

GitHub
Merge pull request #2718 from kivy/release-2022.12.20

877 of 2011 branches covered (43.61%)

Branch coverage included in aggregate %.

38 of 86 new or added lines in 16 files covered. (44.19%)

10 existing lines in 4 files now uncovered.

4515 of 6886 relevant lines covered (65.57%)

2.59 hits per line

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

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

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

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

25

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

30

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

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

41

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

339
    # Public Recipe API to be subclassed if needed
340

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

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

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

370
        shprint(sh.mkdir, '-p', join(self.ctx.packages_path, self.name))
4✔
371

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

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

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

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

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

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

418
        build_dir = self.get_build_container_dir(arch)
×
419

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

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

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

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

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

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

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

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

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

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

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

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

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

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

536
            shprint(sh.touch, join(build_dir, '.patched'))
×
537

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

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

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

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

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

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

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

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

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

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

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

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

616
        for directory in dirs:
×
617
            if exists(directory):
×
618
                info('Deleting {}'.format(directory))
×
619
                shutil.rmtree(directory)
×
620

621
        # Delete any Python distributions to ensure the recipe build
622
        # doesn't persist in site-packages
623
        shutil.rmtree(self.ctx.python_installs_dir)
×
624

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

633
    def has_libs(self, arch, *libs):
4✔
NEW
634
        return all(map(lambda lib: self.ctx.has_lib(arch.arch, lib), libs))
×
635

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

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

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

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

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

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

703
        else:
704
            raise ValueError('Recipe does not exist: {}'.format(name))
4✔
705

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

714

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

720
    def prepare_build_dir(self, arch):
4✔
721
        if self.src_filename is None:
×
722
            raise BuildInterruptingException(
×
723
                'IncludedFilesBehaviour failed: no src_filename specified')
724
        shprint(sh.rm, '-rf', self.get_build_dir(arch))
×
725
        shprint(sh.cp, '-a', join(self.get_recipe_dir(), self.src_filename),
×
726
                self.get_build_dir(arch))
727

728

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

734
    To build an NDK project which is not part of the bootstrap, see
735
    :class:`~pythonforandroid.recipe.NDKRecipe`.
736

737
    To link with python, call the method :meth:`get_recipe_env`
738
    with the kwarg *with_python=True*.
739
    '''
740

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

743
    def get_build_container_dir(self, arch):
4✔
744
        return self.get_jni_dir()
×
745

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

752
    def get_jni_dir(self):
4✔
753
        return join(self.ctx.bootstrap.build_dir, 'jni')
4✔
754

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

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

766

767
class NDKRecipe(Recipe):
4✔
768
    '''A recipe class for any NDK project not included in the bootstrap.'''
769

770
    generated_libraries = []
4✔
771

772
    def should_build(self, arch):
4✔
773
        lib_dir = self.get_lib_dir(arch)
×
774

775
        for lib in self.generated_libraries:
×
776
            if not exists(join(lib_dir, lib)):
×
777
                return True
×
778

779
        return False
×
780

781
    def get_lib_dir(self, arch):
4✔
782
        return join(self.get_build_dir(arch.arch), 'obj', 'local', arch.arch)
×
783

784
    def get_jni_dir(self, arch):
4✔
785
        return join(self.get_build_dir(arch.arch), 'jni')
×
786

787
    def build_arch(self, arch, *extra_args):
4✔
788
        super().build_arch(arch)
×
789

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

801

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

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

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

821
    install_in_targetpython = True
4✔
822
    '''If True, installs the module in the targetpython installation dir.
1✔
823
    This is almost always what you want to do.'''
824

825
    setup_extra_args = []
4✔
826
    '''List of extra arguments to pass to setup.py'''
1✔
827

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

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

840
    def __init__(self, *args, **kwargs):
4✔
841
        super().__init__(*args, **kwargs)
4✔
842

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

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

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

876
    @property
4✔
877
    def hostpython_location(self):
3✔
878
        if not self.call_hostpython_via_targetpython:
4!
879
            return self.real_hostpython_location
4✔
880
        return self.ctx.hostpython
×
881

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

890
    def get_recipe_env(self, arch=None, with_flags_in_cc=True):
4✔
891
        env = super().get_recipe_env(arch, with_flags_in_cc)
4✔
892

893
        env['PYTHONNOUSERSITE'] = '1'
4✔
894

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

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

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

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

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

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

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

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

955
            # If asked, also install in the hostpython build dir
956
            if self.install_in_hostpython:
×
957
                self.install_hostpython_package(arch)
×
958

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

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

972

973
class CompiledComponentsPythonRecipe(PythonRecipe):
4✔
974
    pre_build_ext = False
4✔
975

976
    build_cmd = 'build_ext'
4✔
977

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

986
    def build_compiled_components(self, arch):
4✔
987
        info('Building compiled components in {}'.format(self.name))
×
988

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

1000
    def install_hostpython_package(self, arch):
4✔
1001
        env = self.get_hostrecipe_env(arch)
×
1002
        self.rebuild_compiled_components(arch, env)
×
1003
        super().install_hostpython_package(arch)
×
1004

1005
    def rebuild_compiled_components(self, arch, env):
4✔
1006
        info('Rebuilding compiled components in {}'.format(self.name))
×
1007

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

1013

1014
class CppCompiledComponentsPythonRecipe(CompiledComponentsPythonRecipe):
4✔
1015
    """ Extensions that require the cxx-stl """
1016
    call_hostpython_via_targetpython = False
4✔
1017
    need_stl_shared = True
4✔
1018

1019

1020
class CythonRecipe(PythonRecipe):
4✔
1021
    pre_build_ext = False
4✔
1022
    cythonize = True
4✔
1023
    cython_args = []
4✔
1024
    call_hostpython_via_targetpython = False
4✔
1025

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

1034
    def build_cython_components(self, arch):
4✔
1035
        info('Cythonizing anything necessary in {}'.format(self.name))
×
1036

1037
        env = self.get_recipe_env(arch)
×
1038

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

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

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

1063
            if not self.ctx.with_debug_symbols:
×
1064
                self.strip_object_files(arch, env)
×
1065

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

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

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

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

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

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

1127
        return env
×
1128

1129

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

1134
    def __init__(self, *args, **kwargs):
4✔
1135
        self._ctx = None
4✔
1136
        super().__init__(*args, **kwargs)
4✔
1137

1138
    def prebuild_arch(self, arch):
4✔
1139
        super().prebuild_arch(arch)
×
1140
        self.ctx.python_recipe = self
×
1141

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

1146
    def link_root(self):
4✔
1147
        raise NotImplementedError('Not implemented in TargetPythonRecipe')
1148

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

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

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

1176

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

1183
    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