mirror of
https://github.com/markqvist/Sideband.git
synced 2024-10-01 03:15:37 -04:00
1065 lines
42 KiB
Python
1065 lines
42 KiB
Python
#!/usr/bin/env python3
|
|
|
|
from gzip import GzipFile
|
|
import hashlib
|
|
import json
|
|
from os.path import (
|
|
dirname, join, isfile, realpath,
|
|
relpath, split, exists, basename
|
|
)
|
|
from os import environ, listdir, makedirs, remove
|
|
import os
|
|
import shlex
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
import time
|
|
|
|
from distutils.version import LooseVersion
|
|
from fnmatch import fnmatch
|
|
import jinja2
|
|
|
|
|
|
def get_dist_info_for(key, error_if_missing=True):
|
|
try:
|
|
with open(join(dirname(__file__), 'dist_info.json'), 'r') as fileh:
|
|
info = json.load(fileh)
|
|
value = info[key]
|
|
except (OSError, KeyError) as e:
|
|
if not error_if_missing:
|
|
return None
|
|
print("BUILD FAILURE: Couldn't extract the key `" + key + "` " +
|
|
"from dist_info.json: " + str(e))
|
|
sys.exit(1)
|
|
return value
|
|
|
|
|
|
def get_hostpython():
|
|
return get_dist_info_for('hostpython')
|
|
|
|
|
|
def get_bootstrap_name():
|
|
return get_dist_info_for('bootstrap')
|
|
|
|
|
|
if os.name == 'nt':
|
|
ANDROID = 'android.bat'
|
|
ANT = 'ant.bat'
|
|
else:
|
|
ANDROID = 'android'
|
|
ANT = 'ant'
|
|
|
|
curdir = dirname(__file__)
|
|
|
|
BLACKLIST_PATTERNS = [
|
|
# code versionning
|
|
'^*.hg/*',
|
|
'^*.git/*',
|
|
'^*.bzr/*',
|
|
'^*.svn/*',
|
|
|
|
# temp files
|
|
'~',
|
|
'*.bak',
|
|
'*.swp',
|
|
|
|
# Android artifacts
|
|
'*.apk',
|
|
'*.aab',
|
|
]
|
|
|
|
WHITELIST_PATTERNS = []
|
|
|
|
if os.environ.get("P4A_BUILD_IS_RUNNING_UNITTESTS", "0") != "1":
|
|
PYTHON = get_hostpython()
|
|
_bootstrap_name = get_bootstrap_name()
|
|
else:
|
|
PYTHON = "python3"
|
|
_bootstrap_name = "sdl2"
|
|
|
|
if PYTHON is not None and not exists(PYTHON):
|
|
PYTHON = None
|
|
|
|
if _bootstrap_name in ('sdl2', 'webview', 'service_only'):
|
|
WHITELIST_PATTERNS.append('pyconfig.h')
|
|
|
|
environment = jinja2.Environment(loader=jinja2.FileSystemLoader(
|
|
join(curdir, 'templates')))
|
|
|
|
|
|
DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS = 'org.kivy.android.PythonActivity'
|
|
DEFAULT_PYTHON_SERVICE_JAVA_CLASS = 'org.kivy.android.PythonService'
|
|
|
|
|
|
def ensure_dir(path):
|
|
if not exists(path):
|
|
makedirs(path)
|
|
|
|
|
|
def render(template, dest, **kwargs):
|
|
'''Using jinja2, render `template` to the filename `dest`, supplying the
|
|
|
|
keyword arguments as template parameters.
|
|
'''
|
|
|
|
dest_dir = dirname(dest)
|
|
if dest_dir and not exists(dest_dir):
|
|
makedirs(dest_dir)
|
|
|
|
template = environment.get_template(template)
|
|
text = template.render(**kwargs)
|
|
|
|
f = open(dest, 'wb')
|
|
f.write(text.encode('utf-8'))
|
|
f.close()
|
|
|
|
|
|
def is_whitelist(name):
|
|
return match_filename(WHITELIST_PATTERNS, name)
|
|
|
|
|
|
def is_blacklist(name):
|
|
if is_whitelist(name):
|
|
return False
|
|
return match_filename(BLACKLIST_PATTERNS, name)
|
|
|
|
|
|
def match_filename(pattern_list, name):
|
|
for pattern in pattern_list:
|
|
if pattern.startswith('^'):
|
|
pattern = pattern[1:]
|
|
else:
|
|
pattern = '*/' + pattern
|
|
if fnmatch(name, pattern):
|
|
return True
|
|
|
|
|
|
def listfiles(d):
|
|
basedir = d
|
|
subdirlist = []
|
|
for item in os.listdir(d):
|
|
fn = join(d, item)
|
|
if isfile(fn):
|
|
yield fn
|
|
else:
|
|
subdirlist.append(join(basedir, item))
|
|
for subdir in subdirlist:
|
|
for fn in listfiles(subdir):
|
|
yield fn
|
|
|
|
|
|
def make_tar(tfn, source_dirs, byte_compile_python=False, optimize_python=True):
|
|
'''
|
|
Make a zip file `fn` from the contents of source_dis.
|
|
'''
|
|
|
|
def clean(tinfo):
|
|
"""cleaning function (for reproducible builds)"""
|
|
tinfo.uid = tinfo.gid = 0
|
|
tinfo.uname = tinfo.gname = ''
|
|
tinfo.mtime = 0
|
|
return tinfo
|
|
|
|
# get the files and relpath file of all the directory we asked for
|
|
files = []
|
|
for sd in source_dirs:
|
|
sd = realpath(sd)
|
|
for fn in listfiles(sd):
|
|
if is_blacklist(fn):
|
|
continue
|
|
if fn.endswith('.py') and byte_compile_python:
|
|
fn = compile_py_file(fn, optimize_python=optimize_python)
|
|
files.append((fn, relpath(realpath(fn), sd)))
|
|
files.sort() # deterministic
|
|
|
|
# create tar.gz of thoses files
|
|
gf = GzipFile(tfn, 'wb', mtime=0) # deterministic
|
|
tf = tarfile.open(None, 'w', gf, format=tarfile.USTAR_FORMAT)
|
|
dirs = []
|
|
for fn, afn in files:
|
|
dn = dirname(afn)
|
|
if dn not in dirs:
|
|
# create every dirs first if not exist yet
|
|
d = ''
|
|
for component in split(dn):
|
|
d = join(d, component)
|
|
if d.startswith('/'):
|
|
d = d[1:]
|
|
if d == '' or d in dirs:
|
|
continue
|
|
dirs.append(d)
|
|
tinfo = tarfile.TarInfo(d)
|
|
tinfo.type = tarfile.DIRTYPE
|
|
clean(tinfo)
|
|
tf.addfile(tinfo)
|
|
|
|
# put the file
|
|
tf.add(fn, afn, filter=clean)
|
|
tf.close()
|
|
gf.close()
|
|
|
|
|
|
def compile_py_file(python_file, optimize_python=True):
|
|
'''
|
|
Compile python_file to *.pyc and return the filename of the *.pyc file.
|
|
'''
|
|
|
|
if PYTHON is None:
|
|
return
|
|
|
|
args = [PYTHON, '-m', 'compileall', '-b', '-f', python_file]
|
|
if optimize_python:
|
|
# -OO = strip docstrings
|
|
args.insert(1, '-OO')
|
|
return_code = subprocess.call(args)
|
|
|
|
if return_code != 0:
|
|
print('Error while running "{}"'.format(' '.join(args)))
|
|
print('This probably means one of your Python files has a syntax '
|
|
'error, see logs above')
|
|
exit(1)
|
|
|
|
return ".".join([os.path.splitext(python_file)[0], "pyc"])
|
|
|
|
|
|
def make_package(args):
|
|
# If no launcher is specified, require a main.py/main.pyc:
|
|
if (get_bootstrap_name() != "sdl" or args.launcher is None) and \
|
|
get_bootstrap_name() not in ["webview", "service_library"]:
|
|
# (webview doesn't need an entrypoint, apparently)
|
|
if args.private is None or (
|
|
not exists(join(realpath(args.private), 'main.py')) and
|
|
not exists(join(realpath(args.private), 'main.pyc'))):
|
|
print('''BUILD FAILURE: No main.py(c) found in your app directory. This
|
|
file must exist to act as the entry point for you app. If your app is
|
|
started by a file with a different name, rename it to main.py or add a
|
|
main.py that loads it.''')
|
|
sys.exit(1)
|
|
|
|
assets_dir = "src/main/assets"
|
|
|
|
# Delete the old assets.
|
|
shutil.rmtree(assets_dir, ignore_errors=True)
|
|
ensure_dir(assets_dir)
|
|
|
|
# Add extra environment variable file into tar-able directory:
|
|
env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-")
|
|
with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f:
|
|
if hasattr(args, "window"):
|
|
f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n")
|
|
if hasattr(args, "sdl_orientation_hint"):
|
|
f.write("KIVY_ORIENTATION=" + str(args.sdl_orientation_hint) + "\n")
|
|
f.write("P4A_NUMERIC_VERSION=" + str(args.numeric_version) + "\n")
|
|
f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n")
|
|
|
|
# Package up the private data (public not supported).
|
|
use_setup_py = get_dist_info_for("use_setup_py",
|
|
error_if_missing=False) is True
|
|
private_tar_dirs = [env_vars_tarpath]
|
|
_temp_dirs_to_clean = []
|
|
try:
|
|
if args.private:
|
|
if not use_setup_py or (
|
|
not exists(join(args.private, "setup.py")) and
|
|
not exists(join(args.private, "pyproject.toml"))
|
|
):
|
|
print('No setup.py/pyproject.toml used, copying '
|
|
'full private data into .apk.')
|
|
private_tar_dirs.append(args.private)
|
|
else:
|
|
print("Copying main.py's ONLY, since other app data is "
|
|
"expected in site-packages.")
|
|
main_py_only_dir = tempfile.mkdtemp()
|
|
_temp_dirs_to_clean.append(main_py_only_dir)
|
|
|
|
# Check all main.py files we need to copy:
|
|
copy_paths = ["main.py", join("service", "main.py")]
|
|
for copy_path in copy_paths:
|
|
variants = [
|
|
copy_path,
|
|
copy_path.partition(".")[0] + ".pyc",
|
|
]
|
|
# Check in all variants with all possible endings:
|
|
for variant in variants:
|
|
if exists(join(args.private, variant)):
|
|
# Make sure surrounding directly exists:
|
|
dir_path = os.path.dirname(variant)
|
|
if (len(dir_path) > 0 and
|
|
not exists(
|
|
join(main_py_only_dir, dir_path)
|
|
)):
|
|
os.mkdir(join(main_py_only_dir, dir_path))
|
|
# Copy actual file:
|
|
shutil.copyfile(
|
|
join(args.private, variant),
|
|
join(main_py_only_dir, variant),
|
|
)
|
|
|
|
# Append directory with all main.py's to result apk paths:
|
|
private_tar_dirs.append(main_py_only_dir)
|
|
if get_bootstrap_name() == "webview":
|
|
for asset in listdir('webview_includes'):
|
|
shutil.copy(join('webview_includes', asset), join(assets_dir, asset))
|
|
|
|
for asset in args.assets:
|
|
asset_src, asset_dest = asset.split(":")
|
|
if isfile(realpath(asset_src)):
|
|
ensure_dir(dirname(join(assets_dir, asset_dest)))
|
|
shutil.copy(realpath(asset_src), join(assets_dir, asset_dest))
|
|
else:
|
|
shutil.copytree(realpath(asset_src), join(assets_dir, asset_dest))
|
|
|
|
if args.private or args.launcher:
|
|
for arch in get_dist_info_for("archs"):
|
|
libs_dir = f"libs/{arch}"
|
|
make_tar(
|
|
join(libs_dir, "libpybundle.so"),
|
|
[f"_python_bundle__{arch}"],
|
|
byte_compile_python=args.byte_compile_python,
|
|
optimize_python=args.optimize_python,
|
|
)
|
|
make_tar(
|
|
join(assets_dir, "private.tar"),
|
|
private_tar_dirs,
|
|
byte_compile_python=args.byte_compile_python,
|
|
optimize_python=args.optimize_python,
|
|
)
|
|
finally:
|
|
for directory in _temp_dirs_to_clean:
|
|
shutil.rmtree(directory)
|
|
|
|
# Remove extra env vars tar-able directory:
|
|
shutil.rmtree(env_vars_tarpath)
|
|
|
|
# Prepare some variables for templating process
|
|
res_dir = "src/main/res"
|
|
res_dir_initial = "src/res_initial"
|
|
# make res_dir stateless
|
|
if exists(res_dir_initial):
|
|
pass
|
|
#shutil.rmtree(res_dir, ignore_errors=True)
|
|
shutil.copytree(res_dir_initial, res_dir, dirs_exist_ok=True)
|
|
else:
|
|
shutil.copytree(res_dir, res_dir_initial)
|
|
|
|
# Add user resouces
|
|
for resource in args.resources:
|
|
resource_src, resource_dest = resource.split(":")
|
|
if isfile(realpath(resource_src)):
|
|
ensure_dir(dirname(join(res_dir, resource_dest)))
|
|
shutil.copy(realpath(resource_src), join(res_dir, resource_dest))
|
|
else:
|
|
shutil.copytree(realpath(resource_src),
|
|
join(res_dir, resource_dest), dirs_exist_ok=True)
|
|
|
|
default_icon = 'templates/kivy-icon.png'
|
|
default_presplash = 'templates/kivy-presplash.jpg'
|
|
shutil.copy(
|
|
args.icon or default_icon,
|
|
join(res_dir, 'mipmap/icon.png')
|
|
)
|
|
if args.icon_fg and args.icon_bg:
|
|
shutil.copy(args.icon_fg, join(res_dir, 'mipmap/icon_foreground.png'))
|
|
shutil.copy(args.icon_bg, join(res_dir, 'mipmap/icon_background.png'))
|
|
with open(join(res_dir, 'mipmap-anydpi-v26/icon.xml'), "w") as fd:
|
|
fd.write("""<?xml version="1.0" encoding="utf-8"?>
|
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
<background android:drawable="@mipmap/icon_background"/>
|
|
<foreground android:drawable="@mipmap/icon_foreground"/>
|
|
</adaptive-icon>
|
|
""")
|
|
elif args.icon_fg or args.icon_bg:
|
|
print("WARNING: Received an --icon_fg or an --icon_bg argument, but not both. "
|
|
"Ignoring.")
|
|
|
|
if get_bootstrap_name() != "service_only":
|
|
lottie_splashscreen = join(res_dir, 'raw/splashscreen.json')
|
|
if args.presplash_lottie:
|
|
shutil.copy(
|
|
'templates/lottie.xml',
|
|
join(res_dir, 'layout/lottie.xml')
|
|
)
|
|
ensure_dir(join(res_dir, 'raw'))
|
|
shutil.copy(
|
|
args.presplash_lottie,
|
|
join(res_dir, 'raw/splashscreen.json')
|
|
)
|
|
else:
|
|
if exists(lottie_splashscreen):
|
|
remove(lottie_splashscreen)
|
|
remove(join(res_dir, 'layout/lottie.xml'))
|
|
|
|
shutil.copy(
|
|
args.presplash or default_presplash,
|
|
join(res_dir, 'drawable/presplash.jpg')
|
|
)
|
|
|
|
# If extra Java jars were requested, copy them into the libs directory
|
|
jars = []
|
|
if args.add_jar:
|
|
for jarname in args.add_jar:
|
|
if not exists(jarname):
|
|
print('Requested jar does not exist: {}'.format(jarname))
|
|
sys.exit(-1)
|
|
shutil.copy(jarname, 'src/main/libs')
|
|
jars.append(basename(jarname))
|
|
|
|
# If extra aar were requested, copy them into the libs directory
|
|
aars = []
|
|
if args.add_aar:
|
|
ensure_dir("libs")
|
|
for aarname in args.add_aar:
|
|
if not exists(aarname):
|
|
print('Requested aar does not exists: {}'.format(aarname))
|
|
sys.exit(-1)
|
|
shutil.copy(aarname, 'libs')
|
|
aars.append(basename(aarname).rsplit('.', 1)[0])
|
|
|
|
versioned_name = (args.name.replace(' ', '').replace('\'', '') +
|
|
'-' + args.version)
|
|
|
|
version_code = 0
|
|
if not args.numeric_version:
|
|
"""
|
|
Set version code in format (10 + minsdk + app_version)
|
|
Historically versioning was (arch + minsdk + app_version),
|
|
with arch expressed with a single digit from 6 to 9.
|
|
Since the multi-arch support, has been changed to 10.
|
|
"""
|
|
min_sdk = args.min_sdk_version
|
|
for i in args.version.split('.'):
|
|
version_code *= 100
|
|
version_code += int(i)
|
|
args.numeric_version = "{}{}{}".format("10", min_sdk, version_code)
|
|
|
|
if args.intent_filters:
|
|
with open(args.intent_filters) as fd:
|
|
args.intent_filters = fd.read()
|
|
|
|
if not args.add_activity:
|
|
args.add_activity = []
|
|
|
|
if not args.activity_launch_mode:
|
|
args.activity_launch_mode = ''
|
|
|
|
if args.extra_source_dirs:
|
|
esd = []
|
|
for spec in args.extra_source_dirs:
|
|
if ':' in spec:
|
|
specdir, specincludes = spec.split(':')
|
|
print('WARNING: Currently gradle builds only support including source '
|
|
'directories, so when building using gradle all files in '
|
|
'{} will be included.'.format(specdir))
|
|
else:
|
|
specdir = spec
|
|
specincludes = '**'
|
|
esd.append((realpath(specdir), specincludes))
|
|
args.extra_source_dirs = esd
|
|
else:
|
|
args.extra_source_dirs = []
|
|
|
|
service = False
|
|
if args.private:
|
|
service_main = join(realpath(args.private), 'service', 'main.py')
|
|
if exists(service_main) or exists(service_main + 'o'):
|
|
service = True
|
|
|
|
service_names = []
|
|
base_service_class = args.service_class_name.split('.')[-1]
|
|
for sid, spec in enumerate(args.services):
|
|
spec = spec.split(':')
|
|
name = spec[0]
|
|
entrypoint = spec[1]
|
|
options = spec[2:]
|
|
|
|
foreground = 'foreground' in options
|
|
sticky = 'sticky' in options
|
|
|
|
service_names.append(name)
|
|
service_target_path =\
|
|
'src/main/java/{}/Service{}.java'.format(
|
|
args.package.replace(".", "/"),
|
|
name.capitalize()
|
|
)
|
|
render(
|
|
'Service.tmpl.java',
|
|
service_target_path,
|
|
name=name,
|
|
entrypoint=entrypoint,
|
|
args=args,
|
|
foreground=foreground,
|
|
sticky=sticky,
|
|
service_id=sid + 1,
|
|
base_service_class=base_service_class,
|
|
)
|
|
|
|
# Find the SDK directory and target API
|
|
with open('project.properties', 'r') as fileh:
|
|
target = fileh.read().strip()
|
|
android_api = target.split('-')[1]
|
|
|
|
if android_api.isdigit():
|
|
android_api = int(android_api)
|
|
else:
|
|
raise ValueError(
|
|
"failed to extract the Android API level from " +
|
|
"build.properties. expected int, got: '" +
|
|
str(android_api) + "'"
|
|
)
|
|
|
|
with open('local.properties', 'r') as fileh:
|
|
sdk_dir = fileh.read().strip()
|
|
sdk_dir = sdk_dir[8:]
|
|
|
|
# Try to build with the newest available build tools
|
|
ignored = {".DS_Store", ".ds_store"}
|
|
build_tools_versions = [x for x in listdir(join(sdk_dir, 'build-tools')) if x not in ignored]
|
|
build_tools_versions = sorted(build_tools_versions,
|
|
key=LooseVersion)
|
|
build_tools_version = build_tools_versions[-1]
|
|
|
|
# Folder name for launcher (used by SDL2 bootstrap)
|
|
url_scheme = 'kivy'
|
|
|
|
# Copy backup rules file if specified and update the argument
|
|
res_xml_dir = join(res_dir, 'xml')
|
|
if args.backup_rules:
|
|
ensure_dir(res_xml_dir)
|
|
shutil.copy(join(args.private, args.backup_rules), res_xml_dir)
|
|
args.backup_rules = split(args.backup_rules)[1][:-4]
|
|
|
|
# Copy res_xml files to src/main/res/xml
|
|
if args.res_xmls:
|
|
ensure_dir(res_xml_dir)
|
|
for xmlpath in args.res_xmls:
|
|
if not os.path.exists(xmlpath):
|
|
xmlpath = join(args.private, xmlpath)
|
|
shutil.copy(xmlpath, res_xml_dir)
|
|
|
|
# Render out android manifest:
|
|
manifest_path = "src/main/AndroidManifest.xml"
|
|
render_args = {
|
|
"args": args,
|
|
"service": service,
|
|
"service_names": service_names,
|
|
"android_api": android_api,
|
|
"debug": "debug" in args.build_mode,
|
|
"native_services": args.native_services
|
|
}
|
|
if get_bootstrap_name() == "sdl2":
|
|
render_args["url_scheme"] = url_scheme
|
|
render(
|
|
'AndroidManifest.tmpl.xml',
|
|
manifest_path,
|
|
**render_args)
|
|
|
|
# Copy the AndroidManifest.xml to the dist root dir so that ant
|
|
# can also use it
|
|
if exists('AndroidManifest.xml'):
|
|
remove('AndroidManifest.xml')
|
|
shutil.copy(manifest_path, 'AndroidManifest.xml')
|
|
|
|
# gradle build templates
|
|
render(
|
|
'build.tmpl.gradle',
|
|
'build.gradle',
|
|
args=args,
|
|
aars=aars,
|
|
jars=jars,
|
|
android_api=android_api,
|
|
build_tools_version=build_tools_version,
|
|
debug_build="debug" in args.build_mode,
|
|
is_library=(get_bootstrap_name() == 'service_library'),
|
|
)
|
|
|
|
# gradle properties
|
|
render(
|
|
'gradle.tmpl.properties',
|
|
'gradle.properties',
|
|
args=args)
|
|
|
|
# ant build templates
|
|
render(
|
|
'build.tmpl.xml',
|
|
'build.xml',
|
|
args=args,
|
|
versioned_name=versioned_name)
|
|
|
|
# String resources:
|
|
timestamp = time.time()
|
|
if 'SOURCE_DATE_EPOCH' in environ:
|
|
# for reproducible builds
|
|
timestamp = int(environ['SOURCE_DATE_EPOCH'])
|
|
private_version = "{} {} {}".format(
|
|
args.version,
|
|
args.numeric_version,
|
|
timestamp
|
|
)
|
|
render_args = {
|
|
"args": args,
|
|
"private_version": hashlib.sha1(private_version.encode()).hexdigest()
|
|
}
|
|
if get_bootstrap_name() == "sdl2":
|
|
render_args["url_scheme"] = url_scheme
|
|
render(
|
|
'strings.tmpl.xml',
|
|
join(res_dir, 'values/strings.xml'),
|
|
**render_args)
|
|
|
|
if exists(join("templates", "custom_rules.tmpl.xml")):
|
|
render(
|
|
'custom_rules.tmpl.xml',
|
|
'custom_rules.xml',
|
|
args=args)
|
|
|
|
if get_bootstrap_name() == "webview":
|
|
render('WebViewLoader.tmpl.java',
|
|
'src/main/java/org/kivy/android/WebViewLoader.java',
|
|
args=args)
|
|
|
|
if args.sign:
|
|
render('build.properties', 'build.properties')
|
|
else:
|
|
if exists('build.properties'):
|
|
os.remove('build.properties')
|
|
|
|
# Apply java source patches if any are present:
|
|
if exists(join('src', 'patches')):
|
|
print("Applying Java source code patches...")
|
|
for patch_name in os.listdir(join('src', 'patches')):
|
|
patch_path = join('src', 'patches', patch_name)
|
|
print("Applying patch: " + str(patch_path))
|
|
|
|
# -N: insist this is FORWARD patch, don't reverse apply
|
|
# -p1: strip first path component
|
|
# -t: batch mode, don't ask questions
|
|
patch_command = ["patch", "-N", "-p1", "-t", "-i", patch_path]
|
|
|
|
try:
|
|
# Use a dry run to establish whether the patch is already applied.
|
|
# If we don't check this, the patch may be partially applied (which is bad!)
|
|
subprocess.check_output(patch_command + ["--dry-run"])
|
|
except subprocess.CalledProcessError as e:
|
|
if e.returncode == 1:
|
|
# Return code 1 means not all hunks could be applied, this usually
|
|
# means the patch is already applied.
|
|
print("Warning: failed to apply patch (exit code 1), "
|
|
"assuming it is already applied: ",
|
|
str(patch_path))
|
|
else:
|
|
raise e
|
|
else:
|
|
# The dry run worked, so do the real thing
|
|
subprocess.check_output(patch_command)
|
|
|
|
|
|
def parse_permissions(args_permissions):
|
|
if args_permissions and isinstance(args_permissions[0], list):
|
|
args_permissions = [p for perm in args_permissions for p in perm]
|
|
|
|
def _is_advanced_permission(permission):
|
|
return permission.startswith("(") and permission.endswith(")")
|
|
|
|
def _decode_advanced_permission(permission):
|
|
SUPPORTED_PERMISSION_PROPERTIES = ["name", "maxSdkVersion", "usesPermissionFlags"]
|
|
_permission_args = permission[1:-1].split(";")
|
|
_permission_args = (arg.split("=") for arg in _permission_args)
|
|
advanced_permission = dict(_permission_args)
|
|
|
|
if "name" not in advanced_permission:
|
|
raise ValueError("Advanced permission must have a name property")
|
|
|
|
for key in advanced_permission.keys():
|
|
if key not in SUPPORTED_PERMISSION_PROPERTIES:
|
|
raise ValueError(
|
|
f"Property '{key}' is not supported. "
|
|
"Advanced permission only supports: "
|
|
f"{', '.join(SUPPORTED_PERMISSION_PROPERTIES)} properties"
|
|
)
|
|
|
|
return advanced_permission
|
|
|
|
_permissions = []
|
|
for permission in args_permissions:
|
|
if _is_advanced_permission(permission):
|
|
_permissions.append(_decode_advanced_permission(permission))
|
|
else:
|
|
if "." in permission:
|
|
_permissions.append(dict(name=permission))
|
|
else:
|
|
_permissions.append(dict(name=f"android.permission.{permission}"))
|
|
return _permissions
|
|
|
|
|
|
def get_sdl_orientation_hint(orientations):
|
|
SDL_ORIENTATION_MAP = {
|
|
"landscape": "LandscapeLeft",
|
|
"portrait": "Portrait",
|
|
"portrait-reverse": "PortraitUpsideDown",
|
|
"landscape-reverse": "LandscapeRight",
|
|
}
|
|
return " ".join(
|
|
[SDL_ORIENTATION_MAP[x] for x in orientations if x in SDL_ORIENTATION_MAP]
|
|
)
|
|
|
|
|
|
def get_manifest_orientation(orientations, manifest_orientation=None):
|
|
# If the user has specifically set an orientation to use in the manifest,
|
|
# use that.
|
|
if manifest_orientation is not None:
|
|
return manifest_orientation
|
|
|
|
# If multiple or no orientations are specified, use unspecified in the manifest,
|
|
# as we can only specify one orientation in the manifest.
|
|
if len(orientations) != 1:
|
|
return "unspecified"
|
|
|
|
# Convert the orientation to a value that can be used in the manifest.
|
|
# If the specified orientation is not supported, use unspecified.
|
|
MANIFEST_ORIENTATION_MAP = {
|
|
"landscape": "landscape",
|
|
"portrait": "portrait",
|
|
"portrait-reverse": "reversePortrait",
|
|
"landscape-reverse": "reverseLandscape",
|
|
}
|
|
return MANIFEST_ORIENTATION_MAP.get(orientations[0], "unspecified")
|
|
|
|
|
|
def get_dist_ndk_min_api_level():
|
|
# Get the default minsdk, equal to the NDK API that this dist is built against
|
|
try:
|
|
with open('dist_info.json', 'r') as fileh:
|
|
info = json.load(fileh)
|
|
ndk_api = int(info['ndk_api'])
|
|
except (OSError, KeyError, ValueError, TypeError):
|
|
print('WARNING: Failed to read ndk_api from dist info, defaulting to 12')
|
|
ndk_api = 12 # The old default before ndk_api was introduced
|
|
return ndk_api
|
|
|
|
|
|
def create_argument_parser():
|
|
ndk_api = get_dist_ndk_min_api_level()
|
|
import argparse
|
|
ap = argparse.ArgumentParser(description='''\
|
|
Package a Python application for Android (using
|
|
bootstrap ''' + get_bootstrap_name() + ''').
|
|
|
|
For this to work, Java and Ant need to be in your path, as does the
|
|
tools directory of the Android SDK.
|
|
''')
|
|
|
|
# --private is required unless for sdl2, where there's also --launcher
|
|
ap.add_argument('--private', dest='private',
|
|
help='the directory with the app source code files' +
|
|
' (containing your main.py entrypoint)',
|
|
required=(get_bootstrap_name() != "sdl2"))
|
|
ap.add_argument('--package', dest='package',
|
|
help=('The name of the java package the project will be'
|
|
' packaged under.'),
|
|
required=True)
|
|
ap.add_argument('--name', dest='name',
|
|
help=('The human-readable name of the project.'),
|
|
required=True)
|
|
ap.add_argument('--numeric-version', dest='numeric_version',
|
|
help=('The numeric version number of the project. If not '
|
|
'given, this is automatically computed from the '
|
|
'version.'))
|
|
ap.add_argument('--version', dest='version',
|
|
help=('The version number of the project. This should '
|
|
'consist of numbers and dots, and should have the '
|
|
'same number of groups of numbers as previous '
|
|
'versions.'),
|
|
required=True)
|
|
if get_bootstrap_name() == "sdl2":
|
|
ap.add_argument('--launcher', dest='launcher', action='store_true',
|
|
help=('Provide this argument to build a multi-app '
|
|
'launcher, rather than a single app.'))
|
|
ap.add_argument('--home-app', dest='home_app', action='store_true', default=False,
|
|
help=('Turn your application into a home app (launcher)'))
|
|
ap.add_argument('--permission', dest='permissions', action='append', default=[],
|
|
help='The permissions to give this app.', nargs='+')
|
|
ap.add_argument('--meta-data', dest='meta_data', action='append', default=[],
|
|
help='Custom key=value to add in application metadata')
|
|
ap.add_argument('--uses-library', dest='android_used_libs', action='append', default=[],
|
|
help='Used shared libraries included using <uses-library> tag in AndroidManifest.xml')
|
|
ap.add_argument('--asset', dest='assets',
|
|
action="append", default=[],
|
|
metavar="/path/to/source:dest",
|
|
help='Put this in the assets folder at assets/dest')
|
|
ap.add_argument('--resource', dest='resources',
|
|
action="append", default=[],
|
|
metavar="/path/to/source:kind/asset",
|
|
help='Put this in the res folder at res/kind')
|
|
ap.add_argument('--icon', dest='icon',
|
|
help=('A png file to use as the icon for '
|
|
'the application.'))
|
|
ap.add_argument('--icon-fg', dest='icon_fg',
|
|
help=('A png file to use as the foreground of the adaptive icon '
|
|
'for the application.'))
|
|
ap.add_argument('--icon-bg', dest='icon_bg',
|
|
help=('A png file to use as the background of the adaptive icon '
|
|
'for the application.'))
|
|
ap.add_argument('--service', dest='services', action='append', default=[],
|
|
help='Declare a new service entrypoint: '
|
|
'NAME:PATH_TO_PY[:foreground]')
|
|
ap.add_argument('--native-service', dest='native_services', action='append', default=[],
|
|
help='Declare a new native service: '
|
|
'package.name.service')
|
|
if get_bootstrap_name() != "service_only":
|
|
ap.add_argument('--presplash', dest='presplash',
|
|
help=('A jpeg file to use as a screen while the '
|
|
'application is loading.'))
|
|
ap.add_argument('--presplash-lottie', dest='presplash_lottie',
|
|
help=('A lottie (json) file to use as an animation while the '
|
|
'application is loading.'))
|
|
ap.add_argument('--presplash-color',
|
|
dest='presplash_color',
|
|
default='#000000',
|
|
help=('A string to set the loading screen '
|
|
'background color. '
|
|
'Supported formats are: '
|
|
'#RRGGBB #AARRGGBB or color names '
|
|
'like red, green, blue, etc.'))
|
|
ap.add_argument('--window', dest='window', action='store_true',
|
|
default=False,
|
|
help='Indicate if the application will be windowed')
|
|
ap.add_argument('--manifest-orientation', dest='manifest_orientation',
|
|
help=('The orientation that will be set in the '
|
|
'android:screenOrientation attribute of the activity '
|
|
'in the AndroidManifest.xml file. If not set, '
|
|
'the value will be synthesized from the --orientation option.'))
|
|
ap.add_argument('--orientation', dest='orientation',
|
|
action="append", default=[],
|
|
choices=['portrait', 'landscape', 'landscape-reverse', 'portrait-reverse'],
|
|
help=('The orientations that the app will display in. '
|
|
'Since Android ignores android:screenOrientation '
|
|
'when in multi-window mode (Which is the default on Android 12+), '
|
|
'this option will also set the window orientation hints '
|
|
'for apps using the (default) SDL bootstrap.'
|
|
'If multiple orientations are given, android:screenOrientation '
|
|
'will be set to "unspecified"'))
|
|
|
|
ap.add_argument('--enable-androidx', dest='enable_androidx',
|
|
action='store_true',
|
|
help=('Enable the AndroidX support library, '
|
|
'requires api = 28 or greater'))
|
|
ap.add_argument('--android-entrypoint', dest='android_entrypoint',
|
|
default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS,
|
|
help='Defines which java class will be used for startup, usually a subclass of PythonActivity')
|
|
ap.add_argument('--android-apptheme', dest='android_apptheme',
|
|
default='@android:style/Theme.NoTitleBar',
|
|
help='Defines which app theme should be selected for the main activity')
|
|
ap.add_argument('--add-compile-option', dest='compile_options', default=[],
|
|
action='append', help='add compile options to gradle.build')
|
|
ap.add_argument('--add-gradle-repository', dest='gradle_repositories',
|
|
default=[],
|
|
action='append',
|
|
help='Ddd a repository for gradle')
|
|
ap.add_argument('--add-packaging-option', dest='packaging_options',
|
|
default=[],
|
|
action='append',
|
|
help='Dndroid packaging options')
|
|
|
|
ap.add_argument('--wakelock', dest='wakelock', action='store_true',
|
|
help=('Indicate if the application needs the device '
|
|
'to stay on'))
|
|
ap.add_argument('--blacklist', dest='blacklist',
|
|
default=join(curdir, 'blacklist.txt'),
|
|
help=('Use a blacklist file to match unwanted file in '
|
|
'the final APK'))
|
|
ap.add_argument('--whitelist', dest='whitelist',
|
|
default=join(curdir, 'whitelist.txt'),
|
|
help=('Use a whitelist file to prevent blacklisting of '
|
|
'file in the final APK'))
|
|
ap.add_argument('--release', dest='build_mode', action='store_const',
|
|
const='release', default='debug',
|
|
help='Build your app as a non-debug release build. '
|
|
'(Disables gdb debugging among other things)')
|
|
ap.add_argument('--with-debug-symbols', dest='with_debug_symbols',
|
|
action='store_const', const=True, default=False,
|
|
help='Will keep debug symbols from `.so` files.')
|
|
ap.add_argument('--add-jar', dest='add_jar', action='append',
|
|
help=('Add a Java .jar to the libs, so you can access its '
|
|
'classes with pyjnius. You can specify this '
|
|
'argument more than once to include multiple jars'))
|
|
ap.add_argument('--add-aar', dest='add_aar', action='append',
|
|
help=('Add an aar dependency manually'))
|
|
ap.add_argument('--depend', dest='depends', action='append',
|
|
help=('Add a external dependency '
|
|
'(eg: com.android.support:appcompat-v7:19.0.1)'))
|
|
# The --sdk option has been removed, it is ignored in favour of
|
|
# --android-api handled by toolchain.py
|
|
ap.add_argument('--sdk', dest='sdk_version', default=-1,
|
|
type=int, help=('Deprecated argument, does nothing'))
|
|
ap.add_argument('--minsdk', dest='min_sdk_version',
|
|
default=ndk_api, type=int,
|
|
help=('Minimum Android SDK version that the app supports. '
|
|
'Defaults to {}.'.format(ndk_api)))
|
|
ap.add_argument('--allow-minsdk-ndkapi-mismatch', default=False,
|
|
action='store_true',
|
|
help=('Allow the --minsdk argument to be different from '
|
|
'the discovered ndk_api in the dist'))
|
|
ap.add_argument('--intent-filters', dest='intent_filters',
|
|
help=('Add intent-filters xml rules to the '
|
|
'AndroidManifest.xml file. The argument is a '
|
|
'filename containing xml. The filename should be '
|
|
'located relative to the python-for-android '
|
|
'directory'))
|
|
ap.add_argument('--res_xml', dest='res_xmls', action='append', default=[],
|
|
help='Add files to res/xml directory (for example device-filters)', nargs='+')
|
|
ap.add_argument('--with-billing', dest='billing_pubkey',
|
|
help='If set, the billing service will be added (not implemented)')
|
|
ap.add_argument('--add-source', dest='extra_source_dirs', action='append',
|
|
help='Include additional source dirs in Java build')
|
|
if get_bootstrap_name() == "webview":
|
|
ap.add_argument('--port',
|
|
help='The port on localhost that the WebView will access',
|
|
default='5000')
|
|
ap.add_argument('--try-system-python-compile', dest='try_system_python_compile',
|
|
action='store_true',
|
|
help='Use the system python during compileall if possible.')
|
|
ap.add_argument('--sign', action='store_true',
|
|
help=('Try to sign the APK with your credentials. You must set '
|
|
'the appropriate environment variables.'))
|
|
ap.add_argument('--add-activity', dest='add_activity', action='append',
|
|
help='Add this Java class as an Activity to the manifest.')
|
|
ap.add_argument('--activity-launch-mode',
|
|
dest='activity_launch_mode',
|
|
default='singleTask',
|
|
help='Set the launch mode of the main activity in the manifest.')
|
|
ap.add_argument('--allow-backup', dest='allow_backup', default='true',
|
|
help="if set to 'false', then android won't backup the application.")
|
|
ap.add_argument('--backup-rules', dest='backup_rules', default='',
|
|
help=('Backup rules for Android Auto Backup. Argument is a '
|
|
'filename containing xml. The filename should be '
|
|
'located relative to the private directory containing your source code '
|
|
'files (containing your main.py entrypoint). '
|
|
'See https://developer.android.com/guide/topics/data/'
|
|
'autobackup#IncludingFiles for more information'))
|
|
ap.add_argument('--no-byte-compile-python', dest='byte_compile_python',
|
|
action='store_false', default=True,
|
|
help='Skip byte compile for .py files.')
|
|
ap.add_argument('--no-optimize-python', dest='optimize_python',
|
|
action='store_false', default=True,
|
|
help=('Whether to compile to optimised .pyc files, using -OO '
|
|
'(strips docstrings and asserts)'))
|
|
ap.add_argument('--extra-manifest-xml', default='',
|
|
help=('Extra xml to write directly inside the <manifest> element of'
|
|
'AndroidManifest.xml'))
|
|
ap.add_argument('--extra-manifest-application-arguments', default='',
|
|
help='Extra arguments to be added to the <manifest><application> tag of'
|
|
'AndroidManifest.xml')
|
|
ap.add_argument('--manifest-placeholders', dest='manifest_placeholders',
|
|
default='[:]', help=('Inject build variables into the manifest '
|
|
'via the manifestPlaceholders property'))
|
|
ap.add_argument('--service-class-name', dest='service_class_name', default=DEFAULT_PYTHON_SERVICE_JAVA_CLASS,
|
|
help='Use that parameter if you need to implement your own PythonServive Java class')
|
|
ap.add_argument('--activity-class-name', dest='activity_class_name', default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS,
|
|
help='The full java class name of the main activity')
|
|
|
|
return ap
|
|
|
|
|
|
def parse_args_and_make_package(args=None):
|
|
global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON
|
|
|
|
ndk_api = get_dist_ndk_min_api_level()
|
|
ap = create_argument_parser()
|
|
|
|
# Put together arguments, and add those from .p4a config file:
|
|
if args is None:
|
|
args = sys.argv[1:]
|
|
|
|
def _read_configuration():
|
|
if not exists(".p4a"):
|
|
return
|
|
print("Reading .p4a configuration")
|
|
with open(".p4a") as fd:
|
|
lines = fd.readlines()
|
|
lines = [shlex.split(line)
|
|
for line in lines if not line.startswith("#")]
|
|
for line in lines:
|
|
for arg in line:
|
|
args.append(arg)
|
|
_read_configuration()
|
|
|
|
args = ap.parse_args(args)
|
|
|
|
if args.name and args.name[0] == '"' and args.name[-1] == '"':
|
|
args.name = args.name[1:-1]
|
|
|
|
if ndk_api != args.min_sdk_version:
|
|
print(('WARNING: --minsdk argument does not match the api that is '
|
|
'compiled against. Only proceed if you know what you are '
|
|
'doing, otherwise use --minsdk={} or recompile against api '
|
|
'{}').format(ndk_api, args.min_sdk_version))
|
|
if not args.allow_minsdk_ndkapi_mismatch:
|
|
print('You must pass --allow-minsdk-ndkapi-mismatch to build '
|
|
'with --minsdk different to the target NDK api from the '
|
|
'build step')
|
|
sys.exit(1)
|
|
else:
|
|
print('Proceeding with --minsdk not matching build target api')
|
|
|
|
if args.billing_pubkey:
|
|
print('Billing not yet supported!')
|
|
sys.exit(1)
|
|
|
|
if args.sdk_version == -1:
|
|
print('WARNING: Received a --sdk argument, but this argument is '
|
|
'deprecated and does nothing.')
|
|
args.sdk_version = -1 # ensure it is not used
|
|
|
|
args.permissions = parse_permissions(args.permissions)
|
|
|
|
args.manifest_orientation = get_manifest_orientation(
|
|
args.orientation, args.manifest_orientation
|
|
)
|
|
|
|
if get_bootstrap_name() == "sdl2":
|
|
args.sdl_orientation_hint = get_sdl_orientation_hint(args.orientation)
|
|
|
|
if args.res_xmls and isinstance(args.res_xmls[0], list):
|
|
args.res_xmls = [x for res in args.res_xmls for x in res]
|
|
|
|
if args.try_system_python_compile:
|
|
# Hardcoding python2.7 is okay for now, as python3 skips the
|
|
# compilation anyway
|
|
python_executable = 'python2.7'
|
|
try:
|
|
subprocess.call([python_executable, '--version'])
|
|
except (OSError, subprocess.CalledProcessError):
|
|
pass
|
|
else:
|
|
PYTHON = python_executable
|
|
|
|
if args.blacklist:
|
|
with open(args.blacklist) as fd:
|
|
patterns = [x.strip() for x in fd.read().splitlines()
|
|
if x.strip() and not x.strip().startswith('#')]
|
|
BLACKLIST_PATTERNS += patterns
|
|
|
|
if args.whitelist:
|
|
with open(args.whitelist) as fd:
|
|
patterns = [x.strip() for x in fd.read().splitlines()
|
|
if x.strip() and not x.strip().startswith('#')]
|
|
WHITELIST_PATTERNS += patterns
|
|
|
|
if args.private is None and \
|
|
get_bootstrap_name() == 'sdl2' and args.launcher is None:
|
|
print('Need --private directory or ' +
|
|
'--launcher (SDL2 bootstrap only)' +
|
|
'to have something to launch inside the .apk!')
|
|
sys.exit(1)
|
|
make_package(args)
|
|
|
|
return args
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if get_bootstrap_name() in ('sdl2', 'webview', 'service_only'):
|
|
WHITELIST_PATTERNS.append('pyconfig.h')
|
|
parse_args_and_make_package()
|