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

kivy / python-for-android / 9340524709

02 Jun 2024 07:00PM UTC coverage: 59.062% (-0.08%) from 59.144%
9340524709

push

github

AndreMiras
:mute: Set sh.command log level to warning

sh INFO log level is too verbose by default, refs upstream TODO

1044 of 2355 branches covered (44.33%)

Branch coverage included in aggregate %.

0 of 3 new or added lines in 1 file covered. (0.0%)

4 existing lines in 1 file now uncovered.

4851 of 7626 relevant lines covered (63.61%)

2.53 hits per line

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

45.39
/pythonforandroid/toolchain.py
1
#!/usr/bin/env python
2
"""
4✔
3
Tool for packaging Python apps for Android
4
==========================================
5

6
This module defines the entry point for command line and programmatic use.
7
"""
8

9
from appdirs import user_data_dir
4✔
10
import argparse
4✔
11
from functools import wraps
4✔
12
import glob
4✔
13
import logging
4✔
14
import os
4✔
15
from os import environ
4✔
16
from os.path import (join, dirname, realpath, exists, expanduser, basename)
4✔
17
import re
4✔
18
import shlex
4✔
19
import sys
4✔
20
from sys import platform
4✔
21

22
# This must be imported and run before other third-party or p4a
23
# packages.
24
from pythonforandroid.checkdependencies import check
4✔
25
check()
4✔
26

27
from packaging.version import Version
4✔
28
import sh
4✔
29

30
from pythonforandroid import __version__
4✔
31
from pythonforandroid.bootstrap import Bootstrap
4✔
32
from pythonforandroid.build import Context, build_recipes
4✔
33
from pythonforandroid.distribution import Distribution, pretty_log_dists
4✔
34
from pythonforandroid.entrypoints import main
4✔
35
from pythonforandroid.graph import get_recipe_order_and_bootstrap
4✔
36
from pythonforandroid.logger import (logger, info, warning, setup_color,
4✔
37
                                     Out_Style, Out_Fore,
38
                                     info_notify, info_main, shprint)
39
from pythonforandroid.pythonpackage import get_dep_names_of_package
4✔
40
from pythonforandroid.recipe import Recipe
4✔
41
from pythonforandroid.recommendations import (
4✔
42
    RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API, print_recommendations)
43
from pythonforandroid.util import (
4✔
44
    current_directory,
45
    BuildInterruptingException,
46
    load_source,
47
    rmdir,
48
    max_build_tool_version,
49
)
50

51
user_dir = dirname(realpath(os.path.curdir))
4✔
52
toolchain_dir = dirname(__file__)
4✔
53
sys.path.insert(0, join(toolchain_dir, "tools", "external"))
4✔
54

55

56
def add_boolean_option(parser, names, no_names=None,
4✔
57
                       default=True, dest=None, description=None):
58
    group = parser.add_argument_group(description=description)
4✔
59
    if not isinstance(names, (list, tuple)):
4!
60
        names = [names]
×
61
    if dest is None:
4!
62
        dest = names[0].strip("-").replace("-", "_")
4✔
63

64
    def add_dashes(x):
4✔
65
        return x if x.startswith("-") else "--"+x
4✔
66

67
    opts = [add_dashes(x) for x in names]
4✔
68
    group.add_argument(
4✔
69
        *opts, help=("(this is the default)" if default else None),
70
        dest=dest, action='store_true')
71
    if no_names is None:
4!
72
        def add_no(x):
4✔
73
            x = x.lstrip("-")
4✔
74
            return ("no_"+x) if "_" in x else ("no-"+x)
4✔
75
        no_names = [add_no(x) for x in names]
4✔
76
    opts = [add_dashes(x) for x in no_names]
4✔
77
    group.add_argument(
4✔
78
        *opts, help=(None if default else "(this is the default)"),
79
        dest=dest, action='store_false')
80
    parser.set_defaults(**{dest: default})
4✔
81

82

83
def require_prebuilt_dist(func):
4✔
84
    """Decorator for ToolchainCL methods. If present, the method will
85
    automatically make sure a dist has been built before continuing
86
    or, if no dists are present or can be obtained, will raise an
87
    error.
88
    """
89

90
    @wraps(func)
4✔
91
    def wrapper_func(self, args, **kw):
4✔
92
        ctx = self.ctx
4✔
93
        ctx.set_archs(self._archs)
4✔
94
        ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir,
4✔
95
                                      user_ndk_dir=self.ndk_dir,
96
                                      user_android_api=self.android_api,
97
                                      user_ndk_api=self.ndk_api)
98
        dist = self._dist
4✔
99
        if dist.needs_build:
4!
100
            if dist.folder_exists():  # possible if the dist is being replaced
4!
101
                dist.delete()
×
102
            info_notify('No dist exists that meets your requirements, '
4✔
103
                        'so one will be built.')
104
            build_dist_from_args(ctx, dist, args)
4✔
105
        func(self, args, **kw)
4✔
106
    return wrapper_func
4✔
107

108

109
def dist_from_args(ctx, args):
4✔
110
    """Parses out any distribution-related arguments, and uses them to
111
    obtain a Distribution class instance for the build.
112
    """
113
    return Distribution.get_distribution(
4✔
114
        ctx,
115
        name=args.dist_name,
116
        recipes=split_argument_list(args.requirements),
117
        archs=args.arch,
118
        ndk_api=args.ndk_api,
119
        force_build=args.force_build,
120
        require_perfect_match=args.require_perfect_match,
121
        allow_replace_dist=args.allow_replace_dist)
122

123

124
def build_dist_from_args(ctx, dist, args):
4✔
125
    """Parses out any bootstrap related arguments, and uses them to build
126
    a dist."""
127
    bs = Bootstrap.get_bootstrap(args.bootstrap, ctx)
4✔
128
    blacklist = getattr(args, "blacklist_requirements", "").split(",")
4✔
129
    if len(blacklist) == 1 and blacklist[0] == "":
4!
130
        blacklist = []
4✔
131
    build_order, python_modules, bs = (
4✔
132
        get_recipe_order_and_bootstrap(
133
            ctx, dist.recipes, bs,
134
            blacklist=blacklist
135
        ))
136
    assert set(build_order).intersection(set(python_modules)) == set()
4✔
137
    ctx.recipe_build_order = build_order
4✔
138
    ctx.python_modules = python_modules
4✔
139

140
    info('The selected bootstrap is {}'.format(bs.name))
4✔
141
    info_main('# Creating dist with {} bootstrap'.format(bs.name))
4✔
142
    bs.distribution = dist
4✔
143
    info_notify('Dist will have name {} and requirements ({})'.format(
4✔
144
        dist.name, ', '.join(dist.recipes)))
145
    info('Dist contains the following requirements as recipes: {}'.format(
4✔
146
        ctx.recipe_build_order))
147
    info('Dist will also contain modules ({}) installed from pip'.format(
4✔
148
        ', '.join(ctx.python_modules)))
149
    info(
4✔
150
        'Dist will be build in mode {build_mode}{with_debug_symbols}'.format(
151
            build_mode='debug' if ctx.build_as_debuggable else 'release',
152
            with_debug_symbols=' (with debug symbols)'
153
            if ctx.with_debug_symbols
154
            else '',
155
        )
156
    )
157

158
    ctx.distribution = dist
4✔
159
    ctx.prepare_bootstrap(bs)
4✔
160
    if dist.needs_build:
4!
161
        ctx.prepare_dist()
4✔
162

163
    build_recipes(build_order, python_modules, ctx,
4✔
164
                  getattr(args, "private", None),
165
                  ignore_project_setup_py=getattr(
166
                      args, "ignore_setup_py", False
167
                  ),
168
                 )
169

170
    ctx.bootstrap.assemble_distribution()
4✔
171

172
    info_main('# Your distribution was created successfully, exiting.')
4✔
173
    info('Dist can be found at (for now) {}'
4✔
174
         .format(join(ctx.dist_dir, ctx.distribution.dist_dir)))
175

176

177
def split_argument_list(arg_list):
4✔
178
    if not len(arg_list):
4✔
179
        return []
4✔
180
    return re.split(r'[ ,]+', arg_list)
4✔
181

182

183
class NoAbbrevParser(argparse.ArgumentParser):
4✔
184
    """We want to disable argument abbreviation so as not to interfere
185
    with passing through arguments to build.py, but in python2 argparse
186
    doesn't have this option.
187

188
    This subclass alternative is follows the suggestion at
189
    https://bugs.python.org/issue14910.
190
    """
191
    def _get_option_tuples(self, option_string):
4✔
192
        return []
4✔
193

194

195
class ToolchainCL:
4✔
196

197
    def __init__(self):
4✔
198

199
        argv = sys.argv
4✔
200
        self.warn_on_carriage_return_args(argv)
4✔
201
        # Buildozer used to pass these arguments in a now-invalid order
202
        # If that happens, apply this fix
203
        # This fix will be removed once a fixed buildozer is released
204
        if (len(argv) > 2
4!
205
                and argv[1].startswith('--color')
206
                and argv[2].startswith('--storage-dir')):
207
            argv.append(argv.pop(1))  # the --color arg
×
208
            argv.append(argv.pop(1))  # the --storage-dir arg
×
209

210
        parser = NoAbbrevParser(
4✔
211
            description='A packaging tool for turning Python scripts and apps '
212
                        'into Android APKs')
213

214
        generic_parser = argparse.ArgumentParser(
4✔
215
            add_help=False,
216
            description='Generic arguments applied to all commands')
217
        argparse.ArgumentParser(
4✔
218
            add_help=False, description='Arguments for dist building')
219

220
        generic_parser.add_argument(
4✔
221
            '--debug', dest='debug', action='store_true', default=False,
222
            help='Display debug output and all build info')
223
        generic_parser.add_argument(
4✔
224
            '--color', dest='color', choices=['always', 'never', 'auto'],
225
            help='Enable or disable color output (default enabled on tty)')
226
        generic_parser.add_argument(
4✔
227
            '--sdk-dir', '--sdk_dir', dest='sdk_dir', default='',
228
            help='The filepath where the Android SDK is installed')
229
        generic_parser.add_argument(
4✔
230
            '--ndk-dir', '--ndk_dir', dest='ndk_dir', default='',
231
            help='The filepath where the Android NDK is installed')
232
        generic_parser.add_argument(
4✔
233
            '--android-api',
234
            '--android_api',
235
            dest='android_api',
236
            default=0,
237
            type=int,
238
            help=('The Android API level to build against defaults to {} if '
239
                  'not specified.').format(RECOMMENDED_TARGET_API))
240
        generic_parser.add_argument(
4✔
241
            '--ndk-version', '--ndk_version', dest='ndk_version', default=None,
242
            help=('DEPRECATED: the NDK version is now found automatically or '
243
                  'not at all.'))
244
        generic_parser.add_argument(
4✔
245
            '--ndk-api', type=int, default=None,
246
            help=('The Android API level to compile against. This should be your '
247
                  '*minimal supported* API, not normally the same as your --android-api. '
248
                  'Defaults to min(ANDROID_API, {}) if not specified.').format(RECOMMENDED_NDK_API))
249
        generic_parser.add_argument(
4✔
250
            '--symlink-bootstrap-files', '--ssymlink_bootstrap_files',
251
            action='store_true',
252
            dest='symlink_bootstrap_files',
253
            default=False,
254
            help=('If True, symlinks the bootstrap files '
255
                  'creation. This is useful for development only, it could also'
256
                  ' cause weird problems.'))
257

258
        default_storage_dir = user_data_dir('python-for-android')
4✔
259
        if ' ' in default_storage_dir:
4!
260
            default_storage_dir = '~/.python-for-android'
×
261
        generic_parser.add_argument(
4✔
262
            '--storage-dir', dest='storage_dir', default=default_storage_dir,
263
            help=('Primary storage directory for downloads and builds '
264
                  '(default: {})'.format(default_storage_dir)))
265

266
        generic_parser.add_argument(
4✔
267
            '--arch', help='The archs to build for.',
268
            action='append', default=[])
269

270
        # Options for specifying the Distribution
271
        generic_parser.add_argument(
4✔
272
            '--dist-name', '--dist_name',
273
            help='The name of the distribution to use or create', default='')
274

275
        generic_parser.add_argument(
4✔
276
            '--requirements',
277
            help=('Dependencies of your app, should be recipe names or '
278
                  'Python modules. NOT NECESSARY if you are using '
279
                  'Python 3 with --use-setup-py'),
280
            default='')
281

282
        generic_parser.add_argument(
4✔
283
            '--recipe-blacklist',
284
            help=('Blacklist an internal recipe from use. Allows '
285
                  'disabling Python 3 core modules to save size'),
286
            dest="recipe_blacklist",
287
            default='')
288

289
        generic_parser.add_argument(
4✔
290
            '--blacklist-requirements',
291
            help=('Blacklist an internal recipe from use. Allows '
292
                  'disabling Python 3 core modules to save size'),
293
            dest="blacklist_requirements",
294
            default='')
295

296
        generic_parser.add_argument(
4✔
297
            '--bootstrap',
298
            help='The bootstrap to build with. Leave unset to choose '
299
                 'automatically.',
300
            default=None)
301

302
        generic_parser.add_argument(
4✔
303
            '--hook',
304
            help='Filename to a module that contains python-for-android hooks',
305
            default=None)
306

307
        add_boolean_option(
4✔
308
            generic_parser, ["force-build"],
309
            default=False,
310
            description='Whether to force compilation of a new distribution')
311

312
        add_boolean_option(
4✔
313
            generic_parser, ["require-perfect-match"],
314
            default=False,
315
            description=('Whether the dist recipes must perfectly match '
316
                         'those requested'))
317

318
        add_boolean_option(
4✔
319
            generic_parser, ["allow-replace-dist"],
320
            default=True,
321
            description='Whether existing dist names can be automatically replaced'
322
            )
323

324
        generic_parser.add_argument(
4✔
325
            '--local-recipes', '--local_recipes',
326
            dest='local_recipes', default='./p4a-recipes',
327
            help='Directory to look for local recipes')
328

329
        generic_parser.add_argument(
4✔
330
            '--activity-class-name',
331
            dest='activity_class_name', default='org.kivy.android.PythonActivity',
332
            help='The full java class name of the main activity')
333

334
        generic_parser.add_argument(
4✔
335
            '--service-class-name',
336
            dest='service_class_name', default='org.kivy.android.PythonService',
337
            help='Full java package name of the PythonService class')
338

339
        generic_parser.add_argument(
4✔
340
            '--java-build-tool',
341
            dest='java_build_tool', default='auto',
342
            choices=['auto', 'ant', 'gradle'],
343
            help=('The java build tool to use when packaging the APK, defaults '
344
                  'to automatically selecting an appropriate tool.'))
345

346
        add_boolean_option(
4✔
347
            generic_parser, ['copy-libs'],
348
            default=False,
349
            description='Copy libraries instead of using biglink (Android 4.3+)'
350
        )
351

352
        self._read_configuration()
4✔
353

354
        subparsers = parser.add_subparsers(dest='subparser_name',
4✔
355
                                           help='The command to run')
356

357
        def add_parser(subparsers, *args, **kwargs):
4✔
358
            """
359
            argparse in python2 doesn't support the aliases option,
360
            so we just don't provide the aliases there.
361
            """
362
            if 'aliases' in kwargs and sys.version_info.major < 3:
4!
363
                kwargs.pop('aliases')
×
364
            return subparsers.add_parser(*args, **kwargs)
4✔
365

366
        add_parser(
4✔
367
            subparsers,
368
            'recommendations',
369
            parents=[generic_parser],
370
            help='List recommended p4a dependencies')
371
        parser_recipes = add_parser(
4✔
372
            subparsers,
373
            'recipes',
374
            parents=[generic_parser],
375
            help='List the available recipes')
376
        parser_recipes.add_argument(
4✔
377
            "--compact",
378
            action="store_true", default=False,
379
            help="Produce a compact list suitable for scripting")
380
        add_parser(
4✔
381
            subparsers, 'bootstraps',
382
            help='List the available bootstraps',
383
            parents=[generic_parser])
384
        add_parser(
4✔
385
            subparsers, 'clean_all',
386
            aliases=['clean-all'],
387
            help='Delete all builds, dists and caches',
388
            parents=[generic_parser])
389
        add_parser(
4✔
390
            subparsers, 'clean_dists',
391
            aliases=['clean-dists'],
392
            help='Delete all dists',
393
            parents=[generic_parser])
394
        add_parser(
4✔
395
            subparsers, 'clean_bootstrap_builds',
396
            aliases=['clean-bootstrap-builds'],
397
            help='Delete all bootstrap builds',
398
            parents=[generic_parser])
399
        add_parser(
4✔
400
            subparsers, 'clean_builds',
401
            aliases=['clean-builds'],
402
            help='Delete all builds',
403
            parents=[generic_parser])
404

405
        parser_clean = add_parser(
4✔
406
            subparsers, 'clean',
407
            help='Delete build components.',
408
            parents=[generic_parser])
409
        parser_clean.add_argument(
4✔
410
            'component', nargs='+',
411
            help=('The build component(s) to delete. You can pass any '
412
                  'number of arguments from "all", "builds", "dists", '
413
                  '"distributions", "bootstrap_builds", "downloads".'))
414

415
        parser_clean_recipe_build = add_parser(
4✔
416
            subparsers,
417
            'clean_recipe_build', aliases=['clean-recipe-build'],
418
            help=('Delete the build components of the given recipe. '
419
                  'By default this will also delete built dists'),
420
            parents=[generic_parser])
421
        parser_clean_recipe_build.add_argument(
4✔
422
            'recipe', help='The recipe name')
423
        parser_clean_recipe_build.add_argument(
4✔
424
            '--no-clean-dists', default=False,
425
            dest='no_clean_dists',
426
            action='store_true',
427
            help='If passed, do not delete existing dists')
428

429
        parser_clean_download_cache = add_parser(
4✔
430
            subparsers,
431
            'clean_download_cache', aliases=['clean-download-cache'],
432
            help='Delete cached downloads for requirement builds',
433
            parents=[generic_parser])
434
        parser_clean_download_cache.add_argument(
4✔
435
            'recipes',
436
            nargs='*',
437
            help='The recipes to clean (space-separated). If no recipe name is'
438
                  ' provided, the entire cache is cleared.')
439

440
        parser_export_dist = add_parser(
4✔
441
            subparsers,
442
            'export_dist', aliases=['export-dist'],
443
            help='Copy the named dist to the given path',
444
            parents=[generic_parser])
445
        parser_export_dist.add_argument('output_dir',
4✔
446
                                        help='The output dir to copy to')
447
        parser_export_dist.add_argument(
4✔
448
            '--symlink',
449
            action='store_true',
450
            help='Symlink the dist instead of copying')
451

452
        parser_packaging = argparse.ArgumentParser(
4✔
453
            parents=[generic_parser],
454
            add_help=False,
455
            description='common options for packaging (apk, aar)')
456

457
        # This is actually an internal argument of the build.py
458
        # (see pythonforandroid/bootstraps/common/build/build.py).
459
        # However, it is also needed before the distribution is finally
460
        # assembled for locating the setup.py / other build systems, which
461
        # is why we also add it here:
462
        parser_packaging.add_argument(
4✔
463
            '--add-asset', dest='assets',
464
            action="append", default=[],
465
            help='Put this in the assets folder in the apk.')
466
        parser_packaging.add_argument(
4✔
467
            '--add-resource', dest='resources',
468
            action="append", default=[],
469
            help='Put this in the res folder in the apk.')
470
        parser_packaging.add_argument(
4✔
471
            '--private', dest='private',
472
            help='the directory with the app source code files' +
473
                 ' (containing your main.py entrypoint)',
474
            required=False, default=None)
475
        parser_packaging.add_argument(
4✔
476
            '--use-setup-py', dest="use_setup_py",
477
            action='store_true', default=False,
478
            help="Process the setup.py of a project if present. " +
479
                 "(Experimental!")
480
        parser_packaging.add_argument(
4✔
481
            '--ignore-setup-py', dest="ignore_setup_py",
482
            action='store_true', default=False,
483
            help="Don't run the setup.py of a project if present. " +
484
                 "This may be required if the setup.py is not " +
485
                 "designed to work inside p4a (e.g. by installing " +
486
                 "dependencies that won't work or aren't desired " +
487
                 "on Android")
488
        parser_packaging.add_argument(
4✔
489
            '--release', dest='build_mode', action='store_const',
490
            const='release', default='debug',
491
            help='Build your app as a non-debug release build. '
492
                 '(Disables gdb debugging among other things)')
493
        parser_packaging.add_argument(
4✔
494
            '--with-debug-symbols', dest='with_debug_symbols',
495
            action='store_const', const=True, default=False,
496
            help='Will keep debug symbols from `.so` files.')
497
        parser_packaging.add_argument(
4✔
498
            '--keystore', dest='keystore', action='store', default=None,
499
            help=('Keystore for JAR signing key, will use jarsigner '
500
                  'default if not specified (release build only)'))
501
        parser_packaging.add_argument(
4✔
502
            '--signkey', dest='signkey', action='store', default=None,
503
            help='Key alias to sign PARSER_APK. with (release build only)')
504
        parser_packaging.add_argument(
4✔
505
            '--keystorepw', dest='keystorepw', action='store', default=None,
506
            help='Password for keystore')
507
        parser_packaging.add_argument(
4✔
508
            '--signkeypw', dest='signkeypw', action='store', default=None,
509
            help='Password for key alias')
510

511
        add_parser(
4✔
512
            subparsers,
513
            'aar', help='Build an AAR',
514
            parents=[parser_packaging])
515

516
        add_parser(
4✔
517
            subparsers,
518
            'apk', help='Build an APK',
519
            parents=[parser_packaging])
520

521
        add_parser(
4✔
522
            subparsers,
523
            'aab', help='Build an AAB',
524
            parents=[parser_packaging])
525

526
        add_parser(
4✔
527
            subparsers,
528
            'create', help='Compile a set of requirements into a dist',
529
            parents=[generic_parser])
530
        add_parser(
4✔
531
            subparsers,
532
            'archs', help='List the available target architectures',
533
            parents=[generic_parser])
534
        add_parser(
4✔
535
            subparsers,
536
            'distributions', aliases=['dists'],
537
            help='List the currently available (compiled) dists',
538
            parents=[generic_parser])
539
        add_parser(
4✔
540
            subparsers,
541
            'delete_dist', aliases=['delete-dist'], help='Delete a compiled dist',
542
            parents=[generic_parser])
543

544
        parser_sdk_tools = add_parser(
4✔
545
            subparsers,
546
            'sdk_tools', aliases=['sdk-tools'],
547
            help='Run the given binary from the SDK tools dis',
548
            parents=[generic_parser])
549
        parser_sdk_tools.add_argument(
4✔
550
            'tool', help='The binary tool name to run')
551

552
        add_parser(
4✔
553
            subparsers,
554
            'adb', help='Run adb from the given SDK',
555
            parents=[generic_parser])
556
        add_parser(
4✔
557
            subparsers,
558
            'logcat', help='Run logcat from the given SDK',
559
            parents=[generic_parser])
560
        add_parser(
4✔
561
            subparsers,
562
            'build_status', aliases=['build-status'],
563
            help='Print some debug information about current built components',
564
            parents=[generic_parser])
565

566
        parser.add_argument('-v', '--version', action='version',
4✔
567
                            version=__version__)
568

569
        args, unknown = parser.parse_known_args(sys.argv[1:])
4✔
570
        args.unknown_args = unknown
4✔
571

572
        if hasattr(args, "private") and args.private is not None:
4!
573
            # Pass this value on to the internal bootstrap build.py:
574
            args.unknown_args += ["--private", args.private]
×
575
        if hasattr(args, "build_mode") and args.build_mode == "release":
4!
576
            args.unknown_args += ["--release"]
×
577
        if hasattr(args, "with_debug_symbols") and args.with_debug_symbols:
4!
578
            args.unknown_args += ["--with-debug-symbols"]
×
579
        if hasattr(args, "ignore_setup_py") and args.ignore_setup_py:
4!
580
            args.use_setup_py = False
×
581
        if hasattr(args, "activity_class_name") and args.activity_class_name != 'org.kivy.android.PythonActivity':
4✔
582
            args.unknown_args += ["--activity-class-name", args.activity_class_name]
4✔
583
        if hasattr(args, "service_class_name") and args.service_class_name != 'org.kivy.android.PythonService':
4✔
584
            args.unknown_args += ["--service-class-name", args.service_class_name]
4✔
585

586
        self.args = args
4✔
587

588
        if args.subparser_name is None:
4✔
589
            parser.print_help()
4✔
590
            exit(1)
4✔
591

592
        setup_color(args.color)
4✔
593

594
        if args.debug:
4!
595
            # TODO debugging
596
            # logger.setLevel(logging.DEBUG)
NEW
597
            logger.setLevel
×
NEW
598
            logging.DEBUG
×
NEW
599
            pass
×
600

601
        self.ctx = Context()
4✔
602
        self.ctx.use_setup_py = getattr(args, "use_setup_py", True)
4✔
603
        self.ctx.build_as_debuggable = getattr(
4✔
604
            args, "build_mode", "debug"
605
        ) == "debug"
606
        self.ctx.with_debug_symbols = getattr(
4✔
607
            args, "with_debug_symbols", False
608
        )
609

610
        have_setup_py_or_similar = False
4✔
611
        if getattr(args, "private", None) is not None:
4!
612
            project_dir = getattr(args, "private")
×
613
            if (os.path.exists(os.path.join(project_dir, "setup.py")) or
×
614
                    os.path.exists(os.path.join(project_dir,
615
                                                "pyproject.toml"))):
616
                have_setup_py_or_similar = True
×
617

618
        # Process requirements and put version in environ
619
        if hasattr(args, 'requirements'):
4!
620
            requirements = []
4✔
621

622
            # Add dependencies from setup.py, but only if they are recipes
623
            # (because otherwise, setup.py itself will install them later)
624
            if (have_setup_py_or_similar and
4!
625
                    getattr(args, "use_setup_py", False)):
626
                try:
×
627
                    info("Analyzing package dependencies. MAY TAKE A WHILE.")
×
628
                    # Get all the dependencies corresponding to a recipe:
629
                    dependencies = [
×
630
                        dep.lower() for dep in
631
                        get_dep_names_of_package(
632
                            args.private,
633
                            keep_version_pins=True,
634
                            recursive=True,
635
                            verbose=True,
636
                        )
637
                    ]
638
                    info("Dependencies obtained: " + str(dependencies))
×
639
                    all_recipes = [
×
640
                        recipe.lower() for recipe in
641
                        set(Recipe.list_recipes(self.ctx))
642
                    ]
643
                    dependencies = set(dependencies).intersection(
×
644
                        set(all_recipes)
645
                    )
646
                    # Add dependencies to argument list:
647
                    if len(dependencies) > 0:
×
648
                        if len(args.requirements) > 0:
×
649
                            args.requirements += u","
×
650
                        args.requirements += u",".join(dependencies)
×
651
                except ValueError:
×
652
                    # Not a python package, apparently.
653
                    warning(
×
654
                        "Processing failed, is this project a valid "
655
                        "package? Will continue WITHOUT setup.py deps."
656
                    )
657

658
            # Parse --requirements argument list:
659
            for requirement in split_argument_list(args.requirements):
4✔
660
                if "==" in requirement:
4!
661
                    requirement, version = requirement.split(u"==", 1)
×
662
                    os.environ["VERSION_{}".format(requirement)] = version
×
663
                    info('Recipe {}: version "{}" requested'.format(
×
664
                        requirement, version))
665
                requirements.append(requirement)
4✔
666
            args.requirements = u",".join(requirements)
4✔
667

668
        self.warn_on_deprecated_args(args)
4✔
669

670
        self.storage_dir = args.storage_dir
4✔
671
        self.ctx.setup_dirs(self.storage_dir)
4✔
672
        self.sdk_dir = args.sdk_dir
4✔
673
        self.ndk_dir = args.ndk_dir
4✔
674
        self.android_api = args.android_api
4✔
675
        self.ndk_api = args.ndk_api
4✔
676
        self.ctx.symlink_bootstrap_files = args.symlink_bootstrap_files
4✔
677
        self.ctx.java_build_tool = args.java_build_tool
4✔
678

679
        self._archs = args.arch
4✔
680

681
        self.ctx.local_recipes = realpath(args.local_recipes)
4✔
682
        self.ctx.copy_libs = args.copy_libs
4✔
683

684
        self.ctx.activity_class_name = args.activity_class_name
4✔
685
        self.ctx.service_class_name = args.service_class_name
4✔
686

687
        # Each subparser corresponds to a method
688
        command = args.subparser_name.replace('-', '_')
4✔
689
        getattr(self, command)(args)
4✔
690

691
    @staticmethod
4✔
692
    def warn_on_carriage_return_args(args):
4✔
693
        for check_arg in args:
4✔
694
            if '\r' in check_arg:
4!
695
                warning("Argument '{}' contains a carriage return (\\r).".format(str(check_arg.replace('\r', ''))))
×
696
                warning("Invoking this program via scripts which use CRLF instead of LF line endings will have undefined behaviour.")
×
697

698
    def warn_on_deprecated_args(self, args):
4✔
699
        """
700
        Print warning messages for any deprecated arguments that were passed.
701
        """
702

703
        # Output warning if setup.py is present and neither --ignore-setup-py
704
        # nor --use-setup-py was specified.
705
        if getattr(args, "private", None) is not None and \
4!
706
                (os.path.exists(os.path.join(args.private, "setup.py")) or
707
                 os.path.exists(os.path.join(args.private, "pyproject.toml"))
708
                ):
709
            if not getattr(args, "use_setup_py", False) and \
×
710
                    not getattr(args, "ignore_setup_py", False):
711
                warning("  **** FUTURE BEHAVIOR CHANGE WARNING ****")
×
712
                warning("Your project appears to contain a setup.py file.")
×
713
                warning("Currently, these are ignored by default.")
×
714
                warning("This will CHANGE in an upcoming version!")
×
715
                warning("")
×
716
                warning("To ensure your setup.py is ignored, please specify:")
×
717
                warning("    --ignore-setup-py")
×
718
                warning("")
×
719
                warning("To enable what will some day be the default, specify:")
×
720
                warning("    --use-setup-py")
×
721

722
        # NDK version is now determined automatically
723
        if args.ndk_version is not None:
4!
724
            warning('--ndk-version is deprecated and no longer necessary, '
×
725
                    'the value you passed is ignored')
726
        if 'ANDROIDNDKVER' in environ:
4!
727
            warning('$ANDROIDNDKVER is deprecated and no longer necessary, '
×
728
                    'the value you set is ignored')
729

730
    def hook(self, name):
4✔
731
        if not self.args.hook:
×
732
            return
×
733
        if not hasattr(self, "hook_module"):
×
734
            # first time, try to load the hook module
735
            self.hook_module = load_source(
×
736
                "pythonforandroid.hook", self.args.hook)
737
        if hasattr(self.hook_module, name):
×
738
            info("Hook: execute {}".format(name))
×
739
            getattr(self.hook_module, name)(self)
×
740
        else:
741
            info("Hook: ignore {}".format(name))
×
742

743
    @property
4✔
744
    def default_storage_dir(self):
4✔
745
        udd = user_data_dir('python-for-android')
×
746
        if ' ' in udd:
×
747
            udd = '~/.python-for-android'
×
748
        return udd
×
749

750
    @staticmethod
4✔
751
    def _read_configuration():
4✔
752
        # search for a .p4a configuration file in the current directory
753
        if not exists(".p4a"):
4!
754
            return
4✔
755
        info("Reading .p4a configuration")
×
756
        with open(".p4a") as fd:
×
757
            lines = fd.readlines()
×
758
        lines = [shlex.split(line)
×
759
                 for line in lines if not line.startswith("#")]
760
        for line in lines:
×
761
            for arg in line:
×
762
                sys.argv.append(arg)
×
763

764
    def recipes(self, args):
4✔
765
        """
766
        Prints recipes basic info, e.g.
767
        .. code-block:: bash
768
            python3      3.7.1
769
                depends: ['hostpython3', 'sqlite3', 'openssl', 'libffi']
770
                conflicts: []
771
                optional depends: ['sqlite3', 'libffi', 'openssl']
772
        """
773
        ctx = self.ctx
4✔
774
        if args.compact:
4!
775
            print(" ".join(set(Recipe.list_recipes(ctx))))
×
776
        else:
777
            for name in sorted(Recipe.list_recipes(ctx)):
4✔
778
                try:
4✔
779
                    recipe = Recipe.get_recipe(name, ctx)
4✔
780
                except (IOError, ValueError):
×
781
                    warning('Recipe "{}" could not be loaded'.format(name))
×
782
                except SyntaxError:
×
783
                    import traceback
×
784
                    traceback.print_exc()
×
785
                    warning(('Recipe "{}" could not be loaded due to a '
×
786
                             'syntax error').format(name))
787
                version = str(recipe.version)
4✔
788
                print('{Fore.BLUE}{Style.BRIGHT}{recipe.name:<12} '
4✔
789
                      '{Style.RESET_ALL}{Fore.LIGHTBLUE_EX}'
790
                      '{version:<8}{Style.RESET_ALL}'.format(
791
                            recipe=recipe, Fore=Out_Fore, Style=Out_Style,
792
                            version=version))
793
                print('    {Fore.GREEN}depends: {recipe.depends}'
4✔
794
                      '{Fore.RESET}'.format(recipe=recipe, Fore=Out_Fore))
795
                if recipe.conflicts:
4✔
796
                    print('    {Fore.RED}conflicts: {recipe.conflicts}'
4✔
797
                          '{Fore.RESET}'
798
                          .format(recipe=recipe, Fore=Out_Fore))
799
                if recipe.opt_depends:
4✔
800
                    print('    {Fore.YELLOW}optional depends: '
4✔
801
                          '{recipe.opt_depends}{Fore.RESET}'
802
                          .format(recipe=recipe, Fore=Out_Fore))
803

804
    def bootstraps(self, _args):
4✔
805
        """List all the bootstraps available to build with."""
806
        for bs in Bootstrap.all_bootstraps():
×
807
            bs = Bootstrap.get_bootstrap(bs, self.ctx)
×
808
            print('{Fore.BLUE}{Style.BRIGHT}{bs.name}{Style.RESET_ALL}'
×
809
                  .format(bs=bs, Fore=Out_Fore, Style=Out_Style))
810
            print('    {Fore.GREEN}depends: {bs.recipe_depends}{Fore.RESET}'
×
811
                  .format(bs=bs, Fore=Out_Fore))
812

813
    def clean(self, args):
4✔
814
        components = args.component
×
815

816
        component_clean_methods = {
×
817
            'all': self.clean_all,
818
            'dists': self.clean_dists,
819
            'distributions': self.clean_dists,
820
            'builds': self.clean_builds,
821
            'bootstrap_builds': self.clean_bootstrap_builds,
822
            'downloads': self.clean_download_cache}
823

824
        for component in components:
×
825
            if component not in component_clean_methods:
×
826
                raise BuildInterruptingException((
×
827
                    'Asked to clean "{}" but this argument is not '
828
                    'recognised'.format(component)))
829
            component_clean_methods[component](args)
×
830

831
    def clean_all(self, args):
4✔
832
        """Delete all build components; the package cache, package builds,
833
        bootstrap builds and distributions."""
834
        self.clean_dists(args)
×
835
        self.clean_builds(args)
×
836
        self.clean_download_cache(args)
×
837

838
    def clean_dists(self, _args):
4✔
839
        """Delete all compiled distributions in the internal distribution
840
        directory."""
841
        ctx = self.ctx
×
842
        rmdir(ctx.dist_dir)
×
843

844
    def clean_bootstrap_builds(self, _args):
4✔
845
        """Delete all the bootstrap builds."""
846
        rmdir(join(self.ctx.build_dir, 'bootstrap_builds'))
×
847
        # for bs in Bootstrap.all_bootstraps():
848
        #     bs = Bootstrap.get_bootstrap(bs, self.ctx)
849
        #     if bs.build_dir and exists(bs.build_dir):
850
        #         info('Cleaning build for {} bootstrap.'.format(bs.name))
851
        #         rmdir(bs.build_dir)
852

853
    def clean_builds(self, _args):
4✔
854
        """Delete all build caches for each recipe, python-install, java code
855
        and compiled libs collection.
856

857
        This does *not* delete the package download cache or the final
858
        distributions.  You can also use clean_recipe_build to delete the build
859
        of a specific recipe.
860
        """
861
        ctx = self.ctx
×
862
        rmdir(ctx.build_dir)
×
863
        rmdir(ctx.python_installs_dir)
×
864
        libs_dir = join(self.ctx.build_dir, 'libs_collections')
×
865
        rmdir(libs_dir)
×
866

867
    def clean_recipe_build(self, args):
4✔
868
        """Deletes the build files of the given recipe.
869

870
        This is intended for debug purposes. You may experience
871
        strange behaviour or problems with some recipes if their
872
        build has made unexpected state changes. If this happens, run
873
        clean_builds, or attempt to clean other recipes until things
874
        work again.
875
        """
876
        recipe = Recipe.get_recipe(args.recipe, self.ctx)
×
877
        info('Cleaning build for {} recipe.'.format(recipe.name))
×
878
        recipe.clean_build()
×
879
        if not args.no_clean_dists:
×
880
            self.clean_dists(args)
×
881

882
    def clean_download_cache(self, args):
4✔
883
        """ Deletes a download cache for recipes passed as arguments. If no
884
        argument is passed, it'll delete *all* downloaded caches. ::
885

886
            p4a clean_download_cache kivy,pyjnius
887

888
        This does *not* delete the build caches or final distributions.
889
        """
890
        ctx = self.ctx
×
891
        if hasattr(args, 'recipes') and args.recipes:
×
892
            for package in args.recipes:
×
893
                remove_path = join(ctx.packages_path, package)
×
894
                if exists(remove_path):
×
895
                    rmdir(remove_path)
×
896
                    info('Download cache removed for: "{}"'.format(package))
×
897
                else:
898
                    warning('No download cache found for "{}", skipping'.format(
×
899
                        package))
900
        else:
901
            if exists(ctx.packages_path):
×
902
                rmdir(ctx.packages_path)
×
903
                info('Download cache removed.')
×
904
            else:
905
                print('No cache found at "{}"'.format(ctx.packages_path))
×
906

907
    @require_prebuilt_dist
4✔
908
    def export_dist(self, args):
4✔
909
        """Copies a created dist to an output dir.
910

911
        This makes it easy to navigate to the dist to investigate it
912
        or call build.py, though you do not in general need to do this
913
        and can use the apk command instead.
914
        """
915
        ctx = self.ctx
×
916
        dist = dist_from_args(ctx, args)
×
917
        if dist.needs_build:
×
918
            raise BuildInterruptingException(
×
919
                'You asked to export a dist, but there is no dist '
920
                'with suitable recipes available. For now, you must '
921
                ' create one first with the create argument.')
922
        if args.symlink:
×
923
            shprint(sh.ln, '-s', dist.dist_dir, args.output_dir)
×
924
        else:
925
            shprint(sh.cp, '-r', dist.dist_dir, args.output_dir)
×
926

927
    @property
4✔
928
    def _dist(self):
4✔
929
        ctx = self.ctx
4✔
930
        dist = dist_from_args(ctx, self.args)
4✔
931
        ctx.distribution = dist
4✔
932
        return dist
4✔
933

934
    @staticmethod
4✔
935
    def _fix_args(args):
4✔
936
        """
937
        Manually fixing these arguments at the string stage is
938
        unsatisfactory and should probably be changed somehow, but
939
        we can't leave it until later as the build.py scripts assume
940
        they are in the current directory.
941
        works in-place
942
        :param args: parser args
943
        """
944

945
        fix_args = ('--dir', '--private', '--add-jar', '--add-source',
×
946
                    '--whitelist', '--blacklist', '--presplash', '--icon',
947
                    '--icon-bg', '--icon-fg')
948
        unknown_args = args.unknown_args
×
949

950
        for asset in args.assets:
×
951
            if ":" in asset:
×
952
                asset_src, asset_dest = asset.split(":")
×
953
            else:
954
                asset_src = asset_dest = asset
×
955
            # take abspath now, because build.py will be run in bootstrap dir
956
            unknown_args += ["--asset", os.path.abspath(asset_src)+":"+asset_dest]
×
957
        for resource in args.resources:
×
958
            if ":" in resource:
×
959
                resource_src, resource_dest = resource.split(":")
×
960
            else:
961
                resource_src = resource
×
962
                resource_dest = ""
×
963
            # take abspath now, because build.py will be run in bootstrap dir
964
            unknown_args += ["--resource", os.path.abspath(resource_src)+":"+resource_dest]
×
965
        for i, arg in enumerate(unknown_args):
×
966
            argx = arg.split('=')
×
967
            if argx[0] in fix_args:
×
968
                if len(argx) > 1:
×
969
                    unknown_args[i] = '='.join(
×
970
                        (argx[0], realpath(expanduser(argx[1]))))
971
                elif i + 1 < len(unknown_args):
×
972
                    unknown_args[i+1] = realpath(expanduser(unknown_args[i+1]))
×
973

974
    @staticmethod
4✔
975
    def _prepare_release_env(args):
4✔
976
        """
977
        prepares envitonment dict with the necessary flags for signing an apk
978
        :param args: parser args
979
        """
980
        env = os.environ.copy()
×
981
        if args.build_mode == 'release':
×
982
            if args.keystore:
×
983
                env['P4A_RELEASE_KEYSTORE'] = realpath(expanduser(args.keystore))
×
984
            if args.signkey:
×
985
                env['P4A_RELEASE_KEYALIAS'] = args.signkey
×
986
            if args.keystorepw:
×
987
                env['P4A_RELEASE_KEYSTORE_PASSWD'] = args.keystorepw
×
988
            if args.signkeypw:
×
989
                env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.signkeypw
×
990
            elif args.keystorepw and 'P4A_RELEASE_KEYALIAS_PASSWD' not in env:
×
991
                env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.keystorepw
×
992

993
        return env
×
994

995
    def _build_package(self, args, package_type):
4✔
996
        """
997
        Creates an android package using gradle
998
        :param args: parser args
999
        :param package_type: one of 'apk', 'aar', 'aab'
1000
        :return (gradle output, build_args)
1001
        """
1002
        ctx = self.ctx
×
1003
        dist = self._dist
×
1004
        bs = Bootstrap.get_bootstrap(args.bootstrap, ctx)
×
1005
        ctx.prepare_bootstrap(bs)
×
1006
        self._fix_args(args)
×
1007
        env = self._prepare_release_env(args)
×
1008

1009
        with current_directory(dist.dist_dir):
×
1010
            self.hook("before_apk_build")
×
1011
            os.environ["ANDROID_API"] = str(self.ctx.android_api)
×
1012
            build = load_source('build', join(dist.dist_dir, 'build.py'))
×
1013
            build_args = build.parse_args_and_make_package(
×
1014
                args.unknown_args
1015
            )
1016

1017
            self.hook("after_apk_build")
×
1018
            self.hook("before_apk_assemble")
×
1019
            build_tools_versions = os.listdir(join(ctx.sdk_dir,
×
1020
                                                   'build-tools'))
1021
            build_tools_version = max_build_tool_version(build_tools_versions)
×
1022
            info(('Detected highest available build tools '
×
1023
                  'version to be {}').format(build_tools_version))
1024

1025
            if Version(build_tools_version.replace(" ", "")) < Version('25.0'):
×
1026
                raise BuildInterruptingException(
×
1027
                    'build_tools >= 25 is required, but %s is installed' % build_tools_version)
1028
            if not exists("gradlew"):
×
1029
                raise BuildInterruptingException("gradlew file is missing")
×
1030

1031
            env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir
×
1032
            env["ANDROID_HOME"] = self.ctx.sdk_dir
×
1033

1034
            gradlew = sh.Command('./gradlew')
×
1035

1036
            if exists('/usr/bin/dos2unix'):
×
1037
                # .../dists/bdisttest_python3/gradlew
1038
                # .../build/bootstrap_builds/sdl2-python3/gradlew
1039
                # if docker on windows, gradle contains CRLF
1040
                output = shprint(
×
1041
                    sh.Command('dos2unix'), gradlew._path.decode('utf8'),
1042
                    _tail=20, _critical=True, _env=env
1043
                )
1044
            if args.build_mode == "debug":
×
1045
                if package_type == "aab":
×
1046
                    raise BuildInterruptingException(
×
1047
                        "aab is meant only for distribution and is not available in debug mode. "
1048
                        "Instead, you can use apk while building for debugging purposes."
1049
                    )
1050
                gradle_task = "assembleDebug"
×
1051
            elif args.build_mode == "release":
×
1052
                if package_type in ["apk", "aar"]:
×
1053
                    gradle_task = "assembleRelease"
×
1054
                elif package_type == "aab":
×
1055
                    gradle_task = "bundleRelease"
×
1056
            else:
1057
                raise BuildInterruptingException(
×
1058
                    "Unknown build mode {} for apk()".format(args.build_mode))
1059

1060
            # WARNING: We should make sure to clean the build directory before building.
1061
            # See PR: kivy/python-for-android#2705
1062
            output = shprint(gradlew, "clean", gradle_task, _tail=20,
×
1063
                             _critical=True, _env=env)
1064
        return output, build_args
×
1065

1066
    def _finish_package(self, args, output, build_args, package_type, output_dir):
4✔
1067
        """
1068
        Finishes the package after the gradle script run
1069
        :param args: the parser args
1070
        :param output: RunningCommand output
1071
        :param build_args: build args as returned by build.parse_args
1072
        :param package_type: one of 'apk', 'aar', 'aab'
1073
        :param output_dir: where to put the package file
1074
        """
1075

1076
        package_glob = "*-{}.%s" % package_type
×
1077
        package_add_version = True
×
1078

1079
        self.hook("after_apk_assemble")
×
1080

1081
        info_main('# Copying android package to current directory')
×
1082

1083
        package_re = re.compile(r'.*Package: (.*\.apk)$')
×
1084
        package_file = None
×
1085
        for line in reversed(output.splitlines()):
×
1086
            m = package_re.match(line)
×
1087
            if m:
×
1088
                package_file = m.groups()[0]
×
1089
                break
×
1090
        if not package_file:
×
1091
            info_main('# Android package filename not found in build output. Guessing...')
×
1092
            if args.build_mode == "release":
×
1093
                suffixes = ("release", "release-unsigned")
×
1094
            else:
1095
                suffixes = ("debug", )
×
1096
            for suffix in suffixes:
×
1097

1098
                package_files = glob.glob(join(output_dir, package_glob.format(suffix)))
×
1099
                if package_files:
×
1100
                    if len(package_files) > 1:
×
1101
                        info('More than one built APK found... guessing you '
×
1102
                             'just built {}'.format(package_files[-1]))
1103
                    package_file = package_files[-1]
×
1104
                    break
×
1105
            else:
1106
                raise BuildInterruptingException('Couldn\'t find the built APK')
×
1107

1108
        info_main('# Found android package file: {}'.format(package_file))
×
1109
        package_extension = f".{package_type}"
×
1110
        if package_add_version:
×
1111
            info('# Add version number to android package')
×
1112
            package_name = basename(package_file)[:-len(package_extension)]
×
1113
            package_file_dest = "{}-{}{}".format(
×
1114
                package_name, build_args.version, package_extension)
1115
            info('# Android package renamed to {}'.format(package_file_dest))
×
1116
            shprint(sh.cp, package_file, package_file_dest)
×
1117
        else:
1118
            shprint(sh.cp, package_file, './')
×
1119

1120
    @require_prebuilt_dist
4✔
1121
    def apk(self, args):
4✔
1122
        output, build_args = self._build_package(args, package_type='apk')
×
1123
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'apk', args.build_mode)
×
1124
        self._finish_package(args, output, build_args, 'apk', output_dir)
×
1125

1126
    @require_prebuilt_dist
4✔
1127
    def aar(self, args):
4✔
1128
        output, build_args = self._build_package(args, package_type='aar')
×
1129
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'aar')
×
1130
        self._finish_package(args, output, build_args, 'aar', output_dir)
×
1131

1132
    @require_prebuilt_dist
4✔
1133
    def aab(self, args):
4✔
1134
        output, build_args = self._build_package(args, package_type='aab')
×
1135
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'bundle', args.build_mode)
×
1136
        self._finish_package(args, output, build_args, 'aab', output_dir)
×
1137

1138
    @require_prebuilt_dist
4✔
1139
    def create(self, args):
4✔
1140
        """Create a distribution directory if it doesn't already exist, run
1141
        any recipes if necessary, and build the apk.
1142
        """
1143
        pass  # The decorator does everything
4✔
1144

1145
    def archs(self, _args):
4✔
1146
        """List the target architectures available to be built for."""
1147
        print('{Style.BRIGHT}Available target architectures are:'
×
1148
              '{Style.RESET_ALL}'.format(Style=Out_Style))
1149
        for arch in self.ctx.archs:
×
1150
            print('    {}'.format(arch.arch))
×
1151

1152
    def dists(self, args):
4✔
1153
        """The same as :meth:`distributions`."""
1154
        self.distributions(args)
×
1155

1156
    def distributions(self, _args):
4✔
1157
        """Lists all distributions currently available (i.e. that have already
1158
        been built)."""
1159
        ctx = self.ctx
×
1160
        dists = Distribution.get_distributions(ctx)
×
1161

1162
        if dists:
×
1163
            print('{Style.BRIGHT}Distributions currently installed are:'
×
1164
                  '{Style.RESET_ALL}'.format(Style=Out_Style))
1165
            pretty_log_dists(dists, print)
×
1166
        else:
1167
            print('{Style.BRIGHT}There are no dists currently built.'
×
1168
                  '{Style.RESET_ALL}'.format(Style=Out_Style))
1169

1170
    def delete_dist(self, _args):
4✔
1171
        dist = self._dist
×
1172
        if not dist.folder_exists():
×
1173
            info('No dist exists that matches your specifications, '
×
1174
                 'exiting without deleting.')
1175
            return
×
1176
        dist.delete()
×
1177

1178
    def sdk_tools(self, args):
4✔
1179
        """Runs the android binary from the detected SDK directory, passing
1180
        all arguments straight to it. This binary is used to install
1181
        e.g. platform-tools for different API level targets. This is
1182
        intended as a convenience function if android is not in your
1183
        $PATH.
1184
        """
1185
        ctx = self.ctx
×
1186
        ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir,
×
1187
                                      user_ndk_dir=self.ndk_dir,
1188
                                      user_android_api=self.android_api,
1189
                                      user_ndk_api=self.ndk_api)
1190
        android = sh.Command(join(ctx.sdk_dir, 'tools', args.tool))
×
1191
        output = android(
×
1192
            *args.unknown_args, _iter=True, _out_bufsize=1, _err_to_out=True)
1193
        for line in output:
×
1194
            sys.stdout.write(line)
×
1195
            sys.stdout.flush()
×
1196

1197
    def adb(self, args):
4✔
1198
        """Runs the adb binary from the detected SDK directory, passing all
1199
        arguments straight to it. This is intended as a convenience
1200
        function if adb is not in your $PATH.
1201
        """
1202
        self._adb(args.unknown_args)
×
1203

1204
    def logcat(self, args):
4✔
1205
        """Runs ``adb logcat`` using the adb binary from the detected SDK
1206
        directory. All extra args are passed as arguments to logcat."""
1207
        self._adb(['logcat'] + args.unknown_args)
×
1208

1209
    def _adb(self, commands):
4✔
1210
        """Call the adb executable from the SDK, passing the given commands as
1211
        arguments."""
1212
        ctx = self.ctx
×
1213
        ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir,
×
1214
                                      user_ndk_dir=self.ndk_dir,
1215
                                      user_android_api=self.android_api,
1216
                                      user_ndk_api=self.ndk_api)
1217
        if platform in ('win32', 'cygwin'):
×
1218
            adb = sh.Command(join(ctx.sdk_dir, 'platform-tools', 'adb.exe'))
×
1219
        else:
1220
            adb = sh.Command(join(ctx.sdk_dir, 'platform-tools', 'adb'))
×
1221
        info_notify('Starting adb...')
×
1222
        output = adb(*commands, _iter=True, _out_bufsize=1, _err_to_out=True)
×
1223
        for line in output:
×
1224
            sys.stdout.write(line)
×
1225
            sys.stdout.flush()
×
1226

1227
    def recommendations(self, args):
4✔
1228
        print_recommendations()
4✔
1229

1230
    def build_status(self, _args):
4✔
1231
        """Print the status of the specified build. """
1232
        print('{Style.BRIGHT}Bootstraps whose core components are probably '
×
1233
              'already built:{Style.RESET_ALL}'.format(Style=Out_Style))
1234

1235
        bootstrap_dir = join(self.ctx.build_dir, 'bootstrap_builds')
×
1236
        if exists(bootstrap_dir):
×
1237
            for filen in os.listdir(bootstrap_dir):
×
1238
                print('    {Fore.GREEN}{Style.BRIGHT}{filen}{Style.RESET_ALL}'
×
1239
                      .format(filen=filen, Fore=Out_Fore, Style=Out_Style))
1240

1241
        print('{Style.BRIGHT}Recipes that are probably already built:'
×
1242
              '{Style.RESET_ALL}'.format(Style=Out_Style))
1243
        other_builds_dir = join(self.ctx.build_dir, 'other_builds')
×
1244
        if exists(other_builds_dir):
×
1245
            for filen in sorted(os.listdir(other_builds_dir)):
×
1246
                name = filen.split('-')[0]
×
1247
                dependencies = filen.split('-')[1:]
×
1248
                recipe_str = ('    {Style.BRIGHT}{Fore.GREEN}{name}'
×
1249
                              '{Style.RESET_ALL}'.format(
1250
                                  Style=Out_Style, name=name, Fore=Out_Fore))
1251
                if dependencies:
×
1252
                    recipe_str += (
×
1253
                        ' ({Fore.BLUE}with ' + ', '.join(dependencies) +
1254
                        '{Fore.RESET})').format(Fore=Out_Fore)
1255
                recipe_str += '{Style.RESET_ALL}'.format(Style=Out_Style)
×
1256
                print(recipe_str)
×
1257

1258

1259
if __name__ == "__main__":
1260
    main()
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