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

kivy / python-for-android / 25435676503

06 May 2026 12:37PM UTC coverage: 62.876% (-1.0%) from 63.887%
25435676503

Pull #3278

github

web-flow
Merge 3c07c5446 into 3ab2fd669
Pull Request #3278: Handling system bars and Edge-to-Edge enforcement (android 15+)

1832 of 3180 branches covered (57.61%)

Branch coverage included in aggregate %.

5329 of 8209 relevant lines covered (64.92%)

5.18 hits per line

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

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

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

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

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

55

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

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

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

82

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

108

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

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

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

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

176

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

182

183
class NoAbbrevParser(argparse.ArgumentParser):
8✔
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):
8✔
193
        return []
8✔
194

195

196
class ToolchainCL:
8✔
197

198
    def __init__(self):
8✔
199

200
        argv = sys.argv
8✔
201
        self.warn_on_carriage_return_args(argv)
8✔
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
8!
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(
8✔
212
            description='A packaging tool for turning Python scripts and apps '
213
                        'into Android APKs')
214

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

221
        generic_parser.add_argument(
8✔
222
            '--debug', dest='debug', action='store_true', default=False,
223
            help='Display debug output and all build info')
224
        generic_parser.add_argument(
8✔
225
            '--color', dest='color', choices=['always', 'never', 'auto'],
226
            help='Enable or disable color output (default enabled on tty)')
227
        generic_parser.add_argument(
8✔
228
            '--sdk-dir', '--sdk_dir', dest='sdk_dir', default='',
229
            help='The filepath where the Android SDK is installed')
230
        generic_parser.add_argument(
8✔
231
            '--ndk-dir', '--ndk_dir', dest='ndk_dir', default='',
232
            help='The filepath where the Android NDK is installed')
233
        generic_parser.add_argument(
8✔
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(
8✔
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(
8✔
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(
8✔
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')
8✔
260
        if ' ' in default_storage_dir:
8!
261
            default_storage_dir = '~/.python-for-android'
×
262
        generic_parser.add_argument(
8✔
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(
8✔
268
            '--arch', help='The archs to build for.',
269
            action='append', default=[])
270

271
        generic_parser.add_argument(
8✔
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(
8✔
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(
8✔
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(
8✔
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(
8✔
311
            '--dist-name', '--dist_name',
312
            help='The name of the distribution to use or create', default='')
313

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

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

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

351
        add_boolean_option(
8✔
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(
8✔
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(
8✔
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(
8✔
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(
8✔
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(
8✔
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(
8✔
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()
8✔
392

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

396
        def add_parser(subparsers, *args, **kwargs):
8✔
397
            """
398
            argparse in python2 doesn't support the aliases option,
399
            so we just don't provide the aliases there.
400
            """
401
            if 'aliases' in kwargs and sys.version_info.major < 3:
8!
402
                kwargs.pop('aliases')
×
403
            return subparsers.add_parser(*args, **kwargs)
8✔
404

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

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

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

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

479
        parser_export_dist = add_parser(
8✔
480
            subparsers,
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',
8✔
485
                                        help='The output dir to copy to')
486
        parser_export_dist.add_argument(
8✔
487
            '--symlink',
488
            action='store_true',
489
            help='Symlink the dist instead of copying')
490

491
        parser_packaging = argparse.ArgumentParser(
8✔
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(
8✔
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(
8✔
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(
8✔
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
        parser_packaging.add_argument(
8✔
515
            '--use-setup-py', dest="use_setup_py",
516
            action='store_true', default=False,
517
            help="Process the setup.py of a project if present. " +
518
                 "(Experimental!")
519
        parser_packaging.add_argument(
8✔
520
            '--ignore-setup-py', dest="ignore_setup_py",
521
            action='store_true', default=False,
522
            help="Don't run the setup.py of a project if present. " +
523
                 "This may be required if the setup.py is not " +
524
                 "designed to work inside p4a (e.g. by installing " +
525
                 "dependencies that won't work or aren't desired " +
526
                 "on Android")
527
        parser_packaging.add_argument(
8✔
528
            '--release', dest='build_mode', action='store_const',
529
            const='release', default='debug',
530
            help='Build your app as a non-debug release build. '
531
                 '(Disables gdb debugging among other things)')
532
        parser_packaging.add_argument(
8✔
533
            '--with-debug-symbols', dest='with_debug_symbols',
534
            action='store_const', const=True, default=False,
535
            help='Will keep debug symbols from `.so` files.')
536
        parser_packaging.add_argument(
8✔
537
            '--keystore', dest='keystore', action='store', default=None,
538
            help=('Keystore for JAR signing key, will use jarsigner '
539
                  'default if not specified (release build only)'))
540
        parser_packaging.add_argument(
8✔
541
            '--signkey', dest='signkey', action='store', default=None,
542
            help='Key alias to sign PARSER_APK. with (release build only)')
543
        parser_packaging.add_argument(
8✔
544
            '--keystorepw', dest='keystorepw', action='store', default=None,
545
            help='Password for keystore')
546
        parser_packaging.add_argument(
8✔
547
            '--signkeypw', dest='signkeypw', action='store', default=None,
548
            help='Password for key alias')
549

550
        add_parser(
8✔
551
            subparsers,
552
            'aar', help='Build an AAR',
553
            parents=[parser_packaging])
554

555
        add_parser(
8✔
556
            subparsers,
557
            'apk', help='Build an APK',
558
            parents=[parser_packaging])
559

560
        add_parser(
8✔
561
            subparsers,
562
            'aab', help='Build an AAB',
563
            parents=[parser_packaging])
564

565
        add_parser(
8✔
566
            subparsers,
567
            'create', help='Compile a set of requirements into a dist',
568
            parents=[generic_parser])
569
        add_parser(
8✔
570
            subparsers,
571
            'archs', help='List the available target architectures',
572
            parents=[generic_parser])
573
        add_parser(
8✔
574
            subparsers,
575
            'distributions', aliases=['dists'],
576
            help='List the currently available (compiled) dists',
577
            parents=[generic_parser])
578
        add_parser(
8✔
579
            subparsers,
580
            'delete_dist', aliases=['delete-dist'], help='Delete a compiled dist',
581
            parents=[generic_parser])
582

583
        parser_sdk_tools = add_parser(
8✔
584
            subparsers,
585
            'sdk_tools', aliases=['sdk-tools'],
586
            help='Run the given binary from the SDK tools dis',
587
            parents=[generic_parser])
588
        parser_sdk_tools.add_argument(
8✔
589
            'tool', help='The binary tool name to run')
590

591
        add_parser(
8✔
592
            subparsers,
593
            'adb', help='Run adb from the given SDK',
594
            parents=[generic_parser])
595
        add_parser(
8✔
596
            subparsers,
597
            'logcat', help='Run logcat from the given SDK',
598
            parents=[generic_parser])
599
        add_parser(
8✔
600
            subparsers,
601
            'build_status', aliases=['build-status'],
602
            help='Print some debug information about current built components',
603
            parents=[generic_parser])
604

605
        parser.add_argument('-v', '--version', action='version',
8✔
606
                            version=__version__)
607

608
        args, unknown = parser.parse_known_args(sys.argv[1:])
8✔
609
        args.unknown_args = unknown
8✔
610

611
        if getattr(args, "private", None) is not None:
8!
612
            # Pass this value on to the internal bootstrap build.py:
613
            args.unknown_args += ["--private", args.private]
×
614
        if getattr(args, "build_mode", None) == "release":
8!
615
            args.unknown_args += ["--release"]
×
616
        if getattr(args, "with_debug_symbols", False):
8!
617
            args.unknown_args += ["--with-debug-symbols"]
×
618
        if getattr(args, "ignore_setup_py", False):
8!
619
            args.use_setup_py = False
×
620
        if getattr(args, "activity_class_name", "org.kivy.android.PythonActivity") != 'org.kivy.android.PythonActivity':
8✔
621
            args.unknown_args += ["--activity-class-name", args.activity_class_name]
8✔
622
        if getattr(args, "service_class_name", "org.kivy.android.PythonService") != 'org.kivy.android.PythonService':
8✔
623
            args.unknown_args += ["--service-class-name", args.service_class_name]
8✔
624

625
        self.args = args
8✔
626

627
        if args.subparser_name is None:
8✔
628
            parser.print_help()
8✔
629
            exit(1)
8✔
630

631
        setup_color(args.color)
8✔
632

633
        if args.debug:
8!
634
            logger.setLevel(logging.DEBUG)
×
635

636
        self.ctx = Context()
8✔
637
        self.ctx.use_setup_py = getattr(args, "use_setup_py", True)
8✔
638
        self.ctx.build_as_debuggable = getattr(
8✔
639
            args, "build_mode", "debug"
640
        ) == "debug"
641
        self.ctx.with_debug_symbols = getattr(
8✔
642
            args, "with_debug_symbols", False
643
        )
644

645
        # Process requirements and put version in environ
646
        if hasattr(args, 'requirements'):
8!
647
            requirements = []
8✔
648

649
            # Add dependencies from setup.py, but only if they are recipes
650
            # (because otherwise, setup.py itself will install them later)
651
            if (project_has_setup_py(getattr(args, "private", None)) and
8!
652
                    getattr(args, "use_setup_py", False)):
653
                try:
×
654
                    info("Analyzing package dependencies. MAY TAKE A WHILE.")
×
655
                    # Get all the dependencies corresponding to a recipe:
656
                    dependencies = [
×
657
                        dep.lower() for dep in
658
                        get_dep_names_of_package(
659
                            args.private,
660
                            keep_version_pins=True,
661
                            recursive=True,
662
                            verbose=True,
663
                        )
664
                    ]
665
                    info("Dependencies obtained: " + str(dependencies))
×
666
                    all_recipes = [
×
667
                        recipe.lower() for recipe in
668
                        set(Recipe.list_recipes(self.ctx))
669
                    ]
670
                    dependencies = set(dependencies).intersection(
×
671
                        set(all_recipes)
672
                    )
673
                    # Add dependencies to argument list:
674
                    if len(dependencies) > 0:
×
675
                        if len(args.requirements) > 0:
×
676
                            args.requirements += u","
×
677
                        args.requirements += u",".join(dependencies)
×
678
                except ValueError:
×
679
                    # Not a python package, apparently.
680
                    warning(
×
681
                        "Processing failed, is this project a valid "
682
                        "package? Will continue WITHOUT setup.py deps."
683
                    )
684

685
            # Parse --requirements argument list:
686
            for requirement in split_argument_list(args.requirements):
8✔
687
                if "==" in requirement:
8!
688
                    requirement, version = requirement.split(u"==", 1)
×
689
                    os.environ["VERSION_{}".format(requirement)] = version
×
690
                    info('Recipe {}: version "{}" requested'.format(
×
691
                        requirement, version))
692
                requirements.append(requirement)
8✔
693
            args.requirements = u",".join(requirements)
8✔
694

695
        self.warn_on_deprecated_args(args)
8✔
696

697
        self.storage_dir = args.storage_dir
8✔
698
        self.ctx.setup_dirs(self.storage_dir)
8✔
699
        self.sdk_dir = args.sdk_dir
8✔
700
        self.ndk_dir = args.ndk_dir
8✔
701
        self.android_api = args.android_api
8✔
702
        self.ndk_api = args.ndk_api
8✔
703
        self.ctx.symlink_bootstrap_files = args.symlink_bootstrap_files
8✔
704
        self.ctx.java_build_tool = args.java_build_tool
8✔
705

706
        self._archs = args.arch
8✔
707

708
        self.ctx.local_recipes = realpath(args.local_recipes)
8✔
709
        self.ctx.copy_libs = args.copy_libs
8✔
710

711
        self.ctx.activity_class_name = args.activity_class_name
8✔
712
        self.ctx.service_class_name = args.service_class_name
8✔
713

714
        self.ctx.extra_index_urls = args.extra_index_urls
8✔
715
        self.ctx.skip_prebuilt = args.skip_prebuilt
8✔
716
        self.ctx.use_prebuilt_version_for = args.use_prebuilt_version_for
8✔
717
        self.ctx.save_wheel_dir = args.save_wheel_dir
8✔
718

719
        # Each subparser corresponds to a method
720
        command = args.subparser_name.replace('-', '_')
8✔
721
        getattr(self, command)(args)
8✔
722

723
    @staticmethod
8✔
724
    def warn_on_carriage_return_args(args):
8✔
725
        for check_arg in args:
8✔
726
            if '\r' in check_arg:
8!
727
                warning("Argument '{}' contains a carriage return (\\r).".format(str(check_arg.replace('\r', ''))))
×
728
                warning("Invoking this program via scripts which use CRLF instead of LF line endings will have undefined behaviour.")
×
729

730
    def warn_on_deprecated_args(self, args):
8✔
731
        """
732
        Print warning messages for any deprecated arguments that were passed.
733
        """
734

735
        # Output warning if setup.py is present and neither --ignore-setup-py
736
        # nor --use-setup-py was specified.
737
        if project_has_setup_py(getattr(args, "private", None)):
8!
738
            if not getattr(args, "use_setup_py", False) and \
×
739
                    not getattr(args, "ignore_setup_py", False):
740
                warning("  **** FUTURE BEHAVIOR CHANGE WARNING ****")
×
741
                warning("Your project appears to contain a setup.py file.")
×
742
                warning("Currently, these are ignored by default.")
×
743
                warning("This will CHANGE in an upcoming version!")
×
744
                warning("")
×
745
                warning("To ensure your setup.py is ignored, please specify:")
×
746
                warning("    --ignore-setup-py")
×
747
                warning("")
×
748
                warning("To enable what will some day be the default, specify:")
×
749
                warning("    --use-setup-py")
×
750

751
        # NDK version is now determined automatically
752
        if args.ndk_version is not None:
8!
753
            warning('--ndk-version is deprecated and no longer necessary, '
×
754
                    'the value you passed is ignored')
755
        if 'ANDROIDNDKVER' in environ:
8!
756
            warning('$ANDROIDNDKVER is deprecated and no longer necessary, '
×
757
                    'the value you set is ignored')
758

759
    def hook(self, name):
8✔
760
        if not self.args.hook:
×
761
            return
×
762
        if not hasattr(self, "hook_module"):
×
763
            # first time, try to load the hook module
764
            self.hook_module = load_source(
×
765
                "pythonforandroid.hook", self.args.hook)
766
        if hasattr(self.hook_module, name):
×
767
            info("Hook: execute {}".format(name))
×
768
            getattr(self.hook_module, name)(self)
×
769
        else:
770
            info("Hook: ignore {}".format(name))
×
771

772
    @property
8✔
773
    def default_storage_dir(self):
8✔
774
        udd = user_data_dir('python-for-android')
×
775
        if ' ' in udd:
×
776
            udd = '~/.python-for-android'
×
777
        return udd
×
778

779
    @staticmethod
8✔
780
    def _read_configuration():
8✔
781
        # search for a .p4a configuration file in the current directory
782
        if not exists(".p4a"):
8!
783
            return
8✔
784
        info("Reading .p4a configuration")
×
785
        with open(".p4a") as fd:
×
786
            lines = fd.readlines()
×
787
        lines = [shlex.split(line)
×
788
                 for line in lines if not line.startswith("#")]
789
        for line in lines:
×
790
            for arg in line:
×
791
                sys.argv.append(arg)
×
792

793
    def recipes(self, args):
8✔
794
        """
795
        Prints recipes basic info, e.g.
796
        .. code-block:: bash
797

798
            python3      3.7.1
799
                depends: ['hostpython3', 'sqlite3', 'openssl', 'libffi']
800
                conflicts: []
801
                optional depends: ['sqlite3', 'libffi', 'openssl']
802
        """
803
        ctx = self.ctx
8✔
804
        if args.compact:
8!
805
            print(" ".join(set(Recipe.list_recipes(ctx))))
×
806
        else:
807
            for name in sorted(Recipe.list_recipes(ctx)):
8✔
808
                try:
8✔
809
                    recipe = Recipe.get_recipe(name, ctx)
8✔
810
                except (IOError, ValueError):
×
811
                    warning('Recipe "{}" could not be loaded'.format(name))
×
812
                except SyntaxError:
×
813
                    import traceback
×
814
                    traceback.print_exc()
×
815
                    warning(('Recipe "{}" could not be loaded due to a '
×
816
                             'syntax error').format(name))
817
                version = str(recipe.version)
8✔
818
                print('{Fore.BLUE}{Style.BRIGHT}{recipe.name:<12} '
8✔
819
                      '{Style.RESET_ALL}{Fore.LIGHTBLUE_EX}'
820
                      '{version:<8}{Style.RESET_ALL}'.format(
821
                            recipe=recipe, Fore=Out_Fore, Style=Out_Style,
822
                            version=version))
823
                print('    {Fore.GREEN}depends: {recipe.depends}'
8✔
824
                      '{Fore.RESET}'.format(recipe=recipe, Fore=Out_Fore))
825
                if recipe.conflicts:
8✔
826
                    print('    {Fore.RED}conflicts: {recipe.conflicts}'
8✔
827
                          '{Fore.RESET}'
828
                          .format(recipe=recipe, Fore=Out_Fore))
829
                if recipe.opt_depends:
8✔
830
                    print('    {Fore.YELLOW}optional depends: '
8✔
831
                          '{recipe.opt_depends}{Fore.RESET}'
832
                          .format(recipe=recipe, Fore=Out_Fore))
833

834
    def bootstraps(self, _args):
8✔
835
        """List all the bootstraps available to build with."""
836
        for bs in Bootstrap.all_bootstraps():
×
837
            bs = Bootstrap.get_bootstrap(bs, self.ctx)
×
838
            print('{Fore.BLUE}{Style.BRIGHT}{bs.name}{Style.RESET_ALL}'
×
839
                  .format(bs=bs, Fore=Out_Fore, Style=Out_Style))
840
            print('    {Fore.GREEN}depends: {bs.recipe_depends}{Fore.RESET}'
×
841
                  .format(bs=bs, Fore=Out_Fore))
842

843
    def clean(self, args):
8✔
844
        components = args.component
×
845

846
        component_clean_methods = {
×
847
            'all': self.clean_all,
848
            'dists': self.clean_dists,
849
            'distributions': self.clean_dists,
850
            'builds': self.clean_builds,
851
            'bootstrap_builds': self.clean_bootstrap_builds,
852
            'downloads': self.clean_download_cache}
853

854
        for component in components:
×
855
            if component not in component_clean_methods:
×
856
                raise BuildInterruptingException((
×
857
                    'Asked to clean "{}" but this argument is not '
858
                    'recognised'.format(component)))
859
            component_clean_methods[component](args)
×
860

861
    def clean_all(self, args):
8✔
862
        """Delete all build components; the package cache, package builds,
863
        bootstrap builds and distributions."""
864
        self.clean_dists(args)
×
865
        self.clean_builds(args)
×
866
        self.clean_download_cache(args)
×
867

868
    def clean_dists(self, _args):
8✔
869
        """Delete all compiled distributions in the internal distribution
870
        directory."""
871
        ctx = self.ctx
×
872
        rmdir(ctx.dist_dir)
×
873

874
    def clean_bootstrap_builds(self, _args):
8✔
875
        """Delete all the bootstrap builds."""
876
        rmdir(join(self.ctx.build_dir, 'bootstrap_builds'))
×
877
        # for bs in Bootstrap.all_bootstraps():
878
        #     bs = Bootstrap.get_bootstrap(bs, self.ctx)
879
        #     if bs.build_dir and exists(bs.build_dir):
880
        #         info('Cleaning build for {} bootstrap.'.format(bs.name))
881
        #         rmdir(bs.build_dir)
882

883
    def clean_builds(self, _args):
8✔
884
        """Delete all build caches for each recipe, python-install, java code
885
        and compiled libs collection.
886

887
        This does *not* delete the package download cache or the final
888
        distributions.  You can also use clean_recipe_build to delete the build
889
        of a specific recipe.
890
        """
891
        ctx = self.ctx
×
892
        rmdir(ctx.build_dir)
×
893
        rmdir(ctx.python_installs_dir)
×
894
        libs_dir = join(self.ctx.build_dir, 'libs_collections')
×
895
        rmdir(libs_dir)
×
896

897
    def clean_recipe_build(self, args):
8✔
898
        """Deletes the build files of the given recipe.
899

900
        This is intended for debug purposes. You may experience
901
        strange behaviour or problems with some recipes if their
902
        build has made unexpected state changes. If this happens, run
903
        clean_builds, or attempt to clean other recipes until things
904
        work again.
905
        """
906
        recipe = Recipe.get_recipe(args.recipe, self.ctx)
×
907
        info('Cleaning build for {} recipe.'.format(recipe.name))
×
908
        recipe.clean_build()
×
909
        if not args.no_clean_dists:
×
910
            self.clean_dists(args)
×
911

912
    def clean_download_cache(self, args):
8✔
913
        """ Deletes a download cache for recipes passed as arguments. If no
914
        argument is passed, it'll delete *all* downloaded caches. ::
915

916
            p4a clean_download_cache kivy,pyjnius
917

918
        This does *not* delete the build caches or final distributions.
919
        """
920
        ctx = self.ctx
×
921
        if hasattr(args, 'recipes') and args.recipes:
×
922
            for package in args.recipes:
×
923
                remove_path = join(ctx.packages_path, package)
×
924
                if exists(remove_path):
×
925
                    rmdir(remove_path)
×
926
                    info('Download cache removed for: "{}"'.format(package))
×
927
                else:
928
                    warning('No download cache found for "{}", skipping'.format(
×
929
                        package))
930
        else:
931
            if exists(ctx.packages_path):
×
932
                rmdir(ctx.packages_path)
×
933
                info('Download cache removed.')
×
934
            else:
935
                print('No cache found at "{}"'.format(ctx.packages_path))
×
936

937
    @require_prebuilt_dist
8✔
938
    def export_dist(self, args):
8✔
939
        """Copies a created dist to an output dir.
940

941
        This makes it easy to navigate to the dist to investigate it
942
        or call build.py, though you do not in general need to do this
943
        and can use the apk command instead.
944
        """
945
        ctx = self.ctx
×
946
        dist = dist_from_args(ctx, args)
×
947
        if dist.needs_build:
×
948
            raise BuildInterruptingException(
×
949
                'You asked to export a dist, but there is no dist '
950
                'with suitable recipes available. For now, you must '
951
                ' create one first with the create argument.')
952
        if args.symlink:
×
953
            shprint(sh.ln, '-s', dist.dist_dir, args.output_dir)
×
954
        else:
955
            shprint(sh.cp, '-r', dist.dist_dir, args.output_dir)
×
956

957
    @property
8✔
958
    def _dist(self):
8✔
959
        ctx = self.ctx
8✔
960
        dist = dist_from_args(ctx, self.args)
8✔
961
        ctx.distribution = dist
8✔
962
        return dist
8✔
963

964
    @staticmethod
8✔
965
    def _fix_args(args):
8✔
966
        """
967
        Manually fixing these arguments at the string stage is
968
        unsatisfactory and should probably be changed somehow, but
969
        we can't leave it until later as the build.py scripts assume
970
        they are in the current directory.
971
        works in-place
972
        :param args: parser args
973
        """
974

975
        fix_args = ('--dir', '--private', '--add-jar', '--add-source',
×
976
                    '--whitelist', '--blacklist', '--presplash', '--icon',
977
                    '--icon-bg', '--icon-fg')
978
        unknown_args = args.unknown_args
×
979

980
        for asset in args.assets:
×
981
            if ":" in asset:
×
982
                asset_src, asset_dest = asset.split(":")
×
983
            else:
984
                asset_src = asset_dest = asset
×
985
            # take abspath now, because build.py will be run in bootstrap dir
986
            unknown_args += ["--asset", os.path.abspath(asset_src)+":"+asset_dest]
×
987
        for resource in args.resources:
×
988
            if ":" in resource:
×
989
                resource_src, resource_dest = resource.split(":")
×
990
            else:
991
                resource_src = resource
×
992
                resource_dest = ""
×
993
            # take abspath now, because build.py will be run in bootstrap dir
994
            unknown_args += ["--resource", os.path.abspath(resource_src)+":"+resource_dest]
×
995
        for i, arg in enumerate(unknown_args):
×
996
            argx = arg.split('=')
×
997
            if argx[0] in fix_args:
×
998
                if len(argx) > 1:
×
999
                    unknown_args[i] = '='.join(
×
1000
                        (argx[0], realpath(expanduser(argx[1]))))
1001
                elif i + 1 < len(unknown_args):
×
1002
                    unknown_args[i+1] = realpath(expanduser(unknown_args[i+1]))
×
1003

1004
    @staticmethod
8✔
1005
    def _prepare_release_env(args):
8✔
1006
        """
1007
        prepares envitonment dict with the necessary flags for signing an apk
1008
        :param args: parser args
1009
        """
1010
        env = os.environ.copy()
×
1011
        if args.build_mode == 'release':
×
1012
            if args.keystore:
×
1013
                env['P4A_RELEASE_KEYSTORE'] = realpath(expanduser(args.keystore))
×
1014
            if args.signkey:
×
1015
                env['P4A_RELEASE_KEYALIAS'] = args.signkey
×
1016
            if args.keystorepw:
×
1017
                env['P4A_RELEASE_KEYSTORE_PASSWD'] = args.keystorepw
×
1018
            if args.signkeypw:
×
1019
                env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.signkeypw
×
1020
            elif args.keystorepw and 'P4A_RELEASE_KEYALIAS_PASSWD' not in env:
×
1021
                env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.keystorepw
×
1022

1023
        return env
×
1024

1025
    def _build_package(self, args, package_type):
8✔
1026
        """
1027
        Creates an android package using gradle
1028
        :param args: parser args
1029
        :param package_type: one of 'apk', 'aar', 'aab'
1030
        :return (gradle output, build_args)
1031
        """
1032
        ctx = self.ctx
×
1033
        dist = self._dist
×
1034
        bs = Bootstrap.get_bootstrap(args.bootstrap, ctx)
×
1035
        ctx.prepare_bootstrap(bs)
×
1036
        self._fix_args(args)
×
1037
        env = self._prepare_release_env(args)
×
1038

1039
        with current_directory(dist.dist_dir):
×
1040
            self.hook("before_apk_build")
×
1041
            os.environ["ANDROID_API"] = str(self.ctx.android_api)
×
1042
            build = load_source('build', join(dist.dist_dir, 'build.py'))
×
1043
            build_args = build.parse_args_and_make_package(
×
1044
                args.unknown_args
1045
            )
1046

1047
            self.hook("after_apk_build")
×
1048
            self.hook("before_apk_assemble")
×
1049
            build_tools_versions = os.listdir(join(ctx.sdk_dir,
×
1050
                                                   'build-tools'))
1051
            build_tools_version = max_build_tool_version(build_tools_versions)
×
1052
            info(('Detected highest available build tools '
×
1053
                  'version to be {}').format(build_tools_version))
1054

1055
            if Version(build_tools_version.replace(" ", "")) < Version('25.0'):
×
1056
                raise BuildInterruptingException(
×
1057
                    'build_tools >= 25 is required, but %s is installed' % build_tools_version)
1058
            if not exists("gradlew"):
×
1059
                raise BuildInterruptingException("gradlew file is missing")
×
1060

1061
            env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir
×
1062
            env["ANDROID_HOME"] = self.ctx.sdk_dir
×
1063

1064
            gradlew = sh.Command('./gradlew')
×
1065

1066
            if exists('/usr/bin/dos2unix'):
×
1067
                # .../dists/bdisttest_python3/gradlew
1068
                # .../build/bootstrap_builds/sdl2-python3/gradlew
1069
                # if docker on windows, gradle contains CRLF
1070
                output = shprint(
×
1071
                    sh.Command('dos2unix'), gradlew._path,
1072
                    _tail=20, _critical=True, _env=env
1073
                )
1074
            if args.build_mode == "debug":
×
1075
                if package_type == "aab":
×
1076
                    raise BuildInterruptingException(
×
1077
                        "aab is meant only for distribution and is not available in debug mode. "
1078
                        "Instead, you can use apk while building for debugging purposes."
1079
                    )
1080
                gradle_task = "assembleDebug"
×
1081
            elif args.build_mode == "release":
×
1082
                if package_type in ["apk", "aar"]:
×
1083
                    gradle_task = "assembleRelease"
×
1084
                elif package_type == "aab":
×
1085
                    gradle_task = "bundleRelease"
×
1086
            else:
1087
                raise BuildInterruptingException(
×
1088
                    "Unknown build mode {} for apk()".format(args.build_mode))
1089

1090
            # WARNING: We should make sure to clean the build directory before building.
1091
            # See PR: kivy/python-for-android#2705
1092
            output = shprint(gradlew, "clean", gradle_task, _tail=20,
×
1093
                             _critical=True, _env=env)
1094
        return output, build_args
×
1095

1096
    def _finish_package(self, args, output, build_args, package_type, output_dir):
8✔
1097
        """
1098
        Finishes the package after the gradle script run
1099
        :param args: the parser args
1100
        :param output: RunningCommand output
1101
        :param build_args: build args as returned by build.parse_args
1102
        :param package_type: one of 'apk', 'aar', 'aab'
1103
        :param output_dir: where to put the package file
1104
        """
1105

1106
        package_glob = "*-{}.%s" % package_type
×
1107
        package_add_version = True
×
1108

1109
        self.hook("after_apk_assemble")
×
1110

1111
        info_main('# Copying android package to current directory')
×
1112

1113
        package_re = re.compile(r'.*Package: (.*\.apk)$')
×
1114
        package_file = None
×
1115
        for line in reversed(output.splitlines()):
×
1116
            m = package_re.match(line)
×
1117
            if m:
×
1118
                package_file = m.groups()[0]
×
1119
                break
×
1120
        if not package_file:
×
1121
            info_main('# Android package filename not found in build output. Guessing...')
×
1122
            if args.build_mode == "release":
×
1123
                suffixes = ("release", "release-unsigned")
×
1124
            else:
1125
                suffixes = ("debug", )
×
1126
            for suffix in suffixes:
×
1127

1128
                package_files = glob.glob(join(output_dir, package_glob.format(suffix)))
×
1129
                if package_files:
×
1130
                    if len(package_files) > 1:
×
1131
                        info('More than one built APK found... guessing you '
×
1132
                             'just built {}'.format(package_files[-1]))
1133
                    package_file = package_files[-1]
×
1134
                    break
×
1135
            else:
1136
                raise BuildInterruptingException('Couldn\'t find the built APK')
×
1137

1138
        info_main('# Found android package file: {}'.format(package_file))
×
1139
        package_extension = f".{package_type}"
×
1140
        if package_add_version:
×
1141
            info('# Add version number to android package')
×
1142
            package_name = basename(package_file)[:-len(package_extension)]
×
1143
            package_file_dest = "{}-{}{}".format(
×
1144
                package_name, build_args.version, package_extension)
1145
            info('# Android package renamed to {}'.format(package_file_dest))
×
1146
            shprint(sh.cp, package_file, package_file_dest)
×
1147
        else:
1148
            shprint(sh.cp, package_file, './')
×
1149

1150
    @require_prebuilt_dist
8✔
1151
    def apk(self, args):
8✔
1152
        output, build_args = self._build_package(args, package_type='apk')
×
1153
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'apk', args.build_mode)
×
1154
        self._finish_package(args, output, build_args, 'apk', output_dir)
×
1155

1156
    @require_prebuilt_dist
8✔
1157
    def aar(self, args):
8✔
1158
        output, build_args = self._build_package(args, package_type='aar')
×
1159
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'aar')
×
1160
        self._finish_package(args, output, build_args, 'aar', output_dir)
×
1161

1162
    @require_prebuilt_dist
8✔
1163
    def aab(self, args):
8✔
1164
        output, build_args = self._build_package(args, package_type='aab')
×
1165
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'bundle', args.build_mode)
×
1166
        self._finish_package(args, output, build_args, 'aab', output_dir)
×
1167

1168
    @require_prebuilt_dist
8✔
1169
    def create(self, args):
8✔
1170
        """Create a distribution directory if it doesn't already exist, run
1171
        any recipes if necessary, and build the apk.
1172
        """
1173
        pass  # The decorator does everything
8✔
1174

1175
    def archs(self, _args):
8✔
1176
        """List the target architectures available to be built for."""
1177
        print('{Style.BRIGHT}Available target architectures are:'
×
1178
              '{Style.RESET_ALL}'.format(Style=Out_Style))
1179
        for arch in self.ctx.archs:
×
1180
            print('    {}'.format(arch.arch))
×
1181

1182
    def dists(self, args):
8✔
1183
        """The same as :meth:`distributions`."""
1184
        self.distributions(args)
×
1185

1186
    def distributions(self, _args):
8✔
1187
        """Lists all distributions currently available (i.e. that have already
1188
        been built)."""
1189
        ctx = self.ctx
×
1190
        dists = Distribution.get_distributions(ctx)
×
1191

1192
        if dists:
×
1193
            print('{Style.BRIGHT}Distributions currently installed are:'
×
1194
                  '{Style.RESET_ALL}'.format(Style=Out_Style))
1195
            pretty_log_dists(dists, print)
×
1196
        else:
1197
            print('{Style.BRIGHT}There are no dists currently built.'
×
1198
                  '{Style.RESET_ALL}'.format(Style=Out_Style))
1199

1200
    def delete_dist(self, _args):
8✔
1201
        dist = self._dist
×
1202
        if not dist.folder_exists():
×
1203
            info('No dist exists that matches your specifications, '
×
1204
                 'exiting without deleting.')
1205
            return
×
1206
        dist.delete()
×
1207

1208
    def sdk_tools(self, args):
8✔
1209
        """Runs the android binary from the detected SDK directory, passing
1210
        all arguments straight to it. This binary is used to install
1211
        e.g. platform-tools for different API level targets. This is
1212
        intended as a convenience function if android is not in your
1213
        $PATH.
1214
        """
1215
        ctx = self.ctx
×
1216
        ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir,
×
1217
                                      user_ndk_dir=self.ndk_dir,
1218
                                      user_android_api=self.android_api,
1219
                                      user_ndk_api=self.ndk_api)
1220
        android = sh.Command(join(ctx.sdk_dir, 'tools', args.tool))
×
1221
        output = android(
×
1222
            *args.unknown_args, _iter=True, _out_bufsize=1, _err_to_out=True)
1223
        for line in output:
×
1224
            sys.stdout.write(line)
×
1225
            sys.stdout.flush()
×
1226

1227
    def adb(self, args):
8✔
1228
        """Runs the adb binary from the detected SDK directory, passing all
1229
        arguments straight to it. This is intended as a convenience
1230
        function if adb is not in your $PATH.
1231
        """
1232
        self._adb(args.unknown_args)
×
1233

1234
    def logcat(self, args):
8✔
1235
        """Runs ``adb logcat`` using the adb binary from the detected SDK
1236
        directory. All extra args are passed as arguments to logcat."""
1237
        self._adb(['logcat'] + args.unknown_args)
×
1238

1239
    def _adb(self, commands):
8✔
1240
        """Call the adb executable from the SDK, passing the given commands as
1241
        arguments."""
1242
        ctx = self.ctx
×
1243
        ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir,
×
1244
                                      user_ndk_dir=self.ndk_dir,
1245
                                      user_android_api=self.android_api,
1246
                                      user_ndk_api=self.ndk_api)
1247
        if platform in ('win32', 'cygwin'):
×
1248
            adb = sh.Command(join(ctx.sdk_dir, 'platform-tools', 'adb.exe'))
×
1249
        else:
1250
            adb = sh.Command(join(ctx.sdk_dir, 'platform-tools', 'adb'))
×
1251
        info_notify('Starting adb...')
×
1252
        output = adb(*commands, _iter=True, _out_bufsize=1, _err_to_out=True)
×
1253
        for line in output:
×
1254
            sys.stdout.write(line)
×
1255
            sys.stdout.flush()
×
1256

1257
    def recommendations(self, args):
8✔
1258
        print_recommendations()
8✔
1259

1260
    def build_status(self, _args):
8✔
1261
        """Print the status of the specified build. """
1262
        print('{Style.BRIGHT}Bootstraps whose core components are probably '
×
1263
              'already built:{Style.RESET_ALL}'.format(Style=Out_Style))
1264

1265
        bootstrap_dir = join(self.ctx.build_dir, 'bootstrap_builds')
×
1266
        if exists(bootstrap_dir):
×
1267
            for filen in os.listdir(bootstrap_dir):
×
1268
                print('    {Fore.GREEN}{Style.BRIGHT}{filen}{Style.RESET_ALL}'
×
1269
                      .format(filen=filen, Fore=Out_Fore, Style=Out_Style))
1270

1271
        print('{Style.BRIGHT}Recipes that are probably already built:'
×
1272
              '{Style.RESET_ALL}'.format(Style=Out_Style))
1273
        other_builds_dir = join(self.ctx.build_dir, 'other_builds')
×
1274
        if exists(other_builds_dir):
×
1275
            for filen in sorted(os.listdir(other_builds_dir)):
×
1276
                name = filen.split('-')[0]
×
1277
                dependencies = filen.split('-')[1:]
×
1278
                recipe_str = ('    {Style.BRIGHT}{Fore.GREEN}{name}'
×
1279
                              '{Style.RESET_ALL}'.format(
1280
                                  Style=Out_Style, name=name, Fore=Out_Fore))
1281
                if dependencies:
×
1282
                    recipe_str += (
×
1283
                        ' ({Fore.BLUE}with ' + ', '.join(dependencies) +
1284
                        '{Fore.RESET})').format(Fore=Out_Fore)
1285
                recipe_str += '{Style.RESET_ALL}'.format(Style=Out_Style)
×
1286
                print(recipe_str)
×
1287

1288

1289
if __name__ == "__main__":
1290
    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