Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement --blacklist option and include more modules/recipes by default #1683

Merged
merged 1 commit into from Feb 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions doc/source/apis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -92,14 +92,14 @@ Under SDL2, you can handle the `appropriate events <https://wiki.libsdl.org/SDL_
Advanced Android API use
------------------------

.. _reference-label-for-android-module:

`android` for Android API access
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

As mentioned above, the ``android`` Python module provides a simple
wrapper around many native Android APIS, and it can be included by
adding it to your requirements, e.g. :code:`--requirements=kivy,android`.
It is not automatically included by Kivy unless you use the old (Pygame)
bootstrap.
wrapper around many native Android APIS, and it is *included per default*
unless you blacklist it.

The available functionality of this module is not separately documented.
You can read the source `on
Expand Down Expand Up @@ -136,7 +136,7 @@ This is obviously *much* less verbose than with Pyjnius!


`Pyjnius` - raw lowlevel API access
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Pyjnius lets you call the Android API directly from Python Pyjnius is
works by dynamically wrapping Java classes, so you don't have to wait
Expand Down
22 changes: 22 additions & 0 deletions doc/source/buildoptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,25 @@ options (this list may not be exhaustive):
- ``add-source``: Add a source directory to the app's Java code.
- ``--compile-pyo``: Optimise .py files to .pyo.
- ``--resource``: A key=value pair to add in the string.xml resource file.


Blacklist (APK size optimization)
---------------------------------

To optimize the size of the `.apk` file that p4a builds for you,
you can **blacklist** certain core components. Per default, p4a
will add python *with batteries included* as would be expected on
desktop, including openssl, sqlite3 and other components you may
not use.

To blacklist an item, specify the ``--blacklist`` option::

p4a apk ... --blacklist=sqlite3

At the moment, the following core components can be blacklisted
(if you don't want to use them) to decrease APK size:

- ``android`` disables p4a's android module (see :ref:`reference-label-for-android-module`)
- ``libffi`` disables ctypes stdlib module
- ``openssl`` disables ssl stdlib module
- ``sqlite3`` disables sqlite3 stdlib module
58 changes: 31 additions & 27 deletions doc/source/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,18 +149,34 @@ You have the possibility to configure on any command the PATH to the SDK, NDK an
Usage
-----

Build a Kivy application
~~~~~~~~~~~~~~~~~~~~~~~~
Build a Kivy or SDL2 application
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To build your application, you need to have a name, version, a package
identifier, and explicitly write the bootstrap you want to use, as
well as the requirements::
To build your application, you need to specify name, version, a package
identifier, the bootstrap you want to use (`sdl2` for kivy or sdl2 apps)
and the requirements::

p4a apk --private $HOME/code/myapp --package=org.example.myapp --name "My application" --version 0.1 --bootstrap=sdl2 --requirements=python3,kivy

**Note on `--requirements`: you must add all
libraries/dependencies your app needs to run.**
Example: `--requirements=python3,kivy,vispy`. For an SDL2 app,
`kivy` is not needed, but you need to add any wrappers you might
use (e.g. `pysdl2`).

This `p4a apk ...` command builds a distribution with `python3`,
`kivy`, and everything else you specified in the requirements.
It will be packaged using a SDL2 bootstrap, and produce
an `.apk` file.

p4a apk --private $HOME/code/myapp --package=org.example.myapp --name "My application" --version 0.1 --bootstrap=sdl2 --requirements=python2,kivy
*Compatibility notes:*

This will first build a distribution that contains `python2` and `kivy`, and using a SDL2 bootstrap. Python2 is here explicitely written as kivy can work with python2 or python3.
- While python2 is still supported by python-for-android,
it will possibly no longer receive patches by the python creators
themselves in 2020. Migration to Python 3 is recommended!

You can also use ``--bootstrap=pygame``, but this bootstrap is deprecated for use with Kivy and SDL2 is preferred.
- You can also use ``--bootstrap=pygame``, but this bootstrap
is deprecated and not well-tested.

Build a WebView application
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -171,46 +187,34 @@ well as the requirements::

p4a apk --private $HOME/code/myapp --package=org.example.myapp --name "My WebView Application" --version 0.1 --bootstrap=webview --requirements=flask --port=5000

**Please note as with kivy/SDL2, you need to specify all your
additional requirements/depenencies.**

You can also replace flask with another web framework.

Replace ``--port=5000`` with the port on which your app will serve a
website. The default for Flask is 5000.

Build an SDL2 based application
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This includes e.g. `PySDL2
<https://pysdl2.readthedocs.io/en/latest/>`__.

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
~~~~~~~~~~~~~

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
~~~~~~~~~~~~~~~~~~

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
~~~~~~~~~~~~

Expand Down Expand Up @@ -269,7 +273,7 @@ You can list the available distributions::
And clean all of them::

p4a clean_dists

Configuration file
~~~~~~~~~~~~~~~~~~

Expand Down
1 change: 1 addition & 0 deletions pythonforandroid/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 29 additions & 11 deletions pythonforandroid/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -51,21 +54,26 @@ 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)
AndreMiras marked this conversation as resolved.
Show resolved Hide resolved
]
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
dependencies of each recipe.

'''
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(
Expand All @@ -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:
Expand Down Expand Up @@ -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)

Expand All @@ -132,14 +143,16 @@ 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
conflict check.
"""
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]
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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 = []
Expand Down
2 changes: 1 addition & 1 deletion pythonforandroid/recipes/python3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
26 changes: 21 additions & 5 deletions pythonforandroid/toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)))

Expand Down Expand Up @@ -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='')
AndreMiras marked this conversation as resolved.
Show resolved Hide resolved

generic_parser.add_argument(
'--bootstrap',
help='The bootstrap to build with. Leave unset to choose '
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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)
Expand Down
Loading