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

kivy / python-for-android / 20193032362

13 Dec 2025 01:55PM UTC coverage: 63.841%. First build
20193032362

Pull #3271

github

web-flow
Merge 7afd7dafc into 6494ac165
Pull Request #3271: `toolchain`: auto resolve deps

1806 of 3087 branches covered (58.5%)

Branch coverage included in aggregate %.

43 of 65 new or added lines in 3 files covered. (66.15%)

5251 of 7967 relevant lines covered (65.91%)

5.26 hits per line

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

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

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

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

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

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

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

52
from packaging.utils import parse_wheel_filename
8✔
53
from packaging.requirements import Requirement
8✔
54

55
user_dir = dirname(realpath(os.path.curdir))
8✔
56
toolchain_dir = dirname(__file__)
8✔
57
sys.path.insert(0, join(toolchain_dir, "tools", "external"))
8✔
58

59

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

68
    def add_dashes(x):
8✔
69
        return x if x.startswith("-") else "--"+x
8✔
70

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

86

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

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

112

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

127

128
def is_wheel_platform_independent(whl_name):
8✔
129
    name, version, build, tags = parse_wheel_filename(whl_name)
8✔
130
    return all(tag.platform == "any" for tag in tags)
8✔
131

132

133
def process_python_modules(build_order, modules):
8✔
134
    """
135
    idk what this does
136
    """
137
    modules = list(modules)
8✔
138
    build_order = list(build_order)
8✔
139
    _requirement_names = []
8✔
140
    processed_modules = []
8✔
141

142
    for module in modules+build_order:
8✔
143
        try:
8✔
144
            _requirement_names.append(Requirement(module).name)
8✔
NEW
145
        except Exception:
×
NEW
146
            processed_modules.append(module)
×
NEW
147
            if module in modules:
×
NEW
148
                modules.remove(module)
×
149

150
    if len(processed_modules) > 0:
8!
NEW
151
        warning(f'Ignored by module resolver : {processed_modules}')
×
152

153
    processed_modules.extend(modules)
8✔
154

155
    # need a tempfile for pip report output
156
    path = os.path.realpath(".pip_report.json")
8✔
157

158
    if not exists(path):
8!
159
        shprint(
8✔
160
            sh.pip, 'install', *modules,
161
            '--dry-run', '--break-system-packages', '--ignore-installed',
162
            '--report', path, '-q'
163
        )
164

165
    with open(path, "r") as f:
8!
166
        try:
8✔
167
            report = json.load(f)
8✔
NEW
168
        except Exception:
×
NEW
169
            os.remove(path)
×
NEW
170
            return process_python_modules(modules)
×
171

172
    info('Extra resolved pure python dependencies :')
8✔
173

174
    ignored_str = " (ignored)"
8✔
175
    # did we find any non pure python package?
176
    any_not_pure_python = False
8✔
177

178
    info(" ")
8✔
179
    for module in report["install"]:
8✔
180

181
        mname = module["metadata"]["name"]
8✔
182
        mver = module["metadata"]["version"]
8✔
183
        filename = basename(module["download_info"]["url"])
8✔
184
        pure_python = True
8✔
185

186
        if (filename.endswith(".whl") and not is_wheel_platform_independent(filename)):
8!
NEW
187
            any_not_pure_python = True
×
NEW
188
            pure_python = False
×
189

190
        # does this module matches any recipe name?
191
        if mname.lower() in _requirement_names:
8!
192
            continue
8✔
193

NEW
194
        color = Out_Fore.GREEN if pure_python else Out_Fore.RED
×
NEW
195
        ignored = "" if pure_python else ignored_str
×
196

NEW
197
        info(
×
198
            f"  {color}{mname}{Out_Fore.WHITE} : "
199
            f"{Out_Style.BRIGHT}{mver}{Out_Style.RESET_ALL}"
200
            f"{ignored}"
201
        )
202

NEW
203
        if pure_python:
×
NEW
204
            processed_modules.append(f"{mname}=={mver}")
×
205
    info(" ")
8✔
206

207
    if any_not_pure_python:
8!
NEW
208
        warning("Some packages were ignored because they are not pure Python.")
×
NEW
209
        warning("To install the ignored packages, explicitly list them in your requirements file.")
×
210

211
    return processed_modules
8✔
212

213

214
def build_dist_from_args(ctx, dist, args):
8✔
215
    """Parses out any bootstrap related arguments, and uses them to build
216
    a dist."""
217
    bs = Bootstrap.get_bootstrap(args.bootstrap, ctx)
8✔
218
    blacklist = getattr(args, "blacklist_requirements", "").split(",")
8✔
219
    if len(blacklist) == 1 and blacklist[0] == "":
8!
220
        blacklist = []
8✔
221
    build_order, python_modules, bs = (
8✔
222
        get_recipe_order_and_bootstrap(
223
            ctx, dist.recipes, bs,
224
            blacklist=blacklist
225
        ))
226
    assert set(build_order).intersection(set(python_modules)) == set()
8✔
227

228
    ctx.recipe_build_order = build_order
8✔
229
    ctx.python_modules = process_python_modules(build_order, python_modules)
8✔
230

231
    info('The selected bootstrap is {}'.format(bs.name))
8✔
232
    info_main('# Creating dist with {} bootstrap'.format(bs.name))
8✔
233
    bs.distribution = dist
8✔
234
    info_notify('Dist will have name {} and requirements ({})'.format(
8✔
235
        dist.name, ', '.join(dist.recipes)))
236
    info('Dist contains the following requirements as recipes: {}'.format(
8✔
237
        ctx.recipe_build_order))
238
    info('Dist will also contain modules ({}) installed from pip'.format(
8✔
239
        ', '.join(ctx.python_modules)))
240
    info(
8✔
241
        'Dist will be build in mode {build_mode}{with_debug_symbols}'.format(
242
            build_mode='debug' if ctx.build_as_debuggable else 'release',
243
            with_debug_symbols=' (with debug symbols)'
244
            if ctx.with_debug_symbols
245
            else '',
246
        )
247
    )
248

249
    ctx.distribution = dist
8✔
250
    ctx.prepare_bootstrap(bs)
8✔
251
    if dist.needs_build:
8!
252
        ctx.prepare_dist()
8✔
253

254
    build_recipes(build_order, python_modules, ctx,
8✔
255
                  getattr(args, "private", None),
256
                  ignore_project_setup_py=getattr(
257
                      args, "ignore_setup_py", False
258
                  ),
259
                 )
260

261
    ctx.bootstrap.assemble_distribution()
8✔
262

263
    info_main('# Your distribution was created successfully, exiting.')
8✔
264
    info('Dist can be found at (for now) {}'
8✔
265
         .format(join(ctx.dist_dir, ctx.distribution.dist_dir)))
266

267

268
def split_argument_list(arg_list):
8✔
269
    if not len(arg_list):
8✔
270
        return []
8✔
271
    return re.split(r'[ ,]+', arg_list)
8✔
272

273

274
class NoAbbrevParser(argparse.ArgumentParser):
8✔
275
    """We want to disable argument abbreviation so as not to interfere
276
    with passing through arguments to build.py, but in python2 argparse
277
    doesn't have this option.
278

279
    This subclass alternative is follows the suggestion at
280
    https://bugs.python.org/issue14910.
281
    """
282
    def _get_option_tuples(self, option_string):
8✔
283
        return []
8✔
284

285

286
class ToolchainCL:
8✔
287

288
    def __init__(self):
8✔
289

290
        argv = sys.argv
8✔
291
        self.warn_on_carriage_return_args(argv)
8✔
292
        # Buildozer used to pass these arguments in a now-invalid order
293
        # If that happens, apply this fix
294
        # This fix will be removed once a fixed buildozer is released
295
        if (len(argv) > 2
8!
296
                and argv[1].startswith('--color')
297
                and argv[2].startswith('--storage-dir')):
298
            argv.append(argv.pop(1))  # the --color arg
×
299
            argv.append(argv.pop(1))  # the --storage-dir arg
×
300

301
        parser = NoAbbrevParser(
8✔
302
            description='A packaging tool for turning Python scripts and apps '
303
                        'into Android APKs')
304

305
        generic_parser = argparse.ArgumentParser(
8✔
306
            add_help=False,
307
            description='Generic arguments applied to all commands')
308
        argparse.ArgumentParser(
8✔
309
            add_help=False, description='Arguments for dist building')
310

311
        generic_parser.add_argument(
8✔
312
            '--debug', dest='debug', action='store_true', default=False,
313
            help='Display debug output and all build info')
314
        generic_parser.add_argument(
8✔
315
            '--color', dest='color', choices=['always', 'never', 'auto'],
316
            help='Enable or disable color output (default enabled on tty)')
317
        generic_parser.add_argument(
8✔
318
            '--sdk-dir', '--sdk_dir', dest='sdk_dir', default='',
319
            help='The filepath where the Android SDK is installed')
320
        generic_parser.add_argument(
8✔
321
            '--ndk-dir', '--ndk_dir', dest='ndk_dir', default='',
322
            help='The filepath where the Android NDK is installed')
323
        generic_parser.add_argument(
8✔
324
            '--android-api',
325
            '--android_api',
326
            dest='android_api',
327
            default=0,
328
            type=int,
329
            help=('The Android API level to build against defaults to {} if '
330
                  'not specified.').format(RECOMMENDED_TARGET_API))
331
        generic_parser.add_argument(
8✔
332
            '--ndk-version', '--ndk_version', dest='ndk_version', default=None,
333
            help=('DEPRECATED: the NDK version is now found automatically or '
334
                  'not at all.'))
335
        generic_parser.add_argument(
8✔
336
            '--ndk-api', type=int, default=None,
337
            help=('The Android API level to compile against. This should be your '
338
                  '*minimal supported* API, not normally the same as your --android-api. '
339
                  'Defaults to min(ANDROID_API, {}) if not specified.').format(RECOMMENDED_NDK_API))
340
        generic_parser.add_argument(
8✔
341
            '--symlink-bootstrap-files', '--ssymlink_bootstrap_files',
342
            action='store_true',
343
            dest='symlink_bootstrap_files',
344
            default=False,
345
            help=('If True, symlinks the bootstrap files '
346
                  'creation. This is useful for development only, it could also'
347
                  ' cause weird problems.'))
348

349
        default_storage_dir = user_data_dir('python-for-android')
8✔
350
        if ' ' in default_storage_dir:
8!
351
            default_storage_dir = '~/.python-for-android'
×
352
        generic_parser.add_argument(
8✔
353
            '--storage-dir', dest='storage_dir', default=default_storage_dir,
354
            help=('Primary storage directory for downloads and builds '
355
                  '(default: {})'.format(default_storage_dir)))
356

357
        generic_parser.add_argument(
8✔
358
            '--arch', help='The archs to build for.',
359
            action='append', default=[])
360

361
        # Options for specifying the Distribution
362
        generic_parser.add_argument(
8✔
363
            '--dist-name', '--dist_name',
364
            help='The name of the distribution to use or create', default='')
365

366
        generic_parser.add_argument(
8✔
367
            '--requirements',
368
            help=('Dependencies of your app, should be recipe names or '
369
                  'Python modules. NOT NECESSARY if you are using '
370
                  'Python 3 with --use-setup-py'),
371
            default='')
372

373
        generic_parser.add_argument(
8✔
374
            '--recipe-blacklist',
375
            help=('Blacklist an internal recipe from use. Allows '
376
                  'disabling Python 3 core modules to save size'),
377
            dest="recipe_blacklist",
378
            default='')
379

380
        generic_parser.add_argument(
8✔
381
            '--blacklist-requirements',
382
            help=('Blacklist an internal recipe from use. Allows '
383
                  'disabling Python 3 core modules to save size'),
384
            dest="blacklist_requirements",
385
            default='')
386

387
        generic_parser.add_argument(
8✔
388
            '--bootstrap',
389
            help='The bootstrap to build with. Leave unset to choose '
390
                 'automatically.',
391
            default=None)
392

393
        generic_parser.add_argument(
8✔
394
            '--hook',
395
            help='Filename to a module that contains python-for-android hooks',
396
            default=None)
397

398
        add_boolean_option(
8✔
399
            generic_parser, ["force-build"],
400
            default=False,
401
            description='Whether to force compilation of a new distribution')
402

403
        add_boolean_option(
8✔
404
            generic_parser, ["require-perfect-match"],
405
            default=False,
406
            description=('Whether the dist recipes must perfectly match '
407
                         'those requested'))
408

409
        add_boolean_option(
8✔
410
            generic_parser, ["allow-replace-dist"],
411
            default=True,
412
            description='Whether existing dist names can be automatically replaced'
413
            )
414

415
        generic_parser.add_argument(
8✔
416
            '--local-recipes', '--local_recipes',
417
            dest='local_recipes', default='./p4a-recipes',
418
            help='Directory to look for local recipes')
419

420
        generic_parser.add_argument(
8✔
421
            '--activity-class-name',
422
            dest='activity_class_name', default='org.kivy.android.PythonActivity',
423
            help='The full java class name of the main activity')
424

425
        generic_parser.add_argument(
8✔
426
            '--service-class-name',
427
            dest='service_class_name', default='org.kivy.android.PythonService',
428
            help='Full java package name of the PythonService class')
429

430
        generic_parser.add_argument(
8✔
431
            '--java-build-tool',
432
            dest='java_build_tool', default='auto',
433
            choices=['auto', 'ant', 'gradle'],
434
            help=('The java build tool to use when packaging the APK, defaults '
435
                  'to automatically selecting an appropriate tool.'))
436

437
        add_boolean_option(
8✔
438
            generic_parser, ['copy-libs'],
439
            default=False,
440
            description='Copy libraries instead of using biglink (Android 4.3+)'
441
        )
442

443
        self._read_configuration()
8✔
444

445
        subparsers = parser.add_subparsers(dest='subparser_name',
8✔
446
                                           help='The command to run')
447

448
        def add_parser(subparsers, *args, **kwargs):
8✔
449
            """
450
            argparse in python2 doesn't support the aliases option,
451
            so we just don't provide the aliases there.
452
            """
453
            if 'aliases' in kwargs and sys.version_info.major < 3:
8!
454
                kwargs.pop('aliases')
×
455
            return subparsers.add_parser(*args, **kwargs)
8✔
456

457
        add_parser(
8✔
458
            subparsers,
459
            'recommendations',
460
            parents=[generic_parser],
461
            help='List recommended p4a dependencies')
462
        parser_recipes = add_parser(
8✔
463
            subparsers,
464
            'recipes',
465
            parents=[generic_parser],
466
            help='List the available recipes')
467
        parser_recipes.add_argument(
8✔
468
            "--compact",
469
            action="store_true", default=False,
470
            help="Produce a compact list suitable for scripting")
471
        add_parser(
8✔
472
            subparsers, 'bootstraps',
473
            help='List the available bootstraps',
474
            parents=[generic_parser])
475
        add_parser(
8✔
476
            subparsers, 'clean_all',
477
            aliases=['clean-all'],
478
            help='Delete all builds, dists and caches',
479
            parents=[generic_parser])
480
        add_parser(
8✔
481
            subparsers, 'clean_dists',
482
            aliases=['clean-dists'],
483
            help='Delete all dists',
484
            parents=[generic_parser])
485
        add_parser(
8✔
486
            subparsers, 'clean_bootstrap_builds',
487
            aliases=['clean-bootstrap-builds'],
488
            help='Delete all bootstrap builds',
489
            parents=[generic_parser])
490
        add_parser(
8✔
491
            subparsers, 'clean_builds',
492
            aliases=['clean-builds'],
493
            help='Delete all builds',
494
            parents=[generic_parser])
495

496
        parser_clean = add_parser(
8✔
497
            subparsers, 'clean',
498
            help='Delete build components.',
499
            parents=[generic_parser])
500
        parser_clean.add_argument(
8✔
501
            'component', nargs='+',
502
            help=('The build component(s) to delete. You can pass any '
503
                  'number of arguments from "all", "builds", "dists", '
504
                  '"distributions", "bootstrap_builds", "downloads".'))
505

506
        parser_clean_recipe_build = add_parser(
8✔
507
            subparsers,
508
            'clean_recipe_build', aliases=['clean-recipe-build'],
509
            help=('Delete the build components of the given recipe. '
510
                  'By default this will also delete built dists'),
511
            parents=[generic_parser])
512
        parser_clean_recipe_build.add_argument(
8✔
513
            'recipe', help='The recipe name')
514
        parser_clean_recipe_build.add_argument(
8✔
515
            '--no-clean-dists', default=False,
516
            dest='no_clean_dists',
517
            action='store_true',
518
            help='If passed, do not delete existing dists')
519

520
        parser_clean_download_cache = add_parser(
8✔
521
            subparsers,
522
            'clean_download_cache', aliases=['clean-download-cache'],
523
            help='Delete cached downloads for requirement builds',
524
            parents=[generic_parser])
525
        parser_clean_download_cache.add_argument(
8✔
526
            'recipes',
527
            nargs='*',
528
            help='The recipes to clean (space-separated). If no recipe name is'
529
                  ' provided, the entire cache is cleared.')
530

531
        parser_export_dist = add_parser(
8✔
532
            subparsers,
533
            'export_dist', aliases=['export-dist'],
534
            help='Copy the named dist to the given path',
535
            parents=[generic_parser])
536
        parser_export_dist.add_argument('output_dir',
8✔
537
                                        help='The output dir to copy to')
538
        parser_export_dist.add_argument(
8✔
539
            '--symlink',
540
            action='store_true',
541
            help='Symlink the dist instead of copying')
542

543
        parser_packaging = argparse.ArgumentParser(
8✔
544
            parents=[generic_parser],
545
            add_help=False,
546
            description='common options for packaging (apk, aar)')
547

548
        # This is actually an internal argument of the build.py
549
        # (see pythonforandroid/bootstraps/common/build/build.py).
550
        # However, it is also needed before the distribution is finally
551
        # assembled for locating the setup.py / other build systems, which
552
        # is why we also add it here:
553
        parser_packaging.add_argument(
8✔
554
            '--add-asset', dest='assets',
555
            action="append", default=[],
556
            help='Put this in the assets folder in the apk.')
557
        parser_packaging.add_argument(
8✔
558
            '--add-resource', dest='resources',
559
            action="append", default=[],
560
            help='Put this in the res folder in the apk.')
561
        parser_packaging.add_argument(
8✔
562
            '--private', dest='private',
563
            help='the directory with the app source code files' +
564
                 ' (containing your main.py entrypoint)',
565
            required=False, default=None)
566
        parser_packaging.add_argument(
8✔
567
            '--use-setup-py', dest="use_setup_py",
568
            action='store_true', default=False,
569
            help="Process the setup.py of a project if present. " +
570
                 "(Experimental!")
571
        parser_packaging.add_argument(
8✔
572
            '--ignore-setup-py', dest="ignore_setup_py",
573
            action='store_true', default=False,
574
            help="Don't run the setup.py of a project if present. " +
575
                 "This may be required if the setup.py is not " +
576
                 "designed to work inside p4a (e.g. by installing " +
577
                 "dependencies that won't work or aren't desired " +
578
                 "on Android")
579
        parser_packaging.add_argument(
8✔
580
            '--release', dest='build_mode', action='store_const',
581
            const='release', default='debug',
582
            help='Build your app as a non-debug release build. '
583
                 '(Disables gdb debugging among other things)')
584
        parser_packaging.add_argument(
8✔
585
            '--with-debug-symbols', dest='with_debug_symbols',
586
            action='store_const', const=True, default=False,
587
            help='Will keep debug symbols from `.so` files.')
588
        parser_packaging.add_argument(
8✔
589
            '--keystore', dest='keystore', action='store', default=None,
590
            help=('Keystore for JAR signing key, will use jarsigner '
591
                  'default if not specified (release build only)'))
592
        parser_packaging.add_argument(
8✔
593
            '--signkey', dest='signkey', action='store', default=None,
594
            help='Key alias to sign PARSER_APK. with (release build only)')
595
        parser_packaging.add_argument(
8✔
596
            '--keystorepw', dest='keystorepw', action='store', default=None,
597
            help='Password for keystore')
598
        parser_packaging.add_argument(
8✔
599
            '--signkeypw', dest='signkeypw', action='store', default=None,
600
            help='Password for key alias')
601

602
        add_parser(
8✔
603
            subparsers,
604
            'aar', help='Build an AAR',
605
            parents=[parser_packaging])
606

607
        add_parser(
8✔
608
            subparsers,
609
            'apk', help='Build an APK',
610
            parents=[parser_packaging])
611

612
        add_parser(
8✔
613
            subparsers,
614
            'aab', help='Build an AAB',
615
            parents=[parser_packaging])
616

617
        add_parser(
8✔
618
            subparsers,
619
            'create', help='Compile a set of requirements into a dist',
620
            parents=[generic_parser])
621
        add_parser(
8✔
622
            subparsers,
623
            'archs', help='List the available target architectures',
624
            parents=[generic_parser])
625
        add_parser(
8✔
626
            subparsers,
627
            'distributions', aliases=['dists'],
628
            help='List the currently available (compiled) dists',
629
            parents=[generic_parser])
630
        add_parser(
8✔
631
            subparsers,
632
            'delete_dist', aliases=['delete-dist'], help='Delete a compiled dist',
633
            parents=[generic_parser])
634

635
        parser_sdk_tools = add_parser(
8✔
636
            subparsers,
637
            'sdk_tools', aliases=['sdk-tools'],
638
            help='Run the given binary from the SDK tools dis',
639
            parents=[generic_parser])
640
        parser_sdk_tools.add_argument(
8✔
641
            'tool', help='The binary tool name to run')
642

643
        add_parser(
8✔
644
            subparsers,
645
            'adb', help='Run adb from the given SDK',
646
            parents=[generic_parser])
647
        add_parser(
8✔
648
            subparsers,
649
            'logcat', help='Run logcat from the given SDK',
650
            parents=[generic_parser])
651
        add_parser(
8✔
652
            subparsers,
653
            'build_status', aliases=['build-status'],
654
            help='Print some debug information about current built components',
655
            parents=[generic_parser])
656

657
        parser.add_argument('-v', '--version', action='version',
8✔
658
                            version=__version__)
659

660
        args, unknown = parser.parse_known_args(sys.argv[1:])
8✔
661
        args.unknown_args = unknown
8✔
662

663
        if getattr(args, "private", None) is not None:
8!
664
            # Pass this value on to the internal bootstrap build.py:
665
            args.unknown_args += ["--private", args.private]
×
666
        if getattr(args, "build_mode", None) == "release":
8!
667
            args.unknown_args += ["--release"]
×
668
        if getattr(args, "with_debug_symbols", False):
8!
669
            args.unknown_args += ["--with-debug-symbols"]
×
670
        if getattr(args, "ignore_setup_py", False):
8!
671
            args.use_setup_py = False
×
672
        if getattr(args, "activity_class_name", "org.kivy.android.PythonActivity") != 'org.kivy.android.PythonActivity':
8✔
673
            args.unknown_args += ["--activity-class-name", args.activity_class_name]
8✔
674
        if getattr(args, "service_class_name", "org.kivy.android.PythonService") != 'org.kivy.android.PythonService':
8✔
675
            args.unknown_args += ["--service-class-name", args.service_class_name]
8✔
676

677
        self.args = args
8✔
678

679
        if args.subparser_name is None:
8✔
680
            parser.print_help()
8✔
681
            exit(1)
8✔
682

683
        setup_color(args.color)
8✔
684

685
        if args.debug:
8!
686
            logger.setLevel(logging.DEBUG)
×
687

688
        self.ctx = Context()
8✔
689
        self.ctx.use_setup_py = getattr(args, "use_setup_py", True)
8✔
690
        self.ctx.build_as_debuggable = getattr(
8✔
691
            args, "build_mode", "debug"
692
        ) == "debug"
693
        self.ctx.with_debug_symbols = getattr(
8✔
694
            args, "with_debug_symbols", False
695
        )
696

697
        # Process requirements and put version in environ
698
        if hasattr(args, 'requirements'):
8!
699
            requirements = []
8✔
700

701
            # Add dependencies from setup.py, but only if they are recipes
702
            # (because otherwise, setup.py itself will install them later)
703
            if (project_has_setup_py(getattr(args, "private", None)) and
8!
704
                    getattr(args, "use_setup_py", False)):
705
                try:
×
706
                    info("Analyzing package dependencies. MAY TAKE A WHILE.")
×
707
                    # Get all the dependencies corresponding to a recipe:
708
                    dependencies = [
×
709
                        dep.lower() for dep in
710
                        get_dep_names_of_package(
711
                            args.private,
712
                            keep_version_pins=True,
713
                            recursive=True,
714
                            verbose=True,
715
                        )
716
                    ]
717
                    info("Dependencies obtained: " + str(dependencies))
×
718
                    all_recipes = [
×
719
                        recipe.lower() for recipe in
720
                        set(Recipe.list_recipes(self.ctx))
721
                    ]
722
                    dependencies = set(dependencies).intersection(
×
723
                        set(all_recipes)
724
                    )
725
                    # Add dependencies to argument list:
726
                    if len(dependencies) > 0:
×
727
                        if len(args.requirements) > 0:
×
728
                            args.requirements += u","
×
729
                        args.requirements += u",".join(dependencies)
×
730
                except ValueError:
×
731
                    # Not a python package, apparently.
732
                    warning(
×
733
                        "Processing failed, is this project a valid "
734
                        "package? Will continue WITHOUT setup.py deps."
735
                    )
736

737
            # Parse --requirements argument list:
738
            for requirement in split_argument_list(args.requirements):
8✔
739
                if "==" in requirement:
8!
740
                    requirement, version = requirement.split(u"==", 1)
×
741
                    os.environ["VERSION_{}".format(requirement)] = version
×
742
                    info('Recipe {}: version "{}" requested'.format(
×
743
                        requirement, version))
744
                requirements.append(requirement)
8✔
745
            args.requirements = u",".join(requirements)
8✔
746

747
        self.warn_on_deprecated_args(args)
8✔
748

749
        self.storage_dir = args.storage_dir
8✔
750
        self.ctx.setup_dirs(self.storage_dir)
8✔
751
        self.sdk_dir = args.sdk_dir
8✔
752
        self.ndk_dir = args.ndk_dir
8✔
753
        self.android_api = args.android_api
8✔
754
        self.ndk_api = args.ndk_api
8✔
755
        self.ctx.symlink_bootstrap_files = args.symlink_bootstrap_files
8✔
756
        self.ctx.java_build_tool = args.java_build_tool
8✔
757

758
        self._archs = args.arch
8✔
759

760
        self.ctx.local_recipes = realpath(args.local_recipes)
8✔
761
        self.ctx.copy_libs = args.copy_libs
8✔
762

763
        self.ctx.activity_class_name = args.activity_class_name
8✔
764
        self.ctx.service_class_name = args.service_class_name
8✔
765

766
        # Each subparser corresponds to a method
767
        command = args.subparser_name.replace('-', '_')
8✔
768
        getattr(self, command)(args)
8✔
769

770
    @staticmethod
8✔
771
    def warn_on_carriage_return_args(args):
8✔
772
        for check_arg in args:
8✔
773
            if '\r' in check_arg:
8!
774
                warning("Argument '{}' contains a carriage return (\\r).".format(str(check_arg.replace('\r', ''))))
×
775
                warning("Invoking this program via scripts which use CRLF instead of LF line endings will have undefined behaviour.")
×
776

777
    def warn_on_deprecated_args(self, args):
8✔
778
        """
779
        Print warning messages for any deprecated arguments that were passed.
780
        """
781

782
        # Output warning if setup.py is present and neither --ignore-setup-py
783
        # nor --use-setup-py was specified.
784
        if project_has_setup_py(getattr(args, "private", None)):
8!
785
            if not getattr(args, "use_setup_py", False) and \
×
786
                    not getattr(args, "ignore_setup_py", False):
787
                warning("  **** FUTURE BEHAVIOR CHANGE WARNING ****")
×
788
                warning("Your project appears to contain a setup.py file.")
×
789
                warning("Currently, these are ignored by default.")
×
790
                warning("This will CHANGE in an upcoming version!")
×
791
                warning("")
×
792
                warning("To ensure your setup.py is ignored, please specify:")
×
793
                warning("    --ignore-setup-py")
×
794
                warning("")
×
795
                warning("To enable what will some day be the default, specify:")
×
796
                warning("    --use-setup-py")
×
797

798
        # NDK version is now determined automatically
799
        if args.ndk_version is not None:
8!
800
            warning('--ndk-version is deprecated and no longer necessary, '
×
801
                    'the value you passed is ignored')
802
        if 'ANDROIDNDKVER' in environ:
8!
803
            warning('$ANDROIDNDKVER is deprecated and no longer necessary, '
×
804
                    'the value you set is ignored')
805

806
    def hook(self, name):
8✔
807
        if not self.args.hook:
×
808
            return
×
809
        if not hasattr(self, "hook_module"):
×
810
            # first time, try to load the hook module
811
            self.hook_module = load_source(
×
812
                "pythonforandroid.hook", self.args.hook)
813
        if hasattr(self.hook_module, name):
×
814
            info("Hook: execute {}".format(name))
×
815
            getattr(self.hook_module, name)(self)
×
816
        else:
817
            info("Hook: ignore {}".format(name))
×
818

819
    @property
8✔
820
    def default_storage_dir(self):
8✔
821
        udd = user_data_dir('python-for-android')
×
822
        if ' ' in udd:
×
823
            udd = '~/.python-for-android'
×
824
        return udd
×
825

826
    @staticmethod
8✔
827
    def _read_configuration():
8✔
828
        # search for a .p4a configuration file in the current directory
829
        if not exists(".p4a"):
8!
830
            return
8✔
831
        info("Reading .p4a configuration")
×
832
        with open(".p4a") as fd:
×
833
            lines = fd.readlines()
×
834
        lines = [shlex.split(line)
×
835
                 for line in lines if not line.startswith("#")]
836
        for line in lines:
×
837
            for arg in line:
×
838
                sys.argv.append(arg)
×
839

840
    def recipes(self, args):
8✔
841
        """
842
        Prints recipes basic info, e.g.
843
        .. code-block:: bash
844

845
            python3      3.7.1
846
                depends: ['hostpython3', 'sqlite3', 'openssl', 'libffi']
847
                conflicts: []
848
                optional depends: ['sqlite3', 'libffi', 'openssl']
849
        """
850
        ctx = self.ctx
8✔
851
        if args.compact:
8!
852
            print(" ".join(set(Recipe.list_recipes(ctx))))
×
853
        else:
854
            for name in sorted(Recipe.list_recipes(ctx)):
8✔
855
                try:
8✔
856
                    recipe = Recipe.get_recipe(name, ctx)
8✔
857
                except (IOError, ValueError):
×
858
                    warning('Recipe "{}" could not be loaded'.format(name))
×
859
                except SyntaxError:
×
860
                    import traceback
×
861
                    traceback.print_exc()
×
862
                    warning(('Recipe "{}" could not be loaded due to a '
×
863
                             'syntax error').format(name))
864
                version = str(recipe.version)
8✔
865
                print('{Fore.BLUE}{Style.BRIGHT}{recipe.name:<12} '
8✔
866
                      '{Style.RESET_ALL}{Fore.LIGHTBLUE_EX}'
867
                      '{version:<8}{Style.RESET_ALL}'.format(
868
                            recipe=recipe, Fore=Out_Fore, Style=Out_Style,
869
                            version=version))
870
                print('    {Fore.GREEN}depends: {recipe.depends}'
8✔
871
                      '{Fore.RESET}'.format(recipe=recipe, Fore=Out_Fore))
872
                if recipe.conflicts:
8✔
873
                    print('    {Fore.RED}conflicts: {recipe.conflicts}'
8✔
874
                          '{Fore.RESET}'
875
                          .format(recipe=recipe, Fore=Out_Fore))
876
                if recipe.opt_depends:
8✔
877
                    print('    {Fore.YELLOW}optional depends: '
8✔
878
                          '{recipe.opt_depends}{Fore.RESET}'
879
                          .format(recipe=recipe, Fore=Out_Fore))
880

881
    def bootstraps(self, _args):
8✔
882
        """List all the bootstraps available to build with."""
883
        for bs in Bootstrap.all_bootstraps():
×
884
            bs = Bootstrap.get_bootstrap(bs, self.ctx)
×
885
            print('{Fore.BLUE}{Style.BRIGHT}{bs.name}{Style.RESET_ALL}'
×
886
                  .format(bs=bs, Fore=Out_Fore, Style=Out_Style))
887
            print('    {Fore.GREEN}depends: {bs.recipe_depends}{Fore.RESET}'
×
888
                  .format(bs=bs, Fore=Out_Fore))
889

890
    def clean(self, args):
8✔
891
        components = args.component
×
892

893
        component_clean_methods = {
×
894
            'all': self.clean_all,
895
            'dists': self.clean_dists,
896
            'distributions': self.clean_dists,
897
            'builds': self.clean_builds,
898
            'bootstrap_builds': self.clean_bootstrap_builds,
899
            'downloads': self.clean_download_cache}
900

901
        for component in components:
×
902
            if component not in component_clean_methods:
×
903
                raise BuildInterruptingException((
×
904
                    'Asked to clean "{}" but this argument is not '
905
                    'recognised'.format(component)))
906
            component_clean_methods[component](args)
×
907

908
    def clean_all(self, args):
8✔
909
        """Delete all build components; the package cache, package builds,
910
        bootstrap builds and distributions."""
911
        self.clean_dists(args)
×
912
        self.clean_builds(args)
×
913
        self.clean_download_cache(args)
×
914

915
    def clean_dists(self, _args):
8✔
916
        """Delete all compiled distributions in the internal distribution
917
        directory."""
918
        ctx = self.ctx
×
919
        rmdir(ctx.dist_dir)
×
920

921
    def clean_bootstrap_builds(self, _args):
8✔
922
        """Delete all the bootstrap builds."""
923
        rmdir(join(self.ctx.build_dir, 'bootstrap_builds'))
×
924
        # for bs in Bootstrap.all_bootstraps():
925
        #     bs = Bootstrap.get_bootstrap(bs, self.ctx)
926
        #     if bs.build_dir and exists(bs.build_dir):
927
        #         info('Cleaning build for {} bootstrap.'.format(bs.name))
928
        #         rmdir(bs.build_dir)
929

930
    def clean_builds(self, _args):
8✔
931
        """Delete all build caches for each recipe, python-install, java code
932
        and compiled libs collection.
933

934
        This does *not* delete the package download cache or the final
935
        distributions.  You can also use clean_recipe_build to delete the build
936
        of a specific recipe.
937
        """
938
        ctx = self.ctx
×
939
        rmdir(ctx.build_dir)
×
940
        rmdir(ctx.python_installs_dir)
×
941
        libs_dir = join(self.ctx.build_dir, 'libs_collections')
×
942
        rmdir(libs_dir)
×
943

944
    def clean_recipe_build(self, args):
8✔
945
        """Deletes the build files of the given recipe.
946

947
        This is intended for debug purposes. You may experience
948
        strange behaviour or problems with some recipes if their
949
        build has made unexpected state changes. If this happens, run
950
        clean_builds, or attempt to clean other recipes until things
951
        work again.
952
        """
953
        recipe = Recipe.get_recipe(args.recipe, self.ctx)
×
954
        info('Cleaning build for {} recipe.'.format(recipe.name))
×
955
        recipe.clean_build()
×
956
        if not args.no_clean_dists:
×
957
            self.clean_dists(args)
×
958

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

963
            p4a clean_download_cache kivy,pyjnius
964

965
        This does *not* delete the build caches or final distributions.
966
        """
967
        ctx = self.ctx
×
968
        if hasattr(args, 'recipes') and args.recipes:
×
969
            for package in args.recipes:
×
970
                remove_path = join(ctx.packages_path, package)
×
971
                if exists(remove_path):
×
972
                    rmdir(remove_path)
×
973
                    info('Download cache removed for: "{}"'.format(package))
×
974
                else:
975
                    warning('No download cache found for "{}", skipping'.format(
×
976
                        package))
977
        else:
978
            if exists(ctx.packages_path):
×
979
                rmdir(ctx.packages_path)
×
980
                info('Download cache removed.')
×
981
            else:
982
                print('No cache found at "{}"'.format(ctx.packages_path))
×
983

984
    @require_prebuilt_dist
8✔
985
    def export_dist(self, args):
8✔
986
        """Copies a created dist to an output dir.
987

988
        This makes it easy to navigate to the dist to investigate it
989
        or call build.py, though you do not in general need to do this
990
        and can use the apk command instead.
991
        """
992
        ctx = self.ctx
×
993
        dist = dist_from_args(ctx, args)
×
994
        if dist.needs_build:
×
995
            raise BuildInterruptingException(
×
996
                'You asked to export a dist, but there is no dist '
997
                'with suitable recipes available. For now, you must '
998
                ' create one first with the create argument.')
999
        if args.symlink:
×
1000
            shprint(sh.ln, '-s', dist.dist_dir, args.output_dir)
×
1001
        else:
1002
            shprint(sh.cp, '-r', dist.dist_dir, args.output_dir)
×
1003

1004
    @property
8✔
1005
    def _dist(self):
8✔
1006
        ctx = self.ctx
8✔
1007
        dist = dist_from_args(ctx, self.args)
8✔
1008
        ctx.distribution = dist
8✔
1009
        return dist
8✔
1010

1011
    @staticmethod
8✔
1012
    def _fix_args(args):
8✔
1013
        """
1014
        Manually fixing these arguments at the string stage is
1015
        unsatisfactory and should probably be changed somehow, but
1016
        we can't leave it until later as the build.py scripts assume
1017
        they are in the current directory.
1018
        works in-place
1019
        :param args: parser args
1020
        """
1021

1022
        fix_args = ('--dir', '--private', '--add-jar', '--add-source',
×
1023
                    '--whitelist', '--blacklist', '--presplash', '--icon',
1024
                    '--icon-bg', '--icon-fg')
1025
        unknown_args = args.unknown_args
×
1026

1027
        for asset in args.assets:
×
1028
            if ":" in asset:
×
1029
                asset_src, asset_dest = asset.split(":")
×
1030
            else:
1031
                asset_src = asset_dest = asset
×
1032
            # take abspath now, because build.py will be run in bootstrap dir
1033
            unknown_args += ["--asset", os.path.abspath(asset_src)+":"+asset_dest]
×
1034
        for resource in args.resources:
×
1035
            if ":" in resource:
×
1036
                resource_src, resource_dest = resource.split(":")
×
1037
            else:
1038
                resource_src = resource
×
1039
                resource_dest = ""
×
1040
            # take abspath now, because build.py will be run in bootstrap dir
1041
            unknown_args += ["--resource", os.path.abspath(resource_src)+":"+resource_dest]
×
1042
        for i, arg in enumerate(unknown_args):
×
1043
            argx = arg.split('=')
×
1044
            if argx[0] in fix_args:
×
1045
                if len(argx) > 1:
×
1046
                    unknown_args[i] = '='.join(
×
1047
                        (argx[0], realpath(expanduser(argx[1]))))
1048
                elif i + 1 < len(unknown_args):
×
1049
                    unknown_args[i+1] = realpath(expanduser(unknown_args[i+1]))
×
1050

1051
    @staticmethod
8✔
1052
    def _prepare_release_env(args):
8✔
1053
        """
1054
        prepares envitonment dict with the necessary flags for signing an apk
1055
        :param args: parser args
1056
        """
1057
        env = os.environ.copy()
×
1058
        if args.build_mode == 'release':
×
1059
            if args.keystore:
×
1060
                env['P4A_RELEASE_KEYSTORE'] = realpath(expanduser(args.keystore))
×
1061
            if args.signkey:
×
1062
                env['P4A_RELEASE_KEYALIAS'] = args.signkey
×
1063
            if args.keystorepw:
×
1064
                env['P4A_RELEASE_KEYSTORE_PASSWD'] = args.keystorepw
×
1065
            if args.signkeypw:
×
1066
                env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.signkeypw
×
1067
            elif args.keystorepw and 'P4A_RELEASE_KEYALIAS_PASSWD' not in env:
×
1068
                env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.keystorepw
×
1069

1070
        return env
×
1071

1072
    def _build_package(self, args, package_type):
8✔
1073
        """
1074
        Creates an android package using gradle
1075
        :param args: parser args
1076
        :param package_type: one of 'apk', 'aar', 'aab'
1077
        :return (gradle output, build_args)
1078
        """
1079
        ctx = self.ctx
×
1080
        dist = self._dist
×
1081
        bs = Bootstrap.get_bootstrap(args.bootstrap, ctx)
×
1082
        ctx.prepare_bootstrap(bs)
×
1083
        self._fix_args(args)
×
1084
        env = self._prepare_release_env(args)
×
1085

1086
        with current_directory(dist.dist_dir):
×
1087
            self.hook("before_apk_build")
×
1088
            os.environ["ANDROID_API"] = str(self.ctx.android_api)
×
1089
            build = load_source('build', join(dist.dist_dir, 'build.py'))
×
1090
            build_args = build.parse_args_and_make_package(
×
1091
                args.unknown_args
1092
            )
1093

1094
            self.hook("after_apk_build")
×
1095
            self.hook("before_apk_assemble")
×
1096
            build_tools_versions = os.listdir(join(ctx.sdk_dir,
×
1097
                                                   'build-tools'))
1098
            build_tools_version = max_build_tool_version(build_tools_versions)
×
1099
            info(('Detected highest available build tools '
×
1100
                  'version to be {}').format(build_tools_version))
1101

1102
            if Version(build_tools_version.replace(" ", "")) < Version('25.0'):
×
1103
                raise BuildInterruptingException(
×
1104
                    'build_tools >= 25 is required, but %s is installed' % build_tools_version)
1105
            if not exists("gradlew"):
×
1106
                raise BuildInterruptingException("gradlew file is missing")
×
1107

1108
            env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir
×
1109
            env["ANDROID_HOME"] = self.ctx.sdk_dir
×
1110

1111
            gradlew = sh.Command('./gradlew')
×
1112

1113
            if exists('/usr/bin/dos2unix'):
×
1114
                # .../dists/bdisttest_python3/gradlew
1115
                # .../build/bootstrap_builds/sdl2-python3/gradlew
1116
                # if docker on windows, gradle contains CRLF
1117
                output = shprint(
×
1118
                    sh.Command('dos2unix'), gradlew._path,
1119
                    _tail=20, _critical=True, _env=env
1120
                )
1121
            if args.build_mode == "debug":
×
1122
                if package_type == "aab":
×
1123
                    raise BuildInterruptingException(
×
1124
                        "aab is meant only for distribution and is not available in debug mode. "
1125
                        "Instead, you can use apk while building for debugging purposes."
1126
                    )
1127
                gradle_task = "assembleDebug"
×
1128
            elif args.build_mode == "release":
×
1129
                if package_type in ["apk", "aar"]:
×
1130
                    gradle_task = "assembleRelease"
×
1131
                elif package_type == "aab":
×
1132
                    gradle_task = "bundleRelease"
×
1133
            else:
1134
                raise BuildInterruptingException(
×
1135
                    "Unknown build mode {} for apk()".format(args.build_mode))
1136

1137
            # WARNING: We should make sure to clean the build directory before building.
1138
            # See PR: kivy/python-for-android#2705
1139
            output = shprint(gradlew, "clean", gradle_task, _tail=20,
×
1140
                             _critical=True, _env=env)
1141
        return output, build_args
×
1142

1143
    def _finish_package(self, args, output, build_args, package_type, output_dir):
8✔
1144
        """
1145
        Finishes the package after the gradle script run
1146
        :param args: the parser args
1147
        :param output: RunningCommand output
1148
        :param build_args: build args as returned by build.parse_args
1149
        :param package_type: one of 'apk', 'aar', 'aab'
1150
        :param output_dir: where to put the package file
1151
        """
1152

1153
        package_glob = "*-{}.%s" % package_type
×
1154
        package_add_version = True
×
1155

1156
        self.hook("after_apk_assemble")
×
1157

1158
        info_main('# Copying android package to current directory')
×
1159

1160
        package_re = re.compile(r'.*Package: (.*\.apk)$')
×
1161
        package_file = None
×
1162
        for line in reversed(output.splitlines()):
×
1163
            m = package_re.match(line)
×
1164
            if m:
×
1165
                package_file = m.groups()[0]
×
1166
                break
×
1167
        if not package_file:
×
1168
            info_main('# Android package filename not found in build output. Guessing...')
×
1169
            if args.build_mode == "release":
×
1170
                suffixes = ("release", "release-unsigned")
×
1171
            else:
1172
                suffixes = ("debug", )
×
1173
            for suffix in suffixes:
×
1174

1175
                package_files = glob.glob(join(output_dir, package_glob.format(suffix)))
×
1176
                if package_files:
×
1177
                    if len(package_files) > 1:
×
1178
                        info('More than one built APK found... guessing you '
×
1179
                             'just built {}'.format(package_files[-1]))
1180
                    package_file = package_files[-1]
×
1181
                    break
×
1182
            else:
1183
                raise BuildInterruptingException('Couldn\'t find the built APK')
×
1184

1185
        info_main('# Found android package file: {}'.format(package_file))
×
1186
        package_extension = f".{package_type}"
×
1187
        if package_add_version:
×
1188
            info('# Add version number to android package')
×
1189
            package_name = basename(package_file)[:-len(package_extension)]
×
1190
            package_file_dest = "{}-{}{}".format(
×
1191
                package_name, build_args.version, package_extension)
1192
            info('# Android package renamed to {}'.format(package_file_dest))
×
1193
            shprint(sh.cp, package_file, package_file_dest)
×
1194
        else:
1195
            shprint(sh.cp, package_file, './')
×
1196

1197
    @require_prebuilt_dist
8✔
1198
    def apk(self, args):
8✔
1199
        output, build_args = self._build_package(args, package_type='apk')
×
1200
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'apk', args.build_mode)
×
1201
        self._finish_package(args, output, build_args, 'apk', output_dir)
×
1202

1203
    @require_prebuilt_dist
8✔
1204
    def aar(self, args):
8✔
1205
        output, build_args = self._build_package(args, package_type='aar')
×
1206
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'aar')
×
1207
        self._finish_package(args, output, build_args, 'aar', output_dir)
×
1208

1209
    @require_prebuilt_dist
8✔
1210
    def aab(self, args):
8✔
1211
        output, build_args = self._build_package(args, package_type='aab')
×
1212
        output_dir = join(self._dist.dist_dir, "build", "outputs", 'bundle', args.build_mode)
×
1213
        self._finish_package(args, output, build_args, 'aab', output_dir)
×
1214

1215
    @require_prebuilt_dist
8✔
1216
    def create(self, args):
8✔
1217
        """Create a distribution directory if it doesn't already exist, run
1218
        any recipes if necessary, and build the apk.
1219
        """
1220
        pass  # The decorator does everything
8✔
1221

1222
    def archs(self, _args):
8✔
1223
        """List the target architectures available to be built for."""
1224
        print('{Style.BRIGHT}Available target architectures are:'
×
1225
              '{Style.RESET_ALL}'.format(Style=Out_Style))
1226
        for arch in self.ctx.archs:
×
1227
            print('    {}'.format(arch.arch))
×
1228

1229
    def dists(self, args):
8✔
1230
        """The same as :meth:`distributions`."""
1231
        self.distributions(args)
×
1232

1233
    def distributions(self, _args):
8✔
1234
        """Lists all distributions currently available (i.e. that have already
1235
        been built)."""
1236
        ctx = self.ctx
×
1237
        dists = Distribution.get_distributions(ctx)
×
1238

1239
        if dists:
×
1240
            print('{Style.BRIGHT}Distributions currently installed are:'
×
1241
                  '{Style.RESET_ALL}'.format(Style=Out_Style))
1242
            pretty_log_dists(dists, print)
×
1243
        else:
1244
            print('{Style.BRIGHT}There are no dists currently built.'
×
1245
                  '{Style.RESET_ALL}'.format(Style=Out_Style))
1246

1247
    def delete_dist(self, _args):
8✔
1248
        dist = self._dist
×
1249
        if not dist.folder_exists():
×
1250
            info('No dist exists that matches your specifications, '
×
1251
                 'exiting without deleting.')
1252
            return
×
1253
        dist.delete()
×
1254

1255
    def sdk_tools(self, args):
8✔
1256
        """Runs the android binary from the detected SDK directory, passing
1257
        all arguments straight to it. This binary is used to install
1258
        e.g. platform-tools for different API level targets. This is
1259
        intended as a convenience function if android is not in your
1260
        $PATH.
1261
        """
1262
        ctx = self.ctx
×
1263
        ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir,
×
1264
                                      user_ndk_dir=self.ndk_dir,
1265
                                      user_android_api=self.android_api,
1266
                                      user_ndk_api=self.ndk_api)
1267
        android = sh.Command(join(ctx.sdk_dir, 'tools', args.tool))
×
1268
        output = android(
×
1269
            *args.unknown_args, _iter=True, _out_bufsize=1, _err_to_out=True)
1270
        for line in output:
×
1271
            sys.stdout.write(line)
×
1272
            sys.stdout.flush()
×
1273

1274
    def adb(self, args):
8✔
1275
        """Runs the adb binary from the detected SDK directory, passing all
1276
        arguments straight to it. This is intended as a convenience
1277
        function if adb is not in your $PATH.
1278
        """
1279
        self._adb(args.unknown_args)
×
1280

1281
    def logcat(self, args):
8✔
1282
        """Runs ``adb logcat`` using the adb binary from the detected SDK
1283
        directory. All extra args are passed as arguments to logcat."""
1284
        self._adb(['logcat'] + args.unknown_args)
×
1285

1286
    def _adb(self, commands):
8✔
1287
        """Call the adb executable from the SDK, passing the given commands as
1288
        arguments."""
1289
        ctx = self.ctx
×
1290
        ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir,
×
1291
                                      user_ndk_dir=self.ndk_dir,
1292
                                      user_android_api=self.android_api,
1293
                                      user_ndk_api=self.ndk_api)
1294
        if platform in ('win32', 'cygwin'):
×
1295
            adb = sh.Command(join(ctx.sdk_dir, 'platform-tools', 'adb.exe'))
×
1296
        else:
1297
            adb = sh.Command(join(ctx.sdk_dir, 'platform-tools', 'adb'))
×
1298
        info_notify('Starting adb...')
×
1299
        output = adb(*commands, _iter=True, _out_bufsize=1, _err_to_out=True)
×
1300
        for line in output:
×
1301
            sys.stdout.write(line)
×
1302
            sys.stdout.flush()
×
1303

1304
    def recommendations(self, args):
8✔
1305
        print_recommendations()
8✔
1306

1307
    def build_status(self, _args):
8✔
1308
        """Print the status of the specified build. """
1309
        print('{Style.BRIGHT}Bootstraps whose core components are probably '
×
1310
              'already built:{Style.RESET_ALL}'.format(Style=Out_Style))
1311

1312
        bootstrap_dir = join(self.ctx.build_dir, 'bootstrap_builds')
×
1313
        if exists(bootstrap_dir):
×
1314
            for filen in os.listdir(bootstrap_dir):
×
1315
                print('    {Fore.GREEN}{Style.BRIGHT}{filen}{Style.RESET_ALL}'
×
1316
                      .format(filen=filen, Fore=Out_Fore, Style=Out_Style))
1317

1318
        print('{Style.BRIGHT}Recipes that are probably already built:'
×
1319
              '{Style.RESET_ALL}'.format(Style=Out_Style))
1320
        other_builds_dir = join(self.ctx.build_dir, 'other_builds')
×
1321
        if exists(other_builds_dir):
×
1322
            for filen in sorted(os.listdir(other_builds_dir)):
×
1323
                name = filen.split('-')[0]
×
1324
                dependencies = filen.split('-')[1:]
×
1325
                recipe_str = ('    {Style.BRIGHT}{Fore.GREEN}{name}'
×
1326
                              '{Style.RESET_ALL}'.format(
1327
                                  Style=Out_Style, name=name, Fore=Out_Fore))
1328
                if dependencies:
×
1329
                    recipe_str += (
×
1330
                        ' ({Fore.BLUE}with ' + ', '.join(dependencies) +
1331
                        '{Fore.RESET})').format(Fore=Out_Fore)
1332
                recipe_str += '{Style.RESET_ALL}'.format(Style=Out_Style)
×
1333
                print(recipe_str)
×
1334

1335

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

© 2026 Coveralls, Inc