From 0b81c78f59d033027a88dd68f6952877b492b983 Mon Sep 17 00:00:00 2001 From: Philipp Auersperg-Castell Date: Wed, 1 Apr 2020 11:56:17 +0200 Subject: [PATCH 1/3] p4a support for creating aar libraries * ported @xuhcc's PR 1063 to recent p4a master * bootstraps dirs are cumulatively copied based on their inheritance, can be arbitrarily deep * add symlink param to copy_files, when set the copy target are symlinked * support for the aar directive in buildozer * create a 'p4a aar' command, so that lots of cluttering conditionals can be moved away from toolchain.apk() * began to remove ant support (@inclement allowed me to do so) * renamed library bootstrap to service_library, because that describe it better * test setup setup_testlib_service.py * renamed symlink_java_src to symlink_bootstrap_files * None is not allowed as bootstrap parameter * switched tests to use the sdl2 bootstrap --- .../bootstraps/service_library/__init__.py | 9 + .../jni/application/src/bootstrap_name.h | 6 + .../java/org/kivy/android/PythonActivity.java | 9 + .../java/org/renpy/android/AssetExtract.java | 115 +++++++ .../build/templates/AndroidManifest.tmpl.xml | 18 ++ .../build/templates/Service.tmpl.java | 126 ++++++++ pythonforandroid/recipes/android/__init__.py | 10 +- pythonforandroid/toolchain.py | 289 ++++++++++-------- testapps/on_device_unit_tests/setup.py | 1 + 9 files changed, 440 insertions(+), 143 deletions(-) create mode 100644 pythonforandroid/bootstraps/service_library/__init__.py create mode 100644 pythonforandroid/bootstraps/service_library/build/jni/application/src/bootstrap_name.h create mode 100644 pythonforandroid/bootstraps/service_library/build/src/main/java/org/kivy/android/PythonActivity.java create mode 100644 pythonforandroid/bootstraps/service_library/build/src/main/java/org/renpy/android/AssetExtract.java create mode 100644 pythonforandroid/bootstraps/service_library/build/templates/AndroidManifest.tmpl.xml create mode 100644 pythonforandroid/bootstraps/service_library/build/templates/Service.tmpl.java diff --git a/pythonforandroid/bootstraps/service_library/__init__.py b/pythonforandroid/bootstraps/service_library/__init__.py new file mode 100644 index 0000000000..0b41be87f2 --- /dev/null +++ b/pythonforandroid/bootstraps/service_library/__init__.py @@ -0,0 +1,9 @@ +from pythonforandroid.bootstraps.service_only import ServiceOnlyBootstrap + + +class ServiceLibraryBootstrap(ServiceOnlyBootstrap): + + name = 'service_library' + + +bootstrap = ServiceLibraryBootstrap() diff --git a/pythonforandroid/bootstraps/service_library/build/jni/application/src/bootstrap_name.h b/pythonforandroid/bootstraps/service_library/build/jni/application/src/bootstrap_name.h new file mode 100644 index 0000000000..01fd122890 --- /dev/null +++ b/pythonforandroid/bootstraps/service_library/build/jni/application/src/bootstrap_name.h @@ -0,0 +1,6 @@ + +#define BOOTSTRAP_NAME_LIBRARY +#define BOOTSTRAP_USES_NO_SDL_HEADERS + +const char bootstrap_name[] = "service_library"; + diff --git a/pythonforandroid/bootstraps/service_library/build/src/main/java/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/service_library/build/src/main/java/org/kivy/android/PythonActivity.java new file mode 100644 index 0000000000..c54c19b982 --- /dev/null +++ b/pythonforandroid/bootstraps/service_library/build/src/main/java/org/kivy/android/PythonActivity.java @@ -0,0 +1,9 @@ +package org.kivy.android; + +import android.app.Activity; + +// Required by PythonService class +public class PythonActivity extends Activity { + +} + diff --git a/pythonforandroid/bootstraps/service_library/build/src/main/java/org/renpy/android/AssetExtract.java b/pythonforandroid/bootstraps/service_library/build/src/main/java/org/renpy/android/AssetExtract.java new file mode 100644 index 0000000000..092730dfe1 --- /dev/null +++ b/pythonforandroid/bootstraps/service_library/build/src/main/java/org/renpy/android/AssetExtract.java @@ -0,0 +1,115 @@ +// This string is autogenerated by ChangeAppSettings.sh, do not change +// spaces amount +package org.renpy.android; + +import java.io.*; + +import android.content.Context; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.File; + +import java.util.zip.GZIPInputStream; + +import android.content.res.AssetManager; + +import org.kamranzafar.jtar.*; + +public class AssetExtract { + + private AssetManager mAssetManager = null; + private Context ctx = null; + + public AssetExtract(Context context) { + ctx = context; + mAssetManager = ctx.getAssets(); + } + + public boolean extractTar(String asset, String target) { + + byte buf[] = new byte[1024 * 1024]; + + InputStream assetStream = null; + TarInputStream tis = null; + + try { + assetStream = mAssetManager.open(asset, AssetManager.ACCESS_STREAMING); + tis = new TarInputStream(new BufferedInputStream(new GZIPInputStream(new BufferedInputStream(assetStream, 8192)), 8192)); + } catch (IOException e) { + Log.e("python", "opening up extract tar", e); + return false; + } + + while (true) { + TarEntry entry = null; + + try { + entry = tis.getNextEntry(); + } catch ( java.io.IOException e ) { + Log.e("python", "extracting tar", e); + return false; + } + + if ( entry == null ) { + break; + } + + Log.v("python", "extracting " + entry.getName()); + + if (entry.isDirectory()) { + + try { + new File(target +"/" + entry.getName()).mkdirs(); + } catch ( SecurityException e ) { }; + + continue; + } + + OutputStream out = null; + String path = target + "/" + entry.getName(); + + try { + out = new BufferedOutputStream(new FileOutputStream(path), 8192); + } catch ( FileNotFoundException e ) { + } catch ( SecurityException e ) { }; + + if ( out == null ) { + Log.e("python", "could not open " + path); + return false; + } + + try { + while (true) { + int len = tis.read(buf); + + if (len == -1) { + break; + } + + out.write(buf, 0, len); + } + + out.flush(); + out.close(); + } catch ( java.io.IOException e ) { + Log.e("python", "extracting zip", e); + return false; + } + } + + try { + tis.close(); + assetStream.close(); + } catch (IOException e) { + // pass + } + + return true; + } +} diff --git a/pythonforandroid/bootstraps/service_library/build/templates/AndroidManifest.tmpl.xml b/pythonforandroid/bootstraps/service_library/build/templates/AndroidManifest.tmpl.xml new file mode 100644 index 0000000000..f9b5afe3a5 --- /dev/null +++ b/pythonforandroid/bootstraps/service_library/build/templates/AndroidManifest.tmpl.xml @@ -0,0 +1,18 @@ + + + + + + + + {% for name in service_names %} + + {% endfor %} + + + diff --git a/pythonforandroid/bootstraps/service_library/build/templates/Service.tmpl.java b/pythonforandroid/bootstraps/service_library/build/templates/Service.tmpl.java new file mode 100644 index 0000000000..4f6f7d1e26 --- /dev/null +++ b/pythonforandroid/bootstraps/service_library/build/templates/Service.tmpl.java @@ -0,0 +1,126 @@ +package {{ args.package }}; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; + +import android.os.Build; +import android.content.Intent; +import android.content.Context; +import android.content.res.Resources; +import android.util.Log; + +import org.renpy.android.AssetExtract; +import org.kivy.android.PythonService; + +public class Service{{ name|capitalize }} extends PythonService { + + private static final String TAG = "PythonService"; + + public static void prepare(Context ctx) { + String appRoot = getAppRoot(ctx); + Log.v(TAG, "Ready to unpack"); + File app_root_file = new File(appRoot); + unpackData(ctx, "private", app_root_file); + } + + public static void start(Context ctx, String pythonServiceArgument) { + String appRoot = getAppRoot(ctx); + Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); + intent.putExtra("androidPrivate", appRoot); + intent.putExtra("androidArgument", appRoot); + intent.putExtra("serviceEntrypoint", "{{ entrypoint }}"); + intent.putExtra("serviceTitle", "{{ name|capitalize }}"); + intent.putExtra("serviceDescription", ""); + intent.putExtra("pythonName", "{{ name }}"); + intent.putExtra("serviceStartAsForeground", "{{ foreground|lower }}"); + intent.putExtra("pythonHome", appRoot); + intent.putExtra("androidUnpack", appRoot); + intent.putExtra("pythonPath", appRoot + ":" + appRoot + "/lib"); + intent.putExtra("pythonServiceArgument", pythonServiceArgument); + + //foreground: {{foreground}} + {% if foreground %} + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ctx.startForegroundService(intent); + } else { + ctx.startService(intent); + } + {% else %} + ctx.startService(intent); + {% endif %} + } + + public static String getAppRoot(Context ctx) { + String app_root = ctx.getFilesDir().getAbsolutePath() + "/app"; + return app_root; + } + + public static String getResourceString(Context ctx, String name) { + // Taken from org.renpy.android.ResourceManager + Resources res = ctx.getResources(); + int id = res.getIdentifier(name, "string", ctx.getPackageName()); + return res.getString(id); + } + + public static void unpackData(Context ctx, final String resource, File target) { + // Taken from PythonActivity class + + Log.v(TAG, "UNPACKING!!! " + resource + " " + target.getName()); + + // The version of data in memory and on disk. + String data_version = getResourceString(ctx, resource + "_version"); + String disk_version = null; + + Log.v(TAG, "Data version is " + data_version); + + // If no version, no unpacking is necessary. + if (data_version == null) { + return; + } + + // Check the current disk version, if any. + String filesDir = target.getAbsolutePath(); + String disk_version_fn = filesDir + "/" + resource + ".version"; + + try { + byte buf[] = new byte[64]; + InputStream is = new FileInputStream(disk_version_fn); + int len = is.read(buf); + disk_version = new String(buf, 0, len); + is.close(); + } catch (Exception e) { + disk_version = ""; + } + + // If the disk data is out of date, extract it and write the + // version file. + // if (! data_version.equals(disk_version)) { + if (! data_version.equals(disk_version)) { + Log.v(TAG, "Extracting " + resource + " assets."); + + // Don't delete existing files + // recursiveDelete(target); + target.mkdirs(); + + AssetExtract ae = new AssetExtract(ctx); + if (!ae.extractTar(resource + ".mp3", target.getAbsolutePath())) { + Log.v(TAG, "Could not extract " + resource + " data."); + } + + try { + // Write .nomedia. + new File(target, ".nomedia").createNewFile(); + + // Write version file. + FileOutputStream os = new FileOutputStream(disk_version_fn); + os.write(data_version.getBytes()); + os.close(); + } catch (Exception e) { + Log.w("python", e); + } + } + } + +} diff --git a/pythonforandroid/recipes/android/__init__.py b/pythonforandroid/recipes/android/__init__.py index 6d196fe321..ba45fe95b1 100644 --- a/pythonforandroid/recipes/android/__init__.py +++ b/pythonforandroid/recipes/android/__init__.py @@ -34,14 +34,8 @@ def prebuild_arch(self, arch): if isinstance(ctx_bootstrap, bytes): ctx_bootstrap = ctx_bootstrap.decode('utf-8') bootstrap = bootstrap_name = ctx_bootstrap - - is_sdl2 = bootstrap_name in ('sdl2', 'sdl2python3', 'sdl2_gradle') - is_webview = bootstrap_name == 'webview' - is_service_only = bootstrap_name == 'service_only' - - if is_sdl2 or is_webview or is_service_only: - if is_sdl2: - bootstrap = 'sdl2' + is_sdl2 = (bootstrap_name == "sdl2") + if bootstrap_name in ["sdl2", "webview", "service_only", "service_library"]: java_ns = u'org.kivy.android' jni_ns = u'org/kivy/android' else: diff --git a/pythonforandroid/toolchain.py b/pythonforandroid/toolchain.py index 955d8a8a52..bc427f6d35 100644 --- a/pythonforandroid/toolchain.py +++ b/pythonforandroid/toolchain.py @@ -138,7 +138,7 @@ def require_prebuilt_dist(func): """ @wraps(func) - def wrapper_func(self, args): + def wrapper_func(self, args, **kw): ctx = self.ctx ctx.set_archs(self._archs) ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir, @@ -152,7 +152,7 @@ def wrapper_func(self, args): info_notify('No dist exists that meets your requirements, ' 'so one will be built.') build_dist_from_args(ctx, dist, args) - func(self, args) + func(self, args, **kw) return wrapper_func @@ -196,10 +196,6 @@ def build_dist_from_args(ctx, dist, args): ctx.recipe_build_order)) info('Dist will also contain modules ({}) installed from pip'.format( ', '.join(ctx.python_modules))) - if hasattr(args, "build_mode") and args.build_mode == "debug": - info('Building WITH debugging symbols (no --release option used)') - else: - info('Building WITHOUT debugging symbols (--release option used)') ctx.distribution = dist ctx.prepare_bootstrap(bs) @@ -293,11 +289,11 @@ def __init__(self): '*minimal supported* API, not normally the same as your --android-api. ' 'Defaults to min(ANDROID_API, {}) if not specified.').format(RECOMMENDED_NDK_API)) generic_parser.add_argument( - '--symlink-java-src', '--symlink_java_src', + '--symlink-bootstrap-files', '--ssymlink_bootstrap_files', action='store_true', - dest='symlink_java_src', + dest='symlink_bootstrap_files', default=False, - help=('If True, symlinks the java src folder during build and dist ' + help=('If True, symlinks the bootstrap files ' 'creation. This is useful for development only, it could also' ' cause weird problems.')) @@ -485,31 +481,27 @@ def add_parser(subparsers, *args, **kwargs): action='store_true', help='Symlink the dist instead of copying') - parser_apk = add_parser( - subparsers, - 'apk', help='Build an APK', - parents=[generic_parser]) + parser_packaging = argparse.ArgumentParser( + parents=[generic_parser], + add_help=False, + description='common options for packaging (apk, aar)') + # This is actually an internal argument of the build.py # (see pythonforandroid/bootstraps/common/build/build.py). # However, it is also needed before the distribution is finally # assembled for locating the setup.py / other build systems, which # is why we also add it here: - parser_apk.add_argument( + parser_packaging.add_argument( '--private', dest='private', help='the directory with the app source code files' + ' (containing your main.py entrypoint)', required=False, default=None) - parser_apk.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)') - parser_apk.add_argument( + parser_packaging.add_argument( '--use-setup-py', dest="use_setup_py", action='store_true', default=False, help="Process the setup.py of a project if present. " + "(Experimental!") - parser_apk.add_argument( + parser_packaging.add_argument( '--ignore-setup-py', dest="ignore_setup_py", action='store_true', default=False, help="Don't run the setup.py of a project if present. " + @@ -517,20 +509,35 @@ def add_parser(subparsers, *args, **kwargs): "designed to work inside p4a (e.g. by installing " + "dependencies that won't work or aren't desired " + "on Android") - parser_apk.add_argument( + parser_packaging.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)') + parser_packaging.add_argument( '--keystore', dest='keystore', action='store', default=None, help=('Keystore for JAR signing key, will use jarsigner ' 'default if not specified (release build only)')) - parser_apk.add_argument( + parser_packaging.add_argument( '--signkey', dest='signkey', action='store', default=None, help='Key alias to sign PARSER_APK. with (release build only)') - parser_apk.add_argument( + parser_packaging.add_argument( '--keystorepw', dest='keystorepw', action='store', default=None, help='Password for keystore') - parser_apk.add_argument( + parser_packaging.add_argument( '--signkeypw', dest='signkeypw', action='store', default=None, help='Password for key alias') + add_parser( + subparsers, + 'aar', help='Build an AAR', + parents=[parser_packaging]) + + add_parser( + subparsers, + 'apk', help='Build an APK', + parents=[parser_packaging]) + add_parser( subparsers, 'create', help='Compile a set of requirements into a dist', @@ -668,7 +675,7 @@ def add_parser(subparsers, *args, **kwargs): self.ndk_dir = args.ndk_dir self.android_api = args.android_api self.ndk_api = args.ndk_api - self.ctx.symlink_java_src = args.symlink_java_src + self.ctx.symlink_bootstrap_files = args.symlink_bootstrap_files self.ctx.java_build_tool = args.java_build_tool self._archs = split_argument_list(args.arch) @@ -677,7 +684,8 @@ def add_parser(subparsers, *args, **kwargs): self.ctx.copy_libs = args.copy_libs # Each subparser corresponds to a method - getattr(self, args.subparser_name.replace('-', '_'))(args) + command = args.subparser_name.replace('-', '_') + getattr(self, command)(args) @staticmethod def warn_on_carriage_return_args(args): @@ -927,17 +935,17 @@ def _dist(self): ctx.distribution = dist return dist - @require_prebuilt_dist - def apk(self, args): - """Create an APK using the given distribution.""" - - ctx = self.ctx - dist = self._dist + @staticmethod + def _fix_args(args): + """ + Manually fixing these arguments at the string stage is + unsatisfactory and should probably be changed somehow, but + we can't leave it until later as the build.py scripts assume + they are in the current directory. + works in-place + :param args: parser args + """ - # Manually fixing these arguments at the string stage is - # unsatisfactory and should probably be changed somehow, but - # we can't leave it until later as the build.py scripts assume - # they are in the current directory. fix_args = ('--dir', '--private', '--add-jar', '--add-source', '--whitelist', '--blacklist', '--presplash', '--icon') unknown_args = args.unknown_args @@ -950,6 +958,12 @@ def apk(self, args): elif i + 1 < len(unknown_args): unknown_args[i+1] = realpath(expanduser(unknown_args[i+1])) + @staticmethod + def _prepare_release_env(args): + """ + prepares envitonment dict with the necessary flags for signing an apk + :param args: parser args + """ env = os.environ.copy() if args.build_mode == 'release': if args.keystore: @@ -963,129 +977,134 @@ def apk(self, args): elif args.keystorepw and 'P4A_RELEASE_KEYALIAS_PASSWD' not in env: env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.keystorepw - build = imp.load_source('build', join(dist.dist_dir, 'build.py')) + return env + + def _build_package(self, args, package_type): + """ + Creates an android package using gradle + :param args: parser args + :param package_type: one of 'apk', 'aar' + :return (gradle output, build_args) + """ + ctx = self.ctx + dist = self._dist + bs = Bootstrap.get_bootstrap(args.bootstrap, ctx) + ctx.prepare_bootstrap(bs) + self._fix_args(args) + env = self._prepare_release_env(args) + with current_directory(dist.dist_dir): self.hook("before_apk_build") os.environ["ANDROID_API"] = str(self.ctx.android_api) + build = imp.load_source('build', join(dist.dist_dir, 'build.py')) build_args = build.parse_args_and_make_package( args.unknown_args ) + self.hook("after_apk_build") self.hook("before_apk_assemble") + build_tools_versions = os.listdir(join(ctx.sdk_dir, + 'build-tools')) + build_tools_versions = sorted(build_tools_versions, + key=LooseVersion) + build_tools_version = build_tools_versions[-1] + info(('Detected highest available build tools ' + 'version to be {}').format(build_tools_version)) + + if build_tools_version < '25.0': + raise BuildInterruptingException( + 'build_tools >= 25 is required, but %s is installed' % build_tools_version) + if not exists("gradlew"): + raise BuildInterruptingException("gradlew file is missing") + + env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir + env["ANDROID_HOME"] = self.ctx.sdk_dir + + gradlew = sh.Command('./gradlew') + + if exists('/usr/bin/dos2unix'): + # .../dists/bdisttest_python3/gradlew + # .../build/bootstrap_builds/sdl2-python3/gradlew + # if docker on windows, gradle contains CRLF + output = shprint( + sh.Command('dos2unix'), gradlew._path.decode('utf8'), + _tail=20, _critical=True, _env=env + ) + if args.build_mode == "debug": + gradle_task = "assembleDebug" + elif args.build_mode == "release": + gradle_task = "assembleRelease" + else: + raise BuildInterruptingException( + "Unknown build mode {} for apk()".format(args.build_mode)) + output = shprint(gradlew, gradle_task, _tail=20, + _critical=True, _env=env) + return output, build_args - build_type = ctx.java_build_tool - if build_type == 'auto': - info('Selecting java build tool:') - - build_tools_versions = os.listdir(join(ctx.sdk_dir, - 'build-tools')) - build_tools_versions = sorted(build_tools_versions, - key=LooseVersion) - build_tools_version = build_tools_versions[-1] - info(('Detected highest available build tools ' - 'version to be {}').format(build_tools_version)) - - if build_tools_version >= '25.0' and exists('gradlew'): - build_type = 'gradle' - info(' Building with gradle, as gradle executable is ' - 'present') - else: - build_type = 'ant' - if build_tools_version < '25.0': - info((' Building with ant, as the highest ' - 'build-tools-version is only {}').format( - build_tools_version)) - else: - info(' Building with ant, as no gradle executable ' - 'detected') - - if build_type == 'gradle': - # gradle-based build - env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir - env["ANDROID_HOME"] = self.ctx.sdk_dir - - gradlew = sh.Command('./gradlew') - if exists('/usr/bin/dos2unix'): - # .../dists/bdisttest_python3/gradlew - # .../build/bootstrap_builds/sdl2-python3/gradlew - # if docker on windows, gradle contains CRLF - output = shprint( - sh.Command('dos2unix'), gradlew._path.decode('utf8'), - _tail=20, _critical=True, _env=env - ) - if args.build_mode == "debug": - gradle_task = "assembleDebug" - elif args.build_mode == "release": - gradle_task = "assembleRelease" - else: - raise BuildInterruptingException( - "Unknown build mode {} for apk()". - format(args.build_mode) - ) - output = shprint(gradlew, gradle_task, _tail=20, - _critical=True, _env=env) + def _finish_package(self, args, output, build_args, package_type, output_dir): + """ + Finishes the package after the gradle script run + :param args: the parser args + :param output: RunningCommand output + :param build_args: build args as returned by build.parse_args + :param package_type: one of 'apk', 'aar' + :param output_dir: where to put the package file + """ - # gradle output apks somewhere else - # and don't have version in file - apk_dir = join(dist.dist_dir, - "build", "outputs", "apk", - args.build_mode) - apk_glob = "*-{}.apk" - apk_add_version = True + package_glob = "*-{}.%s" % package_type + package_add_version = True - else: - # ant-based build - try: - ant = sh.Command('ant') - except sh.CommandNotFound: - raise BuildInterruptingException( - 'Could not find ant binary, please install it ' - 'and make sure it is in your $PATH.') - output = shprint(ant, args.build_mode, _tail=20, - _critical=True, _env=env) - apk_dir = join(dist.dist_dir, "bin") - apk_glob = "*-*-{}.apk" - apk_add_version = False - - self.hook("after_apk_assemble") - - info_main('# Copying APK to current directory') - - apk_re = re.compile(r'.*Package: (.*\.apk)$') - apk_file = None + self.hook("after_apk_assemble") + + info_main('# Copying android package to current directory') + + package_re = re.compile(r'.*Package: (.*\.apk)$') + package_file = None for line in reversed(output.splitlines()): - m = apk_re.match(line) + m = package_re.match(line) if m: - apk_file = m.groups()[0] + package_file = m.groups()[0] break - - if not apk_file: - info_main('# APK filename not found in build output. Guessing...') + if not package_file: + info_main('# Android package filename not found in build output. Guessing...') if args.build_mode == "release": suffixes = ("release", "release-unsigned") else: suffixes = ("debug", ) for suffix in suffixes: - apks = glob.glob(join(apk_dir, apk_glob.format(suffix))) - if apks: - if len(apks) > 1: + + package_files = glob.glob(join(output_dir, package_glob.format(suffix))) + if package_files: + if len(package_files) > 1: info('More than one built APK found... guessing you ' - 'just built {}'.format(apks[-1])) - apk_file = apks[-1] + 'just built {}'.format(package_files[-1])) + package_file = package_files[-1] break else: raise BuildInterruptingException('Couldn\'t find the built APK') - info_main('# Found APK file: {}'.format(apk_file)) - if apk_add_version: - info('# Add version number to APK') - apk_name = basename(apk_file)[:-len(APK_SUFFIX)] - apk_file_dest = "{}-{}-{}".format( - apk_name, build_args.version, APK_SUFFIX) - info('# APK renamed to {}'.format(apk_file_dest)) - shprint(sh.cp, apk_file, apk_file_dest) + info_main('# Found android package file: {}'.format(package_file)) + if package_add_version: + info('# Add version number to android package') + package_name = basename(package_file)[:-len(APK_SUFFIX)] + package_file_dest = "{}-{}-{}".format( + package_name, build_args.version, APK_SUFFIX) + info('# Android package renamed to {}'.format(package_file_dest)) + shprint(sh.cp, package_file, package_file_dest) else: - shprint(sh.cp, apk_file, './') + shprint(sh.cp, package_file, './') + + @require_prebuilt_dist + def apk(self, args): + output, build_args = self._build_package(args, package_type='apk') + output_dir = join(self._dist.dist_dir, "build", "outputs", 'apk', args.build_mode) + self._finish_package(args, output, build_args, 'apk', output_dir) + + @require_prebuilt_dist + def aar(self, args): + output, build_args = self._build_package(args, package_type='aar') + output_dir = join(self._dist.dist_dir, "build", "outputs", 'aar') + self._finish_package(args, output, build_args, 'aar', output_dir) @require_prebuilt_dist def create(self, args): diff --git a/testapps/on_device_unit_tests/setup.py b/testapps/on_device_unit_tests/setup.py index 0df67e3f94..0f8574b9da 100644 --- a/testapps/on_device_unit_tests/setup.py +++ b/testapps/on_device_unit_tests/setup.py @@ -44,6 +44,7 @@ 'ndk-api': 21, 'dist-name': 'bdist_unit_tests_app', 'arch': 'armeabi-v7a', + 'bootstrap' : 'sdl2', 'permissions': ['INTERNET', 'VIBRATE'], 'orientation': 'sensor', 'service': 'P4a_test_service:app_service.py', From cea61f59d70a3090ff67b69ddd7768b94bb29810 Mon Sep 17 00:00:00 2001 From: Philipp Auersperg-Castell Date: Sun, 5 Apr 2020 14:52:49 +0200 Subject: [PATCH 2/3] repairted test on number of bootstraps --- tests/test_bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 583cefb81c..3d3b13dde4 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -147,7 +147,7 @@ def test_all_bootstraps(self): returns the expected values, which should be: `empty", `service_only`, `webview` and `sdl2` """ - expected_bootstraps = {"empty", "service_only", "webview", "sdl2"} + expected_bootstraps = {"empty", "service_only", "service_library", "webview", "sdl2"} set_of_bootstraps = Bootstrap.all_bootstraps() self.assertEqual( expected_bootstraps, expected_bootstraps & set_of_bootstraps From 15590dabec8c4505d1ec05963f7bd47879e9681d Mon Sep 17 00:00:00 2001 From: Philipp Auersperg-Castell Date: Wed, 15 Apr 2020 19:12:33 +0200 Subject: [PATCH 3/3] fixed rebase armageddon --- pythonforandroid/bdistapk.py | 26 +++++++++--- pythonforandroid/bootstrap.py | 41 +++++++++++-------- .../bootstraps/common/build/build.py | 3 +- .../java/org/kivy/android/PythonUtil.java | 3 +- .../common/build/templates/build.tmpl.gradle | 4 ++ pythonforandroid/build.py | 10 +++-- setup.py | 1 + .../setup_testapp_python3_sqlite_openssl.py | 1 + testapps/setup_vispy.py | 1 + tests/test_bootstrap.py | 19 +-------- 10 files changed, 64 insertions(+), 45 deletions(-) diff --git a/pythonforandroid/bdistapk.py b/pythonforandroid/bdistapk.py index d22c9947a4..d4b2c7953a 100644 --- a/pythonforandroid/bdistapk.py +++ b/pythonforandroid/bdistapk.py @@ -14,16 +14,16 @@ def argv_contains(t): return False -class BdistAPK(Command): - description = 'Create an APK with python-for-android' +class Bdist(Command): user_options = [] + package_type = None def initialize_options(self): for option in self.user_options: setattr(self, option[0].strip('=').replace('-', '_'), None) - option_dict = self.distribution.get_option_dict('apk') + option_dict = self.distribution.get_option_dict(self.package_type) # This is a hack, we probably aren't supposed to loop through # the option_dict so early because distutils does exactly the @@ -34,7 +34,7 @@ def initialize_options(self): def finalize_options(self): - setup_options = self.distribution.get_option_dict('apk') + setup_options = self.distribution.get_option_dict(self.package_type) for (option, (source, value)) in setup_options.items(): if source == 'command line': continue @@ -75,7 +75,7 @@ def run(self): self.prepare_build_dir() from pythonforandroid.entrypoints import main - sys.argv[1] = 'apk' + sys.argv[1] = self.package_type main() def prepare_build_dir(self): @@ -127,6 +127,22 @@ def prepare_build_dir(self): ) +class BdistAPK(Bdist): + """ + distutil command handler for 'apk' + """ + description = 'Create an APK with python-for-android' + package_type = 'apk' + + +class BdistAAR(Bdist): + """ + distutil command handler for 'aar' + """ + description = 'Create an AAR with python-for-android' + package_type = 'aar' + + def _set_user_options(): # This seems like a silly way to do things, but not sure if there's a # better way to pass arbitrary options onwards to p4a diff --git a/pythonforandroid/bootstrap.py b/pythonforandroid/bootstrap.py index 75b82e555f..798934295b 100755 --- a/pythonforandroid/bootstrap.py +++ b/pythonforandroid/bootstrap.py @@ -14,7 +14,7 @@ from pythonforandroid.recipe import Recipe -def copy_files(src_root, dest_root, override=True): +def copy_files(src_root, dest_root, override=True, symlink=False): for root, dirnames, filenames in walk(src_root): for filename in filenames: subdir = normpath(root.replace(src_root, "")) @@ -29,7 +29,10 @@ def copy_files(src_root, dest_root, override=True): if override and os.path.exists(dest_file): os.unlink(dest_file) if not os.path.exists(dest_file): - shutil.copy(src_file, dest_file) + if symlink: + os.symlink(src_file, dest_file) + else: + shutil.copy(src_file, dest_file) else: os.makedirs(dest_file) @@ -109,7 +112,7 @@ def check_recipe_choices(self): and optional dependencies are being used, and returns a list of these.''' recipes = [] - built_recipes = self.ctx.recipe_build_order + built_recipes = self.ctx.recipe_build_order or [] for recipe in self.recipe_depends: if isinstance(recipe, (tuple, list)): for alternative in recipe: @@ -137,21 +140,27 @@ def name(self): modname = self.__class__.__module__ return modname.split(".", 2)[-1] + def get_bootstrap_dirs(self): + """get all bootstrap directories, following the MRO path""" + + # get all bootstrap names along the __mro__, cutting off Bootstrap and object + classes = self.__class__.__mro__[:-2] + bootstrap_names = [cls.name for cls in classes] + ['common'] + bootstrap_dirs = [ + join(self.ctx.root_dir, 'bootstraps', bootstrap_name) + for bootstrap_name in reversed(bootstrap_names) + ] + return bootstrap_dirs + def prepare_build_dir(self): - '''Ensure that a build dir exists for the recipe. This same single - dir will be used for building all different archs.''' + """Ensure that a build dir exists for the recipe. This same single + dir will be used for building all different archs.""" + bootstrap_dirs = self.get_bootstrap_dirs() + # now do a cumulative copy of all bootstrap dirs self.build_dir = self.get_build_dir() - self.common_dir = self.get_common_dir() - copy_files(join(self.bootstrap_dir, 'build'), self.build_dir) - copy_files(join(self.common_dir, 'build'), self.build_dir, - override=False) - if self.ctx.symlink_java_src: - info('Symlinking java src instead of copying') - shprint(sh.rm, '-r', join(self.build_dir, 'src')) - shprint(sh.mkdir, join(self.build_dir, 'src')) - for dirn in listdir(join(self.bootstrap_dir, 'build', 'src')): - shprint(sh.ln, '-s', join(self.bootstrap_dir, 'build', 'src', dirn), - join(self.build_dir, 'src')) + for bootstrap_dir in bootstrap_dirs: + copy_files(join(bootstrap_dir, 'build'), self.build_dir, symlink=self.ctx.symlink_bootstrap_files) + with current_directory(self.build_dir): with open('project.properties', 'w') as fileh: fileh.write('target=android-{}'.format(self.ctx.android_api)) diff --git a/pythonforandroid/bootstraps/common/build/build.py b/pythonforandroid/bootstraps/common/build/build.py index 6b5c87b5c3..8eb1a6fe0e 100644 --- a/pythonforandroid/bootstraps/common/build/build.py +++ b/pythonforandroid/bootstraps/common/build/build.py @@ -225,7 +225,7 @@ def compile_dir(dfn, optimize_python=True): def make_package(args): # If no launcher is specified, require a main.py/main.pyo: if (get_bootstrap_name() != "sdl" or args.launcher is None) and \ - get_bootstrap_name() != "webview": + 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 @@ -479,6 +479,7 @@ def make_package(args): android_api=android_api, build_tools_version=build_tools_version, debug_build="debug" in args.build_mode, + is_library=(get_bootstrap_name() == 'service_library'), ) # ant build templates diff --git a/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonUtil.java b/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonUtil.java index 6a546a7eb1..f3cf0dbf4e 100644 --- a/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonUtil.java +++ b/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonUtil.java @@ -39,6 +39,7 @@ protected static ArrayList getLibraries(File libsDir) { libsList.add("python3.5m"); libsList.add("python3.6m"); libsList.add("python3.7m"); + libsList.add("python3.8m"); libsList.add("main"); return libsList; } @@ -59,7 +60,7 @@ public static void loadLibraries(File filesDir, File libsDir) { // load, and it has failed, give a more // general error Log.v(TAG, "Library loading error: " + e.getMessage()); - if (lib.startsWith("python3.7") && !foundPython) { + if (lib.startsWith("python3.8") && !foundPython) { throw new java.lang.RuntimeException("Could not load any libpythonXXX.so"); } else if (lib.startsWith("python")) { continue; diff --git a/pythonforandroid/bootstraps/common/build/templates/build.tmpl.gradle b/pythonforandroid/bootstraps/common/build/templates/build.tmpl.gradle index 63ba86258f..7b6ba8390b 100644 --- a/pythonforandroid/bootstraps/common/build/templates/build.tmpl.gradle +++ b/pythonforandroid/bootstraps/common/build/templates/build.tmpl.gradle @@ -22,7 +22,11 @@ allprojects { } } +{% if is_library %} +apply plugin: 'com.android.library' +{% else %} apply plugin: 'com.android.application' +{% endif %} android { compileSdkVersion {{ android_api }} diff --git a/pythonforandroid/build.py b/pythonforandroid/build.py index 3d4428573a..40776893f9 100644 --- a/pythonforandroid/build.py +++ b/pythonforandroid/build.py @@ -117,7 +117,7 @@ class Context: recipe_build_order = None # Will hold the list of all built recipes - symlink_java_src = False # If True, will symlink instead of copying during build + symlink_bootstrap_files = False # If True, will symlink instead of copying during build java_build_tool = 'auto' @@ -481,9 +481,11 @@ def set_archs(self, arch_names): info('Will compile for the following archs: {}'.format( ', '.join([arch.arch for arch in self.archs]))) - def prepare_bootstrap(self, bs): - bs.ctx = self - self.bootstrap = bs + def prepare_bootstrap(self, bootstrap): + if not bootstrap: + raise TypeError("None is not allowed for bootstrap") + bootstrap.ctx = self + self.bootstrap = bootstrap self.bootstrap.prepare_build_dir() self.bootstrap_build_dir = self.bootstrap.build_dir diff --git a/setup.py b/setup.py index cb95b08ccc..173d58ee8c 100644 --- a/setup.py +++ b/setup.py @@ -100,6 +100,7 @@ def recursively_include(results, directory, patterns): ], 'distutils.commands': [ 'apk = pythonforandroid.bdistapk:BdistAPK', + 'aar = pythonforandroid.bdistapk:BdistAAR', ], }, classifiers = [ diff --git a/testapps/setup_testapp_python3_sqlite_openssl.py b/testapps/setup_testapp_python3_sqlite_openssl.py index 0f7485d132..0dfd375bba 100644 --- a/testapps/setup_testapp_python3_sqlite_openssl.py +++ b/testapps/setup_testapp_python3_sqlite_openssl.py @@ -5,6 +5,7 @@ options = {'apk': {'requirements': 'requests,peewee,sdl2,pyjnius,kivy,python3', 'android-api': 27, 'ndk-api': 21, + 'bootstrap': 'sdl2', 'dist-name': 'bdisttest_python3_sqlite_openssl_googlendk', 'ndk-version': '10.3.2', 'arch': 'armeabi-v7a', diff --git a/testapps/setup_vispy.py b/testapps/setup_vispy.py index 49ad47fda3..cbc909627b 100644 --- a/testapps/setup_vispy.py +++ b/testapps/setup_vispy.py @@ -7,6 +7,7 @@ 'blacklist-requirements': 'openssl,sqlite3', 'android-api': 27, 'ndk-api': 21, + 'bootstrap': 'empty', 'ndk-dir': '/home/asandy/android/android-ndk-r17c', 'dist-name': 'bdisttest', 'ndk-version': '10.3.2', diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 3d3b13dde4..6ba39864c0 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -288,16 +288,10 @@ def test_bootstrap_prepare_build_dir( @mock.patch("pythonforandroid.bootstrap.os.unlink") @mock.patch("pythonforandroid.bootstrap.open", create=True) @mock.patch("pythonforandroid.util.chdir") - @mock.patch("pythonforandroid.bootstrap.sh.ln") @mock.patch("pythonforandroid.bootstrap.listdir") - @mock.patch("pythonforandroid.bootstrap.sh.mkdir") - @mock.patch("pythonforandroid.bootstrap.sh.rm") def test_bootstrap_prepare_build_dir_with_java_src( self, - mock_sh_rm, - mock_sh_mkdir, mock_listdir, - mock_sh_ln, mock_chdir, mock_open, mock_os_unlink, @@ -309,7 +303,7 @@ def test_bootstrap_prepare_build_dir_with_java_src( :meth:`~pythonforandroid.bootstrap.Bootstrap.prepare_build_dir`. In here we will simulate that we have `with_java_src` set to some value. """ - self.ctx.symlink_java_src = ["some_java_src"] + self.ctx.symlink_bootstrap_files = True mock_listdir.return_value = [ "jnius", "kivy", @@ -327,18 +321,7 @@ def test_bootstrap_prepare_build_dir_with_java_src( # make sure that the open command has been called only once mock_open.assert_called_with("project.properties", "w") - # check that the symlink was made 4 times and that - self.assertEqual( - len(mock_sh_ln.call_args_list), len(mock_listdir.return_value) - ) - for i, directory in enumerate(mock_listdir.return_value): - self.assertTrue( - mock_sh_ln.call_args_list[i][0][1].endswith(directory) - ) - # check that the other mocks we made are actually called - mock_sh_rm.assert_called() - mock_sh_mkdir.assert_called() mock_chdir.assert_called() mock_os_unlink.assert_called() mock_os_path_exists.assert_called()