diff --git a/README.md b/README.md index 7dd3130d6d..85ba71aed4 100644 --- a/README.md +++ b/README.md @@ -70,18 +70,13 @@ linked above. Relating to all bootstraps: - Some recipes/components aren't stripped properly of doc etc. -- Some command line options of distribute.sh -- Biglink is essential (the p4a disable option isn't implemented) +- Downloaded file md5 and headers aren't checked Relating to SDL2 only: -- Downloaded file md5 and headers aren't checked -- Android services are not implemented at all -- App loading screen - Public dir installation - Keyboard height getter - Billing support - Kivy Launcher build (can now be implemented as a bootstrap...maybe?) -- Several build options for build.py - Probably some other stuff Here are some specific things relating to changes in p4a itself since @@ -89,7 +84,7 @@ the reference commit that the revamp is based on: # Current status -The tool works, testing is welcomed. +python-for-android is fully ready for use. We are working towards a versioned release. # Development notes diff --git a/pythonforandroid/bootstrap.py b/pythonforandroid/bootstrap.py index 59e7efe536..5f831e8583 100644 --- a/pythonforandroid/bootstrap.py +++ b/pythonforandroid/bootstrap.py @@ -135,22 +135,24 @@ def get_bootstrap_from_recipes(cls, recipes, ctx): for name in cls.list_bootstraps()] acceptable_bootstraps = [] for bs in bootstraps: - ok = True if not bs.can_be_chosen_automatically: - ok = False - for recipe in bs.recipe_depends: - recipe = Recipe.get_recipe(recipe, ctx) - if any([conflict in recipes for conflict in recipe.conflicts]): - ok = False - break - for recipe in recipes: - recipe = Recipe.get_recipe(recipe, ctx) - if any([conflict in bs.recipe_depends - for conflict in recipe.conflicts]): - ok = False - break - if ok: - acceptable_bootstraps.append(bs) + continue + possible_dependency_lists = expand_dependencies(bs.recipe_depends) + for possible_dependencies in possible_dependency_lists: + ok = True + for recipe in possible_dependencies: + recipe = Recipe.get_recipe(recipe, ctx) + if any([conflict in recipes for conflict in recipe.conflicts]): + ok = False + break + for recipe in recipes: + recipe = Recipe.get_recipe(recipe, ctx) + if any([conflict in possible_dependencies + for conflict in recipe.conflicts]): + ok = False + break + if ok: + acceptable_bootstraps.append(bs) info('Found {} acceptable bootstraps: {}'.format( len(acceptable_bootstraps), [bs.name for bs in acceptable_bootstraps])) @@ -264,3 +266,20 @@ def fry_eggs(self, sitepackages): if files: shprint(sh.mv, '-t', sitepackages, *files) shprint(sh.rm, '-rf', d) + + +def expand_dependencies(recipes): + recipe_lists = [[]] + for recipe in recipes: + if isinstance(recipe, (tuple, list)): + new_recipe_lists = [] + for alternative in recipe: + for old_list in recipe_lists: + new_list = [i for i in old_list] + new_list.append(alternative) + new_recipe_lists.append(new_list) + recipe_lists = new_recipe_lists + else: + for old_list in recipe_lists: + old_list.append(recipe) + return recipe_lists diff --git a/pythonforandroid/bootstraps/sdl2/build/build.py b/pythonforandroid/bootstraps/sdl2/build/build.py index 89ff955ce1..aa818ed61b 100755 --- a/pythonforandroid/bootstraps/sdl2/build/build.py +++ b/pythonforandroid/bootstraps/sdl2/build/build.py @@ -311,9 +311,8 @@ def make_package(args): entrypoint = spec[1] options = spec[2:] - foreground = False - if 'foreground' in options: - foreground = True + foreground = 'foreground' in options + sticky = 'sticky' in options service_names.append(name) render( @@ -323,6 +322,7 @@ def make_package(args): entrypoint=entrypoint, args=args, foreground=foreground, + sticky=sticky, service_id=sid + 1, ) diff --git a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonService.java b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonService.java index c460ae424c..f8dde3e0d2 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonService.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonService.java @@ -29,12 +29,23 @@ public class PythonService extends Service implements Runnable { private String serviceEntrypoint; // Argument to pass to Python code, private String pythonServiceArgument; - public static Service mService = null; + public static PythonService mService = null; + private Intent startIntent = null; + + private boolean autoRestartService = false; + + public void setAutoRestartService(boolean restart) { + autoRestartService = restart; + } public boolean canDisplayNotification() { return true; } + public int startType() { + return START_NOT_STICKY; + } + @Override public IBinder onBind(Intent arg0) { return null; @@ -52,6 +63,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { return START_NOT_STICKY; } + startIntent = intent; Bundle extras = intent.getExtras(); androidPrivate = extras.getString("androidPrivate"); androidArgument = extras.getString("androidArgument"); @@ -64,31 +76,35 @@ public int onStartCommand(Intent intent, int flags, int startId) { pythonThread = new Thread(this); pythonThread.start(); - doStartForeground(extras); + if (canDisplayNotification()) { + doStartForeground(extras); + } - return START_NOT_STICKY; + return startType(); } protected void doStartForeground(Bundle extras) { - if (canDisplayNotification()) { - String serviceTitle = extras.getString("serviceTitle"); - String serviceDescription = extras.getString("serviceDescription"); - - Context context = getApplicationContext(); - Notification notification = new Notification(context.getApplicationInfo().icon, - serviceTitle, System.currentTimeMillis()); - Intent contextIntent = new Intent(context, PythonActivity.class); - PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - notification.setLatestEventInfo(context, serviceTitle, serviceDescription, pIntent); - startForeground(1, notification); - } + String serviceTitle = extras.getString("serviceTitle"); + String serviceDescription = extras.getString("serviceDescription"); + + Context context = getApplicationContext(); + Notification notification = new Notification(context.getApplicationInfo().icon, + serviceTitle, System.currentTimeMillis()); + Intent contextIntent = new Intent(context, PythonActivity.class); + PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + notification.setLatestEventInfo(context, serviceTitle, serviceDescription, pIntent); + startForeground(1, notification); } @Override public void onDestroy() { super.onDestroy(); pythonThread = null; + if (autoRestartService && startIntent != null) { + Log.v("python service", "service restart requested"); + startService(startIntent); + } Process.killProcess(Process.myPid()); } @@ -101,6 +117,7 @@ public void run(){ serviceEntrypoint, pythonName, pythonHome, pythonPath, pythonServiceArgument); + stopSelf(); } // Native part diff --git a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java index 4b1f390d4f..a488a1b878 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java @@ -14,6 +14,8 @@ protected static String[] getLibraries() { "SDL2_image", "SDL2_mixer", "SDL2_ttf", + "python2.7", + "python3.5m", "main" }; } @@ -21,23 +23,20 @@ protected static String[] getLibraries() { public static void loadLibraries(File filesDir) { String filesDirPath = filesDir.getAbsolutePath(); + boolean skippedPython = false; for (String lib : getLibraries()) { - System.loadLibrary(lib); + try { + System.loadLibrary(lib); + } catch(UnsatisfiedLinkError e) { + if (lib.startsWith("python") && !skippedPython) { + skippedPython = true; + continue; + } + throw e; + } } - try { - System.loadLibrary("python2.7"); - } catch(UnsatisfiedLinkError e) { - Log.v(TAG, "Failed to load libpython2.7"); - } - - try { - System.loadLibrary("python3.5m"); - } catch(UnsatisfiedLinkError e) { - Log.v(TAG, "Failed to load libpython3.5m"); - } - try { System.load(filesDirPath + "/lib/python2.7/lib-dynload/_io.so"); System.load(filesDirPath + "/lib/python2.7/lib-dynload/unicodedata.so"); diff --git a/pythonforandroid/bootstraps/sdl2/build/templates/Service.tmpl.java b/pythonforandroid/bootstraps/sdl2/build/templates/Service.tmpl.java index 12e770b572..bf87996212 100644 --- a/pythonforandroid/bootstraps/sdl2/build/templates/Service.tmpl.java +++ b/pythonforandroid/bootstraps/sdl2/build/templates/Service.tmpl.java @@ -10,6 +10,20 @@ public class Service{{ name|capitalize }} extends PythonService { + {% if sticky %} + @Override + public int startType() { + return START_STICKY; + } + {% endif %} + + {% if not foreground %} + @Override + public boolean canDisplayNotification() { + return false; + } + {% endif %} + @Override protected void doStartForeground(Bundle extras) { Context context = getApplicationContext(); diff --git a/pythonforandroid/build.py b/pythonforandroid/build.py index ae54a29266..3556e472ef 100644 --- a/pythonforandroid/build.py +++ b/pythonforandroid/build.py @@ -212,6 +212,13 @@ def prepare_build_environment(self, user_sdk_dir, user_ndk_dir, android_api = int(android_api) self.android_api = android_api + if self.android_api >= 21 and self.archs[0].arch == 'armeabi': + error('Asked to build for armeabi architecture with API ' + '{}, but API 21 or greater does not support armeabi'.format( + self.android_api)) + error('You probably want to build with --arch=armeabi-v7a instead') + exit(1) + android = sh.Command(join(sdk_dir, 'tools', 'android')) targets = android('list').stdout.decode('utf-8').split('\n') apis = [s for s in targets if re.match(r'^ *API level: ', s)] @@ -330,7 +337,9 @@ def prepare_build_environment(self, user_sdk_dir, user_ndk_dir, if cython: self.cython = cython break - self.cython = 'cython' + else: + error('No cython binary found. Exiting.') + exit(1) if not self.cython: ok = False warning("Missing requirement: cython is not installed") diff --git a/pythonforandroid/graph.py b/pythonforandroid/graph.py index 33f383fb0c..fc491ca277 100644 --- a/pythonforandroid/graph.py +++ b/pythonforandroid/graph.py @@ -60,10 +60,11 @@ def _add(self, graph, dependent, dependency): def conflicts(self, conflict): graphs = self.graphs + initial_num = len(graphs) for i in range(len(graphs)): - graph = graphs[len(graphs) - 1 - i] + graph = graphs[initial_num - 1 - i] if conflict in graph: - graphs.pop(len(graphs) - 1 - i) + graphs.pop(initial_num - 1 - i) return len(graphs) == 0 def remove_remaining_conflicts(self, ctx): @@ -227,6 +228,8 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None): recipe_loaded.append(name) graph.remove_remaining_conflicts(ctx) build_order = list(graph.find_order(0)) + build_order, python_modules, bs = get_recipe_order_and_bootstrap( + ctx, build_order + python_modules, bs) return build_order, python_modules, bs # Do a final check that the new bs doesn't pull in any conflicts diff --git a/pythonforandroid/logger.py b/pythonforandroid/logger.py index a1a43a0829..009b32e86f 100644 --- a/pythonforandroid/logger.py +++ b/pythonforandroid/logger.py @@ -15,6 +15,11 @@ stdout = codecs.getwriter('utf8')(stdout) stderr = codecs.getwriter('utf8')(stderr) +if six.PY2: + unistr = unicode +else: + unistr = str + # monkey patch to show full output sh.ErrorReturnCode.truncate_cap = 999999 @@ -216,6 +221,3 @@ def printtail(out, name, forecolor, tail_n=0, raise return output - - -from pythonforandroid.util import unistr diff --git a/pythonforandroid/recipe.py b/pythonforandroid/recipe.py index 7a20371aab..be95a237e6 100644 --- a/pythonforandroid/recipe.py +++ b/pythonforandroid/recipe.py @@ -580,9 +580,12 @@ def has_libs(self, arch, *libs): @classmethod def recipe_dirs(cls, ctx): - return [ctx.local_recipes, - join(ctx.storage_dir, 'recipes'), - join(ctx.root_dir, "recipes")] + recipe_dirs = [] + if ctx.local_recipes is not None: + recipe_dirs.append(ctx.local_recipes) + recipe_dirs.extend([join(ctx.storage_dir, 'recipes'), + join(ctx.root_dir, "recipes")]) + return recipe_dirs @classmethod def list_recipes(cls, ctx): diff --git a/pythonforandroid/recipes/android/src/android/_android.pyx b/pythonforandroid/recipes/android/src/android/_android.pyx index cd661e5047..f53a55ba60 100644 --- a/pythonforandroid/recipes/android/src/android/_android.pyx +++ b/pythonforandroid/recipes/android/src/android/_android.pyx @@ -168,7 +168,7 @@ cdef extern void android_show_keyboard(int) cdef extern void android_hide_keyboard() -from jnius import autoclass, PythonJavaClass, java_method +from jnius import autoclass, PythonJavaClass, java_method, cast # API versions api_version = autoclass('android.os.Build$VERSION').SDK_INT @@ -318,23 +318,28 @@ IF IS_PYGAME: def ack_stop(): android_ackstop() - # ------------------------------------------------------------------- - # URL Opening. - cdef extern void android_open_url(char *url) - def open_url(url): - android_open_url(url) +# ------------------------------------------------------------------- +# URL Opening. +def open_url(url): + Intent = autoclass('android.content.Intent') + Uri = autoclass('android.net.Uri') + browserIntent = Intent() + browserIntent.setAction(Intent.ACTION_VIEW) + browserIntent.setData(Uri.parse(url)) + currentActivity = cast('android.app.Activity', mActivity) + currentActivity.startActivity(browserIntent) + +# Web browser support. +class AndroidBrowser(object): + def open(self, url, new=0, autoraise=True): + open_url(url) + def open_new(self, url): + open_url(url) + def open_new_tab(self, url): + open_url(url) - # Web browser support. - class AndroidBrowser(object): - def open(self, url, new=0, autoraise=True): - open_url(url) - def open_new(self, url): - open_url(url) - def open_new_tab(self, url): - open_url(url) - - import webbrowser - webbrowser.register('android', AndroidBrowser, None, -1) +import webbrowser +webbrowser.register('android', AndroidBrowser, None, -1) cdef extern void android_start_service(char *, char *, char *) def start_service(title=None, description=None, arg=None): @@ -383,3 +388,5 @@ class AndroidService(object): '''Stop the service. ''' stop_service() + + diff --git a/pythonforandroid/recipes/boost/__init__.py b/pythonforandroid/recipes/boost/__init__.py new file mode 100644 index 0000000000..bbd10bffb3 --- /dev/null +++ b/pythonforandroid/recipes/boost/__init__.py @@ -0,0 +1,65 @@ +from pythonforandroid.toolchain import Recipe, shprint, shutil, current_directory +from os.path import join, exists +import sh + +# This recipe creates a custom toolchain and bootstraps Boost from source to build Boost.Build +# including python bindings +class BoostRecipe(Recipe): + version = '1.60.0' + # Don't forget to change the URL when changing the version + url = 'http://downloads.sourceforge.net/project/boost/boost/{version}/boost_1_60_0.tar.bz2' + depends = ['python2'] + patches = ['disable-so-version.patch', 'use-android-libs.patch'] + + def should_build(self, arch): + return not exists(join(self.get_build_dir(arch.arch), 'b2')) + + def prebuild_arch(self, arch): + super(BoostRecipe, self).prebuild_arch(arch) + env = self.get_recipe_env(arch) + with current_directory(self.get_build_dir(arch.arch)): + # Make custom toolchain + bash = sh.Command('bash') + shprint(bash, join(self.ctx.ndk_dir, 'build/tools/make-standalone-toolchain.sh'), + '--ndk-dir=' + self.ctx.ndk_dir, + '--arch=' + env['ARCH'], + '--platform=android-' + str(self.ctx.android_api), + '--toolchain=' + env['CROSSHOST'] + '-' + env['TOOLCHAIN_VERSION'], + '--install-dir=' + env['CROSSHOME'], + '--system=' + 'linux-x86_64' + ) + # Set custom configuration + shutil.copyfile(join(self.get_recipe_dir(), 'user-config.jam'), + join(env['BOOST_BUILD_PATH'], 'user-config.jam')) + + def build_arch(self, arch): + super(BoostRecipe, self).build_arch(arch) + env = self.get_recipe_env(arch) + with current_directory(self.get_build_dir(arch.arch)): + # Compile Boost.Build engine with this custom toolchain + bash = sh.Command('bash') + shprint(bash, 'bootstrap.sh', + '--with-python=' + join(env['PYTHON_ROOT'], 'bin/python.host'), + '--with-python-version=2.7', + '--with-python-root=' + env['PYTHON_ROOT'] + ) # Do not pass env + # Install app stl + shutil.copyfile(join(env['CROSSHOME'], env['CROSSHOST'], 'lib/libgnustl_shared.so'), + join(self.ctx.get_libs_dir(arch.arch), 'libgnustl_shared.so')) + + def select_build_arch(self, arch): + return arch.arch.replace('eabi', '') + + def get_recipe_env(self, arch): + env = super(BoostRecipe, self).get_recipe_env(arch) + env['BOOST_BUILD_PATH'] = self.get_build_dir(arch.arch) # find user-config.jam + env['BOOST_ROOT'] = env['BOOST_BUILD_PATH'] # find boost source + env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() + env['ARCH'] = self.select_build_arch(arch) + env['ANDROIDAPI'] = str(self.ctx.android_api) + env['CROSSHOST'] = env['ARCH'] + '-linux-androideabi' + env['CROSSHOME'] = join(env['BOOST_ROOT'], 'standalone-' + env['ARCH'] + '-toolchain') + env['TOOLCHAIN_PREFIX'] = join(env['CROSSHOME'], 'bin', env['CROSSHOST']) + return env + +recipe = BoostRecipe() diff --git a/pythonforandroid/recipes/boost/disable-so-version.patch b/pythonforandroid/recipes/boost/disable-so-version.patch new file mode 100644 index 0000000000..6911f8900f --- /dev/null +++ b/pythonforandroid/recipes/boost/disable-so-version.patch @@ -0,0 +1,12 @@ +--- boost/boostcpp.jam 2015-12-14 03:30:09.000000000 +0100 ++++ boost-patch/boostcpp.jam 2016-02-08 16:38:40.510859612 +0100 +@@ -155,8 +155,9 @@ + if $(type) = SHARED_LIB && + ! [ $(property-set).get ] in windows cygwin darwin aix && + ! [ $(property-set).get ] in pgi + { ++ return $(result) ; # disable version suffix for android + result = $(result).$(BOOST_VERSION) ; + } + + return $(result) ; diff --git a/pythonforandroid/recipes/boost/use-android-libs.patch b/pythonforandroid/recipes/boost/use-android-libs.patch new file mode 100644 index 0000000000..650722da86 --- /dev/null +++ b/pythonforandroid/recipes/boost/use-android-libs.patch @@ -0,0 +1,10 @@ +--- boost/tools/build/src/tools/python.jam 2015-10-16 20:55:36.000000000 +0200 ++++ boost-patch/tools/build/src/tools/python.jam 2016-02-09 13:16:09.519261546 +0100 +@@ -646,6 +646,7 @@ + + case aix : return pthread dl ; + ++ case * : return ; # use Android builtin libs + case * : return pthread dl + gcc:util linux:util ; + } diff --git a/pythonforandroid/recipes/boost/user-config.jam b/pythonforandroid/recipes/boost/user-config.jam new file mode 100644 index 0000000000..72643d8a06 --- /dev/null +++ b/pythonforandroid/recipes/boost/user-config.jam @@ -0,0 +1,28 @@ +import os ; + +local ANDROIDNDK = [ os.environ ANDROIDNDK ] ; +local ANDROIDAPI = [ os.environ ANDROIDAPI ] ; +local TOOLCHAIN_VERSION = [ os.environ TOOLCHAIN_VERSION ] ; +local TOOLCHAIN_PREFIX = [ os.environ TOOLCHAIN_PREFIX ] ; +local ARCH = [ os.environ ARCH ] ; +local PYTHON_ROOT = [ os.environ PYTHON_ROOT ] ; + +using gcc : $(ARCH) : $(TOOLCHAIN_PREFIX)-g++ : +$(ARCH) +$(TOOLCHAIN_PREFIX)-ar +-DBOOST_SP_USE_PTHREADS +-DBOOST_AC_USE_PTHREADS +-DBOOST_SP_USE_PTHREADS +-DBOOST_AC_USE_PTHREADS +-frtti +-fexceptions +-I$(ANDROIDNDK)/platforms/android-$(ANDROIDAPI)/arch-$(ARCH)/usr/include +-I$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/include +-I$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/libs/$(ARCH)/include +-I$(PYTHON_ROOT)/include/python2.7 +--sysroot=$(ANDROIDNDK)/platforms/android-$(ANDROIDAPI)/arch-$(ARCH) +-L$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/libs/$(ARCH) +-L$(PYTHON_ROOT)/lib +-lgnustl_shared +-lpython2.7 +; diff --git a/pythonforandroid/recipes/leveldb/__init__.py b/pythonforandroid/recipes/leveldb/__init__.py new file mode 100644 index 0000000000..92dc4fdced --- /dev/null +++ b/pythonforandroid/recipes/leveldb/__init__.py @@ -0,0 +1,45 @@ +from pythonforandroid.toolchain import Recipe, shprint, shutil, current_directory +from os.path import join, exists +import sh + +class LevelDBRecipe(Recipe): + version = '1.18' + url = 'https://github.com/google/leveldb/archive/v{version}.tar.gz' + opt_depends = ['snappy'] + patches = ['disable-so-version.patch', 'find-snappy.patch'] + + def should_build(self, arch): + return not self.has_libs(arch, 'libleveldb.so', 'libgnustl_shared.so') + + def build_arch(self, arch): + super(LevelDBRecipe, self).build_arch(arch) + env = self.get_recipe_env(arch) + with current_directory(self.get_build_dir(arch.arch)): + if 'snappy' in recipe.ctx.recipe_build_order: + # Copy source from snappy recipe + sh.cp('-rf', self.get_recipe('snappy', self.ctx).get_build_dir(arch.arch), 'snappy') + # Build + shprint(sh.make, _env=env) + # Copy the shared library + shutil.copyfile('libleveldb.so', join(self.ctx.get_libs_dir(arch.arch), 'libleveldb.so')) + # Copy stl + shutil.copyfile(self.ctx.ndk_dir + '/sources/cxx-stl/gnu-libstdc++/' + self.ctx.toolchain_version + '/libs/' + arch.arch + '/libgnustl_shared.so', + join(self.ctx.get_libs_dir(arch.arch), 'libgnustl_shared.so')) + + def get_recipe_env(self, arch): + env = super(LevelDBRecipe, self).get_recipe_env(arch) + env['TARGET_OS'] = 'OS_ANDROID_CROSSCOMPILE' + if 'snappy' in recipe.ctx.recipe_build_order: + env['CFLAGS'] += ' -DSNAPPY' + \ + ' -I./snappy' + env['CFLAGS'] += ' -I' + self.ctx.ndk_dir + '/platforms/android-' + str(self.ctx.android_api) + '/arch-' + arch.arch.replace('eabi', '') + '/usr/include' + \ + ' -I' + self.ctx.ndk_dir + '/sources/cxx-stl/gnu-libstdc++/' + self.ctx.toolchain_version + '/include' + \ + ' -I' + self.ctx.ndk_dir + '/sources/cxx-stl/gnu-libstdc++/' + self.ctx.toolchain_version + '/libs/' + arch.arch + '/include' + env['CXXFLAGS'] = env['CFLAGS'] + env['CXXFLAGS'] += ' -frtti' + env['CXXFLAGS'] += ' -fexceptions' + env['LDFLAGS'] += ' -L' + self.ctx.ndk_dir + '/sources/cxx-stl/gnu-libstdc++/' + self.ctx.toolchain_version + '/libs/' + arch.arch + \ + ' -lgnustl_shared' + return env + +recipe = LevelDBRecipe() diff --git a/pythonforandroid/recipes/leveldb/disable-so-version.patch b/pythonforandroid/recipes/leveldb/disable-so-version.patch new file mode 100644 index 0000000000..0f6a7e7280 --- /dev/null +++ b/pythonforandroid/recipes/leveldb/disable-so-version.patch @@ -0,0 +1,10 @@ +--- leveldb/build_detect_platform 2014-09-16 23:19:52.000000000 +0200 ++++ leveldb-patch/build_detect_platform 2016-03-01 20:25:04.074484399 +0100 +@@ -124,6 +124,7 @@ + ;; + OS_ANDROID_CROSSCOMPILE) + PLATFORM=OS_ANDROID ++ PLATFORM_SHARED_VERSIONED= + COMMON_FLAGS="$MEMCMP_FLAG -D_REENTRANT -DOS_ANDROID -DLEVELDB_PLATFORM_POSIX" + PLATFORM_LDFLAGS="" # All pthread features are in the Android C library + PORT_FILE=port/port_posix.cc diff --git a/pythonforandroid/recipes/leveldb/find-snappy.patch b/pythonforandroid/recipes/leveldb/find-snappy.patch new file mode 100644 index 0000000000..ae978d7b07 --- /dev/null +++ b/pythonforandroid/recipes/leveldb/find-snappy.patch @@ -0,0 +1,11 @@ +--- leveldb/build_detect_platform 2014-09-16 23:19:52.000000000 +0200 ++++ leveldb-patch/build_detect_platform 2016-03-01 21:56:04.926650079 +0100 +@@ -156,7 +157,7 @@ + # except for the test and benchmark files. By default, find will output a list + # of all files matching either rule, so we need to append -print to make the + # prune take effect. +-DIRS="$PREFIX/db $PREFIX/util $PREFIX/table" ++DIRS="$PREFIX/snappy $PREFIX/db $PREFIX/util $PREFIX/table" + + set -f # temporarily disable globbing so that our patterns aren't expanded + PRUNE_TEST="-name *test*.cc -prune" diff --git a/pythonforandroid/recipes/libtorrent/__init__.py b/pythonforandroid/recipes/libtorrent/__init__.py new file mode 100644 index 0000000000..1901d0a3df --- /dev/null +++ b/pythonforandroid/recipes/libtorrent/__init__.py @@ -0,0 +1,71 @@ +from pythonforandroid.toolchain import Recipe, shprint, shutil, current_directory +from os.path import join, exists +import sh + +# This recipe builds libtorrent with Python bindings +# It depends on Boost.Build and the source of several Boost libraries present in BOOST_ROOT, +# which is all provided by the boost recipe +class LibtorrentRecipe(Recipe): + version = '1.0.8' + # Don't forget to change the URL when changing the version + url = 'http://github.com/arvidn/libtorrent/archive/libtorrent-1_0_8.tar.gz' + depends = ['boost', 'python2'] + opt_depends = ['openssl'] + patches = ['disable-so-version.patch', 'use-soname-python.patch'] + + def should_build(self, arch): + return not ( self.has_libs(arch, 'libboost_python.so', 'libboost_system.so', 'libtorrent.so') + and self.ctx.has_package('libtorrent', arch.arch) ) + + def prebuild_arch(self, arch): + super(LibtorrentRecipe, self).prebuild_arch(arch) + if 'openssl' in recipe.ctx.recipe_build_order: + # Patch boost user-config.jam to use openssl + self.get_recipe('boost', self.ctx).apply_patch(join(self.get_recipe_dir(), 'user-config-openssl.patch'), arch.arch) + + def build_arch(self, arch): + super(LibtorrentRecipe, self).build_arch(arch) + env = self.get_recipe_env(arch) + with current_directory(join(self.get_build_dir(arch.arch), 'bindings/python')): + # Compile libtorrent with boost libraries and python bindings + b2 = sh.Command(join(env['BOOST_ROOT'], 'b2')) + shprint(b2, + '-q', + '-j5', + 'toolset=gcc-' + env['ARCH'], + 'target-os=android', + 'threading=multi', + 'link=shared', + 'boost-link=shared', + 'boost=source', + 'encryption=openssl' if 'openssl' in recipe.ctx.recipe_build_order else '', + '--prefix=' + env['CROSSHOME'], + 'release' + , _env=env) + # Common build directories + build_subdirs = 'gcc-arm/release/boost-link-shared/boost-source' + if 'openssl' in recipe.ctx.recipe_build_order: + build_subdirs += '/encryption-openssl' + build_subdirs += '/libtorrent-python-pic-on/target-os-android/threading-multi/visibility-hidden' + # Copy the shared libraries into the libs folder + shutil.copyfile(join(env['BOOST_BUILD_PATH'], 'bin.v2/libs/python/build', build_subdirs, 'libboost_python.so'), + join(self.ctx.get_libs_dir(arch.arch), 'libboost_python.so')) + shutil.copyfile(join(env['BOOST_BUILD_PATH'], 'bin.v2/libs/system/build', build_subdirs, 'libboost_system.so'), + join(self.ctx.get_libs_dir(arch.arch), 'libboost_system.so')) + if 'openssl' in recipe.ctx.recipe_build_order: + shutil.copyfile(join(env['BOOST_BUILD_PATH'], 'bin.v2/libs/date_time/build', build_subdirs, 'libboost_date_time.so'), + join(self.ctx.get_libs_dir(arch.arch), 'libboost_date_time.so')) + shutil.copyfile(join(self.get_build_dir(arch.arch), 'bin', build_subdirs, 'libtorrent.so'), + join(self.ctx.get_libs_dir(arch.arch), 'libtorrent.so')) + shutil.copyfile(join(self.get_build_dir(arch.arch), 'bindings/python/bin', build_subdirs, 'libtorrent.so'), + join(self.ctx.get_site_packages_dir(arch.arch), 'libtorrent.so')) + + def get_recipe_env(self, arch): + env = super(LibtorrentRecipe, self).get_recipe_env(arch) + # Copy environment from boost recipe + env.update(self.get_recipe('boost', self.ctx).get_recipe_env(arch)) + if 'openssl' in recipe.ctx.recipe_build_order: + env['OPENSSL_BUILD_PATH'] = self.get_recipe('openssl', self.ctx).get_build_dir(arch.arch) + return env + +recipe = LibtorrentRecipe() diff --git a/pythonforandroid/recipes/libtorrent/disable-so-version.patch b/pythonforandroid/recipes/libtorrent/disable-so-version.patch new file mode 100644 index 0000000000..24cb3e5763 --- /dev/null +++ b/pythonforandroid/recipes/libtorrent/disable-so-version.patch @@ -0,0 +1,10 @@ +--- libtorrent/Jamfile 2016-01-17 23:52:45.000000000 +0100 ++++ libtorrent-patch/Jamfile 2016-02-09 13:37:57.499561750 +0100 +@@ -325,6 +325,7 @@ + if $(type) = SHARED_LIB && + ( ! ( [ $(property-set).get ] in windows cygwin ) ) + { ++ return $(name) ; # disable version suffix for android + name = $(name).$(VERSION) ; + } + diff --git a/pythonforandroid/recipes/libtorrent/use-soname-python.patch b/pythonforandroid/recipes/libtorrent/use-soname-python.patch new file mode 100644 index 0000000000..d1e63918e6 --- /dev/null +++ b/pythonforandroid/recipes/libtorrent/use-soname-python.patch @@ -0,0 +1,11 @@ +--- libtorrent/bindings/python/Jamfile 2016-01-17 23:52:45.000000000 +0100 ++++ libtorrent-patch/bindings/python/Jamfile 2016-02-09 17:11:44.261578000 +0100 +@@ -35,7 +35,7 @@ + + if ( gcc in $(properties) ) + { +- result += -Wl,-Bsymbolic ; ++ result += -Wl,-soname=libtorrentpython.so,-Bsymbolic ; + } + } + diff --git a/pythonforandroid/recipes/libtorrent/user-config-openssl.patch b/pythonforandroid/recipes/libtorrent/user-config-openssl.patch new file mode 100644 index 0000000000..43d28db07b --- /dev/null +++ b/pythonforandroid/recipes/libtorrent/user-config-openssl.patch @@ -0,0 +1,25 @@ +--- boost/user-config.jam 2016-03-02 14:31:41.280414820 +0100 ++++ boost-patch/user-config.jam 2016-03-02 14:32:08.904384741 +0100 +@@ -6,6 +6,7 @@ + local TOOLCHAIN_PREFIX = [ os.environ TOOLCHAIN_PREFIX ] ; + local ARCH = [ os.environ ARCH ] ; + local PYTHON_ROOT = [ os.environ PYTHON_ROOT ] ; ++local OPENSSL_BUILD_PATH = [ os.environ OPENSSL_BUILD_PATH ] ; + + using gcc : $(ARCH) : $(TOOLCHAIN_PREFIX)-g++ : + $(ARCH) +@@ -20,9 +21,14 @@ + -I$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/include + -I$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/libs/$(ARCH)/include + -I$(PYTHON_ROOT)/include/python2.7 ++-I$(OPENSSL_BUILD_PATH)/include ++-I$(OPENSSL_BUILD_PATH)/include/openssl + --sysroot=$(ANDROIDNDK)/platforms/android-$(ANDROIDAPI)/arch-$(ARCH) + -L$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/libs/$(ARCH) + -L$(PYTHON_ROOT)/lib ++-L$(OPENSSL_BUILD_PATH) + -lgnustl_shared + -lpython2.7 ++-lcrypto ++-lssl + ; diff --git a/pythonforandroid/recipes/m2crypto/__init__.py b/pythonforandroid/recipes/m2crypto/__init__.py new file mode 100644 index 0000000000..fae2ca1464 --- /dev/null +++ b/pythonforandroid/recipes/m2crypto/__init__.py @@ -0,0 +1,40 @@ +from pythonforandroid.toolchain import PythonRecipe, shprint, shutil, current_directory +from os.path import join, exists +import sh + +class M2CryptoRecipe(PythonRecipe): + version = '0.23.0' + url = 'https://pypi.python.org/packages/source/M/M2Crypto/M2Crypto-{version}.tar.gz' + #md5sum = '89557730e245294a6cab06de8ad4fb42' + depends = ['openssl', 'hostpython2', 'python2', 'setuptools'] + site_packages_name = 'M2Crypto' + call_hostpython_via_targetpython = False + + def build_arch(self, arch): + env = self.get_recipe_env(arch) + with current_directory(self.get_build_dir(arch.arch)): + # Fix missing build dir + shprint(sh.mkdir, '-p', 'build/lib.' + 'linux-x86_64' + '-2.7/M2Crypto') + # Build M2Crypto + hostpython = sh.Command(self.hostpython_location) + shprint(hostpython, + 'setup.py', + 'build_ext', + '-p' + arch.arch, + '-c' + 'unix', + '-o' + env['OPENSSL_BUILD_PATH'], + '-L' + env['OPENSSL_BUILD_PATH'] + , _env=env) + # Install M2Crypto + super(M2CryptoRecipe, self).build_arch(arch) + + def get_recipe_env(self, arch): + env = super(M2CryptoRecipe, self).get_recipe_env(arch) + env['OPENSSL_BUILD_PATH'] = self.get_recipe('openssl', self.ctx).get_build_dir(arch.arch) + env['CFLAGS'] += ' -I' + join(self.ctx.get_python_install_dir(), 'include/python2.7') + # Set linker to use the correct gcc + env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' + env['LDFLAGS'] += ' -lpython2.7' + return env + +recipe = M2CryptoRecipe() diff --git a/pythonforandroid/recipes/ndghttpsclient b/pythonforandroid/recipes/ndghttpsclient new file mode 100644 index 0000000000..bd971fcdc0 --- /dev/null +++ b/pythonforandroid/recipes/ndghttpsclient @@ -0,0 +1,9 @@ +from pythonforandroid.toolchain import PythonRecipe + +class NdgHttpsClientRecipe(PythonRecipe): + version = '0.4.0' + url = 'https://pypi.python.org/packages/source/n/ndg-httpsclient/ndg_httpsclient-{version}.tar.gz' + depends = ['python2', 'pyopenssl', 'cryptography'] + call_hostpython_via_targetpython = False + +recipe = NdgHttpsClientRecipe() diff --git a/pythonforandroid/recipes/pyjnius/__init__.py b/pythonforandroid/recipes/pyjnius/__init__.py index 999dc6ee3d..03a82ef752 100644 --- a/pythonforandroid/recipes/pyjnius/__init__.py +++ b/pythonforandroid/recipes/pyjnius/__init__.py @@ -12,8 +12,7 @@ class PyjniusRecipe(CythonRecipe): depends = [('python2', 'python3crystax'), ('sdl2', 'sdl'), 'six'] site_packages_name = 'jnius' - patches = [('sdl2_jnienv_getter.patch', will_build('sdl2')), - 'getenv.patch'] + patches = [('sdl2_jnienv_getter.patch', will_build('sdl2'))] def postbuild_arch(self, arch): super(PyjniusRecipe, self).postbuild_arch(arch) diff --git a/pythonforandroid/recipes/pyjnius/getenv.patch b/pythonforandroid/recipes/pyjnius/getenv.patch deleted file mode 100644 index 5c5335fc4c..0000000000 --- a/pythonforandroid/recipes/pyjnius/getenv.patch +++ /dev/null @@ -1,20 +0,0 @@ -diff --git a/setup.py b/setup.py -index b3c422d..f90694c 100644 ---- a/setup.py -+++ b/setup.py -@@ -13,10 +13,11 @@ PY3 = sys.version_info >= (3,0,0) - def getenv(key): - val = environ.get(key) - if val is not None: -- if PY3: -- return val.decode() -- else: -- return val -+ return val -+ # if PY3: -+ # return val.decode() -+ # else: -+ # return val - else: - return val - diff --git a/pythonforandroid/recipes/pyleveldb/__init__.py b/pythonforandroid/recipes/pyleveldb/__init__.py new file mode 100644 index 0000000000..ecb720951c --- /dev/null +++ b/pythonforandroid/recipes/pyleveldb/__init__.py @@ -0,0 +1,42 @@ +from pythonforandroid.toolchain import PythonRecipe, shprint, shutil, current_directory +from os.path import join, exists +import sh + +class PyLevelDBRecipe(PythonRecipe): + version = '0.193' + url = 'https://pypi.python.org/packages/source/l/leveldb/leveldb-{version}.tar.gz' + depends = ['leveldb', 'hostpython2', 'python2', 'setuptools'] + patches = ['bindings-only.patch'] + call_hostpython_via_targetpython = False + site_packages_name = 'leveldb' + + def build_arch(self, arch): + env = self.get_recipe_env(arch) + with current_directory(self.get_build_dir(arch.arch)): + # Remove source in this pypi package + sh.rm('-rf', './leveldb', './leveldb.egg-info', './snappy') + # Use source from leveldb recipe + sh.ln('-s', self.get_recipe('leveldb', self.ctx).get_build_dir(arch.arch), 'leveldb') + # Build python bindings + hostpython = sh.Command(self.hostpython_location) + shprint(hostpython, + 'setup.py', + 'build' + , _env=env) + # Install python bindings + super(PyLevelDBRecipe, self).build_arch(arch) + + def get_recipe_env(self, arch): + env = super(PyLevelDBRecipe, self).get_recipe_env(arch) + # Copy environment from leveldb recipe + env.update(self.get_recipe('leveldb', self.ctx).get_recipe_env(arch)) + env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() + env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python2.7' + # Set linker to use the correct gcc + env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' + env['LDFLAGS'] += ' -L' + env['PYTHON_ROOT'] + '/lib' + \ + ' -lpython2.7' + \ + ' -lleveldb' + return env + +recipe = PyLevelDBRecipe() diff --git a/pythonforandroid/recipes/pyleveldb/bindings-only.patch b/pythonforandroid/recipes/pyleveldb/bindings-only.patch new file mode 100644 index 0000000000..2899f4efaa --- /dev/null +++ b/pythonforandroid/recipes/pyleveldb/bindings-only.patch @@ -0,0 +1,103 @@ +--- pyleveldb/setup.py 2014-03-28 02:51:24.000000000 +0100 ++++ pyleveldb-patch/setup.py 2016-03-02 11:52:13.780678586 +0100 +@@ -7,41 +7,22 @@ + # + # See LICENSE for details. + +-import glob +-import platform +-import sys +- + from setuptools import setup, Extension + +-system,node,release,version,machine,processor = platform.uname() +-common_flags = [ ++extra_compile_args = [ + '-I./leveldb/include', + '-I./leveldb', +- '-I./snappy', ++ '-I./leveldb/snappy', + '-I.', +- '-fno-builtin-memcmp', + '-O2', + '-fPIC', + '-DNDEBUG', + '-DSNAPPY', +-] +- +-if system == 'Darwin': +- extra_compile_args = common_flags + [ +- '-DOS_MACOSX', ++ '-Wall', + '-DLEVELDB_PLATFORM_POSIX', +- '-Wno-error=unused-command-line-argument-hard-error-in-future', +- ] +-elif system == 'Linux': +- extra_compile_args = common_flags + [ +- '-pthread', +- '-Wall', +- '-DOS_LINUX', +- '-DLEVELDB_PLATFORM_POSIX', +- ] +-else: +- print >>sys.stderr, "Don't know how to compile leveldb for %s!" % system +- sys.exit(0) ++ '-D_REENTRANT', ++ '-DOS_ANDROID', ++] + + setup( + name = 'leveldb', +@@ -75,52 +56,6 @@ + ext_modules = [ + Extension('leveldb', + sources = [ +- # snappy +- './snappy/snappy.cc', +- './snappy/snappy-stubs-internal.cc', +- './snappy/snappy-sinksource.cc', +- './snappy/snappy-c.cc', +- +- #leveldb +- 'leveldb/db/builder.cc', +- 'leveldb/db/c.cc', +- 'leveldb/db/db_impl.cc', +- 'leveldb/db/db_iter.cc', +- 'leveldb/db/dbformat.cc', +- 'leveldb/db/filename.cc', +- 'leveldb/db/log_reader.cc', +- 'leveldb/db/log_writer.cc', +- 'leveldb/db/memtable.cc', +- 'leveldb/db/repair.cc', +- 'leveldb/db/table_cache.cc', +- 'leveldb/db/version_edit.cc', +- 'leveldb/db/version_set.cc', +- 'leveldb/db/write_batch.cc', +- 'leveldb/table/block.cc', +- 'leveldb/table/block_builder.cc', +- 'leveldb/table/filter_block.cc', +- 'leveldb/table/format.cc', +- 'leveldb/table/iterator.cc', +- 'leveldb/table/merger.cc', +- 'leveldb/table/table.cc', +- 'leveldb/table/table_builder.cc', +- 'leveldb/table/two_level_iterator.cc', +- 'leveldb/util/arena.cc', +- 'leveldb/util/bloom.cc', +- 'leveldb/util/cache.cc', +- 'leveldb/util/coding.cc', +- 'leveldb/util/comparator.cc', +- 'leveldb/util/crc32c.cc', +- 'leveldb/util/env.cc', +- 'leveldb/util/env_posix.cc', +- 'leveldb/util/filter_policy.cc', +- 'leveldb/util/hash.cc', +- 'leveldb/util/histogram.cc', +- 'leveldb/util/logging.cc', +- 'leveldb/util/options.cc', +- 'leveldb/util/status.cc', +- 'leveldb/port/port_posix.cc', +- + # python stuff + 'leveldb_ext.cc', + 'leveldb_object.cc', diff --git a/pythonforandroid/recipes/python3/__init__.py b/pythonforandroid/recipes/python3/__init__.py index 58500da2f7..e349341cad 100644 --- a/pythonforandroid/recipes/python3/__init__.py +++ b/pythonforandroid/recipes/python3/__init__.py @@ -22,6 +22,8 @@ def prebuild_arch(self, arch): print('Python3 already patched, skipping.') return + self.ctx.cython = 'cython' # Temporary hack + # # self.apply_patch(join('patches_inclement', # # 'python-{version}-define_macro.patch'.format(version=self.version))) # # self.apply_patch(join('patches_inclement', diff --git a/pythonforandroid/recipes/snappy/__init__.py b/pythonforandroid/recipes/snappy/__init__.py new file mode 100644 index 0000000000..ca429b20b0 --- /dev/null +++ b/pythonforandroid/recipes/snappy/__init__.py @@ -0,0 +1,11 @@ +from pythonforandroid.toolchain import Recipe + +class SnappyRecipe(Recipe): + version = '1.1.3' + url = 'https://github.com/google/snappy/releases/download/{version}/snappy-{version}.tar.gz' + + def should_build(self, arch): + # Only download to use in leveldb recipe + return False + +recipe = SnappyRecipe() diff --git a/setup.py b/setup.py index 1947854c03..e4707d817b 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ def recursively_include(results, directory, patterns): recursively_include(package_data, 'pythonforandroid/recipes', ['*.patch', 'Setup*', '*.pyx', '*.py', '*.c', '*.h', - '*.mk', ]) + '*.mk', '*.jam', ]) recursively_include(package_data, 'pythonforandroid/bootstraps', ['*.properties', '*.xml', '*.java', '*.tmpl', '*.txt', '*.png', '*.mk', '*.c', '*.h', '*.py', '*.sh', '*.jpg', '*.aidl', ]) diff --git a/tests/testapp/colours.png b/testapps/testapp/colours.png similarity index 100% rename from tests/testapp/colours.png rename to testapps/testapp/colours.png diff --git a/tests/testapp/main.py b/testapps/testapp/main.py similarity index 100% rename from tests/testapp/main.py rename to testapps/testapp/main.py diff --git a/tests/testapp_nogui/main.py b/testapps/testapp_nogui/main.py similarity index 100% rename from tests/testapp_nogui/main.py rename to testapps/testapp_nogui/main.py diff --git a/tests/vispy_testapp/main.py b/testapps/vispy_testapp/main.py similarity index 100% rename from tests/vispy_testapp/main.py rename to testapps/vispy_testapp/main.py diff --git a/tests/test_graph.py b/tests/test_graph.py new file mode 100644 index 0000000000..7e69ceed7a --- /dev/null +++ b/tests/test_graph.py @@ -0,0 +1,40 @@ + +from pythonforandroid.build import Context +from pythonforandroid.graph import get_recipe_order_and_bootstrap +from pythonforandroid.bootstrap import Bootstrap +from itertools import product + +import pytest + + +ctx = Context() + +name_sets = [['python2'], + ['kivy']] +bootstraps = [None, + Bootstrap.get_bootstrap('pygame', ctx), + Bootstrap.get_bootstrap('sdl2', ctx)] +valid_combinations = list(product(name_sets, bootstraps)) +valid_combinations.extend( + [(['python3crystax'], Bootstrap.get_bootstrap('sdl2', ctx)), + (['kivy', 'python3crystax'], Bootstrap.get_bootstrap('sdl2', ctx))]) + +@pytest.mark.parametrize('names,bootstrap', valid_combinations) +def test_valid_recipe_order_and_bootstrap(names, bootstrap): + get_recipe_order_and_bootstrap(ctx, names, bootstrap) + +invalid_combinations = [[['python2', 'python3crystax'], None], + [['python3'], Bootstrap.get_bootstrap('pygame', ctx)]] + +@pytest.mark.parametrize('names,bootstrap', invalid_combinations) +def test_invalid_recipe_order_and_bootstrap(names, bootstrap): + with pytest.raises(SystemExit): + get_recipe_order_and_bootstrap(ctx, names, bootstrap) + +def test_bootstrap_dependency_addition(): + build_order, python_modules, bs = get_recipe_order_and_bootstrap( + ctx, ['kivy'], None) + assert (('hostpython2' in build_order) or ('hostpython3' in build_order)) + +if __name__ == "__main__": + get_recipe_order_and_bootstrap(ctx, ['python3'], Bootstrap.get_bootstrap('sdl2', ctx))