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

kivy / python-for-android / 6215290912

17 Sep 2023 06:49PM UTC coverage: 59.095% (+1.4%) from 57.68%
6215290912

push

github

web-flow
Merge pull request #2891 from misl6/release-2023.09.16

* Update `cffi` recipe for Python 3.10 (#2800)

* Update __init__.py

version bump to 1.15.1

* Update disable-pkg-config.patch

adjust patch for 1.15.1

* Use build rather than pep517 for building (#2784)

pep517 has been renamed to pyproject-hooks, and as a consequence all of
the deprecated functionality has been removed. build now provides the
functionality required, and since we are only interested in the
metadata, we can leverage a helper function for that. I've also removed
all of the subprocess machinery for calling the wrapping function, since
it appears to not be as noisy as pep517.

* Bump actions/setup-python and actions/checkout versions, as old ones are deprecated (#2827)

* Removes `mysqldb` recipe as does not support Python 3 (#2828)

* Removes `Babel` recipe as it's not needed anymore. (#2826)

* Remove dateutil recipe, as it's not needed anymore (#2829)

* Optimize CI runs, by avoiding unnecessary rebuilds (#2833)

* Remove `pytz` recipe, as it's not needed anymore (#2830)

* `freetype` recipe: Changed the url to use https as http doesn't work (#2846)

* Fix `vlc` recipe build (#2841)

* Correct sys_platform (#2852)

On Window, sys.platform = "win32".

I think "nt" is a reference to os.name.

* Fix code string - quickstart.rst

* Bump `kivy` version to `2.2.1` (#2855)

* Use a pinned version of `Cython` for now, as most of the recipes are incompatible with `Cython==3.x.x` (#2862)

* Automatically generate required pre-requisites (#2858)

`get_required_prerequisites()` maintains a list of Prerequisites required by each platform.

But that same information is already stored in each Prerequisite class.

Rather than rather than maintaining two lists which might become inconsistent, auto-generate one.

* Use `platform.uname` instead of `os.uname` (#2857)

Advantages:

- Works cross platform, not just Unix.
- Is a namedtuple, ... (continued)

944 of 2241 branches covered (0.0%)

Branch coverage included in aggregate %.

174 of 272 new or added lines in 32 files covered. (63.97%)

9 existing lines in 5 files now uncovered.

4725 of 7352 relevant lines covered (64.27%)

2.56 hits per line

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

43.55
/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, InvalidVersion
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, BuildInterruptingException, load_source, rmdir)
45

46
user_dir = dirname(realpath(os.path.curdir))
4✔
47
toolchain_dir = dirname(__file__)
4✔
48
sys.path.insert(0, join(toolchain_dir, "tools", "external"))
4✔
49

50

51
def add_boolean_option(parser, names, no_names=None,
4✔
52
                       default=True, dest=None, description=None):
53
    group = parser.add_argument_group(description=description)
4✔
54
    if not isinstance(names, (list, tuple)):
4!
55
        names = [names]
×
56
    if dest is None:
4!
57
        dest = names[0].strip("-").replace("-", "_")
4✔
58

59
    def add_dashes(x):
4✔
60
        return x if x.startswith("-") else "--"+x
4✔
61

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

77

78
def require_prebuilt_dist(func):
4✔
79
    """Decorator for ToolchainCL methods. If present, the method will
80
    automatically make sure a dist has been built before continuing
81
    or, if no dists are present or can be obtained, will raise an
82
    error.
83
    """
84

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

103

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

118

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

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

153
    ctx.distribution = dist
4✔
154
    ctx.prepare_bootstrap(bs)
4✔
155
    if dist.needs_build:
4!
156
        ctx.prepare_dist()
4✔
157

158
    build_recipes(build_order, python_modules, ctx,
4✔
159
                  getattr(args, "private", None),
160
                  ignore_project_setup_py=getattr(
161
                      args, "ignore_setup_py", False
162
                  ),
163
                 )
164

165
    ctx.bootstrap.assemble_distribution()
4✔
166

167
    info_main('# Your distribution was created successfully, exiting.')
4✔
168
    info('Dist can be found at (for now) {}'
4✔
169
         .format(join(ctx.dist_dir, ctx.distribution.dist_dir)))
170

171

172
def split_argument_list(arg_list):
4✔
173
    if not len(arg_list):
4✔
174
        return []
4✔
175
    return re.split(r'[ ,]+', arg_list)
4✔
176

177

178
class NoAbbrevParser(argparse.ArgumentParser):
4✔
179
    """We want to disable argument abbreviation so as not to interfere
180
    with passing through arguments to build.py, but in python2 argparse
181
    doesn't have this option.
182

183
    This subclass alternative is follows the suggestion at
184
    https://bugs.python.org/issue14910.
185
    """
186
    def _get_option_tuples(self, option_string):
4✔
187
        return []
4✔
188

189

190
class ToolchainCL:
4✔
191

192
    def __init__(self):
4✔
193

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

205
        parser = NoAbbrevParser(
4✔
206
            description='A packaging tool for turning Python scripts and apps '
207
                        'into Android APKs')
208

209
        generic_parser = argparse.ArgumentParser(
4✔
210
            add_help=False,
211
            description='Generic arguments applied to all commands')
212
        argparse.ArgumentParser(
4✔
213
            add_help=False, description='Arguments for dist building')
214

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

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

261
        generic_parser.add_argument(
4✔
262
            '--arch', help='The archs to build for.',
263
            action='append', default=[])
264

265
        # Options for specifying the Distribution
266
        generic_parser.add_argument(
4✔
267
            '--dist-name', '--dist_name',
268
            help='The name of the distribution to use or create', default='')
269

270
        generic_parser.add_argument(
4✔
271
            '--requirements',
272
            help=('Dependencies of your app, should be recipe names or '
273
                  'Python modules. NOT NECESSARY if you are using '
274
                  'Python 3 with --use-setup-py'),
275
            default='')
276

277
        generic_parser.add_argument(
4✔
278
            '--recipe-blacklist',
279
            help=('Blacklist an internal recipe from use. Allows '
280
                  'disabling Python 3 core modules to save size'),
281
            dest="recipe_blacklist",
282
            default='')
283

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

291
        generic_parser.add_argument(
4✔
292
            '--bootstrap',
293
            help='The bootstrap to build with. Leave unset to choose '
294
                 'automatically.',
295
            default=None)
296

297
        generic_parser.add_argument(
4✔
298
            '--hook',
299
            help='Filename to a module that contains python-for-android hooks',
300
            default=None)
301

302
        add_boolean_option(
4✔
303
            generic_parser, ["force-build"],
304
            default=False,
305
            description='Whether to force compilation of a new distribution')
306

307
        add_boolean_option(
4✔
308
            generic_parser, ["require-perfect-match"],
309
            default=False,
310
            description=('Whether the dist recipes must perfectly match '
311
                         'those requested'))
312

313
        add_boolean_option(
4✔
314
            generic_parser, ["allow-replace-dist"],
315
            default=True,
316
            description='Whether existing dist names can be automatically replaced'
317
            )
318

319
        generic_parser.add_argument(
4✔
320
            '--local-recipes', '--local_recipes',
321
            dest='local_recipes', default='./p4a-recipes',
322
            help='Directory to look for local recipes')
323

324
        generic_parser.add_argument(
4✔
325
            '--activity-class-name',
326
            dest='activity_class_name', default='org.kivy.android.PythonActivity',
327
            help='The full java class name of the main activity')
328

329
        generic_parser.add_argument(
4✔
330
            '--service-class-name',
331
            dest='service_class_name', default='org.kivy.android.PythonService',
332
            help='Full java package name of the PythonService class')
333

334
        generic_parser.add_argument(
4✔
335
            '--java-build-tool',
336
            dest='java_build_tool', default='auto',
337
            choices=['auto', 'ant', 'gradle'],
338
            help=('The java build tool to use when packaging the APK, defaults '
339
                  'to automatically selecting an appropriate tool.'))
340

341
        add_boolean_option(
4✔
342
            generic_parser, ['copy-libs'],
343
            default=False,
344
            description='Copy libraries instead of using biglink (Android 4.3+)'
345
        )
346

347
        self._read_configuration()
4✔
348

349
        subparsers = parser.add_subparsers(dest='subparser_name',
4✔
350
                                           help='The command to run')
351

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

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

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

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

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

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

447
        parser_packaging = argparse.ArgumentParser(
4✔
448
            parents=[generic_parser],
449
            add_help=False,
450
            description='common options for packaging (apk, aar)')
451

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

506
        add_parser(
4✔
507
            subparsers,
508
            'aar', help='Build an AAR',
509
            parents=[parser_packaging])
510

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

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

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

539
        parser_sdk_tools = add_parser(
4✔
540
            subparsers,
541
            'sdk_tools', aliases=['sdk-tools'],
542
            help='Run the given binary from the SDK tools dis',
543
            parents=[generic_parser])
544
        parser_sdk_tools.add_argument(
4✔
545
            'tool', help='The binary tool name to run')
546

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

561
        parser.add_argument('-v', '--version', action='version',
4✔
562
                            version=__version__)
563

564
        args, unknown = parser.parse_known_args(sys.argv[1:])
4✔
565
        args.unknown_args = unknown
4✔
566

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

581
        self.args = args
4✔
582

583
        if args.subparser_name is None:
4✔
584
            parser.print_help()
4✔
585
            exit(1)
4✔
586

587
        setup_color(args.color)
4✔
588

589
        if args.debug:
4!
590
            logger.setLevel(logging.DEBUG)
×
591

592
        self.ctx = Context()
4✔
593
        self.ctx.use_setup_py = getattr(args, "use_setup_py", True)
4✔
594
        self.ctx.build_as_debuggable = getattr(
4✔
595
            args, "build_mode", "debug"
596
        ) == "debug"
597
        self.ctx.with_debug_symbols = getattr(
4✔
598
            args, "with_debug_symbols", False
599
        )
600

601
        have_setup_py_or_similar = False
4✔
602
        if getattr(args, "private", None) is not None:
4!
603
            project_dir = getattr(args, "private")
×
604
            if (os.path.exists(os.path.join(project_dir, "setup.py")) or
×
605
                    os.path.exists(os.path.join(project_dir,
606
                                                "pyproject.toml"))):
607
                have_setup_py_or_similar = True
×
608

609
        # Process requirements and put version in environ
610
        if hasattr(args, 'requirements'):
4!
611
            requirements = []
4✔
612

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

649
            # Parse --requirements argument list:
650
            for requirement in split_argument_list(args.requirements):
4✔
651
                if "==" in requirement:
4!
652
                    requirement, version = requirement.split(u"==", 1)
×
653
                    os.environ["VERSION_{}".format(requirement)] = version
×
654
                    info('Recipe {}: version "{}" requested'.format(
×
655
                        requirement, version))
656
                requirements.append(requirement)
4✔
657
            args.requirements = u",".join(requirements)
4✔
658

659
        self.warn_on_deprecated_args(args)
4✔
660

661
        self.storage_dir = args.storage_dir
4✔
662
        self.ctx.setup_dirs(self.storage_dir)
4✔
663
        self.sdk_dir = args.sdk_dir
4✔
664
        self.ndk_dir = args.ndk_dir
4✔
665
        self.android_api = args.android_api
4✔
666
        self.ndk_api = args.ndk_api
4✔
667
        self.ctx.symlink_bootstrap_files = args.symlink_bootstrap_files
4✔
668
        self.ctx.java_build_tool = args.java_build_tool
4✔
669

670
        self._archs = args.arch
4✔
671

672
        self.ctx.local_recipes = realpath(args.local_recipes)
4✔
673
        self.ctx.copy_libs = args.copy_libs
4✔
674

675
        self.ctx.activity_class_name = args.activity_class_name
4✔
676
        self.ctx.service_class_name = args.service_class_name
4✔
677

678
        # Each subparser corresponds to a method
679
        command = args.subparser_name.replace('-', '_')
4✔
680
        getattr(self, command)(args)
4✔
681

682
    @staticmethod
4✔
683
    def warn_on_carriage_return_args(args):
4✔
684
        for check_arg in args:
4✔
685
            if '\r' in check_arg:
4!
686
                warning("Argument '{}' contains a carriage return (\\r).".format(str(check_arg.replace('\r', ''))))
×
687
                warning("Invoking this program via scripts which use CRLF instead of LF line endings will have undefined behaviour.")
×
688

689
    def warn_on_deprecated_args(self, args):
4✔
690
        """
691
        Print warning messages for any deprecated arguments that were passed.
692
        """
693

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

713
        # NDK version is now determined automatically
714
        if args.ndk_version is not None:
4!
715
            warning('--ndk-version is deprecated and no longer necessary, '
×
716
                    'the value you passed is ignored')
717
        if 'ANDROIDNDKVER' in environ:
4!
718
            warning('$ANDROIDNDKVER is deprecated and no longer necessary, '
×
719
                    'the value you set is ignored')
720

721
    def hook(self, name):
4✔
722
        if not self.args.hook:
×
723
            return
×
724
        if not hasattr(self, "hook_module"):
×
725
            # first time, try to load the hook module
726
            self.hook_module = load_source(
×
727
                "pythonforandroid.hook", self.args.hook)
728
        if hasattr(self.hook_module, name):
×
729
            info("Hook: execute {}".format(name))
×
730
            getattr(self.hook_module, name)(self)
×
731
        else:
732
            info("Hook: ignore {}".format(name))
×
733

734
    @property
4✔
735
    def default_storage_dir(self):
4✔
736
        udd = user_data_dir('python-for-android')
×
737
        if ' ' in udd:
×
738
            udd = '~/.python-for-android'
×
739
        return udd
×
740

741
    @staticmethod
4✔
742
    def _read_configuration():
4✔
743
        # search for a .p4a configuration file in the current directory
744
        if not exists(".p4a"):
4!
745
            return
4✔
746
        info("Reading .p4a configuration")
×
747
        with open(".p4a") as fd:
×
748
            lines = fd.readlines()
×
749
        lines = [shlex.split(line)
×
750
                 for line in lines if not line.startswith("#")]
751
        for line in lines:
×
752
            for arg in line:
×
753
                sys.argv.append(arg)
×
754

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

795
    def bootstraps(self, _args):
4✔
796
        """List all the bootstraps available to build with."""
797
        for bs in Bootstrap.all_bootstraps():
×
798
            bs = Bootstrap.get_bootstrap(bs, self.ctx)
×
799
            print('{Fore.BLUE}{Style.BRIGHT}{bs.name}{Style.RESET_ALL}'
×
800
                  .format(bs=bs, Fore=Out_Fore, Style=Out_Style))
801
            print('    {Fore.GREEN}depends: {bs.recipe_depends}{Fore.RESET}'
×
802
                  .format(bs=bs, Fore=Out_Fore))
803

804
    def clean(self, args):
4✔
805
        components = args.component
×
806

807
        component_clean_methods = {
×
808
            'all': self.clean_all,
809
            'dists': self.clean_dists,
810
            'distributions': self.clean_dists,
811
            'builds': self.clean_builds,
812
            'bootstrap_builds': self.clean_bootstrap_builds,
813
            'downloads': self.clean_download_cache}
814

815
        for component in components:
×
816
            if component not in component_clean_methods:
×
817
                raise BuildInterruptingException((
×
818
                    'Asked to clean "{}" but this argument is not '
819
                    'recognised'.format(component)))
820
            component_clean_methods[component](args)
×
821

822
    def clean_all(self, args):
4✔
823
        """Delete all build components; the package cache, package builds,
824
        bootstrap builds and distributions."""
825
        self.clean_dists(args)
×
826
        self.clean_builds(args)
×
827
        self.clean_download_cache(args)
×
828

829
    def clean_dists(self, _args):
4✔
830
        """Delete all compiled distributions in the internal distribution
831
        directory."""
832
        ctx = self.ctx
×
NEW
833
        rmdir(ctx.dist_dir)
×
834

835
    def clean_bootstrap_builds(self, _args):
4✔
836
        """Delete all the bootstrap builds."""
NEW
837
        rmdir(join(self.ctx.build_dir, 'bootstrap_builds'))
×
838
        # for bs in Bootstrap.all_bootstraps():
839
        #     bs = Bootstrap.get_bootstrap(bs, self.ctx)
840
        #     if bs.build_dir and exists(bs.build_dir):
841
        #         info('Cleaning build for {} bootstrap.'.format(bs.name))
842
        #         rmdir(bs.build_dir)
843

844
    def clean_builds(self, _args):
4✔
845
        """Delete all build caches for each recipe, python-install, java code
846
        and compiled libs collection.
847

848
        This does *not* delete the package download cache or the final
849
        distributions.  You can also use clean_recipe_build to delete the build
850
        of a specific recipe.
851
        """
852
        ctx = self.ctx
×
NEW
853
        rmdir(ctx.build_dir)
×
NEW
854
        rmdir(ctx.python_installs_dir)
×
855
        libs_dir = join(self.ctx.build_dir, 'libs_collections')
×
NEW
856
        rmdir(libs_dir)
×
857

858
    def clean_recipe_build(self, args):
4✔
859
        """Deletes the build files of the given recipe.
860

861
        This is intended for debug purposes. You may experience
862
        strange behaviour or problems with some recipes if their
863
        build has made unexpected state changes. If this happens, run
864
        clean_builds, or attempt to clean other recipes until things
865
        work again.
866
        """
867
        recipe = Recipe.get_recipe(args.recipe, self.ctx)
×
868
        info('Cleaning build for {} recipe.'.format(recipe.name))
×
869
        recipe.clean_build()
×
870
        if not args.no_clean_dists:
×
871
            self.clean_dists(args)
×
872

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

877
            p4a clean_download_cache kivy,pyjnius
878

879
        This does *not* delete the build caches or final distributions.
880
        """
881
        ctx = self.ctx
×
882
        if hasattr(args, 'recipes') and args.recipes:
×
883
            for package in args.recipes:
×
884
                remove_path = join(ctx.packages_path, package)
×
885
                if exists(remove_path):
×
NEW
886
                    rmdir(remove_path)
×
887
                    info('Download cache removed for: "{}"'.format(package))
×
888
                else:
889
                    warning('No download cache found for "{}", skipping'.format(
×
890
                        package))
891
        else:
892
            if exists(ctx.packages_path):
×
NEW
893
                rmdir(ctx.packages_path)
×
894
                info('Download cache removed.')
×
895
            else:
896
                print('No cache found at "{}"'.format(ctx.packages_path))
×
897

898
    @require_prebuilt_dist
4✔
899
    def export_dist(self, args):
4✔
900
        """Copies a created dist to an output dir.
901

902
        This makes it easy to navigate to the dist to investigate it
903
        or call build.py, though you do not in general need to do this
904
        and can use the apk command instead.
905
        """
906
        ctx = self.ctx
×
907
        dist = dist_from_args(ctx, args)
×
908
        if dist.needs_build:
×
909
            raise BuildInterruptingException(
×
910
                'You asked to export a dist, but there is no dist '
911
                'with suitable recipes available. For now, you must '
912
                ' create one first with the create argument.')
913
        if args.symlink:
×
914
            shprint(sh.ln, '-s', dist.dist_dir, args.output_dir)
×
915
        else:
916
            shprint(sh.cp, '-r', dist.dist_dir, args.output_dir)
×
917

918
    @property
4✔
919
    def _dist(self):
4✔
920
        ctx = self.ctx
4✔
921
        dist = dist_from_args(ctx, self.args)
4✔
922
        ctx.distribution = dist
4✔
923
        return dist
4✔
924

925
    @staticmethod
4✔
926
    def _fix_args(args):
4✔
927
        """
928
        Manually fixing these arguments at the string stage is
929
        unsatisfactory and should probably be changed somehow, but
930
        we can't leave it until later as the build.py scripts assume
931
        they are in the current directory.
932
        works in-place
933
        :param args: parser args
934
        """
935

936
        fix_args = ('--dir', '--private', '--add-jar', '--add-source',
×
937
                    '--whitelist', '--blacklist', '--presplash', '--icon',
938
                    '--icon-bg', '--icon-fg')
939
        unknown_args = args.unknown_args
×
940

941
        for asset in args.assets:
×
942
            if ":" in asset:
×
943
                asset_src, asset_dest = asset.split(":")
×
944
            else:
945
                asset_src = asset_dest = asset
×
946
            # take abspath now, because build.py will be run in bootstrap dir
947
            unknown_args += ["--asset", os.path.abspath(asset_src)+":"+asset_dest]
×
948
        for resource in args.resources:
×
949
            if ":" in resource:
×
950
                resource_src, resource_dest = resource.split(":")
×
951
            else:
952
                resource_src = resource
×
953
                resource_dest = ""
×
954
            # take abspath now, because build.py will be run in bootstrap dir
955
            unknown_args += ["--resource", os.path.abspath(resource_src)+":"+resource_dest]
×
956
        for i, arg in enumerate(unknown_args):
×
957
            argx = arg.split('=')
×
958
            if argx[0] in fix_args:
×
959
                if len(argx) > 1:
×
960
                    unknown_args[i] = '='.join(
×
961
                        (argx[0], realpath(expanduser(argx[1]))))
962
                elif i + 1 < len(unknown_args):
×
963
                    unknown_args[i+1] = realpath(expanduser(unknown_args[i+1]))
×
964

965
    @staticmethod
4✔
966
    def _prepare_release_env(args):
4✔
967
        """
968
        prepares envitonment dict with the necessary flags for signing an apk
969
        :param args: parser args
970
        """
971
        env = os.environ.copy()
×
972
        if args.build_mode == 'release':
×
973
            if args.keystore:
×
974
                env['P4A_RELEASE_KEYSTORE'] = realpath(expanduser(args.keystore))
×
975
            if args.signkey:
×
976
                env['P4A_RELEASE_KEYALIAS'] = args.signkey
×
977
            if args.keystorepw:
×
978
                env['P4A_RELEASE_KEYSTORE_PASSWD'] = args.keystorepw
×
979
            if args.signkeypw:
×
980
                env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.signkeypw
×
981
            elif args.keystorepw and 'P4A_RELEASE_KEYALIAS_PASSWD' not in env:
×
982
                env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.keystorepw
×
983

984
        return env
×
985

986
    def _build_package(self, args, package_type):
4✔
987
        """
988
        Creates an android package using gradle
989
        :param args: parser args
990
        :param package_type: one of 'apk', 'aar', 'aab'
991
        :return (gradle output, build_args)
992
        """
993
        ctx = self.ctx
×
994
        dist = self._dist
×
995
        bs = Bootstrap.get_bootstrap(args.bootstrap, ctx)
×
996
        ctx.prepare_bootstrap(bs)
×
997
        self._fix_args(args)
×
998
        env = self._prepare_release_env(args)
×
999

1000
        with current_directory(dist.dist_dir):
×
1001
            self.hook("before_apk_build")
×
1002
            os.environ["ANDROID_API"] = str(self.ctx.android_api)
×
1003
            build = load_source('build', join(dist.dist_dir, 'build.py'))
×
1004
            build_args = build.parse_args_and_make_package(
×
1005
                args.unknown_args
1006
            )
1007

1008
            self.hook("after_apk_build")
×
1009
            self.hook("before_apk_assemble")
×
1010
            build_tools_versions = os.listdir(join(ctx.sdk_dir,
×
1011
                                                   'build-tools'))
1012

NEW
1013
            def sort_key(version_text):
×
NEW
1014
                try:
×
1015
                    # Historically, Android build release candidates have had
1016
                    # spaces in the version number.
NEW
1017
                    return Version(version_text.replace(" ", ""))
×
NEW
1018
                except InvalidVersion:
×
1019
                    # Put badly named versions at worst position.
NEW
1020
                    return Version("0")
×
1021

NEW
1022
            build_tools_versions.sort(key=sort_key)
×
1023
            build_tools_version = build_tools_versions[-1]
×
1024
            info(('Detected highest available build tools '
×
1025
                  'version to be {}').format(build_tools_version))
1026

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

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

1036
            gradlew = sh.Command('./gradlew')
×
1037

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

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

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

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

1081
        self.hook("after_apk_assemble")
×
1082

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1260

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