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

kivy / python-for-android / 9340559334

02 Jun 2024 07:04PM UTC coverage: 59.126% (+0.06%) from 59.062%
9340559334

push

github

AndreMiras
:mute: Set sh.command log level to warning

sh INFO log level is too verbose by default, refs upstream TODO

1047 of 2355 branches covered (44.46%)

Branch coverage included in aggregate %.

0 of 4 new or added lines in 1 file covered. (0.0%)

140 existing lines in 1 file now uncovered.

4855 of 7627 relevant lines covered (63.66%)

2.53 hits per line

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

45.33
/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 appdirs import user_data_dir
4✔
10
import argparse
4✔
11
from functools import wraps
4✔
12
import glob
4✔
13
import logging
4✔
14
import os
4✔
15
from os import environ
4✔
16
from os.path import (join, dirname, realpath, exists, expanduser, basename)
4✔
17
import re
4✔
18
import shlex
4✔
19
import sys
4✔
20
from sys import platform
4✔
21

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

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

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

55

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

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

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

82

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

108

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

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

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

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

176

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

182

183
class NoAbbrevParser(argparse.ArgumentParser):
4✔
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
    def _get_option_tuples(self, option_string):
4✔
192
        return []
4✔
193

194

195
class ToolchainCL:
4✔
196

197
    def __init__(self):
4✔
198

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

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

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

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

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

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

270
        # Options for specifying the Distribution
271
        generic_parser.add_argument(
4✔
272
            '--dist-name', '--dist_name',
273
            help='The name of the distribution to use or create', default='')
274

275
        generic_parser.add_argument(
4✔
276
            '--requirements',
277
            help=('Dependencies of your app, should be recipe names or '
278
                  'Python modules. NOT NECESSARY if you are using '
279
                  'Python 3 with --use-setup-py'),
280
            default='')
281

282
        generic_parser.add_argument(
4✔
283
            '--recipe-blacklist',
284
            help=('Blacklist an internal recipe from use. Allows '
285
                  'disabling Python 3 core modules to save size'),
286
            dest="recipe_blacklist",
287
            default='')
288

289
        generic_parser.add_argument(
4✔
290
            '--blacklist-requirements',
291
            help=('Blacklist an internal recipe from use. Allows '
292
                  'disabling Python 3 core modules to save size'),
293
            dest="blacklist_requirements",
294
            default='')
295

296
        generic_parser.add_argument(
4✔
297
            '--bootstrap',
298
            help='The bootstrap to build with. Leave unset to choose '
299
                 'automatically.',
300
            default=None)
301

302
        generic_parser.add_argument(
4✔
303
            '--hook',
304
            help='Filename to a module that contains python-for-android hooks',
305
            default=None)
306

307
        add_boolean_option(
4✔
308
            generic_parser, ["force-build"],
309
            default=False,
310
            description='Whether to force compilation of a new distribution')
311

312
        add_boolean_option(
4✔
313
            generic_parser, ["require-perfect-match"],
314
            default=False,
315
            description=('Whether the dist recipes must perfectly match '
316
                         'those requested'))
317

318
        add_boolean_option(
4✔
319
            generic_parser, ["allow-replace-dist"],
320
            default=True,
321
            description='Whether existing dist names can be automatically replaced'
322
            )
323

324
        generic_parser.add_argument(
4✔
325
            '--local-recipes', '--local_recipes',
326
            dest='local_recipes', default='./p4a-recipes',
327
            help='Directory to look for local recipes')
328

329
        generic_parser.add_argument(
4✔
330
            '--activity-class-name',
331
            dest='activity_class_name', default='org.kivy.android.PythonActivity',
332
            help='The full java class name of the main activity')
333

334
        generic_parser.add_argument(
4✔
335
            '--service-class-name',
336
            dest='service_class_name', default='org.kivy.android.PythonService',
337
            help='Full java package name of the PythonService class')
338

339
        generic_parser.add_argument(
4✔
340
            '--java-build-tool',
341
            dest='java_build_tool', default='auto',
342
            choices=['auto', 'ant', 'gradle'],
343
            help=('The java build tool to use when packaging the APK, defaults '
344
                  'to automatically selecting an appropriate tool.'))
345

346
        add_boolean_option(
4✔
347
            generic_parser, ['copy-libs'],
348
            default=False,
349
            description='Copy libraries instead of using biglink (Android 4.3+)'
350
        )
351

352
        self._read_configuration()
4✔
353

354
        subparsers = parser.add_subparsers(dest='subparser_name',
4✔
355
                                           help='The command to run')
356

357
        def add_parser(subparsers, *args, **kwargs):
4✔
358
            """
359
            argparse in python2 doesn't support the aliases option,
360
            so we just don't provide the aliases there.
361
            """
362
            if 'aliases' in kwargs and sys.version_info.major < 3:
4!
363
                kwargs.pop('aliases')
×
364
            return subparsers.add_parser(*args, **kwargs)
4✔
365

366
        add_parser(
4✔
367
            subparsers,
368
            'recommendations',
369
            parents=[generic_parser],
370
            help='List recommended p4a dependencies')
371
        parser_recipes = add_parser(
4✔
372
            subparsers,
373
            'recipes',
374
            parents=[generic_parser],
375
            help='List the available recipes')
376
        parser_recipes.add_argument(
4✔
377
            "--compact",
378
            action="store_true", default=False,
379
            help="Produce a compact list suitable for scripting")
380
        add_parser(
4✔
381
            subparsers, 'bootstraps',
382
            help='List the available bootstraps',
383
            parents=[generic_parser])
384
        add_parser(
4✔
385
            subparsers, 'clean_all',
386
            aliases=['clean-all'],
387
            help='Delete all builds, dists and caches',
388
            parents=[generic_parser])
389
        add_parser(
4✔
390
            subparsers, 'clean_dists',
391
            aliases=['clean-dists'],
392
            help='Delete all dists',
393
            parents=[generic_parser])
394
        add_parser(
4✔
395
            subparsers, 'clean_bootstrap_builds',
396
            aliases=['clean-bootstrap-builds'],
397
            help='Delete all bootstrap builds',
398
            parents=[generic_parser])
399
        add_parser(
4✔
400
            subparsers, 'clean_builds',
401
            aliases=['clean-builds'],
402
            help='Delete all builds',
403
            parents=[generic_parser])
404

405
        parser_clean = add_parser(
4✔
406
            subparsers, 'clean',
407
            help='Delete build components.',
408
            parents=[generic_parser])
409
        parser_clean.add_argument(
4✔
410
            'component', nargs='+',
411
            help=('The build component(s) to delete. You can pass any '
412
                  'number of arguments from "all", "builds", "dists", '
413
                  '"distributions", "bootstrap_builds", "downloads".'))
414

415
        parser_clean_recipe_build = add_parser(
4✔
416
            subparsers,
417
            'clean_recipe_build', aliases=['clean-recipe-build'],
418
            help=('Delete the build components of the given recipe. '
419
                  'By default this will also delete built dists'),
420
            parents=[generic_parser])
421
        parser_clean_recipe_build.add_argument(
4✔
422
            'recipe', help='The recipe name')
423
        parser_clean_recipe_build.add_argument(
4✔
424
            '--no-clean-dists', default=False,
425
            dest='no_clean_dists',
426
            action='store_true',
427
            help='If passed, do not delete existing dists')
428

429
        parser_clean_download_cache = add_parser(
4✔
430
            subparsers,
431
            'clean_download_cache', aliases=['clean-download-cache'],
432
            help='Delete cached downloads for requirement builds',
433
            parents=[generic_parser])
434
        parser_clean_download_cache.add_argument(
4✔
435
            'recipes',
436
            nargs='*',
437
            help='The recipes to clean (space-separated). If no recipe name is'
438
                  ' provided, the entire cache is cleared.')
439

440
        parser_export_dist = add_parser(
4✔
441
            subparsers,
442
            'export_dist', aliases=['export-dist'],
443
            help='Copy the named dist to the given path',
444
            parents=[generic_parser])
445
        parser_export_dist.add_argument('output_dir',
4✔
446
                                        help='The output dir to copy to')
447
        parser_export_dist.add_argument(
4✔
448
            '--symlink',
449
            action='store_true',
450
            help='Symlink the dist instead of copying')
451

452
        parser_packaging = argparse.ArgumentParser(
4✔
453
            parents=[generic_parser],
454
            add_help=False,
455
            description='common options for packaging (apk, aar)')
456

457
        # This is actually an internal argument of the build.py
458
        # (see pythonforandroid/bootstraps/common/build/build.py).
459
        # However, it is also needed before the distribution is finally
460
        # assembled for locating the setup.py / other build systems, which
461
        # is why we also add it here:
462
        parser_packaging.add_argument(
4✔
463
            '--add-asset', dest='assets',
464
            action="append", default=[],
465
            help='Put this in the assets folder in the apk.')
466
        parser_packaging.add_argument(
4✔
467
            '--add-resource', dest='resources',
468
            action="append", default=[],
469
            help='Put this in the res folder in the apk.')
470
        parser_packaging.add_argument(
4✔
471
            '--private', dest='private',
472
            help='the directory with the app source code files' +
473
                 ' (containing your main.py entrypoint)',
474
            required=False, default=None)
475
        parser_packaging.add_argument(
4✔
476
            '--use-setup-py', dest="use_setup_py",
477
            action='store_true', default=False,
478
            help="Process the setup.py of a project if present. " +
479
                 "(Experimental!")
480
        parser_packaging.add_argument(
4✔
481
            '--ignore-setup-py', dest="ignore_setup_py",
482
            action='store_true', default=False,
483
            help="Don't run the setup.py of a project if present. " +
484
                 "This may be required if the setup.py is not " +
485
                 "designed to work inside p4a (e.g. by installing " +
486
                 "dependencies that won't work or aren't desired " +
487
                 "on Android")
488
        parser_packaging.add_argument(
4✔
489
            '--release', dest='build_mode', action='store_const',
490
            const='release', default='debug',
491
            help='Build your app as a non-debug release build. '
492
                 '(Disables gdb debugging among other things)')
493
        parser_packaging.add_argument(
4✔
494
            '--with-debug-symbols', dest='with_debug_symbols',
495
            action='store_const', const=True, default=False,
496
            help='Will keep debug symbols from `.so` files.')
497
        parser_packaging.add_argument(
4✔
498
            '--keystore', dest='keystore', action='store', default=None,
499
            help=('Keystore for JAR signing key, will use jarsigner '
500
                  'default if not specified (release build only)'))
501
        parser_packaging.add_argument(
4✔
502
            '--signkey', dest='signkey', action='store', default=None,
503
            help='Key alias to sign PARSER_APK. with (release build only)')
504
        parser_packaging.add_argument(
4✔
505
            '--keystorepw', dest='keystorepw', action='store', default=None,
506
            help='Password for keystore')
507
        parser_packaging.add_argument(
4✔
508
            '--signkeypw', dest='signkeypw', action='store', default=None,
509
            help='Password for key alias')
510

511
        add_parser(
4✔
512
            subparsers,
513
            'aar', help='Build an AAR',
514
            parents=[parser_packaging])
515

516
        add_parser(
4✔
517
            subparsers,
518
            'apk', help='Build an APK',
519
            parents=[parser_packaging])
520

521
        add_parser(
4✔
522
            subparsers,
523
            'aab', help='Build an AAB',
524
            parents=[parser_packaging])
525

526
        add_parser(
4✔
527
            subparsers,
528
            'create', help='Compile a set of requirements into a dist',
529
            parents=[generic_parser])
530
        add_parser(
4✔
531
            subparsers,
532
            'archs', help='List the available target architectures',
533
            parents=[generic_parser])
534
        add_parser(
4✔
535
            subparsers,
536
            'distributions', aliases=['dists'],
537
            help='List the currently available (compiled) dists',
538
            parents=[generic_parser])
539
        add_parser(
4✔
540
            subparsers,
541
            'delete_dist', aliases=['delete-dist'], help='Delete a compiled dist',
542
            parents=[generic_parser])
543

544
        parser_sdk_tools = add_parser(
4✔
545
            subparsers,
546
            'sdk_tools', aliases=['sdk-tools'],
547
            help='Run the given binary from the SDK tools dis',
548
            parents=[generic_parser])
549
        parser_sdk_tools.add_argument(
4✔
550
            'tool', help='The binary tool name to run')
551

552
        add_parser(
4✔
553
            subparsers,
554
            'adb', help='Run adb from the given SDK',
555
            parents=[generic_parser])
556
        add_parser(
4✔
557
            subparsers,
558
            'logcat', help='Run logcat from the given SDK',
559
            parents=[generic_parser])
560
        add_parser(
4✔
561
            subparsers,
562
            'build_status', aliases=['build-status'],
563
            help='Print some debug information about current built components',
564
            parents=[generic_parser])
565

566
        parser.add_argument('-v', '--version', action='version',
4✔
567
                            version=__version__)
568

569
        args, unknown = parser.parse_known_args(sys.argv[1:])
4✔
570
        args.unknown_args = unknown
4✔
571

572
        if hasattr(args, "private") and args.private is not None:
4!
573
            # Pass this value on to the internal bootstrap build.py:
574
            args.unknown_args += ["--private", args.private]
×
575
        if hasattr(args, "build_mode") and args.build_mode == "release":
4!
576
            args.unknown_args += ["--release"]
×
577
        if hasattr(args, "with_debug_symbols") and args.with_debug_symbols:
4!
578
            args.unknown_args += ["--with-debug-symbols"]
×
579
        if hasattr(args, "ignore_setup_py") and args.ignore_setup_py:
4!
580
            args.use_setup_py = False
×
581
        if hasattr(args, "activity_class_name") and args.activity_class_name != 'org.kivy.android.PythonActivity':
4✔
582
            args.unknown_args += ["--activity-class-name", args.activity_class_name]
4✔
583
        if hasattr(args, "service_class_name") and args.service_class_name != 'org.kivy.android.PythonService':
4✔
584
            args.unknown_args += ["--service-class-name", args.service_class_name]
4✔
585

586
        self.args = args
4✔
587

588
        if args.subparser_name is None:
4✔
589
            parser.print_help()
4✔
590
            exit(1)
4✔
591

592
        setup_color(args.color)
4✔
593

594
        if args.debug:
4!
NEW
595
            print("=========================== TODO: debugging, setting DEBUG logger ====================")
×
596
            # TODO debugging
597
            # logger.setLevel(logging.DEBUG)
NEW
598
            logger.setLevel
×
NEW
599
            logging.DEBUG
×
NEW
600
            pass
×
601

602
        self.ctx = Context()
4✔
603
        self.ctx.use_setup_py = getattr(args, "use_setup_py", True)
4✔
604
        self.ctx.build_as_debuggable = getattr(
4✔
605
            args, "build_mode", "debug"
606
        ) == "debug"
607
        self.ctx.with_debug_symbols = getattr(
4✔
608
            args, "with_debug_symbols", False
609
        )
610

611
        have_setup_py_or_similar = False
4✔
612
        if getattr(args, "private", None) is not None:
4!
UNCOV
613
            project_dir = getattr(args, "private")
×
UNCOV
614
            if (os.path.exists(os.path.join(project_dir, "setup.py")) or
×
615
                    os.path.exists(os.path.join(project_dir,
616
                                                "pyproject.toml"))):
617
                have_setup_py_or_similar = True
×
618

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

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

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

669
        self.warn_on_deprecated_args(args)
4✔
670

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

680
        self._archs = args.arch
4✔
681

682
        self.ctx.local_recipes = realpath(args.local_recipes)
4✔
683
        self.ctx.copy_libs = args.copy_libs
4✔
684

685
        self.ctx.activity_class_name = args.activity_class_name
4✔
686
        self.ctx.service_class_name = args.service_class_name
4✔
687

688
        # Each subparser corresponds to a method
689
        command = args.subparser_name.replace('-', '_')
4✔
690
        getattr(self, command)(args)
4✔
691

692
    @staticmethod
4✔
693
    def warn_on_carriage_return_args(args):
4✔
694
        for check_arg in args:
4✔
695
            if '\r' in check_arg:
4!
UNCOV
696
                warning("Argument '{}' contains a carriage return (\\r).".format(str(check_arg.replace('\r', ''))))
×
UNCOV
697
                warning("Invoking this program via scripts which use CRLF instead of LF line endings will have undefined behaviour.")
×
698

699
    def warn_on_deprecated_args(self, args):
4✔
700
        """
701
        Print warning messages for any deprecated arguments that were passed.
702
        """
703

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

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

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

744
    @property
4✔
745
    def default_storage_dir(self):
4✔
746
        udd = user_data_dir('python-for-android')
×
UNCOV
747
        if ' ' in udd:
×
UNCOV
748
            udd = '~/.python-for-android'
×
UNCOV
749
        return udd
×
750

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

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

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

814
    def clean(self, args):
4✔
815
        components = args.component
×
816

UNCOV
817
        component_clean_methods = {
×
818
            'all': self.clean_all,
819
            'dists': self.clean_dists,
820
            'distributions': self.clean_dists,
821
            'builds': self.clean_builds,
822
            'bootstrap_builds': self.clean_bootstrap_builds,
823
            'downloads': self.clean_download_cache}
824

UNCOV
825
        for component in components:
×
UNCOV
826
            if component not in component_clean_methods:
×
UNCOV
827
                raise BuildInterruptingException((
×
828
                    'Asked to clean "{}" but this argument is not '
829
                    'recognised'.format(component)))
830
            component_clean_methods[component](args)
×
831

832
    def clean_all(self, args):
4✔
833
        """Delete all build components; the package cache, package builds,
834
        bootstrap builds and distributions."""
UNCOV
835
        self.clean_dists(args)
×
UNCOV
836
        self.clean_builds(args)
×
UNCOV
837
        self.clean_download_cache(args)
×
838

839
    def clean_dists(self, _args):
4✔
840
        """Delete all compiled distributions in the internal distribution
841
        directory."""
UNCOV
842
        ctx = self.ctx
×
UNCOV
843
        rmdir(ctx.dist_dir)
×
844

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

854
    def clean_builds(self, _args):
4✔
855
        """Delete all build caches for each recipe, python-install, java code
856
        and compiled libs collection.
857

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

868
    def clean_recipe_build(self, args):
4✔
869
        """Deletes the build files of the given recipe.
870

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

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

887
            p4a clean_download_cache kivy,pyjnius
888

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

908
    @require_prebuilt_dist
4✔
909
    def export_dist(self, args):
4✔
910
        """Copies a created dist to an output dir.
911

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

928
    @property
4✔
929
    def _dist(self):
4✔
930
        ctx = self.ctx
4✔
931
        dist = dist_from_args(ctx, self.args)
4✔
932
        ctx.distribution = dist
4✔
933
        return dist
4✔
934

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

UNCOV
946
        fix_args = ('--dir', '--private', '--add-jar', '--add-source',
×
947
                    '--whitelist', '--blacklist', '--presplash', '--icon',
948
                    '--icon-bg', '--icon-fg')
UNCOV
949
        unknown_args = args.unknown_args
×
950

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

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

994
        return env
×
995

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

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

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

1026
            if Version(build_tools_version.replace(" ", "")) < Version('25.0'):
×
1027
                raise BuildInterruptingException(
×
1028
                    'build_tools >= 25 is required, but %s is installed' % build_tools_version)
UNCOV
1029
            if not exists("gradlew"):
×
1030
                raise BuildInterruptingException("gradlew file is missing")
×
1031

UNCOV
1032
            env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir
×
1033
            env["ANDROID_HOME"] = self.ctx.sdk_dir
×
1034

UNCOV
1035
            gradlew = sh.Command('./gradlew')
×
1036

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

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

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

UNCOV
1077
        package_glob = "*-{}.%s" % package_type
×
UNCOV
1078
        package_add_version = True
×
1079

UNCOV
1080
        self.hook("after_apk_assemble")
×
1081

1082
        info_main('# Copying android package to current directory')
×
1083

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

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

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

1121
    @require_prebuilt_dist
4✔
1122
    def apk(self, args):
4✔
1123
        output, build_args = self._build_package(args, package_type='apk')
×
UNCOV
1124
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'apk', args.build_mode)
×
UNCOV
1125
        self._finish_package(args, output, build_args, 'apk', output_dir)
×
1126

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

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

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

1146
    def archs(self, _args):
4✔
1147
        """List the target architectures available to be built for."""
UNCOV
1148
        print('{Style.BRIGHT}Available target architectures are:'
×
1149
              '{Style.RESET_ALL}'.format(Style=Out_Style))
UNCOV
1150
        for arch in self.ctx.archs:
×
UNCOV
1151
            print('    {}'.format(arch.arch))
×
1152

1153
    def dists(self, args):
4✔
1154
        """The same as :meth:`distributions`."""
1155
        self.distributions(args)
×
1156

1157
    def distributions(self, _args):
4✔
1158
        """Lists all distributions currently available (i.e. that have already
1159
        been built)."""
UNCOV
1160
        ctx = self.ctx
×
UNCOV
1161
        dists = Distribution.get_distributions(ctx)
×
1162

UNCOV
1163
        if dists:
×
1164
            print('{Style.BRIGHT}Distributions currently installed are:'
×
1165
                  '{Style.RESET_ALL}'.format(Style=Out_Style))
UNCOV
1166
            pretty_log_dists(dists, print)
×
1167
        else:
1168
            print('{Style.BRIGHT}There are no dists currently built.'
×
1169
                  '{Style.RESET_ALL}'.format(Style=Out_Style))
1170

1171
    def delete_dist(self, _args):
4✔
1172
        dist = self._dist
×
UNCOV
1173
        if not dist.folder_exists():
×
UNCOV
1174
            info('No dist exists that matches your specifications, '
×
1175
                 'exiting without deleting.')
1176
            return
×
1177
        dist.delete()
×
1178

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

1198
    def adb(self, args):
4✔
1199
        """Runs the adb binary from the detected SDK directory, passing all
1200
        arguments straight to it. This is intended as a convenience
1201
        function if adb is not in your $PATH.
1202
        """
UNCOV
1203
        self._adb(args.unknown_args)
×
1204

1205
    def logcat(self, args):
4✔
1206
        """Runs ``adb logcat`` using the adb binary from the detected SDK
1207
        directory. All extra args are passed as arguments to logcat."""
UNCOV
1208
        self._adb(['logcat'] + args.unknown_args)
×
1209

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

1228
    def recommendations(self, args):
4✔
1229
        print_recommendations()
4✔
1230

1231
    def build_status(self, _args):
4✔
1232
        """Print the status of the specified build. """
UNCOV
1233
        print('{Style.BRIGHT}Bootstraps whose core components are probably '
×
1234
              'already built:{Style.RESET_ALL}'.format(Style=Out_Style))
1235

UNCOV
1236
        bootstrap_dir = join(self.ctx.build_dir, 'bootstrap_builds')
×
1237
        if exists(bootstrap_dir):
×
UNCOV
1238
            for filen in os.listdir(bootstrap_dir):
×
UNCOV
1239
                print('    {Fore.GREEN}{Style.BRIGHT}{filen}{Style.RESET_ALL}'
×
1240
                      .format(filen=filen, Fore=Out_Fore, Style=Out_Style))
1241

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

1259

1260
if __name__ == "__main__":
1261
    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