From 3b40ba01bb43ce4c6c9a1e7b85fc5a35650b803c Mon Sep 17 00:00:00 2001 From: Jonas Thiem Date: Wed, 6 Feb 2019 03:11:42 +0100 Subject: [PATCH] Implement --blacklist option and include more modules/recipes by default * Adds `--blacklist` option that prevents recipes/packages from being added even when they are in the `depends` of recipes or otherwise added * Makes the following modules included per default: `android` (via `bootstrap.py` dependency of all bootstraps), `openssl`/`libffi`/`sqlite3` (via `python3` recipe dependency) * Documents `--blacklist` option and that `android` is now included by default * Cleans up the packaging kivy/sdl2 apps part in the Quickstart section --- doc/source/apis.rst | 18 +++--- doc/source/buildoptions.rst | 22 ++++++++ doc/source/quickstart.rst | 58 +++++++++++--------- pythonforandroid/bootstrap.py | 1 + pythonforandroid/graph.py | 40 ++++++++++---- pythonforandroid/recipes/python3/__init__.py | 2 +- pythonforandroid/toolchain.py | 26 +++++++-- tests/test_graph.py | 38 ++++++++++++- 8 files changed, 151 insertions(+), 54 deletions(-) diff --git a/doc/source/apis.rst b/doc/source/apis.rst index 2865ecb524..abd4a22447 100644 --- a/doc/source/apis.rst +++ b/doc/source/apis.rst @@ -12,8 +12,8 @@ Runtime permissions With API level >= 21, you will need to request runtime permissions to access the SD card, the camera, and other things. -This can be done through the `android` module, just add it to -your `--requirements` (as `android`) and then use it in your app like this:: +This can be done through the `android` module which is *available per default* +unless you blacklist it. Use it in your app like this:: from android.permissions import request_permission, Permission request_permission(Permission.WRITE_EXTERNAL_STORAGE) @@ -34,8 +34,8 @@ longer than necessary (with your app already being loaded) due to a limitation with the way we check if the app has properly started. In this case, the splash screen overlaps the app gui for a short time. -To dismiss the loading screen explicitely in your code, add p4a's `android` -module to your `--requirements` and use this:: +To dismiss the loading screen explicitely in your code, use the `android` +module:: from android import hide_loading_screen hide_loading_screen() @@ -92,14 +92,14 @@ Under SDL2, you can handle the `appropriate events `__. - -To build your application, you need to have a name, version, a package -identifier, and explicitly write the sdl2 bootstrap, as well as the -requirements:: - - p4a apk --private $HOME/code/myapp --package=org.example.myapp --name "My SDL2 application" --version 0.1 --bootstrap=sdl2 --requirements=your_requirements - -Add your required modules in place of ``your_requirements``, -e.g. ``--requirements=pysdl2`` or ``--requirements=vispy``. - Other options ~~~~~~~~~~~~~ @@ -198,7 +202,7 @@ You can pass other command line arguments to control app behaviours such as orientation, wakelock and app permissions. See :ref:`bootstrap_build_options`. - + Rebuild everything ~~~~~~~~~~~~~~~~~~ @@ -206,11 +210,11 @@ Rebuild everything If anything goes wrong and you want to clean the downloads and builds to retry everything, run:: p4a clean_all - + If you just want to clean the builds to avoid redownloading dependencies, run:: p4a clean_builds && p4a clean_dists - + Getting help ~~~~~~~~~~~~ @@ -269,7 +273,7 @@ You can list the available distributions:: And clean all of them:: p4a clean_dists - + Configuration file ~~~~~~~~~~~~~~~~~~ diff --git a/pythonforandroid/bootstrap.py b/pythonforandroid/bootstrap.py index e90c5b358a..b4a9a9e4c2 100755 --- a/pythonforandroid/bootstrap.py +++ b/pythonforandroid/bootstrap.py @@ -52,6 +52,7 @@ class Bootstrap(object): # All bootstraps should include Python in some way: recipe_depends = [ ("python2", "python2legacy", "python3", "python3crystax"), + 'android', ] can_be_chosen_automatically = True diff --git a/pythonforandroid/graph.py b/pythonforandroid/graph.py index 6958109a82..2e98e8ccda 100644 --- a/pythonforandroid/graph.py +++ b/pythonforandroid/graph.py @@ -39,10 +39,13 @@ def conflicts(self): return False -def get_dependency_tuple_list_for_recipe(recipe, blacklist=[]): +def get_dependency_tuple_list_for_recipe(recipe, blacklist=None): """ Get the dependencies of a recipe with filtered out blacklist, and turned into tuples with fix_deplist() """ + if blacklist is None: + blacklist = set() + assert(type(blacklist) == set) if recipe.depends is None: dependencies = [] else: @@ -51,14 +54,15 @@ def get_dependency_tuple_list_for_recipe(recipe, blacklist=[]): # Filter out blacklisted items and turn lowercase: dependencies = [ - deptuple for deptuple in dependencies - if not set(deptuple).intersection(set(blacklist)) + tuple(set(deptuple) - blacklist) + for deptuple in dependencies + if tuple(set(deptuple) - blacklist) ] return dependencies def recursively_collect_orders( - name, ctx, all_inputs, orders=[], blacklist=[] + name, ctx, all_inputs, orders=None, blacklist=None ): '''For each possible recipe ordering, try to add the new recipe name to that order. Recursively do the same thing with all the @@ -66,6 +70,10 @@ def recursively_collect_orders( ''' name = name.lower() + if orders is None: + orders = [] + if blacklist is None: + blacklist = set() try: recipe = Recipe.get_recipe(name, ctx) dependencies = get_dependency_tuple_list_for_recipe( @@ -75,7 +83,8 @@ def recursively_collect_orders( # handle opt_depends: these impose requirements on the build # order only if already present in the list of recipes to build dependencies.extend(fix_deplist( - [[d] for d in recipe.get_opt_depends_in_list(all_inputs)] + [[d] for d in recipe.get_opt_depends_in_list(all_inputs) + if d.lower() not in blacklist] )) if recipe.conflicts is None: @@ -106,7 +115,9 @@ def recursively_collect_orders( dependency_new_orders = [new_order] for dependency in dependency_set: dependency_new_orders = recursively_collect_orders( - dependency, ctx, all_inputs, dependency_new_orders) + dependency, ctx, all_inputs, dependency_new_orders, + blacklist=blacklist + ) new_orders.extend(dependency_new_orders) @@ -132,7 +143,7 @@ def find_order(graph): bset.discard(result) -def obvious_conflict_checker(ctx, name_tuples, blacklist=[]): +def obvious_conflict_checker(ctx, name_tuples, blacklist=None): """ This is a pre-flight check function that will completely ignore recipe order or choosing an actual value in any of the multiple choice tuples/dependencies, and just do a very basic obvious @@ -140,6 +151,8 @@ def obvious_conflict_checker(ctx, name_tuples, blacklist=[]): """ deps_were_added_by = dict() deps = set() + if blacklist is None: + blacklist = set() # Add dependencies for all recipes: to_be_added = [(name_tuple, None) for name_tuple in name_tuples] @@ -227,7 +240,7 @@ def obvious_conflict_checker(ctx, name_tuples, blacklist=[]): return None -def get_recipe_order_and_bootstrap(ctx, names, bs=None, blacklist=[]): +def get_recipe_order_and_bootstrap(ctx, names, bs=None, blacklist=None): # Get set of recipe/dependency names, clean up and add bootstrap deps: names = set(names) if bs is not None and bs.recipe_depends: @@ -236,7 +249,9 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None, blacklist=[]): ([name] if not isinstance(name, (list, tuple)) else name) for name in names ]) - blacklist = [bitem.lower() for bitem in blacklist] + if blacklist is None: + blacklist = set() + blacklist = {bitem.lower() for bitem in blacklist} # Remove all values that are in the blacklist: names_before_blacklist = list(names) @@ -261,7 +276,9 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None, blacklist=[]): new_possible_orders = [RecipeOrder(ctx)] for name in name_set: new_possible_orders = recursively_collect_orders( - name, ctx, name_set, orders=new_possible_orders) + name, ctx, name_set, orders=new_possible_orders, + blacklist=blacklist + ) possible_orders.extend(new_possible_orders) # turn each order graph into a linear list if possible @@ -305,7 +322,8 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None, blacklist=[]): "Could not find any compatible bootstrap!" ) recipes, python_modules, bs = get_recipe_order_and_bootstrap( - ctx, chosen_order, bs=bs) + ctx, chosen_order, bs=bs, blacklist=blacklist + ) else: # check if each requirement has a recipe recipes = [] diff --git a/pythonforandroid/recipes/python3/__init__.py b/pythonforandroid/recipes/python3/__init__.py index 780abac075..e7a9e5403f 100644 --- a/pythonforandroid/recipes/python3/__init__.py +++ b/pythonforandroid/recipes/python3/__init__.py @@ -23,7 +23,7 @@ class Python3Recipe(GuestPythonRecipe): patches = ["patches/fix-ctypes-util-find-library.patch"] - depends = ['hostpython3'] + depends = ['hostpython3', 'sqlite3', 'openssl', 'libffi'] conflicts = ['python3crystax', 'python2', 'python2legacy'] configure_args = ( diff --git a/pythonforandroid/toolchain.py b/pythonforandroid/toolchain.py index c899ef0d8c..cffe3e5afd 100644 --- a/pythonforandroid/toolchain.py +++ b/pythonforandroid/toolchain.py @@ -171,16 +171,24 @@ def build_dist_from_args(ctx, dist, args): """Parses out any bootstrap related arguments, and uses them to build a dist.""" bs = Bootstrap.get_bootstrap(args.bootstrap, ctx) - build_order, python_modules, bs \ - = get_recipe_order_and_bootstrap(ctx, dist.recipes, bs) + blacklist = getattr(args, "blacklist", "").split(",") + if len(blacklist) == 1 and blacklist[0] == "": + blacklist = [] + build_order, python_modules, bs = ( + get_recipe_order_and_bootstrap( + ctx, dist.recipes, bs, + blacklist=blacklist + )) ctx.recipe_build_order = build_order ctx.python_modules = python_modules info('The selected bootstrap is {}'.format(bs.name)) info_main('# Creating dist with {} bootstrap'.format(bs.name)) bs.distribution = dist - info_notify('Dist will have name {} and recipes ({})'.format( + info_notify('Dist will have name {} and requirements ({})'.format( dist.name, ', '.join(dist.recipes))) + info('Dist contains the following requirements as recipes: {}'.format( + ctx.recipe_build_order)) info('Dist will also contain modules ({}) installed from pip'.format( ', '.join(ctx.python_modules))) @@ -301,6 +309,13 @@ def __init__(self): 'Python modules'), default='') + generic_parser.add_argument( + '--blacklist', + help=('Blacklist an internal recipe from use. Allows ' + 'disabling Python 3 core modules to save size'), + dest="blacklist", + default='') + generic_parser.add_argument( '--bootstrap', help='The bootstrap to build with. Leave unset to choose ' @@ -448,7 +463,6 @@ def add_parser(subparsers, *args, **kwargs): help='Symlink the dist instead of copying') parser_apk = add_parser( - subparsers, 'apk', help='Build an APK', parents=[generic_parser]) @@ -527,9 +541,11 @@ def add_parser(subparsers, *args, **kwargs): if args.debug: logger.setLevel(logging.DEBUG) - # strip version from requirements, and put them in environ + # Process requirements and put version in environ if hasattr(args, 'requirements'): requirements = [] + + # Parse --requirements argument list: for requirement in split_argument_list(args.requirements): if "==" in requirement: requirement, version = requirement.split(u"==", 1) diff --git a/tests/test_graph.py b/tests/test_graph.py index 983e2cdbb5..e113c3bb3b 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -1,6 +1,7 @@ from pythonforandroid.build import Context from pythonforandroid.graph import ( - fix_deplist, get_recipe_order_and_bootstrap, obvious_conflict_checker, + fix_deplist, get_dependency_tuple_list_for_recipe, + get_recipe_order_and_bootstrap, obvious_conflict_checker, ) from pythonforandroid.bootstrap import Bootstrap from pythonforandroid.recipe import Recipe @@ -83,6 +84,41 @@ def test_invalid_recipe_order_and_bootstrap(names, bootstrap): assert "conflict" in e_info.value.message.lower() +def test_blacklist(): + # First, get order without blacklist: + build_order, python_modules, bs = get_recipe_order_and_bootstrap( + ctx, ["python3", "kivy"], None + ) + # Now, obtain again with blacklist: + build_order_2, python_modules_2, bs_2 = get_recipe_order_and_bootstrap( + ctx, ["python3", "kivy"], None, blacklist=["libffi"] + ) + assert "libffi" not in build_order_2 + assert set(build_order_2).union({"libffi"}) == set(build_order) + + # Check that we get a conflict when using webview and kivy combined: + wbootstrap = Bootstrap.get_bootstrap('webview', ctx) + with pytest.raises(BuildInterruptingException) as e_info: + get_recipe_order_and_bootstrap(ctx, ["flask", "kivy"], wbootstrap) + assert "conflict" in e_info.value.message.lower() + + # We should no longer get a conflict blacklisting sdl2 and pygame: + get_recipe_order_and_bootstrap( + ctx, ["flask", "kivy"], wbootstrap, blacklist=["sdl2", "pygame"] + ) + + +def test_get_dependency_tuple_list_for_recipe(monkeypatch): + r = get_fake_recipe("recipe1", depends=[ + "libffi", + ("libffi", "Pillow") + ]) + dep_list = get_dependency_tuple_list_for_recipe( + r, blacklist={"libffi"} + ) + assert(dep_list == [("pillow",)]) + + @pytest.mark.parametrize('names,bootstrap', valid_combinations) def test_valid_obvious_conflict_checker(names, bootstrap): # Note: obvious_conflict_checker is stricter on input