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

kivy / python-for-android / 38890138466

16 Oct 2026 10:52AM UTC coverage: 63.302% (+0.02%) from 63.278%
38890138466

Pull #3394

github

web-flow
Merge 7043b12fb into 8ef9d1dbb
Pull Request #3394: python3: honour --no-optimize-python when byte-compiling

1859 of 3196 branches covered (58.17%)

Branch coverage included in aggregate %.

7 of 8 new or added lines in 3 files covered. (87.5%)

5448 of 8347 relevant lines covered (65.27%)

3.92 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
"""
6 ✔
3
Tool for packaging Python apps for Android
4
==========================================
5

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

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

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

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

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

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

55

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

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

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

82

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

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

108

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

123

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

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

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

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

170
    ctx.bootstrap.assemble_distribution()
6 ✔
171

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

176

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

182

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

188
    This subclass alternative is follows the suggestion at
189
    https://bugs.python.org/issue14910.
190
    """
191

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

195

196
class ToolchainCL:
6 ✔
197

198
    def __init__(self):
6 ✔
199

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

399
        # Also an argument of the bootstrap build.py
400
        generic_parser.add_argument(
6 ✔
401
            '--no-optimize-python', dest='optimize_python',
402
            action='store_false', default=True,
403
            help=('Whether to compile to optimised .pyc files, using -OO '
404
                  '(strips docstrings and asserts)'))
405

406
        add_boolean_option(
6 ✔
407
            generic_parser, ['copy-libs'],
408
            default=False,
409
            description='Copy libraries instead of using biglink (Android 4.3+)'
410
        )
411

412
        self._read_configuration()
6 ✔
413

414
        subparsers = parser.add_subparsers(dest='subparser_name',
6 ✔
415
                                           help='The command to run')
416

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

454
        parser_clean = subparsers.add_parser(
6 ✔
455
            'clean',
456
            help='Delete build components.',
457
            parents=[generic_parser])
458
        parser_clean.add_argument(
6 ✔
459
            'component', nargs='+',
460
            help=('The build component(s) to delete. You can pass any '
461
                  'number of arguments from "all", "builds", "dists", '
462
                  '"distributions", "bootstrap_builds", "downloads".'))
463

464
        parser_clean_recipe_build = subparsers.add_parser(
6 ✔
465
            'clean_recipe_build', aliases=['clean-recipe-build'],
466
            help=('Delete the build components of the given recipe. '
467
                  'By default this will also delete built dists'),
468
            parents=[generic_parser])
469
        parser_clean_recipe_build.add_argument(
6 ✔
470
            'recipe', help='The recipe name')
471
        parser_clean_recipe_build.add_argument(
6 ✔
472
            '--no-clean-dists', default=False,
473
            dest='no_clean_dists',
474
            action='store_true',
475
            help='If passed, do not delete existing dists')
476

477
        parser_clean_download_cache = subparsers.add_parser(
6 ✔
478
            'clean_download_cache', aliases=['clean-download-cache'],
479
            help='Delete cached downloads for requirement builds',
480
            parents=[generic_parser])
481
        parser_clean_download_cache.add_argument(
6 ✔
482
            'recipes',
483
            nargs='*',
484
            help='The recipes to clean (space-separated). If no recipe name is'
485
                  ' provided, the entire cache is cleared.')
486

487
        parser_export_dist = subparsers.add_parser(
6 ✔
488
            'export_dist', aliases=['export-dist'],
489
            help='Copy the named dist to the given path',
490
            parents=[generic_parser])
491
        parser_export_dist.add_argument('output_dir',
6 ✔
492
                                        help='The output dir to copy to')
493
        parser_export_dist.add_argument(
6 ✔
494
            '--symlink',
495
            action='store_true',
496
            help='Symlink the dist instead of copying')
497

498
        parser_packaging = argparse.ArgumentParser(
6 ✔
499
            parents=[generic_parser],
500
            add_help=False,
501
            description='common options for packaging (apk, aar)')
502

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

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

545
        subparsers.add_parser(
6 ✔
546
            'aar', help='Build an AAR',
547
            parents=[parser_packaging])
548

549
        subparsers.add_parser(
6 ✔
550
            'apk', help='Build an APK',
551
            parents=[parser_packaging])
552

553
        subparsers.add_parser(
6 ✔
554
            'aab', help='Build an AAB',
555
            parents=[parser_packaging])
556

557
        subparsers.add_parser(
6 ✔
558
            'create', help='Compile a set of requirements into a dist',
559
            parents=[generic_parser])
560
        subparsers.add_parser(
6 ✔
561
            'archs', help='List the available target architectures',
562
            parents=[generic_parser])
563
        subparsers.add_parser(
6 ✔
564
            'distributions', aliases=['dists'],
565
            help='List the currently available (compiled) dists',
566
            parents=[generic_parser])
567
        subparsers.add_parser(
6 ✔
568
            'delete_dist', aliases=['delete-dist'], help='Delete a compiled dist',
569
            parents=[generic_parser])
570

571
        parser_sdk_tools = subparsers.add_parser(
6 ✔
572
            'sdk_tools', aliases=['sdk-tools'],
573
            help='Run the given binary from the SDK tools dis',
574
            parents=[generic_parser])
575
        parser_sdk_tools.add_argument(
6 ✔
576
            'tool', help='The binary tool name to run')
577

578
        subparsers.add_parser(
6 ✔
579
            'adb', help='Run adb from the given SDK',
580
            parents=[generic_parser])
581
        subparsers.add_parser(
6 ✔
582
            'logcat', help='Run logcat from the given SDK',
583
            parents=[generic_parser])
584
        subparsers.add_parser(
6 ✔
585
            'build_status', aliases=['build-status'],
586
            help='Print some debug information about current built components',
587
            parents=[generic_parser])
588

589
        parser.add_argument('-v', '--version', action='version',
6 ✔
590
                            version=__version__)
591

592
        args, unknown = parser.parse_known_args(sys.argv[1:])
6 ✔
593
        args.unknown_args = unknown
6 ✔
594

595
        if getattr(args, "private", None) is not None:
6 !
596
            # Pass this value on to the internal bootstrap build.py:
597
            args.unknown_args += ["--private", args.private]
×
598
        if getattr(args, "build_mode", None) == "release":
6 !
599
            args.unknown_args += ["--release"]
×
600
        if getattr(args, "with_debug_symbols", False):
6 !
601
            args.unknown_args += ["--with-debug-symbols"]
×
602
        if not getattr(args, "optimize_python", True):
6 !
NEW
603
            args.unknown_args += ["--no-optimize-python"]
×
604
        if getattr(args, "ignore_setup_py", False):
6 !
605
            args.use_setup_py = False
×
606
        if getattr(args, "activity_class_name", "org.kivy.android.PythonActivity") != 'org.kivy.android.PythonActivity':
6 ✔
607
            args.unknown_args += ["--activity-class-name", args.activity_class_name]
6 ✔
608
        if getattr(args, "service_class_name", "org.kivy.android.PythonService") != 'org.kivy.android.PythonService':
6 ✔
609
            args.unknown_args += ["--service-class-name", args.service_class_name]
6 ✔
610

611
        self.args = args
6 ✔
612

613
        if args.subparser_name is None:
6 ✔
614
            parser.print_help()
6 ✔
615
            exit(1)
6 ✔
616

617
        setup_color(args.color)
6 ✔
618

619
        if args.debug:
6 !
620
            logger.setLevel(logging.DEBUG)
×
621

622
        self.ctx = Context()
6 ✔
623
        self.ctx.use_setup_py = getattr(args, "use_setup_py", True)
6 ✔
624
        self.ctx.build_as_debuggable = getattr(
6 ✔
625
            args, "build_mode", "debug"
626
        ) == "debug"
627
        self.ctx.with_debug_symbols = getattr(
6 ✔
628
            args, "with_debug_symbols", False
629
        )
630
        self.ctx.optimize_python = getattr(args, "optimize_python", True)
6 ✔
631

632
        # Process requirements and put version in environ
633
        if hasattr(args, 'requirements'):
6 !
634
            requirements = []
6 ✔
635

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

672
            # Parse --requirements argument list:
673
            for requirement in split_argument_list(args.requirements):
6 ✔
674
                if "==" in requirement:
6 !
675
                    requirement, version = requirement.split(u"==", 1)
×
676
                    os.environ["VERSION_{}".format(requirement)] = version
×
677
                    info('Recipe {}: version "{}" requested'.format(
×
678
                        requirement, version))
679
                requirements.append(requirement)
6 ✔
680
            args.requirements = u",".join(requirements)
6 ✔
681

682
        self.warn_on_deprecated_args(args)
6 ✔
683

684
        self.storage_dir = args.storage_dir
6 ✔
685
        self.ctx.setup_dirs(self.storage_dir)
6 ✔
686
        self.sdk_dir = args.sdk_dir
6 ✔
687
        self.ndk_dir = args.ndk_dir
6 ✔
688
        self.android_api = args.android_api
6 ✔
689
        self.ndk_api = args.ndk_api
6 ✔
690
        self.ctx.symlink_bootstrap_files = args.symlink_bootstrap_files
6 ✔
691
        self.ctx.java_build_tool = args.java_build_tool
6 ✔
692

693
        self._archs = args.arch
6 ✔
694

695
        self.ctx.local_recipes = realpath(args.local_recipes)
6 ✔
696
        self.ctx.copy_libs = args.copy_libs
6 ✔
697

698
        self.ctx.activity_class_name = args.activity_class_name
6 ✔
699
        self.ctx.service_class_name = args.service_class_name
6 ✔
700

701
        self.ctx.extra_index_urls = args.extra_index_urls
6 ✔
702
        self.ctx.skip_prebuilt = args.skip_prebuilt
6 ✔
703
        self.ctx.use_prebuilt_version_for = args.use_prebuilt_version_for
6 ✔
704
        self.ctx.save_wheel_dir = args.save_wheel_dir
6 ✔
705

706
        # Each subparser corresponds to a method
707
        command = args.subparser_name.replace('-', '_')
6 ✔
708
        getattr(self, command)(args)
6 ✔
709

710
    @staticmethod
6 ✔
711
    def warn_on_carriage_return_args(args):
6 ✔
712
        for check_arg in args:
6 ✔
713
            if '\r' in check_arg:
6 !
714
                warning("Argument '{}' contains a carriage return (\\r).".format(str(check_arg.replace('\r', ''))))
×
715
                warning("Invoking this program via scripts which use CRLF instead of LF line endings will have undefined behaviour.")
×
716

717
    def warn_on_deprecated_args(self, args):
6 ✔
718
        """
719
        Print warning messages for any deprecated arguments that were passed.
720
        """
721

722
        # Output warning if setup.py is present and neither --ignore-setup-py
723
        # nor --use-setup-py was specified.
724
        if project_has_setup_py(getattr(args, "private", None)):
6 !
725
            if not getattr(args, "use_setup_py", False) and \
×
726
                    not getattr(args, "ignore_setup_py", False):
727
                warning("  **** FUTURE BEHAVIOR CHANGE WARNING ****")
×
728
                warning("Your project appears to contain a setup.py file.")
×
729
                warning("Currently, these are ignored by default.")
×
730
                warning("This will CHANGE in an upcoming version!")
×
731
                warning("")
×
732
                warning("To ensure your setup.py is ignored, please specify:")
×
733
                warning("    --ignore-setup-py")
×
734
                warning("")
×
735
                warning("To enable what will some day be the default, specify:")
×
736
                warning("    --use-setup-py")
×
737

738
        # NDK version is now determined automatically
739
        if args.ndk_version is not None:
6 !
740
            warning('--ndk-version is deprecated and no longer necessary, '
×
741
                    'the value you passed is ignored')
742
        if 'ANDROIDNDKVER' in environ:
6 !
743
            warning('$ANDROIDNDKVER is deprecated and no longer necessary, '
×
744
                    'the value you set is ignored')
745

746
    def hook(self, name):
6 ✔
747
        if not self.args.hook:
×
748
            return
×
749
        if not hasattr(self, "hook_module"):
×
750
            # first time, try to load the hook module
751
            self.hook_module = load_source(
×
752
                "pythonforandroid.hook", self.args.hook)
753
        if hasattr(self.hook_module, name):
×
754
            info("Hook: execute {}".format(name))
×
755
            getattr(self.hook_module, name)(self)
×
756
        else:
757
            info("Hook: ignore {}".format(name))
×
758

759
    @property
6 ✔
760
    def default_storage_dir(self):
6 ✔
761
        udd = user_data_dir('python-for-android')
×
762
        if ' ' in udd:
×
763
            udd = '~/.python-for-android'
×
764
        return udd
×
765

766
    @staticmethod
6 ✔
767
    def _read_configuration():
6 ✔
768
        # search for a .p4a configuration file in the current directory
769
        if not exists(".p4a"):
6 !
770
            return
6 ✔
771
        info("Reading .p4a configuration")
×
772
        with open(".p4a") as fd:
×
773
            lines = fd.readlines()
×
774
        lines = [shlex.split(line)
×
775
                 for line in lines if not line.startswith("#")]
776
        for line in lines:
×
777
            for arg in line:
×
778
                sys.argv.append(arg)
×
779

780
    def recipes(self, args):
6 ✔
781
        """
782
        Prints recipes basic info, e.g.
783
        .. code-block:: bash
784

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

821
    def bootstraps(self, _args):
6 ✔
822
        """List all the bootstraps available to build with."""
823
        for bs in Bootstrap.all_bootstraps():
×
824
            bs = Bootstrap.get_bootstrap(bs, self.ctx)
×
825
            print('{Fore.BLUE}{Style.BRIGHT}{bs.name}{Style.RESET_ALL}'
×
826
                  .format(bs=bs, Fore=Out_Fore, Style=Out_Style))
827
            print('    {Fore.GREEN}depends: {bs.recipe_depends}{Fore.RESET}'
×
828
                  .format(bs=bs, Fore=Out_Fore))
829

830
    def clean(self, args):
6 ✔
831
        components = args.component
×
832

833
        component_clean_methods = {
×
834
            'all': self.clean_all,
835
            'dists': self.clean_dists,
836
            'distributions': self.clean_dists,
837
            'builds': self.clean_builds,
838
            'bootstrap_builds': self.clean_bootstrap_builds,
839
            'downloads': self.clean_download_cache}
840

841
        for component in components:
×
842
            if component not in component_clean_methods:
×
843
                raise BuildInterruptingException((
×
844
                    'Asked to clean "{}" but this argument is not '
845
                    'recognised'.format(component)))
846
            component_clean_methods[component](args)
×
847

848
    def clean_all(self, args):
6 ✔
849
        """Delete all build components; the package cache, package builds,
850
        bootstrap builds and distributions."""
851
        self.clean_dists(args)
×
852
        self.clean_builds(args)
×
853
        self.clean_download_cache(args)
×
854

855
    def clean_dists(self, _args):
6 ✔
856
        """Delete all compiled distributions in the internal distribution
857
        directory."""
858
        ctx = self.ctx
×
859
        rmdir(ctx.dist_dir)
×
860

861
    def clean_bootstrap_builds(self, _args):
6 ✔
862
        """Delete all the bootstrap builds."""
863
        rmdir(join(self.ctx.build_dir, 'bootstrap_builds'))
×
864
        # for bs in Bootstrap.all_bootstraps():
865
        #     bs = Bootstrap.get_bootstrap(bs, self.ctx)
866
        #     if bs.build_dir and exists(bs.build_dir):
867
        #         info('Cleaning build for {} bootstrap.'.format(bs.name))
868
        #         rmdir(bs.build_dir)
869

870
    def clean_builds(self, _args):
6 ✔
871
        """Delete all build caches for each recipe, python-install, java code
872
        and compiled libs collection.
873

874
        This does *not* delete the package download cache or the final
875
        distributions.  You can also use clean_recipe_build to delete the build
876
        of a specific recipe.
877
        """
878
        ctx = self.ctx
×
879
        rmdir(ctx.build_dir)
×
880
        rmdir(ctx.python_installs_dir)
×
881
        libs_dir = join(self.ctx.build_dir, 'libs_collections')
×
882
        rmdir(libs_dir)
×
883

884
    def clean_recipe_build(self, args):
6 ✔
885
        """Deletes the build files of the given recipe.
886

887
        This is intended for debug purposes. You may experience
888
        strange behaviour or problems with some recipes if their
889
        build has made unexpected state changes. If this happens, run
890
        clean_builds, or attempt to clean other recipes until things
891
        work again.
892
        """
893
        recipe = Recipe.get_recipe(args.recipe, self.ctx)
×
894
        info('Cleaning build for {} recipe.'.format(recipe.name))
×
895
        recipe.clean_build()
×
896
        if not args.no_clean_dists:
×
897
            self.clean_dists(args)
×
898

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

903
            p4a clean_download_cache kivy,pyjnius
904

905
        This does *not* delete the build caches or final distributions.
906
        """
907
        ctx = self.ctx
×
908
        if hasattr(args, 'recipes') and args.recipes:
×
909
            for package in args.recipes:
×
910
                remove_path = join(ctx.packages_path, package)
×
911
                if exists(remove_path):
×
912
                    rmdir(remove_path)
×
913
                    info('Download cache removed for: "{}"'.format(package))
×
914
                else:
915
                    warning('No download cache found for "{}", skipping'.format(
×
916
                        package))
917
        else:
918
            if exists(ctx.packages_path):
×
919
                rmdir(ctx.packages_path)
×
920
                info('Download cache removed.')
×
921
            else:
922
                print('No cache found at "{}"'.format(ctx.packages_path))
×
923

924
    @require_prebuilt_dist
6 ✔
925
    def export_dist(self, args):
6 ✔
926
        """Copies a created dist to an output dir.
927

928
        This makes it easy to navigate to the dist to investigate it
929
        or call build.py, though you do not in general need to do this
930
        and can use the apk command instead.
931
        """
932
        ctx = self.ctx
×
933
        dist = dist_from_args(ctx, args)
×
934
        if dist.needs_build:
×
935
            raise BuildInterruptingException(
×
936
                'You asked to export a dist, but there is no dist '
937
                'with suitable recipes available. For now, you must '
938
                ' create one first with the create argument.')
939
        if args.symlink:
×
940
            shprint(sh.ln, '-s', dist.dist_dir, args.output_dir)
×
941
        else:
942
            shprint(sh.cp, '-r', dist.dist_dir, args.output_dir)
×
943

944
    @property
6 ✔
945
    def _dist(self):
6 ✔
946
        ctx = self.ctx
6 ✔
947
        dist = dist_from_args(ctx, self.args)
6 ✔
948
        ctx.distribution = dist
6 ✔
949
        return dist
6 ✔
950

951
    @staticmethod
6 ✔
952
    def _fix_args(args):
6 ✔
953
        """
954
        Manually fixing these arguments at the string stage is
955
        unsatisfactory and should probably be changed somehow, but
956
        we can't leave it until later as the build.py scripts assume
957
        they are in the current directory.
958
        works in-place
959
        :param args: parser args
960
        """
961

962
        fix_args = ('--dir', '--private', '--add-jar', '--add-source',
×
963
                    '--whitelist', '--blacklist', '--presplash', '--icon',
964
                    '--icon-bg', '--icon-fg')
965
        unknown_args = args.unknown_args
×
966

967
        for asset in args.assets:
×
968
            if ":" in asset:
×
969
                asset_src, asset_dest = asset.split(":")
×
970
            else:
971
                asset_src = asset_dest = asset
×
972
            # take abspath now, because build.py will be run in bootstrap dir
973
            unknown_args += ["--asset", os.path.abspath(asset_src)+":"+asset_dest]
×
974
        for resource in args.resources:
×
975
            if ":" in resource:
×
976
                resource_src, resource_dest = resource.split(":")
×
977
            else:
978
                resource_src = resource
×
979
                resource_dest = ""
×
980
            # take abspath now, because build.py will be run in bootstrap dir
981
            unknown_args += ["--resource", os.path.abspath(resource_src)+":"+resource_dest]
×
982
        for i, arg in enumerate(unknown_args):
×
983
            argx = arg.split('=')
×
984
            if argx[0] in fix_args:
×
985
                if len(argx) > 1:
×
986
                    unknown_args[i] = '='.join(
×
987
                        (argx[0], realpath(expanduser(argx[1]))))
988
                elif i + 1 < len(unknown_args):
×
989
                    unknown_args[i+1] = realpath(expanduser(unknown_args[i+1]))
×
990

991
    @staticmethod
6 ✔
992
    def _prepare_release_env(args):
6 ✔
993
        """
994
        prepares envitonment dict with the necessary flags for signing an apk
995
        :param args: parser args
996
        """
997
        env = os.environ.copy()
×
998
        if args.build_mode == 'release':
×
999
            if args.keystore:
×
1000
                env['P4A_RELEASE_KEYSTORE'] = realpath(expanduser(args.keystore))
×
1001
            if args.signkey:
×
1002
                env['P4A_RELEASE_KEYALIAS'] = args.signkey
×
1003
            if args.keystorepw:
×
1004
                env['P4A_RELEASE_KEYSTORE_PASSWD'] = args.keystorepw
×
1005
            if args.signkeypw:
×
1006
                env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.signkeypw
×
1007
            elif args.keystorepw and 'P4A_RELEASE_KEYALIAS_PASSWD' not in env:
×
1008
                env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.keystorepw
×
1009

1010
        return env
×
1011

1012
    def _build_package(self, args, package_type):
6 ✔
1013
        """
1014
        Creates an android package using gradle
1015
        :param args: parser args
1016
        :param package_type: one of 'apk', 'aar', 'aab'
1017
        :return (gradle output, build_args)
1018
        """
1019
        ctx = self.ctx
×
1020
        dist = self._dist
×
1021
        bs = Bootstrap.get_bootstrap(args.bootstrap, ctx)
×
1022
        ctx.prepare_bootstrap(bs)
×
1023
        self._fix_args(args)
×
1024
        env = self._prepare_release_env(args)
×
1025

1026
        with current_directory(dist.dist_dir):
×
1027
            self.hook("before_apk_build")
×
1028
            os.environ["ANDROID_API"] = str(self.ctx.android_api)
×
1029
            build = load_source('build', join(dist.dist_dir, 'build.py'))
×
1030
            build_args = build.parse_args_and_make_package(
×
1031
                args.unknown_args
1032
            )
1033

1034
            self.hook("after_apk_build")
×
1035
            self.hook("before_apk_assemble")
×
1036
            build_tools_versions = os.listdir(join(ctx.sdk_dir,
×
1037
                                                   'build-tools'))
1038
            build_tools_version = max_build_tool_version(build_tools_versions)
×
1039
            info(('Detected highest available build tools '
×
1040
                  'version to be {}').format(build_tools_version))
1041

1042
            if Version(build_tools_version.replace(" ", "")) < Version('25.0'):
×
1043
                raise BuildInterruptingException(
×
1044
                    'build_tools >= 25 is required, but %s is installed' % build_tools_version)
1045
            if not exists("gradlew"):
×
1046
                raise BuildInterruptingException("gradlew file is missing")
×
1047

1048
            env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir
×
1049
            env["ANDROID_HOME"] = self.ctx.sdk_dir
×
1050

1051
            gradlew = sh.Command('./gradlew')
×
1052

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

1077
            # WARNING: We should make sure to clean the build directory before building.
1078
            # See PR: kivy/python-for-android#2705
1079
            output = shprint(gradlew, "clean", gradle_task, _tail=20,
×
1080
                             _critical=True, _env=env)
1081
        return output, build_args
×
1082

1083
    def _finish_package(self, args, output, build_args, package_type, output_dir):
6 ✔
1084
        """
1085
        Finishes the package after the gradle script run
1086
        :param args: the parser args
1087
        :param output: RunningCommand output
1088
        :param build_args: build args as returned by build.parse_args
1089
        :param package_type: one of 'apk', 'aar', 'aab'
1090
        :param output_dir: where to put the package file
1091
        """
1092

1093
        package_glob = "*-{}.%s" % package_type
×
1094
        package_add_version = True
×
1095

1096
        self.hook("after_apk_assemble")
×
1097

1098
        info_main('# Copying android package to current directory')
×
1099

1100
        package_re = re.compile(r'.*Package: (.*\.apk)$')
×
1101
        package_file = None
×
1102
        for line in reversed(output.splitlines()):
×
1103
            m = package_re.match(line)
×
1104
            if m:
×
1105
                package_file = m.groups()[0]
×
1106
                break
×
1107
        if not package_file:
×
1108
            info_main('# Android package filename not found in build output. Guessing...')
×
1109
            if args.build_mode == "release":
×
1110
                suffixes = ("release", "release-unsigned")
×
1111
            else:
1112
                suffixes = ("debug", )
×
1113
            for suffix in suffixes:
×
1114

1115
                package_files = glob.glob(join(output_dir, package_glob.format(suffix)))
×
1116
                if package_files:
×
1117
                    if len(package_files) > 1:
×
1118
                        info('More than one built APK found... guessing you '
×
1119
                             'just built {}'.format(package_files[-1]))
1120
                    package_file = package_files[-1]
×
1121
                    break
×
1122
            else:
1123
                raise BuildInterruptingException('Couldn\'t find the built APK')
×
1124

1125
        info_main('# Found android package file: {}'.format(package_file))
×
1126
        package_extension = f".{package_type}"
×
1127
        if package_add_version:
×
1128
            info('# Add version number to android package')
×
1129
            package_name = basename(package_file)[:-len(package_extension)]
×
1130
            package_file_dest = "{}-{}{}".format(
×
1131
                package_name, build_args.version, package_extension)
1132
            info('# Android package renamed to {}'.format(package_file_dest))
×
1133
            shprint(sh.cp, package_file, package_file_dest)
×
1134
        else:
1135
            shprint(sh.cp, package_file, './')
×
1136

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

1143
    @require_prebuilt_dist
6 ✔
1144
    def aar(self, args):
6 ✔
1145
        output, build_args = self._build_package(args, package_type='aar')
×
1146
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'aar')
×
1147
        self._finish_package(args, output, build_args, 'aar', output_dir)
×
1148

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

1155
    @require_prebuilt_dist
6 ✔
1156
    def create(self, args):
6 ✔
1157
        """Create a distribution directory if it doesn't already exist, run
1158
        any recipes if necessary, and build the apk.
1159
        """
1160
        pass  # The decorator does everything
6 ✔
1161

1162
    def archs(self, _args):
6 ✔
1163
        """List the target architectures available to be built for."""
1164
        print('{Style.BRIGHT}Available target architectures are:'
×
1165
              '{Style.RESET_ALL}'.format(Style=Out_Style))
1166
        for arch in self.ctx.archs:
×
1167
            print('    {}'.format(arch.arch))
×
1168

1169
    def dists(self, args):
6 ✔
1170
        """The same as :meth:`distributions`."""
1171
        self.distributions(args)
×
1172

1173
    def distributions(self, _args):
6 ✔
1174
        """Lists all distributions currently available (i.e. that have already
1175
        been built)."""
1176
        ctx = self.ctx
×
1177
        dists = Distribution.get_distributions(ctx)
×
1178

1179
        if dists:
×
1180
            print('{Style.BRIGHT}Distributions currently installed are:'
×
1181
                  '{Style.RESET_ALL}'.format(Style=Out_Style))
1182
            pretty_log_dists(dists, print)
×
1183
        else:
1184
            print('{Style.BRIGHT}There are no dists currently built.'
×
1185
                  '{Style.RESET_ALL}'.format(Style=Out_Style))
1186

1187
    def delete_dist(self, _args):
6 ✔
1188
        dist = self._dist
×
1189
        if not dist.folder_exists():
×
1190
            info('No dist exists that matches your specifications, '
×
1191
                 'exiting without deleting.')
1192
            return
×
1193
        dist.delete()
×
1194

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

1214
    def adb(self, args):
6 ✔
1215
        """Runs the adb binary from the detected SDK directory, passing all
1216
        arguments straight to it. This is intended as a convenience
1217
        function if adb is not in your $PATH.
1218
        """
1219
        self._adb(args.unknown_args)
×
1220

1221
    def logcat(self, args):
6 ✔
1222
        """Runs ``adb logcat`` using the adb binary from the detected SDK
1223
        directory. All extra args are passed as arguments to logcat."""
1224
        self._adb(['logcat'] + args.unknown_args)
×
1225

1226
    def _adb(self, commands):
6 ✔
1227
        """Call the adb executable from the SDK, passing the given commands as
1228
        arguments."""
1229
        ctx = self.ctx
×
1230
        ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir,
×
1231
                                      user_ndk_dir=self.ndk_dir,
1232
                                      user_android_api=self.android_api,
1233
                                      user_ndk_api=self.ndk_api)
1234
        if platform in ('win32', 'cygwin'):
×
1235
            adb = sh.Command(join(ctx.sdk_dir, 'platform-tools', 'adb.exe'))
×
1236
        else:
1237
            adb = sh.Command(join(ctx.sdk_dir, 'platform-tools', 'adb'))
×
1238
        info_notify('Starting adb...')
×
1239
        output = adb(*commands, _iter=True, _out_bufsize=1, _err_to_out=True)
×
1240
        for line in output:
×
1241
            sys.stdout.write(line)
×
1242
            sys.stdout.flush()
×
1243

1244
    def recommendations(self, args):
6 ✔
1245
        print_recommendations()
6 ✔
1246

1247
    def build_status(self, _args):
6 ✔
1248
        """Print the status of the specified build. """
1249
        print('{Style.BRIGHT}Bootstraps whose core components are probably '
×
1250
              'already built:{Style.RESET_ALL}'.format(Style=Out_Style))
1251

1252
        bootstrap_dir = join(self.ctx.build_dir, 'bootstrap_builds')
×
1253
        if exists(bootstrap_dir):
×
1254
            for filen in os.listdir(bootstrap_dir):
×
1255
                print('    {Fore.GREEN}{Style.BRIGHT}{filen}{Style.RESET_ALL}'
×
1256
                      .format(filen=filen, Fore=Out_Fore, Style=Out_Style))
1257

1258
        print('{Style.BRIGHT}Recipes that are probably already built:'
×
1259
              '{Style.RESET_ALL}'.format(Style=Out_Style))
1260
        other_builds_dir = join(self.ctx.build_dir, 'other_builds')
×
1261
        if exists(other_builds_dir):
×
1262
            for filen in sorted(os.listdir(other_builds_dir)):
×
1263
                name = filen.split('-')[0]
×
1264
                dependencies = filen.split('-')[1:]
×
1265
                recipe_str = ('    {Style.BRIGHT}{Fore.GREEN}{name}'
×
1266
                              '{Style.RESET_ALL}'.format(
1267
                                  Style=Out_Style, name=name, Fore=Out_Fore))
1268
                if dependencies:
×
1269
                    recipe_str += (
×
1270
                        ' ({Fore.BLUE}with ' + ', '.join(dependencies) +
1271
                        '{Fore.RESET})').format(Fore=Out_Fore)
1272
                recipe_str += '{Style.RESET_ALL}'.format(Style=Out_Style)
×
1273
                print(recipe_str)
×
1274

1275

1276
if __name__ == "__main__":
1277
    main()
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc