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

kivy / python-for-android / 26681571906

30 May 2026 10:31AM UTC coverage: 62.67% (-1.2%) from 63.887%
26681571906

Pull #3278

github

web-flow
Merge 117fe4eef into 74b559a3c
Pull Request #3278: Handling system bars and Edge-to-Edge enforcement (android 15+)

1832 of 3194 branches covered (57.36%)

Branch coverage included in aggregate %.

5407 of 8357 relevant lines covered (64.7%)

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
            '--bootstrap',
337
            help='The bootstrap to build with. Leave unset to choose '
338
                 'automatically.',
339
            default=None)
340

341
        generic_parser.add_argument(
6✔
342
            '--hook',
343
            help='Filename to a module that contains python-for-android hooks',
344
            default=None)
345

346
        add_boolean_option(
6✔
347
            generic_parser, ["force-build"],
348
            default=False,
349
            description='Whether to force compilation of a new distribution')
350

351
        add_boolean_option(
6✔
352
            generic_parser, ["require-perfect-match"],
353
            default=False,
354
            description=('Whether the dist recipes must perfectly match '
355
                         'those requested'))
356

357
        add_boolean_option(
6✔
358
            generic_parser, ["allow-replace-dist"],
359
            default=True,
360
            description='Whether existing dist names can be automatically replaced'
361
            )
362

363
        generic_parser.add_argument(
6✔
364
            '--local-recipes', '--local_recipes',
365
            dest='local_recipes', default='./p4a-recipes',
366
            help='Directory to look for local recipes')
367

368
        generic_parser.add_argument(
6✔
369
            '--activity-class-name',
370
            dest='activity_class_name', default='org.kivy.android.PythonActivity',
371
            help='The full java class name of the main activity')
372

373
        generic_parser.add_argument(
6✔
374
            '--service-class-name',
375
            dest='service_class_name', default='org.kivy.android.PythonService',
376
            help='Full java package name of the PythonService class')
377

378
        generic_parser.add_argument(
6✔
379
            '--java-build-tool',
380
            dest='java_build_tool', default='auto',
381
            choices=['auto', 'ant', 'gradle'],
382
            help=('The java build tool to use when packaging the APK, defaults '
383
                  'to automatically selecting an appropriate tool.'))
384

385
        add_boolean_option(
6✔
386
            generic_parser, ['copy-libs'],
387
            default=False,
388
            description='Copy libraries instead of using biglink (Android 4.3+)'
389
        )
390

391
        self._read_configuration()
6✔
392

393
        subparsers = parser.add_subparsers(dest='subparser_name',
6✔
394
                                           help='The command to run')
395

396
        subparsers.add_parser(
6✔
397
            'recommendations',
398
            parents=[generic_parser],
399
            help='List recommended p4a dependencies')
400
        parser_recipes = subparsers.add_parser(
6✔
401
            'recipes',
402
            parents=[generic_parser],
403
            help='List the available recipes')
404
        parser_recipes.add_argument(
6✔
405
            "--compact",
406
            action="store_true", default=False,
407
            help="Produce a compact list suitable for scripting")
408
        subparsers.add_parser(
6✔
409
            'bootstraps',
410
            help='List the available bootstraps',
411
            parents=[generic_parser])
412
        subparsers.add_parser(
6✔
413
            'clean_all',
414
            aliases=['clean-all'],
415
            help='Delete all builds, dists and caches',
416
            parents=[generic_parser])
417
        subparsers.add_parser(
6✔
418
            'clean_dists',
419
            aliases=['clean-dists'],
420
            help='Delete all dists',
421
            parents=[generic_parser])
422
        subparsers.add_parser(
6✔
423
            'clean_bootstrap_builds',
424
            aliases=['clean-bootstrap-builds'],
425
            help='Delete all bootstrap builds',
426
            parents=[generic_parser])
427
        subparsers.add_parser(
6✔
428
            'clean_builds',
429
            aliases=['clean-builds'],
430
            help='Delete all builds',
431
            parents=[generic_parser])
432

433
        parser_clean = subparsers.add_parser(
6✔
434
            'clean',
435
            help='Delete build components.',
436
            parents=[generic_parser])
437
        parser_clean.add_argument(
6✔
438
            'component', nargs='+',
439
            help=('The build component(s) to delete. You can pass any '
440
                  'number of arguments from "all", "builds", "dists", '
441
                  '"distributions", "bootstrap_builds", "downloads".'))
442

443
        parser_clean_recipe_build = subparsers.add_parser(
6✔
444
            'clean_recipe_build', aliases=['clean-recipe-build'],
445
            help=('Delete the build components of the given recipe. '
446
                  'By default this will also delete built dists'),
447
            parents=[generic_parser])
448
        parser_clean_recipe_build.add_argument(
6✔
449
            'recipe', help='The recipe name')
450
        parser_clean_recipe_build.add_argument(
6✔
451
            '--no-clean-dists', default=False,
452
            dest='no_clean_dists',
453
            action='store_true',
454
            help='If passed, do not delete existing dists')
455

456
        parser_clean_download_cache = subparsers.add_parser(
6✔
457
            'clean_download_cache', aliases=['clean-download-cache'],
458
            help='Delete cached downloads for requirement builds',
459
            parents=[generic_parser])
460
        parser_clean_download_cache.add_argument(
6✔
461
            'recipes',
462
            nargs='*',
463
            help='The recipes to clean (space-separated). If no recipe name is'
464
                  ' provided, the entire cache is cleared.')
465

466
        parser_export_dist = subparsers.add_parser(
6✔
467
            'export_dist', aliases=['export-dist'],
468
            help='Copy the named dist to the given path',
469
            parents=[generic_parser])
470
        parser_export_dist.add_argument('output_dir',
6✔
471
                                        help='The output dir to copy to')
472
        parser_export_dist.add_argument(
6✔
473
            '--symlink',
474
            action='store_true',
475
            help='Symlink the dist instead of copying')
476

477
        parser_packaging = argparse.ArgumentParser(
6✔
478
            parents=[generic_parser],
479
            add_help=False,
480
            description='common options for packaging (apk, aar)')
481

482
        # This is actually an internal argument of the build.py
483
        # (see pythonforandroid/bootstraps/common/build/build.py).
484
        # However, it is also needed before the distribution is finally
485
        # assembled for locating the setup.py / other build systems, which
486
        # is why we also add it here:
487
        parser_packaging.add_argument(
6✔
488
            '--add-asset', dest='assets',
489
            action="append", default=[],
490
            help='Put this in the assets folder in the apk.')
491
        parser_packaging.add_argument(
6✔
492
            '--add-resource', dest='resources',
493
            action="append", default=[],
494
            help='Put this in the res folder in the apk.')
495
        parser_packaging.add_argument(
6✔
496
            '--private', dest='private',
497
            help='the directory with the app source code files' +
498
                 ' (containing your main.py entrypoint)',
499
            required=False, default=None)
500
        parser_packaging.add_argument(
6✔
501
            '--use-setup-py', dest="use_setup_py",
502
            action='store_true', default=False,
503
            help="Process the setup.py of a project if present. " +
504
                 "(Experimental!")
505
        parser_packaging.add_argument(
6✔
506
            '--ignore-setup-py', dest="ignore_setup_py",
507
            action='store_true', default=False,
508
            help="Don't run the setup.py of a project if present. " +
509
                 "This may be required if the setup.py is not " +
510
                 "designed to work inside p4a (e.g. by installing " +
511
                 "dependencies that won't work or aren't desired " +
512
                 "on Android")
513
        parser_packaging.add_argument(
6✔
514
            '--release', dest='build_mode', action='store_const',
515
            const='release', default='debug',
516
            help='Build your app as a non-debug release build. '
517
                 '(Disables gdb debugging among other things)')
518
        parser_packaging.add_argument(
6✔
519
            '--with-debug-symbols', dest='with_debug_symbols',
520
            action='store_const', const=True, default=False,
521
            help='Will keep debug symbols from `.so` files.')
522
        parser_packaging.add_argument(
6✔
523
            '--keystore', dest='keystore', action='store', default=None,
524
            help=('Keystore for JAR signing key, will use jarsigner '
525
                  'default if not specified (release build only)'))
526
        parser_packaging.add_argument(
6✔
527
            '--signkey', dest='signkey', action='store', default=None,
528
            help='Key alias to sign PARSER_APK. with (release build only)')
529
        parser_packaging.add_argument(
6✔
530
            '--keystorepw', dest='keystorepw', action='store', default=None,
531
            help='Password for keystore')
532
        parser_packaging.add_argument(
6✔
533
            '--signkeypw', dest='signkeypw', action='store', default=None,
534
            help='Password for key alias')
535

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

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

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

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

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

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

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

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

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

600
        self.args = args
6✔
601

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

606
        setup_color(args.color)
6✔
607

608
        if args.debug:
6!
609
            logger.setLevel(logging.DEBUG)
×
610

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

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

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

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

670
        self.warn_on_deprecated_args(args)
6✔
671

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

681
        self._archs = args.arch
6✔
682

683
        self.ctx.local_recipes = realpath(args.local_recipes)
6✔
684
        self.ctx.copy_libs = args.copy_libs
6✔
685

686
        self.ctx.activity_class_name = args.activity_class_name
6✔
687
        self.ctx.service_class_name = args.service_class_name
6✔
688

689
        self.ctx.extra_index_urls = args.extra_index_urls
6✔
690
        self.ctx.skip_prebuilt = args.skip_prebuilt
6✔
691
        self.ctx.use_prebuilt_version_for = args.use_prebuilt_version_for
6✔
692
        self.ctx.save_wheel_dir = args.save_wheel_dir
6✔
693

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

891
            p4a clean_download_cache kivy,pyjnius
892

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

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

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

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

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

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

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

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

998
        return env
×
999

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

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

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

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

1036
            env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir
×
1037
            env["ANDROID_HOME"] = self.ctx.sdk_dir
×
1038

1039
            gradlew = sh.Command('./gradlew')
×
1040

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

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

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

1081
        package_glob = "*-{}.%s" % package_type
×
1082
        package_add_version = True
×
1083

1084
        self.hook("after_apk_assemble")
×
1085

1086
        info_main('# Copying android package to current directory')
×
1087

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1232
    def recommendations(self, args):
6✔
1233
        print_recommendations()
6✔
1234

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

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

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

1263

1264
if __name__ == "__main__":
1265
    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