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

kivy / python-for-android / 3770697291

pending completion
3770697291

push

github

GitHub
Merge pull request #2718 from kivy/release-2022.12.20

877 of 2011 branches covered (43.61%)

Branch coverage included in aggregate %.

38 of 86 new or added lines in 16 files covered. (44.19%)

10 existing lines in 4 files now uncovered.

4515 of 6886 relevant lines covered (65.57%)

2.59 hits per line

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

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

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

9
from os import environ
4✔
10
from pythonforandroid import __version__
4✔
11
from pythonforandroid.pythonpackage import get_dep_names_of_package
4✔
12
from pythonforandroid.recommendations import (
4✔
13
    RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API, print_recommendations)
14
from pythonforandroid.util import BuildInterruptingException, load_source
4✔
15
from pythonforandroid.entrypoints import main
4✔
16
from pythonforandroid.prerequisites import check_and_install_default_prerequisites
4✔
17

18

19
def check_python_dependencies():
4✔
20
    # Check if the Python requirements are installed. This appears
21
    # before the imports because otherwise they're imported elsewhere.
22

23
    # Using the ok check instead of failing immediately so that all
24
    # errors are printed at once
25

26
    from distutils.version import LooseVersion
4✔
27
    from importlib import import_module
4✔
28
    import sys
4✔
29

30
    ok = True
4✔
31

32
    modules = [('colorama', '0.3.3'), 'appdirs', ('sh', '1.10'), 'jinja2']
4✔
33

34
    for module in modules:
4✔
35
        if isinstance(module, tuple):
4✔
36
            module, version = module
4✔
37
        else:
38
            version = None
4✔
39

40
        try:
4✔
41
            import_module(module)
4✔
42
        except ImportError:
×
43
            if version is None:
×
44
                print('ERROR: The {} Python module could not be found, please '
×
45
                      'install it.'.format(module))
46
                ok = False
×
47
            else:
48
                print('ERROR: The {} Python module could not be found, '
×
49
                      'please install version {} or higher'.format(
50
                          module, version))
51
                ok = False
×
52
        else:
53
            if version is None:
4✔
54
                continue
4✔
55
            try:
4✔
56
                cur_ver = sys.modules[module].__version__
4✔
57
            except AttributeError:  # this is sometimes not available
×
58
                continue
×
59
            if LooseVersion(cur_ver) < LooseVersion(version):
4!
60
                print('ERROR: {} version is {}, but python-for-android needs '
×
61
                      'at least {}.'.format(module, cur_ver, version))
62
                ok = False
×
63

64
    if not ok:
4!
65
        print('python-for-android is exiting due to the errors logged above')
×
66
        exit(1)
×
67

68

69
if not environ.get('SKIP_PREREQUISITES_CHECK', '0') == '1':
4!
70
    check_and_install_default_prerequisites()
×
71
check_python_dependencies()
4✔
72

73

74
import sys
4✔
75
from sys import platform
4✔
76
from os.path import (join, dirname, realpath, exists, expanduser, basename)
4✔
77
import os
4✔
78
import glob
4✔
79
import shutil
4✔
80
import re
4✔
81
import shlex
4✔
82
from functools import wraps
4✔
83

84
import argparse
4✔
85
import sh
4✔
86
from appdirs import user_data_dir
4✔
87
import logging
4✔
88
from distutils.version import LooseVersion
4✔
89

90
from pythonforandroid.recipe import Recipe
4✔
91
from pythonforandroid.logger import (logger, info, warning, setup_color,
4✔
92
                                     Out_Style, Out_Fore,
93
                                     info_notify, info_main, shprint)
94
from pythonforandroid.util import current_directory
4✔
95
from pythonforandroid.bootstrap import Bootstrap
4✔
96
from pythonforandroid.distribution import Distribution, pretty_log_dists
4✔
97
from pythonforandroid.graph import get_recipe_order_and_bootstrap
4✔
98
from pythonforandroid.build import Context, build_recipes
4✔
99

100
user_dir = dirname(realpath(os.path.curdir))
4✔
101
toolchain_dir = dirname(__file__)
4✔
102
sys.path.insert(0, join(toolchain_dir, "tools", "external"))
4✔
103

104

105
def add_boolean_option(parser, names, no_names=None,
4✔
106
                       default=True, dest=None, description=None):
107
    group = parser.add_argument_group(description=description)
4✔
108
    if not isinstance(names, (list, tuple)):
4!
109
        names = [names]
×
110
    if dest is None:
4!
111
        dest = names[0].strip("-").replace("-", "_")
4✔
112

113
    def add_dashes(x):
4✔
114
        return x if x.startswith("-") else "--"+x
4✔
115

116
    opts = [add_dashes(x) for x in names]
4✔
117
    group.add_argument(
4✔
118
        *opts, help=("(this is the default)" if default else None),
119
        dest=dest, action='store_true')
120
    if no_names is None:
4!
121
        def add_no(x):
4✔
122
            x = x.lstrip("-")
4✔
123
            return ("no_"+x) if "_" in x else ("no-"+x)
4✔
124
        no_names = [add_no(x) for x in names]
4✔
125
    opts = [add_dashes(x) for x in no_names]
4✔
126
    group.add_argument(
4✔
127
        *opts, help=(None if default else "(this is the default)"),
128
        dest=dest, action='store_false')
129
    parser.set_defaults(**{dest: default})
4✔
130

131

132
def require_prebuilt_dist(func):
4✔
133
    """Decorator for ToolchainCL methods. If present, the method will
134
    automatically make sure a dist has been built before continuing
135
    or, if no dists are present or can be obtained, will raise an
136
    error.
137
    """
138

139
    @wraps(func)
4✔
140
    def wrapper_func(self, args, **kw):
3✔
141
        ctx = self.ctx
4✔
142
        ctx.set_archs(self._archs)
4✔
143
        ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir,
4✔
144
                                      user_ndk_dir=self.ndk_dir,
145
                                      user_android_api=self.android_api,
146
                                      user_ndk_api=self.ndk_api)
147
        dist = self._dist
4✔
148
        if dist.needs_build:
4!
149
            if dist.folder_exists():  # possible if the dist is being replaced
4!
150
                dist.delete()
×
151
            info_notify('No dist exists that meets your requirements, '
4✔
152
                        'so one will be built.')
153
            build_dist_from_args(ctx, dist, args)
4✔
154
        func(self, args, **kw)
4✔
155
    return wrapper_func
4✔
156

157

158
def dist_from_args(ctx, args):
4✔
159
    """Parses out any distribution-related arguments, and uses them to
160
    obtain a Distribution class instance for the build.
161
    """
162
    return Distribution.get_distribution(
4✔
163
        ctx,
164
        name=args.dist_name,
165
        recipes=split_argument_list(args.requirements),
166
        archs=args.arch,
167
        ndk_api=args.ndk_api,
168
        force_build=args.force_build,
169
        require_perfect_match=args.require_perfect_match,
170
        allow_replace_dist=args.allow_replace_dist)
171

172

173
def build_dist_from_args(ctx, dist, args):
4✔
174
    """Parses out any bootstrap related arguments, and uses them to build
175
    a dist."""
176
    bs = Bootstrap.get_bootstrap(args.bootstrap, ctx)
4✔
177
    blacklist = getattr(args, "blacklist_requirements", "").split(",")
4✔
178
    if len(blacklist) == 1 and blacklist[0] == "":
4!
179
        blacklist = []
4✔
180
    build_order, python_modules, bs = (
4✔
181
        get_recipe_order_and_bootstrap(
182
            ctx, dist.recipes, bs,
183
            blacklist=blacklist
184
        ))
185
    assert set(build_order).intersection(set(python_modules)) == set()
4✔
186
    ctx.recipe_build_order = build_order
4✔
187
    ctx.python_modules = python_modules
4✔
188

189
    info('The selected bootstrap is {}'.format(bs.name))
4✔
190
    info_main('# Creating dist with {} bootstrap'.format(bs.name))
4✔
191
    bs.distribution = dist
4✔
192
    info_notify('Dist will have name {} and requirements ({})'.format(
4✔
193
        dist.name, ', '.join(dist.recipes)))
194
    info('Dist contains the following requirements as recipes: {}'.format(
4✔
195
        ctx.recipe_build_order))
196
    info('Dist will also contain modules ({}) installed from pip'.format(
4✔
197
        ', '.join(ctx.python_modules)))
198
    info(
4✔
199
        'Dist will be build in mode {build_mode}{with_debug_symbols}'.format(
200
            build_mode='debug' if ctx.build_as_debuggable else 'release',
201
            with_debug_symbols=' (with debug symbols)'
202
            if ctx.with_debug_symbols
203
            else '',
204
        )
205
    )
206

207
    ctx.distribution = dist
4✔
208
    ctx.prepare_bootstrap(bs)
4✔
209
    if dist.needs_build:
4!
210
        ctx.prepare_dist()
4✔
211

212
    build_recipes(build_order, python_modules, ctx,
4✔
213
                  getattr(args, "private", None),
214
                  ignore_project_setup_py=getattr(
215
                      args, "ignore_setup_py", False
216
                  ),
217
                 )
218

219
    ctx.bootstrap.assemble_distribution()
4✔
220

221
    info_main('# Your distribution was created successfully, exiting.')
4✔
222
    info('Dist can be found at (for now) {}'
4✔
223
         .format(join(ctx.dist_dir, ctx.distribution.dist_dir)))
224

225

226
def split_argument_list(arg_list):
4✔
227
    if not len(arg_list):
4✔
228
        return []
4✔
229
    return re.split(r'[ ,]+', arg_list)
4✔
230

231

232
class NoAbbrevParser(argparse.ArgumentParser):
4✔
233
    """We want to disable argument abbreviation so as not to interfere
234
    with passing through arguments to build.py, but in python2 argparse
235
    doesn't have this option.
236

237
    This subclass alternative is follows the suggestion at
238
    https://bugs.python.org/issue14910.
239
    """
240
    def _get_option_tuples(self, option_string):
4✔
241
        return []
4✔
242

243

244
class ToolchainCL:
4✔
245

246
    def __init__(self):
4✔
247

248
        argv = sys.argv
4✔
249
        self.warn_on_carriage_return_args(argv)
4✔
250
        # Buildozer used to pass these arguments in a now-invalid order
251
        # If that happens, apply this fix
252
        # This fix will be removed once a fixed buildozer is released
253
        if (len(argv) > 2
4!
254
                and argv[1].startswith('--color')
255
                and argv[2].startswith('--storage-dir')):
256
            argv.append(argv.pop(1))  # the --color arg
×
257
            argv.append(argv.pop(1))  # the --storage-dir arg
×
258

259
        parser = NoAbbrevParser(
4✔
260
            description='A packaging tool for turning Python scripts and apps '
261
                        'into Android APKs')
262

263
        generic_parser = argparse.ArgumentParser(
4✔
264
            add_help=False,
265
            description='Generic arguments applied to all commands')
266
        argparse.ArgumentParser(
4✔
267
            add_help=False, description='Arguments for dist building')
268

269
        generic_parser.add_argument(
4✔
270
            '--debug', dest='debug', action='store_true', default=False,
271
            help='Display debug output and all build info')
272
        generic_parser.add_argument(
4✔
273
            '--color', dest='color', choices=['always', 'never', 'auto'],
274
            help='Enable or disable color output (default enabled on tty)')
275
        generic_parser.add_argument(
4✔
276
            '--sdk-dir', '--sdk_dir', dest='sdk_dir', default='',
277
            help='The filepath where the Android SDK is installed')
278
        generic_parser.add_argument(
4✔
279
            '--ndk-dir', '--ndk_dir', dest='ndk_dir', default='',
280
            help='The filepath where the Android NDK is installed')
281
        generic_parser.add_argument(
4✔
282
            '--android-api',
283
            '--android_api',
284
            dest='android_api',
285
            default=0,
286
            type=int,
287
            help=('The Android API level to build against defaults to {} if '
288
                  'not specified.').format(RECOMMENDED_TARGET_API))
289
        generic_parser.add_argument(
4✔
290
            '--ndk-version', '--ndk_version', dest='ndk_version', default=None,
291
            help=('DEPRECATED: the NDK version is now found automatically or '
292
                  'not at all.'))
293
        generic_parser.add_argument(
4✔
294
            '--ndk-api', type=int, default=None,
295
            help=('The Android API level to compile against. This should be your '
296
                  '*minimal supported* API, not normally the same as your --android-api. '
297
                  'Defaults to min(ANDROID_API, {}) if not specified.').format(RECOMMENDED_NDK_API))
298
        generic_parser.add_argument(
4✔
299
            '--symlink-bootstrap-files', '--ssymlink_bootstrap_files',
300
            action='store_true',
301
            dest='symlink_bootstrap_files',
302
            default=False,
303
            help=('If True, symlinks the bootstrap files '
304
                  'creation. This is useful for development only, it could also'
305
                  ' cause weird problems.'))
306

307
        default_storage_dir = user_data_dir('python-for-android')
4✔
308
        if ' ' in default_storage_dir:
4!
309
            default_storage_dir = '~/.python-for-android'
×
310
        generic_parser.add_argument(
4✔
311
            '--storage-dir', dest='storage_dir', default=default_storage_dir,
312
            help=('Primary storage directory for downloads and builds '
313
                  '(default: {})'.format(default_storage_dir)))
314

315
        generic_parser.add_argument(
4✔
316
            '--arch', help='The archs to build for.',
317
            action='append', default=[])
318

319
        # Options for specifying the Distribution
320
        generic_parser.add_argument(
4✔
321
            '--dist-name', '--dist_name',
322
            help='The name of the distribution to use or create', default='')
323

324
        generic_parser.add_argument(
4✔
325
            '--requirements',
326
            help=('Dependencies of your app, should be recipe names or '
327
                  'Python modules. NOT NECESSARY if you are using '
328
                  'Python 3 with --use-setup-py'),
329
            default='')
330

331
        generic_parser.add_argument(
4✔
332
            '--recipe-blacklist',
333
            help=('Blacklist an internal recipe from use. Allows '
334
                  'disabling Python 3 core modules to save size'),
335
            dest="recipe_blacklist",
336
            default='')
337

338
        generic_parser.add_argument(
4✔
339
            '--blacklist-requirements',
340
            help=('Blacklist an internal recipe from use. Allows '
341
                  'disabling Python 3 core modules to save size'),
342
            dest="blacklist_requirements",
343
            default='')
344

345
        generic_parser.add_argument(
4✔
346
            '--bootstrap',
347
            help='The bootstrap to build with. Leave unset to choose '
348
                 'automatically.',
349
            default=None)
350

351
        generic_parser.add_argument(
4✔
352
            '--hook',
353
            help='Filename to a module that contains python-for-android hooks',
354
            default=None)
355

356
        add_boolean_option(
4✔
357
            generic_parser, ["force-build"],
358
            default=False,
359
            description='Whether to force compilation of a new distribution')
360

361
        add_boolean_option(
4✔
362
            generic_parser, ["require-perfect-match"],
363
            default=False,
364
            description=('Whether the dist recipes must perfectly match '
365
                         'those requested'))
366

367
        add_boolean_option(
4✔
368
            generic_parser, ["allow-replace-dist"],
369
            default=True,
370
            description='Whether existing dist names can be automatically replaced'
371
            )
372

373
        generic_parser.add_argument(
4✔
374
            '--local-recipes', '--local_recipes',
375
            dest='local_recipes', default='./p4a-recipes',
376
            help='Directory to look for local recipes')
377

378
        generic_parser.add_argument(
4✔
379
            '--activity-class-name',
380
            dest='activity_class_name', default='org.kivy.android.PythonActivity',
381
            help='The full java class name of the main activity')
382

383
        generic_parser.add_argument(
4✔
384
            '--service-class-name',
385
            dest='service_class_name', default='org.kivy.android.PythonService',
386
            help='Full java package name of the PythonService class')
387

388
        generic_parser.add_argument(
4✔
389
            '--java-build-tool',
390
            dest='java_build_tool', default='auto',
391
            choices=['auto', 'ant', 'gradle'],
392
            help=('The java build tool to use when packaging the APK, defaults '
393
                  'to automatically selecting an appropriate tool.'))
394

395
        add_boolean_option(
4✔
396
            generic_parser, ['copy-libs'],
397
            default=False,
398
            description='Copy libraries instead of using biglink (Android 4.3+)'
399
        )
400

401
        self._read_configuration()
4✔
402

403
        subparsers = parser.add_subparsers(dest='subparser_name',
4✔
404
                                           help='The command to run')
405

406
        def add_parser(subparsers, *args, **kwargs):
4✔
407
            """
408
            argparse in python2 doesn't support the aliases option,
409
            so we just don't provide the aliases there.
410
            """
411
            if 'aliases' in kwargs and sys.version_info.major < 3:
4!
412
                kwargs.pop('aliases')
×
413
            return subparsers.add_parser(*args, **kwargs)
4✔
414

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

454
        parser_clean = add_parser(
4✔
455
            subparsers, 'clean',
456
            help='Delete build components.',
457
            parents=[generic_parser])
458
        parser_clean.add_argument(
4✔
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 = add_parser(
4✔
465
            subparsers,
466
            'clean_recipe_build', aliases=['clean-recipe-build'],
467
            help=('Delete the build components of the given recipe. '
468
                  'By default this will also delete built dists'),
469
            parents=[generic_parser])
470
        parser_clean_recipe_build.add_argument(
4✔
471
            'recipe', help='The recipe name')
472
        parser_clean_recipe_build.add_argument(
4✔
473
            '--no-clean-dists', default=False,
474
            dest='no_clean_dists',
475
            action='store_true',
476
            help='If passed, do not delete existing dists')
477

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

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

501
        parser_packaging = argparse.ArgumentParser(
4✔
502
            parents=[generic_parser],
503
            add_help=False,
504
            description='common options for packaging (apk, aar)')
505

506
        # This is actually an internal argument of the build.py
507
        # (see pythonforandroid/bootstraps/common/build/build.py).
508
        # However, it is also needed before the distribution is finally
509
        # assembled for locating the setup.py / other build systems, which
510
        # is why we also add it here:
511
        parser_packaging.add_argument(
4✔
512
            '--add-asset', dest='assets',
513
            action="append", default=[],
514
            help='Put this in the assets folder in the apk.')
515
        parser_packaging.add_argument(
4✔
516
            '--add-resource', dest='resources',
517
            action="append", default=[],
518
            help='Put this in the res folder in the apk.')
519
        parser_packaging.add_argument(
4✔
520
            '--private', dest='private',
521
            help='the directory with the app source code files' +
522
                 ' (containing your main.py entrypoint)',
523
            required=False, default=None)
524
        parser_packaging.add_argument(
4✔
525
            '--use-setup-py', dest="use_setup_py",
526
            action='store_true', default=False,
527
            help="Process the setup.py of a project if present. " +
528
                 "(Experimental!")
529
        parser_packaging.add_argument(
4✔
530
            '--ignore-setup-py', dest="ignore_setup_py",
531
            action='store_true', default=False,
532
            help="Don't run the setup.py of a project if present. " +
533
                 "This may be required if the setup.py is not " +
534
                 "designed to work inside p4a (e.g. by installing " +
535
                 "dependencies that won't work or aren't desired " +
536
                 "on Android")
537
        parser_packaging.add_argument(
4✔
538
            '--release', dest='build_mode', action='store_const',
539
            const='release', default='debug',
540
            help='Build your app as a non-debug release build. '
541
                 '(Disables gdb debugging among other things)')
542
        parser_packaging.add_argument(
4✔
543
            '--with-debug-symbols', dest='with_debug_symbols',
544
            action='store_const', const=True, default=False,
545
            help='Will keep debug symbols from `.so` files.')
546
        parser_packaging.add_argument(
4✔
547
            '--keystore', dest='keystore', action='store', default=None,
548
            help=('Keystore for JAR signing key, will use jarsigner '
549
                  'default if not specified (release build only)'))
550
        parser_packaging.add_argument(
4✔
551
            '--signkey', dest='signkey', action='store', default=None,
552
            help='Key alias to sign PARSER_APK. with (release build only)')
553
        parser_packaging.add_argument(
4✔
554
            '--keystorepw', dest='keystorepw', action='store', default=None,
555
            help='Password for keystore')
556
        parser_packaging.add_argument(
4✔
557
            '--signkeypw', dest='signkeypw', action='store', default=None,
558
            help='Password for key alias')
559

560
        add_parser(
4✔
561
            subparsers,
562
            'aar', help='Build an AAR',
563
            parents=[parser_packaging])
564

565
        add_parser(
4✔
566
            subparsers,
567
            'apk', help='Build an APK',
568
            parents=[parser_packaging])
569

570
        add_parser(
4✔
571
            subparsers,
572
            'aab', help='Build an AAB',
573
            parents=[parser_packaging])
574

575
        add_parser(
4✔
576
            subparsers,
577
            'create', help='Compile a set of requirements into a dist',
578
            parents=[generic_parser])
579
        add_parser(
4✔
580
            subparsers,
581
            'archs', help='List the available target architectures',
582
            parents=[generic_parser])
583
        add_parser(
4✔
584
            subparsers,
585
            'distributions', aliases=['dists'],
586
            help='List the currently available (compiled) dists',
587
            parents=[generic_parser])
588
        add_parser(
4✔
589
            subparsers,
590
            'delete_dist', aliases=['delete-dist'], help='Delete a compiled dist',
591
            parents=[generic_parser])
592

593
        parser_sdk_tools = add_parser(
4✔
594
            subparsers,
595
            'sdk_tools', aliases=['sdk-tools'],
596
            help='Run the given binary from the SDK tools dis',
597
            parents=[generic_parser])
598
        parser_sdk_tools.add_argument(
4✔
599
            'tool', help='The binary tool name to run')
600

601
        add_parser(
4✔
602
            subparsers,
603
            'adb', help='Run adb from the given SDK',
604
            parents=[generic_parser])
605
        add_parser(
4✔
606
            subparsers,
607
            'logcat', help='Run logcat from the given SDK',
608
            parents=[generic_parser])
609
        add_parser(
4✔
610
            subparsers,
611
            'build_status', aliases=['build-status'],
612
            help='Print some debug information about current built components',
613
            parents=[generic_parser])
614

615
        parser.add_argument('-v', '--version', action='version',
4✔
616
                            version=__version__)
617

618
        args, unknown = parser.parse_known_args(sys.argv[1:])
4✔
619
        args.unknown_args = unknown
4✔
620

621
        if hasattr(args, "private") and args.private is not None:
4!
622
            # Pass this value on to the internal bootstrap build.py:
623
            args.unknown_args += ["--private", args.private]
×
624
        if hasattr(args, "build_mode") and args.build_mode == "release":
4!
625
            args.unknown_args += ["--release"]
×
626
        if hasattr(args, "with_debug_symbols") and args.with_debug_symbols:
4!
627
            args.unknown_args += ["--with-debug-symbols"]
×
628
        if hasattr(args, "ignore_setup_py") and args.ignore_setup_py:
4!
629
            args.use_setup_py = False
×
630
        if hasattr(args, "activity_class_name") and args.activity_class_name != 'org.kivy.android.PythonActivity':
4✔
631
            args.unknown_args += ["--activity-class-name", args.activity_class_name]
4✔
632
        if hasattr(args, "service_class_name") and args.service_class_name != 'org.kivy.android.PythonService':
4✔
633
            args.unknown_args += ["--service-class-name", args.service_class_name]
4✔
634

635
        self.args = args
4✔
636

637
        if args.subparser_name is None:
4✔
638
            parser.print_help()
4✔
639
            exit(1)
4✔
640

641
        setup_color(args.color)
4✔
642

643
        if args.debug:
4!
644
            logger.setLevel(logging.DEBUG)
×
645

646
        self.ctx = Context()
4✔
647
        self.ctx.use_setup_py = getattr(args, "use_setup_py", True)
4✔
648
        self.ctx.build_as_debuggable = getattr(
4✔
649
            args, "build_mode", "debug"
650
        ) == "debug"
651
        self.ctx.with_debug_symbols = getattr(
4✔
652
            args, "with_debug_symbols", False
653
        )
654

655
        have_setup_py_or_similar = False
4✔
656
        if getattr(args, "private", None) is not None:
4!
657
            project_dir = getattr(args, "private")
×
658
            if (os.path.exists(os.path.join(project_dir, "setup.py")) or
×
659
                    os.path.exists(os.path.join(project_dir,
660
                                                "pyproject.toml"))):
661
                have_setup_py_or_similar = True
×
662

663
        # Process requirements and put version in environ
664
        if hasattr(args, 'requirements'):
4!
665
            requirements = []
4✔
666

667
            # Add dependencies from setup.py, but only if they are recipes
668
            # (because otherwise, setup.py itself will install them later)
669
            if (have_setup_py_or_similar and
4!
670
                    getattr(args, "use_setup_py", False)):
671
                try:
×
672
                    info("Analyzing package dependencies. MAY TAKE A WHILE.")
×
673
                    # Get all the dependencies corresponding to a recipe:
674
                    dependencies = [
×
675
                        dep.lower() for dep in
676
                        get_dep_names_of_package(
677
                            args.private,
678
                            keep_version_pins=True,
679
                            recursive=True,
680
                            verbose=True,
681
                        )
682
                    ]
683
                    info("Dependencies obtained: " + str(dependencies))
×
684
                    all_recipes = [
×
685
                        recipe.lower() for recipe in
686
                        set(Recipe.list_recipes(self.ctx))
687
                    ]
688
                    dependencies = set(dependencies).intersection(
×
689
                        set(all_recipes)
690
                    )
691
                    # Add dependencies to argument list:
692
                    if len(dependencies) > 0:
×
693
                        if len(args.requirements) > 0:
×
694
                            args.requirements += u","
×
695
                        args.requirements += u",".join(dependencies)
×
696
                except ValueError:
×
697
                    # Not a python package, apparently.
698
                    warning(
×
699
                        "Processing failed, is this project a valid "
700
                        "package? Will continue WITHOUT setup.py deps."
701
                    )
702

703
            # Parse --requirements argument list:
704
            for requirement in split_argument_list(args.requirements):
4✔
705
                if "==" in requirement:
4!
706
                    requirement, version = requirement.split(u"==", 1)
×
707
                    os.environ["VERSION_{}".format(requirement)] = version
×
708
                    info('Recipe {}: version "{}" requested'.format(
×
709
                        requirement, version))
710
                requirements.append(requirement)
4✔
711
            args.requirements = u",".join(requirements)
4✔
712

713
        self.warn_on_deprecated_args(args)
4✔
714

715
        self.storage_dir = args.storage_dir
4✔
716
        self.ctx.setup_dirs(self.storage_dir)
4✔
717
        self.sdk_dir = args.sdk_dir
4✔
718
        self.ndk_dir = args.ndk_dir
4✔
719
        self.android_api = args.android_api
4✔
720
        self.ndk_api = args.ndk_api
4✔
721
        self.ctx.symlink_bootstrap_files = args.symlink_bootstrap_files
4✔
722
        self.ctx.java_build_tool = args.java_build_tool
4✔
723

724
        self._archs = args.arch
4✔
725

726
        self.ctx.local_recipes = realpath(args.local_recipes)
4✔
727
        self.ctx.copy_libs = args.copy_libs
4✔
728

729
        self.ctx.activity_class_name = args.activity_class_name
4✔
730
        self.ctx.service_class_name = args.service_class_name
4✔
731

732
        # Each subparser corresponds to a method
733
        command = args.subparser_name.replace('-', '_')
4✔
734
        getattr(self, command)(args)
4✔
735

736
    @staticmethod
4✔
737
    def warn_on_carriage_return_args(args):
3✔
738
        for check_arg in args:
4✔
739
            if '\r' in check_arg:
4!
740
                warning("Argument '{}' contains a carriage return (\\r).".format(str(check_arg.replace('\r', ''))))
×
741
                warning("Invoking this program via scripts which use CRLF instead of LF line endings will have undefined behaviour.")
×
742

743
    def warn_on_deprecated_args(self, args):
4✔
744
        """
745
        Print warning messages for any deprecated arguments that were passed.
746
        """
747

748
        # Output warning if setup.py is present and neither --ignore-setup-py
749
        # nor --use-setup-py was specified.
750
        if getattr(args, "private", None) is not None and \
4!
751
                (os.path.exists(os.path.join(args.private, "setup.py")) or
752
                 os.path.exists(os.path.join(args.private, "pyproject.toml"))
753
                ):
754
            if not getattr(args, "use_setup_py", False) and \
×
755
                    not getattr(args, "ignore_setup_py", False):
756
                warning("  **** FUTURE BEHAVIOR CHANGE WARNING ****")
×
757
                warning("Your project appears to contain a setup.py file.")
×
758
                warning("Currently, these are ignored by default.")
×
759
                warning("This will CHANGE in an upcoming version!")
×
760
                warning("")
×
761
                warning("To ensure your setup.py is ignored, please specify:")
×
762
                warning("    --ignore-setup-py")
×
763
                warning("")
×
764
                warning("To enable what will some day be the default, specify:")
×
765
                warning("    --use-setup-py")
×
766

767
        # NDK version is now determined automatically
768
        if args.ndk_version is not None:
4!
769
            warning('--ndk-version is deprecated and no longer necessary, '
×
770
                    'the value you passed is ignored')
771
        if 'ANDROIDNDKVER' in environ:
4!
772
            warning('$ANDROIDNDKVER is deprecated and no longer necessary, '
×
773
                    'the value you set is ignored')
774

775
    def hook(self, name):
4✔
776
        if not self.args.hook:
×
777
            return
×
778
        if not hasattr(self, "hook_module"):
×
779
            # first time, try to load the hook module
780
            self.hook_module = load_source(
×
781
                "pythonforandroid.hook", self.args.hook)
782
        if hasattr(self.hook_module, name):
×
783
            info("Hook: execute {}".format(name))
×
784
            getattr(self.hook_module, name)(self)
×
785
        else:
786
            info("Hook: ignore {}".format(name))
×
787

788
    @property
4✔
789
    def default_storage_dir(self):
3✔
790
        udd = user_data_dir('python-for-android')
×
791
        if ' ' in udd:
×
792
            udd = '~/.python-for-android'
×
793
        return udd
×
794

795
    @staticmethod
4✔
796
    def _read_configuration():
3✔
797
        # search for a .p4a configuration file in the current directory
798
        if not exists(".p4a"):
4!
799
            return
4✔
800
        info("Reading .p4a configuration")
×
801
        with open(".p4a") as fd:
×
802
            lines = fd.readlines()
×
803
        lines = [shlex.split(line)
×
804
                 for line in lines if not line.startswith("#")]
805
        for line in lines:
×
806
            for arg in line:
×
807
                sys.argv.append(arg)
×
808

809
    def recipes(self, args):
4✔
810
        """
811
        Prints recipes basic info, e.g.
812
        .. code-block:: bash
813
            python3      3.7.1
814
                depends: ['hostpython3', 'sqlite3', 'openssl', 'libffi']
815
                conflicts: []
816
                optional depends: ['sqlite3', 'libffi', 'openssl']
817
        """
818
        ctx = self.ctx
4✔
819
        if args.compact:
4!
820
            print(" ".join(set(Recipe.list_recipes(ctx))))
×
821
        else:
822
            for name in sorted(Recipe.list_recipes(ctx)):
4✔
823
                try:
4✔
824
                    recipe = Recipe.get_recipe(name, ctx)
4✔
825
                except (IOError, ValueError):
×
826
                    warning('Recipe "{}" could not be loaded'.format(name))
×
827
                except SyntaxError:
×
828
                    import traceback
×
829
                    traceback.print_exc()
×
830
                    warning(('Recipe "{}" could not be loaded due to a '
×
831
                             'syntax error').format(name))
832
                version = str(recipe.version)
4✔
833
                print('{Fore.BLUE}{Style.BRIGHT}{recipe.name:<12} '
4✔
834
                      '{Style.RESET_ALL}{Fore.LIGHTBLUE_EX}'
835
                      '{version:<8}{Style.RESET_ALL}'.format(
836
                            recipe=recipe, Fore=Out_Fore, Style=Out_Style,
837
                            version=version))
838
                print('    {Fore.GREEN}depends: {recipe.depends}'
4✔
839
                      '{Fore.RESET}'.format(recipe=recipe, Fore=Out_Fore))
840
                if recipe.conflicts:
4✔
841
                    print('    {Fore.RED}conflicts: {recipe.conflicts}'
4✔
842
                          '{Fore.RESET}'
843
                          .format(recipe=recipe, Fore=Out_Fore))
844
                if recipe.opt_depends:
4✔
845
                    print('    {Fore.YELLOW}optional depends: '
4✔
846
                          '{recipe.opt_depends}{Fore.RESET}'
847
                          .format(recipe=recipe, Fore=Out_Fore))
848

849
    def bootstraps(self, _args):
4✔
850
        """List all the bootstraps available to build with."""
851
        for bs in Bootstrap.all_bootstraps():
×
852
            bs = Bootstrap.get_bootstrap(bs, self.ctx)
×
853
            print('{Fore.BLUE}{Style.BRIGHT}{bs.name}{Style.RESET_ALL}'
×
854
                  .format(bs=bs, Fore=Out_Fore, Style=Out_Style))
855
            print('    {Fore.GREEN}depends: {bs.recipe_depends}{Fore.RESET}'
×
856
                  .format(bs=bs, Fore=Out_Fore))
857

858
    def clean(self, args):
4✔
859
        components = args.component
×
860

861
        component_clean_methods = {
×
862
            'all': self.clean_all,
863
            'dists': self.clean_dists,
864
            'distributions': self.clean_dists,
865
            'builds': self.clean_builds,
866
            'bootstrap_builds': self.clean_bootstrap_builds,
867
            'downloads': self.clean_download_cache}
868

869
        for component in components:
×
870
            if component not in component_clean_methods:
×
871
                raise BuildInterruptingException((
×
872
                    'Asked to clean "{}" but this argument is not '
873
                    'recognised'.format(component)))
874
            component_clean_methods[component](args)
×
875

876
    def clean_all(self, args):
4✔
877
        """Delete all build components; the package cache, package builds,
878
        bootstrap builds and distributions."""
879
        self.clean_dists(args)
×
880
        self.clean_builds(args)
×
881
        self.clean_download_cache(args)
×
882

883
    def clean_dists(self, _args):
4✔
884
        """Delete all compiled distributions in the internal distribution
885
        directory."""
886
        ctx = self.ctx
×
887
        if exists(ctx.dist_dir):
×
888
            shutil.rmtree(ctx.dist_dir)
×
889

890
    def clean_bootstrap_builds(self, _args):
4✔
891
        """Delete all the bootstrap builds."""
892
        if exists(join(self.ctx.build_dir, 'bootstrap_builds')):
×
893
            shutil.rmtree(join(self.ctx.build_dir, 'bootstrap_builds'))
×
894
        # for bs in Bootstrap.all_bootstraps():
895
        #     bs = Bootstrap.get_bootstrap(bs, self.ctx)
896
        #     if bs.build_dir and exists(bs.build_dir):
897
        #         info('Cleaning build for {} bootstrap.'.format(bs.name))
898
        #         shutil.rmtree(bs.build_dir)
899

900
    def clean_builds(self, _args):
4✔
901
        """Delete all build caches for each recipe, python-install, java code
902
        and compiled libs collection.
903

904
        This does *not* delete the package download cache or the final
905
        distributions.  You can also use clean_recipe_build to delete the build
906
        of a specific recipe.
907
        """
908
        ctx = self.ctx
×
909
        if exists(ctx.build_dir):
×
910
            shutil.rmtree(ctx.build_dir)
×
911
        if exists(ctx.python_installs_dir):
×
912
            shutil.rmtree(ctx.python_installs_dir)
×
913
        libs_dir = join(self.ctx.build_dir, 'libs_collections')
×
914
        if exists(libs_dir):
×
915
            shutil.rmtree(libs_dir)
×
916

917
    def clean_recipe_build(self, args):
4✔
918
        """Deletes the build files of the given recipe.
919

920
        This is intended for debug purposes. You may experience
921
        strange behaviour or problems with some recipes if their
922
        build has made unexpected state changes. If this happens, run
923
        clean_builds, or attempt to clean other recipes until things
924
        work again.
925
        """
926
        recipe = Recipe.get_recipe(args.recipe, self.ctx)
×
927
        info('Cleaning build for {} recipe.'.format(recipe.name))
×
928
        recipe.clean_build()
×
929
        if not args.no_clean_dists:
×
930
            self.clean_dists(args)
×
931

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

936
            p4a clean_download_cache kivy,pyjnius
937

938
        This does *not* delete the build caches or final distributions.
939
        """
940
        ctx = self.ctx
×
941
        if hasattr(args, 'recipes') and args.recipes:
×
942
            for package in args.recipes:
×
943
                remove_path = join(ctx.packages_path, package)
×
944
                if exists(remove_path):
×
945
                    shutil.rmtree(remove_path)
×
946
                    info('Download cache removed for: "{}"'.format(package))
×
947
                else:
948
                    warning('No download cache found for "{}", skipping'.format(
×
949
                        package))
950
        else:
951
            if exists(ctx.packages_path):
×
952
                shutil.rmtree(ctx.packages_path)
×
953
                info('Download cache removed.')
×
954
            else:
955
                print('No cache found at "{}"'.format(ctx.packages_path))
×
956

957
    @require_prebuilt_dist
4✔
958
    def export_dist(self, args):
3✔
959
        """Copies a created dist to an output dir.
960

961
        This makes it easy to navigate to the dist to investigate it
962
        or call build.py, though you do not in general need to do this
963
        and can use the apk command instead.
964
        """
965
        ctx = self.ctx
×
966
        dist = dist_from_args(ctx, args)
×
967
        if dist.needs_build:
×
968
            raise BuildInterruptingException(
×
969
                'You asked to export a dist, but there is no dist '
970
                'with suitable recipes available. For now, you must '
971
                ' create one first with the create argument.')
972
        if args.symlink:
×
973
            shprint(sh.ln, '-s', dist.dist_dir, args.output_dir)
×
974
        else:
975
            shprint(sh.cp, '-r', dist.dist_dir, args.output_dir)
×
976

977
    @property
4✔
978
    def _dist(self):
3✔
979
        ctx = self.ctx
4✔
980
        dist = dist_from_args(ctx, self.args)
4✔
981
        ctx.distribution = dist
4✔
982
        return dist
4✔
983

984
    @staticmethod
4✔
985
    def _fix_args(args):
3✔
986
        """
987
        Manually fixing these arguments at the string stage is
988
        unsatisfactory and should probably be changed somehow, but
989
        we can't leave it until later as the build.py scripts assume
990
        they are in the current directory.
991
        works in-place
992
        :param args: parser args
993
        """
994

995
        fix_args = ('--dir', '--private', '--add-jar', '--add-source',
×
996
                    '--whitelist', '--blacklist', '--presplash', '--icon',
997
                    '--icon-bg', '--icon-fg')
998
        unknown_args = args.unknown_args
×
999

1000
        for asset in args.assets:
×
1001
            if ":" in asset:
×
1002
                asset_src, asset_dest = asset.split(":")
×
1003
            else:
1004
                asset_src = asset_dest = asset
×
1005
            # take abspath now, because build.py will be run in bootstrap dir
1006
            unknown_args += ["--asset", os.path.abspath(asset_src)+":"+asset_dest]
×
NEW
1007
        for resource in args.resources:
×
NEW
1008
            if ":" in resource:
×
NEW
1009
                resource_src, resource_dest = resource.split(":")
×
1010
            else:
NEW
1011
                resource_src = resource
×
NEW
1012
                resource_dest = ""
×
1013
            # take abspath now, because build.py will be run in bootstrap dir
NEW
1014
            unknown_args += ["--resource", os.path.abspath(resource_src)+":"+resource_dest]
×
1015
        for i, arg in enumerate(unknown_args):
×
1016
            argx = arg.split('=')
×
1017
            if argx[0] in fix_args:
×
1018
                if len(argx) > 1:
×
1019
                    unknown_args[i] = '='.join(
×
1020
                        (argx[0], realpath(expanduser(argx[1]))))
1021
                elif i + 1 < len(unknown_args):
×
1022
                    unknown_args[i+1] = realpath(expanduser(unknown_args[i+1]))
×
1023

1024
    @staticmethod
4✔
1025
    def _prepare_release_env(args):
3✔
1026
        """
1027
        prepares envitonment dict with the necessary flags for signing an apk
1028
        :param args: parser args
1029
        """
1030
        env = os.environ.copy()
×
1031
        if args.build_mode == 'release':
×
1032
            if args.keystore:
×
1033
                env['P4A_RELEASE_KEYSTORE'] = realpath(expanduser(args.keystore))
×
1034
            if args.signkey:
×
1035
                env['P4A_RELEASE_KEYALIAS'] = args.signkey
×
1036
            if args.keystorepw:
×
1037
                env['P4A_RELEASE_KEYSTORE_PASSWD'] = args.keystorepw
×
1038
            if args.signkeypw:
×
1039
                env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.signkeypw
×
1040
            elif args.keystorepw and 'P4A_RELEASE_KEYALIAS_PASSWD' not in env:
×
1041
                env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.keystorepw
×
1042

1043
        return env
×
1044

1045
    def _build_package(self, args, package_type):
4✔
1046
        """
1047
        Creates an android package using gradle
1048
        :param args: parser args
1049
        :param package_type: one of 'apk', 'aar', 'aab'
1050
        :return (gradle output, build_args)
1051
        """
1052
        ctx = self.ctx
×
1053
        dist = self._dist
×
1054
        bs = Bootstrap.get_bootstrap(args.bootstrap, ctx)
×
1055
        ctx.prepare_bootstrap(bs)
×
1056
        self._fix_args(args)
×
1057
        env = self._prepare_release_env(args)
×
1058

1059
        with current_directory(dist.dist_dir):
×
1060
            self.hook("before_apk_build")
×
1061
            os.environ["ANDROID_API"] = str(self.ctx.android_api)
×
1062
            build = load_source('build', join(dist.dist_dir, 'build.py'))
×
1063
            build_args = build.parse_args_and_make_package(
×
1064
                args.unknown_args
1065
            )
1066

1067
            self.hook("after_apk_build")
×
1068
            self.hook("before_apk_assemble")
×
1069
            build_tools_versions = os.listdir(join(ctx.sdk_dir,
×
1070
                                                   'build-tools'))
1071
            build_tools_versions = sorted(build_tools_versions,
×
1072
                                          key=LooseVersion)
1073
            build_tools_version = build_tools_versions[-1]
×
1074
            info(('Detected highest available build tools '
×
1075
                  'version to be {}').format(build_tools_version))
1076

1077
            if build_tools_version < '25.0':
×
1078
                raise BuildInterruptingException(
×
1079
                    'build_tools >= 25 is required, but %s is installed' % build_tools_version)
1080
            if not exists("gradlew"):
×
1081
                raise BuildInterruptingException("gradlew file is missing")
×
1082

1083
            env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir
×
1084
            env["ANDROID_HOME"] = self.ctx.sdk_dir
×
1085

1086
            gradlew = sh.Command('./gradlew')
×
1087

1088
            if exists('/usr/bin/dos2unix'):
×
1089
                # .../dists/bdisttest_python3/gradlew
1090
                # .../build/bootstrap_builds/sdl2-python3/gradlew
1091
                # if docker on windows, gradle contains CRLF
1092
                output = shprint(
×
1093
                    sh.Command('dos2unix'), gradlew._path.decode('utf8'),
1094
                    _tail=20, _critical=True, _env=env
1095
                )
1096
            if args.build_mode == "debug":
×
1097
                if package_type == "aab":
×
1098
                    raise BuildInterruptingException(
×
1099
                        "aab is meant only for distribution and is not available in debug mode. "
1100
                        "Instead, you can use apk while building for debugging purposes."
1101
                    )
1102
                gradle_task = "assembleDebug"
×
1103
            elif args.build_mode == "release":
×
1104
                if package_type in ["apk", "aar"]:
×
1105
                    gradle_task = "assembleRelease"
×
1106
                elif package_type == "aab":
×
1107
                    gradle_task = "bundleRelease"
×
1108
            else:
1109
                raise BuildInterruptingException(
×
1110
                    "Unknown build mode {} for apk()".format(args.build_mode))
1111

1112
            # WARNING: We should make sure to clean the build directory before building.
1113
            # See PR: kivy/python-for-android#2705
NEW
1114
            output = shprint(gradlew, "clean", gradle_task, _tail=20,
×
1115
                             _critical=True, _env=env)
1116
        return output, build_args
×
1117

1118
    def _finish_package(self, args, output, build_args, package_type, output_dir):
4✔
1119
        """
1120
        Finishes the package after the gradle script run
1121
        :param args: the parser args
1122
        :param output: RunningCommand output
1123
        :param build_args: build args as returned by build.parse_args
1124
        :param package_type: one of 'apk', 'aar', 'aab'
1125
        :param output_dir: where to put the package file
1126
        """
1127

1128
        package_glob = "*-{}.%s" % package_type
×
1129
        package_add_version = True
×
1130

1131
        self.hook("after_apk_assemble")
×
1132

1133
        info_main('# Copying android package to current directory')
×
1134

1135
        package_re = re.compile(r'.*Package: (.*\.apk)$')
×
1136
        package_file = None
×
1137
        for line in reversed(output.splitlines()):
×
1138
            m = package_re.match(line)
×
1139
            if m:
×
1140
                package_file = m.groups()[0]
×
1141
                break
×
1142
        if not package_file:
×
1143
            info_main('# Android package filename not found in build output. Guessing...')
×
1144
            if args.build_mode == "release":
×
1145
                suffixes = ("release", "release-unsigned")
×
1146
            else:
1147
                suffixes = ("debug", )
×
1148
            for suffix in suffixes:
×
1149

1150
                package_files = glob.glob(join(output_dir, package_glob.format(suffix)))
×
1151
                if package_files:
×
1152
                    if len(package_files) > 1:
×
1153
                        info('More than one built APK found... guessing you '
×
1154
                             'just built {}'.format(package_files[-1]))
1155
                    package_file = package_files[-1]
×
1156
                    break
×
1157
            else:
1158
                raise BuildInterruptingException('Couldn\'t find the built APK')
×
1159

1160
        info_main('# Found android package file: {}'.format(package_file))
×
1161
        package_extension = f".{package_type}"
×
1162
        if package_add_version:
×
1163
            info('# Add version number to android package')
×
1164
            package_name = basename(package_file)[:-len(package_extension)]
×
1165
            package_file_dest = "{}-{}{}".format(
×
1166
                package_name, build_args.version, package_extension)
1167
            info('# Android package renamed to {}'.format(package_file_dest))
×
1168
            shprint(sh.cp, package_file, package_file_dest)
×
1169
        else:
1170
            shprint(sh.cp, package_file, './')
×
1171

1172
    @require_prebuilt_dist
4✔
1173
    def apk(self, args):
3✔
1174
        output, build_args = self._build_package(args, package_type='apk')
×
1175
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'apk', args.build_mode)
×
1176
        self._finish_package(args, output, build_args, 'apk', output_dir)
×
1177

1178
    @require_prebuilt_dist
4✔
1179
    def aar(self, args):
3✔
1180
        output, build_args = self._build_package(args, package_type='aar')
×
1181
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'aar')
×
1182
        self._finish_package(args, output, build_args, 'aar', output_dir)
×
1183

1184
    @require_prebuilt_dist
4✔
1185
    def aab(self, args):
3✔
1186
        output, build_args = self._build_package(args, package_type='aab')
×
1187
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'bundle', args.build_mode)
×
1188
        self._finish_package(args, output, build_args, 'aab', output_dir)
×
1189

1190
    @require_prebuilt_dist
4✔
1191
    def create(self, args):
3✔
1192
        """Create a distribution directory if it doesn't already exist, run
1193
        any recipes if necessary, and build the apk.
1194
        """
1195
        pass  # The decorator does everything
4✔
1196

1197
    def archs(self, _args):
4✔
1198
        """List the target architectures available to be built for."""
1199
        print('{Style.BRIGHT}Available target architectures are:'
×
1200
              '{Style.RESET_ALL}'.format(Style=Out_Style))
1201
        for arch in self.ctx.archs:
×
1202
            print('    {}'.format(arch.arch))
×
1203

1204
    def dists(self, args):
4✔
1205
        """The same as :meth:`distributions`."""
1206
        self.distributions(args)
×
1207

1208
    def distributions(self, _args):
4✔
1209
        """Lists all distributions currently available (i.e. that have already
1210
        been built)."""
1211
        ctx = self.ctx
×
1212
        dists = Distribution.get_distributions(ctx)
×
1213

1214
        if dists:
×
1215
            print('{Style.BRIGHT}Distributions currently installed are:'
×
1216
                  '{Style.RESET_ALL}'.format(Style=Out_Style))
1217
            pretty_log_dists(dists, print)
×
1218
        else:
1219
            print('{Style.BRIGHT}There are no dists currently built.'
×
1220
                  '{Style.RESET_ALL}'.format(Style=Out_Style))
1221

1222
    def delete_dist(self, _args):
4✔
1223
        dist = self._dist
×
1224
        if not dist.folder_exists():
×
1225
            info('No dist exists that matches your specifications, '
×
1226
                 'exiting without deleting.')
1227
            return
×
1228
        dist.delete()
×
1229

1230
    def sdk_tools(self, args):
4✔
1231
        """Runs the android binary from the detected SDK directory, passing
1232
        all arguments straight to it. This binary is used to install
1233
        e.g. platform-tools for different API level targets. This is
1234
        intended as a convenience function if android is not in your
1235
        $PATH.
1236
        """
1237
        ctx = self.ctx
×
1238
        ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir,
×
1239
                                      user_ndk_dir=self.ndk_dir,
1240
                                      user_android_api=self.android_api,
1241
                                      user_ndk_api=self.ndk_api)
1242
        android = sh.Command(join(ctx.sdk_dir, 'tools', args.tool))
×
1243
        output = android(
×
1244
            *args.unknown_args, _iter=True, _out_bufsize=1, _err_to_out=True)
1245
        for line in output:
×
1246
            sys.stdout.write(line)
×
1247
            sys.stdout.flush()
×
1248

1249
    def adb(self, args):
4✔
1250
        """Runs the adb binary from the detected SDK directory, passing all
1251
        arguments straight to it. This is intended as a convenience
1252
        function if adb is not in your $PATH.
1253
        """
1254
        self._adb(args.unknown_args)
×
1255

1256
    def logcat(self, args):
4✔
1257
        """Runs ``adb logcat`` using the adb binary from the detected SDK
1258
        directory. All extra args are passed as arguments to logcat."""
1259
        self._adb(['logcat'] + args.unknown_args)
×
1260

1261
    def _adb(self, commands):
4✔
1262
        """Call the adb executable from the SDK, passing the given commands as
1263
        arguments."""
1264
        ctx = self.ctx
×
1265
        ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir,
×
1266
                                      user_ndk_dir=self.ndk_dir,
1267
                                      user_android_api=self.android_api,
1268
                                      user_ndk_api=self.ndk_api)
1269
        if platform in ('win32', 'cygwin'):
×
1270
            adb = sh.Command(join(ctx.sdk_dir, 'platform-tools', 'adb.exe'))
×
1271
        else:
1272
            adb = sh.Command(join(ctx.sdk_dir, 'platform-tools', 'adb'))
×
1273
        info_notify('Starting adb...')
×
1274
        output = adb(*commands, _iter=True, _out_bufsize=1, _err_to_out=True)
×
1275
        for line in output:
×
1276
            sys.stdout.write(line)
×
1277
            sys.stdout.flush()
×
1278

1279
    def recommendations(self, args):
4✔
1280
        print_recommendations()
4✔
1281

1282
    def build_status(self, _args):
4✔
1283
        """Print the status of the specified build. """
1284
        print('{Style.BRIGHT}Bootstraps whose core components are probably '
×
1285
              'already built:{Style.RESET_ALL}'.format(Style=Out_Style))
1286

1287
        bootstrap_dir = join(self.ctx.build_dir, 'bootstrap_builds')
×
1288
        if exists(bootstrap_dir):
×
1289
            for filen in os.listdir(bootstrap_dir):
×
1290
                print('    {Fore.GREEN}{Style.BRIGHT}{filen}{Style.RESET_ALL}'
×
1291
                      .format(filen=filen, Fore=Out_Fore, Style=Out_Style))
1292

1293
        print('{Style.BRIGHT}Recipes that are probably already built:'
×
1294
              '{Style.RESET_ALL}'.format(Style=Out_Style))
1295
        other_builds_dir = join(self.ctx.build_dir, 'other_builds')
×
1296
        if exists(other_builds_dir):
×
1297
            for filen in sorted(os.listdir(other_builds_dir)):
×
1298
                name = filen.split('-')[0]
×
1299
                dependencies = filen.split('-')[1:]
×
1300
                recipe_str = ('    {Style.BRIGHT}{Fore.GREEN}{name}'
×
1301
                              '{Style.RESET_ALL}'.format(
1302
                                  Style=Out_Style, name=name, Fore=Out_Fore))
1303
                if dependencies:
×
1304
                    recipe_str += (
×
1305
                        ' ({Fore.BLUE}with ' + ', '.join(dependencies) +
1306
                        '{Fore.RESET})').format(Fore=Out_Fore)
1307
                recipe_str += '{Style.RESET_ALL}'.format(Style=Out_Style)
×
1308
                print(recipe_str)
×
1309

1310

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

© 2025 Coveralls, Inc