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

kivy / python-for-android / 26682386815

30 May 2026 11:14AM UTC coverage: 62.67% (-1.2%) from 63.887%
26682386815

Pull #3278

github

web-flow
Merge 77aee3d95 into 74b559a3c
Pull Request #3278: Handling system bars and Edge-to-Edge enforcement (android 15+)

1832 of 3194 branches covered (57.36%)

Branch coverage included in aggregate %.

5407 of 8357 relevant lines covered (64.7%)

3.88 hits per line

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

27.8
/pythonforandroid/bootstraps/common/build/build.py
1
#!/usr/bin/env python3
2

3
from gzip import GzipFile
6✔
4
import hashlib
6✔
5
import json
6✔
6
from os.path import (
6✔
7
    dirname, join, isfile, realpath,
8
    relpath, split, exists, basename
9
)
10
from os import environ, listdir, makedirs, remove
6✔
11
import os
6✔
12
import shlex
6✔
13
import shutil
6✔
14
import subprocess
6✔
15
import sys
6✔
16
import tarfile
6✔
17
import tempfile
6✔
18
import time
6✔
19

20
from fnmatch import fnmatch
6✔
21
import jinja2
6✔
22

23
from pythonforandroid.bootstrap import SDL_BOOTSTRAPS
6✔
24
from pythonforandroid.util import rmdir, ensure_dir, max_build_tool_version
6✔
25

26

27
def get_dist_info_for(key, error_if_missing=True):
6✔
28
    try:
×
29
        with open(join(dirname(__file__), 'dist_info.json'), 'r') as fileh:
×
30
            info = json.load(fileh)
×
31
        value = info[key]
×
32
    except (OSError, KeyError) as e:
×
33
        if not error_if_missing:
×
34
            return None
×
35
        print("BUILD FAILURE: Couldn't extract the key `" + key + "` " +
×
36
              "from dist_info.json: " + str(e))
37
        sys.exit(1)
×
38
    return value
×
39

40

41
def get_hostpython():
6✔
42
    return get_dist_info_for('hostpython')
×
43

44

45
def get_bootstrap_name():
6✔
46
    return get_dist_info_for('bootstrap')
×
47

48

49
if os.name == 'nt':
6!
50
    ANDROID = 'android.bat'
×
51
    ANT = 'ant.bat'
×
52
else:
53
    ANDROID = 'android'
6✔
54
    ANT = 'ant'
6✔
55

56
curdir = dirname(__file__)
6✔
57

58
BLACKLIST_PATTERNS = [
6✔
59
    # code versioning
60
    '^*.hg/*',
61
    '^*.git/*',
62
    '^*.bzr/*',
63
    '^*.svn/*',
64

65
    # temp files
66
    '~',
67
    '*.bak',
68
    '*.swp',
69

70
    # Android artifacts
71
    '*.apk',
72
    '*.aab',
73
]
74

75
WHITELIST_PATTERNS = []
6✔
76

77
if os.environ.get("P4A_BUILD_IS_RUNNING_UNITTESTS", "0") != "1":
6!
78
    PYTHON = get_hostpython()
×
79
    _bootstrap_name = get_bootstrap_name()
×
80
else:
81
    PYTHON = "python3"
6✔
82
    _bootstrap_name = "sdl2"
6✔
83

84
if PYTHON is not None and not exists(PYTHON):
6!
85
    PYTHON = None
6✔
86

87
if _bootstrap_name in ('sdl2', 'sdl3', 'webview', 'service_only', 'qt'):
6!
88
    WHITELIST_PATTERNS.append('pyconfig.h')
6✔
89

90
environment = jinja2.Environment(loader=jinja2.FileSystemLoader(
6✔
91
    join(curdir, 'templates')))
92

93

94
DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS = 'org.kivy.android.PythonActivity'
6✔
95
DEFAULT_PYTHON_SERVICE_JAVA_CLASS = 'org.kivy.android.PythonService'
6✔
96
# Google Play's documented maximum Android versionCode.
97
# https://developer.android.com/tools/publishing/versioning
98
MAX_ANDROID_VERSION_CODE = 2100000000
6✔
99

100

101
def get_android_numeric_version(version, min_sdk_version):
6✔
102
    """
103
    Generate the default Android versionCode value from --version.
104

105
    The format is (10 + minsdk + app_version). Older versioning was
106
    (arch + minsdk + app_version), with arch expressed with a single digit
107
    from 6 to 9. Since multi-arch support, this uses 10.
108
    """
109
    version_code = 0
6✔
110
    try:
6✔
111
        for part in version.split('.'):
6✔
112
            version_code *= 100
6✔
113
            version_code += int(part)
6✔
114
    except ValueError as exc:
6✔
115
        raise ValueError(
6✔
116
            "Could not generate Android versionCode from --version "
117
            "{!r}. --version is Android versionName; when it is not numeric "
118
            "dot-separated text, set --numeric-version to a positive Android "
119
            "versionCode integer no greater than {}.".format(
120
                version, MAX_ANDROID_VERSION_CODE
121
            )
122
        ) from exc
123
    return "{}{}{}".format("10", min_sdk_version, version_code)
6✔
124

125

126
def validate_android_numeric_version(numeric_version, *, generated_from_version=None):
6✔
127
    try:
6✔
128
        normalized_version = int(numeric_version)
6✔
129
    except (TypeError, ValueError) as exc:
6✔
130
        raise ValueError(
6✔
131
            "--numeric-version must be a decimal integer Android versionCode "
132
            "greater than 0 and no greater than {}; got {!r}.".format(
133
                MAX_ANDROID_VERSION_CODE, numeric_version
134
            )
135
        ) from exc
136

137
    if normalized_version <= 0:
6✔
138
        raise ValueError(
6✔
139
            "--numeric-version must be a positive Android versionCode "
140
            "greater than 0; got {!r}.".format(numeric_version)
141
        )
142

143
    if normalized_version > MAX_ANDROID_VERSION_CODE:
6✔
144
        if generated_from_version is not None:
6✔
145
            raise ValueError(
6✔
146
                "Generated Android versionCode {} from --version {!r}, "
147
                "which exceeds the maximum {}. --version is Android "
148
                "versionName; keep this display version by setting "
149
                "--numeric-version to a positive Android versionCode no "
150
                "greater than {}.".format(
151
                    normalized_version,
152
                    generated_from_version,
153
                    MAX_ANDROID_VERSION_CODE,
154
                    MAX_ANDROID_VERSION_CODE,
155
                )
156
            )
157
        raise ValueError(
6✔
158
            "--numeric-version is Android versionCode and must not exceed "
159
            "{}; got {!r}.".format(MAX_ANDROID_VERSION_CODE, numeric_version)
160
        )
161

162
    return str(normalized_version)
6✔
163

164

165
def render(template, dest, **kwargs):
6✔
166
    '''Using jinja2, render `template` to the filename `dest`, supplying the
167

168
    keyword arguments as template parameters.
169
    '''
170

171
    dest_dir = dirname(dest)
×
172
    if dest_dir and not exists(dest_dir):
×
173
        makedirs(dest_dir)
×
174

175
    template = environment.get_template(template)
×
176
    text = template.render(**kwargs)
×
177

178
    f = open(dest, 'wb')
×
179
    f.write(text.encode('utf-8'))
×
180
    f.close()
×
181

182

183
def is_whitelist(name):
6✔
184
    return match_filename(WHITELIST_PATTERNS, name)
×
185

186

187
def is_blacklist(name):
6✔
188
    if is_whitelist(name):
×
189
        return False
×
190
    return match_filename(BLACKLIST_PATTERNS, name)
×
191

192

193
def match_filename(pattern_list, name):
6✔
194
    for pattern in pattern_list:
×
195
        if pattern.startswith('^'):
×
196
            pattern = pattern[1:]
×
197
        else:
198
            pattern = '*/' + pattern
×
199
        if fnmatch(name, pattern):
×
200
            return True
×
201

202

203
def listfiles(d):
6✔
204
    basedir = d
×
205
    subdirlist = []
×
206
    for item in os.listdir(d):
×
207
        fn = join(d, item)
×
208
        if isfile(fn):
×
209
            yield fn
×
210
        else:
211
            subdirlist.append(join(basedir, item))
×
212
    for subdir in subdirlist:
×
213
        for fn in listfiles(subdir):
×
214
            yield fn
×
215

216

217
def make_tar(tfn, source_dirs, byte_compile_python=False, optimize_python=True):
6✔
218
    '''
219
    Make a zip file `fn` from the contents of source_dis.
220
    '''
221

222
    def clean(tinfo):
×
223
        """cleaning function (for reproducible builds)"""
224
        tinfo.uid = tinfo.gid = 0
×
225
        tinfo.uname = tinfo.gname = ''
×
226
        tinfo.mtime = 0
×
227
        return tinfo
×
228

229
    # get the files and relpath file of all the directory we asked for
230
    files = []
×
231
    for sd in source_dirs:
×
232
        sd = realpath(sd)
×
233
        for fn in listfiles(sd):
×
234
            if is_blacklist(fn):
×
235
                continue
×
236
            if fn.endswith('.py') and byte_compile_python:
×
237
                fn = compile_py_file(fn, optimize_python=optimize_python)
×
238
            files.append((fn, relpath(realpath(fn), sd)))
×
239
    files.sort()  # deterministic
×
240

241
    # create tar.gz of those files
242
    gf = GzipFile(tfn, 'wb', mtime=0)  # deterministic
×
243
    tf = tarfile.open(None, 'w', gf, format=tarfile.USTAR_FORMAT)
×
244
    dirs = []
×
245
    for fn, afn in files:
×
246
        dn = dirname(afn)
×
247
        if dn not in dirs:
×
248
            # create every dirs first if not exist yet
249
            d = ''
×
250
            for component in split(dn):
×
251
                d = join(d, component)
×
252
                if d.startswith('/'):
×
253
                    d = d[1:]
×
254
                if d == '' or d in dirs:
×
255
                    continue
×
256
                dirs.append(d)
×
257
                tinfo = tarfile.TarInfo(d)
×
258
                tinfo.type = tarfile.DIRTYPE
×
259
                clean(tinfo)
×
260
                tf.addfile(tinfo)
×
261

262
        # put the file
263
        tf.add(fn, afn, filter=clean)
×
264
    tf.close()
×
265
    gf.close()
×
266

267

268
def compile_py_file(python_file, optimize_python=True):
6✔
269
    '''
270
    Compile python_file to *.pyc and return the filename of the *.pyc file.
271
    '''
272

273
    if PYTHON is None:
×
274
        return
×
275

276
    args = [PYTHON, '-m', 'compileall', '-b', '-f', python_file]
×
277
    if optimize_python:
×
278
        # -OO = strip docstrings
279
        args.insert(1, '-OO')
×
280
    return_code = subprocess.call(args)
×
281

282
    if return_code != 0:
×
283
        print('Error while running "{}"'.format(' '.join(args)))
×
284
        print('This probably means one of your Python files has a syntax '
×
285
              'error, see logs above')
286
        exit(1)
×
287

288
    return ".".join([os.path.splitext(python_file)[0], "pyc"])
×
289

290

291
def is_sdl_bootstrap():
6✔
292
    return get_bootstrap_name() in SDL_BOOTSTRAPS
6✔
293

294

295
def make_package(args):
6✔
296
    # If no launcher is specified, require a main.py/main.pyc:
297
    if (get_bootstrap_name() != "sdl" or args.launcher is None) and \
×
298
            get_bootstrap_name() not in ["webview", "service_library"]:
299
        # (webview doesn't need an entrypoint, apparently)
300
        if args.private is None or (
×
301
                not exists(join(realpath(args.private), 'main.py')) and
302
                not exists(join(realpath(args.private), 'main.pyc'))):
303
            print('''BUILD FAILURE: No main.py(c) found in your app directory. This
×
304
file must exist to act as the entry point for you app. If your app is
305
started by a file with a different name, rename it to main.py or add a
306
main.py that loads it.''')
307
            sys.exit(1)
×
308

309
    assets_dir = "src/main/assets"
×
310

311
    # Delete the old assets.
312
    rmdir(assets_dir, ignore_errors=True)
×
313
    ensure_dir(assets_dir)
×
314

315
    # Add extra environment variable file into tar-able directory:
316
    env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-")
×
317
    with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f:
×
318
        if hasattr(args, "window"):
×
319
            f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n")
×
320
        if hasattr(args, "sdl_orientation_hint"):
×
321
            f.write("KIVY_ORIENTATION=" + str(args.sdl_orientation_hint) + "\n")
×
322
        f.write("P4A_NUMERIC_VERSION=" + str(args.numeric_version) + "\n")
×
323
        f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n")
×
324

325
    # Package up the private data (public not supported).
326
    use_setup_py = get_dist_info_for("use_setup_py",
×
327
                                     error_if_missing=False) is True
328
    private_tar_dirs = [env_vars_tarpath]
×
329
    _temp_dirs_to_clean = []
×
330
    try:
×
331
        if args.private:
×
332
            if not use_setup_py or (
×
333
                    not exists(join(args.private, "setup.py")) and
334
                    not exists(join(args.private, "pyproject.toml"))
335
                    ):
336
                print('No setup.py/pyproject.toml used, copying '
×
337
                      'full private data into .apk.')
338
                private_tar_dirs.append(args.private)
×
339
            else:
340
                print("Copying main.py's ONLY, since other app data is "
×
341
                      "expected in site-packages.")
342
                main_py_only_dir = tempfile.mkdtemp()
×
343
                _temp_dirs_to_clean.append(main_py_only_dir)
×
344

345
                # Check all main.py files we need to copy:
346
                copy_paths = ["main.py", join("service", "main.py")]
×
347
                for copy_path in copy_paths:
×
348
                    variants = [
×
349
                        copy_path,
350
                        copy_path.partition(".")[0] + ".pyc",
351
                    ]
352
                    # Check in all variants with all possible endings:
353
                    for variant in variants:
×
354
                        if exists(join(args.private, variant)):
×
355
                            # Make sure surrounding directly exists:
356
                            dir_path = os.path.dirname(variant)
×
357
                            if (len(dir_path) > 0 and
×
358
                                    not exists(
359
                                        join(main_py_only_dir, dir_path)
360
                                    )):
361
                                ensure_dir(join(main_py_only_dir, dir_path))
×
362
                            # Copy actual file:
363
                            shutil.copyfile(
×
364
                                join(args.private, variant),
365
                                join(main_py_only_dir, variant),
366
                            )
367

368
                # Append directory with all main.py's to result apk paths:
369
                private_tar_dirs.append(main_py_only_dir)
×
370
        if get_bootstrap_name() == "webview":
×
371
            for asset in listdir('webview_includes'):
×
372
                shutil.copy(join('webview_includes', asset), join(assets_dir, asset))
×
373

374
        for asset in args.assets:
×
375
            asset_src, asset_dest = asset.split(":")
×
376
            if isfile(realpath(asset_src)):
×
377
                ensure_dir(dirname(join(assets_dir, asset_dest)))
×
378
                shutil.copy(realpath(asset_src), join(assets_dir, asset_dest))
×
379
            else:
380
                shutil.copytree(realpath(asset_src), join(assets_dir, asset_dest))
×
381

382
        if args.private or args.launcher:
×
383
            for arch in get_dist_info_for("archs"):
×
384
                libs_dir = f"libs/{arch}"
×
385
                make_tar(
×
386
                    join(libs_dir, "libpybundle.so"),
387
                    [f"_python_bundle__{arch}"],
388
                    byte_compile_python=args.byte_compile_python,
389
                    optimize_python=args.optimize_python,
390
                )
391
            make_tar(
×
392
                join(assets_dir, "private.tar"),
393
                private_tar_dirs,
394
                byte_compile_python=args.byte_compile_python,
395
                optimize_python=args.optimize_python,
396
            )
397
    finally:
398
        for directory in _temp_dirs_to_clean:
×
399
            rmdir(directory)
×
400

401
    # Remove extra env vars tar-able directory:
402
    rmdir(env_vars_tarpath)
×
403

404
    # Prepare some variables for templating process
405
    res_dir = "src/main/res"
×
406
    res_dir_initial = "src/res_initial"
×
407
    # make res_dir stateless
408
    if exists(res_dir_initial):
×
409
        rmdir(res_dir, ignore_errors=True)
×
410
        shutil.copytree(res_dir_initial, res_dir)
×
411
    else:
412
        shutil.copytree(res_dir, res_dir_initial)
×
413

414
    # Add user resources
415
    for resource in args.resources:
×
416
        resource_src, resource_dest = resource.split(":")
×
417
        if isfile(realpath(resource_src)):
×
418
            ensure_dir(dirname(join(res_dir, resource_dest)))
×
419
            shutil.copy(realpath(resource_src), join(res_dir, resource_dest))
×
420
        else:
421
            shutil.copytree(realpath(resource_src),
×
422
                            join(res_dir, resource_dest), dirs_exist_ok=True)
423

424
    default_icon = 'templates/kivy-icon.png'
×
425
    default_presplash = 'templates/kivy-presplash.jpg'
×
426
    shutil.copy(
×
427
        args.icon or default_icon,
428
        join(res_dir, 'mipmap/icon.png')
429
    )
430
    if args.icon_fg and args.icon_bg:
×
431
        shutil.copy(args.icon_fg, join(res_dir, 'mipmap/icon_foreground.png'))
×
432
        shutil.copy(args.icon_bg, join(res_dir, 'mipmap/icon_background.png'))
×
433
        with open(join(res_dir, 'mipmap-anydpi-v26/icon.xml'), "w") as fd:
×
434
            fd.write("""<?xml version="1.0" encoding="utf-8"?>
×
435
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
436
    <background android:drawable="@mipmap/icon_background"/>
437
    <foreground android:drawable="@mipmap/icon_foreground"/>
438
</adaptive-icon>
439
""")
440
    elif args.icon_fg or args.icon_bg:
×
441
        print("WARNING: Received an --icon_fg or an --icon_bg argument, but not both. "
×
442
              "Ignoring.")
443

444
    if get_bootstrap_name() != "service_only":
×
445
        lottie_splashscreen = join(res_dir, 'raw/splashscreen.json')
×
446
        if args.presplash_lottie:
×
447
            shutil.copy(
×
448
                'templates/lottie.xml',
449
                join(res_dir, 'layout/lottie.xml')
450
            )
451
            ensure_dir(join(res_dir, 'raw'))
×
452
            shutil.copy(
×
453
                args.presplash_lottie,
454
                join(res_dir, 'raw/splashscreen.json')
455
            )
456
        else:
457
            if exists(lottie_splashscreen):
×
458
                remove(lottie_splashscreen)
×
459
                remove(join(res_dir, 'layout/lottie.xml'))
×
460

461
            shutil.copy(
×
462
                args.presplash or default_presplash,
463
                join(res_dir, 'drawable/presplash.jpg')
464
            )
465

466
    # If extra Java jars were requested, copy them into the libs directory
467
    jars = []
×
468
    if args.add_jar:
×
469
        for jarname in args.add_jar:
×
470
            if not exists(jarname):
×
471
                print('Requested jar does not exist: {}'.format(jarname))
×
472
                sys.exit(-1)
×
473
            shutil.copy(jarname, 'src/main/libs')
×
474
            jars.append(basename(jarname))
×
475

476
    # If extra aar were requested, copy them into the libs directory
477
    aars = []
×
478
    if args.add_aar:
×
479
        ensure_dir("libs")
×
480
        for aarname in args.add_aar:
×
481
            if not exists(aarname):
×
482
                print('Requested aar does not exists: {}'.format(aarname))
×
483
                sys.exit(-1)
×
484
            shutil.copy(aarname, 'libs')
×
485
            aars.append(basename(aarname).rsplit('.', 1)[0])
×
486

487
    versioned_name = (args.name.replace(' ', '').replace('\'', '') +
×
488
                      '-' + args.version)
489

490
    generated_from_version = None
×
491
    if args.numeric_version is None:
×
492
        generated_from_version = args.version
×
493
        args.numeric_version = get_android_numeric_version(
×
494
            args.version,
495
            args.min_sdk_version,
496
        )
497
    args.numeric_version = validate_android_numeric_version(
×
498
        args.numeric_version,
499
        generated_from_version=generated_from_version,
500
    )
501

502
    if args.intent_filters:
×
503
        with open(args.intent_filters) as fd:
×
504
            args.intent_filters = fd.read()
×
505

506
    if not args.add_activity:
×
507
        args.add_activity = []
×
508

509
    if not args.activity_launch_mode:
×
510
        args.activity_launch_mode = ''
×
511

512
    if args.extra_source_dirs:
×
513
        esd = []
×
514
        for spec in args.extra_source_dirs:
×
515
            if ':' in spec:
×
516
                specdir, specincludes = spec.split(':')
×
517
                print('WARNING: Currently gradle builds only support including source '
×
518
                      'directories, so when building using gradle all files in '
519
                      '{} will be included.'.format(specdir))
520
            else:
521
                specdir = spec
×
522
                specincludes = '**'
×
523
            esd.append((realpath(specdir), specincludes))
×
524
        args.extra_source_dirs = esd
×
525
    else:
526
        args.extra_source_dirs = []
×
527

528
    service = False
×
529
    if args.private:
×
530
        service_main = join(realpath(args.private), 'service', 'main.py')
×
531
        if exists(service_main) or exists(service_main + 'o'):
×
532
            service = True
×
533

534
    service_data = []
×
535
    base_service_class = args.service_class_name.split('.')[-1]
×
536
    for sid, spec in enumerate(args.services):
×
537
        spec = spec.split(':')
×
538
        name = spec[0]
×
539
        entrypoint = spec[1]
×
540
        options = spec[2:]
×
541

542
        foreground = 'foreground' in options
×
543
        sticky = 'sticky' in options
×
544
        foreground_type_option = next((s for s in options if s.startswith('foregroundServiceType')), None)
×
545
        foreground_type = None
×
546
        if foreground_type_option:
×
547
            parts = foreground_type_option.split('=', 1)
×
548
            if len(parts) != 2 or not parts[1]:
×
549
                raise ValueError(
×
550
                    'Missing value for `foregroundServiceType` option. '
551
                    'Expected format: foregroundServiceType=location'
552
                )
553
            foreground_type = parts[1]
×
554

555
        service_data.append((name, foreground_type))
×
556
        service_target_path =\
×
557
            'src/main/java/{}/Service{}.java'.format(
558
                args.package.replace(".", "/"),
559
                name.capitalize()
560
            )
561
        render(
×
562
            'Service.tmpl.java',
563
            service_target_path,
564
            name=name,
565
            entrypoint=entrypoint,
566
            args=args,
567
            foreground=foreground,
568
            sticky=sticky,
569
            service_id=sid + 1,
570
            base_service_class=base_service_class,
571
        )
572

573
    # Find the SDK directory and target API
574
    with open('project.properties', 'r') as fileh:
×
575
        target = fileh.read().strip()
×
576
    android_api = target.split('-')[1]
×
577

578
    if android_api.isdigit():
×
579
        android_api = int(android_api)
×
580
    else:
581
        raise ValueError(
×
582
            "failed to extract the Android API level from " +
583
            "build.properties. expected int, got: '" +
584
            str(android_api) + "'"
585
        )
586

587
    with open('local.properties', 'r') as fileh:
×
588
        sdk_dir = fileh.read().strip()
×
589
    sdk_dir = sdk_dir[8:]
×
590

591
    # Try to build with the newest available build tools
592
    ignored = {".DS_Store", ".ds_store"}
×
593
    build_tools_versions = [x for x in listdir(join(sdk_dir, 'build-tools')) if x not in ignored]
×
594
    build_tools_version = max_build_tool_version(build_tools_versions)
×
595

596
    # Folder name for launcher (used by SDL2 bootstrap)
597
    url_scheme = 'kivy'
×
598

599
    # Copy backup rules file if specified and update the argument
600
    res_xml_dir = join(res_dir, 'xml')
×
601
    if args.backup_rules:
×
602
        ensure_dir(res_xml_dir)
×
603
        shutil.copy(join(args.private, args.backup_rules), res_xml_dir)
×
604
        args.backup_rules = split(args.backup_rules)[1][:-4]
×
605

606
    # Copy res_xml files to src/main/res/xml
607
    if args.res_xmls:
×
608
        ensure_dir(res_xml_dir)
×
609
        for xmlpath in args.res_xmls:
×
610
            if not os.path.exists(xmlpath):
×
611
                xmlpath = join(args.private, xmlpath)
×
612
            shutil.copy(xmlpath, res_xml_dir)
×
613

614
    # Render out android manifest:
615
    manifest_path = "src/main/AndroidManifest.xml"
×
616
    render_args = {
×
617
        "args": args,
618
        "service": service,
619
        "service_data": service_data,
620
        "android_api": android_api,
621
        "debug": "debug" in args.build_mode,
622
        "native_services": args.native_services,
623
    }
624
    if is_sdl_bootstrap():
×
625
        render_args["url_scheme"] = url_scheme
×
626

627
    render(
×
628
        'AndroidManifest.tmpl.xml',
629
        manifest_path,
630
        **render_args)
631

632
    # Copy the AndroidManifest.xml to the dist root dir so that ant
633
    # can also use it
634
    if exists('AndroidManifest.xml'):
×
635
        remove('AndroidManifest.xml')
×
636
    shutil.copy(manifest_path, 'AndroidManifest.xml')
×
637

638
    # gradle build templates
639
    render(
×
640
        'build.tmpl.gradle',
641
        'build.gradle',
642
        args=args,
643
        aars=aars,
644
        jars=jars,
645
        android_api=android_api,
646
        build_tools_version=build_tools_version,
647
        debug_build="debug" in args.build_mode,
648
        is_library=(get_bootstrap_name() == 'service_library'),
649
        )
650

651
    # gradle properties
652
    render(
×
653
        'gradle.tmpl.properties',
654
        'gradle.properties',
655
        args=args,
656
        bootstrap_name=get_bootstrap_name())
657

658
    # ant build templates
659
    render(
×
660
        'build.tmpl.xml',
661
        'build.xml',
662
        args=args,
663
        versioned_name=versioned_name)
664

665
    # String resources:
666
    timestamp = time.time()
×
667
    if 'SOURCE_DATE_EPOCH' in environ:
×
668
        # for reproducible builds
669
        timestamp = int(environ['SOURCE_DATE_EPOCH'])
×
670
    private_version = "{} {} {}".format(
×
671
        args.version,
672
        args.numeric_version,
673
        timestamp
674
    )
675
    render_args = {
×
676
        "args": args,
677
        "private_version": hashlib.sha1(private_version.encode()).hexdigest()
678
    }
679
    if is_sdl_bootstrap():
×
680
        render_args["url_scheme"] = url_scheme
×
681
    render(
×
682
        'strings.tmpl.xml',
683
        join(res_dir, 'values/strings.xml'),
684
        **render_args)
685

686
    # Library resources from Qt
687
    # These are referred by QtLoader.java in Qt6AndroidBindings.jar
688
    # qt_libs and load_local_libs are loaded at App startup
689
    if get_bootstrap_name() == "qt":
×
690
        qt_libs = args.qt_libs.split(",")
×
691
        load_local_libs = args.load_local_libs.split(",")
×
692
        init_classes = args.init_classes
×
693
        if init_classes:
×
694
            init_classes = init_classes.split(",")
×
695
            init_classes = ":".join(init_classes)
×
696
        arch = get_dist_info_for("archs")[0]
×
697
        render(
×
698
            'libs.tmpl.xml',
699
            join(res_dir, 'values/libs.xml'),
700
            qt_libs=qt_libs,
701
            load_local_libs=load_local_libs,
702
            init_classes=init_classes,
703
            arch=arch
704
        )
705

706
    if exists(join("templates", "custom_rules.tmpl.xml")):
×
707
        render(
×
708
            'custom_rules.tmpl.xml',
709
            'custom_rules.xml',
710
            args=args)
711

712
    if get_bootstrap_name() == "webview":
×
713
        render('WebViewLoader.tmpl.java',
×
714
               'src/main/java/org/kivy/android/WebViewLoader.java',
715
               args=args)
716

717
    if args.sign:
×
718
        render('build.properties', 'build.properties')
×
719
    else:
720
        if exists('build.properties'):
×
721
            os.remove('build.properties')
×
722

723
    # Apply java source patches if any are present:
724
    if exists(join('src', 'patches')):
×
725
        print("Applying Java source code patches...")
×
726
        for patch_name in os.listdir(join('src', 'patches')):
×
727
            patch_path = join('src', 'patches', patch_name)
×
728
            print("Applying patch: " + str(patch_path))
×
729

730
            # -N: insist this is FORWARD patch, don't reverse apply
731
            # -p1: strip first path component
732
            # -t: batch mode, don't ask questions
733
            patch_command = ["patch", "-N", "-p1", "-t", "-i", patch_path]
×
734

735
            try:
×
736
                # Use a dry run to establish whether the patch is already applied.
737
                # If we don't check this, the patch may be partially applied (which is bad!)
738
                subprocess.check_output(patch_command + ["--dry-run"])
×
739
            except subprocess.CalledProcessError as e:
×
740
                if e.returncode == 1:
×
741
                    # Return code 1 means not all hunks could be applied, this usually
742
                    # means the patch is already applied.
743
                    print("Warning: failed to apply patch (exit code 1), "
×
744
                          "assuming it is already applied: ",
745
                          str(patch_path))
746
                else:
747
                    raise e
×
748
            else:
749
                # The dry run worked, so do the real thing
750
                subprocess.check_output(patch_command)
×
751

752

753
def parse_permissions(args_permissions):
6✔
754
    if args_permissions and isinstance(args_permissions[0], list):
6!
755
        args_permissions = [p for perm in args_permissions for p in perm]
6✔
756

757
    def _is_advanced_permission(permission):
6✔
758
        return permission.startswith("(") and permission.endswith(")")
6✔
759

760
    def _decode_advanced_permission(permission):
6✔
761
        SUPPORTED_PERMISSION_PROPERTIES = ["name", "maxSdkVersion", "usesPermissionFlags"]
6✔
762
        _permission_args = permission[1:-1].split(";")
6✔
763
        _permission_args = (arg.split("=") for arg in _permission_args)
6✔
764
        advanced_permission = dict(_permission_args)
6✔
765

766
        if "name" not in advanced_permission:
6!
767
            raise ValueError("Advanced permission must have a name property")
×
768

769
        for key in advanced_permission.keys():
6✔
770
            if key not in SUPPORTED_PERMISSION_PROPERTIES:
6✔
771
                raise ValueError(
6✔
772
                    f"Property '{key}' is not supported. "
773
                    "Advanced permission only supports: "
774
                    f"{', '.join(SUPPORTED_PERMISSION_PROPERTIES)} properties"
775
                )
776

777
        return advanced_permission
6✔
778

779
    _permissions = []
6✔
780
    for permission in args_permissions:
6✔
781
        if _is_advanced_permission(permission):
6✔
782
            _permissions.append(_decode_advanced_permission(permission))
6✔
783
        else:
784
            if "." in permission:
6✔
785
                _permissions.append(dict(name=permission))
6✔
786
            else:
787
                _permissions.append(dict(name=f"android.permission.{permission}"))
6✔
788
    return _permissions
6✔
789

790

791
def get_sdl_orientation_hint(orientations):
6✔
792
    SDL_ORIENTATION_MAP = {
6✔
793
        "landscape": "LandscapeLeft",
794
        "portrait": "Portrait",
795
        "portrait-reverse": "PortraitUpsideDown",
796
        "landscape-reverse": "LandscapeRight",
797
    }
798
    return " ".join(
6✔
799
        [SDL_ORIENTATION_MAP[x] for x in orientations if x in SDL_ORIENTATION_MAP]
800
    )
801

802

803
def get_manifest_orientation(orientations, manifest_orientation=None):
6✔
804
    # If the user has specifically set an orientation to use in the manifest,
805
    # use that.
806
    if manifest_orientation is not None:
6✔
807
        return manifest_orientation
6✔
808

809
    # If multiple or no orientations are specified, use unspecified in the manifest,
810
    # as we can only specify one orientation in the manifest.
811
    if len(orientations) != 1:
6✔
812
        return "unspecified"
6✔
813

814
    # Convert the orientation to a value that can be used in the manifest.
815
    # If the specified orientation is not supported, use unspecified.
816
    MANIFEST_ORIENTATION_MAP = {
6✔
817
        "landscape": "landscape",
818
        "portrait": "portrait",
819
        "portrait-reverse": "reversePortrait",
820
        "landscape-reverse": "reverseLandscape",
821
    }
822
    return MANIFEST_ORIENTATION_MAP.get(orientations[0], "unspecified")
6✔
823

824

825
def get_dist_ndk_min_api_level():
6✔
826
    # Get the default minsdk, equal to the NDK API that this dist is built against
827
    try:
6✔
828
        with open('dist_info.json', 'r') as fileh:
6!
829
            info = json.load(fileh)
×
830
            ndk_api = int(info['ndk_api'])
×
831
    except (OSError, KeyError, ValueError, TypeError):
6✔
832
        print('WARNING: Failed to read ndk_api from dist info, defaulting to 12')
6✔
833
        ndk_api = 12  # The old default before ndk_api was introduced
6✔
834
    return ndk_api
6✔
835

836

837
def create_argument_parser():
6✔
838
    ndk_api = get_dist_ndk_min_api_level()
6✔
839
    import argparse
6✔
840
    ap = argparse.ArgumentParser(description='''\
6✔
841
Package a Python application for Android (using
842
bootstrap ''' + get_bootstrap_name() + ''').
843

844
For this to work, Java and Ant need to be in your path, as does the
845
tools directory of the Android SDK.
846
''')
847

848
    # --private is required unless for sdl2, where there's also --launcher
849
    ap.add_argument('--private', dest='private',
6✔
850
                    help='the directory with the app source code files' +
851
                         ' (containing your main.py entrypoint)',
852
                    required=(not is_sdl_bootstrap()))
853
    ap.add_argument('--package', dest='package',
6✔
854
                    help=('The name of the java package the project will be'
855
                          ' packaged under.'),
856
                    required=True)
857
    ap.add_argument('--name', dest='name',
6✔
858
                    help=('The human-readable name of the project.'),
859
                    required=True)
860
    ap.add_argument('--numeric-version', dest='numeric_version',
6✔
861
                    help=('The Android versionCode of the project. This must '
862
                          'be a positive decimal integer no greater than '
863
                          '{}. If not given, it is automatically computed '
864
                          'from --version.').format(MAX_ANDROID_VERSION_CODE))
865
    ap.add_argument('--version', dest='version',
6✔
866
                    help=('The Android versionName of the project, shown to '
867
                          'users as the display version. Use '
868
                          '--numeric-version to control Android versionCode '
869
                          'and update ordering.'),
870
                    required=True)
871
    if is_sdl_bootstrap():
6!
872
        ap.add_argument('--launcher', dest='launcher', action='store_true',
6✔
873
                        help=('Provide this argument to build a multi-app '
874
                              'launcher, rather than a single app.'))
875
        ap.add_argument('--home-app', dest='home_app', action='store_true', default=False,
6✔
876
                        help=('Turn your application into a home app (launcher)'))
877
    ap.add_argument('--display-cutout', dest='display_cutout', default='never',
6✔
878
                    help=('Enables display-cutout that renders around the area (notch) on '
879
                          'some devices that extends into the display surface'))
880
    ap.add_argument('--permission', dest='permissions', action='append', default=[],
6✔
881
                    help='The permissions to give this app.', nargs='+')
882
    ap.add_argument('--meta-data', dest='meta_data', action='append', default=[],
6✔
883
                    help='Custom key=value to add in application metadata')
884
    ap.add_argument('--uses-library', dest='android_used_libs', action='append', default=[],
6✔
885
                    help='Used shared libraries included using <uses-library> tag in AndroidManifest.xml')
886
    ap.add_argument('--asset', dest='assets',
6✔
887
                    action="append", default=[],
888
                    metavar="/path/to/source:dest",
889
                    help='Put this in the assets folder at assets/dest')
890
    ap.add_argument('--resource', dest='resources',
6✔
891
                    action="append", default=[],
892
                    metavar="/path/to/source:kind/asset",
893
                    help='Put this in the res folder at res/kind')
894
    ap.add_argument('--icon', dest='icon',
6✔
895
                    help=('A png file to use as the icon for '
896
                          'the application.'))
897
    ap.add_argument('--icon-fg', dest='icon_fg',
6✔
898
                    help=('A png file to use as the foreground of the adaptive icon '
899
                          'for the application.'))
900
    ap.add_argument('--icon-bg', dest='icon_bg',
6✔
901
                    help=('A png file to use as the background of the adaptive icon '
902
                          'for the application.'))
903
    ap.add_argument('--service', dest='services', action='append', default=[],
6✔
904
                    help='Declare a new service entrypoint: '
905
                         'NAME:PATH_TO_PY[:foreground]')
906
    ap.add_argument('--native-service', dest='native_services', action='append', default=[],
6✔
907
                    help='Declare a new native service: '
908
                         'package.name.service')
909
    if get_bootstrap_name() != "service_only":
6!
910
        ap.add_argument('--presplash', dest='presplash',
6✔
911
                        help=('A jpeg file to use as a screen while the '
912
                              'application is loading.'))
913
        ap.add_argument('--presplash-lottie', dest='presplash_lottie',
6✔
914
                        help=('A lottie (json) file to use as an animation while the '
915
                              'application is loading.'))
916
        ap.add_argument('--presplash-color',
6✔
917
                        dest='presplash_color',
918
                        default='#000000',
919
                        help=('A string to set the loading screen '
920
                              'background color. '
921
                              'Supported formats are: '
922
                              '#RRGGBB #AARRGGBB or color names '
923
                              'like red, green, blue, etc.'))
924
        ap.add_argument('--window', dest='window', action='store_true',
6✔
925
                        default=False,
926
                        help='Indicate if the application will be windowed')
927
        ap.add_argument('--manifest-orientation', dest='manifest_orientation',
6✔
928
                        help=('The orientation that will be set in the '
929
                              'android:screenOrientation attribute of the activity '
930
                              'in the AndroidManifest.xml file. If not set, '
931
                              'the value will be synthesized from the --orientation option.'))
932
        ap.add_argument('--orientation', dest='orientation',
6✔
933
                        action="append", default=[],
934
                        choices=['portrait', 'landscape', 'landscape-reverse', 'portrait-reverse'],
935
                        help=('The orientations that the app will display in. '
936
                              'Since Android ignores android:screenOrientation '
937
                              'when in multi-window mode (Which is the default on Android 12+), '
938
                              'this option will also set the window orientation hints '
939
                              'for apps using the (default) SDL bootstrap.'
940
                              'If multiple orientations are given, android:screenOrientation '
941
                              'will be set to "unspecified"'))
942

943
    ap.add_argument('--enable-androidx', dest='enable_androidx',
6✔
944
                    action='store_true',
945
                    help=('Enable the AndroidX support library, '
946
                          'requires api = 28 or greater'))
947
    ap.add_argument('--android-entrypoint', dest='android_entrypoint',
6✔
948
                    default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS,
949
                    help='Defines which java class will be used for startup, usually a subclass of PythonActivity')
950
    ap.add_argument('--android-apptheme', dest='android_apptheme',
6✔
951
                    default='@android:style/Theme.NoTitleBar',
952
                    help='Defines which app theme should be selected for the main activity')
953
    ap.add_argument('--add-compile-option', dest='compile_options', default=[],
6✔
954
                    action='append', help='add compile options to gradle.build')
955
    ap.add_argument('--add-gradle-repository', dest='gradle_repositories',
6✔
956
                    default=[],
957
                    action='append',
958
                    help='Ddd a repository for gradle')
959
    ap.add_argument('--add-packaging-option', dest='packaging_options',
6✔
960
                    default=[],
961
                    action='append',
962
                    help='Dndroid packaging options')
963

964
    ap.add_argument('--wakelock', dest='wakelock', action='store_true',
6✔
965
                    help=('Indicate if the application needs the device '
966
                          'to stay on'))
967
    ap.add_argument('--blacklist', dest='blacklist',
6✔
968
                    default=join(curdir, 'blacklist.txt'),
969
                    help=('Use a blacklist file to match unwanted file in '
970
                          'the final APK'))
971
    ap.add_argument('--whitelist', dest='whitelist',
6✔
972
                    default=join(curdir, 'whitelist.txt'),
973
                    help=('Use a whitelist file to prevent blacklisting of '
974
                          'file in the final APK'))
975
    ap.add_argument('--release', dest='build_mode', action='store_const',
6✔
976
                    const='release', default='debug',
977
                    help='Build your app as a non-debug release build. '
978
                         '(Disables gdb debugging among other things)')
979
    ap.add_argument('--with-debug-symbols', dest='with_debug_symbols',
6✔
980
                    action='store_const', const=True, default=False,
981
                    help='Will keep debug symbols from `.so` files.')
982
    ap.add_argument('--add-jar', dest='add_jar', action='append',
6✔
983
                    help=('Add a Java .jar to the libs, so you can access its '
984
                          'classes with pyjnius. You can specify this '
985
                          'argument more than once to include multiple jars'))
986
    ap.add_argument('--add-aar', dest='add_aar', action='append',
6✔
987
                    help=('Add an aar dependency manually'))
988
    ap.add_argument('--depend', dest='depends', action='append',
6✔
989
                    help=('Add a external dependency '
990
                          '(eg: com.android.support:appcompat-v7:19.0.1)'))
991
    # The --sdk option has been removed, it is ignored in favour of
992
    # --android-api handled by toolchain.py
993
    ap.add_argument('--sdk', dest='sdk_version', default=-1,
6✔
994
                    type=int, help=('Deprecated argument, does nothing'))
995
    ap.add_argument('--minsdk', dest='min_sdk_version',
6✔
996
                    default=ndk_api, type=int,
997
                    help=('Minimum Android SDK version that the app supports. '
998
                          'Defaults to {}.'.format(ndk_api)))
999
    ap.add_argument('--allow-minsdk-ndkapi-mismatch', default=False,
6✔
1000
                    action='store_true',
1001
                    help=('Allow the --minsdk argument to be different from '
1002
                          'the discovered ndk_api in the dist'))
1003
    ap.add_argument('--intent-filters', dest='intent_filters',
6✔
1004
                    help=('Add intent-filters xml rules to the '
1005
                          'AndroidManifest.xml file. The argument is a '
1006
                          'filename containing xml. The filename should be '
1007
                          'located relative to the python-for-android '
1008
                          'directory'))
1009
    ap.add_argument('--res_xml', dest='res_xmls', action='append', default=[],
6✔
1010
                    help='Add files to res/xml directory (for example device-filters)', nargs='+')
1011
    ap.add_argument('--with-billing', dest='billing_pubkey',
6✔
1012
                    help='If set, the billing service will be added (not implemented)')
1013
    ap.add_argument('--add-source', dest='extra_source_dirs', action='append',
6✔
1014
                    help='Include additional source dirs in Java build')
1015
    if get_bootstrap_name() == "webview":
6!
1016
        ap.add_argument('--port',
×
1017
                        help='The port on localhost that the WebView will access',
1018
                        default='5000')
1019
    ap.add_argument('--try-system-python-compile', dest='try_system_python_compile',
6✔
1020
                    action='store_true',
1021
                    help='Use the system python during compileall if possible.')
1022
    ap.add_argument('--sign', action='store_true',
6✔
1023
                    help=('Try to sign the APK with your credentials. You must set '
1024
                          'the appropriate environment variables.'))
1025
    ap.add_argument('--add-activity', dest='add_activity', action='append',
6✔
1026
                    help='Add this Java class as an Activity to the manifest.')
1027
    ap.add_argument('--activity-launch-mode',
6✔
1028
                    dest='activity_launch_mode',
1029
                    default='singleTask',
1030
                    help='Set the launch mode of the main activity in the manifest.')
1031
    ap.add_argument('--allow-backup', dest='allow_backup', default='true',
6✔
1032
                    help="if set to 'false', then android won't backup the application.")
1033
    ap.add_argument('--backup-rules', dest='backup_rules', default='',
6✔
1034
                    help=('Backup rules for Android Auto Backup. Argument is a '
1035
                          'filename containing xml. The filename should be '
1036
                          'located relative to the private directory containing your source code '
1037
                          'files (containing your main.py entrypoint). '
1038
                          'See https://developer.android.com/guide/topics/data/'
1039
                          'autobackup#IncludingFiles for more information'))
1040
    ap.add_argument('--no-byte-compile-python', dest='byte_compile_python',
6✔
1041
                    action='store_false', default=True,
1042
                    help='Skip byte compile for .py files.')
1043
    ap.add_argument('--no-optimize-python', dest='optimize_python',
6✔
1044
                    action='store_false', default=True,
1045
                    help=('Whether to compile to optimised .pyc files, using -OO '
1046
                          '(strips docstrings and asserts)'))
1047
    ap.add_argument('--extra-manifest-xml', default='',
6✔
1048
                    help=('Extra xml to write directly inside the <manifest> element of'
1049
                          'AndroidManifest.xml'))
1050
    ap.add_argument('--extra-manifest-application-arguments', default='',
6✔
1051
                    help='Extra arguments to be added to the <manifest><application> tag of'
1052
                         'AndroidManifest.xml')
1053
    ap.add_argument('--manifest-placeholders', dest='manifest_placeholders',
6✔
1054
                    default='[:]', help=('Inject build variables into the manifest '
1055
                                         'via the manifestPlaceholders property'))
1056
    ap.add_argument('--service-class-name', dest='service_class_name', default=DEFAULT_PYTHON_SERVICE_JAVA_CLASS,
6✔
1057
                    help='Use that parameter if you need to implement your own PythonServive Java class')
1058
    ap.add_argument('--activity-class-name', dest='activity_class_name', default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS,
6✔
1059
                    help='The full java class name of the main activity')
1060
    if get_bootstrap_name() == "qt":
6!
1061
        ap.add_argument('--qt-libs', dest='qt_libs', required=True,
×
1062
                        help='comma separated list of Qt libraries to be loaded')
1063
        ap.add_argument('--load-local-libs', dest='load_local_libs', required=True,
×
1064
                        help='comma separated list of Qt plugin libraries to be loaded')
1065
        ap.add_argument('--init-classes', dest='init_classes', default='',
×
1066
                        help='comma separated list of java class names to be loaded from the Qt jar files, '
1067
                             'specified through add_jar cli option')
1068

1069
    return ap
6✔
1070

1071

1072
def parse_args_and_make_package(args=None):
6✔
1073
    global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON
1074

1075
    ndk_api = get_dist_ndk_min_api_level()
×
1076
    ap = create_argument_parser()
×
1077

1078
    # Put together arguments, and add those from .p4a config file:
1079
    if args is None:
×
1080
        args = sys.argv[1:]
×
1081

1082
    def _read_configuration():
×
1083
        if not exists(".p4a"):
×
1084
            return
×
1085
        print("Reading .p4a configuration")
×
1086
        with open(".p4a") as fd:
×
1087
            lines = fd.readlines()
×
1088
        lines = [shlex.split(line)
×
1089
                 for line in lines if not line.startswith("#")]
1090
        for line in lines:
×
1091
            for arg in line:
×
1092
                args.append(arg)
×
1093
    _read_configuration()
×
1094

1095
    args = ap.parse_args(args)
×
1096

1097
    if args.name and args.name[0] == '"' and args.name[-1] == '"':
×
1098
        args.name = args.name[1:-1]
×
1099

1100
    if ndk_api != args.min_sdk_version:
×
1101
        print(('WARNING: --minsdk argument does not match the api that is '
×
1102
               'compiled against. Only proceed if you know what you are '
1103
               'doing, otherwise use --minsdk={} or recompile against api '
1104
               '{}').format(ndk_api, args.min_sdk_version))
1105
        if not args.allow_minsdk_ndkapi_mismatch:
×
1106
            print('You must pass --allow-minsdk-ndkapi-mismatch to build '
×
1107
                  'with --minsdk different to the target NDK api from the '
1108
                  'build step')
1109
            sys.exit(1)
×
1110
        else:
1111
            print('Proceeding with --minsdk not matching build target api')
×
1112

1113
    if args.billing_pubkey:
×
1114
        print('Billing not yet supported!')
×
1115
        sys.exit(1)
×
1116

1117
    if args.sdk_version != -1:
×
1118
        print('WARNING: Received a --sdk argument, but this argument is '
×
1119
              'deprecated and does nothing.')
1120
        args.sdk_version = -1  # ensure it is not used
×
1121

1122
    args.permissions = parse_permissions(args.permissions)
×
1123

1124
    args.manifest_orientation = get_manifest_orientation(
×
1125
        args.orientation, args.manifest_orientation
1126
    )
1127

1128
    if is_sdl_bootstrap():
×
1129
        args.sdl_orientation_hint = get_sdl_orientation_hint(args.orientation)
×
1130

1131
    if args.res_xmls and isinstance(args.res_xmls[0], list):
×
1132
        args.res_xmls = [x for res in args.res_xmls for x in res]
×
1133

1134
    if args.try_system_python_compile:
×
1135
        # Hardcoding python2.7 is okay for now, as python3 skips the
1136
        # compilation anyway
1137
        python_executable = 'python2.7'
×
1138
        try:
×
1139
            subprocess.call([python_executable, '--version'])
×
1140
        except (OSError, subprocess.CalledProcessError):
×
1141
            pass
×
1142
        else:
1143
            PYTHON = python_executable
×
1144

1145
    if args.blacklist:
×
1146
        with open(args.blacklist) as fd:
×
1147
            patterns = [x.strip() for x in fd.read().splitlines()
×
1148
                        if x.strip() and not x.strip().startswith('#')]
1149
        BLACKLIST_PATTERNS += patterns
×
1150

1151
    if args.whitelist:
×
1152
        with open(args.whitelist) as fd:
×
1153
            patterns = [x.strip() for x in fd.read().splitlines()
×
1154
                        if x.strip() and not x.strip().startswith('#')]
1155
        WHITELIST_PATTERNS += patterns
×
1156

1157
    if args.private is None and is_sdl_bootstrap() and args.launcher is None:
×
1158
        print('Need --private directory or ' +
×
1159
              '--launcher (SDL2/SDL3 bootstrap only)' +
1160
              'to have something to launch inside the .apk!')
1161
        sys.exit(1)
×
1162
    make_package(args)
×
1163

1164
    return args
×
1165

1166

1167
if __name__ == "__main__":
1168
    parse_args_and_make_package()
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