diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000000..e30c701da6 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,12 @@ +version = 1 + +test_patterns = ["tests/**"] + +exclude_patterns = ["testapps/**"] + +[[analyzers]] +name = "python" +enabled = true + + [analyzers.meta] + runtime_version = "3.x.x" \ No newline at end of file diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index b47dac8810..4d576edb8a 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -1,4 +1,4 @@ -name: Unit tests & Build Testapp +name: Unit tests & build apps on: ['push', 'pull_request'] @@ -42,25 +42,35 @@ jobs: make test build: - name: Build testapp + name: Unit test apk needs: [flake8] runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - build-arch: ['arm64-v8a', 'armeabi-v7a'] + build-arch: ['arm64-v8a', 'armeabi-v7a', 'x86_64', 'x86'] steps: - name: Checkout python-for-android uses: actions/checkout@v2 + # helps with GitHub runner getting out of space + - name: Free disk space + run: | + df -h + sudo swapoff -a + sudo rm -f /swapfile + sudo apt -y clean + docker rmi $(docker image ls -aq) + df -h - name: Pull docker image run: | make docker/pull - - name: Build apk for Python 3 ${{ matrix.build-arch }} + - name: Build apk Python 3 ${{ matrix.build-arch }} run: | mkdir -p apks - make docker/run/make/with-artifact/testapps/python3/${{ matrix.build-arch }} + make docker/run/make/with-artifact/testapps-with-numpy/${{ matrix.build-arch }} - uses: actions/upload-artifact@v1 with: - name: bdisttest_python3_sqlite_openssl_googlendk__${{ matrix.build-arch }}-debug-1.1.apk + name: bdist_test_app_unittests__${{ matrix.build-arch }}-debug-1.1.apk path: apks rebuild_updated_recipes: @@ -74,6 +84,15 @@ jobs: uses: actions/checkout@v2 with: ref: 'develop' + # helps with GitHub runner getting out of space + - name: Free disk space + run: | + df -h + sudo swapoff -a + sudo rm -f /swapfile + sudo apt -y clean + docker rmi $(docker image ls -aq) + df -h - name: Pull docker image run: | make docker/pull diff --git a/.gitignore b/.gitignore index 05196a8acb..f36a2f3572 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ python_for_android.egg-info /build/ doc/build __pycache__/ +venv/ #idea/pycharm .idea/ diff --git a/.travis.yml b/.travis.yml index c8ad70c91f..04e49e59c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,9 +23,7 @@ jobs: # See also: https://github.com/travis-ci/travis-ci/issues/8589 - type -t deactivate && deactivate || true - export PATH=/opt/python/3.7/bin:$PATH - # Install tox & virtualenv - # Note: venv/virtualenv are both used by tests/test_pythonpackage.py - - pip3.7 install -U virtualenv + # Install tox - pip3.7 install tox>=2.0 # Install coveralls & dependencies # Note: pyOpenSSL needed to send the coveralls reports @@ -36,9 +34,6 @@ jobs: - tox -- tests/ --ignore tests/test_pythonpackage.py name: "Tox Pep8" env: TOXENV=pep8 - - <<: *unittests - name: "Tox Python 2" - env: TOXENV=py27 - <<: *unittests name: "Tox Python 3 & Coverage" env: TOXENV=py3 @@ -48,7 +43,7 @@ jobs: name: Python 3 arm64-v8a (with numpy) stage: build testapps before_script: make docker/pull - script: make docker/run/make/testapps/python3/arm64-v8a + script: make docker/run/make/testapps-with-numpy/arm64-v8a - <<: *testapps name: Python 3 armeabi-v7a os: osx @@ -57,10 +52,19 @@ jobs: # installs java 1.8, android's SDK/NDK and p4a - make -f ci/makefiles/osx.mk - export JAVA_HOME=/Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home - script: make testapps/python3/armeabi-v7a PYTHON_WITH_VERSION=python3 - - <<: *testapps - name: Python 2 armeabi-v7a - script: make docker/run/make/testapps/python2/armeabi-v7a + script: make testapps-no-venv/armeabi-v7a - <<: *testapps name: Rebuild updated recipes script: travis_wait 30 make docker/run/make/rebuild_updated_recipes + +# Deploy to PyPI using token set in `PYPI_PASSWORD` environment variable +# https://pypi.org/manage/account/token/ +# https://travis-ci.org/github/kivy/python-for-android/settings +deploy: + provider: pypi + distributions: sdist bdist_wheel + user: "__token__" + on: + tags: true + repo: kivy/python-for-android + python: 3.7 diff --git a/Dockerfile.py3 b/Dockerfile similarity index 92% rename from Dockerfile.py3 rename to Dockerfile index 9183c8feaf..07377e4248 100644 --- a/Dockerfile.py3 +++ b/Dockerfile @@ -3,7 +3,7 @@ # - python-for-android dependencies # # Build with: -# docker build --tag=p4a --file Dockerfile.py3 . +# docker build --tag=p4a --file Dockerfile . # # Run with: # docker run -it --rm p4a /bin/sh -c '. venv/bin/activate && p4a apk --help' @@ -66,20 +66,18 @@ RUN dpkg --add-architecture i386 \ libncurses5:i386 \ libpangox-1.0-0:i386 \ libpangoxft-1.0-0:i386 \ + libssl-dev \ libstdc++6:i386 \ libtool \ openjdk-8-jdk \ patch \ pkg-config \ - python \ - python-pip \ python3 \ python3-dev \ python3-pip \ python3-venv \ sudo \ unzip \ - virtualenv \ wget \ zip \ zlib1g-dev \ @@ -95,10 +93,6 @@ RUN useradd --create-home --shell /bin/bash ${USER} RUN usermod -append --groups sudo ${USER} RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers -# install cython for python 2 (for python 3 it's inside the venv) -RUN pip2 install --upgrade Cython==0.28.6 \ - && rm -rf ~/.cache/ - WORKDIR ${WORK_DIR} RUN mkdir ${ANDROID_HOME} && chown --recursive ${USER} ${HOME_DIR} ${ANDROID_HOME} USER ${USER} diff --git a/Dockerfile.py2 b/Dockerfile.py2 deleted file mode 100644 index 19418329ca..0000000000 --- a/Dockerfile.py2 +++ /dev/null @@ -1,94 +0,0 @@ -# Dockerfile with: -# - Android build environment -# - python-for-android dependencies -# -# Build with: -# docker build --tag=p4apy2 --file Dockerfile.py2 . -# -# Run with: -# docker run -it --rm p4apy2 /bin/sh -c '. venv/bin/activate && p4a apk --help' -# -# Or for interactive shell: -# docker run -it --rm p4apy2 -# -# Note: -# Use 'docker run' without '--rm' flag for keeping the container and use -# 'docker commit ' to extend the original image - -FROM ubuntu:18.04 - -ENV ANDROID_HOME="/opt/android" - -# configure locale -RUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends \ - locales && \ - locale-gen en_US.UTF-8 -ENV LANG="en_US.UTF-8" \ - LANGUAGE="en_US.UTF-8" \ - LC_ALL="en_US.UTF-8" - -RUN apt -y update -qq \ - && apt -y install -qq --no-install-recommends curl unzip ca-certificates \ - && apt -y autoremove - -# retry helper script, refs: -# https://github.com/kivy/python-for-android/issues/1306 -ENV RETRY="retry -t 3 --" -RUN curl https://raw.githubusercontent.com/kadwanev/retry/1.0.1/retry \ - --output /usr/local/bin/retry && chmod +x /usr/local/bin/retry - -ENV USER="user" -ENV HOME_DIR="/home/${USER}" -ENV ANDROID_HOME="${HOME_DIR}/.android" -ENV WORK_DIR="${HOME_DIR}" \ - PATH="${HOME_DIR}/.local/bin:${PATH}" - -# install system dependencies -RUN ${RETRY} apt -y install -qq --no-install-recommends \ - python virtualenv python-pip wget lbzip2 patch sudo \ - && apt -y autoremove \ - && apt -y clean - -# build dependencies -# https://buildozer.readthedocs.io/en/latest/installation.html#android-on-ubuntu-16-04-64bit -RUN dpkg --add-architecture i386 \ - && ${RETRY} apt -y update -qq \ - && ${RETRY} apt -y install -qq --no-install-recommends \ - build-essential ccache git python2.7 python2.7-dev \ - libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 \ - libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 \ - zip zlib1g-dev zlib1g:i386 \ - && apt -y autoremove - -# specific recipes dependencies (e.g. libffi requires autoreconf binary) -RUN ${RETRY} apt -y install -qq --no-install-recommends \ - libffi-dev autoconf automake cmake gettext libltdl-dev libtool pkg-config \ - && apt -y autoremove \ - && apt -y clean - -# Install Java, set JAVA_HOME (to accept android's SDK licenses) and clean -RUN ${RETRY} apt -y install -qq --no-install-recommends openjdk-8-jdk \ - && apt -y autoremove && apt -y clean -ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64 - -# prepare non root env -RUN useradd --create-home --shell /bin/bash ${USER} - -# with sudo access and no password -RUN usermod -append --groups sudo ${USER} -RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers - - -WORKDIR ${WORK_DIR} -COPY --chown=user:user . ${WORK_DIR} -RUN mkdir ${ANDROID_HOME} && chown --recursive ${USER} ${ANDROID_HOME} -USER ${USER} - -# Download and install android's NDK/SDK -RUN make -f ci/makefiles/android.mk target_os=linux - -# install python-for-android from current branch -RUN virtualenv --python=python venv \ - && . venv/bin/activate \ - && pip install --upgrade cython==0.28.6 \ - && pip install -e . diff --git a/MANIFEST.in b/MANIFEST.in index 4d6223ed3d..d428a87439 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include LICENSE README.md +include *.toml recursive-include doc * prune doc/build diff --git a/Makefile b/Makefile index f1783c9820..ef0c503eb9 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ ANDROID_NDK_HOME ?= $(HOME)/.android/android-ndk all: virtualenv $(VIRTUAL_ENV): - virtualenv --python=$(PYTHON_WITH_VERSION) $(VIRTUAL_ENV) + python3 -m venv $(VIRTUAL_ENV) $(PIP) install Cython==0.28.6 $(PIP) install -e . @@ -28,28 +28,32 @@ virtualenv: $(VIRTUAL_ENV) # ignores test_pythonpackage.py since it runs for too long test: $(TOX) -- tests/ --ignore tests/test_pythonpackage.py - @if test -n "$$CI"; then .tox/py$(PYTHON_MAJOR_MINOR)/bin/coveralls; fi; \ rebuild_updated_recipes: virtualenv . $(ACTIVATE) && \ ANDROID_SDK_HOME=$(ANDROID_SDK_HOME) ANDROID_NDK_HOME=$(ANDROID_NDK_HOME) \ $(PYTHON) ci/rebuild_updated_recipes.py -testapps/python2/armeabi-v7a: virtualenv - . $(ACTIVATE) && cd testapps/ && \ - python setup_testapp_python2_sqlite_openssl.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ - --requirements sdl2,pyjnius,kivy,python2,openssl,requests,sqlite3,setuptools - -testapps/python3/arm64-v8a: virtualenv - . $(ACTIVATE) && cd testapps/ && \ - python setup_testapp_python3_sqlite_openssl.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ +testapps-with-numpy/%: virtualenv + $(eval $@_APP_ARCH := $(shell basename $*)) + . $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \ + python setup.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ --requirements libffi,sdl2,pyjnius,kivy,python3,openssl,requests,sqlite3,setuptools,numpy \ - --arch=arm64-v8a + --arch=$($@_APP_ARCH) -testapps/python3/armeabi-v7a: virtualenv - . $(ACTIVATE) && cd testapps/ && \ - python setup_testapp_python3_sqlite_openssl.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ - --arch=armeabi-v7a +testapps/%: virtualenv + $(eval $@_APP_ARCH := $(shell basename $*)) + . $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \ + python setup.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ + --arch=$($@_APP_ARCH) + +testapps-no-venv/%: + pip3 install Cython==0.28.6 + pip3 install -e . + $(eval $@_APP_ARCH := $(shell basename $*)) + cd testapps/on_device_unit_tests/ && \ + python3 setup.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ + --arch=$($@_APP_ARCH) clean: find . -type d -name "__pycache__" -exec rm -r {} + @@ -62,7 +66,7 @@ docker/pull: docker pull $(DOCKER_IMAGE):latest || true docker/build: - docker build --cache-from=$(DOCKER_IMAGE) --tag=$(DOCKER_IMAGE) --file=Dockerfile.py3 . + docker build --cache-from=$(DOCKER_IMAGE) --tag=$(DOCKER_IMAGE) . docker/push: docker push $(DOCKER_IMAGE) @@ -77,14 +81,9 @@ docker/run/make/%: docker/build docker run --rm --env-file=.env $(DOCKER_IMAGE) make $* docker/run/make/with-artifact/%: docker/build -ifeq (,$(findstring python3,$($*))) - $(eval $@_APP_NAME := bdisttest_python3_sqlite_openssl_googlendk) -else - $(eval $@_APP_NAME := bdisttest_python2_sqlite_openssl) -endif $(eval $@_APP_ARCH := $(shell basename $*)) docker run --name p4a-latest --env-file=.env $(DOCKER_IMAGE) make $* - docker cp p4a-latest:/home/user/app/testapps/$($@_APP_NAME)__$($@_APP_ARCH)-debug-1.1-.apk ./apks + docker cp p4a-latest:/home/user/app/testapps/on_device_unit_tests/bdist_unit_tests_app__$($@_APP_ARCH)-debug-1.1-.apk ./apks docker rm -fv p4a-latest docker/run/shell: docker/build diff --git a/README.md b/README.md index bf108c3442..ac8a889fe5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ python-for-android ================== [![Build Status](https://travis-ci.org/kivy/python-for-android.svg?branch=develop)](https://travis-ci.org/kivy/python-for-android) +[![Unit tests & build apps](https://github.com/kivy/python-for-android/workflows/Unit%20tests%20&%20build%20apps/badge.svg?branch=develop)](https://github.com/kivy/python-for-android/actions?query=workflow%3A%22Unit+tests+%26+build+apps%22) [![Coverage Status](https://coveralls.io/repos/github/kivy/python-for-android/badge.svg?branch=develop&kill_cache=1)](https://coveralls.io/github/kivy/python-for-android?branch=develop) [![Backers on Open Collective](https://opencollective.com/kivy/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/kivy/sponsors/badge.svg)](#sponsors) @@ -102,23 +103,13 @@ new recipe for python3 (3.7.1) had a new build system which was applied to the ancient python recipe, allowing us to bump the python2 version number to 2.7.15. This change unified the build process for both python recipes, and probably solved various issues detected over the -years. It should also be mentioned that these **unified python recipes** -require a **minimum target api level of 21**, -*Android 5.0 - Lollipop*, so in the case that you need to build targeting an +years. These **unified python recipes** require a **minimum target api level of 21**, +*Android 5.0 - Lollipop*. If you need to build targeting an api level below 21, you should use an older version of python-for-android (<=0.7.1). -Be aware that this project is in constant development so, as per time of writing, -you should use a minimum on Android's NDK r19, and ``we recommend using NDK r19b``. -This is because the toolchains installed by -default with the NDK can be used *in-place* and the python-for-android project -has been adapted for that feature. Also be aware that more recent versions of the -Android's NDK may not work. - -Those mentioned changes has been done this way to make easier the transition -between python3 and python2. We will slowly phase out python2 support -towards 2020...so...if you are using python2 in your projects you should -consider migrating it into python3. +On March of 2020 we dropped support for creating apps that use Python 2. The latest +python-for-android release that supported building Python 2 was version 2019.10.6. ## Contributors diff --git a/ci/constants.py b/ci/constants.py index 427d1de4d4..8b79bb5e54 100644 --- a/ci/constants.py +++ b/ci/constants.py @@ -2,59 +2,12 @@ class TargetPython(Enum): - python2 = 0 python3 = 2 # recipes that currently break the build # a recipe could be broken for a target Python and not for the other, # hence we're maintaining one list per Python target -BROKEN_RECIPES_PYTHON2 = set([ - # pythonhelpers.h:12:18: fatal error: string: No such file or directory - 'atom', - # https://github.com/kivy/python-for-android/issues/550 - 'audiostream', - 'brokenrecipe', - 'evdev', - # distutils.errors.DistutilsError - # Could not find suitable distribution for Requirement.parse('cython') - 'ffpyplayer', - 'flask', - 'groestlcoin_hash', - # https://github.com/kivy/python-for-android/issues/1354 - 'kiwisolver', - 'libmysqlclient', - 'libsecp256k1', - 'libtribler', - 'ndghttpsclient', - 'm2crypto', - # ImportError: No module named setuptools - 'netifaces', - 'Pillow', - # depends on cffi that still seems to have compilation issues - 'protobuf_cpp', - 'xeddsa', - 'x3dh', - 'pynacl', - 'doubleratchet', - 'omemo', - # requires `libpq-dev` system dependency e.g. for `pg_config` binary - 'psycopg2', - # most likely some setup in the Docker container, because it works in host - 'pyjnius', 'pyopenal', - 'pyproj', - 'pysdl2', - 'pyzmq', - 'secp256k1', - 'shapely', - # mpmath package with a version >= 0.19 required - 'sympy', - 'twisted', - 'vlc', - 'websocket-client', - 'zeroconf', - 'zope', -]) BROKEN_RECIPES_PYTHON3 = set([ 'brokenrecipe', # enum34 is not compatible with Python 3.6 standard library @@ -76,11 +29,10 @@ class TargetPython(Enum): ]) BROKEN_RECIPES = { - TargetPython.python2: BROKEN_RECIPES_PYTHON2, TargetPython.python3: BROKEN_RECIPES_PYTHON3, } # recipes that were already built will be skipped CORE_RECIPES = set([ 'pyjnius', 'kivy', 'openssl', 'requests', 'sqlite3', 'setuptools', - 'numpy', 'android', 'hostpython2', 'hostpython3', 'python2', 'python3', + 'numpy', 'android', 'hostpython3', 'python3', ]) diff --git a/ci/rebuild_updated_recipes.py b/ci/rebuild_updated_recipes.py index 54f62ac768..cacc328776 100755 --- a/ci/rebuild_updated_recipes.py +++ b/ci/rebuild_updated_recipes.py @@ -19,15 +19,12 @@ [ERROR]: Didn't find any valid dependency graphs. [ERROR]: This means that some of your requirements pull in conflicting dependencies. - only rebuilds on sdl2 bootstrap -- supports mainly python3 with fallback to python2 """ import sh import os from pythonforandroid.build import Context from pythonforandroid import logger -from pythonforandroid.graph import get_recipe_order_and_bootstrap from pythonforandroid.toolchain import current_directory -from pythonforandroid.util import BuildInterruptingException from pythonforandroid.recipe import Recipe from ci.constants import TargetPython, CORE_RECIPES, BROKEN_RECIPES @@ -55,18 +52,15 @@ def build(target_python, requirements): """ if not requirements: return - testapp = 'setup_testapp_python2.py' android_sdk_home = os.environ['ANDROID_SDK_HOME'] android_ndk_home = os.environ['ANDROID_NDK_HOME'] - if target_python == TargetPython.python3: - testapp = 'setup_testapp_python3_sqlite_openssl.py' requirements.add(target_python.name) requirements = ','.join(requirements) logger.info('requirements: {}'.format(requirements)) - with current_directory('testapps/'): + with current_directory('testapps/on_device_unit_tests/'): # iterates to stream the output for line in sh.python( - testapp, 'apk', '--sdk-dir', android_sdk_home, + 'setup.py', 'apk', '--sdk-dir', android_sdk_home, '--ndk-dir', android_ndk_home, '--requirements', requirements, _err_to_out=True, _iter=True): print(line) @@ -91,16 +85,6 @@ def main(): 'removed {} from recipes because deleted'.format(recipe_name) ) - # forces the default target - recipes_and_target = recipes | set([target_python.name]) - try: - build_order, python_modules, bs = get_recipe_order_and_bootstrap( - context, recipes_and_target, None) - except BuildInterruptingException: - # fallback to python2 if default target is not compatible - logger.info('incompatible with {}'.format(target_python.name)) - target_python = TargetPython.python2 - logger.info('falling back to {}'.format(target_python.name)) # removing the known broken recipe for the given target broken_recipes = BROKEN_RECIPES[target_python] recipes -= broken_recipes diff --git a/doc/source/buildoptions.rst b/doc/source/buildoptions.rst index 5d80020728..19360ff2fa 100644 --- a/doc/source/buildoptions.rst +++ b/doc/source/buildoptions.rst @@ -8,38 +8,14 @@ This page contains instructions for using different build options. Python versions --------------- -python2 -~~~~~~~ - -Select this by adding it in your requirements, e.g. ``--requirements=python2``. - -This option builds Python 2.7.2 for your selected Android -architecture. There are no special requirements, all the building is -done locally. - - -python3 -~~~~~~~ - -Python3 is supported in two ways. The default method uses CPython 3.7+ -and works with any recent version of the Android NDK. - -Select Python 3 by adding it to your requirements, -e.g. ``--requirements=python3``. - -.. note:: ctypes is not included automatically, if you would like to use it - then add libffi to your requirements, - e.g. ``--requirements=kivy,libffi,python3``. - - -CrystaX python3 -~~~~~~~~~~~~~~~ +python-for-android supports using Python 3.7 or higher. To explicitly select a Python +version in your requirements, use e.g. ``--requirements=python3==3.7.1,hostpython3==3.7.1``. -python-for-android no longer supports building for Python 3 using the CrystaX -NDK. Instead, use the python3 recipe, which can be built using the normal -Google NDK. +The last python-for-android version supporting Python2 was `v2019.10.06 + `__ -.. note:: The last python-for-android version supporting CrystaX was `0.7.0. +Python-for-android no longer supports building for Python 3 using the CrystaX +NDK. The last python-for-android version supporting CrystaX was `0.7.0. `__ .. _bootstrap_build_options: @@ -60,7 +36,7 @@ sdl2 ~~~~ Use this with ``--bootstrap=sdl2``, or just include the -``sdl2`` recipe, e.g. ``--requirements=sdl2,python2``. +``sdl2`` recipe, e.g. ``--requirements=sdl2,python3``. SDL2 is a popular cross-platform depelopment library, particularly for games. It has its own Android project support, which @@ -120,7 +96,7 @@ webview ~~~~~~~ You can use this with ``--bootstrap=webview``, or include the -``webviewjni`` recipe, e.g. ``--requirements=webviewjni,python2``. +``webviewjni`` recipe, e.g. ``--requirements=webviewjni,python3``. The webview bootstrap gui is, per the name, a WebView displaying a webpage, but this page is hosted on the device via a Python diff --git a/doc/source/contribute.rst b/doc/source/contribute.rst index 1de0883c4c..4796c7e411 100644 --- a/doc/source/contribute.rst +++ b/doc/source/contribute.rst @@ -75,9 +75,6 @@ Release checklist - `python3 setup_testapp_python3_sqlite_openssl.py apk` - [ ] `armeabi-v7a` - [ ] `arm64-v8a` - - `python3 setup_testapp_python2.py apk` - - [ ] `armeabi-v7a` - - [ ] `arm64-v8a` - [ ] Check that the version number is correct diff --git a/doc/source/distutils.rst b/doc/source/distutils.rst index 2ee999ffef..74055d0f7a 100644 --- a/doc/source/distutils.rst +++ b/doc/source/distutils.rst @@ -25,9 +25,6 @@ This however has these caveats: or even cause build errors. (Sorry, our internal processing is just not smart enough to honor them properly at this point) -- If you don't use Python 3 per default, you still need to specify - ``--requirements python2`` (without any additional dependencies) - - The dependency analysis at the start may be quite slow and delay your build @@ -117,7 +114,7 @@ store them in setup.py by passing the ``options`` parameter to from setuptools import find_packages options = {'apk': {'debug': None, # use None for arguments that don't pass a value - 'requirements': 'sdl2,pyjnius,kivy,python2', + 'requirements': 'sdl2,pyjnius,kivy,python3', 'android-api': 19, 'ndk-dir': '/path/to/ndk', 'dist-name': 'bdisttest', diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 07a3fc6523..425a7ea2c2 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -185,9 +185,7 @@ an `.apk` file. *Compatibility notes:* -- 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! +- Python 2 is no longer supported by python-for-android. The last release supporting Python 2 was v2019.10.06. Build a WebView application diff --git a/doc/source/recipes.rst b/doc/source/recipes.rst index 8702c8d259..c3a97aab4f 100644 --- a/doc/source/recipes.rst +++ b/doc/source/recipes.rst @@ -41,7 +41,7 @@ The basic declaration of a recipe is as follows:: patches = ['some_fix.patch'] # Paths relative to the recipe dir depends = ['kivy', 'sdl2'] # These are just examples - conflicts = ['python2'] + conflicts = ['generickndkbuild'] recipe = YourRecipe() @@ -61,15 +61,15 @@ when the recipe is imported. The actual build process takes place via three core methods:: def prebuild_arch(self, arch): - super(YourRecipe, self).prebuild_arch(arch) + super().prebuild_arch(arch) # Do any pre-initialisation def build_arch(self, arch): - super(YourRecipe, self).build_arch(arch) + super().build_arch(arch) # Do the main recipe build def postbuild_arch(self, arch): - super(YourRecipe, self).build_arch(arch) + super().build_arch(arch) # Do any clearing up These methods are always run in the listed order; prebuild, then @@ -86,7 +86,7 @@ context manager defined in toolchain.py:: from pythonforandroid.toolchain import current_directory def build_arch(self, arch): - super(YourRecipe, self).build_arch(arch) + super().build_arch(arch) with current_directory(self.get_build_dir(arch.arch)): with open('example_file.txt', 'w') as fileh: fileh.write('This is written to a file within the build dir') @@ -178,7 +178,7 @@ environment for any processes that you call. It is convenient to do this using the ``sh`` module as follows:: def build_arch(self, arch): - super(YourRecipe, self).build_arch(arch) + super().build_arch(arch) env = self.get_recipe_env(arch) sh.echo('$PATH', _env=env) # Will print the PATH entry from the # env dict @@ -196,7 +196,7 @@ the following when compiling for SDL2, in order to tell Kivy what backend to use:: def get_recipe_env(self, arch): - env = super(KivySDL2Recipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) env['USE_SDL2'] = '1' env['KIVY_SDL2_PATH'] = ':'.join([ @@ -256,7 +256,7 @@ for the Vispy module:: version = 'master' url = 'https://github.com/vispy/vispy/archive/{version}.zip' - depends = ['python2', 'numpy'] + depends = ['python3', 'numpy'] site_packages_name = 'vispy' @@ -272,7 +272,7 @@ Python installation. For reference, the code that accomplishes this is the following:: def build_arch(self, arch): - super(PythonRecipe, self).build_arch(arch) + super().build_arch(arch) self.install_python_package() def install_python_package(self): @@ -426,7 +426,7 @@ overrides if you do not use them:: url = 'http://example.com/example-{version}.tar.gz' # {version} will be replaced with self.version when downloading - depends = ['python2', 'numpy'] # A list of any other recipe names + depends = ['python3', 'numpy'] # A list of any other recipe names # that must be built before this # one @@ -434,7 +434,7 @@ overrides if you do not use them:: # alongside this one def get_recipe_env(self, arch): - env = super(YourRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) # Manipulate the env here if you want return env @@ -444,19 +444,19 @@ overrides if you do not use them:: return True def prebuild_arch(self, arch): - super(YourRecipe, self).prebuild_arch(self) + super().prebuild_arch(self) # Do any extra prebuilding you want, e.g.: self.apply_patch('path/to/patch.patch') def build_arch(self, arch): - super(YourRecipe, self).build_arch(self) + super().build_arch(self) # Build the code. Make sure to use the right build dir, e.g. with current_directory(self.get_build_dir(arch.arch)): sh.ls('-lathr') # Or run some commands that actually do # something def postbuild_arch(self, arch): - super(YourRecipe, self).prebuild_arch(self) + super().prebuild_arch(self) # Do anything you want after the build, e.g. deleting # unnecessary files such as documentation diff --git a/doc/source/testing_pull_requests.rst b/doc/source/testing_pull_requests.rst index 603828db77..f77748e336 100644 --- a/doc/source/testing_pull_requests.rst +++ b/doc/source/testing_pull_requests.rst @@ -43,11 +43,11 @@ Fetch the pull request by number For the example, we will use `1901` for the example) and the pull request branch that we will use is `feature-fix-numpy`, then you will use a variation of the following git command: -`git fetch origin pull/<#>/head:`, eg.:: +`git fetch origin pull/<#>/head:`, e.g.: - .. codeblock:: bash +.. code-block:: bash - git fetch upstream pull/1901/head:feature-fix-numpy + git fetch upstream pull/1901/head:feature-fix-numpy .. note:: Notice that we fetch from `upstream`, since that is the original project, where the pull request is supposed to be @@ -55,16 +55,16 @@ of the following git command: .. tip:: The amount of work of some users maybe worth it to add his remote to your fork's git configuration, to do so with the imaginary github user `Obi-Wan Kenobi` which nickname is `obiwankenobi`, you - will do:: + will do: - .. codeblock:: bash + .. code-block:: bash git remote add obiwankenobi https://github.com/obiwankenobi/python-for-android.git And to fetch the pull request branch that we put as example, you - would do:: + would do: - .. codeblock:: bash + .. code-block:: bash git fetch obiwankenobi git checkout obiwankenobi/feature-fix-numpy @@ -74,9 +74,9 @@ Clone the pull request branch from the user's fork -------------------------------------------------- Sometimes you may prefer to use directly the fork of the user, so you will get the nickname of the user who created the pull request, let's take the same -imaginary user than before `obiwankenobi`:: +imaginary user than before `obiwankenobi`: - .. codeblock:: bash + .. code-block:: bash git clone -b feature-fix-numpy \ --single-branch \ @@ -103,30 +103,31 @@ Using python-for-android commands directly from the pull request files ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Enter inside the directory of the cloned repository in the above - step and run p4a command with proper args, eg:: - - .. codeblock:: bash - - cd p4a-feature-fix-numpy - PYTHONPATH=. python3 -m pythonforandroid.toolchain apk \ - --private=testapps/testapp_sqlite_openssl \ - --dist-name=dist_test_app_python3_libs \ - --package=org.kivy \ - --name=test_app_python3_sqlite_openssl \ - --version=0.1 \ - --requirements=requests,peewee,sdl2,pyjnius,kivy,python3 \ - --ndk-dir=/media/DEVEL/Android/android-ndk-r20 \ - --sdk-dir=/media/DEVEL/Android/android-sdk-linux \ - --android-api=27 \ - --arch=arm64-v8a \ - --permission=INTERNET \ - --debug + step and run p4a command with proper args, e.g. (to test an modified + `pycryptodome` recipe) + +.. code-block:: bash + + cd p4a-feature-fix-numpy + PYTHONPATH=. python3 -m pythonforandroid.toolchain apk \ + --private=testapps/on_device_unit_tests/test_app \ + --dist-name=dist_unit_tests_app_pycryptodome \ + --package=org.kivy \ + --name=unit_tests_app_pycryptodome \ + --version=0.1 \ + --requirements=sdl2,pyjnius,kivy,python3,pycryptodome \ + --ndk-dir=/media/DEVEL/Android/android-ndk-r20 \ + --sdk-dir=/media/DEVEL/Android/android-sdk-linux \ + --android-api=27 \ + --arch=arm64-v8a \ + --permission=VIBRATE \ + --debug Things that you should know: - - The example above will build an testapp we will make use of the files of - the testapp named `testapp_sqlite_openssl.py` but we don't use the setup + - The example above will build an test app we will make use of the files of + the `on device unit tests` test app but we don't use the setup file to build it so we must tell python-for-android what we want via arguments - be sure to at least edit the following arguments when running the above @@ -152,38 +153,37 @@ Installing python-for-android using the github's branch of the pull request ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Enter inside the directory of the cloned repository mentioned in - `Common steps` and install it via pip, eg.:: + `Common steps` and install it via pip, e.g.: - .. codeblock:: bash +.. code-block:: bash - cd p4a-feature-fix-numpy - pip3 install . --upgrade --user + cd p4a-feature-fix-numpy + pip3 install . --upgrade --user -- Now, go inside the `testapps` directory (we assume that you still are inside - the cloned repository):: +- Now, go inside the `testapps/on_device_unit_tests` directory (we assume that + you still are inside the cloned repository) - .. codeblock:: bash +.. code-block:: bash - cd testapps + cd testapps/on_device_unit_tests - Run the build of the apk via the freshly installed copy of python-for-android - by running a similar command than below:: + by running a similar command than below - .. code-block:: bash +.. code-block:: bash - python3 setup_testapp_python3_sqlite_openssl.py apk \ - --ndk-dir=/media/DEVEL/Android/android-ndk-r20 \ - --sdk-dir=/media/DEVEL/Android/android-sdk-linux \ - --android-api=27 \ - --arch=arm64-v8a \ - --debug + python3 setup.py apk \ + --ndk-dir=/media/DEVEL/Android/android-ndk-r20 \ + --sdk-dir=/media/DEVEL/Android/android-sdk-linux \ + --android-api=27 \ + --arch=arm64-v8a \ + --debug Things that you should know: - In the example above, we override some variables that are set in - `setup_testapp_python3_sqlite_openssl.py`, you could also override them - by editing this file + `setup.py`, you could also override them by editing this file - be sure to at least edit the following arguments when running the above command, since the default set in there it's unlikely that match your installation: @@ -207,7 +207,7 @@ Using buildozer with a custom app p4a.source_dir = /home/user/p4a_pull_requests/p4a-feature-fix-numpy -- Run you buildozer command as usual, eg.:: +- Run you buildozer command as usual, e.g.:: buildozer android debug p4a --dist-name=dist-test-feature-fix-numpy @@ -223,4 +223,4 @@ Using buildozer with a custom app .. tip:: this method it's useful for developing pull requests since you can edit `p4a.source_dir` to point to your python-for-android fork and you can test any branch you want only switching branches with: - `git checkout ` from inside your python-for-android fork \ No newline at end of file + `git checkout ` from inside your python-for-android fork diff --git a/doc/source/troubleshooting.rst b/doc/source/troubleshooting.rst index 4277d51bbf..5974718308 100644 --- a/doc/source/troubleshooting.rst +++ b/doc/source/troubleshooting.rst @@ -132,8 +132,8 @@ AttributeError: 'Context' object has no attribute 'hostpython' This is a known bug in some releases. To work around it, add your python requirement explicitly, -e.g. :code:`--requirements=python2,kivy`. This also applies when using -buildozer, in which case add python2 to your buildozer.spec requirements. +e.g. :code:`--requirements=python3,kivy`. This also applies when using +buildozer, in which case add python3 to your buildozer.spec requirements. linkname too long ~~~~~~~~~~~~~~~~~ diff --git a/pythonforandroid/__init__.py b/pythonforandroid/__init__.py index 114df6901c..9481240230 100644 --- a/pythonforandroid/__init__.py +++ b/pythonforandroid/__init__.py @@ -1 +1 @@ -__version__ = '2020.03.30' +__version__ = '2020.04.29' diff --git a/pythonforandroid/archs.py b/pythonforandroid/archs.py index 50dce55cb9..f1c8e1121b 100644 --- a/pythonforandroid/archs.py +++ b/pythonforandroid/archs.py @@ -8,7 +8,7 @@ from pythonforandroid.util import BuildInterruptingException, build_platform -class Arch(object): +class Arch: toolchain_prefix = None '''The prefix for the toolchain dir in the NDK.''' @@ -46,7 +46,6 @@ class Arch(object): ] def __init__(self, ctx): - super(Arch, self).__init__() self.ctx = ctx # Allows injecting additional linker paths used by any recipe. @@ -309,6 +308,6 @@ class ArchAarch_64(Arch): # NDK r19 build system, because it seems that we don't need it anymore, # do we need them? # def get_env(self, with_flags_in_cc=True): - # env = super(ArchAarch_64, self).get_env(with_flags_in_cc) + # env = super().get_env(with_flags_in_cc) # env['EXTRA_CFLAGS'] = self.arch_cflags[-1] # return env diff --git a/pythonforandroid/bdistapk.py b/pythonforandroid/bdistapk.py index ea1b78ca2f..d4b2c7953a 100644 --- a/pythonforandroid/bdistapk.py +++ b/pythonforandroid/bdistapk.py @@ -1,4 +1,3 @@ -from __future__ import print_function from setuptools import Command import sys @@ -15,16 +14,16 @@ def argv_contains(t): return False -class BdistAPK(Command): - description = 'Create an APK with python-for-android' +class Bdist(Command): user_options = [] + package_type = None 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') + option_dict = self.distribution.get_option_dict(self.package_type) # This is a hack, we probably aren't supposed to loop through # the option_dict so early because distutils does exactly the @@ -35,7 +34,7 @@ def initialize_options(self): def finalize_options(self): - setup_options = self.distribution.get_option_dict('apk') + setup_options = self.distribution.get_option_dict(self.package_type) for (option, (source, value)) in setup_options.items(): if source == 'command line': continue @@ -76,7 +75,7 @@ def run(self): self.prepare_build_dir() from pythonforandroid.entrypoints import main - sys.argv[1] = 'apk' + sys.argv[1] = self.package_type main() def prepare_build_dir(self): @@ -128,6 +127,22 @@ def prepare_build_dir(self): ) +class BdistAPK(Bdist): + """ + distutil command handler for 'apk' + """ + description = 'Create an APK with python-for-android' + package_type = 'apk' + + +class BdistAAR(Bdist): + """ + distutil command handler for 'aar' + """ + description = 'Create an AAR with python-for-android' + package_type = 'aar' + + 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 diff --git a/pythonforandroid/bootstrap.py b/pythonforandroid/bootstrap.py index 03925a3e01..97c62e01a9 100755 --- a/pythonforandroid/bootstrap.py +++ b/pythonforandroid/bootstrap.py @@ -14,7 +14,7 @@ from pythonforandroid.recipe import Recipe -def copy_files(src_root, dest_root, override=True): +def copy_files(src_root, dest_root, override=True, symlink=False): for root, dirnames, filenames in walk(src_root): for filename in filenames: subdir = normpath(root.replace(src_root, "")) @@ -29,7 +29,10 @@ def copy_files(src_root, dest_root, override=True): if override and os.path.exists(dest_file): os.unlink(dest_file) if not os.path.exists(dest_file): - shutil.copy(src_file, dest_file) + if symlink: + os.symlink(src_file, dest_file) + else: + shutil.copy(src_file, dest_file) else: os.makedirs(dest_file) @@ -63,7 +66,7 @@ def rank_bootstrap(bootstrap): return 1 -class Bootstrap(object): +class Bootstrap: '''An Android project template, containing recipe stuff for compilation and templated fields for APK info. ''' @@ -78,7 +81,7 @@ class Bootstrap(object): distribution = None # All bootstraps should include Python in some way: - recipe_depends = [("python2", "python3"), 'android'] + recipe_depends = ['python3', 'android'] can_be_chosen_automatically = True '''Determines whether the bootstrap can be chosen as one that @@ -109,7 +112,7 @@ def check_recipe_choices(self): and optional dependencies are being used, and returns a list of these.''' recipes = [] - built_recipes = self.ctx.recipe_build_order + built_recipes = self.ctx.recipe_build_order or [] for recipe in self.recipe_depends: if isinstance(recipe, (tuple, list)): for alternative in recipe: @@ -137,21 +140,27 @@ def name(self): modname = self.__class__.__module__ return modname.split(".", 2)[-1] + def get_bootstrap_dirs(self): + """get all bootstrap directories, following the MRO path""" + + # get all bootstrap names along the __mro__, cutting off Bootstrap and object + classes = self.__class__.__mro__[:-2] + bootstrap_names = [cls.name for cls in classes] + ['common'] + bootstrap_dirs = [ + join(self.ctx.root_dir, 'bootstraps', bootstrap_name) + for bootstrap_name in reversed(bootstrap_names) + ] + return bootstrap_dirs + def prepare_build_dir(self): - '''Ensure that a build dir exists for the recipe. This same single - dir will be used for building all different archs.''' + """Ensure that a build dir exists for the recipe. This same single + dir will be used for building all different archs.""" + bootstrap_dirs = self.get_bootstrap_dirs() + # now do a cumulative copy of all bootstrap dirs self.build_dir = self.get_build_dir() - self.common_dir = self.get_common_dir() - copy_files(join(self.bootstrap_dir, 'build'), self.build_dir) - copy_files(join(self.common_dir, 'build'), self.build_dir, - override=False) - if self.ctx.symlink_java_src: - info('Symlinking java src instead of copying') - shprint(sh.rm, '-r', join(self.build_dir, 'src')) - shprint(sh.mkdir, join(self.build_dir, 'src')) - for dirn in listdir(join(self.bootstrap_dir, 'build', 'src')): - shprint(sh.ln, '-s', join(self.bootstrap_dir, 'build', 'src', dirn), - join(self.build_dir, 'src')) + for bootstrap_dir in bootstrap_dirs: + copy_files(join(bootstrap_dir, 'build'), self.build_dir, symlink=self.ctx.symlink_bootstrap_files) + with current_directory(self.build_dir): with open('project.properties', 'w') as fileh: fileh.write('target=android-{}'.format(self.ctx.android_api)) @@ -195,7 +204,7 @@ def get_usable_bootstraps_for_recipes(cls, recipes, ctx): # Check if the bootstap's dependencies have an internal conflict: for recipe in possible_dependencies: recipe = Recipe.get_recipe(recipe, ctx) - if any([conflict in recipes for conflict in recipe.conflicts]): + if any(conflict in recipes for conflict in recipe.conflicts): ok = False break # Check if bootstrap's dependencies conflict with chosen @@ -207,8 +216,8 @@ def get_usable_bootstraps_for_recipes(cls, recipes, ctx): conflicts = [] else: conflicts = recipe.conflicts - if any([conflict in possible_dependencies - for conflict in conflicts]): + if any(conflict in possible_dependencies + for conflict in conflicts): ok = False break if ok and bs not in acceptable_bootstraps: @@ -294,15 +303,16 @@ def distribute_libs(self, arch, src_dirs, wildcard='*', dest_dir="libs"): tgt_dir = join(dest_dir, arch.arch) ensure_dir(tgt_dir) for src_dir in src_dirs: - for lib in glob.glob(join(src_dir, wildcard)): - shprint(sh.cp, '-a', lib, tgt_dir) + libs = glob.glob(join(src_dir, wildcard)) + if libs: + shprint(sh.cp, '-a', *libs, tgt_dir) def distribute_javaclasses(self, javaclass_dir, dest_dir="src"): '''Copy existing javaclasses from build dir to current dist dir.''' info('Copying java files') ensure_dir(dest_dir) - for filename in glob.glob(javaclass_dir): - shprint(sh.cp, '-a', filename, dest_dir) + filenames = glob.glob(javaclass_dir) + shprint(sh.cp, '-a', *filenames, dest_dir) def distribute_aars(self, arch): '''Process existing .aar bundles and copy to current dist dir.''' @@ -335,8 +345,7 @@ def _unpack_aar(self, aar, arch): debug(" to {}".format(so_tgt_dir)) ensure_dir(so_tgt_dir) so_files = glob.glob(join(so_src_dir, '*.so')) - for f in so_files: - shprint(sh.cp, '-a', f, so_tgt_dir) + shprint(sh.cp, '-a', *so_files, so_tgt_dir) def strip_libraries(self, arch): info('Stripping libraries') diff --git a/pythonforandroid/bootstraps/common/build/build.py b/pythonforandroid/bootstraps/common/build/build.py index 07a5be01d8..8eb1a6fe0e 100644 --- a/pythonforandroid/bootstraps/common/build/build.py +++ b/pythonforandroid/bootstraps/common/build/build.py @@ -1,6 +1,4 @@ -#!/usr/bin/env python2.7 - -from __future__ import print_function +#!/usr/bin/env python3 import json from os.path import ( @@ -16,7 +14,6 @@ import tarfile import tempfile import time -from zipfile import ZipFile from distutils.version import LooseVersion from fnmatch import fnmatch @@ -78,10 +75,6 @@ def get_bootstrap_name(): # pyc/py if PYTHON is not None: BLACKLIST_PATTERNS.append('*.py') - if PYTHON_VERSION and int(PYTHON_VERSION[0]) == 2: - # we only blacklist `.pyc` for python2 because in python3 the compiled - # extension is `.pyc` (.pyo files not exists for python >= 3.6) - BLACKLIST_PATTERNS.append('*.pyc') WHITELIST_PATTERNS = [] if get_bootstrap_name() in ('sdl2', 'webview', 'service_only'): @@ -156,48 +149,6 @@ def listfiles(d): yield fn -def make_python_zip(): - ''' - Search for all the python related files, and construct the pythonXX.zip - According to - # http://randomsplat.com/id5-cross-compiling-python-for-embedded-linux.html - site-packages, config and lib-dynload will be not included. - ''' - - if not exists('private'): - print('No compiled python is present to zip, skipping.') - return - - global python_files - d = realpath(join('private', 'lib', 'python2.7')) - - def select(fn): - if is_blacklist(fn): - return False - fn = realpath(fn) - assert(fn.startswith(d)) - fn = fn[len(d):] - if (fn.startswith('/site-packages/') - or fn.startswith('/config/') - or fn.startswith('/lib-dynload/') - or fn.startswith('/libpymodules.so')): - return False - return fn - - # get a list of all python file - python_files = [x for x in listfiles(d) if select(x)] - - # create the final zipfile - zfn = join('private', 'lib', 'python27.zip') - zf = ZipFile(zfn, 'w') - - # put all the python files in it - for fn in python_files: - afn = fn[len(d):] - zf.write(fn, afn) - zf.close() - - def make_tar(tfn, source_dirs, ignore_path=[], optimize_python=True): ''' Make a zip file `fn` from the contents of source_dis. @@ -274,7 +225,7 @@ def compile_dir(dfn, optimize_python=True): def make_package(args): # If no launcher is specified, require a main.py/main.pyo: if (get_bootstrap_name() != "sdl" or args.launcher is None) and \ - get_bootstrap_name() != "webview": + get_bootstrap_name() not in ["webview", "service_library"]: # (webview doesn't need an entrypoint, apparently) if args.private is None or ( not exists(join(realpath(args.private), 'main.py')) and @@ -292,10 +243,6 @@ def make_package(args): try_unlink(join(assets_dir, 'private.mp3')) ensure_dir(assets_dir) - # In order to speedup import and initial depack, - # construct a python27.zip - make_python_zip() - # Add extra environment variable file into tar-able directory: env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-") with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f: @@ -506,7 +453,8 @@ def make_package(args): "args": args, "service": service, "service_names": service_names, - "android_api": android_api + "android_api": android_api, + "debug": "debug" in args.build_mode, } if get_bootstrap_name() == "sdl2": render_args["url_scheme"] = url_scheme @@ -529,7 +477,9 @@ def make_package(args): aars=aars, jars=jars, android_api=android_api, - build_tools_version=build_tools_version + build_tools_version=build_tools_version, + debug_build="debug" in args.build_mode, + is_library=(get_bootstrap_name() == 'service_library'), ) # ant build templates @@ -594,7 +544,7 @@ def make_package(args): raise e -def parse_args(args=None): +def parse_args_and_make_package(args=None): global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON # Get the default minsdk, equal to the NDK API that this dist is built against @@ -712,6 +662,10 @@ def parse_args(args=None): default=join(curdir, 'whitelist.txt'), help=('Use a whitelist file to prevent blacklisting of ' 'file in the final APK')) + ap.add_argument('--release', dest='build_mode', action='store_const', + const='release', default='debug', + help='Build your app as a non-debug release build. ' + '(Disables gdb debugging among other things)') ap.add_argument('--add-jar', dest='add_jar', action='append', help=('Add a Java .jar to the libs, so you can access its ' 'classes with pyjnius. You can specify this ' @@ -858,4 +812,4 @@ def _read_configuration(): if __name__ == "__main__": - parse_args() + parse_args_and_make_package() diff --git a/pythonforandroid/bootstraps/common/build/jni/application/src/start.c b/pythonforandroid/bootstraps/common/build/jni/application/src/start.c index 24297accdb..f57098069f 100644 --- a/pythonforandroid/bootstraps/common/build/jni/application/src/start.c +++ b/pythonforandroid/bootstraps/common/build/jni/application/src/start.c @@ -189,17 +189,6 @@ int main(int argc, char *argv[]) { } Py_Initialize(); - -#if PY_MAJOR_VERSION < 3 - // Can't Py_SetPath in python2 but we can set PySys_SetPath, which must - // be applied after Py_Initialize rather than before like Py_SetPath - #if PY_MICRO_VERSION >= 15 - // Only for python native-build - PySys_SetPath(paths); - #endif - PySys_SetArgv(argc, argv); -#endif - LOGP("Initialized python"); /* ensure threads will work. @@ -285,7 +274,7 @@ int main(int argc, char *argv[]) { entrypoint[strlen(env_entrypoint) - 1] = '\0'; LOGP(entrypoint); if (!file_exists(entrypoint)) { - LOGP("Entrypoint not found (.pyc/.pyo, fallback on .py), abort"); + LOGP("Entrypoint not found (.pyc, fallback on .py), abort"); return -1; } } else { @@ -309,7 +298,7 @@ int main(int argc, char *argv[]) { strcpy(entrypoint, env_entrypoint); } } else { - LOGP("Entrypoint have an invalid extension (must be .py or .pyc/.pyo), abort."); + LOGP("Entrypoint have an invalid extension (must be .py or .pyc), abort."); return -1; } // LOGP("Entrypoint is:"); @@ -330,8 +319,7 @@ int main(int argc, char *argv[]) { ret = 1; PyErr_Print(); /* This exits with the right code if SystemExit. */ PyObject *f = PySys_GetObject("stdout"); - if (PyFile_WriteString( - "\n", f)) /* python2 used Py_FlushLine, but this no longer exists */ + if (PyFile_WriteString("\n", f)) PyErr_Clear(); } diff --git a/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonUtil.java b/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonUtil.java index 2bb1cf3607..f3cf0dbf4e 100644 --- a/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonUtil.java +++ b/pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonUtil.java @@ -36,10 +36,10 @@ protected static ArrayList getLibraries(File libsDir) { addLibraryIfExists(libsList, "ffi", libsDir); addLibraryIfExists(libsList, "ssl.*", libsDir); addLibraryIfExists(libsList, "crypto.*", libsDir); - libsList.add("python2.7"); libsList.add("python3.5m"); libsList.add("python3.6m"); libsList.add("python3.7m"); + libsList.add("python3.8m"); libsList.add("main"); return libsList; } @@ -60,7 +60,7 @@ public static void loadLibraries(File filesDir, File libsDir) { // load, and it has failed, give a more // general error Log.v(TAG, "Library loading error: " + e.getMessage()); - if (lib.startsWith("python3.7") && !foundPython) { + if (lib.startsWith("python3.8") && !foundPython) { throw new java.lang.RuntimeException("Could not load any libpythonXXX.so"); } else if (lib.startsWith("python")) { continue; diff --git a/pythonforandroid/bootstraps/common/build/templates/build.tmpl.gradle b/pythonforandroid/bootstraps/common/build/templates/build.tmpl.gradle index fe78dda5a8..7b6ba8390b 100644 --- a/pythonforandroid/bootstraps/common/build/templates/build.tmpl.gradle +++ b/pythonforandroid/bootstraps/common/build/templates/build.tmpl.gradle @@ -22,7 +22,11 @@ allprojects { } } +{% if is_library %} +apply plugin: 'com.android.library' +{% else %} apply plugin: 'com.android.application' +{% endif %} android { compileSdkVersion {{ android_api }} @@ -34,15 +38,22 @@ android { versionName '{{ args.version }}' } - {% if args.sign -%} - signingConfigs { - release { - storeFile file(System.getenv("P4A_RELEASE_KEYSTORE")) - keyAlias System.getenv("P4A_RELEASE_KEYALIAS") - storePassword System.getenv("P4A_RELEASE_KEYSTORE_PASSWD") - keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD") - } - } + {% if debug_build -%} + packagingOptions { + doNotStrip '**/*.so' + } + {%- endif %} + + {% if args.sign -%} + signingConfigs { + release { + storeFile file(System.getenv("P4A_RELEASE_KEYSTORE")) + keyAlias System.getenv("P4A_RELEASE_KEYALIAS") + storePassword System.getenv("P4A_RELEASE_KEYSTORE_PASSWD") + keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD") + } + } + {%- endif %} {% if args.packaging_options -%} diff --git a/pythonforandroid/bootstraps/sdl2/__init__.py b/pythonforandroid/bootstraps/sdl2/__init__.py index dde5f7f79b..2b97552dbc 100644 --- a/pythonforandroid/bootstraps/sdl2/__init__.py +++ b/pythonforandroid/bootstraps/sdl2/__init__.py @@ -47,9 +47,10 @@ def run_distribute(self): with open('blacklist.txt', 'a') as fileh: fileh.write('\nsqlite3/*\nlib-dynload/_sqlite3.so\n') - self.strip_libraries(arch) + if not self.ctx.build_as_debuggable: + self.strip_libraries(arch) self.fry_eggs(site_packages_dir) - super(SDL2GradleBootstrap, self).run_distribute() + super().run_distribute() bootstrap = SDL2GradleBootstrap() diff --git a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonUtil.java b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonUtil.java index 460f704190..84540f55a8 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonUtil.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonUtil.java @@ -40,7 +40,6 @@ protected static ArrayList getLibraries(File libsDir) { libsList.add("SDL2_ttf"); addLibraryIfExists(libsList, "ssl.*", libsDir); addLibraryIfExists(libsList, "crypto.*", libsDir); - libsList.add("python2.7"); libsList.add("python3.5m"); libsList.add("python3.6m"); libsList.add("python3.7m"); @@ -76,20 +75,6 @@ public static void loadLibraries(File filesDir, File libsDir) { } } - try { - System.load(filesDirPath + "/lib/python2.7/lib-dynload/_io.so"); - System.load(filesDirPath + "/lib/python2.7/lib-dynload/unicodedata.so"); - } catch(UnsatisfiedLinkError e) { - Log.v(TAG, "Failed to load _io.so or unicodedata.so...but that's okay."); - } - - try { - // System.loadLibrary("ctypes"); - System.load(filesDirPath + "/lib/python2.7/lib-dynload/_ctypes.so"); - } catch(UnsatisfiedLinkError e) { - Log.v(TAG, "Unsatisfied linker when loading ctypes"); - } - Log.v(TAG, "Loaded everything!"); } } diff --git a/pythonforandroid/bootstraps/sdl2/build/templates/AndroidManifest.tmpl.xml b/pythonforandroid/bootstraps/sdl2/build/templates/AndroidManifest.tmpl.xml index 86e69371db..a37e720753 100644 --- a/pythonforandroid/bootstraps/sdl2/build/templates/AndroidManifest.tmpl.xml +++ b/pythonforandroid/bootstraps/sdl2/build/templates/AndroidManifest.tmpl.xml @@ -55,6 +55,7 @@ An example Java class can be found in README-android.txt --> + + + + + + + {% for name in service_names %} + + {% endfor %} + + + diff --git a/pythonforandroid/bootstraps/service_library/build/templates/Service.tmpl.java b/pythonforandroid/bootstraps/service_library/build/templates/Service.tmpl.java new file mode 100644 index 0000000000..4f6f7d1e26 --- /dev/null +++ b/pythonforandroid/bootstraps/service_library/build/templates/Service.tmpl.java @@ -0,0 +1,126 @@ +package {{ args.package }}; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; + +import android.os.Build; +import android.content.Intent; +import android.content.Context; +import android.content.res.Resources; +import android.util.Log; + +import org.renpy.android.AssetExtract; +import org.kivy.android.PythonService; + +public class Service{{ name|capitalize }} extends PythonService { + + private static final String TAG = "PythonService"; + + public static void prepare(Context ctx) { + String appRoot = getAppRoot(ctx); + Log.v(TAG, "Ready to unpack"); + File app_root_file = new File(appRoot); + unpackData(ctx, "private", app_root_file); + } + + public static void start(Context ctx, String pythonServiceArgument) { + String appRoot = getAppRoot(ctx); + Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); + intent.putExtra("androidPrivate", appRoot); + intent.putExtra("androidArgument", appRoot); + intent.putExtra("serviceEntrypoint", "{{ entrypoint }}"); + intent.putExtra("serviceTitle", "{{ name|capitalize }}"); + intent.putExtra("serviceDescription", ""); + intent.putExtra("pythonName", "{{ name }}"); + intent.putExtra("serviceStartAsForeground", "{{ foreground|lower }}"); + intent.putExtra("pythonHome", appRoot); + intent.putExtra("androidUnpack", appRoot); + intent.putExtra("pythonPath", appRoot + ":" + appRoot + "/lib"); + intent.putExtra("pythonServiceArgument", pythonServiceArgument); + + //foreground: {{foreground}} + {% if foreground %} + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ctx.startForegroundService(intent); + } else { + ctx.startService(intent); + } + {% else %} + ctx.startService(intent); + {% endif %} + } + + public static String getAppRoot(Context ctx) { + String app_root = ctx.getFilesDir().getAbsolutePath() + "/app"; + return app_root; + } + + public static String getResourceString(Context ctx, String name) { + // Taken from org.renpy.android.ResourceManager + Resources res = ctx.getResources(); + int id = res.getIdentifier(name, "string", ctx.getPackageName()); + return res.getString(id); + } + + public static void unpackData(Context ctx, final String resource, File target) { + // Taken from PythonActivity class + + Log.v(TAG, "UNPACKING!!! " + resource + " " + target.getName()); + + // The version of data in memory and on disk. + String data_version = getResourceString(ctx, resource + "_version"); + String disk_version = null; + + Log.v(TAG, "Data version is " + data_version); + + // If no version, no unpacking is necessary. + if (data_version == null) { + return; + } + + // Check the current disk version, if any. + String filesDir = target.getAbsolutePath(); + String disk_version_fn = filesDir + "/" + resource + ".version"; + + try { + byte buf[] = new byte[64]; + InputStream is = new FileInputStream(disk_version_fn); + int len = is.read(buf); + disk_version = new String(buf, 0, len); + is.close(); + } catch (Exception e) { + disk_version = ""; + } + + // If the disk data is out of date, extract it and write the + // version file. + // if (! data_version.equals(disk_version)) { + if (! data_version.equals(disk_version)) { + Log.v(TAG, "Extracting " + resource + " assets."); + + // Don't delete existing files + // recursiveDelete(target); + target.mkdirs(); + + AssetExtract ae = new AssetExtract(ctx); + if (!ae.extractTar(resource + ".mp3", target.getAbsolutePath())) { + Log.v(TAG, "Could not extract " + resource + " data."); + } + + try { + // Write .nomedia. + new File(target, ".nomedia").createNewFile(); + + // Write version file. + FileOutputStream os = new FileOutputStream(disk_version_fn); + os.write(data_version.getBytes()); + os.close(); + } catch (Exception e) { + Log.w("python", e); + } + } + } + +} diff --git a/pythonforandroid/bootstraps/service_only/__init__.py b/pythonforandroid/bootstraps/service_only/__init__.py index 2175f8b61f..f2630bdccc 100644 --- a/pythonforandroid/bootstraps/service_only/__init__.py +++ b/pythonforandroid/bootstraps/service_only/__init__.py @@ -34,7 +34,8 @@ def run_distribute(self): self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)]) self.distribute_aars(arch) - self.distribute_javaclasses(self.ctx.javaclass_dir) + self.distribute_javaclasses(self.ctx.javaclass_dir, + dest_dir=join("src", "main", "java")) python_bundle_dir = join('_python_bundle', '_python_bundle') ensure_dir(python_bundle_dir) @@ -45,9 +46,10 @@ def run_distribute(self): with open('blacklist.txt', 'a') as fileh: fileh.write('\nsqlite3/*\nlib-dynload/_sqlite3.so\n') - self.strip_libraries(arch) + if not self.ctx.build_as_debuggable: + self.strip_libraries(arch) self.fry_eggs(site_packages_dir) - super(ServiceOnlyBootstrap, self).run_distribute() + super().run_distribute() bootstrap = ServiceOnlyBootstrap() diff --git a/pythonforandroid/bootstraps/webview/__init__.py b/pythonforandroid/bootstraps/webview/__init__.py index c59c158356..5a7964e3f3 100644 --- a/pythonforandroid/bootstraps/webview/__init__.py +++ b/pythonforandroid/bootstraps/webview/__init__.py @@ -31,7 +31,8 @@ def run_distribute(self): self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)]) self.distribute_aars(arch) - self.distribute_javaclasses(self.ctx.javaclass_dir) + self.distribute_javaclasses(self.ctx.javaclass_dir, + dest_dir=join("src", "main", "java")) python_bundle_dir = join('_python_bundle', '_python_bundle') ensure_dir(python_bundle_dir) @@ -42,9 +43,10 @@ def run_distribute(self): with open('blacklist.txt', 'a') as fileh: fileh.write('\nsqlite3/*\nlib-dynload/_sqlite3.so\n') - self.strip_libraries(arch) + if not self.ctx.build_as_debuggable: + self.strip_libraries(arch) self.fry_eggs(site_packages_dir) - super(WebViewBootstrap, self).run_distribute() + super().run_distribute() bootstrap = WebViewBootstrap() diff --git a/pythonforandroid/bootstraps/webview/build/src/main/java/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/webview/build/src/main/java/org/kivy/android/PythonActivity.java index 2bd908ce92..2ac2608082 100644 --- a/pythonforandroid/bootstraps/webview/build/src/main/java/org/kivy/android/PythonActivity.java +++ b/pythonforandroid/bootstraps/webview/build/src/main/java/org/kivy/android/PythonActivity.java @@ -109,115 +109,119 @@ public static void initialize() { protected void onCreate(Bundle savedInstanceState) { Log.v(TAG, "My oncreate running"); resourceManager = new ResourceManager(this); - - Log.v(TAG, "Ready to unpack"); - File app_root_file = new File(getAppRoot()); - unpackData("private", app_root_file); - - Log.v(TAG, "About to do super onCreate"); super.onCreate(savedInstanceState); - Log.v(TAG, "Did super onCreate"); this.mActivity = this; - //this.showLoadingScreen(); - Log.v("Python", "Device: " + android.os.Build.DEVICE); - Log.v("Python", "Model: " + android.os.Build.MODEL); + this.showLoadingScreen(); + new UnpackFilesTask().execute(getAppRoot()); + } - //Log.v(TAG, "Ready to unpack"); - //new UnpackFilesTask().execute(getAppRoot()); + private class UnpackFilesTask extends AsyncTask { + @Override + protected String doInBackground(String... params) { + File app_root_file = new File(params[0]); + Log.v(TAG, "Ready to unpack"); + unpackData("private", app_root_file); + return null; + } - PythonActivity.initialize(); + @Override + protected void onPostExecute(String result) { + Log.v("Python", "Device: " + android.os.Build.DEVICE); + Log.v("Python", "Model: " + android.os.Build.MODEL); - // Load shared libraries - String errorMsgBrokenLib = ""; - try { - loadLibraries(); - } catch(UnsatisfiedLinkError e) { - System.err.println(e.getMessage()); - mBrokenLibraries = true; - errorMsgBrokenLib = e.getMessage(); - } catch(Exception e) { - System.err.println(e.getMessage()); - mBrokenLibraries = true; - errorMsgBrokenLib = e.getMessage(); - } + PythonActivity.initialize(); - if (mBrokenLibraries) - { - AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); - dlgAlert.setMessage("An error occurred while trying to load the application libraries. Please try again and/or reinstall." - + System.getProperty("line.separator") - + System.getProperty("line.separator") - + "Error: " + errorMsgBrokenLib); - dlgAlert.setTitle("Python Error"); - dlgAlert.setPositiveButton("Exit", - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog,int id) { - // if this button is clicked, close current activity - PythonActivity.mActivity.finish(); - } - }); - dlgAlert.setCancelable(false); - dlgAlert.create().show(); + // Load shared libraries + String errorMsgBrokenLib = ""; + try { + loadLibraries(); + } catch(UnsatisfiedLinkError e) { + System.err.println(e.getMessage()); + mBrokenLibraries = true; + errorMsgBrokenLib = e.getMessage(); + } catch(Exception e) { + System.err.println(e.getMessage()); + mBrokenLibraries = true; + errorMsgBrokenLib = e.getMessage(); + } - return; - } + if (mBrokenLibraries) + { + AlertDialog.Builder dlgAlert = new AlertDialog.Builder(PythonActivity.mActivity); + dlgAlert.setMessage("An error occurred while trying to load the application libraries. Please try again and/or reinstall." + + System.getProperty("line.separator") + + System.getProperty("line.separator") + + "Error: " + errorMsgBrokenLib); + dlgAlert.setTitle("Python Error"); + dlgAlert.setPositiveButton("Exit", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog,int id) { + // if this button is clicked, close current activity + PythonActivity.mActivity.finish(); + } + }); + dlgAlert.setCancelable(false); + dlgAlert.create().show(); + + return; + } - // Set up the webview - String app_root_dir = getAppRoot(); + // Set up the webview + String app_root_dir = getAppRoot(); - mWebView = new WebView(this); - mWebView.getSettings().setJavaScriptEnabled(true); - mWebView.getSettings().setDomStorageEnabled(true); - mWebView.loadUrl("file:///" + app_root_dir + "/_load.html"); + mWebView = new WebView(PythonActivity.mActivity); + mWebView.getSettings().setJavaScriptEnabled(true); + mWebView.getSettings().setDomStorageEnabled(true); + mWebView.loadUrl("file:///" + app_root_dir + "/_load.html"); - mWebView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); - mWebView.setWebViewClient(new WebViewClient() { - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - view.loadUrl(url); - return false; - } - }); - mLayout = new AbsoluteLayout(this); - mLayout.addView(mWebView); + mWebView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); + mWebView.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + view.loadUrl(url); + return false; + } + }); + mLayout = new AbsoluteLayout(PythonActivity.mActivity); + mLayout.addView(mWebView); - setContentView(mLayout); + setContentView(mLayout); - String mFilesDirectory = mActivity.getFilesDir().getAbsolutePath(); - String entry_point = getEntryPoint(app_root_dir); + String mFilesDirectory = mActivity.getFilesDir().getAbsolutePath(); + String entry_point = getEntryPoint(app_root_dir); - Log.v(TAG, "Setting env vars for start.c and Python to use"); - PythonActivity.nativeSetenv("ANDROID_ENTRYPOINT", entry_point); - PythonActivity.nativeSetenv("ANDROID_ARGUMENT", app_root_dir); - PythonActivity.nativeSetenv("ANDROID_APP_PATH", app_root_dir); - PythonActivity.nativeSetenv("ANDROID_PRIVATE", mFilesDirectory); - PythonActivity.nativeSetenv("ANDROID_UNPACK", app_root_dir); - PythonActivity.nativeSetenv("PYTHONHOME", app_root_dir); - PythonActivity.nativeSetenv("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib"); - PythonActivity.nativeSetenv("PYTHONOPTIMIZE", "2"); + Log.v(TAG, "Setting env vars for start.c and Python to use"); + PythonActivity.nativeSetenv("ANDROID_ENTRYPOINT", entry_point); + PythonActivity.nativeSetenv("ANDROID_ARGUMENT", app_root_dir); + PythonActivity.nativeSetenv("ANDROID_APP_PATH", app_root_dir); + PythonActivity.nativeSetenv("ANDROID_PRIVATE", mFilesDirectory); + PythonActivity.nativeSetenv("ANDROID_UNPACK", app_root_dir); + PythonActivity.nativeSetenv("PYTHONHOME", app_root_dir); + PythonActivity.nativeSetenv("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib"); + PythonActivity.nativeSetenv("PYTHONOPTIMIZE", "2"); - try { - Log.v(TAG, "Access to our meta-data..."); - mActivity.mMetaData = mActivity.getPackageManager().getApplicationInfo( - mActivity.getPackageName(), PackageManager.GET_META_DATA).metaData; - - PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE); - if ( mActivity.mMetaData.getInt("wakelock") == 1 ) { - mActivity.mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "Screen On"); - mActivity.mWakeLock.acquire(); + try { + Log.v(TAG, "Access to our meta-data..."); + mActivity.mMetaData = mActivity.getPackageManager().getApplicationInfo( + mActivity.getPackageName(), PackageManager.GET_META_DATA).metaData; + + PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE); + if ( mActivity.mMetaData.getInt("wakelock") == 1 ) { + mActivity.mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "Screen On"); + mActivity.mWakeLock.acquire(); + } + } catch (PackageManager.NameNotFoundException e) { } - } catch (PackageManager.NameNotFoundException e) { - } - - final Thread pythonThread = new Thread(new PythonMain(), "PythonThread"); - PythonActivity.mPythonThread = pythonThread; - pythonThread.start(); - final Thread wvThread = new Thread(new WebViewLoaderMain(), "WvThread"); - wvThread.start(); + final Thread pythonThread = new Thread(new PythonMain(), "PythonThread"); + PythonActivity.mPythonThread = pythonThread; + pythonThread.start(); + final Thread wvThread = new Thread(new WebViewLoaderMain(), "WvThread"); + wvThread.start(); + } } @Override @@ -367,6 +371,73 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { return super.onKeyDown(keyCode, event); } + // loading screen implementation + public static ImageView mImageView = null; + public void removeLoadingScreen() { + runOnUiThread(new Runnable() { + public void run() { + if (PythonActivity.mImageView != null && + PythonActivity.mImageView.getParent() != null) { + ((ViewGroup)PythonActivity.mImageView.getParent()).removeView( + PythonActivity.mImageView); + PythonActivity.mImageView = null; + } + } + }); + } + + protected void showLoadingScreen() { + // load the bitmap + // 1. if the image is valid and we don't have layout yet, assign this bitmap + // as main view. + // 2. if we have a layout, just set it in the layout. + // 3. If we have an mImageView already, then do nothing because it will have + // already been made the content view or added to the layout. + + if (mImageView == null) { + int presplashId = this.resourceManager.getIdentifier("presplash", "drawable"); + InputStream is = this.getResources().openRawResource(presplashId); + Bitmap bitmap = null; + try { + bitmap = BitmapFactory.decodeStream(is); + } finally { + try { + is.close(); + } catch (IOException e) {}; + } + + mImageView = new ImageView(this); + mImageView.setImageBitmap(bitmap); + + /* + * Set the presplash loading screen background color + * https://developer.android.com/reference/android/graphics/Color.html + * Parse the color string, and return the corresponding color-int. + * If the string cannot be parsed, throws an IllegalArgumentException exception. + * Supported formats are: #RRGGBB #AARRGGBB or one of the following names: + * 'red', 'blue', 'green', 'black', 'white', 'gray', 'cyan', 'magenta', 'yellow', + * 'lightgray', 'darkgray', 'grey', 'lightgrey', 'darkgrey', 'aqua', 'fuchsia', + * 'lime', 'maroon', 'navy', 'olive', 'purple', 'silver', 'teal'. + */ + String backgroundColor = resourceManager.getString("presplash_color"); + if (backgroundColor != null) { + try { + mImageView.setBackgroundColor(Color.parseColor(backgroundColor)); + } catch (IllegalArgumentException e) {} + } + mImageView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT)); + mImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); + + } + + if (mLayout == null) { + setContentView(mImageView); + } else if (PythonActivity.mImageView.getParent() == null){ + mLayout.addView(mImageView); + } + } //---------------------------------------------------------------------------- // Listener interface for onNewIntent diff --git a/pythonforandroid/bootstraps/webview/build/templates/AndroidManifest.tmpl.xml b/pythonforandroid/bootstraps/webview/build/templates/AndroidManifest.tmpl.xml index d71985c66e..8b6b4ca923 100644 --- a/pythonforandroid/bootstraps/webview/build/templates/AndroidManifest.tmpl.xml +++ b/pythonforandroid/bootstraps/webview/build/templates/AndroidManifest.tmpl.xml @@ -49,7 +49,9 @@ android:icon="@drawable/icon" android:allowBackup="true" android:theme="{{args.android_apptheme}}{% if not args.window %}.Fullscreen{% endif %}" - android:hardwareAccelerated="true" > + android:hardwareAccelerated="true" + android:usesCleartextTraffic="true" + > {% for l in args.android_used_libs %} {% endfor %} diff --git a/pythonforandroid/build.py b/pythonforandroid/build.py index 8f0ac40a82..34de065854 100644 --- a/pythonforandroid/build.py +++ b/pythonforandroid/build.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from os.path import ( abspath, join, realpath, dirname, expanduser, exists, split, isdir @@ -15,8 +13,8 @@ import subprocess from pythonforandroid.util import ( - current_directory, ensure_dir, get_virtualenv_executable, - BuildInterruptingException + current_directory, ensure_dir, + BuildInterruptingException, ) from pythonforandroid.logger import (info, warning, info_notify, info_main, shprint) from pythonforandroid.archs import ArchARM, ArchARMv7_a, ArchAarch_64, Archx86, Archx86_64 @@ -79,10 +77,13 @@ def get_available_apis(sdk_dir): return apis -class Context(object): +class Context: '''A build context. If anything will be built, an instance this class will be instantiated and used to hold all the build state.''' + # Whether to build with debugging symbols + build_as_debuggable = False + env = environ.copy() # the filepath of toolchain.py root_dir = None @@ -116,7 +117,7 @@ class Context(object): recipe_build_order = None # Will hold the list of all built recipes - symlink_java_src = False # If True, will symlink instead of copying during build + symlink_bootstrap_files = False # If True, will symlink instead of copying during build java_build_tool = 'auto' @@ -132,34 +133,33 @@ def templates_dir(self): @property def libs_dir(self): # Was previously hardcoded as self.build_dir/libs - dir = join(self.build_dir, 'libs_collections', - self.bootstrap.distribution.name) - ensure_dir(dir) - return dir + directory = join(self.build_dir, 'libs_collections', + self.bootstrap.distribution.name) + ensure_dir(directory) + return directory @property def javaclass_dir(self): # Was previously hardcoded as self.build_dir/java - dir = join(self.build_dir, 'javaclasses', - self.bootstrap.distribution.name) - ensure_dir(dir) - return dir + directory = join(self.build_dir, 'javaclasses', + self.bootstrap.distribution.name) + ensure_dir(directory) + return directory @property def aars_dir(self): - dir = join(self.build_dir, 'aars', self.bootstrap.distribution.name) - ensure_dir(dir) - return dir + directory = join(self.build_dir, 'aars', self.bootstrap.distribution.name) + ensure_dir(directory) + return directory @property def python_installs_dir(self): - dir = join(self.build_dir, 'python-installs') - ensure_dir(dir) - return dir + directory = join(self.build_dir, 'python-installs') + ensure_dir(directory) + return directory def get_python_install_dir(self): - dir = join(self.python_installs_dir, self.bootstrap.distribution.name) - return dir + return join(self.python_installs_dir, self.bootstrap.distribution.name) def setup_dirs(self, storage_dir): '''Calculates all the storage and build dirs, and makes sure @@ -264,10 +264,10 @@ def prepare_build_environment(self, possible_dirs = glob.glob(expanduser(join( '~', '.buildozer', 'android', 'platform', 'android-sdk-*'))) possible_dirs = [d for d in possible_dirs if not - (d.endswith('.bz2') or d.endswith('.gz'))] + d.endswith(('.bz2', '.gz'))] if possible_dirs: info('Found possible SDK dirs in buildozer dir: {}'.format( - ', '.join([d.split(os.sep)[-1] for d in possible_dirs]))) + ', '.join(d.split(os.sep)[-1] for d in possible_dirs))) info('Will attempt to use SDK at {}'.format(possible_dirs[0])) warning('This SDK lookup is intended for debug only, if you ' 'use python-for-android much you should probably ' @@ -328,7 +328,7 @@ def prepare_build_environment(self, '~', '.buildozer', 'android', 'platform', 'android-ndk-r*'))) if possible_dirs: info('Found possible NDK dirs in buildozer dir: {}'.format( - ', '.join([d.split(os.sep)[-1] for d in possible_dirs]))) + ', '.join(d.split(os.sep)[-1] for d in possible_dirs))) info('Will attempt to use NDK at {}'.format(possible_dirs[0])) warning('This NDK lookup is intended for debug only, if you ' 'use python-for-android much you should probably ' @@ -357,13 +357,6 @@ def prepare_build_environment(self, check_ndk_api(ndk_api, self.android_api) - virtualenv = get_virtualenv_executable() - if virtualenv is None: - raise IOError('Couldn\'t find a virtualenv executable, ' - 'you must install this to use p4a.') - self.virtualenv = virtualenv - info('Found virtualenv at {}'.format(virtualenv)) - # path to some tools self.ccache = sh.which("ccache") if not self.ccache: @@ -427,15 +420,13 @@ def prepare_build_environment(self, for executable in ("pkg-config", "autoconf", "automake", "libtoolize", "tar", "bzip2", "unzip", "make", "gcc", "g++"): if not sh.which(executable): - warning("Missing executable: {} is not installed".format( - executable)) + warning(f"Missing executable: {executable} is not installed") if not ok: raise BuildInterruptingException( 'python-for-android cannot continue due to the missing executables above') def __init__(self): - super(Context, self).__init__() self.include_dirs = [] self._build_env_prepared = False @@ -481,11 +472,13 @@ def set_archs(self, arch_names): if not self.archs: raise BuildInterruptingException('Asked to compile for no Archs, so failing.') info('Will compile for the following archs: {}'.format( - ', '.join([arch.arch for arch in self.archs]))) + ', '.join(arch.arch for arch in self.archs))) - def prepare_bootstrap(self, bs): - bs.ctx = self - self.bootstrap = bs + def prepare_bootstrap(self, bootstrap): + if not bootstrap: + raise TypeError("None is not allowed for bootstrap") + bootstrap.ctx = self + self.bootstrap = bootstrap self.bootstrap.prepare_build_dir() self.bootstrap_build_dir = self.bootstrap.build_dir @@ -578,10 +571,10 @@ def build_recipes(build_order, python_modules, ctx, project_dir, info_main('Building {} for {}'.format(recipe.name, arch.arch)) if recipe.should_build(arch): recipe.build_arch(arch) - recipe.install_libraries(arch) else: info('{} said it is already built, skipping' .format(recipe.name)) + recipe.install_libraries(arch) # 4) biglink everything info_main('# Biglinking object files') @@ -605,23 +598,16 @@ def build_recipes(build_order, python_modules, ctx, project_dir, ignore_setup_py=ignore_project_setup_py ) - return - def project_has_setup_py(project_dir): - if project_dir is not None and \ - (os.path.exists(os.path.join(project_dir, - "setup.py")) or - os.path.exists(os.path.join(project_dir, - "pyproject.toml")) - ): - return True - return False + return (project_dir is not None and + (exists(join(project_dir, "setup.py")) or + exists(join(project_dir, "pyproject.toml")) + )) def run_setuppy_install(ctx, project_dir, env=None): - if env is None: - env = dict() + env = env or {} with current_directory(project_dir): info('got setup.py or similar, running project install. ' + @@ -772,18 +758,13 @@ def run_pymodules_install(ctx, modules, project_dir=None, info('Will process project install, if it fails then the ' 'project may not be compatible for Android install.') - venv = sh.Command(ctx.virtualenv) + # Use our hostpython to create the virtualenv + host_python = sh.Command(ctx.hostpython) with current_directory(join(ctx.build_dir)): - shprint(venv, - '--python=python{}'.format( - ctx.python_recipe.major_minor_version_string. - partition(".")[0] - ), - 'venv' - ) + shprint(host_python, '-m', 'venv', 'venv') # Prepare base environment and upgrade pip: - base_env = copy.copy(os.environ) + base_env = dict(copy.copy(os.environ)) base_env["PYTHONPATH"] = ctx.get_site_packages_dir() info('Upgrade pip to latest version') shprint(sh.bash, '-c', ( @@ -851,8 +832,10 @@ def run_pymodules_install(ctx, modules, project_dir=None, ) # Strip object files after potential Cython or native code builds: - standard_recipe.strip_object_files(ctx.archs[0], env, - build_dir=ctx.build_dir) + if not ctx.build_as_debuggable: + standard_recipe.strip_object_files( + ctx.archs[0], env, build_dir=ctx.build_dir + ) def biglink(ctx, arch): @@ -901,7 +884,9 @@ def biglink(ctx, arch): env=env) -def biglink_function(soname, objs_paths, extra_link_dirs=[], env=None): +def biglink_function(soname, objs_paths, extra_link_dirs=None, env=None): + if extra_link_dirs is None: + extra_link_dirs = [] print('objs_paths are', objs_paths) sofiles = [] @@ -948,7 +933,9 @@ def biglink_function(soname, objs_paths, extra_link_dirs=[], env=None): shprint(cc, '-shared', '-O3', '-o', soname, *unique_args, _env=env) -def copylibs_function(soname, objs_paths, extra_link_dirs=[], env=None): +def copylibs_function(soname, objs_paths, extra_link_dirs=None, env=None): + if extra_link_dirs is None: + extra_link_dirs = [] print('objs_paths are', objs_paths) re_needso = re.compile(r'^.*\(NEEDED\)\s+Shared library: \[lib(.*)\.so\]\s*$') @@ -1076,5 +1063,4 @@ def copylibs_function(soname, objs_paths, extra_link_dirs=[], env=None): '\n\t'.join(needed_libs)) print('Copying libraries') - for lib in sofiles: - shprint(sh.cp, lib, dest) + shprint(sh.cp, *sofiles, dest) diff --git a/pythonforandroid/distribution.py b/pythonforandroid/distribution.py index 379cd90852..8607766eba 100644 --- a/pythonforandroid/distribution.py +++ b/pythonforandroid/distribution.py @@ -7,7 +7,7 @@ from shutil import rmtree -class Distribution(object): +class Distribution: '''State container for information about a distribution (i.e. an Android project). diff --git a/pythonforandroid/graph.py b/pythonforandroid/graph.py index 2e98e8ccda..567c77fa3b 100644 --- a/pythonforandroid/graph.py +++ b/pythonforandroid/graph.py @@ -160,7 +160,7 @@ def obvious_conflict_checker(ctx, name_tuples, blacklist=None): current_to_be_added = list(to_be_added) to_be_added = [] for (added_tuple, adding_recipe) in current_to_be_added: - assert(type(added_tuple) == tuple) + assert type(added_tuple) == tuple if len(added_tuple) > 1: # No obvious commitment in what to add, don't check it itself # but throw it into deps for later comparing against diff --git a/pythonforandroid/logger.py b/pythonforandroid/logger.py index 77cb9da323..8f0b15ca31 100644 --- a/pythonforandroid/logger.py +++ b/pythonforandroid/logger.py @@ -7,19 +7,6 @@ from collections import defaultdict from colorama import Style as Colo_Style, Fore as Colo_Fore -# six import left for Python 2 compatibility during initial Python version check -import six - -# This codecs change fixes a bug with log output, but crashes under python3 -if not six.PY3: - import codecs - stdout = codecs.getwriter('utf8')(stdout) - stderr = codecs.getwriter('utf8')(stderr) - -if six.PY2: - unistr = unicode # noqa F821 -else: - unistr = str # monkey patch to show full output sh.ErrorReturnCode.truncate_cap = 999999 @@ -42,7 +29,7 @@ def format(self, record): record.msg = '{}{}[DEBUG]{}{}: '.format( Err_Style.BRIGHT, Err_Fore.LIGHTBLACK_EX, Err_Fore.RESET, Err_Style.RESET_ALL) + record.msg - return super(LevelDifferentiatingFormatter, self).format(record) + return super().format(record) logger = logging.getLogger('p4a') @@ -61,7 +48,7 @@ def format(self, record): error = logger.error -class colorama_shim(object): +class colorama_shim: def __init__(self, real): self._dict = defaultdict(str) @@ -114,12 +101,12 @@ def shorten_string(string, max_width): return string visible = max_width - 16 - int(log10(string_len)) # expected suffix len "...(and XXXXX more)" - if not isinstance(string, unistr): - visstring = unistr(string[:visible], errors='ignore') + if not isinstance(string, str): + visstring = str(string[:visible], errors='ignore') else: visstring = string[:visible] return u''.join((visstring, u'...(and ', - unistr(string_len - visible), u' more)')) + str(string_len - visible), u' more)')) def get_console_width(): diff --git a/pythonforandroid/python.py b/pythonforandroid/python.py deleted file mode 100755 index 9c8b2439a2..0000000000 --- a/pythonforandroid/python.py +++ /dev/null @@ -1,476 +0,0 @@ -''' -This module is kind of special because it contains the base classes used to -build our python3 and python2 recipes and his corresponding hostpython recipes. -''' - -from os.path import dirname, exists, join, isfile -from multiprocessing import cpu_count -from shutil import copy2 -from os import environ -import subprocess -import glob -import sh - -from pythonforandroid.recipe import Recipe, TargetPythonRecipe -from pythonforandroid.logger import info, warning, shprint -from pythonforandroid.util import ( - current_directory, - ensure_dir, - walk_valid_filens, - BuildInterruptingException, -) - - -class GuestPythonRecipe(TargetPythonRecipe): - ''' - Class for target python recipes. Sets ctx.python_recipe to point to itself, - so as to know later what kind of Python was built or used. - - This base class is used for our main python recipes (python2 and python3) - which shares most of the build process. - - .. versionadded:: 0.6.0 - Refactored from the inclement's python3 recipe with a few changes: - - - Splits the python's build process several methods: :meth:`build_arch` - and :meth:`get_recipe_env`. - - Adds the attribute :attr:`configure_args`, which has been moved from - the method :meth:`build_arch` into a static class variable. - - Adds some static class variables used to create the python bundle and - modifies the method :meth:`create_python_bundle`, to adapt to the new - situation. The added static class variables are: - :attr:`stdlib_dir_blacklist`, :attr:`stdlib_filen_blacklist`, - :attr:`site_packages_dir_blacklist`and - :attr:`site_packages_filen_blacklist`. - ''' - - MIN_NDK_API = 21 - '''Sets the minimal ndk api number needed to use the recipe. - - .. warning:: This recipe can be built only against API 21+, so it means - that any class which inherits from class:`GuestPythonRecipe` will have - this limitation. - ''' - - configure_args = () - '''The configure arguments needed to build the python recipe. Those are - used in method :meth:`build_arch` (if not overwritten like python3's - recipe does). - - .. note:: This variable should be properly set in subclass. - ''' - - stdlib_dir_blacklist = { - '__pycache__', - 'test', - 'tests', - 'lib2to3', - 'ensurepip', - 'idlelib', - 'tkinter', - } - '''The directories that we want to omit for our python bundle''' - - stdlib_filen_blacklist = [ - '*.py', - '*.exe', - '*.whl', - ] - '''The file extensions that we want to blacklist for our python bundle''' - - site_packages_dir_blacklist = { - '__pycache__', - 'tests' - } - '''The directories from site packages dir that we don't want to be included - in our python bundle.''' - - site_packages_filen_blacklist = [ - '*.py' - ] - '''The file extensions from site packages dir that we don't want to be - included in our python bundle.''' - - opt_depends = ['sqlite3', 'libffi', 'openssl'] - '''The optional libraries which we would like to get our python linked''' - - compiled_extension = '.pyc' - '''the default extension for compiled python files. - - .. note:: the default extension for compiled python files has been .pyo for - python 2.x-3.4 but as of Python 3.5, the .pyo filename extension is no - longer used and has been removed in favour of extension .pyc - ''' - - def __init__(self, *args, **kwargs): - self._ctx = None - super(GuestPythonRecipe, self).__init__(*args, **kwargs) - - def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = environ.copy() - env['HOSTARCH'] = arch.command_prefix - - env['CC'] = arch.get_clang_exe(with_target=True) - - env['PATH'] = ( - '{hostpython_dir}:{old_path}').format( - hostpython_dir=self.get_recipe( - 'host' + self.name, self.ctx).get_path_to_python(), - old_path=env['PATH']) - - env['CFLAGS'] = ' '.join( - [ - '-fPIC', - '-DANDROID', - '-D__ANDROID_API__={}'.format(self.ctx.ndk_api), - ] - ) - - env['LDFLAGS'] = env.get('LDFLAGS', '') - if sh.which('lld') is not None: - # Note: The -L. is to fix a bug in python 3.7. - # https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234409 - env['LDFLAGS'] += ' -L. -fuse-ld=lld' - else: - warning('lld not found, linking without it. ' - 'Consider installing lld if linker errors occur.') - - return env - - def set_libs_flags(self, env, arch): - '''Takes care to properly link libraries with python depending on our - requirements and the attribute :attr:`opt_depends`. - ''' - def add_flags(include_flags, link_dirs, link_libs): - env['CPPFLAGS'] = env.get('CPPFLAGS', '') + include_flags - env['LDFLAGS'] = env.get('LDFLAGS', '') + link_dirs - env['LIBS'] = env.get('LIBS', '') + link_libs - - if 'sqlite3' in self.ctx.recipe_build_order: - info('Activating flags for sqlite3') - recipe = Recipe.get_recipe('sqlite3', self.ctx) - add_flags(' -I' + recipe.get_build_dir(arch.arch), - ' -L' + recipe.get_lib_dir(arch), ' -lsqlite3') - - if 'libffi' in self.ctx.recipe_build_order: - info('Activating flags for libffi') - recipe = Recipe.get_recipe('libffi', self.ctx) - # In order to force the correct linkage for our libffi library, we - # set the following variable to point where is our libffi.pc file, - # because the python build system uses pkg-config to configure it. - env['PKG_CONFIG_PATH'] = recipe.get_build_dir(arch.arch) - add_flags(' -I' + ' -I'.join(recipe.get_include_dirs(arch)), - ' -L' + join(recipe.get_build_dir(arch.arch), '.libs'), - ' -lffi') - - if 'openssl' in self.ctx.recipe_build_order: - info('Activating flags for openssl') - recipe = Recipe.get_recipe('openssl', self.ctx) - add_flags(recipe.include_flags(arch), - recipe.link_dirs_flags(arch), recipe.link_libs_flags()) - - for library_name in {'libbz2', 'liblzma'}: - if library_name in self.ctx.recipe_build_order: - info(f'Activating flags for {library_name}') - recipe = Recipe.get_recipe(library_name, self.ctx) - add_flags(recipe.get_library_includes(arch), - recipe.get_library_ldflags(arch), - recipe.get_library_libs_flag()) - - # python build system contains hardcoded zlib version which prevents - # the build of zlib module, here we search for android's zlib version - # and sets the right flags, so python can be build with android's zlib - info("Activating flags for android's zlib") - zlib_lib_path = join(self.ctx.ndk_platform, 'usr', 'lib') - zlib_includes = join(self.ctx.ndk_dir, 'sysroot', 'usr', 'include') - zlib_h = join(zlib_includes, 'zlib.h') - try: - with open(zlib_h) as fileh: - zlib_data = fileh.read() - except IOError: - raise BuildInterruptingException( - "Could not determine android's zlib version, no zlib.h ({}) in" - " the NDK dir includes".format(zlib_h) - ) - for line in zlib_data.split('\n'): - if line.startswith('#define ZLIB_VERSION '): - break - else: - raise BuildInterruptingException( - 'Could not parse zlib.h...so we cannot find zlib version,' - 'required by python build,' - ) - env['ZLIB_VERSION'] = line.replace('#define ZLIB_VERSION ', '') - add_flags(' -I' + zlib_includes, ' -L' + zlib_lib_path, ' -lz') - - return env - - @property - def _libpython(self): - '''return the python's library name (with extension)''' - py_version = self.major_minor_version_string - if self.major_minor_version_string[0] == '3': - py_version += 'm' - return 'libpython{version}.so'.format(version=py_version) - - def should_build(self, arch): - return not isfile(join(self.link_root(arch.arch), self._libpython)) - - def prebuild_arch(self, arch): - super(TargetPythonRecipe, self).prebuild_arch(arch) - self.ctx.python_recipe = self - - def build_arch(self, arch): - if self.ctx.ndk_api < self.MIN_NDK_API: - raise BuildInterruptingException( - 'Target ndk-api is {}, but the python3 recipe supports only' - ' {}+'.format(self.ctx.ndk_api, self.MIN_NDK_API)) - - recipe_build_dir = self.get_build_dir(arch.arch) - - # Create a subdirectory to actually perform the build - build_dir = join(recipe_build_dir, 'android-build') - ensure_dir(build_dir) - - # TODO: Get these dynamically, like bpo-30386 does - sys_prefix = '/usr/local' - sys_exec_prefix = '/usr/local' - - with current_directory(build_dir): - env = self.get_recipe_env(arch) - env = self.set_libs_flags(env, arch) - - android_build = sh.Command( - join(recipe_build_dir, - 'config.guess'))().stdout.strip().decode('utf-8') - - if not exists('config.status'): - shprint( - sh.Command(join(recipe_build_dir, 'configure')), - *(' '.join(self.configure_args).format( - android_host=env['HOSTARCH'], - android_build=android_build, - prefix=sys_prefix, - exec_prefix=sys_exec_prefix)).split(' '), - _env=env) - - shprint( - sh.make, 'all', '-j', str(cpu_count()), - 'INSTSONAME={lib_name}'.format(lib_name=self._libpython), - _env=env - ) - - # TODO: Look into passing the path to pyconfig.h in a - # better way, although this is probably acceptable - sh.cp('pyconfig.h', join(recipe_build_dir, 'Include')) - - def include_root(self, arch_name): - return join(self.get_build_dir(arch_name), 'Include') - - def link_root(self, arch_name): - return join(self.get_build_dir(arch_name), 'android-build') - - def compile_python_files(self, dir): - ''' - Compile the python files (recursively) for the python files inside - a given folder. - - .. note:: python2 compiles the files into extension .pyo, but in - python3, and as of Python 3.5, the .pyo filename extension is no - longer used...uses .pyc (https://www.python.org/dev/peps/pep-0488) - ''' - args = [self.ctx.hostpython] - if self.ctx.python_recipe.name == 'python3': - args += ['-OO', '-m', 'compileall', '-b', '-f', dir] - else: - args += ['-OO', '-m', 'compileall', '-f', dir] - subprocess.call(args) - - def create_python_bundle(self, dirn, arch): - """ - Create a packaged python bundle in the target directory, by - copying all the modules and standard library to the right - place. - """ - # Todo: find a better way to find the build libs folder - modules_build_dir = join( - self.get_build_dir(arch.arch), - 'android-build', - 'build', - 'lib.linux{}-{}-{}'.format( - '2' if self.version[0] == '2' else '', - arch.command_prefix.split('-')[0], - self.major_minor_version_string - )) - - # Compile to *.pyc/*.pyo the python modules - self.compile_python_files(modules_build_dir) - # Compile to *.pyc/*.pyo the standard python library - self.compile_python_files(join(self.get_build_dir(arch.arch), 'Lib')) - # Compile to *.pyc/*.pyo the other python packages (site-packages) - self.compile_python_files(self.ctx.get_python_install_dir()) - - # Bundle compiled python modules to a folder - modules_dir = join(dirn, 'modules') - c_ext = self.compiled_extension - ensure_dir(modules_dir) - module_filens = (glob.glob(join(modules_build_dir, '*.so')) + - glob.glob(join(modules_build_dir, '*' + c_ext))) - info("Copy {} files into the bundle".format(len(module_filens))) - for filen in module_filens: - info(" - copy {}".format(filen)) - copy2(filen, modules_dir) - - # zip up the standard library - stdlib_zip = join(dirn, 'stdlib.zip') - with current_directory(join(self.get_build_dir(arch.arch), 'Lib')): - stdlib_filens = list(walk_valid_filens( - '.', self.stdlib_dir_blacklist, self.stdlib_filen_blacklist)) - info("Zip {} files into the bundle".format(len(stdlib_filens))) - shprint(sh.zip, stdlib_zip, *stdlib_filens) - - # copy the site-packages into place - ensure_dir(join(dirn, 'site-packages')) - ensure_dir(self.ctx.get_python_install_dir()) - # TODO: Improve the API around walking and copying the files - with current_directory(self.ctx.get_python_install_dir()): - filens = list(walk_valid_filens( - '.', self.site_packages_dir_blacklist, - self.site_packages_filen_blacklist)) - info("Copy {} files into the site-packages".format(len(filens))) - for filen in filens: - info(" - copy {}".format(filen)) - ensure_dir(join(dirn, 'site-packages', dirname(filen))) - copy2(filen, join(dirn, 'site-packages', filen)) - - # copy the python .so files into place - python_build_dir = join(self.get_build_dir(arch.arch), - 'android-build') - python_lib_name = 'libpython' + self.major_minor_version_string - if self.major_minor_version_string[0] == '3': - python_lib_name += 'm' - shprint( - sh.cp, - join(python_build_dir, python_lib_name + '.so'), - join(self.ctx.bootstrap.dist_dir, 'libs', arch.arch) - ) - - info('Renaming .so files to reflect cross-compile') - self.reduce_object_file_names(join(dirn, 'site-packages')) - - return join(dirn, 'site-packages') - - -class HostPythonRecipe(Recipe): - ''' - This is the base class for hostpython3 and hostpython2 recipes. This class - will take care to do all the work to build a hostpython recipe but, be - careful, it is intended to be subclassed because some of the vars needs to - be set: - - - :attr:`name` - - :attr:`version` - - .. versionadded:: 0.6.0 - Refactored from the hostpython3's recipe by inclement - ''' - - name = '' - '''The hostpython's recipe name. This should be ``hostpython2`` or - ``hostpython3`` - - .. warning:: This must be set in inherited class.''' - - version = '' - '''The hostpython's recipe version. - - .. warning:: This must be set in inherited class.''' - - build_subdir = 'native-build' - '''Specify the sub build directory for the hostpython recipe. Defaults - to ``native-build``.''' - - url = 'https://www.python.org/ftp/python/{version}/Python-{version}.tgz' - '''The default url to download our host python recipe. This url will - change depending on the python version set in attribute :attr:`version`.''' - - @property - def _exe_name(self): - ''' - Returns the name of the python executable depending on the version. - ''' - if not self.version: - raise BuildInterruptingException( - 'The hostpython recipe must have set version' - ) - version = self.version.split('.')[0] - return 'python{major_version}'.format(major_version=version) - - @property - def python_exe(self): - '''Returns the full path of the hostpython executable.''' - return join(self.get_path_to_python(), self._exe_name) - - def should_build(self, arch): - if exists(self.python_exe): - # no need to build, but we must set hostpython for our Context - self.ctx.hostpython = self.python_exe - return False - return True - - def get_build_container_dir(self, arch=None): - choices = self.check_recipe_choices() - dir_name = '-'.join([self.name] + choices) - return join(self.ctx.build_dir, 'other_builds', dir_name, 'desktop') - - def get_build_dir(self, arch=None): - ''' - .. note:: Unlike other recipes, the hostpython build dir doesn't - depend on the target arch - ''' - return join(self.get_build_container_dir(), self.name) - - def get_path_to_python(self): - return join(self.get_build_dir(), self.build_subdir) - - def build_arch(self, arch): - recipe_build_dir = self.get_build_dir(arch.arch) - - # Create a subdirectory to actually perform the build - build_dir = join(recipe_build_dir, self.build_subdir) - ensure_dir(build_dir) - - with current_directory(recipe_build_dir): - # Configure the build - with current_directory(build_dir): - if not exists('config.status'): - shprint(sh.Command(join(recipe_build_dir, 'configure'))) - - # Create the Setup file. This copying from Setup.dist is - # the normal and expected procedure before Python 3.8, but - # after this the file with default options is already named "Setup" - setup_dist_location = join('Modules', 'Setup.dist') - if exists(setup_dist_location): - shprint(sh.cp, setup_dist_location, - join(build_dir, 'Modules', 'Setup')) - else: - # Check the expected file does exist - setup_location = join('Modules', 'Setup') - if not exists(setup_location): - raise BuildInterruptingException( - "Could not find Setup.dist or Setup in Python build") - - shprint(sh.make, '-j', str(cpu_count()), '-C', build_dir) - - # make a copy of the python executable giving it the name we want, - # because we got different python's executable names depending on - # the fs being case-insensitive (Mac OS X, Cygwin...) or - # case-sensitive (linux)...so this way we will have an unique name - # for our hostpython, regarding the used fs - for exe_name in ['python.exe', 'python']: - exe = join(self.get_path_to_python(), exe_name) - if isfile(exe): - shprint(sh.cp, exe, self.python_exe) - break - - self.ctx.hostpython = self.python_exe diff --git a/pythonforandroid/pythonpackage.py b/pythonforandroid/pythonpackage.py index f96042d982..66dec4fc31 100644 --- a/pythonforandroid/pythonpackage.py +++ b/pythonforandroid/pythonpackage.py @@ -33,12 +33,8 @@ """ -from io import open # needed for python 2 import functools import os -from pep517.envbuild import BuildEnvironment -from pep517.wrappers import Pep517HookCaller -import pytoml import shutil import subprocess import sys @@ -46,13 +42,14 @@ import tempfile import textwrap import time -try: - from urllib.parse import urlparse - from urllib.parse import unquote as urlunquote -except ImportError: # Python 2... - from urlparse import urlparse - from urlparse import unquote as urlunquote import zipfile +from io import open # needed for python 2 +from urllib.parse import unquote as urlunquote +from urllib.parse import urlparse + +import toml +from pep517.envbuild import BuildEnvironment +from pep517.wrappers import Pep517HookCaller def transform_dep_for_pip(dependency): @@ -111,7 +108,8 @@ def extract_metainfo_files_from_package( if is_filesystem_path(package): shutil.copytree( parse_as_folder_reference(package), - os.path.join(temp_folder, "package") + os.path.join(temp_folder, "package"), + ignore=shutil.ignore_patterns(".tox") ) package = os.path.join(temp_folder, "package") @@ -486,7 +484,7 @@ def _extract_metainfo_files_from_package_unsafe( # Get build backend and requirements from pyproject.toml: with open(os.path.join(path, 'pyproject.toml')) as f: - build_sys = pytoml.load(f)['build-system'] + build_sys = toml.load(f)['build-system'] backend = build_sys["build-backend"] build_requires.extend(build_sys["requires"]) @@ -630,7 +628,7 @@ def _extract_info_from_package(dependency, ) and include_build_requirements: # Read build system from pyproject.toml file: (PEP518) with open(os.path.join(output_folder, 'pyproject.toml')) as f: - build_sys = pytoml.load(f)['build-system'] + build_sys = toml.load(f)['build-system'] if "requires" in build_sys: requirements += build_sys["requires"] elif include_build_requirements: diff --git a/pythonforandroid/recipe.py b/pythonforandroid/recipe.py index 3d814d5c4e..378c1e2777 100644 --- a/pythonforandroid/recipe.py +++ b/pythonforandroid/recipe.py @@ -1,7 +1,7 @@ from os.path import basename, dirname, exists, isdir, isfile, join, realpath, split import glob from shutil import rmtree -from six import PY2, with_metaclass +from six import with_metaclass import hashlib from re import match @@ -22,26 +22,17 @@ def import_recipe(module, filename): - if PY2: - import imp - import warnings - with warnings.catch_warnings(): - # ignores warnings raised by hierarchical module names - # (names containing dots) on Python 2 - warnings.simplefilter("ignore", RuntimeWarning) - return imp.load_source(module, filename) + # Python 3.5+ + import importlib.util + if hasattr(importlib.util, 'module_from_spec'): + spec = importlib.util.spec_from_file_location(module, filename) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod else: - # Python 3.5+ - import importlib.util - if hasattr(importlib.util, 'module_from_spec'): - spec = importlib.util.spec_from_file_location(module, filename) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - else: - # Python 3.3 and 3.4: - from importlib.machinery import SourceFileLoader - return SourceFileLoader(module, filename).load_module() + # Python 3.3 and 3.4: + from importlib.machinery import SourceFileLoader + return SourceFileLoader(module, filename).load_module() class RecipeMeta(type): @@ -52,7 +43,7 @@ def __new__(cls, name, bases, dct): if 'version' in dct: dct['_version'] = dct.pop('version') - return super(RecipeMeta, cls).__new__(cls, name, bases, dct) + return super().__new__(cls, name, bases, dct) class Recipe(with_metaclass(RecipeMeta)): @@ -769,8 +760,7 @@ def get_jni_dir(self): return join(self.ctx.bootstrap.build_dir, 'jni') def get_recipe_env(self, arch=None, with_flags_in_cc=True, with_python=False): - env = super(BootstrapNDKRecipe, self).get_recipe_env( - arch, with_flags_in_cc) + env = super().get_recipe_env(arch, with_flags_in_cc) if not with_python: return env @@ -804,13 +794,14 @@ def get_jni_dir(self, arch): return join(self.get_build_dir(arch.arch), 'jni') def build_arch(self, arch, *extra_args): - super(NDKRecipe, self).build_arch(arch) + super().build_arch(arch) env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): shprint( sh.ndk_build, 'V=1', + 'NDK_DEBUG=' + ("1" if self.ctx.build_as_debuggable else "0"), 'APP_PLATFORM=android-' + str(self.ctx.ndk_api), 'APP_ABI=' + arch.arch, *extra_args, _env=env @@ -843,7 +834,7 @@ class PythonRecipe(Recipe): setup_extra_args = [] '''List of extra arguments to pass to setup.py''' - depends = [('python2', 'python3')] + depends = ['python3'] ''' .. note:: it's important to keep this depends as a class attribute outside `__init__` because sometimes we only initialize the class, so the @@ -856,25 +847,20 @@ class PythonRecipe(Recipe): ''' def __init__(self, *args, **kwargs): - super(PythonRecipe, self).__init__(*args, **kwargs) - if not any( - [ - d - for d in {'python2', 'python3', ('python2', 'python3')} - if d in self.depends - ] - ): + super().__init__(*args, **kwargs) + + if 'python3' not in self.depends: # We ensure here that the recipe depends on python even it overrode # `depends`. We only do this if it doesn't already depend on any # python, since some recipes intentionally don't depend on/work # with all python variants depends = self.depends - depends.append(('python2', 'python3')) + depends.append('python3') depends = list(set(depends)) self.depends = depends def clean_build(self, arch=None): - super(PythonRecipe, self).clean_build(arch=arch) + super().clean_build(arch=arch) name = self.folder_name python_install_dirs = glob.glob(join(self.ctx.python_installs_dir, '*')) for python_install in python_install_dirs: @@ -889,7 +875,7 @@ def clean_build(self, arch=None): @property def real_hostpython_location(self): host_name = 'host{}'.format(self.ctx.python_recipe.name) - if host_name in ['hostpython2', 'hostpython3']: + if host_name == 'hostpython3': python_recipe = Recipe.get_recipe(host_name, self.ctx) return python_recipe.python_exe else: @@ -911,7 +897,7 @@ def folder_name(self): return name def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(PythonRecipe, self).get_recipe_env(arch, with_flags_in_cc) + env = super().get_recipe_env(arch, with_flags_in_cc) env['PYTHONNOUSERSITE'] = '1' @@ -956,7 +942,7 @@ def should_build(self, arch): def build_arch(self, arch): '''Install the Python module by calling setup.py install with the target Python dir.''' - super(PythonRecipe, self).build_arch(arch) + super().build_arch(arch) self.install_python_package(arch) def install_python_package(self, arch, name=None, env=None, is_dir=True): @@ -1026,7 +1012,7 @@ def build_compiled_components(self, arch): def install_hostpython_package(self, arch): env = self.get_hostrecipe_env(arch) self.rebuild_compiled_components(arch, env) - super(CompiledComponentsPythonRecipe, self).install_hostpython_package(arch) + super().install_hostpython_package(arch) def rebuild_compiled_components(self, arch, env): info('Rebuilding compiled components in {}'.format(self.name)) @@ -1086,7 +1072,8 @@ def build_cython_components(self, arch): info('First build appeared to complete correctly, skipping manual' 'cythonising.') - self.strip_object_files(arch, env) + if not self.ctx.build_as_debuggable: + self.strip_object_files(arch, env) def strip_object_files(self, arch, env, build_dir=None): if build_dir is None: @@ -1128,7 +1115,7 @@ def cythonize_build(self, env, build_dir="."): self.cythonize_file(env, build_dir, join(root, filename)) def get_recipe_env(self, arch, with_flags_in_cc=True): - env = super(CythonRecipe, self).get_recipe_env(arch, with_flags_in_cc) + env = super().get_recipe_env(arch, with_flags_in_cc) env['LDFLAGS'] = env['LDFLAGS'] + ' -L{} '.format( self.ctx.get_libs_dir(arch.arch) + ' -L{} '.format(self.ctx.libs_dir) + @@ -1158,10 +1145,10 @@ class TargetPythonRecipe(Recipe): def __init__(self, *args, **kwargs): self._ctx = None - super(TargetPythonRecipe, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def prebuild_arch(self, arch): - super(TargetPythonRecipe, self).prebuild_arch(arch) + super().prebuild_arch(arch) self.ctx.python_recipe = self def include_root(self, arch): diff --git a/pythonforandroid/recipes/Pillow/__init__.py b/pythonforandroid/recipes/Pillow/__init__.py index 22289463a1..dbf4b01909 100644 --- a/pythonforandroid/recipes/Pillow/__init__.py +++ b/pythonforandroid/recipes/Pillow/__init__.py @@ -13,7 +13,7 @@ class PillowRecipe(CompiledComponentsPythonRecipe): call_hostpython_via_targetpython = False def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(PillowRecipe, self).get_recipe_env(arch, with_flags_in_cc) + env = super().get_recipe_env(arch, with_flags_in_cc) env['ANDROID_ROOT'] = join(self.ctx.ndk_platform, 'usr') ndk_lib_dir = join(self.ctx.ndk_platform, 'usr', 'lib') diff --git a/pythonforandroid/recipes/android/__init__.py b/pythonforandroid/recipes/android/__init__.py index 3658c772ba..ba45fe95b1 100644 --- a/pythonforandroid/recipes/android/__init__.py +++ b/pythonforandroid/recipes/android/__init__.py @@ -1,7 +1,5 @@ -from __future__ import unicode_literals from pythonforandroid.recipe import CythonRecipe, IncludedFilesBehaviour from pythonforandroid.util import current_directory -from pythonforandroid.patching import will_build from pythonforandroid import logger from os.path import join @@ -19,12 +17,12 @@ class AndroidRecipe(IncludedFilesBehaviour, CythonRecipe): config_env = {} def get_recipe_env(self, arch): - env = super(AndroidRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) env.update(self.config_env) return env def prebuild_arch(self, arch): - super(AndroidRecipe, self).prebuild_arch(arch) + super().prebuild_arch(arch) ctx_bootstrap = self.ctx.bootstrap.name # define macros for Cython, C, Python @@ -36,14 +34,8 @@ def prebuild_arch(self, arch): if isinstance(ctx_bootstrap, bytes): ctx_bootstrap = ctx_bootstrap.decode('utf-8') bootstrap = bootstrap_name = ctx_bootstrap - - is_sdl2 = bootstrap_name in ('sdl2', 'sdl2python3', 'sdl2_gradle') - is_webview = bootstrap_name == 'webview' - is_service_only = bootstrap_name == 'service_only' - - if is_sdl2 or is_webview or is_service_only: - if is_sdl2: - bootstrap = 'sdl2' + is_sdl2 = (bootstrap_name == "sdl2") + if bootstrap_name in ["sdl2", "webview", "service_only", "service_library"]: java_ns = u'org.kivy.android' jni_ns = u'org/kivy/android' else: @@ -56,7 +48,7 @@ def prebuild_arch(self, arch): config = { 'BOOTSTRAP': bootstrap, 'IS_SDL2': int(is_sdl2), - 'PY2': int(will_build('python2')(self)), + 'PY2': 0, 'JAVA_NAMESPACE': java_ns, 'JNI_NAMESPACE': jni_ns, } diff --git a/pythonforandroid/recipes/android/src/android/_android_billing.pyx b/pythonforandroid/recipes/android/src/android/_android_billing.pyx index bd6bb2ef7f..d5ed2a00d0 100644 --- a/pythonforandroid/recipes/android/src/android/_android_billing.pyx +++ b/pythonforandroid/recipes/android/src/android/_android_billing.pyx @@ -15,7 +15,7 @@ class BillingService(object): BILLING_TYPE_SUBSCRIPTION = 'subs' def __init__(self, callback): - super(BillingService, self).__init__() + super().__init__() self.callback = callback self.purchased_items = None android_billing_service_start() diff --git a/pythonforandroid/recipes/android/src/android/activity.py b/pythonforandroid/recipes/android/src/android/activity.py index cafbbdab70..9c215075c6 100644 --- a/pythonforandroid/recipes/android/src/android/activity.py +++ b/pythonforandroid/recipes/android/src/android/activity.py @@ -14,7 +14,7 @@ class NewIntentListener(PythonJavaClass): __javacontext__ = 'app' def __init__(self, callback, **kwargs): - super(NewIntentListener, self).__init__(**kwargs) + super().__init__(**kwargs) self.callback = callback @java_method('(Landroid/content/Intent;)V') @@ -27,7 +27,7 @@ class ActivityResultListener(PythonJavaClass): __javacontext__ = 'app' def __init__(self, callback): - super(ActivityResultListener, self).__init__() + super().__init__() self.callback = callback @java_method('(IILandroid/content/Intent;)V') diff --git a/pythonforandroid/recipes/android/src/android/broadcast.py b/pythonforandroid/recipes/android/src/android/broadcast.py index cb34cd9d19..aa454b7c13 100644 --- a/pythonforandroid/recipes/android/src/android/broadcast.py +++ b/pythonforandroid/recipes/android/src/android/broadcast.py @@ -20,7 +20,7 @@ def onReceive(self, context, intent): self.callback(context, intent) def __init__(self, callback, actions=None, categories=None): - super(BroadcastReceiver, self).__init__() + super().__init__() self.callback = callback if not actions and not categories: diff --git a/pythonforandroid/recipes/android/src/android/runnable.py b/pythonforandroid/recipes/android/src/android/runnable.py index 8d2d1161d1..74268c803f 100644 --- a/pythonforandroid/recipes/android/src/android/runnable.py +++ b/pythonforandroid/recipes/android/src/android/runnable.py @@ -1,15 +1,18 @@ ''' Runnable ======== - ''' from jnius import PythonJavaClass, java_method, autoclass from android.config import JAVA_NAMESPACE -# reference to the activity +# Reference to the activity _PythonActivity = autoclass(JAVA_NAMESPACE + '.PythonActivity') +# Cache of functions table. In older Android versions the number of JNI references +# is limited, so by caching them we avoid running out. +__functionstable__ = {} + class Runnable(PythonJavaClass): '''Wrapper around Java Runnable class. This class can be used to schedule a @@ -20,7 +23,7 @@ class Runnable(PythonJavaClass): __runnables__ = [] def __init__(self, func): - super(Runnable, self).__init__() + super().__init__() self.func = func def __call__(self, *args, **kwargs): @@ -44,6 +47,12 @@ def run_on_ui_thread(f): '''Decorator to create automatically a :class:`Runnable` object with the function. The function will be delayed and call into the Activity thread. ''' + if f not in __functionstable__: + rfunction = Runnable(f) # store the runnable function + __functionstable__[f] = {"rfunction": rfunction} + rfunction = __functionstable__[f]["rfunction"] + def f2(*args, **kwargs): - Runnable(f)(*args, **kwargs) + rfunction(*args, **kwargs) + return f2 diff --git a/pythonforandroid/recipes/apsw/__init__.py b/pythonforandroid/recipes/apsw/__init__.py index 6098e4b98c..42ad3ba337 100644 --- a/pythonforandroid/recipes/apsw/__init__.py +++ b/pythonforandroid/recipes/apsw/__init__.py @@ -6,7 +6,7 @@ class ApswRecipe(PythonRecipe): version = '3.15.0-r1' url = 'https://github.com/rogerbinns/apsw/archive/{version}.tar.gz' - depends = ['sqlite3', ('python2', 'python3'), 'setuptools'] + depends = ['sqlite3', 'setuptools'] call_hostpython_via_targetpython = False site_packages_name = 'apsw' @@ -20,10 +20,10 @@ def build_arch(self, arch): 'build_ext', '--enable=fts4', _env=env) # Install python bindings - super(ApswRecipe, self).build_arch(arch) + super().build_arch(arch) def get_recipe_env(self, arch): - env = super(ApswRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) sqlite_recipe = self.get_recipe('sqlite3', self.ctx) env['CFLAGS'] += ' -I' + sqlite_recipe.get_build_dir(arch.arch) env['LDFLAGS'] += ' -L' + sqlite_recipe.get_lib_dir(arch) diff --git a/pythonforandroid/recipes/audiostream/__init__.py b/pythonforandroid/recipes/audiostream/__init__.py index 518d1dda8e..66fd42f081 100644 --- a/pythonforandroid/recipes/audiostream/__init__.py +++ b/pythonforandroid/recipes/audiostream/__init__.py @@ -7,10 +7,10 @@ class AudiostreamRecipe(CythonRecipe): version = 'master' url = 'https://github.com/kivy/audiostream/archive/{version}.zip' name = 'audiostream' - depends = [('python2', 'python3'), 'sdl2', 'pyjnius'] + depends = ['python3', 'sdl2', 'pyjnius'] def get_recipe_env(self, arch): - env = super(AudiostreamRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) sdl_include = 'SDL2' sdl_mixer_include = 'SDL2_mixer' env['USE_SDL2'] = 'True' diff --git a/pythonforandroid/recipes/bcrypt/__init__.py b/pythonforandroid/recipes/bcrypt/__init__.py index e7a5659b8f..da220ff30d 100644 --- a/pythonforandroid/recipes/bcrypt/__init__.py +++ b/pythonforandroid/recipes/bcrypt/__init__.py @@ -9,7 +9,7 @@ class BCryptRecipe(CompiledComponentsPythonRecipe): call_hostpython_via_targetpython = False def get_recipe_env(self, arch): - env = super(BCryptRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) openssl_recipe = Recipe.get_recipe('openssl', self.ctx) env['CFLAGS'] += openssl_recipe.include_flags(arch) diff --git a/pythonforandroid/recipes/boost/__init__.py b/pythonforandroid/recipes/boost/__init__.py index 978ab6f5b0..195b4d5b96 100644 --- a/pythonforandroid/recipes/boost/__init__.py +++ b/pythonforandroid/recipes/boost/__init__.py @@ -39,7 +39,7 @@ class BoostRecipe(Recipe): 'http://downloads.sourceforge.net/project/boost/' 'boost/{version}/boost_{version_underscore}.tar.bz2' ) - depends = [('python2', 'python3')] + depends = ['python3'] patches = [ 'disable-so-version.patch', 'use-android-libs.patch', @@ -60,7 +60,7 @@ 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) + super().prebuild_arch(arch) env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): # Set custom configuration @@ -70,7 +70,7 @@ def prebuild_arch(self, arch): ) def build_arch(self, arch): - super(BoostRecipe, self).build_arch(arch) + super().build_arch(arch) env = self.get_recipe_env(arch) env['PYTHON_HOST'] = self.ctx.hostpython with current_directory(self.get_build_dir(arch.arch)): diff --git a/pythonforandroid/recipes/cdecimal/__init__.py b/pythonforandroid/recipes/cdecimal/__init__.py index 94929c7807..a444eb1b25 100644 --- a/pythonforandroid/recipes/cdecimal/__init__.py +++ b/pythonforandroid/recipes/cdecimal/__init__.py @@ -13,7 +13,7 @@ class CdecimalRecipe(CompiledComponentsPythonRecipe): 'cross-compile.patch'] def prebuild_arch(self, arch): - super(CdecimalRecipe, self).prebuild_arch(arch) + super().prebuild_arch(arch) if not is_darwin(): if '64' in arch.arch: machine = 'ansi64' diff --git a/pythonforandroid/recipes/cffi/__init__.py b/pythonforandroid/recipes/cffi/__init__.py index aa291dca46..3416fec620 100644 --- a/pythonforandroid/recipes/cffi/__init__.py +++ b/pythonforandroid/recipes/cffi/__init__.py @@ -7,7 +7,7 @@ class CffiRecipe(CompiledComponentsPythonRecipe): Extra system dependencies: autoconf, automake and libtool. """ name = 'cffi' - version = '1.11.5' + version = '1.13.2' url = 'https://pypi.python.org/packages/source/c/cffi/cffi-{version}.tar.gz' depends = ['setuptools', 'pycparser', 'libffi'] @@ -19,14 +19,14 @@ class CffiRecipe(CompiledComponentsPythonRecipe): def get_hostrecipe_env(self, arch=None): # fixes missing ffi.h on some host systems (e.g. gentoo) - env = super(CffiRecipe, self).get_hostrecipe_env(arch) + env = super().get_hostrecipe_env(arch) libffi = self.get_recipe('libffi', self.ctx) includes = libffi.get_include_dirs(arch) env['FFI_INC'] = ",".join(includes) return env def get_recipe_env(self, arch=None): - env = super(CffiRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) libffi = self.get_recipe('libffi', self.ctx) includes = libffi.get_include_dirs(arch) env['CFLAGS'] = ' -I'.join([env.get('CFLAGS', '')] + includes) diff --git a/pythonforandroid/recipes/coverage/__init__.py b/pythonforandroid/recipes/coverage/__init__.py index 95f08f1f4a..2ee2d059c8 100644 --- a/pythonforandroid/recipes/coverage/__init__.py +++ b/pythonforandroid/recipes/coverage/__init__.py @@ -7,7 +7,7 @@ class CoverageRecipe(PythonRecipe): url = 'https://pypi.python.org/packages/2d/10/6136c8e10644c16906edf4d9f7c782c0f2e7ed47ff2f41f067384e432088/coverage-{version}.tar.gz' - depends = [('hostpython2', 'hostpython3'), 'setuptools'] + depends = ['hostpython3', 'setuptools'] patches = ['fallback-utf8.patch'] diff --git a/pythonforandroid/recipes/cryptography/__init__.py b/pythonforandroid/recipes/cryptography/__init__.py index 1b7babafca..182c745996 100644 --- a/pythonforandroid/recipes/cryptography/__init__.py +++ b/pythonforandroid/recipes/cryptography/__init__.py @@ -3,14 +3,13 @@ class CryptographyRecipe(CompiledComponentsPythonRecipe): name = 'cryptography' - version = '2.6.1' + version = '2.8' url = 'https://github.com/pyca/cryptography/archive/{version}.tar.gz' - depends = ['openssl', 'idna', 'asn1crypto', 'six', 'setuptools', - 'enum34', 'ipaddress', 'cffi'] + depends = ['openssl', 'six', 'setuptools', 'cffi'] call_hostpython_via_targetpython = False def get_recipe_env(self, arch): - env = super(CryptographyRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) openssl_recipe = Recipe.get_recipe('openssl', self.ctx) env['CFLAGS'] += openssl_recipe.include_flags(arch) diff --git a/pythonforandroid/recipes/enum34/__init__.py b/pythonforandroid/recipes/enum34/__init__.py index 9a438c5503..35dcda9a9f 100644 --- a/pythonforandroid/recipes/enum34/__init__.py +++ b/pythonforandroid/recipes/enum34/__init__.py @@ -16,7 +16,7 @@ def should_build(self, arch): # attribute `depends` because otherwise we will not be able to # build the cryptography recipe. return False - return super(Enum34Recipe, self).should_build(arch) + return super().should_build(arch) recipe = Enum34Recipe() diff --git a/pythonforandroid/recipes/evdev/__init__.py b/pythonforandroid/recipes/evdev/__init__.py index afd542e2a0..2d53f9b0b8 100644 --- a/pythonforandroid/recipes/evdev/__init__.py +++ b/pythonforandroid/recipes/evdev/__init__.py @@ -17,7 +17,7 @@ class EvdevRecipe(CompiledComponentsPythonRecipe): 'evdev-permissions.patch'] def get_recipe_env(self, arch=None): - env = super(EvdevRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) env['NDKPLATFORM'] = self.ctx.ndk_platform return env diff --git a/pythonforandroid/recipes/ffmpeg/__init__.py b/pythonforandroid/recipes/ffmpeg/__init__.py index 3418ed1a90..b5e5605a1d 100644 --- a/pythonforandroid/recipes/ffmpeg/__init__.py +++ b/pythonforandroid/recipes/ffmpeg/__init__.py @@ -19,7 +19,7 @@ def prebuild_arch(self, arch): self.apply_patches(arch) def get_recipe_env(self, arch): - env = super(FFMpegRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) env['NDK'] = self.ctx.ndk_dir return env diff --git a/pythonforandroid/recipes/ffpyplayer/__init__.py b/pythonforandroid/recipes/ffpyplayer/__init__.py index cb9e7071e7..831e4a8b15 100644 --- a/pythonforandroid/recipes/ffpyplayer/__init__.py +++ b/pythonforandroid/recipes/ffpyplayer/__init__.py @@ -6,11 +6,11 @@ class FFPyPlayerRecipe(CythonRecipe): version = 'c99913f2317bf3840eeacf1c1c3db3b3d1f78007' url = 'https://github.com/matham/ffpyplayer/archive/{version}.zip' - depends = [('python2', 'python3'), 'sdl2', 'ffmpeg'] + depends = ['python3', 'sdl2', 'ffmpeg'] opt_depends = ['openssl', 'ffpyplayer_codecs'] def get_recipe_env(self, arch, with_flags_in_cc=True): - env = super(FFPyPlayerRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) build_dir = Recipe.get_recipe('ffmpeg', self.ctx).get_build_dir(arch.arch) env["FFMPEG_INCLUDE_DIR"] = join(build_dir, "include") diff --git a/pythonforandroid/recipes/freetype/__init__.py b/pythonforandroid/recipes/freetype/__init__.py index cf8de5ee85..00dbeec6a1 100644 --- a/pythonforandroid/recipes/freetype/__init__.py +++ b/pythonforandroid/recipes/freetype/__init__.py @@ -24,20 +24,17 @@ class FreetypeRecipe(Recipe): https://sourceforge.net/projects/freetype/files/freetype2/2.5.3/ """ - version = '2.5.5' + version = '2.10.1' url = 'http://download.savannah.gnu.org/releases/freetype/freetype-{version}.tar.gz' # noqa built_libraries = {'libfreetype.so': 'objs/.libs'} def get_recipe_env(self, arch=None, with_harfbuzz=False): - env = super(FreetypeRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) if with_harfbuzz: harfbuzz_build = self.get_recipe( 'harfbuzz', self.ctx ).get_build_dir(arch.arch) freetype_install = join(self.get_build_dir(arch.arch), 'install') - env['CFLAGS'] = ' '.join( - [env['CFLAGS'], '-DFT_CONFIG_OPTION_USE_HARFBUZZ'] - ) env['HARFBUZZ_CFLAGS'] = '-I{harfbuzz} -I{harfbuzz}/src'.format( harfbuzz=harfbuzz_build @@ -48,6 +45,19 @@ def get_recipe_env(self, arch=None, with_harfbuzz=False): freetype=freetype_install, harfbuzz=harfbuzz_build ) ) + + # android's zlib support + zlib_lib_path = join(self.ctx.ndk_platform, 'usr', 'lib') + zlib_includes = join(self.ctx.ndk_dir, 'sysroot', 'usr', 'include') + + def add_flag_if_not_added(flag, env_key): + if flag not in env[env_key]: + env[env_key] += flag + + add_flag_if_not_added(' -I' + zlib_includes, 'CFLAGS') + add_flag_if_not_added(' -L' + zlib_lib_path, 'LDFLAGS') + add_flag_if_not_added(' -lz', 'LDLIBS') + return env def build_arch(self, arch, with_harfbuzz=False): @@ -66,14 +76,17 @@ def build_arch(self, arch, with_harfbuzz=False): config_args = { '--host={}'.format(arch.command_prefix), '--prefix={}'.format(prefix_path), - '--without-zlib', '--without-bzip2', '--with-png=no', } if not harfbuzz_in_recipes: info('Build freetype (without harfbuzz)') config_args = config_args.union( - {'--disable-static', '--enable-shared', '--with-harfbuzz=no'} + {'--disable-static', + '--enable-shared', + '--with-harfbuzz=no', + '--with-zlib=yes', + } ) elif not with_harfbuzz: info('Build freetype for First time (without harfbuzz)') @@ -82,12 +95,16 @@ def build_arch(self, arch, with_harfbuzz=False): # symbols/functions, so we avoid to have two freetype shared # libraries which will be confusing and harder to link with them config_args = config_args.union( - {'--disable-shared', '--with-harfbuzz=no'} + {'--disable-shared', '--with-harfbuzz=no', '--with-zlib=no'} ) else: info('Build freetype for Second time (with harfbuzz)') config_args = config_args.union( - {'--disable-static', '--enable-shared', '--with-harfbuzz=yes'} + {'--disable-static', + '--enable-shared', + '--with-harfbuzz=yes', + '--with-zlib=yes', + } ) info('Configure args are:\n\t-{}'.format('\n\t-'.join(config_args))) diff --git a/pythonforandroid/recipes/genericndkbuild/__init__.py b/pythonforandroid/recipes/genericndkbuild/__init__.py index e6cccb6e8d..0e6354958f 100644 --- a/pythonforandroid/recipes/genericndkbuild/__init__.py +++ b/pythonforandroid/recipes/genericndkbuild/__init__.py @@ -7,15 +7,17 @@ class GenericNDKBuildRecipe(BootstrapNDKRecipe): version = None url = None - depends = [('python2', 'python3')] + depends = ['python3'] conflicts = ['sdl2'] def should_build(self, arch): return True def get_recipe_env(self, arch=None, with_flags_in_cc=True, with_python=True): - env = super(GenericNDKBuildRecipe, self).get_recipe_env( - arch=arch, with_flags_in_cc=with_flags_in_cc, with_python=with_python) + env = super().get_recipe_env( + arch=arch, with_flags_in_cc=with_flags_in_cc, + with_python=with_python, + ) env['APP_ALLOW_MISSING_DEPS'] = 'true' return env diff --git a/pythonforandroid/recipes/harfbuzz/__init__.py b/pythonforandroid/recipes/harfbuzz/__init__.py index 17c72a27e2..fd1dbe9de7 100644 --- a/pythonforandroid/recipes/harfbuzz/__init__.py +++ b/pythonforandroid/recipes/harfbuzz/__init__.py @@ -20,13 +20,13 @@ class HarfbuzzRecipe(Recipe): https://sourceforge.net/projects/freetype/files/freetype2/2.5.3/ """ - version = '0.9.40' - url = 'http://www.freedesktop.org/software/harfbuzz/release/harfbuzz-{version}.tar.bz2' # noqa + version = '2.6.4' + url = 'http://www.freedesktop.org/software/harfbuzz/release/harfbuzz-{version}.tar.xz' # noqa opt_depends = ['freetype'] built_libraries = {'libharfbuzz.so': 'src/.libs'} def get_recipe_env(self, arch=None): - env = super(HarfbuzzRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) if 'freetype' in self.ctx.recipe_build_order: freetype = self.get_recipe('freetype', self.ctx) freetype_install = join( @@ -50,7 +50,6 @@ def build_arch(self, arch): configure = sh.Command('./configure') shprint( configure, - '--without-icu', '--host={}'.format(arch.command_prefix), '--prefix={}'.format(self.get_build_dir(arch.arch)), '--with-freetype={}'.format( @@ -58,7 +57,10 @@ def build_arch(self, arch): if 'freetype' in self.ctx.recipe_build_order else 'no' ), - '--without-glib', + '--with-icu=no', + '--with-cairo=no', + '--with-fontconfig=no', + '--with-glib=no', _env=env, ) shprint(sh.make, '-j', str(cpu_count()), _env=env) diff --git a/pythonforandroid/recipes/hostpython2/__init__.py b/pythonforandroid/recipes/hostpython2/__init__.py deleted file mode 100644 index 36eb178d8a..0000000000 --- a/pythonforandroid/recipes/hostpython2/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from pythonforandroid.python import HostPythonRecipe - - -class Hostpython2Recipe(HostPythonRecipe): - ''' - The hostpython2's recipe. - - .. versionchanged:: 0.6.0 - Updated to version 2.7.15 and the build process has been changed in - favour of the recently added class - :class:`~pythonforandroid.python.HostPythonRecipe` - ''' - version = '2.7.15' - name = 'hostpython2' - conflicts = ['hostpython3'] - - -recipe = Hostpython2Recipe() diff --git a/pythonforandroid/recipes/hostpython3/__init__.py b/pythonforandroid/recipes/hostpython3/__init__.py index 3068b85da4..651fb019aa 100644 --- a/pythonforandroid/recipes/hostpython3/__init__.py +++ b/pythonforandroid/recipes/hostpython3/__init__.py @@ -1,17 +1,120 @@ -from pythonforandroid.python import HostPythonRecipe +import sh +from multiprocessing import cpu_count +from os.path import exists, join, isfile -class Hostpython3Recipe(HostPythonRecipe): +from pythonforandroid.logger import shprint +from pythonforandroid.recipe import Recipe +from pythonforandroid.util import ( + BuildInterruptingException, + current_directory, + ensure_dir, +) + + +class Hostpython3Recipe(Recipe): ''' The hostpython3's recipe. + .. versionchanged:: 2019.10.06.post0 + Refactored from deleted class ``python.HostPythonRecipe`` into here. + .. versionchanged:: 0.6.0 Refactored into the new class :class:`~pythonforandroid.python.HostPythonRecipe` ''' + version = '3.8.1' name = 'hostpython3' - conflicts = ['hostpython2'] + + build_subdir = 'native-build' + '''Specify the sub build directory for the hostpython3 recipe. Defaults + to ``native-build``.''' + + url = 'https://www.python.org/ftp/python/{version}/Python-{version}.tgz' + '''The default url to download our host python recipe. This url will + change depending on the python version set in attribute :attr:`version`.''' + + @property + def _exe_name(self): + ''' + Returns the name of the python executable depending on the version. + ''' + if not self.version: + raise BuildInterruptingException( + 'The hostpython recipe must have set version' + ) + version = self.version.split('.')[0] + return 'python{major_version}'.format(major_version=version) + + @property + def python_exe(self): + '''Returns the full path of the hostpython executable.''' + return join(self.get_path_to_python(), self._exe_name) + + def should_build(self, arch): + if exists(self.python_exe): + # no need to build, but we must set hostpython for our Context + self.ctx.hostpython = self.python_exe + return False + return True + + def get_build_container_dir(self, arch=None): + choices = self.check_recipe_choices() + dir_name = '-'.join([self.name] + choices) + return join(self.ctx.build_dir, 'other_builds', dir_name, 'desktop') + + def get_build_dir(self, arch=None): + ''' + .. note:: Unlike other recipes, the hostpython build dir doesn't + depend on the target arch + ''' + return join(self.get_build_container_dir(), self.name) + + def get_path_to_python(self): + return join(self.get_build_dir(), self.build_subdir) + + def build_arch(self, arch): + recipe_build_dir = self.get_build_dir(arch.arch) + + # Create a subdirectory to actually perform the build + build_dir = join(recipe_build_dir, self.build_subdir) + ensure_dir(build_dir) + + with current_directory(recipe_build_dir): + # Configure the build + with current_directory(build_dir): + if not exists('config.status'): + shprint(sh.Command(join(recipe_build_dir, 'configure'))) + + # Create the Setup file. This copying from Setup.dist is + # the normal and expected procedure before Python 3.8, but + # after this the file with default options is already named "Setup" + setup_dist_location = join('Modules', 'Setup.dist') + if exists(setup_dist_location): + shprint(sh.cp, setup_dist_location, + join(build_dir, 'Modules', 'Setup')) + else: + # Check the expected file does exist + setup_location = join('Modules', 'Setup') + if not exists(setup_location): + raise BuildInterruptingException( + "Could not find Setup.dist or Setup in Python build") + + shprint(sh.make, '-j', str(cpu_count()), '-C', build_dir) + + # make a copy of the python executable giving it the name we want, + # because we got different python's executable names depending on + # the fs being case-insensitive (Mac OS X, Cygwin...) or + # case-sensitive (linux)...so this way we will have an unique name + # for our hostpython, regarding the used fs + for exe_name in ['python.exe', 'python']: + exe = join(self.get_path_to_python(), exe_name) + if isfile(exe): + shprint(sh.cp, exe, self.python_exe) + break + + self.ctx.hostpython = self.python_exe recipe = Hostpython3Recipe() diff --git a/pythonforandroid/recipes/icu/__init__.py b/pythonforandroid/recipes/icu/__init__.py index 43c5ac9eb8..56e2d49798 100644 --- a/pythonforandroid/recipes/icu/__init__.py +++ b/pythonforandroid/recipes/icu/__init__.py @@ -14,7 +14,7 @@ class ICURecipe(Recipe): url = ('http://download.icu-project.org/files/icu4c/' '{version}/icu4c-{version_underscore}-src.tgz') - depends = [('hostpython2', 'hostpython3')] # installs in python + depends = ['hostpython3'] # installs in python patches = ['disable-libs-version.patch'] built_libraries = { @@ -109,7 +109,7 @@ def make_build_dest(dest): shprint(sh.make, "install", _env=env) def install_libraries(self, arch): - super(ICURecipe, self).install_libraries(arch) + super().install_libraries(arch) src_include = join( self.get_build_dir(arch.arch), "icu_build", "include") diff --git a/pythonforandroid/recipes/ifaddrs/__init__.py b/pythonforandroid/recipes/ifaddrs/__init__.py index 47c0008fa5..7d44f9cd72 100644 --- a/pythonforandroid/recipes/ifaddrs/__init__.py +++ b/pythonforandroid/recipes/ifaddrs/__init__.py @@ -10,7 +10,7 @@ class IFAddrRecipe(CompiledComponentsPythonRecipe): version = '8f9a87c' url = 'https://github.com/morristech/android-ifaddrs/archive/{version}.zip' - depends = [('hostpython2', 'hostpython3')] + depends = ['hostpython3'] call_hostpython_via_targetpython = False site_packages_name = 'ifaddrs' diff --git a/pythonforandroid/recipes/kivy/__init__.py b/pythonforandroid/recipes/kivy/__init__.py index a93627a021..0e01a91d6a 100644 --- a/pythonforandroid/recipes/kivy/__init__.py +++ b/pythonforandroid/recipes/kivy/__init__.py @@ -14,7 +14,7 @@ class KivyRecipe(CythonRecipe): python_depends = ['certifi'] def cythonize_build(self, env, build_dir='.'): - super(KivyRecipe, self).cythonize_build(env, build_dir=build_dir) + super().cythonize_build(env, build_dir=build_dir) if not exists(join(build_dir, 'kivy', 'include')): return @@ -35,10 +35,10 @@ def cythonize_file(self, env, build_dir, filename): do_not_cythonize = ['window_x11.pyx', ] if basename(filename) in do_not_cythonize: return - super(KivyRecipe, self).cythonize_file(env, build_dir, filename) + super().cythonize_file(env, build_dir, filename) def get_recipe_env(self, arch): - env = super(KivyRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) if 'sdl2' in self.ctx.recipe_build_order: env['USE_SDL2'] = '1' env['KIVY_SPLIT_EXAMPLES'] = '1' diff --git a/pythonforandroid/recipes/libglob/__init__.py b/pythonforandroid/recipes/libglob/__init__.py index 4c69657c87..f63db42628 100644 --- a/pythonforandroid/recipes/libglob/__init__.py +++ b/pythonforandroid/recipes/libglob/__init__.py @@ -22,7 +22,7 @@ class LibGlobRecipe(Recipe): name = 'libglob' built_libraries = {'libglob.so': '.'} - depends = [('hostpython2', 'hostpython3')] + depends = ['hostpython3'] patches = ['glob.patch'] def should_build(self, arch): diff --git a/pythonforandroid/recipes/libmysqlclient/__init__.py b/pythonforandroid/recipes/libmysqlclient/__init__.py index 9235ad4325..31ebd3c540 100644 --- a/pythonforandroid/recipes/libmysqlclient/__init__.py +++ b/pythonforandroid/recipes/libmysqlclient/__init__.py @@ -38,7 +38,7 @@ def build_arch(self, arch): self.install_libs(arch, join('libmysql', 'libmysql.so')) # def get_recipe_env(self, arch=None): - # env = super(LibmysqlclientRecipe, self).get_recipe_env(arch) + # env = super().get_recipe_env(arch) # env['WITHOUT_SERVER'] = 'ON' # ncurses = self.get_recipe('ncurses', self) # # env['CFLAGS'] += ' -I' + join(ncurses.get_build_dir(arch.arch), diff --git a/pythonforandroid/recipes/libnacl/__init__.py b/pythonforandroid/recipes/libnacl/__init__.py index 3fc5da82f0..50689feb5d 100644 --- a/pythonforandroid/recipes/libnacl/__init__.py +++ b/pythonforandroid/recipes/libnacl/__init__.py @@ -4,7 +4,7 @@ class LibNaClRecipe(PythonRecipe): version = '1.4.4' url = 'https://github.com/saltstack/libnacl/archive/v{version}.tar.gz' - depends = [('hostpython2', 'hostpython3'), 'setuptools', 'libsodium'] + depends = ['hostpython3', 'setuptools', 'libsodium'] site_packages_name = 'libnacl' call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/libsodium/__init__.py b/pythonforandroid/recipes/libsodium/__init__.py index 8165ebc57e..f66fc18e7f 100644 --- a/pythonforandroid/recipes/libsodium/__init__.py +++ b/pythonforandroid/recipes/libsodium/__init__.py @@ -27,7 +27,7 @@ def build_arch(self, arch): shprint(sh.make, '-j', str(cpu_count()), _env=env) def get_recipe_env(self, arch): - env = super(LibsodiumRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) env['CFLAGS'] += ' -Os' return env diff --git a/pythonforandroid/recipes/libtorrent/__init__.py b/pythonforandroid/recipes/libtorrent/__init__.py index 0eb50672b7..b9debc2175 100644 --- a/pythonforandroid/recipes/libtorrent/__init__.py +++ b/pythonforandroid/recipes/libtorrent/__init__.py @@ -61,14 +61,14 @@ def should_build(self, arch): self.ctx.has_package('libtorrent', arch.arch)) def prebuild_arch(self, arch): - super(LibtorrentRecipe, self).prebuild_arch(arch) + super().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) + super().build_arch(arch) env = self.get_recipe_env(arch) env['PYTHON_HOST'] = self.ctx.hostpython diff --git a/pythonforandroid/recipes/libvorbis/__init__.py b/pythonforandroid/recipes/libvorbis/__init__.py index 87c7a449db..5f1e3254c1 100644 --- a/pythonforandroid/recipes/libvorbis/__init__.py +++ b/pythonforandroid/recipes/libvorbis/__init__.py @@ -12,7 +12,7 @@ class VorbisRecipe(NDKRecipe): generated_libraries = ['libvorbis.so', 'libvorbisfile.so', 'libvorbisenc.so'] def get_recipe_env(self, arch=None): - env = super(VorbisRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) ogg = self.get_recipe('libogg', self.ctx) env['CFLAGS'] += ' -I{}'.format(join(ogg.get_build_dir(arch.arch), 'include')) return env diff --git a/pythonforandroid/recipes/libxml2/__init__.py b/pythonforandroid/recipes/libxml2/__init__.py index bcccc4ebd8..c8788d9d78 100644 --- a/pythonforandroid/recipes/libxml2/__init__.py +++ b/pythonforandroid/recipes/libxml2/__init__.py @@ -43,7 +43,7 @@ def build_arch(self, arch): shprint(sh.make, "libxml2.la", _env=env) def get_recipe_env(self, arch): - env = super(Libxml2Recipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) env['CONFIG_SHELL'] = '/bin/bash' env['SHELL'] = '/bin/bash' env['CC'] += ' -I' + self.get_build_dir(arch.arch) diff --git a/pythonforandroid/recipes/libxslt/__init__.py b/pythonforandroid/recipes/libxslt/__init__.py index b22e2b328a..ca65fbb9d8 100644 --- a/pythonforandroid/recipes/libxslt/__init__.py +++ b/pythonforandroid/recipes/libxslt/__init__.py @@ -45,7 +45,7 @@ def build_arch(self, arch): shprint(sh.make, "V=1", _env=env) def get_recipe_env(self, arch): - env = super(LibxsltRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) env['CONFIG_SHELL'] = '/bin/bash' env['SHELL'] = '/bin/bash' diff --git a/pythonforandroid/recipes/libzbar/__init__.py b/pythonforandroid/recipes/libzbar/__init__.py index 5b1b62d8c1..7a9c15650d 100644 --- a/pythonforandroid/recipes/libzbar/__init__.py +++ b/pythonforandroid/recipes/libzbar/__init__.py @@ -19,7 +19,7 @@ class LibZBarRecipe(Recipe): built_libraries = {'libzbar.so': 'zbar/.libs'} def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(LibZBarRecipe, self).get_recipe_env(arch, with_flags_in_cc) + env = super().get_recipe_env(arch, with_flags_in_cc) libiconv = self.get_recipe('libiconv', self.ctx) libiconv_dir = libiconv.get_build_dir(arch.arch) env['CFLAGS'] += ' -I' + os.path.join(libiconv_dir, 'include') diff --git a/pythonforandroid/recipes/lxml/__init__.py b/pythonforandroid/recipes/lxml/__init__.py index 6d4b91c25b..75e7fcd9f7 100644 --- a/pythonforandroid/recipes/lxml/__init__.py +++ b/pythonforandroid/recipes/lxml/__init__.py @@ -12,7 +12,7 @@ class LXMLRecipe(CompiledComponentsPythonRecipe): call_hostpython_via_targetpython = False # Due to setuptools def should_build(self, arch): - super(LXMLRecipe, self).should_build(arch) + super().should_build(arch) py_ver = self.ctx.python_recipe.major_minor_version_string build_platform = '{system}-{machine}'.format( @@ -24,7 +24,7 @@ def should_build(self, arch): return not all([exists(join(build_dir, lib)) for lib in py_libs]) def get_recipe_env(self, arch): - env = super(LXMLRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) # libxslt flags libxslt_recipe = Recipe.get_recipe('libxslt', self.ctx) diff --git a/pythonforandroid/recipes/m2crypto/__init__.py b/pythonforandroid/recipes/m2crypto/__init__.py index 653eeca85c..57860493e2 100644 --- a/pythonforandroid/recipes/m2crypto/__init__.py +++ b/pythonforandroid/recipes/m2crypto/__init__.py @@ -32,7 +32,7 @@ def build_compiled_components(self, arch): env['STRIP'], '{}', ';', _env=env) def get_recipe_env(self, arch): - env = super(M2CryptoRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) env['OPENSSL_BUILD_PATH'] = self.get_recipe('openssl', self.ctx).get_build_dir(arch.arch) return env diff --git a/pythonforandroid/recipes/matplotlib/__init__.py b/pythonforandroid/recipes/matplotlib/__init__.py index 05fa5c424b..b48a24c6de 100644 --- a/pythonforandroid/recipes/matplotlib/__init__.py +++ b/pythonforandroid/recipes/matplotlib/__init__.py @@ -12,7 +12,6 @@ class MatplotlibRecipe(CppCompiledComponentsPythonRecipe): url = 'https://github.com/matplotlib/matplotlib/archive/v{version}.zip' depends = ['numpy', 'png', 'setuptools', 'freetype', 'kiwisolver'] - conflicts = ['python2'] python_depends = ['pyparsing', 'cycler', 'python-dateutil'] @@ -86,7 +85,7 @@ def download_web_backend_dependencies(self, arch): jquery_sha = ( 'f8233674366ab36b2c34c577ec77a3d70cac75d2e387d8587f3836345c0f624d' ) - url = f'https://jqueryui.com/resources/download/jquery-ui-1.12.1.zip' + url = "https://jqueryui.com/resources/download/jquery-ui-1.12.1.zip" target_file = join(env['XDG_CACHE_HOME'], 'matplotlib', jquery_sha) info_notify(f'Will download into {env["XDG_CACHE_HOME"]}') diff --git a/pythonforandroid/recipes/mysqldb/__init__.py b/pythonforandroid/recipes/mysqldb/__init__.py index f08458567b..768cb72af2 100644 --- a/pythonforandroid/recipes/mysqldb/__init__.py +++ b/pythonforandroid/recipes/mysqldb/__init__.py @@ -23,15 +23,15 @@ def convert_newlines(self, filename): f.write(data.replace(b'\r\n', b'\n').replace(b'\r', b'\n')) def prebuild_arch(self, arch): - super(MysqldbRecipe, self).prebuild_arch(arch) + super().prebuild_arch(arch) setupbase = join(self.get_build_dir(arch.arch), 'setup') self.convert_newlines(setupbase + '.py') self.convert_newlines(setupbase + '_posix.py') def get_recipe_env(self, arch=None): - env = super(MysqldbRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) - hostpython = self.get_recipe('hostpython2', self.ctx) + hostpython = self.get_recipe('hostpython3', self.ctx) # TODO: fix hardcoded path env['PYTHONPATH'] = (join(hostpython.get_build_dir(arch.arch), 'build', 'lib.linux-x86_64-2.7') + diff --git a/pythonforandroid/recipes/ndghttpsclient b/pythonforandroid/recipes/ndghttpsclient index 35e996f0d6..bfe581a119 100644 --- a/pythonforandroid/recipes/ndghttpsclient +++ b/pythonforandroid/recipes/ndghttpsclient @@ -1,9 +1,9 @@ from pythonforandroid.recipe import PythonRecipe class NdgHttpsClientRecipe(PythonRecipe): - version = '0.4.0' + version = '0.5.1' url = 'https://pypi.python.org/packages/source/n/ndg-httpsclient/ndg_httpsclient-{version}.tar.gz' - depends = ['python2', 'pyopenssl', 'cryptography'] + depends = ['python3', 'pyopenssl', 'cryptography'] call_hostpython_via_targetpython = False recipe = NdgHttpsClientRecipe() diff --git a/pythonforandroid/recipes/numpy/__init__.py b/pythonforandroid/recipes/numpy/__init__.py index 1d78e6c083..334886f792 100644 --- a/pythonforandroid/recipes/numpy/__init__.py +++ b/pythonforandroid/recipes/numpy/__init__.py @@ -18,11 +18,6 @@ class NumpyRecipe(CompiledComponentsPythonRecipe): call_hostpython_via_targetpython = False - def apply_patches(self, arch, build_dir=None): - if 'python2' in self.ctx.recipe_build_order: - self.patches.append(join('patches', 'fix-py2-numpy-import.patch')) - super().apply_patches(arch, build_dir=build_dir) - def build_compiled_components(self, arch): self.setup_extra_args = ['-j', str(cpu_count())] super().build_compiled_components(arch) diff --git a/pythonforandroid/recipes/numpy/patches/fix-py2-numpy-import.patch b/pythonforandroid/recipes/numpy/patches/fix-py2-numpy-import.patch deleted file mode 100644 index e75bd37dfd..0000000000 --- a/pythonforandroid/recipes/numpy/patches/fix-py2-numpy-import.patch +++ /dev/null @@ -1,15 +0,0 @@ -There is an open issue at numpy talking about the import error with numpy: - https://github.com/numpy/numpy/issues/13248 -The solution applied here is mentioned there and it seems to only affect our -python2 version. ---- numpy-1.16.4/numpy/core/overrides.py.orig 2019-05-27 12:41:06.000000000 +0200 -+++ numpy-1.16.4/numpy/core/overrides.py 2019-10-13 18:51:06.387037239 +0200 -@@ -140,6 +140,8 @@ def array_function_dispatch(dispatcher, - ------- - Function suitable for decorating the implementation of a NumPy function. - """ -+ if dispatcher.__doc__ is None: -+ dispatcher.__doc__ = "" - - if not ENABLE_ARRAY_FUNCTION: - # __array_function__ requires an explicit opt-in for now diff --git a/pythonforandroid/recipes/opencv/__init__.py b/pythonforandroid/recipes/opencv/__init__.py index e52c20de28..a57089920f 100644 --- a/pythonforandroid/recipes/opencv/__init__.py +++ b/pythonforandroid/recipes/opencv/__init__.py @@ -38,7 +38,7 @@ def get_lib_dir(self, arch): return join(self.get_build_dir(arch.arch), 'build', 'lib', arch.arch) def get_recipe_env(self, arch): - env = super(OpenCVRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) env['ANDROID_NDK'] = self.ctx.ndk_dir env['ANDROID_SDK'] = self.ctx.sdk_dir return env diff --git a/pythonforandroid/recipes/openssl/__init__.py b/pythonforandroid/recipes/openssl/__init__.py index c3ac556b8f..8efe4f8e0f 100644 --- a/pythonforandroid/recipes/openssl/__init__.py +++ b/pythonforandroid/recipes/openssl/__init__.py @@ -93,7 +93,7 @@ def link_flags(self, arch): return self.link_dirs_flags(arch) + self.link_libs_flags() def get_recipe_env(self, arch=None): - env = super(OpenSSLRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) env['OPENSSL_VERSION'] = self.version env['MAKE'] = 'make' # This removes the '-j5', which isn't safe env['ANDROID_NDK'] = self.ctx.ndk_dir diff --git a/pythonforandroid/recipes/pandas/__init__.py b/pythonforandroid/recipes/pandas/__init__.py index 7999d02402..e165b9db74 100644 --- a/pythonforandroid/recipes/pandas/__init__.py +++ b/pythonforandroid/recipes/pandas/__init__.py @@ -8,7 +8,6 @@ class PandasRecipe(CppCompiledComponentsPythonRecipe): url = 'https://github.com/pandas-dev/pandas/releases/download/v{version}/pandas-{version}.tar.gz' # noqa depends = ['cython', 'numpy', 'pytz', 'libbz2', 'liblzma'] - conflicts = ['python2'] python_depends = ['python-dateutil'] patches = ['fix_numpy_includes.patch'] @@ -16,7 +15,7 @@ class PandasRecipe(CppCompiledComponentsPythonRecipe): call_hostpython_via_targetpython = False def get_recipe_env(self, arch): - env = super(PandasRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) # we need the includes from our installed numpy at site packages # because we need some includes generated at numpy's compile time env['NUMPY_INCLUDES'] = join( diff --git a/pythonforandroid/recipes/pil/__init__.py b/pythonforandroid/recipes/pil/__init__.py index 92f633714f..46bace12b7 100644 --- a/pythonforandroid/recipes/pil/__init__.py +++ b/pythonforandroid/recipes/pil/__init__.py @@ -17,7 +17,7 @@ def build_arch(self, arch): 'This should be a drop-in replacement.') warning('It is recommended to change "pil" to "pillow" in your requirements, ' 'to ensure future compatibility') - super(PilRecipe, self).build_arch(arch) + super().build_arch(arch) recipe = PilRecipe() diff --git a/pythonforandroid/recipes/protobuf_cpp/__init__.py b/pythonforandroid/recipes/protobuf_cpp/__init__.py index 36f73def13..9bde198376 100644 --- a/pythonforandroid/recipes/protobuf_cpp/__init__.py +++ b/pythonforandroid/recipes/protobuf_cpp/__init__.py @@ -25,7 +25,7 @@ class ProtobufCppRecipe(CppCompiledComponentsPythonRecipe): protoc_dir = None def prebuild_arch(self, arch): - super(ProtobufCppRecipe, self).prebuild_arch(arch) + super().prebuild_arch(arch) patch_mark = join(self.get_build_dir(arch.arch), '.protobuf-patched') if self.ctx.python_recipe.name == 'python3' and not exists(patch_mark): @@ -129,7 +129,7 @@ def install_python_package(self, arch): ).close() def get_recipe_env(self, arch): - env = super(ProtobufCppRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) if self.protoc_dir is not None: # we need protoc with binary for host platform env['PROTOC'] = join(self.protoc_dir, 'bin', 'protoc') diff --git a/pythonforandroid/recipes/psycopg2/__init__.py b/pythonforandroid/recipes/psycopg2/__init__.py index fd15790f96..20187ead47 100644 --- a/pythonforandroid/recipes/psycopg2/__init__.py +++ b/pythonforandroid/recipes/psycopg2/__init__.py @@ -26,7 +26,7 @@ def prebuild_arch(self, arch): 'setup.py') def get_recipe_env(self, arch): - env = super(Psycopg2Recipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) env['LDFLAGS'] = "{} -L{}".format(env['LDFLAGS'], self.ctx.get_libs_dir(arch.arch)) env['EXTRA_CFLAGS'] = "--host linux-armv" return env diff --git a/pythonforandroid/recipes/pycrypto/__init__.py b/pythonforandroid/recipes/pycrypto/__init__.py index 8f70df28e9..ae2a375df4 100644 --- a/pythonforandroid/recipes/pycrypto/__init__.py +++ b/pythonforandroid/recipes/pycrypto/__init__.py @@ -10,13 +10,13 @@ class PyCryptoRecipe(CompiledComponentsPythonRecipe): version = '2.7a1' url = 'https://github.com/dlitz/pycrypto/archive/v{version}.zip' - depends = ['openssl', ('python2', 'python3')] + depends = ['openssl', 'python3'] site_packages_name = 'Crypto' call_hostpython_via_targetpython = False patches = ['add_length.patch'] def get_recipe_env(self, arch=None): - env = super(PyCryptoRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) openssl_recipe = Recipe.get_recipe('openssl', self.ctx) env['CC'] = env['CC'] + openssl_recipe.include_flags(arch) @@ -38,7 +38,7 @@ def build_compiled_components(self, arch): shprint(configure, '--host=arm-eabi', '--prefix={}'.format(self.ctx.get_python_install_dir()), '--enable-shared', _env=env) - super(PyCryptoRecipe, self).build_compiled_components(arch) + super().build_compiled_components(arch) recipe = PyCryptoRecipe() diff --git a/pythonforandroid/recipes/pyicu/__init__.py b/pythonforandroid/recipes/pyicu/__init__.py index 37969079f8..17b7c619b3 100644 --- a/pythonforandroid/recipes/pyicu/__init__.py +++ b/pythonforandroid/recipes/pyicu/__init__.py @@ -10,7 +10,7 @@ class PyICURecipe(CppCompiledComponentsPythonRecipe): patches = ['locale.patch'] def get_recipe_env(self, arch): - env = super(PyICURecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) icu_include = join( self.ctx.get_python_install_dir(), "include", "icu") diff --git a/pythonforandroid/recipes/pyjnius/__init__.py b/pythonforandroid/recipes/pyjnius/__init__.py index 12f3717872..206d851a7f 100644 --- a/pythonforandroid/recipes/pyjnius/__init__.py +++ b/pythonforandroid/recipes/pyjnius/__init__.py @@ -18,7 +18,7 @@ class PyjniusRecipe(CythonRecipe): ('genericndkbuild_jnienv_getter.patch', will_build('genericndkbuild'))] def postbuild_arch(self, arch): - super(PyjniusRecipe, self).postbuild_arch(arch) + super().postbuild_arch(arch) info('Copying pyjnius java class to classes build dir') with current_directory(self.get_build_dir(arch.arch)): shprint(sh.cp, '-a', join('jnius', 'src', 'org'), self.ctx.javaclass_dir) diff --git a/pythonforandroid/recipes/pyleveldb/__init__.py b/pythonforandroid/recipes/pyleveldb/__init__.py index 60b86bb18b..54dfb64657 100644 --- a/pythonforandroid/recipes/pyleveldb/__init__.py +++ b/pythonforandroid/recipes/pyleveldb/__init__.py @@ -10,7 +10,7 @@ class PyLevelDBRecipe(CppCompiledComponentsPythonRecipe): site_packages_name = 'leveldb' def get_recipe_env(self, arch): - env = super(PyLevelDBRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) snappy_recipe = self.get_recipe('snappy', self.ctx) leveldb_recipe = self.get_recipe('leveldb', self.ctx) diff --git a/pythonforandroid/recipes/pymunk/__init__.py b/pythonforandroid/recipes/pymunk/__init__.py index 5f027ab0fb..391ae9b743 100644 --- a/pythonforandroid/recipes/pymunk/__init__.py +++ b/pythonforandroid/recipes/pymunk/__init__.py @@ -10,7 +10,7 @@ class PymunkRecipe(CompiledComponentsPythonRecipe): call_hostpython_via_targetpython = False def get_recipe_env(self, arch): - env = super(PymunkRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() env['LDFLAGS'] += " -shared -llog" env['LDFLAGS'] += ' -L{}'.format(join(self.ctx.ndk_platform, 'usr', 'lib')) diff --git a/pythonforandroid/recipes/pynacl/__init__.py b/pythonforandroid/recipes/pynacl/__init__.py index eb9ca2df50..0ab9352eeb 100644 --- a/pythonforandroid/recipes/pynacl/__init__.py +++ b/pythonforandroid/recipes/pynacl/__init__.py @@ -7,11 +7,11 @@ class PyNaCLRecipe(CompiledComponentsPythonRecipe): version = '1.3.0' url = 'https://pypi.python.org/packages/source/P/PyNaCl/PyNaCl-{version}.tar.gz' - depends = [('hostpython2', 'hostpython3'), 'six', 'setuptools', 'cffi', 'libsodium'] + depends = ['hostpython3', 'six', 'setuptools', 'cffi', 'libsodium'] call_hostpython_via_targetpython = False def get_recipe_env(self, arch): - env = super(PyNaCLRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) env['SODIUM_INSTALL'] = 'system' libsodium_build_dir = self.get_recipe( diff --git a/pythonforandroid/recipes/pysha3/__init__.py b/pythonforandroid/recipes/pysha3/__init__.py index c171c3f662..af389460ff 100644 --- a/pythonforandroid/recipes/pysha3/__init__.py +++ b/pythonforandroid/recipes/pysha3/__init__.py @@ -10,7 +10,7 @@ class Pysha3Recipe(PythonRecipe): call_hostpython_via_targetpython = False def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(Pysha3Recipe, self).get_recipe_env(arch, with_flags_in_cc) + env = super().get_recipe_env(arch, with_flags_in_cc) # CFLAGS may only be used to specify C compiler flags, for macro definitions use CPPFLAGS env['CPPFLAGS'] = env['CFLAGS'] env['CFLAGS'] = '' diff --git a/pythonforandroid/recipes/python2/__init__.py b/pythonforandroid/recipes/python2/__init__.py deleted file mode 100644 index 0f14e5e617..0000000000 --- a/pythonforandroid/recipes/python2/__init__.py +++ /dev/null @@ -1,83 +0,0 @@ -from os.path import join, exists -from pythonforandroid.recipe import Recipe -from pythonforandroid.python import GuestPythonRecipe -from pythonforandroid.logger import shprint, warning -import sh - - -class Python2Recipe(GuestPythonRecipe): - ''' - The python2's recipe. - - .. note:: This recipe can be built only against API 21+ - - .. versionchanged:: 0.6.0 - Updated to version 2.7.15 and the build process has been changed in - favour of the recently added class - :class:`~pythonforandroid.python.GuestPythonRecipe` - ''' - version = "2.7.15" - url = 'https://www.python.org/ftp/python/{version}/Python-{version}.tgz' - name = 'python2' - - depends = ['hostpython2', 'libffi'] - conflicts = ['python3'] - - patches = [ - # new 2.7.15 patches - # ('patches/fix-api-minor-than-21.patch', - # is_api_lt(21)), # Todo: this should be tested - 'patches/fix-missing-extensions.patch', - 'patches/fix-filesystem-default-encoding.patch', - 'patches/fix-gethostbyaddr.patch', - 'patches/fix-posix-declarations.patch', - 'patches/fix-pwd-gecos.patch', - 'patches/fix-ctypes-util-find-library.patch', - 'patches/fix-interpreter-version.patch', - 'patches/fix-zlib-version.patch', - ] - - configure_args = ('--host={android_host}', - '--build={android_build}', - '--enable-shared', - '--disable-ipv6', - '--disable-toolbox-glue', - '--disable-framework', - 'ac_cv_file__dev_ptmx=yes', - 'ac_cv_file__dev_ptc=no', - '--without-ensurepip', - 'ac_cv_little_endian_double=yes', - 'ac_cv_header_langinfo_h=no', - '--prefix={prefix}', - '--exec-prefix={exec_prefix}') - - compiled_extension = '.pyo' - - def prebuild_arch(self, arch): - super(Python2Recipe, self).prebuild_arch(arch) - patch_mark = join(self.get_build_dir(arch.arch), '.openssl-patched') - if 'openssl' in self.ctx.recipe_build_order and not exists(patch_mark): - self.apply_patch(join('patches', 'enable-openssl.patch'), arch.arch) - shprint(sh.touch, patch_mark) - - def build_arch(self, arch): - warning('DEPRECATION: Support for the Python 2 recipe will be ' - 'removed in 2020, please upgrade to Python 3.') - super().build_arch(arch) - - def set_libs_flags(self, env, arch): - env = super(Python2Recipe, self).set_libs_flags(env, arch) - if 'libffi' in self.ctx.recipe_build_order: - # For python2 we need to tell configure that we want to use our - # compiled libffi, this step is not necessary for python3. - self.configure_args += ('--with-system-ffi',) - - if 'openssl' in self.ctx.recipe_build_order: - recipe = Recipe.get_recipe('openssl', self.ctx) - openssl_build = recipe.get_build_dir(arch.arch) - env['OPENSSL_BUILD'] = openssl_build - env['OPENSSL_VERSION'] = recipe.version - return env - - -recipe = Python2Recipe() diff --git a/pythonforandroid/recipes/python2/patches/enable-openssl.patch b/pythonforandroid/recipes/python2/patches/enable-openssl.patch deleted file mode 100644 index 490e065c5f..0000000000 --- a/pythonforandroid/recipes/python2/patches/enable-openssl.patch +++ /dev/null @@ -1,46 +0,0 @@ ---- Python-2.7.15.orig/setup.py 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/setup.py 2018-07-05 11:08:57.305125432 +0200 -@@ -812,18 +840,15 @@ class PyBuildExt(build_ext): - '/usr/local/ssl/include', - '/usr/contrib/ssl/include/' - ] -- ssl_incs = find_file('openssl/ssl.h', inc_dirs, -- search_for_ssl_incs_in -- ) -+ ssl_incs = [ -+ os.path.join(os.environ["OPENSSL_BUILD"], 'include'), -+ os.path.join(os.environ["OPENSSL_BUILD"], 'include', 'openssl')] - if ssl_incs is not None: - krb5_h = find_file('krb5.h', inc_dirs, - ['/usr/kerberos/include']) - if krb5_h: - ssl_incs += krb5_h -- ssl_libs = find_library_file(self.compiler, 'ssl',lib_dirs, -- ['/usr/local/ssl/lib', -- '/usr/contrib/ssl/lib/' -- ] ) -+ ssl_libs = [os.environ["OPENSSL_BUILD"]] - - if (ssl_incs is not None and - ssl_libs is not None): -@@ -841,8 +866,8 @@ class PyBuildExt(build_ext): - '^\s*#\s*define\s+OPENSSL_VERSION_NUMBER\s+(0x[0-9a-fA-F]+)' ) - - # look for the openssl version header on the compiler search path. -- opensslv_h = find_file('openssl/opensslv.h', [], -- inc_dirs + search_for_ssl_incs_in) -+ opensslv_h = [os.path.join(os.environ["OPENSSL_BUILD"], 'include'), -+ os.path.join(os.environ["OPENSSL_BUILD"], 'include', 'openssl')] - if opensslv_h: - name = os.path.join(opensslv_h[0], 'openssl/opensslv.h') - if host_platform == 'darwin' and is_macosx_sdk_path(name): -@@ -859,8 +884,7 @@ class PyBuildExt(build_ext): - - min_openssl_ver = 0x00907000 - have_any_openssl = ssl_incs is not None and ssl_libs is not None -- have_usable_openssl = (have_any_openssl and -- openssl_ver >= min_openssl_ver) -+ have_usable_openssl = (have_any_openssl and True) - - if have_any_openssl: - if have_usable_openssl: diff --git a/pythonforandroid/recipes/python2/patches/fix-api-minor-than-21.patch b/pythonforandroid/recipes/python2/patches/fix-api-minor-than-21.patch deleted file mode 100644 index 73dfc981ec..0000000000 --- a/pythonforandroid/recipes/python2/patches/fix-api-minor-than-21.patch +++ /dev/null @@ -1,229 +0,0 @@ -diff -Naurp Python-2.7.15.orig/configure.ac Python-2.7.15/configure.ac ---- Python-2.7.15.orig/configure.ac 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/configure.ac 2018-07-05 17:44:50.500985727 +0200 -@@ -1790,7 +1790,7 @@ fi - # structures (such as rlimit64) without declaring them. As a - # work-around, disable LFS on such configurations - --use_lfs=yes -+use_lfs=no - AC_MSG_CHECKING(Solaris LFS bug) - AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ - #define _LARGEFILE_SOURCE 1 -diff -Naurp Python-2.7.15.orig/Modules/mmapmodule.c Python-2.7.15/Modules/mmapmodule.c ---- Python-2.7.15.orig/Modules/mmapmodule.c 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/Modules/mmapmodule.c 2018-07-05 16:18:40.953035027 +0200 -@@ -78,6 +78,12 @@ my_getpagesize(void) - # define MAP_ANONYMOUS MAP_ANON - #endif - -+//PMPP API<21 -+#if defined(__ANDROID_API__) && __ANDROID_API__ < 21 -+ extern void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); -+#endif -+//PMPP API<21 -+ - static PyObject *mmap_module_error; - - typedef enum -diff -Naurp Python-2.7.15.orig/Modules/posixmodule.c Python-2.7.15/Modules/posixmodule.c ---- Python-2.7.15.orig/Modules/posixmodule.c 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/Modules/posixmodule.c 2018-07-05 16:20:48.933033807 +0200 -@@ -9477,6 +9477,12 @@ all_ins(PyObject *d) - #define MODNAME "posix" - #endif - -+//PMPP API<21 -+#if defined(__ANDROID_API__) && __ANDROID_API__ < 21 -+ extern ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); -+#endif -+//PMPP API<21 -+ - PyMODINIT_FUNC - INITFUNC(void) - { -diff -Naurp Python-2.7.15.orig/Modules/signalmodule.c Python-2.7.15/Modules/signalmodule.c ---- Python-2.7.15.orig/Modules/signalmodule.c 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/Modules/signalmodule.c 2018-07-05 16:40:46.601022385 +0200 -@@ -32,6 +32,13 @@ - #include - #endif - -+//PMPP API<21 -+#if defined(__ANDROID_API__) && __ANDROID_API__ < 21 -+ #define SIGRTMIN 32 -+ #define SIGRTMAX _NSIG -+#endif -+//PMPP API<21 -+ - #ifndef NSIG - # if defined(_NSIG) - # define NSIG _NSIG /* For BSD/SysV */ -diff -Naurp Python-2.7.15.orig/Modules/termios.c Python-2.7.15/Modules/termios.c ---- Python-2.7.15.orig/Modules/termios.c 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/Modules/termios.c 2018-07-05 16:43:16.457020956 +0200 -@@ -357,7 +357,11 @@ static struct constant { - #endif - - /* tcsetattr() constants */ -+#if defined(__ANDROID_API__) && __ANDROID_API__ > 0 -+ {"TCSANOW", TCSETS}, // https://github.com/android-ndk/ndk/issues/441 -+#else - {"TCSANOW", TCSANOW}, -+#endif - {"TCSADRAIN", TCSADRAIN}, - {"TCSAFLUSH", TCSAFLUSH}, - #ifdef TCSASOFT -diff -Naurp Python-2.7.15.orig/Objects/obmalloc.c Python-2.7.15/Objects/obmalloc.c ---- Python-2.7.15.orig/Objects/obmalloc.c 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/Objects/obmalloc.c 2018-07-05 16:52:27.577015700 +0200 -@@ -1,5 +1,11 @@ - #include "Python.h" - -+//PMPP API<21 -+#if __ANDROID_API__ < 21 -+ extern void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); -+#endif -+//PMPP API<21 -+ - #if defined(__has_feature) /* Clang */ - #if __has_feature(address_sanitizer) /* is ASAN enabled? */ - #define ATTRIBUTE_NO_ADDRESS_SAFETY_ANALYSIS \ -############################################################### -######### ANDROID LOCALE PATCHES FOR ANDROID API < 21 ######### -############################################################### ---- Python-2.7.15.orig/Modules/_localemodule.c 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/Modules/_localemodule.c 2018-07-05 16:39:08.241023323 +0200 -@@ -170,6 +170,12 @@ PyLocale_setlocale(PyObject* self, PyObj - PyErr_SetString(Error, "invalid locale category"); - return NULL; - } -+#else -+ #ifdef __ANDROID__ -+ #if defined(__ANDROID_API__) && __ANDROID_API__ < 20 -+ return PyUnicode_FromFormat("%s", "C"); -+ #endif -+ #endif - #endif - - if (locale) { -@@ -215,7 +221,15 @@ PyLocale_localeconv(PyObject* self) - return NULL; - - /* if LC_NUMERIC is different in the C library, use saved value */ -- l = localeconv(); -+ //PMPP API<21 -+ #if defined(__ANDROID_API__) && __ANDROID_API__ < 21 -+ /* Don't even try on Android's broken locale.h. */ -+ goto failed; -+ #else -+ /* if LC_NUMERIC is different in the C library, use saved value */ -+ l = localeconv(); //PATCHED -+ #endif -+ //PMPP API<21 - - /* hopefully, the localeconv result survives the C library calls - involved herein */ -@@ -215,7 +215,11 @@ PyLocale_localeconv(PyObject* self) - return NULL; - - /* if LC_NUMERIC is different in the C library, use saved value */ -+#if defined(__ANDROID_API__) && __ANDROID_API__ >= 21 - l = localeconv(); -+#else -+ decimal_point = "."; -+#endif - - /* hopefully, the localeconv result survives the C library calls - involved herein */ ---- Python-2.7.15/Objects/stringlib/formatter.h.orig 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/Objects/stringlib/formatter.h 2018-12-26 11:37:08.771315390 +0100 -@@ -640,11 +640,17 @@ get_locale_info(int type, LocaleInfo *lo - { - switch (type) { - case LT_CURRENT_LOCALE: { -+#if defined(__ANDROID_API__) && __ANDROID_API__ >= 21 -+/* NDK version for SDK below 21 doesn't implement the whole C++11 standard in -+ the STL. locale.h header has stubs for localeconv() method, but the library -+ doesn't implement it. The closest Android SDK that implement localeconv() -+ is SDK 21*/ - struct lconv *locale_data = localeconv(); - locale_info->decimal_point = locale_data->decimal_point; - locale_info->thousands_sep = locale_data->thousands_sep; - locale_info->grouping = locale_data->grouping; - break; -+#endif - } - case LT_DEFAULT_LOCALE: - locale_info->decimal_point = "."; ---- Python-2.7.15/Objects/stringlib/localeutil.h.orig 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/Objects/stringlib/localeutil.h 2018-12-26 11:38:10.003314806 +0100 -@@ -202,9 +202,18 @@ _Py_InsertThousandsGroupingLocale(STRING - Py_ssize_t n_digits, - Py_ssize_t min_width) - { -+#if defined(__ANDROID_API__) && __ANDROID_API__ >= 21 -+/* NDK version for SDK below 21 doesn't implement the whole C++11 standard in -+ the STL. locale.h header has stubs for localeconv() method, but the library -+ doesn't implement it. The closest Android SDK that implement localeconv() -+ is SDK 21*/ - struct lconv *locale_data = localeconv(); - const char *grouping = locale_data->grouping; - const char *thousands_sep = locale_data->thousands_sep; -+#else -+ const char *grouping = "\3\0"; -+ const char *thousands_sep = ","; -+#endif - - return _Py_InsertThousandsGrouping(buffer, n_buffer, digits, n_digits, - min_width, grouping, thousands_sep); ---- Python-2.7.15/Python/pystrtod.c.orig 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/Python/pystrtod.c 2018-12-26 11:47:54.723309229 +0100 -@@ -126,7 +126,13 @@ _PyOS_ascii_strtod(const char *nptr, cha - { - char *fail_pos; - double val = -1.0; -+#if defined(__ANDROID_API__) && __ANDROID_API__ >= 21 -+/* NDK version for SDK below 21 doesn't implement the whole C++11 standard in -+ the STL. locale.h header has stubs for localeconv() method, but the library -+ doesn't implement it. The closest Android SDK that implement localeconv() -+ is SDK 21*/ - struct lconv *locale_data; -+#endif - const char *decimal_point; - size_t decimal_point_len; - const char *p, *decimal_point_pos; -@@ -138,8 +144,16 @@ _PyOS_ascii_strtod(const char *nptr, cha - - fail_pos = NULL; - -+#if defined(__ANDROID_API__) && __ANDROID_API__ >= 21 -+/* NDK version for SDK below 21 doesn't implement the whole C++11 standard in -+ the STL. locale.h header has stubs for localeconv() method, but the library -+ doesn't implement it. The closest Android SDK that implement localeconv() -+ is SDK 21*/ - locale_data = localeconv(); - decimal_point = locale_data->decimal_point; -+#else -+ decimal_point = "."; -+#endif - decimal_point_len = strlen(decimal_point); - - assert(decimal_point_len != 0); -@@ -375,8 +389,16 @@ PyOS_string_to_double(const char *s, - Py_LOCAL_INLINE(void) - change_decimal_from_locale_to_dot(char* buffer) - { -+#if defined(__ANDROID_API__) && __ANDROID_API__ >= 21 -+/* NDK version for SDK below 21 doesn't implement the whole C++11 standard in -+ the STL. locale.h header has stubs for localeconv() method, but the library -+ doesn't implement it. The closest Android SDK that implement localeconv() -+ is SDK 21*/ - struct lconv *locale_data = localeconv(); - const char *decimal_point = locale_data->decimal_point; -+#else -+ decimal_point = "."; -+#endif - - if (decimal_point[0] != '.' || decimal_point[1] != 0) { - size_t decimal_point_len = strlen(decimal_point); diff --git a/pythonforandroid/recipes/python2/patches/fix-ctypes-util-find-library.patch b/pythonforandroid/recipes/python2/patches/fix-ctypes-util-find-library.patch deleted file mode 100644 index 4f29b047ad..0000000000 --- a/pythonforandroid/recipes/python2/patches/fix-ctypes-util-find-library.patch +++ /dev/null @@ -1,19 +0,0 @@ -diff -u a/Lib/ctypes/util.py b/Lib/ctypes/util.py ---- a/Lib/ctypes/util.py 2019-06-20 18:52:01.372205226 +0200 -+++ b/Lib/ctypes/util.py 2019-06-20 18:51:39.612970779 +0200 -@@ -70,7 +70,14 @@ - def find_library(name): - return name - --if os.name == "posix" and sys.platform == "darwin": -+# This patch overrides the find_library to look in the right places on -+# Android -+if True: -+ from android._ctypes_library_finder import find_library as _find_lib -+ def find_library(name): -+ return _find_lib(name) -+ -+elif os.name == "posix" and sys.platform == "darwin": - from ctypes.macholib.dyld import dyld_find as _dyld_find - def find_library(name): - possible = ['lib%s.dylib' % name, diff --git a/pythonforandroid/recipes/python2/patches/fix-filesystem-default-encoding.patch b/pythonforandroid/recipes/python2/patches/fix-filesystem-default-encoding.patch deleted file mode 100644 index 7cf1a8f860..0000000000 --- a/pythonforandroid/recipes/python2/patches/fix-filesystem-default-encoding.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- Python-2.7.15.orig/Python/bltinmodule.c 2017-12-29 01:44:57.018079845 +0200 -+++ Python-2.7.15/Python/bltinmodule.c 2017-12-29 01:45:02.650079649 +0200 -@@ -22,7 +22,7 @@ - #elif defined(__APPLE__) - const char *Py_FileSystemDefaultEncoding = "utf-8"; - #else --const char *Py_FileSystemDefaultEncoding = NULL; /* use default */ -+const char *Py_FileSystemDefaultEncoding = "utf-8"; /* use default */ - #endif - - /* Forward */ diff --git a/pythonforandroid/recipes/python2/patches/fix-gethostbyaddr.patch b/pythonforandroid/recipes/python2/patches/fix-gethostbyaddr.patch deleted file mode 100644 index a3824f4348..0000000000 --- a/pythonforandroid/recipes/python2/patches/fix-gethostbyaddr.patch +++ /dev/null @@ -1,12 +0,0 @@ ---- Python-2.7.15/Modules/socketmodule.c.orig 2017-12-29 01:40:09.915694810 +0100 -+++ Python-2.7.15/Modules/socketmodule.c 2017-12-29 01:40:36.967694486 +0100 -@@ -156,6 +156,9 @@ - On the other hand, not all Linux versions agree, so there the settings - computed by the configure script are needed! */ - -+/* Android hack, same reason are what is described above */ -+#undef HAVE_GETHOSTBYNAME_R -+ - #ifndef linux - # undef HAVE_GETHOSTBYNAME_R_3_ARG - # undef HAVE_GETHOSTBYNAME_R_5_ARG diff --git a/pythonforandroid/recipes/python2/patches/fix-interpreter-version.patch b/pythonforandroid/recipes/python2/patches/fix-interpreter-version.patch deleted file mode 100644 index a1828bf582..0000000000 --- a/pythonforandroid/recipes/python2/patches/fix-interpreter-version.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- Python-2.7.15/configure.orig 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/configure 2019-08-16 14:09:48.696030207 +0200 -@@ -2903,7 +2903,7 @@ case $host_os in *\ *) host_os=`echo "$h - # pybuilddir.txt will be created by --generate-posix-vars in the Makefile - rm -f pybuilddir.txt - --for ac_prog in python$PACKAGE_VERSION python3 python -+for ac_prog in python$PACKAGE_VERSION python2 python - do - # Extract the first word of "$ac_prog", so it can be a program name with args. - set dummy $ac_prog; ac_word=$2 diff --git a/pythonforandroid/recipes/python2/patches/fix-missing-extensions.patch b/pythonforandroid/recipes/python2/patches/fix-missing-extensions.patch deleted file mode 100644 index cb777d3b1f..0000000000 --- a/pythonforandroid/recipes/python2/patches/fix-missing-extensions.patch +++ /dev/null @@ -1,108 +0,0 @@ -diff -Naurp Python-2.7.15.orig/Makefile.pre.in Python-2.7.15/Makefile.pre.in ---- Python-2.7.15.orig/Makefile.pre.in 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/Makefile.pre.in 2018-11-18 00:43:58.777379280 +0100 -@@ -20,6 +20,7 @@ - - # === Variables set by makesetup === - -+MODNAMES= _MODNAMES_ - MODOBJS= _MODOBJS_ - MODLIBS= _MODLIBS_ - -diff -Naurp Python-2.7.15.orig/Modules/_ctypes/libffi/src/arm/sysv.S Python-2.7.15/Modules/_ctypes/libffi/src/arm/sysv.S ---- Python-2.7.15.orig/Modules/_ctypes/libffi/src/arm/sysv.S 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/Modules/_ctypes/libffi/src/arm/sysv.S 2018-11-17 22:28:50.925456603 +0100 -@@ -396,7 +396,7 @@ LSYM(Lbase_args): - beq LSYM(Lepilogue_vfp) - - cmp r3, #FFI_TYPE_SINT64 -- stmeqia r2, {r0, r1} -+ stmiaeq r2, {r0, r1} - beq LSYM(Lepilogue_vfp) - - cmp r3, #FFI_TYPE_FLOAT -diff -Naurp Python-2.7.15.orig/Modules/makesetup Python-2.7.15/Modules/makesetup ---- Python-2.7.15.orig/Modules/makesetup 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/Modules/makesetup 2018-11-18 00:43:10.289379743 +0100 -@@ -110,6 +110,7 @@ sed -e 's/[ ]*#.*//' -e '/^[ ]*$/d' | - # Rules appended by makedepend - " >$rulesf - DEFS= -+ NAMES= - MODS= - SHAREDMODS= - OBJS= -@@ -181,7 +182,7 @@ sed -e 's/[ ]*#.*//' -e '/^[ ]*$/d' | - *.*) echo 1>&2 "bad word $arg in $line" - exit 1;; - -u) skip=libs; libs="$libs -u";; -- [a-zA-Z_]*) mods="$mods $arg";; -+ [a-zA-Z_]*) NAMES="$NAMES $arg"; mods="$mods $arg";; - *) echo 1>&2 "bad word $arg in $line" - exit 1;; - esac -@@ -284,6 +285,7 @@ sed -e 's/[ ]*#.*//' -e '/^[ ]*$/d' | - echo "1i\\" >$sedf - str="# Generated automatically from $makepre by makesetup." - echo "$str" >>$sedf -+ echo "s%_MODNAMES_%$NAMES%" >>$sedf - echo "s%_MODOBJS_%$OBJS%" >>$sedf - echo "s%_MODLIBS_%$LIBS%" >>$sedf - echo "/Definitions added by makesetup/a$NL$NL$DEFS" >>$sedf -diff -Naurp Python-2.7.15.orig/setup.py Python-2.7.15/setup.py ---- Python-2.7.15.orig/setup.py 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/setup.py 2018-11-18 00:40:50.021381080 +0100 -@@ -217,7 +217,11 @@ class PyBuildExt(build_ext): - # Python header files - headers = [sysconfig.get_config_h_filename()] - headers += glob(os.path.join(sysconfig.get_path('include'), "*.h")) -- for ext in self.extensions[:]: -+ # The sysconfig variable built by makesetup, listing the already -+ # built modules as configured by the Setup files. -+ modnames = sysconfig.get_config_var('MODNAMES').split() -+ removed_modules = [] -+ for ext in self.extensions: - ext.sources = [ find_module_file(filename, moddirlist) - for filename in ext.sources ] - if ext.depends is not None: -@@ -231,10 +235,10 @@ class PyBuildExt(build_ext): - # platform specific include directories - ext.include_dirs.extend(incdirlist) - -- # If a module has already been built statically, -- # don't build it here -- if ext.name in sys.builtin_module_names: -- self.extensions.remove(ext) -+ # If a module has already been built by the Makefile, -+ # don't build it here. -+ if ext.name in modnames: -+ removed_modules.append(ext) - - # Parse Modules/Setup and Modules/Setup.local to figure out which - # modules are turned on in the file. -@@ -249,8 +253,9 @@ class PyBuildExt(build_ext): - input.close() - - for ext in self.extensions[:]: -- if ext.name in remove_modules: -- self.extensions.remove(ext) -+ if removed_modules: -+ self.extensions = [x for x in self.extensions if x not in -+ removed_modules] - - # When you run "make CC=altcc" or something similar, you really want - # those environment variables passed into the setup.py phase. Here's -@@ -290,6 +295,13 @@ class PyBuildExt(build_ext): - " detect_modules() for the module's name.") - print - -+ if removed_modules: -+ print("The following modules found by detect_modules() in" -+ " setup.py, have been") -+ print("built by the Makefile instead, as configured by the" -+ " Setup files:") -+ print_three_column([ext.name for ext in removed_modules]) -+ - if self.failed: - failed = self.failed[:] - print diff --git a/pythonforandroid/recipes/python2/patches/fix-posix-declarations.patch b/pythonforandroid/recipes/python2/patches/fix-posix-declarations.patch deleted file mode 100644 index c2a80499b9..0000000000 --- a/pythonforandroid/recipes/python2/patches/fix-posix-declarations.patch +++ /dev/null @@ -1,24 +0,0 @@ ---- Python-2.7.15/Modules/posixmodule.c.orig 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/Modules/posixmodule.c 2018-12-26 13:46:37.307241303 +0100 -@@ -35,6 +35,12 @@ - # include - #endif /* defined(__VMS) */ - -+/* On android API level 21, 'AT_EACCESS' is not declared although -+ * HAVE_FACCESSAT is defined. */ -+#ifdef __ANDROID__ -+#undef HAVE_FACCESSAT -+#endif -+ - #ifdef __cplusplus - extern "C" { - #endif -@@ -3991,7 +3997,7 @@ posix_openpty(PyObject *self, PyObject * - slave_fd = open(slave_name, O_RDWR | O_NOCTTY); /* open slave */ - if (slave_fd < 0) - return posix_error(); --#if !defined(__CYGWIN__) && !defined(HAVE_DEV_PTC) -+#if !defined(__CYGWIN__) && !defined(__ANDROID__) && !defined(HAVE_DEV_PTC) - ioctl(slave_fd, I_PUSH, "ptem"); /* push ptem */ - ioctl(slave_fd, I_PUSH, "ldterm"); /* push ldterm */ - #ifndef __hpux diff --git a/pythonforandroid/recipes/python2/patches/fix-pwd-gecos.patch b/pythonforandroid/recipes/python2/patches/fix-pwd-gecos.patch deleted file mode 100644 index cdc06fd4ad..0000000000 --- a/pythonforandroid/recipes/python2/patches/fix-pwd-gecos.patch +++ /dev/null @@ -1,89 +0,0 @@ ---- Python-2.7.15/configure.orig 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/configure 2018-12-26 12:46:08.163275913 +0100 -@@ -12177,6 +12177,32 @@ _ACEOF - - fi - -+ac_fn_c_check_member "$LINENO" "struct passwd" "pw_gecos" "ac_cv_member_struct_passwd_pw_gecos" " -+ #include -+ #include -+ -+" -+if test "x$ac_cv_member_struct_passwd_pw_gecos" = xyes; then : -+ -+cat >>confdefs.h <<_ACEOF -+#define HAVE_STRUCT_PASSWD_PW_GECOS 1 -+_ACEOF -+ -+ -+fi -+ac_fn_c_check_member "$LINENO" "struct passwd" "pw_passwd" "ac_cv_member_struct_passwd_pw_passwd" " -+ #include -+ #include -+ -+" -+if test "x$ac_cv_member_struct_passwd_pw_passwd" = xyes; then : -+ -+cat >>confdefs.h <<_ACEOF -+#define HAVE_STRUCT_PASSWD_PW_PASSWD 1 -+_ACEOF -+ -+ -+fi - - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for time.h that defines altzone" >&5 - $as_echo_n "checking for time.h that defines altzone... " >&6; } ---- Python-2.7.15/configure.ac.orig 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/configure.ac 2018-12-26 12:50:20.679273505 +0100 -@@ -3562,6 +3562,10 @@ AC_CHECK_MEMBERS([struct stat.st_flags]) - AC_CHECK_MEMBERS([struct stat.st_gen]) - AC_CHECK_MEMBERS([struct stat.st_birthtime]) - AC_CHECK_MEMBERS([struct stat.st_blocks]) -+AC_CHECK_MEMBERS([struct passwd.pw_gecos, struct passwd.pw_passwd], [], [], [[ -+ #include -+ #include -+]]) - - AC_MSG_CHECKING(for time.h that defines altzone) - AC_CACHE_VAL(ac_cv_header_time_altzone,[ ---- Python-2.7.15/pyconfig.h.in.orig 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/pyconfig.h.in 2018-12-26 12:52:13.247272432 +0100 -@@ -737,6 +737,12 @@ - /* Define to 1 if you have the header file. */ - #undef HAVE_STROPTS_H - -+/* Define to 1 if `pw_gecos' is a member of `struct passwd'. */ -+#undef HAVE_STRUCT_PASSWD_PW_GECOS -+ -+/* Define to 1 if `pw_passwd' is a member of `struct passwd'. */ -+#undef HAVE_STRUCT_PASSWD_PW_PASSWD -+ - /* Define to 1 if `st_birthtime' is a member of `struct stat'. */ - #undef HAVE_STRUCT_STAT_ST_BIRTHTIME - ---- Python-2.7.15/Modules/pwdmodule.c.orig 2018-04-30 00:47:33.000000000 +0200 -+++ Python-2.7.15/Modules/pwdmodule.c 2018-12-26 12:38:47.611280115 +0100 -@@ -68,17 +68,17 @@ mkpwent(struct passwd *p) - #define SETS(i,val) sets(v, i, val) - - SETS(setIndex++, p->pw_name); --#ifdef __VMS -- SETS(setIndex++, ""); --#else -+#if defined(HAVE_STRUCT_PASSWD_PW_PASSWD) - SETS(setIndex++, p->pw_passwd); -+#else -+ SETS(setIndex++, ""); - #endif - PyStructSequence_SET_ITEM(v, setIndex++, _PyInt_FromUid(p->pw_uid)); - PyStructSequence_SET_ITEM(v, setIndex++, _PyInt_FromGid(p->pw_gid)); --#ifdef __VMS -- SETS(setIndex++, ""); --#else -+#if defined(HAVE_STRUCT_PASSWD_PW_GECOS) - SETS(setIndex++, p->pw_gecos); -+#else -+ SETS(setIndex++, ""); - #endif - SETS(setIndex++, p->pw_dir); - SETS(setIndex++, p->pw_shell); diff --git a/pythonforandroid/recipes/python2/patches/fix-zlib-version.patch b/pythonforandroid/recipes/python2/patches/fix-zlib-version.patch deleted file mode 100644 index b7c14dbf80..0000000000 --- a/pythonforandroid/recipes/python2/patches/fix-zlib-version.patch +++ /dev/null @@ -1,12 +0,0 @@ ---- Python-3.7.1/setup.py.orig 2018-10-20 08:04:19.000000000 +0200 -+++ Python-3.7.1/setup.py 2019-02-17 00:24:30.715904412 +0100 -@@ -1416,7 +1416,8 @@ class PyBuildExt(build_ext): - if zlib_inc is not None: - zlib_h = zlib_inc[0] + '/zlib.h' - version = '"0.0.0"' -- version_req = '"1.1.3"' -+ version_req = '"{}"'.format( -+ os.environ.get('ZLIB_VERSION', '1.1.3')) - if host_platform == 'darwin' and is_macosx_sdk_path(zlib_h): - zlib_h = os.path.join(macosx_sdk_root(), zlib_h[1:]) - with open(zlib_h) as fp: diff --git a/pythonforandroid/recipes/python3/__init__.py b/pythonforandroid/recipes/python3/__init__.py index c80875ccc3..2da78f59ac 100644 --- a/pythonforandroid/recipes/python3/__init__.py +++ b/pythonforandroid/recipes/python3/__init__.py @@ -1,10 +1,24 @@ +import glob import sh -from pythonforandroid.python import GuestPythonRecipe -from pythonforandroid.recipe import Recipe +import subprocess + +from multiprocessing import cpu_count +from os import environ +from os.path import dirname, exists, join, isfile +from shutil import copy2 + +from pythonforandroid.logger import info, warning, shprint from pythonforandroid.patching import version_starts_with +from pythonforandroid.recipe import Recipe, TargetPythonRecipe +from pythonforandroid.util import ( + current_directory, + ensure_dir, + walk_valid_filens, + BuildInterruptingException, +) -class Python3Recipe(GuestPythonRecipe): +class Python3Recipe(TargetPythonRecipe): ''' The python3's recipe ^^^^^^^^^^^^^^^^^^^^ @@ -27,8 +41,10 @@ class Python3Recipe(GuestPythonRecipe): .. note:: This recipe can be built only against API 21+. .. versionchanged:: 2019.10.06.post0 - Added optional dependencies: :mod:`~pythonforandroid.recipes.libbz2` - and :mod:`~pythonforandroid.recipes.liblzma` + - Refactored from deleted class ``python.GuestPythonRecipe`` into here + - Added optional dependencies: :mod:`~pythonforandroid.recipes.libbz2` + and :mod:`~pythonforandroid.recipes.liblzma` + .. versionchanged:: 0.6.0 Refactored into class :class:`~pythonforandroid.python.GuestPythonRecipe` @@ -58,7 +74,7 @@ class Python3Recipe(GuestPythonRecipe): # - _bz2.so # - _lzma.so opt_depends = ['libbz2', 'liblzma'] - conflicts = ['python2'] + '''The optional libraries which we would like to get our python linked''' configure_args = ( '--host={android_host}', @@ -71,14 +87,314 @@ class Python3Recipe(GuestPythonRecipe): 'ac_cv_little_endian_double=yes', '--prefix={prefix}', '--exec-prefix={exec_prefix}') + '''The configure arguments needed to build the python recipe. Those are + used in method :meth:`build_arch` (if not overwritten like python3's + recipe does). + ''' + + MIN_NDK_API = 21 + '''Sets the minimal ndk api number needed to use the recipe. + + .. warning:: This recipe can be built only against API 21+, so it means + that any class which inherits from class:`GuestPythonRecipe` will have + this limitation. + ''' + + stdlib_dir_blacklist = { + '__pycache__', + 'test', + 'tests', + 'lib2to3', + 'ensurepip', + 'idlelib', + 'tkinter', + } + '''The directories that we want to omit for our python bundle''' + + stdlib_filen_blacklist = [ + '*.py', + '*.exe', + '*.whl', + ] + '''The file extensions that we want to blacklist for our python bundle''' + + site_packages_dir_blacklist = { + '__pycache__', + 'tests' + } + '''The directories from site packages dir that we don't want to be included + in our python bundle.''' + + site_packages_filen_blacklist = [ + '*.py' + ] + '''The file extensions from site packages dir that we don't want to be + included in our python bundle.''' + + compiled_extension = '.pyc' + '''the default extension for compiled python files. + + .. note:: the default extension for compiled python files has been .pyo for + python 2.x-3.4 but as of Python 3.5, the .pyo filename extension is no + longer used and has been removed in favour of extension .pyc + ''' + + def __init__(self, *args, **kwargs): + self._ctx = None + super().__init__(*args, **kwargs) + + @property + def _libpython(self): + '''return the python's library name (with extension)''' + py_version = self.major_minor_version_string + if self.major_minor_version_string[0] == '3': + py_version += 'm' + return 'libpython{version}.so'.format(version=py_version) + + def include_root(self, arch_name): + return join(self.get_build_dir(arch_name), 'Include') + + def link_root(self, arch_name): + return join(self.get_build_dir(arch_name), 'android-build') + + def should_build(self, arch): + return not isfile(join(self.link_root(arch.arch), self._libpython)) + + def prebuild_arch(self, arch): + super().prebuild_arch(arch) + self.ctx.python_recipe = self + + def get_recipe_env(self, arch=None, with_flags_in_cc=True): + env = environ.copy() + env['HOSTARCH'] = arch.command_prefix + + env['CC'] = arch.get_clang_exe(with_target=True) + + env['PATH'] = ( + '{hostpython_dir}:{old_path}').format( + hostpython_dir=self.get_recipe( + 'host' + self.name, self.ctx).get_path_to_python(), + old_path=env['PATH']) + + env['CFLAGS'] = ' '.join( + [ + '-fPIC', + '-DANDROID', + '-D__ANDROID_API__={}'.format(self.ctx.ndk_api), + ] + ) + + env['LDFLAGS'] = env.get('LDFLAGS', '') + if sh.which('lld') is not None: + # Note: The -L. is to fix a bug in python 3.7. + # https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234409 + env['LDFLAGS'] += ' -L. -fuse-ld=lld' + else: + warning('lld not found, linking without it. ' + 'Consider installing lld if linker errors occur.') + + return env def set_libs_flags(self, env, arch): - env = super(Python3Recipe, self).set_libs_flags(env, arch) + '''Takes care to properly link libraries with python depending on our + requirements and the attribute :attr:`opt_depends`. + ''' + def add_flags(include_flags, link_dirs, link_libs): + env['CPPFLAGS'] = env.get('CPPFLAGS', '') + include_flags + env['LDFLAGS'] = env.get('LDFLAGS', '') + link_dirs + env['LIBS'] = env.get('LIBS', '') + link_libs + + if 'sqlite3' in self.ctx.recipe_build_order: + info('Activating flags for sqlite3') + recipe = Recipe.get_recipe('sqlite3', self.ctx) + add_flags(' -I' + recipe.get_build_dir(arch.arch), + ' -L' + recipe.get_lib_dir(arch), ' -lsqlite3') + + if 'libffi' in self.ctx.recipe_build_order: + info('Activating flags for libffi') + recipe = Recipe.get_recipe('libffi', self.ctx) + # In order to force the correct linkage for our libffi library, we + # set the following variable to point where is our libffi.pc file, + # because the python build system uses pkg-config to configure it. + env['PKG_CONFIG_PATH'] = recipe.get_build_dir(arch.arch) + add_flags(' -I' + ' -I'.join(recipe.get_include_dirs(arch)), + ' -L' + join(recipe.get_build_dir(arch.arch), '.libs'), + ' -lffi') + if 'openssl' in self.ctx.recipe_build_order: + info('Activating flags for openssl') recipe = Recipe.get_recipe('openssl', self.ctx) self.configure_args += \ ('--with-openssl=' + recipe.get_build_dir(arch.arch),) + add_flags(recipe.include_flags(arch), + recipe.link_dirs_flags(arch), recipe.link_libs_flags()) + + for library_name in {'libbz2', 'liblzma'}: + if library_name in self.ctx.recipe_build_order: + info(f'Activating flags for {library_name}') + recipe = Recipe.get_recipe(library_name, self.ctx) + add_flags(recipe.get_library_includes(arch), + recipe.get_library_ldflags(arch), + recipe.get_library_libs_flag()) + + # python build system contains hardcoded zlib version which prevents + # the build of zlib module, here we search for android's zlib version + # and sets the right flags, so python can be build with android's zlib + info("Activating flags for android's zlib") + zlib_lib_path = join(self.ctx.ndk_platform, 'usr', 'lib') + zlib_includes = join(self.ctx.ndk_dir, 'sysroot', 'usr', 'include') + zlib_h = join(zlib_includes, 'zlib.h') + try: + with open(zlib_h) as fileh: + zlib_data = fileh.read() + except IOError: + raise BuildInterruptingException( + "Could not determine android's zlib version, no zlib.h ({}) in" + " the NDK dir includes".format(zlib_h) + ) + for line in zlib_data.split('\n'): + if line.startswith('#define ZLIB_VERSION '): + break + else: + raise BuildInterruptingException( + 'Could not parse zlib.h...so we cannot find zlib version,' + 'required by python build,' + ) + env['ZLIB_VERSION'] = line.replace('#define ZLIB_VERSION ', '') + add_flags(' -I' + zlib_includes, ' -L' + zlib_lib_path, ' -lz') + return env + def build_arch(self, arch): + if self.ctx.ndk_api < self.MIN_NDK_API: + raise BuildInterruptingException( + 'Target ndk-api is {}, but the python3 recipe supports only' + ' {}+'.format(self.ctx.ndk_api, self.MIN_NDK_API)) + + recipe_build_dir = self.get_build_dir(arch.arch) + + # Create a subdirectory to actually perform the build + build_dir = join(recipe_build_dir, 'android-build') + ensure_dir(build_dir) + + # TODO: Get these dynamically, like bpo-30386 does + sys_prefix = '/usr/local' + sys_exec_prefix = '/usr/local' + + with current_directory(build_dir): + env = self.get_recipe_env(arch) + env = self.set_libs_flags(env, arch) + + android_build = sh.Command( + join(recipe_build_dir, + 'config.guess'))().stdout.strip().decode('utf-8') + + if not exists('config.status'): + shprint( + sh.Command(join(recipe_build_dir, 'configure')), + *(' '.join(self.configure_args).format( + android_host=env['HOSTARCH'], + android_build=android_build, + prefix=sys_prefix, + exec_prefix=sys_exec_prefix)).split(' '), + _env=env) + + shprint( + sh.make, 'all', '-j', str(cpu_count()), + 'INSTSONAME={lib_name}'.format(lib_name=self._libpython), + _env=env + ) + + # TODO: Look into passing the path to pyconfig.h in a + # better way, although this is probably acceptable + sh.cp('pyconfig.h', join(recipe_build_dir, 'Include')) + + def compile_python_files(self, dir): + ''' + Compile the python files (recursively) for the python files inside + a given folder. + + .. note:: python2 compiles the files into extension .pyo, but in + python3, and as of Python 3.5, the .pyo filename extension is no + longer used...uses .pyc (https://www.python.org/dev/peps/pep-0488) + ''' + args = [self.ctx.hostpython] + args += ['-OO', '-m', 'compileall', '-b', '-f', dir] + subprocess.call(args) + + def create_python_bundle(self, dirn, arch): + """ + Create a packaged python bundle in the target directory, by + copying all the modules and standard library to the right + place. + """ + # Todo: find a better way to find the build libs folder + modules_build_dir = join( + self.get_build_dir(arch.arch), + 'android-build', + 'build', + 'lib.linux{}-{}-{}'.format( + '2' if self.version[0] == '2' else '', + arch.command_prefix.split('-')[0], + self.major_minor_version_string + )) + + # Compile to *.pyc/*.pyo the python modules + self.compile_python_files(modules_build_dir) + # Compile to *.pyc/*.pyo the standard python library + self.compile_python_files(join(self.get_build_dir(arch.arch), 'Lib')) + # Compile to *.pyc/*.pyo the other python packages (site-packages) + self.compile_python_files(self.ctx.get_python_install_dir()) + + # Bundle compiled python modules to a folder + modules_dir = join(dirn, 'modules') + c_ext = self.compiled_extension + ensure_dir(modules_dir) + module_filens = (glob.glob(join(modules_build_dir, '*.so')) + + glob.glob(join(modules_build_dir, '*' + c_ext))) + info("Copy {} files into the bundle".format(len(module_filens))) + for filen in module_filens: + info(" - copy {}".format(filen)) + copy2(filen, modules_dir) + + # zip up the standard library + stdlib_zip = join(dirn, 'stdlib.zip') + with current_directory(join(self.get_build_dir(arch.arch), 'Lib')): + stdlib_filens = list(walk_valid_filens( + '.', self.stdlib_dir_blacklist, self.stdlib_filen_blacklist)) + info("Zip {} files into the bundle".format(len(stdlib_filens))) + shprint(sh.zip, stdlib_zip, *stdlib_filens) + + # copy the site-packages into place + ensure_dir(join(dirn, 'site-packages')) + ensure_dir(self.ctx.get_python_install_dir()) + # TODO: Improve the API around walking and copying the files + with current_directory(self.ctx.get_python_install_dir()): + filens = list(walk_valid_filens( + '.', self.site_packages_dir_blacklist, + self.site_packages_filen_blacklist)) + info("Copy {} files into the site-packages".format(len(filens))) + for filen in filens: + info(" - copy {}".format(filen)) + ensure_dir(join(dirn, 'site-packages', dirname(filen))) + copy2(filen, join(dirn, 'site-packages', filen)) + + # copy the python .so files into place + python_build_dir = join(self.get_build_dir(arch.arch), + 'android-build') + python_lib_name = 'libpython' + self.major_minor_version_string + if self.major_minor_version_string[0] == '3': + python_lib_name += 'm' + shprint( + sh.cp, + join(python_build_dir, python_lib_name + '.so'), + join(self.ctx.bootstrap.dist_dir, 'libs', arch.arch) + ) + + info('Renaming .so files to reflect cross-compile') + self.reduce_object_file_names(join(dirn, 'site-packages')) + + return join(dirn, 'site-packages') + recipe = Python3Recipe() diff --git a/pythonforandroid/recipes/pyzbar/__init__.py b/pythonforandroid/recipes/pyzbar/__init__.py index ccfcd9b2ea..32aaf89185 100644 --- a/pythonforandroid/recipes/pyzbar/__init__.py +++ b/pythonforandroid/recipes/pyzbar/__init__.py @@ -13,7 +13,7 @@ class PyZBarRecipe(PythonRecipe): depends = ['setuptools', 'libzbar'] def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(PyZBarRecipe, self).get_recipe_env(arch, with_flags_in_cc) + env = super().get_recipe_env(arch, with_flags_in_cc) libzbar = self.get_recipe('libzbar', self.ctx) libzbar_dir = libzbar.get_build_dir(arch.arch) env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() diff --git a/pythonforandroid/recipes/pyzmq/__init__.py b/pythonforandroid/recipes/pyzmq/__init__.py index 02b2a406a9..d113840aff 100644 --- a/pythonforandroid/recipes/pyzmq/__init__.py +++ b/pythonforandroid/recipes/pyzmq/__init__.py @@ -19,7 +19,7 @@ class PyZMQRecipe(CythonRecipe): '-Izmq/devices'] def get_recipe_env(self, arch=None): - env = super(PyZMQRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) # TODO: fix hardcoded path # This is required to prevent issue with _io.so import. # hostpython = self.get_recipe('hostpython2', self.ctx) @@ -45,7 +45,7 @@ def build_cython_components(self, arch): skip_check_zmq = True """.format(libzmq_prefix).encode()) - return super(PyZMQRecipe, self).build_cython_components(arch) + return super().build_cython_components(arch) with current_directory(self.get_build_dir(arch.arch)): hostpython = sh.Command(self.hostpython_location) diff --git a/pythonforandroid/recipes/reportlab/__init__.py b/pythonforandroid/recipes/reportlab/__init__.py index d5e8001e46..574f26c67b 100644 --- a/pythonforandroid/recipes/reportlab/__init__.py +++ b/pythonforandroid/recipes/reportlab/__init__.py @@ -13,7 +13,7 @@ class ReportLabRecipe(CompiledComponentsPythonRecipe): def prebuild_arch(self, arch): if not self.is_patched(arch): - super(ReportLabRecipe, self).prebuild_arch(arch) + super().prebuild_arch(arch) recipe_dir = self.get_build_dir(arch.arch) # Some versions of reportlab ship with a GPL-licensed font. diff --git a/pythonforandroid/recipes/scrypt/__init__.py b/pythonforandroid/recipes/scrypt/__init__.py index 26b8048a05..7f235396a0 100644 --- a/pythonforandroid/recipes/scrypt/__init__.py +++ b/pythonforandroid/recipes/scrypt/__init__.py @@ -13,7 +13,7 @@ def get_recipe_env(self, arch, with_flags_in_cc=True): """ Adds openssl recipe to include and library path. """ - env = super(ScryptRecipe, self).get_recipe_env(arch, with_flags_in_cc) + env = super().get_recipe_env(arch, with_flags_in_cc) openssl_recipe = self.get_recipe('openssl', self.ctx) env['CFLAGS'] += openssl_recipe.include_flags(arch) env['LDFLAGS'] += ' -L{}'.format(self.ctx.get_libs_dir(arch.arch)) diff --git a/pythonforandroid/recipes/sdl2/__init__.py b/pythonforandroid/recipes/sdl2/__init__.py index 00345e89a5..830e9dde44 100644 --- a/pythonforandroid/recipes/sdl2/__init__.py +++ b/pythonforandroid/recipes/sdl2/__init__.py @@ -13,7 +13,7 @@ class LibSDL2Recipe(BootstrapNDKRecipe): depends = ['sdl2_image', 'sdl2_mixer', 'sdl2_ttf'] def get_recipe_env(self, arch=None, with_flags_in_cc=True, with_python=True): - env = super(LibSDL2Recipe, self).get_recipe_env( + env = super().get_recipe_env( arch=arch, with_flags_in_cc=with_flags_in_cc, with_python=with_python) env['APP_ALLOW_MISSING_DEPS'] = 'true' return env @@ -22,7 +22,12 @@ def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_jni_dir()): - shprint(sh.ndk_build, "V=1", _env=env) + shprint( + sh.ndk_build, + "V=1", + "NDK_DEBUG=" + ("1" if self.ctx.build_as_debuggable else "0"), + _env=env + ) recipe = LibSDL2Recipe() diff --git a/pythonforandroid/recipes/secp256k1/__init__.py b/pythonforandroid/recipes/secp256k1/__init__.py index 338c7b90c3..1b30642312 100644 --- a/pythonforandroid/recipes/secp256k1/__init__.py +++ b/pythonforandroid/recipes/secp256k1/__init__.py @@ -11,8 +11,8 @@ class Secp256k1Recipe(CppCompiledComponentsPythonRecipe): depends = [ 'openssl', - ('hostpython3', 'hostpython2'), - ('python2', 'python3'), + 'hostpython3', + 'python3', 'setuptools', 'libffi', 'cffi', @@ -24,7 +24,7 @@ class Secp256k1Recipe(CppCompiledComponentsPythonRecipe): "pkg-config.patch", "find_lib.patch", "no-download.patch"] def get_recipe_env(self, arch=None): - env = super(Secp256k1Recipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) libsecp256k1 = self.get_recipe('libsecp256k1', self.ctx) libsecp256k1_dir = libsecp256k1.get_build_dir(arch.arch) env['CFLAGS'] += ' -I' + os.path.join(libsecp256k1_dir, 'include') diff --git a/pythonforandroid/recipes/shapely/__init__.py b/pythonforandroid/recipes/shapely/__init__.py index f70876132e..fb3da7caac 100644 --- a/pythonforandroid/recipes/shapely/__init__.py +++ b/pythonforandroid/recipes/shapely/__init__.py @@ -7,11 +7,6 @@ class ShapelyRecipe(CythonRecipe): url = 'https://github.com/Toblerity/Shapely/archive/{version}.tar.gz' depends = ['setuptools', 'libgeos'] - # Actually, this recipe seems to compile/install fine for python2, but it - # fails at runtime when importing module with: - # `[Errno 2] No such file or directory` - conflicts = ['python2'] - call_hostpython_via_targetpython = False # Patch to avoid libgeos check (because it fails), insert environment @@ -23,7 +18,7 @@ class ShapelyRecipe(CythonRecipe): # setup_extra_args = ['sdist'] def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(ShapelyRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) libgeos_install = join(self.get_recipe( 'libgeos', self.ctx).get_build_dir(arch.arch), 'install_target') diff --git a/pythonforandroid/recipes/sqlite3/__init__.py b/pythonforandroid/recipes/sqlite3/__init__.py index cfdcb0f6ab..e1324f3e9c 100644 --- a/pythonforandroid/recipes/sqlite3/__init__.py +++ b/pythonforandroid/recipes/sqlite3/__init__.py @@ -14,20 +14,20 @@ def should_build(self, arch): return not self.has_libs(arch, 'libsqlite3.so') def prebuild_arch(self, arch): - super(Sqlite3Recipe, self).prebuild_arch(arch) + super().prebuild_arch(arch) # Copy the Android make file sh.mkdir('-p', join(self.get_build_dir(arch.arch), 'jni')) shutil.copyfile(join(self.get_recipe_dir(), 'Android.mk'), join(self.get_build_dir(arch.arch), 'jni/Android.mk')) def build_arch(self, arch, *extra_args): - super(Sqlite3Recipe, self).build_arch(arch) + super().build_arch(arch) # Copy the shared library shutil.copyfile(join(self.get_build_dir(arch.arch), 'libs', arch.arch, 'libsqlite3.so'), join(self.ctx.get_libs_dir(arch.arch), 'libsqlite3.so')) def get_recipe_env(self, arch): - env = super(Sqlite3Recipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) env['NDK_PROJECT_PATH'] = self.get_build_dir(arch.arch) return env diff --git a/pythonforandroid/recipes/twisted/__init__.py b/pythonforandroid/recipes/twisted/__init__.py index 11da3094d5..7264aad23c 100644 --- a/pythonforandroid/recipes/twisted/__init__.py +++ b/pythonforandroid/recipes/twisted/__init__.py @@ -1,23 +1,32 @@ +import os +import shutil + from pythonforandroid.recipe import CythonRecipe class TwistedRecipe(CythonRecipe): - version = '19.7.0' + version = '20.3.0' url = 'https://github.com/twisted/twisted/archive/twisted-{version}.tar.gz' depends = ['setuptools', 'zope_interface', 'incremental', 'constantly'] - patches = ['incremental.patch'] + patches = ['incremental.patch', 'remove_tests.patch'] call_hostpython_via_targetpython = False install_in_hostpython = False def prebuild_arch(self, arch): - super(TwistedRecipe, self).prebuild_arch(arch) + super().prebuild_arch(arch) # TODO Need to whitelist tty.pyo and termios.so here - print('Should remove twisted tests etc. here, but skipping for now') + + # remove the unit test dirs + source_dir = os.path.join(self.get_build_dir(arch.arch), 'src/twisted') + for item in os.walk(source_dir): + if os.path.basename(item[0]) == 'test': + full_path = os.path.join(source_dir, item[0]) + shutil.rmtree(full_path, ignore_errors=True) def get_recipe_env(self, arch): - env = super(TwistedRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) # We add BUILDLIB_PATH to PYTHONPATH so twisted can find _io.so env['PYTHONPATH'] = ':'.join([ self.ctx.get_site_packages_dir(), diff --git a/pythonforandroid/recipes/twisted/remove_tests.patch b/pythonforandroid/recipes/twisted/remove_tests.patch new file mode 100644 index 0000000000..492062b984 --- /dev/null +++ b/pythonforandroid/recipes/twisted/remove_tests.patch @@ -0,0 +1,16 @@ +diff --git a/src/twisted/python/_setup.py b/src/twisted/python/_setup.py +index 32cb096c7..a607fef07 100644 +--- a/src/twisted/python/_setup.py ++++ b/src/twisted/python/_setup.py +@@ -160,11 +160,6 @@ class ConditionalExtension(Extension, object): + + # The C extensions used for Twisted. + _EXTENSIONS = [ +- ConditionalExtension( +- "twisted.test.raiser", +- sources=["src/twisted/test/raiser.c"], +- condition=lambda _: _isCPython), +- + ConditionalExtension( + "twisted.internet.iocpreactor.iocpsupport", + sources=[ diff --git a/pythonforandroid/recipes/vlc/__init__.py b/pythonforandroid/recipes/vlc/__init__.py index 66f51b96a1..490c4f2a18 100644 --- a/pythonforandroid/recipes/vlc/__init__.py +++ b/pythonforandroid/recipes/vlc/__init__.py @@ -18,7 +18,7 @@ class VlcRecipe(Recipe): aars = {} # for future use of multiple arch def prebuild_arch(self, arch): - super(VlcRecipe, self).prebuild_arch(arch) + super().prebuild_arch(arch) build_dir = self.get_build_dir(arch.arch) port_dir = join(build_dir, 'vlc-port-android') if self.ENV_LIBVLC_AAR in environ: @@ -50,7 +50,7 @@ def prebuild_arch(self, arch): # _tail=20, _critical=True) def build_arch(self, arch): - super(VlcRecipe, self).build_arch(arch) + super().build_arch(arch) build_dir = self.get_build_dir(arch.arch) port_dir = join(build_dir, 'vlc-port-android') aar = self.aars[arch] diff --git a/pythonforandroid/recipes/zbar/__init__.py b/pythonforandroid/recipes/zbar/__init__.py index 62aa85bbe7..1656895795 100644 --- a/pythonforandroid/recipes/zbar/__init__.py +++ b/pythonforandroid/recipes/zbar/__init__.py @@ -20,7 +20,7 @@ class ZBarRecipe(PythonRecipe): patches = ["zbar-0.10-python-crash.patch"] def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(ZBarRecipe, self).get_recipe_env(arch, with_flags_in_cc) + env = super().get_recipe_env(arch, with_flags_in_cc) libzbar = self.get_recipe('libzbar', self.ctx) libzbar_dir = libzbar.get_build_dir(arch.arch) env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() diff --git a/pythonforandroid/recipes/zbarlight/__init__.py b/pythonforandroid/recipes/zbarlight/__init__.py index 966c7fb241..0f1d718835 100644 --- a/pythonforandroid/recipes/zbarlight/__init__.py +++ b/pythonforandroid/recipes/zbarlight/__init__.py @@ -13,7 +13,7 @@ class ZBarLightRecipe(PythonRecipe): depends = ['setuptools', 'libzbar'] def get_recipe_env(self, arch=None, with_flags_in_cc=True): - env = super(ZBarLightRecipe, self).get_recipe_env(arch, with_flags_in_cc) + env = super().get_recipe_env(arch, with_flags_in_cc) libzbar = self.get_recipe('libzbar', self.ctx) libzbar_dir = libzbar.get_build_dir(arch.arch) env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() diff --git a/pythonforandroid/recipes/zeroconf/__init__.py b/pythonforandroid/recipes/zeroconf/__init__.py index 5ca57084a1..a23bd6e25c 100644 --- a/pythonforandroid/recipes/zeroconf/__init__.py +++ b/pythonforandroid/recipes/zeroconf/__init__.py @@ -3,9 +3,9 @@ class ZeroconfRecipe(PythonRecipe): name = 'zeroconf' - version = '0.17.4' + version = '0.24.5' url = 'https://pypi.python.org/packages/source/z/zeroconf/zeroconf-{version}.tar.gz' - depends = ['setuptools', 'enum34', 'six'] + depends = ['setuptools', 'ifaddr', 'typing;python_version<"3.5"'] call_hostpython_via_targetpython = False diff --git a/pythonforandroid/recipes/zope/__init__.py b/pythonforandroid/recipes/zope/__init__.py index 579a7600c5..9d439cccde 100644 --- a/pythonforandroid/recipes/zope/__init__.py +++ b/pythonforandroid/recipes/zope/__init__.py @@ -11,7 +11,7 @@ class ZopeRecipe(PythonRecipe): depends = [] def get_recipe_env(self, arch): - env = super(ZopeRecipe, self).get_recipe_env(arch) + env = super().get_recipe_env(arch) # These are in the old zope recipe but seem like they shouldn't actually be necessary env['LDFLAGS'] = env['LDFLAGS'] + ' -L{}'.format( @@ -19,7 +19,7 @@ def get_recipe_env(self, arch): env['LDSHARED'] = join(self.ctx.root_dir, 'tools', 'liblink') def postbuild_arch(self, arch): - super(ZopeRecipe, self).postbuild_arch(arch) + super().postbuild_arch(arch) # Should do some deleting here diff --git a/pythonforandroid/recipes/zope_interface/__init__.py b/pythonforandroid/recipes/zope_interface/__init__.py index 245ebc8082..f4142a5b00 100644 --- a/pythonforandroid/recipes/zope_interface/__init__.py +++ b/pythonforandroid/recipes/zope_interface/__init__.py @@ -14,7 +14,7 @@ class ZopeInterfaceRecipe(PythonRecipe): patches = ['no_tests.patch'] def build_arch(self, arch): - super(ZopeInterfaceRecipe, self).build_arch(arch) + super().build_arch(arch) # The zope.interface module lacks of the __init__.py file in one of his # folders (once is installed), that leads into an ImportError. # Here we intentionally apply a patch to solve that, so, in case that @@ -23,7 +23,7 @@ def build_arch(self, arch): self.apply_patch('fix-init.patch', arch.arch, build_dir=zope_install) def prebuild_arch(self, arch): - super(ZopeInterfaceRecipe, self).prebuild_arch(arch) + super().prebuild_arch(arch) with current_directory(self.get_build_dir(arch.arch)): sh.rm( '-rf', diff --git a/pythonforandroid/recommendations.py b/pythonforandroid/recommendations.py index 163e4ae6d5..00caad1484 100644 --- a/pythonforandroid/recommendations.py +++ b/pythonforandroid/recommendations.py @@ -12,7 +12,7 @@ MAX_NDK_VERSION = 20 # DO NOT CHANGE LINE FORMAT: buildozer parses the existence of a RECOMMENDED_NDK_VERSION -RECOMMENDED_NDK_VERSION = "19b" +RECOMMENDED_NDK_VERSION = "19c" NDK_DOWNLOAD_URL = "https://developer.android.com/ndk/downloads/" @@ -194,10 +194,8 @@ def check_ndk_api(ndk_api, android_api): minor=MIN_PYTHON_MINOR_VERSION)) PY2_ERROR_TEXT = ( 'python-for-android no longer supports running under Python 2. Either upgrade to ' - 'Python {min_version} or higher (recommended), or revert to python-for-android 2019.07.08. ' - 'Note that you *can* still target Python 2 on Android by including python2 in your ' - 'requirements.').format( - min_version=MIN_PYTHON_VERSION) + 'Python {min_version} or higher (recommended), or revert to python-for-android 2019.07.08.' +).format(min_version=MIN_PYTHON_VERSION) PY_VERSION_ERROR_TEXT = ( 'Your Python version {user_major}.{user_minor} is not supported by python-for-android, ' diff --git a/pythonforandroid/toolchain.py b/pythonforandroid/toolchain.py index adb46acdf0..bc427f6d35 100644 --- a/pythonforandroid/toolchain.py +++ b/pythonforandroid/toolchain.py @@ -6,7 +6,6 @@ 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 @@ -139,7 +138,7 @@ def require_prebuilt_dist(func): """ @wraps(func) - def wrapper_func(self, args): + def wrapper_func(self, args, **kw): ctx = self.ctx ctx.set_archs(self._archs) ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir, @@ -153,7 +152,7 @@ def wrapper_func(self, args): info_notify('No dist exists that meets your requirements, ' 'so one will be built.') build_dist_from_args(ctx, dist, args) - func(self, args) + func(self, args, **kw) return wrapper_func @@ -235,7 +234,7 @@ def _get_option_tuples(self, option_string): return [] -class ToolchainCL(object): +class ToolchainCL: def __init__(self): @@ -290,11 +289,11 @@ def __init__(self): '*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', + '--symlink-bootstrap-files', '--ssymlink_bootstrap_files', action='store_true', - dest='symlink_java_src', + dest='symlink_bootstrap_files', default=False, - help=('If True, symlinks the java src folder during build and dist ' + help=('If True, symlinks the bootstrap files ' 'creation. This is useful for development only, it could also' ' cause weird problems.')) @@ -482,30 +481,27 @@ def add_parser(subparsers, *args, **kwargs): action='store_true', help='Symlink the dist instead of copying') - parser_apk = add_parser( - subparsers, - 'apk', help='Build an APK', - parents=[generic_parser]) + parser_packaging = argparse.ArgumentParser( + parents=[generic_parser], + add_help=False, + description='common options for packaging (apk, aar)') + # This is actually an internal argument of the build.py # (see pythonforandroid/bootstraps/common/build/build.py). # However, it is also needed before the distribution is finally # assembled for locating the setup.py / other build systems, which # is why we also add it here: - parser_apk.add_argument( + parser_packaging.add_argument( '--private', dest='private', help='the directory with the app source code files' + ' (containing your main.py entrypoint)', required=False, default=None) - parser_apk.add_argument( - '--release', dest='build_mode', action='store_const', - const='release', default='debug', - help='Build the PARSER_APK. in Release mode') - parser_apk.add_argument( + parser_packaging.add_argument( '--use-setup-py', dest="use_setup_py", action='store_true', default=False, help="Process the setup.py of a project if present. " + "(Experimental!") - parser_apk.add_argument( + parser_packaging.add_argument( '--ignore-setup-py', dest="ignore_setup_py", action='store_true', default=False, help="Don't run the setup.py of a project if present. " + @@ -513,20 +509,35 @@ def add_parser(subparsers, *args, **kwargs): "designed to work inside p4a (e.g. by installing " + "dependencies that won't work or aren't desired " + "on Android") - parser_apk.add_argument( + parser_packaging.add_argument( + '--release', dest='build_mode', action='store_const', + const='release', default='debug', + help='Build your app as a non-debug release build. ' + '(Disables gdb debugging among other things)') + parser_packaging.add_argument( '--keystore', dest='keystore', action='store', default=None, help=('Keystore for JAR signing key, will use jarsigner ' 'default if not specified (release build only)')) - parser_apk.add_argument( + parser_packaging.add_argument( '--signkey', dest='signkey', action='store', default=None, help='Key alias to sign PARSER_APK. with (release build only)') - parser_apk.add_argument( + parser_packaging.add_argument( '--keystorepw', dest='keystorepw', action='store', default=None, help='Password for keystore') - parser_apk.add_argument( + parser_packaging.add_argument( '--signkeypw', dest='signkeypw', action='store', default=None, help='Password for key alias') + add_parser( + subparsers, + 'aar', help='Build an AAR', + parents=[parser_packaging]) + + add_parser( + subparsers, + 'apk', help='Build an APK', + parents=[parser_packaging]) + add_parser( subparsers, 'create', help='Compile a set of requirements into a dist', @@ -576,6 +587,8 @@ def add_parser(subparsers, *args, **kwargs): if hasattr(args, "private") and args.private is not None: # Pass this value on to the internal bootstrap build.py: args.unknown_args += ["--private", args.private] + if hasattr(args, "build_mode") and args.build_mode == "release": + args.unknown_args += ["--release"] if hasattr(args, "ignore_setup_py") and args.ignore_setup_py: args.use_setup_py = False @@ -592,6 +605,9 @@ def add_parser(subparsers, *args, **kwargs): self.ctx = Context() self.ctx.use_setup_py = getattr(args, "use_setup_py", True) + self.ctx.build_as_debuggable = getattr( + args, "build_mode", "debug" + ) == "debug" have_setup_py_or_similar = False if getattr(args, "private", None) is not None: @@ -659,7 +675,7 @@ def add_parser(subparsers, *args, **kwargs): self.ndk_dir = args.ndk_dir self.android_api = args.android_api self.ndk_api = args.ndk_api - self.ctx.symlink_java_src = args.symlink_java_src + self.ctx.symlink_bootstrap_files = args.symlink_bootstrap_files self.ctx.java_build_tool = args.java_build_tool self._archs = split_argument_list(args.arch) @@ -668,7 +684,8 @@ def add_parser(subparsers, *args, **kwargs): self.ctx.copy_libs = args.copy_libs # Each subparser corresponds to a method - getattr(self, args.subparser_name.replace('-', '_'))(args) + command = args.subparser_name.replace('-', '_') + getattr(self, command)(args) @staticmethod def warn_on_carriage_return_args(args): @@ -749,7 +766,7 @@ def recipes(self, args): .. code-block:: bash python3 3.7.1 depends: ['hostpython3', 'sqlite3', 'openssl', 'libffi'] - conflicts: ['python2'] + conflicts: [] optional depends: ['sqlite3', 'libffi', 'openssl'] """ ctx = self.ctx @@ -918,17 +935,17 @@ def _dist(self): ctx.distribution = dist return dist - @require_prebuilt_dist - def apk(self, args): - """Create an APK using the given distribution.""" - - ctx = self.ctx - dist = self._dist + @staticmethod + def _fix_args(args): + """ + Manually fixing these arguments at the string stage is + unsatisfactory and should probably be changed somehow, but + we can't leave it until later as the build.py scripts assume + they are in the current directory. + works in-place + :param args: parser args + """ - # Manually fixing these arguments at the string stage is - # unsatisfactory and should probably be changed somehow, but - # we can't leave it until later as the build.py scripts assume - # they are in the current directory. fix_args = ('--dir', '--private', '--add-jar', '--add-source', '--whitelist', '--blacklist', '--presplash', '--icon') unknown_args = args.unknown_args @@ -941,6 +958,12 @@ def apk(self, args): elif i + 1 < len(unknown_args): unknown_args[i+1] = realpath(expanduser(unknown_args[i+1])) + @staticmethod + def _prepare_release_env(args): + """ + prepares envitonment dict with the necessary flags for signing an apk + :param args: parser args + """ env = os.environ.copy() if args.build_mode == 'release': if args.keystore: @@ -954,125 +977,134 @@ def apk(self, args): elif args.keystorepw and 'P4A_RELEASE_KEYALIAS_PASSWD' not in env: env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.keystorepw - build = imp.load_source('build', join(dist.dist_dir, 'build.py')) + return env + + def _build_package(self, args, package_type): + """ + Creates an android package using gradle + :param args: parser args + :param package_type: one of 'apk', 'aar' + :return (gradle output, build_args) + """ + ctx = self.ctx + dist = self._dist + bs = Bootstrap.get_bootstrap(args.bootstrap, ctx) + ctx.prepare_bootstrap(bs) + self._fix_args(args) + env = self._prepare_release_env(args) + with current_directory(dist.dist_dir): self.hook("before_apk_build") os.environ["ANDROID_API"] = str(self.ctx.android_api) - build_args = build.parse_args(args.unknown_args) + build = imp.load_source('build', join(dist.dist_dir, 'build.py')) + build_args = build.parse_args_and_make_package( + args.unknown_args + ) + self.hook("after_apk_build") self.hook("before_apk_assemble") + build_tools_versions = os.listdir(join(ctx.sdk_dir, + 'build-tools')) + build_tools_versions = sorted(build_tools_versions, + key=LooseVersion) + build_tools_version = build_tools_versions[-1] + info(('Detected highest available build tools ' + 'version to be {}').format(build_tools_version)) + + if build_tools_version < '25.0': + raise BuildInterruptingException( + 'build_tools >= 25 is required, but %s is installed' % build_tools_version) + if not exists("gradlew"): + raise BuildInterruptingException("gradlew file is missing") + + env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir + env["ANDROID_HOME"] = self.ctx.sdk_dir + + gradlew = sh.Command('./gradlew') + + if exists('/usr/bin/dos2unix'): + # .../dists/bdisttest_python3/gradlew + # .../build/bootstrap_builds/sdl2-python3/gradlew + # if docker on windows, gradle contains CRLF + output = shprint( + sh.Command('dos2unix'), gradlew._path.decode('utf8'), + _tail=20, _critical=True, _env=env + ) + if args.build_mode == "debug": + gradle_task = "assembleDebug" + elif args.build_mode == "release": + gradle_task = "assembleRelease" + else: + raise BuildInterruptingException( + "Unknown build mode {} for apk()".format(args.build_mode)) + output = shprint(gradlew, gradle_task, _tail=20, + _critical=True, _env=env) + return output, build_args - build_type = ctx.java_build_tool - if build_type == 'auto': - info('Selecting java build tool:') - - build_tools_versions = os.listdir(join(ctx.sdk_dir, - 'build-tools')) - build_tools_versions = sorted(build_tools_versions, - key=LooseVersion) - build_tools_version = build_tools_versions[-1] - info(('Detected highest available build tools ' - 'version to be {}').format(build_tools_version)) - - if build_tools_version >= '25.0' and exists('gradlew'): - build_type = 'gradle' - info(' Building with gradle, as gradle executable is ' - 'present') - else: - build_type = 'ant' - if build_tools_version < '25.0': - info((' Building with ant, as the highest ' - 'build-tools-version is only {}').format( - build_tools_version)) - else: - info(' Building with ant, as no gradle executable ' - 'detected') - - if build_type == 'gradle': - # gradle-based build - env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir - env["ANDROID_HOME"] = self.ctx.sdk_dir - - gradlew = sh.Command('./gradlew') - if exists('/usr/bin/dos2unix'): - # .../dists/bdisttest_python3/gradlew - # .../build/bootstrap_builds/sdl2-python3/gradlew - # if docker on windows, gradle contains CRLF - output = shprint( - sh.Command('dos2unix'), gradlew._path.decode('utf8'), - _tail=20, _critical=True, _env=env - ) - if args.build_mode == "debug": - gradle_task = "assembleDebug" - elif args.build_mode == "release": - gradle_task = "assembleRelease" - else: - raise BuildInterruptingException( - "Unknown build mode {} for apk()".format(args.build_mode)) - output = shprint(gradlew, gradle_task, _tail=20, - _critical=True, _env=env) - - # gradle output apks somewhere else - # and don't have version in file - apk_dir = join(dist.dist_dir, - "build", "outputs", "apk", - args.build_mode) - apk_glob = "*-{}.apk" - apk_add_version = True + def _finish_package(self, args, output, build_args, package_type, output_dir): + """ + Finishes the package after the gradle script run + :param args: the parser args + :param output: RunningCommand output + :param build_args: build args as returned by build.parse_args + :param package_type: one of 'apk', 'aar' + :param output_dir: where to put the package file + """ - else: - # ant-based build - try: - ant = sh.Command('ant') - except sh.CommandNotFound: - raise BuildInterruptingException( - 'Could not find ant binary, please install it ' - 'and make sure it is in your $PATH.') - output = shprint(ant, args.build_mode, _tail=20, - _critical=True, _env=env) - apk_dir = join(dist.dist_dir, "bin") - apk_glob = "*-*-{}.apk" - apk_add_version = False - - self.hook("after_apk_assemble") - - info_main('# Copying APK to current directory') - - apk_re = re.compile(r'.*Package: (.*\.apk)$') - apk_file = None + package_glob = "*-{}.%s" % package_type + package_add_version = True + + self.hook("after_apk_assemble") + + info_main('# Copying android package to current directory') + + package_re = re.compile(r'.*Package: (.*\.apk)$') + package_file = None for line in reversed(output.splitlines()): - m = apk_re.match(line) + m = package_re.match(line) if m: - apk_file = m.groups()[0] + package_file = m.groups()[0] break - - if not apk_file: - info_main('# APK filename not found in build output. Guessing...') + if not package_file: + info_main('# Android package filename not found in build output. Guessing...') if args.build_mode == "release": suffixes = ("release", "release-unsigned") else: suffixes = ("debug", ) for suffix in suffixes: - apks = glob.glob(join(apk_dir, apk_glob.format(suffix))) - if apks: - if len(apks) > 1: + + package_files = glob.glob(join(output_dir, package_glob.format(suffix))) + if package_files: + if len(package_files) > 1: info('More than one built APK found... guessing you ' - 'just built {}'.format(apks[-1])) - apk_file = apks[-1] + 'just built {}'.format(package_files[-1])) + package_file = package_files[-1] break else: raise BuildInterruptingException('Couldn\'t find the built APK') - info_main('# Found APK file: {}'.format(apk_file)) - if apk_add_version: - info('# Add version number to APK') - apk_name = basename(apk_file)[:-len(APK_SUFFIX)] - apk_file_dest = "{}-{}-{}".format( - apk_name, build_args.version, APK_SUFFIX) - info('# APK renamed to {}'.format(apk_file_dest)) - shprint(sh.cp, apk_file, apk_file_dest) + info_main('# Found android package file: {}'.format(package_file)) + if package_add_version: + info('# Add version number to android package') + package_name = basename(package_file)[:-len(APK_SUFFIX)] + package_file_dest = "{}-{}-{}".format( + package_name, build_args.version, APK_SUFFIX) + info('# Android package renamed to {}'.format(package_file_dest)) + shprint(sh.cp, package_file, package_file_dest) else: - shprint(sh.cp, apk_file, './') + shprint(sh.cp, package_file, './') + + @require_prebuilt_dist + def apk(self, args): + output, build_args = self._build_package(args, package_type='apk') + output_dir = join(self._dist.dist_dir, "build", "outputs", 'apk', args.build_mode) + self._finish_package(args, output, build_args, 'apk', output_dir) + + @require_prebuilt_dist + def aar(self, args): + output, build_args = self._build_package(args, package_type='aar') + output_dir = join(self._dist.dist_dir, "build", "outputs", 'aar') + self._finish_package(args, output, build_args, 'aar', output_dir) @require_prebuilt_dist def create(self, args): diff --git a/pythonforandroid/tools/biglink b/pythonforandroid/tools/biglink index e2652d52c9..8a8e561fba 100755 --- a/pythonforandroid/tools/biglink +++ b/pythonforandroid/tools/biglink @@ -1,6 +1,5 @@ #!/usr/bin/env python -from __future__ import print_function import os import sys import subprocess diff --git a/pythonforandroid/tools/liblink b/pythonforandroid/tools/liblink index 69f8ef23af..de837e6ca5 100755 --- a/pythonforandroid/tools/liblink +++ b/pythonforandroid/tools/liblink @@ -1,6 +1,5 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python -from __future__ import print_function import sys import subprocess from os import environ @@ -22,7 +21,7 @@ while i < len(sys.argv): i += 1 continue - if opt.startswith("-l") or opt.startswith("-L"): + if opt.startswith(("-l", "-L")): libs.append(opt) continue @@ -34,27 +33,8 @@ while i < len(sys.argv): i += 1 continue - if opt.startswith("-I") or opt.startswith('-isystem'): - continue - - if opt.startswith("-m"): - continue - - if opt.startswith("-f"): - continue - - if opt.startswith("-O"): - continue - - if opt.startswith("-g"): - continue - - if opt.startswith("-D"): - continue - - if opt.startswith("-R"): - # for -rpath, not implemented yet. - continue + if opt.startswith( + ("-I", "-isystem", "-m", "-f", "-O", "-g", "-D", "-R")): if opt.startswith("-"): print(sys.argv) diff --git a/pythonforandroid/util.py b/pythonforandroid/util.py index 839858cb1e..c9b779829a 100644 --- a/pythonforandroid/util.py +++ b/pythonforandroid/util.py @@ -1,16 +1,11 @@ import contextlib from os.path import exists, join from os import getcwd, chdir, makedirs, walk, uname -import sh import shutil from fnmatch import fnmatch from tempfile import mkdtemp -# This Python version workaround left for compatibility during initial version check -try: # Python 3 - from urllib.request import FancyURLopener -except ImportError: # Python 2 - from urllib import FancyURLopener +from urllib.request import FancyURLopener from pythonforandroid.logger import (logger, Err_Fore, error, info) @@ -59,17 +54,6 @@ def ensure_dir(filename): makedirs(filename) -def get_virtualenv_executable(): - virtualenv = None - if virtualenv is None: - virtualenv = sh.which('virtualenv2') - if virtualenv is None: - virtualenv = sh.which('virtualenv-2.7') - if virtualenv is None: - virtualenv = sh.which('virtualenv') - return virtualenv - - def walk_valid_filens(base_dir, invalid_dir_names, invalid_file_patterns): """Recursively walks all the files and directories in ``dirn``, ignoring directories that match any pattern in ``invalid_dirns`` @@ -102,7 +86,7 @@ def walk_valid_filens(base_dir, invalid_dir_names, invalid_file_patterns): class BuildInterruptingException(Exception): def __init__(self, message, instructions=None): - super(BuildInterruptingException, self).__init__(message, instructions) + super().__init__(message, instructions) self.message = message self.instructions = instructions diff --git a/setup.py b/setup.py index cb95b08ccc..d59d630797 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,6 @@ from io import open # for open(..,encoding=...) parameter in python 2 from os import walk from os.path import join, dirname, sep -import os import re from setuptools import setup, find_packages @@ -17,15 +16,15 @@ data_files = [] - # must be a single statement since buildozer is currently parsing it, refs: # https://github.com/kivy/buildozer/issues/722 install_reqs = [ 'appdirs', 'colorama>=0.3.3', 'jinja2', 'six', 'enum34; python_version<"3.4"', 'sh>=1.10; sys_platform!="nt"', - 'pep517<0.7.0"', 'pytoml', 'virtualenv<20' + 'pep517<0.7.0"', 'toml', ] -# (pep517, pytoml and virtualenv are used by pythonpackage.py) +# (pep517 and toml are used by pythonpackage.py) + # By specifying every file manually, package_data will be able to # include them in binary distributions. Note that we have to add @@ -34,7 +33,7 @@ def recursively_include(results, directory, patterns): for root, subfolders, files in walk(directory): for fn in files: - if not any([glob.fnmatch.fnmatch(fn, pattern) for pattern in patterns]): + if not any(glob.fnmatch.fnmatch(fn, pattern) for pattern in patterns): continue filename = join(root, fn) directory = 'pythonforandroid' @@ -42,6 +41,7 @@ def recursively_include(results, directory, patterns): results[directory] = [] results[directory].append(join(*filename.split(sep)[1:])) + recursively_include(package_data, 'pythonforandroid/recipes', ['*.patch', 'Setup*', '*.pyx', '*.py', '*.c', '*.h', '*.mk', '*.jam', ]) @@ -88,6 +88,7 @@ def recursively_include(results, directory, patterns): description='Android APK packager for Python scripts and apps', long_description=long_description, long_description_content_type='text/markdown', + python_requires=">=3.6.0", author='The Kivy team', author_email='kivy-dev@googlegroups.com', url='https://github.com/kivy/python-for-android', @@ -100,9 +101,10 @@ def recursively_include(results, directory, patterns): ], 'distutils.commands': [ 'apk = pythonforandroid.bdistapk:BdistAPK', + 'aar = pythonforandroid.bdistapk:BdistAAR', ], }, - classifiers = [ + classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', @@ -113,6 +115,9 @@ def recursively_include(results, directory, patterns): 'Operating System :: Android', 'Programming Language :: C', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Topic :: Software Development', 'Topic :: Utilities', ], diff --git a/testapps/on_device_unit_tests/README.rst b/testapps/on_device_unit_tests/README.rst index 1415fbb6a4..6c0eb9626e 100644 --- a/testapps/on_device_unit_tests/README.rst +++ b/testapps/on_device_unit_tests/README.rst @@ -4,18 +4,38 @@ On device unit tests This test app runs a set of unit tests, to help confirm that the python-for-android build is actually working properly. +Also it's dynamic, because it will run one app or another depending on the +supplied recipes at build time. + +It currently supports three app `modes`: + - `kivy app` (with sdl2 bootstrap): if kivy in recipes + - `flask app` (with webview bootstrap): if flask in recipes + - `no gui`: if neither of above cases is taken + The main tests are for the recipes built in the apk. Each module (or other tool) is at least imported and subject to some basic check. -This app is experimental, it doesn't yet support things like testing -only the requirements you ask for (so if you build with requirements -other than those specified, the tests may fail). It also has no gui -yet, the results must be checked via logcat. +This test app can be build via `setup.py` or via buildozer. In both +cases it will build a basic kivy app with a set of tests defined via the +`requirements` keyword (specified at build time). + +In case that you build the `test app with no-gui`, the unittests results must +be checked via command `adb logcat` or some logging apk (you may need root +permissions in your device to use such app). + +Building the app with python-for-android +======================================== + +You can use the provided file `setup.py`. Check our `Makefile +`__ to guess +how to build the test app, or also you can look at `testing pull requests documentation +`__, +which describes some of the methods that you can use to build the test app. -Building the app -================ +Building the app with buildozer +=============================== -This app should be built using buildozer, which it also serves as a +This app can be built using buildozer, which it also serves as a test for:: $ buildozer android debug diff --git a/testapps/on_device_unit_tests/buildozer.spec b/testapps/on_device_unit_tests/buildozer.spec index a2b55302c8..b372d5faa5 100644 --- a/testapps/on_device_unit_tests/buildozer.spec +++ b/testapps/on_device_unit_tests/buildozer.spec @@ -13,7 +13,7 @@ package.domain = org.kivy source.dir = test_app # (list) Source files to include (let empty to include all the files) -source.include_exts = py,png,jpg,kv,atlas +source.include_exts = py,png,jpg,kv,atlas,html,css,otf,txt # (list) List of inclusions using pattern matching #source.include_patterns = assets/*,images/*.png @@ -36,7 +36,7 @@ version = 0.1 # (list) Application requirements # comma separated e.g. requirements = sqlite3,kivy -requirements = python3,kivy,openssl,numpy,sqlite3 +requirements = python3,kivy,libffi,openssl,numpy,sqlite3 # (str) Custom source folders for requirements # Sets custom source for any requirements with recipes @@ -52,7 +52,7 @@ requirements = python3,kivy,openssl,numpy,sqlite3 #icon.filename = %(source.dir)s/data/icon.png # (str) Supported orientation (one of landscape, sensorLandscape, portrait or all) -orientation = portrait +orientation = all # (list) List of service to declare #services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY diff --git a/testapps/on_device_unit_tests/setup.py b/testapps/on_device_unit_tests/setup.py new file mode 100644 index 0000000000..0f8574b9da --- /dev/null +++ b/testapps/on_device_unit_tests/setup.py @@ -0,0 +1,93 @@ +""" +This is the `setup.py` file for the `on device unit test app`. + +In this module we can control how will be built our test app. Depending on +our requirements we can build an kivy, flask or a non-gui app. We default to an +kivy app, since the python-for-android project its a sister project of kivy. + +The parameter `requirements` is crucial to determine the unit tests we will +perform with our app, so we must explicitly name the recipe we want to test +and, of course, we should have the proper test for the given recipe at +`tests.test_requirements.py` or nothing will be tested. We control our default +app requirements via the dictionary `options`. Here you have some examples +to build the supported app modes:: + + - kivy *basic*: `sqlite3,libffi,openssl,pyjnius,kivy,python3,requests` + - kivy *images/graphs*: `kivy,python3,numpy,matplotlib,Pillow` + - kivy *encryption*: `kivy,python3,cryptography,pycryptodome,scrypt, + m2crypto,pysha3` + - flask (with webview bootstrap): `sqlite3,libffi,openssl,pyjnius,flask, + python3,genericndkbuild` + + +.. note:: just noting that, for the `kivy basic` app, we add the requirements: + `sqlite3,libffi,openssl` so this way we will trigger the unit tests + that we have for such recipes. + +.. tip:: to force `python-for-android` generate an `flask` app without using + the kwarg `bootstrap`, we add the recipe `genericndkbuild`, which will + trigger the `webview bootstrap` at build time. +""" + +import os +import sys +from distutils.core import setup +from setuptools import find_packages + +# define a basic test app, which can be override passing the proper args to cli +options = { + 'apk': + { + 'requirements': + 'sqlite3,libffi,openssl,pyjnius,kivy,python3,requests', + 'android-api': 27, + 'ndk-api': 21, + 'dist-name': 'bdist_unit_tests_app', + 'arch': 'armeabi-v7a', + 'bootstrap' : 'sdl2', + 'permissions': ['INTERNET', 'VIBRATE'], + 'orientation': 'sensor', + 'service': 'P4a_test_service:app_service.py', + } +} + +# check if we overwrote the default test_app requirements via `cli` +requirements = options['apk']['requirements'].rsplit(',') +for n, arg in enumerate(sys.argv): + if arg == '--requirements': + print('found requirements') + requirements = sys.argv[n + 1].rsplit(',') + break + +# remove `orientation` in case that we don't detect a kivy or flask app, +# since the `service_only` bootstrap does not support such argument +if not ({'kivy', 'flask'} & set(requirements)): + options['apk'].pop('orientation') + +# write a file to let the test_app know which requirements we want to test +# Note: later, when running the app, we will guess if we have the right test. +app_requirements_txt = os.path.join( + os.path.split(__file__)[0], + 'test_app', + 'app_requirements.txt', +) +with open(app_requirements_txt, 'w') as requirements_file: + for req in requirements: + requirements_file.write(f'{req.split("==")[0]}\n') + +# run the install +setup( + name='unit_tests_app', + version='1.1', + description='p4a on device unit test app', + author='Alexander Taylor, Pol Canelles', + author_email='alexanderjohntaylor@gmail.com, canellestudi@gmail.com', + packages=find_packages(), + options=options, + package_data={ + 'test_app': ['*.py', '*.kv', '*.txt'], + 'test_app/static': ['*.png', '*.css', '*.otf'], + 'test_app/templates': ['*.html'], + 'test_app/tests': ['*.py'], + } +) diff --git a/testapps/on_device_unit_tests/test_app/app_flask.py b/testapps/on_device_unit_tests/test_app/app_flask.py new file mode 100644 index 0000000000..ee23532035 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/app_flask.py @@ -0,0 +1,138 @@ +print('main.py was successfully called') +print('this is the new main.py') + +import sys +print('python version is: ' + sys.version) +print('python path is', sys.path) + +import os +print('imported os') +print('contents of this dir', os.listdir('./')) + +from flask import ( + Flask, + render_template, + request, + Markup +) + +print('imported flask etc') + +from constants import RUNNING_ON_ANDROID +from tools import ( + run_test_suites_into_buffer, + get_failed_unittests_from, + vibrate_with_pyjnius, + get_android_python_activity, + set_device_orientation, +) + + +app = Flask(__name__) +TESTS_TO_PERFORM = dict() +NON_ANDROID_DEVICE_MSG = 'Not running from Android device' + + +def get_html_for_tested_modules(tested_modules, failed_tests): + modules_text = '' + for n, module in enumerate(sorted(tested_modules)): + print(module) + base_text = '' + if TESTS_TO_PERFORM[module] in failed_tests: + color = 'text-red' + else: + color = 'text-green' + if n != len(tested_modules) - 1: + base_text += ', ' + + modules_text += base_text.format(color=color, module=module) + + return Markup(modules_text) + + +@app.route('/') +def index(): + return render_template( + 'index.html', + platform='Android' if RUNNING_ON_ANDROID else 'Desktop', + ) + + +@app.route('/unittests') +def unittests(): + import unittest + print('Imported unittest') + + print("loading tests...") + suites = unittest.TestLoader().loadTestsFromNames( + list(TESTS_TO_PERFORM.values()), + ) + + print("running unittest...") + terminal_output = run_test_suites_into_buffer(suites) + + print("unittest result is:") + unittest_error_text = terminal_output.split('\n') + print(terminal_output) + + # get a nice colored `html` output for our tested recipes + failed_tests = get_failed_unittests_from( + terminal_output, TESTS_TO_PERFORM.values(), + ) + colored_tested_recipes = get_html_for_tested_modules( + TESTS_TO_PERFORM.keys(), failed_tests, + ) + + return render_template( + 'unittests.html', + tested_recipes=colored_tested_recipes, + unittests_output=unittest_error_text, + platform='Android' if RUNNING_ON_ANDROID else 'Desktop', + ) + + +@app.route('/page2') +def page2(): + return render_template( + 'page2.html', + platform='Android' if RUNNING_ON_ANDROID else 'Desktop', + ) + + +@app.route('/loadUrl') +def loadUrl(): + if not RUNNING_ON_ANDROID: + print(NON_ANDROID_DEVICE_MSG, '...cancelled loadUrl.') + return NON_ANDROID_DEVICE_MSG + args = request.args + if 'url' not in args: + print('ERROR: asked to open an url but without url argument') + print('asked to open url', args['url']) + activity = get_android_python_activity() + activity.loadUrl(args['url']) + + +@app.route('/vibrate') +def vibrate(): + if not RUNNING_ON_ANDROID: + print(NON_ANDROID_DEVICE_MSG, '...cancelled vibrate.') + return NON_ANDROID_DEVICE_MSG + + args = request.args + if 'time' not in args: + print('ERROR: asked to vibrate but without time argument') + print('asked to vibrate', args['time']) + return vibrate_with_pyjnius(int(float(args['time']) * 1000)) + + +@app.route('/orientation') +def orientation(): + if not RUNNING_ON_ANDROID: + print(NON_ANDROID_DEVICE_MSG, '...cancelled orientation.') + return NON_ANDROID_DEVICE_MSG + args = request.args + if 'dir' not in args: + print('ERROR: asked to orient but no dir specified') + return 'No direction specified ' + direction = args['dir'] + return set_device_orientation(direction) diff --git a/testapps/on_device_unit_tests/test_app/app_kivy.py b/testapps/on_device_unit_tests/test_app/app_kivy.py new file mode 100644 index 0000000000..8095998819 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/app_kivy.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +import subprocess + +from os.path import split + +from kivy.app import App +from kivy.clock import Clock +from kivy.properties import ( + BooleanProperty, + DictProperty, + ListProperty, + StringProperty, +) +from kivy.lang import Builder + +from constants import RUNNING_ON_ANDROID +from tools import ( + get_android_python_activity, + get_failed_unittests_from, + get_images_with_extension, + load_kv_from, + raise_error, + run_test_suites_into_buffer, + vibrate_with_pyjnius, +) +from widgets import TestImage + +# define our app's screen manager and load the screen templates +screen_manager_app = ''' +ScreenManager: + ScreenUnittests: + ScreenKeyboard: + ScreenOrientation: + ScreenService: +''' +load_kv_from('screen_unittests.kv') +load_kv_from('screen_keyboard.kv') +load_kv_from('screen_orientation.kv') +load_kv_from('screen_service.kv') + + +class TestKivyApp(App): + + tests_to_perform = DictProperty() + unittest_error_text = StringProperty('Running unittests...') + test_packages = StringProperty('Unittest recipes:') + generated_images = ListProperty() + service_running = BooleanProperty(False) + + def build(self): + self.reset_unittests_results() + self.sm = Builder.load_string(screen_manager_app) + return self.sm + + def reset_unittests_results(self, refresh_ui=False): + for img in get_images_with_extension(): + subprocess.call(["rm", "-r", img]) + print('removed image: ', img) + if refresh_ui: + self.set_color_for_tested_modules(restart=True) + self.unittest_error_text = '' + screen_unittests = self.sm.get_screen('unittests') + images_box = screen_unittests.ids.test_images_box + images_box.clear_widgets() + self.generated_images = [] + + def on_tests_to_perform(self, *args): + """ + Check `test_to_perform` so we can build some special tests in our ui. + Also will schedule the run of our tests. + """ + print('on_tests_to_perform: ', self.tests_to_perform.keys()) + self.set_color_for_tested_modules(restart=True) + Clock.schedule_once(self.run_unittests, 3) + + def run_unittests(self, *args): + import unittest + print('Imported unittest') + + print("loading tests...") + suites = unittest.TestLoader().loadTestsFromNames( + list(self.tests_to_perform.values()), + ) + self.test_packages = ', '.join(self.tests_to_perform.keys()) + + print("running unittest...") + self.unittest_error_text = run_test_suites_into_buffer(suites) + + print("unittest result is:") + print(self.unittest_error_text) + print('Ran tests') + + self.set_color_for_tested_modules() + + # check generated images by unittests + self.generated_images = get_images_with_extension() + + def set_color_for_tested_modules(self, restart=False): + tests_made = sorted(list(self.tests_to_perform.keys())) + failed_tests = get_failed_unittests_from( + self.unittest_error_text, + self.tests_to_perform.values(), + ) + + modules_text = 'Unittest recipes: ' + for n, module in enumerate(tests_made): + base_text = '[color={color}]{module}[/color]' + if restart: + color = '#838383' # grey + elif self.tests_to_perform[module] in failed_tests: + color = '#ff0000' # red + else: + color = '#5d8000' # green + if n != len(tests_made) - 1: + base_text += ', ' + + modules_text += base_text.format(color=color, module=module) + + self.test_packages = modules_text + + def on_generated_images(self, *args): + screen_unittests = self.sm.get_screen('unittests') + images_box = screen_unittests.ids.test_images_box + for i in self.generated_images: + img = TestImage( + text='Generated image by unittests: {}'.format(split(i)[1]), + source=i, + ) + images_box.add_widget(img) + + def test_vibration_with_pyjnius(self, *args): + vibrate_with_pyjnius() + + @property + def service_time(self): + from jnius import autoclass + + return autoclass('org.test.unit_tests_app.ServiceP4a_test_service') + + def on_service_running(self, *args): + if RUNNING_ON_ANDROID: + if self.service_running: + print('Starting service') + self.start_service() + else: + print('Stopping service') + self.stop_service() + else: + raise_error('Service test not supported on desktop') + + def start_service(self): + activity = get_android_python_activity() + service = self.service_time + try: + service.start(activity, 'Some argument') + except Exception as err: + raise_error(str(err)) + + def stop_service(self): + service = self.service_time + activity = get_android_python_activity() + service.stop(activity) diff --git a/testapps/on_device_unit_tests/test_app/app_service.py b/testapps/on_device_unit_tests/test_app/app_service.py new file mode 100644 index 0000000000..953d865632 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/app_service.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import datetime +import threading +import time + +from os import environ + +argument = environ.get('PYTHON_SERVICE_ARGUMENT', '') +print( + 'app_service.py was successfully called with argument: "{}"'.format( + argument, + ), +) + +next_call = time.time() +next_call_in = 5 # seconds + + +def service_timer(): + global next_call + print('P4a test service: {}'.format(datetime.datetime.now())) + + next_call += next_call_in + threading.Timer(next_call - time.time(), service_timer).start() + + +print('Starting the test service timer...') +service_timer() diff --git a/testapps/on_device_unit_tests/test_app/constants.py b/testapps/on_device_unit_tests/test_app/constants.py new file mode 100644 index 0000000000..55b61d48b5 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/constants.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from os import environ + +RUNNING_ON_ANDROID = "ANDROID_APP_PATH" in environ + +FONT_SIZE_TITLE = 32 if RUNNING_ON_ANDROID else 60 +FONT_SIZE_SUBTITLE = 16 if RUNNING_ON_ANDROID else 32 +FONT_SIZE_TEXT = 8 if RUNNING_ON_ANDROID else 16 diff --git a/testapps/on_device_unit_tests/test_app/main.py b/testapps/on_device_unit_tests/test_app/main.py index e0cd9c527d..349890d4ce 100644 --- a/testapps/on_device_unit_tests/test_app/main.py +++ b/testapps/on_device_unit_tests/test_app/main.py @@ -1,45 +1,96 @@ -import sys -if sys.version_info.major < 3: - print(('Running under Python {} but these tests ' - 'require Python 3+').format(sys.version_info.major)) - -import unittest -import importlib +#!/usr/bin/env python +# -*- coding: utf-8 -*- -print('Imported unittest') +""" +On device unit test app +======================= +This is a dynamic test app, which means that depending on the requirements +supplied at build time, will perform some tests or others. Also, this app will +have an ui, or not, depending on requirements as well. -class PythonTestMixIn(object): +For now, we contemplate three possibilities: + - A kivy unittest app (sdl2 bootstrap) + - A unittest app (webview bootstrap) + - A non-gui unittests app - module_import = None +If you install/build this app via the `setup.py` file, a file named +`app_requirements.txt` will be generated which will contain the requirements +that we passed to the `setup.py` via arguments, which will determine +the unittests that this app will run. - def test_import_module(self): - """Test importing the specified Python module name. This import test - is common to all Python modules, it does not test any further - functionality. - """ - self.assertIsNotNone( - self.module_import, - 'module_import is not set (was default None)') +.. note:: This app is made to be working on desktop and on an android device. + Be aware that some of the functionality of this app will only work on + an android device. - importlib.import_module(self.module_import) +.. tip:: you can write more unit tests at `tests/test_requirements.py` and test + these on desktop just by editing the file `app_requirements.txt`, + which should be located at the same location than this file. This + `app_requirements.txt` file, it's autogenerated when the + `setup.py` is ran, so in certain circumstances, you may need + to create it. Also be aware that each `python-for-android` recipe + that you want to test should be in a new line, taking into account the + case of the recipe. - def test_run_module(self): - """Import the specified module and do something with it as a minimal - check that it actually works. +.. warning:: If you use buildozer you only will get the basic `kivy unittest + app`, with a basic set of tests: sqlite3, libffi, openssl and + pyjnius. +""" - This test fails by default, it must be overridden by every - child test class. - """ +import sys +import unittest - self.fail('This test must be overridden by {}'.format(self)) +from os import curdir +from os.path import isfile, realpath -print('Defined test case') +print('Imported unittest') -import sys sys.path.append('./') -from tests import test_requirements -suite = unittest.TestLoader().loadTestsFromModule(test_requirements) -unittest.TextTestRunner().run(suite) -print('Ran tests') +# read `app_requirements.txt` and find out which tests to perform +tests_to_perform = {} +requirements = None +if isfile('app_requirements.txt'): + with open('app_requirements.txt', 'r') as requirements_file: + requirements = set(requirements_file.read().splitlines()) +if not requirements: + # we will test a basic set of recipes + requirements = {'sqlite3', 'libffi', 'openssl', 'pyjnius'} +print('App requirements are: ', requirements) + +for recipe in requirements: + test_name = 'tests.test_requirements.{recipe}TestCase'.format( + recipe=recipe.capitalize() + ) + try: + exist_test = unittest.TestLoader().loadTestsFromName(test_name) + except AttributeError: + # python2 case + pass + else: + if '_exception' not in exist_test._tests[0].__dict__: + print('Adding Testcase: ', test_name) + tests_to_perform[recipe] = test_name +print('Tests to perform are: ', tests_to_perform) + +# Find out which app we want to run +if 'kivy' in requirements: + from app_kivy import TestKivyApp + + test_app = TestKivyApp() + test_app.tests_to_perform = tests_to_perform + test_app.run() +elif 'flask' in requirements: + import app_flask + app_flask.TESTS_TO_PERFORM = tests_to_perform + + print('Current directory is ', realpath(curdir)) + if realpath(curdir).startswith('/data'): + app_flask.app.run(debug=False) + else: + app_flask.app.run(debug=True) +else: + # we don't have kivy or flask in our + # requirements, so we run unittests in terminal + suite = unittest.TestLoader().loadTestsFromNames(list(tests_to_perform)) + unittest.TextTestRunner().run(suite) diff --git a/testapps/on_device_unit_tests/test_app/screen_keyboard.kv b/testapps/on_device_unit_tests/test_app/screen_keyboard.kv new file mode 100644 index 0000000000..d0b08bbd67 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/screen_keyboard.kv @@ -0,0 +1,109 @@ +#:import Window kivy.core.window.Window + +#:import FONT_SIZE_SUBTITLE constants.FONT_SIZE_SUBTITLE +#:import FONT_SIZE_TEXT constants.FONT_SIZE_TEXT +#:import FONT_SIZE_TITLE constants.FONT_SIZE_TITLE +#:import Spacer20 widgets.Spacer20 + +: + name: 'keyboard' + BoxLayout: + orientation: 'vertical' + Button: + text: 'Back to unittests' + font_size: sp(FONT_SIZE_SUBTITLE) + size_hint_y: None + height: dp(60) + on_press: root.parent.current = 'unittests' + Image: + keep_ratio: False + allow_stretch: True + source: 'static/coloursinv.png' + size_hint_y: None + height: dp(100) + Label: + text: + '[color=#999999]Test[/color] kivy ' \ + '[color=#999999]keyboard[/color] modes' + height: self.texture_size[1] + size_hint_y: None + padding: 0, 20 + font_size: sp(FONT_SIZE_TITLE) + font_name: 'static/Blanka-Regular.otf' + text_size: root.width, None + markup: True + halign: 'center' + Label: + text: + 'Specifies the behavior of window contents on display ' \ + 'of the soft keyboard on Android.\n\n\n\n' \ + '[color=#ff5900]WARNING:[/color] ' \ + 'these tests only works on an Android device' + markup: True + padding: 0, 20 + size_hint_y: None + text_size: root.width, None + font_size: sp(FONT_SIZE_TEXT) + height: self.texture_size[1] + halign: 'center' + Spacer20: + Spacer20: + RelativeLayout: + size_hint_y: None + height: dp(50) + BoxLayout: + size_hint_x: None + width: min(dp(500), root.width) + orientation: 'horizontal' + pos_hint: {'center_x': .5} + ToggleButton: + text: 'None' + group: 'keyboard_modes' + state: 'down' + on_press: Window.softinput_mode = '' + ToggleButton: + text: 'pan' + group: 'keyboard_modes' + on_press: Window.softinput_mode = 'pan' + ToggleButton: + text: 'below_target' + group: 'keyboard_modes' + on_press: Window.softinput_mode = 'below_target' + ToggleButton: + text: 'resize' + group: 'keyboard_modes' + on_press: Window.softinput_mode = 'resize' + Widget: + Scatter: + id: scatter + size_hint: None, None + size: dp(300), dp(80) + on_parent: self.pos = (300, 100) + BoxLayout: + size: scatter.size + orientation: 'horizontal' + canvas: + Color: + rgba: 1, 0, 1, .25 + Rectangle: + pos: 0, 0 + size: self.size + Label: + size_hint_x: None + width: dp(30) + text: 'drag me' + canvas.before: + Color: + rgb: 1, 1, 1 + PushMatrix + Translate: + xy: self.center_x, self.center_y + Rotate: + angle: 90 + axis: 0, 0, 1 + Translate: + xy: -self.center_x, -self.center_y + canvas.after: + PopMatrix + TextInput: + text: 'type in me' diff --git a/testapps/on_device_unit_tests/test_app/screen_orientation.kv b/testapps/on_device_unit_tests/test_app/screen_orientation.kv new file mode 100644 index 0000000000..aa0bdfb56e --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/screen_orientation.kv @@ -0,0 +1,74 @@ +#:import FONT_SIZE_SUBTITLE constants.FONT_SIZE_SUBTITLE +#:import FONT_SIZE_TEXT constants.FONT_SIZE_TEXT +#:import FONT_SIZE_TITLE constants.FONT_SIZE_TITLE + +#:import set_device_orientation tools.set_device_orientation +#:import Spacer20 widgets.Spacer20 + +: + name: 'orientation' + ScrollView: + BoxLayout: + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height + Button: + text: 'Back to unittests' + font_size: sp(FONT_SIZE_SUBTITLE) + size_hint_y: None + height: dp(60) + on_press: root.parent.current = 'unittests' + Image: + keep_ratio: False + allow_stretch: True + source: 'static/coloursinv.png' + size_hint_y: None + height: dp(100) + Label: + text: + '[color=#999999]Test[/color] device ' \ + '[color=#999999]orientation[/color]' + height: self.texture_size[1] + size_hint_y: None + padding: 0, 20 + font_size: sp(FONT_SIZE_TITLE) + font_name: 'static/Blanka-Regular.otf' + text_size: root.width, None + markup: True + halign: 'center' + Spacer20: + Spacer20: + RelativeLayout: + size_hint_y: None + height: dp(50) + BoxLayout: + size_hint_x: None + width: min(dp(500), root.width) + pos_hint: {'center_x': .5} + orientation: 'horizontal' + ToggleButton: + text: 'Sensor' + group: 'device_orientations' + state: 'down' + on_press: set_device_orientation('sensor') + ToggleButton: + text: 'Horizontal' + group: 'device_orientations' + on_press: set_device_orientation('horizontal') + ToggleButton: + text: 'Vertical' + group: 'device_orientations' + on_press: set_device_orientation('vertical') + Spacer20: + Spacer20: + Label: + text: + '[color=#ff5900]WARNING:[/color] ' \ + 'these tests only works on an Android device' + markup: True + padding: 20, 20 + size_hint_y: None + text_size: root.width, None + font_size: sp(FONT_SIZE_TEXT) + height: self.texture_size[1] + halign: 'center' diff --git a/testapps/on_device_unit_tests/test_app/screen_service.kv b/testapps/on_device_unit_tests/test_app/screen_service.kv new file mode 100644 index 0000000000..8e5398f13e --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/screen_service.kv @@ -0,0 +1,67 @@ +#:import FONT_SIZE_SUBTITLE constants.FONT_SIZE_SUBTITLE +#:import FONT_SIZE_TEXT constants.FONT_SIZE_TEXT +#:import FONT_SIZE_TITLE constants.FONT_SIZE_TITLE + +#:import set_device_orientation tools.set_device_orientation +#:import Spacer20 widgets.Spacer20 +#:import CircularButton widgets.CircularButton + +#:set green_color (0.3, 0.5, 0, 1) +#:set red_color (1.0, 0, 0, 1) + +: + name: 'service' + ScrollView: + BoxLayout: + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height + Button: + text: 'Back to unittests' + font_size: sp(FONT_SIZE_SUBTITLE) + size_hint_y: None + height: dp(60) + on_press: root.parent.current = 'unittests' + Image: + keep_ratio: False + allow_stretch: True + source: 'static/coloursinv.png' + size_hint_y: None + height: dp(100) + Label: + text: + '[color=#999999]Test[/color] P4A ' \ + '[color=#999999]service[/color]' + size_hint_y: None + padding: 0, 20 + height: self.texture_size[1] + halign: 'center' + font_size: sp(FONT_SIZE_TITLE) + font_name: 'static/Blanka-Regular.otf' + text_size: root.width, None + markup: True + Spacer20: + Spacer20: + RelativeLayout: + size_hint_y: None + height: dp(100) + CircularButton: + text: 'Start service' if not app.service_running else 'Stop Service' + pos_hint: {'center_x': .5} + background_color: red_color + on_press: + app.service_running = not app.service_running; + self.background_color = green_color \ + if app.service_running else red_color + Spacer20: + Spacer20: + Label: + text: + '[color=#ff5900]WARNING:[/color] ' \ + 'this test only works on an Android device' + markup: True + size_hint_y: None + height: self.texture_size[1] + halign: 'center' + text_size: root.width, None + font_size: sp(FONT_SIZE_TEXT) diff --git a/testapps/on_device_unit_tests/test_app/screen_unittests.kv b/testapps/on_device_unit_tests/test_app/screen_unittests.kv new file mode 100644 index 0000000000..b04d98fd41 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/screen_unittests.kv @@ -0,0 +1,155 @@ +#:import sys sys + +#:import Clock kivy.clock.Clock +#:import Metrics kivy.metrics.Metrics + +#:import FONT_SIZE_SUBTITLE constants.FONT_SIZE_SUBTITLE +#:import FONT_SIZE_TEXT constants.FONT_SIZE_TEXT +#:import FONT_SIZE_TITLE constants.FONT_SIZE_TITLE +#:import Spacer20 widgets.Spacer20 + +: + name: 'unittests' + ScrollView: + id: scroll_view + GridLayout: + id: grid + cols: 1 + size_hint_y: None + height: self.minimum_height + BoxLayout: + id: header_box + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height + GridLayout: + rows: 1 if root.width > root.height else 2 + size_hint_y: None + height: dp(60) * self.rows + Button: + text: 'Test vibration' + font_size: sp(FONT_SIZE_SUBTITLE) + on_press: app.test_vibration_with_pyjnius() + Button: + text: 'Test Keyboard' + font_size: sp(FONT_SIZE_SUBTITLE) + on_press: root.parent.current = 'keyboard' + Button: + text: 'Test Orientation' + font_size: sp(FONT_SIZE_SUBTITLE) + on_press: root.parent.current = 'orientation' + Button: + text: 'Test Service' + font_size: sp(FONT_SIZE_SUBTITLE) + on_press: root.parent.current = 'service' + Image: + keep_ratio: False + allow_stretch: True + source: 'static/colours.png' + size_hint_y: None + height: dp(100) + Label: + height: self.texture_size[1] + size_hint_y: None + padding: 0, 20 + font_name: 'static/Blanka-Regular.otf' + font_size: sp(FONT_SIZE_TITLE) + text_size: self.size[0], None + markup: True + text: + '[color=#999999]Kivy[/color] on ' \ + '[color=#999999]SDL2[/color] on ' \ + '[color=#999999]Android[/color] !' + halign: 'center' + Label: + height: self.texture_size[1] + size_hint_y: None + text_size: self.size[0], None + font_size: sp(FONT_SIZE_TEXT) + markup: True + text: sys.version + halign: 'center' + padding_y: dp(10) + Spacer20: + Label: + height: self.texture_size[1] + size_hint_y: None + font_size: sp(FONT_SIZE_SUBTITLE) + text_size: self.size[0], None + markup: True + text: + 'Dpi: {}\nDensity: {}\nFontscale: {}'.format( + Metrics.dpi, Metrics.density, Metrics.fontscale) + halign: 'center' + Spacer20: + BoxLayout: + id: output_box + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height + canvas.before: + Color: + rgba: 1, 0, 1, .25 + Rectangle: + pos: self.pos + size: self.size + Spacer20: + Label: + id: test_packages_text + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + font_size: sp(FONT_SIZE_SUBTITLE) + padding: 40, 20 + markup: True + text: app.test_packages + canvas.before: + Color: + rgba: 0, 0, 0, .65 + Rectangle: + pos: self.x + 20, self.y + size: self.width - 40, self.height + Label: + id: output_text + height: self.texture_size[1] + size_hint: None, None + pos_hint: {'center_x': .5 } + width: output_box.width - 40 + padding: 20, 20 + font_size: sp(FONT_SIZE_TEXT) + text_size: self.size[0], None + markup: True + text: app.unittest_error_text + halign: 'justify' + canvas.before: + Color: + rgba: 0, 0, 0, .35 + Rectangle: + pos: self.pos + size: self.size + Widget: + id: fill_space + size_hint_y: None + height: + max(20, root.height - header_box.height - output_box.height + 20) + canvas.before: + Color: + rgba: 1, 0, 1, .25 + Rectangle: + pos: self.pos + size: self.size + BoxLayout: + id: test_images_box + orientation: 'vertical' + size_hint_y: None + height: self.minimum_height if self.children else 0 + padding: 0, 20 + Button: + size_hint_y: None + height: dp(60) + text: 'Restart unittests' + font_size: sp(FONT_SIZE_SUBTITLE) + on_press: + app.reset_unittests_results(refresh_ui=True); + root.ids.scroll_view.scroll_y = 1; + Clock.schedule_once(app.run_unittests, 2) diff --git a/testapps/on_device_unit_tests/test_app/static/Blanka-Regular.otf b/testapps/on_device_unit_tests/test_app/static/Blanka-Regular.otf new file mode 100644 index 0000000000..60b18d7816 Binary files /dev/null and b/testapps/on_device_unit_tests/test_app/static/Blanka-Regular.otf differ diff --git a/testapps/testapp/colours.png b/testapps/on_device_unit_tests/test_app/static/colours.png similarity index 100% rename from testapps/testapp/colours.png rename to testapps/on_device_unit_tests/test_app/static/colours.png diff --git a/testapps/testapp_flask/static/coloursinv.png b/testapps/on_device_unit_tests/test_app/static/coloursinv.png similarity index 100% rename from testapps/testapp_flask/static/coloursinv.png rename to testapps/on_device_unit_tests/test_app/static/coloursinv.png diff --git a/testapps/on_device_unit_tests/test_app/static/flask.css b/testapps/on_device_unit_tests/test_app/static/flask.css new file mode 100644 index 0000000000..80a9e88807 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/static/flask.css @@ -0,0 +1,133 @@ + +@font-face{ + font-family: Blanka; + src: url('Blanka-Regular.otf'); + font-weight: normal; +} + +body { + margin: 0; + padding: 0; + font-family: "Roboto Slab", Roboto, sans-serif; + color: #444; +} + +/* + * Formatting the header area + */ +header { + background-color: #6600b8; + height: 90px; + width: 100%; + opacity: .9; + margin-bottom: 10px; +} +header h1.logo { + margin-top: 4px; + font-size: 1.4em; + font-family: Blanka, sans-serif; + color: #fff; + text-transform: uppercase; + float: left; + text-align: center; + width: 100%; +} +header h1.logo:hover { + color: #000000; + text-decoration: none; +} + +/* + * Navbar + */ +.menu { + float: right; + margin-top: 0px; + width: 100%; + text-align: center; + font-family: Blanka, sans-serif; +} +.menu li { + display: inline-block; +} +.menu li + li { + margin-left: 35px; +} +.menu li a { + color: #999999; + text-decoration: none; + text-transform: uppercase; +} + +/* + * Centering the body content + */ +.container { + width: auto; + margin: 0 8px; + text-align: center; +} + +h2.page-title { + text-align: center; + text-transform: uppercase; + font-family: Blanka, sans-serif; +} + +.text-underline { + border-bottom: 0px solid #333; + font-family: Blanka, sans-serif; + width: 100%; + display: block; +} + +.center { + margin: auto; + width: 50%; +} + +/* + * Unittests page + */ +.text-green { + color: #5d8000; +} + +.text-red { + color: #ff0000; +} + +.terminal-box { + background-color: #333; +} + +.terminal-content { + color: #fff; + margin: 8px; + padding-top: 8px; +} + +/* + * Adapt header to bigger screens + */ +@media only screen and (min-width: 530px) { + header { + height: 45px; + } + + header h1.logo { + width: auto; + text-align: left; + } + + .menu { + width: auto; + margin-top: 12px; + } + .container { + text-align: left; + } + .text-underline { + border-bottom: 1px solid #333; + } +} \ No newline at end of file diff --git a/testapps/on_device_unit_tests/test_app/templates/base.html b/testapps/on_device_unit_tests/test_app/templates/base.html new file mode 100644 index 0000000000..50d3383e56 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/templates/base.html @@ -0,0 +1,35 @@ + + + + + + + + {% block title %} + Flask on {{ platform }} + {% endblock %} + + + + +
+
+

Flask on {{ platform }}!

+ + + +
+
+ +
+ {% block body %} + {% endblock %} +
+ + + diff --git a/testapps/on_device_unit_tests/test_app/templates/index.html b/testapps/on_device_unit_tests/test_app/templates/index.html new file mode 100644 index 0000000000..797724e541 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/templates/index.html @@ -0,0 +1,97 @@ +{% extends "base.html" %} + + +{% block body %} + +

Main Page

+ + + +
+

Perform unittests

+
+ + +
+
+ +
+

Test navigation

+
+ +
+
+ +
+

Android tests

+ +
+ + + +
+ +
+ + + + +
+ +
+ + + + + + + +
+
+
+
+ +{% endblock %} diff --git a/testapps/on_device_unit_tests/test_app/templates/page2.html b/testapps/on_device_unit_tests/test_app/templates/page2.html new file mode 100644 index 0000000000..f2fd122b78 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/templates/page2.html @@ -0,0 +1,22 @@ + +{% extends "base.html" %} + + +{% block body %} + +

Page two

+ + + +
+
+ Yeah, it seems to work, I would suggest to go to: +
+
+ +
+
+ ...far more interesting ;) +
+ +{% endblock %} diff --git a/testapps/on_device_unit_tests/test_app/templates/unittests.html b/testapps/on_device_unit_tests/test_app/templates/unittests.html new file mode 100644 index 0000000000..95b23995e8 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/templates/unittests.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + + +{% block body %} + +

Unittests Page

+ +
+Unittest recipes: {{ tested_recipes }} +
+ +
+
+ {% for line in unittests_output %} + {{ line }}
+ {% endfor %} +
+
+ +{% endblock %} \ No newline at end of file diff --git a/testapps/on_device_unit_tests/test_app/tests/mixin.py b/testapps/on_device_unit_tests/test_app/tests/mixin.py new file mode 100644 index 0000000000..af10dae693 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/tests/mixin.py @@ -0,0 +1,30 @@ +import importlib + + +class PythonTestMixIn(object): + + module_import = None + + def test_import_module(self): + """Test importing the specified Python module name. This import test + is common to all Python modules, it does not test any further + functionality. + """ + self.assertIsNotNone( + self.module_import, + 'module_import is not set (was default None)') + + importlib.import_module(self.module_import) + + def test_run_module(self): + """Import the specified module and do something with it as a minimal + check that it actually works. + + This test fails by default, it must be overridden by every + child test class. + """ + + self.fail('This test must be overridden by {}'.format(self)) + + +print('Defined test case') diff --git a/testapps/on_device_unit_tests/test_app/tests/test_requirements.py b/testapps/on_device_unit_tests/test_app/tests/test_requirements.py index 625a99e5db..cffb404409 100644 --- a/testapps/on_device_unit_tests/test_app/tests/test_requirements.py +++ b/testapps/on_device_unit_tests/test_app/tests/test_requirements.py @@ -1,6 +1,6 @@ -from main import PythonTestMixIn from unittest import TestCase +from .mixin import PythonTestMixIn class NumpyTestCase(PythonTestMixIn, TestCase): @@ -21,9 +21,9 @@ def test_run_module(self): ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ctx.options &= ~ssl.OP_NO_SSLv3 - -class SqliteTestCase(PythonTestMixIn, TestCase): + +class Sqlite3TestCase(PythonTestMixIn, TestCase): module_import = 'sqlite3' def test_run_module(self): @@ -31,7 +31,7 @@ def test_run_module(self): conn = sqlite3.connect('example.db') conn.cursor() - + class KivyTestCase(PythonTestMixIn, TestCase): module_import = 'kivy' @@ -47,4 +47,188 @@ class PyjniusTestCase(PythonTestMixIn, TestCase): def test_run_module(self): from jnius import autoclass + autoclass('org.kivy.android.PythonActivity') + + +class LibffiTestCase(PythonTestMixIn, TestCase): + module_import = 'ctypes' + + def test_run_module(self): + from os import environ + from ctypes import cdll + + if "ANDROID_APP_PATH" in environ: + libc = cdll.LoadLibrary("libc.so") + else: + from ctypes.util import find_library + path_libc = find_library("c") + libc = cdll.LoadLibrary(path_libc) + libc.printf(b"%s\n", b"Using the C printf function from Python ... ") + + +class RequestsTestCase(PythonTestMixIn, TestCase): + module_import = 'requests' + + def test_run_module(self): + import requests + + requests.get('https://kivy.org/') + + +class PillowTestCase(PythonTestMixIn, TestCase): + module_import = 'PIL' + + def test_run_module(self): + import os + from PIL import ( + Image as PilImage, + ImageOps, + ImageFont, + ImageDraw, + ImageFilter, + ImageChops, + ) + + text_to_draw = "Kivy" + img_target = "pillow_text_draw.png" + image_width = 200 + image_height = 100 + + img = PilImage.open("static/colours.png") + img = img.resize((image_width, image_height), PilImage.ANTIALIAS) + font = ImageFont.truetype("static/Blanka-Regular.otf", 55) + + draw = ImageDraw.Draw(img) + for n in range(2, image_width, 2): + draw.rectangle( + (n, n, image_width - n, image_height - n), outline="black" + ) + img.filter(ImageFilter.GaussianBlur(radius=1.5)) + + text_pos = (image_width / 2.0 - 55, 5) + halo = PilImage.new("RGBA", img.size, (0, 0, 0, 0)) + ImageDraw.Draw(halo).text( + text_pos, text_to_draw, font=font, fill="black" + ) + blurred_halo = halo.filter(ImageFilter.BLUR) + ImageDraw.Draw(blurred_halo).text( + text_pos, text_to_draw, font=font, fill="white" + ) + img = PilImage.composite( + img, blurred_halo, ImageChops.invert(blurred_halo) + ) + + img.save(img_target, "PNG") + self.assertTrue(os.path.isfile(img_target)) + + +class MatplotlibTestCase(PythonTestMixIn, TestCase): + module_import = 'matplotlib' + + def test_run_module(self): + import os + import numpy as np + from matplotlib import pyplot as plt + + fig, ax = plt.subplots() + ax.set_xlabel('test xlabel') + ax.set_ylabel('test ylabel') + ax.plot(np.random.random(50)) + ax.plot(np.sin(np.linspace(0, 3 * np.pi, 30))) + + ax.legend(['random numbers', 'sin']) + + fig.set_size_inches((5, 4)) + fig.tight_layout() + + fig.savefig('matplotlib_test.png', dpi=150) + self.assertTrue(os.path.isfile("matplotlib_test.png")) + + +class CryptographyTestCase(PythonTestMixIn, TestCase): + module_import = 'cryptography' + + def test_run_module(self): + from cryptography.fernet import Fernet + + key = Fernet.generate_key() + f = Fernet(key) + cryptography_encrypted = f.encrypt( + b'A really secret message. Not for prying eyes.') + cryptography_decrypted = f.decrypt(cryptography_encrypted) + + +class PycryptoTestCase(PythonTestMixIn, TestCase): + module_import = 'Crypto' + + def test_run_module(self): + from Crypto.Hash import SHA256 + + crypto_hash_message = 'A secret message' + hash = SHA256.new() + hash.update(crypto_hash_message) + crypto_hash_hexdigest = hash.hexdigest() + + +class PycryptodomeTestCase(PythonTestMixIn, TestCase): + module_import = 'Crypto' + + def test_run_module(self): + import os + from Crypto.PublicKey import RSA + + print('Ok imported pycryptodome, testing some basic operations...') + secret_code = "Unguessable" + key = RSA.generate(2048) + encrypted_key = key.export_key(passphrase=secret_code, pkcs=8, + protection="scryptAndAES128-CBC") + print('\t -> Testing key for secret code "Unguessable": {}'.format( + encrypted_key)) + + file_out = open("rsa_key.bin", "wb") + file_out.write(encrypted_key) + print('\t -> Testing key write: {}'.format( + 'ok' if os.path.exists("rsa_key.bin") else 'fail')) + self.assertTrue(os.path.exists("rsa_key.bin")) + + print('\t -> Testing Public key:'.format(key.publickey().export_key())) + + +class ScryptTestCase(PythonTestMixIn, TestCase): + module_import = 'scrypt' + + def test_run_module(self): + import scrypt + h1 = scrypt.hash('password', 'random salt') + # The hash should be 64 bytes (default value) + self.assertEqual(64, len(h1)) + + +class M2CryptoTestCase(PythonTestMixIn, TestCase): + module_import = 'M2Crypto' + + def test_run_module(self): + from M2Crypto import SSL + ctx = SSL.Context('sslv23') + + +class Pysha3TestCase(PythonTestMixIn, TestCase): + module_import = 'sha3' + + def test_run_module(self): + import sha3 + + print('Ok imported pysha3, testing some basic operations...') + k = sha3.keccak_512() + k.update(b"data") + print('Test pysha3 operation (keccak_512): {}'.format(k.hexdigest())) + + +class LibtorrentTestCase(PythonTestMixIn, TestCase): + module_import = 'libtorrent' + + def test_run_module(self): + import libtorrent as lt + + print('Imported libtorrent version {}'.format(lt.version)) diff --git a/testapps/on_device_unit_tests/test_app/tools.py b/testapps/on_device_unit_tests/test_app/tools.py new file mode 100644 index 0000000000..16dac01022 --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/tools.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- + +import glob +import unittest + +try: + # python2 case + from StringIO import StringIO +except ImportError: + # python3 case + from io import StringIO +from os.path import abspath, split, join + +from constants import RUNNING_ON_ANDROID + +APP_PATH = split(abspath(__file__))[0] + + +def run_test_suites_into_buffer(suites): + """Run a suite of unittests but into a buffer so we can read the result.""" + terminal_output = StringIO() + unittest.TextTestRunner(stream=terminal_output).run(suites) + return terminal_output.getvalue() + + +def get_images_with_extension(path=APP_PATH, extension='*.png'): + """ + Return a list of image files given a path and an file extension. + + .. note:: those image files are supposed to be created by our unittests + inside the app's root folder. + """ + return glob.glob(join(path, extension)) + + +def load_kv_from(kv_name): + """ + Load a kivy's kv file givel a kv filename. + + .. note:: requires `.kv` extension. + """ + from kivy.lang import Builder + + kv_file = join(APP_PATH, kv_name) + return Builder.load_file(kv_file) + + +def raise_error(error): + """ + A function to notify an error without raising an exception. + + .. warning:: we will try to notify via an kivy's Popup, but if kivy is not + installed, it will only print an error message. + """ + try: + from widgets import ErrorPopup + except ImportError: + print('raise_error:', error) + return + ErrorPopup(error_text=error).open() + + +def get_failed_unittests_from(unittests_output, set_of_tests): + """Parse unittests output trying to find the failed tests""" + failed_tests = set() + for test in set_of_tests: + if test in unittests_output: + failed_tests.add(test) + return failed_tests + + +def skip_if_not_running_from_android_device(func): + """ + Skip run of the function in case that we are running the app form android. + + .. note:: this is useful for some kind of tests that are supposed to be run + from an android device and relies on `pyjnius`. + """ + + def wrapper(*arg, **kwarg): + if RUNNING_ON_ANDROID: + return func(*arg, **kwarg) + raise_error( + 'Function `{func_name}` only available for android devices'.format( + func_name=func.__name__, + ), + ) + return None + + return wrapper + + +@skip_if_not_running_from_android_device +def get_android_python_activity(): + """ + Return the `PythonActivity.mActivity` using `pyjnius`. + + .. warning:: This function will only be ran if executed from android""" + from jnius import autoclass + + PythonActivity = autoclass('org.kivy.android.PythonActivity') + return PythonActivity.mActivity + + +@skip_if_not_running_from_android_device +def vibrate_with_pyjnius(time=1000): + """ + Vibrate an android device using `pyjnius`. + + .. warning:: This function will only be ran if executed from android.""" + from jnius import autoclass, cast + + print('Attempting to vibrate with pyjnius') + ANDROID_VERSION = autoclass('android.os.Build$VERSION') + SDK_INT = ANDROID_VERSION.SDK_INT + + Context = autoclass("android.content.Context") + activity = get_android_python_activity() + + vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) + vibrator = cast("android.os.Vibrator", vibrator_service) + + if vibrator and SDK_INT >= 26: + print("Using android's `VibrationEffect` (SDK >= 26)") + VibrationEffect = autoclass("android.os.VibrationEffect") + vibrator.vibrate( + VibrationEffect.createOneShot( + time, VibrationEffect.DEFAULT_AMPLITUDE, + ), + ) + elif vibrator: + print("Using deprecated android's vibrate (SDK < 26)") + vibrator.vibrate(time) + else: + print('Something happened...vibrator service disabled?') + + +@skip_if_not_running_from_android_device +def set_device_orientation(direction): + """ + Modifies the app orientation for an android device. + + .. warning:: This function will only be ran if executed from android.""" + if direction not in ('sensor', 'horizontal', 'vertical'): + print( + 'ERROR: asked to orient to `{direction}`, but we only support: ' + 'sensor, horizontal or vertical'.format(direction=direction) + ) + from jnius import autoclass + + activity = get_android_python_activity() + ActivityInfo = autoclass('android.content.pm.ActivityInfo') + + if direction == 'sensor': + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) + elif direction == 'horizontal': + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) + else: + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) diff --git a/testapps/on_device_unit_tests/test_app/widgets.kv b/testapps/on_device_unit_tests/test_app/widgets.kv new file mode 100644 index 0000000000..997e81000e --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/widgets.kv @@ -0,0 +1,84 @@ +#:import get_color_from_hex kivy.utils.get_color_from_hex +#:import FONT_SIZE_SUBTITLE constants.FONT_SIZE_SUBTITLE + +: + size_hint_y: None + height: dp(20) + + +: + text: '' + source: '' + size_hint_y: None + height: self.minimum_height + orientation: 'vertical' + canvas.before: + Color: + rgba: 1, 0, 1, .25 + Rectangle: + pos: self.pos + size: self.size + Spacer20: + Label: + text: root.text + size_hint_y: None + height: dp(60) + font_size: sp(FONT_SIZE_SUBTITLE) + canvas.before: + Color: + rgba: 0, 0, 0, .65 + Rectangle: + pos: self.x + 20, self.y + size: self.width - 40, self.height + BoxLayout: + size_hint_y: None + height: self.minimum_height + orientation: 'vertical' + canvas.before: + Color: + rgba: 0, 0, 0, .35 + Rectangle: + pos: self.x + 20, self.y + size: self.width - 40, self.height + Spacer20: + Image: + source: root.source + allow_stretch: True + size_hint_y: None + height: dp(120) + Spacer20: + Spacer20: + + +: + size_hint: None, None + size: dp(120), dp(120) + text: '' + background_color: None + canvas.before: + Color: + rgba: .34, .34, .34, 1 + Ellipse: + pos: self.pos + size: self.size + canvas: + Color: + rgba: + root.background_color \ + if root.background_color \ + else (1., 0., 1., .65) # purple + Ellipse: + pos: self.x + dp(2), self.y + dp(2) + size: self.width - dp(4), self.height - dp(4) + Label: + text: root.text + pos: root.pos + size_hint: None, None + size: root.size + + +: + title: 'Error' + size_hint: 0.75, 0.75 + Label: + text: root.error_text \ No newline at end of file diff --git a/testapps/on_device_unit_tests/test_app/widgets.py b/testapps/on_device_unit_tests/test_app/widgets.py new file mode 100644 index 0000000000..eab93405ae --- /dev/null +++ b/testapps/on_device_unit_tests/test_app/widgets.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +from kivy.properties import StringProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.popup import Popup +from kivy.uix.behaviors.button import ButtonBehavior +from kivy.uix.widget import Widget +from kivy.vector import Vector +from tools import load_kv_from + +load_kv_from('widgets.kv') + + +class Spacer20(Widget): + pass + + +class TestImage(BoxLayout): + text = StringProperty() + source = StringProperty() + + +class CircularButton(ButtonBehavior, Widget): + def collide_point(self, x, y): + return Vector(x, y).distance(self.center) <= self.width / 2 + + +class ErrorPopup(Popup): + error_text = StringProperty('') diff --git a/testapps/setup_keyboard.py b/testapps/setup_keyboard.py deleted file mode 100644 index 26499a639a..0000000000 --- a/testapps/setup_keyboard.py +++ /dev/null @@ -1,32 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'debug': None, - 'requirements': 'sdl2,pyjnius,kivy,python3', - 'blacklist-requirements': 'openssl,sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'ndk-dir': '/home/asandy/android/android-ndk-r17c', - 'dist-name': 'bdisttest', - 'ndk-version': '10.3.2', - 'permission': 'VIBRATE', - }} - -package_data = {'': ['*.py', - '*.png'] - } - -packages = find_packages() -print('packages are', packages) - -setup( - name='testapp_keyboard', - version='1.1', - description='p4a setup.py test', - author='Alexander Taylor', - author_email='alexanderjohntaylor@gmail.com', - packages=find_packages(), - options=options, - package_data={'setup_keyboard': ['*.py', '*.png']} -) diff --git a/testapps/setup_testapp_flask.py b/testapps/setup_testapp_flask.py deleted file mode 100644 index 3b2536e579..0000000000 --- a/testapps/setup_testapp_flask.py +++ /dev/null @@ -1,37 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'debug': None, - 'requirements': 'python3,flask,pyjnius', - 'blacklist-requirements': 'openssl,sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'ndk-dir': '/home/asandy/android/android-ndk-r17c', - 'dist-name': 'testapp_flask', - 'ndk-version': '10.3.2', - 'bootstrap': 'webview', - 'permissions': ['INTERNET', 'VIBRATE'], - 'arch': 'armeabi-v7a', - 'window': None, - }} - -package_data = {'': ['*.py', - '*.png'] - } - -packages = find_packages() -print('packages are', packages) - -setup( - name='testapp_flask', - version='1.0', - description='p4a flask testapp', - author='Alexander Taylor', - author_email='alexanderjohntaylor@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp_flask': ['*.py', '*.png'], - 'testapp_flask/static': ['*.png', '*.css'], - 'testapp_flask/templates': ['*.html']} -) diff --git a/testapps/setup_testapp_python2.py b/testapps/setup_testapp_python2.py deleted file mode 100644 index 9499c80c73..0000000000 --- a/testapps/setup_testapp_python2.py +++ /dev/null @@ -1,32 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'requirements': 'sdl2,numpy,pyjnius,kivy,python2', - 'android-api': 27, - 'ndk-api': 21, - 'ndk-dir': '/home/asandy/android/android-ndk-r17c', - 'dist-name': 'bdisttest_python2', - 'ndk-version': '10.3.2', - 'permission': 'VIBRATE', - 'arch': 'armeabi-v7a', - 'window': None, - }} - -package_data = {'': ['*.py', - '*.png'] - } - -packages = find_packages() -print('packages are', packages) - -setup( - name='testapp_python2', - 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/setup_testapp_python2_sqlite_openssl.py b/testapps/setup_testapp_python2_sqlite_openssl.py deleted file mode 100644 index c1dcf53efc..0000000000 --- a/testapps/setup_testapp_python2_sqlite_openssl.py +++ /dev/null @@ -1,25 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'requirements': 'sdl2,pyjnius,kivy,python2,openssl,requests,peewee,sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'ndk-dir': '/home/sandy/android/android-ndk-r17c', - 'dist-name': 'bdisttest_python2_sqlite_openssl', - 'ndk-version': '10.3.2', - 'permissions': ['INTERNET', 'VIBRATE'], - 'arch': 'armeabi-v7a', - 'window': None, - }} - -setup( - name='testapp_python2_sqlite_openssl', - version='1.1', - description='p4a setup.py test', - author='Alexander Taylor', - author_email='alexanderjohntaylor@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp_sqlite_openssl': ['*.py', '*.png']} -) diff --git a/testapps/setup_testapp_python3.py b/testapps/setup_testapp_python3.py deleted file mode 100644 index 33caf9eaac..0000000000 --- a/testapps/setup_testapp_python3.py +++ /dev/null @@ -1,30 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'requirements': 'sdl2,numpy,pyjnius,kivy,python3,hostpython3', - 'blacklist-requirements': 'openssl,sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'dist-name': 'bdisttest_python3_googlendk', - 'ndk-version': '10.3.2', - 'permission': 'VIBRATE', - }} - -package_data = {'': ['*.py', - '*.png'] - } - -packages = find_packages() -print('packages are', packages) - -setup( - name='python3_googlendk', - 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/setup_testapp_python3_matplotlib.py b/testapps/setup_testapp_python3_matplotlib.py deleted file mode 100644 index 014f4492b2..0000000000 --- a/testapps/setup_testapp_python3_matplotlib.py +++ /dev/null @@ -1,30 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'requirements': 'sdl2,python3,matplotlib,pyparsing,cycler,python-dateutil,numpy,kiwisolver,kivy', - 'blacklist-requirements': 'openssl,sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'dist-name': 'matplotlib_testapp', - 'ndk-version': '10.3.2', - 'permission': 'VIBRATE', - }} - -package_data = {'': ['*.py', - '*.png'] - } - -packages = find_packages() -print('packages are', packages) - -setup( - name='matplotlib_testapp', - version='0.1', - description='p4a setup.py test', - author='Alexander Taylor', - author_email='alexanderjohntaylor@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp_matplotlib': ['*.py', '*.png']} -) diff --git a/testapps/setup_testapp_python3_pillow.py b/testapps/setup_testapp_python3_pillow.py deleted file mode 100644 index a13c6af7e8..0000000000 --- a/testapps/setup_testapp_python3_pillow.py +++ /dev/null @@ -1,27 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'requirements': 'harfbuzz,sdl2,pillow,kivy,python3', - 'blacklist-requirements': 'sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'dist-name': 'pillow_testapp', - 'arch': 'armeabi-v7a', - 'permissions': ['VIBRATE'], - }} - -package_data = {'': ['*.py', - '*.png'] - } - -setup( - name='testapp_pillow', - version='1.0', - description='p4a setup.py test', - author='Pol Canelles', - author_email='canellestudi@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp_pillow': ['*.py', '*.png', '*.ttf']} -) diff --git a/testapps/setup_testapp_python3_sqlite_openssl.py b/testapps/setup_testapp_python3_sqlite_openssl.py index 0f7485d132..0dfd375bba 100644 --- a/testapps/setup_testapp_python3_sqlite_openssl.py +++ b/testapps/setup_testapp_python3_sqlite_openssl.py @@ -5,6 +5,7 @@ options = {'apk': {'requirements': 'requests,peewee,sdl2,pyjnius,kivy,python3', 'android-api': 27, 'ndk-api': 21, + 'bootstrap': 'sdl2', 'dist-name': 'bdisttest_python3_sqlite_openssl_googlendk', 'ndk-version': '10.3.2', 'arch': 'armeabi-v7a', diff --git a/testapps/setup_testapp_python_encryption.py b/testapps/setup_testapp_python_encryption.py deleted file mode 100644 index 2a468ade86..0000000000 --- a/testapps/setup_testapp_python_encryption.py +++ /dev/null @@ -1,30 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'requirements': 'sdl2,pyjnius,kivy,python3,cryptography,' - 'pycrypto,scrypt,m2crypto,pysha3,' - 'pycryptodome,libtorrent', - 'blacklist-requirements': 'sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'dist-name': 'bdisttest_encryption', - 'ndk-version': '10.3.2', - 'arch': 'armeabi-v7a', - 'permissions': ['INTERNET', 'VIBRATE'], - }} - -package_data = {'': ['*.py', - '*.png'] - } - -setup( - name='testapp_encryption', - version='1.0', - description='p4a setup.py test', - author='Pol Canelles', - author_email='canellestudi@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp_encryption': ['*.py', '*.png']} -) diff --git a/testapps/setup_testapp_service.py b/testapps/setup_testapp_service.py deleted file mode 100644 index a3342d43a6..0000000000 --- a/testapps/setup_testapp_service.py +++ /dev/null @@ -1,34 +0,0 @@ - -from distutils.core import setup -from setuptools import find_packages - -options = {'apk': {'debug': None, - 'requirements': 'python3,genericndkbuild,pyjnius', - 'blacklist-requirements': 'openssl,sqlite3', - 'android-api': 27, - 'ndk-api': 21, - 'sdk-dir':'/opt/android/android-sdk/', - 'ndk-dir':'/opt/android/android-ndk-r17c/', - 'dist-name': 'testapp_service', - 'ndk-version': '10.3.2', - 'bootstrap': 'service_only', - 'permissions': ['INTERNET', 'VIBRATE'], - 'arch': 'armeabi-v7a', - 'service': 'time:p4atime.py', - }} - -package_data = {'': ['*.py']} - -packages = find_packages() -print('packages are', packages) - -setup( - name='testapp_service', - version='1.1', - description='p4a service testapp', - author='Alexander Taylor', - author_email='alexanderjohntaylor@gmail.com', - packages=find_packages(), - options=options, - package_data={'testapp_service': ['*.py']} -) diff --git a/testapps/setup_vispy.py b/testapps/setup_vispy.py index 49ad47fda3..cbc909627b 100644 --- a/testapps/setup_vispy.py +++ b/testapps/setup_vispy.py @@ -7,6 +7,7 @@ 'blacklist-requirements': 'openssl,sqlite3', 'android-api': 27, 'ndk-api': 21, + 'bootstrap': 'empty', 'ndk-dir': '/home/asandy/android/android-ndk-r17c', 'dist-name': 'bdisttest', 'ndk-version': '10.3.2', diff --git a/testapps/testapp/main.py b/testapps/testapp/main.py deleted file mode 100644 index 2148e7c68a..0000000000 --- a/testapps/testapp/main.py +++ /dev/null @@ -1,166 +0,0 @@ -print('main.py was successfully called') - -import os -print('imported os') - - -print('this dir is', os.path.abspath(os.curdir)) - -print('contents of this dir', os.listdir('./')) - -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 -#:import sys sys - -: - 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' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: sys.version - halign: 'center' - padding_y: dp(10) - 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, cast - except ImportError: - raise_error('Could not import pyjnius') - return - print('Attempting to vibrate with pyjnius') - ANDROID_VERSION = autoclass('android.os.Build$VERSION') - SDK_INT = ANDROID_VERSION.SDK_INT - - Context = autoclass("android.content.Context") - PythonActivity = autoclass('org.kivy.android.PythonActivity') - activity = PythonActivity.mActivity - - vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) - vibrator = cast("android.os.Vibrator", vibrator_service) - - if vibrator and SDK_INT >= 26: - print("Using android's `VibrationEffect` (SDK >= 26)") - VibrationEffect = autoclass("android.os.VibrationEffect") - vibrator.vibrate( - VibrationEffect.createOneShot( - 1000, VibrationEffect.DEFAULT_AMPLITUDE, - ), - ) - elif vibrator: - print("Using deprecated android's vibrate (SDK < 26)") - vibrator.vibrate(1000) - else: - print('Something happened...vibrator service disabled?') - - 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() diff --git a/testapps/testapp_encryption/colours.png b/testapps/testapp_encryption/colours.png deleted file mode 100644 index 30b685e32b..0000000000 Binary files a/testapps/testapp_encryption/colours.png and /dev/null differ diff --git a/testapps/testapp_encryption/main.py b/testapps/testapp_encryption/main.py deleted file mode 100644 index 3b4db91020..0000000000 --- a/testapps/testapp_encryption/main.py +++ /dev/null @@ -1,360 +0,0 @@ -print('main.py was successfully called') - -import os - -print('imported os') - -print('this dir is', os.path.abspath(os.curdir)) - -print('contents of this dir', os.listdir('./')) - -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) - -# Test cryptography -try: - from cryptography.fernet import Fernet - - key = Fernet.generate_key() - f = Fernet(key) - cryptography_encrypted = f.encrypt( - b'A really secret message. Not for prying eyes.') - cryptography_decrypted = f.decrypt(cryptography_encrypted) -except Exception as e1: - print('**************************') - print('Error on cryptography operations:\n{}'.format(e1)) - print('**************************') - cryptography_encrypted = 'Error' - cryptography_decrypted = 'Error' - -# Test pycrypto -crypto_hash_message = 'A secret message' -try: - from Crypto.Hash import SHA256 - - hash = SHA256.new() - hash.update(crypto_hash_message) - crypto_hash_hexdigest = hash.hexdigest() -except Exception as e2: - print('**************************') - print('Error on Crypto operations:\n{}'.format(e2)) - print('**************************') - crypto_hash_hexdigest = 'Error' - -# Test scrypt -try: - from scrypt import * - - status_import_scrypt = 'Success' -except ImportError as e3: - print('**************************') - print('Unable to import scrypt:\n{}'.format(e3)) - print('**************************') - status_import_scrypt = 'Error' - -# Test M2Crypto -try: - from M2Crypto import * - - status_import_m2crypto = 'Success' -except ImportError as e5: - print('**************************') - print('Unable to import M2Crypto:\n{}'.format(e5)) - print('**************************\n') - status_import_m2crypto = 'Error' - -# Test pysha3 -try: - import sha3 - - print('Ok imported pysha3, testing some basic operations...') - k = sha3.keccak_512() - k.update(b"data") - print('Test pysha3 operation (keccak_512): {}'.format(k.hexdigest())) - status_import_pysha3 = 'Success' -except ImportError as e6: - print('**************************') - print('Unable to import/operate with pysha3:\n{}'.format(e6)) - print('**************************') - status_import_pysha3 = 'Error' - -# Test pycryptodome -try: - from Crypto.PublicKey import RSA - - print('Ok imported pycryptodome, testing some basic operations...') - secret_code = "Unguessable" - key = RSA.generate(2048) - encrypted_key = key.export_key(passphrase=secret_code, pkcs=8, - protection="scryptAndAES128-CBC") - print('\t -> Testing key for secret code "Unguessable": {}'.format( - encrypted_key)) - - file_out = open("rsa_key.bin", "wb") - file_out.write(encrypted_key) - print('\t -> Testing key write: {}'.format( - 'ok' if os.path.exists(file_out) else 'fail')) - - print('\t -> Testing Public key:'.format(key.publickey().export_key())) - status_import_pycryptodome = 'Success (import and doing simple operations)' -except ImportError as e6: - print('**************************') - print('Unable to import/operate with pycryptodome:\n{}'.format(e6)) - print('**************************') - status_import_pycryptodome = 'Error' - -# Test libtorrent -try: - import libtorrent as lt - - print('Imported libtorrent version {}'.format(lt.version)) - status_import_libtorrent = 'Success (version is: {})'.format(lt.version) -except Exception as e4: - print('**************************') - print('Unable to import libtorrent:\n{}'.format(e4)) - print('**************************') - status_import_libtorrent = 'Error' - -kv = ''' -#:import Metrics kivy.metrics.Metrics -#:import sys sys - -: - size_hint_y: None - height: dp(60) - -: - orientation: 'vertical' - size_hint_y: None - height: self.minimum_height - test_module: '' - test_result: '' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: '[b]*** TEST {} MODULE ***[/b]'.format(self.parent.test_module) - halign: 'center' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: - 'Import {}: [color=a0a0a0]{}[/color]'.format( - self.parent.test_module, self.parent.test_result) - halign: 'left' - Widget: - size_hint_y: None - height: 20 - - -ScrollView: - GridLayout: - cols: 1 - size_hint_y: None - height: self.minimum_height - FixedSizeButton: - text: 'test pyjnius' - on_press: app.test_pyjnius() - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: '[b]*** TEST CRYPTOGRAPHY MODULE ***[/b]' - halign: 'center' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: - 'Cryptography decrypted:\\n[color=a0a0a0]%s[/color]\\n' \\ - 'Cryptography encrypted:\\n[color=a0a0a0]%s[/color]' % ( - app.cryptography_decrypted, app.cryptography_encrypted) - halign: 'left' - Widget: - size_hint_y: None - height: 20 - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: '[b]*** TEST CRYPTO MODULE ***[/b]' - halign: 'center' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: - 'Crypto message: \\n[color=a0a0a0]%s[/color]\\n'\\ - 'Crypto hex: \\n[color=a0a0a0]%s[/color]' % ( - app.crypto_hash_message, app.crypto_hash_hexdigest) - halign: 'left' - Widget: - size_hint_y: None - height: 20 - TestImport: - test_module: 'scrypt' - test_result: app.status_import_scrypt - TestImport: - test_module: 'm2crypto' - test_result: app.status_import_m2crypto - TestImport: - test_module: 'pysha3' - test_result: app.status_import_pysha3 - TestImport: - test_module: 'pycryptodome' - test_result: app.status_import_pycryptodome - TestImport: - test_module: 'libtorrent' - test_result: app.status_import_libtorrent - 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' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - markup: True - text: sys.version - halign: 'center' - padding_y: dp(10) - 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: [color=a0a0a0]%s[/color]\\n'\\ - 'density: [color=a0a0a0]%s[/color]\\n'\\ - 'fontscale: [color=a0a0a0]%s[/color]' % ( - Metrics.dpi, Metrics.density, Metrics.fontscale) - halign: 'center' - FixedSizeButton: - text: 'test ctypes' - on_press: app.test_ctypes() - 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): - cryptography_encrypted = cryptography_encrypted - cryptography_decrypted = cryptography_decrypted - crypto_hash_message = crypto_hash_message - crypto_hash_hexdigest = crypto_hash_hexdigest - status_import_scrypt = status_import_scrypt - status_import_m2crypto = status_import_m2crypto - status_import_pysha3 = status_import_pysha3 - status_import_pycryptodome = status_import_pycryptodome - status_import_libtorrent = status_import_libtorrent - - 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, cast - except ImportError: - raise_error('Could not import pyjnius') - return - print('Attempting to vibrate with pyjnius') - ANDROID_VERSION = autoclass('android.os.Build$VERSION') - SDK_INT = ANDROID_VERSION.SDK_INT - - Context = autoclass("android.content.Context") - PythonActivity = autoclass('org.kivy.android.PythonActivity') - activity = PythonActivity.mActivity - - vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) - vibrator = cast("android.os.Vibrator", vibrator_service) - - if vibrator and SDK_INT >= 26: - print("Using android's `VibrationEffect` (SDK >= 26)") - VibrationEffect = autoclass("android.os.VibrationEffect") - vibrator.vibrate( - VibrationEffect.createOneShot( - 1000, VibrationEffect.DEFAULT_AMPLITUDE, - ), - ) - elif vibrator: - print("Using deprecated android's vibrate (SDK < 26)") - vibrator.vibrate(1000) - else: - print('Something happened...vibrator service disabled?') - - def test_ctypes(self, *args): - import ctypes - - -TestApp().run() diff --git a/testapps/testapp_flask/main.py b/testapps/testapp_flask/main.py deleted file mode 100644 index 1f74d1e9e6..0000000000 --- a/testapps/testapp_flask/main.py +++ /dev/null @@ -1,104 +0,0 @@ -print('main.py was successfully called') -print('this is the new main.py') - -import sys -print('python version is: ' + sys.version) -print('python path is', sys.path) - -import os -print('imported os') - -import flask -print('flask1???') - -print('contents of this dir', os.listdir('./')) - -import flask -print('flask???') - - -from flask import Flask -app = Flask(__name__) - -from flask import (Flask, url_for, render_template, request, redirect, - flash) - -print('imported flask etc') -print('importing pyjnius') - -from jnius import autoclass, cast - -ANDROID_VERSION = autoclass('android.os.Build$VERSION') -SDK_INT = ANDROID_VERSION.SDK_INT -Context = autoclass('android.content.Context') -PythonActivity = autoclass('org.kivy.android.PythonActivity') -activity = PythonActivity.mActivity - -vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) -vibrator = cast("android.os.Vibrator", vibrator_service) - -ActivityInfo = autoclass('android.content.pm.ActivityInfo') - -@app.route('/') -def page1(): - return render_template('index.html') - -@app.route('/page2') -def page2(): - return render_template('page2.html') - -@app.route('/vibrate') -def vibrate(): - args = request.args - if 'time' not in args: - print('ERROR: asked to vibrate but without time argument') - print('asked to vibrate', args['time']) - - if vibrator and SDK_INT >= 26: - print("Using android's `VibrationEffect` (SDK >= 26)") - VibrationEffect = autoclass("android.os.VibrationEffect") - vibrator.vibrate( - VibrationEffect.createOneShot( - int(float(args['time']) * 1000), - VibrationEffect.DEFAULT_AMPLITUDE, - ), - ) - elif vibrator: - print("Using deprecated android's vibrate (SDK < 26)") - vibrator.vibrate(int(float(args['time']) * 1000)) - else: - print('Something happened...vibrator service disabled?') - print('vibrated') - -@app.route('/loadUrl') -def loadUrl(): - args = request.args - if 'url' not in args: - print('ERROR: asked to open an url but without url argument') - print('asked to open url', args['url']) - activity.loadUrl(args['url']) - -@app.route('/orientation') -def orientation(): - args = request.args - if 'dir' not in args: - print('ERROR: asked to orient but no dir specified') - direction = args['dir'] - if direction not in ('horizontal', 'vertical'): - print('ERROR: asked to orient to neither horizontal nor vertical') - - if direction == 'horizontal': - activity.setRequestedOrientation( - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) - else: - activity.setRequestedOrientation( - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) - - -from os import curdir -from os.path import realpath -print('curdir', realpath(curdir)) -if realpath(curdir).startswith('/data'): - app.run(debug=False) -else: - app.run(debug=True) diff --git a/testapps/testapp_flask/static/colours.png b/testapps/testapp_flask/static/colours.png deleted file mode 100644 index 30b685e32b..0000000000 Binary files a/testapps/testapp_flask/static/colours.png and /dev/null differ diff --git a/testapps/testapp_flask/templates/base.html b/testapps/testapp_flask/templates/base.html deleted file mode 100644 index 63b4881302..0000000000 --- a/testapps/testapp_flask/templates/base.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - {% block title %} - Flask on Android - {% endblock %} - - - - - - -

- Flask on Android! -

- - {% block body %} - {% endblock %} - - - - diff --git a/testapps/testapp_flask/templates/index.html b/testapps/testapp_flask/templates/index.html deleted file mode 100644 index 78c38b3eaa..0000000000 --- a/testapps/testapp_flask/templates/index.html +++ /dev/null @@ -1,63 +0,0 @@ -{% extends "base.html" %} - - -{% block body %} - -

Page one

- - - -
- -
- - - - - - - - - - - - - - - - - -{% endblock %} diff --git a/testapps/testapp_flask/templates/page2.html b/testapps/testapp_flask/templates/page2.html deleted file mode 100644 index 70fca15f03..0000000000 --- a/testapps/testapp_flask/templates/page2.html +++ /dev/null @@ -1,15 +0,0 @@ - -{% extends "base.html" %} - - -{% block body %} - -

Page two

- - - -
- -
- -{% endblock %} diff --git a/testapps/testapp_keyboard/colours.png b/testapps/testapp_keyboard/colours.png deleted file mode 100644 index 30b685e32b..0000000000 Binary files a/testapps/testapp_keyboard/colours.png and /dev/null differ diff --git a/testapps/testapp_keyboard/main.py b/testapps/testapp_keyboard/main.py deleted file mode 100644 index 27cea40d18..0000000000 --- a/testapps/testapp_keyboard/main.py +++ /dev/null @@ -1,163 +0,0 @@ -print('main.py was successfully called') - -import os -print('imported os') -import sys -print('imported sys') - -from kivy import platform - -if platform == 'android': - site_dir_path = './_python_bundle/site-packages' - if not os.path.exists(site_dir_path): - print('warning: site-packages dir not found: ' + site_dir_path) - else: - print('contents of ' + site_dir_path) - print(os.listdir(site_dir_path)) - - print('this dir is', os.path.abspath(os.curdir)) - - print('contents of this dir', os.listdir('./')) - - if (os.path.exists(site_dir_path) and - os.path.exists(site_dir_path + '/kivy/app.pyo') - ): - with open(site_dir_path + '/kivy/app.pyo', 'rb') as fileh: - print('app.pyo size is', len(fileh.read())) - -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 -#:import Window kivy.core.window.Window - -: - size_hint_y: None - height: dp(60) - - -BoxLayout: - orientation: 'vertical' - BoxLayout: - size_hint_y: None - height: dp(50) - orientation: 'horizontal' - Button: - text: 'None' - on_press: Window.softinput_mode = '' - Button: - text: 'pan' - on_press: Window.softinput_mode = 'pan' - Button: - text: 'below_target' - on_press: Window.softinput_mode = 'below_target' - Button: - text: 'resize' - on_press: Window.softinput_mode = 'resize' - Widget: - Scatter: - id: scatter - size_hint: None, None - size: dp(300), dp(80) - on_parent: self.pos = (300, 100) - BoxLayout: - size: scatter.size - orientation: 'horizontal' - canvas: - Color: - rgba: 1, 0, 0, 1 - Rectangle: - pos: 0, 0 - size: self.size - Widget: - size_hint_x: None - width: dp(30) - TextInput: - text: 'type in me' -''' - - -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, cast - except ImportError: - raise_error('Could not import pyjnius') - return - print('Attempting to vibrate with pyjnius') - ANDROID_VERSION = autoclass('android.os.Build$VERSION') - SDK_INT = ANDROID_VERSION.SDK_INT - - Context = autoclass("android.content.Context") - PythonActivity = autoclass('org.kivy.android.PythonActivity') - activity = PythonActivity.mActivity - - vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) - vibrator = cast("android.os.Vibrator", vibrator_service) - - if vibrator and SDK_INT >= 26: - print("Using android's `VibrationEffect` (SDK >= 26)") - VibrationEffect = autoclass("android.os.VibrationEffect") - vibrator.vibrate( - VibrationEffect.createOneShot( - 1000, VibrationEffect.DEFAULT_AMPLITUDE, - ), - ) - elif vibrator: - print("Using deprecated android's vibrate (SDK < 26)") - vibrator.vibrate(1000) - else: - print('Something happened...vibrator service disabled?') - - 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() diff --git a/testapps/testapp_matplotlib/main.py b/testapps/testapp_matplotlib/main.py deleted file mode 100644 index 2c2d9d00e7..0000000000 --- a/testapps/testapp_matplotlib/main.py +++ /dev/null @@ -1,75 +0,0 @@ -print('importing numpy') -import numpy as np -print('imported numpy') - -print('importing matplotlib') - -import matplotlib -print('imported matplotlib') - -print('importing pyplot') - -from matplotlib import pyplot as plt - -print('imported pyplot') - -fig, ax = plt.subplots() - -print('created fig and ax') - -ax.plot(np.random.random(50)) - -print('plotted something') - -ax.set_xlabel('test label') - -print('set a label') - -fig.set_size_inches((5, 4)) -fig.savefig('test.png') - -print('saved fig') - -from kivy.app import App -from kivy.base import runTouchApp -from kivy.uix.image import Image -from kivy.lang import Builder - -class MatplotlibApp(App): - def build(self): - root = Builder.load_string(""" -BoxLayout: - orientation: 'vertical' - Image: - id: the_image - source: 'test.png' - allow_stretch: True - Button: - size_hint_y: None - height: dp(40) - text: 'new plot' - on_release: app.generate_new_plot() - """) - return root - - def generate_new_plot(self): - fig, ax = plt.subplots() - ax.set_xlabel('test xlabel') - ax.set_ylabel('test ylabel') - ax.plot(np.random.random(50)) - ax.plot(np.sin(np.linspace(0, 3*np.pi, 30))) - - ax.legend(['random numbers', 'sin']) - - fig.set_size_inches((5, 4)) - fig.tight_layout() - - fig.savefig('test.png', dpi=150) - - self.root.ids.the_image.reload() - - - - -MatplotlibApp().run() -runTouchApp(Image(source='test.png', allow_stretch=True)) diff --git a/testapps/testapp_nogui/main.py b/testapps/testapp_nogui/main.py deleted file mode 100644 index bb7506fb48..0000000000 --- a/testapps/testapp_nogui/main.py +++ /dev/null @@ -1,73 +0,0 @@ - -from math import sqrt -print('import math worked') - -import sys - -print('sys.path is', sys.path) - -for i in range(45, 50): - print(i, sqrt(i)) - -print('trying to import six') -try: - import six -except ImportError: - print('import failed') - - -print('trying to import six again') -try: - import six -except ImportError: - print('import failed (again?)') -print('import six worked!') - -print('Just printing stuff apparently worked, trying pyjnius') - -import jnius - -print('Importing jnius worked') - -print('trying to import stuff') - -try: - from jnius import cast -except ImportError: - print('cast failed') - -try: - from jnius import ensureclass -except ImportError: - print('ensureclass failed') - -try: - from jnius import JavaClass -except ImportError: - print('JavaClass failed') - -try: - from jnius import jnius -except ImportError: - print('jnius failed') - -try: - from jnius import reflect -except ImportError: - print('reflect failed') - -try: - from jnius import find_javaclass -except ImportError: - print('find_javaclass failed') - -print('Trying to autoclass activity') - -from jnius import autoclass - -print('Imported autoclass') - -PythonActivity = autoclass('org.kivy.android.PythonActivity') - -print(':o the autoclass worked!') - diff --git a/testapps/testapp_pillow/colours.png b/testapps/testapp_pillow/colours.png deleted file mode 100644 index 30b685e32b..0000000000 Binary files a/testapps/testapp_pillow/colours.png and /dev/null differ diff --git a/testapps/testapp_pillow/main.py b/testapps/testapp_pillow/main.py deleted file mode 100644 index fa0c1255df..0000000000 --- a/testapps/testapp_pillow/main.py +++ /dev/null @@ -1,416 +0,0 @@ -print("main.py was successfully called") - -import os -import subprocess -from threading import Thread -from functools import partial - -import kivy -from kivy.app import App -from kivy.lang import Builder -from kivy.properties import StringProperty -from kivy.uix.image import Image -from kivy.uix.popup import Popup -from kivy.clock import Clock -from kivy.metrics import dp, sp -from kivy.properties import BooleanProperty, ListProperty - -try: - from PIL import ( - Image as PilImage, - ImageOps, - ImageFont, - ImageDraw, - ImageFilter, - ImageChops, - ) - - status_import_pil = "Succeed" -except Exception as e1: - print("Error on import Pil:\n{}".format(e1)) - status_import_pil = "Errored" - - -def start_func_thread(func, *args, **kwargs): - """Take a function as an argument and start a thread to execute it""" - func_thread = Thread(None, target=func, args=args, kwargs=kwargs) - func_thread.setDaemon(True) - func_thread.start() - - -def add_processed_image(image_path, evt): - """A function that gets our running app and add the processed image we - provide via argument to the ListProperty :attr:`TestApp.processed_images`, - so our app can automatically display the image via kivy's events. - """ - app = App.get_running_app() - app.processed_images.append(image_path) - - -def remove_test_file_if_exist(file_name): - """Check if the provided file (as an argument), exists and remove it in - case that exists.""" - if os.path.isfile(file_name): - print("\t- Removing test file: {}".format(file_name)) - subprocess.call(["rm", file_name]) - - -def test_pil_draw( - text_to_draw="Kivy", - img_target="text_draw.png", - image_width=180, - image_height=100, -): - """A method to test some of the `Pillow`'s package operations: - - - `Image.open` - - `ImageDraw.Draw` - - `ImageFont.truetype` (here we will now if our freetype's recipe - (:class:`~pythonforandroid.recipes.freetype.FreetypeRecipe`) works - as expected - - `ImageFilter.GaussianBlur` - - `Image.composite` - - `ImageChops.invert` - - .. note:: With this test we will know if our freetype library - (:class:`~pythonforandroid.recipes.freetype.FreetypeRecipe`) is - working as expected because we load the default kivy's font (Roboto) - and we use it to draw a text - """ - print( - "Test draw font with pil: {} [width: {}]".format( - text_to_draw, image_width - ) - ) - remove_test_file_if_exist(img_target) - - try: - ttf_font = os.path.join( - os.path.dirname(kivy.__file__), - "data", - "fonts", - "Roboto-Regular.ttf", - ) - img = PilImage.open("colours.png") - img = img.resize((image_width, image_height), PilImage.ANTIALIAS) - font = ImageFont.truetype(ttf_font, int(sp(55))) - - draw = ImageDraw.Draw(img) - for n in range(2, image_width, 2): - draw.rectangle( - (n, n, image_width - n, image_height - n), outline="black" - ) - img.filter(ImageFilter.GaussianBlur(radius=1.5)) - - text_pos = (image_width / 2.0 - int(sp(40)), int(sp(15))) - halo = PilImage.new("RGBA", img.size, (0, 0, 0, 0)) - ImageDraw.Draw(halo).text( - text_pos, text_to_draw, font=font, fill="black" - ) - blurred_halo = halo.filter(ImageFilter.BLUR) - ImageDraw.Draw(blurred_halo).text( - text_pos, text_to_draw, font=font, fill="white" - ) - img = PilImage.composite( - img, blurred_halo, ImageChops.invert(blurred_halo) - ) - - img.save(img_target, "PNG") - except Exception as e: - print("Cannot draw text with pil, error: {}".format(e)) - - if os.path.isfile(img_target): - Clock.schedule_once(partial(add_processed_image, img_target), 0.1) - else: - raise_error("Could not draw text with pil") - - -def test_pil_filter(img_source="text_draw.png", img_target="text_blur.png"): - """A method to test `Pillows`'s `ImageFilter.GaussianBlur`.""" - print("Test pil's gaussian filter: {}".format(img_source)) - remove_test_file_if_exist(img_target) - img = PilImage.open(img_source) - gaussian_image = img.filter(ImageFilter.GaussianBlur(radius=1.5)) - gaussian_image.save(img_target) - if os.path.isfile(img_target): - Clock.schedule_once(partial(add_processed_image, img_target), 0.1) - else: - raise_error("Could not draw text with pil") - - -def test_pil_mirror(img_source="text_draw.png", img_target="text_mirror.png"): - """A method to test `Pillows`'s `ImageOps.mirror`.""" - print("Test convert image to mirror: {}".format(img_source)) - remove_test_file_if_exist(img_target) - - try: - im = PilImage.open(img_source) - mirror_image = ImageOps.mirror(im) - mirror_image.save(img_target) - except Exception as e: - print( - "Cannot make mirrored image for `{}` [ERROR: {}]".format( - img_source, e - ) - ) - - if os.path.isfile(img_target): - Clock.schedule_once(partial(add_processed_image, img_target), 0.1) - else: - raise_error("Could not create a mirrored image") - - -def test_pil_rotate( - img_source="text_draw.png", img_target="text_draw_rotated.png" -): - """A method to test `Pillows`'s `Image.rotate`.""" - print("Test image rotate with image: {}".format(img_source)) - remove_test_file_if_exist(img_target) - - try: - im = PilImage.open(img_source) - im.rotate(180, expand=True).save(img_target, "png") - except Exception as e: - print("Cannot rotate image `{}` [ERROR: {}]".format(img_source, e)) - - if os.path.isfile(img_target): - Clock.schedule_once(partial(add_processed_image, img_target), 0.1) - else: - raise_error("Could not rotate image") - - -kv = """ -#:import Metrics kivy.metrics.Metrics -#:import sys sys - -: - size_hint_y: None - height: dp(100) - -ScrollView: - BoxLayout: - orientation: 'vertical' - size_hint_y: None - height: self.minimum_height - spacing: dp(10) - 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' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - font_size: 50 - markup: True - text: '[color=a0a0a0]{}[/color]'.format(sys.version) - halign: 'center' - padding_y: dp(10) - Label: - height: self.texture_size[1] - size_hint_y: None - font_size: 50 - text_size: self.size[0], None - markup: True - text: - 'dpi: [color=a0a0a0]{}[/color]\\n'\\ - 'density: [color=a0a0a0]{}[/color]\\n'\\ - 'fontscale: [color=a0a0a0]{}[/color]'.format( - Metrics.dpi, Metrics.density, Metrics.fontscale) - halign: 'center' - Label: - height: self.texture_size[1] - size_hint_y: None - text_size: self.size[0], None - text_color: "0c8916" if "Succeed" in self.text else "bc1607" - font_size: 50 - markup: True - text: - 'Import PIL module: [color={}]{}[/color]'.format( - self.text_color, app.status_import_pil) - halign: 'center' - FixedSizeButton: - text: 'test ctypes' - on_press: app.test_ctypes() - height: dp(60) - FixedSizeButton: - text: 'test pyjnius' - on_press: app.test_pyjnius() - height: dp(60) - BoxLayout: - orientation: 'horizontal' - size_hint_y: None - height: dp(430) - BoxLayout: - orientation: 'vertical' - spacing: dp(10) - FixedSizeButton: - text: 'test Pil draw text' - disabled: - ("Error" in app.status_import_pil or app.processed_draw) - on_release: app.test_pil_draw() - FixedSizeButton: - text: 'test Pil gaussian filter' - disabled: (not app.processed_draw or app.processed_filter) - on_release: app.test_pil_filter() - FixedSizeButton: - text: 'test Pil mirror' - disabled: (not app.processed_filter or app.processed_mirror) - on_release: app.test_pil_mirror() - FixedSizeButton: - text: 'test Pil rotate 180' - disabled: (not app.processed_mirror or app.processed_rotate) - on_release: app.test_pil_rotate() - Widget: - size_hint_x: None - width: dp(10) - BoxLayout: - id: images_container - orientation: 'vertical' - spacing: dp(10) - Widget: - size_hint_y: None - height: dp(430 - 110 * (len(self.parent.children) - 1)) - canvas.before: - Color: - rgba: - (.1, .1, .1, - 1 if len(self.parent.children) < 5 - else 0) - Rectangle: - pos: self.pos[0], self.pos[1] + dp(2) - size: self.size - Widget: - size_hint_y: None - height: dp(10) - -: - title: 'Error' - size_hint: 0.75, 0.75 - Label: - text: root.error_text -""" - - -class ErrorPopup(Popup): - """A pop intended to be used to display error messages""" - error_text = StringProperty("") - - -def raise_error(error): - """Method that will take a message as an argument and will display it in a - a Popup :class:`ErrorPopup`.""" - print("ERROR:", error) - ErrorPopup(error_text=error).open() - - -class TestApp(App): - status_import_pil = status_import_pil - processed_draw = BooleanProperty(False) - processed_filter = BooleanProperty(False) - processed_rotate = BooleanProperty(False) - processed_mirror = BooleanProperty(False) - processed_images = ListProperty() - - def build(self): - return Builder.load_string(kv) - - def on_pause(self): - return True - - def on_stop(self): - print("Removing generated test images...") - for w in self.root.ids.images_container.children[:]: - if hasattr(w, "source"): - self.root.ids.images_container.remove_widget(w) - remove_test_file_if_exist(w.source) - - def test_pyjnius(self, *args): - try: - from jnius import autoclass, cast - except ImportError: - raise_error('Could not import pyjnius') - return - print('Attempting to vibrate with pyjnius') - ANDROID_VERSION = autoclass('android.os.Build$VERSION') - SDK_INT = ANDROID_VERSION.SDK_INT - - Context = autoclass("android.content.Context") - PythonActivity = autoclass('org.kivy.android.PythonActivity') - activity = PythonActivity.mActivity - - vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) - vibrator = cast("android.os.Vibrator", vibrator_service) - - if vibrator and SDK_INT >= 26: - print("Using android's `VibrationEffect` (SDK >= 26)") - VibrationEffect = autoclass("android.os.VibrationEffect") - vibrator.vibrate( - VibrationEffect.createOneShot( - 1000, VibrationEffect.DEFAULT_AMPLITUDE, - ), - ) - elif vibrator: - print("Using deprecated android's vibrate (SDK < 26)") - vibrator.vibrate(1000) - else: - print('Something happened...vibrator service disabled?') - - def test_ctypes(self, *args): - try: - import ctypes - except ImportError: - raise_error("Could not import ctypes") - return - - def test_pil_draw(self, *args): - self.processed_draw = True - start_func_thread( - test_pil_draw, - image_width=int(self.root.width / 2.0), - image_height=int(dp(100)), - ) - - def test_pil_filter(self, *args): - self.processed_filter = True - start_func_thread(test_pil_filter) - - def test_pil_mirror(self, *args): - self.processed_mirror = True - start_func_thread(test_pil_mirror) - - def test_pil_rotate(self, *args): - self.processed_rotate = True - start_func_thread(test_pil_rotate) - - def on_processed_images(self, *args): - print( - "New processed images detected [{} image/s]".format( - len(self.processed_images) - ) - ) - # Display the processed images - while self.processed_images: - img_path = self.processed_images.pop(-1) - print("Building image widget for: {}".format(img_path)) - im = Image( - source=img_path, - size_hint=(1.0, None), - height=dp(100), - keep_ratio=True, - allow_stretch=True, - ) - self.root.ids.images_container.add_widget(im, index=1) - - -TestApp().run() diff --git a/testapps/testapp_service/main.py b/testapps/testapp_service/main.py deleted file mode 100644 index dc781cd3bd..0000000000 --- a/testapps/testapp_service/main.py +++ /dev/null @@ -1,19 +0,0 @@ -print('Service Test App main.py was successfully called') - -import sys -print('python version is: ', sys.version) -print('python sys.path is: ', sys.path) - -from math import sqrt -print('import math worked') - -for i in range(45, 50): - print(i, sqrt(i)) - -print('Just printing stuff apparently worked, trying a simple service') - -from jnius import autoclass -service = autoclass('org.test.testapp_service.ServiceTime') -mActivity = autoclass('org.kivy.android.PythonActivity').mActivity -argument = 'test argument ok' -service.start(mActivity, argument) diff --git a/testapps/testapp_service/p4atime.py b/testapps/testapp_service/p4atime.py deleted file mode 100644 index 75a1d76e0f..0000000000 --- a/testapps/testapp_service/p4atime.py +++ /dev/null @@ -1,19 +0,0 @@ -import datetime -import threading -import time -from os import environ -argument = environ.get('PYTHON_SERVICE_ARGUMENT', '') -print('p4atime.py was successfully called with argument: "{}"'.format(argument)) - -next_call = time.time() - - -def service_timer(): - global next_call - print('P4a datetime service: {}'.format(datetime.datetime.now())) - next_call = next_call + 1 - threading.Timer(next_call - time.time(), service_timer).start() - - -print('Starting the service timer...') -service_timer() diff --git a/testapps/testlauncher_setup/sdl2.py b/testapps/testlauncher_setup/sdl2.py index 9db55dc497..11f1a3785a 100644 --- a/testapps/testlauncher_setup/sdl2.py +++ b/testapps/testlauncher_setup/sdl2.py @@ -5,7 +5,7 @@ 'bootstrap': 'sdl2', 'launcher': None, 'requirements': ( - 'python2,sdl2,android,' + 'python3,sdl2,android,' 'sqlite3,docutils,pygments,kivy,pyjnius,plyer,' 'cymunk,lxml,pil,openssl,pyopenssl,' 'twisted'), # audiostream, ffmpeg, numpy diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index aa84fba446..6ba39864c0 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -76,7 +76,7 @@ def test_attributes(self): bs = Bootstrap().get_bootstrap("sdl2", self.ctx) self.assertEqual(bs.name, "sdl2") self.assertEqual(bs.jni_dir, "sdl2/jni") - self.assertEqual(bs.get_build_dir_name(), "sdl2-python3") + self.assertEqual(bs.get_build_dir_name(), "sdl2") # bs.dist_dir should raise an error if there is no distribution to query bs.distribution = None @@ -100,7 +100,7 @@ def test_build_dist_dirs(self): bs = Bootstrap.get_bootstrap("sdl2", self.ctx) self.assertTrue( - bs.get_build_dir().endswith("build/bootstrap_builds/sdl2-python3") + bs.get_build_dir().endswith("build/bootstrap_builds/sdl2") ) self.assertTrue(bs.get_dist_dir("test_prj").endswith("dists/test_prj")) self.assertTrue( @@ -147,7 +147,7 @@ def test_all_bootstraps(self): returns the expected values, which should be: `empty", `service_only`, `webview` and `sdl2` """ - expected_bootstraps = {"empty", "service_only", "webview", "sdl2"} + expected_bootstraps = {"empty", "service_only", "service_library", "webview", "sdl2"} set_of_bootstraps = Bootstrap.all_bootstraps() self.assertEqual( expected_bootstraps, expected_bootstraps & set_of_bootstraps @@ -181,8 +181,8 @@ def test_expand_dependencies_with_pure_python_package(self): expanded_result = expand_dependencies( ["python3", "kivy", "peewee"], self.ctx ) - # we expect to have two results (one for python2 and one for python3) - self.assertEqual(len(expanded_result), 2) + # we expect to one results for python3 + self.assertEqual(len(expanded_result), 1) self.assertIsInstance(expanded_result, list) for i in expanded_result: self.assertIsInstance(i, list) @@ -244,11 +244,6 @@ def _add_sdl2_conflicting_recipe(name, ctx): ) self.assertEqual(bs.name, "service_only") - # Test wrong recipes - wrong_recipes = {"python2", "python3", "pyjnius"} - bs = Bootstrap.get_bootstrap_from_recipes(wrong_recipes, self.ctx) - self.assertIsNone(bs) - @mock.patch("pythonforandroid.bootstrap.ensure_dir") def test_prepare_dist_dir(self, mock_ensure_dir): """A test which will initialize a bootstrap and will check if the @@ -293,16 +288,10 @@ def test_bootstrap_prepare_build_dir( @mock.patch("pythonforandroid.bootstrap.os.unlink") @mock.patch("pythonforandroid.bootstrap.open", create=True) @mock.patch("pythonforandroid.util.chdir") - @mock.patch("pythonforandroid.bootstrap.sh.ln") @mock.patch("pythonforandroid.bootstrap.listdir") - @mock.patch("pythonforandroid.bootstrap.sh.mkdir") - @mock.patch("pythonforandroid.bootstrap.sh.rm") def test_bootstrap_prepare_build_dir_with_java_src( self, - mock_sh_rm, - mock_sh_mkdir, mock_listdir, - mock_sh_ln, mock_chdir, mock_open, mock_os_unlink, @@ -314,7 +303,7 @@ def test_bootstrap_prepare_build_dir_with_java_src( :meth:`~pythonforandroid.bootstrap.Bootstrap.prepare_build_dir`. In here we will simulate that we have `with_java_src` set to some value. """ - self.ctx.symlink_java_src = ["some_java_src"] + self.ctx.symlink_bootstrap_files = True mock_listdir.return_value = [ "jnius", "kivy", @@ -332,18 +321,7 @@ def test_bootstrap_prepare_build_dir_with_java_src( # make sure that the open command has been called only once mock_open.assert_called_with("project.properties", "w") - # check that the symlink was made 4 times and that - self.assertEqual( - len(mock_sh_ln.call_args_list), len(mock_listdir.return_value) - ) - for i, directory in enumerate(mock_listdir.return_value): - self.assertTrue( - mock_sh_ln.call_args_list[i][0][1].endswith(directory) - ) - # check that the other mocks we made are actually called - mock_sh_rm.assert_called() - mock_sh_mkdir.assert_called() mock_chdir.assert_called() mock_os_unlink.assert_called() mock_os_path_exists.assert_called() @@ -371,9 +349,6 @@ def bootstrap_name(self): @mock.patch("pythonforandroid.bootstraps.webview.open", create=True) @mock.patch("pythonforandroid.bootstraps.sdl2.open", create=True) @mock.patch("pythonforandroid.distribution.open", create=True) - @mock.patch( - "pythonforandroid.python.GuestPythonRecipe.create_python_bundle" - ) @mock.patch("pythonforandroid.bootstrap.Bootstrap.strip_libraries") @mock.patch("pythonforandroid.util.exists") @mock.patch("pythonforandroid.util.chdir") @@ -388,7 +363,6 @@ def test_run_distribute( mock_chdir, mock_ensure_dir, mock_strip_libraries, - mock_create_python_bundle, mock_open_dist_files, mock_open_sdl2_files, mock_open_webview_files, @@ -420,6 +394,7 @@ def test_run_distribute( self.ctx.hostpython = "/some/fake/hostpython3" self.ctx.python_recipe = Recipe.get_recipe("python3", self.ctx) + self.ctx.python_recipe.create_python_bundle = mock.MagicMock() self.ctx.python_modules = ["requests"] self.ctx.archs = [ArchARMv7_a(self.ctx)] @@ -462,7 +437,16 @@ def test_run_distribute( mock_chdir.assert_called() mock_listdir.assert_called() mock_strip_libraries.assert_called() - mock_create_python_bundle.assert_called() + expected__python_bundle = os.path.join( + self.ctx.dist_dir, + f"{self.ctx.bootstrap.distribution.name}__{self.TEST_ARCH}", + "_python_bundle", + "_python_bundle", + ) + self.assertIn( + mock.call(expected__python_bundle, self.ctx.archs[0]), + self.ctx.python_recipe.create_python_bundle.call_args_list, + ) @mock.patch("pythonforandroid.bootstrap.shprint") @mock.patch("pythonforandroid.bootstrap.glob.glob") @@ -492,12 +476,11 @@ def reset_mocks(): libs_dir = os.path.join("libs", arch.arch) # we expect two calls to glob/copy command via shprint self.assertEqual(len(mock_glob.call_args_list), 2) - self.assertEqual(len(mock_shprint.call_args_list), 2) - for i, lib in enumerate(mock_glob.return_value): - self.assertEqual( - mock_shprint.call_args_list[i], - mock.call(sh.cp, "-a", lib, libs_dir), - ) + self.assertEqual(len(mock_shprint.call_args_list), 1) + self.assertEqual( + mock_shprint.call_args_list, + [mock.call(sh.cp, "-a", *mock_glob.return_value, libs_dir)] + ) mock_build_dir.assert_called() mock_bs_dir.assert_called_once_with(libs_dir) reset_mocks() diff --git a/tests/test_build.py b/tests/test_build.py index 6e659a5837..f76a1dabd6 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -18,3 +18,32 @@ def test_run_pymodules_install_optional_project_dir(self): assert run_pymodules_install(ctx, modules, project_dir) is None assert m_info.call_args_list[-1] == mock.call( 'No Python modules and no setup.py to process, skipping') + + def test_strip_if_debuggable(self): + ctx = mock.Mock() + ctx.python_recipe.major_minor_version_string = "python3.6" + ctx.get_site_packages_dir.return_value = "test-doesntexist" + ctx.build_dir = "nonexistant_directory" + ctx.archs = ["arm64"] + + modules = ["mymodule"] + project_dir = None + with mock.patch('pythonforandroid.build.info'), \ + mock.patch('sh.Command'),\ + mock.patch('pythonforandroid.build.open'),\ + mock.patch('pythonforandroid.build.shprint'),\ + mock.patch('pythonforandroid.build.current_directory'),\ + mock.patch('pythonforandroid.build.CythonRecipe') as m_CythonRecipe, \ + mock.patch('pythonforandroid.build.project_has_setup_py') as m_project_has_setup_py, \ + mock.patch('pythonforandroid.build.run_setuppy_install'): + m_project_has_setup_py.return_value = False + + # Make sure it is NOT called when debug build: + ctx.build_as_debuggable = True + assert run_pymodules_install(ctx, modules, project_dir) is None + assert m_CythonRecipe().strip_object_files.called is False + + # Make sure strip object files IS called when release build: + ctx.build_as_debuggable = False + assert run_pymodules_install(ctx, modules, project_dir) is None + assert m_CythonRecipe().strip_object_files.called is True diff --git a/tests/test_entrypoints_python2.py b/tests/test_entrypoints_python2.py deleted file mode 100644 index 0a2f6ebb4f..0000000000 --- a/tests/test_entrypoints_python2.py +++ /dev/null @@ -1,38 +0,0 @@ - -# This test is a special case that we expect to run under Python 2, so -# include the necessary compatibility imports: -try: # Python 3 - from unittest import mock -except ImportError: # Python 2 - import mock - -from pythonforandroid.recommendations import PY2_ERROR_TEXT -from pythonforandroid import entrypoints - - -def test_main_python2(): - """Test that running under Python 2 leads to the build failing, while - running under a suitable version works fine. - - Note that this test must be run *using* Python 2 to truly test - that p4a can reach the Python version check before importing some - Python-3-only syntax and crashing. - """ - - # Under Python 2, we should get a normal control flow exception - # that is handled properly, not any other type of crash - handle_exception_path = 'pythonforandroid.entrypoints.handle_build_exception' - with mock.patch('sys.version_info') as fake_version_info, \ - mock.patch(handle_exception_path) as handle_build_exception: # noqa: E127 - - fake_version_info.major = 2 - fake_version_info.minor = 7 - - def check_python2_exception(exc): - """Check that the exception message is Python 2 specific.""" - assert exc.message == PY2_ERROR_TEXT - handle_build_exception.side_effect = check_python2_exception - - entrypoints.main() - - handle_build_exception.assert_called_once() diff --git a/tests/test_graph.py b/tests/test_graph.py index ebe9fb400d..9cc44fc041 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -13,7 +13,7 @@ ctx = Context() -name_sets = [['python2'], +name_sets = [['python3'], ['kivy']] bootstraps = [None, Bootstrap.get_bootstrap('sdl2', ctx)] @@ -26,7 +26,7 @@ ] ) invalid_combinations = [ - [['python2', 'python3'], None], + [['pil', 'pillow'], None], [['pysdl2', 'genericndkbuild'], None], ] invalid_combinations_simple = list(invalid_combinations) @@ -211,7 +211,7 @@ def test_multichoice_obvious_conflict_checker(monkeypatch): 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)) + assert ('hostpython3' in build_order) def test_graph_deplist_transformation(): @@ -227,8 +227,8 @@ def test_graph_deplist_transformation(): def test_bootstrap_dependency_addition2(): build_order, python_modules, bs = get_recipe_order_and_bootstrap( - ctx, ['kivy', 'python2'], None) - assert 'hostpython2' in build_order + ctx, ['kivy', 'python3'], None) + assert 'hostpython3' in build_order if __name__ == "__main__": diff --git a/tests/test_pythonpackage_basic.py b/tests/test_pythonpackage_basic.py index a39d8a44e3..9e6596d69e 100644 --- a/tests/test_pythonpackage_basic.py +++ b/tests/test_pythonpackage_basic.py @@ -6,7 +6,6 @@ """ import os -import pytest import shutil import sys import subprocess @@ -279,43 +278,6 @@ def test_systemwide_python(self): else: raise - def test_virtualenv(self): - """ Verifies that _get_system_python_executable() works correctly - if called with a python binary as found inside a virtualenv. - """ - - # Get system-wide python bin seen from here first: - pybin = _get_system_python_executable() - # (this call was not a test, we really just need the path here) - - test_dir = tempfile.mkdtemp() - try: - # Check that in a virtualenv, the system-wide python is returned: - subprocess.check_output([ - pybin, "-m", "virtualenv", - "--python=" + str(sys.executable), - "--", - os.path.join(test_dir, "virtualenv") - ]) - subprocess.check_output([ - os.path.join(test_dir, "virtualenv", "bin", "pip"), - "install", "-U", "pip" - ]) - subprocess.check_output([ - os.path.join(test_dir, "virtualenv", "bin", "pip"), - "install", "-U", "pep517<0.7.0" - ]) - sys_python_path = self.run__get_system_python_executable( - os.path.join(test_dir, "virtualenv", "bin", "python") - ) - assert os.path.normpath(sys_python_path).startswith( - os.path.normpath(pybin) - ) - finally: - shutil.rmtree(test_dir) - - @pytest.mark.skipif(int(sys.version.partition(".")[0]) < 3, - reason="venv is python 3 only") def test_venv(self): """ Verifies that _get_system_python_executable() works correctly in a 'venv' (Python 3 only feature). @@ -340,6 +302,10 @@ def test_venv(self): os.path.join(test_dir, "venv", "bin", "pip"), "install", "-U", "pep517<0.7.0" ]) + subprocess.check_output([ + os.path.join(test_dir, "venv", "bin", "pip"), + "install", "-U", "toml" + ]) sys_python_path = self.run__get_system_python_executable( os.path.join(test_dir, "venv", "bin", "python") ) diff --git a/tests/test_recipe.py b/tests/test_recipe.py index 9ff1713c95..18a1849ab6 100644 --- a/tests/test_recipe.py +++ b/tests/test_recipe.py @@ -189,7 +189,7 @@ def setUp(self): Initialize a Context with a Bootstrap and a Distribution to properly test an library recipe, to do so we reuse `BaseClassSetupBootstrap` """ - super(TestLibraryRecipe, self).setUp() + super().setUp() self.ctx.bootstrap = Bootstrap().get_bootstrap('sdl2', self.ctx) self.setUp_distribution_with_bootstrap(self.ctx.bootstrap) @@ -242,7 +242,7 @@ def setUp(self): test a recipe which depends on android's STL library, to do so we reuse `BaseClassSetupBootstrap` """ - super(TesSTLRecipe, self).setUp() + super().setUp() self.ctx.bootstrap = Bootstrap().get_bootstrap('sdl2', self.ctx) self.setUp_distribution_with_bootstrap(self.ctx.bootstrap) self.ctx.python_recipe = Recipe.get_recipe('python3', self.ctx) diff --git a/tests/test_util.py b/tests/test_util.py index 0653991639..ff57dc7a47 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -70,46 +70,6 @@ def test_current_directory_exception(self): ): pass - @mock.patch("pythonforandroid.util.sh.which") - def test_get_virtualenv_executable(self, mock_sh_which): - """ - Test method :meth:`~pythonforandroid.util.get_virtualenv_executable`. - In here we test: - - - that all calls to `sh.which` are performed, so we expect the - first two `sh.which` calls should be None and the last one should - return the expected virtualenv (the python3 one) - - that we don't have virtualenv installed, so all calls to - `sh.which` should return None - """ - expected_venv = os.path.join( - os.path.expanduser("~"), ".local/bin/virtualenv" - ) - mock_sh_which.side_effect = [None, None, expected_venv] - self.assertEqual(util.get_virtualenv_executable(), expected_venv) - mock_sh_which.assert_has_calls( - [ - mock.call("virtualenv2"), - mock.call("virtualenv-2.7"), - mock.call("virtualenv"), - ] - ) - self.assertEqual(mock_sh_which.call_count, 3) - mock_sh_which.reset_mock() - - # Now test that we don't have virtualenv installed, so all calls to - # `sh.which` should return None - mock_sh_which.side_effect = [None, None, None] - self.assertIsNone(util.get_virtualenv_executable()) - self.assertEqual(mock_sh_which.call_count, 3) - mock_sh_which.assert_has_calls( - [ - mock.call("virtualenv2"), - mock.call("virtualenv-2.7"), - mock.call("virtualenv"), - ] - ) - @mock.patch("pythonforandroid.util.walk") def test_walk_valid_filens(self, mock_walk): """ diff --git a/tox.ini b/tox.ini index 6a90ef435d..76da905c69 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,10 @@ [tox] -envlist = pep8,py27,py3 +envlist = pep8,py3 basepython = python3 [testenv] deps = - py27: mock pytest - virtualenv py3: coveralls backports.tempfile # posargs will be replaced by the tox args, so you can override pytest @@ -16,12 +14,6 @@ passenv = TRAVIS TRAVIS_* setenv = PYTHONPATH={toxinidir} -[testenv:py27] -# Note that the set of tests is not posargs-configurable here: we only -# check a minimal set of Python 2 tests for the remaining Python 2 -# functionality that we support -commands = pytest tests/test_androidmodule_ctypes_finder.py tests/test_entrypoints_python2.py - [testenv:py3] # for py3 env we will get code coverage commands = @@ -30,7 +22,7 @@ commands = [testenv:pep8] deps = flake8 -commands = flake8 pythonforandroid/ tests/ ci/ +commands = flake8 pythonforandroid/ tests/ ci/ setup.py [flake8] ignore =