diff --git a/doc/source/distutils.rst b/doc/source/distutils.rst new file mode 100644 index 0000000000..80926548cc --- /dev/null +++ b/doc/source/distutils.rst @@ -0,0 +1,91 @@ + +distutils/setuptools integration +================================ + +Instead of running p4a via the command line, you can integrate with +distutils and setup.py. + +The base command is:: + + python setup.py apk + +The files included in the APK will be all those specified in the +``package_data`` argument to setup. For instance, the following +example will include all .py and .png files in the ``testapp`` +folder:: + + from distutils.core import setup + from setup + + setup( + name='testapp_setup', + version='1.1', + description='p4a setup.py example', + author='Your Name', + author_email='youremail@address.com', + packages=find_packages(), + options=options, + package_data={'testapp': ['*.py', '*.png']} + ) + +The app name and version will also be read automatically from the +setup.py. + +The Android package name uses ``org.test.lowercaseappname`` +if not set explicitly. + +The ``--private`` argument is set automatically using the +package_data, you should *not* set this manually. + +The target architecture defaults to ``--armeabi``. + +All of these automatic arguments can be overridden by passing them manually on the command line, e.g.:: + + python setup.py apk --name="Testapp Setup" --version=2.5 + +Adding p4a arguments in setup.py +-------------------------------- + +Instead of providing extra arguments on the command line, you can +store them in setup.py by passing the ``options`` parameter to +:code:`setup`. For instance:: + + from distutils.core import setup + from setuptools import find_packages + + options = {'apk': {'debug': None, # use None for arguments that don't pass a value + 'requirements': 'sdl2,pyjnius,kivy,python2', + 'android-api': 19, + 'ndk-dir': '/path/to/ndk', + 'dist-name': 'bdisttest', + }} + + packages = find_packages() + print('packages are', packages) + + setup( + name='testapp_setup', + version='1.1', + description='p4a setup.py example', + author='Your Name', + author_email='youremail@address.com', + packages=find_packages(), + options=options, + package_data={'testapp': ['*.py', '*.png']} + ) + +These options will be automatically included when you run ``python +setup.py apk``. Any options passed on the command line will override +these values. + +Adding p4a arguments in setup.cfg +--------------------------------- + +You can also provide p4a arguments in the setup.cfg file, as normal +for distutils. The syntax is:: + + [apk] + + argument=value + + requirements=sdl2,kivy diff --git a/doc/source/index.rst b/doc/source/index.rst index c7ec2cf922..84fd26dbb3 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -29,6 +29,7 @@ Contents quickstart buildoptions commands + distutils recipes bootstraps services diff --git a/pythonforandroid/bdist_apk.py b/pythonforandroid/bdist_apk.py deleted file mode 100644 index 5e58781d2e..0000000000 --- a/pythonforandroid/bdist_apk.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import print_function -from setuptools import Command -from pythonforandroid import toolchain - -class BdistAPK(Command): - description = 'Create an APK with python-for-android' - user_options = [] - - def initialize_options(sel): - print('initialising!') - - def finalize_options(self): - print('finalising!') - - def run(self): - print('running!') diff --git a/pythonforandroid/bdistapk.py b/pythonforandroid/bdistapk.py new file mode 100644 index 0000000000..a87fc76c20 --- /dev/null +++ b/pythonforandroid/bdistapk.py @@ -0,0 +1,140 @@ +from __future__ import print_function +from setuptools import Command +from pythonforandroid import toolchain + +import sys +from os.path import realpath, join, exists, dirname, curdir, basename, split +from os import makedirs +from glob import glob +from shutil import rmtree, copyfile + + +def argv_contains(t): + for arg in sys.argv: + if arg.startswith(t): + return True + return False + + +class BdistAPK(Command): + description = 'Create an APK with python-for-android' + + user_options = [] + + 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') + + # This is a hack, we probably aren't supposed to loop through + # the option_dict so early because distutils does exactly the + # same thing later to check that we support the + # options. However, it works... + for (option, (source, value)) in option_dict.items(): + setattr(self, option, str(value)) + + + def finalize_options(self): + + setup_options = self.distribution.get_option_dict('apk') + for (option, (source, value)) in setup_options.items(): + if source == 'command line': + continue + if not argv_contains('--' + option): + if value in (None, 'None'): + sys.argv.append('--{}'.format(option)) + else: + sys.argv.append('--{}={}'.format(option, value)) + + # Inject some argv options from setup.py if the user did not + # provide them + if not argv_contains('--name'): + name = self.distribution.get_name() + sys.argv.append('--name={}'.format(name)) + self.name = name + + if not argv_contains('--package'): + package = 'org.test.{}'.format(self.name.lower().replace(' ', '')) + print('WARNING: You did not supply an Android package ' + 'identifier, trying {} instead.'.format(package)) + print(' This may fail if this is not a valid identifier') + sys.argv.append('--package={}'.format(package)) + + if not argv_contains('--version'): + version = self.distribution.get_version() + sys.argv.append('--version={}'.format(version)) + + if not argv_contains('--arch'): + arch = 'armeabi' + self.arch = arch + sys.argv.append('--arch={}'.format(arch)) + + def run(self): + + self.prepare_build_dir() + + from pythonforandroid.toolchain import main + sys.argv[1] = 'apk' + main() + + def prepare_build_dir(self): + + if argv_contains('--private'): + print('WARNING: Received --private argument when this would ' + 'normally be generated automatically.') + print(' This is probably bad unless you meant to do ' + 'that.') + + bdist_dir = 'build/bdist.android-{}'.format(self.arch) + rmtree(bdist_dir) + makedirs(bdist_dir) + + globs = [] + for directory, patterns in self.distribution.package_data.items(): + for pattern in patterns: + globs.append(join(directory, pattern)) + + filens = [] + for pattern in globs: + filens.extend(glob(pattern)) + + main_py_dirs = [] + for filen in filens: + new_dir = join(bdist_dir, dirname(filen)) + if not exists(new_dir): + makedirs(new_dir) + print('Including {}'.format(filen)) + copyfile(filen, join(bdist_dir, filen)) + if basename(filen) in ('main.py', 'main.pyo'): + main_py_dirs.append(filen) + + # This feels ridiculous, but how else to define the main.py dir? + # Maybe should just fail? + if len(main_py_dirs) == 0: + print('ERROR: Could not find main.py, so no app build dir defined') + print('You should name your app entry point main.py') + exit(1) + if len(main_py_dirs) > 1: + print('WARNING: Multiple main.py dirs found, using the shortest path') + main_py_dirs = sorted(main_py_dirs, key=lambda j: len(split(j))) + + sys.argv.append('--private={}'.format(join(realpath(curdir), bdist_dir, + dirname(main_py_dirs[0])))) + + +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 + user_options = [('requirements=', None, None),] + for i, arg in enumerate(sys.argv): + if arg.startswith('--'): + if ('=' in arg or + (i < (len(sys.argv) - 1) and not sys.argv[i+1].startswith('-'))): + user_options.append((arg[2:].split('=')[0] + '=', None, None)) + else: + user_options.append((arg[2:], None, None)) + + BdistAPK.user_options = user_options + +_set_user_options() diff --git a/setup.py b/setup.py index 97ed536fbb..6e228f3cba 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ def recursively_include(results, directory, patterns): 'p4a = pythonforandroid.toolchain:main', ], 'distutils.commands': [ - 'bdist_apk = pythonforandroid.bdist_apk:BdistAPK', + 'apk = pythonforandroid.bdistapk:BdistAPK', ], }, classifiers = [ diff --git a/testapps/testapp_setup/setup.cfg b/testapps/testapp_setup/setup.cfg new file mode 100644 index 0000000000..e69de29bb2 diff --git a/testapps/testapp_setup/setup.py b/testapps/testapp_setup/setup.py new file mode 100644 index 0000000000..e6d7148720 --- /dev/null +++ b/testapps/testapp_setup/setup.py @@ -0,0 +1,29 @@ + +from distutils.core import setup +from setuptools import find_packages + +options = {'apk': {'debug': None, + 'requirements': 'sdl2,pyjnius,kivy,python2', + 'android-api': 19, + 'ndk-dir': '/home/asandy/android/crystax-ndk-10.3.1', + 'dist-name': 'bdisttest', + 'ndk-version': '10.3.1', + }} + +package_data = {'': ['*.py', + '*.png'] + } + +packages = find_packages() +print('packages are', packages) + +setup( + name='testapp_setup', + version='1.1', + description='p4a setup.py test', + author='Alexander Taylor', + author_email='alexanderjohntaylor@gmail.com', + packages=find_packages(), + options=options, + package_data={'testapp': ['*.py', '*.png']} +) diff --git a/testapps/testapp_setup/testapp/colours.png b/testapps/testapp_setup/testapp/colours.png new file mode 100644 index 0000000000..30b685e32b Binary files /dev/null and b/testapps/testapp_setup/testapp/colours.png differ diff --git a/testapps/testapp_setup/testapp/main.py b/testapps/testapp_setup/testapp/main.py new file mode 100644 index 0000000000..a9f8f47e06 --- /dev/null +++ b/testapps/testapp_setup/testapp/main.py @@ -0,0 +1,150 @@ +print('main.py was successfully called') + +import os +print('imported os') + +print('contents of ./lib/python2.7/site-packages/ etc.') +print(os.listdir('./lib')) +print(os.listdir('./lib/python2.7')) +print(os.listdir('./lib/python2.7/site-packages')) + +print('contents of this dir', os.listdir('./')) + +with open('./lib/python2.7/site-packages/kivy/app.pyo', 'rb') as fileh: + print('app.pyo size is', len(fileh.read())) + +import sys +print('pythonpath is', sys.path) + +import kivy +print('imported kivy') +print('file is', kivy.__file__) + +from kivy.app import App + +from kivy.lang import Builder +from kivy.properties import StringProperty + +from kivy.uix.popup import Popup +from kivy.clock import Clock + +print('Imported kivy') +from kivy.utils import platform +print('platform is', platform) + + +kv = ''' +#:import Metrics kivy.metrics.Metrics + +: + size_hint_y: None + height: dp(60) + + +ScrollView: + GridLayout: + cols: 1 + size_hint_y: None + height: self.minimum_height + FixedSizeButton: + text: 'test pyjnius' + on_press: app.test_pyjnius() + Image: + keep_ratio: False + allow_stretch: True + source: 'colours.png' + size_hint_y: None + height: dp(100) + Label: + height: self.texture_size[1] + size_hint_y: None + font_size: 100 + text_size: self.size[0], None + markup: True + text: '[b]Kivy[/b] on [b]SDL2[/b] on [b]Android[/b]!' + halign: 'center' + Widget: + size_hint_y: None + height: 20 + Label: + height: self.texture_size[1] + size_hint_y: None + font_size: 50 + text_size: self.size[0], None + markup: True + text: 'dpi: {}\\ndensity: {}\\nfontscale: {}'.format(Metrics.dpi, Metrics.density, Metrics.fontscale) + halign: 'center' + FixedSizeButton: + text: 'test ctypes' + on_press: app.test_ctypes() + FixedSizeButton: + text: 'test numpy' + on_press: app.test_numpy() + Widget: + size_hint_y: None + height: 1000 + on_touch_down: print 'touched at', args[-1].pos + +: + title: 'Error' + size_hint: 0.75, 0.75 + Label: + text: root.error_text +''' + + +class ErrorPopup(Popup): + error_text = StringProperty('') + +def raise_error(error): + print('ERROR:', error) + ErrorPopup(error_text=error).open() + +class TestApp(App): + def build(self): + root = Builder.load_string(kv) + Clock.schedule_interval(self.print_something, 2) + # Clock.schedule_interval(self.test_pyjnius, 5) + print('testing metrics') + from kivy.metrics import Metrics + print('dpi is', Metrics.dpi) + print('density is', Metrics.density) + print('fontscale is', Metrics.fontscale) + return root + + def print_something(self, *args): + print('App print tick', Clock.get_boottime()) + + def on_pause(self): + return True + + def test_pyjnius(self, *args): + try: + from jnius import autoclass + except ImportError: + raise_error('Could not import pyjnius') + return + + print('Attempting to vibrate with pyjnius') + # PythonActivity = autoclass('org.renpy.android.PythonActivity') + # activity = PythonActivity.mActivity + PythonActivity = autoclass('org.kivy.android.PythonActivity') + activity = PythonActivity.mActivity + Intent = autoclass('android.content.Intent') + Context = autoclass('android.content.Context') + vibrator = activity.getSystemService(Context.VIBRATOR_SERVICE) + + vibrator.vibrate(1000) + + def test_ctypes(self, *args): + import ctypes + + def test_numpy(self, *args): + import numpy + + print(numpy.zeros(5)) + print(numpy.arange(5)) + print(numpy.random.random((3, 3))) + + +TestApp().run()