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

kivy / python-for-android / 28562119440

02 Jul 2026 02:57AM UTC coverage: 62.693% (+0.02%) from 62.67%
28562119440

Pull #3352

github

web-flow
Merge 932635897 into 10d4798ec
Pull Request #3352: - Add recipe for selectolax

1832 of 3194 branches covered (57.36%)

Branch coverage included in aggregate %.

7 of 7 new or added lines in 1 file covered. (100.0%)

120 existing lines in 1 file now uncovered.

5414 of 8364 relevant lines covered (64.73%)

3.88 hits per line

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

48.85
/pythonforandroid/toolchain.py
1
#!/usr/bin/env python
2
"""
6✔
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
6✔
10
import argparse
6✔
11
from functools import wraps
6✔
12
import glob
6✔
13
import logging
6✔
14
import os
6✔
15
from os import environ
6✔
16
from os.path import (join, dirname, realpath, exists, expanduser, basename)
6✔
17
import re
6✔
18
import shlex
6✔
19
import sys
6✔
20
from sys import platform
6✔
21

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

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

30
from pythonforandroid import __version__
6✔
31
from pythonforandroid.bootstrap import Bootstrap
6✔
32
from pythonforandroid.build import Context, build_recipes, project_has_setup_py
6✔
33
from pythonforandroid.distribution import Distribution, pretty_log_dists
6✔
34
from pythonforandroid.entrypoints import main
6✔
35
from pythonforandroid.graph import get_recipe_order_and_bootstrap
6✔
36
from pythonforandroid.logger import (logger, info, warning, setup_color,
6✔
37
                                     Out_Style, Out_Fore,
38
                                     info_notify, info_main, shprint)
39
from pythonforandroid.pythonpackage import get_dep_names_of_package
6✔
40
from pythonforandroid.recipe import Recipe
6✔
41
from pythonforandroid.recommendations import (
6✔
42
    RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API, print_recommendations)
43
from pythonforandroid.util import (
6✔
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))
6✔
52
toolchain_dir = dirname(__file__)
6✔
53
sys.path.insert(0, join(toolchain_dir, "tools", "external"))
6✔
54

55

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

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

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

82

83
def require_prebuilt_dist(func):
6✔
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)
6✔
91
    def wrapper_func(self, args, **kw):
6✔
92
        ctx = self.ctx
6✔
93
        ctx.set_archs(self._archs)
6✔
94
        ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir,
6✔
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
6✔
99
        if dist.needs_build:
6!
100
            if dist.folder_exists():  # possible if the dist is being replaced
6!
101
                dist.delete()
×
102
            info_notify('No dist exists that meets your requirements, '
6✔
103
                        'so one will be built.')
104
            build_dist_from_args(ctx, dist, args)
6✔
105
        func(self, args, **kw)
6✔
106
    return wrapper_func
6✔
107

108

109
def dist_from_args(ctx, args):
6✔
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(
6✔
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):
6✔
125
    """Parses out any bootstrap related arguments, and uses them to build
126
    a dist."""
127
    bs = Bootstrap.get_bootstrap(args.bootstrap, ctx)
6✔
128
    blacklist = getattr(args, "blacklist_requirements", "").split(",")
6✔
129
    if len(blacklist) == 1 and blacklist[0] == "":
6!
130
        blacklist = []
6✔
131
    build_order, python_modules, bs = (
6✔
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()
6✔
137
    ctx.recipe_build_order = build_order
6✔
138
    ctx.python_modules = python_modules
6✔
139

140
    info('The selected bootstrap is {}'.format(bs.name))
6✔
141
    info_main('# Creating dist with {} bootstrap'.format(bs.name))
6✔
142
    bs.distribution = dist
6✔
143
    info_notify('Dist will have name {} and requirements ({})'.format(
6✔
144
        dist.name, ', '.join(dist.recipes)))
145
    info('Dist contains the following requirements as recipes: {}'.format(
6✔
146
        ctx.recipe_build_order))
147
    info('Dist will also contain modules ({}) installed from pip'.format(
6✔
148
        ', '.join(ctx.python_modules)))
149
    info(
6✔
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
6✔
159
    ctx.prepare_bootstrap(bs)
6✔
160
    if dist.needs_build:
6!
161
        ctx.prepare_dist()
6✔
162

163
    build_recipes(build_order, python_modules, ctx,
6✔
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()
6✔
171

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

176

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

182

183
class NoAbbrevParser(argparse.ArgumentParser):
6✔
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

192
    def _get_option_tuples(self, option_string):
6✔
193
        return []
6✔
194

195

196
class ToolchainCL:
6✔
197

198
    def __init__(self):
6✔
199

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

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

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

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

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

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

271
        generic_parser.add_argument(
6✔
272
            '--extra-index-url',
273
            help=(
274
                'Extra package indexes to look for prebuilt Android wheels. '
275
                'Can be used multiple times.'
276
            ),
277
            action='append',
278
            default=[],
279
            dest="extra_index_urls",
280
        )
281

282
        generic_parser.add_argument(
6✔
283
            '--skip-prebuilt',
284
            help='Always build from source; do not use prebuilt wheels.',
285
            action='store_true',
286
            default=False,
287
            dest="skip_prebuilt",
288
        )
289

290
        generic_parser.add_argument(
6✔
291
            '--use-prebuilt-version-for',
292
            help=(
293
                'For these packages, ignore pinned versions and use the latest '
294
                'prebuilt version from the extra index if available. '
295
                'Only applies to packages with a recipe.'
296
            ),
297
            action='append',
298
            default=[],
299
            dest="use_prebuilt_version_for",
300
        )
301

302
        generic_parser.add_argument(
6✔
303
            '--save-wheel-dir',
304
            dest='save_wheel_dir',
305
            default='',
306
            help='Directory to store wheels built by PyProjectRecipe.',
307
        )
308

309
        # Options for specifying the Distribution
310
        generic_parser.add_argument(
6✔
311
            '--dist-name', '--dist_name',
312
            help='The name of the distribution to use or create', default='')
313

314
        generic_parser.add_argument(
6✔
315
            '--requirements',
316
            help=('Dependencies of your app, should be recipe names or '
317
                  'Python modules. NOT NECESSARY if you are using '
318
                  'Python 3 with --use-setup-py'),
319
            default='')
320

321
        generic_parser.add_argument(
6✔
322
            '--recipe-blacklist',
323
            help=('Blacklist an internal recipe from use. Allows '
324
                  'disabling Python 3 core modules to save size'),
325
            dest="recipe_blacklist",
326
            default='')
327

328
        generic_parser.add_argument(
6✔
329
            '--blacklist-requirements',
330
            help=('Blacklist an internal recipe from use. Allows '
331
                  'disabling Python 3 core modules to save size'),
332
            dest="blacklist_requirements",
333
            default='')
334

335
        generic_parser.add_argument(
6✔
336
            '--use-setup-py', dest="use_setup_py",
337
            action='store_true', default=False,
338
            help="Process the setup.py of a project if present. " +
339
                 "(Experimental!")
340
        generic_parser.add_argument(
6✔
341
            '--ignore-setup-py', dest="ignore_setup_py",
342
            action='store_true', default=False,
343
            help="Don't run the setup.py of a project if present. " +
344
                 "This may be required if the setup.py is not " +
345
                 "designed to work inside p4a (e.g. by installing " +
346
                 "dependencies that won't work or aren't desired " +
347
                 "on Android")
348

349
        generic_parser.add_argument(
6✔
350
            '--bootstrap',
351
            help='The bootstrap to build with. Leave unset to choose '
352
                 'automatically.',
353
            default=None)
354

355
        generic_parser.add_argument(
6✔
356
            '--hook',
357
            help='Filename to a module that contains python-for-android hooks',
358
            default=None)
359

360
        add_boolean_option(
6✔
361
            generic_parser, ["force-build"],
362
            default=False,
363
            description='Whether to force compilation of a new distribution')
364

365
        add_boolean_option(
6✔
366
            generic_parser, ["require-perfect-match"],
367
            default=False,
368
            description=('Whether the dist recipes must perfectly match '
369
                         'those requested'))
370

371
        add_boolean_option(
6✔
372
            generic_parser, ["allow-replace-dist"],
373
            default=True,
374
            description='Whether existing dist names can be automatically replaced'
375
            )
376

377
        generic_parser.add_argument(
6✔
378
            '--local-recipes', '--local_recipes',
379
            dest='local_recipes', default='./p4a-recipes',
380
            help='Directory to look for local recipes')
381

382
        generic_parser.add_argument(
6✔
383
            '--activity-class-name',
384
            dest='activity_class_name', default='org.kivy.android.PythonActivity',
385
            help='The full java class name of the main activity')
386

387
        generic_parser.add_argument(
6✔
388
            '--service-class-name',
389
            dest='service_class_name', default='org.kivy.android.PythonService',
390
            help='Full java package name of the PythonService class')
391

392
        generic_parser.add_argument(
6✔
393
            '--java-build-tool',
394
            dest='java_build_tool', default='auto',
395
            choices=['auto', 'ant', 'gradle'],
396
            help=('The java build tool to use when packaging the APK, defaults '
397
                  'to automatically selecting an appropriate tool.'))
398

399
        add_boolean_option(
6✔
400
            generic_parser, ['copy-libs'],
401
            default=False,
402
            description='Copy libraries instead of using biglink (Android 4.3+)'
403
        )
404

405
        self._read_configuration()
6✔
406

407
        subparsers = parser.add_subparsers(dest='subparser_name',
6✔
408
                                           help='The command to run')
409

410
        subparsers.add_parser(
6✔
411
            'recommendations',
412
            parents=[generic_parser],
413
            help='List recommended p4a dependencies')
414
        parser_recipes = subparsers.add_parser(
6✔
415
            'recipes',
416
            parents=[generic_parser],
417
            help='List the available recipes')
418
        parser_recipes.add_argument(
6✔
419
            "--compact",
420
            action="store_true", default=False,
421
            help="Produce a compact list suitable for scripting")
422
        subparsers.add_parser(
6✔
423
            'bootstraps',
424
            help='List the available bootstraps',
425
            parents=[generic_parser])
426
        subparsers.add_parser(
6✔
427
            'clean_all',
428
            aliases=['clean-all'],
429
            help='Delete all builds, dists and caches',
430
            parents=[generic_parser])
431
        subparsers.add_parser(
6✔
432
            'clean_dists',
433
            aliases=['clean-dists'],
434
            help='Delete all dists',
435
            parents=[generic_parser])
436
        subparsers.add_parser(
6✔
437
            'clean_bootstrap_builds',
438
            aliases=['clean-bootstrap-builds'],
439
            help='Delete all bootstrap builds',
440
            parents=[generic_parser])
441
        subparsers.add_parser(
6✔
442
            'clean_builds',
443
            aliases=['clean-builds'],
444
            help='Delete all builds',
445
            parents=[generic_parser])
446

447
        parser_clean = subparsers.add_parser(
6✔
448
            'clean',
449
            help='Delete build components.',
450
            parents=[generic_parser])
451
        parser_clean.add_argument(
6✔
452
            'component', nargs='+',
453
            help=('The build component(s) to delete. You can pass any '
454
                  'number of arguments from "all", "builds", "dists", '
455
                  '"distributions", "bootstrap_builds", "downloads".'))
456

457
        parser_clean_recipe_build = subparsers.add_parser(
6✔
458
            'clean_recipe_build', aliases=['clean-recipe-build'],
459
            help=('Delete the build components of the given recipe. '
460
                  'By default this will also delete built dists'),
461
            parents=[generic_parser])
462
        parser_clean_recipe_build.add_argument(
6✔
463
            'recipe', help='The recipe name')
464
        parser_clean_recipe_build.add_argument(
6✔
465
            '--no-clean-dists', default=False,
466
            dest='no_clean_dists',
467
            action='store_true',
468
            help='If passed, do not delete existing dists')
469

470
        parser_clean_download_cache = subparsers.add_parser(
6✔
471
            'clean_download_cache', aliases=['clean-download-cache'],
472
            help='Delete cached downloads for requirement builds',
473
            parents=[generic_parser])
474
        parser_clean_download_cache.add_argument(
6✔
475
            'recipes',
476
            nargs='*',
477
            help='The recipes to clean (space-separated). If no recipe name is'
478
                  ' provided, the entire cache is cleared.')
479

480
        parser_export_dist = subparsers.add_parser(
6✔
481
            'export_dist', aliases=['export-dist'],
482
            help='Copy the named dist to the given path',
483
            parents=[generic_parser])
484
        parser_export_dist.add_argument('output_dir',
6✔
485
                                        help='The output dir to copy to')
486
        parser_export_dist.add_argument(
6✔
487
            '--symlink',
488
            action='store_true',
489
            help='Symlink the dist instead of copying')
490

491
        parser_packaging = argparse.ArgumentParser(
6✔
492
            parents=[generic_parser],
493
            add_help=False,
494
            description='common options for packaging (apk, aar)')
495

496
        # This is actually an internal argument of the build.py
497
        # (see pythonforandroid/bootstraps/common/build/build.py).
498
        # However, it is also needed before the distribution is finally
499
        # assembled for locating the setup.py / other build systems, which
500
        # is why we also add it here:
501
        parser_packaging.add_argument(
6✔
502
            '--add-asset', dest='assets',
503
            action="append", default=[],
504
            help='Put this in the assets folder in the apk.')
505
        parser_packaging.add_argument(
6✔
506
            '--add-resource', dest='resources',
507
            action="append", default=[],
508
            help='Put this in the res folder in the apk.')
509
        parser_packaging.add_argument(
6✔
510
            '--private', dest='private',
511
            help='the directory with the app source code files' +
512
                 ' (containing your main.py entrypoint)',
513
            required=False, default=None)
514

515
        parser_packaging.add_argument(
6✔
516
            '--release', dest='build_mode', action='store_const',
517
            const='release', default='debug',
518
            help='Build your app as a non-debug release build. '
519
                 '(Disables gdb debugging among other things)')
520
        parser_packaging.add_argument(
6✔
521
            '--with-debug-symbols', dest='with_debug_symbols',
522
            action='store_const', const=True, default=False,
523
            help='Will keep debug symbols from `.so` files.')
524
        parser_packaging.add_argument(
6✔
525
            '--keystore', dest='keystore', action='store', default=None,
526
            help=('Keystore for JAR signing key, will use jarsigner '
527
                  'default if not specified (release build only)'))
528
        parser_packaging.add_argument(
6✔
529
            '--signkey', dest='signkey', action='store', default=None,
530
            help='Key alias to sign PARSER_APK. with (release build only)')
531
        parser_packaging.add_argument(
6✔
532
            '--keystorepw', dest='keystorepw', action='store', default=None,
533
            help='Password for keystore')
534
        parser_packaging.add_argument(
6✔
535
            '--signkeypw', dest='signkeypw', action='store', default=None,
536
            help='Password for key alias')
537

538
        subparsers.add_parser(
6✔
539
            'aar', help='Build an AAR',
540
            parents=[parser_packaging])
541

542
        subparsers.add_parser(
6✔
543
            'apk', help='Build an APK',
544
            parents=[parser_packaging])
545

546
        subparsers.add_parser(
6✔
547
            'aab', help='Build an AAB',
548
            parents=[parser_packaging])
549

550
        subparsers.add_parser(
6✔
551
            'create', help='Compile a set of requirements into a dist',
552
            parents=[generic_parser])
553
        subparsers.add_parser(
6✔
554
            'archs', help='List the available target architectures',
555
            parents=[generic_parser])
556
        subparsers.add_parser(
6✔
557
            'distributions', aliases=['dists'],
558
            help='List the currently available (compiled) dists',
559
            parents=[generic_parser])
560
        subparsers.add_parser(
6✔
561
            'delete_dist', aliases=['delete-dist'], help='Delete a compiled dist',
562
            parents=[generic_parser])
563

564
        parser_sdk_tools = subparsers.add_parser(
6✔
565
            'sdk_tools', aliases=['sdk-tools'],
566
            help='Run the given binary from the SDK tools dis',
567
            parents=[generic_parser])
568
        parser_sdk_tools.add_argument(
6✔
569
            'tool', help='The binary tool name to run')
570

571
        subparsers.add_parser(
6✔
572
            'adb', help='Run adb from the given SDK',
573
            parents=[generic_parser])
574
        subparsers.add_parser(
6✔
575
            'logcat', help='Run logcat from the given SDK',
576
            parents=[generic_parser])
577
        subparsers.add_parser(
6✔
578
            'build_status', aliases=['build-status'],
579
            help='Print some debug information about current built components',
580
            parents=[generic_parser])
581

582
        parser.add_argument('-v', '--version', action='version',
6✔
583
                            version=__version__)
584

585
        args, unknown = parser.parse_known_args(sys.argv[1:])
6✔
586
        args.unknown_args = unknown
6✔
587

588
        if getattr(args, "private", None) is not None:
6!
589
            # Pass this value on to the internal bootstrap build.py:
590
            args.unknown_args += ["--private", args.private]
×
591
        if getattr(args, "build_mode", None) == "release":
6!
592
            args.unknown_args += ["--release"]
×
593
        if getattr(args, "with_debug_symbols", False):
6!
594
            args.unknown_args += ["--with-debug-symbols"]
×
595
        if getattr(args, "ignore_setup_py", False):
6!
UNCOV
596
            args.use_setup_py = False
×
597
        if getattr(args, "activity_class_name", "org.kivy.android.PythonActivity") != 'org.kivy.android.PythonActivity':
6✔
598
            args.unknown_args += ["--activity-class-name", args.activity_class_name]
6✔
599
        if getattr(args, "service_class_name", "org.kivy.android.PythonService") != 'org.kivy.android.PythonService':
6✔
600
            args.unknown_args += ["--service-class-name", args.service_class_name]
6✔
601

602
        self.args = args
6✔
603

604
        if args.subparser_name is None:
6✔
605
            parser.print_help()
6✔
606
            exit(1)
6✔
607

608
        setup_color(args.color)
6✔
609

610
        if args.debug:
6!
UNCOV
611
            logger.setLevel(logging.DEBUG)
×
612

613
        self.ctx = Context()
6✔
614
        self.ctx.use_setup_py = getattr(args, "use_setup_py", True)
6✔
615
        self.ctx.build_as_debuggable = getattr(
6✔
616
            args, "build_mode", "debug"
617
        ) == "debug"
618
        self.ctx.with_debug_symbols = getattr(
6✔
619
            args, "with_debug_symbols", False
620
        )
621

622
        # Process requirements and put version in environ
623
        if hasattr(args, 'requirements'):
6!
624
            requirements = []
6✔
625

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

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

672
        self.warn_on_deprecated_args(args)
6✔
673

674
        self.storage_dir = args.storage_dir
6✔
675
        self.ctx.setup_dirs(self.storage_dir)
6✔
676
        self.sdk_dir = args.sdk_dir
6✔
677
        self.ndk_dir = args.ndk_dir
6✔
678
        self.android_api = args.android_api
6✔
679
        self.ndk_api = args.ndk_api
6✔
680
        self.ctx.symlink_bootstrap_files = args.symlink_bootstrap_files
6✔
681
        self.ctx.java_build_tool = args.java_build_tool
6✔
682

683
        self._archs = args.arch
6✔
684

685
        self.ctx.local_recipes = realpath(args.local_recipes)
6✔
686
        self.ctx.copy_libs = args.copy_libs
6✔
687

688
        self.ctx.activity_class_name = args.activity_class_name
6✔
689
        self.ctx.service_class_name = args.service_class_name
6✔
690

691
        self.ctx.extra_index_urls = args.extra_index_urls
6✔
692
        self.ctx.skip_prebuilt = args.skip_prebuilt
6✔
693
        self.ctx.use_prebuilt_version_for = args.use_prebuilt_version_for
6✔
694
        self.ctx.save_wheel_dir = args.save_wheel_dir
6✔
695

696
        # Each subparser corresponds to a method
697
        command = args.subparser_name.replace('-', '_')
6✔
698
        getattr(self, command)(args)
6✔
699

700
    @staticmethod
6✔
701
    def warn_on_carriage_return_args(args):
6✔
702
        for check_arg in args:
6✔
703
            if '\r' in check_arg:
6!
UNCOV
704
                warning("Argument '{}' contains a carriage return (\\r).".format(str(check_arg.replace('\r', ''))))
×
UNCOV
705
                warning("Invoking this program via scripts which use CRLF instead of LF line endings will have undefined behaviour.")
×
706

707
    def warn_on_deprecated_args(self, args):
6✔
708
        """
709
        Print warning messages for any deprecated arguments that were passed.
710
        """
711

712
        # Output warning if setup.py is present and neither --ignore-setup-py
713
        # nor --use-setup-py was specified.
714
        if project_has_setup_py(getattr(args, "private", None)):
6!
715
            if not getattr(args, "use_setup_py", False) and \
×
716
                    not getattr(args, "ignore_setup_py", False):
717
                warning("  **** FUTURE BEHAVIOR CHANGE WARNING ****")
×
718
                warning("Your project appears to contain a setup.py file.")
×
719
                warning("Currently, these are ignored by default.")
×
720
                warning("This will CHANGE in an upcoming version!")
×
721
                warning("")
×
722
                warning("To ensure your setup.py is ignored, please specify:")
×
723
                warning("    --ignore-setup-py")
×
724
                warning("")
×
UNCOV
725
                warning("To enable what will some day be the default, specify:")
×
UNCOV
726
                warning("    --use-setup-py")
×
727

728
        # NDK version is now determined automatically
729
        if args.ndk_version is not None:
6!
UNCOV
730
            warning('--ndk-version is deprecated and no longer necessary, '
×
731
                    'the value you passed is ignored')
732
        if 'ANDROIDNDKVER' in environ:
6!
UNCOV
733
            warning('$ANDROIDNDKVER is deprecated and no longer necessary, '
×
734
                    'the value you set is ignored')
735

736
    def hook(self, name):
6✔
737
        if not self.args.hook:
×
UNCOV
738
            return
×
739
        if not hasattr(self, "hook_module"):
×
740
            # first time, try to load the hook module
741
            self.hook_module = load_source(
×
742
                "pythonforandroid.hook", self.args.hook)
743
        if hasattr(self.hook_module, name):
×
UNCOV
744
            info("Hook: execute {}".format(name))
×
745
            getattr(self.hook_module, name)(self)
×
746
        else:
UNCOV
747
            info("Hook: ignore {}".format(name))
×
748

749
    @property
6✔
750
    def default_storage_dir(self):
6✔
751
        udd = user_data_dir('python-for-android')
×
752
        if ' ' in udd:
×
UNCOV
753
            udd = '~/.python-for-android'
×
UNCOV
754
        return udd
×
755

756
    @staticmethod
6✔
757
    def _read_configuration():
6✔
758
        # search for a .p4a configuration file in the current directory
759
        if not exists(".p4a"):
6!
760
            return
6✔
761
        info("Reading .p4a configuration")
×
762
        with open(".p4a") as fd:
×
UNCOV
763
            lines = fd.readlines()
×
764
        lines = [shlex.split(line)
×
765
                 for line in lines if not line.startswith("#")]
766
        for line in lines:
×
UNCOV
767
            for arg in line:
×
UNCOV
768
                sys.argv.append(arg)
×
769

770
    def recipes(self, args):
6✔
771
        """
772
        Prints recipes basic info, e.g.
773
        .. code-block:: bash
774

775
            python3      3.7.1
776
                depends: ['hostpython3', 'sqlite3', 'openssl', 'libffi']
777
                conflicts: []
778
                optional depends: ['sqlite3', 'libffi', 'openssl']
779
        """
780
        ctx = self.ctx
6✔
781
        if args.compact:
6!
UNCOV
782
            print(" ".join(set(Recipe.list_recipes(ctx))))
×
783
        else:
784
            for name in sorted(Recipe.list_recipes(ctx)):
6✔
785
                try:
6✔
786
                    recipe = Recipe.get_recipe(name, ctx)
6✔
787
                except (IOError, ValueError):
×
788
                    warning('Recipe "{}" could not be loaded'.format(name))
×
789
                except SyntaxError:
×
790
                    import traceback
×
UNCOV
791
                    traceback.print_exc()
×
UNCOV
792
                    warning(('Recipe "{}" could not be loaded due to a '
×
793
                             'syntax error').format(name))
794
                version = str(recipe.version)
6✔
795
                print('{Fore.BLUE}{Style.BRIGHT}{recipe.name:<12} '
6✔
796
                      '{Style.RESET_ALL}{Fore.LIGHTBLUE_EX}'
797
                      '{version:<8}{Style.RESET_ALL}'.format(
798
                            recipe=recipe, Fore=Out_Fore, Style=Out_Style,
799
                            version=version))
800
                print('    {Fore.GREEN}depends: {recipe.depends}'
6✔
801
                      '{Fore.RESET}'.format(recipe=recipe, Fore=Out_Fore))
802
                if recipe.conflicts:
6✔
803
                    print('    {Fore.RED}conflicts: {recipe.conflicts}'
6✔
804
                          '{Fore.RESET}'
805
                          .format(recipe=recipe, Fore=Out_Fore))
806
                if recipe.opt_depends:
6✔
807
                    print('    {Fore.YELLOW}optional depends: '
6✔
808
                          '{recipe.opt_depends}{Fore.RESET}'
809
                          .format(recipe=recipe, Fore=Out_Fore))
810

811
    def bootstraps(self, _args):
6✔
812
        """List all the bootstraps available to build with."""
813
        for bs in Bootstrap.all_bootstraps():
×
UNCOV
814
            bs = Bootstrap.get_bootstrap(bs, self.ctx)
×
815
            print('{Fore.BLUE}{Style.BRIGHT}{bs.name}{Style.RESET_ALL}'
×
816
                  .format(bs=bs, Fore=Out_Fore, Style=Out_Style))
UNCOV
817
            print('    {Fore.GREEN}depends: {bs.recipe_depends}{Fore.RESET}'
×
818
                  .format(bs=bs, Fore=Out_Fore))
819

820
    def clean(self, args):
6✔
821
        components = args.component
×
822

UNCOV
823
        component_clean_methods = {
×
824
            'all': self.clean_all,
825
            'dists': self.clean_dists,
826
            'distributions': self.clean_dists,
827
            'builds': self.clean_builds,
828
            'bootstrap_builds': self.clean_bootstrap_builds,
829
            'downloads': self.clean_download_cache}
830

831
        for component in components:
×
UNCOV
832
            if component not in component_clean_methods:
×
UNCOV
833
                raise BuildInterruptingException((
×
834
                    'Asked to clean "{}" but this argument is not '
835
                    'recognised'.format(component)))
UNCOV
836
            component_clean_methods[component](args)
×
837

838
    def clean_all(self, args):
6✔
839
        """Delete all build components; the package cache, package builds,
840
        bootstrap builds and distributions."""
841
        self.clean_dists(args)
×
UNCOV
842
        self.clean_builds(args)
×
UNCOV
843
        self.clean_download_cache(args)
×
844

845
    def clean_dists(self, _args):
6✔
846
        """Delete all compiled distributions in the internal distribution
847
        directory."""
UNCOV
848
        ctx = self.ctx
×
UNCOV
849
        rmdir(ctx.dist_dir)
×
850

851
    def clean_bootstrap_builds(self, _args):
6✔
852
        """Delete all the bootstrap builds."""
UNCOV
853
        rmdir(join(self.ctx.build_dir, 'bootstrap_builds'))
×
854
        # for bs in Bootstrap.all_bootstraps():
855
        #     bs = Bootstrap.get_bootstrap(bs, self.ctx)
856
        #     if bs.build_dir and exists(bs.build_dir):
857
        #         info('Cleaning build for {} bootstrap.'.format(bs.name))
858
        #         rmdir(bs.build_dir)
859

860
    def clean_builds(self, _args):
6✔
861
        """Delete all build caches for each recipe, python-install, java code
862
        and compiled libs collection.
863

864
        This does *not* delete the package download cache or the final
865
        distributions.  You can also use clean_recipe_build to delete the build
866
        of a specific recipe.
867
        """
868
        ctx = self.ctx
×
869
        rmdir(ctx.build_dir)
×
870
        rmdir(ctx.python_installs_dir)
×
UNCOV
871
        libs_dir = join(self.ctx.build_dir, 'libs_collections')
×
UNCOV
872
        rmdir(libs_dir)
×
873

874
    def clean_recipe_build(self, args):
6✔
875
        """Deletes the build files of the given recipe.
876

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

889
    def clean_download_cache(self, args):
6✔
890
        """ Deletes a download cache for recipes passed as arguments. If no
891
        argument is passed, it'll delete *all* downloaded caches. ::
892

893
            p4a clean_download_cache kivy,pyjnius
894

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

914
    @require_prebuilt_dist
6✔
915
    def export_dist(self, args):
6✔
916
        """Copies a created dist to an output dir.
917

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

934
    @property
6✔
935
    def _dist(self):
6✔
936
        ctx = self.ctx
6✔
937
        dist = dist_from_args(ctx, self.args)
6✔
938
        ctx.distribution = dist
6✔
939
        return dist
6✔
940

941
    @staticmethod
6✔
942
    def _fix_args(args):
6✔
943
        """
944
        Manually fixing these arguments at the string stage is
945
        unsatisfactory and should probably be changed somehow, but
946
        we can't leave it until later as the build.py scripts assume
947
        they are in the current directory.
948
        works in-place
949
        :param args: parser args
950
        """
951

UNCOV
952
        fix_args = ('--dir', '--private', '--add-jar', '--add-source',
×
953
                    '--whitelist', '--blacklist', '--presplash', '--icon',
954
                    '--icon-bg', '--icon-fg')
955
        unknown_args = args.unknown_args
×
956

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

981
    @staticmethod
6✔
982
    def _prepare_release_env(args):
6✔
983
        """
984
        prepares envitonment dict with the necessary flags for signing an apk
985
        :param args: parser args
986
        """
987
        env = os.environ.copy()
×
988
        if args.build_mode == 'release':
×
989
            if args.keystore:
×
990
                env['P4A_RELEASE_KEYSTORE'] = realpath(expanduser(args.keystore))
×
991
            if args.signkey:
×
992
                env['P4A_RELEASE_KEYALIAS'] = args.signkey
×
993
            if args.keystorepw:
×
994
                env['P4A_RELEASE_KEYSTORE_PASSWD'] = args.keystorepw
×
995
            if args.signkeypw:
×
996
                env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.signkeypw
×
UNCOV
997
            elif args.keystorepw and 'P4A_RELEASE_KEYALIAS_PASSWD' not in env:
×
998
                env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.keystorepw
×
999

UNCOV
1000
        return env
×
1001

1002
    def _build_package(self, args, package_type):
6✔
1003
        """
1004
        Creates an android package using gradle
1005
        :param args: parser args
1006
        :param package_type: one of 'apk', 'aar', 'aab'
1007
        :return (gradle output, build_args)
1008
        """
1009
        ctx = self.ctx
×
1010
        dist = self._dist
×
1011
        bs = Bootstrap.get_bootstrap(args.bootstrap, ctx)
×
1012
        ctx.prepare_bootstrap(bs)
×
UNCOV
1013
        self._fix_args(args)
×
1014
        env = self._prepare_release_env(args)
×
1015

1016
        with current_directory(dist.dist_dir):
×
1017
            self.hook("before_apk_build")
×
1018
            os.environ["ANDROID_API"] = str(self.ctx.android_api)
×
UNCOV
1019
            build = load_source('build', join(dist.dist_dir, 'build.py'))
×
UNCOV
1020
            build_args = build.parse_args_and_make_package(
×
1021
                args.unknown_args
1022
            )
1023

1024
            self.hook("after_apk_build")
×
UNCOV
1025
            self.hook("before_apk_assemble")
×
1026
            build_tools_versions = os.listdir(join(ctx.sdk_dir,
×
1027
                                                   'build-tools'))
UNCOV
1028
            build_tools_version = max_build_tool_version(build_tools_versions)
×
UNCOV
1029
            info(('Detected highest available build tools '
×
1030
                  'version to be {}').format(build_tools_version))
1031

UNCOV
1032
            if Version(build_tools_version.replace(" ", "")) < Version('25.0'):
×
1033
                raise BuildInterruptingException(
×
1034
                    'build_tools >= 25 is required, but %s is installed' % build_tools_version)
UNCOV
1035
            if not exists("gradlew"):
×
1036
                raise BuildInterruptingException("gradlew file is missing")
×
1037

UNCOV
1038
            env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir
×
1039
            env["ANDROID_HOME"] = self.ctx.sdk_dir
×
1040

1041
            gradlew = sh.Command('./gradlew')
×
1042

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

1067
            # WARNING: We should make sure to clean the build directory before building.
1068
            # See PR: kivy/python-for-android#2705
1069
            output = shprint(gradlew, "clean", gradle_task, _tail=20,
×
1070
                             _critical=True, _env=env)
UNCOV
1071
        return output, build_args
×
1072

1073
    def _finish_package(self, args, output, build_args, package_type, output_dir):
6✔
1074
        """
1075
        Finishes the package after the gradle script run
1076
        :param args: the parser args
1077
        :param output: RunningCommand output
1078
        :param build_args: build args as returned by build.parse_args
1079
        :param package_type: one of 'apk', 'aar', 'aab'
1080
        :param output_dir: where to put the package file
1081
        """
1082

UNCOV
1083
        package_glob = "*-{}.%s" % package_type
×
1084
        package_add_version = True
×
1085

1086
        self.hook("after_apk_assemble")
×
1087

1088
        info_main('# Copying android package to current directory')
×
1089

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

1105
                package_files = glob.glob(join(output_dir, package_glob.format(suffix)))
×
1106
                if package_files:
×
UNCOV
1107
                    if len(package_files) > 1:
×
1108
                        info('More than one built APK found... guessing you '
×
1109
                             'just built {}'.format(package_files[-1]))
UNCOV
1110
                    package_file = package_files[-1]
×
1111
                    break
×
1112
            else:
1113
                raise BuildInterruptingException('Couldn\'t find the built APK')
×
1114

1115
        info_main('# Found android package file: {}'.format(package_file))
×
1116
        package_extension = f".{package_type}"
×
1117
        if package_add_version:
×
1118
            info('# Add version number to android package')
×
UNCOV
1119
            package_name = basename(package_file)[:-len(package_extension)]
×
1120
            package_file_dest = "{}-{}{}".format(
×
1121
                package_name, build_args.version, package_extension)
UNCOV
1122
            info('# Android package renamed to {}'.format(package_file_dest))
×
1123
            shprint(sh.cp, package_file, package_file_dest)
×
1124
        else:
UNCOV
1125
            shprint(sh.cp, package_file, './')
×
1126

1127
    @require_prebuilt_dist
6✔
1128
    def apk(self, args):
6✔
1129
        output, build_args = self._build_package(args, package_type='apk')
×
UNCOV
1130
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'apk', args.build_mode)
×
UNCOV
1131
        self._finish_package(args, output, build_args, 'apk', output_dir)
×
1132

1133
    @require_prebuilt_dist
6✔
1134
    def aar(self, args):
6✔
1135
        output, build_args = self._build_package(args, package_type='aar')
×
UNCOV
1136
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'aar')
×
UNCOV
1137
        self._finish_package(args, output, build_args, 'aar', output_dir)
×
1138

1139
    @require_prebuilt_dist
6✔
1140
    def aab(self, args):
6✔
1141
        output, build_args = self._build_package(args, package_type='aab')
×
UNCOV
1142
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'bundle', args.build_mode)
×
UNCOV
1143
        self._finish_package(args, output, build_args, 'aab', output_dir)
×
1144

1145
    @require_prebuilt_dist
6✔
1146
    def create(self, args):
6✔
1147
        """Create a distribution directory if it doesn't already exist, run
1148
        any recipes if necessary, and build the apk.
1149
        """
1150
        pass  # The decorator does everything
6✔
1151

1152
    def archs(self, _args):
6✔
1153
        """List the target architectures available to be built for."""
1154
        print('{Style.BRIGHT}Available target architectures are:'
×
1155
              '{Style.RESET_ALL}'.format(Style=Out_Style))
UNCOV
1156
        for arch in self.ctx.archs:
×
UNCOV
1157
            print('    {}'.format(arch.arch))
×
1158

1159
    def dists(self, args):
6✔
1160
        """The same as :meth:`distributions`."""
UNCOV
1161
        self.distributions(args)
×
1162

1163
    def distributions(self, _args):
6✔
1164
        """Lists all distributions currently available (i.e. that have already
1165
        been built)."""
UNCOV
1166
        ctx = self.ctx
×
1167
        dists = Distribution.get_distributions(ctx)
×
1168

UNCOV
1169
        if dists:
×
1170
            print('{Style.BRIGHT}Distributions currently installed are:'
×
1171
                  '{Style.RESET_ALL}'.format(Style=Out_Style))
1172
            pretty_log_dists(dists, print)
×
1173
        else:
UNCOV
1174
            print('{Style.BRIGHT}There are no dists currently built.'
×
1175
                  '{Style.RESET_ALL}'.format(Style=Out_Style))
1176

1177
    def delete_dist(self, _args):
6✔
1178
        dist = self._dist
×
UNCOV
1179
        if not dist.folder_exists():
×
1180
            info('No dist exists that matches your specifications, '
×
1181
                 'exiting without deleting.')
UNCOV
1182
            return
×
UNCOV
1183
        dist.delete()
×
1184

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

1204
    def adb(self, args):
6✔
1205
        """Runs the adb binary from the detected SDK directory, passing all
1206
        arguments straight to it. This is intended as a convenience
1207
        function if adb is not in your $PATH.
1208
        """
UNCOV
1209
        self._adb(args.unknown_args)
×
1210

1211
    def logcat(self, args):
6✔
1212
        """Runs ``adb logcat`` using the adb binary from the detected SDK
1213
        directory. All extra args are passed as arguments to logcat."""
UNCOV
1214
        self._adb(['logcat'] + args.unknown_args)
×
1215

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

1234
    def recommendations(self, args):
6✔
1235
        print_recommendations()
6✔
1236

1237
    def build_status(self, _args):
6✔
1238
        """Print the status of the specified build. """
UNCOV
1239
        print('{Style.BRIGHT}Bootstraps whose core components are probably '
×
1240
              'already built:{Style.RESET_ALL}'.format(Style=Out_Style))
1241

1242
        bootstrap_dir = join(self.ctx.build_dir, 'bootstrap_builds')
×
1243
        if exists(bootstrap_dir):
×
UNCOV
1244
            for filen in os.listdir(bootstrap_dir):
×
UNCOV
1245
                print('    {Fore.GREEN}{Style.BRIGHT}{filen}{Style.RESET_ALL}'
×
1246
                      .format(filen=filen, Fore=Out_Fore, Style=Out_Style))
1247

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

1265

1266
if __name__ == "__main__":
1267
    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

© 2026 Coveralls, Inc