#!/usr/bin/env python """ Tool for packaging Python apps for Android ========================================== This module defines the entry point for command line and programmatic use. """ from __future__ import print_function from os import environ from pythonforandroid import __version__ from pythonforandroid.pythonpackage import get_dep_names_of_package from pythonforandroid.recommendations import ( RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API) from pythonforandroid.util import BuildInterruptingException, handle_build_exception def check_python_dependencies(): # Check if the Python requirements are installed. This appears # before the imports because otherwise they're imported elsewhere. # Using the ok check instead of failing immediately so that all # errors are printed at once from distutils.version import LooseVersion from importlib import import_module import sys ok = True modules = [('colorama', '0.3.3'), 'appdirs', ('sh', '1.10'), 'jinja2', 'six'] for module in modules: if isinstance(module, tuple): module, version = module else: version = None try: import_module(module) except ImportError: if version is None: print('ERROR: The {} Python module could not be found, please ' 'install it.'.format(module)) ok = False else: print('ERROR: The {} Python module could not be found, ' 'please install version {} or higher'.format( module, version)) ok = False else: if version is None: continue try: cur_ver = sys.modules[module].__version__ except AttributeError: # this is sometimes not available continue if LooseVersion(cur_ver) < LooseVersion(version): print('ERROR: {} version is {}, but python-for-android needs ' 'at least {}.'.format(module, cur_ver, version)) ok = False if not ok: print('python-for-android is exiting due to the errors logged above') exit(1) check_python_dependencies() import sys from sys import platform from os.path import (join, dirname, realpath, exists, expanduser, basename) import os import glob import shutil import re import shlex from functools import wraps import argparse import sh import imp from appdirs import user_data_dir import logging from distutils.version import LooseVersion from pythonforandroid.recipe import Recipe from pythonforandroid.logger import (logger, info, warning, setup_color, Out_Style, Out_Fore, info_notify, info_main, shprint) from pythonforandroid.util import current_directory from pythonforandroid.bootstrap import Bootstrap from pythonforandroid.distribution import Distribution, pretty_log_dists from pythonforandroid.graph import get_recipe_order_and_bootstrap from pythonforandroid.build import Context, build_recipes user_dir = dirname(realpath(os.path.curdir)) toolchain_dir = dirname(__file__) sys.path.insert(0, join(toolchain_dir, "tools", "external")) def add_boolean_option(parser, names, no_names=None, default=True, dest=None, description=None): group = parser.add_argument_group(description=description) if not isinstance(names, (list, tuple)): names = [names] if dest is None: dest = names[0].strip("-").replace("-", "_") def add_dashes(x): return x if x.startswith("-") else "--"+x opts = [add_dashes(x) for x in names] group.add_argument( *opts, help=("(this is the default)" if default else None), dest=dest, action='store_true') if no_names is None: def add_no(x): x = x.lstrip("-") return ("no_"+x) if "_" in x else ("no-"+x) no_names = [add_no(x) for x in names] opts = [add_dashes(x) for x in no_names] group.add_argument( *opts, help=(None if default else "(this is the default)"), dest=dest, action='store_false') parser.set_defaults(**{dest: default}) def require_prebuilt_dist(func): """Decorator for ToolchainCL methods. If present, the method will automatically make sure a dist has been built before continuing or, if no dists are present or can be obtained, will raise an error. """ @wraps(func) def wrapper_func(self, args): ctx = self.ctx ctx.set_archs(self._archs) ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir, user_ndk_dir=self.ndk_dir, user_android_api=self.android_api, user_ndk_api=self.ndk_api) dist = self._dist if dist.needs_build: if dist.folder_exists(): # possible if the dist is being replaced dist.delete() info_notify('No dist exists that meets your requirements, ' 'so one will be built.') build_dist_from_args(ctx, dist, args) func(self, args) return wrapper_func def dist_from_args(ctx, args): """Parses out any distribution-related arguments, and uses them to obtain a Distribution class instance for the build. """ return Distribution.get_distribution( ctx, name=args.dist_name, recipes=split_argument_list(args.requirements), ndk_api=args.ndk_api, force_build=args.force_build, require_perfect_match=args.require_perfect_match, allow_replace_dist=args.allow_replace_dist) 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) blacklist = getattr(args, "blacklist_requirements", "").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 )) assert set(build_order).intersection(set(python_modules)) == set() 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 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))) ctx.dist_name = bs.distribution.name ctx.prepare_bootstrap(bs) if dist.needs_build: ctx.prepare_dist(ctx.dist_name) build_recipes(build_order, python_modules, ctx, getattr(args, "private", None), ignore_project_setup_py=getattr( args, "ignore_setup_py", False ), ) ctx.bootstrap.run_distribute() info_main('# Your distribution was created successfully, exiting.') info('Dist can be found at (for now) {}' .format(join(ctx.dist_dir, ctx.dist_name))) def split_argument_list(l): if not len(l): return [] return re.split(r'[ ,]+', l) class NoAbbrevParser(argparse.ArgumentParser): """We want to disable argument abbreviation so as not to interfere with passing through arguments to build.py, but in python2 argparse doesn't have this option. This subclass alternative is follows the suggestion at https://bugs.python.org/issue14910. """ def _get_option_tuples(self, option_string): return [] class ToolchainCL(object): def __init__(self): argv = sys.argv self.warn_on_carriage_return_args(argv) # Buildozer used to pass these arguments in a now-invalid order # If that happens, apply this fix # This fix will be removed once a fixed buildozer is released if (len(argv) > 2 and argv[1].startswith('--color') and argv[2].startswith('--storage-dir')): argv.append(argv.pop(1)) # the --color arg argv.append(argv.pop(1)) # the --storage-dir arg parser = NoAbbrevParser( description='A packaging tool for turning Python scripts and apps ' 'into Android APKs') generic_parser = argparse.ArgumentParser( add_help=False, description='Generic arguments applied to all commands') argparse.ArgumentParser( add_help=False, description='Arguments for dist building') generic_parser.add_argument( '--debug', dest='debug', action='store_true', default=False, help='Display debug output and all build info') generic_parser.add_argument( '--color', dest='color', choices=['always', 'never', 'auto'], help='Enable or disable color output (default enabled on tty)') generic_parser.add_argument( '--sdk-dir', '--sdk_dir', dest='sdk_dir', default='', help='The filepath where the Android SDK is installed') generic_parser.add_argument( '--ndk-dir', '--ndk_dir', dest='ndk_dir', default='', help='The filepath where the Android NDK is installed') generic_parser.add_argument( '--android-api', '--android_api', dest='android_api', default=0, type=int, help=('The Android API level to build against defaults to {} if ' 'not specified.').format(RECOMMENDED_TARGET_API)) generic_parser.add_argument( '--ndk-version', '--ndk_version', dest='ndk_version', default=None, help=('DEPRECATED: the NDK version is now found automatically or ' 'not at all.')) generic_parser.add_argument( '--ndk-api', type=int, default=None, help=('The Android API level to compile against. This should be your ' '*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', action='store_true', dest='symlink_java_src', default=False, help=('If True, symlinks the java src folder during build and dist ' 'creation. This is useful for development only, it could also' ' cause weird problems.')) default_storage_dir = user_data_dir('python-for-android') if ' ' in default_storage_dir: default_storage_dir = '~/.python-for-android' generic_parser.add_argument( '--storage-dir', dest='storage_dir', default=default_storage_dir, help=('Primary storage directory for downloads and builds ' '(default: {})'.format(default_storage_dir))) generic_parser.add_argument( '--arch', help='The archs to build for, separated by commas.', default='armeabi-v7a') # Options for specifying the Distribution generic_parser.add_argument( '--dist-name', '--dist_name', help='The name of the distribution to use or create', default='') generic_parser.add_argument( '--requirements', help=('Dependencies of your app, should be recipe names or ' 'Python modules. NOT NECESSARY if you are using ' 'Python 3 with --use-setup-py'), default='') generic_parser.add_argument( '--recipe-blacklist', help=('Blacklist an internal recipe from use. Allows ' 'disabling Python 3 core modules to save size'), dest="recipe_blacklist", default='') gen