Repository: kivy/python-for-android Branch: develop Commit: a4a8b2814424 Files: 589 Total size: 1.9 MB Directory structure: gitextract_039kc1dw/ ├── .coveragerc ├── .deepsource.toml ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE.md │ └── workflows/ │ ├── custom-build.yml │ ├── docker.yml │ ├── no-response.yml │ ├── push.yml │ ├── pypi-release.yml │ └── support.yml ├── .gitignore ├── .projectile ├── .readthedocs.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTACT.md ├── CONTRIBUTING.md ├── Dockerfile ├── FAQ.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── ci/ │ ├── __init__.py │ ├── constants.py │ ├── makefiles/ │ │ ├── android.mk │ │ └── osx.mk │ ├── rebuild_updated_recipes.py │ └── run_emulator_tests.sh ├── distribute.sh ├── doc/ │ ├── Makefile │ ├── make.bat │ ├── requirements.txt │ └── source/ │ ├── _static/ │ │ └── .empty │ ├── apis.rst │ ├── bootstraps.rst │ ├── buildoptions.rst │ ├── commands.rst │ ├── conf.py │ ├── contact.rst │ ├── contribute.rst │ ├── distutils.rst │ ├── docker.rst │ ├── faq.rst │ ├── index.rst │ ├── quickstart.rst │ ├── recipes.rst │ ├── services.rst │ ├── testing_pull_requests.rst │ └── troubleshooting.rst ├── pythonforandroid/ │ ├── __init__.py │ ├── androidndk.py │ ├── archs.py │ ├── bdistapk.py │ ├── bootstrap.py │ ├── bootstraps/ │ │ ├── __init__.py │ │ ├── _sdl_common/ │ │ │ ├── __init__.py │ │ │ └── build/ │ │ │ ├── .gitignore │ │ │ ├── blacklist.txt │ │ │ ├── jni/ │ │ │ │ └── Application.mk │ │ │ ├── src/ │ │ │ │ └── main/ │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── java/ │ │ │ │ │ ├── .gitkeep │ │ │ │ │ └── org/ │ │ │ │ │ └── kivy/ │ │ │ │ │ └── android/ │ │ │ │ │ └── launcher/ │ │ │ │ │ ├── Project.java │ │ │ │ │ ├── ProjectAdapter.java │ │ │ │ │ └── ProjectChooser.java │ │ │ │ ├── jniLibs/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── libs/ │ │ │ │ │ └── .gitkeep │ │ │ │ └── res/ │ │ │ │ ├── drawable/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── layout/ │ │ │ │ │ ├── chooser_item.xml │ │ │ │ │ ├── main.xml │ │ │ │ │ ├── project_chooser.xml │ │ │ │ │ └── project_empty.xml │ │ │ │ ├── mipmap/ │ │ │ │ │ └── .gitkeep │ │ │ │ └── mipmap-anydpi-v26/ │ │ │ │ └── .gitkeep │ │ │ └── templates/ │ │ │ ├── AndroidManifest.tmpl.xml │ │ │ └── strings.tmpl.xml │ │ ├── build.gradle │ │ ├── common/ │ │ │ └── build/ │ │ │ ├── ant.properties │ │ │ ├── build.py │ │ │ ├── gradle/ │ │ │ │ └── wrapper/ │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ │ ├── gradlew │ │ │ ├── gradlew.bat │ │ │ ├── jni/ │ │ │ │ ├── Android.mk │ │ │ │ └── application/ │ │ │ │ ├── Android.mk │ │ │ │ └── src/ │ │ │ │ ├── Android.mk │ │ │ │ └── start.c │ │ │ ├── src/ │ │ │ │ └── main/ │ │ │ │ └── java/ │ │ │ │ └── org/ │ │ │ │ ├── kamranzafar/ │ │ │ │ │ └── jtar/ │ │ │ │ │ ├── Octal.java │ │ │ │ │ ├── TarConstants.java │ │ │ │ │ ├── TarEntry.java │ │ │ │ │ ├── TarHeader.java │ │ │ │ │ ├── TarInputStream.java │ │ │ │ │ ├── TarOutputStream.java │ │ │ │ │ └── TarUtils.java │ │ │ │ ├── kivy/ │ │ │ │ │ └── android/ │ │ │ │ │ ├── GenericBroadcastReceiver.java │ │ │ │ │ ├── GenericBroadcastReceiverCallback.java │ │ │ │ │ ├── PythonService.java │ │ │ │ │ └── PythonUtil.java │ │ │ │ └── renpy/ │ │ │ │ └── android/ │ │ │ │ ├── AssetExtract.java │ │ │ │ ├── Hardware.java │ │ │ │ └── ResourceManager.java │ │ │ ├── templates/ │ │ │ │ ├── Service.tmpl.java │ │ │ │ ├── build.properties │ │ │ │ ├── build.tmpl.gradle │ │ │ │ ├── build.tmpl.xml │ │ │ │ ├── custom_rules.tmpl.xml │ │ │ │ ├── gradle.tmpl.properties │ │ │ │ └── lottie.xml │ │ │ └── whitelist.txt │ │ ├── empty/ │ │ │ ├── __init__.py │ │ │ └── build/ │ │ │ └── .gitkeep │ │ ├── gradle.properties │ │ ├── qt/ │ │ │ ├── __init__.py │ │ │ └── build/ │ │ │ ├── .gitignore │ │ │ ├── blacklist.txt │ │ │ ├── jni/ │ │ │ │ ├── Application.mk │ │ │ │ └── application/ │ │ │ │ └── src/ │ │ │ │ ├── Android.mk │ │ │ │ ├── Android_static.mk │ │ │ │ └── bootstrap_name.h │ │ │ ├── src/ │ │ │ │ └── main/ │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── java/ │ │ │ │ │ ├── .gitkeep │ │ │ │ │ └── org/ │ │ │ │ │ └── kivy/ │ │ │ │ │ └── android/ │ │ │ │ │ └── PythonActivity.java │ │ │ │ ├── jniLibs/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── libs/ │ │ │ │ │ └── .gitkeep │ │ │ │ └── res/ │ │ │ │ ├── drawable/ │ │ │ │ │ └── .gitkeep │ │ │ │ └── mipmap/ │ │ │ │ └── .gitkeep │ │ │ └── templates/ │ │ │ ├── AndroidManifest.tmpl.xml │ │ │ ├── libs.tmpl.xml │ │ │ └── strings.tmpl.xml │ │ ├── sdl2/ │ │ │ ├── __init__.py │ │ │ └── build/ │ │ │ ├── jni/ │ │ │ │ └── application/ │ │ │ │ └── src/ │ │ │ │ ├── Android.mk │ │ │ │ ├── Android_static.mk │ │ │ │ └── bootstrap_name.h │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ └── java/ │ │ │ │ ├── .gitkeep │ │ │ │ └── org/ │ │ │ │ └── kivy/ │ │ │ │ └── android/ │ │ │ │ └── PythonActivity.java │ │ │ └── patches/ │ │ │ ├── SDLActivity.java.patch │ │ │ └── SDLSurface.java.patch │ │ ├── sdl3/ │ │ │ ├── __init__.py │ │ │ └── build/ │ │ │ ├── jni/ │ │ │ │ └── application/ │ │ │ │ └── src/ │ │ │ │ ├── Android.mk │ │ │ │ ├── Android_static.mk │ │ │ │ └── bootstrap_name.h │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ └── java/ │ │ │ │ └── org/ │ │ │ │ └── kivy/ │ │ │ │ └── android/ │ │ │ │ └── PythonActivity.java │ │ │ └── patches/ │ │ │ ├── SDLActivity.java.patch │ │ │ └── SDLSurface.java.patch │ │ ├── service_library/ │ │ │ ├── __init__.py │ │ │ └── build/ │ │ │ ├── jni/ │ │ │ │ ├── Application.mk │ │ │ │ └── application/ │ │ │ │ └── src/ │ │ │ │ └── bootstrap_name.h │ │ │ ├── src/ │ │ │ │ └── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── org/ │ │ │ │ │ └── kivy/ │ │ │ │ │ └── android/ │ │ │ │ │ └── PythonActivity.java │ │ │ │ └── res/ │ │ │ │ └── mipmap/ │ │ │ │ └── .gitkeep │ │ │ └── templates/ │ │ │ ├── AndroidManifest.tmpl.xml │ │ │ └── Service.tmpl.java │ │ ├── service_only/ │ │ │ ├── __init__.py │ │ │ └── build/ │ │ │ ├── blacklist.txt │ │ │ ├── jni/ │ │ │ │ ├── Android.mk │ │ │ │ ├── Application.mk │ │ │ │ └── application/ │ │ │ │ └── src/ │ │ │ │ ├── Android.mk │ │ │ │ ├── Android_static.mk │ │ │ │ ├── bootstrap_name.h │ │ │ │ └── pyjniusjni.c │ │ │ ├── src/ │ │ │ │ └── main/ │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── java/ │ │ │ │ │ └── org/ │ │ │ │ │ └── kivy/ │ │ │ │ │ └── android/ │ │ │ │ │ └── PythonActivity.java │ │ │ │ ├── jniLibs/ │ │ │ │ │ └── .gitkeep │ │ │ │ └── res/ │ │ │ │ ├── drawable/ │ │ │ │ │ └── .gitkeep │ │ │ │ └── mipmap/ │ │ │ │ └── .gitkeep │ │ │ └── templates/ │ │ │ ├── AndroidManifest.tmpl.xml │ │ │ ├── Service.tmpl.java │ │ │ └── strings.tmpl.xml │ │ ├── settings.gradle │ │ └── webview/ │ │ ├── __init__.py │ │ └── build/ │ │ ├── blacklist.txt │ │ ├── jni/ │ │ │ ├── Android.mk │ │ │ ├── Application.mk │ │ │ └── application/ │ │ │ └── src/ │ │ │ ├── Android.mk │ │ │ ├── Android_static.mk │ │ │ ├── bootstrap_name.h │ │ │ └── pyjniusjni.c │ │ ├── proguard-project.txt │ │ ├── src/ │ │ │ └── main/ │ │ │ ├── assets/ │ │ │ │ └── .gitkeep │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── kivy/ │ │ │ │ └── android/ │ │ │ │ └── PythonActivity.java │ │ │ ├── jniLibs/ │ │ │ │ └── .gitkeep │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── .gitkeep │ │ │ ├── layout/ │ │ │ │ └── main.xml │ │ │ ├── mipmap/ │ │ │ │ └── .gitkeep │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ └── .gitkeep │ │ │ └── values/ │ │ │ └── strings.xml │ │ ├── templates/ │ │ │ ├── AndroidManifest.tmpl.xml │ │ │ ├── WebViewLoader.tmpl.java │ │ │ ├── strings.tmpl.xml │ │ │ └── test/ │ │ │ ├── build.tmpl.xml │ │ │ └── build.xml.tmpl │ │ └── webview_includes/ │ │ ├── _load.html │ │ └── _loading_style.css │ ├── build.py │ ├── checkdependencies.py │ ├── distribution.py │ ├── entrypoints.py │ ├── graph.py │ ├── includes/ │ │ └── arm64-v8a/ │ │ └── machine/ │ │ └── cpu-features.h │ ├── logger.py │ ├── patching.py │ ├── prerequisites.py │ ├── pythonpackage.py │ ├── recipe.py │ ├── recipes/ │ │ ├── Pillow/ │ │ │ ├── __init__.py │ │ │ └── setup.py.patch │ │ ├── __init__.py │ │ ├── aiohttp/ │ │ │ └── __init__.py │ │ ├── android/ │ │ │ ├── __init__.py │ │ │ └── src/ │ │ │ ├── android/ │ │ │ │ ├── __init__.py │ │ │ │ ├── _android.pyx │ │ │ │ ├── _android_billing.pyx │ │ │ │ ├── _android_billing_jni.c │ │ │ │ ├── _android_jni.c │ │ │ │ ├── _android_sound.pyx │ │ │ │ ├── _android_sound_jni.c │ │ │ │ ├── _ctypes_library_finder.py │ │ │ │ ├── activity.py │ │ │ │ ├── billing.py │ │ │ │ ├── broadcast.py │ │ │ │ ├── display_cutout.py │ │ │ │ ├── loadingscreen.py │ │ │ │ ├── mixer.py │ │ │ │ ├── permissions.py │ │ │ │ ├── runnable.py │ │ │ │ ├── storage.py │ │ │ │ └── touch.py │ │ │ └── setup.py │ │ ├── apsw/ │ │ │ └── __init__.py │ │ ├── argon2-cffi/ │ │ │ └── __init__.py │ │ ├── atom/ │ │ │ ├── __init__.py │ │ │ └── pyproject.toml.patch │ │ ├── aubio/ │ │ │ └── __init__.py │ │ ├── audiostream/ │ │ │ └── __init__.py │ │ ├── av/ │ │ │ ├── __init__.py │ │ │ └── patches/ │ │ │ └── compilation_syntax_errors.patch │ │ ├── av_codecs/ │ │ │ └── __init__.py │ │ ├── bcrypt/ │ │ │ └── __init__.py │ │ ├── bitarray/ │ │ │ └── __init__.py │ │ ├── boost/ │ │ │ ├── __init__.py │ │ │ ├── disable-so-version.patch │ │ │ ├── fix-android-issues.patch │ │ │ ├── use-android-libs.patch │ │ │ └── user-config.jam │ │ ├── brokenrecipe/ │ │ │ └── __init__.py │ │ ├── cffi/ │ │ │ ├── __init__.py │ │ │ └── disable-pkg-config.patch │ │ ├── coincurve/ │ │ │ ├── __init__.py │ │ │ └── coincurve.patch │ │ ├── coverage/ │ │ │ ├── __init__.py │ │ │ └── fallback-utf8.patch │ │ ├── cryptography/ │ │ │ └── __init__.py │ │ ├── cymunk/ │ │ │ └── __init__.py │ │ ├── cython/ │ │ │ └── __init__.py │ │ ├── decorator/ │ │ │ └── __init__.py │ │ ├── enaml/ │ │ │ ├── 0001-Update-setup.py.patch │ │ │ └── __init__.py │ │ ├── ethash/ │ │ │ └── __init__.py │ │ ├── evdev/ │ │ │ ├── __init__.py │ │ │ ├── evcnt.patch │ │ │ ├── evdev-permissions.patch │ │ │ ├── include-dir.patch │ │ │ ├── keycnt.patch │ │ │ └── remove-uinput.patch │ │ ├── feedparser/ │ │ │ └── __init__.py │ │ ├── ffmpeg/ │ │ │ ├── __init__.py │ │ │ └── patches/ │ │ │ ├── backport-Android15-MediaCodec-fix.patch │ │ │ └── configure.patch │ │ ├── ffpyplayer/ │ │ │ ├── __init__.py │ │ │ └── setup.py.patch │ │ ├── ffpyplayer_codecs/ │ │ │ └── __init__.py │ │ ├── flask/ │ │ │ └── __init__.py │ │ ├── fontconfig/ │ │ │ └── __init__.py │ │ ├── fortran/ │ │ │ └── __init__.py │ │ ├── freetype/ │ │ │ └── __init__.py │ │ ├── freetype-py/ │ │ │ ├── __init__.py │ │ │ └── fix_import.patch │ │ ├── genericndkbuild/ │ │ │ └── __init__.py │ │ ├── gevent/ │ │ │ ├── __init__.py │ │ │ └── cross_compiling.patch │ │ ├── greenlet/ │ │ │ └── __init__.py │ │ ├── groestlcoin_hash/ │ │ │ └── __init__.py │ │ ├── grpcio/ │ │ │ ├── __init__.py │ │ │ ├── comment-getserverbyport-r-args.patch │ │ │ ├── remove-android-log-write.patch │ │ │ └── use-ndk-zlib-and-openssl-recipe-include.patch │ │ ├── harfbuzz/ │ │ │ └── __init__.py │ │ ├── hostpython3/ │ │ │ ├── __init__.py │ │ │ └── fix_ensurepip.patch │ │ ├── httpx/ │ │ │ └── __init__.py │ │ ├── icu/ │ │ │ ├── __init__.py │ │ │ └── disable-libs-version.patch │ │ ├── ifaddr/ │ │ │ ├── __init__.py │ │ │ └── getifaddrs.patch │ │ ├── ifaddrs/ │ │ │ └── __init__.py │ │ ├── jedi/ │ │ │ ├── __init__.py │ │ │ └── fix_MergedNamesDict_get.patch │ │ ├── jpeg/ │ │ │ ├── Application.mk │ │ │ ├── __init__.py │ │ │ ├── build-static.patch │ │ │ └── remove-version.patch │ │ ├── kivy/ │ │ │ ├── __init__.py │ │ │ ├── no-ast-str.patch │ │ │ ├── sdl-gl-swapwindow-nogil.patch │ │ │ └── use_cython.patch │ │ ├── kivy3/ │ │ │ └── __init__.py │ │ ├── kiwisolver/ │ │ │ └── __init__.py │ │ ├── leveldb/ │ │ │ └── __init__.py │ │ ├── libbz2/ │ │ │ ├── __init__.py │ │ │ └── lib_android.patch │ │ ├── libcairo/ │ │ │ ├── __init__.py │ │ │ └── meson.patch │ │ ├── libcurl/ │ │ │ └── __init__.py │ │ ├── libexpat/ │ │ │ └── __init__.py │ │ ├── libffi/ │ │ │ ├── Application.mk │ │ │ ├── __init__.py │ │ │ ├── disable-mips-check.patch │ │ │ └── remove-version-info.patch │ │ ├── libgeos/ │ │ │ └── __init__.py │ │ ├── libglob/ │ │ │ ├── __init__.py │ │ │ └── glob.patch │ │ ├── libiconv/ │ │ │ └── __init__.py │ │ ├── liblzma/ │ │ │ └── __init__.py │ │ ├── libogg/ │ │ │ └── __init__.py │ │ ├── libopenblas/ │ │ │ └── __init__.py │ │ ├── libpcre/ │ │ │ └── __init__.py │ │ ├── libpq/ │ │ │ └── __init__.py │ │ ├── libpthread/ │ │ │ └── __init__.py │ │ ├── librt/ │ │ │ └── __init__.py │ │ ├── libsecp256k1/ │ │ │ └── __init__.py │ │ ├── libshine/ │ │ │ └── __init__.py │ │ ├── libsodium/ │ │ │ ├── __init__.py │ │ │ └── size_max_fix.patch │ │ ├── libtorrent/ │ │ │ ├── __init__.py │ │ │ ├── disable-so-version.patch │ │ │ ├── setup-lib-name.patch │ │ │ ├── use-soname-python.patch │ │ │ └── user-config-openssl.patch │ │ ├── libtribler/ │ │ │ └── __init__.py │ │ ├── libvorbis/ │ │ │ └── __init__.py │ │ ├── libvpx/ │ │ │ ├── __init__.py │ │ │ └── patches/ │ │ │ └── 0001-android-force-neon-runtime.patch │ │ ├── libwebp/ │ │ │ └── __init__.py │ │ ├── libx264/ │ │ │ └── __init__.py │ │ ├── libxml2/ │ │ │ ├── __init__.py │ │ │ ├── add-glob.c.patch │ │ │ ├── glob.c │ │ │ └── glob.h │ │ ├── libxslt/ │ │ │ ├── __init__.py │ │ │ └── fix-dlopen.patch │ │ ├── libzbar/ │ │ │ ├── __init__.py │ │ │ └── werror.patch │ │ ├── libzmq/ │ │ │ └── __init__.py │ │ ├── lxml/ │ │ │ └── __init__.py │ │ ├── m2crypto/ │ │ │ └── __init__.py │ │ ├── materialyoucolor/ │ │ │ └── __init__.py │ │ ├── matplotlib/ │ │ │ ├── __init__.py │ │ │ └── meson.patch │ │ ├── moderngl/ │ │ │ └── __init__.py │ │ ├── msgpack-python/ │ │ │ └── __init__.py │ │ ├── ndghttpsclient │ │ ├── netifaces/ │ │ │ ├── __init__.py │ │ │ └── fix-build.patch │ │ ├── numpy/ │ │ │ └── __init__.py │ │ ├── omemo/ │ │ │ └── __init__.py │ │ ├── omemo-backend-signal/ │ │ │ └── __init__.py │ │ ├── openal/ │ │ │ └── __init__.py │ │ ├── opencv/ │ │ │ ├── __init__.py │ │ │ └── patches/ │ │ │ └── p4a_build.patch │ │ ├── opencv_extras/ │ │ │ └── __init__.py │ │ ├── openssl/ │ │ │ └── __init__.py │ │ ├── pandas/ │ │ │ ├── __init__.py │ │ │ └── fix_numpy_includes.patch │ │ ├── pil/ │ │ │ └── __init__.py │ │ ├── png/ │ │ │ ├── __init__.py │ │ │ └── build_shared_library.patch │ │ ├── preppy/ │ │ │ ├── __init__.py │ │ │ └── fix-setup.patch │ │ ├── primp/ │ │ │ └── __init__.py │ │ ├── protobuf_cpp/ │ │ │ ├── __init__.py │ │ │ └── fix-python3-compatibility.patch │ │ ├── psycopg2/ │ │ │ └── __init__.py │ │ ├── py3dns/ │ │ │ ├── __init__.py │ │ │ └── patches/ │ │ │ └── android.patch │ │ ├── pyaml/ │ │ │ └── __init__.py │ │ ├── pybind11/ │ │ │ └── __init__.py │ │ ├── pycairo/ │ │ │ ├── __init__.py │ │ │ └── meson.patch │ │ ├── pycparser/ │ │ │ └── __init__.py │ │ ├── pycrypto/ │ │ │ ├── __init__.py │ │ │ └── add_length.patch │ │ ├── pycryptodome/ │ │ │ └── __init__.py │ │ ├── pydantic-core/ │ │ │ └── __init__.py │ │ ├── pygame/ │ │ │ └── __init__.py │ │ ├── pyicu/ │ │ │ ├── __init__.py │ │ │ └── locale.patch │ │ ├── pyjnius/ │ │ │ ├── __init__.py │ │ │ ├── genericndkbuild_jnienv_getter.patch │ │ │ ├── sdl3_jnienv_getter.patch │ │ │ └── use_cython.patch │ │ ├── pyleveldb/ │ │ │ ├── __init__.py │ │ │ └── bindings-only.patch │ │ ├── pymunk/ │ │ │ └── __init__.py │ │ ├── pynacl/ │ │ │ └── __init__.py │ │ ├── pyogg/ │ │ │ ├── __init__.py │ │ │ └── patches/ │ │ │ └── fix-find-lib.patch │ │ ├── pyopenal/ │ │ │ ├── __init__.py │ │ │ └── patches/ │ │ │ └── fix-find-lib.patch │ │ ├── pyopenssl/ │ │ │ ├── __init__.py │ │ │ └── fix-dlfcn.patch │ │ ├── pyproj/ │ │ │ └── __init__.py │ │ ├── pyrxp/ │ │ │ └── __init__.py │ │ ├── pysdl2/ │ │ │ └── __init__.py │ │ ├── pysha3/ │ │ │ └── __init__.py │ │ ├── python3/ │ │ │ ├── __init__.py │ │ │ └── patches/ │ │ │ ├── 3.14_armv7l_fix.patch │ │ │ ├── 3.14_fix_remote_debug.patch │ │ │ ├── cpython-311-ctypes-find-library.patch │ │ │ ├── py3.7.1_fix-ctypes-util-find-library.patch │ │ │ ├── py3.7.1_fix-zlib-version.patch │ │ │ ├── py3.7.1_fix_cortex_a8.patch │ │ │ ├── py3.8.1.patch │ │ │ ├── py3.8.1_fix_cortex_a8.patch │ │ │ ├── pyconfig_detection.patch │ │ │ └── reproducible-buildinfo.diff │ │ ├── pyusb/ │ │ │ ├── __init__.py │ │ │ └── fix-android.patch │ │ ├── pyzbar/ │ │ │ └── __init__.py │ │ ├── pyzmq/ │ │ │ └── __init__.py │ │ ├── regex/ │ │ │ └── __init__.py │ │ ├── reportlab/ │ │ │ ├── __init__.py │ │ │ └── patches/ │ │ │ └── fix-setup.patch │ │ ├── ruamel.yaml/ │ │ │ ├── __init__.py │ │ │ └── disable-pip-req.patch │ │ ├── scipy/ │ │ │ ├── __init__.py │ │ │ ├── meson.patch │ │ │ └── wrapper.py │ │ ├── scrypt/ │ │ │ ├── __init__.py │ │ │ └── remove_librt.patch │ │ ├── sdl2/ │ │ │ └── __init__.py │ │ ├── sdl2_image/ │ │ │ ├── __init__.py │ │ │ └── enable-webp.patch │ │ ├── sdl2_mixer/ │ │ │ └── __init__.py │ │ ├── sdl2_ttf/ │ │ │ └── __init__.py │ │ ├── sdl3/ │ │ │ └── __init__.py │ │ ├── sdl3_image/ │ │ │ ├── __init__.py │ │ │ └── enable-webp.patch │ │ ├── sdl3_mixer/ │ │ │ ├── __init__.py │ │ │ └── disable-libgme.patch │ │ ├── sdl3_ttf/ │ │ │ └── __init__.py │ │ ├── secp256k1/ │ │ │ ├── __init__.py │ │ │ ├── cross_compile.patch │ │ │ ├── drop_setup_requires.patch │ │ │ ├── find_lib.patch │ │ │ ├── no-download.patch │ │ │ └── pkg-config.patch │ │ ├── setuptools/ │ │ │ └── __init__.py │ │ ├── shapely/ │ │ │ ├── __init__.py │ │ │ └── setup.patch │ │ ├── snappy/ │ │ │ └── __init__.py │ │ ├── spine/ │ │ │ └── __init__.py │ │ ├── sqlalchemy/ │ │ │ └── __init__.py │ │ ├── sqlite3/ │ │ │ └── __init__.py │ │ ├── storm/ │ │ │ └── __init__.py │ │ ├── tflite-runtime/ │ │ │ ├── CMakeLists.patch │ │ │ ├── __init__.py │ │ │ └── build_with_cmake.patch │ │ ├── tiktoken/ │ │ │ └── __init__.py │ │ ├── twisted/ │ │ │ ├── __init__.py │ │ │ ├── incremental.patch │ │ │ └── remove_tests.patch │ │ ├── ujson/ │ │ │ └── __init__.py │ │ ├── uvloop/ │ │ │ └── __init__.py │ │ ├── vispy/ │ │ │ ├── __init__.py │ │ │ ├── disable_font_triage.patch │ │ │ ├── disable_freetype.patch │ │ │ ├── disable_freetype.patch_backup │ │ │ ├── remove_ati_check.patch │ │ │ └── use_es2.patch │ │ ├── vlc/ │ │ │ └── __init__.py │ │ ├── wsaccel/ │ │ │ └── __init__.py │ │ ├── x3dh/ │ │ │ ├── __init__.py │ │ │ └── requires_fix.patch │ │ ├── xeddsa/ │ │ │ ├── __init__.py │ │ │ └── remove_dependencies.patch │ │ ├── zbar/ │ │ │ ├── __init__.py │ │ │ └── zbar-0.10-python-crash.patch │ │ ├── zbarlight/ │ │ │ └── __init__.py │ │ ├── zeroconf/ │ │ │ ├── __init__.py │ │ │ └── patches/ │ │ │ └── setup.patch │ │ ├── zope/ │ │ │ └── __init__.py │ │ └── zope_interface/ │ │ ├── __init__.py │ │ ├── fix-init.patch │ │ └── no_tests.patch │ ├── recommendations.py │ ├── toolchain.py │ ├── tools/ │ │ ├── biglink │ │ ├── liblink │ │ └── liblink.sh │ └── util.py ├── setup.py ├── testapps/ │ ├── on_device_unit_tests/ │ │ ├── README.rst │ │ ├── buildozer.spec │ │ ├── setup.py │ │ ├── test_app/ │ │ │ ├── app_flask.py │ │ │ ├── app_kivy.py │ │ │ ├── app_service.py │ │ │ ├── constants.py │ │ │ ├── main.py │ │ │ ├── screen_keyboard.kv │ │ │ ├── screen_orientation.kv │ │ │ ├── screen_service.kv │ │ │ ├── screen_unittests.kv │ │ │ ├── static/ │ │ │ │ ├── Blanka-Regular.otf │ │ │ │ └── flask.css │ │ │ ├── templates/ │ │ │ │ ├── base.html │ │ │ │ ├── index.html │ │ │ │ ├── page2.html │ │ │ │ └── unittests.html │ │ │ ├── tests/ │ │ │ │ ├── __init__.py │ │ │ │ ├── mixin.py │ │ │ │ └── test_requirements.py │ │ │ ├── tools.py │ │ │ ├── widgets.kv │ │ │ └── widgets.py │ │ └── test_qt/ │ │ ├── jar/ │ │ │ └── PySide6/ │ │ │ └── jar/ │ │ │ ├── Qt6Android.jar │ │ │ └── Qt6AndroidBindings.jar │ │ └── recipes/ │ │ ├── PySide6/ │ │ │ └── __init__.py │ │ └── shiboken6/ │ │ └── __init__.py │ ├── setup_testapp_python3_sqlite_openssl.py │ ├── setup_vispy.py │ ├── testapp_sqlite_openssl/ │ │ └── main.py │ ├── testapp_vispy/ │ │ └── main.py │ ├── testlauncher_setup/ │ │ └── sdl2.py │ └── testlauncherreboot_setup/ │ └── sdl2.py ├── tests/ │ ├── recipes/ │ │ ├── recipe_ctx.py │ │ ├── recipe_lib_test.py │ │ ├── test_freetype.py │ │ ├── test_gevent.py │ │ ├── test_harfbuzz.py │ │ ├── test_hostpython3.py │ │ ├── test_icu.py │ │ ├── test_jpeg.py │ │ ├── test_leveldb.py │ │ ├── test_libbz2.py │ │ ├── test_libcurl.py │ │ ├── test_libexpat.py │ │ ├── test_libffi.py │ │ ├── test_libgeos.py │ │ ├── test_libiconv.py │ │ ├── test_liblzma.py │ │ ├── test_libogg.py │ │ ├── test_libpq.py │ │ ├── test_libsecp256k1.py │ │ ├── test_libshine.py │ │ ├── test_libvorbis.py │ │ ├── test_libvpx.py │ │ ├── test_libx264.py │ │ ├── test_libxml2.py │ │ ├── test_libxslt.py │ │ ├── test_openal.py │ │ ├── test_openssl.py │ │ ├── test_pandas.py │ │ ├── test_png.py │ │ ├── test_pyicu.py │ │ ├── test_python3.py │ │ ├── test_reportlab.py │ │ ├── test_sdl2_mixer.py │ │ └── test_snappy.py │ ├── test_androidmodule_ctypes_finder.py │ ├── test_androidndk.py │ ├── test_archs.py │ ├── test_bdistapk.py │ ├── test_bootstrap.py │ ├── test_bootstrap_build.py │ ├── test_build.py │ ├── test_checkdependencies.py │ ├── test_distribution.py │ ├── test_entrypoints.py │ ├── test_graph.py │ ├── test_logger.py │ ├── test_patching.py │ ├── test_prerequisites.py │ ├── test_pythonpackage.py │ ├── test_pythonpackage_basic.py │ ├── test_recipe.py │ ├── test_recommendations.py │ ├── test_toolchain.py │ └── test_util.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveragerc ================================================ [run] relative_files = True omit = *test* [report] exclude_lines = pragma: no cover def __repr__ raise NotImplementedError if __name__ == .__main__.: ================================================ FILE: .deepsource.toml ================================================ version = 1 test_patterns = ["tests/**"] exclude_patterns = ["testapps/**"] [[analyzers]] name = "python" enabled = true [analyzers.meta] runtime_version = "3.x.x" ================================================ FILE: .dockerignore ================================================ venv/ .buildozer/ **/.pytest_cache/ .tox/ bin/ *.pyc **/__pycache__ *.egg-info/ docker-data/ ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ### Checklist - [ ] the issue is indeed a bug and not a support request - [ ] issue doesn't already exist: https://github.com/kivy/python-for-android/issues - [ ] I have a short, runnable example that reproduces the issue - [ ] I reproduced the problem with the latest development version (`p4a.branch = develop`) - [ ] I used the grave accent (aka backticks) to format code or logs when appropriated ### Versions - Python: - OS: - Kivy: - Cython: - OpenJDK: ### Description // REPLACE ME: What are you trying to get done, what has happened, what went wrong, and what did you expect? ### buildozer.spec Command: ```sh // REPLACE ME: buildozer command ran? e.g. buildozer android debug // Keep the triple grave accent (aka backquote/backtick) to have the code formatted ``` Spec file: ``` // REPLACE ME: Paste your buildozer.spec file here ``` ### Logs ``` // REPLACE ME: Paste the build output containing the error // Keep the triple grave accent (a.k.a. backquote/backtick) to have the code formatted ``` ================================================ FILE: .github/workflows/custom-build.yml ================================================ name: Custom build on: workflow_dispatch: inputs: arch: description: "Comma separated architectures (e.g., armeabi-v7a, arm64-v8a, x86_64, x86)" required: true default: "armeabi-v7a,arm64-v8a,x86_64,x86" artifact: description: "Artifact type" required: true default: "apk" type: choice options: - "aab" - "aar" - "apk" bootstrap: description: "Bootstrap to use" required: true default: "sdl2" type: choice options: - "qt" - "sdl2" - "service_library" - "service_only" - "webview" mode: description: "Build mode" required: true default: "debug" type: choice options: - "debug" - "release" os: description: "Operating system to run on" required: true default: "ubuntu-latest" type: choice options: - "ubuntu-latest" - "macos-latest" requirements: description: "Comma separated requirements" required: true default: "python3,kivy" env: APK_ARTIFACT_FILENAME: bdist_unit_tests_app-debug-1.1.apk AAB_ARTIFACT_FILENAME: bdist_unit_tests_app-release-1.1.aab AAR_ARTIFACT_FILENAME: bdist_unit_tests_app-release-1.1.aar PYTHONFORANDROID_PREREQUISITES_INSTALL_INTERACTIVE: 0 jobs: build: name: Build test APP [ ${{ github.event.inputs.arch }} | ${{ github.event.inputs.artifact }} | ${{ github.event.inputs.bootstrap }} | ${{ github.event.inputs.mode }} | ${{ github.event.inputs.os }} | ${{ github.event.inputs.requirements }}] runs-on: ${{ github.event.inputs.os }} steps: - name: Checkout python-for-android uses: actions/checkout@v4 - name: Pull the python-for-android docker image run: make docker/pull - name: Build python-for-android docker image run: make docker/build - name: Build multi-arch artifact with docker run: | docker run --name p4a-latest kivy/python-for-android make ARCH=${{ github.event.inputs.arch }} ARTIFACT=${{ github.event.inputs.artifact }} BOOTSTRAP=${{ github.event.inputs.bootstrap }} MODE=${{ github.event.inputs.mode }} REQUIREMENTS=${{ github.event.inputs.requirements }} testapps-generic - name: Copy produced artifacts from docker container (*.apk, *.aab) if: github.event.inputs.bootstrap != 'service_library' run: | mkdir -p dist docker cp p4a-latest:/home/user/app/testapps/on_device_unit_tests/${{ env.APK_ARTIFACT_FILENAME }} dist/ || true docker cp p4a-latest:/home/user/app/testapps/on_device_unit_tests/${{ env.AAB_ARTIFACT_FILENAME }} dist/ || true - name: Copy produced artifacts from docker container (*.aar) if: github.event.inputs.bootstrap == 'service_library' run: | mkdir -p dist docker cp p4a-latest:/home/user/app/testapps/on_device_unit_tests/${{ env.AAR_ARTIFACT_FILENAME }} dist/ - name: Rename artifacts to include the build platform name (*.apk, *.aab, *.aar) run: | if [ -f dist/${{ env.APK_ARTIFACT_FILENAME }} ]; then mv dist/${{ env.APK_ARTIFACT_FILENAME }} dist/${{ github.event.inputs.os }}-${{ github.event.inputs.bootstrap }}-${{ env.APK_ARTIFACT_FILENAME }}; fi if [ -f dist/${{ env.AAB_ARTIFACT_FILENAME }} ]; then mv dist/${{ env.AAB_ARTIFACT_FILENAME }} dist/${{ github.event.inputs.os }}-${{ github.event.inputs.bootstrap }}-${{ env.AAB_ARTIFACT_FILENAME }}; fi if [ -f dist/${{ env.AAR_ARTIFACT_FILENAME }} ]; then mv dist/${{ env.AAR_ARTIFACT_FILENAME }} dist/${{ github.event.inputs.os }}-${{ github.event.inputs.bootstrap }}-${{ env.AAR_ARTIFACT_FILENAME }}; fi - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: ${{ github.event.inputs.os }}-${{ github.event.inputs.bootstrap }}-artifacts path: dist # Cleanup the container after all steps are done - name: Cleanup Docker container run: docker rm p4a-latest if: always() ================================================ FILE: .github/workflows/docker.yml ================================================ name: Docker on: workflow_dispatch: push: branches: - develop tags: - "*" pull_request: jobs: docker: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - run: make docker/build - name: docker login if: github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/') env: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} run: make docker/login - name: docker push if: github.ref == 'refs/heads/develop' run: make docker/push - name: docker push (tag) if: startsWith(github.ref, 'refs/tags/') run: | make docker/tag DOCKER_TAG=${GITHUB_REF#refs/tags/} make docker/push ================================================ FILE: .github/workflows/no-response.yml ================================================ name: No Response # Both `issue_comment` and `scheduled` event types are required for this Action # to work properly. on: issue_comment: types: [created] schedule: # Schedule for an arbitrary time (5am) once every day - cron: '* 5 * * *' jobs: noResponse: # Don't run if in a fork if: github.repository_owner == 'kivy' runs-on: ubuntu-latest steps: - uses: lee-dohm/no-response@9bb0a4b5e6a45046f00353d5de7d90fb8bd773bb # This commit hash targets release v0.5.0 of lee-dohm/no-response. # Targeting a commit hash instead of a tag has been done for security reasons. # Please be aware that the commit hash specifically targets the "Automatic compilation" # done by `github-actions[bot]` as the `no-response` Action needs to be compiled. with: token: ${{ github.token }} daysUntilClose: 42 responseRequiredLabel: 'awaiting-reply' closeComment: > This issue has been automatically closed because there has been no response to our request for more information from the original author. With only the information that is currently in the issue, we don't have the means to take action. Please reach out if you have or find the answers we need so that we can investigate further. ================================================ FILE: .github/workflows/push.yml ================================================ name: Unit tests & build apps on: ['push', 'pull_request'] env: APK_ARTIFACT_FILENAME: bdist_unit_tests_app-debug-1.1.apk AAB_ARTIFACT_FILENAME: bdist_unit_tests_app-release-1.1.aab AAR_ARTIFACT_FILENAME: bdist_unit_tests_app-release-1.1.aar PYTHONFORANDROID_PREREQUISITES_INSTALL_INTERACTIVE: 0 concurrency: group: build-${{ github.ref }} cancel-in-progress: true jobs: flake8: name: Flake8 tests runs-on: ubuntu-latest steps: - name: Checkout python-for-android uses: actions/checkout@v5 - name: Set up Python 3.x uses: actions/setup-python@v6 with: python-version: 3.x - name: Run flake8 run: | python -m pip install --upgrade pip pip install tox>=2.0 tox -e pep8 spotless: name: Java Spotless check runs-on: ubuntu-latest steps: - name: Checkout python-for-android uses: actions/checkout@v5 - name: Set up Java 17 uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 - name: Run Spotless check working-directory: pythonforandroid/bootstraps run: ./common/build/gradlew spotlessCheck test: name: Pytest [Python ${{ matrix.python-version }} | ${{ matrix.os }}] needs: [flake8, spotless] runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t'] os: [ubuntu-latest, macos-latest] steps: - name: Checkout python-for-android uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - name: Tox tests run: | python -m pip install --upgrade pip pip install tox>=2.0 make test - name: Coveralls uses: AndreMiras/coveralls-python-action@develop if: ${{ matrix.os == 'ubuntu-latest' }} with: parallel: true flag-name: run-${{ matrix.os }}-${{ matrix.python-version }} ubuntu_build: name: Build test APP [ ${{ matrix.runs_on }} | ${{ matrix.bootstrap.name }} ] needs: [flake8, spotless] runs-on: ${{ matrix.runs_on }} strategy: matrix: runs_on: [ubuntu-latest] bootstrap: - name: sdl2 target: testapps-with-numpy # - name: sdl2_scipy # TODO: Re-enable this failing test. # target: testapps-with-scipy - name: webview target: testapps-webview - name: service_library target: testapps-service_library-aar - name: qt target: testapps-qt steps: - name: Maximize build space uses: easimon/maximize-build-space@v10 with: root-reserve-mb: 4096 swap-size-mb: 1024 remove-dotnet: 'true' remove-android: 'true' remove-haskell: 'true' remove-codeql: 'true' remove-docker-images: 'true' - name: Checkout python-for-android uses: actions/checkout@v5 - name: Relocate Docker data directory run: | sudo systemctl stop docker sudo mkdir -p "${GITHUB_WORKSPACE}/docker-data" echo '{"data-root": "'${GITHUB_WORKSPACE}/docker-data'"}' | sudo tee /etc/docker/daemon.json sudo systemctl start docker docker info | grep "Docker Root Dir" - name: Build python-for-android docker image run: | docker build --tag=kivy/python-for-android . - name: Build multi-arch ${{ matrix.bootstrap.target }} artifact with docker run: | docker run --name p4a-latest kivy/python-for-android make ${{ matrix.bootstrap.target }} - name: Copy produced artifacts from docker container (*.apk, *.aab) if: matrix.bootstrap.name != 'service_library' run: | mkdir -p dist docker cp p4a-latest:/home/user/app/testapps/on_device_unit_tests/${{ env.APK_ARTIFACT_FILENAME }} dist/ docker cp p4a-latest:/home/user/app/testapps/on_device_unit_tests/${{ env.AAB_ARTIFACT_FILENAME }} dist/ - name: Copy produced artifacts from docker container (*.aar) if: matrix.bootstrap.name == 'service_library' run: | mkdir -p dist docker cp p4a-latest:/home/user/app/testapps/on_device_unit_tests/${{ env.AAR_ARTIFACT_FILENAME }} dist/ - name: Rename artifacts to include the build platform name (*.apk, *.aab, *.aar) run: | if [ -f dist/${{ env.APK_ARTIFACT_FILENAME }} ]; then mv dist/${{ env.APK_ARTIFACT_FILENAME }} dist/${{ matrix.runs_on }}-${{ matrix.bootstrap.name }}-${{ env.APK_ARTIFACT_FILENAME }}; fi if [ -f dist/${{ env.AAB_ARTIFACT_FILENAME }} ]; then mv dist/${{ env.AAB_ARTIFACT_FILENAME }} dist/${{ matrix.runs_on }}-${{ matrix.bootstrap.name }}-${{ env.AAB_ARTIFACT_FILENAME }}; fi if [ -f dist/${{ env.AAR_ARTIFACT_FILENAME }} ]; then mv dist/${{ env.AAR_ARTIFACT_FILENAME }} dist/${{ matrix.runs_on }}-${{ matrix.bootstrap.name }}-${{ env.AAR_ARTIFACT_FILENAME }}; fi - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: ${{ matrix.runs_on }}-${{ matrix.bootstrap.name }}-artifacts path: dist macos_build: name: Build test APP [ ${{ matrix.runs_on }} | ${{ matrix.bootstrap.name }} ] needs: [flake8, spotless] runs-on: ${{ matrix.runs_on }} strategy: matrix: # macos-latest (ATM macos-15) runs on Apple Silicon, # macos-15-intel runs on Intel runs_on: ['macos-latest', 'macos-15-intel'] bootstrap: - name: sdl2 target: testapps-with-numpy - name: webview target: testapps-webview env: ANDROID_HOME: ${HOME}/.android ANDROID_SDK_ROOT: ${HOME}/.android/android-sdk ANDROID_SDK_HOME: ${HOME}/.android/android-sdk ANDROID_NDK_HOME: ${HOME}/.android/android-ndk steps: - name: Checkout python-for-android uses: actions/checkout@v5 - name: Set up Python 3.x uses: actions/setup-python@v6 with: python-version: 3.x - name: Install python-for-android run: | python3 -m pip install --editable . - name: Install prerequisites via pythonforandroid/prerequisites.py (Experimental) run: | python3 pythonforandroid/prerequisites.py - name: Install dependencies (Legacy) run: | make --file ci/makefiles/osx.mk - name: Build multi-arch apk Python 3 (armeabi-v7a, arm64-v8a, x86_64, x86) run: | make ${{ matrix.bootstrap.target }} - name: Copy produced artifacts into dist/ (*.apk, *.aab) run: | mkdir -p dist cp testapps/on_device_unit_tests/*.apk dist/ cp testapps/on_device_unit_tests/*.aab dist/ ls -l dist/ - name: Rename artifacts to include the build platform name (*.apk, *.aab) run: | if [ -f dist/${{ env.APK_ARTIFACT_FILENAME }} ]; then mv dist/${{ env.APK_ARTIFACT_FILENAME }} dist/${{ matrix.runs_on }}-${{ matrix.bootstrap.name }}-${{ env.APK_ARTIFACT_FILENAME }}; fi if [ -f dist/${{ env.AAB_ARTIFACT_FILENAME }} ]; then mv dist/${{ env.AAB_ARTIFACT_FILENAME }} dist/${{ matrix.runs_on }}-${{ matrix.bootstrap.name }}-${{ env.AAB_ARTIFACT_FILENAME }}; fi - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: ${{ matrix.runs_on }}-${{ matrix.bootstrap.name }}-artifacts path: dist test_on_emulator: name: Run App on Emulator needs: ubuntu_build runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Download Artifacts uses: actions/download-artifact@v5 with: name: ubuntu-latest-sdl2-artifacts path: dist/ - name: Setup and start Android Emulator uses: reactivecircus/android-emulator-runner@v2 with: api-level: 30 arch: x86_64 script: ci/run_emulator_tests.sh ubuntu_rebuild_updated_recipes: name: Test updated recipes for arch ${{ matrix.android_arch }} [ ubuntu-latest ] needs: [flake8, spotless] runs-on: ubuntu-latest # continue on error to see failures across all architectures continue-on-error: true strategy: matrix: android_arch: ["arm64-v8a", "armeabi-v7a", "x86_64", "x86"] env: REBUILD_UPDATED_RECIPES_EXTRA_ARGS: --arch=${{ matrix.android_arch }} steps: - name: Maximize build space uses: easimon/maximize-build-space@v10 with: root-reserve-mb: 4096 swap-size-mb: 1024 remove-dotnet: 'true' remove-android: 'true' remove-haskell: 'true' remove-codeql: 'true' remove-docker-images: 'true' - name: Checkout python-for-android (all-history) uses: actions/checkout@v5 with: fetch-depth: 0 - name: Relocate Docker data directory run: | sudo systemctl stop docker sudo mkdir -p "${GITHUB_WORKSPACE}/docker-data" echo '{"data-root": "'${GITHUB_WORKSPACE}/docker-data'"}' | sudo tee /etc/docker/daemon.json sudo systemctl start docker docker info | grep "Docker Root Dir" - name: Pull docker image run: | make docker/pull - name: Rebuild updated recipes run: | make docker/run/make/rebuild_updated_recipes macos_rebuild_updated_recipes: name: Test updated recipes for arch ${{ matrix.android_arch }} [ ${{ matrix.runs_on }} ] needs: [flake8, spotless] runs-on: ${{ matrix.runs_on }} # continue on error to see failures across all architectures continue-on-error: true strategy: matrix: android_arch: ["arm64-v8a", "armeabi-v7a", "x86_64", "x86"] # macos-latest (ATM macos-15) runs on Apple Silicon, # macos-15-intel runs on Intel runs_on: ['macos-latest', 'macos-15-intel'] env: ANDROID_HOME: ${HOME}/.android ANDROID_SDK_ROOT: ${HOME}/.android/android-sdk ANDROID_SDK_HOME: ${HOME}/.android/android-sdk ANDROID_NDK_HOME: ${HOME}/.android/android-ndk REBUILD_UPDATED_RECIPES_EXTRA_ARGS: --arch=${{ matrix.android_arch }} steps: - name: Checkout python-for-android (all-history) uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up Python 3.x uses: actions/setup-python@v6 with: python-version: 3.x - name: Install python-for-android run: | python3 -m pip install --editable . - name: Install prerequisites via pythonforandroid/prerequisites.py (Experimental) run: | python3 pythonforandroid/prerequisites.py - name: Install dependencies (Legacy) run: | make --file ci/makefiles/osx.mk - name: Rebuild updated recipes run: | make rebuild_updated_recipes coveralls_finish: needs: test runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: AndreMiras/coveralls-python-action@develop with: parallel-finished: true documentation: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Requirements run: | python -m pip install --upgrade pip pip install -r doc/requirements.txt - name: Check links run: sphinx-build -b linkcheck doc/source doc/build - name: Generate documentation run: sphinx-build doc/source doc/build ================================================ FILE: .github/workflows/pypi-release.yml ================================================ name: PyPI release on: [push] jobs: pypi_release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python 3.x uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade setuptools wheel twine - name: Build run: | python setup.py sdist bdist_wheel twine check dist/* - name: Publish package if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@v1.13.0 with: user: __token__ password: ${{ secrets.pypi_password }} ================================================ FILE: .github/workflows/support.yml ================================================ # When a user creates an issue that is actually a support request, it should # be closed with a friendly comment. # # This triggers on an issue being labelled with the `support` tag. name: 'Support Requests' on: issues: types: [labeled, unlabeled, reopened] permissions: issues: write jobs: action: runs-on: ubuntu-latest steps: - uses: dessant/support-requests@v4 with: github-token: ${{ github.token }} support-label: 'support' issue-comment: > 👋 @{issue-author}, Sorry to hear you are having difficulties with Kivy's python-for-android; Kivy unites a number of different technologies, so building apps can be temperamental. We try to use GitHub issues only to track work for developers to do to fix bugs and add new features to python-for-android. However, this issue appears to be a support request. Please use our [support channels](https://github.com/kivy/python-for-android/blob/master/CONTACT.md) to get help with the project. If you're having trouble installing python-for-android, please see our [quickstart](https://python-for-android.readthedocs.io/en/latest/quickstart) guide. If you're having trouble using python-for-android, please see our [troubleshooting guide](https://python-for-android.readthedocs.io/en/latest/troubleshooting) and [FAQ](https://github.com/kivy/python-for-android/blob/master/FAQ.md). Let us know if this comment was made in error, and we'll be happy to reopen the issue. close-issue: true lock-issue: false ================================================ FILE: .gitignore ================================================ *.swp *.swo *~ #ECLIPSE + PYDEV .project .pydevproject .deps .optional-deps *.pyc *.pyo *.apk .packages python_for_android.egg-info /build/ doc/build __pycache__/ venv/ #idea/pycharm .idea/ # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache coverage.xml *.cover .pytest_cache/ # testapp's build folder testapps/build/ # Gradle build artifacts (Java linting) pythonforandroid/bootstraps/.gradle/ pythonforandroid/bootstraps/build/ # Dolphin (the KDE file manager autogenerates the file `.directory`) .directory ================================================ FILE: .projectile ================================================ ================================================ FILE: .readthedocs.yaml ================================================ # Read the Docs configuration file for Sphinx projects # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-22.04 tools: python: "3" python: install: - requirements: doc/requirements.txt sphinx: configuration: doc/source/conf.py ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [v2024.01.21](https://github.com/kivy/python-for-android/tree/v2024.01.21) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2023.09.16...v2024.01.21) **Fixed bugs:** - Update documentation copyright [\#2921](https://github.com/kivy/python-for-android/issues/2921) - Support mail address is broken [\#2899](https://github.com/kivy/python-for-android/issues/2899) - doc/macos/jdk: invalid brew install command provided. [\#2896](https://github.com/kivy/python-for-android/issues/2896) - pyzmq recipe build fail [\#2818](https://github.com/kivy/python-for-android/issues/2818) - Existing distribution not detected due to pip package casing mismatch [\#2494](https://github.com/kivy/python-for-android/issues/2494) - unknown argument "fp-model" and strict is not a directory or a file [\#2359](https://github.com/kivy/python-for-android/issues/2359) - Copy past is not working on kivy mobile app [\#2270](https://github.com/kivy/python-for-android/issues/2270) - Flaky test failure in blacklist\(?\) - investigation needed [\#1781](https://github.com/kivy/python-for-android/issues/1781) - Problem with loading gevent: BadZipfile: File is not a zip file [\#1739](https://github.com/kivy/python-for-android/issues/1739) - ImportError when importing files containing \N{name} escape sequence [\#1060](https://github.com/kivy/python-for-android/issues/1060) - Error with permission specification via setup.cfg [\#985](https://github.com/kivy/python-for-android/issues/985) **Closed issues:** - Build failed: Could not find `android` or `sdkmanager` binaries in Android SDK [\#2956](https://github.com/kivy/python-for-android/issues/2956) - Libffi - configure: error: C compiler cannot create executables \(WSL 2\) [\#2953](https://github.com/kivy/python-for-android/issues/2953) - G [\#2951](https://github.com/kivy/python-for-android/issues/2951) - Hh [\#2949](https://github.com/kivy/python-for-android/issues/2949) - Can't build for Android on macOS on M2 [\#2947](https://github.com/kivy/python-for-android/issues/2947) - BroadcastReceiver does not invoke the callback [\#2946](https://github.com/kivy/python-for-android/issues/2946) - Add pdf2docx library recipe [\#2941](https://github.com/kivy/python-for-android/issues/2941) - use build aar in kotlin app ,can't load /lib/arm64/libpybundle.so file [\#2940](https://github.com/kivy/python-for-android/issues/2940) - Feature Request: Pymssql [\#2936](https://github.com/kivy/python-for-android/issues/2936) - LXML v4.8.0 fails to build. [\#2928](https://github.com/kivy/python-for-android/issues/2928) - Trying to apply a plugin fails [\#2926](https://github.com/kivy/python-for-android/issues/2926) - ModuleNotFoundError: No module named '\_sysconfigdata\_\_darwin\_darwin' [\#2925](https://github.com/kivy/python-for-android/issues/2925) - ReadTheDocs version is unclear. [\#2920](https://github.com/kivy/python-for-android/issues/2920) - How to get real file path from uri [\#2911](https://github.com/kivy/python-for-android/issues/2911) - And [\#2910](https://github.com/kivy/python-for-android/issues/2910) - ModuleNotFoundError: No module named 'backports' [\#2909](https://github.com/kivy/python-for-android/issues/2909) - not able to access files unless connected to adb once [\#2907](https://github.com/kivy/python-for-android/issues/2907) - opening files in other apps [\#2906](https://github.com/kivy/python-for-android/issues/2906) - ImportError: dlopen failed: cannot locate symbol "\_ZTVSt9bad\_alloc" [\#2903](https://github.com/kivy/python-for-android/issues/2903) - Fails to build pyjnius [\#2902](https://github.com/kivy/python-for-android/issues/2902) - Kivy app crashes on startup [\#2895](https://github.com/kivy/python-for-android/issues/2895) - aar file does not import properly in version v2023.09.16 [\#2894](https://github.com/kivy/python-for-android/issues/2894) - App is crashing with Pyrebase4 [\#2893](https://github.com/kivy/python-for-android/issues/2893) - shared libs builds with 32 bit arch instead of 64 bit [\#2888](https://github.com/kivy/python-for-android/issues/2888) - liblzma download error [\#2885](https://github.com/kivy/python-for-android/issues/2885) - Misconfiguration causing failure in compilation. [\#2879](https://github.com/kivy/python-for-android/issues/2879) - cygrpc.so is for EM\_X86\_64 \(62\) instead of EM\_AARCH64 \(183\) [\#2853](https://github.com/kivy/python-for-android/issues/2853) - Are you able to build cffi==1.15.1? [\#2847](https://github.com/kivy/python-for-android/issues/2847) - java.lang.IllegalStateException [\#2844](https://github.com/kivy/python-for-android/issues/2844) - \[BUG\]: ctypes: AttributeError: undefined symbol: PyCapsule\_New [\#2840](https://github.com/kivy/python-for-android/issues/2840) - kivy can't load image in requesturl android [\#2832](https://github.com/kivy/python-for-android/issues/2832) - Feature Request: Add Python `3.11` support [\#2798](https://github.com/kivy/python-for-android/issues/2798) - Error Build APK FIle using Flask [\#2783](https://github.com/kivy/python-for-android/issues/2783) - macOS: gwadlew fails at build tools stage \(newest build tools is 34.0.0-rc3, brew/openjdk@20\). [\#2781](https://github.com/kivy/python-for-android/issues/2781) - Kivy python Error loading video on some android device [\#2780](https://github.com/kivy/python-for-android/issues/2780) - buildozer/p4a.prerequisites: enable automation build with no questions asked. [\#2778](https://github.com/kivy/python-for-android/issues/2778) - \_python\_bundle does not exist...this not looks good, all python recipes should have this folder, should we expect a crash soon? [\#2773](https://github.com/kivy/python-for-android/issues/2773) - Background service implemented using Pyjnius does not auto-restart on Kivy APK close [\#2772](https://github.com/kivy/python-for-android/issues/2772) - \[JVM\]: FLAG\_IMMUTABLE or FLAG\_MUTABLE is required when a PendingIntent is created [\#2759](https://github.com/kivy/python-for-android/issues/2759) - there is an issue with playing video from URL on the latest p4a releases [\#2744](https://github.com/kivy/python-for-android/issues/2744) - App crashes at launch on specific devices \(\[libpython3.9.so\] \_PyEval\_EvalFrameDefault\) \(Adreno 730?\) [\#2723](https://github.com/kivy/python-for-android/issues/2723) - Pandas giving error in Buildozer [\#2719](https://github.com/kivy/python-for-android/issues/2719) - buildozer -v android debug [\#2711](https://github.com/kivy/python-for-android/issues/2711) - \[proposed feature-request\] Lacking psutil recipe [\#2707](https://github.com/kivy/python-for-android/issues/2707) - \[ERROR\]: Build failed: Asked to compile for no Archs, so failing. [\#2685](https://github.com/kivy/python-for-android/issues/2685) - Feature Request: Give more access to the android project folder inside of the dist folder [\#2614](https://github.com/kivy/python-for-android/issues/2614) - `shutil.copy()` fails on external removable storage devices [\#2589](https://github.com/kivy/python-for-android/issues/2589) - jnius can't find class "org.kivy.android.PythonActivity" with webview [\#2533](https://github.com/kivy/python-for-android/issues/2533) - \[MACOS\] Android app crashes on start when using macos to build [\#2519](https://github.com/kivy/python-for-android/issues/2519) - Pillow-SIMD recipe? [\#2420](https://github.com/kivy/python-for-android/issues/2420) - --asset & directories [\#2413](https://github.com/kivy/python-for-android/issues/2413) - dlopen failed: cannot locate symbol "\_\_register\_atfork" on Android 5.0 [\#2410](https://github.com/kivy/python-for-android/issues/2410) - dlib module not found error [\#2395](https://github.com/kivy/python-for-android/issues/2395) - lxml build failed for x86 arch [\#2369](https://github.com/kivy/python-for-android/issues/2369) - Android 10 storage permission denied [\#2364](https://github.com/kivy/python-for-android/issues/2364) - for pytorch [\#2353](https://github.com/kivy/python-for-android/issues/2353) - Problem with ffmpeg on Android [\#2345](https://github.com/kivy/python-for-android/issues/2345) - NLTK recipe for python for android [\#2320](https://github.com/kivy/python-for-android/issues/2320) - build\_tools\_versions comparison code fails for 'Android Rebuilds' SDKs because of different folder naming conventions [\#2318](https://github.com/kivy/python-for-android/issues/2318) - verify downloads using sha256? [\#2294](https://github.com/kivy/python-for-android/issues/2294) - outdated recipes [\#2277](https://github.com/kivy/python-for-android/issues/2277) - Custom recipe for scipy fails with permission issue [\#2267](https://github.com/kivy/python-for-android/issues/2267) - Kivy application generated crashes instantly with dlopen failed [\#2266](https://github.com/kivy/python-for-android/issues/2266) - EGLlib: validate\_display: 92 error 3008 \(EGL\_BAD\_DISPLAY\) : App crashes immediately \(kivymd\) \(Buildozer\) [\#2258](https://github.com/kivy/python-for-android/issues/2258) - libEGL : EGLNativeWindowType disconnect failed [\#2253](https://github.com/kivy/python-for-android/issues/2253) - Hao to support multiprocess Queue in Android [\#2249](https://github.com/kivy/python-for-android/issues/2249) - autoclass: Class only found when called in specific places? [\#2242](https://github.com/kivy/python-for-android/issues/2242) - the app crash in time of import psycopg2 [\#2240](https://github.com/kivy/python-for-android/issues/2240) - env must be a dict [\#2170](https://github.com/kivy/python-for-android/issues/2170) - Pandas doesn't work [\#2157](https://github.com/kivy/python-for-android/issues/2157) - Webview bootstrap can't find 'org.jnius.NativeInvocationHandler'. [\#2140](https://github.com/kivy/python-for-android/issues/2140) - clang++: error: linker command failed with exit code 1 [\#2082](https://github.com/kivy/python-for-android/issues/2082) - ModuleNotFoundError: No module named 'setuptools' [\#2078](https://github.com/kivy/python-for-android/issues/2078) - Scraping web pages with javascript [\#2052](https://github.com/kivy/python-for-android/issues/2052) - open webbrowser register\(\) error [\#2047](https://github.com/kivy/python-for-android/issues/2047) - Missing javaclass when using able with previously working recipe [\#2041](https://github.com/kivy/python-for-android/issues/2041) - :Class not found b'org/kivy/android/PythonActivity$ActivityResultListener' [\#2039](https://github.com/kivy/python-for-android/issues/2039) - App\(using socket and opencv\) crash on opening [\#2038](https://github.com/kivy/python-for-android/issues/2038) - android apk is crashing after displaying splash screen on phone [\#2030](https://github.com/kivy/python-for-android/issues/2030) - Leverage Docker image caching [\#2009](https://github.com/kivy/python-for-android/issues/2009) - entrypoint confusion with python3 [\#1999](https://github.com/kivy/python-for-android/issues/1999) - Android app crash on opening - Python Initialize [\#1987](https://github.com/kivy/python-for-android/issues/1987) - Error building APK: "Missing 'name' key attribute on element activity at AndroidManifest.xml" [\#1979](https://github.com/kivy/python-for-android/issues/1979) - Ugent issues on Webview \(Android Back Button to main App\) [\#1961](https://github.com/kivy/python-for-android/issues/1961) - JavaException: JVM exception occurred: Fail to connect to camera service [\#1943](https://github.com/kivy/python-for-android/issues/1943) - Python version number must have subversion? cannot find Python-3.7.tgz [\#1941](https://github.com/kivy/python-for-android/issues/1941) - dlopen failed: jnius.so is for EM\_ARM \(40\) instead of EM\_386 \(3\) [\#1927](https://github.com/kivy/python-for-android/issues/1927) - Matplotlib recipe depends on local environment [\#1900](https://github.com/kivy/python-for-android/issues/1900) - main window jumps up and down [\#1876](https://github.com/kivy/python-for-android/issues/1876) - ctypes.pythonapi issues; getting AttributeError: undefined symbol [\#1866](https://github.com/kivy/python-for-android/issues/1866) - \[enhancement\] do not rebuild already built packages [\#1860](https://github.com/kivy/python-for-android/issues/1860) - Matplotlib recipe fails sometimes [\#1859](https://github.com/kivy/python-for-android/issues/1859) - p4a build with NDK r18b: clang: error: unknown argument: '-mandroid' [\#1853](https://github.com/kivy/python-for-android/issues/1853) - Activity lifecycle issues. after onDestroy, application will become unusable [\#1844](https://github.com/kivy/python-for-android/issues/1844) - Service AutoRestart did not work [\#1823](https://github.com/kivy/python-for-android/issues/1823) - Android debug results in error involving clang++ and linker. [\#1796](https://github.com/kivy/python-for-android/issues/1796) - seek\(\) method on a file object doesn't use right arguments [\#1768](https://github.com/kivy/python-for-android/issues/1768) - Same issue w/ -lpython2.7 not found, workaround [\#1753](https://github.com/kivy/python-for-android/issues/1753) - Several issues when installing packages via pip [\#1745](https://github.com/kivy/python-for-android/issues/1745) - Publish a new Kivy Launcher for Python 3 [\#1638](https://github.com/kivy/python-for-android/issues/1638) - Travis conditional bootstrap build support [\#1588](https://github.com/kivy/python-for-android/issues/1588) - Error when execute APK only on device: ImportError: cannot import name \_htmlparser [\#1523](https://github.com/kivy/python-for-android/issues/1523) - onSensorChanged continuously called during app execution [\#1498](https://github.com/kivy/python-for-android/issues/1498) - GC deadlock on subprocess [\#1461](https://github.com/kivy/python-for-android/issues/1461) - Code runs on old pygame backend but not on SDL2 [\#1411](https://github.com/kivy/python-for-android/issues/1411) - build-tools below 25 will not add jars [\#1345](https://github.com/kivy/python-for-android/issues/1345) - Flaky continuous integration [\#1306](https://github.com/kivy/python-for-android/issues/1306) - Icon/Logo Proposal [\#1264](https://github.com/kivy/python-for-android/issues/1264) - Unable to write the config [\#1151](https://github.com/kivy/python-for-android/issues/1151) - p4a does not yet work with clang [\#1097](https://github.com/kivy/python-for-android/issues/1097) - android module seems to eat up a character from java properties [\#945](https://github.com/kivy/python-for-android/issues/945) - TypeError: a bytes-like object is required, not 'str' [\#856](https://github.com/kivy/python-for-android/issues/856) - Feature request: access to all permissions [\#843](https://github.com/kivy/python-for-android/issues/843) - Extending the launcher [\#565](https://github.com/kivy/python-for-android/issues/565) **Merged pull requests:** - Update OpenSSL version to `1.1.1w` [\#2958](https://github.com/kivy/python-for-android/pull/2958) ([prolenodev](https://github.com/prolenodev)) - Bump Kivy version to `2.3.0` [\#2952](https://github.com/kivy/python-for-android/pull/2952) ([misl6](https://github.com/misl6)) - `sourceCompatibility` 1.7 and `targetCompatibility` 1.7 are obsolete, use 1.8 by default [\#2942](https://github.com/kivy/python-for-android/pull/2942) ([misl6](https://github.com/misl6)) - Remove redundant append into WHITELIST\_PATTERNS [\#2935](https://github.com/kivy/python-for-android/pull/2935) ([shyamnathp](https://github.com/shyamnathp)) - Update sdl2 deps to reflect the same targeted in kivy/kivy [\#2927](https://github.com/kivy/python-for-android/pull/2927) ([misl6](https://github.com/misl6)) - Update `python-for-android` prerequisites \(`Dockerfile`, `prerequisites.py`, docs\) [\#2923](https://github.com/kivy/python-for-android/pull/2923) ([misl6](https://github.com/misl6)) - Update Contributing Guidelines and Readme [\#2922](https://github.com/kivy/python-for-android/pull/2922) ([Julian-O](https://github.com/Julian-O)) - Initial support for PySide6 and Qt [\#2918](https://github.com/kivy/python-for-android/pull/2918) ([shyamnathp](https://github.com/shyamnathp)) - Introduce FAQ [\#2917](https://github.com/kivy/python-for-android/pull/2917) ([Julian-O](https://github.com/Julian-O)) - Add \(now mandatory\) `.readthedocs.yaml` file, add docs `requirements.txt` and update sphinx conf [\#2916](https://github.com/kivy/python-for-android/pull/2916) ([misl6](https://github.com/misl6)) - enable json1 extension in sqlite3 [\#2915](https://github.com/kivy/python-for-android/pull/2915) ([HyTurtle](https://github.com/HyTurtle)) - Bump `pyjnius` version to `1.6.1` [\#2914](https://github.com/kivy/python-for-android/pull/2914) ([misl6](https://github.com/misl6)) - Remove `distutils` usage, as is not available anymore on Python `3.12` [\#2912](https://github.com/kivy/python-for-android/pull/2912) ([misl6](https://github.com/misl6)) - Update Lottie player version [\#2900](https://github.com/kivy/python-for-android/pull/2900) ([HugoDaniel](https://github.com/HugoDaniel)) - Merge master into develop [\#2892](https://github.com/kivy/python-for-android/pull/2892) ([misl6](https://github.com/misl6)) - Add doc tests, make them pass. [\#2890](https://github.com/kivy/python-for-android/pull/2890) ([Julian-O](https://github.com/Julian-O)) - Update Android gradle plugin to `8.1.1` and gradle to `8.0.2` [\#2887](https://github.com/kivy/python-for-android/pull/2887) ([misl6](https://github.com/misl6)) - Add support for Python `3.11` and make it the default while building `hostpython3` and `python3` [\#2850](https://github.com/kivy/python-for-android/pull/2850) ([T-Dynamos](https://github.com/T-Dynamos)) ## [v2023.09.16](https://github.com/kivy/python-for-android/tree/v2023.09.16) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2023.05.21...v2023.09.16) **Closed issues:** - Microphone And Audio permissions doesn't work [\#2889](https://github.com/kivy/python-for-android/issues/2889) - Error with Scipy [\#2883](https://github.com/kivy/python-for-android/issues/2883) - Download failed \( Downloading sqlite3 from https://www.sqlite.org/2021/sqlite-amalgamation-3350500.zip \) during buildozer -v android debug [\#2881](https://github.com/kivy/python-for-android/issues/2881) - ONNXruntime lib open failed due to 64-bit [\#2880](https://github.com/kivy/python-for-android/issues/2880) - Question: how to write a recipe to download and install\(coz build fail\) a wheel package directly? [\#2878](https://github.com/kivy/python-for-android/issues/2878) - Impossible to install python for android [\#2867](https://github.com/kivy/python-for-android/issues/2867) - scipy with kivy [\#2861](https://github.com/kivy/python-for-android/issues/2861) - False positive parser warning. [\#2856](https://github.com/kivy/python-for-android/issues/2856) - After successfully building app, its crashes with this error, using firebase-admin [\#2854](https://github.com/kivy/python-for-android/issues/2854) - Kivy [\#2837](https://github.com/kivy/python-for-android/issues/2837) - not installing on windows 10 [\#2836](https://github.com/kivy/python-for-android/issues/2836) - Could not find com.android.tools.build:gradle:7.2.0. in android studio [\#2834](https://github.com/kivy/python-for-android/issues/2834) - vlc recipe build fail [\#2822](https://github.com/kivy/python-for-android/issues/2822) - mysqldb recipe build fail [\#2813](https://github.com/kivy/python-for-android/issues/2813) - babel recipe build fail [\#2803](https://github.com/kivy/python-for-android/issues/2803) - Python 3.10 cffi build fails [\#2799](https://github.com/kivy/python-for-android/issues/2799) - \[Recipe request\] OpenColorIO & rawpy \(libraw\) [\#2789](https://github.com/kivy/python-for-android/issues/2789) - Longer app load time, bigger apk size and unnecessary logs [\#2704](https://github.com/kivy/python-for-android/issues/2704) - -fp-model argument not found, directory strict not found [\#2329](https://github.com/kivy/python-for-android/issues/2329) - Kivy crashes on Android: ImportError: dlopen failed: library "libpython3.7m.so" not found [\#2237](https://github.com/kivy/python-for-android/issues/2237) - pyconfig.h issue [\#2074](https://github.com/kivy/python-for-android/issues/2074) **Merged pull requests:** - Use Python's touch\(\) rather than shelling out. [\#2886](https://github.com/kivy/python-for-android/pull/2886) ([Julian-O](https://github.com/Julian-O)) - Standardise on move [\#2884](https://github.com/kivy/python-for-android/pull/2884) ([Julian-O](https://github.com/Julian-O)) - Remove deprecated FlatDir in Gradle template [\#2876](https://github.com/kivy/python-for-android/pull/2876) ([Julian-O](https://github.com/Julian-O)) - :rotating\_light: linter fixes [\#2874](https://github.com/kivy/python-for-android/pull/2874) ([AndreMiras](https://github.com/AndreMiras)) - Standardise ensure\_dir and rmdir [\#2871](https://github.com/kivy/python-for-android/pull/2871) ([Julian-O](https://github.com/Julian-O)) - Correct check for --sdk option [\#2870](https://github.com/kivy/python-for-android/pull/2870) ([Julian-O](https://github.com/Julian-O)) - Python versions: Update documentation & CI testing [\#2869](https://github.com/kivy/python-for-android/pull/2869) ([Julian-O](https://github.com/Julian-O)) - Patching cleanup [\#2868](https://github.com/kivy/python-for-android/pull/2868) ([Julian-O](https://github.com/Julian-O)) - Factor out dependency checking. Use modern version handling [\#2866](https://github.com/kivy/python-for-android/pull/2866) ([Julian-O](https://github.com/Julian-O)) - `build_platform` should be all-lowercase [\#2864](https://github.com/kivy/python-for-android/pull/2864) ([misl6](https://github.com/misl6)) - Fix simple typos in comments [\#2863](https://github.com/kivy/python-for-android/pull/2863) ([Julian-O](https://github.com/Julian-O)) - Use a pinned version of `Cython` for now, as most of the recipes are incompatible with `Cython==3.x.x` [\#2862](https://github.com/kivy/python-for-android/pull/2862) ([misl6](https://github.com/misl6)) - Docs: Fix typos and updated command to build apk - README [\#2860](https://github.com/kivy/python-for-android/pull/2860) ([kulothunganug](https://github.com/kulothunganug)) - Docs: Fix code string - quickstart.rst [\#2859](https://github.com/kivy/python-for-android/pull/2859) ([kulothunganug](https://github.com/kulothunganug)) - Automatically generate required pre-requisites [\#2858](https://github.com/kivy/python-for-android/pull/2858) ([Julian-O](https://github.com/Julian-O)) - Use platform.uname instead of os.uname [\#2857](https://github.com/kivy/python-for-android/pull/2857) ([Julian-O](https://github.com/Julian-O)) - Bump `kivy` version to `2.2.1` [\#2855](https://github.com/kivy/python-for-android/pull/2855) ([misl6](https://github.com/misl6)) - Correct sys\_platform [\#2852](https://github.com/kivy/python-for-android/pull/2852) ([Julian-O](https://github.com/Julian-O)) - Changed the url to use https as http fails [\#2846](https://github.com/kivy/python-for-android/pull/2846) ([kuzeyron](https://github.com/kuzeyron)) - vlc: fix build [\#2841](https://github.com/kivy/python-for-android/pull/2841) ([T-Dynamos](https://github.com/T-Dynamos)) - Optimize CI runs, by avoiding unnecessary rebuilds [\#2833](https://github.com/kivy/python-for-android/pull/2833) ([misl6](https://github.com/misl6)) - Remove `pytz` recipe, as it's not needed anymore [\#2830](https://github.com/kivy/python-for-android/pull/2830) ([misl6](https://github.com/misl6)) - Remove `dateutil` recipe, as it's not needed anymore [\#2829](https://github.com/kivy/python-for-android/pull/2829) ([misl6](https://github.com/misl6)) - Removes `mysqldb` recipe as does not support Python 3 [\#2828](https://github.com/kivy/python-for-android/pull/2828) ([misl6](https://github.com/misl6)) - Bump `actions/setup-python` and `actions/checkout` versions, as old ones are deprecated [\#2827](https://github.com/kivy/python-for-android/pull/2827) ([misl6](https://github.com/misl6)) - Removes `Babel` recipe as it's not needed anymore. [\#2826](https://github.com/kivy/python-for-android/pull/2826) ([misl6](https://github.com/misl6)) - Cffi update [\#2800](https://github.com/kivy/python-for-android/pull/2800) ([HyTurtle](https://github.com/HyTurtle)) - Merge master into develop [\#2797](https://github.com/kivy/python-for-android/pull/2797) ([misl6](https://github.com/misl6)) - Use build rather than pep517 for building [\#2784](https://github.com/kivy/python-for-android/pull/2784) ([s-t-e-v-e-n-k](https://github.com/s-t-e-v-e-n-k)) ## [v2023.05.21](https://github.com/kivy/python-for-android/tree/v2023.05.21) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2023.02.10...v2023.05.21) **Closed issues:** - python [\#2795](https://github.com/kivy/python-for-android/issues/2795) - Create APK from PyQt app [\#2794](https://github.com/kivy/python-for-android/issues/2794) - psutil/\_psutil\_linux.so" is 64-bit instead of 32-bit [\#2785](https://github.com/kivy/python-for-android/issues/2785) - pythonforandroid.toolchain.py: error: unrecognized arguments: --dir [\#2775](https://github.com/kivy/python-for-android/issues/2775) - App [\#2774](https://github.com/kivy/python-for-android/issues/2774) - org.kivy.android.PythonActivity$NewIntentListener is not visible from class loader java.lang.IllegalArgumentException [\#2770](https://github.com/kivy/python-for-android/issues/2770) - Service don t start anymore, as smallIconName extra is now mandatory [\#2768](https://github.com/kivy/python-for-android/issues/2768) - Start a background sticky service that auto-restart. [\#2767](https://github.com/kivy/python-for-android/issues/2767) - Fail installation [\#2764](https://github.com/kivy/python-for-android/issues/2764) - Python exception when using colorlog due to incomplete IO implementation in sys.stderr [\#2762](https://github.com/kivy/python-for-android/issues/2762) - AttributeError: 'org.kivy.android.PythonService' object has no attribute 'getComponentName' [\#2760](https://github.com/kivy/python-for-android/issues/2760) - https://code.videolan.org not available [\#2758](https://github.com/kivy/python-for-android/issues/2758) - Cannot install Python-for-Android [\#2754](https://github.com/kivy/python-for-android/issues/2754) - c/\_cffi\_backend.c:407:23: error: expression is not assignable [\#2753](https://github.com/kivy/python-for-android/issues/2753) - not install [\#2749](https://github.com/kivy/python-for-android/issues/2749) - APK crashes upon launch. logcat error: null pointer dereference \(occurs with imported modules\) [\#2358](https://github.com/kivy/python-for-android/issues/2358) - Error occurred while building the application using buildozer [\#2104](https://github.com/kivy/python-for-android/issues/2104) - "Could Not Extract Public Data" Needs very explicit instructions or feedback to the user [\#260](https://github.com/kivy/python-for-android/issues/260) **Merged pull requests:** - Update Kivy recipe for 2.2.0 [\#2793](https://github.com/kivy/python-for-android/pull/2793) ([misl6](https://github.com/misl6)) - Update `pyjnius` version to `1.5.0` [\#2791](https://github.com/kivy/python-for-android/pull/2791) ([misl6](https://github.com/misl6)) - fix tools/liblink: syntax error [\#2771](https://github.com/kivy/python-for-android/pull/2771) ([SomberNight](https://github.com/SomberNight)) - fix \#2768 smallIconName null can t be compared to String [\#2769](https://github.com/kivy/python-for-android/pull/2769) ([brvier](https://github.com/brvier)) - android\_api to integer [\#2765](https://github.com/kivy/python-for-android/pull/2765) ([kuzeyron](https://github.com/kuzeyron)) - Use io.IOBase for LogFile [\#2763](https://github.com/kivy/python-for-android/pull/2763) ([dylanmccall](https://github.com/dylanmccall)) - Home app functionality [\#2761](https://github.com/kivy/python-for-android/pull/2761) ([kuzeyron](https://github.com/kuzeyron)) - Add debug loggings for identifying a matching dist [\#2751](https://github.com/kivy/python-for-android/pull/2751) ([BitcoinWukong](https://github.com/BitcoinWukong)) - Add PyAV recipe [\#2750](https://github.com/kivy/python-for-android/pull/2750) ([DexerBR](https://github.com/DexerBR)) - Merge master into develop [\#2748](https://github.com/kivy/python-for-android/pull/2748) ([misl6](https://github.com/misl6)) - Add support for Python 3.10 and make it the default while building hostpython3 and python3 [\#2577](https://github.com/kivy/python-for-android/pull/2577) ([misl6](https://github.com/misl6)) ## [v2023.02.10](https://github.com/kivy/python-for-android/tree/v2023.02.10) (2023-02-10) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2023.01.28...v2023.02.10) **Closed issues:** - AttributeError: 'str' object has no attribute 'stdout' [\#2745](https://github.com/kivy/python-for-android/issues/2745) - Android app crash on starting up by accessing texture. Error:No module named 'typing\_extensions' [\#2743](https://github.com/kivy/python-for-android/issues/2743) **Merged pull requests:** - restrict sh version [\#2746](https://github.com/kivy/python-for-android/pull/2746) ([HyTurtle](https://github.com/HyTurtle)) - 🐛 fix: Update `pydantic` recipe [\#2742](https://github.com/kivy/python-for-android/pull/2742) ([FilipeMarch](https://github.com/FilipeMarch)) - Merge master into develop [\#2741](https://github.com/kivy/python-for-android/pull/2741) ([misl6](https://github.com/misl6)) ## [v2023.01.28](https://github.com/kivy/python-for-android/tree/v2023.01.28) (2023-01-28) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2022.12.20...v2023.01.28) **Closed issues:** - Python [\#2737](https://github.com/kivy/python-for-android/issues/2737) - AndroidX Issue [\#2736](https://github.com/kivy/python-for-android/issues/2736) - Kivy build failed [\#2735](https://github.com/kivy/python-for-android/issues/2735) - Can't build apk using READ\_EXTERNAL\_STORAGE, WRITE\_EXTERNAL\_STORAGE in buildozer.spec [\#2732](https://github.com/kivy/python-for-android/issues/2732) - BUILD FAILURE: No main.py\(o\) found in your app directory. [\#2731](https://github.com/kivy/python-for-android/issues/2731) - Your app currently targets API level 30 and must target at least API level 31 to ensure that it is built on the latest APIs optimised for security and performance. Change your app's target API level to at least 31 [\#2729](https://github.com/kivy/python-for-android/issues/2729) - Your app currently targets API level 30 and must target at least API level 31 to ensure that it is built on the latest APIs optimised for security and performance. Change your app's target API level to at least 31 [\#2727](https://github.com/kivy/python-for-android/issues/2727) - `sh.CommandNotFound: ./download.sh` [\#2726](https://github.com/kivy/python-for-android/issues/2726) - Setting `android:screenOrientation` via `--orientation` has no effect when targeting API 31 [\#2724](https://github.com/kivy/python-for-android/issues/2724) - \[Question\]: How to set 'compileSdkVersion' to 31 or higher. [\#2722](https://github.com/kivy/python-for-android/issues/2722) - Bug in the keyboard event listener [\#2423](https://github.com/kivy/python-for-android/issues/2423) **Merged pull requests:** - Implements `--manifest-orientation` and changes how `--orientation` works so we can now pass the setting to the SDL orientation hint [\#2739](https://github.com/kivy/python-for-android/pull/2739) ([misl6](https://github.com/misl6)) - Update \_\_init\_\_.py from `scrypt` recipe [\#2738](https://github.com/kivy/python-for-android/pull/2738) ([FilipeMarch](https://github.com/FilipeMarch)) - Apply a patch from SDL upstream that fixes orientation settings [\#2730](https://github.com/kivy/python-for-android/pull/2730) ([misl6](https://github.com/misl6)) - Support permission properties \(`maxSdkVersion` and `usesPermissionFlags`\) + remove `WRITE_EXTERNAL_STORAGE` permission, which has been previously declared by default [\#2725](https://github.com/kivy/python-for-android/pull/2725) ([misl6](https://github.com/misl6)) - Merge master in develop [\#2721](https://github.com/kivy/python-for-android/pull/2721) ([misl6](https://github.com/misl6)) ## [v2022.12.20](https://github.com/kivy/python-for-android/tree/v2022.12.20) (2022-12-20) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2022.09.04...v2022.12.20) **Fixed bugs:** - `liblzma` fails to build on macOS \(sh.ErrorReturnCode\_2. when running buildozer android debug\) [\#2343](https://github.com/kivy/python-for-android/issues/2343) **Closed issues:** - SDL\_ttf 2.0.15 download missing \(deprecated\) related to \#2698 [\#2710](https://github.com/kivy/python-for-android/issues/2710) - Update kivy app that's already published on google play store. [\#2709](https://github.com/kivy/python-for-android/issues/2709) - Installing deepspeech [\#2702](https://github.com/kivy/python-for-android/issues/2702) - ImportError: dlopen failed: library "libc++\_shared.so" not found [\#2699](https://github.com/kivy/python-for-android/issues/2699) - ffpyplayer recipe broken after SDL2 upgrade [\#2698](https://github.com/kivy/python-for-android/issues/2698) - ModuleNotFoundError: No module named 'android' [\#2697](https://github.com/kivy/python-for-android/issues/2697) - Error When I set android.api = 31 [\#2696](https://github.com/kivy/python-for-android/issues/2696) - Is there any way to protect .py code [\#2695](https://github.com/kivy/python-for-android/issues/2695) - args.service\_class\_name results in link error [\#2679](https://github.com/kivy/python-for-android/issues/2679) - Remove `x86_64` suffix from ndk download link [\#2676](https://github.com/kivy/python-for-android/issues/2676) - Pillow 9.2.0 recipe? [\#2671](https://github.com/kivy/python-for-android/issues/2671) - `ffmpeg`: unable to find library -lvpx [\#2665](https://github.com/kivy/python-for-android/issues/2665) - Buildozer fails while build numpy recipe 'UnixCCompiler' object has no attribute 'cxx\_compiler' [\#2664](https://github.com/kivy/python-for-android/issues/2664) - \[PEP 517\] Relax installation-time "pep517\<0.7.0" requirement [\#2573](https://github.com/kivy/python-for-android/issues/2573) - Add support for custom resources [\#2298](https://github.com/kivy/python-for-android/issues/2298) - Auto-correct / word suggestion does not work with Swiftkey/Samsung keyboard [\#2010](https://github.com/kivy/python-for-android/issues/2010) **Merged pull requests:** - `InputType.TYPE_TEXT_FLAG_MULTI_LINE` forces `InputType.TYPE_TEXT` even if `SDLActivity.keyboardInputType` is `NULL` [\#2716](https://github.com/kivy/python-for-android/pull/2716) ([misl6](https://github.com/misl6)) - secp256k1 Update "--host=" [\#2714](https://github.com/kivy/python-for-android/pull/2714) ([RobertFlatt](https://github.com/RobertFlatt)) - Delete pythonforandroid/recipes/cdecimal directory [\#2713](https://github.com/kivy/python-for-android/pull/2713) ([RobertFlatt](https://github.com/RobertFlatt)) - Bump `sdl2` version to `2.26.1` [\#2712](https://github.com/kivy/python-for-android/pull/2712) ([misl6](https://github.com/misl6)) - Flake8 does not support inline comments for any of the keys. [\#2708](https://github.com/kivy/python-for-android/pull/2708) ([misl6](https://github.com/misl6)) - Gradle: Run the clean task before anything else to make sure nothing is cached. [\#2705](https://github.com/kivy/python-for-android/pull/2705) ([misl6](https://github.com/misl6)) - Custom Service notification [\#2703](https://github.com/kivy/python-for-android/pull/2703) ([RobertFlatt](https://github.com/RobertFlatt)) - Include paths for sdl2\_mixer have changed. Added a method to return the right one. [\#2700](https://github.com/kivy/python-for-android/pull/2700) ([misl6](https://github.com/misl6)) - WRITE\_EXTERNAL\_STORAGE maxSdk [\#2694](https://github.com/kivy/python-for-android/pull/2694) ([RobertFlatt](https://github.com/RobertFlatt)) - Fixes an issue regarding blacklist and bytecode compile + some cleanup [\#2693](https://github.com/kivy/python-for-android/pull/2693) ([misl6](https://github.com/misl6)) - Bump to a version of `SDL` with patches for the TextInput / TextEditing \(SDL `2.26.0`\) [\#2692](https://github.com/kivy/python-for-android/pull/2692) ([misl6](https://github.com/misl6)) - Make CI compile aiohttp again. [\#2690](https://github.com/kivy/python-for-android/pull/2690) ([xavierfiechter](https://github.com/xavierfiechter)) - Add resources [\#2684](https://github.com/kivy/python-for-android/pull/2684) ([RobertFlatt](https://github.com/RobertFlatt)) - Update `MIN_TARGET_API` to `30` and `RECOMMENDED_TARGET_API` to `33` [\#2683](https://github.com/kivy/python-for-android/pull/2683) ([misl6](https://github.com/misl6)) - recipe.download\_file: implement shallow git cloning [\#2682](https://github.com/kivy/python-for-android/pull/2682) ([SomberNight](https://github.com/SomberNight)) - requirements: relax version bound on "pep517" [\#2680](https://github.com/kivy/python-for-android/pull/2680) ([SomberNight](https://github.com/SomberNight)) - Add new Android permissions [\#2677](https://github.com/kivy/python-for-android/pull/2677) ([RobertFlatt](https://github.com/RobertFlatt)) - Resize webview when keyboard is shown [\#2674](https://github.com/kivy/python-for-android/pull/2674) ([dbnicholson](https://github.com/dbnicholson)) - Update `SDL2`, `SDL2_ttf`, `SDL2_mixer`, `SDL2_image` to latest releases [\#2673](https://github.com/kivy/python-for-android/pull/2673) ([misl6](https://github.com/misl6)) - Fixes libvpx build [\#2672](https://github.com/kivy/python-for-android/pull/2672) ([misl6](https://github.com/misl6)) - `toml` may not be available on systemwide python [\#2670](https://github.com/kivy/python-for-android/pull/2670) ([misl6](https://github.com/misl6)) - android/activity: Add Application.ActivityLifecycleCallbacks helpers [\#2669](https://github.com/kivy/python-for-android/pull/2669) ([dbnicholson](https://github.com/dbnicholson)) - Bump minimal and recommended Android NDK version to 25b [\#2668](https://github.com/kivy/python-for-android/pull/2668) ([misl6](https://github.com/misl6)) - Include HOME in build environment [\#2582](https://github.com/kivy/python-for-android/pull/2582) ([dbnicholson](https://github.com/dbnicholson)) ## [v2022.09.04](https://github.com/kivy/python-for-android/tree/v2022.09.04) (2022-09-04) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2022.07.20...v2022.09.04) **Fixed bugs:** - Matplotlib failed to import properly on an APK from Buildozer and Kivy [\#2643](https://github.com/kivy/python-for-android/issues/2643) **Closed issues:** - KeyError: Matplotlib with kivy android [\#2658](https://github.com/kivy/python-for-android/issues/2658) - KeyError: Matplotlib [\#2659](https://github.com/kivy/python-for-android/issues/2659) - Upgrade from NDK 19b to 23b causes problems with Pandas library [\#2654](https://github.com/kivy/python-for-android/issues/2654) - Update Dockerfile for ARM [\#2653](https://github.com/kivy/python-for-android/issues/2653) - Apple M2 chip doesn't generate apk: compiling error on liblzma [\#2652](https://github.com/kivy/python-for-android/issues/2652) - aiohttp/\_http\_parser.pyx:46:0: '\_headers.pxi' not found [\#2651](https://github.com/kivy/python-for-android/issues/2651) - \[Question\] Pip SSL ? [\#2649](https://github.com/kivy/python-for-android/issues/2649) - Colab gives me as error "No module named 'typing\_extensions' ", even if before with the same compilation it worked [\#2648](https://github.com/kivy/python-for-android/issues/2648) - \[Question\] Java Files [\#2646](https://github.com/kivy/python-for-android/issues/2646) - Using foreground services will cause wired behaviour on Android 8 [\#2641](https://github.com/kivy/python-for-android/issues/2641) - Can't apply patches with relative paths for local recipe [\#2623](https://github.com/kivy/python-for-android/issues/2623) - Compile for x86 on MacOS [\#2215](https://github.com/kivy/python-for-android/issues/2215) - splash always loading [\#1907](https://github.com/kivy/python-for-android/issues/1907) - python-for-android.readthedocs.io has problems updating, apparently [\#1709](https://github.com/kivy/python-for-android/issues/1709) - Webview apps not working on Android [\#1644](https://github.com/kivy/python-for-android/issues/1644) **Merged pull requests:** - `liblzma`: Use `p4a_install` instead of `install`, as a file named `INSTALL` is already present. [\#2663](https://github.com/kivy/python-for-android/pull/2663) ([misl6](https://github.com/misl6)) - Force `--platform=linux/amd64` in Dockerfile [\#2660](https://github.com/kivy/python-for-android/pull/2660) ([misl6](https://github.com/misl6)) - Remove six and enum34 dependency [\#2657](https://github.com/kivy/python-for-android/pull/2657) ([misl6](https://github.com/misl6)) - Update supported Python versions [\#2656](https://github.com/kivy/python-for-android/pull/2656) ([misl6](https://github.com/misl6)) - Fixes some E275 - assert is a keyword. [\#2647](https://github.com/kivy/python-for-android/pull/2647) ([misl6](https://github.com/misl6)) - Updates matplotlib, fixes an issue related to shared libc++ [\#2645](https://github.com/kivy/python-for-android/pull/2645) ([misl6](https://github.com/misl6)) - RTSP support for ffmpeg [\#2644](https://github.com/kivy/python-for-android/pull/2644) ([alicakici1234](https://github.com/alicakici1234)) - Fixes TypeError: str.join\(\) takes exactly one argument \(2 given\) in hostpython3/\_\_init\_\_.py", line 69 [\#2642](https://github.com/kivy/python-for-android/pull/2642) ([Furtif](https://github.com/Furtif)) - Resolve absolute path to local recipes [\#2640](https://github.com/kivy/python-for-android/pull/2640) ([dbnicholson](https://github.com/dbnicholson)) - Merges master into develop after release 2022.07.20 [\#2639](https://github.com/kivy/python-for-android/pull/2639) ([misl6](https://github.com/misl6)) - Fix webview Back button behaviour [\#2636](https://github.com/kivy/python-for-android/pull/2636) ([interlark](https://github.com/interlark)) - Add icon-bg and icon-fg to fix\_args [\#2633](https://github.com/kivy/python-for-android/pull/2633) ([danigm](https://github.com/danigm)) - Remove stray - in output file name [\#2581](https://github.com/kivy/python-for-android/pull/2581) ([dbnicholson](https://github.com/dbnicholson)) - Add option for adding files to res/xml without touching manifest [\#2330](https://github.com/kivy/python-for-android/pull/2330) ([rambo](https://github.com/rambo)) ## [v2022.07.20](https://github.com/kivy/python-for-android/tree/v2022.07.20) (2022-07-20) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2022.03.13...v2022.07.20) **Fixed bugs:** - Current default Python version \(3.8.9\) is failing to build on latest macOS releases [\#2568](https://github.com/kivy/python-for-android/issues/2568) - Build failed for Pillow recipe when targeting x86\_64 arch [\#2259](https://github.com/kivy/python-for-android/issues/2259) - UnboundLocalError: local variable 'toolchain\_version' referenced before assignment [\#2190](https://github.com/kivy/python-for-android/issues/2190) - Numpy on MacOsX fails in our `CI` tests [\#2087](https://github.com/kivy/python-for-android/issues/2087) **Closed issues:** - pyzbar android building error [\#2635](https://github.com/kivy/python-for-android/issues/2635) - `tflite-runtime` build every time [\#2630](https://github.com/kivy/python-for-android/issues/2630) - Failed to build `matplotlib` because `kiwisolver` [\#2629](https://github.com/kivy/python-for-android/issues/2629) - Trying to build pandas with buildozer results in missing headers errors [\#2626](https://github.com/kivy/python-for-android/issues/2626) - https://github.com/kivy/python-for-android.git [\#2625](https://github.com/kivy/python-for-android/issues/2625) - \[SSL : CERTIFICATE\_VERIFY\_FAILED \] in Android [\#2620](https://github.com/kivy/python-for-android/issues/2620) - How to run Python script in background in android? [\#2618](https://github.com/kivy/python-for-android/issues/2618) - USB permission [\#2611](https://github.com/kivy/python-for-android/issues/2611) - ffmpeg recipe for 23b build fails [\#2608](https://github.com/kivy/python-for-android/issues/2608) - Broken jpeg recipe for NDK 23b? [\#2603](https://github.com/kivy/python-for-android/issues/2603) - Need a help [\#2595](https://github.com/kivy/python-for-android/issues/2595) - Termux build fails [\#2585](https://github.com/kivy/python-for-android/issues/2585) - lapack build error [\#2584](https://github.com/kivy/python-for-android/issues/2584) - still this issue is happening [\#2572](https://github.com/kivy/python-for-android/issues/2572) - "Unit test apk" + "Unit test aab" + "Test updated recipes" test jobs should be run also on macOS \(both Intel and Apple Silicon\) [\#2569](https://github.com/kivy/python-for-android/issues/2569) - unpackPyBundle\(\) on startup crashes already running service [\#2564](https://github.com/kivy/python-for-android/issues/2564) - Webview app fail to startup. [\#2559](https://github.com/kivy/python-for-android/issues/2559) - GenericNDKBuildRecipe Not compiling with android api \> 28 [\#2555](https://github.com/kivy/python-for-android/issues/2555) - Is there a way to build smaller apks? [\#2553](https://github.com/kivy/python-for-android/issues/2553) - Webview, icon [\#2552](https://github.com/kivy/python-for-android/issues/2552) - SONAME header not present in libpython3.8.so [\#2548](https://github.com/kivy/python-for-android/issues/2548) - How to mention Python modules in Kivy buildozer.spec file? [\#2547](https://github.com/kivy/python-for-android/issues/2547) - Issue with pyaudio and portaudio [\#2535](https://github.com/kivy/python-for-android/issues/2535) - \[Temporary Resolved\] Python 4 android in mac os with Apple Silicon via Roseta [\#2528](https://github.com/kivy/python-for-android/issues/2528) - Scipy is not installed due to "Error: 'numpy' must be installed before running the build." [\#2509](https://github.com/kivy/python-for-android/issues/2509) - Lapack depends on arm-linux-androideabi-gfortran [\#2508](https://github.com/kivy/python-for-android/issues/2508) - Apk file built by buildozer is large in comparison to other apks [\#2473](https://github.com/kivy/python-for-android/issues/2473) - p4a is not compatible with ndk \>= 22 [\#2391](https://github.com/kivy/python-for-android/issues/2391) - Sympy module. Error in buildozer: no module named sympy.testing [\#2381](https://github.com/kivy/python-for-android/issues/2381) - build.gradle 'compile' depreciated [\#2362](https://github.com/kivy/python-for-android/issues/2362) - API 29 support [\#2360](https://github.com/kivy/python-for-android/issues/2360) - python for android [\#2307](https://github.com/kivy/python-for-android/issues/2307) - application is not working in android made with buildozer kivy [\#2260](https://github.com/kivy/python-for-android/issues/2260) - hostpython3 unpack error [\#2247](https://github.com/kivy/python-for-android/issues/2247) - no recipe for pyaudio \_portaudio. [\#2223](https://github.com/kivy/python-for-android/issues/2223) - How to add a native Python package for kivy? [\#2089](https://github.com/kivy/python-for-android/issues/2089) - scipy module fails loading for 32 bit and 64 bit APK builds. [\#2061](https://github.com/kivy/python-for-android/issues/2061) - Support for androidx [\#2020](https://github.com/kivy/python-for-android/issues/2020) - Cannot build apk using buidozer [\#2005](https://github.com/kivy/python-for-android/issues/2005) - Android NDK - "$NDK/platforms/android-25" missing? [\#1992](https://github.com/kivy/python-for-android/issues/1992) - Tidy up NDK 19+ support [\#1962](https://github.com/kivy/python-for-android/issues/1962) - Support for NDK 19 [\#1613](https://github.com/kivy/python-for-android/issues/1613) - Android NDK 18b issues [\#1525](https://github.com/kivy/python-for-android/issues/1525) - Google requiring 64 bits binary in August 2019 [\#1519](https://github.com/kivy/python-for-android/issues/1519) - Investigate Azure Pipelines [\#1400](https://github.com/kivy/python-for-android/issues/1400) **Merged pull requests:** - Use `shutil.which` instead of `sh.which` [\#2637](https://github.com/kivy/python-for-android/pull/2637) ([misl6](https://github.com/misl6)) - add service\_lib and aar to the docs [\#2634](https://github.com/kivy/python-for-android/pull/2634) ([mzakharo](https://github.com/mzakharo)) - Fix issue \#2630 [\#2631](https://github.com/kivy/python-for-android/pull/2631) ([Neizvestnyj](https://github.com/Neizvestnyj)) - lapack/scipy: support NDK r21e, x86/64 archs [\#2619](https://github.com/kivy/python-for-android/pull/2619) ([mzakharo](https://github.com/mzakharo)) - add scipy/lapack CI tests [\#2617](https://github.com/kivy/python-for-android/pull/2617) ([mzakharo](https://github.com/mzakharo)) - use LEGACY\_NDK option to build lapack/scipy with a separate NDK [\#2615](https://github.com/kivy/python-for-android/pull/2615) ([mzakharo](https://github.com/mzakharo)) - Fixing service\_library bootstrap + .aar build. [\#2612](https://github.com/kivy/python-for-android/pull/2612) ([mzakharo](https://github.com/mzakharo)) - Bump groestlcoin\_hash to 1.0.3 [\#2607](https://github.com/kivy/python-for-android/pull/2607) ([gruve-p](https://github.com/gruve-p)) - removed `usr` and `lib` from ndk library path in `librt` recipe [\#2606](https://github.com/kivy/python-for-android/pull/2606) ([kengoon](https://github.com/kengoon)) - changed arch.ndk\_platform to arch.ndk\_lib\_dir in `librt` recipe [\#2605](https://github.com/kivy/python-for-android/pull/2605) ([kengoon](https://github.com/kengoon)) - Our self-hosted Apple Silicon runner now has been migrated to actions/runner v2.292.0 which now supports arm64 natively [\#2602](https://github.com/kivy/python-for-android/pull/2602) ([misl6](https://github.com/misl6)) - Introduces pkg\_config\_location in Prerequisite and use OpenSSLPrerequisite\(\).pkg\_config\_location in hostpython3, so we can support ssl on hostpython3 just out of the box also on macOS [\#2599](https://github.com/kivy/python-for-android/pull/2599) ([misl6](https://github.com/misl6)) - Add service to webview test app [\#2598](https://github.com/kivy/python-for-android/pull/2598) ([dbnicholson](https://github.com/dbnicholson)) - Fix webview testapp jnius usage [\#2597](https://github.com/kivy/python-for-android/pull/2597) ([dbnicholson](https://github.com/dbnicholson)) - Support multiarch in webview bootstrap [\#2596](https://github.com/kivy/python-for-android/pull/2596) ([dbnicholson](https://github.com/dbnicholson)) - Handle all the macOS prerequisites \(except NDK/SDK\) via prerequisites.py [\#2594](https://github.com/kivy/python-for-android/pull/2594) ([misl6](https://github.com/misl6)) - Prefer avdmanager from cmdline-tools [\#2593](https://github.com/kivy/python-for-android/pull/2593) ([dbnicholson](https://github.com/dbnicholson)) - \*\_rebuild\_updated\_recipes CI jobs now test the updated recipe along all the supported Android archs \(arm64-v8a, armeabi-v7a, x86\_64, x86\) [\#2592](https://github.com/kivy/python-for-android/pull/2592) ([misl6](https://github.com/misl6)) - Introduces pythonforandroid/prerequisites.py \(Experimental\). This allows a more granular check and install process for dependencies on both CI jobs and users installation. [\#2591](https://github.com/kivy/python-for-android/pull/2591) ([misl6](https://github.com/misl6)) - Added py3dns recipe [\#2590](https://github.com/kivy/python-for-android/pull/2590) ([Neizvestnyj](https://github.com/Neizvestnyj)) - Upload artifacts produced from every build platform, not only ubuntu-latest [\#2588](https://github.com/kivy/python-for-android/pull/2588) ([misl6](https://github.com/misl6)) - Fixes a typo in macos\_rebuild\_updated\_recipes [\#2587](https://github.com/kivy/python-for-android/pull/2587) ([misl6](https://github.com/misl6)) - Added pythonforandroid.androidndk.AndroidNDK + some changes needed in order to support build on Apple Silicon macs. [\#2586](https://github.com/kivy/python-for-android/pull/2586) ([misl6](https://github.com/misl6)) - Set PATH using real SDK and NDK directories [\#2583](https://github.com/kivy/python-for-android/pull/2583) ([dbnicholson](https://github.com/dbnicholson)) - Add missing fetch-depth: 0 on macos\_rebuild\_updated\_recipes [\#2579](https://github.com/kivy/python-for-android/pull/2579) ([misl6](https://github.com/misl6)) - Bumps libffi to v3.4.2 + adds -fPIC on i686-linux-android [\#2578](https://github.com/kivy/python-for-android/pull/2578) ([misl6](https://github.com/misl6)) - Bumps numpy version to 1.22.3, cython version to 0.29.28 and fixes numpy build on macOS [\#2575](https://github.com/kivy/python-for-android/pull/2575) ([misl6](https://github.com/misl6)) - macOS CI: ADD APK, AAB & Updated Recipes build [\#2574](https://github.com/kivy/python-for-android/pull/2574) ([misl6](https://github.com/misl6)) - add version check to unpackPyBundle [\#2565](https://github.com/kivy/python-for-android/pull/2565) ([mzakharo](https://github.com/mzakharo)) - Merges master into develop after release 2022.03.13 [\#2562](https://github.com/kivy/python-for-android/pull/2562) ([misl6](https://github.com/misl6)) - Fixes App Icon and Presplash\_Screen For Webview bootstrap [\#2556](https://github.com/kivy/python-for-android/pull/2556) ([kengoon](https://github.com/kengoon)) - NDK 23 + Gradle 7 support [\#2550](https://github.com/kivy/python-for-android/pull/2550) ([misl6](https://github.com/misl6)) ## [v2022.03.13](https://github.com/kivy/python-for-android/tree/v2022.03.13) (2022-03-13) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2021.09.05...v2022.03.13) **Closed issues:** - ModuleNotFoundError: No module named 'msvcrt' [\#2536](https://github.com/kivy/python-for-android/issues/2536) - Pyarrow module do not working [\#2531](https://github.com/kivy/python-for-android/issues/2531) - Error when building Android Application with Google modules [\#2530](https://github.com/kivy/python-for-android/issues/2530) - arm64-v8a \(apk and aab lib\) crashes [\#2524](https://github.com/kivy/python-for-android/issues/2524) - Python for android [\#2521](https://github.com/kivy/python-for-android/issues/2521) - ValueError: name is too long [\#2517](https://github.com/kivy/python-for-android/issues/2517) - With the target API 31, I got an error on Android 12 phone and cannot install it. [\#2511](https://github.com/kivy/python-for-android/issues/2511) - How to get libnumpy.so & numpy py libs [\#2510](https://github.com/kivy/python-for-android/issues/2510) - pydantic compiling in Buildozer with 'crypt.h' file not found [\#2507](https://github.com/kivy/python-for-android/issues/2507) - p4a built x86\_64 library\(psutil, "ELF 64-bit LSB shared object, x86-64"\) for ARM [\#2506](https://github.com/kivy/python-for-android/issues/2506) - matplotlib's recipe problem [\#2502](https://github.com/kivy/python-for-android/issues/2502) - cffi's recipe problem in aab generation [\#2501](https://github.com/kivy/python-for-android/issues/2501) - Pillow's recipe problem in aab generation [\#2497](https://github.com/kivy/python-for-android/issues/2497) - Python compilation error: LXMLRecipe' object has no attribute 'ndk\_include\_dir' [\#2493](https://github.com/kivy/python-for-android/issues/2493) - Android App crashing at launch - SDL seems crashing [\#2491](https://github.com/kivy/python-for-android/issues/2491) - build an android app with ffpyplayer [\#2453](https://github.com/kivy/python-for-android/issues/2453) - How to change "PythonActivity.java" to load mp4 file on presplash [\#2439](https://github.com/kivy/python-for-android/issues/2439) - Kivy app krashes on android 10 [\#2434](https://github.com/kivy/python-for-android/issues/2434) - Kivy app crashing on android after installation, what is the issue here? [\#2433](https://github.com/kivy/python-for-android/issues/2433) - Build failed [\#2366](https://github.com/kivy/python-for-android/issues/2366) - Reportlab bitbucket link is not working anymore [\#2310](https://github.com/kivy/python-for-android/issues/2310) - Need to be able to create App Bundles \(.aab\) files in addition to APK files [\#2084](https://github.com/kivy/python-for-android/issues/2084) - What exactly is mangling p4a's output with `[lots of missing output]... (and X more)`? [\#1939](https://github.com/kivy/python-for-android/issues/1939) - Unable to compile. [\#1710](https://github.com/kivy/python-for-android/issues/1710) **Merged pull requests:** - Upgrading the flask version to avoid exception at the start of webview application [\#2560](https://github.com/kivy/python-for-android/pull/2560) ([Prashanth-BC](https://github.com/Prashanth-BC)) - add recipe for freetype-py to not include the prebuilt .so for the wr… [\#2558](https://github.com/kivy/python-for-android/pull/2558) ([domedave](https://github.com/domedave)) - Update to Kivy 2.1.0 [\#2557](https://github.com/kivy/python-for-android/pull/2557) ([RobertFlatt](https://github.com/RobertFlatt)) - tflite-runtime recipe [\#2554](https://github.com/kivy/python-for-android/pull/2554) ([RobertFlatt](https://github.com/RobertFlatt)) - Update AndroidManifest.tmpl.xml [\#2551](https://github.com/kivy/python-for-android/pull/2551) ([drahba](https://github.com/drahba)) - Update recipe.py \(\#2544\) [\#2546](https://github.com/kivy/python-for-android/pull/2546) ([misl6](https://github.com/misl6)) - Update python versions matrix on CI [\#2534](https://github.com/kivy/python-for-android/pull/2534) ([misl6](https://github.com/misl6)) - Add ifaddr recipe [\#2527](https://github.com/kivy/python-for-android/pull/2527) ([syrykh](https://github.com/syrykh)) - Remove websocket-client recipe [\#2526](https://github.com/kivy/python-for-android/pull/2526) ([syrykh](https://github.com/syrykh)) - Fix build [\#2525](https://github.com/kivy/python-for-android/pull/2525) ([correa](https://github.com/correa)) - Add aaptOptions noCompress [\#2523](https://github.com/kivy/python-for-android/pull/2523) ([RobertFlatt](https://github.com/RobertFlatt)) - Updated version of pygame from 2.0.1 to 2.1.0 [\#2520](https://github.com/kivy/python-for-android/pull/2520) ([CAPTAIN1947](https://github.com/CAPTAIN1947)) - Bump Pillow version to 8.4.0 [\#2516](https://github.com/kivy/python-for-android/pull/2516) ([misl6](https://github.com/misl6)) - Moved support-request to v2. v1 has been shut down. [\#2515](https://github.com/kivy/python-for-android/pull/2515) ([misl6](https://github.com/misl6)) - Add support-requests configuration. [\#2514](https://github.com/kivy/python-for-android/pull/2514) ([misl6](https://github.com/misl6)) - proper --host for libsecp256k1, libogg, libvorbis, libcurl [\#2512](https://github.com/kivy/python-for-android/pull/2512) ([accumulator](https://github.com/accumulator)) - Fix broken Contribute link [\#2505](https://github.com/kivy/python-for-android/pull/2505) ([daMatz](https://github.com/daMatz)) - Makes pep8 happy on sdl2 recipe [\#2504](https://github.com/kivy/python-for-android/pull/2504) ([misl6](https://github.com/misl6)) - Fix broken recipes with missing arch.arch in get\_site\_packages\_dir [\#2503](https://github.com/kivy/python-for-android/pull/2503) ([misl6](https://github.com/misl6)) - added android permission ACCESS\_BACKGROUND\_LOCATION [\#2500](https://github.com/kivy/python-for-android/pull/2500) ([xloem](https://github.com/xloem)) - GitHub Actions: Fixes wrong actions/checkout@v2 usage [\#2496](https://github.com/kivy/python-for-android/pull/2496) ([misl6](https://github.com/misl6)) - Fixes ndk\_include\_dir on lxml recipe. [\#2495](https://github.com/kivy/python-for-android/pull/2495) ([misl6](https://github.com/misl6)) - Move coveralls to github actions [\#2490](https://github.com/kivy/python-for-android/pull/2490) ([misl6](https://github.com/misl6)) - Master [\#2489](https://github.com/kivy/python-for-android/pull/2489) ([misl6](https://github.com/misl6)) - Add should\_build method to sdl2 recipe [\#2482](https://github.com/kivy/python-for-android/pull/2482) ([AndyRusso](https://github.com/AndyRusso)) - AAB support related changes [\#2467](https://github.com/kivy/python-for-android/pull/2467) ([misl6](https://github.com/misl6)) - services: fix START\_STICKY [\#2401](https://github.com/kivy/python-for-android/pull/2401) ([mzakharo](https://github.com/mzakharo)) ## [v2021.09.05](https://github.com/kivy/python-for-android/tree/v2021.09.05) (2021-09-05) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2020.06.02...v2021.09.05) **Fixed bugs:** - Execution failed for task ':compileDebugJavaWithJavac'. "DialogInterface", "AlertDialog" and "KeyEvent" not found. [\#2228](https://github.com/kivy/python-for-android/issues/2228) **Closed issues:** - Global variable/objetct isn't being shared between multiple files on Android, while the same code works normally under Windows. [\#2485](https://github.com/kivy/python-for-android/issues/2485) - audiostream recipe does not copy .java files, leading to a crash when trying to open mic on android [\#2483](https://github.com/kivy/python-for-android/issues/2483) - Applying patches for hostpython3 fails [\#2476](https://github.com/kivy/python-for-android/issues/2476) - p4a failing to build webapp [\#2475](https://github.com/kivy/python-for-android/issues/2475) - Failed to build with recipe MySQLdb \(setuptools,libmysqlclient\) [\#2474](https://github.com/kivy/python-for-android/issues/2474) - ctypes.util.find\_library 64-bit error [\#2468](https://github.com/kivy/python-for-android/issues/2468) - Android x86\_64 instant crash with hello world app, missing zlib [\#2460](https://github.com/kivy/python-for-android/issues/2460) - pycrypto recipe is arm7 only [\#2457](https://github.com/kivy/python-for-android/issues/2457) - lazy loading of recycleview and wa [\#2454](https://github.com/kivy/python-for-android/issues/2454) - a kivy app can not send request to any website in phone [\#2450](https://github.com/kivy/python-for-android/issues/2450) - Android permissions not working on Android 10 [\#2444](https://github.com/kivy/python-for-android/issues/2444) - Feature request for upcoming fushia OS [\#2442](https://github.com/kivy/python-for-android/issues/2442) - where is 'main.py' ? [\#2441](https://github.com/kivy/python-for-android/issues/2441) - "diff" files are ignored during "pip install ." [\#2435](https://github.com/kivy/python-for-android/issues/2435) - Travis CI misconfigured? [\#2428](https://github.com/kivy/python-for-android/issues/2428) - NumPy | ImportError: dlopen failed: cannot locate symbol "log10f" referenced by "\_multiarray\_tests.so" [\#2426](https://github.com/kivy/python-for-android/issues/2426) - Pandas recipe doesn't work [\#2425](https://github.com/kivy/python-for-android/issues/2425) - Error \(Downloading matplotlib from https://jqueryui.com/resources/download/jquery-ui-1.12.1.zip\) [\#2418](https://github.com/kivy/python-for-android/issues/2418) - buildozer failed -\_- [\#2417](https://github.com/kivy/python-for-android/issues/2417) - "Python for android ended." When printing \x00 [\#2415](https://github.com/kivy/python-for-android/issues/2415) - Does Kivy supports SDK 30? [\#2411](https://github.com/kivy/python-for-android/issues/2411) - Pymunk,kivy apk crashing on Android 5.1 [\#2388](https://github.com/kivy/python-for-android/issues/2388) - Select specific version of python [\#2373](https://github.com/kivy/python-for-android/issues/2373) - MDRaisedButton doesn't work on android [\#2371](https://github.com/kivy/python-for-android/issues/2371) - Add Python 3.9 support as it builds fine [\#2365](https://github.com/kivy/python-for-android/issues/2365) - Gradle Problem [\#2352](https://github.com/kivy/python-for-android/issues/2352) - service\_library lacks mActivity support [\#2350](https://github.com/kivy/python-for-android/issues/2350) - Possible bug in the "opencv\_extras" recipe [\#2349](https://github.com/kivy/python-for-android/issues/2349) - Pygame recipe \(sdl confusion\) [\#2342](https://github.com/kivy/python-for-android/issues/2342) - Youtube-dl [\#2336](https://github.com/kivy/python-for-android/issues/2336) - building apk using pygame [\#2333](https://github.com/kivy/python-for-android/issues/2333) - Buildozer ValueError: specify a path with --storage-dir [\#2331](https://github.com/kivy/python-for-android/issues/2331) - Webview app crashes when trying to request permissions [\#2325](https://github.com/kivy/python-for-android/issues/2325) - ffmpeg requirement causes apk crash on android [\#2324](https://github.com/kivy/python-for-android/issues/2324) - ModuleNotFoundError: No module named 'fcntl' [\#2321](https://github.com/kivy/python-for-android/issues/2321) - error python-for-android: git clone [\#2319](https://github.com/kivy/python-for-android/issues/2319) - Pillow with kivy and kivymd error ImportError: dlopen failed: cannot locate symbol "log" referenced by "\_imaging.so"... [\#2317](https://github.com/kivy/python-for-android/issues/2317) - pyconfig\_detection.patch breaks \(at least\) --use-setup-py [\#2313](https://github.com/kivy/python-for-android/issues/2313) - Cython is called in incompatible way, not all options are supported \(especially -I\) [\#2311](https://github.com/kivy/python-for-android/issues/2311) - PR 2285 breaks requestPermissions [\#2304](https://github.com/kivy/python-for-android/issues/2304) - Python services [\#2297](https://github.com/kivy/python-for-android/issues/2297) - Integrating ads in P4A [\#2295](https://github.com/kivy/python-for-android/issues/2295) - How to hiding Android keyboard in kivy? [\#2268](https://github.com/kivy/python-for-android/issues/2268) - KivyMD \#257 Bug vs MatPlotLib, one or the other??? [\#2262](https://github.com/kivy/python-for-android/issues/2262) - MatPlotLib not building with Buildozer \(but it did 2 months ago\) [\#2261](https://github.com/kivy/python-for-android/issues/2261) - How to add webp support to pillow? [\#2254](https://github.com/kivy/python-for-android/issues/2254) - Unable to run executable on Android [\#2251](https://github.com/kivy/python-for-android/issues/2251) - Provide a native service option [\#2243](https://github.com/kivy/python-for-android/issues/2243) - the kivy app crash in time of import psycopg2 [\#2241](https://github.com/kivy/python-for-android/issues/2241) - How can we make a camera App with zoom by Kivy [\#2238](https://github.com/kivy/python-for-android/issues/2238) - Building pycrypto for arm64-v8a fails [\#2230](https://github.com/kivy/python-for-android/issues/2230) - buildozer error Building pycrypto for arm64-v8a and x86\_64 [\#2216](https://github.com/kivy/python-for-android/issues/2216) - Does the opencv recipe for buildozer not include the extra face class? [\#2166](https://github.com/kivy/python-for-android/issues/2166) - Crash at relaunch in pythonlib in SDL2Thread [\#1787](https://github.com/kivy/python-for-android/issues/1787) - Background Service is killed when app is swiped \(close by user\) [\#1783](https://github.com/kivy/python-for-android/issues/1783) - tarfile failure with long user ID [\#1013](https://github.com/kivy/python-for-android/issues/1013) - Apps are too big \(in size\) [\#202](https://github.com/kivy/python-for-android/issues/202) **Merged pull requests:** - Drop travis-ci.org and use github-actions for pypi release [\#2487](https://github.com/kivy/python-for-android/pull/2487) ([misl6](https://github.com/misl6)) - Upgrade grunt version re: CVE-2020-7729 [\#2484](https://github.com/kivy/python-for-android/pull/2484) ([Zen-CODE](https://github.com/Zen-CODE)) - Recipe for pydantic [\#2480](https://github.com/kivy/python-for-android/pull/2480) ([FilipeMarch](https://github.com/FilipeMarch)) - fix pandas recipe [\#2478](https://github.com/kivy/python-for-android/pull/2478) ([mzakharo](https://github.com/mzakharo)) - Add \*.diff to manifest and package\_data [\#2471](https://github.com/kivy/python-for-android/pull/2471) ([viblo](https://github.com/viblo)) - Fix bad library found by ctypes for 64-bit arch \(\#2468\) [\#2469](https://github.com/kivy/python-for-android/pull/2469) ([macdems](https://github.com/macdems)) - Updated version of pygame from 2.0.0-dev7 to 2.0.1 [\#2466](https://github.com/kivy/python-for-android/pull/2466) ([ljnath](https://github.com/ljnath)) - Adds libm to Pillow recipe [\#2465](https://github.com/kivy/python-for-android/pull/2465) ([Sahil-pixel](https://github.com/Sahil-pixel)) - Add missing mipmap directories to bootstraps. [\#2463](https://github.com/kivy/python-for-android/pull/2463) ([rnixx](https://github.com/rnixx)) - Move PythonActivityUtil.unpackData to PythonUtil.unpackData [\#2462](https://github.com/kivy/python-for-android/pull/2462) ([rnixx](https://github.com/rnixx)) - Websocket-client updated to 1.0.1 from 0.40.0 [\#2458](https://github.com/kivy/python-for-android/pull/2458) ([akshayaurora](https://github.com/akshayaurora)) - fixed redirection for download liblzma [\#2452](https://github.com/kivy/python-for-android/pull/2452) ([td1803](https://github.com/td1803)) - update \(host\)python3 to 3.8.9 [\#2451](https://github.com/kivy/python-for-android/pull/2451) ([obfusk](https://github.com/obfusk)) - update sqlite3 [\#2449](https://github.com/kivy/python-for-android/pull/2449) ([obfusk](https://github.com/obfusk)) - build.py: also clean\(\) tarfile directory entries [\#2447](https://github.com/kivy/python-for-android/pull/2447) ([obfusk](https://github.com/obfusk)) - android: add support for adaptive icon/launcher [\#2446](https://github.com/kivy/python-for-android/pull/2446) ([SomberNight](https://github.com/SomberNight)) - Fix ImportError bug: cannot locate symbol "modf" [\#2445](https://github.com/kivy/python-for-android/pull/2445) ([Neizvestnyj](https://github.com/Neizvestnyj)) - update openssl [\#2443](https://github.com/kivy/python-for-android/pull/2443) ([obfusk](https://github.com/obfusk)) - Add libvpx recipe, reference it in ffpyplayer\_codecs and ffmpeg [\#2440](https://github.com/kivy/python-for-android/pull/2440) ([Cheaterman](https://github.com/Cheaterman)) - Add setuptools dependency for Groestlcoin\_hash recipe [\#2438](https://github.com/kivy/python-for-android/pull/2438) ([gruve-p](https://github.com/gruve-p)) - set urllib user-agent [\#2437](https://github.com/kivy/python-for-android/pull/2437) ([obfusk](https://github.com/obfusk)) - setup.py: add \*.diff to package\_data [\#2436](https://github.com/kivy/python-for-android/pull/2436) ([obfusk](https://github.com/obfusk)) - Reintroduce documentation of android module [\#2432](https://github.com/kivy/python-for-android/pull/2432) ([tito](https://github.com/tito)) - Update \_\_init\_\_.py [\#2429](https://github.com/kivy/python-for-android/pull/2429) ([Neizvestnyj](https://github.com/Neizvestnyj)) - webview: flush cookies & improve shouldOverrideUrlLoading [\#2424](https://github.com/kivy/python-for-android/pull/2424) ([obfusk](https://github.com/obfusk)) - travis: update pip \(fixes CI\) [\#2422](https://github.com/kivy/python-for-android/pull/2422) ([obfusk](https://github.com/obfusk)) - update openssl [\#2421](https://github.com/kivy/python-for-android/pull/2421) ([obfusk](https://github.com/obfusk)) - Avoids build error for opencv and bumps version to 4.5.1 [\#2419](https://github.com/kivy/python-for-android/pull/2419) ([thescheff](https://github.com/thescheff)) - strip null bytes in call to androidembed.log\(\) [\#2416](https://github.com/kivy/python-for-android/pull/2416) ([obfusk](https://github.com/obfusk)) - webview: put webview\_includes in assets dir [\#2412](https://github.com/kivy/python-for-android/pull/2412) ([obfusk](https://github.com/obfusk)) - update setuptools [\#2409](https://github.com/kivy/python-for-android/pull/2409) ([obfusk](https://github.com/obfusk)) - update sqlite3 [\#2408](https://github.com/kivy/python-for-android/pull/2408) ([obfusk](https://github.com/obfusk)) - Update quickstart.rst macOS brew cask command [\#2407](https://github.com/kivy/python-for-android/pull/2407) ([yestoday-tv](https://github.com/yestoday-tv)) - Added ability to set input type on android [\#2405](https://github.com/kivy/python-for-android/pull/2405) ([dwmoffatt](https://github.com/dwmoffatt)) - PyZQM recipe needs setuptools, list it explicitly in deps [\#2404](https://github.com/kivy/python-for-android/pull/2404) ([rambo](https://github.com/rambo)) - recipes: print recipe ENV on failure [\#2403](https://github.com/kivy/python-for-android/pull/2403) ([mzakharo](https://github.com/mzakharo)) - recipes: scipy - fix build for armeabi-v7a [\#2402](https://github.com/kivy/python-for-android/pull/2402) ([mzakharo](https://github.com/mzakharo)) - don't run git pull when not on a branch [\#2400](https://github.com/kivy/python-for-android/pull/2400) ([obfusk](https://github.com/obfusk)) - Fix Pymunk crash on older versions of Android [\#2399](https://github.com/kivy/python-for-android/pull/2399) ([viblo](https://github.com/viblo)) - Recipe for argon2-cffi [\#2398](https://github.com/kivy/python-for-android/pull/2398) ([Arjun-Somvanshi](https://github.com/Arjun-Somvanshi)) - update sqlite3 to 3.34.0 [\#2397](https://github.com/kivy/python-for-android/pull/2397) ([obfusk](https://github.com/obfusk)) - update openssl to 1.1.1i [\#2396](https://github.com/kivy/python-for-android/pull/2396) ([obfusk](https://github.com/obfusk)) - support Python 3.9 [\#2394](https://github.com/kivy/python-for-android/pull/2394) ([obfusk](https://github.com/obfusk)) - reproducible builds [\#2390](https://github.com/kivy/python-for-android/pull/2390) ([obfusk](https://github.com/obfusk)) - Update Pymunk recipe to 6.0.0 [\#2389](https://github.com/kivy/python-for-android/pull/2389) ([viblo](https://github.com/viblo)) - webview: add mOpenExternalLinksInBrowser field [\#2387](https://github.com/kivy/python-for-android/pull/2387) ([obfusk](https://github.com/obfusk)) - webview: add enableZoom\(\) method [\#2386](https://github.com/kivy/python-for-android/pull/2386) ([obfusk](https://github.com/obfusk)) - Enable AndroidX [\#2385](https://github.com/kivy/python-for-android/pull/2385) ([RobertFlatt](https://github.com/RobertFlatt)) - :arrow\_up: Updates to Kivy2 [\#2384](https://github.com/kivy/python-for-android/pull/2384) ([kuzeyron](https://github.com/kuzeyron)) - fix travis [\#2383](https://github.com/kivy/python-for-android/pull/2383) ([obfusk](https://github.com/obfusk)) - :bug: Fixes pip command on Travis and bumps actions/setup-python [\#2382](https://github.com/kivy/python-for-android/pull/2382) ([AndreMiras](https://github.com/AndreMiras)) - docs: fix simple typo, packaged -\> packaged [\#2377](https://github.com/kivy/python-for-android/pull/2377) ([timgates42](https://github.com/timgates42)) - Fix master only merges [\#2376](https://github.com/kivy/python-for-android/pull/2376) ([inclement](https://github.com/inclement)) - Audiostream Fix [\#2375](https://github.com/kivy/python-for-android/pull/2375) ([xloem](https://github.com/xloem)) - Add service information for buildozer.spec [\#2372](https://github.com/kivy/python-for-android/pull/2372) ([xloem](https://github.com/xloem)) - recipes: add scipy support [\#2370](https://github.com/kivy/python-for-android/pull/2370) ([mzakharo](https://github.com/mzakharo)) - fix CI [\#2368](https://github.com/kivy/python-for-android/pull/2368) ([obfusk](https://github.com/obfusk)) - support activity\_launch\_mode in webview bootstrap [\#2367](https://github.com/kivy/python-for-android/pull/2367) ([obfusk](https://github.com/obfusk)) - Support running tests on any arch [\#2355](https://github.com/kivy/python-for-android/pull/2355) ([jayvdb](https://github.com/jayvdb)) - setup.py: Fix dependency syntax [\#2354](https://github.com/kivy/python-for-android/pull/2354) ([jayvdb](https://github.com/jayvdb)) - added missing digest support to recipes [\#2351](https://github.com/kivy/python-for-android/pull/2351) ([fuzzyTew](https://github.com/fuzzyTew)) - android: call non-static methods on .mActivity [\#2341](https://github.com/kivy/python-for-android/pull/2341) ([obfusk](https://github.com/obfusk)) - fix webview jni [\#2340](https://github.com/kivy/python-for-android/pull/2340) ([obfusk](https://github.com/obfusk)) - missing mActivity [\#2339](https://github.com/kivy/python-for-android/pull/2339) ([zworkb](https://github.com/zworkb)) - added few input parameters to make possible to use custom java classes and tweak AndroidManifest.xml [\#2338](https://github.com/kivy/python-for-android/pull/2338) ([vesellov](https://github.com/vesellov)) - ffmpeg and ffpyplayer improvements [\#2335](https://github.com/kivy/python-for-android/pull/2335) ([rnixx](https://github.com/rnixx)) - Add recipe for https://docs.aiohttp.org/en/stable/ [\#2332](https://github.com/kivy/python-for-android/pull/2332) ([rambo](https://github.com/rambo)) - Update \_\_init\_\_.py for Report Lab recipe [\#2323](https://github.com/kivy/python-for-android/pull/2323) ([marcets](https://github.com/marcets)) - Print patched message to stderr [\#2314](https://github.com/kivy/python-for-android/pull/2314) ([rambo](https://github.com/rambo)) - Call cython via the setuptools entrypoint, fixes \#2311 [\#2312](https://github.com/kivy/python-for-android/pull/2312) ([rambo](https://github.com/rambo)) - Allow using background color with lottie splashscreen [\#2305](https://github.com/kivy/python-for-android/pull/2305) ([tshirtman](https://github.com/tshirtman)) - Add manifestPlaceholders [\#2301](https://github.com/kivy/python-for-android/pull/2301) ([misl6](https://github.com/misl6)) - Allow using lottie files for splashscreen \(SDL2 bootstrap\) [\#2296](https://github.com/kivy/python-for-android/pull/2296) ([tshirtman](https://github.com/tshirtman)) - use https download for boost & zope [\#2293](https://github.com/kivy/python-for-android/pull/2293) ([obfusk](https://github.com/obfusk)) - \(host\)python3: rm version check for pyconfig patch [\#2292](https://github.com/kivy/python-for-android/pull/2292) ([obfusk](https://github.com/obfusk)) - download\_file: show error + exponential sleep [\#2291](https://github.com/kivy/python-for-android/pull/2291) ([obfusk](https://github.com/obfusk)) - remove cruft from webview\_includes/\_load.html [\#2289](https://github.com/kivy/python-for-android/pull/2289) ([obfusk](https://github.com/obfusk)) - tiny whitespace fix in buildoptions.rst [\#2288](https://github.com/kivy/python-for-android/pull/2288) ([obfusk](https://github.com/obfusk)) - update libffi to 3.3 [\#2287](https://github.com/kivy/python-for-android/pull/2287) ([obfusk](https://github.com/obfusk)) - update openssl to 1.1.1g [\#2286](https://github.com/kivy/python-for-android/pull/2286) ([obfusk](https://github.com/obfusk)) - update pyjnius to 1.3.0 [\#2285](https://github.com/kivy/python-for-android/pull/2285) ([obfusk](https://github.com/obfusk)) - update setuptools to 49.2.1 [\#2284](https://github.com/kivy/python-for-android/pull/2284) ([obfusk](https://github.com/obfusk)) - update six to 1.15.0 [\#2283](https://github.com/kivy/python-for-android/pull/2283) ([obfusk](https://github.com/obfusk)) - update flask to 1.1.2 [\#2282](https://github.com/kivy/python-for-android/pull/2282) ([obfusk](https://github.com/obfusk)) - update python3 & hostpython3 to 3.8.5 [\#2281](https://github.com/kivy/python-for-android/pull/2281) ([obfusk](https://github.com/obfusk)) - update sqlite3 to 3.32.3 [\#2280](https://github.com/kivy/python-for-android/pull/2280) ([obfusk](https://github.com/obfusk)) - fix travis [\#2279](https://github.com/kivy/python-for-android/pull/2279) ([obfusk](https://github.com/obfusk)) - `libpython3.8m.so` should not have `m` suffix [\#2278](https://github.com/kivy/python-for-android/pull/2278) ([davidhewitt](https://github.com/davidhewitt)) - add recipe for libpcre [\#2276](https://github.com/kivy/python-for-android/pull/2276) ([obfusk](https://github.com/obfusk)) - build python3 with loadable-sqlite-extensions [\#2275](https://github.com/kivy/python-for-android/pull/2275) ([obfusk](https://github.com/obfusk)) - fix indentation [\#2273](https://github.com/kivy/python-for-android/pull/2273) ([obfusk](https://github.com/obfusk)) - LogFile: rename .buffer \(to fix newer flask/click\) [\#2264](https://github.com/kivy/python-for-android/pull/2264) ([obfusk](https://github.com/obfusk)) - Add Recipe for Kivy3 module [\#2263](https://github.com/kivy/python-for-android/pull/2263) ([excepterror](https://github.com/excepterror)) - :sparkles: Rework of Pillow recipe adding WebP support [\#2256](https://github.com/kivy/python-for-android/pull/2256) ([opacam](https://github.com/opacam)) - :sparkles: Add libwebp recipe [\#2255](https://github.com/kivy/python-for-android/pull/2255) ([opacam](https://github.com/opacam)) - added --activity-class-name and --activity-package parameters [\#2248](https://github.com/kivy/python-for-android/pull/2248) ([vesellov](https://github.com/vesellov)) - Fix runtime psycopg2 error [\#2246](https://github.com/kivy/python-for-android/pull/2246) ([Progdrasil](https://github.com/Progdrasil)) - Bump libpq version [\#2245](https://github.com/kivy/python-for-android/pull/2245) ([Progdrasil](https://github.com/Progdrasil)) - Support for native services [\#2244](https://github.com/kivy/python-for-android/pull/2244) ([lerela](https://github.com/lerela)) - Added missing semicolon on service-only bootstrap [\#2236](https://github.com/kivy/python-for-android/pull/2236) ([Swpolo](https://github.com/Swpolo)) - :green\_heart: Fixes Travis build post OpenJDK bump [\#2235](https://github.com/kivy/python-for-android/pull/2235) ([AndreMiras](https://github.com/AndreMiras)) - Updated gradle plugin version [\#2234](https://github.com/kivy/python-for-android/pull/2234) ([shashi278](https://github.com/shashi278)) - :bug: Fixes service\_only and webview symbol errors, closes \#2228 [\#2233](https://github.com/kivy/python-for-android/pull/2233) ([AndreMiras](https://github.com/AndreMiras)) - :bento: Add CHANGELOG.md [\#2232](https://github.com/kivy/python-for-android/pull/2232) ([opacam](https://github.com/opacam)) - :arrow\_up: Bumps to OpenJDK 13 [\#2231](https://github.com/kivy/python-for-android/pull/2231) ([AndreMiras](https://github.com/AndreMiras)) - Fixed KeyError not found [\#2229](https://github.com/kivy/python-for-android/pull/2229) ([sak96](https://github.com/sak96)) - :rotating\_light: Depreciation warning fixes [\#2227](https://github.com/kivy/python-for-android/pull/2227) ([AndreMiras](https://github.com/AndreMiras)) - Master [\#2226](https://github.com/kivy/python-for-android/pull/2226) ([AndreMiras](https://github.com/AndreMiras)) - Add possibility to add a backup rules xml file [\#2208](https://github.com/kivy/python-for-android/pull/2208) ([tcdude](https://github.com/tcdude)) ## [v2020.06.02](https://github.com/kivy/python-for-android/tree/v2020.06.02) (2020-06-02) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2020.04.29...v2020.06.02) **Fixed bugs:** - Issues introduced by PR \#2113 \(SDL2\) [\#2169](https://github.com/kivy/python-for-android/issues/2169) - App exists immediately when importing kivy.core.window.Window [\#2167](https://github.com/kivy/python-for-android/issues/2167) **Closed issues:** - Python [\#2214](https://github.com/kivy/python-for-android/issues/2214) - build failed. [\#2212](https://github.com/kivy/python-for-android/issues/2212) - apk size growing [\#2207](https://github.com/kivy/python-for-android/issues/2207) - My despair at trying to simply import the opencv face class \(python-for-android\) [\#2206](https://github.com/kivy/python-for-android/issues/2206) - running python 3.6 [\#2204](https://github.com/kivy/python-for-android/issues/2204) - specify python version [\#2203](https://github.com/kivy/python-for-android/issues/2203) - p4a gets stuck at downloading setuptools [\#2199](https://github.com/kivy/python-for-android/issues/2199) - Kivy app crashes after asking for permission [\#2054](https://github.com/kivy/python-for-android/issues/2054) **Merged pull requests:** - Release 2020.06.02 [\#2225](https://github.com/kivy/python-for-android/pull/2225) ([AndreMiras](https://github.com/AndreMiras)) - :arrow_up: Bumps to Gradle 6.4.1 [\#2222](https://github.com/kivy/python-for-android/pull/2222) ([AndreMiras](https://github.com/AndreMiras)) - :bug: Adds missing requests sub dependencies [\#2221](https://github.com/kivy/python-for-android/pull/2221) ([AndreMiras](https://github.com/AndreMiras)) - :arrow_up: Bumps to Cython==0.29.19 [\#2220](https://github.com/kivy/python-for-android/pull/2220) ([AndreMiras](https://github.com/AndreMiras)) - :pencil: Updates install and troubleshooting docs [\#2219](https://github.com/kivy/python-for-android/pull/2219) ([AndreMiras](https://github.com/AndreMiras)) - :arrow_up: Bumps to Ubuntu 20.04 [\#2218](https://github.com/kivy/python-for-android/pull/2218) ([AndreMiras](https://github.com/AndreMiras)) - :pencil: Attempt to improve the issue template [\#2217](https://github.com/kivy/python-for-android/pull/2217) ([AndreMiras](https://github.com/AndreMiras)) - :package: Split logic for build modes & debug symbols [\#2213](https://github.com/kivy/python-for-android/pull/2213) ([opacam](https://github.com/opacam)) - :sparkles: Add `opencv_extras` recipe [\#2209](https://github.com/kivy/python-for-android/pull/2209) ([opacam](https://github.com/opacam)) - :books: Troubleshoot SSL error [\#2205](https://github.com/kivy/python-for-android/pull/2205) ([AndreMiras](https://github.com/AndreMiras)) - Remove superfluous recipes fixes \#1387 [\#2202](https://github.com/kivy/python-for-android/pull/2202) ([AndreMiras](https://github.com/AndreMiras)) - :white_check_mark: Add tests for hostpython3 recipe [\#2196](https://github.com/kivy/python-for-android/pull/2196) ([opacam](https://github.com/opacam)) - Fix for 'ImportError: No module named setuptools', encountered when building with buildozer [\#2195](https://github.com/kivy/python-for-android/pull/2195) ([atom2626](https://github.com/atom2626)) - :pencil2: Rename `Hostpython3Recipe` class to camel case [\#2194](https://github.com/kivy/python-for-android/pull/2194) ([opacam](https://github.com/opacam)) - :ambulance: Fix `test_should_build` [\#2193](https://github.com/kivy/python-for-android/pull/2193) ([opacam](https://github.com/opacam)) - :white_check_mark: Add initial tests for python3 recipe [\#2192](https://github.com/kivy/python-for-android/pull/2192) ([opacam](https://github.com/opacam)) - :bug: Fixes flake8 errors post update [\#2191](https://github.com/kivy/python-for-android/pull/2191) ([AndreMiras](https://github.com/AndreMiras)) - PythonActivityUtil helper for unpacking data [\#2189](https://github.com/kivy/python-for-android/pull/2189) ([AndreMiras](https://github.com/AndreMiras)) - Share PythonUtil.java between bootstraps [\#2188](https://github.com/kivy/python-for-android/pull/2188) ([AndreMiras](https://github.com/AndreMiras)) - Java code linting using PMD 6.23.0 [\#2187](https://github.com/kivy/python-for-android/pull/2187) ([AndreMiras](https://github.com/AndreMiras)) - Deletes deprecated renpy Python{Activity,Service}.java [\#2186](https://github.com/kivy/python-for-android/pull/2186) ([AndreMiras](https://github.com/AndreMiras)) - Removes java concurrency/ folder [\#2185](https://github.com/kivy/python-for-android/pull/2185) ([AndreMiras](https://github.com/AndreMiras)) - Moves kamranzafar/ java directory to common/ [\#2184](https://github.com/kivy/python-for-android/pull/2184) ([AndreMiras](https://github.com/AndreMiras)) - Use common Hardware.java [\#2183](https://github.com/kivy/python-for-android/pull/2183) ([AndreMiras](https://github.com/AndreMiras)) - Reuse common AssetExtract.java [\#2182](https://github.com/kivy/python-for-android/pull/2182) ([AndreMiras](https://github.com/AndreMiras)) - Fixes service only unittest loading [\#2181](https://github.com/kivy/python-for-android/pull/2181) ([AndreMiras](https://github.com/AndreMiras)) - Downgrades to SDL2 2.0.9 [\#2180](https://github.com/kivy/python-for-android/pull/2180) ([AndreMiras](https://github.com/AndreMiras)) - Narrows some context manager scopes [\#2179](https://github.com/kivy/python-for-android/pull/2179) ([AndreMiras](https://github.com/AndreMiras)) - Updates release documentation [\#2177](https://github.com/kivy/python-for-android/pull/2177) ([AndreMiras](https://github.com/AndreMiras)) - Post release version bump 2020.04.29.dev0 [\#2176](https://github.com/kivy/python-for-android/pull/2176) ([AndreMiras](https://github.com/AndreMiras)) - Updates version number to 2020.04.29 [\#2175](https://github.com/kivy/python-for-android/pull/2175) ([AndreMiras](https://github.com/AndreMiras)) - Adds macOS install instructions [\#2165](https://github.com/kivy/python-for-android/pull/2165) ([AndreMiras](https://github.com/AndreMiras)) - Adds pygame recipe [\#2164](https://github.com/kivy/python-for-android/pull/2164) ([AndreMiras](https://github.com/AndreMiras)) - Removed python2 support mention from README [\#2162](https://github.com/kivy/python-for-android/pull/2162) ([inclement](https://github.com/inclement)) - Fixes hostpython build with macOS venv [\#2159](https://github.com/kivy/python-for-android/pull/2159) ([AndreMiras](https://github.com/AndreMiras)) - Get --add-source working for dirs in Gradle builds [\#2156](https://github.com/kivy/python-for-android/pull/2156) ([kollivier](https://github.com/kollivier)) - Adding more assets [\#2132](https://github.com/kivy/python-for-android/pull/2132) ([robertpfeiffer](https://github.com/robertpfeiffer)) - Bump to SDL2 2.0.10 & extract .java from SDL2 tarball: merge conflicts fixed [\#2113](https://github.com/kivy/python-for-android/pull/2113) ([inclement](https://github.com/inclement)) ## [v2020.04.29](https://github.com/kivy/python-for-android/tree/v2020.04.29) (2020-05-07) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2020.03.30...v2020.04.29) **Closed issues:** - BUILD FAILURE: No main.py\(o\) found in your app directory. [\#2171](https://github.com/kivy/python-for-android/issues/2171) - \[ERROR\] Building cffi for armeabi-v7a [\#2161](https://github.com/kivy/python-for-android/issues/2161) - Setting p4a.source_dir in buildozer causes AttributeError: module 'build' has no attribute 'parse\_args\_and\_make\_package' [\#2149](https://github.com/kivy/python-for-android/issues/2149) - Full screen apps have significantly degraded performance [\#2148](https://github.com/kivy/python-for-android/issues/2148) - Build failed: Couldn't find executable for CC [\#2146](https://github.com/kivy/python-for-android/issues/2146) - Sign in apk is not working [\#2139](https://github.com/kivy/python-for-android/issues/2139) - openssl 1.1.1 has moved, recipe fails [\#2119](https://github.com/kivy/python-for-android/issues/2119) - App not asking for permission [\#2086](https://github.com/kivy/python-for-android/issues/2086) - app on android 6.0.1 does not work, but on android 8.0 if [\#1801](https://github.com/kivy/python-for-android/issues/1801) **Merged pull requests:** - Release 2020.04.29 [\#2174](https://github.com/kivy/python-for-android/pull/2174) ([AndreMiras](https://github.com/AndreMiras)) - Fixes sh `_env` should be a dictionary [\#2160](https://github.com/kivy/python-for-android/pull/2160) ([AndreMiras](https://github.com/AndreMiras)) - :rotating light: Fix linting for setup.py [\#2158](https://github.com/kivy/python-for-android/pull/2158) ([opacam](https://github.com/opacam)) - When bootstraps were unified, sources moved from src to src/main/java… [\#2154](https://github.com/kivy/python-for-android/pull/2154) ([kollivier](https://github.com/kollivier)) - Show loading screen while unpacking for webview bootstrap [\#2153](https://github.com/kivy/python-for-android/pull/2153) ([kollivier](https://github.com/kollivier)) - Use python3's venv instead of virtualenv [\#2152](https://github.com/kivy/python-for-android/pull/2152) ([opacam](https://github.com/opacam)) - Update setup.py: add classifiers & python\_requires [\#2151](https://github.com/kivy/python-for-android/pull/2151) ([opacam](https://github.com/opacam)) - Twisted recipe [\#2147](https://github.com/kivy/python-for-android/pull/2147) ([pavelsof](https://github.com/pavelsof)) - Update AndroidManifest.tmpl.xml to support HTTP [\#2143](https://github.com/kivy/python-for-android/pull/2143) ([yingshaoxo](https://github.com/yingshaoxo)) - Simplifies Dockerfile and fix GitHub runner space [\#2142](https://github.com/kivy/python-for-android/pull/2142) ([AndreMiras](https://github.com/AndreMiras)) - Fix some code quality and bug-risk issues [\#2141](https://github.com/kivy/python-for-android/pull/2141) ([pnijhara](https://github.com/pnijhara)) - Update bootstrap.py [\#2137](https://github.com/kivy/python-for-android/pull/2137) ([yingshaoxo](https://github.com/yingshaoxo)) - :lock: Bump twisted version to `20.3.0` [\#2135](https://github.com/kivy/python-for-android/pull/2135) ([opacam](https://github.com/opacam)) - release-2020.03.30 to develop [\#2134](https://github.com/kivy/python-for-android/pull/2134) ([AndreMiras](https://github.com/AndreMiras)) - Bumps cffi==1.13.2 fixes under Python 3.8 [\#2131](https://github.com/kivy/python-for-android/pull/2131) ([AndreMiras](https://github.com/AndreMiras)) - recipe: update 'cryptography' and rm unnecessary dependencies [\#2130](https://github.com/kivy/python-for-android/pull/2130) ([SomberNight](https://github.com/SomberNight)) - Fix coveralls error on GitHub Actions [\#2129](https://github.com/kivy/python-for-android/pull/2129) ([AndreMiras](https://github.com/AndreMiras)) - bump zeroconf version to 0.24.5, fix depends [\#2128](https://github.com/kivy/python-for-android/pull/2128) ([mikevlz](https://github.com/mikevlz)) - Install already compiled libraries [\#2127](https://github.com/kivy/python-for-android/pull/2127) ([robertpfeiffer](https://github.com/robertpfeiffer)) - Fixes code block directives [\#2126](https://github.com/kivy/python-for-android/pull/2126) ([AndreMiras](https://github.com/AndreMiras)) - Merge master into develop [\#2125](https://github.com/kivy/python-for-android/pull/2125) ([inclement](https://github.com/inclement)) - Merge master into develop [\#2123](https://github.com/kivy/python-for-android/pull/2123) ([inclement](https://github.com/inclement)) - Auto deploys to PyPI using Travis on tags [\#2122](https://github.com/kivy/python-for-android/pull/2122) ([AndreMiras](https://github.com/AndreMiras)) - Update recommended NDK version to 19c [\#2116](https://github.com/kivy/python-for-android/pull/2116) ([inclement](https://github.com/inclement)) - Minor fixes and cleanup [\#2115](https://github.com/kivy/python-for-android/pull/2115) ([AndreMiras](https://github.com/AndreMiras)) - Fixes linting errors in runnable.py [\#2114](https://github.com/kivy/python-for-android/pull/2114) ([AndreMiras](https://github.com/AndreMiras)) - :package: Refactor python module into hostpython3/python3 recipes [\#2108](https://github.com/kivy/python-for-android/pull/2108) ([opacam](https://github.com/opacam)) - :fire: Move to python3 `super` calls [\#2106](https://github.com/kivy/python-for-android/pull/2106) ([opacam](https://github.com/opacam)) - :fire: Drop Python 2 support [\#2105](https://github.com/kivy/python-for-android/pull/2105) ([opacam](https://github.com/opacam)) - Android library [\#2092](https://github.com/kivy/python-for-android/pull/2092) ([zworkb](https://github.com/zworkb)) - \[recipes\] Update harfbuzz to v2.6.4 [\#2069](https://github.com/kivy/python-for-android/pull/2069) ([opacam](https://github.com/opacam)) - \[recipes\] Update freetype & add zlib support [\#2068](https://github.com/kivy/python-for-android/pull/2068) ([opacam](https://github.com/opacam)) - Unify most of the test apps into `on device unit test app` [\#2046](https://github.com/kivy/python-for-android/pull/2046) ([opacam](https://github.com/opacam)) - Fix debug build not resulting in gdb-debuggable build [\#1867](https://github.com/kivy/python-for-android/pull/1867) ([etc0de](https://github.com/etc0de)) - fix for the problem with decorator run\_on\_ui\_thread and the local ref… [\#1830](https://github.com/kivy/python-for-android/pull/1830) ([oukiar](https://github.com/oukiar)) ## [v2020.03.30](https://github.com/kivy/python-for-android/tree/v2020.03.30) (2020-04-04) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2019.10.06...v2020.03.30) **Fixed bugs:** - Remove `--sysroot` from LDFLAGS for cffi and pymunk [\#1965](https://github.com/kivy/python-for-android/pull/1965) ([opacam](https://github.com/opacam)) - Fix build for case-insensitive FS and add CI test for OSX [\#1951](https://github.com/kivy/python-for-android/pull/1951) ([opacam](https://github.com/opacam)) - Also copy the service/main.py when building with setup.py [\#1936](https://github.com/kivy/python-for-android/pull/1936) ([etc0de](https://github.com/etc0de)) **Closed issues:** - Version bump for zeroconf to 0.25.4 [\#2107](https://github.com/kivy/python-for-android/issues/2107) - ValueError: read of closed file after download of psycopg2 [\#2098](https://github.com/kivy/python-for-android/issues/2098) - Why advise us to use Python2??? [\#2090](https://github.com/kivy/python-for-android/issues/2090) - KiwiSolver error led build fail when require matplotlib [\#2080](https://github.com/kivy/python-for-android/issues/2080) - Is it possible to run matplotlib script in android? [\#2079](https://github.com/kivy/python-for-android/issues/2079) - How to create my app name automatically on usb connect [\#2071](https://github.com/kivy/python-for-android/issues/2071) - Default buildozer.spec fails to build - fails on openssl [\#2060](https://github.com/kivy/python-for-android/issues/2060) - ImportError: dlopen failed: cannot locate symbol - Matplotlib module [\#2059](https://github.com/kivy/python-for-android/issues/2059) - ft2font build error with Matplotlib [\#2058](https://github.com/kivy/python-for-android/issues/2058) - SDL Error: Error Could not load any libpythonXXX.so [\#2056](https://github.com/kivy/python-for-android/issues/2056) - Crashing on phone. SDL Error Could not load any libpythonXXX.so [\#2051](https://github.com/kivy/python-for-android/issues/2051) - Hadi [\#2048](https://github.com/kivy/python-for-android/issues/2048) - p4a \(2019.10.6\) project build file management [\#2045](https://github.com/kivy/python-for-android/issues/2045) - listdir of primary\_external\_storage\_path\(\) fails [\#2032](https://github.com/kivy/python-for-android/issues/2032) - Can't use AsyncImage with HTTPS URL \(or any HTTPS url with any request\): fix is to manually load certifi [\#1827](https://github.com/kivy/python-for-android/issues/1827) **Merged pull requests:** - Bumps openssl to 1.1.1f [\#2118](https://github.com/kivy/python-for-android/pull/2118) ([AndreMiras](https://github.com/AndreMiras)) - Release 2020.03.30 [\#2111](https://github.com/kivy/python-for-android/pull/2111) ([inclement](https://github.com/inclement)) - Updates quickstart.rst "Installing Dependencies" [\#2109](https://github.com/kivy/python-for-android/pull/2109) ([AndreMiras](https://github.com/AndreMiras)) - :alien: Remove deprecated key `sudo` from `travis.yml` [\#2102](https://github.com/kivy/python-for-android/pull/2102) ([opacam](https://github.com/opacam)) - :arrow_up: Update `pytz` to version `2019.3` [\#2101](https://github.com/kivy/python-for-android/pull/2101) ([opacam](https://github.com/opacam)) - :sparkles: Add `pandas` recipe [\#2100](https://github.com/kivy/python-for-android/pull/2100) ([opacam](https://github.com/opacam)) - Fixes psycopg2 URL, closes \#2098 [\#2099](https://github.com/kivy/python-for-android/pull/2099) ([AndreMiras](https://github.com/AndreMiras)) - :sparkles: Compression libraries - episode III: add support for libbz2 & liblzma to python3 [\#2097](https://github.com/kivy/python-for-android/pull/2097) ([opacam](https://github.com/opacam)) - :sparkles: Compression libraries - episode II: liblzma [\#2096](https://github.com/kivy/python-for-android/pull/2096) ([opacam](https://github.com/opacam)) - :sparkles: Compression libraries - episode I: libbz2 [\#2095](https://github.com/kivy/python-for-android/pull/2095) ([opacam](https://github.com/opacam)) - Update quickstart.rst [\#2094](https://github.com/kivy/python-for-android/pull/2094) ([BornForFever](https://github.com/BornForFever)) - Fixes gevent recipe on arm64-v8a arch [\#2093](https://github.com/kivy/python-for-android/pull/2093) ([AndreMiras](https://github.com/AndreMiras)) - :bug: Add `-fPIC` to `CFLAGS` for Arch `x86_64` [\#2085](https://github.com/kivy/python-for-android/pull/2085) ([opacam](https://github.com/opacam)) - Fix Python 3.8.1 patch naming and content [\#2083](https://github.com/kivy/python-for-android/pull/2083) ([opacam](https://github.com/opacam)) - Update PythonService.java [\#2081](https://github.com/kivy/python-for-android/pull/2081) ([erikhu](https://github.com/erikhu)) - Update `numpy` to v1.18.1 \(add `cython` recipe\) [\#2077](https://github.com/kivy/python-for-android/pull/2077) ([opacam](https://github.com/opacam)) - Fix `matplotlib` and update to `v3.1.3` [\#2076](https://github.com/kivy/python-for-android/pull/2076) ([opacam](https://github.com/opacam)) - Fix recipe `kiwisolver` \(add `cppy` recipe\) [\#2075](https://github.com/kivy/python-for-android/pull/2075) ([opacam](https://github.com/opacam)) - \[gh-actions\] Move to actions/checkout@v2 [\#2070](https://github.com/kivy/python-for-android/pull/2070) ([opacam](https://github.com/opacam)) - \[recipes\] Update Pillow to v7.0.0 [\#2067](https://github.com/kivy/python-for-android/pull/2067) ([opacam](https://github.com/opacam)) - Fix missing renames of Bootstrap.list\_bootstraps -\> Bootstrap.all\_bootstraps [\#2066](https://github.com/kivy/python-for-android/pull/2066) ([touilleMan](https://github.com/touilleMan)) - fixed patch's name to apply correctly [\#2064](https://github.com/kivy/python-for-android/pull/2064) ([HirotsuguMINOWA](https://github.com/HirotsuguMINOWA)) - virtualenv 20 breaks the osx build [\#2063](https://github.com/kivy/python-for-android/pull/2063) ([AndreMiras](https://github.com/AndreMiras)) - automatically load/enable certifi in kivy [\#2055](https://github.com/kivy/python-for-android/pull/2055) ([tshirtman](https://github.com/tshirtman)) - \[protobuf\_cpp\] fixed python binding installation [\#2050](https://github.com/kivy/python-for-android/pull/2050) ([goffi-contrib](https://github.com/goffi-contrib)) - \[omemo\] updated to 0.11.0 + updated omemo-backend-signal to 0.2.5 [\#2049](https://github.com/kivy/python-for-android/pull/2049) ([goffi-contrib](https://github.com/goffi-contrib)) - Python 3.8 support on Android [\#2044](https://github.com/kivy/python-for-android/pull/2044) ([inclement](https://github.com/inclement)) - Updated version after 2019.10.06 release [\#2043](https://github.com/kivy/python-for-android/pull/2043) ([inclement](https://github.com/inclement)) - Updated version to 2019.10.06 [\#2042](https://github.com/kivy/python-for-android/pull/2042) ([inclement](https://github.com/inclement)) - Added a build.py argument to add arbitrary xml to the AndroidManifest.xml [\#2040](https://github.com/kivy/python-for-android/pull/2040) ([inclement](https://github.com/inclement)) - update pyjnius recipe [\#2036](https://github.com/kivy/python-for-android/pull/2036) ([tshirtman](https://github.com/tshirtman)) - added a recipe for bcrypt library [\#2035](https://github.com/kivy/python-for-android/pull/2035) ([tomgold182](https://github.com/tomgold182)) - Fix vibration for testapps [\#2034](https://github.com/kivy/python-for-android/pull/2034) ([opacam](https://github.com/opacam)) - \[gh-actions\] Add new testapp and upload artifacts... [\#2033](https://github.com/kivy/python-for-android/pull/2033) ([opacam](https://github.com/opacam)) - from kivy.base import runTouchApp for line 75 [\#2028](https://github.com/kivy/python-for-android/pull/2028) ([cclauss](https://github.com/cclauss)) - Fix libshine and re-enable it for ffmpeg & ffpyplayer\_codecs [\#2027](https://github.com/kivy/python-for-android/pull/2027) ([opacam](https://github.com/opacam)) - Introduce github-actions \(push & pull\_requests\) [\#2025](https://github.com/kivy/python-for-android/pull/2025) ([opacam](https://github.com/opacam)) - Fix rebuild updated recipes, refs \#2011 [\#2024](https://github.com/kivy/python-for-android/pull/2024) ([opacam](https://github.com/opacam)) - Exposes ANDROID\_SDK\_HOME to rebuild\_updated\_recipes [\#2018](https://github.com/kivy/python-for-android/pull/2018) ([AndreMiras](https://github.com/AndreMiras)) - update pyproj recipe [\#2017](https://github.com/kivy/python-for-android/pull/2017) ([joergbrech](https://github.com/joergbrech)) - Documentation: android hide loading screen [\#2014](https://github.com/kivy/python-for-android/pull/2014) ([adityabhawsingka](https://github.com/adityabhawsingka)) - Travis CI revamp part 1, refs \#2008 [\#2011](https://github.com/kivy/python-for-android/pull/2011) ([AndreMiras](https://github.com/AndreMiras)) - twisted: updated to 19.7.0 + removed inceremental.path [\#2006](https://github.com/kivy/python-for-android/pull/2006) ([goffi-contrib](https://github.com/goffi-contrib)) - Fix error on py2 when `import numpy` [\#2003](https://github.com/kivy/python-for-android/pull/2003) ([opacam](https://github.com/opacam)) - Fix libcurl with openssl [\#2000](https://github.com/kivy/python-for-android/pull/2000) ([tito](https://github.com/tito)) - Fixes ffmpeg build on ndk 19 [\#1997](https://github.com/kivy/python-for-android/pull/1997) ([misl6](https://github.com/misl6)) - Fixes test\_virtualenv and test\_venv failing, closes \#1994 [\#1995](https://github.com/kivy/python-for-android/pull/1995) ([AndreMiras](https://github.com/AndreMiras)) - Fixes libiconv & libzbar configure host [\#1993](https://github.com/kivy/python-for-android/pull/1993) ([AndreMiras](https://github.com/AndreMiras)) - Updates Java version troubleshooting [\#1991](https://github.com/kivy/python-for-android/pull/1991) ([AndreMiras](https://github.com/AndreMiras)) - Made p4a use per-arch dist build dirs [\#1986](https://github.com/kivy/python-for-android/pull/1986) ([inclement](https://github.com/inclement)) - Made on-device unit tests app use the develop branch by default [\#1985](https://github.com/kivy/python-for-android/pull/1985) ([inclement](https://github.com/inclement)) - Recipes tests enhancements [\#1984](https://github.com/kivy/python-for-android/pull/1984) ([opacam](https://github.com/opacam)) - Proof of concept - A bunch of tests for library recipes [\#1982](https://github.com/kivy/python-for-android/pull/1982) ([opacam](https://github.com/opacam)) - Updated README.md to clarify NDK versions [\#1981](https://github.com/kivy/python-for-android/pull/1981) ([Zen-CODE](https://github.com/Zen-CODE)) - Fix CI's test for `arm64-v8a` [\#1977](https://github.com/kivy/python-for-android/pull/1977) ([opacam](https://github.com/opacam)) - Added missing recommendations command and cleaned up code [\#1975](https://github.com/kivy/python-for-android/pull/1975) ([inclement](https://github.com/inclement)) - Added libffi headers troubleshooting note to doc [\#1972](https://github.com/kivy/python-for-android/pull/1972) ([inclement](https://github.com/inclement)) - \[WIP\]\[LIBS - PART VII\] Rework of libtorrent and boost [\#1971](https://github.com/kivy/python-for-android/pull/1971) ([opacam](https://github.com/opacam)) - \[WIP\]\[LIBS - PART VI\] Rework of protobuf\_cpp [\#1969](https://github.com/kivy/python-for-android/pull/1969) ([opacam](https://github.com/opacam)) - \[WIP\]\[LIBS - PART V\] Rework of libzmq [\#1968](https://github.com/kivy/python-for-android/pull/1968) ([opacam](https://github.com/opacam)) - \[WIP\]\[LIBS - PART IV\] Rework of shapely and libgeos [\#1967](https://github.com/kivy/python-for-android/pull/1967) ([opacam](https://github.com/opacam)) - \[WIP\]\[LIBS - PART III\] Rework of pyleveldb, leveldb and snappy [\#1966](https://github.com/kivy/python-for-android/pull/1966) ([opacam](https://github.com/opacam)) - Updated version number for develop branch following 2019.08.09 release [\#1960](https://github.com/kivy/python-for-android/pull/1960) ([inclement](https://github.com/inclement)) - Merge release-2019.08.09 branch into develop [\#1959](https://github.com/kivy/python-for-android/pull/1959) ([inclement](https://github.com/inclement)) - Fix and update regex's recipe [\#1958](https://github.com/kivy/python-for-android/pull/1958) ([opacam](https://github.com/opacam)) - Fix/doc quotes [\#1956](https://github.com/kivy/python-for-android/pull/1956) ([tshirtman](https://github.com/tshirtman)) - \[LIBS - PART II\] Part II of NDK r19 migration - Initial STL lib migration [\#1947](https://github.com/kivy/python-for-android/pull/1947) ([opacam](https://github.com/opacam)) - \[LIBS - PART I\] Initial refactor of library recipes [\#1944](https://github.com/kivy/python-for-android/pull/1944) ([opacam](https://github.com/opacam)) - customizability [\#1869](https://github.com/kivy/python-for-android/pull/1869) ([zworkb](https://github.com/zworkb)) - Initial migration to NDK r19 \(Part I - The core\) [\#1722](https://github.com/kivy/python-for-android/pull/1722) ([opacam](https://github.com/opacam)) ## [v2019.10.06](https://github.com/kivy/python-for-android/tree/v2019.10.06) (2019-12-22) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2019.08.09...v2019.10.06) **Fixed bugs:** - TestGetSystemPythonExecutable.test\_virtualenv test fail [\#1994](https://github.com/kivy/python-for-android/issues/1994) **Closed issues:** - Presplash is removed prematurely. [\#2029](https://github.com/kivy/python-for-android/issues/2029) - Sorry posted in the wrong repository. Closing this issue [\#2022](https://github.com/kivy/python-for-android/issues/2022) - p4a building apk error on Mac OS [\#2016](https://github.com/kivy/python-for-android/issues/2016) - Revamp .travis.yml file [\#2008](https://github.com/kivy/python-for-android/issues/2008) - Possible SDL2 issues introduced with P4A 2019.06.06 [\#2002](https://github.com/kivy/python-for-android/issues/2002) - Error message about python2 [\#2001](https://github.com/kivy/python-for-android/issues/2001) - ffmpeg recipe is broken on ndk19 [\#1996](https://github.com/kivy/python-for-android/issues/1996) - Error while running ".buildozer.../native-build/python -OO -m compileall -b -f /.../app [\#1990](https://github.com/kivy/python-for-android/issues/1990) - The mpl\_android\_fixes.patch didn't work [\#1989](https://github.com/kivy/python-for-android/issues/1989) - Importing numpy yields: TypeError: add\_docstring\(\) argument 2 must be str, not None [\#1988](https://github.com/kivy/python-for-android/issues/1988) - p4a apk :compileDebugJavaWithJavac error [\#1980](https://github.com/kivy/python-for-android/issues/1980) - \[question\]Python for android no longer supports Error ! [\#1978](https://github.com/kivy/python-for-android/issues/1978) - Can not Find on Google Play or Buy Premium [\#1974](https://github.com/kivy/python-for-android/issues/1974) - Build failed [\#1970](https://github.com/kivy/python-for-android/issues/1970) - Can't build a package with bcrypt as requirement [\#1910](https://github.com/kivy/python-for-android/issues/1910) - import wxpy module fail [\#1897](https://github.com/kivy/python-for-android/issues/1897) - Cannot build APK with buildozer [\#1817](https://github.com/kivy/python-for-android/issues/1817) - Kivy crashes on Android: ImportError: dlopen failed. [\#1810](https://github.com/kivy/python-for-android/issues/1810) - App build failing on MacOS [\#1647](https://github.com/kivy/python-for-android/issues/1647) - Remove superfluous recipes [\#1387](https://github.com/kivy/python-for-android/issues/1387) **Merged pull requests:** - Release 2019.10.06 [\#1998](https://github.com/kivy/python-for-android/pull/1998) ([inclement](https://github.com/inclement)) ## [v2019.08.09](https://github.com/kivy/python-for-android/tree/v2019.08.09) (2019-08-19) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2019.07.08...v2019.08.09) **Fixed bugs:** - Call Cython via `python -m Cython` rather than system-wide binary [\#1937](https://github.com/kivy/python-for-android/pull/1937) ([etc0de](https://github.com/etc0de)) **Closed issues:** - Building an Android Library [\#1957](https://github.com/kivy/python-for-android/issues/1957) - dlopen failed: library "../../../../src/main/jniLibs/armeabi-v7a/libpython3.7m.so" not found [\#1954](https://github.com/kivy/python-for-android/issues/1954) - App crashing on startup - Import Error: dlopen failed: \_portaudio.so is 64 bit instead of 32 bit [\#1953](https://github.com/kivy/python-for-android/issues/1953) - How to overcome:? \#error "LONG\_BIT definition appears wrong for platform \(bad gcc/glibc config?\)." [\#1949](https://github.com/kivy/python-for-android/issues/1949) - copy paste option is not working in mobile client \(android \)after cloning from updated p4a [\#1942](https://github.com/kivy/python-for-android/issues/1942) - It seems kivy has no support for tkinter, os, sys, random modules [\#1934](https://github.com/kivy/python-for-android/issues/1934) - Mxnet recipe for running kivy app on android [\#1929](https://github.com/kivy/python-for-android/issues/1929) - java.lang.UnsatisfiedLinkError: dlopen failed: library "libffi.so.7" not found [\#1924](https://github.com/kivy/python-for-android/issues/1924) - Ndk19c compiled numpy problems [\#1923](https://github.com/kivy/python-for-android/issues/1923) - run\_on\_ui\_thread crash [\#1908](https://github.com/kivy/python-for-android/issues/1908) - please provide recipes for libraries dlib, easygui, Colormath , keras ,imutils [\#1906](https://github.com/kivy/python-for-android/issues/1906) - About TextInput, I can't type korean character. I only can type english. [\#1904](https://github.com/kivy/python-for-android/issues/1904) - app crash when bootstrap=webview [\#1894](https://github.com/kivy/python-for-android/issues/1894) - apk file crash on app launch generated using kivy, buildozer [\#1891](https://github.com/kivy/python-for-android/issues/1891) - StartService in Android Oreo and More should use startForegroundService [\#1785](https://github.com/kivy/python-for-android/issues/1785) - Remove WRITE\_EXTERNAL\_STORAGE default permission [\#1081](https://github.com/kivy/python-for-android/issues/1081) **Merged pull requests:** - Release 2019.08.09 [\#1955](https://github.com/kivy/python-for-android/pull/1955) ([inclement](https://github.com/inclement)) - Unit tests Recipe.download\_file\(\) partly [\#1952](https://github.com/kivy/python-for-android/pull/1952) ([AndreMiras](https://github.com/AndreMiras)) - Unit tests Recipe download feature [\#1946](https://github.com/kivy/python-for-android/pull/1946) ([AndreMiras](https://github.com/AndreMiras)) - Added setuptools to Kivy recipe requirements [\#1938](https://github.com/kivy/python-for-android/pull/1938) ([inclement](https://github.com/inclement)) - Bumps to Kivy==1.11.1 [\#1935](https://github.com/kivy/python-for-android/pull/1935) ([AndreMiras](https://github.com/AndreMiras)) - Increases toolchain.py test coverage [\#1933](https://github.com/kivy/python-for-android/pull/1933) ([AndreMiras](https://github.com/AndreMiras)) - Add a document describing how p4a interacts with pip & python packages [\#1931](https://github.com/kivy/python-for-android/pull/1931) ([etc0de](https://github.com/etc0de)) - Fix pyzmq \(libunwind and arm64-v8a\) [\#1930](https://github.com/kivy/python-for-android/pull/1930) ([tito](https://github.com/tito)) - Basic toolchain.py unit tests [\#1928](https://github.com/kivy/python-for-android/pull/1928) ([AndreMiras](https://github.com/AndreMiras)) - Merge 2019.07.08 release branch to develop [\#1921](https://github.com/kivy/python-for-android/pull/1921) ([inclement](https://github.com/inclement)) - Drop Python 2 support [\#1918](https://github.com/kivy/python-for-android/pull/1918) ([inclement](https://github.com/inclement)) - Remove `nosetests.xml` from gitignore [\#1915](https://github.com/kivy/python-for-android/pull/1915) ([opacam](https://github.com/opacam)) - Drop CrystaX support and code base [\#1913](https://github.com/kivy/python-for-android/pull/1913) ([opacam](https://github.com/opacam)) - Release 2019.07.08 [\#1909](https://github.com/kivy/python-for-android/pull/1909) ([inclement](https://github.com/inclement)) - Add documentation: testing a pull request [\#1901](https://github.com/kivy/python-for-android/pull/1901) ([opacam](https://github.com/opacam)) - Fix foreground notification being mandatory and more \(attempt 2\) [\#1888](https://github.com/kivy/python-for-android/pull/1888) ([etc0de](https://github.com/etc0de)) - Make it raise an error if an old ndk is used [\#1883](https://github.com/kivy/python-for-android/pull/1883) ([opacam](https://github.com/opacam)) - Hotfix 2019.06.06.post0: added long\_description\_content\_type to setup.py [\#1850](https://github.com/kivy/python-for-android/pull/1850) ([inclement](https://github.com/inclement)) - feat: Allows registering the onRequestPermissionsResult callback. [\#1818](https://github.com/kivy/python-for-android/pull/1818) ([gbm001](https://github.com/gbm001)) - Add functions for obtaining the default storage paths [\#1598](https://github.com/kivy/python-for-android/pull/1598) ([etc0de](https://github.com/etc0de)) ## [v2019.07.08](https://github.com/kivy/python-for-android/tree/v2019.07.08) (2019-07-11) [Full Changelog](https://github.com/kivy/python-for-android/compare/v2019.06.06...v2019.07.08) **Fixed bugs:** - Fix crash when guessing Bootstrap \(expand\_dependencies\) [\#1914](https://github.com/kivy/python-for-android/pull/1914) ([opacam](https://github.com/opacam)) - Fix `run_pymodules_install` when `project_dir` isn't supplied [\#1898](https://github.com/kivy/python-for-android/pull/1898) ([opacam](https://github.com/opacam)) - Fix Bootstrap.get\_bootstrap\_from\_recipes\(\) so it's smarter and deterministic, fixes \#1875 [\#1887](https://github.com/kivy/python-for-android/pull/1887) ([etc0de](https://github.com/etc0de)) - Typo [\#1880](https://github.com/kivy/python-for-android/pull/1880) ([JensGe](https://github.com/JensGe)) - Fix wrong env variable for `hostpython build path` in `archs.py` [\#1871](https://github.com/kivy/python-for-android/pull/1871) ([opacam](https://github.com/opacam)) - Fix various setup.py processing bugs [\#1862](https://github.com/kivy/python-for-android/pull/1862) ([etc0de](https://github.com/etc0de)) - Add `--without-bzip2` to freetype's configure args [\#1857](https://github.com/kivy/python-for-android/pull/1857) ([opacam](https://github.com/opacam)) - pythonpackage can't return build requirements for wheels, make it return an error if attempted [\#1852](https://github.com/kivy/python-for-android/pull/1852) ([etc0de](https://github.com/etc0de)) **Closed issues:** - Proposal: drop CrystaX support and code base [\#1905](https://github.com/kivy/python-for-android/issues/1905) - android hardware back button does not work in kivy [\#1903](https://github.com/kivy/python-for-android/issues/1903) - import wxpy module fail:python for android ended [\#1896](https://github.com/kivy/python-for-android/issues/1896) - error compile with cristax-ndk [\#1895](https://github.com/kivy/python-for-android/issues/1895) - is recipe for the library "kivymd" available in p4a? [\#1893](https://github.com/kivy/python-for-android/issues/1893) - deleted [\#1889](https://github.com/kivy/python-for-android/issues/1889) - p4a isn't finding directories in apk on launch \(ModuleNotFoundError\) [\#1881](https://github.com/kivy/python-for-android/issues/1881) - Little but essential Typo [\#1879](https://github.com/kivy/python-for-android/issues/1879) - clang crashes with an unclear error if ncurses5 isn't available \(specifically libtinfo.so.5\) [\#1878](https://github.com/kivy/python-for-android/issues/1878) - get\_bootstrap\_from\_recipes\(\) result consistency [\#1875](https://github.com/kivy/python-for-android/issues/1875) - install recursive dependencies of pure python packages [\#1874](https://github.com/kivy/python-for-android/issues/1874) - numpy recipe bug arm64-v8a [\#1873](https://github.com/kivy/python-for-android/issues/1873) - how do i support numba\(a python module\) [\#1865](https://github.com/kivy/python-for-android/issues/1865) - apk build error: bzlib.h: No such file or directory [\#1854](https://github.com/kivy/python-for-android/issues/1854) - Compilation error, previously worked [\#1846](https://github.com/kivy/python-for-android/issues/1846) - Keep track of coverage testing [\#1788](https://github.com/kivy/python-for-android/issues/1788) - p4a's overtaking of dependency order in place of --no-deps is problematic and IMHO should go [\#1490](https://github.com/kivy/python-for-android/issues/1490) **Merged pull requests:** - Fixes ffmpeg and libx264 recipes for arm64-v8 [\#1916](https://github.com/kivy/python-for-android/pull/1916) ([misl6](https://github.com/misl6)) - Docker - Update android's sdk tools to `28.0.2` [\#1912](https://github.com/kivy/python-for-android/pull/1912) ([opacam](https://github.com/opacam)) - Feature gitignore additions [\#1911](https://github.com/kivy/python-for-android/pull/1911) ([opacam](https://github.com/opacam)) - Simple run\_pymodules\_install test, refs \#1898 [\#1899](https://github.com/kivy/python-for-android/pull/1899) ([AndreMiras](https://github.com/AndreMiras)) - Updated numpy recipe to version 1.16.4 [\#1892](https://github.com/kivy/python-for-android/pull/1892) ([inclement](https://github.com/inclement)) - fix ctypes-util-find-library issue for python2 [\#1877](https://github.com/kivy/python-for-android/pull/1877) ([surbhicis](https://github.com/surbhicis)) - Add unittest for module `pythonforandroid.bootstrap` [\#1872](https://github.com/kivy/python-for-android/pull/1872) ([opacam](https://github.com/opacam)) - Remove legacy version of openssl [\#1870](https://github.com/kivy/python-for-android/pull/1870) ([opacam](https://github.com/opacam)) - Make the tox jobs run in parallel &... [\#1864](https://github.com/kivy/python-for-android/pull/1864) ([opacam](https://github.com/opacam)) - Try to be more clear in README about api levels & quickstart [\#1863](https://github.com/kivy/python-for-android/pull/1863) ([etc0de](https://github.com/etc0de)) - Fix locating system python when it's not in $PATH \(weird but happens, apparently\) [\#1856](https://github.com/kivy/python-for-android/pull/1856) ([etc0de](https://github.com/etc0de)) - Add unittest for `pythonforandroid.util` and... [\#1855](https://github.com/kivy/python-for-android/pull/1855) ([opacam](https://github.com/opacam)) - Merge 2019.06.06.post0 hotfix: set long\_description\_content\_type in setup.py [\#1851](https://github.com/kivy/python-for-android/pull/1851) ([inclement](https://github.com/inclement)) - Improved release model documentation [\#1849](https://github.com/kivy/python-for-android/pull/1849) ([inclement](https://github.com/inclement)) - Merge release-2019.06.06 to develop [\#1848](https://github.com/kivy/python-for-android/pull/1848) ([inclement](https://github.com/inclement)) - Add unittest for module `pythonforandroid.distribution` [\#1847](https://github.com/kivy/python-for-android/pull/1847) ([opacam](https://github.com/opacam)) - bugfix: unpack for nonzip archives also needs to compare basename\(dir\) [\#1845](https://github.com/kivy/python-for-android/pull/1845) ([sfoerster](https://github.com/sfoerster)) - Add unittest for module `pythonforandroid.archs` [\#1842](https://github.com/kivy/python-for-android/pull/1842) ([opacam](https://github.com/opacam)) - Replaced one of the python2 travis builds with python3 arm64-v8a [\#1840](https://github.com/kivy/python-for-android/pull/1840) ([inclement](https://github.com/inclement)) ## [v2019.06.06](https://github.com/kivy/python-for-android/tree/v2019.06.06) (2019-06-08) [Full Changelog](https://github.com/kivy/python-for-android/compare/0.7.0...v2019.06.06) **Fixed bugs:** - AttributeError: 'Namespace' object has no attribute 'ignore\_setup\_py' [\#1808](https://github.com/kivy/python-for-android/issues/1808) - libzmq recipe compiling error [\#1802](https://github.com/kivy/python-for-android/issues/1802) - Cannot build APK - IndexError: List index out of range [\#1774](https://github.com/kivy/python-for-android/issues/1774) - ctypes.util.find\_library\(\) doesn't work on arch arm64-v8a [\#1770](https://github.com/kivy/python-for-android/issues/1770) - error building compiled components in netifaces [\#1539](https://github.com/kivy/python-for-android/issues/1539) - \[WIP\] Fix crashes when using other commands than 'apk' [\#1809](https://github.com/kivy/python-for-android/pull/1809) ([etc0de](https://github.com/etc0de)) **Closed issues:** - Create a release checklist [\#1836](https://github.com/kivy/python-for-android/issues/1836) - Sorting out python-for-android releases [\#1833](https://github.com/kivy/python-for-android/issues/1833) - Error - Runtime permissions - object 'Permission' has no attribute 'ACCESS\_FINE\_LOCATION' [\#1824](https://github.com/kivy/python-for-android/issues/1824) - buildozer android debug deploy run -\> sh.CommandNotFound: ./gradlew [\#1804](https://github.com/kivy/python-for-android/issues/1804) - buildozer failed clang++: error: linker command failed with exit code 1 [\#1800](https://github.com/kivy/python-for-android/issues/1800) - Crash on "--orientation sensor" when rotate [\#1797](https://github.com/kivy/python-for-android/issues/1797) - error when compiling with flask\_sqlalmechy and sqlamechy [\#1793](https://github.com/kivy/python-for-android/issues/1793) - Some links are broken in the docs [\#1780](https://github.com/kivy/python-for-android/issues/1780) - packaged python is built with IPv6 disabled [\#1771](https://github.com/kivy/python-for-android/issues/1771) - `p4a recipes` terminates with error [\#1769](https://github.com/kivy/python-for-android/issues/1769) - the application does not work with scipy package [\#1767](https://github.com/kivy/python-for-android/issues/1767) - openssl not in the build order when compiling cryptography [\#1764](https://github.com/kivy/python-for-android/issues/1764) - "--orientation fullUser" is not working [\#1763](https://github.com/kivy/python-for-android/issues/1763) - pydub problem [\#1759](https://github.com/kivy/python-for-android/issues/1759) - App crashes using python3 and android's run\_on\_ui\_thread [\#1755](https://github.com/kivy/python-for-android/issues/1755) - str.decode\(\) issue again for Python 3 [\#1749](https://github.com/kivy/python-for-android/issues/1749) - Error while in gradlew [\#1740](https://github.com/kivy/python-for-android/issues/1740) - Buildozer [\#1736](https://github.com/kivy/python-for-android/issues/1736) - C compiler cannot create executables [\#1735](https://github.com/kivy/python-for-android/issues/1735) - Gevent reciepe problem [\#1732](https://github.com/kivy/python-for-android/issues/1732) - Permission RECORD\_AUDIO not working [\#1730](https://github.com/kivy/python-for-android/issues/1730) - Unable to build with flask, conflicting with genericndkbuild [\#1728](https://github.com/kivy/python-for-android/issues/1728) - Android setup.py not working for windows [\#1726](https://github.com/kivy/python-for-android/issues/1726) - Fullscreen mode does not work [\#1724](https://github.com/kivy/python-for-android/issues/1724) - ImportError: sh 1.12.14 is currently only supported on linux and osx. please install pbs 0.110 \(http://pypi.python.org/pypi/pbs\) for windows support. [\#1721](https://github.com/kivy/python-for-android/issues/1721) - App crashing following successful build [\#1719](https://github.com/kivy/python-for-android/issues/1719) - Null pointer when finding libraries [\#1717](https://github.com/kivy/python-for-android/issues/1717) - Psycopg2 error after the apk installation. [\#1711](https://github.com/kivy/python-for-android/issues/1711) - Match official requestPermissions interface [\#1704](https://github.com/kivy/python-for-android/issues/1704) - Webview build can't find `SDL_setenv`. [\#1702](https://github.com/kivy/python-for-android/issues/1702) - lxml and requests recipe: build interrupted, "\_ctype" error [\#1700](https://github.com/kivy/python-for-android/issues/1700) - weird orientation behavior [\#1698](https://github.com/kivy/python-for-android/issues/1698) - SDLActivity.java\:1948\: error: cannot find symbol case MotionEvent.ACTION\_BUTTON\_PRESS: [\#1697](https://github.com/kivy/python-for-android/issues/1697) - ImportError [\#1694](https://github.com/kivy/python-for-android/issues/1694) - unicode error during startup \(python3, numpy, opencv\) - patch included [\#1691](https://github.com/kivy/python-for-android/issues/1691) - "--orientation sensor" no longer works [\#1688](https://github.com/kivy/python-for-android/issues/1688) - kivy==master Window undefined [\#1687](https://github.com/kivy/python-for-android/issues/1687) - Pillow Python 3 compile error [\#1679](https://github.com/kivy/python-for-android/issues/1679) - numpy/opencv fails with latest p4a at runtime \(import\) [\#1678](https://github.com/kivy/python-for-android/issues/1678) - Remove pygame bootstrap [\#1668](https://github.com/kivy/python-for-android/issues/1668) - TODO: bring the kivy.org p4a documentation up to date [\#1657](https://github.com/kivy/python-for-android/issues/1657) - Discussion: should the default recipe set for python3 unconditionally include libffi to build ctypes? What about sqlite3 and other core modules? [\#1576](https://github.com/kivy/python-for-android/issues/1576) - Project's setup.py is not run, it should be \(at least as an option\) [\#1488](https://github.com/kivy/python-for-android/issues/1488) - Destroying SDL\_Renderer in app background event with the intention to restore it in foreground event leads to crash [\#1424](https://github.com/kivy/python-for-android/issues/1424) - Broken libglob recipe [\#1399](https://github.com/kivy/python-for-android/issues/1399) - Crash when my Python program starts to load [\#1299](https://github.com/kivy/python-for-android/issues/1299) - Calendar.getTimeInMillis\(\) returns a negative value [\#942](https://github.com/kivy/python-for-android/issues/942) - plyer requirement does not work with ==master version [\#879](https://github.com/kivy/python-for-android/issues/879) - ImportError: No module named pysqlite2 [\#860](https://github.com/kivy/python-for-android/issues/860) - Local recipe dir is not returned by get\_recipe\_dir\(\) [\#613](https://github.com/kivy/python-for-android/issues/613) - SQLite gets compiled without Full Text Search \(FTS3\) support [\#431](https://github.com/kivy/python-for-android/issues/431) - BroadcastReceiver broken in service [\#278](https://github.com/kivy/python-for-android/issues/278) **Merged pull requests:** - Release 2019.06.06 [\#1839](https://github.com/kivy/python-for-android/pull/1839) ([inclement](https://github.com/inclement)) - Documented the development and release models [\#1838](https://github.com/kivy/python-for-android/pull/1838) ([inclement](https://github.com/inclement)) - Added readme for on device unit tests [\#1837](https://github.com/kivy/python-for-android/pull/1837) ([inclement](https://github.com/inclement)) - Add coverage with coveralls and make use of travis stages [\#1835](https://github.com/kivy/python-for-android/pull/1835) ([opacam](https://github.com/opacam)) - Update Kivy version to 1.11.0 [\#1832](https://github.com/kivy/python-for-android/pull/1832) ([inclement](https://github.com/inclement)) - \[R.I.P.\] Remove `python2legacy` and `hospython2legacy` [\#1831](https://github.com/kivy/python-for-android/pull/1831) ([opacam](https://github.com/opacam)) - Update permissions.py [\#1828](https://github.com/kivy/python-for-android/pull/1828) ([tamano123](https://github.com/tamano123)) - Add a Pillow test app [\#1826](https://github.com/kivy/python-for-android/pull/1826) ([opacam](https://github.com/opacam)) - Rework of freetype/harfbuzz recipes [\#1825](https://github.com/kivy/python-for-android/pull/1825) ([opacam](https://github.com/opacam)) - Added a matplotlib recipe [\#1822](https://github.com/kivy/python-for-android/pull/1822) ([inclement](https://github.com/inclement)) - Fix travis log doesn't produce any output for more than 10 minutes [\#1821](https://github.com/kivy/python-for-android/pull/1821) ([opacam](https://github.com/opacam)) - Fix `CI` errors with latest tox [\#1820](https://github.com/kivy/python-for-android/pull/1820) ([opacam](https://github.com/opacam)) - Fixed build file for service\_only bootstrap [\#1819](https://github.com/kivy/python-for-android/pull/1819) ([devos50](https://github.com/devos50)) - enable IPv6 for packaged python3 [\#1815](https://github.com/kivy/python-for-android/pull/1815) ([SomberNight](https://github.com/SomberNight)) - Update pymunk to v5.5.0 [\#1814](https://github.com/kivy/python-for-android/pull/1814) ([viblo](https://github.com/viblo)) - Fixes ffpyplayer build [\#1813](https://github.com/kivy/python-for-android/pull/1813) ([misl6](https://github.com/misl6)) - Fix corner case of pip hack workaround we should get rid of anyway [\#1807](https://github.com/kivy/python-for-android/pull/1807) ([etc0de](https://github.com/etc0de)) - Fixed doc links to plyer and pyjnius [\#1806](https://github.com/kivy/python-for-android/pull/1806) ([inclement](https://github.com/inclement)) - Fixed libzqm recipe linking issues [\#1803](https://github.com/kivy/python-for-android/pull/1803) ([hpsaturn](https://github.com/hpsaturn)) - Update recipes.rst [\#1799](https://github.com/kivy/python-for-android/pull/1799) ([FunmiKesa](https://github.com/FunmiKesa)) - Update recipes.rst [\#1798](https://github.com/kivy/python-for-android/pull/1798) ([FunmiKesa](https://github.com/FunmiKesa)) - Bumps to setuptools==40.9.0 [\#1795](https://github.com/kivy/python-for-android/pull/1795) ([AndreMiras](https://github.com/AndreMiras)) - Fix sqlalchemy recipe [\#1794](https://github.com/kivy/python-for-android/pull/1794) ([sduenasg](https://github.com/sduenasg)) - Move ffmpeg download url to github repo [\#1791](https://github.com/kivy/python-for-android/pull/1791) ([misl6](https://github.com/misl6)) - Ignores Python 2 import\_recipe\(\) warnings [\#1789](https://github.com/kivy/python-for-android/pull/1789) ([AndreMiras](https://github.com/AndreMiras)) - \[kivy\] updated to 44a8a6f [\#1777](https://github.com/kivy/python-for-android/pull/1777) ([goffi-contrib](https://github.com/goffi-contrib)) - Fix outdated PySDL2 version and non-PyPI install source [\#1775](https://github.com/kivy/python-for-android/pull/1775) ([etc0de](https://github.com/etc0de)) - Properly search native lib dir in ctypes, fixes \#1770 [\#1772](https://github.com/kivy/python-for-android/pull/1772) ([etc0de](https://github.com/etc0de)) - \[kivy\] updated recipe to c4d6894 revision [\#1766](https://github.com/kivy/python-for-android/pull/1766) ([goffi-contrib](https://github.com/goffi-contrib)) - Fixes libglob recipe, closes \#1399 [\#1765](https://github.com/kivy/python-for-android/pull/1765) ([AndreMiras](https://github.com/AndreMiras)) - \[doubleratchet\] removed this recipe [\#1762](https://github.com/kivy/python-for-android/pull/1762) ([goffi-contrib](https://github.com/goffi-contrib)) - \[cryptography\] updated to 2.6.1 [\#1761](https://github.com/kivy/python-for-android/pull/1761) ([goffi-contrib](https://github.com/goffi-contrib)) - \[omemo-backend-signal\] updated to 0.2.3 [\#1760](https://github.com/kivy/python-for-android/pull/1760) ([goffi-contrib](https://github.com/goffi-contrib)) - \[omemo\] updated to v0.10.4 [\#1758](https://github.com/kivy/python-for-android/pull/1758) ([goffi-contrib](https://github.com/goffi-contrib)) - \[protobuf\_cpp\] fixed runtime issues [\#1757](https://github.com/kivy/python-for-android/pull/1757) ([goffi-contrib](https://github.com/goffi-contrib)) - \[xeddsa\] fixed shared library copying [\#1756](https://github.com/kivy/python-for-android/pull/1756) ([goffi-contrib](https://github.com/goffi-contrib)) - remove call to decode on str in \_android.pyx [\#1752](https://github.com/kivy/python-for-android/pull/1752) ([tshirtman](https://github.com/tshirtman)) - \[libxml2\] fixed crash on missing lzma.h [\#1751](https://github.com/kivy/python-for-android/pull/1751) ([goffi-contrib](https://github.com/goffi-contrib)) - Remove string decoding in \_android.pyx for Python3 compatibility [\#1748](https://github.com/kivy/python-for-android/pull/1748) ([darosior](https://github.com/darosior)) - decode JAVA\_NAMESPACE in \_android.pyx [\#1747](https://github.com/kivy/python-for-android/pull/1747) ([tshirtman](https://github.com/tshirtman)) - fix: "cwd is" log message was printed directly, not via debug\(\) [\#1746](https://github.com/kivy/python-for-android/pull/1746) ([mkg20001](https://github.com/mkg20001)) - Fix libffi recipe, and build + runtime linker errors when compiling on WSL [\#1744](https://github.com/kivy/python-for-android/pull/1744) ([Aralox](https://github.com/Aralox)) - Updated sdl2\_mixer version to 2.0.4 [\#1742](https://github.com/kivy/python-for-android/pull/1742) ([misl6](https://github.com/misl6)) - Requests runtime permissions list, fixes \#1704 [\#1741](https://github.com/kivy/python-for-android/pull/1741) ([AndreMiras](https://github.com/AndreMiras)) - fix groestlcoin\_hash recipe [\#1738](https://github.com/kivy/python-for-android/pull/1738) ([tshirtman](https://github.com/tshirtman)) - Fixes object is not subscriptable error, refs \#1733 [\#1734](https://github.com/kivy/python-for-android/pull/1734) ([AndreMiras](https://github.com/AndreMiras)) - Add missing arch to version code generator. [\#1733](https://github.com/kivy/python-for-android/pull/1733) ([OptimusGREEN](https://github.com/OptimusGREEN)) - Fix import in 'Using a PythonRecipe' doc [\#1731](https://github.com/kivy/python-for-android/pull/1731) ([b3b](https://github.com/b3b)) - Removed unnecessary genericndkbuild dependency from the flask recipe [\#1729](https://github.com/kivy/python-for-android/pull/1729) ([inclement](https://github.com/inclement)) - Add ci\_mode to disable download progress [\#1727](https://github.com/kivy/python-for-android/pull/1727) ([mkg20001](https://github.com/mkg20001)) - Zworkb services [\#1725](https://github.com/kivy/python-for-android/pull/1725) ([zworkb](https://github.com/zworkb)) - Fixes psycopg2 lib install dir, closes \#1711 [\#1723](https://github.com/kivy/python-for-android/pull/1723) ([AndreMiras](https://github.com/AndreMiras)) - Generate android version code accounting for arch and min sdk [\#1720](https://github.com/kivy/python-for-android/pull/1720) ([OptimusGREEN](https://github.com/OptimusGREEN)) - Fix sdl2\_image compile error when arch is x86 [\#1718](https://github.com/kivy/python-for-android/pull/1718) ([j-devel](https://github.com/j-devel)) - Fix compile error of recipe "android" for non-sdl bootstrap build [\#1715](https://github.com/kivy/python-for-android/pull/1715) ([j-devel](https://github.com/j-devel)) - Fix setenv\(\) of non-SDL2 bootstrap [\#1714](https://github.com/kivy/python-for-android/pull/1714) ([j-devel](https://github.com/j-devel)) - Update enum34, pyasn1and pyopenssl versions. [\#1713](https://github.com/kivy/python-for-android/pull/1713) ([rnixx](https://github.com/rnixx)) - Added warning for arguments containing carriage returns. This happens… [\#1712](https://github.com/kivy/python-for-android/pull/1712) ([Aralox](https://github.com/Aralox)) - Fix loadLibraries\(\) failing for 64bit arch [\#1701](https://github.com/kivy/python-for-android/pull/1701) ([j-devel](https://github.com/j-devel)) - Fixes pyleveldb recipe [\#1699](https://github.com/kivy/python-for-android/pull/1699) ([AndreMiras](https://github.com/AndreMiras)) - Make testapps use python3 per default and adapt to `blacklist-requirements` [\#1696](https://github.com/kivy/python-for-android/pull/1696) ([opacam](https://github.com/opacam)) - change dependency from jdk7 to jdk8 [\#1695](https://github.com/kivy/python-for-android/pull/1695) ([Meteorix](https://github.com/Meteorix)) - FIX: copy additional jar files into the correct libs directory [\#1693](https://github.com/kivy/python-for-android/pull/1693) ([OptimusGREEN](https://github.com/OptimusGREEN)) - SDL2\_image update to 2.0.4 and set kivy's version to master [\#1692](https://github.com/kivy/python-for-android/pull/1692) ([opacam](https://github.com/opacam)) - Use nativeSetenv\(\) provided by SDL2 and cleanups [\#1690](https://github.com/kivy/python-for-android/pull/1690) ([j-devel](https://github.com/j-devel)) - Rename misguided shadowing --blacklist to --blacklist-requirements [\#1689](https://github.com/kivy/python-for-android/pull/1689) ([etc0de](https://github.com/etc0de)) - Deleted kivent recipes from constants.py [\#1686](https://github.com/kivy/python-for-android/pull/1686) ([inclement](https://github.com/inclement)) - Downgrade to pycryptodome==3.6.3 [\#1685](https://github.com/kivy/python-for-android/pull/1685) ([AndreMiras](https://github.com/AndreMiras)) - Added option to support custom shared libraries with \ tag [\#1684](https://github.com/kivy/python-for-android/pull/1684) ([pax0r](https://github.com/pax0r)) - Implement --blacklist option and include more modules/recipes by default [\#1683](https://github.com/kivy/python-for-android/pull/1683) ([etc0de](https://github.com/etc0de)) - Origin [\#1681](https://github.com/kivy/python-for-android/pull/1681) ([strubbi77](https://github.com/strubbi77)) - Delete kivent recipes [\#1680](https://github.com/kivy/python-for-android/pull/1680) ([inclement](https://github.com/inclement)) - Fix zope\_interface and add python3 compatibility [\#1677](https://github.com/kivy/python-for-android/pull/1677) ([opacam](https://github.com/opacam)) - Recipe class unit tests [\#1676](https://github.com/kivy/python-for-android/pull/1676) ([AndreMiras](https://github.com/AndreMiras)) - Bumps netiffaces version & removes from broken list, refs \#1539 [\#1675](https://github.com/kivy/python-for-android/pull/1675) ([AndreMiras](https://github.com/AndreMiras)) - tox update and linter fixes [\#1674](https://github.com/kivy/python-for-android/pull/1674) ([AndreMiras](https://github.com/AndreMiras)) - tox update and linter fixes [\#1673](https://github.com/kivy/python-for-android/pull/1673) ([AndreMiras](https://github.com/AndreMiras)) - Delete the pygame bootstrap [\#1670](https://github.com/kivy/python-for-android/pull/1670) ([inclement](https://github.com/inclement)) - Fix various issues with graphs and recipes [\#1669](https://github.com/kivy/python-for-android/pull/1669) ([etc0de](https://github.com/etc0de)) - Fix setup.py install breaking due to unicode characters in README.md on Python 3 [\#1667](https://github.com/kivy/python-for-android/pull/1667) ([etc0de](https://github.com/etc0de)) - s/README.rst/README.md/ refs \#1664 [\#1666](https://github.com/kivy/python-for-android/pull/1666) ([AndreMiras](https://github.com/AndreMiras)) - Update and rename README.rst to README.md [\#1664](https://github.com/kivy/python-for-android/pull/1664) ([tito](https://github.com/tito)) - Defaults to post kivy==1.10.1 commit for SDL 2.0.9 fixes [\#1663](https://github.com/kivy/python-for-android/pull/1663) ([AndreMiras](https://github.com/AndreMiras)) - Rework opencv's recipe \(enable cv2.so and the extra opencv libraries\) [\#1661](https://github.com/kivy/python-for-android/pull/1661) ([opacam](https://github.com/opacam)) - Updated version to 0.7.1 [\#1660](https://github.com/kivy/python-for-android/pull/1660) ([inclement](https://github.com/inclement)) - \[WIP\] Run project's setup.py if present, unless --ignore-setup-py was set [\#1625](https://github.com/kivy/python-for-android/pull/1625) ([etc0de](https://github.com/etc0de)) ## [0.7.0](https://github.com/kivy/python-for-android/tree/0.7.0) (2019-02-01) [Full Changelog](https://github.com/kivy/python-for-android/compare/0.6.0...0.7.0) **Fixed bugs:** - python3 + openssl compilation fail [\#1590](https://github.com/kivy/python-for-android/issues/1590) - Update conditional build to python3 [\#1485](https://github.com/kivy/python-for-android/issues/1485) - building apk fails \( python 3 \) [\#746](https://github.com/kivy/python-for-android/issues/746) **Closed issues:** - 'Orientation' and 'Fullscreen' settings in spec file: Possible issue. [\#1655](https://github.com/kivy/python-for-android/issues/1655) - cffi UnicodeEncodeError: 'ascii' codec can't encode character '\u2018' [\#1654](https://github.com/kivy/python-for-android/issues/1654) - Create an app for testing p4a builds on the device [\#1630](https://github.com/kivy/python-for-android/issues/1630) - Build crashes if NDK is installed system-wide without write permissions [\#1621](https://github.com/kivy/python-for-android/issues/1621) - libFFI recipe doesn't work with clang [\#1612](https://github.com/kivy/python-for-android/issues/1612) - How do I use this software? [\#1606](https://github.com/kivy/python-for-android/issues/1606) - no work [\#1599](https://github.com/kivy/python-for-android/issues/1599) - python2legacy - various warnings then no valid dependency graphs [\#1582](https://github.com/kivy/python-for-android/issues/1582) - pyjnius import crash / TypeError [\#1578](https://github.com/kivy/python-for-android/issues/1578) - reportlab broken, possibly using wrong python header or compiler flags with python 3? [\#1575](https://github.com/kivy/python-for-android/issues/1575) - p4a apk crash "IndexError: list index out of range" [\#1570](https://github.com/kivy/python-for-android/issues/1570) - Libffi build fail on Mac [\#1569](https://github.com/kivy/python-for-android/issues/1569) - All the python functions can run on kivy [\#1567](https://github.com/kivy/python-for-android/issues/1567) - --force-build is --force\_build in the docs [\#1565](https://github.com/kivy/python-for-android/issues/1565) - apk with sqlite3 python3 kivy No module named '\_sqlite3' [\#1564](https://github.com/kivy/python-for-android/issues/1564) - Android crash on run, Fatal signal 6 \(SIGABRT\), code -6 in tid 10265 \(SDLThread\), avc: denied { search } [\#1562](https://github.com/kivy/python-for-android/issues/1562) - Minor exclude extensions code simplification [\#1560](https://github.com/kivy/python-for-android/issues/1560) - SSLError python 3.7.1 [\#1559](https://github.com/kivy/python-for-android/issues/1559) - Error message claiming conflicting dependencies when requesting a recipe \(here, `python3`\) that was not \(yet\) available [\#1557](https://github.com/kivy/python-for-android/issues/1557) - The virtual machine \(VM 0.5\) does not collect packages with dependencies, except Python and Kivy [\#1542](https://github.com/kivy/python-for-android/issues/1542) - raise exc [\#1540](https://github.com/kivy/python-for-android/issues/1540) - raise exc sh.ErrorReturnCode\_2: [\#1538](https://github.com/kivy/python-for-android/issues/1538) - How to build other ABI versions of "libcrypto.so" and "libssl.so"? [\#1536](https://github.com/kivy/python-for-android/issues/1536) - How to build other versions of ABI? [\#1535](https://github.com/kivy/python-for-android/issues/1535) - No module named sh [\#1531](https://github.com/kivy/python-for-android/issues/1531) - p4a crash "Couldn't find the built APK" [\#1530](https://github.com/kivy/python-for-android/issues/1530) - UnicodeEncodeError in logger.py [\#1529](https://github.com/kivy/python-for-android/issues/1529) - Hello, is there a Chinese document? It is still very difficult for me to read the document after using Google Translate. [\#1527](https://github.com/kivy/python-for-android/issues/1527) - ValueError: storage dir path cannot contain spaces, please specify a path with --storage-dir [\#1526](https://github.com/kivy/python-for-android/issues/1526) - Build fails: Could not find com.android.tools.lint:lint-gradle:26.1.4 [\#1520](https://github.com/kivy/python-for-android/issues/1520) - App doesn't support pause mode' when using on\_pause method [\#1518](https://github.com/kivy/python-for-android/issues/1518) - Comprehensive list of broken python3 recipes [\#1514](https://github.com/kivy/python-for-android/issues/1514) - BUILD FAILURE: No main.py\(o\) found in your app directory. [\#1510](https://github.com/kivy/python-for-android/issues/1510) - testapp\_flask doesn't build with webview bootstrap, final bootstrap compiler options appear to have some sort of issue [\#1509](https://github.com/kivy/python-for-android/issues/1509) - Bootstrap detection for "service\_only" and "webview" is broken - always picks sdl2 [\#1508](https://github.com/kivy/python-for-android/issues/1508) - p4a latest master / Python 2.7 / API target 28 / NDK 21 / openjdk8 crashes during gradle step [\#1506](https://github.com/kivy/python-for-android/issues/1506) - --requirements=android gives crash [\#1504](https://github.com/kivy/python-for-android/issues/1504) - pyjnius build failure with NDK r17c, NDK 21, SDK 28 [\#1502](https://github.com/kivy/python-for-android/issues/1502) - Need libpython3.7m.so.1.0 in android phone [\#1501](https://github.com/kivy/python-for-android/issues/1501) - Tech debt: webview, pygame and service\_only bootstraps are hardwired to Python 2.7 [\#1497](https://github.com/kivy/python-for-android/issues/1497) - ImportError: No module named android [\#1492](https://github.com/kivy/python-for-android/issues/1492) - p4a --requirements ignores absolute folder paths [\#1487](https://github.com/kivy/python-for-android/issues/1487) - Set the api level from 19 to 28 failed [\#1482](https://github.com/kivy/python-for-android/issues/1482) - libxml2 build broken on latest p4a master with python 3 [\#1479](https://github.com/kivy/python-for-android/issues/1479) - working: make: \*\*\* \[Makefile\:426\: sharedmods\] Error 139 [\#1474](https://github.com/kivy/python-for-android/issues/1474) - .pxd files of dependency targeted recipe'd library not found [\#1473](https://github.com/kivy/python-for-android/issues/1473) - Python 3 recipe follow up issues [\#1455](https://github.com/kivy/python-for-android/issues/1455) - Python-4-Android NumPy error: 'struct lconv' has no member named 'decimal\_point' [\#1450](https://github.com/kivy/python-for-android/issues/1450) - Update to latest SDL required for proper key handling [\#1449](https://github.com/kivy/python-for-android/issues/1449) - socket.getaddrinfo appears to be completely broken, all name resolutions fail [\#1447](https://github.com/kivy/python-for-android/issues/1447) - lxml recipe doesn't work with Python 3 [\#1445](https://github.com/kivy/python-for-android/issues/1445) - C Compiler can not create executables [\#1436](https://github.com/kivy/python-for-android/issues/1436) - Unable to create APK [\#1434](https://github.com/kivy/python-for-android/issues/1434) - Destroying SDL\_Renderer in SDL\_APP\_DIDENTERBACKGROUND \(with intention of recreating it\) will lead to crash [\#1425](https://github.com/kivy/python-for-android/issues/1425) - AndroidManifest.xml.tmpl should set screenLayout & smallScreenSize [\#1422](https://github.com/kivy/python-for-android/issues/1422) - pyjnius really should be version pinned. [\#1415](https://github.com/kivy/python-for-android/issues/1415) - p4a git master pyjnius build breaks [\#1414](https://github.com/kivy/python-for-android/issues/1414) - enaml recipe compilation fails [\#1409](https://github.com/kivy/python-for-android/issues/1409) - Cython projects that don't need any special option should work without a CythonRecipe [\#1406](https://github.com/kivy/python-for-android/issues/1406) - libpq recipe compilation fails [\#1405](https://github.com/kivy/python-for-android/issues/1405) - cryptography + python3crystax fails [\#1404](https://github.com/kivy/python-for-android/issues/1404) - Comprehensive list of broken recipes [\#1402](https://github.com/kivy/python-for-android/issues/1402) - ifaddrs compilation error [\#1398](https://github.com/kivy/python-for-android/issues/1398) - Python project build Buildozer issue - Please define SDL\_JAVA\_PACKAGE\_PATH to the path of your Java package with dots replaced with underscores [\#1391](https://github.com/kivy/python-for-android/issues/1391) - error: kivy/graphics/texture.c: No such file or directory [\#1384](https://github.com/kivy/python-for-android/issues/1384) - Investigate conditional builds [\#1382](https://github.com/kivy/python-for-android/issues/1382) - Unit test recipes \(reportlab to begin with\) [\#1380](https://github.com/kivy/python-for-android/issues/1380) - There is insufficient memory for the Java Runtime Environment to continue [\#1373](https://github.com/kivy/python-for-android/issues/1373) - SSL/TLS is broken with Python 3: ImportError: missing module \_ssl [\#1372](https://github.com/kivy/python-for-android/issues/1372) - Buildozer fail to build numpy recipe [\#1369](https://github.com/kivy/python-for-android/issues/1369) - Buildozer weird error\(Still trying to use Kivy only\) [\#1368](https://github.com/kivy/python-for-android/issues/1368) - Error when trying to use numpy \(Broken Toolchain\) [\#1367](https://github.com/kivy/python-for-android/issues/1367) - java.lang.UnsatisfiedLinkError: No implementation found for void org.libsdl.app.SDLActivity.nativeQuit\(\) \(tried Java\_org\_libsdl\_app\_SDLActivity\_nativeQuit and Java\_org\_libsdl\_app\_SDLActivity\_nativeQuit\_\_\) [\#1365](https://github.com/kivy/python-for-android/issues/1365) - Migrate away from ndk\_build? [\#1362](https://github.com/kivy/python-for-android/issues/1362) - Fix/clean-up LDSHARED [\#1360](https://github.com/kivy/python-for-android/issues/1360) - buildozer error: no module named kivy [\#1354](https://github.com/kivy/python-for-android/issues/1354) - Spurious nullpointer crash on app resume [\#1353](https://github.com/kivy/python-for-android/issues/1353) - Docs say ANDROIDAPI=19 sets minimum API level, but it sets target API level [\#1352](https://github.com/kivy/python-for-android/issues/1352) - Bug or support request? [\#1346](https://github.com/kivy/python-for-android/issues/1346) - Issue with not finding JNI and nativeSetEnv in SDLActivity [\#1344](https://github.com/kivy/python-for-android/issues/1344) - python-for-android packages wrong manifest for ANDROIDAPI="19", doesn't include configChanges="...|screenSize" which leads to app crash on rotation [\#1342](https://github.com/kivy/python-for-android/issues/1342) - python2: jpeg recipe broken due to missing libcutils [\#1341](https://github.com/kivy/python-for-android/issues/1341) - Orientation change causes bogus SDL\_QUIT and SDL\_APP\_TERMINATING events [\#1338](https://github.com/kivy/python-for-android/issues/1338) - ctypes.util.find\_library doesn't work with python 3 [\#1337](https://github.com/kivy/python-for-android/issues/1337) - 'import lzma' fails with Python 3 [\#1336](https://github.com/kivy/python-for-android/issues/1336) - Read & write to entire SD card is an unreasonable default permission for most games and basic UI applications [\#1335](https://github.com/kivy/python-for-android/issues/1335) - Build failed [\#1333](https://github.com/kivy/python-for-android/issues/1333) - Auto-close awaiting-reply labeled issues [\#1331](https://github.com/kivy/python-for-android/issues/1331) - App crashes when sending POST request http [\#1329](https://github.com/kivy/python-for-android/issues/1329) - kivy build error with python3crystax [\#1328](https://github.com/kivy/python-for-android/issues/1328) - No "pil" or "pillow" available for Python 3 [\#1326](https://github.com/kivy/python-for-android/issues/1326) - pillow fails on import [\#1325](https://github.com/kivy/python-for-android/issues/1325) - Unavoidable PySDL2 crash on app resume [\#1323](https://github.com/kivy/python-for-android/issues/1323) - Tons of different PySDL2 crashes when tabbing in/out of application during loading or right after it finished [\#1321](https://github.com/kivy/python-for-android/issues/1321) - Build doesn't pick up gradle even though it is present, tries using ant instead and fails [\#1320](https://github.com/kivy/python-for-android/issues/1320) - Crystax NDK size is larger than Android Studio + SDK + \(regular\) NDK + ... combined [\#1319](https://github.com/kivy/python-for-android/issues/1319) - Generated md5sum does not match expected md5sum for sdl2 recipe [\#1318](https://github.com/kivy/python-for-android/issues/1318) - It doesn't work about "android.activity.bind\(on\_new\_intent=myFunc\)",need help,thanks [\#1317](https://github.com/kivy/python-for-android/issues/1317) - CMake error with OpenCV [\#1315](https://github.com/kivy/python-for-android/issues/1315) - libpython2.7.so: is missing DT\_SONAME using buildozer with latest VirtualBox VM for Android Buildozer \(Version 2.0, released the 13 May 2017\). [\#1314](https://github.com/kivy/python-for-android/issues/1314) - On Virtual Machine, this error arises when openCV is filled in the requirement in the spec file [\#1313](https://github.com/kivy/python-for-android/issues/1313) - p4a apk command results in: No such file or directory: 'src/main/assets/private.mp3' [\#1312](https://github.com/kivy/python-for-android/issues/1312) - Build error to import shapely \(libgeos\) [\#1311](https://github.com/kivy/python-for-android/issues/1311) - "/data/user/0/org.gmail.gmail/files/app/libpymodules.so" not found [\#1309](https://github.com/kivy/python-for-android/issues/1309) - numpy Python3/Crystax broken toolchain can't link a simple C program [\#1303](https://github.com/kivy/python-for-android/issues/1303) - Screen Rotation and Re-layout [\#1302](https://github.com/kivy/python-for-android/issues/1302) - IOError: \[Errno socket error\] \[Errno 104\] Connection reset by peer [\#1301](https://github.com/kivy/python-for-android/issues/1301) - Python2 Build fails with make: \*\*\* \[Makefile\:426\: sharedmods\] Error 139 [\#1297](https://github.com/kivy/python-for-android/issues/1297) - ffmpeg breaks buildozer android debug compilation process. [\#1294](https://github.com/kivy/python-for-android/issues/1294) - Android: python startup complaining about missing hashlib functions [\#1293](https://github.com/kivy/python-for-android/issues/1293) - sh.CommandNotFound: ndk\_build [\#1292](https://github.com/kivy/python-for-android/issues/1292) - SDL Error: ... could not load library "libpython2.7.so" ... on Android 4.2.2 [\#1290](https://github.com/kivy/python-for-android/issues/1290) - Facing issues in making webbrowser.open\(url\) work [\#1287](https://github.com/kivy/python-for-android/issues/1287) - Travis download caching [\#1280](https://github.com/kivy/python-for-android/issues/1280) - When you fix the error "error: \[Errno 2\] No such file or directory: 'src/main/assets/private.mp3'" [\#1279](https://github.com/kivy/python-for-android/issues/1279) - Little typo [\#1275](https://github.com/kivy/python-for-android/issues/1275) - Fix numpy x86 build using \#1252 [\#1274](https://github.com/kivy/python-for-android/issues/1274) - Some phones don't allow access to /sdcard [\#1272](https://github.com/kivy/python-for-android/issues/1272) - Kivy Android app running in background crashes when intent tries to pull it to top [\#1271](https://github.com/kivy/python-for-android/issues/1271) - Sometimes sdl2 - UnicodeDecodeError: 'utf-8' codec can't decode byte 0x98 [\#1270](https://github.com/kivy/python-for-android/issues/1270) - App crash when connecting to mysql [\#1269](https://github.com/kivy/python-for-android/issues/1269) - \[wishlist\] Android launcher: Please build with Python 3 [\#1268](https://github.com/kivy/python-for-android/issues/1268) - Opencv doesn't work on kivy ImportError: dlopen failed: "/data/data/com.mydomain.myapp/files/\_applibs/cv2/cv2.so" is 64-bit instead of 32-bit [\#1267](https://github.com/kivy/python-for-android/issues/1267) - Why an error occurs 'python-for-android cannot continue; aborting'? [\#1266](https://github.com/kivy/python-for-android/issues/1266) - Internet connection impossible with kivy app on android [\#1265](https://github.com/kivy/python-for-android/issues/1265) - python-for-android recipe tests [\#1263](https://github.com/kivy/python-for-android/issues/1263) - When the apk is turned on it gives me an error in the hashlib python3.6 [\#1260](https://github.com/kivy/python-for-android/issues/1260) - fail to build application ERROR 'WindowInfoX11' is not a type identifier [\#1259](https://github.com/kivy/python-for-android/issues/1259) - Buildozer command failed [\#1258](https://github.com/kivy/python-for-android/issues/1258) - IOError: \[Errno 2\] No such file or directory: 'src/main/assets/private.mp3' [\#1257](https://github.com/kivy/python-for-android/issues/1257) - Prebuilt python does not contain binaries for any architecture. [\#1254](https://github.com/kivy/python-for-android/issues/1254) - Didn't find any valid dependency graphs. - Flask and websocket-client [\#1253](https://github.com/kivy/python-for-android/issues/1253) - assertion PyBytes\_Check failed [\#1247](https://github.com/kivy/python-for-android/issues/1247) - Issue of AttributeError [\#1246](https://github.com/kivy/python-for-android/issues/1246) - Python3 + greenlet install issue [\#1245](https://github.com/kivy/python-for-android/issues/1245) - Confusing / Outdated Bootstraps [\#1244](https://github.com/kivy/python-for-android/issues/1244) - openSSL recipe uses system-wide headers; app fails to run: cannot locate symbol EC\_curve\_nist2nid [\#1243](https://github.com/kivy/python-for-android/issues/1243) - APK Immediately Closes After Opening in Debug, Release, and Zipaligned & Signed Versions [\#1242](https://github.com/kivy/python-for-android/issues/1242) - Custom Recipes For Pandas, Matplotlib and Statsmodels [\#1241](https://github.com/kivy/python-for-android/issues/1241) - WebView.setWebContentsDebuggingEnabled [\#1240](https://github.com/kivy/python-for-android/issues/1240) - amreabi-v7a build cannot find SDL\_GetTicks\(\) [\#1239](https://github.com/kivy/python-for-android/issues/1239) - x86 inline assembly fails to build [\#1238](https://github.com/kivy/python-for-android/issues/1238) - Can't compile dependency in 32bit on 64bit system [\#1237](https://github.com/kivy/python-for-android/issues/1237) - Cannot import name 'uname' on Windows [\#1234](https://github.com/kivy/python-for-android/issues/1234) - Uses Arm builds for x86, if Arm builds already exist [\#1233](https://github.com/kivy/python-for-android/issues/1233) - The sh Python module could not be found [\#1232](https://github.com/kivy/python-for-android/issues/1232) - Issue with Android API 23+ [\#1231](https://github.com/kivy/python-for-android/issues/1231) - Failed to build application: 'WindowInfoX11' is not a type identifier [\#1230](https://github.com/kivy/python-for-android/issues/1230) - Missing arm-linux-androideabi-gcc [\#1229](https://github.com/kivy/python-for-android/issues/1229) - Android build issues: raise CommandNotFound\(path\) [\#1228](https://github.com/kivy/python-for-android/issues/1228) - TextInput display text only when suggestion validate on Asus ZenPhone3 [\#1227](https://github.com/kivy/python-for-android/issues/1227) - on\_stop not called on Android [\#1226](https://github.com/kivy/python-for-android/issues/1226) - The python3crystax recipe can only be built when using the CrystaX NDK. [\#1225](https://github.com/kivy/python-for-android/issues/1225) - Didn't find any valid dependency graphs. [\#1222](https://github.com/kivy/python-for-android/issues/1222) - Google requiring API Target 26 in Aug/Nov 2018 [\#1219](https://github.com/kivy/python-for-android/issues/1219) - p4a can't find android sdk [\#1218](https://github.com/kivy/python-for-android/issues/1218) - IOError: \[Errno 2\] No such file or directory: u'/Users/gauravgupta/kivy/.buildozer/android/platform/build/dists/myellipse/build/outputs/apk/myellipse-debug.apk' [\#1216](https://github.com/kivy/python-for-android/issues/1216) - Buildozer android application crashes on android [\#1215](https://github.com/kivy/python-for-android/issues/1215) - Multiple issues with latest Android SDK [\#1212](https://github.com/kivy/python-for-android/issues/1212) - sdkmanager doesn't exist [\#1210](https://github.com/kivy/python-for-android/issues/1210) - add-jar doesn't work [\#1208](https://github.com/kivy/python-for-android/issues/1208) - Kivy not working on Sony devices [\#1206](https://github.com/kivy/python-for-android/issues/1206) - sqlite pre-populated database not being found [\#1203](https://github.com/kivy/python-for-android/issues/1203) - Try python3.7 with Google NDK [\#1202](https://github.com/kivy/python-for-android/issues/1202) - commit 3534a761 [\#1200](https://github.com/kivy/python-for-android/issues/1200) - Kivy basic button program doesn't work [\#1199](https://github.com/kivy/python-for-android/issues/1199) - Error Pythonforandroid.toolchain -m [\#1196](https://github.com/kivy/python-for-android/issues/1196) - assert keyword do not work [\#1193](https://github.com/kivy/python-for-android/issues/1193) - macOS High Siera installation issue [\#1192](https://github.com/kivy/python-for-android/issues/1192) - failed to setup p4a on ubuntu \(multiple issues\) [\#1191](https://github.com/kivy/python-for-android/issues/1191) - Cryptography woes: Can we freshen up our Python version... [\#1190](https://github.com/kivy/python-for-android/issues/1190) - Kivy crashes immediately on start, on Sony devices [\#1188](https://github.com/kivy/python-for-android/issues/1188) - UnicodeDecodeError [\#1187](https://github.com/kivy/python-for-android/issues/1187) - \[INFO\]: Building with ant, as no gradle executable detected [\#1186](https://github.com/kivy/python-for-android/issues/1186) - Local recipes can not be patched any longer [\#1185](https://github.com/kivy/python-for-android/issues/1185) - Cymunk build fail on python3crystax [\#1184](https://github.com/kivy/python-for-android/issues/1184) - p4a doesn't handle runtime permissions [\#1183](https://github.com/kivy/python-for-android/issues/1183) - Android app freeze on screen rotation \(again?\) [\#1179](https://github.com/kivy/python-for-android/issues/1179) - custom java class [\#1177](https://github.com/kivy/python-for-android/issues/1177) - Dockerfile [\#1175](https://github.com/kivy/python-for-android/issues/1175) - python2 recipe always builds for armeabi regardless of what arch you tell it to target [\#1174](https://github.com/kivy/python-for-android/issues/1174) - The webview bootstrap does not support gradle [\#1172](https://github.com/kivy/python-for-android/issues/1172) - 0.6 release checklist [\#1170](https://github.com/kivy/python-for-android/issues/1170) - python 2.7 compile with NDK 15c [\#1169](https://github.com/kivy/python-for-android/issues/1169) - Reopen running instance instead of starting a new one upon tapping app icon [\#1161](https://github.com/kivy/python-for-android/issues/1161) - python3 incompatibility [\#1154](https://github.com/kivy/python-for-android/issues/1154) - ffi.h: No such file or directory \(solutions included\) [\#1148](https://github.com/kivy/python-for-android/issues/1148) - After building FFMpeg recipe, I still am not able to do ffmpeg -v [\#1146](https://github.com/kivy/python-for-android/issues/1146) - Kivy/Buildozer/Psycopg2 [\#1144](https://github.com/kivy/python-for-android/issues/1144) - SDL Error: Error Could not load any libpythonXXX.so [\#1142](https://github.com/kivy/python-for-android/issues/1142) - Can't build numpy with current master, python 2, NDK 15 [\#1141](https://github.com/kivy/python-for-android/issues/1141) - pyopenssl cryptography dependence [\#1127](https://github.com/kivy/python-for-android/issues/1127) - Check if SDL2 libraries are up to date [\#1126](https://github.com/kivy/python-for-android/issues/1126) - bind recipes to well defined versions [\#1115](https://github.com/kivy/python-for-android/issues/1115) - pil and pillow modules for python3 [\#1114](https://github.com/kivy/python-for-android/issues/1114) - Kivy python android build issue? [\#1110](https://github.com/kivy/python-for-android/issues/1110) - simple flask app on android fails to start [\#1108](https://github.com/kivy/python-for-android/issues/1108) - "crystax\_python does not exist" with python3crystax [\#1105](https://github.com/kivy/python-for-android/issues/1105) - Running on Android 4.0 doesn't work when building for target api 19 [\#1104](https://github.com/kivy/python-for-android/issues/1104) - Can't type anything into textinput using new toolchain [\#1102](https://github.com/kivy/python-for-android/issues/1102) - "android" recipe isn't compatible with Python 3 [\#1093](https://github.com/kivy/python-for-android/issues/1093) - Recipe does not exist: matplotlib [\#1090](https://github.com/kivy/python-for-android/issues/1090) - Django App is not running. Web View does not load it [\#1083](https://github.com/kivy/python-for-android/issues/1083) - Android 7 complains about Kivy 1.10.0 apps: "detected problems with app native libraries" [\#1078](https://github.com/kivy/python-for-android/issues/1078) - Numpy recipe broken \(atlas, blas, lapack, -lcrystax\) [\#1074](https://github.com/kivy/python-for-android/issues/1074) - requests module not compiling in buildozer when used with Python3crystax [\#1072](https://github.com/kivy/python-for-android/issues/1072) - ImportError: No module named audioop [\#1067](https://github.com/kivy/python-for-android/issues/1067) - sqlite3 not working with android\_new [\#1053](https://github.com/kivy/python-for-android/issues/1053) - dlopen failed: python2.7/site-packages/grpc/\_cython/cygrpc.so not 32-bit: 2 [\#1052](https://github.com/kivy/python-for-android/issues/1052) - Can't start service app [\#1049](https://github.com/kivy/python-for-android/issues/1049) - Cannot build APK with python3crystax and flask - conflicting dependencies [\#1041](https://github.com/kivy/python-for-android/issues/1041) - Slow build process since sh 1.12.5 [\#1038](https://github.com/kivy/python-for-android/issues/1038) - Python3 + PyYaml conflict [\#1031](https://github.com/kivy/python-for-android/issues/1031) - Can't write ti SD-card on Android 6.0.1 [\#1024](https://github.com/kivy/python-for-android/issues/1024) - pygame\_sdl2 compile failure \# include \ [\#1023](https://github.com/kivy/python-for-android/issues/1023) - Build error on Mac: no archive symbol table \(run ranlib\) [\#1012](https://github.com/kivy/python-for-android/issues/1012) - Shouldn't P4A Raise Exception On User File Having Syntax Error [\#1009](https://github.com/kivy/python-for-android/issues/1009) - jnius is not working with webview bootstrap [\#1003](https://github.com/kivy/python-for-android/issues/1003) - Built APK fails with ImportError: dlopen failed on \_clock.so [\#998](https://github.com/kivy/python-for-android/issues/998) - apk not build using crystax NDK [\#992](https://github.com/kivy/python-for-android/issues/992) - Create a space for common bootstrap code along with a base class for all bootstraps [\#988](https://github.com/kivy/python-for-android/issues/988) - Unpacking and copying app contents causes app to appear hung [\#983](https://github.com/kivy/python-for-android/issues/983) - kivy app crashing on launch [\#982](https://github.com/kivy/python-for-android/issues/982) - Android Emulator support [\#979](https://github.com/kivy/python-for-android/issues/979) - Kivy with SDL2 bootstrap crashes on pausing if app doesn't support pause mode [\#978](https://github.com/kivy/python-for-android/issues/978) - sqlite3 recipe not working with new toolchain [\#977](https://github.com/kivy/python-for-android/issues/977) - lxml is needed in new toolchain [\#976](https://github.com/kivy/python-for-android/issues/976) - P4A wants to start "ant" without using full SDK path [\#974](https://github.com/kivy/python-for-android/issues/974) - API automatic lookup doesn't use available SDK API [\#973](https://github.com/kivy/python-for-android/issues/973) - JNI ERROR \(app bug\): local reference table overflow \(max=512\) [\#971](https://github.com/kivy/python-for-android/issues/971) - Kivy with SDL2 bootstrap crushes on resuming in some cases [\#967](https://github.com/kivy/python-for-android/issues/967) - Could not ping localhost:5000 [\#961](https://github.com/kivy/python-for-android/issues/961) - Not a valid ELF executable [\#957](https://github.com/kivy/python-for-android/issues/957) - How to completely remove installed app? [\#953](https://github.com/kivy/python-for-android/issues/953) - ImportError android [\#943](https://github.com/kivy/python-for-android/issues/943) - Older android version can't load libraries properly [\#925](https://github.com/kivy/python-for-android/issues/925) - sed: 1: "Modules/Setup.local": invalid command code M [\#924](https://github.com/kivy/python-for-android/issues/924) - Python3: armeabi used to copy, but armeabi-v7a chosen [\#913](https://github.com/kivy/python-for-android/issues/913) - ImportError for sqlite3 [\#910](https://github.com/kivy/python-for-android/issues/910) - PyGame backend: error while using android.copy\_libs = 1 [\#888](https://github.com/kivy/python-for-android/issues/888) - pytz installation works, but requires user to make build folder manually [\#884](https://github.com/kivy/python-for-android/issues/884) - Numpy support w/ python3crystax [\#882](https://github.com/kivy/python-for-android/issues/882) - Scipy recipe [\#874](https://github.com/kivy/python-for-android/issues/874) - opencv recipe build error [\#871](https://github.com/kivy/python-for-android/issues/871) - Flask with Python3 does not seem to work. [\#870](https://github.com/kivy/python-for-android/issues/870) - p4a generates deprecated code under Android API 23 [\#864](https://github.com/kivy/python-for-android/issues/864) - Kivy builds failing [\#861](https://github.com/kivy/python-for-android/issues/861) - error when running an apk compiled with python3crystax [\#859](https://github.com/kivy/python-for-android/issues/859) - my application using ctypes crashes on Kivy 1.9.2 and not on 1.8 [\#858](https://github.com/kivy/python-for-android/issues/858) - apk, built with openssl launch error: "libssl1.0.2h.so" not found [\#850](https://github.com/kivy/python-for-android/issues/850) - Can't install on Windows using pip [\#819](https://github.com/kivy/python-for-android/issues/819) - FFmpeg recipe broken [\#810](https://github.com/kivy/python-for-android/issues/810) - Todo: add rebuild-dist option [\#807](https://github.com/kivy/python-for-android/issues/807) - p4a create fails if cython is installed in ~/.local [\#771](https://github.com/kivy/python-for-android/issues/771) - Completely clean install of minimal application fails to launch on Android 6 [\#752](https://github.com/kivy/python-for-android/issues/752) - "NoBackendError: No backend available": Pyusb recipe for android [\#740](https://github.com/kivy/python-for-android/issues/740) - app crash on close [\#734](https://github.com/kivy/python-for-android/issues/734) - App crash when changing orientation [\#730](https://github.com/kivy/python-for-android/issues/730) - Default extraction of NDK version not compatible with most recent stable NDK release... [\#723](https://github.com/kivy/python-for-android/issues/723) - Enabling SSL for python3.5 using crystax [\#705](https://github.com/kivy/python-for-android/issues/705) - Need to set locale env variable for python3 package recipes [\#703](https://github.com/kivy/python-for-android/issues/703) - static jfieldID xxx not valid for class java.lang.Class\ ImportError: cannot import name core [\#288](https://github.com/kivy/python-for-android/issues/288) - Python build for android fails - cp: cannot stat ‘HOSTPYTHON=/home/inderpal/python-for-android/build/python/Python-2.7.2/hostpython’: No such file or directory [\#286](https://github.com/kivy/python-for-android/issues/286) - Use Debian's Python packages for ARM instead of cross-compiling? [\#242](https://github.com/kivy/python-for-android/issues/242) - Feature request: Possibility to choose the sensors' delay [\#207](https://github.com/kivy/python-for-android/issues/207) - Problems with posixpath [\#188](https://github.com/kivy/python-for-android/issues/188) - Pure Python Module: flufl.i18n fails to load when installed as a pure python module. [\#182](https://github.com/kivy/python-for-android/issues/182) - socket.AF\_UNIX is not supported [\#163](https://github.com/kivy/python-for-android/issues/163) - Recipe for pyzmq \($25 bounty\) \[$25\] [\#122](https://github.com/kivy/python-for-android/issues/122) **Merged pull requests:** - Updated version to 0.7.0 [\#1659](https://github.com/kivy/python-for-android/pull/1659) ([inclement](https://github.com/inclement)) - Updates broken recipes list, refs \#1514 [\#1658](https://github.com/kivy/python-for-android/pull/1658) ([AndreMiras](https://github.com/AndreMiras)) - Feature/ticket1654 cffi unicode encode error [\#1656](https://github.com/kivy/python-for-android/pull/1656) ([AndreMiras](https://github.com/AndreMiras)) - Speed up Docker chown via COPY parameter [\#1652](https://github.com/kivy/python-for-android/pull/1652) ([AndreMiras](https://github.com/AndreMiras)) - Speed up Python and NumPy compilation process [\#1651](https://github.com/kivy/python-for-android/pull/1651) ([AndreMiras](https://github.com/AndreMiras)) - Fixes opencv compilation, fixes \#1313 [\#1650](https://github.com/kivy/python-for-android/pull/1650) ([AndreMiras](https://github.com/AndreMiras)) - Remove unused variable in archs.py [\#1649](https://github.com/kivy/python-for-android/pull/1649) ([opacam](https://github.com/opacam)) - Fix linux hardcoded entry in archs.py [\#1648](https://github.com/kivy/python-for-android/pull/1648) ([opacam](https://github.com/opacam)) - Made the activity launch mode default to singleTask [\#1646](https://github.com/kivy/python-for-android/pull/1646) ([inclement](https://github.com/inclement)) - Made build.py stop running if compileall failed [\#1645](https://github.com/kivy/python-for-android/pull/1645) ([inclement](https://github.com/inclement)) - Retry on download hiccups, refs \#1306 [\#1643](https://github.com/kivy/python-for-android/pull/1643) ([AndreMiras](https://github.com/AndreMiras)) - Set $LANG in PythonRecipe [\#1642](https://github.com/kivy/python-for-android/pull/1642) ([inclement](https://github.com/inclement)) - Remove old toolchain doc and add short note about overriding recipe sources [\#1641](https://github.com/kivy/python-for-android/pull/1641) ([inclement](https://github.com/inclement)) - Added separate module for checking user SDK, NDK, API etc. [\#1640](https://github.com/kivy/python-for-android/pull/1640) ([inclement](https://github.com/inclement)) - Added app for on-device unit tests [\#1636](https://github.com/kivy/python-for-android/pull/1636) ([inclement](https://github.com/inclement)) - Revert use of shlex.quote to avoid problems with python 2 [\#1635](https://github.com/kivy/python-for-android/pull/1635) ([etc0de](https://github.com/etc0de)) - Default Travis builds to Python3 [\#1634](https://github.com/kivy/python-for-android/pull/1634) ([AndreMiras](https://github.com/AndreMiras)) - Fixes ifaddrs recipe, closes \#1398 [\#1633](https://github.com/kivy/python-for-android/pull/1633) ([AndreMiras](https://github.com/AndreMiras)) - Do not verbose the "tar tf" command [\#1631](https://github.com/kivy/python-for-android/pull/1631) ([AndreMiras](https://github.com/AndreMiras)) - psycopg2 recipe fixes and doc, fixes \#1405 [\#1629](https://github.com/kivy/python-for-android/pull/1629) ([AndreMiras](https://github.com/AndreMiras)) - Use enaml {version} rather than master, fixes \#1409 [\#1628](https://github.com/kivy/python-for-android/pull/1628) ([AndreMiras](https://github.com/AndreMiras)) - Clean-up LDSHARED, fixes \#1360 [\#1627](https://github.com/kivy/python-for-android/pull/1627) ([AndreMiras](https://github.com/AndreMiras)) - Fix ctypes.util.find\_library\(\) not finding any libraries on Android [\#1624](https://github.com/kivy/python-for-android/pull/1624) ([etc0de](https://github.com/etc0de)) - Fix librt recipe requires that NDK folder is writable [\#1623](https://github.com/kivy/python-for-android/pull/1623) ([etc0de](https://github.com/etc0de)) - Update of Recipes for python3 test [\#1622](https://github.com/kivy/python-for-android/pull/1622) ([strubbi77](https://github.com/strubbi77)) - - let cymunk also be built with python3 recipe [\#1620](https://github.com/kivy/python-for-android/pull/1620) ([maho](https://github.com/maho)) - Make python flags to be absolute paths for Android.mk files [\#1619](https://github.com/kivy/python-for-android/pull/1619) ([opacam](https://github.com/opacam)) - Create a `dumb` librt recipe and refactor the affected recipes to make use of this [\#1618](https://github.com/kivy/python-for-android/pull/1618) ([opacam](https://github.com/opacam)) - Made recipe graph resolution respect opt\_depends [\#1617](https://github.com/kivy/python-for-android/pull/1617) ([inclement](https://github.com/inclement)) - Fix C code being wrong for python2 in start.c \(char \* to wchar\_t \*\) [\#1616](https://github.com/kivy/python-for-android/pull/1616) ([opacam](https://github.com/opacam)) - Removed argument to cp that doesn't exist on macOS [\#1614](https://github.com/kivy/python-for-android/pull/1614) ([inclement](https://github.com/inclement)) - Fix incorrect site-packages path breaking keyboard test app at runtime [\#1610](https://github.com/kivy/python-for-android/pull/1610) ([etc0de](https://github.com/etc0de)) - Fix libffi/ctypes - wrong libffi headers when building python [\#1609](https://github.com/kivy/python-for-android/pull/1609) ([opacam](https://github.com/opacam)) - Fix getting empty "modules" directory when arch is not armeabi-v7a [\#1608](https://github.com/kivy/python-for-android/pull/1608) ([j-devel](https://github.com/j-devel)) - Fix strip in bootstrap [\#1607](https://github.com/kivy/python-for-android/pull/1607) ([j-devel](https://github.com/j-devel)) - Conditional build script fixes [\#1604](https://github.com/kivy/python-for-android/pull/1604) ([AndreMiras](https://github.com/AndreMiras)) - Migrates greenlet to new python3 recipe, fixes \#1245 [\#1603](https://github.com/kivy/python-for-android/pull/1603) ([AndreMiras](https://github.com/AndreMiras)) - Fix sdk license error for travis tests \(CI\) [\#1602](https://github.com/kivy/python-for-android/pull/1602) ([opacam](https://github.com/opacam)) - \[WIP\] Restores the ability to compile the python files into .pyo/.pyc \(for both versions of python\) [\#1601](https://github.com/kivy/python-for-android/pull/1601) ([opacam](https://github.com/opacam)) - Migrates gevent to new python3 recipe [\#1600](https://github.com/kivy/python-for-android/pull/1600) ([AndreMiras](https://github.com/AndreMiras)) - Fix hardcoded entries \(build platform\) for core modules: archs and python [\#1597](https://github.com/kivy/python-for-android/pull/1597) ([opacam](https://github.com/opacam)) - Fix zeroconf compilation and grants python3 compatibility [\#1596](https://github.com/kivy/python-for-android/pull/1596) ([opacam](https://github.com/opacam)) - Fix reportlab's recipe `crypt.h` error [\#1595](https://github.com/kivy/python-for-android/pull/1595) ([opacam](https://github.com/opacam)) - Fix --force-build incorrectly listed as --force\_build, fixes \#1565 [\#1593](https://github.com/kivy/python-for-android/pull/1593) ([etc0de](https://github.com/etc0de)) - Allow patching from any folder + fix pygame components issues [\#1592](https://github.com/kivy/python-for-android/pull/1592) ([opacam](https://github.com/opacam)) - Fix --private and others showing weird error when used without argument [\#1591](https://github.com/kivy/python-for-android/pull/1591) ([etc0de](https://github.com/etc0de)) - Minimal fixes to make pygame bootstrap work with python2legacy [\#1587](https://github.com/kivy/python-for-android/pull/1587) ([opacam](https://github.com/opacam)) - Corrections for `Fix bootstraps for webview and service_only` \(recently merged\) [\#1586](https://github.com/kivy/python-for-android/pull/1586) ([opacam](https://github.com/opacam)) - \[CORE FIX/ENHANCEMENT\] Speedup copy that can be very very long \(up to 2 minutes\) [\#1585](https://github.com/kivy/python-for-android/pull/1585) ([opacam](https://github.com/opacam)) - Move libffi to mainline repo [\#1584](https://github.com/kivy/python-for-android/pull/1584) ([opacam](https://github.com/opacam)) - \[WIP\] Rework zbar \(add python3 compatibility + add recipes: pyzbar and zbarlight\) [\#1583](https://github.com/kivy/python-for-android/pull/1583) ([opacam](https://github.com/opacam)) - fix missing gethostbyname\_r on Android 5.1 [\#1581](https://github.com/kivy/python-for-android/pull/1581) ([opacam](https://github.com/opacam)) - \[WIP\] Rework libxml2, libxslt and lxml \(update versions\) [\#1580](https://github.com/kivy/python-for-android/pull/1580) ([opacam](https://github.com/opacam)) - Fixes ffmpeg compilation w/ openssl 1.1.1 [\#1579](https://github.com/kivy/python-for-android/pull/1579) ([misl6](https://github.com/misl6)) - Fix incorrect call assuming that OS python minor version matches hostpython [\#1577](https://github.com/kivy/python-for-android/pull/1577) ([etc0de](https://github.com/etc0de)) - Add download retries to deal better with connection hiccups during build [\#1574](https://github.com/kivy/python-for-android/pull/1574) ([etc0de](https://github.com/etc0de)) - Rework for Pillow/pil recipes & update jpeg and png [\#1573](https://github.com/kivy/python-for-android/pull/1573) ([opacam](https://github.com/opacam)) - Fix APP\_PLATFORM not properly passed in NDKRecipe [\#1572](https://github.com/kivy/python-for-android/pull/1572) ([etc0de](https://github.com/etc0de)) - Fix outdated hardcoded python recipe references in lxml, reportlab & Pillow recipe [\#1571](https://github.com/kivy/python-for-android/pull/1571) ([etc0de](https://github.com/etc0de)) - Fix linkage problems with python's versioned library \(reintroduce `INSTSONAME`\) [\#1568](https://github.com/kivy/python-for-android/pull/1568) ([opacam](https://github.com/opacam)) - \[OMEMO\] updated omemo recipe [\#1566](https://github.com/kivy/python-for-android/pull/1566) ([goffi-contrib](https://github.com/goffi-contrib)) - Render format string argument on BuildInterruptingException [\#1561](https://github.com/kivy/python-for-android/pull/1561) ([AndreMiras](https://github.com/AndreMiras)) - \[WIP\]\[CORE UPDATE - PART XV\] Add encryption test app [\#1556](https://github.com/kivy/python-for-android/pull/1556) ([opacam](https://github.com/opacam)) - \[WIP\]\[CORE UPDATE - PART XIV\] Libtorrent+boost for both versions of python and updated versions [\#1555](https://github.com/kivy/python-for-android/pull/1555) ([opacam](https://github.com/opacam)) - \[CORE UPDATE - PART XIII\] Pysha3 for both versions of python [\#1554](https://github.com/kivy/python-for-android/pull/1554) ([opacam](https://github.com/opacam)) - \[WIP\]\[CORE UPDATE - PART XII\] Pycryptodome for both versions of python [\#1553](https://github.com/kivy/python-for-android/pull/1553) ([opacam](https://github.com/opacam)) - \[CORE UPDATE - PART XI\] M2crypto for both versions of python and updated version [\#1552](https://github.com/kivy/python-for-android/pull/1552) ([opacam](https://github.com/opacam)) - \[WIP\]\[CORE UPDATE - PART X\] Protobuf\_cpp fixes and updated version [\#1551](https://github.com/kivy/python-for-android/pull/1551) ([opacam](https://github.com/opacam)) - \[CORE UPDATE - PART IX\] Pymunk for both versions of python and enhance flags [\#1550](https://github.com/kivy/python-for-android/pull/1550) ([opacam](https://github.com/opacam)) - \[CORE UPDATE - PART VIII\] Netifaces for both versions of python \(updates the netifaces version\) [\#1549](https://github.com/kivy/python-for-android/pull/1549) ([opacam](https://github.com/opacam)) - \[CORE UPDATE - PART VII\] Apsw for both versions of python [\#1548](https://github.com/kivy/python-for-android/pull/1548) ([opacam](https://github.com/opacam)) - \[CORE UPDATE - PART VI\] Fix scrypt [\#1547](https://github.com/kivy/python-for-android/pull/1547) ([opacam](https://github.com/opacam)) - \[CORE UPDATE - PART V\] Fix pycrypto [\#1546](https://github.com/kivy/python-for-android/pull/1546) ([opacam](https://github.com/opacam)) - \[CORE UPDATE - PART IV\] Fix cryptography+cffi [\#1545](https://github.com/kivy/python-for-android/pull/1545) ([opacam](https://github.com/opacam)) - fix wrong conditional for build custom\_rules.tmpl.xml [\#1544](https://github.com/kivy/python-for-android/pull/1544) ([bit4bit](https://github.com/bit4bit)) - \[CORE UPDATE - PART II\] Fix bootstraps for webview and service\_only [\#1541](https://github.com/kivy/python-for-android/pull/1541) ([opacam](https://github.com/opacam)) - \[CORE UPDATE - PART I\] Refactor python recipes + openssl + sqlite3 [\#1537](https://github.com/kivy/python-for-android/pull/1537) ([opacam](https://github.com/opacam)) - Re-added argument that was lost during build.py merge [\#1533](https://github.com/kivy/python-for-android/pull/1533) ([inclement](https://github.com/inclement)) - Use API 27 as new default for travis & docs [\#1532](https://github.com/kivy/python-for-android/pull/1532) ([etc0de](https://github.com/etc0de)) - Bump SDL2 to 2.0.9 & Add API \>=23 runtime permissions API [\#1528](https://github.com/kivy/python-for-android/pull/1528) ([etc0de](https://github.com/etc0de)) - Unify build.py contents [\#1524](https://github.com/kivy/python-for-android/pull/1524) ([etc0de](https://github.com/etc0de)) - Unify configChanges manifest entry and add missing values [\#1522](https://github.com/kivy/python-for-android/pull/1522) ([etc0de](https://github.com/etc0de)) - Add google repository at allprojects [\#1521](https://github.com/kivy/python-for-android/pull/1521) ([wo01](https://github.com/wo01)) - Fix bytes/unicode issues in android recipe [\#1516](https://github.com/kivy/python-for-android/pull/1516) ([KeyWeeUsr](https://github.com/KeyWeeUsr)) - Uses target python3 on conditional builds, fixes \#1485 [\#1515](https://github.com/kivy/python-for-android/pull/1515) ([AndreMiras](https://github.com/AndreMiras)) - Updates websocket-client recipe, fixes \#1253 [\#1513](https://github.com/kivy/python-for-android/pull/1513) ([AndreMiras](https://github.com/AndreMiras)) - No need to decode into unicode when running in python 3 [\#1512](https://github.com/kivy/python-for-android/pull/1512) ([jtoledo1974](https://github.com/jtoledo1974)) - Update gradle version [\#1507](https://github.com/kivy/python-for-android/pull/1507) ([opacam](https://github.com/opacam)) - Fix libnacl recipe missing libsodium [\#1505](https://github.com/kivy/python-for-android/pull/1505) ([KeyWeeUsr](https://github.com/KeyWeeUsr)) - Make SDL2 & services\_only bootstrap properly error with missing --private [\#1503](https://github.com/kivy/python-for-android/pull/1503) ([etc0de](https://github.com/etc0de)) - Unify start.c of all bootstraps to one file [\#1500](https://github.com/kivy/python-for-android/pull/1500) ([etc0de](https://github.com/etc0de)) - Minor fixes to basic common bootstrap handling code [\#1499](https://github.com/kivy/python-for-android/pull/1499) ([etc0de](https://github.com/etc0de)) - Rework common bootstrap area based on kollivier's work [\#1496](https://github.com/kivy/python-for-android/pull/1496) ([etc0de](https://github.com/etc0de)) - Fixes audiostream recipe on Python3 [\#1495](https://github.com/kivy/python-for-android/pull/1495) ([misl6](https://github.com/misl6)) - when listing distributions, if one has no ndk\_api, consider it to be 0 [\#1494](https://github.com/kivy/python-for-android/pull/1494) ([tshirtman](https://github.com/tshirtman)) - Make Cython work without recipe [\#1483](https://github.com/kivy/python-for-android/pull/1483) ([etc0de](https://github.com/etc0de)) - Allow Python 3 To Be Built On Non-ARM Architectures [\#1481](https://github.com/kivy/python-for-android/pull/1481) ([TheBrokenRail](https://github.com/TheBrokenRail)) - Remove crystax docker and optimize Dockerfile [\#1471](https://github.com/kivy/python-for-android/pull/1471) ([KeyWeeUsr](https://github.com/KeyWeeUsr)) - Replaced many `exit(1)`s with exception raising [\#1468](https://github.com/kivy/python-for-android/pull/1468) ([inclement](https://github.com/inclement)) - Add ctypes support for python3's recipe [\#1465](https://github.com/kivy/python-for-android/pull/1465) ([opacam](https://github.com/opacam)) - Fix jpeg build for newer NDKs [\#1363](https://github.com/kivy/python-for-android/pull/1363) ([mkg20001](https://github.com/mkg20001)) - Added sympy recipe [\#1236](https://github.com/kivy/python-for-android/pull/1236) ([inclement](https://github.com/inclement)) - Added --no-optimize-python option to remove -OO in sdl2 bootstrap [\#1221](https://github.com/kivy/python-for-android/pull/1221) ([inclement](https://github.com/inclement)) - android\_new: fix force\_build option [\#1006](https://github.com/kivy/python-for-android/pull/1006) ([ZingBallyhoo](https://github.com/ZingBallyhoo)) ## [0.6.0](https://github.com/kivy/python-for-android/tree/0.6.0) (2017-11-25) [Full Changelog](https://github.com/kivy/python-for-android/compare/0.5.3...0.6.0) **Closed issues:** - buildozer cannot download sdl2 ,,, help [\#1176](https://github.com/kivy/python-for-android/issues/1176) - \_multiprocessing [\#1168](https://github.com/kivy/python-for-android/issues/1168) - p4a: command not found [\#1167](https://github.com/kivy/python-for-android/issues/1167) - no module named tty [\#1165](https://github.com/kivy/python-for-android/issues/1165) - Openssl recipe crashes on x86 arch [\#1162](https://github.com/kivy/python-for-android/issues/1162) - Please help building the cffi recipe [\#1159](https://github.com/kivy/python-for-android/issues/1159) - Build failed for Numpy [\#1158](https://github.com/kivy/python-for-android/issues/1158) - Base: Failed to import "android" module. Could not remove android presplash. [\#1153](https://github.com/kivy/python-for-android/issues/1153) - --ndk\_ver cli option not working, but ANDROIDNDKVER does [\#1149](https://github.com/kivy/python-for-android/issues/1149) - lxml uses etree.so that throws an unexpected e\_machine error [\#1147](https://github.com/kivy/python-for-android/issues/1147) - Incompatible pyopenssl and cryptography versions [\#1138](https://github.com/kivy/python-for-android/issues/1138) - "undefined reference to 'OBJ\_obj2txt'" error on building openssl with NDK 15b [\#1135](https://github.com/kivy/python-for-android/issues/1135) - buildozer can't download hostpython2 [\#1132](https://github.com/kivy/python-for-android/issues/1132) - App crashing on startup- ImportError: dlopen failed: \_imaging.so is 64-bit [\#1131](https://github.com/kivy/python-for-android/issues/1131) - Error on building FFPYPLAYER for VideoPlayer Widget [\#1130](https://github.com/kivy/python-for-android/issues/1130) - Kivy App Crashes Immediately on Android [\#1128](https://github.com/kivy/python-for-android/issues/1128) - Remove the python3 and hostpython3 recipes [\#1125](https://github.com/kivy/python-for-android/issues/1125) - building with opencv show error [\#1124](https://github.com/kivy/python-for-android/issues/1124) - Webview loading animation doesn't work [\#1123](https://github.com/kivy/python-for-android/issues/1123) - Old toolchain is now deprecated [\#1122](https://github.com/kivy/python-for-android/issues/1122) - `pip install kivy` fails with `'../include/config.pxi' not found` [\#1120](https://github.com/kivy/python-for-android/issues/1120) - Suggestion: Allow a recipe to checkout a module from a local git repository [\#1119](https://github.com/kivy/python-for-android/issues/1119) - Please add a 'version' command to p4a [\#1116](https://github.com/kivy/python-for-android/issues/1116) - Websocket error: SSL not available [\#1107](https://github.com/kivy/python-for-android/issues/1107) - Pure python module as requirements aren't installed via pip [\#1098](https://github.com/kivy/python-for-android/issues/1098) - Current android sdk has removed the ant/build.xml [\#1069](https://github.com/kivy/python-for-android/issues/1069) - python-for-android 0.5 release checklist [\#1043](https://github.com/kivy/python-for-android/issues/1043) - Numpy recipes build fail [\#1040](https://github.com/kivy/python-for-android/issues/1040) - SDL2 launcher does not work with python3 [\#980](https://github.com/kivy/python-for-android/issues/980) - ffpyplayer can't be built with new toolchain [\#951](https://github.com/kivy/python-for-android/issues/951) - "Couldn't load python3.5m: findLibrary returned null" on older versions of Android [\#866](https://github.com/kivy/python-for-android/issues/866) - Problems in creation of recipe for zbar [\#854](https://github.com/kivy/python-for-android/issues/854) - freshly built old\_toolchain crashes with 'cannot locate symbol "\_Py\_asinh"' [\#487](https://github.com/kivy/python-for-android/issues/487) - The SDL2 bootstrap can't make a Kivy Launcher [\#468](https://github.com/kivy/python-for-android/issues/468) - Error: JAVA\_HOME is not defined correctly. [\#427](https://github.com/kivy/python-for-android/issues/427) - Compilation Error at ARM Environment [\#352](https://github.com/kivy/python-for-android/issues/352) - Build errors on OSX 10.10 [\#311](https://github.com/kivy/python-for-android/issues/311) - Easily reproducible crash accessing Context constants [\#235](https://github.com/kivy/python-for-android/issues/235) - AttributeError: 'java.io.File' object has no attribute 'endswith' [\#170](https://github.com/kivy/python-for-android/issues/170) - KeyEvent.getCharacters\(\) returns `null` instead of `KEYCODE_UNKNOWN` [\#142](https://github.com/kivy/python-for-android/issues/142) - Carousel: add\_widget after build\(\) [\#69](https://github.com/kivy/python-for-android/issues/69) - sound.length not returning correctly [\#67](https://github.com/kivy/python-for-android/issues/67) - KEYCODE\_HOME and KEYCODE\_POWER can't be trapped [\#43](https://github.com/kivy/python-for-android/issues/43) **Merged pull requests:** - Removed '-j5' from openssl make [\#1180](https://github.com/kivy/python-for-android/pull/1180) ([inclement](https://github.com/inclement)) - add recipes for pyrxp & reportlab [\#1173](https://github.com/kivy/python-for-android/pull/1173) ([replabrobin](https://github.com/replabrobin)) - Add the ndk platform libs dir during biglink [\#1171](https://github.com/kivy/python-for-android/pull/1171) ([inclement](https://github.com/inclement)) - Update troubleshooting.rst [\#1164](https://github.com/kivy/python-for-android/pull/1164) ([AndreMiras](https://github.com/AndreMiras)) - Fix OpenSSL recipe crashes on x86 [\#1163](https://github.com/kivy/python-for-android/pull/1163) ([gdyuldin](https://github.com/gdyuldin)) - Cleaned up some old comments [\#1160](https://github.com/kivy/python-for-android/pull/1160) ([inclement](https://github.com/inclement)) - The pypi python return http 403 error on http [\#1157](https://github.com/kivy/python-for-android/pull/1157) ([brvier](https://github.com/brvier)) - Accept pypi fragmented URLs \(\#md5...\) [\#1155](https://github.com/kivy/python-for-android/pull/1155) ([wexi](https://github.com/wexi)) - Add note about NDK version on 32-bit OS [\#1150](https://github.com/kivy/python-for-android/pull/1150) ([ghost](https://github.com/ghost)) - Adds zbar \(and dependencies\) support, refs \#854 [\#1145](https://github.com/kivy/python-for-android/pull/1145) ([AndreMiras](https://github.com/AndreMiras)) - Remove unused `extract_source()` method [\#1143](https://github.com/kivy/python-for-android/pull/1143) ([AndreMiras](https://github.com/AndreMiras)) - This current Twisted version actually runs! [\#1140](https://github.com/kivy/python-for-android/pull/1140) ([wexi](https://github.com/wexi)) - Two humble changes [\#1139](https://github.com/kivy/python-for-android/pull/1139) ([wexi](https://github.com/wexi)) - Made start.c search ANDROID\_UNPACK for crystax [\#1137](https://github.com/kivy/python-for-android/pull/1137) ([inclement](https://github.com/inclement)) - Update ffpyplayer recipe and it's dependencies [\#1134](https://github.com/kivy/python-for-android/pull/1134) ([germn](https://github.com/germn)) - Re-added --sdk argument for sdl2 bootstrap, but with a warning [\#1133](https://github.com/kivy/python-for-android/pull/1133) ([inclement](https://github.com/inclement)) - Moved webview loading animation to bootstrap [\#1129](https://github.com/kivy/python-for-android/pull/1129) ([inclement](https://github.com/inclement)) - Added --version argument [\#1118](https://github.com/kivy/python-for-android/pull/1118) ([inclement](https://github.com/inclement)) - Add help regarding websocket-client & SSL [\#1113](https://github.com/kivy/python-for-android/pull/1113) ([brentpicasso](https://github.com/brentpicasso)) - Improve documentation for websocket-client to account for SSL compatibility [\#1112](https://github.com/kivy/python-for-android/pull/1112) ([brentpicasso](https://github.com/brentpicasso)) - Try system python when performing compileall [\#1109](https://github.com/kivy/python-for-android/pull/1109) ([inclement](https://github.com/inclement)) - Fixed ssl, sqlite and crystax library loads on Android versions before ~4.4 [\#1106](https://github.com/kivy/python-for-android/pull/1106) ([inclement](https://github.com/inclement)) - Fixes for NDK 15+ [\#1103](https://github.com/kivy/python-for-android/pull/1103) ([inclement](https://github.com/inclement)) - Fix: only first line of note was displayed in the blue box [\#1101](https://github.com/kivy/python-for-android/pull/1101) ([Fogapod](https://github.com/Fogapod)) - Added "regex" module recipe [\#1100](https://github.com/kivy/python-for-android/pull/1100) ([germn](https://github.com/germn)) - SDL2/Gradle bootstrap with fixes [\#1071](https://github.com/kivy/python-for-android/pull/1071) ([inclement](https://github.com/inclement)) - Updated Kivy icons to newer logo under sdl2 [\#1033](https://github.com/kivy/python-for-android/pull/1033) ([inclement](https://github.com/inclement)) ## [0.5.3](https://github.com/kivy/python-for-android/tree/0.5.3) (2017-08-26) [Full Changelog](https://github.com/kivy/python-for-android/compare/6234a5d11d35d4a1b7fee9433461499f68a915d9...0.5.3) **Closed issues:** - Building with Crystax NDK : "Android NDK : Could not find application project directory" [\#1084](https://github.com/kivy/python-for-android/issues/1084) - recipes \_\_init\_\_.py indentation error [\#1082](https://github.com/kivy/python-for-android/issues/1082) - AttributeError: 'Context' object has no attribute 'hostpython' [\#1077](https://github.com/kivy/python-for-android/issues/1077) - 'Context' object has no attribute 'hostpython' [\#1073](https://github.com/kivy/python-for-android/issues/1073) - Error after update of SDK [\#1070](https://github.com/kivy/python-for-android/issues/1070) - wakelock == 1 not preventing screen from locking on sdl2 [\#1061](https://github.com/kivy/python-for-android/issues/1061) - running p4a from git fails [\#1058](https://github.com/kivy/python-for-android/issues/1058) - 'Context' object has no attribute 'hostpython' [\#1056](https://github.com/kivy/python-for-android/issues/1056) - Can p4a be used without a bootstrap? [\#1055](https://github.com/kivy/python-for-android/issues/1055) - Screen rotation with "orientation=all" is broken [\#1054](https://github.com/kivy/python-for-android/issues/1054) - python-for-android doesn't work with current Android SDK [\#1050](https://github.com/kivy/python-for-android/issues/1050) - p4a should fetch Kivy 1.10 instead of master [\#1044](https://github.com/kivy/python-for-android/issues/1044) - Android Browser Not Launching for OAuth 2.0 [\#1032](https://github.com/kivy/python-for-android/issues/1032) - flash quite,adb log [\#1030](https://github.com/kivy/python-for-android/issues/1030) - Can't build, sh.py raise a exception. [\#1029](https://github.com/kivy/python-for-android/issues/1029) - Python 3 branch still uses python 2.x [\#1022](https://github.com/kivy/python-for-android/issues/1022) - service fails to start [\#1020](https://github.com/kivy/python-for-android/issues/1020) - path to service file [\#1019](https://github.com/kivy/python-for-android/issues/1019) - Crash trap with custom logger and sys.stdout.encoding [\#1018](https://github.com/kivy/python-for-android/issues/1018) - pyjnius build failed [\#1016](https://github.com/kivy/python-for-android/issues/1016) - JNI ERROR \(app bug\): local reference table overflow \(max=512\) while executing Couchbae Lite Query [\#1008](https://github.com/kivy/python-for-android/issues/1008) - Custom recipes hinders the downloading of other ones. [\#1001](https://github.com/kivy/python-for-android/issues/1001) - documentation: how to run without pip install \( development mode \) [\#996](https://github.com/kivy/python-for-android/issues/996) - PythonActivity.mActivity causes app crash with new toolchain [\#995](https://github.com/kivy/python-for-android/issues/995) - Failure deploying apk files using buildozer android debug [\#989](https://github.com/kivy/python-for-android/issues/989) - 'Window.request\_keyboard' without showing keyboard [\#986](https://github.com/kivy/python-for-android/issues/986) - TypeError: slice indices must be integers or None or have an \_\_index\_\_ method [\#984](https://github.com/kivy/python-for-android/issues/984) - --presplash and --icon aren't mentioned in revamp docs [\#975](https://github.com/kivy/python-for-android/issues/975) - NDK automatic lookup tries to pick a tarball [\#972](https://github.com/kivy/python-for-android/issues/972) - Kivy is broken on recent master [\#970](https://github.com/kivy/python-for-android/issues/970) - device doesn't go on sleep mode [\#969](https://github.com/kivy/python-for-android/issues/969) - The python2 build imports cython from the system python in /usr/lib/... [\#964](https://github.com/kivy/python-for-android/issues/964) - "Could not remove android presplash" if 'android' is not in requirements [\#963](https://github.com/kivy/python-for-android/issues/963) - "AndroidJoystick is not supported by your version of linux" confusing message in log [\#962](https://github.com/kivy/python-for-android/issues/962) - Could not ping localhost:5000 [\#960](https://github.com/kivy/python-for-android/issues/960) - Using pyjnius leads to crash \(sometimes?\) if app built by new toolchain [\#959](https://github.com/kivy/python-for-android/issues/959) - Android screen rotation is probably broken [\#955](https://github.com/kivy/python-for-android/issues/955) - Compiling pyo with sdl is breaking ply / enaml [\#947](https://github.com/kivy/python-for-android/issues/947) - Access WiFi information? [\#940](https://github.com/kivy/python-for-android/issues/940) - p4a erroring on SSL connection [\#939](https://github.com/kivy/python-for-android/issues/939) - Compiling PIL seems to use pyconfig.h from the wrong directory [\#937](https://github.com/kivy/python-for-android/issues/937) - ImportError for ssl [\#934](https://github.com/kivy/python-for-android/issues/934) - My app crashed by raising error about Python3.5m, but i made apk by python2.7..!!! [\#933](https://github.com/kivy/python-for-android/issues/933) - \[launcher\] icon= and splash= parameters [\#932](https://github.com/kivy/python-for-android/issues/932) - \[launcher\] app update by http\(s\) from external website \(https:// for github required\) [\#931](https://github.com/kivy/python-for-android/issues/931) - Presplash delay [\#928](https://github.com/kivy/python-for-android/issues/928) - Python3 APK fails to build! [\#927](https://github.com/kivy/python-for-android/issues/927) - MQTT [\#926](https://github.com/kivy/python-for-android/issues/926) - Can't build apk on OS X El Capitan [\#922](https://github.com/kivy/python-for-android/issues/922) - command not found exception. [\#921](https://github.com/kivy/python-for-android/issues/921) - ffmpeg recipe possibly broken [\#920](https://github.com/kivy/python-for-android/issues/920) - ERROR: /usr/bin/ant failed! [\#918](https://github.com/kivy/python-for-android/issues/918) - Feature request / Idea / Poll: Create kex packages [\#917](https://github.com/kivy/python-for-android/issues/917) - AttributeError: 'module' object has no attribute 'recipe' [\#907](https://github.com/kivy/python-for-android/issues/907) - Kivy .so is too small to be an ELF executable \[pygame bootstrap\] [\#897](https://github.com/kivy/python-for-android/issues/897) - p4a recipes crashes on matplotlib [\#895](https://github.com/kivy/python-for-android/issues/895) - onResume deadlock with pyjnius/pygame/sdl on android [\#890](https://github.com/kivy/python-for-android/issues/890) - dlopen failed: cannot locate symbol "\_Py\_NoneStruct" [\#887](https://github.com/kivy/python-for-android/issues/887) - SDL2 continually passes joyaxismotion events [\#885](https://github.com/kivy/python-for-android/issues/885) - Cloud Builder - 500 Internal Server Error [\#883](https://github.com/kivy/python-for-android/issues/883) - Is it possible to add a argument to set the background color of the "loading screen"? [\#881](https://github.com/kivy/python-for-android/issues/881) - Building apk problem for android on OSX EL Capitan 10.11.5 [\#878](https://github.com/kivy/python-for-android/issues/878) - python3crystax conflicts with python3 [\#877](https://github.com/kivy/python-for-android/issues/877) - No instructions for utilizing in Arch linux \(i686 / x86\_64\) [\#876](https://github.com/kivy/python-for-android/issues/876) - Can't compile with openssl [\#868](https://github.com/kivy/python-for-android/issues/868) - p4a recipes error: missing matplotlib [\#865](https://github.com/kivy/python-for-android/issues/865) - AndroidBrowser.open\(\) should return a value [\#855](https://github.com/kivy/python-for-android/issues/855) - Can't import PIL on python for android and kivy? [\#853](https://github.com/kivy/python-for-android/issues/853) - How can i use the custom broadcast by myself in the background service? [\#849](https://github.com/kivy/python-for-android/issues/849) - Building python for android's requirements for 64 bit Android processors [\#848](https://github.com/kivy/python-for-android/issues/848) - The Kivy Option "softinput\_mode" does not work on Android with bootstrap=sdl2 [\#847](https://github.com/kivy/python-for-android/issues/847) - webbrowser.open\(\) doesn't work on Android with bootstrap=sdl2 [\#846](https://github.com/kivy/python-for-android/issues/846) - non debug apk? [\#844](https://github.com/kivy/python-for-android/issues/844) - Rotation Lock Ignored [\#842](https://github.com/kivy/python-for-android/issues/842) - Plyer GPS example works on android but not android\_new toolchain [\#833](https://github.com/kivy/python-for-android/issues/833) - p4a create Error with openssl: start.c\:2\:20\: Python.h: No such file or directory [\#830](https://github.com/kivy/python-for-android/issues/830) - MD5sum - UnboundLocalError: current\_md5 referenced before assignment [\#828](https://github.com/kivy/python-for-android/issues/828) - Recipes are still not resolved properly sometimes [\#826](https://github.com/kivy/python-for-android/issues/826) - Failed to build Pillow-3.3.0 gcc: error: \_imaging.o: No such file or directory [\#823](https://github.com/kivy/python-for-android/issues/823) - p4a create error: kivy/\_clock.pxd\:6\:4\: Executable statement not allowed here [\#822](https://github.com/kivy/python-for-android/issues/822) - No such file or directory: ".../whitelist.txt" [\#821](https://github.com/kivy/python-for-android/issues/821) - Docs - connected toctrees, too deep? [\#820](https://github.com/kivy/python-for-android/issues/820) - Showcase with launcher [\#814](https://github.com/kivy/python-for-android/issues/814) - Can't target api, --sdk argument broken [\#813](https://github.com/kivy/python-for-android/issues/813) - Lxml, docutils need recipe [\#812](https://github.com/kivy/python-for-android/issues/812) - \[Pygame\] start.c fatal error: Python.h: No such file or directory [\#809](https://github.com/kivy/python-for-android/issues/809) - Presplash does not work with SDL2. [\#806](https://github.com/kivy/python-for-android/issues/806) - netifaces recipe broken [\#802](https://github.com/kivy/python-for-android/issues/802) - sdl2 recipe builds wrong bootstrap jni source [\#801](https://github.com/kivy/python-for-android/issues/801) - On resume crash in SDL2 bootstrap [\#797](https://github.com/kivy/python-for-android/issues/797) - Pure python requirements does not install \(plyer for example\) [\#795](https://github.com/kivy/python-for-android/issues/795) - E/linker: site-packages/android/\_android.so too small to be an ELF executable [\#768](https://github.com/kivy/python-for-android/issues/768) - Cryptography recipe does not compile [\#766](https://github.com/kivy/python-for-android/issues/766) - Threads need a wrapper for calling detach\(\) [\#758](https://github.com/kivy/python-for-android/issues/758) - Activity \(android.activity\) piece of code [\#756](https://github.com/kivy/python-for-android/issues/756) - ImportError: Import by filename is not supported. [\#751](https://github.com/kivy/python-for-android/issues/751) - Hostpython not found by Recipe.py [\#748](https://github.com/kivy/python-for-android/issues/748) - P4A and C++/SDL2/Python2/OpenGL game [\#747](https://github.com/kivy/python-for-android/issues/747) - Why does the boot image is deformed? [\#745](https://github.com/kivy/python-for-android/issues/745) - problem with p4a recipe for kivent [\#744](https://github.com/kivy/python-for-android/issues/744) - Webview - back button bug [\#741](https://github.com/kivy/python-for-android/issues/741) - webview - flask server crashes immediately [\#739](https://github.com/kivy/python-for-android/issues/739) - p4a apk webview bug [\#738](https://github.com/kivy/python-for-android/issues/738) - Libffi recipe fails with "unrecognized options: --enable-shared" [\#733](https://github.com/kivy/python-for-android/issues/733) - App close when device is flipped [\#732](https://github.com/kivy/python-for-android/issues/732) - recipe for pyjinius fails [\#731](https://github.com/kivy/python-for-android/issues/731) - Doc clarification on p4a requirements for basic kivy app with SDL2 bootstrap [\#724](https://github.com/kivy/python-for-android/issues/724) - PIL recipe is broken [\#722](https://github.com/kivy/python-for-android/issues/722) - raise exc\_info\[0\], exc\_info\[1\], exc\_info\[2\] - Syntax Error [\#721](https://github.com/kivy/python-for-android/issues/721) - Some input files use or override a deprecated API. [\#719](https://github.com/kivy/python-for-android/issues/719) - Unexpected "malformed start tag" error with HTMLParser [\#715](https://github.com/kivy/python-for-android/issues/715) - open\(\) built-in function don't work as expected [\#706](https://github.com/kivy/python-for-android/issues/706) - Webview bootstrap. Where to start? \[$100\] [\#700](https://github.com/kivy/python-for-android/issues/700) - Back button doesn't work [\#699](https://github.com/kivy/python-for-android/issues/699) - FileNotFoundError '/bin/sh' with subprocess.py python3crystax [\#691](https://github.com/kivy/python-for-android/issues/691) - static jfieldID not valid for class java.lang.Class\ [\#686](https://github.com/kivy/python-for-android/issues/686) - Incorrect SDK variable in build.xml with pygame bootstrap and direct p4a invocation [\#684](https://github.com/kivy/python-for-android/issues/684) - Failure to build apk due to incorrect invocation of "ant" by "sh.ant"... [\#681](https://github.com/kivy/python-for-android/issues/681) - Missing recipes: cherrypy, libnacl, requests [\#674](https://github.com/kivy/python-for-android/issues/674) - Multiple permissions in .p4a [\#673](https://github.com/kivy/python-for-android/issues/673) - Python compiled components recipe: "bad gcc/glibc config?" [\#669](https://github.com/kivy/python-for-android/issues/669) - No "--window" option [\#666](https://github.com/kivy/python-for-android/issues/666) - .jam files not installed [\#661](https://github.com/kivy/python-for-android/issues/661) - AttributeError: 'NoneType' object has no attribute 'from\_crystax' [\#659](https://github.com/kivy/python-for-android/issues/659) - will\_build does not work as expected [\#657](https://github.com/kivy/python-for-android/issues/657) - Check presence of main.py during build time [\#656](https://github.com/kivy/python-for-android/issues/656) - md5 not handled yet [\#650](https://github.com/kivy/python-for-android/issues/650) - App crash on resume with new tool chain [\#646](https://github.com/kivy/python-for-android/issues/646) - Unable to find libpython2.7.so on older versions of Android [\#645](https://github.com/kivy/python-for-android/issues/645) - Corrupted window size with Window.softinput\_mode = 'below\_target' [\#635](https://github.com/kivy/python-for-android/issues/635) - Patch files not found [\#633](https://github.com/kivy/python-for-android/issues/633) - Unintended rollback patches [\#632](https://github.com/kivy/python-for-android/issues/632) - \[master\] AttributeError: 'tuple' object has no attribute 'startswith' [\#631](https://github.com/kivy/python-for-android/issues/631) - Python recipe from Crystax undefined [\#629](https://github.com/kivy/python-for-android/issues/629) - SDL2\_image error Unknown or unsupported ARM architecture [\#627](https://github.com/kivy/python-for-android/issues/627) - Building python2 for armeabi fails due to unknown option "-single\_module" \(OS X\) [\#623](https://github.com/kivy/python-for-android/issues/623) - Building python2 for armeabi fails due to space character in storage\_dir \(OS X\) [\#622](https://github.com/kivy/python-for-android/issues/622) - AttributeError: 'Context' object has no attribute 'hostpython' [\#620](https://github.com/kivy/python-for-android/issues/620) - Jpeg recipe is broken [\#617](https://github.com/kivy/python-for-android/issues/617) - build.py TypeError args.services object is not iterable [\#616](https://github.com/kivy/python-for-android/issues/616) - OpenSSL 1.0.2e outdated \(replaced by 1.0.2f\) [\#614](https://github.com/kivy/python-for-android/issues/614) - Matplotlib recipe [\#607](https://github.com/kivy/python-for-android/issues/607) - setup.py install doesn't include the recipes folder [\#591](https://github.com/kivy/python-for-android/issues/591) - missing recipes/pyjnius/getenv.patch [\#590](https://github.com/kivy/python-for-android/issues/590) - standard includes not found by boost [\#576](https://github.com/kivy/python-for-android/issues/576) - HTTP 302 recipe download file [\#573](https://github.com/kivy/python-for-android/issues/573) - SDL2 bootstrap broken with blacklist? [\#567](https://github.com/kivy/python-for-android/issues/567) - Kivy Launcher 1.9.1 APK doesn't work on Lollipop [\#548](https://github.com/kivy/python-for-android/issues/548) - Logo aspect ratio problem [\#545](https://github.com/kivy/python-for-android/issues/545) - Window.softinput\_mode/TextInput - Window moves up/down when switching softinput\_mode to 'below\_target'/'resize' [\#544](https://github.com/kivy/python-for-android/issues/544) - native code in kivyAndroid, possible? [\#542](https://github.com/kivy/python-for-android/issues/542) - Error compiling [\#541](https://github.com/kivy/python-for-android/issues/541) - import sh module problem [\#540](https://github.com/kivy/python-for-android/issues/540) - Inconsistent dependency graph behaviour [\#515](https://github.com/kivy/python-for-android/issues/515) - We demand Python 3 support [\#512](https://github.com/kivy/python-for-android/issues/512) - CythonRecipe: how to handle different settings for different .pyx files? [\#511](https://github.com/kivy/python-for-android/issues/511) - Arch support is broken [\#492](https://github.com/kivy/python-for-android/issues/492) - function should\_build [\#491](https://github.com/kivy/python-for-android/issues/491) - verbose output [\#490](https://github.com/kivy/python-for-android/issues/490) - compiler problem with gcc \>= 4.8 [\#489](https://github.com/kivy/python-for-android/issues/489) - error when execute p4a in line from urlparse import urlparse [\#488](https://github.com/kivy/python-for-android/issues/488) - Can't get off the ground [\#485](https://github.com/kivy/python-for-android/issues/485) - Python3 doesn't work on Android [\#484](https://github.com/kivy/python-for-android/issues/484) - Allow scaling of the presplash image to device resolution [\#481](https://github.com/kivy/python-for-android/issues/481) - python multiprocess.dummy do not work [\#479](https://github.com/kivy/python-for-android/issues/479) - Question: compatibility with cx\_Freeze [\#478](https://github.com/kivy/python-for-android/issues/478) - Purge inclement where needed [\#477](https://github.com/kivy/python-for-android/issues/477) - Missing dependency in quickstart? [\#476](https://github.com/kivy/python-for-android/issues/476) - No service support with SDL2 bootstrap [\#467](https://github.com/kivy/python-for-android/issues/467) - Kivy can't get the keyboard height with SDL2 [\#466](https://github.com/kivy/python-for-android/issues/466) - SDL2 backend doesn't support a loading screen [\#465](https://github.com/kivy/python-for-android/issues/465) - Many recipes from the old toolchain need porting [\#464](https://github.com/kivy/python-for-android/issues/464) - \[revamp\] Android NDK API 21 issue [\#455](https://github.com/kivy/python-for-android/issues/455) - \[revamp\] Twisted [\#454](https://github.com/kivy/python-for-android/issues/454) - \[revamp\] Can't load unicodedata module [\#453](https://github.com/kivy/python-for-android/issues/453) - \[revamp\] The revamp branch always prints the ToolchainCL object after running [\#452](https://github.com/kivy/python-for-android/issues/452) - \[revamp\] setuptools "wrong ELF class" issues [\#451](https://github.com/kivy/python-for-android/issues/451) - \[revamp\] Unpack archives that don't list their root directory [\#450](https://github.com/kivy/python-for-android/issues/450) - \[revamp\] Recipes can only depend on other recipes [\#449](https://github.com/kivy/python-for-android/issues/449) - \[revamp\] p4a silently fails if --private is not absolute [\#448](https://github.com/kivy/python-for-android/issues/448) - \[revamp\] Invalid syntax for python3 [\#444](https://github.com/kivy/python-for-android/issues/444) - \[revamp\] toolchain.py ignores recipes with errors [\#440](https://github.com/kivy/python-for-android/issues/440) - Error when trying to create an apk package with buildozer or with distribute.sh [\#435](https://github.com/kivy/python-for-android/issues/435) - pylibpd fails to compile [\#434](https://github.com/kivy/python-for-android/issues/434) - \[revamp\] --android\_api is ignored on SDL2 bootstrap [\#425](https://github.com/kivy/python-for-android/issues/425) - OSError: \[Errno 2\] No such file or directory: '/home/username/code/kivy/examples/demo/touchtracer' [\#424](https://github.com/kivy/python-for-android/issues/424) - \[revamp\] - Darwin patches applied on Linux [\#423](https://github.com/kivy/python-for-android/issues/423) - swift: md5sum changed - fix URL on a static content [\#421](https://github.com/kivy/python-for-android/issues/421) - PLATFORM \> 19: there is no sys/timeb.h [\#419](https://github.com/kivy/python-for-android/issues/419) - Numpy build fails if it detects system libraries and tries to link with them [\#417](https://github.com/kivy/python-for-android/issues/417) - MD5 opencv is incorrect in recipe opencv [\#411](https://github.com/kivy/python-for-android/issues/411) - numpy fails to build [\#409](https://github.com/kivy/python-for-android/issues/409) - twisted [\#403](https://github.com/kivy/python-for-android/issues/403) - ctypes callback function SIGSEGV [\#401](https://github.com/kivy/python-for-android/issues/401) - gstreamer recipe [\#400](https://github.com/kivy/python-for-android/issues/400) - buildozer needs markupsafe to build [\#399](https://github.com/kivy/python-for-android/issues/399) - Ctypes still not found \[$50\] [\#397](https://github.com/kivy/python-for-android/issues/397) - Documentation: example using startActivityForResult with bind\(on\_activity\_result=\) [\#388](https://github.com/kivy/python-for-android/issues/388) - Does not build on OSX [\#387](https://github.com/kivy/python-for-android/issues/387) - with softinput\_mode="pan", the window no longer pans back down when the keyboard is dismissed [\#380](https://github.com/kivy/python-for-android/issues/380) - android app crash on screen rotation [\#379](https://github.com/kivy/python-for-android/issues/379) - Harfbuzz compile issue on 15.04 - fatal error: asm-generic/posix\_types.h: No such file or directory [\#376](https://github.com/kivy/python-for-android/issues/376) - python fabric recipe fails [\#374](https://github.com/kivy/python-for-android/issues/374) - Build a release so this can be included in F-Droid [\#369](https://github.com/kivy/python-for-android/issues/369) - Enable armeabi-v7a-hard [\#366](https://github.com/kivy/python-for-android/issues/366) - bulldozer and distribute.sh [\#364](https://github.com/kivy/python-for-android/issues/364) - does this matter ? arm-linux-androideabi-gcc: error: kivy/graphics/opengl.c: No such file or directory [\#362](https://github.com/kivy/python-for-android/issues/362) - python 3 compatibility [\#359](https://github.com/kivy/python-for-android/issues/359) - softinput\_mode='pan' does not work well with orientation change of the device screen. [\#348](https://github.com/kivy/python-for-android/issues/348) - How can I pass String value from EditText In Android Activity to Python Script and also make the activity able to retrieve the String result from a function in the python script such as displaying the retrieved String in TextView ? [\#346](https://github.com/kivy/python-for-android/issues/346) - pygame.midi.init\(\) Failing on Android 4.4.4 - ImportError: No module named pypm [\#342](https://github.com/kivy/python-for-android/issues/342) - Error In building kivy android on Mac OSX [\#340](https://github.com/kivy/python-for-android/issues/340) - ButtonBehavior.on\_touch\_up dispatches on\_release immediately [\#339](https://github.com/kivy/python-for-android/issues/339) - Failed to build pure Python module included after `twisted` [\#337](https://github.com/kivy/python-for-android/issues/337) - The compiled APK crashes with error on \_imaging.so: not found [\#335](https://github.com/kivy/python-for-android/issues/335) - ctypes.py and \_ctypes.so are not available [\#333](https://github.com/kivy/python-for-android/issues/333) - After installing the Pillow does not compile the project. \(Mac OS\) [\#332](https://github.com/kivy/python-for-android/issues/332) - pygame.display.set\_mode runs out of memory [\#331](https://github.com/kivy/python-for-android/issues/331) - Python Service multiple instances [\#329](https://github.com/kivy/python-for-android/issues/329) - Kivy Launcher should include Plyer [\#328](https://github.com/kivy/python-for-android/issues/328) - Issue in Cython file compilation and building kivy.graphics.vertex\_instruction extensions [\#326](https://github.com/kivy/python-for-android/issues/326) - Failed Cython Compilation [\#325](https://github.com/kivy/python-for-android/issues/325) - Python3 Branch: jinja2 traceback on buildozer --verbose android debug [\#322](https://github.com/kivy/python-for-android/issues/322) - About ctypes [\#319](https://github.com/kivy/python-for-android/issues/319) - Audio loop not working on Android [\#318](https://github.com/kivy/python-for-android/issues/318) - Command ./distribute.sh -m "openssl pil kivy" results in error: command 'ccache' failed with exit status 1 [\#306](https://github.com/kivy/python-for-android/issues/306) - Bug with android.p4a\_whitelist in buildozer.spec file. [\#302](https://github.com/kivy/python-for-android/issues/302) - ctypes module not loaded [\#301](https://github.com/kivy/python-for-android/issues/301) - P4A builds stable instead of master [\#300](https://github.com/kivy/python-for-android/issues/300) - Use of SL4A [\#299](https://github.com/kivy/python-for-android/issues/299) - Github zipball doesn't work anymore [\#297](https://github.com/kivy/python-for-android/issues/297) - My `./distribute.sh` suddenly stop building today. [\#294](https://github.com/kivy/python-for-android/issues/294) - Create recipes for storm and psycopg2 [\#293](https://github.com/kivy/python-for-android/issues/293) - error in your VM Image after ./distribute.sh -m kivy [\#291](https://github.com/kivy/python-for-android/issues/291) - Error in Apk process building [\#290](https://github.com/kivy/python-for-android/issues/290) - IOError: \[Errno 2\] No usable temporary directory [\#289](https://github.com/kivy/python-for-android/issues/289) - Path commands on readme and official-website-toolchain don't seem to work [\#281](https://github.com/kivy/python-for-android/issues/281) - Twisted/recipe.sh can't find zope.interface [\#280](https://github.com/kivy/python-for-android/issues/280) - cython uses system python instead of hostpython [\#277](https://github.com/kivy/python-for-android/issues/277) - Twisted recipe doesn't work in 32-bit build environment [\#276](https://github.com/kivy/python-for-android/issues/276) - distribute fails to build flask or sqlite3 : Error: libpymodules.so - no input files [\#275](https://github.com/kivy/python-for-android/issues/275) - \# Command failed: ./distribute.sh -m "kivy" -d "myapp" [\#271](https://github.com/kivy/python-for-android/issues/271) - creating new service does not start the service [\#270](https://github.com/kivy/python-for-android/issues/270) - Touch input causes big slowdown \( from get\_keyboard\_height \) - can use 12% to 30%+ of cpu before even passing to kivy [\#268](https://github.com/kivy/python-for-android/issues/268) - Sticky services [\#267](https://github.com/kivy/python-for-android/issues/267) - --private clobers /data/data/\[package name\]/files directory [\#263](https://github.com/kivy/python-for-android/issues/263) - touchtracer not working [\#262](https://github.com/kivy/python-for-android/issues/262) - compile error: sys/timeb.h not found [\#261](https://github.com/kivy/python-for-android/issues/261) - Python file is not converted to bytecode when building apk [\#257](https://github.com/kivy/python-for-android/issues/257) - New Version of NDK [\#256](https://github.com/kivy/python-for-android/issues/256) - Full control of the AndroidManifest.xml \[$20\] [\#255](https://github.com/kivy/python-for-android/issues/255) - extra characters in textinput on Android [\#247](https://github.com/kivy/python-for-android/issues/247) - Feature Request: add checkNetwork to \_android.pyx [\#244](https://github.com/kivy/python-for-android/issues/244) - Env variables for ANDROIDAPI ignored [\#241](https://github.com/kivy/python-for-android/issues/241) - Kivy Android VM doesn't have plyer recipe [\#239](https://github.com/kivy/python-for-android/issues/239) - Switch from .sh to pythonic toolchain [\#238](https://github.com/kivy/python-for-android/issues/238) - Update twisted to 14.0.0 [\#237](https://github.com/kivy/python-for-android/issues/237) - ./distribute.sh -u option doesn't work [\#236](https://github.com/kivy/python-for-android/issues/236) - \*.so is too small to be an ELF executable [\#234](https://github.com/kivy/python-for-android/issues/234) - Cannot run Py4A APKs on Android x86 [\#233](https://github.com/kivy/python-for-android/issues/233) - C extension overlap [\#232](https://github.com/kivy/python-for-android/issues/232) - Can't build django [\#231](https://github.com/kivy/python-for-android/issues/231) - System.currentTimeMillis\(\) returns a negative number [\#229](https://github.com/kivy/python-for-android/issues/229) - `future_builtins` shouldn't be blacklisted [\#228](https://github.com/kivy/python-for-android/issues/228) - Non-clear direction in readme guide [\#227](https://github.com/kivy/python-for-android/issues/227) - OSX build error if more than one pure-python module [\#226](https://github.com/kivy/python-for-android/issues/226) - unknown type name 'SDL\_BlitMap' while buildozer apk generation [\#225](https://github.com/kivy/python-for-android/issues/225) - error for package apk [\#223](https://github.com/kivy/python-for-android/issues/223) - collect2: ld returned 1 exit status [\#221](https://github.com/kivy/python-for-android/issues/221) - buildozer not works more [\#220](https://github.com/kivy/python-for-android/issues/220) - \[moved\] pyo files are not being recreated by ./build.py in python for android [\#216](https://github.com/kivy/python-for-android/issues/216) - error: could not create '/usr/local/lib/python2.7/site-packages/PIL': Permission denied [\#215](https://github.com/kivy/python-for-android/issues/215) - collect2: ld returned 1 exit status [\#213](https://github.com/kivy/python-for-android/issues/213) - virtualenv not entering [\#212](https://github.com/kivy/python-for-android/issues/212) - cymunk doesn't get copied to dist/default/ [\#211](https://github.com/kivy/python-for-android/issues/211) - No Python 3 Support [\#210](https://github.com/kivy/python-for-android/issues/210) - HELP! ./distribute.sh failed to locate arm-linux-androideabi-gcc [\#209](https://github.com/kivy/python-for-android/issues/209) - TextInput not behaving with SwiftKey [\#198](https://github.com/kivy/python-for-android/issues/198) - Example Applications? [\#197](https://github.com/kivy/python-for-android/issues/197) - installing Pydev has encountered a problem [\#196](https://github.com/kivy/python-for-android/issues/196) - buildozer apk build problem [\#195](https://github.com/kivy/python-for-android/issues/195) - Zope2 at "Downloading/unpacking zope.security" [\#190](https://github.com/kivy/python-for-android/issues/190) - \_scproxy import error when building on Mac with 'requests' lib [\#186](https://github.com/kivy/python-for-android/issues/186) - Kivy TextInput doesn't work with SwiftKey keyboard [\#184](https://github.com/kivy/python-for-android/issues/184) - Error in using debug flag, calling ANT debugger? [\#179](https://github.com/kivy/python-for-android/issues/179) - Update setuptools version [\#176](https://github.com/kivy/python-for-android/issues/176) - Problems with distribute.sh [\#175](https://github.com/kivy/python-for-android/issues/175) - Rst editor crashing on android [\#174](https://github.com/kivy/python-for-android/issues/174) - Doubt about distribute.sh [\#173](https://github.com/kivy/python-for-android/issues/173) - Own Activities in AndroidManifest.xml [\#172](https://github.com/kivy/python-for-android/issues/172) - Error to execute command ./distribute.sh -m "openssl pil kivy" [\#167](https://github.com/kivy/python-for-android/issues/167) - keyboard stays on screen in android, after app exit [\#166](https://github.com/kivy/python-for-android/issues/166) - ffmpeg ndk r9 incompatibility [\#165](https://github.com/kivy/python-for-android/issues/165) - kivy touchtracer apk not running on emmulator [\#162](https://github.com/kivy/python-for-android/issues/162) - fatal error: stdlib.h: No such file or directory [\#159](https://github.com/kivy/python-for-android/issues/159) - Add psutil recipe [\#157](https://github.com/kivy/python-for-android/issues/157) - Failure to compile .py should cause exit [\#156](https://github.com/kivy/python-for-android/issues/156) - Docs on the readthedocs are old [\#155](https://github.com/kivy/python-for-android/issues/155) - non full screen touch offset on android [\#153](https://github.com/kivy/python-for-android/issues/153) - Android NDK r9 Fails [\#149](https://github.com/kivy/python-for-android/issues/149) - Android app crashes on rotation to landscape [\#148](https://github.com/kivy/python-for-android/issues/148) - configure: error: C compiler cannot create executables [\#145](https://github.com/kivy/python-for-android/issues/145) - GCC 4.4.3 depreciated in android NDK [\#143](https://github.com/kivy/python-for-android/issues/143) - dlopen fail on android 4.3 [\#141](https://github.com/kivy/python-for-android/issues/141) - new dependency ordering broke some usecases with buildozer [\#140](https://github.com/kivy/python-for-android/issues/140) - Android Keyboard information [\#139](https://github.com/kivy/python-for-android/issues/139) - Android keyboards do not recognize Password fields as secure passwords [\#138](https://github.com/kivy/python-for-android/issues/138) - kivy.network.urlrequest: No callback parameter: on\_failure [\#137](https://github.com/kivy/python-for-android/issues/137) - UnicodeDecodeError [\#136](https://github.com/kivy/python-for-android/issues/136) - arm-linux-androideabi-gcc: no input files [\#133](https://github.com/kivy/python-for-android/issues/133) - Unable to build APK [\#132](https://github.com/kivy/python-for-android/issues/132) - apk doesn't unpack on first load [\#131](https://github.com/kivy/python-for-android/issues/131) - kivy 1.8 \(testing\) UnicodeDecodeError: 'ascii' codec can't decode byte... [\#129](https://github.com/kivy/python-for-android/issues/129) - presplash "crazy" position when change the orientation the cellphone [\#127](https://github.com/kivy/python-for-android/issues/127) - subprocess.check\_output\(\["ping", "-c", "3", hostname\]\) non-zero exit code 2 [\#126](https://github.com/kivy/python-for-android/issues/126) - Testing/Enabling armeabi-v7a [\#123](https://github.com/kivy/python-for-android/issues/123) - distribute.sh fails when using the 64bit Android NDK [\#116](https://github.com/kivy/python-for-android/issues/116) - encoding error: UnicodeDecodeError: 'ascii' codec can't decode byte 0xd0 in position 0: ordinal not in range\(128\) [\#114](https://github.com/kivy/python-for-android/issues/114) - IOError: \[Errno 20\] Not a directory: [\#113](https://github.com/kivy/python-for-android/issues/113) - Add setting in building package to allow switching to sleep mode [\#111](https://github.com/kivy/python-for-android/issues/111) - Android keyboard autosuggestion bug [\#110](https://github.com/kivy/python-for-android/issues/110) - Command ./distribute.sh -m "pyjnius kivy" ends with error [\#109](https://github.com/kivy/python-for-android/issues/109) - Feature request: Android service [\#107](https://github.com/kivy/python-for-android/issues/107) - \[recipe - pylibpd\] unpacking does not work [\#103](https://github.com/kivy/python-for-android/issues/103) - \[master\] Unzip fails [\#102](https://github.com/kivy/python-for-android/issues/102) - old unixcompiler.py bug when using ccache g++ [\#100](https://github.com/kivy/python-for-android/issues/100) - Non ascii key inputs not dispatched [\#97](https://github.com/kivy/python-for-android/issues/97) - Github archives are named master by default, breaking build [\#95](https://github.com/kivy/python-for-android/issues/95) - Python for Android not compiling sqlite3 module [\#91](https://github.com/kivy/python-for-android/issues/91) - Buggy dependencies handling with multiple ./distribute.sh [\#90](https://github.com/kivy/python-for-android/issues/90) - build.py has "/usr/bin/python2" hard-coded [\#88](https://github.com/kivy/python-for-android/issues/88) - touchtracer bug - dp migration? [\#87](https://github.com/kivy/python-for-android/issues/87) - kivy build failing [\#86](https://github.com/kivy/python-for-android/issues/86) - Unable to resolve project target [\#85](https://github.com/kivy/python-for-android/issues/85) - Compile stop when it copy java code [\#83](https://github.com/kivy/python-for-android/issues/83) - touchtracer.apk not working [\#82](https://github.com/kivy/python-for-android/issues/82) - If the kivy module is built at the same time as others, it fails [\#81](https://github.com/kivy/python-for-android/issues/81) - Build with lxml not working [\#79](https://github.com/kivy/python-for-android/issues/79) - Compilation Error [\#78](https://github.com/kivy/python-for-android/issues/78) - properties.so is not a valid ELF object [\#77](https://github.com/kivy/python-for-android/issues/77) - Unfocusing kivy's TextInput [\#76](https://github.com/kivy/python-for-android/issues/76) - Camera on android [\#75](https://github.com/kivy/python-for-android/issues/75) - Using stable source... [\#74](https://github.com/kivy/python-for-android/issues/74) - fails to import text\_sdlttf [\#73](https://github.com/kivy/python-for-android/issues/73) - Standard module for SQLite not available [\#72](https://github.com/kivy/python-for-android/issues/72) - Kivy on Android galaxy s3 apk run exception [\#70](https://github.com/kivy/python-for-android/issues/70) - Check for build dependencies [\#68](https://github.com/kivy/python-for-android/issues/68) - Compile failing [\#66](https://github.com/kivy/python-for-android/issues/66) - First label not rendered on android [\#64](https://github.com/kivy/python-for-android/issues/64) - arm/limits.h: No such file or directory [\#63](https://github.com/kivy/python-for-android/issues/63) - Compiling hostpython doesn't work [\#62](https://github.com/kivy/python-for-android/issues/62) - when android-sdk platform tool is not installed. build process run into error. [\#61](https://github.com/kivy/python-for-android/issues/61) - Incorrect default Python executable [\#60](https://github.com/kivy/python-for-android/issues/60) - error when build distribution on debian squeeze. [\#57](https://github.com/kivy/python-for-android/issues/57) - Many small build issues in Ubuntu 12.04.1 x64 [\#55](https://github.com/kivy/python-for-android/issues/55) - csv module [\#54](https://github.com/kivy/python-for-android/issues/54) - Allow the screen to timeout in Android [\#53](https://github.com/kivy/python-for-android/issues/53) - Order matters in ./distribute.sh -m options [\#50](https://github.com/kivy/python-for-android/issues/50) - Initial distribution build fails with "unterminated substitute pattern" [\#47](https://github.com/kivy/python-for-android/issues/47) - Creating Widgets [\#46](https://github.com/kivy/python-for-android/issues/46) - Unable to use the crypt lib. [\#45](https://github.com/kivy/python-for-android/issues/45) - verify me [\#44](https://github.com/kivy/python-for-android/issues/44) - Graphics lost returning from "HOME" on Motorola Photon \(Sprint built 2.3.4\) [\#42](https://github.com/kivy/python-for-android/issues/42) - Name clash with http://code.google.com/p/python-for-android/ [\#39](https://github.com/kivy/python-for-android/issues/39) - Installed apk crash at loading... [\#38](https://github.com/kivy/python-for-android/issues/38) - Fail to include any module other than Kivy [\#37](https://github.com/kivy/python-for-android/issues/37) - Many issues on OS X running ./distribute.sh -m "kivy" [\#36](https://github.com/kivy/python-for-android/issues/36) - Unable to build python-for-android [\#34](https://github.com/kivy/python-for-android/issues/34) - Kivy / Python-for-android : Build.py fails to build an android package apk [\#33](https://github.com/kivy/python-for-android/issues/33) - Unable to use \ as loader! [\#31](https://github.com/kivy/python-for-android/issues/31) - build stuck at `assets/private.mp3: private/include/python2.7/pyconfig.h` [\#29](https://github.com/kivy/python-for-android/issues/29) - Can't build with all modules [\#27](https://github.com/kivy/python-for-android/issues/27) - ask a question about the touchtracer demo [\#26](https://github.com/kivy/python-for-android/issues/26) - Error: Target id 'android-8' is not valid. [\#25](https://github.com/kivy/python-for-android/issues/25) - Build Error: "/usr/lib/libpython2.7.so: file not recognized: File format not recognized" [\#16](https://github.com/kivy/python-for-android/issues/16) - FFMpeg doesn't build [\#13](https://github.com/kivy/python-for-android/issues/13) - Problem init environment [\#12](https://github.com/kivy/python-for-android/issues/12) - error when build the APK [\#10](https://github.com/kivy/python-for-android/issues/10) - arm-linux-androideabi-gcc: Internal error: Killed \(program cc1\) [\#9](https://github.com/kivy/python-for-android/issues/9) - Build: Pyrex error [\#7](https://github.com/kivy/python-for-android/issues/7) - Where is the example? [\#5](https://github.com/kivy/python-for-android/issues/5) - Build error [\#4](https://github.com/kivy/python-for-android/issues/4) - Restore libpymodules.so behavior [\#3](https://github.com/kivy/python-for-android/issues/3) - Guide step 2 issues [\#2](https://github.com/kivy/python-for-android/issues/2) - compile failing [\#1](https://github.com/kivy/python-for-android/issues/1) **Merged pull requests:** - Update README.md [\#1096](https://github.com/kivy/python-for-android/pull/1096) ([zed](https://github.com/zed)) - Deleted whitespace [\#1091](https://github.com/kivy/python-for-android/pull/1091) ([Fogapod](https://github.com/Fogapod)) - Made graph recognise python\_depends [\#1089](https://github.com/kivy/python-for-android/pull/1089) ([inclement](https://github.com/inclement)) - Moved logger bytes conversion to affect all lines [\#1088](https://github.com/kivy/python-for-android/pull/1088) ([inclement](https://github.com/inclement)) - Made logger convert output to utf-8 including errors [\#1087](https://github.com/kivy/python-for-android/pull/1087) ([inclement](https://github.com/inclement)) - Recipe import fixes [\#1086](https://github.com/kivy/python-for-android/pull/1086) ([inclement](https://github.com/inclement)) - Various Ethereum related recipes fixes [\#1080](https://github.com/kivy/python-for-android/pull/1080) ([AndreMiras](https://github.com/AndreMiras)) - Ethereum related recipes [\#1068](https://github.com/kivy/python-for-android/pull/1068) ([AndreMiras](https://github.com/AndreMiras)) - implement wakelock for sdl2 [\#1066](https://github.com/kivy/python-for-android/pull/1066) ([brentpicasso](https://github.com/brentpicasso)) - Fix typo in pythonforandroid/recipe.py [\#1065](https://github.com/kivy/python-for-android/pull/1065) ([jupart](https://github.com/jupart)) - Rewrite recipe graph [\#1064](https://github.com/kivy/python-for-android/pull/1064) ([inclement](https://github.com/inclement)) - Use Kivy setup.py flag instead of rmtree [\#1063](https://github.com/kivy/python-for-android/pull/1063) ([KeyWeeUsr](https://github.com/KeyWeeUsr)) - Delete the kivy-examples dir from the dist under python3 [\#1062](https://github.com/kivy/python-for-android/pull/1062) ([inclement](https://github.com/inclement)) - Added command handling if p4a is run with no arguments [\#1059](https://github.com/kivy/python-for-android/pull/1059) ([inclement](https://github.com/inclement)) - Call avdmanager instead of android in the SDK [\#1057](https://github.com/kivy/python-for-android/pull/1057) ([inclement](https://github.com/inclement)) - Updated Kivy recipe to pull 1.10.0 [\#1048](https://github.com/kivy/python-for-android/pull/1048) ([inclement](https://github.com/inclement)) - \[Core\] Get the latest version of requests. [\#1045](https://github.com/kivy/python-for-android/pull/1045) ([hobbestigrou](https://github.com/hobbestigrou)) - Recipe for websocket-client [\#1039](https://github.com/kivy/python-for-android/pull/1039) ([debauchery1st](https://github.com/debauchery1st)) - Recipe updates and small fixes to build process [\#1034](https://github.com/kivy/python-for-android/pull/1034) ([bobatsar](https://github.com/bobatsar)) - Added warning about different path behavior in new toolchain service [\#1028](https://github.com/kivy/python-for-android/pull/1028) ([Bakterija](https://github.com/Bakterija)) - Recipe for Pymunk [\#1026](https://github.com/kivy/python-for-android/pull/1026) ([viblo](https://github.com/viblo)) - Fixed protobuf cpp [\#1021](https://github.com/kivy/python-for-android/pull/1021) ([Deniskore](https://github.com/Deniskore)) - Fix packaging failure when user has large UID by using GNU format over USTAR format [\#1015](https://github.com/kivy/python-for-android/pull/1015) ([pts-dorianpula](https://github.com/pts-dorianpula)) - Remove the excessive ccache in the command [\#1014](https://github.com/kivy/python-for-android/pull/1014) ([MaChengxin](https://github.com/MaChengxin)) - Added support for Python 3.6 [\#1011](https://github.com/kivy/python-for-android/pull/1011) ([inclement](https://github.com/inclement)) - Made dist names different for py2/py3 testapps [\#1010](https://github.com/kivy/python-for-android/pull/1010) ([inclement](https://github.com/inclement)) - Documentation mistake corrected [\#1005](https://github.com/kivy/python-for-android/pull/1005) ([yaki29](https://github.com/yaki29)) - Fixed the blacklisting of sqlite3 under sdl2 [\#1000](https://github.com/kivy/python-for-android/pull/1000) ([inclement](https://github.com/inclement)) - documentation recipe.rst spelling change [\#993](https://github.com/kivy/python-for-android/pull/993) ([yaki29](https://github.com/yaki29)) - Move the unpackFiles step into an AsyncTask [\#990](https://github.com/kivy/python-for-android/pull/990) ([kollivier](https://github.com/kollivier)) - Add recipe for google protobuf cpp implementation [\#987](https://github.com/kivy/python-for-android/pull/987) ([bakwc](https://github.com/bakwc)) - Fix python2 for webview [\#981](https://github.com/kivy/python-for-android/pull/981) ([KeyWeeUsr](https://github.com/KeyWeeUsr)) - Updated Kivy recipe to work with Kivy master [\#968](https://github.com/kivy/python-for-android/pull/968) ([inclement](https://github.com/inclement)) - Switch --permission to accept \>=1 parameters [\#966](https://github.com/kivy/python-for-android/pull/966) ([KeyWeeUsr](https://github.com/KeyWeeUsr)) - Add ``screenSize`` to android:configChanges in AndroidManifest.xml if API \>= 13 [\#956](https://github.com/kivy/python-for-android/pull/956) ([rnixx](https://github.com/rnixx)) - Add ffpyplayer and dependencies recipes for new toolchain. [\#954](https://github.com/kivy/python-for-android/pull/954) ([germn](https://github.com/germn)) - Doc fixes [\#950](https://github.com/kivy/python-for-android/pull/950) ([inclement](https://github.com/inclement)) - Fixed release mode \(--release\) with sdl2 [\#949](https://github.com/kivy/python-for-android/pull/949) ([inclement](https://github.com/inclement)) - Made the sdl2 bootstrap strip unneeded symbols with python3 [\#948](https://github.com/kivy/python-for-android/pull/948) ([inclement](https://github.com/inclement)) - Compile pyo with sdl2 [\#944](https://github.com/kivy/python-for-android/pull/944) ([inclement](https://github.com/inclement)) - Update apis.rst [\#941](https://github.com/kivy/python-for-android/pull/941) ([codytrey](https://github.com/codytrey)) - Update recipes sqlite3 to 3.15.1 and apsw to 3.15.0-r1 both with FTS4 enabled [\#936](https://github.com/kivy/python-for-android/pull/936) ([brussee](https://github.com/brussee)) - Add remove\_presplash [\#930](https://github.com/kivy/python-for-android/pull/930) ([KeyWeeUsr](https://github.com/KeyWeeUsr)) - Complete closure of the application with SDL2. [\#923](https://github.com/kivy/python-for-android/pull/923) ([dkrukouski](https://github.com/dkrukouski)) - Testapp improvements [\#919](https://github.com/kivy/python-for-android/pull/919) ([inclement](https://github.com/inclement)) - Make boost recipe compatible with Google NDK 13.0 [\#916](https://github.com/kivy/python-for-android/pull/916) ([brussee](https://github.com/brussee)) - Completing md5sum comparison and download [\#915](https://github.com/kivy/python-for-android/pull/915) ([thopiekar](https://github.com/thopiekar)) - Empty: Adding .gitkeep in bootstraps/empty/build [\#912](https://github.com/kivy/python-for-android/pull/912) ([thopiekar](https://github.com/thopiekar)) - fix layout listener related issues. Closes \#890 [\#911](https://github.com/kivy/python-for-android/pull/911) ([akshayaurora](https://github.com/akshayaurora)) - Fixed app path for SDL2 services [\#909](https://github.com/kivy/python-for-android/pull/909) ([inclement](https://github.com/inclement)) - fixed recipe zope\_interface [\#908](https://github.com/kivy/python-for-android/pull/908) ([goffi-contrib](https://github.com/goffi-contrib)) - Added "presplash"\_color argument [\#906](https://github.com/kivy/python-for-android/pull/906) ([mrhdias](https://github.com/mrhdias)) - Added argument to set the loading screen background color [\#905](https://github.com/kivy/python-for-android/pull/905) ([mrhdias](https://github.com/mrhdias)) - Easy way to set the presplash background color [\#904](https://github.com/kivy/python-for-android/pull/904) ([mrhdias](https://github.com/mrhdias)) - Allow installation on Windows [\#902](https://github.com/kivy/python-for-android/pull/902) ([ethanhs](https://github.com/ethanhs)) - Made clean-recipe-build delete dists [\#901](https://github.com/kivy/python-for-android/pull/901) ([inclement](https://github.com/inclement)) - Improved log for missing python modules [\#900](https://github.com/kivy/python-for-android/pull/900) ([inclement](https://github.com/inclement)) - Added textinput scatter testapp [\#899](https://github.com/kivy/python-for-android/pull/899) ([inclement](https://github.com/inclement)) - Allow list of permissions for bdist [\#898](https://github.com/kivy/python-for-android/pull/898) ([KeyWeeUsr](https://github.com/KeyWeeUsr)) - Fix bdistapk for launcher [\#896](https://github.com/kivy/python-for-android/pull/896) ([KeyWeeUsr](https://github.com/KeyWeeUsr)) - Added symlink\_java\_src dev option [\#894](https://github.com/kivy/python-for-android/pull/894) ([inclement](https://github.com/inclement)) - Port launcher to SDL2 bootstrap [\#891](https://github.com/kivy/python-for-android/pull/891) ([KeyWeeUsr](https://github.com/KeyWeeUsr)) \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* ================================================ FILE: CODE_OF_CONDUCT.md ================================================ In the interest of fostering an open and welcoming community, we as contributors and maintainers need to ensure participation in our project and our sister projects is a harassment-free and positive experience for everyone. It is vital that all interaction is conducted in a manner conveying respect, open-mindedness and gratitude. Please consult the [latest Kivy Code of Conduct](https://github.com/kivy/kivy/blob/master/CODE_OF_CONDUCT.md). ================================================ FILE: CONTACT.md ================================================ # Contacting the Kivy Team If you are looking to contact the Kivy Team (who are responsible for managing the python-for-android project), including looking for support, please see our latest [Contact Us](https://github.com/kivy/kivy/blob/master/CONTACT.md) document. ================================================ FILE: CONTRIBUTING.md ================================================ # Contribution Guidelines python-for-android is part of the [Kivy](https://kivy.org) ecosystem - a large group of products used by many thousands of developers for free, but it is built entirely by the contributions of volunteers. We welcome (and rely on) users who want to give back to the community by contributing to the project. Contributions can come in many forms. See the latest [Contribution Guidelines](https://github.com/kivy/kivy/blob/master/CONTRIBUTING.md) for how you can help us. .. warning:: The python-for-android process differs in small but important ways from the Kivy framework's process. See below. ## Development model Unlike the Kivy framework, python-for-android is developed using the following model: - The `master` branch always represents the latest stable release. - The `develop` branch is the most up to date with new contributions. - Releases happen periodically, and consist of merging the current `develop` branch into `master`. This means pull requests for python-for-android code and documentation submissions should be made to the `develop` branch, not the `master` branch. For reference, this is based on a [Git flow](https://nvie.com/posts/a-successful-git-branching-model/) model, although we don't follow this religiously. ## Versioning python-for-android releases currently use [calendar versioning](https://calver.org/). Release numbers are of the form YYYY.MM.DD. We use calendar versioning because in practice, changes in python-for-android are often driven by updates or adjustments in the Android build tools. It's usually best for users to be working from the latest release. We try to maintain backwards compatibility even while internals are changing. ## History In 2015, these tools were rewritten to provide a new, easier-to-use and easier-to-extend interface. If you'd like to browse the old toolchain, its status is [recorded for posterity](https://github.com/kivy/python-for-android/tree/old_toolchain). In the last quarter of 2018, the Python recipes were changed. The 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. 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). On March 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. On August 2021, we added support for Android App Bundle (aab). As a collateral benefit, we now support multi-arch apk. ## Code Quality ### Python Linting Python code is linted using flake8. Run it locally with: ```bash tox -e pep8 ``` ### Java Linting Java source files in the bootstrap directories are linted using [Spotless](https://github.com/diffplug/spotless) with Google Java Format (AOSP style). The CI runs this check automatically. **Local execution** (requires Java 17+): ```bash # Check for violations make java-lint # Auto-fix violations make java-lint-fix ``` The Makefile uses the Gradle wrapper (`gradlew`), which automatically downloads the correct Gradle version on first run. No manual Gradle installation is required. **Using Docker** (if you don't have Java 17): ```bash # Check for violations make docker/java-lint # Auto-fix violations make docker/java-lint-fix ``` The Docker approach builds the project's Docker image (which includes Java 17) and runs the linting inside the container. **What gets linted:** - All `.java` files in `pythonforandroid/bootstraps/*/build/src/main/java/` - Excludes third-party code (`org/kamranzafar/jtar/`) **Formatting rules applied:** - Google Java Format with AOSP style (Android-friendly indentation) - Removal of unused imports - Trailing whitespace trimming - Files end with newline ## Creating a new release (These instructions are for core developers, not casual contributors.) New releases follow these steps: - Create a new branch `release-YYYY.MM.DD` based on the `develop` branch. - `git checkout -b release-YYYY.MM.DD develop` - Create a Github pull request to merge `release-YYYY.MM.DD` into `master`. - Complete all steps in the [release checklist](#Release_checklist), and document this in the pull request (copy the checklist into the PR text) At this point, wait for reviewer approval and conclude any discussion that arises. To complete the release: - Merge the release branch to the `master` branch. - Also merge the release branch to the `develop` branch. - Tag the release commit in `master`, with tag `vYYYY.MM.DD`. Include a short summary of the changes. - Release distributions and PyPI upload should be [handled by the CI](https://github.com/kivy/python-for-android/blob/v2020.04.29/.travis.yml#L60-L70). - Add to the GitHub release page (see e.g. [this example](https://github.com/kivy/python-for-android/releases/tag/v2019.06.06): - The python-for-android README summary - A short list of major changes in this release, if any - A changelog summarising merge commits since the last release - The release sdist and wheel(s) ## Release checklist - [ ] Check that the builds are passing - [ ] [GitHub Action](https://github.com/kivy/python-for-android/actions) - [ ] Run the tests locally via `tox`: this performs some long-running tests that are skipped on github-actions. - [ ] Build and run the [on_device_unit_tests](https://github.com/kivy/python-for-android/tree/master/testapps/on_device_unit_tests) app using buildozer. Check that they all pass. - [ ] Build (or download from github actions) and run the following [testapps](https://github.com/kivy/python-for-android/tree/master/testapps/on_device_unit_tests) for arch `armeabi-v7a` and `arm64-v8a`: - [ ] on_device_unit_tests - [ ] `armeabi-v7a` (`cd testapps/on_device_unit_tests && PYTHONPATH=.:../../ python3 setup.py apk --ndk-dir= --sdk-dir= --arch=armeabi-v7a --debug`) - [ ] `arm64-v8a` (`cd testapps/on_device_unit_tests && PYTHONPATH=.:../../ python3 setup.py apk --ndk-dir= --sdk-dir= --arch=arm64-v8a --debug`) - [ ] Check that the version number is correct ## How python-for-android uses `pip` *Last update: July 2019* This section is meant to provide a quick summary how python-for-android uses pip and Python packages in its build process. **It is written for a Python packager's point of view, not for regular end users or contributors,** to assist with making pip developers and other packaging experts aware of p4a's packaging needs. Please note this section just attempts to neutrally list the current mechanisms, so some of this isn't necessarily meant to stay but just how things work inside p4a in this very moment. ### Basic concepts *(This part repeats other parts of the docs, for the sake of making this a more independent read)* p4a builds & packages a Python application for use on Android. It does this by providing a Java wrapper, and for graphical applications an SDL2-based wrapper which can be used with the Kivy framework if desired (or alternatively just plain PySDL2). Any such the Python application will likely have further library dependencies to do its work. p4a supports two types of package dependencies for a project: **Recipe:** Install a script in custom p4a format. Can either install C/C++ or other software that cannot be pulled in via pip, or software that can be installed via pip but break on Android by default. These are maintained primarily inside the p4a source tree by p4a contributors and interested folks. **Python package:** any random pip python package can be directly installed if it doesn't need adjustments to work for Android. p4a will map any dependency to an internal recipe if present, and otherwise use pip to obtain it regularly from whatever external source. ### Install process regarding packages The install/build process of a p4a project, as triggered by the `p4a apk` command, roughly works as follows in regards to Python packages: 1. The user has specified a project folder to install. This is either just a folder with Python scripts and a `main.py`, or it may also have a `pyproject.toml` for a more standardized install. 2. Dependencies are collected: they can be either specified via ``--requirements`` as a list of names or pip-style URLs, or p4a can optionally scan them from a project folder via the pep517 library (if there is a `pyproject.toml` or `setup.py`). 3. The collected dependencies are mapped to p4a's recipes if any are available for them, otherwise they're kept around as external regular package references. 4. All the dependencies mapped to recipes are built via p4a's internal mechanisms to build these recipes. (This may or may not indirectly use pip, depending on whether the recipe wraps a python package or not and uses pip to install or not.) 5. **If the user has specified to install the project in standardized ways,** then the `setup.py`/whatever build system of the project will be run. This happens with cross compilation set up (`CC`/`CFLAGS`/... set to use the proper toolchain) and a custom site-packages location. The actual command is a simple `pip install .` in the project folder with some extra options: e.g. all dependencies that were already installed by recipes will be pinned with a `-c` constraints file to make sure pip won't install them, and build isolation will be disabled via ``--no-build-isolation`` so pip doesn't reinstall recipe-packages on its own. **If the user has not specified to use standardized build approaches**, p4a will simply install all the remaining dependencies that weren't mapped to recipes directly and just plain copy in the user project without installing. Any `setup.py` or `pyproject.toml` of the user project will then be ignored in this step. 6. Google's gradle is invoked to package it all up into an `.apk`. ### Overall process / package relevant notes for p4a Here are some common things worth knowing about python-for-android's dealing with python packages: - Packages will work fine without a recipe if: * they would also build on Linux ARM, * don't use any API not available in the NDK if they use native code, and * don't use any weird compiler flags the toolchain doesn't like if they use native code. * works with cross compilation. - There is currently no easy way for a package to know it is being cross-compiled (at least that we know of) other than examining the `CC` compiler that was set, or that it is being cross-compiled for Android specifically. If that breaks a package, it currently needs to be worked around with a recipe. - If a package does **not** work, p4a developers will often create a recipe instead of getting upstream to fix it because p4a simply is too niche. - Most packages without native code will just work out of the box. Many with native code tend not to, especially if complex, e.g. numpy. - Anything mapped to a p4a recipe cannot be just reinstalled by pip, specifically also not inside build isolation as a dependency. (It *may* work if the patches of the recipe are just relevant to fix runtime issues.) Therefore as of now, the best way to deal with this limitation seems to be to keep build isolation always off. ### Ideas for the future regarding packaging - We in overall prefer to use the recipe mechanism less if we can. Overall, the recipes are just a collection of workarounds. It may look quite hacky from the outside, since p4a version pins recipe-wrapped packages usually to make the patches reliably apply. This creates work for the recipes to be kept up-to-date, and obviously this approach doesn't scale too well. However, it has ended up as a quite practical interim solution until better ways are found. - Obviously, it would be nice if packages could know they are being cross-compiled, and for Android specifically. We aren't currently aware of any good mechanism for that. - If pip could actually run the recipes (instead of p4a wrapping pip and doing so) then this might even allow build isolation to work - but this might be too complex to get working. It might be more practical to just gradually reduce the reliance on recipes instead and make more packages work out of the box. This has been done e.g. with improvements to the cross-compile environment being set up automatically, and we're open for any ideas on how to improve this. ================================================ FILE: Dockerfile ================================================ # Dockerfile with: # - Android build environment # - python-for-android dependencies # # Build with: # docker build --tag=p4a --file Dockerfile . # # Run with: # docker run -it --rm p4a /bin/sh -c '. venv/bin/activate && p4a apk --help' # # Or for interactive shell: # docker run -it --rm p4a # # Note: # Use 'docker run' without '--rm' flag for keeping the container and use # 'docker commit ' to extend the original image # If platform is not specified, by default the target platform of the build request is used. # This is not what we want, as Google doesn't provide a linux/arm64 compatible NDK. # See: https://docs.docker.com/engine/reference/builder/#from FROM --platform=linux/amd64 ubuntu:22.04 # configure locale RUN apt -y update -qq > /dev/null \ && DEBIAN_FRONTEND=noninteractive 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 > /dev/null \ && DEBIAN_FRONTEND=noninteractive apt install -qq --yes --no-install-recommends \ ca-certificates \ curl \ && apt -y autoremove \ && apt -y clean \ && rm -rf /var/lib/apt/lists/* # 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 WORK_DIR="${HOME_DIR}/app" \ PATH="${HOME_DIR}/.local/bin:${PATH}" \ ANDROID_HOME="${HOME_DIR}/.android" \ JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64" # install system dependencies RUN ${RETRY} apt -y update -qq > /dev/null \ && ${RETRY} DEBIAN_FRONTEND=noninteractive apt install -qq --yes --no-install-recommends \ ant \ autoconf \ automake \ autopoint \ ccache \ cmake \ g++ \ gcc \ git \ lbzip2 \ libffi-dev \ libltdl-dev \ libtool \ libssl-dev \ make \ openjdk-17-jdk \ patch \ pkg-config \ python3 \ python3-dev \ python3-pip \ python3-venv \ sudo \ unzip \ wget \ zip \ && apt -y autoremove \ && apt -y clean \ && rm -rf /var/lib/apt/lists/* # 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} RUN mkdir ${ANDROID_HOME} && chown --recursive ${USER} ${HOME_DIR} ${ANDROID_HOME} USER ${USER} # Download and install android's NDK/SDK COPY --chown=user:user ci/makefiles/android.mk /tmp/android.mk RUN make --file /tmp/android.mk \ && sudo rm /tmp/android.mk # install python-for-android from current branch COPY --chown=user:user Makefile README.md setup.py pythonforandroid/__init__.py ${WORK_DIR}/ RUN mkdir pythonforandroid \ && mv __init__.py pythonforandroid/ \ && make virtualenv \ && rm -rf ~/.cache/ COPY --chown=user:user . ${WORK_DIR} ================================================ FILE: FAQ.md ================================================ # FAQ for python-for-android (p4a) ## Introduction python-for-android (p4a) is a development tool that packages Python apps into binaries that can run on Android devices. ### Sibling Projects: This tool was originally developed for apps produced with the [Kivy framework](https://github.com/kivy/kivy), and is managed by the same team. However, it can be used to build other types of Python apps for Android. p4a is often used in conjunction with [Buildozer](https://github.com/kivy/buildozer), which can download, install and keep up-to-date any necessary prerequisites (including p4a itself), for a number of target platforms, using a specification file to define the build. ### Is it possible to have a kiosk app on Android? Thomas Hansen wrote a detailed answer on [the (old) kivy-users mailing list](https://groups.google.com/d/msg/kivy-users/QKoCekAR1c0/yV-85Y_iAwoJ) Basically, you need to root the device, remove the SystemUI package, add some lines to the xml configuration, and you're done. ### Common Errors The following are common problems and resolutions that users have reported. #### 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. `--requirements=python3,kivy`. This also applies when using buildozer, in which case add python3 to your buildozer.spec requirements. #### linkname too long This can happen when you try to include a very long filename, which doesn’t normally happen but can occur accidentally if the p4a directory contains a `.buildozer` directory that is not excluded from the build (e.g. if buildozer was previously used). Removing this directory should fix the problem, and is desirable anyway since you don’t want it in the APK. #### Requested API target XX is not available, install it with the SDK android tool This means that your SDK is missing the required platform tools. You need to install the `platforms;android-XX` package in your SDK, using the android or sdkmanager tools (depending on SDK version). If using buildozer this should be done automatically, but as a workaround you can run these from `~/.buildozer/android/platform/android-sdk-XX/tools/android` #### SSLError(“Can’t connect to HTTPS URL because the SSL module is not available.”) Your hostpython3 was compiled without SSL support. You need to install the SSL development files before rebuilding the hostpython3 recipe. Remember to always clean the build before rebuilding (`p4a clean builds`, or with buildozer `buildozer android clean`). On Ubuntu and derivatives: apt install libssl-dev p4a clean builds # or with: buildozer `buildozer android clean On macOS: brew install openssl p4a clean builds # or with: buildozer `buildozer android clean #### AttributeError: 'AnsiCodes' object has no attribute 'LIGHTBLUE_EX' This occurs if your version of `colorama` is too low, install version 0.3.3 or higher. If you install python-for-android with `pip` or via `setup.py`, this dependency should be taken care of automatically. #### 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=python3,kivy`. This also applies when using buildozer, in which case add python3 to your buildozer.spec requirements. #### linkname too long This can happen when you try to include a very long filename, which doesn't normally happen but can occur accidentally if the p4a directory contains a .buildozer directory that is not excluded from the build (e.g. if buildozer was previously used). Removing this directory should fix the problem, and is desirable anyway since you don't want it in the APK. #### Requested API target 19 is not available, install it with the SDK android tool This means that your SDK is missing the required platform tools. You need to install the ``platforms;android-19`` package in your SDK, using the ``android`` or ``sdkmanager`` tools (depending on SDK version). If using buildozer this should be done automatically, but as a workaround you can run these from ``~/.buildozer/android/platform/android-sdk-20/tools/android``. #### ModuleNotFoundError: No module named '_ctypes' You do not have the libffi headers available to python-for-android, so you need to install them. On Ubuntu and derivatives these come from the `libffi-dev` package. After installing the headers, clean the build (`p4a clean builds`, or with buildozer delete the `.buildozer` directory within your app directory) and run python-for-android again. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2010-2025 Kivy Team and other contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include LICENSE README.md include *.toml recursive-include doc * prune doc/build recursive-include pythonforandroid *.py *.tmpl biglink liblink recursive-include pythonforandroid/recipes *.py *.patch *.diff *.c *.pyx Setup *.h recursive-include pythonforandroid/bootstraps *.properties *.xml *.java *.tmpl *.txt *.png *.aidl *.py *.sh *.c *.h *.html *.patch prune .git ================================================ FILE: Makefile ================================================ VIRTUAL_ENV ?= venv PIP=$(VIRTUAL_ENV)/bin/pip TOX=`which tox` ACTIVATE=$(VIRTUAL_ENV)/bin/activate PYTHON=$(VIRTUAL_ENV)/bin/python DOCKER_IMAGE=kivy/python-for-android DOCKER_TAG=latest ANDROID_SDK_HOME ?= $(HOME)/.android/android-sdk ANDROID_NDK_HOME ?= $(HOME)/.android/android-ndk ANDROID_NDK_HOME_LEGACY ?= $(HOME)/.android/android-ndk-legacy REBUILD_UPDATED_RECIPES_EXTRA_ARGS ?= '' all: virtualenv $(VIRTUAL_ENV): python3 -m venv $(VIRTUAL_ENV) $(PIP) install Cython==0.29.36 $(PIP) install -e . virtualenv: $(VIRTUAL_ENV) # ignores test_pythonpackage.py since it runs for too long test: $(TOX) -- tests/ --ignore tests/test_pythonpackage.py # Java linting using Spotless (requires Java 17+, uses Gradle wrapper) java-lint: cd pythonforandroid/bootstraps && ./common/build/gradlew spotlessCheck java-lint-fix: cd pythonforandroid/bootstraps && ./common/build/gradlew spotlessApply # Java linting via Docker (no local Java required) docker/java-lint: docker/build docker run --rm -v $(CURDIR):/home/user/app -w /home/user/app/pythonforandroid/bootstraps $(DOCKER_IMAGE) ./common/build/gradlew spotlessCheck docker/java-lint-fix: docker/build docker run --rm -v $(CURDIR):/home/user/app -w /home/user/app/pythonforandroid/bootstraps $(DOCKER_IMAGE) ./common/build/gradlew spotlessApply # Also install and configure rust rebuild_updated_recipes: virtualenv . $(ACTIVATE) && \ curl https://sh.rustup.rs -sSf | sh -s -- -y && \ . "$(HOME)/.cargo/env" && \ rustup target list && \ ANDROID_SDK_HOME=$(ANDROID_SDK_HOME) ANDROID_NDK_HOME=$(ANDROID_NDK_HOME) \ $(PYTHON) ci/rebuild_updated_recipes.py $(REBUILD_UPDATED_RECIPES_EXTRA_ARGS) # make ARCH=armeabi-v7a,arm64-v8a ARTIFACT=apk BOOTSTRAP=sdl2 MODE=debug REQUIREMENTS=python testapps-generic testapps-generic: virtualenv @if [ -z "$(ARCH)" ]; then echo "ARCH is not set"; exit 1; fi @if [ -z "$(ARTIFACT)" ]; then echo "ARTIFACT is not set"; exit 1; fi @if [ -z "$(BOOTSTRAP)" ]; then echo "BOOTSTRAP is not set"; exit 1; fi @if [ -z "$(MODE)" ]; then echo "MODE is not set"; exit 1; fi @if [ -z "$(REQUIREMENTS)" ]; then echo "REQUIREMENTS is not set"; exit 1; fi @ARCH_FLAGS=$$(echo "$(ARCH)" | tr ',' ' ' | sed 's/\([^ ]\+\)/--arch=\1/g'); \ . $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \ python setup.py $(ARTIFACT) \ --sdk-dir $(ANDROID_SDK_HOME) \ --ndk-dir $(ANDROID_NDK_HOME) \ $$ARCH_FLAGS --bootstrap $(BOOTSTRAP) --$(MODE) --requirements $(REQUIREMENTS) testapps-with-numpy: testapps-with-numpy/debug/apk testapps-with-numpy/release/aab # testapps-with-numpy/MODE/ARTIFACT testapps-with-numpy/%: virtualenv $(eval MODE := $(word 2, $(subst /, ,$@))) $(eval ARTIFACT := $(word 3, $(subst /, ,$@))) @echo Building testapps-with-numpy for $(MODE) mode and $(ARTIFACT) artifact . $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \ python setup.py $(ARTIFACT) --$(MODE) --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ --requirements libffi,sdl2,pyjnius,kivy,python3,openssl,requests,urllib3,chardet,idna,sqlite3,setuptools,numpy \ --arch=armeabi-v7a --arch=arm64-v8a --arch=x86_64 --arch=x86 \ --permission "(name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=18)" --permission "(name=android.permission.INTERNET)" testapps-with-scipy: testapps-with-scipy/debug/apk testapps-with-scipy/release/aab # testapps-with-scipy/MODE/ARTIFACT testapps-with-scipy/%: virtualenv $(eval MODE := $(word 2, $(subst /, ,$@))) $(eval ARTIFACT := $(word 3, $(subst /, ,$@))) @echo Building testapps-with-scipy for $(MODE) mode and $(ARTIFACT) artifact . $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \ export LEGACY_NDK=$(ANDROID_NDK_HOME_LEGACY) && \ python setup.py $(ARTIFACT) --$(MODE) --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ --requirements python3,scipy,kivy \ --arch=armeabi-v7a --arch=arm64-v8a testapps-webview: testapps-webview/debug/apk testapps-webview/release/aab # testapps-webview/MODE/ARTIFACT testapps-webview/%: virtualenv $(eval MODE := $(word 2, $(subst /, ,$@))) $(eval ARTIFACT := $(word 3, $(subst /, ,$@))) @echo Building testapps-webview for $(MODE) mode and $(ARTIFACT) artifact . $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \ python setup.py $(ARTIFACT) --$(MODE) --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ --bootstrap webview \ --requirements sqlite3,libffi,openssl,pyjnius,flask,python3,genericndkbuild \ --arch=armeabi-v7a --arch=arm64-v8a --arch=x86_64 --arch=x86 testapps-service_library-aar: virtualenv . $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \ python setup.py aar --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ --bootstrap service_library \ --requirements python3 \ --arch=arm64-v8a --arch=x86 --release testapps-qt: testapps-qt/debug/apk testapps-qt/release/aab # testapps-webview/MODE/ARTIFACT testapps-qt/%: virtualenv $(eval MODE := $(word 2, $(subst /, ,$@))) $(eval ARTIFACT := $(word 3, $(subst /, ,$@))) @echo Building testapps-qt for $(MODE) mode and $(ARTIFACT) artifact . $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \ python setup.py $(ARTIFACT) --$(MODE) --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \ --bootstrap qt \ --requirements python3,shiboken6,pyside6 \ --arch=arm64-v8a \ --local-recipes ./test_qt/recipes \ --qt-libs Core \ --load-local-libs plugins_platforms_qtforandroid \ --add-jar ./test_qt/jar/PySide6/jar/Qt6Android.jar \ --add-jar ./test_qt/jar/PySide6/jar/Qt6AndroidBindings.jar \ --permission android.permission.WRITE_EXTERNAL_STORAGE \ --permission android.permission.INTERNET 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) clean: find . -type d -name "__pycache__" -exec rm -r {} + find . -type d -name "*.egg-info" -exec rm -r {} + clean/all: clean rm -rf $(VIRTUAL_ENV) .tox/ docker/pull: docker pull $(DOCKER_IMAGE):latest || true docker/build: docker build --cache-from=$(DOCKER_IMAGE) --tag=$(DOCKER_IMAGE) . docker/login: @echo $$DOCKERHUB_TOKEN | docker login --username $(DOCKERHUB_USERNAME) --password-stdin docker/tag: docker tag $(DOCKER_IMAGE):latest $(DOCKER_IMAGE):$(DOCKER_TAG) docker/push: docker push $(DOCKER_IMAGE):$(DOCKER_TAG) docker/run/test: docker/build docker run --rm --env-file=.env $(DOCKER_IMAGE) 'make test' docker/run/command: docker/build docker run --rm --env-file=.env $(DOCKER_IMAGE) /bin/sh -c "$(COMMAND)" docker/run/make/rebuild_updated_recipes: docker/build docker run --name p4a-latest -e REBUILD_UPDATED_RECIPES_EXTRA_ARGS --env-file=.env $(DOCKER_IMAGE) make rebuild_updated_recipes docker/run/make/%: docker/build docker run --rm --env-file=.env $(DOCKER_IMAGE) make $* docker/run/shell: docker/build docker run --rm --env-file=.env -it $(DOCKER_IMAGE) ================================================ FILE: README.md ================================================ # python-for-android python-for-android (p4a) is a development tool that packages Python apps into binaries that can run on Android devices. It can generate: * [Android Package](https://en.wikipedia.org/wiki/Apk_(file_format)) (APK) files, ready to install locally on a device, especially for testing. This format is used by many [app stores](https://en.wikipedia.org/wiki/List_of_Android_app_stores) but not [Google Play Store](https://play.google.com/store/). * [Android App Bundle](https://developer.android.com/guide/app-bundle/faq) (AAB) files which can be shared on [Google Play Store](https://play.google.com/store/). * [Android Archive](https://developer.android.com/studio/projects/android-library) (AAR) files which can be used as a reusable bundle of resources for other projects. It supports multiple CPU architectures. It supports apps developed with [Kivy framework](http://kivy.org), but was built to be flexible about the backend libraries (through "bootstraps"), and also supports [PySDL2](https://pypi.org/project/PySDL2/), and a [WebView](https://developer.android.com/reference/android/webkit/WebView) with a Python web server. It automatically supports dependencies on most pure Python packages. For other packages, including those that depend on C code, a special "recipe" must be written to support cross-compiling. python-for-android comes with recipes for many of the most popular libraries (e.g. numpy and sqlalchemy) built in. python-for-android works by cross-compiling the Python interpreter and its dependencies for Android devices, and bundling it with the app's python code and dependencies. The Python code is then interpreted on the Android device. It is recommended that python-for-android be used via [Buildozer](https://buildozer.readthedocs.io/), which ensures the correct dependencies are pre-installed, and centralizes the configuration. However, python-for-android is not limited to being used with Buildozer. [![Backers on Open Collective](https://opencollective.com/kivy/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/kivy/sponsors/badge.svg)](#sponsors) [![GitHub contributors](https://img.shields.io/github/contributors-anon/kivy/python-for-android)](https://github.com/kivy/python-for-android/graphs/contributors) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) ![PyPI - Version](https://img.shields.io/pypi/v/python-for-android) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/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) [![Docker](https://github.com/kivy/python-for-android/actions/workflows/docker.yml/badge.svg)](https://github.com/kivy/python-for-android/actions/workflows/docker.yml) ## Documentation More information is available in the [online documentation](https://python-for-android.readthedocs.io) including a [quickstart guide](https://python-for-android.readthedocs.io/en/latest/quickstart.html). python-for-android is managed by the [Kivy team](https://kivy.org). ## Support Are you having trouble using python-for-android or any of its related projects in the Kivy ecosystem? Is there an error you don’t understand? Are you trying to figure out how to use it? We have volunteers who can help! The best channels to contact us for support are listed in the latest [Contact Us](https://github.com/kivy/pyton-for-android/blob/master/CONTACT.md) document. ## Code of Conduct In the interest of fostering an open and welcoming community, we as contributors and maintainers need to ensure participation in our project and our sister projects is a harassment-free and positive experience for everyone. It is vital that all interaction is conducted in a manner conveying respect, open-mindedness and gratitude. Please consult the [latest Code of Conduct](https://github.com/kivy/python-for-android/blob/master/CODE_OF_CONDUCT.md). ## Contributors This project exists thanks to [all the people who contribute](https://github.com/kivy/python-for-android/graphs/contributors). [[Become a contributor](CONTRIBUTING.md)]. ## Backers Thank you to [all of our backers](https://opencollective.com/kivy)! 🙏 [[Become a backer](https://opencollective.com/kivy#backer)] ## Sponsors Special thanks to [all of our sponsors, past and present](https://opencollective.com/kivy). Support this project by [[becoming a sponsor](https://opencollective.com/kivy#sponsor)]. Here are our top current sponsors. Please click through to see their websites, and support them as they support us. ================================================ FILE: ci/__init__.py ================================================ ================================================ FILE: ci/constants.py ================================================ from enum import Enum class TargetPython(Enum): 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_PYTHON3 = set([ 'brokenrecipe', # enum34 is not compatible with Python 3.6 standard library # https://stackoverflow.com/a/45716067/185510 'enum34', # build_dir = glob.glob('build/lib.*')[0] # IndexError: list index out of range 'secp256k1', # 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', # SyntaxError: invalid syntax (Python2) 'storm', # mpmath package with a version >= 0.19 required 'sympy', 'vlc', # GitHub CI runs out of storage while building it 'scipy', 'fortran', # Outdated and there's a chance that is now useless. 'zope_interface', # Requires zope_interface, which is broken. 'twisted', # genericndkbuild is incompatible with sdl2 (which is build by default when targeting sdl2 bootstrap) 'genericndkbuild', # boost gives errors (requires numpy? syntax error in .jam?) 'boost', # libtorrent gives errors (requires boost. Also, see issue #2809, to start with) 'libtorrent', # pybind11 build fails on macos 'pybind11', # pygame (likely need to be updated) is broken with newer SDL2 versions 'pygame', ]) BROKEN_RECIPES = { TargetPython.python3: BROKEN_RECIPES_PYTHON3, } # recipes that were already built will be skipped CORE_RECIPES = set([ 'pyjnius', 'kivy', 'openssl', 'requests', 'sqlite3', 'setuptools', 'numpy', 'android', 'hostpython3', 'python3', ]) ================================================ FILE: ci/makefiles/android.mk ================================================ # Downloads and installs the Android SDK depending on supplied platform: darwin or linux # Those android NDK/SDK variables can be override when running the file ANDROID_NDK_VERSION ?= 28c ANDROID_NDK_VERSION_LEGACY ?= 21e ANDROID_SDK_TOOLS_VERSION ?= 6514223 ANDROID_SDK_BUILD_TOOLS_VERSION ?= 29.0.3 ANDROID_HOME ?= $(HOME)/.android ANDROID_API_LEVEL ?= 36 # per OS dictionary-like UNAME_S := $(shell uname -s) TARGET_OS_Linux = linux TARGET_OS_ALIAS_Linux = $(TARGET_OS_Linux) TARGET_OS_Darwin = darwin TARGET_OS_ALIAS_Darwin = mac TARGET_OS = $(TARGET_OS_$(UNAME_S)) TARGET_OS_ALIAS = $(TARGET_OS_ALIAS_$(UNAME_S)) ANDROID_SDK_HOME=$(ANDROID_HOME)/android-sdk ANDROID_SDK_TOOLS_ARCHIVE=commandlinetools-$(TARGET_OS_ALIAS)-$(ANDROID_SDK_TOOLS_VERSION)_latest.zip ANDROID_SDK_TOOLS_DL_URL=https://dl.google.com/android/repository/$(ANDROID_SDK_TOOLS_ARCHIVE) ANDROID_NDK_HOME=$(ANDROID_HOME)/android-ndk ANDROID_NDK_FOLDER=$(ANDROID_HOME)/android-ndk-r$(ANDROID_NDK_VERSION) ANDROID_NDK_ARCHIVE=android-ndk-r$(ANDROID_NDK_VERSION)-$(TARGET_OS).zip ANDROID_NDK_HOME_LEGACY=$(ANDROID_HOME)/android-ndk-legacy ANDROID_NDK_FOLDER_LEGACY=$(ANDROID_HOME)/android-ndk-r$(ANDROID_NDK_VERSION_LEGACY) ANDROID_NDK_ARCHIVE_LEGACY=android-ndk-r$(ANDROID_NDK_VERSION_LEGACY)-$(TARGET_OS)-x86_64.zip ANDROID_NDK_GFORTRAN_ARCHIVE_ARM64=gcc-arm64-linux-x86_64.tar.bz2 ANDROID_NDK_GFORTRAN_ARCHIVE_ARM=gcc-arm-linux-x86_64.tar.bz2 ANDROID_NDK_DL_URL=https://dl.google.com/android/repository/$(ANDROID_NDK_ARCHIVE) ANDROID_NDK_DL_URL_LEGACY=https://dl.google.com/android/repository/$(ANDROID_NDK_ARCHIVE_LEGACY) $(info Target install OS is : $(target_os)) $(info Android SDK home is : $(ANDROID_SDK_HOME)) $(info Android NDK home is : $(ANDROID_NDK_HOME)) $(info Android NDK Legacy home is : $(ANDROID_NDK_HOME_LEGACY)) $(info Android SDK download url is : $(ANDROID_SDK_TOOLS_DL_URL)) $(info Android NDK download url is : $(ANDROID_NDK_DL_URL)) $(info Android API level is : $(ANDROID_API_LEVEL)) $(info Android NDK version is : $(ANDROID_NDK_VERSION)) $(info Android NDK Legacy version is : $(ANDROID_NDK_VERSION_LEGACY)) $(info JAVA_HOME is : $(JAVA_HOME)) all: install_sdk install_ndk install_sdk: download_android_sdk extract_android_sdk update_android_sdk install_ndk: download_android_ndk download_android_ndk_legacy download_android_ndk_gfortran extract_android_ndk extract_android_ndk_legacy extract_android_ndk_gfortran download_android_sdk: curl --location --progress-bar --continue-at - \ $(ANDROID_SDK_TOOLS_DL_URL) --output $(ANDROID_SDK_TOOLS_ARCHIVE) download_android_ndk: curl --location --progress-bar --continue-at - \ $(ANDROID_NDK_DL_URL) --output $(ANDROID_NDK_ARCHIVE) download_android_ndk_legacy: curl --location --progress-bar --continue-at - \ $(ANDROID_NDK_DL_URL_LEGACY) --output $(ANDROID_NDK_ARCHIVE_LEGACY) download_android_ndk_gfortran: curl --location --progress-bar --continue-at - \ https://github.com/mzakharo/android-gfortran/releases/download/r$(ANDROID_NDK_VERSION_LEGACY)/$(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM64) --output $(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM64) curl --location --progress-bar --continue-at - \ https://github.com/mzakharo/android-gfortran/releases/download/r$(ANDROID_NDK_VERSION_LEGACY)/$(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM) --output $(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM) # Extract android SDK and remove the compressed file extract_android_sdk: mkdir -p $(ANDROID_SDK_HOME) \ && unzip -q $(ANDROID_SDK_TOOLS_ARCHIVE) -d $(ANDROID_SDK_HOME) \ && rm -f $(ANDROID_SDK_TOOLS_ARCHIVE) # Extract android NDK and remove the compressed file extract_android_ndk: mkdir -p $(ANDROID_NDK_FOLDER) \ && unzip -q $(ANDROID_NDK_ARCHIVE) -d $(ANDROID_HOME) \ && mv $(ANDROID_NDK_FOLDER) $(ANDROID_NDK_HOME) \ && rm -f $(ANDROID_NDK_ARCHIVE) extract_android_ndk_legacy: mkdir -p $(ANDROID_NDK_FOLDER_LEGACY) \ && unzip -q $(ANDROID_NDK_ARCHIVE_LEGACY) -d $(ANDROID_HOME) \ && mv $(ANDROID_NDK_FOLDER_LEGACY) $(ANDROID_NDK_HOME_LEGACY) \ && rm -f $(ANDROID_NDK_ARCHIVE_LEGACY) extract_android_ndk_gfortran: rm -rf $(ANDROID_NDK_HOME_LEGACY)/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/ \ && mkdir $(ANDROID_NDK_HOME_LEGACY)/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/ \ && tar -xf $(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM64) -C $(ANDROID_NDK_HOME_LEGACY)/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/ --strip-components 1 \ && rm -f $(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM64) \ && rm -rf $(ANDROID_NDK_HOME_LEGACY)/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/ \ && mkdir $(ANDROID_NDK_HOME_LEGACY)/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/ \ && tar -xf $(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM) -C $(ANDROID_NDK_HOME_LEGACY)/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/ --strip-components 1 \ && rm -f $(ANDROID_NDK_GFORTRAN_ARCHIVE_ARM) # updates Android SDK, install Android API, Build Tools and accept licenses update_android_sdk: touch $(ANDROID_HOME)/repositories.cfg yes | $(ANDROID_SDK_HOME)/tools/bin/sdkmanager --sdk_root=$(ANDROID_SDK_HOME) --licenses > /dev/null $(ANDROID_SDK_HOME)/tools/bin/sdkmanager --sdk_root=$(ANDROID_SDK_HOME) "build-tools;$(ANDROID_SDK_BUILD_TOOLS_VERSION)" > /dev/null $(ANDROID_SDK_HOME)/tools/bin/sdkmanager --sdk_root=$(ANDROID_SDK_HOME) "platforms;android-$(ANDROID_API_LEVEL)" > /dev/null # Set avdmanager permissions (executable) chmod +x $(ANDROID_SDK_HOME)/tools/bin/avdmanager ================================================ FILE: ci/makefiles/osx.mk ================================================ # installs Android's SDK/NDK, cython # The following variable/s can be override when running the file ANDROID_HOME ?= $(HOME)/.android all: upgrade_cython install_android_ndk_sdk upgrade_cython: pip3 install --upgrade Cython install_android_ndk_sdk: mkdir -p $(ANDROID_HOME) make -f ci/makefiles/android.mk ================================================ FILE: ci/rebuild_updated_recipes.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- """ Continuous Integration helper script. Automatically detects recipes modified in a changeset (compares with master) and recompiles them. To run locally, set the environment variables before running: ``` ANDROID_SDK_HOME=~/.buildozer/android/platform/android-sdk-20 ANDROID_NDK_HOME=~/.buildozer/android/platform/android-ndk-r9c ./ci/rebuild_update_recipes.py ``` Current limitations: - will fail on conflicting requirements e.g. https://travis-ci.org/AndreMiras/python-for-android/builds/438840800 the list of recipes was huge and result was: [ERROR]: Didn't find any valid dependency graphs. [ERROR]: This means that some of your requirements pull in conflicting dependencies. """ import sh import os import sys import argparse from pythonforandroid.build import Context from pythonforandroid import logger from pythonforandroid.toolchain import current_directory from pythonforandroid.recipe import Recipe from ci.constants import TargetPython, CORE_RECIPES, BROKEN_RECIPES def modified_recipes(branch='origin/develop'): """ Returns a set of modified recipes between the current branch and the one in param. """ # using the contrib version on purpose rather than sh.git, since it comes # with a bunch of fixes, e.g. disabled TTY, see: # https://stackoverflow.com/a/20128598/185510 git_diff = sh.contrib.git.diff('--name-only', branch).split("\n") recipes = set() for file_path in git_diff: if 'pythonforandroid/recipes/' in file_path: recipe = file_path.split('/')[2] recipes.add(recipe) return recipes def build(target_python, requirements, archs): """ Builds an APK given a target Python and a set of requirements. """ if not requirements: return android_sdk_home = os.environ['ANDROID_SDK_HOME'] android_ndk_home = os.environ['ANDROID_NDK_HOME'] requirements.add(target_python.name) requirements_str = ','.join(requirements) logger.info('requirements: {}'.format(requirements_str)) # Detect bootstrap based on requirements # SDL3 recipes conflict with SDL2, so we need the sdl3 bootstrap # when any SDL3-related recipe (sdl3, sdl3_image, sdl3_mixer, sdl3_ttf) is present bootstrap = None if any(r.startswith('sdl3') for r in requirements): bootstrap = 'sdl3' logger.info('Detected sdl3 recipe in requirements, using sdl3 bootstrap') build_command = [ 'setup.py', 'apk', '--sdk-dir', android_sdk_home, '--ndk-dir', android_ndk_home, '--requirements', requirements_str ] if bootstrap: build_command.extend(['--bootstrap', bootstrap]) build_command.extend([f"--arch={arch}" for arch in archs]) build_command_str = " ".join(build_command) logger.info(f"Build command: {build_command_str}") with current_directory('testapps/on_device_unit_tests/'): # iterates to stream the output for line in sh.python(*build_command, _err_to_out=True, _iter=True): print(line) def main(): parser = argparse.ArgumentParser("rebuild_updated_recipes") parser.add_argument( "--arch", help="The archs to build for during tests", action="append", default=[], ) args, unknown = parser.parse_known_args(sys.argv[1:]) logger.info(f"Building updated recipes for the following archs: {args.arch}") target_python = TargetPython.python3 recipes = modified_recipes() logger.info('recipes modified: {}'.format(recipes)) recipes -= CORE_RECIPES logger.info('recipes to build: {}'.format(recipes)) context = Context() # removing the deleted recipes for the given target (if any) for recipe_name in recipes.copy(): try: Recipe.get_recipe(recipe_name, context) except ValueError: # recipe doesn't exist, so probably we remove it recipes.remove(recipe_name) logger.warning( 'removed {} from recipes because deleted'.format(recipe_name) ) # removing the known broken recipe for the given target broken_recipes = BROKEN_RECIPES[target_python] recipes -= broken_recipes logger.info('recipes to build (no broken): {}'.format(recipes)) build(target_python, recipes, args.arch) if __name__ == '__main__': main() ================================================ FILE: ci/run_emulator_tests.sh ================================================ #!/bin/bash set -euxo pipefail # Find the built APK file APK_FILE=$(find dist -name "*.apk" -print -quit) if [ -z "$APK_FILE" ]; then echo "Error: No APK file found in dist/" exit 1 fi echo "Installing $APK_FILE..." adb install "$APK_FILE" # Extract package and activity names AAPT2_PATH=$(find ${ANDROID_HOME}/build-tools/ -name aapt2 | sort -r | head -n 1) APP_PACKAGE=$(${AAPT2_PATH} dump badging "${APK_FILE}" | awk -F"'" '/package: name=/{print $2}') APP_ACTIVITY=$(${AAPT2_PATH} dump badging "${APK_FILE}" | awk -F"'" '/launchable-activity/ {print $2}') echo "Launching $APP_PACKAGE/$APP_ACTIVITY..." adb shell am start -n "$APP_PACKAGE/$APP_ACTIVITY" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER # Poll for test completion with timeout MAX_WAIT=300 POLL_INTERVAL=5 elapsed=0 echo "Waiting for tests to complete (max ${MAX_WAIT}s)..." while [ $elapsed -lt $MAX_WAIT ]; do # Dump current logs adb logcat -d -s python:I *:S > app_logs.txt # Check if all success patterns are present if grep --extended-regexp --quiet "I python[ ]+: Initialized python" app_logs.txt && \ grep --extended-regexp --quiet "I python[ ]+: Ran 14 tests in" app_logs.txt && \ grep --extended-regexp --quiet "I python[ ]+: OK" app_logs.txt; then echo "✅ SUCCESS: App launched and all unit tests passed in ${elapsed}s." exit 0 fi # Check for early failure indicators if grep --extended-regexp --quiet "I python[ ]+: FAILED" app_logs.txt; then echo "❌ FAILURE: Tests failed after ${elapsed}s." echo "--- Full Logs ---" cat app_logs.txt echo "-----------------" exit 1 fi sleep $POLL_INTERVAL elapsed=$((elapsed + POLL_INTERVAL)) echo "Still waiting... (${elapsed}s elapsed)" done echo "❌ TIMEOUT: Tests did not complete within ${MAX_WAIT}s." echo "--- Full Logs ---" cat app_logs.txt echo "-----------------" exit 1 ================================================ FILE: distribute.sh ================================================ #!/usr/bin/env sh # This file is just a shim to report an error messaage if some tool # tries to run the old python-for-android. # An alternative would be to implement argument handling and pass # things to the new toolchain so that it works the same as before, but # that would be harder. cat </dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-for-android.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-for-android.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/python-for-android" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-for-android" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ================================================ FILE: doc/make.bat ================================================ @ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build2 ) set BUILDDIR=build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source set I18NSPHINXOPTS=%SPHINXOPTS% source if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build2 is available and fallback to Python version if any %SPHINXBUILD% 2> nul if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build2' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build2' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-for-android.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-for-android.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end ================================================ FILE: doc/requirements.txt ================================================ Sphinx~=7.2.6 furo==2023.9.10 ================================================ FILE: doc/source/_static/.empty ================================================ ================================================ FILE: doc/source/apis.rst ================================================ Working on Android ================== This page gives details on accessing Android APIs and managing other interactions on Android. Storage paths ------------- If you want to store and retrieve data, you shouldn't just save to the current directory, and not hardcode `/sdcard/` or some other path either - it might differ per device. Instead, the `android` module which you can add to your `--requirements` allows you to query the most commonly required paths:: from android.storage import app_storage_path settings_path = app_storage_path() from android.storage import primary_external_storage_path primary_ext_storage = primary_external_storage_path() from android.storage import secondary_external_storage_path secondary_ext_storage = secondary_external_storage_path() `app_storage_path()` gives you Android's so-called "internal storage" which is specific to your app and cannot seen by others or the user. It compares best to the AppData directory on Windows. `primary_external_storage_path()` returns Android's so-called "primary external storage", often found at `/sdcard/` and potentially accessible to any other app. It compares best to the Documents directory on Windows. Requires `Permission.WRITE_EXTERNAL_STORAGE` to read and write to. `secondary_external_storage_path()` returns Android's so-called "secondary external storage", often found at `/storage/External_SD/`. It compares best to an external disk plugged to a Desktop PC, and can after a device restart become inaccessible if removed. Requires `Permission.WRITE_EXTERNAL_STORAGE` to read and write to. .. warning:: Even if `secondary_external_storage_path` returns a path the external sd card may still not be present. Only non-empty contents or a successful write indicate that it is. Read more on all the different storage types and what to use them for in the Android documentation: https://developer.android.com/training/data-storage A note on permissions ~~~~~~~~~~~~~~~~~~~~~ Only the internal storage is always accessible with no additional permissions. For both primary and secondary external storage, you need to obtain `Permission.WRITE_EXTERNAL_STORAGE` **and the user may deny it.** Also, if you get it, both forms of external storage may only allow your app to write to the common pre-existing folders like "Music", "Documents", and so on. (see the Android Docs linked above for details) Runtime permissions ------------------- With API level >= 21, you will need to request runtime permissions to access the SD card, the camera, and other things. This can be done through the `android` module which is *available per default* unless you blacklist it. Use it in your app like this:: from android.permissions import request_permissions, Permission request_permissions([Permission.WRITE_EXTERNAL_STORAGE]) The available permissions are listed here: https://developer.android.com/reference/android/Manifest.permission Other common tasks ------------------ Running executables ~~~~~~~~~~~~~~~~~~~ Android restricts executing files from application data directories. ``python-for-android`` works around this by creating symbolic links to a small set of supported executables inside an internal executable directory that is added to ``PATH``. During startup, ``start.c`` (compiled into ``libmain.so``) scans loaded native libraries and creates symlinks for any library matching:: libbin.so Each matching library is exposed at runtime as:: For example:: libffmpegbin.so -> ffmpeg Recipe developers may expose additional executables by renaming them using the same naming convention. The following example performs a minimal FFmpeg sanity check using an in-memory test source and discards the output:: import subprocess subprocess.run( ["ffmpeg", "-f", "lavfi", "-i", "testsrc", "-t", "1", "-f", "null", "-"], check=True ) This verifies that ``ffmpeg`` is available and executable on the device. Ensure ``ffmpeg`` is included in your ``requirements``. If video encoding is required, the following codec options must also be enabled in the build configuration: - ``av_codecs`` - ``libx264`` Without these, FFmpeg may be present but lack the required codec support. See also: `APK native library execution restrictions `_ Dismissing the splash screen ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ With the SDL2 bootstrap, the app's splash screen may be visible longer than necessary (with your app already being loaded) due to a limitation with the way we check if the app has properly started. In this case, the splash screen overlaps the app gui for a short time. To dismiss the loading screen explicitly in your code, use the `android` module:: from android import loadingscreen loadingscreen.hide_loading_screen() You can call it e.g. using ``kivy.clock.Clock.schedule_once`` to run it in the first active frame of your app, or use the app build method. Handling the back button ~~~~~~~~~~~~~~~~~~~~~~~~ Android phones always have a back button, which users expect to perform an appropriate in-app function. If you do not handle it, Kivy apps will actually shut down and appear to have crashed. In SDL2 bootstraps, the back button appears as the escape key (keycode 27, codepoint 270). You can handle this key to perform actions when it is pressed. For instance, in your App class in Kivy:: from kivy.core.window import Window class YourApp(App): def build(self): Window.bind(on_keyboard=self.key_input) return Widget() # your root widget here as normal def key_input(self, window, key, scancode, codepoint, modifier): if key == 27: return True # override the default behaviour else: # the key now does nothing return False Pausing the App ~~~~~~~~~~~~~~~ When the user leaves an App, it is automatically paused by Android, although it gets a few seconds to store data etc. if necessary. Once paused, there is no guarantee that your app will run again. With Kivy, add an ``on_pause`` method to your App class, which returns True:: def on_pause(self): return True With the webview bootstrap, pausing should work automatically. Under SDL2, you can handle the `appropriate events `__ (see SDL_APP_WILLENTERBACKGROUND etc.). Observing Activity result ~~~~~~~~~~~~~~~~~~~~~~~~~ .. module:: android.activity The default PythonActivity has a observer pattern for `onActivityResult `_ and `onNewIntent `_. .. function:: bind(eventname=callback, ...) This allows you to bind a callback to an Android event: - ``on_new_intent`` is the event associated to the onNewIntent java call - ``on_activity_result`` is the event associated to the onActivityResult java call .. warning:: This method is not thread-safe. Call it in the mainthread of your app. (tips: use kivy.clock.mainthread decorator) .. function:: unbind(eventname=callback, ...) Unregister a previously registered callback with :func:`bind`. Example:: # This example is a snippet from an NFC p2p app implemented with Kivy. from android import activity def on_new_intent(self, intent): if intent.getAction() != NfcAdapter.ACTION_NDEF_DISCOVERED: return rawmsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES) if not rawmsgs: return for message in rawmsgs: message = cast(NdefMessage, message) payload = message.getRecords()[0].getPayload() print('payload: {}'.format(''.join(map(chr, payload)))) def nfc_enable(self): activity.bind(on_new_intent=self.on_new_intent) # ... def nfc_disable(self): activity.unbind(on_new_intent=self.on_new_intent) # ... Activity lifecycle handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The Android ``Application`` class provides the `ActivityLifecycleCallbacks `_ interface where callbacks can be registered corresponding to `activity lifecycle `_ changes. These callbacks can be used to implement logic in the Python app when the activity changes lifecycle states. Note that some of the callbacks are not useful in the Python app. For example, an `onActivityCreated` callback will never be run since the the activity's `onCreate` callback will complete before the Python app is running. Similarly, saving instance state in an `onActivitySaveInstanceState` callback will not be helpful since the Python app doesn't have access to the restored instance state. .. function:: register_activity_lifecycle_callbacks(callbackname=callback, ...) This allows you to bind a callbacks to Activity lifecycle state changes. The callback names correspond to ``ActivityLifecycleCallbacks`` method names such as ``onActivityStarted``. See the `ActivityLifecycleCallbacks `_ documentation for names and function signatures for the callbacks. .. function:: unregister_activity_lifecycle_callbacks(instance) Unregister a ``ActivityLifecycleCallbacks`` instance previously registered with :func:`register_activity_lifecycle_callbacks`. Example:: from android.activity import register_activity_lifecycle_callbacks def on_activity_stopped(activity): print('Activity is stopping') register_activity_lifecycle_callbacks( onActivityStopped=on_activity_stopped, ) Receiving Broadcast message ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. module:: android.broadcast Implementation of the android `BroadcastReceiver `_. You can specify the callback that will receive the broadcast event, and actions or categories filters. .. class:: BroadcastReceiver .. warning:: The callback will be called in another thread than the main thread. In that thread, be careful not to access OpenGL or something like that. .. method:: __init__(callback, actions=None, categories=None) :param callback: function or method that will receive the event. Will receive the context and intent as argument. :param actions: list of strings that represent an action. :param categories: list of strings that represent a category. For actions and categories, the string must be in lower case, without the prefix:: # In java: Intent.ACTION_HEADSET_PLUG # In python: 'headset_plug' .. method:: start() Register the receiver with all the actions and categories, and start handling events. .. method:: stop() Unregister the receiver with all the actions and categories, and stop handling events. Example:: class TestApp(App): def build(self): self.br = BroadcastReceiver( self.on_broadcast, actions=['headset_plug']) self.br.start() # ... def on_broadcast(self, context, intent): extras = intent.getExtras() headset_state = bool(extras.get('state')) if headset_state: print('The headset is plugged') else: print('The headset is unplugged') # Don't forget to stop and restart the receiver when the app is going # to pause / resume mode def on_pause(self): self.br.stop() return True def on_resume(self): self.br.start() Runnable ~~~~~~~~ .. module:: android.runnable :class:`Runnable` is a wrapper around the Java `Runnable `_ class. This class can be used to schedule a call of a Python function into the `PythonActivity` thread. Example:: from android.runnable import Runnable def helloworld(arg): print 'Called from PythonActivity with arg:', arg Runnable(helloworld)('hello') Or use our decorator:: from android.runnable import run_on_ui_thread @run_on_ui_thread def helloworld(arg): print 'Called from PythonActivity with arg:', arg helloworld('arg1') This can be used to prevent errors like: - W/System.err( 9514): java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() - NullPointerException in ActivityThread.currentActivityThread() .. warning:: Because the python function is called from the PythonActivity thread, you need to be careful about your own calls. Advanced Android API use ------------------------ .. _reference-label-for-android-module: `android` for Android API access ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As mentioned above, the ``android`` Python module provides a simple wrapper around many native Android APIS, and it is *included by default* unless you blacklist it. The available functionality of this module is not separately documented. You can read the source `on Github `__. Also please note you can replicate most functionality without it using `pyjnius`. (see below) `Plyer` - a more comprehensive API wrapper ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Plyer provides a more thorough wrapper than `android` for a much larger area of platform-specific APIs, supporting not only Android but also iOS and desktop operating systems. (Though plyer is a work in progress and not all platforms support all Plyer calls yet) Plyer does not support all APIs yet, but you can always use Pyjnius to call anything that is currently missing. You can include Plyer in your APKs by adding the `Plyer` recipe to your build requirements, e.g. :code:`--requirements=plyer`. You should check the `Plyer documentation `_ for details of all supported facades (platform APIs), but as an example the following is how you would achieve vibration as described in the Pyjnius section above:: from plyer.vibrator import vibrate vibrate(10) # in Plyer, the argument is in seconds This is obviously *much* less verbose than with Pyjnius! `Pyjnius` - raw lowlevel API access ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pyjnius lets you call the Android API directly from Python Pyjnius is works by dynamically wrapping Java classes, so you don't have to wait for any particular feature to be pre-supported. This is particularly useful when `android` and `plyer` don't already provide a convenient access to the API, or you need more control. You can include Pyjnius in your APKs by adding `pyjnius` to your build requirements, e.g. :code:`--requirements=flask,pyjnius`. It is automatically included in any APK containing Kivy, in which case you don't need to specify it manually. The basic mechanism of Pyjnius is the `autoclass` command, which wraps a Java class. For instance, here is the code to vibrate your device:: from jnius import autoclass # We need a reference to the Java activity running the current # application, this reference is stored automatically by # Kivy's PythonActivity bootstrap # This one works with SDL2 PythonActivity = autoclass('org.kivy.android.PythonActivity') activity = PythonActivity.mActivity Context = autoclass('android.content.Context') vibrator = activity.getSystemService(Context.VIBRATOR_SERVICE) vibrator.vibrate(10000) # the argument is in milliseconds Things to note here are: - The class that must be wrapped depends on the bootstrap. This is because Pyjnius is using the bootstrap's java source code to get a reference to the current activity, which the bootstraps store in the ``mActivity`` static variable. This difference isn't always important, but it's important to know about. - The code closely follows the Java API - this is exactly the same set of function calls that you'd use to achieve the same thing from Java code. - This is quite verbose - it's a lot of lines to achieve a simple vibration! These emphasise both the advantages and disadvantage of Pyjnius; you *can* achieve just about any API call with it (though the syntax is sometimes a little more involved, particularly if making Java classes from Python code), but it's not Pythonic and it's not short. These are problems that Plyer, explained below, attempts to address. You can check the `Pyjnius documentation `_ for further details. ================================================ FILE: doc/source/bootstraps.rst ================================================ Bootstraps ========== This page is about creating new bootstrap backends. For build options of existing bootstraps (i.e. with SDL2, Webview, etc.), see :ref:`build options `. python-for-android (p4a) supports multiple *bootstraps*. These fulfill a similar role to recipes, but instead of describing how to compile a specific module they describe how a full Android project may be put together from a combination of individual recipes and other components such as Android source code and various build files. This page describes the basics of how bootstraps work so that you can create and use your own if you like, making it easy to build new kinds of Python projects for Android. Creating a new bootstrap ------------------------ A bootstrap class consists of just a few basic components, though one of them must do a lot of work. For instance, the SDL2 bootstrap looks like the following:: from pythonforandroid.toolchain import Bootstrap, shprint, current_directory, info, warning, ArchAndroid, logger, info_main, which from os.path import join, exists from os import walk import glob import sh class SDL2Bootstrap(Bootstrap): name = 'sdl2' recipe_depends = ['sdl2'] def run_distribute(self): # much work is done here... The declaration of the bootstrap name and recipe dependencies should be clear. However, the :code:`run_distribute` method must do all the work of creating a build directory, copying recipes etc into it, and adding or removing any extra components as necessary. If you'd like to create a bootstrap, the best resource is to check the existing ones in the p4a source code. You can also :doc:`contact the developers ` if you have problems or questions. ================================================ FILE: doc/source/buildoptions.rst ================================================ Build options ============= This page contains instructions for using different build options. Python versions --------------- python-for-android supports using Python 3.8 or higher. To explicitly select a Python version in your requirements, use e.g. ``--requirements=python3==3.10.11,hostpython3==3.10.11``. The last python-for-android version supporting Python2 was `v2019.10.06 `__ 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: Bootstrap options ----------------- python-for-android supports multiple app backends with different types of interface. These are called *bootstraps*. Currently the following bootstraps are supported, but we hope that it should be easy to add others if your project has different requirements. `Let us know `__ if you'd like help adding a new one. sdl2 ~~~~ Use this with ``--bootstrap=sdl2``, or just include the ``sdl2`` recipe, e.g. ``--requirements=sdl2,python3``. SDL2 is a popular cross-platform development library, particularly for games. It has its own Android project support, which python-for-android uses as a bootstrap, and to which it adds the Python build and JNI code to start it. From the point of view of a Python program, SDL2 should behave as normal. For instance, you can build apps with Kivy or PySDL2 and have them work with this bootstrap. It should also be possible to use e.g. pygame_sdl2, but this would need a build recipe and doesn't yet have one. Build options %%%%%%%%%%%%% The sdl2 bootstrap supports the following additional command line options (this list may not be exhaustive): - ``--private``: The directory containing your project files. - ``--package``: The Java package name for your project. e.g. ``org.example.yourapp``. - ``--name``: The app name. - ``--version``: The version number. - ``--orientation``: The orientations that the app will display in. (Available options are ``portrait``, ``landscape``, ``portrait-reverse``, ``landscape-reverse``). Since Android ignores ``android:screenOrientation`` when in multi-window mode (Which is the default on Android 12+), this option will also set the window orientation hints for the SDL bootstrap. If multiple orientations are given, ``android:screenOrientation`` will be set to ``unspecified``. - ``--manifest-orientation``: The orientation that will be set for the ``android:screenOrientation`` attribute of the activity in the ``AndroidManifest.xml`` file. If not set, the value will be synthesized from the ``--orientation`` option. The full list of valid options is given under ``android:screenOrientation`` in the `Android documentation `__. - ``--icon``: A path to the png file to use as the application icon. - ``--permission``: A permission that needs to be declared into the App ``AndroidManifest.xml``. For multiple permissions, add multiple ``--permission`` arguments. ``--home-app`` Gives you the option to set your application as a home app (launcher) on your Android device. ``--display-cutout``: A display cutout is an area on some devices that extends into the display surface. It allows for an edge-to-edge experience while providing space for important sensors on the front of the device. (Available options are ``default``, ``shortEdges``, ``never`` and defaults to ``never``) `Android documentation `__. .. Note :: ``--permission`` accepts the following syntaxes: ``--permission (name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=18)`` or ``--permission android.permission.WRITE_EXTERNAL_STORAGE``. The first syntax is used to set additional properties to the permission (``android:maxSdkVersion`` and ``android:usesPermissionFlags`` are the only ones supported for now). The second one can be used when there's no need to add any additional properties. .. Warning :: The syntax ``--permission VIBRATE`` (only the permission name, without the prefix), is also supported for backward compatibility, but it will be removed in the future. - ``--meta-data``: Custom key=value pairs to add in the application metadata. - ``--presplash``: A path to the image file to use as a screen while the application is loading. - ``--presplash-color``: The presplash screen background color, of the form ``#RRGGBB`` or a color name ``red``, ``green``, ``blue`` etc. - ``--presplash-lottie``: use a lottie (json) file as a presplash animation. If used, this will replace the static presplash image. - ``--wakelock``: If the argument is included, the application will prevent the device from sleeping. - ``--window``: If the argument is included, the application will not cover the Android status bar. - ``--blacklist``: The path to a file containing blacklisted patterns that will be excluded from the final APK. Defaults to ``./blacklist.txt``. - ``--whitelist``: The path to a file containing whitelisted patterns that will be included in the APK even if also blacklisted. - ``--add-jar``: The path to a .jar file to include in the APK. To include multiple jar files, pass this argument multiple times. - ``--intent-filters``: A file path containing intent filter xml to be included in AndroidManifest.xml. - ``--service``: A service name and the Python script it should run. See :ref:`arbitrary_scripts_services`. - ``--add-source``: Add a source directory to the app's Java code. - ``--no-byte-compile-python``: Skip byte compile for .py files. - ``--enable-androidx``: Enable AndroidX support library. - ``--add-resource``: Put this file or directory in the apk res directory. webview ~~~~~~~ You can use this with ``--bootstrap=webview``, or include the ``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 webserver. For instance, your Python code can start a Flask application, and your app will display and allow the user to navigate this website. .. note:: Your Flask script must start the webserver *without* :code:``debug=True``. Debug mode doesn't seem to work on Android due to use of a subprocess. This bootstrap will automatically try to load a website on port 5000 (the default for Flask), or you can specify a different option with the `--port` command line option. If the webserver is not immediately present (e.g. during the short Python loading time when first started), it will instead display a loading screen until the server is ready. - ``--private``: The directory containing your project files. - ``--package``: The Java package name for your project. e.g. ``org.example.yourapp``. - ``--name``: The app name. - ``--version``: The version number. - ``--orientation``: The orientations that the app will display in. (Available options are ``portrait``, ``landscape``, ``portrait-reverse``, ``landscape-reverse``). Since Android ignores ``android:screenOrientation`` when in multi-window mode (Which is the default on Android 12+), this setting is not guaranteed to work, and you should consider to implement a custom orientation change handler in your app. - ``--manifest-orientation``: The orientation that will be set in the ``android:screenOrientation`` attribute of the activity in the ``AndroidManifest.xml`` file. If not set, the value will be synthesized from the ``--orientation`` option. The full list of valid options is given under ``android:screenOrientation`` in the `Android documentation `__. - ``--icon``: A path to the png file to use as the application icon. - ``--permission``: A permission name for the app, e.g. ``--permission VIBRATE``. For multiple permissions, add multiple ``--permission`` arguments. - ``--meta-data``: Custom key=value pairs to add in the application metadata. - ``--presplash``: A path to the image file to use as a screen while the application is loading. - ``--presplash-color``: The presplash screen background color, of the form ``#RRGGBB`` or a color name ``red``, ``green``, ``blue`` etc. - ``--wakelock``: If the argument is included, the application will prevent the device from sleeping. - ``--window``: If the argument is included, the application will not cover the Android status bar. - ``--blacklist``: The path to a file containing blacklisted patterns that will be excluded from the final APK. Defaults to ``./blacklist.txt``. - ``--whitelist``: The path to a file containing whitelisted patterns that will be included in the APK even if also blacklisted. - ``--add-jar``: The path to a .jar file to include in the APK. To include multiple jar files, pass this argument multiple times. - ``--intent-filters``: A file path containing intent filter xml to be included in AndroidManifest.xml. - ``--service``: A service name and the Python script it should run. See :ref:`arbitrary_scripts_services`. - ``add-source``: Add a source directory to the app's Java code. - ``--port``: The port on localhost that the WebView will access. Defaults to 5000. service_library ~~~~~~~~~~~~~~~ You can use this with ``--bootstrap=service_library`` option. This bootstrap can be used together with ``aar`` output target to generate a library, containing Python services that can be used with other build systems and frameworks. - ``--private``: The directory containing your project files. - ``--package``: The Java package name for your project. e.g. ``org.example.yourapp``. - ``--name``: The library name. - ``--version``: The version number. - ``--service``: A service name and the Python script it should run. See :ref:`arbitrary_scripts_services`. - ``--blacklist``: The path to a file containing blacklisted patterns that will be excluded from the final AAR. Defaults to ``./blacklist.txt``. - ``--whitelist``: The path to a file containing whitelisted patterns that will be included in the AAR even if also blacklisted. - ``--add-jar``: The path to a .jar file to include in the APK. To include multiple jar files, pass this argument multiple times. - ``add-source``: Add a source directory to the app's Java code. Qt ~~ This bootstrap can be used with ``--bootstrap=qt`` or by including the ``PySide6`` or ``shiboken6`` recipe, e.g. ``--requirements=pyside6,shiboken6``. Currently, the only way to use this bootstrap is through `pyside6-android-deploy `__ tool shipped with ``PySide6``, as the recipes for ``PySide6`` and ``shiboken6`` are created dynamically. The tool builds ``PySide6`` and ``shiboken6`` wheels for a specific Android platform and the recipes simply unpack the built wheels. You can see the recipes `here `__. .. note:: The ``pyside6-android-deploy`` tool and hence the Qt bootstrap does not support multi-architecture builds currently. What are Qt and PySide? %%%%%%%%%%%%%%%%%%%%%%%% `Qt `__ is a popularly used cross-platform C++ framework for developing GUI applications. `PySide6 `__ refers to the Python bindings for Qt6, and enables the Python developers access to the Qt6 API. `Shiboken6 `__ is the binding generator tool used for generating the Python bindings from C++ code. .. note:: The `shiboken6` recipe is for the `Shiboken Python module `__ which includes a couple of utility functions for inspecting and debugging PySide6 code. Build Options %%%%%%%%%%%%% ``pyside6-android-deploy`` works by generating a ``buildozer.spec`` file and thereby using `buildozer `__ to control the build options used by ``python-for-android`` with the Qt bootstrap. Apart from the general build options that works across all the other bootstraps, the Qt bootstrap introduces the following 3 new build options. - ``--qt-libs``: list of Qt libraries(modules) to be loaded. - ``--load-local-libs``: list of Qt plugin libraries to be loaded. - ``--init-classes``: list of Java class names to the loaded from the Qt jar files supplied through the ``--add-jar`` option. These build options are automatically populated by the ``pyside6-android-deploy`` tool, but can be modified by updating the ``buildozer.spec`` file. Apart from the above 3 build options, the tool also automatically identifies the values to be fed into the cli options ``--permission``, ``--add-jar`` depending on the PySide6 modules used by the application. Requirements blacklist (APK size optimization) ---------------------------------------------- To optimize the size of the `.apk` file that p4a builds for you, you can **blacklist** certain core components. Per default, p4a will add python *with batteries included* as would be expected on desktop, including openssl, sqlite3 and other components you may not use. To blacklist an item, specify the ``--blacklist-requirements`` option:: p4a apk ... --blacklist-requirements=sqlite3 At the moment, the following core components can be blacklisted (if you don't want to use them) to decrease APK size: - ``android`` disables p4a's android module (see :ref:`reference-label-for-android-module`) - ``libffi`` disables ctypes stdlib module - ``openssl`` disables ssl stdlib module - ``sqlite3`` disables sqlite3 stdlib module ================================================ FILE: doc/source/commands.rst ================================================ Commands ======== This page documents all the commands and options that can be passed to toolchain.py. Commands index -------------- The commands available are the methods of the ToolchainCL class, documented below. They may have options of their own, or you can always pass `general arguments`_ or `distribution arguments`_ to any command (though if irrelevant they may not have an effect). .. autoclass:: toolchain.ToolchainCL :members: General arguments ----------------- These arguments may be passed to any command in order to modify its behaviour, though not all commands make use of them. ``--debug`` Print extra debug information about the build, including all compilation output. ``--sdk_dir`` The filepath where the Android SDK is installed. This can alternatively be set in several other ways. ``--android_api`` The Android API level to target; python-for-android will check if the platform tools for this level are installed. ``--ndk_dir`` The filepath where the Android NDK is installed. This can alternatively be set in several other ways. ``--ndk_version`` The version of the NDK installed, important because the internal filepaths to build tools depend on this. This can alternatively be set in several other ways, or if your NDK dir contains a RELEASE.TXT containing the version this is automatically checked so you don't need to manually set it. Distribution arguments ---------------------- p4a supports several arguments used for specifying which compiled Android distribution you want to use. You may pass any of these arguments to any command, and if a distribution is required they will be used to load, or compile, or download this as necessary. None of these options are essential, and in principle you need only supply those that you need. ``--name NAME`` The name of the distribution. Only one distribution with a given name can be created. ``--requirements LIST,OF,REQUIREMENTS`` The recipes that your distribution must contain, as a comma separated list. These must be names of recipes or the pypi names of Python modules. ``--force-build BOOL`` Whether the distribution must be compiled from scratch. ``--arch`` The architecture to build for. You can specify multiple architectures to build for at the same time. As an example ``p4a ... --arch arm64-v8a --arch armeabi-v7a ...`` will build a distribution for both ``arm64-v8a`` and ``armeabi-v7a``. ``--bootstrap BOOTSTRAP`` The Java bootstrap to use for your application. You mostly don't need to worry about this or set it manually, as an appropriate bootstrap will be chosen from your ``--requirements``. Current choices are ``sdl2`` (used with Kivy and most other apps), ``webview`` or ``qt``. .. note:: These options are preliminary. Others will include toggles for allowing downloads, and setting additional directories from which to load user dists. ================================================ FILE: doc/source/conf.py ================================================ # -*- coding: utf-8 -*- # # python-for-android documentation build configuration file, created by # sphinx-quickstart2 on Wed Jun 24 22:46:06 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import datetime import os import re import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) sys.path.append(os.path.abspath('../../pythonforandroid')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'python-for-android' _today = datetime.datetime.now() author = 'Kivy Team and other contributors' copyright = f'2015-{_today.year}, {author}' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # Lookup the version from the pyjnius module, without installing it # since readthedocs.org may have issue to install it. # Read the version from the __init__.py file, without importing it. def get_version(): with open( os.path.join(os.path.abspath("../.."), "pythonforandroid", "__init__.py") ) as fp: for line in fp: m = re.search(r'^\s*__version__\s*=\s*([\'"])([^\'"]+)\1\s*$', line) if m: return m.group(2) # The short X.Y version. version = get_version() # The full version, including alpha/beta/rc tags. release = get_version() # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'furo' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'python-for-androiddoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'python-for-android.tex', 'python-for-android Documentation', author, 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'python-for-android', u'python-for-android Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'python-for-android', u'python-for-android Documentation', author, 'python-for-android', 'A development tool that packages Python apps into binaries that can run on ' 'Android devices', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} # Ignore some troublesome links that are actually fine. linkcheck_ignore = [ # Special characters in URL seems to confuse link-checker. r"https://developer.android.com/reference/android/app/Activity#onActivity.*", # GitHub parses anchor tags differently to pure HTML r"https://github.com/kivy/python-for-android/blob.*", ] # Allow redirects for URLs where we prefer to keep the original form linkcheck_allowed_redirects = { # Kivy chat redirects to Discord invite r"https://chat\.kivy\.org/": r"https://discord\.com/.*", # GitHub archive URLs redirect to codeload r"https://github\.com/kivy/python-for-android/archive/.*": r"https://codeload\.github\.com/.*", # GitHub gist homepage redirects to starred r"https://gist\.github\.com/$": r"https://gist\.github\.com/.*", # Google Play Store redirects r"https://play\.google\.com/store/$": r"https://play\.google\.com/store/.*", } ================================================ FILE: doc/source/contact.rst ================================================ Contact Us ========== If you are looking to contact the Kivy Team (who are responsible for managing the python-for-android project), including looking for support, please see our `latest contact details `_. ================================================ FILE: doc/source/contribute.rst ================================================ .. _contributing: .. _contribute: Contribution Guidelines ======================= Buildozer is part of the `Kivy `_ ecosystem - a large group of products used by many thousands of developers for free, but it is built entirely by the contributions of volunteers. We welcome (and rely on) users who want to give back to the community by contributing to the project. Contributions can come in many forms. See the latest `Contribution Guidelines `_ for general guidelines of how you can help us, and specific instructions for python-for-android development. .. warning:: The python-for-android process differs in small but important ways from the Kivy framework's process. ================================================ FILE: doc/source/distutils.rst ================================================ distutils/setuptools integration ================================ Have `p4a apk` run setup.py (replaces ``--requirements``) --------------------------------------------------------- If your project has a `setup.py` file, then it can be executed by `p4a` when your app is packaged such that your app properly ends up in the packaged site-packages. (Use ``--use-setup-py`` to enable this, ``--ignore-setup-py`` to prevent it) This is functionality to run **setup.py INSIDE `p4a apk`,** as opposed to the other section below, which is about running *p4a inside setup.py*. This however has these caveats: - **Only your ``main.py`` from your app's ``--private`` data is copied into the .apk!** Everything else needs to be installed by your ``setup.py`` into the site-packages, or it won't be packaged. - All dependencies that map to recipes can only be pinned to exact versions, all other constraints will either just plain not work or even cause build errors. (Sorry, our internal processing is just not smart enough to honor them properly at this point) - The dependency analysis at the start may be quite slow and delay your build Reasons why you would want to use a `setup.py` to be processed (and omit specifying ``--requirements``): - You want to use a more standard mechanism to specify dependencies instead of ``--requirements`` - You already use a `setup.py` for other platforms - Your application imports itself in a way that won't work unless installed to site-packages) Reasons **not** to use a `setup.py` (that is to use the usual ``--requirements`` mechanism instead): - You don't use a `setup.py` yet, and prefer the simplicity of just specifying ``--requirements`` - Your `setup.py` assumes a desktop platform and pulls in Android-incompatible dependencies, and you are not willing to change this, or you want to keep it separate from Android deployment for other organizational reasons - You need data files to be around that aren't installed by your `setup.py` into the site-packages folder Use your setup.py to call p4a ----------------------------- Instead of running p4a via the command line, you can call it via `setup.py` instead, by it integrating with distutils and setup.py. This is functionality to run **p4a INSIDE setup.py,** as opposed to the other section above, which is about running *setup.py inside `p4a apk`*. The base command is:: python setup.py apk The files included in the APK will be all those specified in the ``package_data`` argument to setup. For instance, the following example will include all .py and .png files in the ``testapp`` folder:: from distutils.core import setup from setuptools import find_packages setup( name='testapp_setup', version='1.1', description='p4a setup.py example', author='Your Name', author_email='youremail@address.com', packages=find_packages(), options=options, package_data={'testapp': ['*.py', '*.png']} ) The app name and version will also be read automatically from the setup.py. The Android package name uses ``org.test.lowercaseappname`` if not set explicitly. The ``--private`` argument is set automatically using the package_data. You should *not* set this manually. The target architecture defaults to ``--armeabi``. All of these automatic arguments can be overridden by passing them manually on the command line, e.g.:: python setup.py apk --name="Testapp Setup" --version=2.5 Adding p4a arguments in setup.py ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Instead of providing extra arguments on the command line, you can store them in setup.py by passing the ``options`` parameter to :code:`setup`. For instance:: from distutils.core import setup from setuptools import find_packages options = {'apk': {'debug': None, # use None for arguments that don't pass a value 'requirements': 'sdl2,pyjnius,kivy,python3', 'android-api': 19, 'ndk-dir': '/path/to/ndk', 'dist-name': 'bdisttest', }} packages = find_packages() print('packages are', packages) setup( name='testapp_setup', version='1.1', description='p4a setup.py example', author='Your Name', author_email='youremail@address.com', packages=find_packages(), options=options, package_data={'testapp': ['*.py', '*.png']} ) These options will be automatically included when you run ``python setup.py apk``. Any options passed on the command line will override these values. Adding p4a arguments in setup.cfg ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can also provide p4a arguments in the setup.cfg file, as normal for distutils. The syntax is:: [apk] argument=value requirements=sdl2,kivy ================================================ FILE: doc/source/docker.rst ================================================ .. _docker: Docker ====== Currently we use a containerized build for testing Python for Android recipes. Docker supports three big platforms either directly with the kernel or via using headless VirtualBox and a small distro to run itself on. While this is not the actively supported way to build applications, if you are willing to play with the approach, you can use the ``Dockerfile`` to build the Docker image we use for CI builds and create an Android application with that in a container. This approach allows you to build Android applications on all platforms Docker engine supports. These steps assume you already have Docker preinstalled and set up. .. warning:: This approach is highly space unfriendly! The more layers (``commit``) or even Docker images (``build``) you create the more space it'll consume. Within the Docker image there is Android SDK and NDK + various dependencies. Within the custom diff made by building the distribution there is another big chunk of space eaten. The very basic stuff such as a distribution with: CPython 3, setuptools, Python for Android ``android`` module, SDL2 (+ deps), PyJNIus and Kivy takes almost 2 GB. Check your free space first! 1. Clone the repository:: git clone https://github.com/kivy/python-for-android 2. Build the image with name ``p4a``:: docker build --tag p4a . .. note:: You need to be in the ``python-for-android`` for the Docker build context and you can optionally use ``--file`` flag to specify the path to the ``Dockerfile`` location. 3. Create a container from ``p4a`` image with copied ``testapps`` folder in the image mounted to the same one in the cloned repo on the host:: docker run \ --interactive \ --tty \ --volume ".../testapps":/home/user/testapps \ p4a sh -c '. venv/bin/activate \ && cd testapps \ && python setup_testapp_python3.py apk \ --sdk-dir $ANDROID_SDK_HOME \ --ndk-dir $ANDROID_NDK_HOME' .. note:: On Windows you might need to use quotes and forward-slash path for volume "/c/Users/.../python-for-android/testapps":/home/user/testapps .. warning:: On Windows ``gradlew`` will attempt to use 'bash\r' command which is a result of Windows line endings. For that you'll need to install ``dos2unix`` package into the image. 4. Preserve the distribution you've already built (optional, but recommended): docker commit $(docker ps --last=1 --quiet) my_p4a_dist 5. Find the ``.APK`` file on this location:: ls -lah testapps ================================================ FILE: doc/source/faq.rst ================================================ FAQ === python-for-android has an `online FAQ `_. It contains the answers to questions that repeatedly come up. ================================================ FILE: doc/source/index.rst ================================================ python-for-android ================== python-for-android (p4a) is a development tool that packages Python apps into binaries that can run on Android devices. It can generate: * `Android Package `_ (APK) files, ready to install locally on a device, especially for testing. This format is used by many `app stores `_ but not `Google Play Store `_. * `Android App Bundle `_ (AAB) files which can be shared on `Google Play Store `_. * `Android Archive `_ (AAR) files which can be used as a reusable bundle of resources for other projects. It supports multiple CPU architectures. It supports apps developed with `Kivy framework `_, but was built to be flexible about the backend libraries (through "bootstraps"), and also supports `PySDL2 `_, and a `WebView `_ with a Python web server. It automatically supports dependencies on most pure Python packages. For other packages, including those that depend on C code, a special "recipe" must be written to support cross-compiling. python-for-android comes with recipes for many of the most popular libraries (e.g. numpy and sqlalchemy) built in. python-for-android works by cross-compiling the Python interpreter and its dependencies for Android devices, and bundling it with the app's python code and dependencies. The Python code is then interpreted on the Android device. It is recommended that python-for-android be used via `Buildozer `_, which ensures the correct dependencies are pre-installed, and centralizes the configuration. However, python-for-android is not limited to being used with Buildozer. Buildozer is released and distributed under the terms of the MIT license. You should have received a copy of the MIT license alongside your distribution. Our `latest license `_ is also available. Contents ======== .. toctree:: :maxdepth: 2 quickstart buildoptions commands apis distutils recipes bootstraps services troubleshooting docker testing_pull_requests faq contribute contact Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ================================================ FILE: doc/source/quickstart.rst ================================================ Getting Started =============== Getting up and running on python-for-android (p4a) is a simple process and should only take you a couple of minutes. We'll refer to Python for android as p4a in this documentation. Concepts -------- *Basic:* - **requirements:** For p4a, all your app's dependencies must be specified via ``--requirements`` similar to the standard `requirements.txt`. (Unless you specify them via a `setup.py`/`install_requires`) All dependencies will be mapped to "recipes" if any exist, so that many common libraries will just work. See "recipe" below for details. - **distribution:** A distribution is the final "build" of your compiled project + requirements, as an Android project assembled by p4a that can be turned directly into an APK. p4a can contain multiple distributions with different sets of requirements. - **build:** A build refers to a compiled recipe or distribution. - **bootstrap:** A bootstrap is the app backend that will start your application. The default for graphical applications is SDL2. You can also use e.g. the webview for web apps, or service_only/service_library for background services, or qt for PySide6 apps. Different bootstraps have different additional build options. *Advanced:* - **recipe:** A recipe is a file telling p4a how to install a requirement that isn't by default fully Android compatible. This is often necessary for Cython or C/C++-using python extensions. p4a has recipes for many common libraries already included, and any dependency you specified will be automatically mapped to its recipe. If a dependency doesn't work and has no recipe included in p4a, then it may need one to work. Installation ------------ Installing p4a ~~~~~~~~~~~~~~ p4a is now available on Pypi, so you can install it using pip:: pip install python-for-android You can also test the master branch from Github using:: pip install git+https://github.com/kivy/python-for-android.git Installing Prerequisites ~~~~~~~~~~~~~~~~~~~~~~~~ p4a requires a few dependencies to be installed on your system to work properly. While we're working on a way to automate pre-requisites checks, suggestions and installation on all platforms (macOS is already supported), on Linux distros you'll need to install them manually. On recent versions of Ubuntu and its derivatives you can easily install them via the following command (re-adapted from the `Dockerfile` we use to perform CI builds):: sudo apt-get update sudo apt-get install -y \ ant \ autoconf \ automake \ autopoint \ ccache \ cmake \ g++ \ gcc \ git \ lbzip2 \ libffi-dev \ libltdl-dev \ libtool \ libssl-dev \ make \ openjdk-17-jdk \ patch \ pkg-config \ python3 \ python3-dev \ python3-pip \ python3-venv \ sudo \ unzip \ wget \ zip Installing Android SDK ~~~~~~~~~~~~~~~~~~~~~~ .. warning:: python-for-android is often picky about the **SDK/NDK versions.** Pick the recommended ones from below to avoid problems. Basic SDK install ````````````````` You need to download and unpack the Android SDK and NDK to a directory (let's say $HOME/Documents/): - `Android SDK `_ - `Android NDK `_ For the Android SDK, you can download 'just the command line tools'. When you have extracted these you'll see only a directory named ``tools``, and you will need to run extra commands to install the SDK packages needed. For Android NDK, note that modern releases will only work on a 64-bit operating system. **The minimal, and recommended, NDK version to use is r28c:** - `Go to ndk downloads page `_ - Windows users should create a virtual machine with an GNU Linux os installed, and then you can follow the described instructions from within your virtual machine. Platform and build tools ```````````````````````` First, install an API platform to target. **The recommended *target* API level is 27**, you can replace it with a different number but keep in mind other API versions are less well-tested and older devices are still supported down to the **recommended specified *minimum* API/NDK API level 21**:: $SDK_DIR/tools/bin/sdkmanager "platforms;android-27" Second, install the build-tools. You can use ``$SDK_DIR/tools/bin/sdkmanager --list`` to see all the possibilities, but 28.0.2 is the latest version at the time of writing:: $SDK_DIR/tools/bin/sdkmanager "build-tools;28.0.2" Configure p4a to use your SDK/NDK ````````````````````````````````` Then, you can edit your ``~/.bashrc`` or other favorite shell to include new environment variables necessary for building on android:: # Adjust the paths! export ANDROIDSDK="$HOME/Documents/android-sdk-27" export ANDROIDNDK="$HOME/Documents/android-ndk-r23b" export ANDROIDAPI="36" # Target API version of your application export NDKAPI="21" # Minimum supported API version of your application export ANDROIDNDKVER="r10e" # Version of the NDK you installed You have the possibility to configure on any command the PATH to the SDK, NDK and Android API using: - :code:`--sdk-dir PATH` as an equivalent of `$ANDROIDSDK` - :code:`--ndk-dir PATH` as an equivalent of `$ANDROIDNDK` - :code:`--android-api VERSION` as an equivalent of `$ANDROIDAPI` - :code:`--ndk-api VERSION` as an equivalent of `$NDKAPI` - :code:`--ndk-version VERSION` as an equivalent of `$ANDROIDNDKVER` Usage ----- Build a Kivy or SDL2 application ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To build your application, you need to specify name, version, a package identifier, the bootstrap you want to use (`sdl2` for kivy or sdl2 apps) and the requirements:: p4a apk --private $HOME/code/myapp --package=org.example.myapp --name "My application" --version 0.1 --bootstrap=sdl2 --requirements=python3,kivy **Note on** ``--requirements``: **you must add all libraries/dependencies your app needs to run.** Example: ``--requirements=python3,kivy,vispy``. For an SDL2 app, `kivy` is not needed, but you need to add any wrappers you might use (e.g. `pysdl2`). This `p4a apk ...` command builds a distribution with `python3`, `kivy`, and everything else you specified in the requirements. It will be packaged using a SDL2 bootstrap, and produce an `.apk` file. *Compatibility notes:* - Python 2 is no longer supported by python-for-android. The last release supporting Python 2 was v2019.10.06. Build a WebView application ~~~~~~~~~~~~~~~~~~~~~~~~~~~ To build your application, you need to have a name, version, a package identifier, and explicitly use the webview bootstrap, as well as the requirements:: p4a apk --private $HOME/code/myapp --package=org.example.myapp --name "My WebView Application" --version 0.1 --bootstrap=webview --requirements=flask --port=5000 **Please note as with kivy/SDL2, you need to specify all your additional requirements/dependencies.** You can also replace flask with another web framework. Replace ``--port=5000`` with the port on which your app will serve a website. The default for Flask is 5000. Build a Service library archive ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To build an android archive (.aar), containing an android service , you need a name, version, package identifier, explicitly use the service_library bootstrap, and declare service entry point (See :ref:`services ` for more options), as well as the requirements and arch(s):: p4a aar --private $HOME/code/myapp --package=org.example.myapp --name "My library" --version 0.1 --bootstrap=service_library --requirements=python3 --release --service=myservice:service.py --arch=arm64-v8a --arch=armeabi-v7a You can then call the generated Java entrypoint(s) for your Python service(s) in other apk build frameworks. Exporting the Android App Bundle (aab) for distributing it on Google Play ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Starting from August 2021 for new apps and from November 2021 for updates to existings apps, Google Play Console will require the Android App Bundle instead of the long lived apk. python-for-android handles by itself the needed work to accomplish the new requirements:: p4a aab --private $HOME/code/myapp --package=org.example.myapp --name="My App" --version 0.1 --bootstrap=sdl2 --requirements=python3,kivy --arch=arm64-v8a --arch=armeabi-v7a --release This `p4a aab ...` command builds a distribution with `python3`, `kivy`, and everything else you specified in the requirements. It will be packaged using a SDL2 bootstrap, and produce an `.aab` file that contains binaries for both `armeabi-v7a` and `arm64-v8a` ABIs. The Android App Bundle, is supposed to be used for distributing your app. If you need to test it locally, on your device, you can use `bundletool ` Other options ~~~~~~~~~~~~~ You can pass other command line arguments to control app behaviours such as orientation, wakelock and app permissions. See :ref:`bootstrap_build_options`. Rebuild everything ~~~~~~~~~~~~~~~~~~ If anything goes wrong and you want to clean the downloads and builds to retry everything, run:: p4a clean_all If you just want to clean the builds to avoid redownloading dependencies, run:: p4a clean_builds && p4a clean_dists Getting help ~~~~~~~~~~~~ If something goes wrong and you don't know how to fix it, add the ``--debug`` option and post the output log to the `kivy-users Google group `__ or the kivy `#support Discord channel `_. See :doc:`troubleshooting` for more information. Advanced usage -------------- Recipe management ~~~~~~~~~~~~~~~~~ You can see the list of the available recipes with:: p4a recipes If you are contributing to p4a and want to test a recipes again, you need to clean the build and rebuild your distribution:: p4a clean_recipe_build RECIPENAME p4a clean_dists # then rebuild your distribution You can write "private" recipes for your application, just create a ``p4a-recipes`` folder in your build directory, and place a recipe in it (edit the ``__init__.py``):: mkdir -p p4a-recipes/myrecipe touch p4a-recipes/myrecipe/__init__.py Distribution management ~~~~~~~~~~~~~~~~~~~~~~~ Every time you start a new project, python-for-android will internally create a new distribution (an Android build project including Python and your other dependencies compiled for Android), according to the requirements you added on the command line. You can force the reuse of an existing distribution by adding:: p4a apk --dist_name=myproject ... This will ensure your distribution will always be built in the same directory, and avoids using more disk space every time you adjust a requirement. You can list the available distributions:: p4a distributions And clean all of them:: p4a clean_dists Configuration file ~~~~~~~~~~~~~~~~~~ python-for-android checks in the current directory for a configuration file named ``.p4a``. If found, it adds all the lines as options to the command line. For example, you can add the options you would always include such as:: --dist_name my_example --android_api 27 --requirements kivy,openssl Overriding recipes sources ~~~~~~~~~~~~~~~~~~~~~~~~~~ You can override the source of any recipe using the ``$P4A_recipename_DIR`` environment variable. For instance, to test your own Kivy branch you might set:: export P4A_kivy_DIR=/home/username/kivy The specified directory will be copied into python-for-android instead of downloading from the normal url specified in the recipe. setup.py file (experimental) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If your application is also packaged for desktop using `setup.py`, you may want to use your `setup.py` instead of the ``--requirements`` option to avoid specifying things twice. For that purpose, check out :doc:`distutils` Going further ~~~~~~~~~~~~~ See the other pages of this doc for more information on specific topics: - :doc:`buildoptions` - :doc:`commands` - :doc:`recipes` - :doc:`bootstraps` - :doc:`apis` - :doc:`troubleshooting` - :doc:`contribute` ================================================ FILE: doc/source/recipes.rst ================================================ Recipes ======= This page describes how python-for-android (p4a) compilation recipes work, and how to build your own. If you just want to build an APK, ignore this and jump straight to the :doc:`quickstart`. Recipes are special scripts for compiling and installing different programs (including Python modules) into a p4a distribution. They are necessary to take care of compilation for any compiled components, as these must be compiled for Android with the correct architecture. python-for-android comes with many recipes for popular modules. No recipe is necessary for Python modules which have no compiled components; these are installed automatically via pip. If you are new to building recipes, it is recommended that you first read all of this page, at least up to the Recipe reference documentation. The different recipe sections include a number of examples of how recipes are built or overridden for specific purposes. Creating your own Recipe ------------------------ The formal reference documentation of the Recipe class can be found in the `Recipe class `_ section and below. Check the `recipe template section `_ for a template that combines all of these ideas, in which you can replace whichever components you like. The basic declaration of a recipe is as follows:: class YourRecipe(Recipe): url = 'http://example.com/example-{version}.tar.gz' version = '2.0.3' md5sum = '4f3dc9a9d857734a488bcbefd9cd64ed' patches = ['some_fix.patch'] # Paths relative to the recipe dir depends = ['kivy', 'sdl2'] # These are just examples conflicts = ['generickndkbuild'] recipe = YourRecipe() See the `Recipe class documentation `_ for full information about each parameter. These core options are vital for all recipes, though the url may be omitted if the source is somehow loaded from elsewhere. You must include ``recipe = YourRecipe()``. This variable is accessed when the recipe is imported. Specifying the URL ------------------ .. note:: The url includes the ``{version}`` tag. You should only access the url with the ``versioned_url`` property, which replaces this with the version attribute. .. note:: you may need to specify additional headers to allow python-for-android to download the archive. Specify your additional headers by setting the download_headers property. For example, when downloading from a private github repository, you can specify the following: (For the download_headers property in your recipe) ``` [('Authorization', 'token '), ('Accept', 'application/vnd.github+json')] ``` (For the DOWNLOAD_HEADERS_my-package-name environment variable - specify as a JSON formatted set of values) .. code-block:: bash [["Authorization","token "],["Accept", "application/vnd.github+json"]] The actual build process takes place via three core methods:: def prebuild_arch(self, arch): super().prebuild_arch(arch) # Do any pre-initialisation def build_arch(self, arch): super().build_arch(arch) # Do the main recipe build def postbuild_arch(self, arch): super().build_arch(arch) # Do any clearing up These methods are always run in the listed order; prebuild, then build, then postbuild. If you defined a url for your recipe, you do *not* need to manually download it, this is handled automatically. The recipe will automatically be built in a special isolated build directory, which you can access with :code:`self.get_build_dir(arch.arch)`. You should only work within this directory. It may be convenient to use the ``current_directory`` context manager defined in toolchain.py:: from pythonforandroid.toolchain import current_directory def build_arch(self, 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') The argument to each method, ``arch``, is an object relating to the architecture currently being built for. You can mostly ignore it, though may need to use the arch name ``arch.arch``. .. note:: You can also implement arch-specific versions of each method, which are called (if they exist) by the superclass, e.g. ``def prebuild_armeabi(self, arch)``. This is the core of what's necessary to write a recipe, but has not covered any of the details of how one actually writes code to compile for android. This is covered in the next sections, including the `standard mechanisms `_ used as part of the build, and the details of specific recipe classes for Python, Cython, and some generic compiled recipes. If your module is one of the latter, you should use these later classes rather than reimplementing the functionality from scratch. .. _standard_mechanisms: Methods and tools to help with compilation ------------------------------------------ Patching modules before installation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can easily apply patches to your recipes by adding them to the ``patches`` declaration, e.g.:: patches = ['some_fix.patch', 'another_fix.patch'] The paths should be relative to the recipe file. Patches are automatically applied just once (i.e. not reapplied the second time python-for-android is run). You can also use the helper functions in ``pythonforandroid.patching`` to apply patches depending on certain conditions, e.g.:: from pythonforandroid.patching import will_build, is_arch ... class YourRecipe(Recipe): patches = [('x86_patch.patch', is_arch('x86')), ('sdl2_compatibility.patch', will_build('sdl2'))] ... You can include your own conditions by passing any function as the second entry of the tuple. It will receive the ``arch`` (e.g. x86, armeabi) and ``recipe`` (i.e. the Recipe object) as kwargs. The patch will be applied only if the function returns True. Installing libs ~~~~~~~~~~~~~~~ Some recipes generate .so files that must be manually copied into the android project. You can use code like the following to accomplish this, copying to the correct lib cache dir:: def build_arch(self, arch): do_the_build() # e.g. running ./configure and make import shutil shutil.copyfile('a_generated_binary.so', self.ctx.get_libs_dir(arch.arch)) Any libs copied to this dir will automatically be included in the appropriate libs dir of the generated android project. Compiling for the Android architecture ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When performing any compilation, it is vital to do so with appropriate environment variables set, ensuring that the Android libraries are properly linked and the compilation target is the correct architecture. You can get a dictionary of appropriate environment variables with the ``get_recipe_env`` method. You should make sure to set this 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().build_arch(arch) env = self.get_recipe_env(arch) sh.echo('$PATH', _env=env) # Will print the PATH entry from the # env dict You can also use the ``shprint`` helper function from the p4a toolchain module, which will print information about the process and its current status:: from pythonforandroid.toolchain import shprint shprint(sh.echo, '$PATH', _env=env) You can also override the ``get_recipe_env`` method to add new env vars for use in your recipe. For instance, the Kivy recipe does the following when compiling for SDL2, in order to tell Kivy what backend to use:: def get_recipe_env(self, arch): env = super().get_recipe_env(arch) env['USE_SDL2'] = '1' env['KIVY_SDL2_PATH'] = ':'.join([ join(self.ctx.bootstrap.build_dir, 'jni', 'SDL', 'include'), join(self.ctx.bootstrap.build_dir, 'jni', 'SDL2_image'), join(self.ctx.bootstrap.build_dir, 'jni', 'SDL2_mixer'), join(self.ctx.bootstrap.build_dir, 'jni', 'SDL2_ttf'), ]) return env .. warning:: When using the sh module like this the new env *completely replaces* the normal environment, so you must define any env vars you want to access. Including files with your recipe ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The should_build method ~~~~~~~~~~~~~~~~~~~~~~~ The Recipe class has a ``should_build`` method, which returns a boolean. This is called for each architecture before running ``build_arch``, and if it returns False then the build is skipped. This is useful to avoid building a recipe more than once for different dists. By default, should_build returns True, but you can override it however you like. For instance, PythonRecipe and its subclasses all replace it with a check for whether the recipe is already installed in the Python distribution:: def should_build(self, arch): name = self.site_packages_name if name is None: name = self.name if self.ctx.has_package(name): info('Python package already exists in site-packages') return False info('{} apparently isn\'t already in site-packages'.format(name)) return True Using a PythonRecipe -------------------- If your recipe is to install a Python module without compiled components, you should use a PythonRecipe. This overrides ``build_arch`` to automatically call the normal ``python setup.py install`` with an appropriate environment. For instance, the following is all that's necessary to create a recipe for the Vispy module:: from pythonforandroid.recipe import PythonRecipe class VispyRecipe(PythonRecipe): version = 'master' url = 'https://github.com/vispy/vispy/archive/{version}.zip' depends = ['python3', 'numpy'] site_packages_name = 'vispy' recipe = VispyRecipe() The ``site_packages_name`` is a new attribute that identifies the folder in which the module will be installed in the Python package. This is only essential to add if the name is different to the recipe name. It is used to check if the recipe installation can be skipped, which is the case if the folder is already present in the Python installation. For reference, the code that accomplishes this is the following:: def build_arch(self, arch): super().build_arch(arch) self.install_python_package() def install_python_package(self): '''Automate the installation of a Python package (or a cython package where the cython components are pre-built).''' arch = self.filtered_archs[0] env = self.get_recipe_env(arch) info('Installing {} into site-packages'.format(self.name)) with current_directory(self.get_build_dir(arch.arch)): hostpython = sh.Command(self.ctx.hostpython) shprint(hostpython, 'setup.py', 'install', '-O2', _env=env) This combines techniques and tools from the above documentation to create a generic mechanism for all Python modules. .. note:: The hostpython is the path to the Python binary that should be used for any kind of installation. You *must* run Python in a similar way if you need to do so in any of your own recipes. Using a CythonRecipe -------------------- If your recipe is to install a Python module that uses Cython, you should use a CythonRecipe. This overrides ``build_arch`` to both build the cython components and to install the Python module just like a normal PythonRecipe. For instance, the following is all that's necessary to make a recipe for Kivy:: class KivyRecipe(CythonRecipe): version = 'stable' url = 'https://github.com/kivy/kivy/archive/{version}.zip' name = 'kivy' depends = ['sdl2', 'pyjnius'] recipe = KivyRecipe() For reference, the code that accomplishes this is the following:: def build_arch(self, arch): Recipe.build_arch(self, arch) # a hack to avoid calling # PythonRecipe.build_arch self.build_cython_components(arch) self.install_python_package() # this is the same as in a PythonRecipe def build_cython_components(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): hostpython = sh.Command(self.ctx.hostpython) # This first attempt *will* fail, because cython isn't # installed in the hostpython try: shprint(hostpython, 'setup.py', 'build_ext', _env=env) except sh.ErrorReturnCode_1: pass # ...so we manually run cython from the user's system shprint(sh.find, self.get_build_dir('armeabi'), '-iname', '*.pyx', '-exec', self.ctx.cython, '{}', ';', _env=env) # now cython has already been run so the build works shprint(hostpython, 'setup.py', 'build_ext', '-v', _env=env) # stripping debug symbols lowers the file size a lot build_lib = glob.glob('./build/lib*') shprint(sh.find, build_lib[0], '-name', '*.o', '-exec', env['STRIP'], '{}', ';', _env=env) The failing build and manual cythonisation is necessary, firstly to make sure that any .pyx files have been generated by setup.py, and secondly because cython isn't installed in the hostpython build. This may actually fail if the setup.py tries to import cython before making any .pyx files (in which case it crashes too early), although this is probably not usually an issue. If this happens to you, try patching to remove this import or make it fail quietly. Other than this, these methods follow the techniques in the above documentation to make a generic recipe for most cython based modules. Using a CompiledComponentsPythonRecipe -------------------------------------- This is similar to a CythonRecipe but is intended for modules like numpy which include compiled but non-cython components. It uses a similar mechanism to compile with the right environment. This isn't documented yet because it will probably be changed so that CythonRecipe inherits from it (to avoid code duplication). Using an NDKRecipe ------------------ If you are writing a recipe not for a Python module but for something that would normally go in the JNI dir of an Android project (i.e. it has an ``Application.mk`` and ``Android.mk`` that the Android build system can use), you can use an NDKRecipe to automatically set it up. The NDKRecipe overrides the normal ``get_build_dir`` method to place things in the Android project. .. warning:: The NDKRecipe does *not* currently actually call ndk-build, you must add this call (for your module) by manually making a build_arch method. This may be fixed later. For instance, the following recipe is all that's necessary to place SDL2_ttf in the jni dir. This is built later by the SDL2 recipe, which calls ndk-build with this as a dependency:: class LibSDL2TTF(NDKRecipe): version = '2.0.12' url = 'https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-{version}.tar.gz' dir_name = 'SDL2_ttf' recipe = LibSDL2TTF() The dir_name argument is a new class attribute that tells the recipe what the jni dir folder name should be. If it is omitted, the recipe name is used. Be careful here, sometimes the folder name is important, especially if this folder is a dependency of something else. .. _recipe_template: A Recipe template ----------------- The following template includes all the recipe sections you might use. None are compulsory, feel free to delete method overrides if you do not use them:: from pythonforandroid.toolchain import Recipe, shprint, current_directory from os.path import exists, join import sh import glob class YourRecipe(Recipe): # This could also inherit from PythonRecipe etc. if you want to # use their pre-written build processes version = 'some_version_string' url = 'http://example.com/example-{version}.tar.gz' # {version} will be replaced with self.version when downloading depends = ['python3', 'numpy'] # A list of any other recipe names # that must be built before this # one conflicts = [] # A list of any recipe names that cannot be built # alongside this one def get_recipe_env(self, arch): env = super().get_recipe_env(arch) # Manipulate the env here if you want return env def should_build(self, arch): # Add a check for whether the recipe is already built if you # want, and return False if it is. return True def prebuild_arch(self, arch): 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().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().prebuild_arch(self) # Do anything you want after the build, e.g. deleting # unnecessary files such as documentation recipe = YourRecipe() Examples of recipes ------------------- This documentation covers most of what is ever necessary to make a recipe work. For further examples, python-for-android includes many recipes for popular modules, which are an excellent resource to find out how to add your own. You can find these in the `python-for-android Github page `__. .. _recipe_class: The ``Recipe`` class -------------------- The ``Recipe`` is the base class for all p4a recipes. The core documentation of this class is given below, followed by discussion of how to create your own Recipe subclass. .. autoclass:: toolchain.Recipe :members: :member-order: bysource ================================================ FILE: doc/source/services.rst ================================================ Services ======== python-for-android supports the use of Android Services, background tasks running in separate processes. These are the closest Android equivalent to multiprocessing on e.g. desktop platforms, and it is not possible to use normal multiprocessing on Android. Services are also the only way to run code when your app is not currently opened by the user. Services must be declared when building your APK. Each one will have its own main.py file with the Python script to be run. Please note that python-for-android explicitly runs services as separated processes by having a colon ":" in the beginning of the name assigned to the ``android:process`` attribute of the ``AndroidManifest.xml`` file. This is not the default behavior, see `Android service documentation `__. You can communicate with the service process from your app using e.g. `osc `__ or (a heavier option) `twisted `__. Service creation ---------------- There are two ways to have services included in your APK. Service folder ~~~~~~~~~~~~~~ This is the older method of handling services. It is recommended to use the second method (below) where possible. Create a folder named ``service`` in your app directory, and add a file ``service/main.py``. This file should contain the Python code that you want the service to run. To start the service, use the :code:`start_service` function from the :code:`android` module (you may need to add ``android`` to your app requirements):: import android android.start_service(title='service name', description='service description', arg='argument to service') .. _arbitrary_scripts_services: Arbitrary service scripts ~~~~~~~~~~~~~~~~~~~~~~~~~ This method is recommended for non-trivial use of services as it is more flexible, supporting multiple services and a wider range of options. To create the service, create a python script with your service code and add a :code:`--service=myservice:PATH_TO_SERVICE_PY` argument when calling python-for-android, or in buildozer.spec, a :code:`services = myservice:PATH_TO_SERVICE_PY` [app] setting. The ``myservice`` name before the colon is the name of the service class, via which you will interact with it later. The ``PATH_TO_SERVICE_PY`` is the relative path to the service entry point (like ``services/myservice.py``) You can optionally specify the following parameters: - :code:`:foreground` for launching a service as an Android foreground service - :code:`:sticky` for launching a service that gets restarted by the Android OS on exit/error - :code:`:foregroundServiceType=TYPE` to specify the type of foreground service, where TYPE is one of the valid Android foreground service types (see `Android documentation `__ for more details). You can specify multiple types separated by a pipe character "|", e.g. :code:`:foregroundServiceType=location|mediaPlayback`. Mandatory if :code:`:foreground` is used on Android 14+. Full command with all the optional parameters included would be: :code:`--service=myservice:services/myservice.py:foreground:sticky:foregroundServiceType=location` You can add multiple :code:`--service` arguments to include multiple services, or separate them with a comma in buildozer.spec, all of which you will later be able to stop and start from your app. To run the services (i.e. starting them from within your main app code), you must use PyJNIus to interact with the java class python-for-android creates for each one, as follows:: from jnius import autoclass service = autoclass('your.package.domain.package.name.ServiceMyservice') mActivity = autoclass('org.kivy.android.PythonActivity').mActivity argument = '' service.start(mActivity, argument) Here, ``your.package.domain.package.name`` refers to the package identifier of your APK. If you are using buildozer, the identifier is set by the ``package.name`` and ``package.domain`` values in your buildozer.spec file. The name of the service is ``ServiceMyservice``, where ``Myservice`` is the name specified by one of the ``services`` values, but with the first letter upper case. If you are using python-for-android directly, the identifier is set by the ``--package`` argument to python-for-android. The name of the service is ``ServiceMyservice``, where ``Myservice`` is the identifier that was previously passed to the ``--service`` argument, but with the first letter upper case. You must also pass the ``argument`` parameter even if (as here) it is an empty string. If you do pass it, the service can make use of this argument. The service argument is made available to your service via the 'PYTHON_SERVICE_ARGUMENT' environment variable. It is exposed as a simple string, so if you want to pass in multiple values, we would recommend using the json module to encode and decode more complex data. :: from os import environ argument = environ.get('PYTHON_SERVICE_ARGUMENT', '') To customize the notification icon, title, and text use three optional arguments to service.start():: service.start(mActivity, 'small_icon', 'title', 'content' , argument) Where 'small_icon' is the name of an Android drawable or mipmap resource, and 'title' and 'content' are strings in the notification. Services support a range of options and interactions not yet documented here but all accessible via calling other methods of the ``service`` reference. .. note:: The app root directory for Python imports will be in the app root folder even if the service file is in a subfolder. To import from your service folder you must use e.g. ``import service.module`` instead of ``import module``, if the service file is in the ``service/`` folder. Service auto-restart ~~~~~~~~~~~~~~~~~~~~ It is possible to make services restart automatically when they exit by calling ``setAutoRestartService(True)`` on the service object. The call to this method should be done within the service code:: from jnius import autoclass PythonService = autoclass('org.kivy.android.PythonService') PythonService.mService.setAutoRestartService(True) ================================================ FILE: doc/source/testing_pull_requests.rst ================================================ Testing an python-for-android pull request ========================================== In order to test a pull request, we recommend to consider the following points: #. of course, check if the overall thing makes sense #. is the CI passing? if not what specifically fails #. is it working locally at compile time? #. is it working on device at runtime? This document will focus on the third point: `is it working locally at compile time?` so we will give some hints about how to proceed in order to create a local copy of the pull requests and build an apk. We expect that the contributors has enough criteria/knowledge to perform the other steps mentioned, so let's begin... To create an apk from a python-for-android pull request we contemplate three possible scenarios: - using python-for-android commands directly from the pull request files that we want to test, without installing it (the recommended way for most of the test cases) - installing python-for-android using the github's branch of the pull request - using buildozer and a custom app We will explain the first two methods using one of the distributed python-for-android test apps and we assume that you already have the python-for-android dependencies installed. For the `buildozer` method we also expect that you already have a a properly working app to test and a working installation/configuration of `buildozer`. There is one step that it's shared with all the testing methods that we propose in here...we named it `Common steps`. Common steps ^^^^^^^^^^^^ The first step to do it's to get a copy of the pull request, we can do it of several ways, and that it will depend of the circumstances but all the methods presented here will do the job, so... 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:`, e.g.: .. code-block:: bash 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 .. 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: .. 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: .. code-block:: bash git fetch obiwankenobi git checkout obiwankenobi/feature-fix-numpy 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`: .. code-block:: bash git clone -b feature-fix-numpy \ --single-branch \ https://github.com/obiwankenobi/python-for-android.git \ p4a-feature-fix-numpy Here's the above command explained line by line: - `git clone -b feature-fix-numpy`: we tell git that we want to clone the branch named `feature-fix-numpy` - `--single-branch`: we tell git that we only want that branch - `https://github.com/obiwankenobi/python-for-android.git`: noticed the nickname of the user that created the pull request: `obiwankenobi` in the middle of the line? that should be changed as needed for each pull request that you want to test - `p4a-feature-fix-numpy`: the name of the cloned repository, so we can have multiple clones of different prs in the same folder .. note:: You can view the author/branch information looking at the subtitle of the pull request, near the pull request status (expected an `open` status) 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, 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=36 \ --arch=arm64-v8a \ --permission=VIBRATE \ --debug Things that you should know: - 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 command, since the default set in there it's unlikely that match your installation: - `--ndk-dir`: An absolute path to your android's NDK dir - `--sdk-dir`: An absolute path to your android's SDK dir - `--debug`: this one enables the debug mode of python-for-android, which will show all log messages of the build. You can omit this one but it's worth it to be mentioned, since this it's useful to us when trying to find the source of the problem when things goes wrong - The apk generated by the above command should be located at the root of of the cloned repository, were you run the command to build the apk - The testapps distributed with python-for-android are located at `testapps` folder under the main folder project - All the builds of python-for-android are located at `~/.local/share/python-for-android` - You should have a downloaded copy of the android's NDK and SDK 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, e.g.: .. code-block:: bash cd p4a-feature-fix-numpy pip3 install . --upgrade --user - Now, go inside the `testapps/on_device_unit_tests` directory (we assume that you still are inside the cloned repository) .. code-block:: bash 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 .. code-block:: bash python3 setup.py apk \ --ndk-dir=/media/DEVEL/Android/android-ndk-r20 \ --sdk-dir=/media/DEVEL/Android/android-sdk-linux \ --android-api=36 \ --arch=arm64-v8a \ --debug Things that you should know: - In the example above, we override some variables that are set in `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: - `--ndk-dir`: An absolute path to your android's NDK dir - `--sdk-dir`: An absolute path to your android's SDK dir .. tip:: if you don't want to mess up with the system's python, you could do the same steps but inside a virtualenv .. warning:: Once you finish the pull request tests remember to go back to the master or develop versions of python-for-android, since you just installed the python-for-android files of the `pull request` Using buildozer with a custom app ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Edit your `buildozer.spec` file. You should search for the key `p4a.source_dir` and set the right value so in the example posted in `Common steps` it would look like this:: p4a.source_dir = /home/user/p4a_pull_requests/p4a-feature-fix-numpy - Run you buildozer command as usual, e.g.:: buildozer android debug p4a --dist-name=dist-test-feature-fix-numpy .. note:: this method has the advantage, can be run without installing the pull request version of python-for-android nor the android's dependencies but has one problem...when things goes wrong you must determine if it's a buildozer issue or a python-for-android one .. warning:: Once you finish the pull request tests remember to comment/edit the `p4a.source_dir` constant that you just edited to test the pull request .. 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 ================================================ FILE: doc/source/troubleshooting.rst ================================================ .. _troubleshooting: Troubleshooting =============== Debug output ------------ Add the ``--debug`` option to any python-for-android command to see full debug output including the output of all the external tools used in the compilation and packaging steps. If reporting a problem by email or Discord, it is usually helpful to include this full log, e.g. via a `pastebin `_ or `Github gist `_. Debugging on Android -------------------- When a python-for-android APK doesn't work, often the only indication that you get is that it closes. It is important to be able to find out what went wrong. python-for-android redirects Python's stdout and stderr to the Android logcat stream. You can see this by enabling developer mode on your Android device, enabling adb on the device, connecting it to your PC (you should see a notification that USB debugging is connected) and running ``adb logcat``. If adb is not in your PATH, you can find it at ``/path/to/Android/SDK/platform-tools/adb``, or access it through python-for-android with the shortcut:: python-for-android logcat or:: python-for-android adb logcat Running logcat command gives a lot of information about what Android is doing. You can usually see important lines by using logcat's built in functionality to see only lines with the ``python`` tag (or just grepping this). When your app crashes, you'll see the normal Python traceback here, as well as the output of any print statements etc. that your app runs. Use these to diagnose the problem just as normal. The adb command passes its arguments straight to adb itself, so you can also do other debugging tasks such as ``python-for-android adb devices`` to get the list of connected devices. For further information, see the Android docs on `adb `_, and on `logcat `_ in particular. Unpacking an APK ---------------- It is sometimes useful to unpack a packaged APK to see what is inside, especially when debugging python-for-android itself. APKs are just zip files, so you can extract the contents easily:: unzip YourApk.apk At the top level, this will always contain the same set of files:: $ ls AndroidManifest.xml classes.dex META-INF res assets lib YourApk.apk resources.arsc The user app data (code, images, fonts ..) is packaged into a single tarball contained in the assets folder:: $ cd assets $ ls private.tar ``private.tar`` is a tarball containing all your packaged data. Extract it:: $ tar xf private.tar This will reveal all the user app data (the files shown below are from the touchtracer demo):: $ ls README.txt android.txt icon.png main.pyc p4a_env_vars.txt particle.png private.tar touchtracer.kv Due to how We're required to ship ABI-specific things in Android App Bundle, the Python installation is packaged separately, as (most of it) is ABI-specific. For example, the Python installation for ``arm64-v8a`` is available in ``lib/arm64-v8a/libpybundle.so`` ``libpybundle.so`` is a tarball (but named like a library for packaging requirements), that contains our ``_python_bundle``:: $ tar xf libpybundle.so $ cd _python_bundle $ ls modules site-packages stdlib.zip FAQ --- Check out the `online FAQ `_ for common errors. ================================================ FILE: pythonforandroid/__init__.py ================================================ __version__ = '2024.01.21' ================================================ FILE: pythonforandroid/androidndk.py ================================================ import sys import os class AndroidNDK: """ This class is used to get the current NDK information. """ ndk_dir = "" def __init__(self, ndk_dir): self.ndk_dir = ndk_dir @property def host_tag(self): """ Returns the host tag for the current system. Note: The host tag is ``darwin-x86_64`` even on Apple Silicon macs. """ return f"{sys.platform}-x86_64" @property def llvm_prebuilt_dir(self): return os.path.join( self.ndk_dir, "toolchains", "llvm", "prebuilt", self.host_tag ) @property def llvm_bin_dir(self): return os.path.join(self.llvm_prebuilt_dir, "bin") @property def clang(self): return os.path.join(self.llvm_bin_dir, "clang") @property def clang_cxx(self): return os.path.join(self.llvm_bin_dir, "clang++") @property def llvm_binutils_prefix(self): return os.path.join(self.llvm_bin_dir, "llvm-") @property def llvm_ar(self): return f"{self.llvm_binutils_prefix}ar" @property def llvm_ranlib(self): return f"{self.llvm_binutils_prefix}ranlib" @property def llvm_objcopy(self): return f"{self.llvm_binutils_prefix}objcopy" @property def llvm_objdump(self): return f"{self.llvm_binutils_prefix}objdump" @property def llvm_readelf(self): return f"{self.llvm_binutils_prefix}readelf" @property def llvm_strip(self): return f"{self.llvm_binutils_prefix}strip" @property def llvm_nm(self): return f"{self.llvm_binutils_prefix}nm" @property def sysroot(self): return os.path.join(self.llvm_prebuilt_dir, "sysroot") @property def sysroot_include_dir(self): return os.path.join(self.sysroot, "usr", "include") @property def sysroot_lib_dir(self): return os.path.join(self.sysroot, "usr", "lib") @property def libcxx_include_dir(self): return os.path.join(self.sysroot_include_dir, "c++", "v1") ================================================ FILE: pythonforandroid/archs.py ================================================ from os import environ from os.path import join from multiprocessing import cpu_count import shutil from pythonforandroid.recipe import Recipe from pythonforandroid.util import BuildInterruptingException, build_platform class Arch: command_prefix = None '''The prefix for NDK commands such as gcc.''' arch = "" '''Name of the arch such as: `armeabi-v7a`, `arm64-v8a`, `x86`...''' arch_cflags = [] '''Specific arch `cflags`, expect to be overwrote in subclass if needed.''' common_cflags = [ '-target {target}', '-fomit-frame-pointer' ] common_cppflags = [ '-DANDROID', '-I{ctx.ndk.sysroot_include_dir}', '-I{python_includes}', ] common_ldflags = ['-L{ctx_libs_dir}'] common_ldlibs = ['-lm'] common_ldshared = [ '-pthread', '-shared', '-Wl,-O1', '-Wl,-Bsymbolic-functions', ] def __init__(self, ctx): self.ctx = ctx # Allows injecting additional linker paths used by any recipe. # This can also be modified by recipes (like the librt recipe) # to make sure that some sort of global resource is available & # linked for all others. self.extra_global_link_paths = [] def __str__(self): return self.arch @property def ndk_lib_dir(self): return join(self.ctx.ndk.sysroot_lib_dir, self.command_prefix) @property def ndk_lib_dir_versioned(self): return join(self.ndk_lib_dir, str(self.ctx.ndk_api)) @property def include_dirs(self): return [ "{}/{}".format( self.ctx.include_dir, d.format(arch=self)) for d in self.ctx.include_dirs] @property def target(self): # As of NDK r19, the toolchains installed by default with the # NDK may be used in-place. The make_standalone_toolchain.py script # is no longer needed for interfacing with arbitrary build systems. # See: https://developer.android.com/ndk/guides/other_build_systems return '{triplet}{ndk_api}'.format( triplet=self.command_prefix, ndk_api=self.ctx.ndk_api ) @property def clang_exe(self): """Full path of the clang compiler depending on the android's ndk version used.""" return self.get_clang_exe() @property def clang_exe_cxx(self): """Full path of the clang++ compiler depending on the android's ndk version used.""" return self.get_clang_exe(plus_plus=True) def get_clang_exe(self, with_target=False, plus_plus=False): """Returns the full path of the clang/clang++ compiler, supports two kwargs: - `with_target`: prepend `target` to clang - `plus_plus`: will return the clang++ compiler (defaults to `False`) """ compiler = 'clang' if with_target: compiler = '{target}-{compiler}'.format( target=self.target, compiler=compiler ) if plus_plus: compiler += '++' return join(self.ctx.ndk.llvm_bin_dir, compiler) def get_env(self, with_flags_in_cc=True): env = {} # HOME: User's home directory # # Many tools including p4a store outputs in the user's home # directory. This is found from the HOME environment variable # and falls back to the system account database. Setting HOME # can be used to globally divert these tools to use a different # path. Furthermore, in containerized environments the user may # not exist in the account database, so if HOME isn't set than # these tools will fail. if 'HOME' in environ: env['HOME'] = environ['HOME'] # CFLAGS/CXXFLAGS: the processor flags env['CFLAGS'] = ' '.join(self.common_cflags).format(target=self.target) if self.arch_cflags: # each architecture may have has his own CFLAGS env['CFLAGS'] += ' ' + ' '.join(self.arch_cflags) env['CXXFLAGS'] = env['CFLAGS'] # CPPFLAGS (for macros and includes) env['CPPFLAGS'] = ' '.join(self.common_cppflags).format( ctx=self.ctx, command_prefix=self.command_prefix, python_includes=join(Recipe.get_recipe("python3", self.ctx).include_root(self.arch)) ) # LDFLAGS: Link the extra global link paths first before anything else # (such that overriding system libraries with them is possible) env['LDFLAGS'] = ( ' ' + " ".join( [ "-L" + link_path for link_path in self.extra_global_link_paths ] ) + ' ' + ' '.join(self.common_ldflags).format( ctx_libs_dir=self.ctx.get_libs_dir(self.arch) ) ) # LDLIBS: Library flags or names given to compilers when they are # supposed to invoke the linker. env['LDLIBS'] = ' '.join(self.common_ldlibs) # CCACHE ccache = '' if self.ctx.ccache and bool(int(environ.get('USE_CCACHE', '1'))): # print('ccache found, will optimize builds') ccache = self.ctx.ccache + ' ' env['USE_CCACHE'] = '1' env['NDK_CCACHE'] = self.ctx.ccache env.update( {k: v for k, v in environ.items() if k.startswith('CCACHE_')} ) # Compiler: `CC` and `CXX` (and make sure that the compiler exists) env['PATH'] = self.ctx.env['PATH'] cc = shutil.which(self.clang_exe, path=env['PATH']) if cc is None: print('Searching path are: {!r}'.format(env['PATH'])) raise BuildInterruptingException( 'Couldn\'t find executable for CC. This indicates a ' 'problem locating the {} executable in the Android ' 'NDK, not that you don\'t have a normal compiler ' 'installed. Exiting.'.format(self.clang_exe)) if with_flags_in_cc: env['CC'] = '{ccache}{exe} {cflags}'.format( exe=self.clang_exe, ccache=ccache, cflags=env['CFLAGS']) env['CXX'] = '{ccache}{execxx} {cxxflags}'.format( execxx=self.clang_exe_cxx, ccache=ccache, cxxflags=env['CXXFLAGS']) else: env['CC'] = '{ccache}{exe}'.format( exe=self.clang_exe, ccache=ccache) env['CXX'] = '{ccache}{execxx}'.format( execxx=self.clang_exe_cxx, ccache=ccache) # Android's LLVM binutils env['AR'] = self.ctx.ndk.llvm_ar env['RANLIB'] = self.ctx.ndk.llvm_ranlib env['STRIP'] = f'{self.ctx.ndk.llvm_strip} --strip-unneeded' env['READELF'] = self.ctx.ndk.llvm_readelf env['OBJCOPY'] = self.ctx.ndk.llvm_objcopy env['MAKE'] = 'make -j{}'.format(str(cpu_count())) # Android's arch/toolchain env['ARCH'] = self.arch env['NDK_API'] = 'android-{}'.format(str(self.ctx.ndk_api)) # Custom linker options env['LDSHARED'] = env['CC'] + ' ' + ' '.join(self.common_ldshared) # Host python (used by some recipes) hostpython_recipe = Recipe.get_recipe( 'host' + self.ctx.python_recipe.name, self.ctx) env['BUILDLIB_PATH'] = join( hostpython_recipe.get_build_dir(self.arch), 'native-build', 'build', 'lib.{}-{}'.format( build_platform, self.ctx.python_recipe.major_minor_version_string, ), ) # for reproducible builds if 'SOURCE_DATE_EPOCH' in environ: for k in 'LC_ALL TZ SOURCE_DATE_EPOCH PYTHONHASHSEED BUILD_DATE BUILD_TIME'.split(): if k in environ: env[k] = environ[k] return env class ArchARM(Arch): arch = "armeabi" command_prefix = 'arm-linux-androideabi' @property def target(self): target_data = self.command_prefix.split('-') return '{triplet}{ndk_api}'.format( triplet='-'.join(['armv7a', target_data[1], target_data[2]]), ndk_api=self.ctx.ndk_api, ) class ArchARMv7_a(ArchARM): arch = 'armeabi-v7a' arch_cflags = [ '-march=armv7-a', '-mfloat-abi=softfp', '-mfpu=vfp', '-mthumb', '-fPIC', ] class Archx86(Arch): arch = 'x86' command_prefix = 'i686-linux-android' arch_cflags = [ '-march=i686', '-mssse3', '-mfpmath=sse', '-m32', '-fPIC', ] class Archx86_64(Arch): arch = 'x86_64' command_prefix = 'x86_64-linux-android' arch_cflags = [ '-march=x86-64', '-msse4.2', '-mpopcnt', '-m64', '-fPIC', ] class ArchAarch_64(Arch): arch = 'arm64-v8a' command_prefix = 'aarch64-linux-android' arch_cflags = [ '-march=armv8-a', '-fPIC' # '-I' + join(dirname(__file__), 'includes', 'arm64-v8a'), ] # Note: This `EXTRA_CFLAGS` below should target the commented `include` # above in `arch_cflags`. The original lines were added during the Sdl2's # bootstrap creation, and modified/commented during the migration to the # 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().get_env(with_flags_in_cc) # env['EXTRA_CFLAGS'] = self.arch_cflags[-1] # return env ================================================ FILE: pythonforandroid/bdistapk.py ================================================ from glob import glob from os.path import realpath, join, dirname, curdir, basename, split from setuptools import Command from shutil import copyfile import sys from pythonforandroid.util import rmdir, ensure_dir def argv_contains(t): for arg in sys.argv: if arg.startswith(t): return True return False 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(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 # same thing later to check that we support the # options. However, it works... for (option, (source, value)) in option_dict.items(): setattr(self, option, str(value)) def finalize_options(self): setup_options = self.distribution.get_option_dict(self.package_type) for (option, (source, value)) in setup_options.items(): if source == 'command line': continue if not argv_contains('--' + option): # allow 'permissions': ['permission', 'permission] in apk if option == 'permissions': for perm in value: sys.argv.append('--permission={}'.format(perm)) elif option == 'orientation': for orient in value: sys.argv.append('--orientation={}'.format(orient)) elif value in (None, 'None'): sys.argv.append('--{}'.format(option)) else: sys.argv.append('--{}={}'.format(option, value)) # Inject some argv options from setup.py if the user did not # provide them if not argv_contains('--name'): name = self.distribution.get_name() sys.argv.append('--name="{}"'.format(name)) self.name = name if not argv_contains('--package'): package = 'org.test.{}'.format(self.name.lower().replace(' ', '')) print('WARNING: You did not supply an Android package ' 'identifier, trying {} instead.'.format(package)) print(' This may fail if this is not a valid identifier') sys.argv.append('--package={}'.format(package)) if not argv_contains('--version'): version = self.distribution.get_version() sys.argv.append('--version={}'.format(version)) if not argv_contains('--arch'): arch = 'armeabi-v7a' self.arch = arch sys.argv.append('--arch={}'.format(arch)) def run(self): self.prepare_build_dir() from pythonforandroid.entrypoints import main sys.argv[1] = self.package_type main() def prepare_build_dir(self): if argv_contains('--private') and not argv_contains('--launcher'): print('WARNING: Received --private argument when this would ' 'normally be generated automatically.') print(' This is probably bad unless you meant to do ' 'that.') bdist_dir = 'build/bdist.android-{}'.format(self.arch) rmdir(bdist_dir) ensure_dir(bdist_dir) globs = [] for directory, patterns in self.distribution.package_data.items(): for pattern in patterns: globs.append(join(directory, pattern)) filens = [] for pattern in globs: filens.extend(glob(pattern)) main_py_dirs = [] if not argv_contains('--launcher'): for filen in filens: new_dir = join(bdist_dir, dirname(filen)) ensure_dir(new_dir) print('Including {}'.format(filen)) copyfile(filen, join(bdist_dir, filen)) if basename(filen) in ('main.py', 'main.pyc'): main_py_dirs.append(filen) # This feels ridiculous, but how else to define the main.py dir? # Maybe should just fail? if not main_py_dirs and not argv_contains('--launcher'): print('ERROR: Could not find main.py, so no app build dir defined') print('You should name your app entry point main.py') exit(1) if len(main_py_dirs) > 1: print('WARNING: Multiple main.py dirs found, using the shortest path') main_py_dirs = sorted(main_py_dirs, key=lambda j: len(split(j))) if not argv_contains('--launcher'): sys.argv.append('--private={}'.format( join(realpath(curdir), bdist_dir, dirname(main_py_dirs[0]))) ) 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' class BdistAAB(Bdist): """distutil command handler for 'aab'.""" description = 'Create an AAB with python-for-android' package_type = 'aab' def _set_user_options(): # This seems like a silly way to do things, but not sure if there's a # better way to pass arbitrary options onwards to p4a user_options = [('requirements=', None, None), ] for i, arg in enumerate(sys.argv): if arg.startswith('--'): if ('=' in arg or (i < (len(sys.argv) - 1) and not sys.argv[i+1].startswith('-'))): user_options.append((arg[2:].split('=')[0] + '=', None, None)) else: user_options.append((arg[2:], None, None)) BdistAPK.user_options = user_options BdistAAB.user_options = user_options BdistAAR.user_options = user_options _set_user_options() ================================================ FILE: pythonforandroid/bootstrap.py ================================================ import functools import glob import importlib import os from os.path import (join, dirname, isdir, normpath, splitext, basename) from os import listdir, walk, sep import sh import shlex import shutil from pythonforandroid.logger import (shprint, info, info_main, logger, debug) from pythonforandroid.util import ( current_directory, ensure_dir, temp_directory, BuildInterruptingException, rmdir, move) from pythonforandroid.recipe import Recipe SDL_BOOTSTRAPS = ("sdl2", "sdl3") 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, "")) if subdir.startswith(sep): # ensure it is relative subdir = subdir[1:] dest_dir = join(dest_root, subdir) if not os.path.exists(dest_dir): os.makedirs(dest_dir) src_file = join(root, filename) dest_file = join(dest_dir, filename) if os.path.isfile(src_file): if override and os.path.exists(dest_file): os.unlink(dest_file) if not os.path.exists(dest_file): if symlink: os.symlink(src_file, dest_file) else: shutil.copy(src_file, dest_file) else: os.makedirs(dest_file) default_recipe_priorities = [ "webview", "sdl2", "sdl3", "service_only" # last is highest ] # ^^ NOTE: these are just the default priorities if no special rules # apply (which you can find in the code below), so basically if no # known graphical lib or web lib is used - in which case service_only # is the most reasonable guess. def _cmp_bootstraps_by_priority(a, b): def rank_bootstrap(bootstrap): """ Returns a ranking index for each bootstrap, with higher priority ranked with higher number. """ if bootstrap.name in default_recipe_priorities: return default_recipe_priorities.index(bootstrap.name) + 1 return 0 # Rank bootstraps in order: rank_a = rank_bootstrap(a) rank_b = rank_bootstrap(b) if rank_a != rank_b: return (rank_b - rank_a) else: if a.name < b.name: # alphabetic sort for determinism return -1 else: return 1 class Bootstrap: '''An Android project template, containing recipe stuff for compilation and templated fields for APK info. ''' jni_subdir = '/jni' ctx = None bootstrap_dir = None build_dir = None dist_name = None distribution = None # All bootstraps should include Python in some way: recipe_depends = ['python3', 'android'] can_be_chosen_automatically = True '''Determines whether the bootstrap can be chosen as one that satisfies user requirements. If False, it will not be returned from Bootstrap.get_bootstrap_from_recipes. ''' # Other things a Bootstrap might need to track (maybe separately): # ndk_main.c # whitelist.txt # blacklist.txt @property def dist_dir(self): '''The dist dir at which to place the finished distribution.''' if self.distribution is None: raise BuildInterruptingException( 'Internal error: tried to access {}.dist_dir, but {}.distribution ' 'is None'.format(self, self)) return self.distribution.dist_dir @property def jni_dir(self): return self.name + self.jni_subdir def check_recipe_choices(self): '''Checks what recipes are being built to see which of the alternative and optional dependencies are being used, and returns a list of these.''' recipes = [] built_recipes = self.ctx.recipe_build_order or [] for recipe in self.recipe_depends: if isinstance(recipe, (tuple, list)): for alternative in recipe: if alternative in built_recipes: recipes.append(alternative) break return sorted(recipes) def get_build_dir_name(self): choices = self.check_recipe_choices() dir_name = '-'.join([self.name] + choices) return dir_name def get_build_dir(self): return join(self.ctx.build_dir, 'bootstrap_builds', self.get_build_dir_name()) def get_dist_dir(self, name): return join(self.ctx.dist_dir, name) @property 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 _copy_in_final_files(self): if self.name in SDL_BOOTSTRAPS: # Get the paths for copying SDL's java source code: sdl_recipe = Recipe.get_recipe(self.name, self.ctx) sdl_build_dir = sdl_recipe.get_jni_dir() src_dir = join(sdl_build_dir, "SDL", "android-project", "app", "src", "main", "java", "org", "libsdl", "app") target_dir = join(self.dist_dir, 'src', 'main', 'java', 'org', 'libsdl', 'app') # Do actual copying: info('Copying in SDL .java files from: ' + str(src_dir)) if not os.path.exists(target_dir): os.makedirs(target_dir) copy_files(src_dir, target_dir, override=True) 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.""" bootstrap_dirs = self.get_bootstrap_dirs() # now do a cumulative copy of all bootstrap dirs self.build_dir = self.get_build_dir() 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)) def prepare_dist_dir(self): ensure_dir(self.dist_dir) def _assemble_distribution_for_arch(self, arch): """Per-architecture distribution assembly. Override this method to customize per-arch behavior. Called once for each architecture in self.ctx.archs. """ self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)]) self.distribute_aars(arch) python_bundle_dir = join(f'_python_bundle__{arch.arch}', '_python_bundle') ensure_dir(python_bundle_dir) site_packages_dir = self.ctx.python_recipe.create_python_bundle( join(self.dist_dir, python_bundle_dir), arch) if not self.ctx.with_debug_symbols: self.strip_libraries(arch) self.fry_eggs(site_packages_dir) def assemble_distribution(self): """Assemble the distribution by copying files and creating Python bundle. This default implementation works for most bootstraps. Override _assemble_distribution_for_arch() for per-arch customization, or override this entire method for fundamentally different behavior. """ info_main(f'# Creating Android project ({self.name})') rmdir(self.dist_dir) shprint(sh.cp, '-r', self.build_dir, self.dist_dir) with current_directory(self.dist_dir): with open('local.properties', 'w') as fileh: fileh.write('sdk.dir={}'.format(self.ctx.sdk_dir)) with current_directory(self.dist_dir): info('Copying Python distribution') self.distribute_javaclasses(self.ctx.javaclass_dir, dest_dir=join("src", "main", "java")) for arch in self.ctx.archs: self._assemble_distribution_for_arch(arch) if 'sqlite3' not in self.ctx.recipe_build_order: with open('blacklist.txt', 'a') as fileh: fileh.write('\nsqlite3/*\nlib-dynload/_sqlite3.so\n') self._copy_in_final_files() self.distribution.save_info(self.dist_dir) @classmethod def all_bootstraps(cls): '''Find all the available bootstraps and return them.''' forbidden_dirs = ('__pycache__', 'common', '_sdl_common') bootstraps_dir = join(dirname(__file__), 'bootstraps') result = set() for name in listdir(bootstraps_dir): if name in forbidden_dirs: continue filen = join(bootstraps_dir, name) if isdir(filen): result.add(name) return result @classmethod def get_usable_bootstraps_for_recipes(cls, recipes, ctx): '''Returns all bootstrap whose recipe requirements do not conflict with the given recipes, in no particular order.''' info('Trying to find a bootstrap that matches the given recipes.') bootstraps = [cls.get_bootstrap(name, ctx) for name in cls.all_bootstraps()] acceptable_bootstraps = set() # Find out which bootstraps are acceptable: for bs in bootstraps: if not bs.can_be_chosen_automatically: continue possible_dependency_lists = expand_dependencies(bs.recipe_depends, ctx) for possible_dependencies in possible_dependency_lists: ok = True # 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): ok = False break # Check if bootstrap's dependencies conflict with chosen # packages: for recipe in recipes: try: recipe = Recipe.get_recipe(recipe, ctx) except ValueError: conflicts = [] else: conflicts = recipe.conflicts if any(conflict in possible_dependencies for conflict in conflicts): ok = False break if ok and bs not in acceptable_bootstraps: acceptable_bootstraps.add(bs) info('Found {} acceptable bootstraps: {}'.format( len(acceptable_bootstraps), [bs.name for bs in acceptable_bootstraps])) return acceptable_bootstraps @classmethod def get_bootstrap_from_recipes(cls, recipes, ctx): '''Picks a single recommended default bootstrap out of all_usable_bootstraps_from_recipes() for the given reicpes, and returns it.''' known_web_packages = {"flask"} # to pick webview over service_only recipes_with_deps_lists = expand_dependencies(recipes, ctx) acceptable_bootstraps = cls.get_usable_bootstraps_for_recipes( recipes, ctx ) def have_dependency_in_recipes(dep): for dep_list in recipes_with_deps_lists: if dep in dep_list: return True return False # Special rule: return SDL2 bootstrap if there's an sdl2 dep: if (have_dependency_in_recipes("sdl2") and "sdl2" in [b.name for b in acceptable_bootstraps] ): info('Using sdl2 bootstrap since it is in dependencies') return cls.get_bootstrap("sdl2", ctx) # Special rule: return SDL3 bootstrap if there's an sdl3 dep: if (have_dependency_in_recipes("sdl3") and "sdl3" in [b.name for b in acceptable_bootstraps] ): info('Using sdl3 bootstrap since it is in dependencies') return cls.get_bootstrap("sdl3", ctx) # Special rule: return "webview" if we depend on common web recipe: for possible_web_dep in known_web_packages: if have_dependency_in_recipes(possible_web_dep): # We have a web package dep! if "webview" in [b.name for b in acceptable_bootstraps]: info('Using webview bootstrap since common web packages ' 'were found {}'.format( known_web_packages.intersection(recipes) )) return cls.get_bootstrap("webview", ctx) prioritized_acceptable_bootstraps = sorted( list(acceptable_bootstraps), key=functools.cmp_to_key(_cmp_bootstraps_by_priority) ) if prioritized_acceptable_bootstraps: info('Using the highest ranked/first of these: {}' .format(prioritized_acceptable_bootstraps[0].name)) return prioritized_acceptable_bootstraps[0] return None @classmethod def get_bootstrap(cls, name, ctx): '''Returns an instance of a bootstrap with the given name. This is the only way you should access a bootstrap class, as it sets the bootstrap directory correctly. ''' if name is None: return None if not hasattr(cls, 'bootstraps'): cls.bootstraps = {} if name in cls.bootstraps: return cls.bootstraps[name] mod = importlib.import_module('pythonforandroid.bootstraps.{}' .format(name)) if len(logger.handlers) > 1: logger.removeHandler(logger.handlers[1]) bootstrap = mod.bootstrap bootstrap.bootstrap_dir = join(ctx.root_dir, 'bootstraps', name) bootstrap.ctx = ctx return bootstrap def distribute_libs(self, arch, src_dirs, wildcard='*', dest_dir="libs"): '''Copy existing arch libs from build dirs to current dist dir.''' info('Copying libs') tgt_dir = join(dest_dir, arch.arch) ensure_dir(tgt_dir) for src_dir in src_dirs: 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) 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.''' info('Unpacking aars') for aar in glob.glob(join(self.ctx.aars_dir, '*.aar')): self._unpack_aar(aar, arch) def _unpack_aar(self, aar, arch): '''Unpack content of .aar bundle and copy to current dist dir.''' with temp_directory() as temp_dir: name = splitext(basename(aar))[0] jar_name = name + '.jar' info("unpack {} aar".format(name)) debug(" from {}".format(aar)) debug(" to {}".format(temp_dir)) shprint(sh.unzip, '-o', aar, '-d', temp_dir) jar_src = join(temp_dir, 'classes.jar') jar_tgt = join('libs', jar_name) debug("copy {} jar".format(name)) debug(" from {}".format(jar_src)) debug(" to {}".format(jar_tgt)) ensure_dir('libs') shprint(sh.cp, '-a', jar_src, jar_tgt) so_src_dir = join(temp_dir, 'jni', arch.arch) so_tgt_dir = join('libs', arch.arch) debug("copy {} .so".format(name)) debug(" from {}".format(so_src_dir)) debug(" to {}".format(so_tgt_dir)) ensure_dir(so_tgt_dir) so_files = glob.glob(join(so_src_dir, '*.so')) shprint(sh.cp, '-a', *so_files, so_tgt_dir) def strip_libraries(self, arch): info('Stripping libraries') env = arch.get_env() tokens = shlex.split(env['STRIP']) strip = sh.Command(tokens[0]) if len(tokens) > 1: strip = strip.bake(tokens[1:]) libs_dir = join(self.dist_dir, f'_python_bundle__{arch.arch}', '_python_bundle', 'modules') filens = shprint(sh.find, libs_dir, join(self.dist_dir, 'libs'), '-iname', '*.so', _env=env).stdout.decode('utf-8') logger.info('Stripping libraries in private dir') for filen in filens.split('\n'): if not filen: continue # skip the last '' try: strip(filen, _env=env) except sh.ErrorReturnCode_1: logger.debug('Failed to strip ' + filen) def fry_eggs(self, sitepackages): info('Frying eggs in {}'.format(sitepackages)) for d in listdir(sitepackages): rd = join(sitepackages, d) if isdir(rd) and d.endswith('.egg'): info(' ' + d) files = [join(rd, f) for f in listdir(rd) if f != 'EGG-INFO'] for f in files: move(f, sitepackages) rmdir(d) def expand_dependencies(recipes, ctx): """ This function expands to lists of all different available alternative recipe combinations, with the dependencies added in ONLY for all the not-with-alternative recipes. (So this is like the deps graph very simplified and incomplete, but hopefully good enough for most basic bootstrap compatibility checks) """ # Add in all the deps of recipes where there is no alternative: recipes_with_deps = list(recipes) for entry in recipes: if not isinstance(entry, (tuple, list)) or len(entry) == 1: if isinstance(entry, (tuple, list)): entry = entry[0] try: recipe = Recipe.get_recipe(entry, ctx) recipes_with_deps += recipe.depends except ValueError: # it's a pure python package without a recipe, so we # don't know the dependencies...skipping for now pass # Split up lists by available alternatives: recipe_lists = [[]] for recipe in recipes_with_deps: if isinstance(recipe, (tuple, list)): new_recipe_lists = [] for alternative in recipe: for old_list in recipe_lists: new_list = [i for i in old_list] new_list.append(alternative) new_recipe_lists.append(new_list) recipe_lists = new_recipe_lists else: for existing_list in recipe_lists: existing_list.append(recipe) return recipe_lists ================================================ FILE: pythonforandroid/bootstraps/__init__.py ================================================ ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/__init__.py ================================================ from os.path import join from pythonforandroid.toolchain import Bootstrap from pythonforandroid.util import ensure_dir class SDLGradleBootstrap(Bootstrap): name = "_sdl_common" recipe_depends = [] def _assemble_distribution_for_arch(self, arch): """SDL bootstrap skips distribute_aars() - handled differently.""" self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)]) # Note: SDL bootstrap does not call distribute_aars() python_bundle_dir = join(f'_python_bundle__{arch.arch}', '_python_bundle') ensure_dir(python_bundle_dir) site_packages_dir = self.ctx.python_recipe.create_python_bundle( join(self.dist_dir, python_bundle_dir), arch) if not self.ctx.with_debug_symbols: self.strip_libraries(arch) self.fry_eggs(site_packages_dir) ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/.gitignore ================================================ .gradle /build/ # Ignore Gradle GUI config gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar # Cache of project .gradletasknamecache # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # gradle/wrapper/gradle-wrapper.properties ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/blacklist.txt ================================================ # prevent user to include invalid extensions *.apk *.aab *.apks *.pxd # eggs *.egg-info # unit test unittest/* # python config config/makesetup # unused kivy files (platform specific) kivy/input/providers/wm_* kivy/input/providers/mactouch* kivy/input/providers/probesysfs* kivy/input/providers/mtdev* kivy/input/providers/hidinput* kivy/core/camera/camera_videocapture* kivy/core/spelling/*osx* kivy/core/video/video_pyglet* kivy/tools kivy/tests/* kivy/*/*.h kivy/*/*.pxi # unused encodings lib-dynload/*codec* encodings/cp*.pyo encodings/tis* encodings/shift* encodings/bz2* encodings/iso* encodings/undefined* encodings/johab* encodings/p* encodings/m* encodings/euc* encodings/k* encodings/unicode_internal* encodings/quo* encodings/gb* encodings/big5* encodings/hp* encodings/hz* # unused python modules bsddb/* wsgiref/* hotshot/* pydoc_data/* tty.pyo anydbm.pyo nturl2path.pyo LICENCE.txt macurl2path.pyo dummy_threading.pyo audiodev.pyo antigravity.pyo dumbdbm.pyo sndhdr.pyo __phello__.foo.pyo sunaudio.pyo os2emxpath.pyo multiprocessing/dummy* # unused binaries python modules lib-dynload/termios.so lib-dynload/_lsprof.so lib-dynload/*audioop.so lib-dynload/_hotshot.so lib-dynload/_heapq.so lib-dynload/_json.so lib-dynload/grp.so lib-dynload/resource.so lib-dynload/pyexpat.so lib-dynload/_ctypes_test.so lib-dynload/_testcapi.so # odd files plat-linux3/regen ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/jni/Application.mk ================================================ # Uncomment this if you're using STL in your project # See CPLUSPLUS-SUPPORT.html in the NDK documentation for more information # APP_STL := stlport_static # APP_ABI := armeabi armeabi-v7a x86 APP_ABI := $(ARCH) APP_PLATFORM := $(NDK_API) ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/src/main/assets/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/src/main/java/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/src/main/java/org/kivy/android/launcher/Project.java ================================================ package org.kivy.android.launcher; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.util.Log; import java.io.File; import java.io.FileInputStream; import java.io.UnsupportedEncodingException; import java.util.Properties; /** This represents a project we've scanned for. */ public class Project { public String dir = null; String title = null; String author = null; Bitmap icon = null; public boolean landscape = false; static String decode(String s) { try { return new String(s.getBytes("ISO-8859-1"), "UTF-8"); } catch (UnsupportedEncodingException e) { return s; } } /** * Scans directory for a android.txt file. If it finds one, and it looks valid enough, then it * creates a new Project, and returns that. Otherwise, returns null. */ public static Project scanDirectory(File dir) { // We might have a link file. if (dir.getAbsolutePath().endsWith(".link")) { try { // Scan the android.txt file. File propfile = new File(dir, "android.txt"); FileInputStream in = new FileInputStream(propfile); Properties p = new Properties(); p.load(in); in.close(); String directory = p.getProperty("directory", null); if (directory == null) { return null; } dir = new File(directory); } catch (Exception e) { Log.i("Project", "Couldn't open link file " + dir, e); } } // Make sure we're dealing with a directory. if (!dir.isDirectory()) { return null; } try { // Scan the android.txt file. File propfile = new File(dir, "android.txt"); FileInputStream in = new FileInputStream(propfile); Properties p = new Properties(); p.load(in); in.close(); // Get the various properties. String title = decode(p.getProperty("title", "Untitled")); String author = decode(p.getProperty("author", "")); boolean landscape = p.getProperty("orientation", "portrait").equals("landscape"); // Create the project object. Project rv = new Project(); rv.title = title; rv.author = author; rv.icon = BitmapFactory.decodeFile(new File(dir, "icon.png").getAbsolutePath()); rv.landscape = landscape; rv.dir = dir.getAbsolutePath(); return rv; } catch (Exception e) { Log.i("Project", "Couldn't open android.txt", e); } return null; } } ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/src/main/java/org/kivy/android/launcher/ProjectAdapter.java ================================================ package org.kivy.android.launcher; import android.app.Activity; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; import org.renpy.android.ResourceManager; public class ProjectAdapter extends ArrayAdapter { private ResourceManager resourceManager; public ProjectAdapter(Activity context) { super(context, 0); resourceManager = new ResourceManager(context); } public View getView(int position, View convertView, ViewGroup parent) { Project p = getItem(position); View v = resourceManager.inflateView("chooser_item"); TextView title = (TextView) resourceManager.getViewById(v, "title"); TextView author = (TextView) resourceManager.getViewById(v, "author"); ImageView icon = (ImageView) resourceManager.getViewById(v, "icon"); title.setText(p.title); author.setText(p.author); icon.setImageBitmap(p.icon); return v; } } ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/src/main/java/org/kivy/android/launcher/ProjectChooser.java ================================================ package org.kivy.android.launcher; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Environment; import android.view.View; import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; import java.io.File; import java.util.Arrays; import org.renpy.android.ResourceManager; public class ProjectChooser extends Activity implements AdapterView.OnItemClickListener { ResourceManager resourceManager; String urlScheme; @Override public void onStart() { super.onStart(); resourceManager = new ResourceManager(this); urlScheme = resourceManager.getString("urlScheme"); // Set the window title. setTitle(resourceManager.getString("appName")); // Scan the sdcard for files, and sort them. File dir = new File(Environment.getExternalStorageDirectory(), urlScheme); File entries[] = dir.listFiles(); if (entries == null) { entries = new File[0]; } Arrays.sort(entries); // Create a ProjectAdapter and fill it with projects. ProjectAdapter projectAdapter = new ProjectAdapter(this); // Populate it with the properties files. for (File d : entries) { Project p = Project.scanDirectory(d); if (p != null) { projectAdapter.add(p); } } if (projectAdapter.getCount() != 0) { View v = resourceManager.inflateView("project_chooser"); ListView l = (ListView) resourceManager.getViewById(v, "projectList"); l.setAdapter(projectAdapter); l.setOnItemClickListener(this); setContentView(v); } else { View v = resourceManager.inflateView("project_empty"); TextView emptyText = (TextView) resourceManager.getViewById(v, "emptyText"); emptyText.setText( "No projects are available to launch. Please place a project into " + dir + " and restart this application. Press the back button to exit."); setContentView(v); } } public void onItemClick(AdapterView parent, View view, int position, long id) { Project p = (Project) parent.getItemAtPosition(position); Intent intent = new Intent("org.kivy.LAUNCH", Uri.fromParts(urlScheme, p.dir, "")); intent.setClassName(getPackageName(), "org.kivy.android.PythonActivity"); this.startActivity(intent); this.finish(); } } ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/src/main/jniLibs/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/src/main/libs/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/src/main/res/drawable/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/src/main/res/layout/chooser_item.xml ================================================ ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/src/main/res/layout/main.xml ================================================ ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/src/main/res/layout/project_chooser.xml ================================================ ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/src/main/res/layout/project_empty.xml ================================================ ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/src/main/res/mipmap/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/src/main/res/mipmap-anydpi-v26/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/templates/AndroidManifest.tmpl.xml ================================================ = 9 %} android:xlargeScreens="true" {% endif %} /> {% for perm in args.permissions %} {% endfor %} {% if args.wakelock %} {% endif %} {% if args.billing_pubkey %} {% endif %} {{ args.extra_manifest_xml }} {% for l in args.android_used_libs %} {% endfor %} {% for m in args.meta_data %} {% endfor %} {% if args.launcher %} {% else %} {% endif %} {% if args.home_app %} {% endif %} {%- if args.intent_filters -%} {{- args.intent_filters -}} {%- endif -%} {% if args.launcher %} {% endif %} {% if service or args.launcher %} {% endif %} {% for name, foreground_type in service_data %} {% endfor %} {% for name in native_services %} {% endfor %} {% if args.billing_pubkey %} {% endif %} {% for a in args.add_activity %} {% endfor %} ================================================ FILE: pythonforandroid/bootstraps/_sdl_common/build/templates/strings.tmpl.xml ================================================ {{ args.name }} {{ private_version }} {{ args.presplash_color }} {{ url_scheme }} ================================================ FILE: pythonforandroid/bootstraps/build.gradle ================================================ // Java Lint Configuration for python-for-android // This file configures Spotless to lint Java source files across all bootstraps plugins { id 'java' id 'com.diffplug.spotless' version '6.25.0' } // Repositories for plugin dependencies (e.g., google-java-format) repositories { mavenCentral() } // Define the root directory for bootstrap Java sources def bootstrapsDir = "${rootProject.projectDir}" // Collect all Java source directories from all bootstraps def javaSourceDirs = [] file(bootstrapsDir).eachDir { bootstrapDir -> def srcDir = new File(bootstrapDir, 'build/src/main/java') if (srcDir.exists()) { javaSourceDirs.add(srcDir.absolutePath) } } sourceSets { main { java { srcDirs = javaSourceDirs } } } spotless { java { // Target all Java files from the source directories target fileTree(bootstrapsDir) { include '**/build/src/main/java/**/*.java' // Exclude third-party vendored code exclude '**/org/kamranzafar/jtar/**' } // Use Google Java Format with AOSP style (Android-friendly, slightly relaxed) googleJavaFormat('1.19.2').aosp() // Remove unused imports removeUnusedImports() // Trim trailing whitespace trimTrailingWhitespace() // Ensure files end with a newline endWithNewline() } } // Disable compilation - we only want to lint, not build tasks.withType(JavaCompile).configureEach { enabled = false } ================================================ FILE: pythonforandroid/bootstraps/common/build/ant.properties ================================================ # This file is used to override default values used by the Ant build system. # # This file must be checked into Version Control Systems, as it is # integral to the build system of your project. # This file is only used by the Ant script. # You can use this to override default values such as # 'source.dir' for the location of your java source folder and # 'out.dir' for the location of your output folder. # You can also use it define how the release builds are signed by declaring # the following properties: # 'key.store' for the location of your keystore and # 'key.alias' for the name of the key to use. # The password will be asked during the build when you use the 'release' target. source.absolute.dir = tmp-src resource.absolute.dir = src/main/res asset.absolute.dir = src/main/assets ================================================ FILE: pythonforandroid/bootstraps/common/build/build.py ================================================ #!/usr/bin/env python3 from gzip import GzipFile import hashlib import json from os.path import ( dirname, join, isfile, realpath, relpath, split, exists, basename ) from os import environ, listdir, makedirs, remove import os import shlex import shutil import subprocess import sys import tarfile import tempfile import time from fnmatch import fnmatch import jinja2 from pythonforandroid.bootstrap import SDL_BOOTSTRAPS from pythonforandroid.util import rmdir, ensure_dir, max_build_tool_version def get_dist_info_for(key, error_if_missing=True): try: with open(join(dirname(__file__), 'dist_info.json'), 'r') as fileh: info = json.load(fileh) value = info[key] except (OSError, KeyError) as e: if not error_if_missing: return None print("BUILD FAILURE: Couldn't extract the key `" + key + "` " + "from dist_info.json: " + str(e)) sys.exit(1) return value def get_hostpython(): return get_dist_info_for('hostpython') def get_bootstrap_name(): return get_dist_info_for('bootstrap') if os.name == 'nt': ANDROID = 'android.bat' ANT = 'ant.bat' else: ANDROID = 'android' ANT = 'ant' curdir = dirname(__file__) BLACKLIST_PATTERNS = [ # code versioning '^*.hg/*', '^*.git/*', '^*.bzr/*', '^*.svn/*', # temp files '~', '*.bak', '*.swp', # Android artifacts '*.apk', '*.aab', ] WHITELIST_PATTERNS = [] if os.environ.get("P4A_BUILD_IS_RUNNING_UNITTESTS", "0") != "1": PYTHON = get_hostpython() _bootstrap_name = get_bootstrap_name() else: PYTHON = "python3" _bootstrap_name = "sdl2" if PYTHON is not None and not exists(PYTHON): PYTHON = None if _bootstrap_name in ('sdl2', 'sdl3', 'webview', 'service_only', 'qt'): WHITELIST_PATTERNS.append('pyconfig.h') environment = jinja2.Environment(loader=jinja2.FileSystemLoader( join(curdir, 'templates'))) DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS = 'org.kivy.android.PythonActivity' DEFAULT_PYTHON_SERVICE_JAVA_CLASS = 'org.kivy.android.PythonService' def render(template, dest, **kwargs): '''Using jinja2, render `template` to the filename `dest`, supplying the keyword arguments as template parameters. ''' dest_dir = dirname(dest) if dest_dir and not exists(dest_dir): makedirs(dest_dir) template = environment.get_template(template) text = template.render(**kwargs) f = open(dest, 'wb') f.write(text.encode('utf-8')) f.close() def is_whitelist(name): return match_filename(WHITELIST_PATTERNS, name) def is_blacklist(name): if is_whitelist(name): return False return match_filename(BLACKLIST_PATTERNS, name) def match_filename(pattern_list, name): for pattern in pattern_list: if pattern.startswith('^'): pattern = pattern[1:] else: pattern = '*/' + pattern if fnmatch(name, pattern): return True def listfiles(d): basedir = d subdirlist = [] for item in os.listdir(d): fn = join(d, item) if isfile(fn): yield fn else: subdirlist.append(join(basedir, item)) for subdir in subdirlist: for fn in listfiles(subdir): yield fn def make_tar(tfn, source_dirs, byte_compile_python=False, optimize_python=True): ''' Make a zip file `fn` from the contents of source_dis. ''' def clean(tinfo): """cleaning function (for reproducible builds)""" tinfo.uid = tinfo.gid = 0 tinfo.uname = tinfo.gname = '' tinfo.mtime = 0 return tinfo # get the files and relpath file of all the directory we asked for files = [] for sd in source_dirs: sd = realpath(sd) for fn in listfiles(sd): if is_blacklist(fn): continue if fn.endswith('.py') and byte_compile_python: fn = compile_py_file(fn, optimize_python=optimize_python) files.append((fn, relpath(realpath(fn), sd))) files.sort() # deterministic # create tar.gz of those files gf = GzipFile(tfn, 'wb', mtime=0) # deterministic tf = tarfile.open(None, 'w', gf, format=tarfile.USTAR_FORMAT) dirs = [] for fn, afn in files: dn = dirname(afn) if dn not in dirs: # create every dirs first if not exist yet d = '' for component in split(dn): d = join(d, component) if d.startswith('/'): d = d[1:] if d == '' or d in dirs: continue dirs.append(d) tinfo = tarfile.TarInfo(d) tinfo.type = tarfile.DIRTYPE clean(tinfo) tf.addfile(tinfo) # put the file tf.add(fn, afn, filter=clean) tf.close() gf.close() def compile_py_file(python_file, optimize_python=True): ''' Compile python_file to *.pyc and return the filename of the *.pyc file. ''' if PYTHON is None: return args = [PYTHON, '-m', 'compileall', '-b', '-f', python_file] if optimize_python: # -OO = strip docstrings args.insert(1, '-OO') return_code = subprocess.call(args) if return_code != 0: print('Error while running "{}"'.format(' '.join(args))) print('This probably means one of your Python files has a syntax ' 'error, see logs above') exit(1) return ".".join([os.path.splitext(python_file)[0], "pyc"]) def is_sdl_bootstrap(): return get_bootstrap_name() in SDL_BOOTSTRAPS def make_package(args): # If no launcher is specified, require a main.py/main.pyc: if (get_bootstrap_name() != "sdl" or args.launcher is None) and \ 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 not exists(join(realpath(args.private), 'main.pyc'))): print('''BUILD FAILURE: No main.py(c) found in your app directory. This file must exist to act as the entry point for you app. If your app is started by a file with a different name, rename it to main.py or add a main.py that loads it.''') sys.exit(1) assets_dir = "src/main/assets" # Delete the old assets. rmdir(assets_dir, ignore_errors=True) ensure_dir(assets_dir) # 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: if hasattr(args, "window"): f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n") if hasattr(args, "sdl_orientation_hint"): f.write("KIVY_ORIENTATION=" + str(args.sdl_orientation_hint) + "\n") f.write("P4A_NUMERIC_VERSION=" + str(args.numeric_version) + "\n") f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n") # Package up the private data (public not supported). use_setup_py = get_dist_info_for("use_setup_py", error_if_missing=False) is True private_tar_dirs = [env_vars_tarpath] _temp_dirs_to_clean = [] try: if args.private: if not use_setup_py or ( not exists(join(args.private, "setup.py")) and not exists(join(args.private, "pyproject.toml")) ): print('No setup.py/pyproject.toml used, copying ' 'full private data into .apk.') private_tar_dirs.append(args.private) else: print("Copying main.py's ONLY, since other app data is " "expected in site-packages.") main_py_only_dir = tempfile.mkdtemp() _temp_dirs_to_clean.append(main_py_only_dir) # Check all main.py files we need to copy: copy_paths = ["main.py", join("service", "main.py")] for copy_path in copy_paths: variants = [ copy_path, copy_path.partition(".")[0] + ".pyc", ] # Check in all variants with all possible endings: for variant in variants: if exists(join(args.private, variant)): # Make sure surrounding directly exists: dir_path = os.path.dirname(variant) if (len(dir_path) > 0 and not exists( join(main_py_only_dir, dir_path) )): ensure_dir(join(main_py_only_dir, dir_path)) # Copy actual file: shutil.copyfile( join(args.private, variant), join(main_py_only_dir, variant), ) # Append directory with all main.py's to result apk paths: private_tar_dirs.append(main_py_only_dir) if get_bootstrap_name() == "webview": for asset in listdir('webview_includes'): shutil.copy(join('webview_includes', asset), join(assets_dir, asset)) for asset in args.assets: asset_src, asset_dest = asset.split(":") if isfile(realpath(asset_src)): ensure_dir(dirname(join(assets_dir, asset_dest))) shutil.copy(realpath(asset_src), join(assets_dir, asset_dest)) else: shutil.copytree(realpath(asset_src), join(assets_dir, asset_dest)) if args.private or args.launcher: for arch in get_dist_info_for("archs"): libs_dir = f"libs/{arch}" make_tar( join(libs_dir, "libpybundle.so"), [f"_python_bundle__{arch}"], byte_compile_python=args.byte_compile_python, optimize_python=args.optimize_python, ) make_tar( join(assets_dir, "private.tar"), private_tar_dirs, byte_compile_python=args.byte_compile_python, optimize_python=args.optimize_python, ) finally: for directory in _temp_dirs_to_clean: rmdir(directory) # Remove extra env vars tar-able directory: rmdir(env_vars_tarpath) # Prepare some variables for templating process res_dir = "src/main/res" res_dir_initial = "src/res_initial" # make res_dir stateless if exists(res_dir_initial): rmdir(res_dir, ignore_errors=True) shutil.copytree(res_dir_initial, res_dir) else: shutil.copytree(res_dir, res_dir_initial) # Add user resources for resource in args.resources: resource_src, resource_dest = resource.split(":") if isfile(realpath(resource_src)): ensure_dir(dirname(join(res_dir, resource_dest))) shutil.copy(realpath(resource_src), join(res_dir, resource_dest)) else: shutil.copytree(realpath(resource_src), join(res_dir, resource_dest), dirs_exist_ok=True) default_icon = 'templates/kivy-icon.png' default_presplash = 'templates/kivy-presplash.jpg' shutil.copy( args.icon or default_icon, join(res_dir, 'mipmap/icon.png') ) if args.icon_fg and args.icon_bg: shutil.copy(args.icon_fg, join(res_dir, 'mipmap/icon_foreground.png')) shutil.copy(args.icon_bg, join(res_dir, 'mipmap/icon_background.png')) with open(join(res_dir, 'mipmap-anydpi-v26/icon.xml'), "w") as fd: fd.write(""" """) elif args.icon_fg or args.icon_bg: print("WARNING: Received an --icon_fg or an --icon_bg argument, but not both. " "Ignoring.") if get_bootstrap_name() != "service_only": lottie_splashscreen = join(res_dir, 'raw/splashscreen.json') if args.presplash_lottie: shutil.copy( 'templates/lottie.xml', join(res_dir, 'layout/lottie.xml') ) ensure_dir(join(res_dir, 'raw')) shutil.copy( args.presplash_lottie, join(res_dir, 'raw/splashscreen.json') ) else: if exists(lottie_splashscreen): remove(lottie_splashscreen) remove(join(res_dir, 'layout/lottie.xml')) shutil.copy( args.presplash or default_presplash, join(res_dir, 'drawable/presplash.jpg') ) # If extra Java jars were requested, copy them into the libs directory jars = [] if args.add_jar: for jarname in args.add_jar: if not exists(jarname): print('Requested jar does not exist: {}'.format(jarname)) sys.exit(-1) shutil.copy(jarname, 'src/main/libs') jars.append(basename(jarname)) # If extra aar were requested, copy them into the libs directory aars = [] if args.add_aar: ensure_dir("libs") for aarname in args.add_aar: if not exists(aarname): print('Requested aar does not exists: {}'.format(aarname)) sys.exit(-1) shutil.copy(aarname, 'libs') aars.append(basename(aarname).rsplit('.', 1)[0]) versioned_name = (args.name.replace(' ', '').replace('\'', '') + '-' + args.version) version_code = 0 if not args.numeric_version: """ Set version code in format (10 + minsdk + app_version) Historically versioning was (arch + minsdk + app_version), with arch expressed with a single digit from 6 to 9. Since the multi-arch support, has been changed to 10. """ min_sdk = args.min_sdk_version for i in args.version.split('.'): version_code *= 100 version_code += int(i) args.numeric_version = "{}{}{}".format("10", min_sdk, version_code) if args.intent_filters: with open(args.intent_filters) as fd: args.intent_filters = fd.read() if not args.add_activity: args.add_activity = [] if not args.activity_launch_mode: args.activity_launch_mode = '' if args.extra_source_dirs: esd = [] for spec in args.extra_source_dirs: if ':' in spec: specdir, specincludes = spec.split(':') print('WARNING: Currently gradle builds only support including source ' 'directories, so when building using gradle all files in ' '{} will be included.'.format(specdir)) else: specdir = spec specincludes = '**' esd.append((realpath(specdir), specincludes)) args.extra_source_dirs = esd else: args.extra_source_dirs = [] service = False if args.private: service_main = join(realpath(args.private), 'service', 'main.py') if exists(service_main) or exists(service_main + 'o'): service = True service_data = [] base_service_class = args.service_class_name.split('.')[-1] for sid, spec in enumerate(args.services): spec = spec.split(':') name = spec[0] entrypoint = spec[1] options = spec[2:] foreground = 'foreground' in options sticky = 'sticky' in options foreground_type_option = next((s for s in options if s.startswith('foregroundServiceType')), None) foreground_type = None if foreground_type_option: parts = foreground_type_option.split('=', 1) if len(parts) != 2 or not parts[1]: raise ValueError( 'Missing value for `foregroundServiceType` option. ' 'Expected format: foregroundServiceType=location' ) foreground_type = parts[1] service_data.append((name, foreground_type)) service_target_path =\ 'src/main/java/{}/Service{}.java'.format( args.package.replace(".", "/"), name.capitalize() ) render( 'Service.tmpl.java', service_target_path, name=name, entrypoint=entrypoint, args=args, foreground=foreground, sticky=sticky, service_id=sid + 1, base_service_class=base_service_class, ) # Find the SDK directory and target API with open('project.properties', 'r') as fileh: target = fileh.read().strip() android_api = target.split('-')[1] if android_api.isdigit(): android_api = int(android_api) else: raise ValueError( "failed to extract the Android API level from " + "build.properties. expected int, got: '" + str(android_api) + "'" ) with open('local.properties', 'r') as fileh: sdk_dir = fileh.read().strip() sdk_dir = sdk_dir[8:] # Try to build with the newest available build tools ignored = {".DS_Store", ".ds_store"} build_tools_versions = [x for x in listdir(join(sdk_dir, 'build-tools')) if x not in ignored] build_tools_version = max_build_tool_version(build_tools_versions) # Folder name for launcher (used by SDL2 bootstrap) url_scheme = 'kivy' # Copy backup rules file if specified and update the argument res_xml_dir = join(res_dir, 'xml') if args.backup_rules: ensure_dir(res_xml_dir) shutil.copy(join(args.private, args.backup_rules), res_xml_dir) args.backup_rules = split(args.backup_rules)[1][:-4] # Copy res_xml files to src/main/res/xml if args.res_xmls: ensure_dir(res_xml_dir) for xmlpath in args.res_xmls: if not os.path.exists(xmlpath): xmlpath = join(args.private, xmlpath) shutil.copy(xmlpath, res_xml_dir) # Render out android manifest: manifest_path = "src/main/AndroidManifest.xml" render_args = { "args": args, "service": service, "service_data": service_data, "android_api": android_api, "debug": "debug" in args.build_mode, "native_services": args.native_services, } if is_sdl_bootstrap(): render_args["url_scheme"] = url_scheme render( 'AndroidManifest.tmpl.xml', manifest_path, **render_args) # Copy the AndroidManifest.xml to the dist root dir so that ant # can also use it if exists('AndroidManifest.xml'): remove('AndroidManifest.xml') shutil.copy(manifest_path, 'AndroidManifest.xml') # gradle build templates render( 'build.tmpl.gradle', 'build.gradle', args=args, aars=aars, jars=jars, android_api=android_api, build_tools_version=build_tools_version, debug_build="debug" in args.build_mode, is_library=(get_bootstrap_name() == 'service_library'), ) # gradle properties render( 'gradle.tmpl.properties', 'gradle.properties', args=args, bootstrap_name=get_bootstrap_name()) # ant build templates render( 'build.tmpl.xml', 'build.xml', args=args, versioned_name=versioned_name) # String resources: timestamp = time.time() if 'SOURCE_DATE_EPOCH' in environ: # for reproducible builds timestamp = int(environ['SOURCE_DATE_EPOCH']) private_version = "{} {} {}".format( args.version, args.numeric_version, timestamp ) render_args = { "args": args, "private_version": hashlib.sha1(private_version.encode()).hexdigest() } if is_sdl_bootstrap(): render_args["url_scheme"] = url_scheme render( 'strings.tmpl.xml', join(res_dir, 'values/strings.xml'), **render_args) # Library resources from Qt # These are referred by QtLoader.java in Qt6AndroidBindings.jar # qt_libs and load_local_libs are loaded at App startup if get_bootstrap_name() == "qt": qt_libs = args.qt_libs.split(",") load_local_libs = args.load_local_libs.split(",") init_classes = args.init_classes if init_classes: init_classes = init_classes.split(",") init_classes = ":".join(init_classes) arch = get_dist_info_for("archs")[0] render( 'libs.tmpl.xml', join(res_dir, 'values/libs.xml'), qt_libs=qt_libs, load_local_libs=load_local_libs, init_classes=init_classes, arch=arch ) if exists(join("templates", "custom_rules.tmpl.xml")): render( 'custom_rules.tmpl.xml', 'custom_rules.xml', args=args) if get_bootstrap_name() == "webview": render('WebViewLoader.tmpl.java', 'src/main/java/org/kivy/android/WebViewLoader.java', args=args) if args.sign: render('build.properties', 'build.properties') else: if exists('build.properties'): os.remove('build.properties') # Apply java source patches if any are present: if exists(join('src', 'patches')): print("Applying Java source code patches...") for patch_name in os.listdir(join('src', 'patches')): patch_path = join('src', 'patches', patch_name) print("Applying patch: " + str(patch_path)) # -N: insist this is FORWARD patch, don't reverse apply # -p1: strip first path component # -t: batch mode, don't ask questions patch_command = ["patch", "-N", "-p1", "-t", "-i", patch_path] try: # Use a dry run to establish whether the patch is already applied. # If we don't check this, the patch may be partially applied (which is bad!) subprocess.check_output(patch_command + ["--dry-run"]) except subprocess.CalledProcessError as e: if e.returncode == 1: # Return code 1 means not all hunks could be applied, this usually # means the patch is already applied. print("Warning: failed to apply patch (exit code 1), " "assuming it is already applied: ", str(patch_path)) else: raise e else: # The dry run worked, so do the real thing subprocess.check_output(patch_command) def parse_permissions(args_permissions): if args_permissions and isinstance(args_permissions[0], list): args_permissions = [p for perm in args_permissions for p in perm] def _is_advanced_permission(permission): return permission.startswith("(") and permission.endswith(")") def _decode_advanced_permission(permission): SUPPORTED_PERMISSION_PROPERTIES = ["name", "maxSdkVersion", "usesPermissionFlags"] _permission_args = permission[1:-1].split(";") _permission_args = (arg.split("=") for arg in _permission_args) advanced_permission = dict(_permission_args) if "name" not in advanced_permission: raise ValueError("Advanced permission must have a name property") for key in advanced_permission.keys(): if key not in SUPPORTED_PERMISSION_PROPERTIES: raise ValueError( f"Property '{key}' is not supported. " "Advanced permission only supports: " f"{', '.join(SUPPORTED_PERMISSION_PROPERTIES)} properties" ) return advanced_permission _permissions = [] for permission in args_permissions: if _is_advanced_permission(permission): _permissions.append(_decode_advanced_permission(permission)) else: if "." in permission: _permissions.append(dict(name=permission)) else: _permissions.append(dict(name=f"android.permission.{permission}")) return _permissions def get_sdl_orientation_hint(orientations): SDL_ORIENTATION_MAP = { "landscape": "LandscapeLeft", "portrait": "Portrait", "portrait-reverse": "PortraitUpsideDown", "landscape-reverse": "LandscapeRight", } return " ".join( [SDL_ORIENTATION_MAP[x] for x in orientations if x in SDL_ORIENTATION_MAP] ) def get_manifest_orientation(orientations, manifest_orientation=None): # If the user has specifically set an orientation to use in the manifest, # use that. if manifest_orientation is not None: return manifest_orientation # If multiple or no orientations are specified, use unspecified in the manifest, # as we can only specify one orientation in the manifest. if len(orientations) != 1: return "unspecified" # Convert the orientation to a value that can be used in the manifest. # If the specified orientation is not supported, use unspecified. MANIFEST_ORIENTATION_MAP = { "landscape": "landscape", "portrait": "portrait", "portrait-reverse": "reversePortrait", "landscape-reverse": "reverseLandscape", } return MANIFEST_ORIENTATION_MAP.get(orientations[0], "unspecified") def get_dist_ndk_min_api_level(): # Get the default minsdk, equal to the NDK API that this dist is built against try: with open('dist_info.json', 'r') as fileh: info = json.load(fileh) ndk_api = int(info['ndk_api']) except (OSError, KeyError, ValueError, TypeError): print('WARNING: Failed to read ndk_api from dist info, defaulting to 12') ndk_api = 12 # The old default before ndk_api was introduced return ndk_api def create_argument_parser(): ndk_api = get_dist_ndk_min_api_level() import argparse ap = argparse.ArgumentParser(description='''\ Package a Python application for Android (using bootstrap ''' + get_bootstrap_name() + '''). For this to work, Java and Ant need to be in your path, as does the tools directory of the Android SDK. ''') # --private is required unless for sdl2, where there's also --launcher ap.add_argument('--private', dest='private', help='the directory with the app source code files' + ' (containing your main.py entrypoint)', required=(not is_sdl_bootstrap())) ap.add_argument('--package', dest='package', help=('The name of the java package the project will be' ' packaged under.'), required=True) ap.add_argument('--name', dest='name', help=('The human-readable name of the project.'), required=True) ap.add_argument('--numeric-version', dest='numeric_version', help=('The numeric version number of the project. If not ' 'given, this is automatically computed from the ' 'version.')) ap.add_argument('--version', dest='version', help=('The version number of the project. This should ' 'consist of numbers and dots, and should have the ' 'same number of groups of numbers as previous ' 'versions.'), required=True) if is_sdl_bootstrap(): ap.add_argument('--launcher', dest='launcher', action='store_true', help=('Provide this argument to build a multi-app ' 'launcher, rather than a single app.')) ap.add_argument('--home-app', dest='home_app', action='store_true', default=False, help=('Turn your application into a home app (launcher)')) ap.add_argument('--display-cutout', dest='display_cutout', default='never', help=('Enables display-cutout that renders around the area (notch) on ' 'some devices that extends into the display surface')) ap.add_argument('--permission', dest='permissions', action='append', default=[], help='The permissions to give this app.', nargs='+') ap.add_argument('--meta-data', dest='meta_data', action='append', default=[], help='Custom key=value to add in application metadata') ap.add_argument('--uses-library', dest='android_used_libs', action='append', default=[], help='Used shared libraries included using tag in AndroidManifest.xml') ap.add_argument('--asset', dest='assets', action="append", default=[], metavar="/path/to/source:dest", help='Put this in the assets folder at assets/dest') ap.add_argument('--resource', dest='resources', action="append", default=[], metavar="/path/to/source:kind/asset", help='Put this in the res folder at res/kind') ap.add_argument('--icon', dest='icon', help=('A png file to use as the icon for ' 'the application.')) ap.add_argument('--icon-fg', dest='icon_fg', help=('A png file to use as the foreground of the adaptive icon ' 'for the application.')) ap.add_argument('--icon-bg', dest='icon_bg', help=('A png file to use as the background of the adaptive icon ' 'for the application.')) ap.add_argument('--service', dest='services', action='append', default=[], help='Declare a new service entrypoint: ' 'NAME:PATH_TO_PY[:foreground]') ap.add_argument('--native-service', dest='native_services', action='append', default=[], help='Declare a new native service: ' 'package.name.service') if get_bootstrap_name() != "service_only": ap.add_argument('--presplash', dest='presplash', help=('A jpeg file to use as a screen while the ' 'application is loading.')) ap.add_argument('--presplash-lottie', dest='presplash_lottie', help=('A lottie (json) file to use as an animation while the ' 'application is loading.')) ap.add_argument('--presplash-color', dest='presplash_color', default='#000000', help=('A string to set the loading screen ' 'background color. ' 'Supported formats are: ' '#RRGGBB #AARRGGBB or color names ' 'like red, green, blue, etc.')) ap.add_argument('--window', dest='window', action='store_true', default=False, help='Indicate if the application will be windowed') ap.add_argument('--manifest-orientation', dest='manifest_orientation', help=('The orientation that will be set in the ' 'android:screenOrientation attribute of the activity ' 'in the AndroidManifest.xml file. If not set, ' 'the value will be synthesized from the --orientation option.')) ap.add_argument('--orientation', dest='orientation', action="append", default=[], choices=['portrait', 'landscape', 'landscape-reverse', 'portrait-reverse'], help=('The orientations that the app will display in. ' 'Since Android ignores android:screenOrientation ' 'when in multi-window mode (Which is the default on Android 12+), ' 'this option will also set the window orientation hints ' 'for apps using the (default) SDL bootstrap.' 'If multiple orientations are given, android:screenOrientation ' 'will be set to "unspecified"')) ap.add_argument('--enable-androidx', dest='enable_androidx', action='store_true', help=('Enable the AndroidX support library, ' 'requires api = 28 or greater')) ap.add_argument('--android-entrypoint', dest='android_entrypoint', default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS, help='Defines which java class will be used for startup, usually a subclass of PythonActivity') ap.add_argument('--android-apptheme', dest='android_apptheme', default='@android:style/Theme.NoTitleBar', help='Defines which app theme should be selected for the main activity') ap.add_argument('--add-compile-option', dest='compile_options', default=[], action='append', help='add compile options to gradle.build') ap.add_argument('--add-gradle-repository', dest='gradle_repositories', default=[], action='append', help='Ddd a repository for gradle') ap.add_argument('--add-packaging-option', dest='packaging_options', default=[], action='append', help='Dndroid packaging options') ap.add_argument('--wakelock', dest='wakelock', action='store_true', help=('Indicate if the application needs the device ' 'to stay on')) ap.add_argument('--blacklist', dest='blacklist', default=join(curdir, 'blacklist.txt'), help=('Use a blacklist file to match unwanted file in ' 'the final APK')) ap.add_argument('--whitelist', dest='whitelist', 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('--with-debug-symbols', dest='with_debug_symbols', action='store_const', const=True, default=False, help='Will keep debug symbols from `.so` files.') 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 ' 'argument more than once to include multiple jars')) ap.add_argument('--add-aar', dest='add_aar', action='append', help=('Add an aar dependency manually')) ap.add_argument('--depend', dest='depends', action='append', help=('Add a external dependency ' '(eg: com.android.support:appcompat-v7:19.0.1)')) # The --sdk option has been removed, it is ignored in favour of # --android-api handled by toolchain.py ap.add_argument('--sdk', dest='sdk_version', default=-1, type=int, help=('Deprecated argument, does nothing')) ap.add_argument('--minsdk', dest='min_sdk_version', default=ndk_api, type=int, help=('Minimum Android SDK version that the app supports. ' 'Defaults to {}.'.format(ndk_api))) ap.add_argument('--allow-minsdk-ndkapi-mismatch', default=False, action='store_true', help=('Allow the --minsdk argument to be different from ' 'the discovered ndk_api in the dist')) ap.add_argument('--intent-filters', dest='intent_filters', help=('Add intent-filters xml rules to the ' 'AndroidManifest.xml file. The argument is a ' 'filename containing xml. The filename should be ' 'located relative to the python-for-android ' 'directory')) ap.add_argument('--res_xml', dest='res_xmls', action='append', default=[], help='Add files to res/xml directory (for example device-filters)', nargs='+') ap.add_argument('--with-billing', dest='billing_pubkey', help='If set, the billing service will be added (not implemented)') ap.add_argument('--add-source', dest='extra_source_dirs', action='append', help='Include additional source dirs in Java build') if get_bootstrap_name() == "webview": ap.add_argument('--port', help='The port on localhost that the WebView will access', default='5000') ap.add_argument('--try-system-python-compile', dest='try_system_python_compile', action='store_true', help='Use the system python during compileall if possible.') ap.add_argument('--sign', action='store_true', help=('Try to sign the APK with your credentials. You must set ' 'the appropriate environment variables.')) ap.add_argument('--add-activity', dest='add_activity', action='append', help='Add this Java class as an Activity to the manifest.') ap.add_argument('--activity-launch-mode', dest='activity_launch_mode', default='singleTask', help='Set the launch mode of the main activity in the manifest.') ap.add_argument('--allow-backup', dest='allow_backup', default='true', help="if set to 'false', then android won't backup the application.") ap.add_argument('--backup-rules', dest='backup_rules', default='', help=('Backup rules for Android Auto Backup. Argument is a ' 'filename containing xml. The filename should be ' 'located relative to the private directory containing your source code ' 'files (containing your main.py entrypoint). ' 'See https://developer.android.com/guide/topics/data/' 'autobackup#IncludingFiles for more information')) ap.add_argument('--no-byte-compile-python', dest='byte_compile_python', action='store_false', default=True, help='Skip byte compile for .py files.') ap.add_argument('--no-optimize-python', dest='optimize_python', action='store_false', default=True, help=('Whether to compile to optimised .pyc files, using -OO ' '(strips docstrings and asserts)')) ap.add_argument('--extra-manifest-xml', default='', help=('Extra xml to write directly inside the element of' 'AndroidManifest.xml')) ap.add_argument('--extra-manifest-application-arguments', default='', help='Extra arguments to be added to the tag of' 'AndroidManifest.xml') ap.add_argument('--manifest-placeholders', dest='manifest_placeholders', default='[:]', help=('Inject build variables into the manifest ' 'via the manifestPlaceholders property')) ap.add_argument('--service-class-name', dest='service_class_name', default=DEFAULT_PYTHON_SERVICE_JAVA_CLASS, help='Use that parameter if you need to implement your own PythonServive Java class') ap.add_argument('--activity-class-name', dest='activity_class_name', default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS, help='The full java class name of the main activity') if get_bootstrap_name() == "qt": ap.add_argument('--qt-libs', dest='qt_libs', required=True, help='comma separated list of Qt libraries to be loaded') ap.add_argument('--load-local-libs', dest='load_local_libs', required=True, help='comma separated list of Qt plugin libraries to be loaded') ap.add_argument('--init-classes', dest='init_classes', default='', help='comma separated list of java class names to be loaded from the Qt jar files, ' 'specified through add_jar cli option') return ap def parse_args_and_make_package(args=None): global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON ndk_api = get_dist_ndk_min_api_level() ap = create_argument_parser() # Put together arguments, and add those from .p4a config file: if args is None: args = sys.argv[1:] def _read_configuration(): if not exists(".p4a"): return print("Reading .p4a configuration") with open(".p4a") as fd: lines = fd.readlines() lines = [shlex.split(line) for line in lines if not line.startswith("#")] for line in lines: for arg in line: args.append(arg) _read_configuration() args = ap.parse_args(args) if args.name and args.name[0] == '"' and args.name[-1] == '"': args.name = args.name[1:-1] if ndk_api != args.min_sdk_version: print(('WARNING: --minsdk argument does not match the api that is ' 'compiled against. Only proceed if you know what you are ' 'doing, otherwise use --minsdk={} or recompile against api ' '{}').format(ndk_api, args.min_sdk_version)) if not args.allow_minsdk_ndkapi_mismatch: print('You must pass --allow-minsdk-ndkapi-mismatch to build ' 'with --minsdk different to the target NDK api from the ' 'build step') sys.exit(1) else: print('Proceeding with --minsdk not matching build target api') if args.billing_pubkey: print('Billing not yet supported!') sys.exit(1) if args.sdk_version != -1: print('WARNING: Received a --sdk argument, but this argument is ' 'deprecated and does nothing.') args.sdk_version = -1 # ensure it is not used args.permissions = parse_permissions(args.permissions) args.manifest_orientation = get_manifest_orientation( args.orientation, args.manifest_orientation ) if is_sdl_bootstrap(): args.sdl_orientation_hint = get_sdl_orientation_hint(args.orientation) if args.res_xmls and isinstance(args.res_xmls[0], list): args.res_xmls = [x for res in args.res_xmls for x in res] if args.try_system_python_compile: # Hardcoding python2.7 is okay for now, as python3 skips the # compilation anyway python_executable = 'python2.7' try: subprocess.call([python_executable, '--version']) except (OSError, subprocess.CalledProcessError): pass else: PYTHON = python_executable if args.blacklist: with open(args.blacklist) as fd: patterns = [x.strip() for x in fd.read().splitlines() if x.strip() and not x.strip().startswith('#')] BLACKLIST_PATTERNS += patterns if args.whitelist: with open(args.whitelist) as fd: patterns = [x.strip() for x in fd.read().splitlines() if x.strip() and not x.strip().startswith('#')] WHITELIST_PATTERNS += patterns if args.private is None and is_sdl_bootstrap() and args.launcher is None: print('Need --private directory or ' + '--launcher (SDL2/SDL3 bootstrap only)' + 'to have something to launch inside the .apk!') sys.exit(1) make_package(args) return args if __name__ == "__main__": parse_args_and_make_package() ================================================ FILE: pythonforandroid/bootstraps/common/build/gradle/wrapper/gradle-wrapper.properties ================================================ #Mon Mar 09 17:19:02 CET 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip ================================================ FILE: pythonforandroid/bootstraps/common/build/gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # For Cygwin, ensure paths are in UNIX format before anything is touched. if $cygwin ; then [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` fi # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: pythonforandroid/bootstraps/common/build/gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: pythonforandroid/bootstraps/common/build/jni/Android.mk ================================================ include $(call all-subdir-makefiles) ================================================ FILE: pythonforandroid/bootstraps/common/build/jni/application/Android.mk ================================================ include $(call all-subdir-makefiles) ================================================ FILE: pythonforandroid/bootstraps/common/build/jni/application/src/Android.mk ================================================ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := main SDL_PATH := ../../SDL LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include # Add your application source files here... LOCAL_SRC_FILES := start.c LOCAL_CFLAGS += -I$(PYTHON_INCLUDE_ROOT) $(EXTRA_CFLAGS) LOCAL_SHARED_LIBRARIES := python_shared LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog $(EXTRA_LDLIBS) LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS) include $(BUILD_SHARED_LIBRARY) ================================================ FILE: pythonforandroid/bootstraps/common/build/jni/application/src/start.c ================================================ #define PY_SSIZE_T_CLEAN #include "Python.h" #ifndef Py_PYTHON_H #error Python headers needed to compile C extensions, please install development version of Python. #else #include #include #include #include #include #include #include #include #include #include #include "bootstrap_name.h" #ifdef BOOTSTRAP_NAME_SDL2 #include "SDL.h" #include "SDL_opengles2.h" #endif #ifdef BOOTSTRAP_NAME_SDL3 #include "SDL3/SDL.h" #include "SDL3/SDL_main.h" #endif #include "android/log.h" #define ENTRYPOINT_MAXLEN 128 #define LOG(n, x) __android_log_write(ANDROID_LOG_INFO, (n), (x)) #define P4A_MIN_VER 11 static void LOGP(const char *fmt, ...) { va_list args; va_start(args, fmt); __android_log_vprint(ANDROID_LOG_INFO, "python", fmt, args); va_end(args); } static PyObject *androidembed_log(PyObject *self, PyObject *args) { char *logstr = NULL; if (!PyArg_ParseTuple(args, "s", &logstr)) { return NULL; } LOG(getenv("PYTHON_NAME"), logstr); Py_RETURN_NONE; } static PyMethodDef AndroidEmbedMethods[] = { {"log", androidembed_log, METH_VARARGS, "Log on android platform"}, {NULL, NULL, 0, NULL}}; #if PY_MAJOR_VERSION >= 3 static struct PyModuleDef androidembed = {PyModuleDef_HEAD_INIT, "androidembed", "", -1, AndroidEmbedMethods}; PyMODINIT_FUNC initandroidembed(void) { return PyModule_Create(&androidembed); } #else PyMODINIT_FUNC initandroidembed(void) { (void)Py_InitModule("androidembed", AndroidEmbedMethods); } #endif int dir_exists(char *filename) { struct stat st; if (stat(filename, &st) == 0) { if (S_ISDIR(st.st_mode)) return 1; } return 0; } int file_exists(const char *filename) { return access(filename, F_OK) == 0; } static void get_dirname(const char *path, char *dir, size_t size) { strncpy(dir, path, size - 1); dir[size - 1] = '\0'; char *last_slash = strrchr(dir, '/'); if (last_slash) *last_slash = '\0'; else dir[0] = '\0'; } // strip "lib" prefix and "bin.so" suffix static void get_exe_name(const char *filename, char *out, size_t size) { size_t len = strlen(filename); if (len < 7) { // too short to be valid strncpy(out, filename, size - 1); out[size - 1] = '\0'; return; } const char *start = filename; if (strncmp(filename, "lib", 3) == 0) start += 3; size_t start_len = strlen(start); if (start_len > 6) { size_t copy_len = start_len - 6; // remove "bin.so" if (copy_len >= size) copy_len = size - 1; strncpy(out, start, copy_len); out[copy_len] = '\0'; } else { strncpy(out, start, size - 1); out[size - 1] = '\0'; } } char *setup_symlinks() { Dl_info info; char lib_path[512]; char *interpreter = NULL; if (!(dladdr((void*)setup_symlinks, &info) && info.dli_fname)) { LOGP("symlinking failed: failed to get libdir"); return interpreter; } strncpy(lib_path, info.dli_fname, sizeof(lib_path) - 1); lib_path[sizeof(lib_path) - 1] = '\0'; char native_lib_dir[512]; get_dirname(lib_path, native_lib_dir, sizeof(native_lib_dir)); if (native_lib_dir[0] == '\0') { LOGP("symlinking failed: could not determine lib directory"); return interpreter; } const char *files_dir_env = getenv("ANDROID_APP_PATH"); char bin_dir[512]; snprintf(bin_dir, sizeof(bin_dir), "%s/.bin", files_dir_env); if (mkdir(bin_dir, 0755) != 0 && errno != EEXIST) { LOGP("Failed to create .bin directory"); return interpreter; } DIR *dir = opendir(native_lib_dir); if (!dir) { LOGP("Failed to open native lib dir"); return interpreter; } struct dirent *entry; while ((entry = readdir(dir)) != NULL) { const char *name = entry->d_name; size_t len = strlen(name); if (len < 7) continue; if (strcmp(name + len - 6, "bin.so") != 0) continue; // only bin.so at end // get cleaned executable name char exe_name[128]; get_exe_name(name, exe_name, sizeof(exe_name)); char src[512], dst[512]; snprintf(src, sizeof(src), "%s/%s", native_lib_dir, name); snprintf(dst, sizeof(dst), "%s/%s", bin_dir, exe_name); // interpreter found? if (strcmp(exe_name, "python") == 0) { interpreter = strdup(dst); } struct stat st; if (lstat(dst, &st) == 0) continue; // already exists if (symlink(src, dst) == 0) { LOGP("symlink: %s -> %s", name, exe_name); } else { LOGP("Symlink failed"); } } closedir(dir); // append bin_dir to PATH const char *old_path = getenv("PATH"); char new_path[1024]; if (old_path && strlen(old_path) > 0) { snprintf(new_path, sizeof(new_path), "%s:%s", old_path, bin_dir); } else { snprintf(new_path, sizeof(new_path), "%s", bin_dir); } setenv("PATH", new_path, 1); // set lib path setenv("LD_LIBRARY_PATH", native_lib_dir, 1); return interpreter; } /* int main(int argc, char **argv) { */ int main(int argc, char *argv[]) { char *env_argument = NULL; char *env_entrypoint = NULL; char *env_logname = NULL; char entrypoint[ENTRYPOINT_MAXLEN]; int ret = 0; FILE *fd; LOGP("Initializing Python for Android"); // Set a couple of built-in environment vars: setenv("P4A_BOOTSTRAP", bootstrap_name, 1); // env var to identify p4a to applications env_argument = getenv("ANDROID_ARGUMENT"); setenv("ANDROID_APP_PATH", env_argument, 1); env_entrypoint = getenv("ANDROID_ENTRYPOINT"); env_logname = getenv("PYTHON_NAME"); if (!getenv("ANDROID_UNPACK")) { /* ANDROID_UNPACK currently isn't set in services */ setenv("ANDROID_UNPACK", env_argument, 1); } if (env_logname == NULL) { env_logname = "python"; setenv("PYTHON_NAME", "python", 1); } // Set additional file-provided environment vars: LOGP("Setting additional env vars from p4a_env_vars.txt"); char env_file_path[256]; snprintf(env_file_path, sizeof(env_file_path), "%s/p4a_env_vars.txt", getenv("ANDROID_UNPACK")); FILE *env_file_fd = fopen(env_file_path, "r"); if (env_file_fd) { char* line = NULL; size_t len = 0; while (getline(&line, &len, env_file_fd) != -1) { if (strlen(line) > 0) { char *eqsubstr = strstr(line, "="); if (eqsubstr) { size_t eq_pos = eqsubstr - line; // Extract name: char env_name[256]; strncpy(env_name, line, sizeof(env_name)); env_name[eq_pos] = '\0'; // Extract value (with line break removed: char env_value[256]; strncpy(env_value, (char*)(line + eq_pos + 1), sizeof(env_value)); if (strlen(env_value) > 0 && env_value[strlen(env_value)-1] == '\n') { env_value[strlen(env_value)-1] = '\0'; if (strlen(env_value) > 0 && env_value[strlen(env_value)-1] == '\r') { // Also remove windows line breaks (\r\n) env_value[strlen(env_value)-1] = '\0'; } } // Set value: setenv(env_name, env_value, 1); } } } fclose(env_file_fd); } else { LOGP("Warning: no p4a_env_vars.txt found / failed to open!"); } LOGP("Changing directory to '%s'", env_argument); chdir(env_argument); char *interpreter = setup_symlinks(); #if PY_MAJOR_VERSION < 3 Py_NoSiteFlag=1; #endif #if PY_MAJOR_VERSION >= 3 /* our logging module for android */ PyImport_AppendInittab("androidembed", initandroidembed); #endif LOGP("Preparing to initialize python"); // Set up the python path char paths[256]; char python_bundle_dir[256]; snprintf(python_bundle_dir, 256, "%s/_python_bundle", getenv("ANDROID_UNPACK")); #if PY_MAJOR_VERSION >= 3 #if PY_MINOR_VERSION >= P4A_MIN_VER PyConfig config; PyConfig_InitPythonConfig(&config); config.program_name = L"android_python"; #else Py_SetProgramName(L"android_python"); #endif #else Py_SetProgramName("android_python"); #endif if (dir_exists(python_bundle_dir)) { LOGP("_python_bundle dir exists"); #if PY_MAJOR_VERSION >= 3 #if PY_MINOR_VERSION >= P4A_MIN_VER wchar_t wchar_zip_path[256]; wchar_t wchar_modules_path[256]; swprintf(wchar_zip_path, 256, L"%s/stdlib.zip", python_bundle_dir); swprintf(wchar_modules_path, 256, L"%s/modules", python_bundle_dir); config.module_search_paths_set = 1; PyWideStringList_Append(&config.module_search_paths, wchar_zip_path); PyWideStringList_Append(&config.module_search_paths, wchar_modules_path); #else char paths[512]; snprintf(paths, 512, "%s/stdlib.zip:%s/modules", python_bundle_dir, python_bundle_dir); wchar_t *wchar_paths = Py_DecodeLocale(paths, NULL); Py_SetPath(wchar_paths); #endif #endif LOGP("set wchar paths..."); } else { LOGP("_python_bundle does not exist...this not looks good, all python" " recipes should have this folder, should we expect a crash soon?"); } #if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= P4A_MIN_VER PyStatus status = Py_InitializeFromConfig(&config); if (PyStatus_Exception(status)) { LOGP("Python initialization failed:"); LOGP(status.err_msg); } #else Py_Initialize(); LOGP("Python initialized using legacy Py_Initialize()."); #endif LOGP("Initialized python"); /* < 3.9 requires explicit GIL initialization * 3.9+ PyEval_InitThreads() is deprecated and unnecessary */ #if PY_VERSION_HEX < 0x03090000 LOGP("Initializing threads (required for Python < 3.9)"); PyEval_InitThreads(); #endif #if PY_MAJOR_VERSION < 3 initandroidembed(); #endif PyRun_SimpleString( "import androidembed\n" "androidembed.log('testing python print redirection')" ); /* inject our bootstrap code to redirect python stdin/stdout * replace sys.path with our path */ PyRun_SimpleString("import io, sys, posix\n"); char add_site_packages_dir[256]; if (dir_exists(python_bundle_dir)) { snprintf(add_site_packages_dir, 256, "sys.path.append('%s/site-packages')", python_bundle_dir); PyRun_SimpleString("import sys, os\n" "from os.path import realpath, join, dirname"); char buf_exec[512]; char buf_argv[512]; snprintf(buf_exec, sizeof(buf_exec), "sys.executable = '%s'\n", interpreter); snprintf(buf_argv, sizeof(buf_argv), "sys.argv = ['%s']\n", interpreter); PyRun_SimpleString(buf_exec); PyRun_SimpleString(buf_argv); PyRun_SimpleString(add_site_packages_dir); /* "sys.path.append(join(dirname(realpath(__file__)), 'site-packages'))") */ PyRun_SimpleString("sys.path = ['.'] + sys.path"); PyRun_SimpleString("os.environ['PYTHONPATH'] = ':'.join(sys.path)"); } PyRun_SimpleString( "class LogFile(io.IOBase):\n" " def __init__(self):\n" " self.__buffer = ''\n" " def readable(self):\n" " return False\n" " def writable(self):\n" " return True\n" " def write(self, s):\n" " s = self.__buffer + s\n" " lines = s.split('\\n')\n" " for l in lines[:-1]:\n" " androidembed.log(l.replace('\\x00', ''))\n" " self.__buffer = lines[-1]\n" "sys.stdout = sys.stderr = LogFile()\n" "print('Android kivy bootstrap done. __name__ is', __name__)"); #if PY_MAJOR_VERSION < 3 PyRun_SimpleString("import site; print site.getsitepackages()\n"); #endif char *dot = strrchr(env_entrypoint, '.'); char *ext = ".pyc"; if (dot <= 0) { LOGP("Invalid entrypoint, abort."); return -1; } if (strlen(env_entrypoint) > ENTRYPOINT_MAXLEN - 2) { LOGP("Entrypoint path is too long, try increasing ENTRYPOINT_MAXLEN."); return -1; } if (!strcmp(dot, ext)) { if (!file_exists(env_entrypoint)) { /* fallback on .py */ strcpy(entrypoint, env_entrypoint); entrypoint[strlen(env_entrypoint) - 1] = '\0'; LOGP(entrypoint); if (!file_exists(entrypoint)) { LOGP("Entrypoint not found (.pyc, fallback on .py), abort"); return -1; } } else { strcpy(entrypoint, env_entrypoint); } } else if (!strcmp(dot, ".py")) { /* if .py is passed, check the pyc version first */ strcpy(entrypoint, env_entrypoint); entrypoint[strlen(env_entrypoint) + 1] = '\0'; entrypoint[strlen(env_entrypoint)] = 'c'; if (!file_exists(entrypoint)) { /* fallback on pure python version */ if (!file_exists(env_entrypoint)) { LOGP("Entrypoint not found (.py), abort."); return -1; } strcpy(entrypoint, env_entrypoint); } } else { LOGP("Entrypoint have an invalid extension (must be .py or .pyc), abort."); return -1; } // LOGP("Entrypoint is:"); // LOGP(entrypoint); fd = fopen(entrypoint, "r"); if (fd == NULL) { LOGP("Open the entrypoint failed"); LOGP(entrypoint); return -1; } /* run python ! */ ret = PyRun_SimpleFile(fd, entrypoint); fclose(fd); if (PyErr_Occurred() != NULL) { ret = 1; PyErr_Print(); /* This exits with the right code if SystemExit. */ PyObject *f = PySys_GetObject("stdout"); if (PyFile_WriteString("\n", f)) PyErr_Clear(); } LOGP("Python for android ended."); #if PY_MAJOR_VERSION < 3 Py_Finalize(); LOGP("Unexpectedly reached Py_FinalizeEx(), but was successful."); #else if (Py_FinalizeEx() != 0) { // properly check success on Python 3 LOGP("Unexpectedly reached Py_FinalizeEx(), and got error!"); } #endif exit(ret); return ret; } JNIEXPORT void JNICALL Java_org_kivy_android_PythonService_nativeStart( JNIEnv *env, jobject thiz, jstring j_android_private, jstring j_android_argument, jstring j_service_entrypoint, jstring j_python_name, jstring j_python_home, jstring j_python_path, jstring j_arg) { jboolean iscopy; const char *android_private = (*env)->GetStringUTFChars(env, j_android_private, &iscopy); const char *android_argument = (*env)->GetStringUTFChars(env, j_android_argument, &iscopy); const char *service_entrypoint = (*env)->GetStringUTFChars(env, j_service_entrypoint, &iscopy); const char *python_name = (*env)->GetStringUTFChars(env, j_python_name, &iscopy); const char *python_home = (*env)->GetStringUTFChars(env, j_python_home, &iscopy); const char *python_path = (*env)->GetStringUTFChars(env, j_python_path, &iscopy); const char *arg = (*env)->GetStringUTFChars(env, j_arg, &iscopy); setenv("ANDROID_PRIVATE", android_private, 1); setenv("ANDROID_ARGUMENT", android_argument, 1); setenv("ANDROID_APP_PATH", android_argument, 1); setenv("ANDROID_ENTRYPOINT", service_entrypoint, 1); setenv("PYTHONOPTIMIZE", "2", 1); setenv("PYTHON_NAME", python_name, 1); setenv("PYTHONHOME", python_home, 1); setenv("PYTHONPATH", python_path, 1); setenv("PYTHON_SERVICE_ARGUMENT", arg, 1); setenv("P4A_BOOTSTRAP", bootstrap_name, 1); char *argv[] = {"."}; /* ANDROID_ARGUMENT points to service subdir, * so main() will run main.py from this dir */ main(1, argv); } #if defined(BOOTSTRAP_NAME_WEBVIEW) || defined(BOOTSTRAP_NAME_SERVICEONLY) // Webview and service_only uses some more functions: void Java_org_kivy_android_PythonActivity_nativeSetenv( JNIEnv* env, jclass cls, jstring name, jstring value) //JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetenv)( // JNIEnv* env, jclass cls, // jstring name, jstring value) { const char *utfname = (*env)->GetStringUTFChars(env, name, NULL); const char *utfvalue = (*env)->GetStringUTFChars(env, value, NULL); setenv(utfname, utfvalue, 1); (*env)->ReleaseStringUTFChars(env, name, utfname); (*env)->ReleaseStringUTFChars(env, value, utfvalue); } void Java_org_kivy_android_PythonActivity_nativeInit(JNIEnv* env, jclass cls, jobject obj) { /* This nativeInit follows SDL2 */ /* This interface could expand with ABI negotiation, callbacks, etc. */ /* SDL_Android_Init(env, cls); */ /* SDL_SetMainReady(); */ /* Run the application code! */ int status; char *argv[2]; argv[0] = "Python_app"; argv[1] = NULL; /* status = SDL_main(1, argv); */ main(1, argv); /* Do not issue an exit or the whole application will terminate instead of just the SDL thread */ /* exit(status); */ } #endif #endif ================================================ FILE: pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/Octal.java ================================================ /** * Copyright 2012 Kamran Zafar * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.kamranzafar.jtar; /** * @author Kamran Zafar * */ public class Octal { /** * Parse an octal string from a header buffer. This is used for the file * permission mode value. * * @param header * The header buffer from which to parse. * @param offset * The offset into the buffer from which to parse. * @param length * The number of header bytes to parse. * * @return The long value of the octal string. */ public static long parseOctal(byte[] header, int offset, int length) { long result = 0; boolean stillPadding = true; int end = offset + length; for (int i = offset; i < end; ++i) { if (header[i] == 0) break; if (header[i] == (byte) ' ' || header[i] == '0') { if (stillPadding) continue; if (header[i] == (byte) ' ') break; } stillPadding = false; result = ( result << 3 ) + ( header[i] - '0' ); } return result; } /** * Parse an octal integer from a header buffer. * * @param value * @param buf * The header buffer from which to parse. * @param offset * The offset into the buffer from which to parse. * @param length * The number of header bytes to parse. * * @return The integer value of the octal bytes. */ public static int getOctalBytes(long value, byte[] buf, int offset, int length) { int idx = length - 1; buf[offset + idx] = 0; --idx; buf[offset + idx] = (byte) ' '; --idx; if (value == 0) { buf[offset + idx] = (byte) '0'; --idx; } else { for (long val = value; idx >= 0 && val > 0; --idx) { buf[offset + idx] = (byte) ( (byte) '0' + (byte) ( val & 7 ) ); val = val >> 3; } } for (; idx >= 0; --idx) { buf[offset + idx] = (byte) ' '; } return offset + length; } /** * Parse the checksum octal integer from a header buffer. * * @param value * @param buf * The header buffer from which to parse. * @param offset * The offset into the buffer from which to parse. * @param length * The number of header bytes to parse. * @return The integer value of the entry's checksum. */ public static int getCheckSumOctalBytes(long value, byte[] buf, int offset, int length) { getOctalBytes( value, buf, offset, length ); buf[offset + length - 1] = (byte) ' '; buf[offset + length - 2] = 0; return offset + length; } /** * Parse an octal long integer from a header buffer. * * @param value * @param buf * The header buffer from which to parse. * @param offset * The offset into the buffer from which to parse. * @param length * The number of header bytes to parse. * * @return The long value of the octal bytes. */ public static int getLongOctalBytes(long value, byte[] buf, int offset, int length) { byte[] temp = new byte[length + 1]; getOctalBytes( value, temp, 0, length + 1 ); System.arraycopy( temp, 0, buf, offset, length ); return offset + length; } } ================================================ FILE: pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarConstants.java ================================================ /** * Copyright 2012 Kamran Zafar * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.kamranzafar.jtar; /** * @author Kamran Zafar * */ public class TarConstants { public static final int EOF_BLOCK = 1024; public static final int DATA_BLOCK = 512; public static final int HEADER_BLOCK = 512; } ================================================ FILE: pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarEntry.java ================================================ /** * Copyright 2012 Kamran Zafar * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.kamranzafar.jtar; import java.io.File; import java.util.Date; /** * @author Kamran Zafar * */ public class TarEntry { protected File file; protected TarHeader header; private TarEntry() { this.file = null; header = new TarHeader(); } public TarEntry(File file, String entryName) { this(); this.file = file; this.extractTarHeader(entryName); } public TarEntry(byte[] headerBuf) { this(); this.parseTarHeader(headerBuf); } /** * Constructor to create an entry from an existing TarHeader object. * * This method is useful to add new entries programmatically (e.g. for * adding files or directories that do not exist in the file system). * * @param header * */ public TarEntry(TarHeader header) { this.file = null; this.header = header; } public boolean equals(TarEntry it) { return header.name.toString().equals(it.header.name.toString()); } public boolean isDescendent(TarEntry desc) { return desc.header.name.toString().startsWith(header.name.toString()); } public TarHeader getHeader() { return header; } public String getName() { String name = header.name.toString(); if (header.namePrefix != null && !header.namePrefix.toString().equals("")) { name = header.namePrefix.toString() + "/" + name; } return name; } public void setName(String name) { header.name = new StringBuffer(name); } public int getUserId() { return header.userId; } public void setUserId(int userId) { header.userId = userId; } public int getGroupId() { return header.groupId; } public void setGroupId(int groupId) { header.groupId = groupId; } public String getUserName() { return header.userName.toString(); } public void setUserName(String userName) { header.userName = new StringBuffer(userName); } public String getGroupName() { return header.groupName.toString(); } public void setGroupName(String groupName) { header.groupName = new StringBuffer(groupName); } public void setIds(int userId, int groupId) { this.setUserId(userId); this.setGroupId(groupId); } public void setModTime(long time) { header.modTime = time / 1000; } public void setModTime(Date time) { header.modTime = time.getTime() / 1000; } public Date getModTime() { return new Date(header.modTime * 1000); } public File getFile() { return this.file; } public long getSize() { return header.size; } public void setSize(long size) { header.size = size; } /** * Checks if the org.kamrazafar.jtar entry is a directory * * @return */ public boolean isDirectory() { if (this.file != null) return this.file.isDirectory(); if (header != null) { if (header.linkFlag == TarHeader.LF_DIR) return true; if (header.name.toString().endsWith("/")) return true; } return false; } /** * Extract header from File * * @param entryName */ public void extractTarHeader(String entryName) { header = TarHeader.createHeader(entryName, file.length(), file.lastModified() / 1000, file.isDirectory()); } /** * Calculate checksum * * @param buf * @return */ public long computeCheckSum(byte[] buf) { long sum = 0; for (int i = 0; i < buf.length; ++i) { sum += 255 & buf[i]; } return sum; } /** * Writes the header to the byte buffer * * @param outbuf */ public void writeEntryHeader(byte[] outbuf) { int offset = 0; offset = TarHeader.getNameBytes(header.name, outbuf, offset, TarHeader.NAMELEN); offset = Octal.getOctalBytes(header.mode, outbuf, offset, TarHeader.MODELEN); offset = Octal.getOctalBytes(header.userId, outbuf, offset, TarHeader.UIDLEN); offset = Octal.getOctalBytes(header.groupId, outbuf, offset, TarHeader.GIDLEN); long size = header.size; offset = Octal.getLongOctalBytes(size, outbuf, offset, TarHeader.SIZELEN); offset = Octal.getLongOctalBytes(header.modTime, outbuf, offset, TarHeader.MODTIMELEN); int csOffset = offset; for (int c = 0; c < TarHeader.CHKSUMLEN; ++c) outbuf[offset++] = (byte) ' '; outbuf[offset++] = header.linkFlag; offset = TarHeader.getNameBytes(header.linkName, outbuf, offset, TarHeader.NAMELEN); offset = TarHeader.getNameBytes(header.magic, outbuf, offset, TarHeader.USTAR_MAGICLEN); offset = TarHeader.getNameBytes(header.userName, outbuf, offset, TarHeader.USTAR_USER_NAMELEN); offset = TarHeader.getNameBytes(header.groupName, outbuf, offset, TarHeader.USTAR_GROUP_NAMELEN); offset = Octal.getOctalBytes(header.devMajor, outbuf, offset, TarHeader.USTAR_DEVLEN); offset = Octal.getOctalBytes(header.devMinor, outbuf, offset, TarHeader.USTAR_DEVLEN); offset = TarHeader.getNameBytes(header.namePrefix, outbuf, offset, TarHeader.USTAR_FILENAME_PREFIX); for (; offset < outbuf.length;) outbuf[offset++] = 0; long checkSum = this.computeCheckSum(outbuf); Octal.getCheckSumOctalBytes(checkSum, outbuf, csOffset, TarHeader.CHKSUMLEN); } /** * Parses the tar header to the byte buffer * * @param header * @param bh */ public void parseTarHeader(byte[] bh) { int offset = 0; header.name = TarHeader.parseName(bh, offset, TarHeader.NAMELEN); offset += TarHeader.NAMELEN; header.mode = (int) Octal.parseOctal(bh, offset, TarHeader.MODELEN); offset += TarHeader.MODELEN; header.userId = (int) Octal.parseOctal(bh, offset, TarHeader.UIDLEN); offset += TarHeader.UIDLEN; header.groupId = (int) Octal.parseOctal(bh, offset, TarHeader.GIDLEN); offset += TarHeader.GIDLEN; header.size = Octal.parseOctal(bh, offset, TarHeader.SIZELEN); offset += TarHeader.SIZELEN; header.modTime = Octal.parseOctal(bh, offset, TarHeader.MODTIMELEN); offset += TarHeader.MODTIMELEN; header.checkSum = (int) Octal.parseOctal(bh, offset, TarHeader.CHKSUMLEN); offset += TarHeader.CHKSUMLEN; header.linkFlag = bh[offset++]; header.linkName = TarHeader.parseName(bh, offset, TarHeader.NAMELEN); offset += TarHeader.NAMELEN; header.magic = TarHeader.parseName(bh, offset, TarHeader.USTAR_MAGICLEN); offset += TarHeader.USTAR_MAGICLEN; header.userName = TarHeader.parseName(bh, offset, TarHeader.USTAR_USER_NAMELEN); offset += TarHeader.USTAR_USER_NAMELEN; header.groupName = TarHeader.parseName(bh, offset, TarHeader.USTAR_GROUP_NAMELEN); offset += TarHeader.USTAR_GROUP_NAMELEN; header.devMajor = (int) Octal.parseOctal(bh, offset, TarHeader.USTAR_DEVLEN); offset += TarHeader.USTAR_DEVLEN; header.devMinor = (int) Octal.parseOctal(bh, offset, TarHeader.USTAR_DEVLEN); offset += TarHeader.USTAR_DEVLEN; header.namePrefix = TarHeader.parseName(bh, offset, TarHeader.USTAR_FILENAME_PREFIX); } } ================================================ FILE: pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarHeader.java ================================================ /** * Copyright 2012 Kamran Zafar * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.kamranzafar.jtar; import java.io.File; /** * Header * *
 * Offset  Size     Field
 * 0       100      File name
 * 100     8        File mode
 * 108     8        Owner's numeric user ID
 * 116     8        Group's numeric user ID
 * 124     12       File size in bytes
 * 136     12       Last modification time in numeric Unix time format
 * 148     8        Checksum for header block
 * 156     1        Link indicator (file type)
 * 157     100      Name of linked file
 * 
* * * File Types * *
 * Value        Meaning
 * '0'          Normal file
 * (ASCII NUL)  Normal file (now obsolete)
 * '1'          Hard link
 * '2'          Symbolic link
 * '3'          Character special
 * '4'          Block special
 * '5'          Directory
 * '6'          FIFO
 * '7'          Contiguous
 * 
* * * * Ustar header * *
 * Offset  Size    Field
 * 257     6       UStar indicator "ustar"
 * 263     2       UStar version "00"
 * 265     32      Owner user name
 * 297     32      Owner group name
 * 329     8       Device major number
 * 337     8       Device minor number
 * 345     155     Filename prefix
 * 
*/ public class TarHeader { /* * Header */ public static final int NAMELEN = 100; public static final int MODELEN = 8; public static final int UIDLEN = 8; public static final int GIDLEN = 8; public static final int SIZELEN = 12; public static final int MODTIMELEN = 12; public static final int CHKSUMLEN = 8; public static final byte LF_OLDNORM = 0; /* * File Types */ public static final byte LF_NORMAL = (byte) '0'; public static final byte LF_LINK = (byte) '1'; public static final byte LF_SYMLINK = (byte) '2'; public static final byte LF_CHR = (byte) '3'; public static final byte LF_BLK = (byte) '4'; public static final byte LF_DIR = (byte) '5'; public static final byte LF_FIFO = (byte) '6'; public static final byte LF_CONTIG = (byte) '7'; /* * Ustar header */ public static final String USTAR_MAGIC = "ustar"; // POSIX public static final int USTAR_MAGICLEN = 8; public static final int USTAR_USER_NAMELEN = 32; public static final int USTAR_GROUP_NAMELEN = 32; public static final int USTAR_DEVLEN = 8; public static final int USTAR_FILENAME_PREFIX = 155; // Header values public StringBuffer name; public int mode; public int userId; public int groupId; public long size; public long modTime; public int checkSum; public byte linkFlag; public StringBuffer linkName; public StringBuffer magic; // ustar indicator and version public StringBuffer userName; public StringBuffer groupName; public int devMajor; public int devMinor; public StringBuffer namePrefix; public TarHeader() { this.magic = new StringBuffer(TarHeader.USTAR_MAGIC); this.name = new StringBuffer(); this.linkName = new StringBuffer(); String user = System.getProperty("user.name", ""); if (user.length() > 31) user = user.substring(0, 31); this.userId = 0; this.groupId = 0; this.userName = new StringBuffer(user); this.groupName = new StringBuffer(""); this.namePrefix = new StringBuffer(); } /** * Parse an entry name from a header buffer. * * @param name * @param header * The header buffer from which to parse. * @param offset * The offset into the buffer from which to parse. * @param length * The number of header bytes to parse. * @return The header's entry name. */ public static StringBuffer parseName(byte[] header, int offset, int length) { StringBuffer result = new StringBuffer(length); int end = offset + length; for (int i = offset; i < end; ++i) { if (header[i] == 0) break; result.append((char) header[i]); } return result; } /** * Determine the number of bytes in an entry name. * * @param name * @param header * The header buffer from which to parse. * @param offset * The offset into the buffer from which to parse. * @param length * The number of header bytes to parse. * @return The number of bytes in a header's entry name. */ public static int getNameBytes(StringBuffer name, byte[] buf, int offset, int length) { int i; for (i = 0; i < length && i < name.length(); ++i) { buf[offset + i] = (byte) name.charAt(i); } for (; i < length; ++i) { buf[offset + i] = 0; } return offset + length; } /** * Creates a new header for a file/directory entry. * * * @param name * File name * @param size * File size in bytes * @param modTime * Last modification time in numeric Unix time format * @param dir * Is directory * * @return */ public static TarHeader createHeader(String entryName, long size, long modTime, boolean dir) { String name = entryName; name = TarUtils.trim(name.replace(File.separatorChar, '/'), '/'); TarHeader header = new TarHeader(); header.linkName = new StringBuffer(""); if (name.length() > 100) { header.namePrefix = new StringBuffer(name.substring(0, name.lastIndexOf('/'))); header.name = new StringBuffer(name.substring(name.lastIndexOf('/') + 1)); } else { header.name = new StringBuffer(name); } if (dir) { header.mode = 040755; header.linkFlag = TarHeader.LF_DIR; if (header.name.charAt(header.name.length() - 1) != '/') { header.name.append("/"); } header.size = 0; } else { header.mode = 0100644; header.linkFlag = TarHeader.LF_NORMAL; header.size = size; } header.modTime = modTime; header.checkSum = 0; header.devMajor = 0; header.devMinor = 0; return header; } } ================================================ FILE: pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarInputStream.java ================================================ /** * Copyright 2012 Kamran Zafar * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.kamranzafar.jtar; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; /** * @author Kamran Zafar * */ public class TarInputStream extends FilterInputStream { private static final int SKIP_BUFFER_SIZE = 2048; private TarEntry currentEntry; private long currentFileSize; private long bytesRead; private boolean defaultSkip = false; public TarInputStream(InputStream in) { super(in); currentFileSize = 0; bytesRead = 0; } @Override public boolean markSupported() { return false; } /** * Not supported * */ @Override public synchronized void mark(int readlimit) { } /** * Not supported * */ @Override public synchronized void reset() throws IOException { throw new IOException("mark/reset not supported"); } /** * Read a byte * * @see java.io.FilterInputStream#read() */ @Override public int read() throws IOException { byte[] buf = new byte[1]; int res = this.read(buf, 0, 1); if (res != -1) { return 0xFF & buf[0]; } return res; } /** * Checks if the bytes being read exceed the entry size and adjusts the byte * array length. Updates the byte counters * * * @see java.io.FilterInputStream#read(byte[], int, int) */ @Override public int read(byte[] b, int off, int len) throws IOException { if (currentEntry != null) { if (currentFileSize == currentEntry.getSize()) { return -1; } else if ((currentEntry.getSize() - currentFileSize) < len) { len = (int) (currentEntry.getSize() - currentFileSize); } } int br = super.read(b, off, len); if (br != -1) { if (currentEntry != null) { currentFileSize += br; } bytesRead += br; } return br; } /** * Returns the next entry in the tar file * * @return TarEntry * @throws IOException */ public TarEntry getNextEntry() throws IOException { closeCurrentEntry(); byte[] header = new byte[TarConstants.HEADER_BLOCK]; byte[] theader = new byte[TarConstants.HEADER_BLOCK]; int tr = 0; // Read full header while (tr < TarConstants.HEADER_BLOCK) { int res = read(theader, 0, TarConstants.HEADER_BLOCK - tr); if (res < 0) { break; } System.arraycopy(theader, 0, header, tr, res); tr += res; } // Check if record is null boolean eof = true; for (byte b : header) { if (b != 0) { eof = false; break; } } if (!eof) { currentEntry = new TarEntry(header); } return currentEntry; } /** * Returns the current offset (in bytes) from the beginning of the stream. * This can be used to find out at which point in a tar file an entry's content begins, for instance. */ public long getCurrentOffset() { return bytesRead; } /** * Closes the current tar entry * * @throws IOException */ protected void closeCurrentEntry() throws IOException { if (currentEntry != null) { if (currentEntry.getSize() > currentFileSize) { // Not fully read, skip rest of the bytes long bs = 0; while (bs < currentEntry.getSize() - currentFileSize) { long res = skip(currentEntry.getSize() - currentFileSize - bs); if (res == 0 && currentEntry.getSize() - currentFileSize > 0) { // I suspect file corruption throw new IOException("Possible tar file corruption"); } bs += res; } } currentEntry = null; currentFileSize = 0L; skipPad(); } } /** * Skips the pad at the end of each tar entry file content * * @throws IOException */ protected void skipPad() throws IOException { if (bytesRead > 0) { int extra = (int) (bytesRead % TarConstants.DATA_BLOCK); if (extra > 0) { long bs = 0; while (bs < TarConstants.DATA_BLOCK - extra) { long res = skip(TarConstants.DATA_BLOCK - extra - bs); bs += res; } } } } /** * Skips 'n' bytes on the InputStream
* Overrides default implementation of skip * */ @Override public long skip(long n) throws IOException { if (defaultSkip) { // use skip method of parent stream // may not work if skip not implemented by parent long bs = super.skip(n); bytesRead += bs; return bs; } if (n <= 0) { return 0; } long left = n; byte[] sBuff = new byte[SKIP_BUFFER_SIZE]; while (left > 0) { int res = read(sBuff, 0, (int) (left < SKIP_BUFFER_SIZE ? left : SKIP_BUFFER_SIZE)); if (res < 0) { break; } left -= res; } return n - left; } public boolean isDefaultSkip() { return defaultSkip; } public void setDefaultSkip(boolean defaultSkip) { this.defaultSkip = defaultSkip; } } ================================================ FILE: pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarOutputStream.java ================================================ /** * Copyright 2012 Kamran Zafar * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.kamranzafar.jtar; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.RandomAccessFile; /** * @author Kamran Zafar * */ public class TarOutputStream extends OutputStream { private final OutputStream out; private long bytesWritten; private long currentFileSize; private TarEntry currentEntry; public TarOutputStream(OutputStream out) { this.out = out; bytesWritten = 0; currentFileSize = 0; } public TarOutputStream(final File fout) throws FileNotFoundException { this.out = new BufferedOutputStream(new FileOutputStream(fout)); bytesWritten = 0; currentFileSize = 0; } /** * Opens a file for writing. */ public TarOutputStream(final File fout, final boolean append) throws IOException { @SuppressWarnings("resource") RandomAccessFile raf = new RandomAccessFile(fout, "rw"); final long fileSize = fout.length(); if (append && fileSize > TarConstants.EOF_BLOCK) { raf.seek(fileSize - TarConstants.EOF_BLOCK); } out = new BufferedOutputStream(new FileOutputStream(raf.getFD())); } /** * Appends the EOF record and closes the stream * * @see java.io.FilterOutputStream#close() */ @Override public void close() throws IOException { closeCurrentEntry(); write( new byte[TarConstants.EOF_BLOCK] ); out.close(); } /** * Writes a byte to the stream and updates byte counters * * @see java.io.FilterOutputStream#write(int) */ @Override public void write(int b) throws IOException { out.write( b ); bytesWritten += 1; if (currentEntry != null) { currentFileSize += 1; } } /** * Checks if the bytes being written exceed the current entry size. * * @see java.io.FilterOutputStream#write(byte[], int, int) */ @Override public void write(byte[] b, int off, int len) throws IOException { if (currentEntry != null && !currentEntry.isDirectory()) { if (currentEntry.getSize() < currentFileSize + len) { throw new IOException( "The current entry[" + currentEntry.getName() + "] size[" + currentEntry.getSize() + "] is smaller than the bytes[" + ( currentFileSize + len ) + "] being written." ); } } out.write( b, off, len ); bytesWritten += len; if (currentEntry != null) { currentFileSize += len; } } /** * Writes the next tar entry header on the stream * * @param entry * @throws IOException */ public void putNextEntry(TarEntry entry) throws IOException { closeCurrentEntry(); byte[] header = new byte[TarConstants.HEADER_BLOCK]; entry.writeEntryHeader( header ); write( header ); currentEntry = entry; } /** * Closes the current tar entry * * @throws IOException */ protected void closeCurrentEntry() throws IOException { if (currentEntry != null) { if (currentEntry.getSize() > currentFileSize) { throw new IOException( "The current entry[" + currentEntry.getName() + "] of size[" + currentEntry.getSize() + "] has not been fully written." ); } currentEntry = null; currentFileSize = 0; pad(); } } /** * Pads the last content block * * @throws IOException */ protected void pad() throws IOException { if (bytesWritten > 0) { int extra = (int) ( bytesWritten % TarConstants.DATA_BLOCK ); if (extra > 0) { write( new byte[TarConstants.DATA_BLOCK - extra] ); } } } } ================================================ FILE: pythonforandroid/bootstraps/common/build/src/main/java/org/kamranzafar/jtar/TarUtils.java ================================================ /** * Copyright 2012 Kamran Zafar * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.kamranzafar.jtar; import java.io.File; /** * @author Kamran * */ public class TarUtils { /** * Determines the tar file size of the given folder/file path * * @param path * @return */ public static long calculateTarSize(File path) { return tarSize(path) + TarConstants.EOF_BLOCK; } private static long tarSize(File dir) { long size = 0; if (dir.isFile()) { return entrySize(dir.length()); } else { File[] subFiles = dir.listFiles(); if (subFiles != null && subFiles.length > 0) { for (File file : subFiles) { if (file.isFile()) { size += entrySize(file.length()); } else { size += tarSize(file); } } } else { // Empty folder header return TarConstants.HEADER_BLOCK; } } return size; } private static long entrySize(long fileSize) { long size = 0; size += TarConstants.HEADER_BLOCK; // Header size += fileSize; // File size long extra = size % TarConstants.DATA_BLOCK; if (extra > 0) { size += (TarConstants.DATA_BLOCK - extra); // pad } return size; } public static String trim(String s, char c) { StringBuffer tmp = new StringBuffer(s); for (int i = 0; i < tmp.length(); i++) { if (tmp.charAt(i) != c) { break; } else { tmp.deleteCharAt(i); } } for (int i = tmp.length() - 1; i >= 0; i--) { if (tmp.charAt(i) != c) { break; } else { tmp.deleteCharAt(i); } } return tmp.toString(); } } ================================================ FILE: pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/GenericBroadcastReceiver.java ================================================ package org.kivy.android; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; public class GenericBroadcastReceiver extends BroadcastReceiver { GenericBroadcastReceiverCallback listener; public GenericBroadcastReceiver(GenericBroadcastReceiverCallback listener) { super(); this.listener = listener; } public void onReceive(Context context, Intent intent) { this.listener.onReceive(context, intent); } } ================================================ FILE: pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/GenericBroadcastReceiverCallback.java ================================================ package org.kivy.android; import android.content.Context; import android.content.Intent; public interface GenericBroadcastReceiverCallback { void onReceive(Context context, Intent intent); } ; ================================================ FILE: pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonService.java ================================================ package org.kivy.android; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.Process; import android.util.Log; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class PythonService extends Service implements Runnable { // Thread for Python code private Thread pythonThread = null; // Python environment variables private String androidPrivate; private String androidArgument; private String pythonName; private String pythonHome; private String pythonPath; private String serviceEntrypoint; // Argument to pass to Python code, private String pythonServiceArgument; public static PythonService mService = null; private Intent startIntent = null; private boolean autoRestartService = false; public void setAutoRestartService(boolean restart) { autoRestartService = restart; } public int startType() { return START_NOT_STICKY; } @Override public IBinder onBind(Intent arg0) { return null; } @Override public void onCreate() { super.onCreate(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (pythonThread != null) { Log.v("python service", "service exists, do not start again"); return startType(); } // intent is null if OS restarts a STICKY service if (intent == null) { Context context = getApplicationContext(); intent = getThisDefaultIntent(context, ""); } startIntent = intent; Bundle extras = intent.getExtras(); androidPrivate = extras.getString("androidPrivate"); androidArgument = extras.getString("androidArgument"); serviceEntrypoint = extras.getString("serviceEntrypoint"); pythonName = extras.getString("pythonName"); pythonHome = extras.getString("pythonHome"); pythonPath = extras.getString("pythonPath"); boolean serviceStartAsForeground = (extras.getString("serviceStartAsForeground").equals("true")); pythonServiceArgument = extras.getString("pythonServiceArgument"); pythonThread = new Thread(this); pythonThread.start(); if (serviceStartAsForeground) { doStartForeground(extras); } return startType(); } protected int getServiceId() { return 1; } protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) { return null; } protected void doStartForeground(Bundle extras) { String serviceTitle = extras.getString("serviceTitle"); String smallIconName = extras.getString("smallIconName"); String contentTitle = extras.getString("contentTitle"); String contentText = extras.getString("contentText"); Notification notification; Context context = getApplicationContext(); Intent contextIntent = new Intent(context, PythonActivity.class); PendingIntent pIntent = PendingIntent.getActivity( context, 0, contextIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); // Unspecified icon uses default. int smallIconId = context.getApplicationInfo().icon; if (smallIconName != null) { if (!smallIconName.equals("")) { int resId = getResources().getIdentifier(smallIconName, "mipmap", getPackageName()); if (resId == 0) { resId = getResources() .getIdentifier(smallIconName, "drawable", getPackageName()); } if (resId != 0) { smallIconId = resId; } } } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // This constructor is deprecated notification = new Notification(smallIconId, serviceTitle, System.currentTimeMillis()); try { // prevent using NotificationCompat, this saves 100kb on apk Method func = notification .getClass() .getMethod( "setLatestEventInfo", Context.class, CharSequence.class, CharSequence.class, PendingIntent.class); func.invoke(notification, context, contentTitle, contentText, pIntent); } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { } } else { // for android 8+ we need to create our own channel // https://stackoverflow.com/questions/47531742/startforeground-fail-after-upgrade-to-android-8-1 String NOTIFICATION_CHANNEL_ID = "org.kivy.p4a" + getServiceId(); String channelName = "Background Service" + getServiceId(); NotificationChannel chan = new NotificationChannel( NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_NONE); chan.setLightColor(Color.BLUE); chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); manager.createNotificationChannel(chan); Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID); builder.setContentTitle(contentTitle); builder.setContentText(contentText); builder.setContentIntent(pIntent); builder.setSmallIcon(smallIconId); notification = builder.build(); } startForeground(getServiceId(), notification); } @Override public void onDestroy() { super.onDestroy(); pythonThread = null; if (autoRestartService && startIntent != null) { Log.v("python service", "service restart requested"); startService(startIntent); } Process.killProcess(Process.myPid()); } /** * Stops the task gracefully when killed. Calling stopSelf() will trigger a onDestroy() call * from the system. */ @Override public void onTaskRemoved(Intent rootIntent) { super.onTaskRemoved(rootIntent); // sticky service runtime/restart is managed by the OS. leave it running when app is closed if (startType() != START_STICKY) { stopSelf(); } } @Override public void run() { String app_root = getFilesDir().getAbsolutePath() + "/app"; File app_root_file = new File(app_root); PythonUtil.loadLibraries(app_root_file, new File(getApplicationInfo().nativeLibraryDir)); this.mService = this; nativeStart( androidPrivate, androidArgument, serviceEntrypoint, pythonName, pythonHome, pythonPath, pythonServiceArgument); stopSelf(); } // Native part public static native void nativeStart( String androidPrivate, String androidArgument, String serviceEntrypoint, String pythonName, String pythonHome, String pythonPath, String pythonServiceArgument); } ================================================ FILE: pythonforandroid/bootstraps/common/build/src/main/java/org/kivy/android/PythonUtil.java ================================================ package org.kivy.android; import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.util.Log; import android.widget.Toast; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.regex.Pattern; import org.renpy.android.AssetExtract; public class PythonUtil { private static final String TAG = "pythonutil"; protected static void addLibraryIfExists( ArrayList libsList, String pattern, File libsDir) { // pattern should be the name of the lib file, without the // preceding "lib" or suffix ".so", for instance "ssl.*" will // match files of the form "libssl.*.so". File[] files = libsDir.listFiles(); pattern = "lib" + pattern + "\\.so"; Pattern p = Pattern.compile(pattern); for (int i = 0; i < files.length; ++i) { File file = files[i]; String name = file.getName(); Log.v(TAG, "Checking pattern " + pattern + " against " + name); if (p.matcher(name).matches()) { Log.v(TAG, "Pattern " + pattern + " matched file " + name); libsList.add(name.substring(3, name.length() - 3)); } } } protected static ArrayList getLibraries(File libsDir) { ArrayList libsList = new ArrayList<>(); String[] libNames = { "sqlite3", "ffi", "png16", "ssl.*", "crypto.*", "SDL2", "SDL2_image", "SDL2_mixer", "SDL2_ttf", "SDL3", "SDL3_image", "SDL3_mixer", "SDL3_ttf" }; for (String name : libNames) { addLibraryIfExists(libsList, name, libsDir); } for (int v = 14; v >= 8; v--) { libsList.add("python3." + v); } libsList.add("main"); return libsList; } public static void loadLibraries(File filesDir, File libsDir) { boolean foundPython = false; for (String lib : getLibraries(libsDir)) { if (lib.startsWith("python") && foundPython) { continue; } Log.v(TAG, "Loading library: " + lib); try { System.loadLibrary(lib); if (lib.startsWith("python")) { foundPython = true; } } catch (UnsatisfiedLinkError e) { // If this is the last possible libpython // load, and it has failed, give a more // general error Log.v(TAG, "Library loading error: " + e.getMessage()); if (lib.startsWith("python3.8") && !foundPython) { throw new RuntimeException("Could not load any libpythonXXX.so"); } else if (lib.startsWith("python")) { continue; } else { Log.v(TAG, "An UnsatisfiedLinkError occurred loading " + lib); throw e; } } } Log.v(TAG, "Loaded everything!"); } public static String getAppRoot(Context ctx) { String appRoot = ctx.getFilesDir().getAbsolutePath() + "/app"; return appRoot; } 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); } /** Show an error using a toast. (Only makes sense from non-UI threads.) */ protected static void toastError(final Activity activity, final String msg) { activity.runOnUiThread( new Runnable() { public void run() { Toast.makeText(activity, msg, Toast.LENGTH_LONG).show(); } }); // Wait to show the error. synchronized (activity) { try { activity.wait(1000); } catch (InterruptedException e) { } } } protected static void recursiveDelete(File f) { if (f.isDirectory()) { for (File r : f.listFiles()) { recursiveDelete(r); } } f.delete(); } public static void unpackAsset( Context ctx, final String resource, File target, boolean cleanup_on_version_update) { Log.v(TAG, "Unpacking " + resource + " " + target.getName()); // The version of data in memory and on disk. String dataVersion = getResourceString(ctx, resource + "_version"); String diskVersion = null; Log.v(TAG, "Data version is " + dataVersion); // If no version, no unpacking is necessary. if (dataVersion == null) { return; } // Check the current disk version, if any. String filesDir = target.getAbsolutePath(); String diskVersionFn = filesDir + "/" + resource + ".version"; try { byte buf[] = new byte[64]; InputStream is = new FileInputStream(diskVersionFn); int len = is.read(buf); diskVersion = new String(buf, 0, len); is.close(); } catch (Exception e) { diskVersion = ""; } // If the disk data is out of date, extract it and write the version file. if (!dataVersion.equals(diskVersion)) { Log.v(TAG, "Extracting " + resource + " assets."); if (cleanup_on_version_update) { recursiveDelete(target); } target.mkdirs(); AssetExtract ae = new AssetExtract(ctx); if (!ae.extractTar(resource + ".tar", target.getAbsolutePath(), "private")) { String msg = "Could not extract " + resource + " data."; if (ctx instanceof Activity) { toastError((Activity) ctx, msg); } else { Log.v(TAG, msg); } } try { // Write .nomedia. new File(target, ".nomedia").createNewFile(); // Write version file. FileOutputStream os = new FileOutputStream(diskVersionFn); os.write(dataVersion.getBytes()); os.close(); } catch (Exception e) { Log.w(TAG, e); } } } public static void unpackPyBundle( Context ctx, final String resource, File target, boolean cleanup_on_version_update) { Log.v(TAG, "Unpacking " + resource + " " + target.getName()); // The version of data in memory and on disk. String dataVersion = getResourceString(ctx, "private_version"); String diskVersion = null; Log.v(TAG, "Data version is " + dataVersion); // If no version, no unpacking is necessary. if (dataVersion == null) { return; } // Check the current disk version, if any. String filesDir = target.getAbsolutePath(); String diskVersionFn = filesDir + "/" + "libpybundle" + ".version"; try { byte buf[] = new byte[64]; InputStream is = new FileInputStream(diskVersionFn); int len = is.read(buf); diskVersion = new String(buf, 0, len); is.close(); } catch (Exception e) { diskVersion = ""; } if (!dataVersion.equals(diskVersion)) { // If the disk data is out of date, extract it and write the version file. Log.v(TAG, "Extracting " + resource + " assets."); if (cleanup_on_version_update) { recursiveDelete(target); } target.mkdirs(); AssetExtract ae = new AssetExtract(ctx); if (!ae.extractTar(resource + ".so", target.getAbsolutePath(), "pybundle")) { String msg = "Could not extract " + resource + " data."; if (ctx instanceof Activity) { toastError((Activity) ctx, msg); } else { Log.v(TAG, msg); } } try { // Write version file. FileOutputStream os = new FileOutputStream(diskVersionFn); os.write(dataVersion.getBytes()); os.close(); } catch (Exception e) { Log.w(TAG, e); } } } } ================================================ FILE: pythonforandroid/bootstraps/common/build/src/main/java/org/renpy/android/AssetExtract.java ================================================ // This string is autogenerated by ChangeAppSettings.sh, do not change // spaces amount package org.renpy.android; import android.content.Context; import android.content.res.AssetManager; import android.util.Log; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.zip.GZIPInputStream; import org.kamranzafar.jtar.TarEntry; import org.kamranzafar.jtar.TarInputStream; public class AssetExtract { private AssetManager mAssetManager = null; public AssetExtract(Context context) { mAssetManager = context.getAssets(); } public boolean extractTar(String asset, String target, String method) { byte buf[] = new byte[1024 * 1024]; InputStream assetStream = null; TarInputStream tis = null; try { if (method == "private") { assetStream = mAssetManager.open(asset, AssetManager.ACCESS_STREAMING); } else if (method == "pybundle") { assetStream = new FileInputStream(asset); } tis = new TarInputStream( new BufferedInputStream( new GZIPInputStream(new BufferedInputStream(assetStream, 8192)), 8192)); } catch (IOException e) { Log.e("python", "opening up extract tar", e); return false; } while (true) { TarEntry entry = null; try { entry = tis.getNextEntry(); } catch (IOException e) { Log.e("python", "extracting tar", e); return false; } if (entry == null) { break; } Log.v("python", "extracting " + entry.getName()); if (entry.isDirectory()) { try { new File(target + "/" + entry.getName()).mkdirs(); } catch (SecurityException e) { } ; continue; } OutputStream out = null; String path = target + "/" + entry.getName(); try { out = new BufferedOutputStream(new FileOutputStream(path), 8192); } catch (FileNotFoundException | SecurityException e) { } if (out == null) { Log.e("python", "could not open " + path); return false; } try { while (true) { int len = tis.read(buf); if (len == -1) { break; } out.write(buf, 0, len); } out.flush(); out.close(); } catch (IOException e) { Log.e("python", "extracting zip", e); return false; } } try { tis.close(); assetStream.close(); } catch (IOException e) { // pass } return true; } } ================================================ FILE: pythonforandroid/bootstraps/common/build/src/main/java/org/renpy/android/Hardware.java ================================================ package org.renpy.android; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.wifi.ScanResult; import android.net.wifi.WifiManager; import android.os.Vibrator; import android.util.DisplayMetrics; import android.view.View; import android.view.inputmethod.InputMethodManager; import java.util.List; import org.kivy.android.PythonActivity; /** * Methods that are expected to be called via JNI, to access the device's non-screen hardware. (For * example, the vibration and accelerometer.) */ public class Hardware { // The context. static Context context; static View view; public static final float defaultRv[] = {0f, 0f, 0f}; /** Vibrate for s seconds. */ public static void vibrate(double s) { Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); if (v != null) { v.vibrate((int) (1000 * s)); } } /** Get an Overview of all Hardware Sensors of an Android Device */ public static String getHardwareSensors() { SensorManager sm = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); List allSensors = sm.getSensorList(Sensor.TYPE_ALL); if (allSensors != null) { String resultString = ""; for (Sensor s : allSensors) { resultString += String.format("Name=" + s.getName()); resultString += String.format(",Vendor=" + s.getVendor()); resultString += String.format(",Version=" + s.getVersion()); resultString += String.format(",MaximumRange=" + s.getMaximumRange()); // XXX MinDelay is not in the 2.2 // resultString += String.format(",MinDelay=" + s.getMinDelay()); resultString += String.format(",Power=" + s.getPower()); resultString += String.format(",Type=" + s.getType() + "\n"); } return resultString; } return ""; } /** * Get Access to 3 Axis Hardware Sensors Accelerometer, Orientation and Magnetic Field Sensors */ public static class generic3AxisSensor implements SensorEventListener { private final SensorManager sSensorManager; private final Sensor sSensor; private final int sSensorType; SensorEvent sSensorEvent; public generic3AxisSensor(int sensorType) { sSensorType = sensorType; sSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); sSensor = sSensorManager.getDefaultSensor(sSensorType); } public void onAccuracyChanged(Sensor sensor, int accuracy) {} public void onSensorChanged(SensorEvent event) { sSensorEvent = event; } /** Enable or disable the Sensor by registering/unregistering */ public void changeStatus(boolean enable) { if (enable) { sSensorManager.registerListener(this, sSensor, SensorManager.SENSOR_DELAY_NORMAL); } else { sSensorManager.unregisterListener(this, sSensor); } } /** Read the Sensor */ public float[] readSensor() { if (sSensorEvent != null) { return sSensorEvent.values; } else { return defaultRv; } } } public static generic3AxisSensor accelerometerSensor = null; public static generic3AxisSensor orientationSensor = null; public static generic3AxisSensor magneticFieldSensor = null; /** functions for backward compatibility reasons */ public static void accelerometerEnable(boolean enable) { if (accelerometerSensor == null) accelerometerSensor = new generic3AxisSensor(Sensor.TYPE_ACCELEROMETER); accelerometerSensor.changeStatus(enable); } public static float[] accelerometerReading() { if (accelerometerSensor == null) return defaultRv; return (float[]) accelerometerSensor.readSensor(); } public static void orientationSensorEnable(boolean enable) { if (orientationSensor == null) orientationSensor = new generic3AxisSensor(Sensor.TYPE_ORIENTATION); orientationSensor.changeStatus(enable); } public static float[] orientationSensorReading() { if (orientationSensor == null) return defaultRv; return (float[]) orientationSensor.readSensor(); } public static void magneticFieldSensorEnable(boolean enable) { if (magneticFieldSensor == null) magneticFieldSensor = new generic3AxisSensor(Sensor.TYPE_MAGNETIC_FIELD); magneticFieldSensor.changeStatus(enable); } public static float[] magneticFieldSensorReading() { if (magneticFieldSensor == null) return defaultRv; return (float[]) magneticFieldSensor.readSensor(); } public static DisplayMetrics metrics = new DisplayMetrics(); /** Get display DPI. */ public static int getDPI() { // AND: Shouldn't have to get the metrics like this every time... PythonActivity.mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics); return metrics.densityDpi; } // /** // * Show the soft keyboard. // */ // public static void showKeyboard(int input_type) { // //Log.i("python", "hardware.Java show_keyword " input_type); // InputMethodManager imm = (InputMethodManager) // context.getSystemService(Context.INPUT_METHOD_SERVICE); // SDLSurfaceView vw = (SDLSurfaceView) view; // int inputType = input_type; // if (vw.inputType != inputType){ // vw.inputType = inputType; // imm.restartInput(view); // } // imm.showSoftInput(view, InputMethodManager.SHOW_FORCED); // } /** Hide the soft keyboard. */ public static void hideKeyboard() { InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(view.getWindowToken(), 0); } /** Scan WiFi networks */ static List latestResult; public static void enableWifiScanner() { IntentFilter i = new IntentFilter(); i.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); context.registerReceiver( new BroadcastReceiver() { @Override public void onReceive(Context c, Intent i) { // Code to execute when SCAN_RESULTS_AVAILABLE_ACTION event occurs WifiManager w = (WifiManager) c.getSystemService(Context.WIFI_SERVICE); latestResult = w.getScanResults(); // Returns a of scanResults } }, i); } public static String scanWifi() { // Now you can call this and it should execute the broadcastReceiver's // onReceive() if (latestResult != null) { String latestResultString = ""; for (ScanResult result : latestResult) { latestResultString += String.format("%s\t%s\t%d\n", result.SSID, result.BSSID, result.level); } return latestResultString; } return ""; } /** network state */ public static boolean network_state = false; /** * Check network state directly * *

(only one connection can be active at a given moment, detects all network type) */ public static boolean checkNetwork() { boolean state = false; final ConnectivityManager conMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); final NetworkInfo activeNetwork = conMgr.getActiveNetworkInfo(); if (activeNetwork != null && activeNetwork.isConnected()) { state = true; } else { state = false; } return state; } /** To receive network state changes */ public static void registerNetworkCheck() { IntentFilter i = new IntentFilter(); i.addAction(ConnectivityManager.CONNECTIVITY_ACTION); context.registerReceiver( new BroadcastReceiver() { @Override public void onReceive(Context c, Intent i) { network_state = checkNetwork(); } }, i); } } ================================================ FILE: pythonforandroid/bootstraps/common/build/src/main/java/org/renpy/android/ResourceManager.java ================================================ /** * This class takes care of managing resources for us. In our code, we can't use R, since the name * of the package containing R will change. So this is the next best thing. */ package org.renpy.android; import android.app.Activity; import android.content.res.Resources; import android.util.Log; import android.view.View; public class ResourceManager { private Activity act; private Resources res; public ResourceManager(Activity activity) { act = activity; res = act.getResources(); } public int getIdentifier(String name, String kind) { Log.v("SDL", "getting identifier"); Log.v("SDL", "kind is " + kind + " and name " + name); Log.v("SDL", "result is " + res.getIdentifier(name, kind, act.getPackageName())); return res.getIdentifier(name, kind, act.getPackageName()); } public String getString(String name) { try { Log.v("SDL", "asked to get string " + name); return res.getString(getIdentifier(name, "string")); } catch (Exception e) { Log.v("SDL", "got exception looking for string!"); return null; } } public View inflateView(String name) { int id = getIdentifier(name, "layout"); return act.getLayoutInflater().inflate(id, null); } public View getViewById(View v, String name) { int id = getIdentifier(name, "id"); return v.findViewById(id); } } ================================================ FILE: pythonforandroid/bootstraps/common/build/templates/Service.tmpl.java ================================================ package {{ args.package }}; import android.content.Intent; import android.content.Context; import {{ args.service_class_name }}; public class Service{{ name|capitalize }} extends {{ base_service_class }} { {% if sticky %} @Override public int startType() { return START_STICKY; } {% endif %} @Override protected int getServiceId() { return {{ service_id }}; } static private void _start(Context ctx, String smallIconName, String contentTitle, String contentText, String pythonServiceArgument) { Intent intent = getDefaultIntent(ctx, smallIconName, contentTitle, contentText, pythonServiceArgument); ctx.startService(intent); } static public void start(Context ctx, String pythonServiceArgument) { _start(ctx, "", "{{ args.name }}", "{{ name|capitalize }}", pythonServiceArgument); } static public void start(Context ctx, String smallIconName, String contentTitle, String contentText, String pythonServiceArgument) { _start(ctx, smallIconName, contentTitle, contentText, pythonServiceArgument); } static public Intent getDefaultIntent(Context ctx, String smallIconName, String contentTitle, String contentText, String pythonServiceArgument) { Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); String argument = ctx.getFilesDir().getAbsolutePath() + "/app"; intent.putExtra("androidPrivate", ctx.getFilesDir().getAbsolutePath()); intent.putExtra("androidArgument", argument); intent.putExtra("serviceTitle", "{{ args.name }}"); intent.putExtra("serviceEntrypoint", "{{ entrypoint }}"); intent.putExtra("pythonName", "{{ name }}"); intent.putExtra("serviceStartAsForeground", "{{ foreground|lower }}"); intent.putExtra("pythonHome", argument); intent.putExtra("pythonPath", argument + ":" + argument + "/lib"); intent.putExtra("pythonServiceArgument", pythonServiceArgument); intent.putExtra("smallIconName", smallIconName); intent.putExtra("contentTitle", contentTitle); intent.putExtra("contentText", contentText); return intent; } @Override protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) { return Service{{ name|capitalize }}.getDefaultIntent(ctx, "", "", "", pythonServiceArgument); } static public void stop(Context ctx) { Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); ctx.stopService(intent); } } ================================================ FILE: pythonforandroid/bootstraps/common/build/templates/build.properties ================================================ # This file is used to override default values used by the Ant build system. # # This file must be checked in Version Control Systems, as it is # integral to the build system of your project. # This file is only used by the Ant script. # You can use this to override default values such as # 'source.dir' for the location of your java source folder and # 'out.dir' for the location of your output folder. # You can also use it define how the release builds are signed by declaring # the following properties: # 'key.store' for the location of your keystore and # 'key.alias' for the name of the key to use. # The password will be asked during the build when you use the 'release' target. key.store=${env.P4A_RELEASE_KEYSTORE} key.alias=${env.P4A_RELEASE_KEYALIAS} key.store.password=${env.P4A_RELEASE_KEYSTORE_PASSWD} key.alias.password=${env.P4A_RELEASE_KEYALIAS_PASSWD} ================================================ FILE: pythonforandroid/bootstraps/common/build/templates/build.tmpl.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:8.11.0' } } allprojects { repositories { google() mavenCentral() {%- for repo in args.gradle_repositories %} {{repo}} {%- endfor %} } } {% if is_library %} apply plugin: 'com.android.library' {% else %} apply plugin: 'com.android.application' {% endif %} android { namespace '{{ args.package }}' compileSdkVersion {{ android_api }} buildToolsVersion '{{ build_tools_version }}' defaultConfig { minSdkVersion {{ args.min_sdk_version }} targetSdkVersion {{ android_api }} versionCode {{ args.numeric_version }} versionName '{{ args.version }}' manifestPlaceholders = {{ args.manifest_placeholders}} } packagingOptions { jniLibs { useLegacyPackaging = true } {% if debug_build -%} doNotStrip '**/*.so' {% else %} exclude 'lib/**/gdbserver' exclude 'lib/**/gdb.setup' {%- 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 -%} packagingOptions { {%- for option in args.packaging_options %} {{option}} {%- endfor %} } {%- endif %} buildTypes { debug { } release { {% if args.sign -%} signingConfig signingConfigs.release {%- endif %} } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 {%- for option in args.compile_options %} {{option}} {%- endfor %} } sourceSets { main { jniLibs.srcDir 'libs' java { {%- for adir, pattern in args.extra_source_dirs -%} srcDir '{{adir}}' {%- endfor -%} } } } aaptOptions { noCompress "tflite" } } dependencies { {%- for aar in aars %} implementation(name: '{{ aar }}', ext: 'aar') {%- endfor -%} {%- for jar in jars %} implementation files('src/main/libs/{{ jar }}') {%- endfor -%} {%- if args.depends -%} {%- for depend in args.depends %} implementation '{{ depend }}' {%- endfor %} {%- endif %} {% if args.presplash_lottie %} implementation 'com.airbnb.android:lottie:6.1.0' {%- endif %} } ================================================ FILE: pythonforandroid/bootstraps/common/build/templates/build.tmpl.xml ================================================ ================================================ FILE: pythonforandroid/bootstraps/common/build/templates/custom_rules.tmpl.xml ================================================ {% if args.launcher %} {% else %} {% endif %} {% for dir, includes in args.extra_source_dirs %} {% endfor %} ================================================ FILE: pythonforandroid/bootstraps/common/build/templates/gradle.tmpl.properties ================================================ {% if bootstrap_name == "qt" %} # For tweaking memory settings. Otherwise, a p4a session with Qt bootstrap and PySide6 recipe # terminates with a Java out of memory exception org.gradle.jvmargs=-Xmx2500m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 {% endif %} {% if args.enable_androidx %} android.useAndroidX=true android.enableJetifier=true {% endif %} ================================================ FILE: pythonforandroid/bootstraps/common/build/templates/lottie.xml ================================================ ================================================ FILE: pythonforandroid/bootstraps/common/build/whitelist.txt ================================================ # put files here that you need to un-blacklist ================================================ FILE: pythonforandroid/bootstraps/empty/__init__.py ================================================ from pythonforandroid.toolchain import Bootstrap class EmptyBootstrap(Bootstrap): name = 'empty' recipe_depends = [] can_be_chosen_automatically = False def assemble_distribution(self): print('empty bootstrap has no distribute') exit(1) bootstrap = EmptyBootstrap() ================================================ FILE: pythonforandroid/bootstraps/empty/build/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/gradle.properties ================================================ # Gradle properties for Java lint project # Disable daemon for CI environments org.gradle.daemon=false # Use parallel execution where possible org.gradle.parallel=true # Configure JVM memory org.gradle.jvmargs=-Xmx512m ================================================ FILE: pythonforandroid/bootstraps/qt/__init__.py ================================================ import sh from os.path import join from pythonforandroid.toolchain import ( Bootstrap, current_directory, info, info_main, shprint) from pythonforandroid.util import ensure_dir, rmdir class QtBootstrap(Bootstrap): name = 'qt' recipe_depends = ['python3', 'genericndkbuild', 'PySide6', 'shiboken6'] # this is needed because the recipes PySide6 and shiboken6 resides in the PySide Qt repository # - https://code.qt.io/cgit/pyside/pyside-setup.git/ # Without this some tests will error because it cannot find the recipes within pythonforandroid # repository can_be_chosen_automatically = False def assemble_distribution(self): info_main("# Creating Android project using Qt bootstrap") rmdir(self.dist_dir) info("Copying gradle build") shprint(sh.cp, '-r', self.build_dir, self.dist_dir) with current_directory(self.dist_dir): with open('local.properties', 'w') as fileh: fileh.write('sdk.dir={}'.format(self.ctx.sdk_dir)) arch = self.ctx.archs[0] if len(self.ctx.archs) > 1: raise ValueError("Trying to build for more than one arch. Qt bootstrap cannot handle that yet") info(f"Bootstrap running with arch {arch}") with current_directory(self.dist_dir): info("Copying Python distribution") self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)]) self.distribute_aars(arch) self.distribute_javaclasses(self.ctx.javaclass_dir, dest_dir=join("src", "main", "java")) python_bundle_dir = join(f'_python_bundle__{arch.arch}', '_python_bundle') ensure_dir(python_bundle_dir) site_packages_dir = self.ctx.python_recipe.create_python_bundle( join(self.dist_dir, python_bundle_dir), arch) if not self.ctx.with_debug_symbols: self.strip_libraries(arch) self.fry_eggs(site_packages_dir) super().assemble_distribution() bootstrap = QtBootstrap() ================================================ FILE: pythonforandroid/bootstraps/qt/build/.gitignore ================================================ .gradle /build/ # Ignore Gradle GUI config gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar # Cache of project .gradletasknamecache # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # gradle/wrapper/gradle-wrapper.properties ================================================ FILE: pythonforandroid/bootstraps/qt/build/blacklist.txt ================================================ # prevent user to include invalid extensions *.apk *.aab *.apks *.pxd # eggs *.egg-info # unit test unittest/* # python config config/makesetup # unused encodings lib-dynload/*codec* encodings/cp*.pyo encodings/tis* encodings/shift* encodings/bz2* encodings/iso* encodings/undefined* encodings/johab* encodings/p* encodings/m* encodings/euc* encodings/k* encodings/unicode_internal* encodings/quo* encodings/gb* encodings/big5* encodings/hp* encodings/hz* # unused python modules bsddb/* wsgiref/* hotshot/* pydoc_data/* tty.pyo anydbm.pyo nturl2path.pyo LICENCE.txt macurl2path.pyo dummy_threading.pyo audiodev.pyo antigravity.pyo dumbdbm.pyo sndhdr.pyo __phello__.foo.pyo sunaudio.pyo os2emxpath.pyo multiprocessing/dummy* # unused binaries python modules lib-dynload/termios.so lib-dynload/_lsprof.so lib-dynload/*audioop.so lib-dynload/_hotshot.so lib-dynload/_heapq.so lib-dynload/_json.so lib-dynload/grp.so lib-dynload/resource.so lib-dynload/pyexpat.so lib-dynload/_ctypes_test.so lib-dynload/_testcapi.so # odd files plat-linux3/regen ================================================ FILE: pythonforandroid/bootstraps/qt/build/jni/Application.mk ================================================ # Uncomment this if you're using STL in your project # See CPLUSPLUS-SUPPORT.html in the NDK documentation for more information # APP_STL := stlport_static # APP_ABI := armeabi armeabi-v7a x86 APP_ABI := $(ARCH) APP_PLATFORM := $(NDK_API) ================================================ FILE: pythonforandroid/bootstraps/qt/build/jni/application/src/Android.mk ================================================ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := main_$(PREFERRED_ABI) # Add your application source files here... LOCAL_SRC_FILES := start.c LOCAL_CFLAGS += -I$(PYTHON_INCLUDE_ROOT) $(EXTRA_CFLAGS) LOCAL_SHARED_LIBRARIES := python_shared LOCAL_LDLIBS := -llog $(EXTRA_LDLIBS) LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS) include $(BUILD_SHARED_LIBRARY) ================================================ FILE: pythonforandroid/bootstraps/qt/build/jni/application/src/Android_static.mk ================================================ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := main_$(PREFERRED_ABI) LOCAL_SRC_FILES := start.c include $(BUILD_SHARED_LIBRARY) ================================================ FILE: pythonforandroid/bootstraps/qt/build/jni/application/src/bootstrap_name.h ================================================ const char bootstrap_name[] = "qt"; ================================================ FILE: pythonforandroid/bootstraps/qt/build/src/main/assets/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/qt/build/src/main/java/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/qt/build/src/main/java/org/kivy/android/PythonActivity.java ================================================ package org.kivy.android; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.PowerManager; import android.os.SystemClock; import android.util.Log; import android.view.KeyEvent; import android.widget.Toast; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import org.qtproject.qt.android.bindings.QtActivity; public class PythonActivity extends QtActivity { private static final String TAG = "PythonActivity"; public static PythonActivity mActivity = null; private Bundle mMetaData = null; private PowerManager.WakeLock mWakeLock = null; public String getAppRoot() { String app_root = getFilesDir().getAbsolutePath() + "/app"; return app_root; } public String getEntryPoint(String search_dir) { /* Get the main file (.pyc|.py) depending on if we * have a compiled version or not. */ List entryPoints = new ArrayList(); entryPoints.add("main.pyc"); // python 3 compiled files for (String value : entryPoints) { File mainFile = new File(search_dir + "/" + value); if (mainFile.exists()) { return value; } } return "main.py"; } public void setEnvironmentVariable(String key, String value) { /** Sets an environment variable based on key/value. */ try { android.system.Os.setenv(key, value, true); } catch (Exception e) { Log.e("Qt bootstrap", "Unable set environment variable:" + key + "=" + value); e.printStackTrace(); } } @Override public void onCreate(Bundle savedInstanceState) { this.mActivity = this; Log.v(TAG, "Ready to unpack"); File app_root_file = new File(getAppRoot()); PythonUtil.unpackAsset(mActivity, "private", app_root_file, true); PythonUtil.unpackPyBundle( mActivity, getApplicationInfo().nativeLibraryDir + "/" + "libpybundle", app_root_file, false); Log.v("Python", "Device: " + android.os.Build.DEVICE); Log.v("Python", "Model: " + android.os.Build.MODEL); // Set up the Python environment String app_root_dir = getAppRoot(); 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"); setEnvironmentVariable("ANDROID_ENTRYPOINT", entry_point); setEnvironmentVariable("ANDROID_ARGUMENT", app_root_dir); setEnvironmentVariable("ANDROID_APP_PATH", app_root_dir); setEnvironmentVariable("ANDROID_PRIVATE", mFilesDirectory); setEnvironmentVariable("ANDROID_UNPACK", app_root_dir); setEnvironmentVariable("PYTHONHOME", app_root_dir); setEnvironmentVariable("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib"); setEnvironmentVariable("PYTHONOPTIMIZE", "2"); Log.v(TAG, "About to do super onCreate"); super.onCreate(savedInstanceState); Log.v(TAG, "Did super onCreate"); this.mActivity = this; 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) { } } @Override public void onDestroy() { Log.i("Destroy", "end of app"); super.onDestroy(); // make sure all child threads (python_thread) are stopped android.os.Process.killProcess(android.os.Process.myPid()); } long lastBackClick = SystemClock.elapsedRealtime(); @Override public boolean onKeyDown(int keyCode, KeyEvent event) { // If it wasn't the Back key or there's no web page history, bubble up to the default // system behavior (probably exit the activity) if (SystemClock.elapsedRealtime() - lastBackClick > 2000) { lastBackClick = SystemClock.elapsedRealtime(); Toast.makeText(this, "Click again to close the app", Toast.LENGTH_LONG).show(); return true; } lastBackClick = SystemClock.elapsedRealtime(); return super.onKeyDown(keyCode, event); } // ---------------------------------------------------------------------------- // Listener interface for onNewIntent // public interface NewIntentListener { void onNewIntent(Intent intent); } private List newIntentListeners = null; public void registerNewIntentListener(NewIntentListener listener) { if (this.newIntentListeners == null) this.newIntentListeners = Collections.synchronizedList(new ArrayList()); this.newIntentListeners.add(listener); } public void unregisterNewIntentListener(NewIntentListener listener) { if (this.newIntentListeners == null) return; this.newIntentListeners.remove(listener); } @Override protected void onNewIntent(Intent intent) { if (this.newIntentListeners == null) return; this.onResume(); synchronized (this.newIntentListeners) { Iterator iterator = this.newIntentListeners.iterator(); while (iterator.hasNext()) { (iterator.next()).onNewIntent(intent); } } } // ---------------------------------------------------------------------------- // Listener interface for onActivityResult // public interface ActivityResultListener { void onActivityResult(int requestCode, int resultCode, Intent data); } private List activityResultListeners = null; public void registerActivityResultListener(ActivityResultListener listener) { if (this.activityResultListeners == null) this.activityResultListeners = Collections.synchronizedList(new ArrayList()); this.activityResultListeners.add(listener); } public void unregisterActivityResultListener(ActivityResultListener listener) { if (this.activityResultListeners == null) return; this.activityResultListeners.remove(listener); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if (this.activityResultListeners == null) return; this.onResume(); synchronized (this.activityResultListeners) { Iterator iterator = this.activityResultListeners.iterator(); while (iterator.hasNext()) (iterator.next()).onActivityResult(requestCode, resultCode, intent); } } public static void start_service( String serviceTitle, String serviceDescription, String pythonServiceArgument) { _do_start_service(serviceTitle, serviceDescription, pythonServiceArgument, true); } public static void start_service_not_as_foreground( String serviceTitle, String serviceDescription, String pythonServiceArgument) { _do_start_service(serviceTitle, serviceDescription, pythonServiceArgument, false); } public static void _do_start_service( String serviceTitle, String serviceDescription, String pythonServiceArgument, boolean showForegroundNotification) { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath(); String app_root_dir = PythonActivity.mActivity.getAppRoot(); String entry_point = PythonActivity.mActivity.getEntryPoint(app_root_dir + "/service"); serviceIntent.putExtra("androidPrivate", argument); serviceIntent.putExtra("androidArgument", app_root_dir); serviceIntent.putExtra("serviceEntrypoint", "service/" + entry_point); serviceIntent.putExtra("pythonName", "python"); serviceIntent.putExtra("pythonHome", app_root_dir); serviceIntent.putExtra("pythonPath", app_root_dir + ":" + app_root_dir + "/lib"); serviceIntent.putExtra( "serviceStartAsForeground", (showForegroundNotification ? "true" : "false")); serviceIntent.putExtra("serviceTitle", serviceTitle); serviceIntent.putExtra("serviceDescription", serviceDescription); serviceIntent.putExtra("pythonServiceArgument", pythonServiceArgument); PythonActivity.mActivity.startService(serviceIntent); } public static void stop_service() { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); PythonActivity.mActivity.stopService(serviceIntent); } } ================================================ FILE: pythonforandroid/bootstraps/qt/build/src/main/jniLibs/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/qt/build/src/main/libs/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/qt/build/src/main/res/drawable/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/qt/build/src/main/res/mipmap/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/qt/build/templates/AndroidManifest.tmpl.xml ================================================ = 9 %} android:xlargeScreens="true" {% endif %} /> {% for perm in args.permissions %} {% endfor %} {% if args.wakelock %} {% endif %} {% if args.billing_pubkey %} {% endif %} {{ args.extra_manifest_xml }} {% for l in args.android_used_libs %} {% endfor %} {% for m in args.meta_data %} {% endfor %} {%- if args.intent_filters -%} {{- args.intent_filters -}} {%- endif -%} {% if service or args.launcher %} {% endif %} {% for name, foreground_type in service_data %} {% endfor %} {% for name in native_services %} {% endfor %} {% for a in args.add_activity %} {% endfor %} ================================================ FILE: pythonforandroid/bootstraps/qt/build/templates/libs.tmpl.xml ================================================ {{ arch }};c++_shared {%- for qt_lib in qt_libs %} {{ arch }};Qt6{{ qt_lib }}_{{ arch }} {%- endfor -%} {%- for load_local_lib in load_local_libs %} {{ arch }};lib{{ load_local_lib }}_{{ arch }}.so {%- endfor -%} {{ arch }};libshiboken6.abi3.so {{ arch }};libpyside6.abi3.so {%- for qt_lib in qt_libs %} {{ arch }};Qt{{ qt_lib }}.abi3.so {% if qt_lib == "Qml" -%} {{ arch }};libpyside6qml.abi3.so {% endif %} {%- endfor -%} {{ init_classes }} 1 1 ================================================ FILE: pythonforandroid/bootstraps/qt/build/templates/strings.tmpl.xml ================================================ {{ args.name }} {{ private_version }} {{ args.presplash_color }} ================================================ FILE: pythonforandroid/bootstraps/sdl2/__init__.py ================================================ from pythonforandroid.bootstraps._sdl_common import SDLGradleBootstrap class SDL2GradleBootstrap(SDLGradleBootstrap): name = "sdl2" recipe_depends = list( set(SDLGradleBootstrap.recipe_depends).union({"sdl2"}) ) bootstrap = SDL2GradleBootstrap() ================================================ FILE: pythonforandroid/bootstraps/sdl2/build/jni/application/src/Android.mk ================================================ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := main SDL_PATH := ../../SDL LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include # Add your application source files here... LOCAL_SRC_FILES := start.c LOCAL_CFLAGS += -I$(PYTHON_INCLUDE_ROOT) $(EXTRA_CFLAGS) LOCAL_SHARED_LIBRARIES := SDL2 python_shared LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog $(EXTRA_LDLIBS) LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS) include $(BUILD_SHARED_LIBRARY) ================================================ FILE: pythonforandroid/bootstraps/sdl2/build/jni/application/src/Android_static.mk ================================================ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := main LOCAL_SRC_FILES := start.c LOCAL_STATIC_LIBRARIES := SDL2_static include $(BUILD_SHARED_LIBRARY) $(call import-module,SDL)LOCAL_PATH := $(call my-dir) ================================================ FILE: pythonforandroid/bootstraps/sdl2/build/jni/application/src/bootstrap_name.h ================================================ #define BOOTSTRAP_NAME_SDL2 const char bootstrap_name[] = "SDL2"; // capitalized for historic reasons ================================================ FILE: pythonforandroid/bootstraps/sdl2/build/src/main/java/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java ================================================ package org.kivy.android; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Resources.NotFoundException; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.PixelFormat; import android.os.AsyncTask; import android.os.Bundle; import android.os.PowerManager; import android.util.Log; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.ImageView; import android.widget.Toast; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Timer; import java.util.TimerTask; import org.kivy.android.launcher.Project; import org.libsdl.app.SDLActivity; import org.renpy.android.ResourceManager; public class PythonActivity extends SDLActivity { private static final String TAG = "PythonActivity"; public static PythonActivity mActivity = null; private ResourceManager resourceManager = null; private Bundle mMetaData = null; private PowerManager.WakeLock mWakeLock = null; public String getAppRoot() { String app_root = getFilesDir().getAbsolutePath() + "/app"; return app_root; } @Override protected void onCreate(Bundle savedInstanceState) { Log.v(TAG, "PythonActivity onCreate running"); resourceManager = new ResourceManager(this); Log.v(TAG, "About to do super onCreate"); super.onCreate(savedInstanceState); Log.v(TAG, "Did super onCreate"); this.mActivity = this; this.showLoadingScreen(this.getLoadingScreen()); new UnpackFilesTask().execute(getAppRoot()); } public void loadLibraries() { String app_root = new String(getAppRoot()); File app_root_file = new File(app_root); PythonUtil.loadLibraries(app_root_file, new File(getApplicationInfo().nativeLibraryDir)); } /** Show an error using a toast. (Only makes sense from non-UI threads.) */ public void toastError(final String msg) { final Activity thisActivity = this; runOnUiThread( new Runnable() { public void run() { Toast.makeText(thisActivity, msg, Toast.LENGTH_LONG).show(); } }); // Wait to show the error. synchronized (this) { try { this.wait(1000); } catch (InterruptedException e) { } } } 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"); PythonUtil.unpackAsset(mActivity, "private", app_root_file, true); PythonUtil.unpackPyBundle( mActivity, getApplicationInfo().nativeLibraryDir + "/" + "libpybundle", app_root_file, false); return null; } @Override protected void onPostExecute(String result) { // Figure out the directory where the game is. If the game was // given to us via an intent, then we use the scheme-specific // part of that intent to determine the file to launch. We // also use the android.txt file to determine the orientation. // // Otherwise, we use the public data, if we have it, or the // private data if we do not. mActivity.finishLoad(); // finishLoad called setContentView with the SDL view, which // removed the loading screen. However, we still need it to // show until the app is ready to render, so pop it back up // on top of the SDL view. mActivity.showLoadingScreen(getLoadingScreen()); String app_root_dir = getAppRoot(); if (getIntent() != null && getIntent().getAction() != null && getIntent().getAction().equals("org.kivy.LAUNCH")) { File path = new File(getIntent().getData().getSchemeSpecificPart()); Project p = Project.scanDirectory(path); String entry_point = getEntryPoint(p.dir); SDLActivity.nativeSetenv("ANDROID_ENTRYPOINT", p.dir + "/" + entry_point); SDLActivity.nativeSetenv("ANDROID_ARGUMENT", p.dir); SDLActivity.nativeSetenv("ANDROID_APP_PATH", p.dir); if (p != null) { if (p.landscape) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } } // Let old apps know they started. try { FileWriter f = new FileWriter(new File(path, ".launch")); f.write("started"); f.close(); } catch (IOException e) { // pass } } else { String entry_point = getEntryPoint(app_root_dir); SDLActivity.nativeSetenv("ANDROID_ENTRYPOINT", entry_point); SDLActivity.nativeSetenv("ANDROID_ARGUMENT", app_root_dir); SDLActivity.nativeSetenv("ANDROID_APP_PATH", app_root_dir); } String mFilesDirectory = mActivity.getFilesDir().getAbsolutePath(); Log.v(TAG, "Setting env vars for start.c and Python to use"); SDLActivity.nativeSetenv("ANDROID_PRIVATE", mFilesDirectory); SDLActivity.nativeSetenv("ANDROID_UNPACK", app_root_dir); SDLActivity.nativeSetenv("PYTHONHOME", app_root_dir); SDLActivity.nativeSetenv("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib"); SDLActivity.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(); } if (mActivity.mMetaData.getInt("surface.transparent") != 0) { Log.v(TAG, "Surface will be transparent."); getSurface().setZOrderOnTop(true); getSurface().getHolder().setFormat(PixelFormat.TRANSPARENT); } else { Log.i(TAG, "Surface will NOT be transparent"); } } catch (PackageManager.NameNotFoundException e) { } // Launch app if that hasn't been done yet: if (mActivity.mHasFocus && ( // never went into proper resume state: mActivity.mCurrentNativeState == NativeState.INIT || ( // resumed earlier but wasn't ready yet mActivity.mCurrentNativeState == NativeState.RESUMED && mActivity.mSDLThread == null))) { // Because sometimes the app will get stuck here and never // actually run, ensure that it gets launched if we're active: mActivity.resumeNativeThread(); } } @Override protected void onPreExecute() {} @Override protected void onProgressUpdate(Void... values) {} } public static ViewGroup getLayout() { return mLayout; } public static SurfaceView getSurface() { return mSurface; } // ---------------------------------------------------------------------------- // Listener interface for onNewIntent // public interface NewIntentListener { void onNewIntent(Intent intent); } private List newIntentListeners = null; public void registerNewIntentListener(NewIntentListener listener) { if (this.newIntentListeners == null) this.newIntentListeners = Collections.synchronizedList(new ArrayList()); this.newIntentListeners.add(listener); } public void unregisterNewIntentListener(NewIntentListener listener) { if (this.newIntentListeners == null) return; this.newIntentListeners.remove(listener); } @Override protected void onNewIntent(Intent intent) { if (this.newIntentListeners == null) return; this.onResume(); synchronized (this.newIntentListeners) { Iterator iterator = this.newIntentListeners.iterator(); while (iterator.hasNext()) { (iterator.next()).onNewIntent(intent); } } } // ---------------------------------------------------------------------------- // Listener interface for onActivityResult // public interface ActivityResultListener { void onActivityResult(int requestCode, int resultCode, Intent data); } private List activityResultListeners = null; public void registerActivityResultListener(ActivityResultListener listener) { if (this.activityResultListeners == null) this.activityResultListeners = Collections.synchronizedList(new ArrayList()); this.activityResultListeners.add(listener); } public void unregisterActivityResultListener(ActivityResultListener listener) { if (this.activityResultListeners == null) return; this.activityResultListeners.remove(listener); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if (this.activityResultListeners == null) return; this.onResume(); synchronized (this.activityResultListeners) { Iterator iterator = this.activityResultListeners.iterator(); while (iterator.hasNext()) (iterator.next()).onActivityResult(requestCode, resultCode, intent); } } public static void start_service( String serviceTitle, String serviceDescription, String pythonServiceArgument) { _do_start_service(serviceTitle, serviceDescription, pythonServiceArgument, true); } public static void start_service_not_as_foreground( String serviceTitle, String serviceDescription, String pythonServiceArgument) { _do_start_service(serviceTitle, serviceDescription, pythonServiceArgument, false); } public static void _do_start_service( String serviceTitle, String serviceDescription, String pythonServiceArgument, boolean showForegroundNotification) { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath(); String app_root_dir = PythonActivity.mActivity.getAppRoot(); String entry_point = PythonActivity.mActivity.getEntryPoint(app_root_dir + "/service"); serviceIntent.putExtra("androidPrivate", argument); serviceIntent.putExtra("androidArgument", app_root_dir); serviceIntent.putExtra("serviceEntrypoint", "service/" + entry_point); serviceIntent.putExtra("pythonName", "python"); serviceIntent.putExtra("pythonHome", app_root_dir); serviceIntent.putExtra("pythonPath", app_root_dir + ":" + app_root_dir + "/lib"); serviceIntent.putExtra( "serviceStartAsForeground", (showForegroundNotification ? "true" : "false")); serviceIntent.putExtra("serviceTitle", serviceTitle); serviceIntent.putExtra("serviceDescription", serviceDescription); serviceIntent.putExtra("pythonServiceArgument", pythonServiceArgument); PythonActivity.mActivity.startService(serviceIntent); } public static void stop_service() { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); PythonActivity.mActivity.stopService(serviceIntent); } /** Loading screen view * */ public static ImageView mImageView = null; public static View mLottieView = null; /** Whether main routine/actual app has started yet * */ protected boolean mAppConfirmedActive = false; /** Timer for delayed loading screen removal. * */ protected Timer loadingScreenRemovalTimer = null; // Overridden since it's called often, to check whether to remove the // loading screen: @Override protected boolean sendCommand(int command, Object data) { boolean result = super.sendCommand(command, data); considerLoadingScreenRemoval(); return result; } /** Confirm that the app's main routine has been launched. */ @Override public void appConfirmedActive() { if (!mAppConfirmedActive) { Log.v(TAG, "appConfirmedActive() -> preparing loading screen removal"); mAppConfirmedActive = true; considerLoadingScreenRemoval(); } } /** * This is called from various places to check whether the app's main routine has been launched * already, and if it has, then the loading screen will be removed. */ public void considerLoadingScreenRemoval() { if (loadingScreenRemovalTimer != null) return; runOnUiThread( new Runnable() { public void run() { if (((PythonActivity) PythonActivity.mSingleton).mAppConfirmedActive && loadingScreenRemovalTimer == null) { // Remove loading screen but with a delay. // (app can use p4a's android.loadingscreen module to // do it quicker if it wants to) // get a handler (call from main thread) // this will run when timer elapses TimerTask removalTask = new TimerTask() { @Override public void run() { // post a runnable to the handler runOnUiThread( new Runnable() { @Override public void run() { PythonActivity activity = ((PythonActivity) PythonActivity .mSingleton); if (activity != null) activity.removeLoadingScreen(); } }); } }; loadingScreenRemovalTimer = new Timer(); loadingScreenRemovalTimer.schedule(removalTask, 5000); } } }); } public void removeLoadingScreen() { runOnUiThread( new Runnable() { public void run() { View view = mLottieView != null ? mLottieView : mImageView; if (view != null && view.getParent() != null) { ((ViewGroup) view.getParent()).removeView(view); mLottieView = null; mImageView = null; } } }); } public String getEntryPoint(String search_dir) { /* Get the main file (.pyc|.py) depending on if we * have a compiled version or not. */ List entryPoints = new ArrayList(); entryPoints.add("main.pyc"); // python 3 compiled files for (String value : entryPoints) { File mainFile = new File(search_dir + "/" + value); if (mainFile.exists()) { return value; } } return "main.py"; } protected void showLoadingScreen(View view) { try { if (mLayout == null) { setContentView(view); } else if (view.getParent() == null) { mLayout.addView(view); } } catch (IllegalStateException e) { // The loading screen can be attempted to be applied twice if app // is tabbed in/out, quickly. // (Gives error "The specified child already has a parent. // You must call removeView() on the child's parent first.") } } protected void setBackgroundColor(View view) { /* * 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 { view.setBackgroundColor(Color.parseColor(backgroundColor)); } catch (IllegalArgumentException e) { } } } protected View getLoadingScreen() { // If we have an mLottieView or mImageView already, then do // nothing because it will have already been made the content // view or added to the layout. if (mLottieView != null || mImageView != null) { // we already have a splash screen return mLottieView != null ? mLottieView : mImageView; } // first try to load the lottie one try { mLottieView = getLayoutInflater() .inflate( this.resourceManager.getIdentifier("lottie", "layout"), mLayout, false); try { if (mLayout == null) { setContentView(mLottieView); } else if (PythonActivity.mLottieView.getParent() == null) { mLayout.addView(mLottieView); } } catch (IllegalStateException e) { // The loading screen can be attempted to be applied twice if app // is tabbed in/out, quickly. // (Gives error "The specified child already has a parent. // You must call removeView() on the child's parent first.") } setBackgroundColor(mLottieView); return mLottieView; } catch (NotFoundException e) { Log.v("SDL", "couldn't find lottie layout or animation, trying static splash"); } // no lottie asset, try to load the static image then 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); setBackgroundColor(mImageView); mImageView.setLayoutParams( new ViewGroup.LayoutParams( ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); mImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); return mImageView; } @Override protected void onPause() { if (this.mWakeLock != null && mWakeLock.isHeld()) { this.mWakeLock.release(); } Log.v(TAG, "onPause()"); try { super.onPause(); } catch (UnsatisfiedLinkError e) { // Catch pause while still in loading screen failing to // call native function (since it's not yet loaded) } } @Override protected void onResume() { if (this.mWakeLock != null) { this.mWakeLock.acquire(); } Log.v(TAG, "onResume()"); try { super.onResume(); } catch (UnsatisfiedLinkError e) { // Catch resume while still in loading screen failing to // call native function (since it's not yet loaded) } considerLoadingScreenRemoval(); } @Override public void onWindowFocusChanged(boolean hasFocus) { try { super.onWindowFocusChanged(hasFocus); } catch (UnsatisfiedLinkError e) { // Catch window focus while still in loading screen failing to // call native function (since it's not yet loaded) } considerLoadingScreenRemoval(); } /** * Used by android.permissions p4a module to register a call back after requesting runtime * permissions */ public interface PermissionsCallback { void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults); } private PermissionsCallback permissionCallback; private boolean havePermissionsCallback = false; public void addPermissionsCallback(PermissionsCallback callback) { permissionCallback = callback; havePermissionsCallback = true; Log.v(TAG, "addPermissionsCallback(): Added callback for onRequestPermissionsResult"); } @Override public void onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) { Log.v(TAG, "onRequestPermissionsResult()"); if (havePermissionsCallback) { Log.v(TAG, "onRequestPermissionsResult passed to callback"); permissionCallback.onRequestPermissionsResult(requestCode, permissions, grantResults); } super.onRequestPermissionsResult(requestCode, permissions, grantResults); } /** Used by android.permissions p4a module to check a permission */ public boolean checkCurrentPermission(String permission) { if (android.os.Build.VERSION.SDK_INT < 23) return true; try { java.lang.reflect.Method methodCheckPermission = Activity.class.getMethod("checkSelfPermission", String.class); Object resultObj = methodCheckPermission.invoke(this, permission); int result = Integer.parseInt(resultObj.toString()); if (result == PackageManager.PERMISSION_GRANTED) return true; } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { } return false; } /** Used by android.permissions p4a module to request runtime permissions */ public void requestPermissionsWithRequestCode(String[] permissions, int requestCode) { if (android.os.Build.VERSION.SDK_INT < 23) return; try { java.lang.reflect.Method methodRequestPermission = Activity.class.getMethod("requestPermissions", String[].class, int.class); methodRequestPermission.invoke(this, permissions, requestCode); } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { } } public void requestPermissions(String[] permissions) { requestPermissionsWithRequestCode(permissions, 1); } public static void changeKeyboard(int inputType) { if (SDLActivity.keyboardInputType != inputType) { SDLActivity.keyboardInputType = inputType; InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.restartInput(mTextEdit); } } } ================================================ FILE: pythonforandroid/bootstraps/sdl2/build/src/patches/SDLActivity.java.patch ================================================ --- a/src/main/java/org/libsdl/app/SDLActivity.java +++ b/src/main/java/org/libsdl/app/SDLActivity.java @@ -221,6 +221,8 @@ // This is what SDL runs in. It invokes SDL_main(), eventually protected static Thread mSDLThread; + + public static int keyboardInputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; protected static SDLGenericMotionListener_API12 getMotionListener() { if (mMotionListener == null) { @@ -323,6 +325,15 @@ Log.v(TAG, "Model: " + Build.MODEL); Log.v(TAG, "onCreate()"); super.onCreate(savedInstanceState); + + SDLActivity.initialize(); + // So we can call stuff from static callbacks + mSingleton = this; + } + + // We don't do this in onCreate because we unpack and load the app data on a thread + // and we can't run setup tasks until that thread completes. + protected void finishLoad() { try { Thread.currentThread().setName("SDLActivity"); @@ -837,7 +848,7 @@ Handler commandHandler = new SDLCommandHandler(); // Send a message from the SDLMain thread - boolean sendCommand(int command, Object data) { + protected boolean sendCommand(int command, Object data) { Message msg = commandHandler.obtainMessage(); msg.arg1 = command; msg.obj = data; @@ -1385,7 +1396,22 @@ return null; } return SDLActivity.mSurface.getNativeSurface(); + } + + /** + * Calls turnActive() on singleton to keep loading screen active + */ + public static void triggerAppConfirmedActive() { + mSingleton.appConfirmedActive(); } + + /** + * Trick needed for loading screen, overridden by PythonActivity + * to keep loading screen active + */ + public void appConfirmedActive() { + } + // Input @@ -1881,6 +1907,7 @@ Log.v("SDL", "Running main function " + function + " from library " + library); + SDLActivity.mSingleton.appConfirmedActive(); SDLActivity.nativeRunMain(library, function, arguments); Log.v("SDL", "Finished main function"); @@ -1938,8 +1965,7 @@ public InputConnection onCreateInputConnection(EditorInfo outAttrs) { ic = new SDLInputConnection(this, true); - outAttrs.inputType = InputType.TYPE_CLASS_TEXT | - InputType.TYPE_TEXT_FLAG_MULTI_LINE; + outAttrs.inputType = SDLActivity.keyboardInputType; outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */; ================================================ FILE: pythonforandroid/bootstraps/sdl2/build/src/patches/SDLSurface.java.patch ================================================ --- a/src/main/java/org/libsdl/app/SDLSurface.java +++ b/src/main/java/org/libsdl/app/SDLSurface.java @@ -193,9 +193,22 @@ return SDLActivity.handleKeyEvent(v, keyCode, event, null); } + public interface OnInterceptTouchListener { + boolean onTouch(MotionEvent ev); + } + + private OnInterceptTouchListener mOnInterceptTouchListener = null; + + public void setInterceptTouchListener(OnInterceptTouchListener listener) { + this.mOnInterceptTouchListener = listener; + } + // Touch events @Override public boolean onTouch(View v, MotionEvent event) { + if (mOnInterceptTouchListener != null) + if (mOnInterceptTouchListener.onTouch(event)) + return false; /* Ref: http://developer.android.com/training/gestures/multi.html */ int touchDevId = event.getDeviceId(); final int pointerCount = event.getPointerCount(); ================================================ FILE: pythonforandroid/bootstraps/sdl3/__init__.py ================================================ from pythonforandroid.bootstraps._sdl_common import SDLGradleBootstrap class SDL3GradleBootstrap(SDLGradleBootstrap): name = "sdl3" recipe_depends = list( set(SDLGradleBootstrap.recipe_depends).union({"sdl3"}) ) bootstrap = SDL3GradleBootstrap() ================================================ FILE: pythonforandroid/bootstraps/sdl3/build/jni/application/src/Android.mk ================================================ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := main SDL_PATH := ../../SDL LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include # Add your application source files here... LOCAL_SRC_FILES := start.c LOCAL_CFLAGS += -I$(PYTHON_INCLUDE_ROOT) $(EXTRA_CFLAGS) LOCAL_SHARED_LIBRARIES := SDL3 python_shared LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog $(EXTRA_LDLIBS) LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS) include $(BUILD_SHARED_LIBRARY) ================================================ FILE: pythonforandroid/bootstraps/sdl3/build/jni/application/src/Android_static.mk ================================================ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := main LOCAL_SRC_FILES := start.c LOCAL_STATIC_LIBRARIES := SDL3_static include $(BUILD_SHARED_LIBRARY) $(call import-module,SDL)LOCAL_PATH := $(call my-dir) ================================================ FILE: pythonforandroid/bootstraps/sdl3/build/jni/application/src/bootstrap_name.h ================================================ #define BOOTSTRAP_NAME_SDL3 const char bootstrap_name[] = "SDL3"; // capitalized for historic reasons ================================================ FILE: pythonforandroid/bootstraps/sdl3/build/src/main/java/org/kivy/android/PythonActivity.java ================================================ package org.kivy.android; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Resources.NotFoundException; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.PixelFormat; import android.os.AsyncTask; import android.os.Bundle; import android.os.PowerManager; import android.util.Log; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.Toast; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Timer; import java.util.TimerTask; import org.kivy.android.launcher.Project; import org.libsdl.app.SDLActivity; import org.renpy.android.ResourceManager; public class PythonActivity extends SDLActivity { private static final String TAG = "PythonActivity"; public static PythonActivity mActivity = null; private ResourceManager resourceManager = null; private Bundle mMetaData = null; private PowerManager.WakeLock mWakeLock = null; public String getAppRoot() { String app_root = getFilesDir().getAbsolutePath() + "/app"; return app_root; } @Override protected void onCreate(Bundle savedInstanceState) { Log.v(TAG, "PythonActivity onCreate running"); resourceManager = new ResourceManager(this); Log.v(TAG, "About to do super onCreate"); super.onCreate(savedInstanceState); Log.v(TAG, "Did super onCreate"); this.mActivity = this; this.showLoadingScreen(this.getLoadingScreen()); new UnpackFilesTask().execute(getAppRoot()); } public void loadLibraries() { String app_root = new String(getAppRoot()); File app_root_file = new File(app_root); PythonUtil.loadLibraries(app_root_file, new File(getApplicationInfo().nativeLibraryDir)); } /** Show an error using a toast. (Only makes sense from non-UI threads.) */ public void toastError(final String msg) { final Activity thisActivity = this; runOnUiThread( new Runnable() { public void run() { Toast.makeText(thisActivity, msg, Toast.LENGTH_LONG).show(); } }); // Wait to show the error. synchronized (this) { try { this.wait(1000); } catch (InterruptedException e) { } } } 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"); PythonUtil.unpackAsset(mActivity, "private", app_root_file, true); PythonUtil.unpackPyBundle( mActivity, getApplicationInfo().nativeLibraryDir + "/" + "libpybundle", app_root_file, false); return null; } @Override protected void onPostExecute(String result) { // Figure out the directory where the game is. If the game was // given to us via an intent, then we use the scheme-specific // part of that intent to determine the file to launch. We // also use the android.txt file to determine the orientation. // // Otherwise, we use the public data, if we have it, or the // private data if we do not. mActivity.finishLoad(); // finishLoad called setContentView with the SDL view, which // removed the loading screen. However, we still need it to // show until the app is ready to render, so pop it back up // on top of the SDL view. mActivity.showLoadingScreen(getLoadingScreen()); String app_root_dir = getAppRoot(); if (getIntent() != null && getIntent().getAction() != null && getIntent().getAction().equals("org.kivy.LAUNCH")) { File path = new File(getIntent().getData().getSchemeSpecificPart()); Project p = Project.scanDirectory(path); String entry_point = getEntryPoint(p.dir); SDLActivity.nativeSetenv("ANDROID_ENTRYPOINT", p.dir + "/" + entry_point); SDLActivity.nativeSetenv("ANDROID_ARGUMENT", p.dir); SDLActivity.nativeSetenv("ANDROID_APP_PATH", p.dir); if (p != null) { if (p.landscape) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } } // Let old apps know they started. try { FileWriter f = new FileWriter(new File(path, ".launch")); f.write("started"); f.close(); } catch (IOException e) { // pass } } else { String entry_point = getEntryPoint(app_root_dir); SDLActivity.nativeSetenv("ANDROID_ENTRYPOINT", entry_point); SDLActivity.nativeSetenv("ANDROID_ARGUMENT", app_root_dir); SDLActivity.nativeSetenv("ANDROID_APP_PATH", app_root_dir); } String mFilesDirectory = mActivity.getFilesDir().getAbsolutePath(); Log.v(TAG, "Setting env vars for start.c and Python to use"); SDLActivity.nativeSetenv("ANDROID_PRIVATE", mFilesDirectory); SDLActivity.nativeSetenv("ANDROID_UNPACK", app_root_dir); SDLActivity.nativeSetenv("PYTHONHOME", app_root_dir); SDLActivity.nativeSetenv("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib"); SDLActivity.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(); } if (mActivity.mMetaData.getInt("surface.transparent") != 0) { Log.v(TAG, "Surface will be transparent."); getSurface().setZOrderOnTop(true); getSurface().getHolder().setFormat(PixelFormat.TRANSPARENT); } else { Log.i(TAG, "Surface will NOT be transparent"); } } catch (PackageManager.NameNotFoundException e) { } // Launch app if that hasn't been done yet: if (mActivity.mHasFocus && ( // never went into proper resume state: mActivity.mCurrentNativeState == NativeState.INIT || ( // resumed earlier but wasn't ready yet mActivity.mCurrentNativeState == NativeState.RESUMED && mActivity.mSDLThread == null))) { // Because sometimes the app will get stuck here and never // actually run, ensure that it gets launched if we're active: mActivity.resumeNativeThread(); } } @Override protected void onPreExecute() {} @Override protected void onProgressUpdate(Void... values) {} } public static ViewGroup getLayout() { return mLayout; } public static SurfaceView getSurface() { return mSurface; } // ---------------------------------------------------------------------------- // Listener interface for onNewIntent // public interface NewIntentListener { void onNewIntent(Intent intent); } private List newIntentListeners = null; public void registerNewIntentListener(NewIntentListener listener) { if (this.newIntentListeners == null) this.newIntentListeners = Collections.synchronizedList(new ArrayList()); this.newIntentListeners.add(listener); } public void unregisterNewIntentListener(NewIntentListener listener) { if (this.newIntentListeners == null) return; this.newIntentListeners.remove(listener); } @Override protected void onNewIntent(Intent intent) { if (this.newIntentListeners == null) return; this.onResume(); synchronized (this.newIntentListeners) { Iterator iterator = this.newIntentListeners.iterator(); while (iterator.hasNext()) { (iterator.next()).onNewIntent(intent); } } } // ---------------------------------------------------------------------------- // Listener interface for onActivityResult // public interface ActivityResultListener { void onActivityResult(int requestCode, int resultCode, Intent data); } private List activityResultListeners = null; public void registerActivityResultListener(ActivityResultListener listener) { if (this.activityResultListeners == null) this.activityResultListeners = Collections.synchronizedList(new ArrayList()); this.activityResultListeners.add(listener); } public void unregisterActivityResultListener(ActivityResultListener listener) { if (this.activityResultListeners == null) return; this.activityResultListeners.remove(listener); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if (this.activityResultListeners == null) return; this.onResume(); synchronized (this.activityResultListeners) { Iterator iterator = this.activityResultListeners.iterator(); while (iterator.hasNext()) (iterator.next()).onActivityResult(requestCode, resultCode, intent); } } public static void start_service( String serviceTitle, String serviceDescription, String pythonServiceArgument) { _do_start_service(serviceTitle, serviceDescription, pythonServiceArgument, true); } public static void start_service_not_as_foreground( String serviceTitle, String serviceDescription, String pythonServiceArgument) { _do_start_service(serviceTitle, serviceDescription, pythonServiceArgument, false); } public static void _do_start_service( String serviceTitle, String serviceDescription, String pythonServiceArgument, boolean showForegroundNotification) { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath(); String app_root_dir = PythonActivity.mActivity.getAppRoot(); String entry_point = PythonActivity.mActivity.getEntryPoint(app_root_dir + "/service"); serviceIntent.putExtra("androidPrivate", argument); serviceIntent.putExtra("androidArgument", app_root_dir); serviceIntent.putExtra("serviceEntrypoint", "service/" + entry_point); serviceIntent.putExtra("pythonName", "python"); serviceIntent.putExtra("pythonHome", app_root_dir); serviceIntent.putExtra("pythonPath", app_root_dir + ":" + app_root_dir + "/lib"); serviceIntent.putExtra( "serviceStartAsForeground", (showForegroundNotification ? "true" : "false")); serviceIntent.putExtra("serviceTitle", serviceTitle); serviceIntent.putExtra("serviceDescription", serviceDescription); serviceIntent.putExtra("pythonServiceArgument", pythonServiceArgument); PythonActivity.mActivity.startService(serviceIntent); } public static void stop_service() { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); PythonActivity.mActivity.stopService(serviceIntent); } /** Loading screen view * */ public static ImageView mImageView = null; public static View mLottieView = null; /** Whether main routine/actual app has started yet * */ protected boolean mAppConfirmedActive = false; /** Timer for delayed loading screen removal. * */ protected Timer loadingScreenRemovalTimer = null; // Overridden since it's called often, to check whether to remove the // loading screen: @Override protected boolean sendCommand(int command, Object data) { boolean result = super.sendCommand(command, data); considerLoadingScreenRemoval(); return result; } /** Confirm that the app's main routine has been launched. */ @Override public void appConfirmedActive() { if (!mAppConfirmedActive) { Log.v(TAG, "appConfirmedActive() -> preparing loading screen removal"); mAppConfirmedActive = true; considerLoadingScreenRemoval(); } } /** * This is called from various places to check whether the app's main routine has been launched * already, and if it has, then the loading screen will be removed. */ public void considerLoadingScreenRemoval() { if (loadingScreenRemovalTimer != null) return; runOnUiThread( new Runnable() { public void run() { if (((PythonActivity) PythonActivity.mSingleton).mAppConfirmedActive && loadingScreenRemovalTimer == null) { // Remove loading screen but with a delay. // (app can use p4a's android.loadingscreen module to // do it quicker if it wants to) // get a handler (call from main thread) // this will run when timer elapses TimerTask removalTask = new TimerTask() { @Override public void run() { // post a runnable to the handler runOnUiThread( new Runnable() { @Override public void run() { PythonActivity activity = ((PythonActivity) PythonActivity .mSingleton); if (activity != null) activity.removeLoadingScreen(); } }); } }; loadingScreenRemovalTimer = new Timer(); loadingScreenRemovalTimer.schedule(removalTask, 5000); } } }); } public void removeLoadingScreen() { runOnUiThread( new Runnable() { public void run() { View view = mLottieView != null ? mLottieView : mImageView; if (view != null && view.getParent() != null) { ((ViewGroup) view.getParent()).removeView(view); mLottieView = null; mImageView = null; } } }); } public String getEntryPoint(String search_dir) { /* Get the main file (.pyc|.py) depending on if we * have a compiled version or not. */ List entryPoints = new ArrayList(); entryPoints.add("main.pyc"); // python 3 compiled files for (String value : entryPoints) { File mainFile = new File(search_dir + "/" + value); if (mainFile.exists()) { return value; } } return "main.py"; } protected void showLoadingScreen(View view) { try { if (mLayout == null) { setContentView(view); } else if (view.getParent() == null) { mLayout.addView(view); } } catch (IllegalStateException e) { // The loading screen can be attempted to be applied twice if app // is tabbed in/out, quickly. // (Gives error "The specified child already has a parent. // You must call removeView() on the child's parent first.") } } protected void setBackgroundColor(View view) { /* * 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 { view.setBackgroundColor(Color.parseColor(backgroundColor)); } catch (IllegalArgumentException e) { } } } protected View getLoadingScreen() { // If we have an mLottieView or mImageView already, then do // nothing because it will have already been made the content // view or added to the layout. if (mLottieView != null || mImageView != null) { // we already have a splash screen return mLottieView != null ? mLottieView : mImageView; } // first try to load the lottie one try { mLottieView = getLayoutInflater() .inflate( this.resourceManager.getIdentifier("lottie", "layout"), mLayout, false); try { if (mLayout == null) { setContentView(mLottieView); } else if (PythonActivity.mLottieView.getParent() == null) { mLayout.addView(mLottieView); } } catch (IllegalStateException e) { // The loading screen can be attempted to be applied twice if app // is tabbed in/out, quickly. // (Gives error "The specified child already has a parent. // You must call removeView() on the child's parent first.") } setBackgroundColor(mLottieView); return mLottieView; } catch (NotFoundException e) { Log.v("SDL", "couldn't find lottie layout or animation, trying static splash"); } // no lottie asset, try to load the static image then 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); setBackgroundColor(mImageView); mImageView.setLayoutParams( new ViewGroup.LayoutParams( ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); mImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); return mImageView; } @Override protected void onPause() { if (this.mWakeLock != null && mWakeLock.isHeld()) { this.mWakeLock.release(); } Log.v(TAG, "onPause()"); try { super.onPause(); } catch (UnsatisfiedLinkError e) { // Catch pause while still in loading screen failing to // call native function (since it's not yet loaded) } } @Override protected void onResume() { if (this.mWakeLock != null) { this.mWakeLock.acquire(); } Log.v(TAG, "onResume()"); try { super.onResume(); } catch (UnsatisfiedLinkError e) { // Catch resume while still in loading screen failing to // call native function (since it's not yet loaded) } considerLoadingScreenRemoval(); } @Override public void onWindowFocusChanged(boolean hasFocus) { try { super.onWindowFocusChanged(hasFocus); } catch (UnsatisfiedLinkError e) { // Catch window focus while still in loading screen failing to // call native function (since it's not yet loaded) } considerLoadingScreenRemoval(); } /** * Used by android.permissions p4a module to register a call back after requesting runtime * permissions */ public interface PermissionsCallback { void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults); } private PermissionsCallback permissionCallback; private boolean havePermissionsCallback = false; public void addPermissionsCallback(PermissionsCallback callback) { permissionCallback = callback; havePermissionsCallback = true; Log.v(TAG, "addPermissionsCallback(): Added callback for onRequestPermissionsResult"); } @Override public void onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) { Log.v(TAG, "onRequestPermissionsResult()"); if (havePermissionsCallback) { Log.v(TAG, "onRequestPermissionsResult passed to callback"); permissionCallback.onRequestPermissionsResult(requestCode, permissions, grantResults); } super.onRequestPermissionsResult(requestCode, permissions, grantResults); } /** Used by android.permissions p4a module to check a permission */ public boolean checkCurrentPermission(String permission) { if (android.os.Build.VERSION.SDK_INT < 23) return true; try { java.lang.reflect.Method methodCheckPermission = Activity.class.getMethod("checkSelfPermission", String.class); Object resultObj = methodCheckPermission.invoke(this, permission); int result = Integer.parseInt(resultObj.toString()); if (result == PackageManager.PERMISSION_GRANTED) return true; } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { } return false; } /** Used by android.permissions p4a module to request runtime permissions */ public void requestPermissionsWithRequestCode(String[] permissions, int requestCode) { if (android.os.Build.VERSION.SDK_INT < 23) return; try { java.lang.reflect.Method methodRequestPermission = Activity.class.getMethod("requestPermissions", String[].class, int.class); methodRequestPermission.invoke(this, permissions, requestCode); } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { } } public void requestPermissions(String[] permissions) { requestPermissionsWithRequestCode(permissions, 1); } public static void changeKeyboard(int inputType) { /* if (SDLActivity.keyboardInputType != inputType){ SDLActivity.keyboardInputType = inputType; InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.restartInput(mTextEdit); } */ } } ================================================ FILE: pythonforandroid/bootstraps/sdl3/build/src/patches/SDLActivity.java.patch ================================================ --- a/src/main/java/org/libsdl/app/SDLActivity.java +++ b/src/main/java/org/libsdl/app/SDLActivity.java @@ -259,6 +259,7 @@ String[] arguments = SDLActivity.mSingleton.getArguments(); Log.v("SDL", "Running main function " + function + " from library " + library); + SDLActivity.mSingleton.appConfirmedActive(); SDLActivity.nativeRunMain(library, function, arguments); Log.v("SDL", "Finished main function"); } @@ -351,6 +352,15 @@ Log.v(TAG, "Model: " + Build.MODEL); Log.v(TAG, "onCreate()"); super.onCreate(savedInstanceState); + + SDL.initialize(); + // So we can call stuff from static callbacks + mSingleton = this; + } + + // We don't do this in onCreate because we unpack and load the app data on a thread + // and we can't run setup tasks until that thread completes. + protected void finishLoad() { /* Control activity re-creation */ @@ -1541,8 +1551,22 @@ return null; } return SDLActivity.mSurface.getNativeSurface(); + } + + /** + * Calls turnActive() on singleton to keep loading screen active + */ + public static void triggerAppConfirmedActive() { + mSingleton.appConfirmedActive(); } + /** + * Trick needed for loading screen, overridden by PythonActivity + * to keep loading screen active + */ + public void appConfirmedActive() { + } + // Input /** ================================================ FILE: pythonforandroid/bootstraps/sdl3/build/src/patches/SDLSurface.java.patch ================================================ --- a/src/main/java/org/libsdl/app/SDLSurface.java +++ b/src/main/java/org/libsdl/app/SDLSurface.java @@ -232,9 +232,23 @@ } } + public interface OnInterceptTouchListener { + boolean onTouch(MotionEvent ev); + } + + private OnInterceptTouchListener mOnInterceptTouchListener = null; + + public void setInterceptTouchListener(OnInterceptTouchListener listener) { + this.mOnInterceptTouchListener = listener; + } + // Touch events @Override public boolean onTouch(View v, MotionEvent event) { + // Allow touch to be intercepted by python application + if (mOnInterceptTouchListener != null) + if (mOnInterceptTouchListener.onTouch(event)) + return false; /* Ref: http://developer.android.com/training/gestures/multi.html */ int touchDevId = event.getDeviceId(); final int pointerCount = event.getPointerCount(); ================================================ FILE: pythonforandroid/bootstraps/service_library/__init__.py ================================================ from pythonforandroid.bootstraps.service_only import ServiceOnlyBootstrap class ServiceLibraryBootstrap(ServiceOnlyBootstrap): name = 'service_library' bootstrap = ServiceLibraryBootstrap() ================================================ FILE: pythonforandroid/bootstraps/service_library/build/jni/Application.mk ================================================ APP_PLATFORM := $(NDK_API) APP_ABI := $(ARCH) ================================================ FILE: pythonforandroid/bootstraps/service_library/build/jni/application/src/bootstrap_name.h ================================================ #define BOOTSTRAP_NAME_LIBRARY const char bootstrap_name[] = "service_library"; ================================================ FILE: pythonforandroid/bootstraps/service_library/build/src/main/java/org/kivy/android/PythonActivity.java ================================================ package org.kivy.android; import android.app.Activity; // Required by PythonService class public class PythonActivity extends Activity { public static PythonActivity mActivity = null; } ================================================ FILE: pythonforandroid/bootstraps/service_library/build/src/main/res/mipmap/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/service_library/build/templates/AndroidManifest.tmpl.xml ================================================ {% for name, foreground_type in service_data %} {% endfor %} ================================================ FILE: pythonforandroid/bootstraps/service_library/build/templates/Service.tmpl.java ================================================ package {{ args.package }}; import java.io.File; import android.os.Build; import android.content.Intent; import android.content.Context; import android.content.res.Resources; import android.util.Log; import org.kivy.android.PythonService; import org.kivy.android.PythonUtil; public class Service{{ name|capitalize }} extends PythonService { private static final String TAG = "PythonService"; {% if sticky %} @Override public int startType() { return START_STICKY; } {% endif %} @Override protected int getServiceId() { return {{ service_id }}; } public static void prepare(Context ctx) { String appRoot = PythonUtil.getAppRoot(ctx); Log.v(TAG, "Ready to unpack"); File app_root_file = new File(appRoot); PythonUtil.unpackAsset(ctx, "private", app_root_file, true); PythonUtil.unpackPyBundle(ctx, ctx.getApplicationInfo().nativeLibraryDir + "/" + "libpybundle", app_root_file, false); } static private void _start(Context ctx, String smallIconName, String contentTitle, String contentText, String pythonServiceArgument) { Intent intent = getDefaultIntent(ctx, smallIconName, contentTitle, contentText, 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 void start(Context ctx, String pythonServiceArgument) { _start(ctx, "", "{{ args.name }}", "{{ name|capitalize }}", pythonServiceArgument); } static public void start(Context ctx, String smallIconName, String contentTitle, String contentText, String pythonServiceArgument) { _start(ctx, smallIconName, contentTitle, contentText, pythonServiceArgument); } static public Intent getDefaultIntent(Context ctx, String smallIconName, String contentTitle, String contentText, String pythonServiceArgument) { String appRoot = PythonUtil.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("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); intent.putExtra("smallIconName", smallIconName); intent.putExtra("contentTitle", contentTitle); intent.putExtra("contentText", contentText); return intent; } @Override protected Intent getThisDefaultIntent(Context ctx, String pythonServiceArgument) { return Service{{ name|capitalize }}.getDefaultIntent(ctx, "", "", "", pythonServiceArgument); } static public void stop(Context ctx) { Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); ctx.stopService(intent); } } ================================================ FILE: pythonforandroid/bootstraps/service_only/__init__.py ================================================ from pythonforandroid.toolchain import Bootstrap class ServiceOnlyBootstrap(Bootstrap): name = 'service_only' recipe_depends = list( set(Bootstrap.recipe_depends).union({'genericndkbuild'}) ) bootstrap = ServiceOnlyBootstrap() ================================================ FILE: pythonforandroid/bootstraps/service_only/build/blacklist.txt ================================================ # prevent user to include invalid extensions *.apk *.aab *.apks *.pxd # eggs *.egg-info # unit test unittest/* # python config config/makesetup # unused kivy files (platform specific) kivy/input/providers/wm_* kivy/input/providers/mactouch* kivy/input/providers/probesysfs* kivy/input/providers/mtdev* kivy/input/providers/hidinput* kivy/core/camera/camera_videocapture* kivy/core/spelling/*osx* kivy/core/video/video_pyglet* kivy/tools kivy/tests/* kivy/*/*.h kivy/*/*.pxi # unused encodings lib-dynload/*codec* encodings/cp*.pyo encodings/tis* encodings/shift* encodings/bz2* encodings/iso* encodings/undefined* encodings/johab* encodings/p* encodings/m* encodings/euc* encodings/k* encodings/unicode_internal* encodings/quo* encodings/gb* encodings/big5* encodings/hp* encodings/hz* # unused python modules bsddb/* wsgiref/* hotshot/* pydoc_data/* tty.pyo anydbm.pyo nturl2path.pyo LICENCE.txt macurl2path.pyo dummy_threading.pyo audiodev.pyo antigravity.pyo dumbdbm.pyo sndhdr.pyo __phello__.foo.pyo sunaudio.pyo os2emxpath.pyo multiprocessing/dummy* # unused binaries python modules lib-dynload/termios.so lib-dynload/_lsprof.so lib-dynload/*audioop.so lib-dynload/_hotshot.so lib-dynload/_heapq.so lib-dynload/_json.so lib-dynload/grp.so lib-dynload/resource.so lib-dynload/pyexpat.so lib-dynload/_ctypes_test.so lib-dynload/_testcapi.so # odd files plat-linux3/regen #>sqlite3 # conditional include depending if some recipes are included or not. sqlite3/* lib-dynload/_sqlite3.so # #include #define LOGI(...) do {} while (0) #define LOGE(...) do {} while (0) #include "android/log.h" /* These JNI management functions are taken from SDL2, but modified to refer to pyjnius */ /* #define LOG(n, x) __android_log_write(ANDROID_LOG_INFO, (n), (x)) */ /* #define LOGP(x) LOG("python", (x)) */ #define LOG_TAG "Python_android" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) /* Function headers */ JNIEnv* Android_JNI_GetEnv(void); static void Android_JNI_ThreadDestroyed(void*); static pthread_key_t mThreadKey; static JavaVM* mJavaVM; int Android_JNI_SetupThread(void) { Android_JNI_GetEnv(); return 1; } /* Library init */ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv *env; mJavaVM = vm; LOGI("JNI_OnLoad called"); if ((*mJavaVM)->GetEnv(mJavaVM, (void**) &env, JNI_VERSION_1_4) != JNI_OK) { LOGE("Failed to get the environment using GetEnv()"); return -1; } /* * Create mThreadKey so we can keep track of the JNIEnv assigned to each thread * Refer to http://developer.android.com/guide/practices/design/jni.html for the rationale behind this */ if (pthread_key_create(&mThreadKey, Android_JNI_ThreadDestroyed) != 0) { __android_log_print(ANDROID_LOG_ERROR, "pyjniusjni", "Error initializing pthread key"); } Android_JNI_SetupThread(); return JNI_VERSION_1_4; } JNIEnv* Android_JNI_GetEnv(void) { /* From http://developer.android.com/guide/practices/jni.html * All threads are Linux threads, scheduled by the kernel. * They're usually started from managed code (using Thread.start), but they can also be created elsewhere and then * attached to the JavaVM. For example, a thread started with pthread_create can be attached with the * JNI AttachCurrentThread or AttachCurrentThreadAsDaemon functions. Until a thread is attached, it has no JNIEnv, * and cannot make JNI calls. * Attaching a natively-created thread causes a java.lang.Thread object to be constructed and added to the "main" * ThreadGroup, making it visible to the debugger. Calling AttachCurrentThread on an already-attached thread * is a no-op. * Note: You can call this function any number of times for the same thread, there's no harm in it */ JNIEnv *env; int status = (*mJavaVM)->AttachCurrentThread(mJavaVM, &env, NULL); if(status < 0) { LOGE("failed to attach current thread"); return 0; } /* From http://developer.android.com/guide/practices/jni.html * Threads attached through JNI must call DetachCurrentThread before they exit. If coding this directly is awkward, * in Android 2.0 (Eclair) and higher you can use pthread_key_create to define a destructor function that will be * called before the thread exits, and call DetachCurrentThread from there. (Use that key with pthread_setspecific * to store the JNIEnv in thread-local-storage; that way it'll be passed into your destructor as the argument.) * Note: The destructor is not called unless the stored value is != NULL * Note: You can call this function any number of times for the same thread, there's no harm in it * (except for some lost CPU cycles) */ pthread_setspecific(mThreadKey, (void*) env); return env; } static void Android_JNI_ThreadDestroyed(void* value) { /* The thread is being destroyed, detach it from the Java VM and set the mThreadKey value to NULL as required */ JNIEnv *env = (JNIEnv*) value; if (env != NULL) { (*mJavaVM)->DetachCurrentThread(mJavaVM); pthread_setspecific(mThreadKey, NULL); } } void *WebView_AndroidGetJNIEnv() { return Android_JNI_GetEnv(); } ================================================ FILE: pythonforandroid/bootstraps/service_only/build/src/main/assets/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/service_only/build/src/main/java/org/kivy/android/PythonActivity.java ================================================ package org.kivy.android; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.PowerManager; import android.os.SystemClock; import android.util.Log; import android.view.KeyEvent; import android.widget.Toast; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import org.renpy.android.ResourceManager; public class PythonActivity extends Activity { // This activity is modified from a mixture of the SDLActivity and // PythonActivity in the SDL2 bootstrap, but removing all the SDL2 // specifics. private static final String TAG = "PythonActivity"; public static PythonActivity mActivity = null; /** If shared libraries (e.g. the native application) could not be loaded. */ public static boolean mBrokenLibraries; protected static Thread mPythonThread; private ResourceManager resourceManager = null; private Bundle mMetaData = null; private PowerManager.WakeLock mWakeLock = null; public String getAppRoot() { String app_root = getFilesDir().getAbsolutePath() + "/app"; return app_root; } public String getEntryPoint(String search_dir) { /* Get the main file (.pyc|.py) depending on if we * have a compiled version or not. */ List entryPoints = new ArrayList(); entryPoints.add("main.pyc"); // python 3 compiled files for (String value : entryPoints) { File mainFile = new File(search_dir + "/" + value); if (mainFile.exists()) { return value; } } return "main.py"; } public static void initialize() { // The static nature of the singleton and Android quirkiness force us to initialize // everything here // Otherwise, when exiting the app and returning to it, these variables *keep* their pre // exit values mBrokenLibraries = false; } @Override 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()); PythonUtil.unpackAsset(mActivity, "private", app_root_file, true); PythonUtil.unpackPyBundle( mActivity, getApplicationInfo().nativeLibraryDir + "/" + "libpybundle", app_root_file, false); 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); // Log.v(TAG, "Ready to unpack"); // new UnpackFilesTask().execute(getAppRoot()); PythonActivity.initialize(); // 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(); } 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(); return; } // Set up the Python environment String app_root_dir = getAppRoot(); 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"); 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) { } final Thread pythonThread = new Thread(new PythonMain(), "PythonThread"); PythonActivity.mPythonThread = pythonThread; pythonThread.start(); } @Override public void onDestroy() { Log.i("Destroy", "end of app"); super.onDestroy(); // make sure all child threads (python_thread) are stopped android.os.Process.killProcess(android.os.Process.myPid()); } public void loadLibraries() { String app_root = new String(getAppRoot()); File app_root_file = new File(app_root); PythonUtil.loadLibraries(app_root_file, new File(getApplicationInfo().nativeLibraryDir)); } long lastBackClick = 0; @Override public boolean onKeyDown(int keyCode, KeyEvent event) { // Check if the key event was the Back button if (keyCode == KeyEvent.KEYCODE_BACK) { // If there's no web page history, bubble up to the default // system behavior (probably exit the activity) if (SystemClock.elapsedRealtime() - lastBackClick > 2000) { lastBackClick = SystemClock.elapsedRealtime(); Toast.makeText(this, "Tap again to close the app", Toast.LENGTH_LONG).show(); return true; } lastBackClick = SystemClock.elapsedRealtime(); } return super.onKeyDown(keyCode, event); } // ---------------------------------------------------------------------------- // Listener interface for onNewIntent // public interface NewIntentListener { void onNewIntent(Intent intent); } private List newIntentListeners = null; public void registerNewIntentListener(NewIntentListener listener) { if (this.newIntentListeners == null) this.newIntentListeners = Collections.synchronizedList(new ArrayList()); this.newIntentListeners.add(listener); } public void unregisterNewIntentListener(NewIntentListener listener) { if (this.newIntentListeners == null) return; this.newIntentListeners.remove(listener); } @Override protected void onNewIntent(Intent intent) { if (this.newIntentListeners == null) return; this.onResume(); synchronized (this.newIntentListeners) { Iterator iterator = this.newIntentListeners.iterator(); while (iterator.hasNext()) { (iterator.next()).onNewIntent(intent); } } } // ---------------------------------------------------------------------------- // Listener interface for onActivityResult // public interface ActivityResultListener { void onActivityResult(int requestCode, int resultCode, Intent data); } private List activityResultListeners = null; public void registerActivityResultListener(ActivityResultListener listener) { if (this.activityResultListeners == null) this.activityResultListeners = Collections.synchronizedList(new ArrayList()); this.activityResultListeners.add(listener); } public void unregisterActivityResultListener(ActivityResultListener listener) { if (this.activityResultListeners == null) return; this.activityResultListeners.remove(listener); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if (this.activityResultListeners == null) return; this.onResume(); synchronized (this.activityResultListeners) { Iterator iterator = this.activityResultListeners.iterator(); while (iterator.hasNext()) (iterator.next()).onActivityResult(requestCode, resultCode, intent); } } public static void start_service( String serviceTitle, String serviceDescription, String pythonServiceArgument) { _do_start_service(serviceTitle, serviceDescription, pythonServiceArgument, true); } public static void start_service_not_as_foreground( String serviceTitle, String serviceDescription, String pythonServiceArgument) { _do_start_service(serviceTitle, serviceDescription, pythonServiceArgument, false); } public static void _do_start_service( String serviceTitle, String serviceDescription, String pythonServiceArgument, boolean showForegroundNotification) { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath(); String app_root_dir = PythonActivity.mActivity.getAppRoot(); String entry_point = PythonActivity.mActivity.getEntryPoint(app_root_dir + "/service"); serviceIntent.putExtra("androidPrivate", argument); serviceIntent.putExtra("androidArgument", app_root_dir); serviceIntent.putExtra("serviceEntrypoint", "service/" + entry_point); serviceIntent.putExtra("pythonName", "python"); serviceIntent.putExtra("pythonHome", app_root_dir); serviceIntent.putExtra("pythonPath", app_root_dir + ":" + app_root_dir + "/lib"); serviceIntent.putExtra( "serviceStartAsForeground", (showForegroundNotification ? "true" : "false")); serviceIntent.putExtra("serviceTitle", serviceTitle); serviceIntent.putExtra("serviceDescription", serviceDescription); serviceIntent.putExtra("pythonServiceArgument", pythonServiceArgument); PythonActivity.mActivity.startService(serviceIntent); } public static void stop_service() { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); PythonActivity.mActivity.stopService(serviceIntent); } public static native void nativeSetenv(String name, String value); public static native int nativeInit(Object arguments); } class PythonMain implements Runnable { @Override public void run() { PythonActivity.nativeInit(new String[0]); } } ================================================ FILE: pythonforandroid/bootstraps/service_only/build/src/main/jniLibs/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/service_only/build/src/main/res/drawable/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/service_only/build/src/main/res/mipmap/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/service_only/build/templates/AndroidManifest.tmpl.xml ================================================ = 9 %} android:xlargeScreens="true" {% endif %} /> {% for perm in args.permissions %} {% endfor %} {% if args.wakelock %} {% endif %} {% if args.billing_pubkey %} {% endif %} {% for l in args.android_used_libs %} {% endfor %} {% for m in args.meta_data %} {% endfor %} {%- if args.intent_filters -%} {{- args.intent_filters -}} {%- endif -%} {% if service %} {% endif %} {% for name, foreground_type in service_data %} {% endfor %} {% if args.billing_pubkey %} {% endif %} ================================================ FILE: pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java ================================================ package {{ args.package }}; import android.os.Binder; import android.os.IBinder; import android.content.Intent; import android.content.Context; import org.kivy.android.PythonService; public class Service{{ name|capitalize }} extends PythonService { /** * Binder given to clients */ private final IBinder mBinder = new Service{{ name|capitalize }}Binder(); {% if sticky %} /** * {@inheritDoc} */ @Override public int startType() { return START_STICKY; } {% endif %} @Override protected int getServiceId() { return {{ service_id }}; } public static void start(Context ctx, String pythonServiceArgument) { String argument = ctx.getFilesDir().getAbsolutePath() + "/app"; Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); intent.putExtra("androidPrivate", argument); intent.putExtra("androidArgument", argument); intent.putExtra("serviceEntrypoint", "{{ entrypoint }}"); intent.putExtra("serviceTitle", "{{ name|capitalize }}"); intent.putExtra("pythonName", "{{ name }}"); intent.putExtra("serviceStartAsForeground", "{{ foreground|lower }}"); intent.putExtra("pythonHome", argument); intent.putExtra("androidUnpack", argument); intent.putExtra("pythonPath", argument + ":" + argument + "/lib"); intent.putExtra("pythonServiceArgument", pythonServiceArgument); intent.putExtra("smallIconName", ""); intent.putExtra("contentTitle", "{{ name|capitalize }}"); intent.putExtra("contentText", ""); ctx.startService(intent); } public static void start(Context ctx, String smallIconName, String contentTitle, String contentText, String pythonServiceArgument) { String argument = ctx.getFilesDir().getAbsolutePath() + "/app"; Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); intent.putExtra("androidPrivate", argument); intent.putExtra("androidArgument", argument); intent.putExtra("serviceEntrypoint", "{{ entrypoint }}"); intent.putExtra("serviceTitle", "{{ name|capitalize }}"); intent.putExtra("pythonName", "{{ name }}"); intent.putExtra("serviceStartAsForeground", "{{ foreground|lower }}"); intent.putExtra("pythonHome", argument); intent.putExtra("androidUnpack", argument); intent.putExtra("pythonPath", argument + ":" + argument + "/lib"); intent.putExtra("pythonServiceArgument", pythonServiceArgument); intent.putExtra("smallIconName", smallIconName); intent.putExtra("contentTitle", contentTitle); intent.putExtra("contentText", contentText); ctx.startService(intent); } public static void stop(Context ctx) { Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class); ctx.stopService(intent); } /** * {@inheritDoc} */ @Override public IBinder onBind(Intent intent) { return mBinder; } /** * Class used for the client Binder. Because we know this service always * runs in the same process as its clients, we don't need to deal with IPC. */ public class Service{{ name|capitalize }}Binder extends Binder { Service{{ name|capitalize }} getService() { // Return this instance of Service{{ name|capitalize }} so clients can call public methods return Service{{ name|capitalize }}.this; } } } ================================================ FILE: pythonforandroid/bootstraps/service_only/build/templates/strings.tmpl.xml ================================================ {{ args.name }} {{ private_version }} ================================================ FILE: pythonforandroid/bootstraps/settings.gradle ================================================ // Java Lint Project Settings // This project is used for linting Java source files in CI rootProject.name = 'p4a-java-lint' ================================================ FILE: pythonforandroid/bootstraps/webview/__init__.py ================================================ from pythonforandroid.toolchain import Bootstrap class WebViewBootstrap(Bootstrap): name = 'webview' recipe_depends = list( set(Bootstrap.recipe_depends).union({'genericndkbuild'}) ) bootstrap = WebViewBootstrap() ================================================ FILE: pythonforandroid/bootstraps/webview/build/blacklist.txt ================================================ # prevent user to include invalid extensions *.apk *.aab *.apks *.pxd # eggs *.egg-info # unit test unittest/* # python config config/makesetup # unused kivy files (platform specific) kivy/input/providers/wm_* kivy/input/providers/mactouch* kivy/input/providers/probesysfs* kivy/input/providers/mtdev* kivy/input/providers/hidinput* kivy/core/camera/camera_videocapture* kivy/core/spelling/*osx* kivy/core/video/video_pyglet* kivy/tools kivy/tests/* kivy/*/*.h kivy/*/*.pxi # unused encodings lib-dynload/*codec* encodings/cp*.pyo encodings/tis* encodings/shift* encodings/bz2* encodings/iso* encodings/undefined* encodings/johab* encodings/p* encodings/m* encodings/euc* encodings/k* encodings/unicode_internal* encodings/quo* encodings/gb* encodings/big5* encodings/hp* encodings/hz* # unused python modules bsddb/* wsgiref/* hotshot/* pydoc_data/* tty.pyo anydbm.pyo nturl2path.pyo LICENCE.txt macurl2path.pyo dummy_threading.pyo audiodev.pyo antigravity.pyo dumbdbm.pyo sndhdr.pyo __phello__.foo.pyo sunaudio.pyo os2emxpath.pyo multiprocessing/dummy* # unused binaries python modules lib-dynload/termios.so lib-dynload/_lsprof.so lib-dynload/*audioop.so lib-dynload/_hotshot.so lib-dynload/_heapq.so lib-dynload/_json.so lib-dynload/grp.so lib-dynload/resource.so lib-dynload/pyexpat.so lib-dynload/_ctypes_test.so lib-dynload/_testcapi.so # odd files plat-linux3/regen #>sqlite3 # conditional include depending if some recipes are included or not. sqlite3/* lib-dynload/_sqlite3.so # #include #define LOGI(...) do {} while (0) #define LOGE(...) do {} while (0) #include "android/log.h" /* These JNI management functions are taken from SDL2, but modified to refer to pyjnius */ /* #define LOG(n, x) __android_log_write(ANDROID_LOG_INFO, (n), (x)) */ /* #define LOGP(x) LOG("python", (x)) */ #define LOG_TAG "Python_android" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) /* Function headers */ JNIEnv* Android_JNI_GetEnv(void); static void Android_JNI_ThreadDestroyed(void*); static pthread_key_t mThreadKey; static JavaVM* mJavaVM; int Android_JNI_SetupThread(void) { Android_JNI_GetEnv(); return 1; } /* Library init */ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv *env; mJavaVM = vm; LOGI("JNI_OnLoad called"); if ((*mJavaVM)->GetEnv(mJavaVM, (void**) &env, JNI_VERSION_1_4) != JNI_OK) { LOGE("Failed to get the environment using GetEnv()"); return -1; } /* * Create mThreadKey so we can keep track of the JNIEnv assigned to each thread * Refer to http://developer.android.com/guide/practices/design/jni.html for the rationale behind this */ if (pthread_key_create(&mThreadKey, Android_JNI_ThreadDestroyed) != 0) { __android_log_print(ANDROID_LOG_ERROR, "pyjniusjni", "Error initializing pthread key"); } Android_JNI_SetupThread(); return JNI_VERSION_1_4; } JNIEnv* Android_JNI_GetEnv(void) { /* From http://developer.android.com/guide/practices/jni.html * All threads are Linux threads, scheduled by the kernel. * They're usually started from managed code (using Thread.start), but they can also be created elsewhere and then * attached to the JavaVM. For example, a thread started with pthread_create can be attached with the * JNI AttachCurrentThread or AttachCurrentThreadAsDaemon functions. Until a thread is attached, it has no JNIEnv, * and cannot make JNI calls. * Attaching a natively-created thread causes a java.lang.Thread object to be constructed and added to the "main" * ThreadGroup, making it visible to the debugger. Calling AttachCurrentThread on an already-attached thread * is a no-op. * Note: You can call this function any number of times for the same thread, there's no harm in it */ JNIEnv *env; int status = (*mJavaVM)->AttachCurrentThread(mJavaVM, &env, NULL); if(status < 0) { LOGE("failed to attach current thread"); return 0; } /* From http://developer.android.com/guide/practices/jni.html * Threads attached through JNI must call DetachCurrentThread before they exit. If coding this directly is awkward, * in Android 2.0 (Eclair) and higher you can use pthread_key_create to define a destructor function that will be * called before the thread exits, and call DetachCurrentThread from there. (Use that key with pthread_setspecific * to store the JNIEnv in thread-local-storage; that way it'll be passed into your destructor as the argument.) * Note: The destructor is not called unless the stored value is != NULL * Note: You can call this function any number of times for the same thread, there's no harm in it * (except for some lost CPU cycles) */ pthread_setspecific(mThreadKey, (void*) env); return env; } static void Android_JNI_ThreadDestroyed(void* value) { /* The thread is being destroyed, detach it from the Java VM and set the mThreadKey value to NULL as required */ JNIEnv *env = (JNIEnv*) value; if (env != NULL) { (*mJavaVM)->DetachCurrentThread(mJavaVM); pthread_setspecific(mThreadKey, NULL); } } void *WebView_AndroidGetJNIEnv() { return Android_JNI_GetEnv(); } ================================================ FILE: pythonforandroid/bootstraps/webview/build/proguard-project.txt ================================================ # To enable ProGuard in your project, edit project.properties # to define the proguard.config property as described in that file. # # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in ${sdk.dir}/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the ProGuard # include property in project.properties. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: pythonforandroid/bootstraps/webview/build/src/main/assets/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/webview/build/src/main/java/org/kivy/android/PythonActivity.java ================================================ package org.kivy.android; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.PowerManager; import android.os.SystemClock; import android.util.Log; import android.view.KeyEvent; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.webkit.CookieManager; import android.webkit.WebBackForwardList; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.AbsoluteLayout; import android.widget.ImageView; import android.widget.Toast; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import org.renpy.android.ResourceManager; public class PythonActivity extends Activity { // This activity is modified from a mixture of the SDLActivity and // PythonActivity in the SDL2 bootstrap, but removing all the SDL2 // specifics. private static final String TAG = "PythonActivity"; public static PythonActivity mActivity = null; public static boolean mOpenExternalLinksInBrowser = false; /** If shared libraries (e.g. SDL or the native application) could not be loaded. */ public static boolean mBrokenLibraries; protected static ViewGroup mLayout; protected static WebView mWebView; protected static Thread mPythonThread; private ResourceManager resourceManager = null; private Bundle mMetaData = null; private PowerManager.WakeLock mWakeLock = null; public String getAppRoot() { String app_root = getFilesDir().getAbsolutePath() + "/app"; return app_root; } public String getEntryPoint(String search_dir) { /* Get the main file (.pyc|.py) depending on if we * have a compiled version or not. */ List entryPoints = new ArrayList(); entryPoints.add("main.pyc"); // python 3 compiled files for (String value : entryPoints) { File mainFile = new File(search_dir + "/" + value); if (mainFile.exists()) { return value; } } return "main.py"; } public static void initialize() { // The static nature of the singleton and Android quirkyness force us to initialize // everything here // Otherwise, when exiting the app and returning to it, these variables *keep* their pre // exit values mWebView = null; mLayout = null; mBrokenLibraries = false; } @Override protected void onCreate(Bundle savedInstanceState) { Log.v(TAG, "My oncreate running"); resourceManager = new ResourceManager(this); super.onCreate(savedInstanceState); this.mActivity = this; this.showLoadingScreen(); 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"); PythonUtil.unpackAsset(mActivity, "private", app_root_file, true); PythonUtil.unpackPyBundle( mActivity, getApplicationInfo().nativeLibraryDir + "/" + "libpybundle", app_root_file, false); return null; } @Override protected void onPostExecute(String result) { Log.v("Python", "Device: " + android.os.Build.DEVICE); Log.v("Python", "Model: " + android.os.Build.MODEL); PythonActivity.initialize(); // 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(); } 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(); mWebView = new WebView(PythonActivity.mActivity); mWebView.getSettings().setJavaScriptEnabled(true); mWebView.getSettings().setDomStorageEnabled(true); mWebView.loadUrl("file:///android_asset/_load.html"); mWebView.setLayoutParams( new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); mWebView.setWebViewClient( new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { Uri u = Uri.parse(url); if (mOpenExternalLinksInBrowser) { if (!(u.getScheme().equals("file") || u.getHost().equals("127.0.0.1"))) { Intent i = new Intent(Intent.ACTION_VIEW, u); startActivity(i); return true; } } return false; } @Override public void onPageFinished(WebView view, String url) { CookieManager.getInstance().flush(); } }); mLayout = new AbsoluteLayout(PythonActivity.mActivity); mLayout.addView(mWebView); setContentView(mLayout); 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"); 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) { } final Thread pythonThread = new Thread(new PythonMain(), "PythonThread"); PythonActivity.mPythonThread = pythonThread; pythonThread.start(); final Thread wvThread = new Thread(new WebViewLoaderMain(), "WvThread"); wvThread.start(); } } @Override public void onDestroy() { Log.i("Destroy", "end of app"); super.onDestroy(); // make sure all child threads (python_thread) are stopped android.os.Process.killProcess(android.os.Process.myPid()); } public void loadLibraries() { String app_root = new String(getAppRoot()); File app_root_file = new File(app_root); PythonUtil.loadLibraries(app_root_file, new File(getApplicationInfo().nativeLibraryDir)); } public static void loadUrl(String url) { class LoadUrl implements Runnable { private String mUrl; public LoadUrl(String url) { mUrl = url; } public void run() { mWebView.loadUrl(mUrl); } } Log.i(TAG, "Opening URL: " + url); mActivity.runOnUiThread(new LoadUrl(url)); } public static void enableZoom() { mActivity.runOnUiThread( new Runnable() { @Override public void run() { mWebView.getSettings().setBuiltInZoomControls(true); mWebView.getSettings().setDisplayZoomControls(false); } }); } public static ViewGroup getLayout() { return mLayout; } long lastBackClick = 0; @Override public boolean onKeyDown(int keyCode, KeyEvent event) { // Check if the key event was the Back button if (keyCode == KeyEvent.KEYCODE_BACK) { // Go back if there is web page history behind, // but not to the start preloader WebBackForwardList webViewBackForwardList = mWebView.copyBackForwardList(); if (webViewBackForwardList.getCurrentIndex() > 1) { mWebView.goBack(); return true; } // If there's no web page history, bubble up to the default // system behavior (probably exit the activity) if (SystemClock.elapsedRealtime() - lastBackClick > 2000) { lastBackClick = SystemClock.elapsedRealtime(); Toast.makeText(this, "Tap again to close the app", Toast.LENGTH_LONG).show(); return true; } lastBackClick = SystemClock.elapsedRealtime(); } 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 // public interface NewIntentListener { void onNewIntent(Intent intent); } private List newIntentListeners = null; public void registerNewIntentListener(NewIntentListener listener) { if (this.newIntentListeners == null) this.newIntentListeners = Collections.synchronizedList(new ArrayList()); this.newIntentListeners.add(listener); } public void unregisterNewIntentListener(NewIntentListener listener) { if (this.newIntentListeners == null) return; this.newIntentListeners.remove(listener); } @Override protected void onNewIntent(Intent intent) { if (this.newIntentListeners == null) return; this.onResume(); synchronized (this.newIntentListeners) { Iterator iterator = this.newIntentListeners.iterator(); while (iterator.hasNext()) { (iterator.next()).onNewIntent(intent); } } } // ---------------------------------------------------------------------------- // Listener interface for onActivityResult // public interface ActivityResultListener { void onActivityResult(int requestCode, int resultCode, Intent data); } private List activityResultListeners = null; public void registerActivityResultListener(ActivityResultListener listener) { if (this.activityResultListeners == null) this.activityResultListeners = Collections.synchronizedList(new ArrayList()); this.activityResultListeners.add(listener); } public void unregisterActivityResultListener(ActivityResultListener listener) { if (this.activityResultListeners == null) return; this.activityResultListeners.remove(listener); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if (this.activityResultListeners == null) return; this.onResume(); synchronized (this.activityResultListeners) { Iterator iterator = this.activityResultListeners.iterator(); while (iterator.hasNext()) (iterator.next()).onActivityResult(requestCode, resultCode, intent); } } public static void start_service( String serviceTitle, String serviceDescription, String pythonServiceArgument) { _do_start_service(serviceTitle, serviceDescription, pythonServiceArgument, true); } public static void start_service_not_as_foreground( String serviceTitle, String serviceDescription, String pythonServiceArgument) { _do_start_service(serviceTitle, serviceDescription, pythonServiceArgument, false); } public static void _do_start_service( String serviceTitle, String serviceDescription, String pythonServiceArgument, boolean showForegroundNotification) { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath(); String app_root_dir = PythonActivity.mActivity.getAppRoot(); String entry_point = PythonActivity.mActivity.getEntryPoint(app_root_dir + "/service"); serviceIntent.putExtra("androidPrivate", argument); serviceIntent.putExtra("androidArgument", app_root_dir); serviceIntent.putExtra("serviceEntrypoint", "service/" + entry_point); serviceIntent.putExtra("pythonName", "python"); serviceIntent.putExtra("pythonHome", app_root_dir); serviceIntent.putExtra("pythonPath", app_root_dir + ":" + app_root_dir + "/lib"); serviceIntent.putExtra( "serviceStartAsForeground", (showForegroundNotification ? "true" : "false")); serviceIntent.putExtra("serviceTitle", serviceTitle); serviceIntent.putExtra("serviceDescription", serviceDescription); serviceIntent.putExtra("pythonServiceArgument", pythonServiceArgument); PythonActivity.mActivity.startService(serviceIntent); } public static void stop_service() { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); PythonActivity.mActivity.stopService(serviceIntent); } public static native void nativeSetenv(String name, String value); public static native int nativeInit(Object arguments); /** * Used by android.permissions p4a module to register a call back after requesting runtime * permissions */ public interface PermissionsCallback { void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults); } private PermissionsCallback permissionCallback; private boolean havePermissionsCallback = false; public void addPermissionsCallback(PermissionsCallback callback) { permissionCallback = callback; havePermissionsCallback = true; Log.v(TAG, "addPermissionsCallback(): Added callback for onRequestPermissionsResult"); } @Override public void onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) { Log.v(TAG, "onRequestPermissionsResult()"); if (havePermissionsCallback) { Log.v(TAG, "onRequestPermissionsResult passed to callback"); permissionCallback.onRequestPermissionsResult(requestCode, permissions, grantResults); } super.onRequestPermissionsResult(requestCode, permissions, grantResults); } /** Used by android.permissions p4a module to check a permission */ public boolean checkCurrentPermission(String permission) { if (android.os.Build.VERSION.SDK_INT < 23) return true; try { java.lang.reflect.Method methodCheckPermission = Activity.class.getMethod("checkSelfPermission", String.class); Object resultObj = methodCheckPermission.invoke(this, permission); int result = Integer.parseInt(resultObj.toString()); if (result == PackageManager.PERMISSION_GRANTED) return true; } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { } return false; } /** Used by android.permissions p4a module to request runtime permissions */ public void requestPermissionsWithRequestCode(String[] permissions, int requestCode) { if (android.os.Build.VERSION.SDK_INT < 23) return; try { java.lang.reflect.Method methodRequestPermission = Activity.class.getMethod("requestPermissions", String[].class, int.class); methodRequestPermission.invoke(this, permissions, requestCode); } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { } } public void requestPermissions(String[] permissions) { requestPermissionsWithRequestCode(permissions, 1); } } class PythonMain implements Runnable { @Override public void run() { PythonActivity.nativeInit(new String[0]); } } class WebViewLoaderMain implements Runnable { @Override public void run() { WebViewLoader.testConnection(); } } ================================================ FILE: pythonforandroid/bootstraps/webview/build/src/main/jniLibs/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/webview/build/src/main/res/drawable/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/webview/build/src/main/res/layout/main.xml ================================================ ================================================ FILE: pythonforandroid/bootstraps/webview/build/src/main/res/mipmap/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/webview/build/src/main/res/mipmap-anydpi-v26/.gitkeep ================================================ ================================================ FILE: pythonforandroid/bootstraps/webview/build/src/main/res/values/strings.xml ================================================ SDL App 0.1 ================================================ FILE: pythonforandroid/bootstraps/webview/build/templates/AndroidManifest.tmpl.xml ================================================ = 9 %} android:xlargeScreens="true" {% endif %} /> {% for perm in args.permissions %} {% endfor %} {% if args.wakelock %} {% endif %} {% if args.billing_pubkey %} {% endif %} {% for l in args.android_used_libs %} {% endfor %} {% for m in args.meta_data %} {% endfor %} {%- if args.intent_filters -%} {{- args.intent_filters -}} {%- endif -%} {% if service %} {% endif %} {% for name, foreground_type in service_data %} {% endfor %} {% if args.billing_pubkey %} {% endif %} ================================================ FILE: pythonforandroid/bootstraps/webview/build/templates/WebViewLoader.tmpl.java ================================================ package org.kivy.android; import android.util.Log; import java.io.IOException; import java.net.Socket; import java.net.InetSocketAddress; import android.os.SystemClock; import android.os.Handler; import org.kivy.android.PythonActivity; public class WebViewLoader { private static final String TAG = "WebViewLoader"; public static void testConnection() { while (true) { if (WebViewLoader.pingHost("localhost", {{ args.port }}, 100)) { Log.v(TAG, "Successfully pinged localhost:{{ args.port }}"); Handler mainHandler = new Handler(PythonActivity.mActivity.getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { PythonActivity.mActivity.loadUrl("http://127.0.0.1:{{ args.port }}/"); Log.v(TAG, "Loaded webserver in webview"); } }; mainHandler.post(myRunnable); break; } else { Log.v(TAG, "Could not ping localhost:{{ args.port }}"); try { Thread.sleep(100); } catch(InterruptedException e) { Log.v(TAG, "InterruptedException occurred when sleeping"); } } } } public static boolean pingHost(String host, int port, int timeout) { Socket socket = new Socket(); try { socket.connect(new InetSocketAddress(host, port), timeout); socket.close(); return true; } catch (IOException e) { try {socket.close();} catch (IOException f) {return false;} return false; // Either timeout or unreachable or failed DNS lookup. } } } ================================================ FILE: pythonforandroid/bootstraps/webview/build/templates/strings.tmpl.xml ================================================ {{ args.name }} {{ private_version }} {{ args.presplash_color }} ================================================ FILE: pythonforandroid/bootstraps/webview/build/templates/test/build.tmpl.xml ================================================ ================================================ FILE: pythonforandroid/bootstraps/webview/build/templates/test/build.xml.tmpl ================================================ ================================================ FILE: pythonforandroid/bootstraps/webview/build/webview_includes/_load.html ================================================ Python WebView loader

Loading...
================================================ FILE: pythonforandroid/bootstraps/webview/build/webview_includes/_loading_style.css ================================================ h1 { font-size: 30px; color: blue; font-weight: bold; text-align:center; } h2 { text-align:center; } button { margin-left: auto; margin-right: auto; display: block; margin-top: 50px; font-size: 30px; } /* Loader from http://projects.lukehaas.me/css-loaders/#load1 */ .loader, .loader:before, .loader:after { background: #aaaaff; -webkit-animation: load1 1s infinite ease-in-out; animation: load1 1s infinite ease-in-out; width: 1em; height: 4em; } .loader:before, .loader:after { position: absolute; top: 0; content: ''; } .loader:before { left: -1.5em; } .loader { text-indent: -9999em; margin: 8em auto; position: relative; font-size: 11px; -webkit-animation-delay: -0.16s; animation-delay: -0.16s; } .loader:after { left: 1.5em; -webkit-animation-delay: -0.32s; animation-delay: -0.32s; } @-webkit-keyframes load1 { 0%, 80%, 100% { box-shadow: 0 0 #aaaaff; height: 4em; } 40% { box-shadow: 0 -2em #aaaaff; height: 5em; } } @keyframes load1 { 0%, 80%, 100% { box-shadow: 0 0 #aaaaff; height: 4em; } 40% { box-shadow: 0 -2em #aaaaff; height: 5em; } } ================================================ FILE: pythonforandroid/build.py ================================================ from contextlib import suppress import copy import glob import os import json import tempfile from os import environ from os.path import ( abspath, join, realpath, dirname, expanduser, exists, basename ) import re import shutil import subprocess import sh from packaging.utils import parse_wheel_filename from packaging.requirements import Requirement from pythonforandroid.androidndk import AndroidNDK from pythonforandroid.archs import ArchARM, ArchARMv7_a, ArchAarch_64, Archx86, Archx86_64 from pythonforandroid.logger import (info, warning, info_notify, info_main, shprint, Out_Style, Out_Fore) from pythonforandroid.pythonpackage import get_package_name from pythonforandroid.recipe import CythonRecipe, Recipe from pythonforandroid.recommendations import ( check_ndk_version, check_target_api, check_ndk_api, RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API) from pythonforandroid.util import ( current_directory, ensure_dir, BuildInterruptingException, rmdir ) def get_targets(sdk_dir): if exists(join(sdk_dir, 'cmdline-tools', 'latest', 'bin', 'avdmanager')): avdmanager = sh.Command(join(sdk_dir, 'cmdline-tools', 'latest', 'bin', 'avdmanager')) targets = avdmanager('list', 'target').split('\n') elif exists(join(sdk_dir, 'tools', 'bin', 'avdmanager')): avdmanager = sh.Command(join(sdk_dir, 'tools', 'bin', 'avdmanager')) targets = avdmanager('list', 'target').split('\n') elif exists(join(sdk_dir, 'tools', 'android')): android = sh.Command(join(sdk_dir, 'tools', 'android')) targets = android('list').split('\n') else: raise BuildInterruptingException( 'Could not find `android` or `sdkmanager` binaries in Android SDK', instructions='Make sure the path to the Android SDK is correct') return targets def get_available_apis(sdk_dir): targets = get_targets(sdk_dir) apis = [s for s in targets if re.match(r'^ *API level: ', s)] apis = [re.findall(r'[0-9]+', s) for s in apis] apis = [int(s[0]) for s in apis if s] return apis 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 make a debug or release build build_as_debuggable = False # Whether to strip debug symbols in `.so` files with_debug_symbols = False env = environ.copy() # the filepath of toolchain.py root_dir = None # the root dir where builds and dists will be stored storage_dir = None # in which bootstraps are copied for building # and recipes are built build_dir = None distribution = None """The Distribution object representing the current build target location.""" # the Android project folder where everything ends up dist_dir = None # Whether setup.py or similar should be used if present: use_setup_py = False ccache = None # whether to use ccache ndk = None bootstrap = None bootstrap_build_dir = None recipe_build_order = None # Will hold the list of all built recipes python_modules = None # Will hold resolved pure python packages symlink_bootstrap_files = False # If True, will symlink instead of copying during build java_build_tool = 'auto' @property def packages_path(self): '''Where packages are downloaded before being unpacked''' return join(self.storage_dir, 'packages') @property def templates_dir(self): return join(self.root_dir, 'templates') @property def libs_dir(self): """ where Android libs are cached after build but before being placed in dists """ # Was previously hardcoded as self.build_dir/libs 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 directory = join(self.build_dir, 'javaclasses', self.bootstrap.distribution.name) ensure_dir(directory) return directory @property def aars_dir(self): directory = join(self.build_dir, 'aars', self.bootstrap.distribution.name) ensure_dir(directory) return directory @property def python_installs_dir(self): directory = join(self.build_dir, 'python-installs') ensure_dir(directory) return directory def get_python_install_dir(self, arch): return join(self.python_installs_dir, self.bootstrap.distribution.name, arch) def setup_dirs(self, storage_dir): '''Calculates all the storage and build dirs, and makes sure the directories exist where necessary.''' self.storage_dir = expanduser(storage_dir) if ' ' in self.storage_dir: raise ValueError('storage dir path cannot contain spaces, please ' 'specify a path with --storage-dir') self.build_dir = join(self.storage_dir, 'build') self.dist_dir = join(self.storage_dir, 'dists') def ensure_dirs(self): ensure_dir(self.storage_dir) ensure_dir(self.build_dir) ensure_dir(self.dist_dir) ensure_dir(join(self.build_dir, 'bootstrap_builds')) ensure_dir(join(self.build_dir, 'other_builds')) @property def android_api(self): '''The Android API being targeted.''' if self._android_api is None: raise ValueError('Tried to access android_api but it has not ' 'been set - this should not happen, something ' 'went wrong!') return self._android_api @android_api.setter def android_api(self, value): self._android_api = value @property def ndk_api(self): '''The API number compile against''' if self._ndk_api is None: raise ValueError('Tried to access ndk_api but it has not ' 'been set - this should not happen, something ' 'went wrong!') return self._ndk_api @ndk_api.setter def ndk_api(self, value): self._ndk_api = value @property def sdk_dir(self): '''The path to the Android SDK.''' if self._sdk_dir is None: raise ValueError('Tried to access sdk_dir but it has not ' 'been set - this should not happen, something ' 'went wrong!') return self._sdk_dir @sdk_dir.setter def sdk_dir(self, value): self._sdk_dir = value @property def ndk_dir(self): '''The path to the Android NDK.''' if self._ndk_dir is None: raise ValueError('Tried to access ndk_dir but it has not ' 'been set - this should not happen, something ' 'went wrong!') return self._ndk_dir @ndk_dir.setter def ndk_dir(self, value): self._ndk_dir = value def prepare_build_environment(self, user_sdk_dir, user_ndk_dir, user_android_api, user_ndk_api): '''Checks that build dependencies exist and sets internal variables for the Android SDK etc. ..warning:: This *must* be called before trying any build stuff ''' self.ensure_dirs() if self._build_env_prepared: return # Work out where the Android SDK is sdk_dir = None if user_sdk_dir: sdk_dir = user_sdk_dir # This is the old P4A-specific var if sdk_dir is None: sdk_dir = environ.get('ANDROIDSDK', None) # This seems used more conventionally if sdk_dir is None: sdk_dir = environ.get('ANDROID_HOME', None) # Checks in the buildozer SDK dir, useful for debug tests of p4a if sdk_dir is None: possible_dirs = glob.glob(expanduser(join( '~', '.buildozer', 'android', 'platform', 'android-sdk-*'))) possible_dirs = [d for d in possible_dirs if not 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))) 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 ' 'maintain your own SDK download.') sdk_dir = possible_dirs[0] if sdk_dir is None: raise BuildInterruptingException('Android SDK dir was not specified, exiting.') self.sdk_dir = realpath(sdk_dir) # Check what Android API we're using android_api = None if user_android_api: android_api = user_android_api info('Getting Android API version from user argument: {}'.format(android_api)) elif 'ANDROIDAPI' in environ: android_api = environ['ANDROIDAPI'] info('Found Android API target in $ANDROIDAPI: {}'.format(android_api)) else: info('Android API target was not set manually, using ' 'the default of {}'.format(RECOMMENDED_TARGET_API)) android_api = RECOMMENDED_TARGET_API android_api = int(android_api) self.android_api = android_api for arch in self.archs: # Maybe We could remove this one in a near future (ARMv5 is definitely old) check_target_api(android_api, arch) apis = get_available_apis(self.sdk_dir) info('Available Android APIs are ({})'.format( ', '.join(map(str, apis)))) if android_api in apis: info(('Requested API target {} is available, ' 'continuing.').format(android_api)) else: raise BuildInterruptingException( ('Requested API target {} is not available, install ' 'it with the SDK android tool.').format(android_api)) # Find the Android NDK # Could also use ANDROID_NDK, but doesn't look like many tools use this ndk_dir = None if user_ndk_dir: ndk_dir = user_ndk_dir info('Getting NDK dir from from user argument') if ndk_dir is None: # The old P4A-specific dir ndk_dir = environ.get('ANDROIDNDK', None) if ndk_dir is not None: info('Found NDK dir in $ANDROIDNDK: {}'.format(ndk_dir)) if ndk_dir is None: # Apparently the most common convention ndk_dir = environ.get('NDK_HOME', None) if ndk_dir is not None: info('Found NDK dir in $NDK_HOME: {}'.format(ndk_dir)) if ndk_dir is None: # Another convention (with maven?) ndk_dir = environ.get('ANDROID_NDK_HOME', None) if ndk_dir is not None: info('Found NDK dir in $ANDROID_NDK_HOME: {}'.format(ndk_dir)) if ndk_dir is None: # Checks in the buildozer NDK dir, useful # # for debug tests of p4a possible_dirs = glob.glob(expanduser(join( '~', '.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))) 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 ' 'maintain your own NDK download.') ndk_dir = possible_dirs[0] if ndk_dir is None: raise BuildInterruptingException('Android NDK dir was not specified') self.ndk_dir = realpath(ndk_dir) check_ndk_version(ndk_dir) ndk_api = None if user_ndk_api: ndk_api = user_ndk_api info('Getting NDK API version (i.e. minimum supported API) from user argument') elif 'NDKAPI' in environ: ndk_api = environ.get('NDKAPI', None) info('Found Android API target in $NDKAPI') else: ndk_api = min(self.android_api, RECOMMENDED_NDK_API) warning('NDK API target was not set manually, using ' 'the default of {} = min(android-api={}, default ndk-api={})'.format( ndk_api, self.android_api, RECOMMENDED_NDK_API)) ndk_api = int(ndk_api) self.ndk_api = ndk_api check_ndk_api(ndk_api, self.android_api) self.ndk = AndroidNDK(self.ndk_dir) # path to some tools self.ccache = shutil.which("ccache") if not self.ccache: info('ccache is missing, the build will not be optimized in the ' 'future.') try: subprocess.check_output([ "python3", "-m", "cython", "--help", ]) except subprocess.CalledProcessError: warning('Cython for python3 missing. If you are building for ' ' a python 3 target (which is the default)' ' then THINGS WILL BREAK.') self.env["PATH"] = ":".join( [ self.ndk.llvm_bin_dir, self.ndk_dir, f"{self.sdk_dir}/tools", environ.get("PATH"), ] ) def __init__(self): self.include_dirs = [] self._build_env_prepared = False self._sdk_dir = None self._ndk_dir = None self._android_api = None self._ndk_api = None self.ndk = None self.local_recipes = None self.copy_libs = False self.activity_class_name = u'org.kivy.android.PythonActivity' self.service_class_name = u'org.kivy.android.PythonService' # this list should contain all Archs, it is pruned later self.archs = ( ArchARM(self), ArchARMv7_a(self), Archx86(self), Archx86_64(self), ArchAarch_64(self), ) self.root_dir = realpath(dirname(__file__)) # remove the most obvious flags that can break the compilation self.env.pop("LDFLAGS", None) self.env.pop("ARCHFLAGS", None) self.env.pop("CFLAGS", None) self.python_recipe = None # Set by TargetPythonRecipe def set_archs(self, arch_names): all_archs = self.archs new_archs = set() for name in arch_names: matching = [arch for arch in all_archs if arch.arch == name] for match in matching: new_archs.add(match) self.archs = list(new_archs) 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))) 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 def prepare_dist(self): self.bootstrap.prepare_dist_dir() def get_site_packages_dir(self, arch): '''Returns the location of site-packages in the python-install build dir. ''' return self.get_python_install_dir(arch.arch) def get_libs_dir(self, arch): '''The libs dir for a given arch.''' ensure_dir(join(self.libs_dir, arch)) return join(self.libs_dir, arch) def has_lib(self, arch, lib): return exists(join(self.get_libs_dir(arch), lib)) def has_package(self, name, arch=None): # If this is a file path, it'll need special handling: if (name.find("/") >= 0 or name.find("\\") >= 0) and \ name.find("://") < 0: # (:// would indicate an url) if not os.path.exists(name): # Non-existing dir, cannot look this up. return False try: name = get_package_name(os.path.abspath(name)) except ValueError: # Failed to look up any meaningful name. return False # normalize name to remove version tags try: name = Requirement(name).name except Exception: pass # Try to look up recipe by name: try: recipe = Recipe.get_recipe(name, self) except ValueError: pass else: name = getattr(recipe, 'site_packages_name', None) or name name = name.replace('.', '/') site_packages_dir = self.get_site_packages_dir(arch) return (exists(join(site_packages_dir, name)) or exists(join(site_packages_dir, name + '.py')) or exists(join(site_packages_dir, name + '.pyc')) or exists(join(site_packages_dir, name + '.so')) or glob.glob(join(site_packages_dir, name + '-*.egg'))) def not_has_package(self, name, arch=None): return not self.has_package(name, arch) def build_recipes(build_order, python_modules, ctx, project_dir, ignore_project_setup_py=False ): # Put recipes in correct build order info_notify("Recipe build order is {}".format(build_order)) if python_modules: python_modules = sorted(set(python_modules)) info_notify( ('The requirements ({}) were not found as recipes, they will be ' 'installed with pip.').format(', '.join(python_modules))) recipes = [Recipe.get_recipe(name, ctx) for name in build_order] # download is arch independent info_main('# Downloading recipes ') for recipe in recipes: recipe.download_if_necessary() for arch in ctx.archs: info_main('# Building all recipes for arch {}'.format(arch.arch)) info_main('# Unpacking recipes') for recipe in recipes: ensure_dir(recipe.get_build_container_dir(arch.arch)) recipe.prepare_build_dir(arch.arch) info_main('# Prebuilding recipes') # 2) prebuild packages for recipe in recipes: info_main('Prebuilding {} for {}'.format(recipe.name, arch.arch)) recipe.prebuild_arch(arch) recipe.apply_patches(arch) # 3) build packages info_main('# Building recipes') for recipe in recipes: info_main('Building {} for {}'.format(recipe.name, arch.arch)) if recipe.should_build(arch): recipe.build_arch(arch) else: info('{} said it is already built, skipping' .format(recipe.name)) recipe.install_libraries(arch) # 4) biglink everything info_main('# Biglinking object files') if not ctx.python_recipe: biglink(ctx, arch) else: warning( "Context's python recipe found, " "skipping biglink (will this work?)" ) # 5) postbuild packages info_main('# Postbuilding recipes') for recipe in recipes: info_main('Postbuilding {} for {}'.format(recipe.name, arch.arch)) recipe.postbuild_arch(arch) info_main('# Installing pure Python modules') for arch in ctx.archs: run_pymodules_install( ctx, arch, python_modules, project_dir, ignore_setup_py=ignore_project_setup_py ) def project_has_setup_py(project_dir): 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, arch=None): env = env or {} with current_directory(project_dir): info('got setup.py or similar, running project install. ' + '(disable this behavior with --ignore-setup-py)') # Compute & output the constraints we will use: info('Contents that will be used for constraints.txt:') constraints = subprocess.check_output([ join( ctx.build_dir, "venv", "bin", "pip" ), "freeze" ], env=copy.copy(env)) with suppress(AttributeError): constraints = constraints.decode("utf-8", "replace") info(constraints) # Make sure all packages found are fixed in version # by writing a constraint file, to avoid recipes being # upgraded & reinstalled: with open('._tmp_p4a_recipe_constraints.txt', 'wb') as fileh: fileh.write(constraints.encode("utf-8", "replace")) try: info('Populating venv\'s site-packages with ' 'ctx.get_site_packages_dir()...') # Copy dist contents into site-packages for discovery. # Why this is needed: # --target is somewhat evil and messes with discovery of # packages in PYTHONPATH if that also includes the target # folder. So we need to use the regular virtualenv # site-packages folder instead. # Reference: # https://github.com/pypa/pip/issues/6223 ctx_site_packages_dir = os.path.normpath( os.path.abspath(ctx.get_site_packages_dir(arch)) ) venv_site_packages_dir = os.path.normpath(os.path.join( ctx.build_dir, "venv", "lib", [ f for f in os.listdir(os.path.join( ctx.build_dir, "venv", "lib" )) if f.startswith("python") ][0], "site-packages" )) copied_over_contents = [] for f in os.listdir(ctx_site_packages_dir): full_path = os.path.join(ctx_site_packages_dir, f) if not os.path.exists(os.path.join( venv_site_packages_dir, f )): if os.path.isdir(full_path): shutil.copytree(full_path, os.path.join( venv_site_packages_dir, f )) else: shutil.copy2(full_path, os.path.join( venv_site_packages_dir, f )) copied_over_contents.append(f) # Get listing of virtualenv's site-packages, to see the # newly added things afterwards & copy them back into # the distribution folder / build context site-packages: previous_venv_contents = os.listdir( venv_site_packages_dir ) # Actually run setup.py: info('Launching package install...') shprint(sh.bash, '-c', ( "'" + join( ctx.build_dir, "venv", "bin", "pip" ).replace("'", "'\"'\"'") + "' " + "install -c ._tmp_p4a_recipe_constraints.txt -v ." ).format(ctx.get_site_packages_dir(arch). replace("'", "'\"'\"'")), _env=copy.copy(env)) # Go over all new additions and copy them back: info('Copying additions resulting from setup.py back ' 'into ctx.get_site_packages_dir()...') new_venv_additions = [] for f in (set(os.listdir(venv_site_packages_dir)) - set(previous_venv_contents)): new_venv_additions.append(f) full_path = os.path.join(venv_site_packages_dir, f) if os.path.isdir(full_path): shutil.copytree(full_path, os.path.join( ctx_site_packages_dir, f )) else: shutil.copy2(full_path, os.path.join( ctx_site_packages_dir, f )) # Undo all the changes we did to the venv-site packages: info('Reverting additions to ' 'virtualenv\'s site-packages...') for f in set(copied_over_contents + new_venv_additions): full_path = os.path.join(venv_site_packages_dir, f) if os.path.isdir(full_path): rmdir(full_path) else: os.remove(full_path) finally: os.remove("._tmp_p4a_recipe_constraints.txt") def is_wheel_platform_independent(whl_name): name, version, build, tags = parse_wheel_filename(whl_name) return all(tag.platform == "any" for tag in tags) def process_python_modules(ctx, modules): """Use pip --dry-run to resolve dependencies and filter for pure-Python packages """ modules = list(modules) build_order = list(ctx.recipe_build_order) _requirement_names = [] processed_modules = [] for module in modules+build_order: try: # we need to normalize names # eg Requests>=2.0 becomes requests _requirement_names.append(Requirement(module).name) except Exception: # name parsing failed; skip processing this module via pip processed_modules.append(module) if module in modules: modules.remove(module) if len(processed_modules) > 0: warning(f'Ignored by module resolver : {processed_modules}') # preserve the original module list processed_modules.extend(modules) if len(modules) == 0: return processed_modules # temp file for pip report fd, path = tempfile.mkstemp() os.close(fd) # setup hostpython recipe env = environ.copy() try: host_recipe = Recipe.get_recipe("hostpython3", ctx) _python_path = host_recipe.get_path_to_python() libdir = glob.glob(join(_python_path, "build", "lib*")) env['PYTHONPATH'] = host_recipe.site_dir + ":" + join( _python_path, "Modules") + ":" + (libdir[0] if libdir else "") pip = host_recipe.pip except Exception: # hostpython3 non available so we use system pip (like in tests) pip = sh.Command("pip") try: shprint( pip, 'install', *modules, '--dry-run', '--break-system-packages', '--ignore-installed', '--report', path, '-q', _env=env ) except Exception as e: warning(f"Auto module resolution failed: {e}") return processed_modules with open(path, "r") as f: try: report = json.load(f) except Exception: report = {} os.remove(path) if "install" not in report.keys(): # pip changed json reporting format? warning("Auto module resolution failed: invalid json!") return processed_modules info('Extra resolved pure python dependencies :') ignored_str = " (ignored)" # did we find any non pure python package? any_not_pure_python = False # just for style info(" ") for module in report["install"]: mname = module["metadata"]["name"] mver = module["metadata"]["version"] filename = basename(module["download_info"]["url"]) pure_python = True if (filename.endswith(".whl") and not is_wheel_platform_independent(filename)): any_not_pure_python = True pure_python = False # does this module matches any recipe name? if mname.lower().replace("-", "_") in _requirement_names: continue color = Out_Fore.GREEN if pure_python else Out_Fore.RED ignored = "" if pure_python else ignored_str info( f" {color}{mname}{Out_Fore.WHITE} : " f"{Out_Style.BRIGHT}{mver}{Out_Style.RESET_ALL}" f"{ignored}" ) if pure_python: processed_modules.append(f"{mname}=={mver}") info(" ") if any_not_pure_python: warning("Some packages were ignored because they are not pure Python.") warning("To install the ignored packages, explicitly list them in your requirements file.") return processed_modules def run_pymodules_install(ctx, arch, modules, project_dir=None, ignore_setup_py=False): """ This function will take care of all non-recipe things, by: 1. Processing them from --requirements (the modules argument) and installing them 2. Installing the user project/app itself via setup.py if ignore_setup_py=True """ info('*** PYTHON PACKAGE / PROJECT INSTALL STAGE FOR ARCH: {} ***'.format(arch)) modules = process_python_modules(ctx, modules) modules = [m for m in modules if ctx.not_has_package(m, arch)] # We change current working directory later, so this has to be an absolute # path or `None` in case that we didn't supply the `project_dir` via kwargs project_dir = abspath(project_dir) if project_dir else None # Bail out if no python deps and no setup.py to process: if not modules and ( ignore_setup_py or not project_has_setup_py(project_dir) ): info('No Python modules and no setup.py to process, skipping') return # Output messages about what we're going to do: if modules: info( "The requirements ({}) don\'t have recipes, attempting to " "install them with pip".format(', '.join(modules)) ) info( "If this fails, it may mean that the module has compiled " "components and needs a recipe." ) if project_has_setup_py(project_dir) and not ignore_setup_py: info( "Will process project install, if it fails then the " "project may not be compatible for Android install." ) # Use our hostpython to create the virtualenv host_python = sh.Command(ctx.hostpython) with current_directory(join(ctx.build_dir)): shprint(host_python, '-m', 'venv', 'venv') # Prepare base environment and upgrade pip: base_env = dict(copy.copy(os.environ)) base_env["PYTHONPATH"] = ctx.get_site_packages_dir(arch) info('Upgrade pip to latest version') shprint(sh.bash, '-c', ( "source venv/bin/activate && pip install -U pip" ), _env=copy.copy(base_env)) # Install Cython in case modules need it to build: info('Install Cython in case one of the modules needs it to build') shprint(sh.bash, '-c', ( "venv/bin/pip install Cython" ), _env=copy.copy(base_env)) # Get environment variables for build (with CC/compiler set): standard_recipe = CythonRecipe() standard_recipe.ctx = ctx # (note: following line enables explicit -lpython... linker options) standard_recipe.call_hostpython_via_targetpython = False recipe_env = standard_recipe.get_recipe_env(ctx.archs[0]) env = copy.copy(base_env) env.update(recipe_env) # Make sure our build package dir is available, and the virtualenv # site packages come FIRST (so the proper pip version is used): env["PYTHONPATH"] += ":" + ctx.get_site_packages_dir(arch) env["PYTHONPATH"] = os.path.abspath(join( ctx.build_dir, "venv", "lib", "python" + ctx.python_recipe.major_minor_version_string, "site-packages")) + ":" + env["PYTHONPATH"] # Install the manually specified requirements first: if not modules: info('There are no Python modules to install, skipping') else: info('Creating a requirements.txt file for the Python modules') with open('requirements.txt', 'w') as fileh: for module in modules: key = 'VERSION_' + module if key in environ: line = '{}=={}\n'.format(module, environ[key]) else: line = '{}\n'.format(module) fileh.write(line) info('Installing Python modules with pip') info( "IF THIS FAILS, THE MODULES MAY NEED A RECIPE. " "A reason for this is often modules compiling " "native code that is unaware of Android cross-compilation " "and does not work without additional " "changes / workarounds." ) shprint(sh.bash, '-c', ( "venv/bin/pip " + "install -v --target '{0}' --no-deps -r requirements.txt" ).format(ctx.get_site_packages_dir(arch).replace("'", "'\"'\"'")), _env=copy.copy(env)) # Afterwards, run setup.py if present: if project_has_setup_py(project_dir) and not ignore_setup_py: run_setuppy_install(ctx, project_dir, env, arch) elif not ignore_setup_py: info("No setup.py found in project directory: " + str(project_dir)) # Strip object files after potential Cython or native code builds: if not ctx.with_debug_symbols: standard_recipe.strip_object_files( arch, env, build_dir=ctx.build_dir ) def biglink(ctx, arch): # First, collate object files from each recipe info('Collating object files from each recipe') obj_dir = join(ctx.bootstrap.build_dir, 'collated_objects') ensure_dir(obj_dir) recipes = [Recipe.get_recipe(name, ctx) for name in ctx.recipe_build_order] for recipe in recipes: recipe_obj_dir = join(recipe.get_build_container_dir(arch.arch), 'objects_{}'.format(recipe.name)) if not exists(recipe_obj_dir): info('{} recipe has no biglinkable files dir, skipping' .format(recipe.name)) continue files = glob.glob(join(recipe_obj_dir, '*')) if not len(files): info('{} recipe has no biglinkable files, skipping' .format(recipe.name)) continue info('{} recipe has object files, copying'.format(recipe.name)) files.append(obj_dir) shprint(sh.cp, '-r', *files) env = arch.get_env() env['LDFLAGS'] = env['LDFLAGS'] + ' -L{}'.format( join(ctx.bootstrap.build_dir, 'obj', 'local', arch.arch)) if not len(glob.glob(join(obj_dir, '*'))): info('There seem to be no libraries to biglink, skipping.') return info('Biglinking') info('target {}'.format(join(ctx.get_libs_dir(arch.arch), 'libpymodules.so'))) do_biglink = copylibs_function if ctx.copy_libs else biglink_function # Move to the directory containing crtstart_so.o and crtend_so.o # This is necessary with newer NDKs? A gcc bug? with current_directory(arch.ndk_lib_dir): do_biglink( join(ctx.get_libs_dir(arch.arch), 'libpymodules.so'), obj_dir.split(' '), extra_link_dirs=[join(ctx.bootstrap.build_dir, 'obj', 'local', arch.arch), os.path.abspath('.')], env=env) 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 = [] for directory in objs_paths: for fn in os.listdir(directory): fn = os.path.join(directory, fn) if not fn.endswith(".so.o"): continue if not os.path.exists(fn[:-2] + ".libs"): continue sofiles.append(fn[:-2]) # The raw argument list. args = [] for fn in sofiles: afn = fn + ".o" libsfn = fn + ".libs" args.append(afn) with open(libsfn) as fd: data = fd.read() args.extend(data.split(" ")) unique_args = [] while args: a = args.pop() if a in ('-L', ): continue if a not in unique_args: unique_args.insert(0, a) for dir in extra_link_dirs: link = '-L{}'.format(dir) if link not in unique_args: unique_args.append(link) cc_name = env['CC'] cc = sh.Command(cc_name.split()[0]) cc = cc.bake(*cc_name.split()[1:]) shprint(cc, '-shared', '-O3', '-o', soname, *unique_args, _env=env) 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*$') blacklist_libs = ( 'c', 'stdc++', 'dl', 'python2.7', 'sdl', 'sdl_image', 'sdl_ttf', 'z', 'm', 'GLESv2', 'jpeg', 'png', 'log', # bootstrap takes care of sdl2 libs (if applicable) 'SDL2', 'SDL2_ttf', 'SDL2_image', 'SDL2_mixer', 'SDL3', 'SDL3_ttf', 'SDL3_image', 'SDL3_mixer', ) found_libs = [] sofiles = [] if env and 'READELF' in env: readelf = env['READELF'] elif 'READELF' in os.environ: readelf = os.environ['READELF'] else: readelf = shutil.which('readelf').strip() readelf = sh.Command(readelf).bake('-d') dest = dirname(soname) for directory in objs_paths: for fn in os.listdir(directory): fn = join(directory, fn) if not fn.endswith('.libs'): continue dirfn = fn[:-1] + 'dirs' if not exists(dirfn): continue with open(fn) as f: libs = f.read().strip().split(' ') needed_libs = [lib for lib in libs if lib and lib not in blacklist_libs and lib not in found_libs] while needed_libs: print('need libs:\n\t' + '\n\t'.join(needed_libs)) start_needed_libs = needed_libs[:] found_sofiles = [] with open(dirfn) as f: libdirs = f.read().split() for libdir in libdirs: if not needed_libs: break if libdir == dest: # don't need to copy from dest to dest! continue libdir = libdir.strip() print('scanning', libdir) for lib in needed_libs[:]: if lib in found_libs: continue if lib.endswith('.a'): needed_libs.remove(lib) found_libs.append(lib) continue lib_a = 'lib' + lib + '.a' libpath_a = join(libdir, lib_a) lib_so = 'lib' + lib + '.so' libpath_so = join(libdir, lib_so) plain_so = lib + '.so' plainpath_so = join(libdir, plain_so) sopath = None if exists(libpath_so): sopath = libpath_so elif exists(plainpath_so): sopath = plainpath_so if sopath: print('found', lib, 'in', libdir) found_sofiles.append(sopath) needed_libs.remove(lib) found_libs.append(lib) continue if exists(libpath_a): print('found', lib, '(static) in', libdir) needed_libs.remove(lib) found_libs.append(lib) continue for sofile in found_sofiles: print('scanning dependencies for', sofile) out = readelf(sofile) for line in out.splitlines(): needso = re_needso.match(line) if needso: lib = needso.group(1) if (lib not in needed_libs and lib not in found_libs and lib not in blacklist_libs): needed_libs.append(needso.group(1)) sofiles += found_sofiles if needed_libs == start_needed_libs: raise RuntimeError( 'Failed to locate needed libraries!\n\t' + '\n\t'.join(needed_libs)) print('Copying libraries') shprint(sh.cp, *sofiles, dest) ================================================ FILE: pythonforandroid/checkdependencies.py ================================================ from importlib import import_module from os import environ import sys from packaging.version import Version from pythonforandroid.prerequisites import ( check_and_install_default_prerequisites, ) def check_python_dependencies(): """ Check if the Python requirements are installed. This must appears before other imports because otherwise they're imported elsewhere. Using the ok check instead of failing immediately so that all errors are printed at once. """ ok = True modules = [("colorama", "0.3.3"), "appdirs", ("sh", "1.10"), "jinja2"] for module in modules: if isinstance(module, tuple): module, version = module else: version = None try: import_module(module) except ImportError: if version is None: print( "ERROR: The {} Python module could not be found, please " "install it.".format(module) ) ok = False else: print( "ERROR: The {} Python module could not be found, " "please install version {} or higher".format( module, version ) ) ok = False else: if version is None: continue try: cur_ver = sys.modules[module].__version__ except AttributeError: # this is sometimes not available continue if Version(cur_ver) < Version(version): print( "ERROR: {} version is {}, but python-for-android needs " "at least {}.".format(module, cur_ver, version) ) ok = False if not ok: print("python-for-android is exiting due to the errors logged above") exit(1) def check(): if not environ.get("SKIP_PREREQUISITES_CHECK", "0") == "1": check_and_install_default_prerequisites() check_python_dependencies() ================================================ FILE: pythonforandroid/distribution.py ================================================ import json import glob from os.path import exists, join from pythonforandroid.logger import ( debug, info, info_notify, warning, Err_Style, Err_Fore) from pythonforandroid.util import ( current_directory, BuildInterruptingException, rmdir) class Distribution: '''State container for information about a distribution (i.e. an Android project). This is separate from a Bootstrap because the Bootstrap is concerned with building and populating the dist directory, whereas the dist itself could also come from e.g. a binary download. ''' ctx = None name = None # A name identifying the dist. May not be None. needs_build = False # Whether the dist needs compiling url = None dist_dir = None # Where the dist dir ultimately is. Should not be None. ndk_api = None archs = [] '''The names of the arch targets that the dist is built for.''' recipes = [] description = '' # A long description def __init__(self, ctx): self.ctx = ctx def __str__(self): return ''.format( # self.name, ', '.join([recipe.name for recipe in self.recipes])) self.name, ', '.join(self.recipes)) def __repr__(self): return str(self) @classmethod def get_distribution( cls, ctx, *, archs, # required keyword argument: there is no sensible default name=None, recipes=[], ndk_api=None, force_build=False, extra_dist_dirs=[], require_perfect_match=False, allow_replace_dist=True ): '''Takes information about the distribution, and decides what kind of distribution it will be. If parameters conflict (e.g. a dist with that name already exists, but doesn't have the right set of recipes), an error is thrown. Parameters ---------- name : str The name of the distribution. If a dist with this name already ' exists, it will be used. ndk_api : int The NDK API to compile against, included in the dist because it cannot be changed later during APK packaging. archs : list The target architectures list to compile against, included in the dist because it cannot be changed later during APK packaging. recipes : list The recipes that the distribution must contain. force_download: bool If True, only downloaded dists are considered. force_build : bool If True, the dist is forced to be built locally. extra_dist_dirs : list Any extra directories in which to search for dists. require_perfect_match : bool If True, will only match distributions with precisely the correct set of recipes. allow_replace_dist : bool If True, will allow an existing dist with the specified name but incompatible requirements to be overwritten by a new one with the current requirements. ''' possible_dists = Distribution.get_distributions(ctx) debug(f"All possible dists: {possible_dists}") # Will hold dists that would be built in the same folder as an existing dist folder_match_dist = None # 0) Check if a dist with that name and architecture already exists if name is not None and name: possible_dists = [ d for d in possible_dists if (d.name == name) and all(arch_name in d.archs for arch_name in archs)] debug(f"Dist matching name and arch: {possible_dists}") if possible_dists: # There should only be one folder with a given dist name *and* arch. # We could check that here, but for compatibility let's let it slide # and just record the details of one of them. We only use this data to # possibly fail the build later, so it doesn't really matter if there # was more than one clash. folder_match_dist = possible_dists[0] # 1) Check if any existing dists meet the requirements _possible_dists = [] for dist in possible_dists: if ( ndk_api is not None and dist.ndk_api != ndk_api ) or dist.ndk_api is None: debug( f"dist {dist} failed to match ndk_api, target api {ndk_api}, dist api {dist.ndk_api}" ) continue for recipe in recipes: if recipe not in dist.recipes: debug(f"dist {dist} missing recipe {recipe}") break else: _possible_dists.append(dist) possible_dists = _possible_dists debug(f"Dist matching ndk_api and recipe: {possible_dists}") if possible_dists: info('Of the existing distributions, the following meet ' 'the given requirements:') pretty_log_dists(possible_dists) else: info('No existing dists meet the given requirements!') # If any dist has perfect recipes, arch and NDK API, return it for dist in possible_dists: if force_build: debug("Skipping dist due to forced build") continue if ndk_api is not None and dist.ndk_api != ndk_api: debug("Skipping dist due to ndk_api mismatch") continue if not all(arch_name in dist.archs for arch_name in archs): debug("Skipping dist due to arch mismatch") continue if (set(dist.recipes) == set(recipes) or (set(recipes).issubset(set(dist.recipes)) and not require_perfect_match)): info_notify('{} has compatible recipes, using this one' .format(dist.name)) return dist else: debug( f"Skipping dist due to recipes mismatch, expected {set(recipes)}, actual {set(dist.recipes)}" ) # If there was a name match but we didn't already choose it, # then the existing dist is incompatible with the requested # configuration and the build cannot continue if folder_match_dist is not None and not allow_replace_dist: raise BuildInterruptingException( 'Asked for dist with name {name} with recipes ({req_recipes}) and ' 'NDK API {req_ndk_api}, but a dist ' 'with this name already exists and has either incompatible recipes ' '({dist_recipes}) or NDK API {dist_ndk_api}'.format( name=name, req_ndk_api=ndk_api, dist_ndk_api=folder_match_dist.ndk_api, req_recipes=', '.join(recipes), dist_recipes=', '.join(folder_match_dist.recipes))) assert len(possible_dists) < 2 # If we got this far, we need to build a new dist dist = Distribution(ctx) dist.needs_build = True if not name: filen = 'unnamed_dist_{}' i = 1 while exists(join(ctx.dist_dir, filen.format(i))): i += 1 name = filen.format(i) dist.name = name dist.dist_dir = join( ctx.dist_dir, name) dist.recipes = recipes dist.ndk_api = ctx.ndk_api dist.archs = archs return dist def folder_exists(self): return exists(self.dist_dir) def delete(self): rmdir(self.dist_dir) @classmethod def get_distributions(cls, ctx, extra_dist_dirs=[]): '''Returns all the distributions found locally.''' if extra_dist_dirs: raise BuildInterruptingException( 'extra_dist_dirs argument to get_distributions ' 'is not yet implemented') dist_dir = ctx.dist_dir folders = glob.glob(join(dist_dir, '*')) for dir in extra_dist_dirs: folders.extend(glob.glob(join(dir, '*'))) dists = [] for folder in folders: if exists(join(folder, 'dist_info.json')): with open(join(folder, 'dist_info.json')) as fileh: dist_info = json.load(fileh) dist = cls(ctx) dist.name = dist_info['dist_name'] dist.dist_dir = folder dist.needs_build = False dist.recipes = dist_info['recipes'] if 'archs' in dist_info: dist.archs = dist_info['archs'] if 'ndk_api' in dist_info: dist.ndk_api = dist_info['ndk_api'] else: dist.ndk_api = None warning( "Distribution {distname}: ({distdir}) has been " "built with an unknown api target, ignoring it, " "you might want to delete it".format( distname=dist.name, distdir=dist.dist_dir ) ) dists.append(dist) return dists def save_info(self, dirn): ''' Save information about the distribution in its dist_dir. ''' with current_directory(dirn): info('Saving distribution info') with open('dist_info.json', 'w') as fileh: json.dump({'dist_name': self.name, 'bootstrap': self.ctx.bootstrap.name, 'archs': [arch.arch for arch in self.ctx.archs], 'ndk_api': self.ctx.ndk_api, 'use_setup_py': self.ctx.use_setup_py, 'recipes': self.ctx.recipe_build_order + self.ctx.python_modules, 'hostpython': self.ctx.hostpython, 'python_version': self.ctx.python_recipe.major_minor_version_string}, fileh) def pretty_log_dists(dists, log_func=info): infos = [] for dist in dists: ndk_api = 'unknown' if dist.ndk_api is None else dist.ndk_api infos.append('{Fore.GREEN}{Style.BRIGHT}{name}{Style.RESET_ALL}: min API {ndk_api}, ' 'includes recipes ({Fore.GREEN}{recipes}' '{Style.RESET_ALL}), built for archs ({Fore.BLUE}' '{archs}{Style.RESET_ALL})'.format( ndk_api=ndk_api, name=dist.name, recipes=', '.join(dist.recipes), archs=', '.join(dist.archs) if dist.archs else 'UNKNOWN', Fore=Err_Fore, Style=Err_Style)) for line in infos: log_func('\t' + line) ================================================ FILE: pythonforandroid/entrypoints.py ================================================ from pythonforandroid.recommendations import check_python_version from pythonforandroid.util import BuildInterruptingException, handle_build_exception def main(): """ Main entrypoint for running python-for-android as a script. """ try: # Check the Python version before importing anything heavier than # the util functions. This lets us provide a nice message about # incompatibility rather than having the interpreter crash if it # reaches unsupported syntax from a newer Python version. check_python_version() from pythonforandroid.toolchain import ToolchainCL ToolchainCL() except BuildInterruptingException as exc: handle_build_exception(exc) ================================================ FILE: pythonforandroid/graph.py ================================================ from copy import deepcopy from itertools import product from pythonforandroid.logger import info from pythonforandroid.recipe import Recipe from pythonforandroid.bootstrap import Bootstrap from pythonforandroid.util import BuildInterruptingException def fix_deplist(deps): """ Turn a dependency list into lowercase, and make sure all entries that are just a string become a tuple of strings """ deps = [ ((dep.lower(),) if not isinstance(dep, (list, tuple)) else tuple([dep_entry.lower() for dep_entry in dep ])) for dep in deps ] return deps class RecipeOrder(dict): def __init__(self, ctx): self.ctx = ctx def conflicts(self): for name in self.keys(): try: recipe = Recipe.get_recipe(name, self.ctx) conflicts = [dep.lower() for dep in recipe.conflicts] except ValueError: conflicts = [] if any([c in self for c in conflicts]): return True return False def get_dependency_tuple_list_for_recipe(recipe, blacklist=None): """ Get the dependencies of a recipe with filtered out blacklist, and turned into tuples with fix_deplist() """ if blacklist is None: blacklist = set() assert type(blacklist) is set if recipe.depends is None: dependencies = [] else: # Turn all dependencies into tuples so that product will work dependencies = fix_deplist(recipe.depends) # Filter out blacklisted items and turn lowercase: dependencies = [ tuple(set(deptuple) - blacklist) for deptuple in dependencies if tuple(set(deptuple) - blacklist) ] return dependencies def recursively_collect_orders( name, ctx, all_inputs, orders=None, blacklist=None ): '''For each possible recipe ordering, try to add the new recipe name to that order. Recursively do the same thing with all the dependencies of each recipe. ''' name = name.lower() if orders is None: orders = [] if blacklist is None: blacklist = set() try: recipe = Recipe.get_recipe(name, ctx) dependencies = get_dependency_tuple_list_for_recipe( recipe, blacklist=blacklist ) # handle opt_depends: these impose requirements on the build # order only if already present in the list of recipes to build dependencies.extend(fix_deplist( [[d] for d in recipe.get_opt_depends_in_list(all_inputs) if d.lower() not in blacklist] )) if recipe.conflicts is None: conflicts = [] else: conflicts = [dep.lower() for dep in recipe.conflicts] except ValueError: # The recipe does not exist, so we assume it can be installed # via pip with no extra dependencies dependencies = [] conflicts = [] new_orders = [] # for each existing recipe order, see if we can add the new recipe name for order in orders: if name in order: new_orders.append(deepcopy(order)) continue if order.conflicts(): continue if any([conflict in order for conflict in conflicts]): continue for dependency_set in product(*dependencies): new_order = deepcopy(order) new_order[name] = set(dependency_set) dependency_new_orders = [new_order] for dependency in dependency_set: dependency_new_orders = recursively_collect_orders( dependency, ctx, all_inputs, dependency_new_orders, blacklist=blacklist ) new_orders.extend(dependency_new_orders) return new_orders def find_order(graph): ''' Do a topological sort on the dependency graph dict. ''' while graph: # Find all items without a parent leftmost = [name for name, dep in graph.items() if not dep] if not leftmost: raise ValueError('Dependency cycle detected! %s' % graph) # If there is more than one, sort them for predictable order leftmost.sort() for result in leftmost: # Yield and remove them from the graph yield result graph.pop(result) for bset in graph.values(): bset.discard(result) def obvious_conflict_checker(ctx, name_tuples, blacklist=None): """ This is a pre-flight check function that will completely ignore recipe order or choosing an actual value in any of the multiple choice tuples/dependencies, and just do a very basic obvious conflict check. """ deps_were_added_by = dict() deps = set() if blacklist is None: blacklist = set() # Add dependencies for all recipes: to_be_added = [(name_tuple, None) for name_tuple in name_tuples] while len(to_be_added) > 0: 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) is 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 # (Remember this function only catches obvious issues) deps.add(added_tuple) continue name = added_tuple[0] recipe_conflicts = set() recipe_dependencies = [] try: # Get recipe to add and who's ultimately adding it: recipe = Recipe.get_recipe(name, ctx) recipe_conflicts = {c.lower() for c in recipe.conflicts} recipe_dependencies = get_dependency_tuple_list_for_recipe( recipe, blacklist=blacklist ) except ValueError: pass adder_first_recipe_name = adding_recipe or name # Collect the conflicts: triggered_conflicts = [] for dep_tuple_list in deps: # See if the new deps conflict with things added before: if set(dep_tuple_list).intersection( recipe_conflicts) == set(dep_tuple_list): triggered_conflicts.append(dep_tuple_list) continue # See if what was added before conflicts with the new deps: if len(dep_tuple_list) > 1: # Not an obvious commitment to a specific recipe/dep # to be added, so we won't check. # (remember this function only catches obvious issues) continue try: dep_recipe = Recipe.get_recipe(dep_tuple_list[0], ctx) except ValueError: continue conflicts = [c.lower() for c in dep_recipe.conflicts] if name in conflicts: triggered_conflicts.append(dep_tuple_list) # Throw error on conflict: if triggered_conflicts: # Get first conflict and see who added that one: adder_second_recipe_name = "'||'".join(triggered_conflicts[0]) second_recipe_original_adder = deps_were_added_by.get( (adder_second_recipe_name,), None ) if second_recipe_original_adder: adder_second_recipe_name = second_recipe_original_adder # Prompt error: raise BuildInterruptingException( "Conflict detected: '{}'" " inducing dependencies {}, and '{}'" " inducing conflicting dependencies {}".format( adder_first_recipe_name, (recipe.name,), adder_second_recipe_name, triggered_conflicts[0] )) # Actually add it to our list: deps.add(added_tuple) deps_were_added_by[added_tuple] = adding_recipe # Schedule dependencies to be added to_be_added += [ (dep, adder_first_recipe_name or name) for dep in recipe_dependencies if dep not in deps ] # If we came here, then there were no obvious conflicts. return None def get_recipe_order_and_bootstrap(ctx, names, bs=None, blacklist=None): # Get set of recipe/dependency names, clean up and add bootstrap deps: names = set(names) if bs is not None and bs.recipe_depends: names = names.union(set(bs.recipe_depends)) names = fix_deplist([ ([name] if not isinstance(name, (list, tuple)) else name) for name in names ]) if blacklist is None: blacklist = set() blacklist = {bitem.lower() for bitem in blacklist} # Remove all values that are in the blacklist: names_before_blacklist = list(names) names = [] for name in names_before_blacklist: cleaned_up_tuple = tuple([ item for item in name if item not in blacklist ]) if cleaned_up_tuple: names.append(cleaned_up_tuple) # Do check for obvious conflicts (that would trigger in any order, and # without committing to any specific choice in a multi-choice tuple of # dependencies): obvious_conflict_checker(ctx, names, blacklist=blacklist) # If we get here, no obvious conflicts! # get all possible order graphs, as names may include tuples/lists # of alternative dependencies possible_orders = [] for name_set in product(*names): new_possible_orders = [RecipeOrder(ctx)] for name in name_set: new_possible_orders = recursively_collect_orders( name, ctx, name_set, orders=new_possible_orders, blacklist=blacklist ) possible_orders.extend(new_possible_orders) # turn each order graph into a linear list if possible orders = [] for possible_order in possible_orders: try: order = find_order(possible_order) except ValueError: # a circular dependency was found info('Circular dependency found in graph {}, skipping it.'.format( possible_order)) continue orders.append(list(order)) # prefer python3 and SDL2 if available orders = sorted(orders, key=lambda order: -('python3' in order) - ('sdl2' in order)) if not orders: raise BuildInterruptingException( 'Didn\'t find any valid dependency graphs. ' 'This means that some of your ' 'requirements pull in conflicting dependencies.') # It would be better to check against possible orders other # than the first one, but in practice clashes will be rare, # and can be resolved by specifying more parameters chosen_order = orders[0] if len(orders) > 1: info('Found multiple valid dependency orders:') for order in orders: info(' {}'.format(order)) info('Using the first of these: {}'.format(chosen_order)) else: info('Found a single valid recipe set: {}'.format(chosen_order)) if bs is None: bs = Bootstrap.get_bootstrap_from_recipes(chosen_order, ctx) if bs is None: # Note: don't remove this without thought, causes infinite loop raise BuildInterruptingException( "Could not find any compatible bootstrap!" ) recipes, python_modules, bs = get_recipe_order_and_bootstrap( ctx, chosen_order, bs=bs, blacklist=blacklist ) else: # check if each requirement has a recipe recipes = [] python_modules = [] for name in chosen_order: try: recipe = Recipe.get_recipe(name, ctx) python_modules += recipe.python_depends except ValueError: python_modules.append(name) else: recipes.append(name) python_modules = list(set(python_modules)) return recipes, python_modules, bs ================================================ FILE: pythonforandroid/includes/arm64-v8a/machine/cpu-features.h ================================================ #ifndef _ARM64_CPU_FEATURES #define _ARM64_CPU_FEATURES #define __ARM_ARCH__ 8 #define __ARM_HAVE_HALFWORD_MULTIPLY 1 #endif // _ARM64_CPU_FEATURES ================================================ FILE: pythonforandroid/logger.py ================================================ import logging import os import re import sh from sys import stdout, stderr from math import log10 from collections import defaultdict from colorama import Style as Colo_Style, Fore as Colo_Fore # monkey patch to show full output sh.ErrorReturnCode.truncate_cap = 999999 class LevelDifferentiatingFormatter(logging.Formatter): def format(self, record): if record.levelno > 30: record.msg = '{}{}[ERROR]{}{}: '.format( Err_Style.BRIGHT, Err_Fore.RED, Err_Fore.RESET, Err_Style.RESET_ALL) + record.msg elif record.levelno > 20: record.msg = '{}{}[WARNING]{}{}: '.format( Err_Style.BRIGHT, Err_Fore.RED, Err_Fore.RESET, Err_Style.RESET_ALL) + record.msg elif record.levelno > 10: record.msg = '{}[INFO]{}: '.format( Err_Style.BRIGHT, Err_Style.RESET_ALL) + record.msg else: record.msg = '{}{}[DEBUG]{}{}: '.format( Err_Style.BRIGHT, Err_Fore.LIGHTBLACK_EX, Err_Fore.RESET, Err_Style.RESET_ALL) + record.msg return super().format(record) logger = logging.getLogger('p4a') # Necessary as importlib reloads this, # which would add a second handler and reset the level if not hasattr(logger, 'touched'): logger.setLevel(logging.INFO) logger.touched = True ch = logging.StreamHandler(stderr) formatter = LevelDifferentiatingFormatter('%(message)s') ch.setFormatter(formatter) logger.addHandler(ch) info = logger.info debug = logger.debug warning = logger.warning error = logger.error class colorama_shim: def __init__(self, real): self._dict = defaultdict(str) self._real = real self._enabled = False def __getattr__(self, key): return getattr(self._real, key) if self._enabled else self._dict[key] def enable(self, enable): self._enabled = enable Out_Style = colorama_shim(Colo_Style) Out_Fore = colorama_shim(Colo_Fore) Err_Style = colorama_shim(Colo_Style) Err_Fore = colorama_shim(Colo_Fore) def setup_color(color): enable_out = (False if color == 'never' else True if color == 'always' else stdout.isatty()) Out_Style.enable(enable_out) Out_Fore.enable(enable_out) enable_err = (False if color == 'never' else True if color == 'always' else stderr.isatty()) Err_Style.enable(enable_err) Err_Fore.enable(enable_err) def info_main(*args): logger.info(''.join([Err_Style.BRIGHT, Err_Fore.GREEN] + list(args) + [Err_Style.RESET_ALL, Err_Fore.RESET])) def info_notify(s): info('{}{}{}{}'.format(Err_Style.BRIGHT, Err_Fore.LIGHTBLUE_EX, s, Err_Style.RESET_ALL)) def shorten_string(string, max_width): ''' make limited length string in form: "the string is very lo...(and 15 more)" ''' string_len = len(string) if string_len <= max_width: return string visible = max_width - 16 - int(log10(string_len)) # expected suffix len "...(and XXXXX more)" if not isinstance(string, str): visstring = str(string[:visible], errors='ignore') else: visstring = string[:visible] return u''.join((visstring, u'...(and ', str(string_len - visible), u' more)')) def get_console_width(): try: cols = int(os.environ['COLUMNS']) except (KeyError, ValueError): pass else: if cols >= 25: return cols try: cols = max(25, int(os.popen('stty size', 'r').read().split()[1])) except Exception: pass else: return cols return 100 def shprint(command, *args, **kwargs): '''Runs the command (which should be an sh.Command instance), while logging the output.''' kwargs["_iter"] = True kwargs["_out_bufsize"] = 1 kwargs["_err_to_out"] = True kwargs["_bg"] = True is_critical = kwargs.pop('_critical', False) tail_n = kwargs.pop('_tail', None) full_debug = False if "P4A_FULL_DEBUG" in os.environ: tail_n = 0 full_debug = True filter_in = kwargs.pop('_filter', None) filter_out = kwargs.pop('_filterout', None) if len(logger.handlers) > 1: logger.removeHandler(logger.handlers[1]) columns = get_console_width() command_path = str(command).split('/') command_string = command_path[-1] string = ' '.join(['{}->{} running'.format(Out_Fore.LIGHTBLACK_EX, Out_Style.RESET_ALL), command_string] + list(args)) # If logging is not in DEBUG mode, trim the command if necessary if logger.level > logging.DEBUG: logger.info('{}{}'.format(shorten_string(string, columns - 12), Err_Style.RESET_ALL)) else: logger.debug('{}{}'.format(string, Err_Style.RESET_ALL)) need_closing_newline = False try: msg_hdr = ' working: ' msg_width = columns - len(msg_hdr) - 1 output = command(*args, **kwargs) for line in output: if isinstance(line, bytes): line = line.decode('utf-8', errors='replace') if logger.level > logging.DEBUG: if full_debug: stdout.write(line) stdout.flush() continue msg = line.replace( '\n', ' ').replace( '\t', ' ').replace( '\b', ' ').rstrip() if msg: if "CI" not in os.environ: stdout.write(u'{}\r{}{:<{width}}'.format( Err_Style.RESET_ALL, msg_hdr, shorten_string(msg, msg_width), width=msg_width)) stdout.flush() need_closing_newline = True else: logger.debug(''.join(['\t', line.rstrip()])) if need_closing_newline: stdout.write('{}\r{:>{width}}\r'.format( Err_Style.RESET_ALL, ' ', width=(columns - 1))) stdout.flush() except sh.ErrorReturnCode as err: if need_closing_newline: stdout.write('{}\r{:>{width}}\r'.format( Err_Style.RESET_ALL, ' ', width=(columns - 1))) stdout.flush() if tail_n is not None or filter_in or filter_out: def printtail(out, name, forecolor, tail_n=0, re_filter_in=None, re_filter_out=None): lines = out.splitlines() if re_filter_in is not None: lines = [line for line in lines if re_filter_in.search(line)] if re_filter_out is not None: lines = [line for line in lines if not re_filter_out.search(line)] if tail_n == 0 or len(lines) <= tail_n: info('{}:\n{}\t{}{}'.format( name, forecolor, '\t\n'.join(lines), Out_Fore.RESET)) else: info('{} (last {} lines of {}):\n{}\t{}{}'.format( name, tail_n, len(lines), forecolor, '\t\n'.join([s for s in lines[-tail_n:]]), Out_Fore.RESET)) printtail(err.stdout.decode('utf-8'), 'STDOUT', Out_Fore.YELLOW, tail_n, re.compile(filter_in) if filter_in else None, re.compile(filter_out) if filter_out else None) printtail(err.stderr.decode('utf-8'), 'STDERR', Err_Fore.RED) if is_critical or full_debug: env = kwargs.get("_env") if env is not None: info("{}ENV:{}\n{}\n".format( Err_Fore.YELLOW, Err_Fore.RESET, "\n".join( "export {}='{}'".format(n, v) for n, v in env.items()))) info("{}COMMAND:{}\ncd {} && {} {}\n".format( Err_Fore.YELLOW, Err_Fore.RESET, os.getcwd(), command, ' '.join(args))) warning("{}ERROR: {} failed!{}".format( Err_Fore.RED, command, Err_Fore.RESET)) if is_critical: exit(1) else: raise return output ================================================ FILE: pythonforandroid/patching.py ================================================ """ Helper functions for recipes. Recipes must supply a list of patches. Patches consist of a filename and an optional conditional, which is any function of the form: def patch_check(arch: string, recipe : Recipe) -> bool This library provides some helpful conditionals and mechanisms to join multiple conditionals. Example: patches = [ ("linux_or_darwin_only.patch", check_any(is_linux, is_darwin), ("recent_android_API.patch", is_apt_gte(27)), ] """ from platform import uname from packaging.version import Version # Platform checks def is_platform(platform): """ Returns true if the host platform matches the parameter given. """ def check(arch, recipe): return uname().system.lower() == platform.lower() return check is_linux = is_platform("Linux") is_darwin = is_platform("Darwin") is_windows = is_platform("Windows") def is_arch(xarch): """ Returns true if the target architecture platform matches the parameter given. """ def check(arch): return arch.arch == xarch return check # Android API comparisons: # Return true if the Android API level being targeted # is equal (or >, >=, <, <= as appropriate) the given parameter def is_api(apiver: int): def check(arch, recipe): return recipe.ctx.android_api == apiver return check def is_api_gt(apiver: int): def check(arch, recipe): return recipe.ctx.android_api > apiver return check def is_api_gte(apiver: int): def check(arch, recipe): return recipe.ctx.android_api >= apiver return check def is_api_lt(apiver: int): def check(arch, recipe): return recipe.ctx.android_api < apiver return check def is_api_lte(apiver: int): def check(arch, recipe): return recipe.ctx.android_api <= apiver return check # Android API comparisons: def is_ndk(ndk): """ Return true if the Minimum Supported Android NDK level being targeted is equal the given parameter (which should be an AndroidNDK instance) """ def check(arch, recipe): return recipe.ctx.ndk == ndk return check # Recipe Version comparisons: # These compare the Recipe's version with the provided string (or # Packaging.Version). # # Warning: Both strings must conform to PEP 440 - e.g. "3.2.1" or "1.0rc1" def is_version_gt(version): """Return true if the Recipe's version is greater""" def check(arch, recipe): return Version(recipe.version) > Version(version) return check def is_version_lt(version): """Return true if the Recipe's version is less than""" def check(arch, recipe): return Version(recipe.version) < Version(version) return check def version_starts_with(version_prefix): def check(arch, recipe): return recipe.version.startswith(version_prefix) return check # Will Build def will_build(recipe_name): """Return true if the recipe with this name is planned to be included in the distribution.""" def check(arch, recipe): return recipe_name in recipe.ctx.recipe_build_order return check # Conjunctions def check_all(*patch_checks): """ Given a collection of patch_checks as params, return if all returned true. """ def check(arch, recipe): return all(patch_check(arch, recipe) for patch_check in patch_checks) return check def check_any(*patch_checks): """ Given a collection of patch_checks as params, return if any returned true. """ def check(arch, recipe): return any(patch_check(arch, recipe) for patch_check in patch_checks) return check ================================================ FILE: pythonforandroid/prerequisites.py ================================================ #!/usr/bin/env python3 import os import platform import shutil import subprocess import sys from pythonforandroid.logger import info, warning, error from pythonforandroid.util import ensure_dir class Prerequisite(object): name = "Default" homebrew_formula_name = "" mandatory = dict(linux=False, darwin=False) installer_is_supported = dict(linux=False, darwin=False) def is_valid(self): if self.checker(): info(f"Prerequisite {self.name} is met") return (True, "") elif not self.mandatory[sys.platform]: warning( f"Prerequisite {self.name} is not met, but is marked as non-mandatory" ) else: error(f"Prerequisite {self.name} is not met") def checker(self): if sys.platform == "darwin": return self.darwin_checker() elif sys.platform == "linux": return self.linux_checker() else: raise Exception("Unsupported platform") def ask_to_install(self): if ( os.environ.get("PYTHONFORANDROID_PREREQUISITES_INSTALL_INTERACTIVE", "1") == "1" ): res = input( f"Do you want automatically install prerequisite {self.name}? [y/N] " ) if res.lower() == "y": return True else: return False else: info( "Session is not interactive (usually this happens during a CI run), so let's consider it as a YES" ) return True def install(self): info(f"python-for-android can automatically install prerequisite: {self.name}") if self.ask_to_install(): if sys.platform == "darwin": self.darwin_installer() elif sys.platform == "linux": self.linux_installer() else: raise Exception("Unsupported platform") else: info( f"Skipping installation of prerequisite {self.name} as per user request" ) def show_helper(self): if sys.platform == "darwin": self.darwin_helper() elif sys.platform == "linux": self.linux_helper() else: raise Exception("Unsupported platform") def install_is_supported(self): return self.installer_is_supported[sys.platform] def linux_checker(self): raise Exception(f"Unsupported prerequisite check on linux for {self.name}") def darwin_checker(self): raise Exception(f"Unsupported prerequisite check on macOS for {self.name}") def linux_installer(self): raise Exception(f"Unsupported prerequisite installer on linux for {self.name}") def darwin_installer(self): raise Exception(f"Unsupported prerequisite installer on macOS for {self.name}") def darwin_helper(self): info(f"No helper available for prerequisite: {self.name} on macOS") def linux_helper(self): info(f"No helper available for prerequisite: {self.name} on linux") def _darwin_get_brew_formula_location_prefix(self, formula, installed=False): opts = ["--installed"] if installed else [] p = subprocess.Popen( ["brew", "--prefix", formula, *opts], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) _stdout_res, _stderr_res = p.communicate() if p.returncode != 0: error(_stderr_res.decode("utf-8").strip()) return None else: return _stdout_res.decode("utf-8").strip() def darwin_pkg_config_location(self): warning( f"pkg-config location is not supported on macOS for prerequisite: {self.name}" ) return "" def linux_pkg_config_location(self): warning( f"pkg-config location is not supported on linux for prerequisite: {self.name}" ) return "" @property def pkg_config_location(self): if sys.platform == "darwin": return self.darwin_pkg_config_location() elif sys.platform == "linux": return self.linux_pkg_config_location() class HomebrewPrerequisite(Prerequisite): name = "homebrew" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=False) def darwin_checker(self): return shutil.which("brew") is not None def darwin_helper(self): info( "Installer for homebrew is not yet supported on macOS," "the nice news is that the installation process is easy!" "See: https://brew.sh for further instructions." ) class JDKPrerequisite(Prerequisite): name = "JDK" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=True) supported_version = 17 def darwin_checker(self): if "JAVA_HOME" in os.environ: info("Found JAVA_HOME environment variable, using it") jdk_path = os.environ["JAVA_HOME"] else: jdk_path = self._darwin_get_libexec_jdk_path(version=None) return self._darwin_jdk_is_supported(jdk_path) def _darwin_get_libexec_jdk_path(self, version=None): version_args = [] if version is not None: version_args = ["-v", version] return ( subprocess.run( ["/usr/libexec/java_home", *version_args], stdout=subprocess.PIPE, ) .stdout.strip() .decode() ) def _darwin_jdk_is_supported(self, jdk_path): if not jdk_path: return False javac_bin = os.path.join(jdk_path, "bin", "javac") if not os.path.exists(javac_bin): return False p = subprocess.Popen( [javac_bin, "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) _stdout_res, _stderr_res = p.communicate() if p.returncode != 0: error("Failed to run javac to check JDK version") return False if not _stdout_res: _stdout_res = _stderr_res res = _stdout_res.strip().decode() major_version = int(res.split(" ")[-1].split(".")[0]) if major_version == self.supported_version: info(f"Found a valid JDK at {jdk_path}") return True else: error(f"JDK version {major_version} is not supported") return False def darwin_helper(self): info( f"python-for-android requires a JDK {self.supported_version} to be installed on macOS," "but seems like you don't have one installed." ) info( "If you think that a valid JDK is already installed, please verify that " f"you have a JDK {self.supported_version} installed and that `/usr/libexec/java_home` " "shows the correct path." ) info( "If you have multiple JDK installations, please make sure that you have " "`JAVA_HOME` environment variable set to the correct JDK installation." ) def darwin_installer(self): info( f"Looking for a JDK {self.supported_version} installation which is not the default one ..." ) jdk_path = self._darwin_get_libexec_jdk_path(version=str(self.supported_version)) if not self._darwin_jdk_is_supported(jdk_path): info(f"We're unlucky, there's no JDK {self.supported_version} or higher installation available") base_url = "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/" if platform.machine() == "arm64": filename = "OpenJDK17U-jdk_aarch64_mac_hotspot_17.0.2_8.tar.gz" else: filename = "OpenJDK17U-jdk_x64_mac_hotspot_17.0.2_8.tar.gz" info(f"Downloading {filename} from {base_url}") subprocess.check_output( [ "curl", "-L", f"{base_url}{filename}", "-o", f"/tmp/{filename}", ] ) user_library_java_path = os.path.expanduser( "~/Library/Java/JavaVirtualMachines" ) info(f"Extracting {filename} to {user_library_java_path}") ensure_dir(user_library_java_path) subprocess.check_output( ["tar", "xzf", f"/tmp/{filename}", "-C", user_library_java_path], ) jdk_path = self._darwin_get_libexec_jdk_path(version="17.0.2+8") info(f"Setting JAVA_HOME to {jdk_path}") os.environ["JAVA_HOME"] = jdk_path class OpenSSLPrerequisite(Prerequisite): name = "openssl" homebrew_formula_name = "openssl@3" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=True) def darwin_checker(self): return ( self._darwin_get_brew_formula_location_prefix( self.homebrew_formula_name, installed=True ) is not None ) def darwin_pkg_config_location(self): return os.path.join( self._darwin_get_brew_formula_location_prefix(self.homebrew_formula_name), "lib/pkgconfig", ) def darwin_installer(self): info("Installing OpenSSL ...") subprocess.check_output(["brew", "install", self.homebrew_formula_name]) class AutoconfPrerequisite(Prerequisite): name = "autoconf" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=True) def darwin_checker(self): return ( self._darwin_get_brew_formula_location_prefix("autoconf", installed=True) is not None ) def darwin_installer(self): info("Installing Autoconf ...") subprocess.check_output(["brew", "install", "autoconf"]) class AutomakePrerequisite(Prerequisite): name = "automake" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=True) def darwin_checker(self): return ( self._darwin_get_brew_formula_location_prefix("automake", installed=True) is not None ) def darwin_installer(self): info("Installing Automake ...") subprocess.check_output(["brew", "install", "automake"]) class LibtoolPrerequisite(Prerequisite): name = "libtool" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=True) def darwin_checker(self): return ( self._darwin_get_brew_formula_location_prefix("libtool", installed=True) is not None ) def darwin_installer(self): info("Installing Libtool ...") subprocess.check_output(["brew", "install", "libtool"]) class PkgConfigPrerequisite(Prerequisite): name = "pkg-config" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=True) def darwin_checker(self): return ( self._darwin_get_brew_formula_location_prefix("pkg-config", installed=True) is not None ) def darwin_installer(self): info("Installing Pkg-Config ...") subprocess.check_output(["brew", "install", "pkg-config"]) class CmakePrerequisite(Prerequisite): name = "cmake" mandatory = dict(linux=False, darwin=True) installer_is_supported = dict(linux=False, darwin=True) def darwin_checker(self): return ( self._darwin_get_brew_formula_location_prefix("cmake", installed=True) is not None ) def darwin_installer(self): info("Installing cmake ...") subprocess.check_output(["brew", "install", "cmake"]) def get_required_prerequisites(platform="linux"): return [ prerequisite_cls() for prerequisite_cls in [ HomebrewPrerequisite, AutoconfPrerequisite, AutomakePrerequisite, LibtoolPrerequisite, PkgConfigPrerequisite, CmakePrerequisite, OpenSSLPrerequisite, JDKPrerequisite, ] if prerequisite_cls.mandatory.get(platform, False) ] def check_and_install_default_prerequisites(): prerequisites_not_met = [] warning( "prerequisites.py is experimental and does not support all prerequisites yet." ) warning("Please report any issues to the python-for-android issue tracker.") # Phase 1: Check if all prerequisites are met and add the ones # which are not to `prerequisites_not_met` for prerequisite in get_required_prerequisites(sys.platform): if not prerequisite.is_valid(): prerequisites_not_met.append(prerequisite) # Phase 2: Setup/Install all prerequisites that are not met # (where possible), otherwise show an helper. for prerequisite in prerequisites_not_met: prerequisite.show_helper() if prerequisite.install_is_supported(): prerequisite.install() if __name__ == "__main__": check_and_install_default_prerequisites() ================================================ FILE: pythonforandroid/pythonpackage.py ================================================ """ This module offers highlevel functions to get package metadata like the METADATA file, the name, or a list of dependencies. Usage examples: # Getting package name from pip reference: from pythonforandroid.pythonpackage import get_package_name print(get_package_name("pillow")) # Outputs: "Pillow" (note the spelling!) # Getting package dependencies: from pythonforandroid.pythonpackage import get_package_dependencies print(get_package_dependencies("pep517")) # Outputs: "['pytoml']" # Get package name from arbitrary package source: from pythonforandroid.pythonpackage import get_package_name print(get_package_name("/some/local/project/folder/")) # Outputs package name NOTE: Yes, this module doesn't fit well into python-for-android, but this functionality isn't available ANYWHERE ELSE, and upstream (pip, ...) currently has no interest in taking this over, so it has no other place to go. (Unless someone reading this puts it into yet another packaging lib) Reference discussion/upstream inclusion attempt: https://github.com/pypa/packaging-problems/issues/247 """ import functools from io import open # needed for python 2 import os import shutil import subprocess import sys import tarfile import tempfile import time from urllib.parse import unquote as urlunquote from urllib.parse import urlparse import zipfile import toml import build.util from pythonforandroid.util import rmdir, ensure_dir def transform_dep_for_pip(dependency): if dependency.find("@") > 0 and ( dependency.find("@") < dependency.find("://") or "://" not in dependency ): # WORKAROUND FOR UPSTREAM BUG: # https://github.com/pypa/pip/issues/6097 # (Please REMOVE workaround once that is fixed & released upstream!) # # Basically, setup_requires() can contain a format pip won't install # from a requirements.txt (PEP 508 URLs). # To avoid this, translate to an #egg= reference: if dependency.endswith("#"): dependency = dependency[:-1] url = (dependency.partition("@")[2].strip().partition("#egg")[0] + "#egg=" + dependency.partition("@")[0].strip() ) return url return dependency def extract_metainfo_files_from_package( package, output_folder, debug=False ): """ Extracts metadata files from the given package to the given folder, which may be referenced in any way that is permitted in a requirements.txt file or install_requires=[] listing. Current supported metadata files that will be extracted: - pytoml.yml (only if package wasn't obtained as wheel) - METADATA """ if package is None: raise ValueError("package cannot be None") if not os.path.exists(output_folder) or os.path.isfile(output_folder): raise ValueError("output folder needs to be existing folder") if debug: print("extract_metainfo_files_from_package: extracting for " + "package: " + str(package)) # A temp folder for making a package copy in case it's a local folder, # because extracting metadata might modify files # (creating sdists/wheels...) temp_folder = tempfile.mkdtemp(prefix="pythonpackage-package-copy-") try: # Package is indeed a folder! Get a temp copy to work on: if is_filesystem_path(package): shutil.copytree( parse_as_folder_reference(package), os.path.join(temp_folder, "package"), ignore=shutil.ignore_patterns(".tox") ) package = os.path.join(temp_folder, "package") _extract_metainfo_files_from_package_unsafe(package, output_folder) finally: rmdir(temp_folder) def _get_system_python_executable(): """ Returns the path the system-wide python binary. (In case we're running in a virtualenv or venv) """ # This function is required by get_package_as_folder() to work # inside a virtualenv, since venv creation will fail with # the virtualenv's local python binary. # (venv/virtualenv incompatibility) # Abort if not in virtualenv or venv: if not hasattr(sys, "real_prefix") and ( not hasattr(sys, "base_prefix") or os.path.normpath(sys.base_prefix) == os.path.normpath(sys.prefix)): return sys.executable # Extract prefix we need to look in: if hasattr(sys, "real_prefix"): search_prefix = sys.real_prefix # virtualenv else: search_prefix = sys.base_prefix # venv def python_binary_from_folder(path): def binary_is_usable(python_bin): """ Helper function to see if a given binary name refers to a usable python interpreter binary """ # Abort if path isn't present at all or a directory: if not os.path.exists( os.path.join(path, python_bin) ) or os.path.isdir(os.path.join(path, python_bin)): return # We should check file not found anyway trying to run it, # since it might be a dead symlink: try: filenotfounderror = FileNotFoundError except NameError: # Python 2 filenotfounderror = OSError try: # Run it and see if version output works with no error: subprocess.check_output([ os.path.join(path, python_bin), "--version" ], stderr=subprocess.STDOUT) return True except (subprocess.CalledProcessError, filenotfounderror): return False python_name = "python" + sys.version while (not binary_is_usable(python_name) and python_name.find(".") > 0): # Try less specific binary name: python_name = python_name.rpartition(".")[0] if binary_is_usable(python_name): return os.path.join(path, python_name) return None # Return from sys.real_prefix if present: result = python_binary_from_folder(search_prefix) if result is not None: return result # Check out all paths in $PATH: bad_candidates = [] good_candidates = [] ever_had_nonvenv_path = False ever_had_path_starting_with_prefix = False for p in os.environ.get("PATH", "").split(":"): # Skip if not possibly the real system python: if not os.path.normpath(p).startswith( os.path.normpath(search_prefix) ): continue ever_had_path_starting_with_prefix = True # First folders might be virtualenv/venv we want to avoid: if not ever_had_nonvenv_path: sep = os.path.sep if ( ("system32" not in p.lower() and "usr" not in p and not p.startswith("/opt/python")) or {"home", ".tox"}.intersection(set(p.split(sep))) or "users" in p.lower() ): # Doesn't look like bog-standard system path. if (p.endswith(os.path.sep + "bin") or p.endswith(os.path.sep + "bin" + os.path.sep)): # Also ends in "bin" -> likely virtualenv/venv. # Add as unfavorable / end of candidates: bad_candidates.append(p) continue ever_had_nonvenv_path = True good_candidates.append(p) # If we have a bad env with PATH not containing any reference to our # real python (travis, why would you do that to me?) then just guess # based from the search prefix location itself: if not ever_had_path_starting_with_prefix: # ... and yes we're scanning all the folders for that, it's dumb # but i'm not aware of a better way: (@JonasT) for root, dirs, files in os.walk(search_prefix, topdown=True): for name in dirs: bad_candidates.append(os.path.join(root, name)) # Sort candidates by length (to prefer shorter ones): def candidate_cmp(a, b): return len(a) - len(b) good_candidates = sorted( good_candidates, key=functools.cmp_to_key(candidate_cmp) ) bad_candidates = sorted( bad_candidates, key=functools.cmp_to_key(candidate_cmp) ) # See if we can now actually find the system python: for p in good_candidates + bad_candidates: result = python_binary_from_folder(p) if result is not None: return result raise RuntimeError( "failed to locate system python in: {}" " - checked candidates were: {}, {}" .format(sys.real_prefix, good_candidates, bad_candidates) ) def get_package_as_folder(dependency): """ This function downloads the given package / dependency and extracts the raw contents into a folder. Afterwards, it returns a tuple with the type of distribution obtained, and the temporary folder it extracted to. It is the caller's responsibility to delete the returned temp folder after use. Examples of returned values: ("source", "/tmp/pythonpackage-venv-e84toiwjw") ("wheel", "/tmp/pythonpackage-venv-85u78uj") What the distribution type will be depends on what pip decides to download. """ venv_parent = tempfile.mkdtemp( prefix="pythonpackage-venv-" ) try: # Create a venv to install into: try: if int(sys.version.partition(".")[0]) < 3: # Python 2.x has no venv. subprocess.check_output([ sys.executable, # no venv conflict possible, # -> no need to use system python "-m", "virtualenv", "--python=" + _get_system_python_executable(), os.path.join(venv_parent, 'venv') ], cwd=venv_parent) else: # On modern Python 3, use venv. subprocess.check_output([ _get_system_python_executable(), "-m", "venv", os.path.join(venv_parent, 'venv') ], cwd=venv_parent) except subprocess.CalledProcessError as e: output = e.output.decode('utf-8', 'replace') raise ValueError( 'venv creation unexpectedly ' + 'failed. error output: ' + str(output) ) venv_path = os.path.join(venv_parent, "venv") # Update pip and wheel in venv for latest feature support: try: filenotfounderror = FileNotFoundError except NameError: # Python 2. filenotfounderror = OSError try: subprocess.check_output([ os.path.join(venv_path, "bin", "pip"), "install", "-U", "pip", "wheel", ]) except filenotfounderror: raise RuntimeError( "venv appears to be missing pip. " "did we fail to use a proper system python??\n" "system python path detected: {}\n" "os.environ['PATH']: {}".format( _get_system_python_executable(), os.environ.get("PATH", "") ) ) # Create download subfolder: ensure_dir(os.path.join(venv_path, "download")) # Write a requirements.txt with our package and download: with open(os.path.join(venv_path, "requirements.txt"), "w", encoding="utf-8" ) as f: def to_unicode(s): # Needed for Python 2. try: return s.decode("utf-8") except AttributeError: return s f.write(to_unicode(transform_dep_for_pip(dependency))) try: subprocess.check_output( [ os.path.join(venv_path, "bin", "pip"), "download", "--no-deps", "-r", "../requirements.txt", "-d", os.path.join(venv_path, "download") ], stderr=subprocess.STDOUT, cwd=os.path.join(venv_path, "download") ) except subprocess.CalledProcessError as e: raise RuntimeError("package download failed: " + str(e.output)) if len(os.listdir(os.path.join(venv_path, "download"))) == 0: # No download. This can happen if the dependency has a condition # which prohibits install in our environment. # (the "package ; ... conditional ... " type of condition) return (None, None) # Get the result and make sure it's an extracted directory: result_folder_or_file = os.path.join( venv_path, "download", os.listdir(os.path.join(venv_path, "download"))[0] ) dl_type = "source" if not os.path.isdir(result_folder_or_file): # Must be an archive. if result_folder_or_file.endswith((".zip", ".whl")): if result_folder_or_file.endswith(".whl"): dl_type = "wheel" with zipfile.ZipFile(result_folder_or_file) as f: f.extractall(os.path.join(venv_path, "download", "extracted" )) result_folder_or_file = os.path.join( venv_path, "download", "extracted" ) elif result_folder_or_file.find(".tar.") > 0: # Probably a tarball. with tarfile.open(result_folder_or_file) as f: f.extractall(os.path.join(venv_path, "download", "extracted" )) result_folder_or_file = os.path.join( venv_path, "download", "extracted" ) else: raise RuntimeError( "unknown archive or download " + "type: " + str(result_folder_or_file) ) # If the result is hidden away in an additional subfolder, # descend into it: while os.path.isdir(result_folder_or_file) and \ len(os.listdir(result_folder_or_file)) == 1 and \ os.path.isdir(os.path.join( result_folder_or_file, os.listdir(result_folder_or_file)[0] )): result_folder_or_file = os.path.join( result_folder_or_file, os.listdir(result_folder_or_file)[0] ) # Copy result to new dedicated folder so we can throw away # our entire virtualenv nonsense after returning: result_path = tempfile.mkdtemp() rmdir(result_path) shutil.copytree(result_folder_or_file, result_path) return (dl_type, result_path) finally: rmdir(venv_parent) def _extract_metainfo_files_from_package_unsafe( package, output_path ): # This is the unwrapped function that will # 1. make lots of stdout/stderr noise # 2. possibly modify files (if the package source is a local folder) # Use extract_metainfo_files_from_package_folder instead which avoids # these issues. clean_up_path = False path_type = "source" path = parse_as_folder_reference(package) if path is None: # This is not a path. Download it: (path_type, path) = get_package_as_folder(package) if path_type is None: # Download failed. raise ValueError( "cannot get info for this package, " + "pip says it has no downloads (conditional dependency?)" ) clean_up_path = True try: metadata_path = None if path_type != "wheel": # Use a build helper function to fetch the metadata directly metadata = build.util.project_wheel_metadata(path) # And write it to a file metadata_path = os.path.join(output_path, "built_metadata") with open(metadata_path, 'w') as f: for key in metadata.keys(): for value in metadata.get_all(key): f.write("{}: {}\n".format(key, value)) else: # This is a wheel, so metadata should be in *.dist-info folder: metadata_path = os.path.join( path, [f for f in os.listdir(path) if f.endswith(".dist-info")][0], "METADATA" ) # Store type of metadata source. Can be "wheel", "source" for source # distribution, and others get_package_as_folder() may support # in the future. with open(os.path.join(output_path, "metadata_source"), "w") as f: try: f.write(path_type) except TypeError: # in python 2 path_type may be str/bytes: f.write(path_type.decode("utf-8", "replace")) # Copy the metadata file: shutil.copyfile(metadata_path, os.path.join(output_path, "METADATA")) finally: if clean_up_path: rmdir(path) def is_filesystem_path(dep): """ Convenience function around parse_as_folder_reference() to check if a dependency refers to a folder path or something remote. Returns True if local, False if remote. """ return (parse_as_folder_reference(dep) is not None) def parse_as_folder_reference(dep): """ See if a dependency reference refers to a folder path. If it does, return the folder path (which parses and resolves file:// urls in the process). If it doesn't, return None. """ # Special case: pep508 urls if dep.find("@") > 0 and ( (dep.find("@") < dep.find("/") or "/" not in dep) and (dep.find("@") < dep.find(":") or ":" not in dep) ): # This should be a 'pkgname @ https://...' style path, or # 'pkname @ /local/file/path'. return parse_as_folder_reference(dep.partition("@")[2].lstrip()) # Check if this is either not an url, or a file URL: if dep.startswith(("/", "file://")) or ( dep.find("/") > 0 and dep.find("://") < 0) or (dep in ["", "."]): if dep.startswith("file://"): dep = urlunquote(urlparse(dep).path) return dep return None def _extract_info_from_package(dependency, extract_type=None, debug=False, include_build_requirements=False ): """ Internal function to extract metainfo from a package. Currently supported info types: - name - dependencies (a list of dependencies) """ if debug: print("_extract_info_from_package called with " "extract_type={} include_build_requirements={}".format( extract_type, include_build_requirements, )) output_folder = tempfile.mkdtemp(prefix="pythonpackage-metafolder-") try: extract_metainfo_files_from_package( dependency, output_folder, debug=debug ) # Extract the type of data source we used to get the metadata: with open(os.path.join(output_folder, "metadata_source"), "r") as f: metadata_source_type = f.read().strip() # Extract main METADATA file: with open(os.path.join(output_folder, "METADATA"), "r", encoding="utf-8" ) as f: # Get metadata and cut away description (is after 2 linebreaks) metadata_entries = f.read().partition("\n\n")[0].splitlines() if extract_type == "name": name = None for meta_entry in metadata_entries: if meta_entry.lower().startswith("name:"): return meta_entry.partition(":")[2].strip() if name is None: raise ValueError("failed to obtain package name") return name elif extract_type == "dependencies": # First, make sure we don't attempt to return build requirements # for wheels since they usually come without pyproject.toml # and we haven't implemented another way to get them: if include_build_requirements and \ metadata_source_type == "wheel": if debug: print("_extract_info_from_package: was called " "with include_build_requirements=True on " "package obtained as wheel, raising error...") raise NotImplementedError( "fetching build requirements for " "wheels is not implemented" ) # Get build requirements from pyproject.toml if requested: requirements = [] pyproject_toml_path = os.path.join(output_folder, 'pyproject.toml') if os.path.exists(pyproject_toml_path) and include_build_requirements: # Read build system from pyproject.toml file: (PEP518) with open(pyproject_toml_path) as f: build_sys = toml.load(f)['build-system'] if "requires" in build_sys: requirements += build_sys["requires"] elif include_build_requirements: # For legacy packages with no pyproject.toml, we have to # add setuptools as default build system. requirements.append("setuptools") # Add requirements from metadata: requirements += [ entry.rpartition("Requires-Dist:")[2].strip() for entry in metadata_entries if entry.startswith("Requires-Dist") ] return list(set(requirements)) # remove duplicates finally: rmdir(output_folder) package_name_cache = dict() def get_package_name(dependency, use_cache=True): def timestamp(): try: return time.monotonic() except AttributeError: return time.time() # Python 2. try: value = package_name_cache[dependency] if value[0] + 600.0 > timestamp() and use_cache: return value[1] except KeyError: pass result = _extract_info_from_package(dependency, extract_type="name") package_name_cache[dependency] = (timestamp(), result) return result def get_package_dependencies(package, recursive=False, verbose=False, include_build_requirements=False): """ Obtain the dependencies from a package. Please note this function is possibly SLOW, especially if you enable the recursive mode. """ packages_processed = set() package_queue = [package] reqs = set() reqs_as_names = set() while len(package_queue) > 0: current_queue = package_queue package_queue = [] for package_dep in current_queue: new_reqs = set() if verbose: print("get_package_dependencies: resolving dependency " f"to package name: {package_dep}") package = get_package_name(package_dep) if package.lower() in packages_processed: continue if verbose: print("get_package_dependencies: " "processing package: {}".format(package)) print("get_package_dependencies: " "Packages seen so far: {}".format( packages_processed )) packages_processed.add(package.lower()) # Use our regular folder processing to examine: new_reqs = new_reqs.union(_extract_info_from_package( package_dep, extract_type="dependencies", debug=verbose, include_build_requirements=include_build_requirements, )) # Process new requirements: if verbose: print('get_package_dependencies: collected ' "deps of '{}': {}".format( package_dep, str(new_reqs), )) for new_req in new_reqs: try: req_name = get_package_name(new_req) except ValueError as e: if new_req.find(";") >= 0: # Conditional dep where condition isn't met? # --> ignore it continue if verbose: print("get_package_dependencies: " + "unexpected failure to get name " + "of '" + str(new_req) + "': " + str(e)) raise RuntimeError( "failed to get " + "name of dependency: " + str(e) ) if req_name.lower() in reqs_as_names: continue if req_name.lower() not in packages_processed: package_queue.append(new_req) reqs.add(new_req) reqs_as_names.add(req_name.lower()) # Bail out here if we're not scanning recursively: if not recursive: package_queue[:] = [] # wipe queue break if verbose: print("get_package_dependencies: returning result: {}".format(reqs)) return reqs def get_dep_names_of_package( package, keep_version_pins=False, recursive=False, verbose=False, include_build_requirements=False ): """ Gets the dependencies from the package in the given folder, then attempts to deduce the actual package name resulting from each dependency line, stripping away everything else. """ # First, obtain the dependencies: dependencies = get_package_dependencies( package, recursive=recursive, verbose=verbose, include_build_requirements=include_build_requirements, ) if verbose: print("get_dep_names_of_package_folder: " + "processing dependency list to names: " + str(dependencies)) # Transform dependencies to their stripped down names: # (they can still have version pins/restrictions, conditionals, ...) dependency_names = set() for dep in dependencies: # If we are supposed to keep exact version pins, extract first: pin_to_append = "" if keep_version_pins and "(==" in dep and dep.endswith(")"): # This is a dependency of the format: 'pkg (==1.0)' pin_to_append = "==" + dep.rpartition("==")[2][:-1] elif keep_version_pins and "==" in dep and not dep.endswith(")"): # This is a dependency of the format: 'pkg==1.0' pin_to_append = "==" + dep.rpartition("==")[2] # Now get true (and e.g. case-corrected) dependency name: dep_name = get_package_name(dep) + pin_to_append dependency_names.add(dep_name) return dependency_names ================================================ FILE: pythonforandroid/recipe.py ================================================ from os.path import basename, dirname, exists, isdir, isfile, join, realpath, split import glob import hashlib import json from re import match import sh import shutil import fnmatch import zipfile import urllib.request from urllib.request import urlretrieve from os import listdir, unlink, environ, curdir, walk from sys import stdout from multiprocessing import cpu_count import time try: from urlparse import urlparse except ImportError: from urllib.parse import urlparse import packaging.version from pythonforandroid.logger import ( logger, info, warning, debug, shprint, info_main, error) from pythonforandroid.util import ( current_directory, ensure_dir, BuildInterruptingException, rmdir, move, touch, patch_wheel_setuptools_logging) from pythonforandroid.util import load_source as import_recipe url_opener = urllib.request.build_opener() url_orig_headers = url_opener.addheaders urllib.request.install_opener(url_opener) class RecipeMeta(type): def __new__(cls, name, bases, dct): if name != 'Recipe': if 'url' in dct: dct['_url'] = dct.pop('url') if 'version' in dct: dct['_version'] = dct.pop('version') return super().__new__(cls, name, bases, dct) class Recipe(metaclass=RecipeMeta): _url = None '''The address from which the recipe may be downloaded. This is not essential, it may be omitted if the source is available some other way, such as via the :class:`IncludedFilesBehaviour` mixin. If the url includes the version, you may (and probably should) replace this with ``{version}``, which will automatically be replaced by the :attr:`version` string during download. .. note:: Methods marked (internal) are used internally and you probably don't need to call them, but they are available if you want. ''' _download_headers = None '''Add additional headers used when downloading the package, typically for authorization purposes. Specified as an array of tuples: [("header1", "foo"), ("header2", "bar")] When specifying as an environment variable (DOWNLOAD_HEADER_my-package-name), use a JSON formatted fragement: [["header1","foo"],["header2", "bar"]] For example, when downloading from a private github repository, you can specify the following: [('Authorization', 'token '), ('Accept', 'application/vnd.github+json')] ''' _version = None '''A string giving the version of the software the recipe describes, e.g. ``2.0.3`` or ``master``.''' md5sum = None '''The md5sum of the source from the :attr:`url`. Non-essential, but you should try to include this, it is used to check that the download finished correctly. ''' sha512sum = None '''The sha512sum of the source from the :attr:`url`. Non-essential, but you should try to include this, it is used to check that the download finished correctly. ''' blake2bsum = None '''The blake2bsum of the source from the :attr:`url`. Non-essential, but you should try to include this, it is used to check that the download finished correctly. ''' depends = [] '''A list containing the names of any recipes that this recipe depends on. ''' conflicts = [] '''A list containing the names of any recipes that are known to be incompatible with this one.''' opt_depends = [] '''A list of optional dependencies, that must be built before this recipe if they are built at all, but whose presence is not essential.''' patches = [] '''A list of patches to apply to the source. Values can be either a string referring to the patch file relative to the recipe dir, or a tuple of the string patch file and a callable, which will receive the kwargs `arch` and `recipe`, which should return True if the patch should be applied.''' python_depends = [] '''A list of pure-Python packages that this package requires. These packages will NOT be available at build time, but will be added to the list of pure-Python packages to install via pip. If you need these packages at build time, you must create a recipe.''' archs = ['armeabi'] # Not currently implemented properly built_libraries = {} """Each recipe that builds a system library (e.g.:libffi, openssl, etc...) should contain a dict holding the relevant information of the library. The keys should be the generated libraries and the values the relative path of the library inside his build folder. This dict will be used to perform different operations: - copy the library into the right location, depending on if it's shared or static) - check if we have to rebuild the library Here an example of how it would look like for `libffi` recipe: - `built_libraries = {'libffi.so': '.libs'}` .. note:: in case that the built library resides in recipe's build directory, you can set the following values for the relative path: `'.', None or ''` """ need_stl_shared = False '''Some libraries or python packages may need the c++_shared in APK. We can automatically do this for any recipe if we set this property to `True`''' stl_lib_name = 'c++_shared' ''' The default STL shared lib to use: `c++_shared`. .. note:: Android NDK version > 17 only supports 'c++_shared', because starting from NDK r18 the `gnustl_shared` lib has been deprecated. ''' min_ndk_api_support = 20 ''' Minimum ndk api recipe will support. ''' def get_stl_library(self, arch): return join( arch.ndk_lib_dir, 'lib{name}.so'.format(name=self.stl_lib_name), ) def install_stl_lib(self, arch): if not self.ctx.has_lib( arch.arch, 'lib{name}.so'.format(name=self.stl_lib_name) ): self.install_libs(arch, self.get_stl_library(arch)) @property def version(self): key = 'VERSION_' + self.name return environ.get(key, self._version) @property def url(self): key = 'URL_' + self.name return environ.get(key, self._url) @property def versioned_url(self): '''A property returning the url of the recipe with ``{version}`` replaced by the :attr:`url`. If accessing the url, you should use this property, *not* access the url directly.''' if self.url is None: return None return self.url.format(version=self.version) @property def download_headers(self): key = "DOWNLOAD_HEADERS_" + self.name env_headers = environ.get(key) if env_headers: try: return [tuple(h) for h in json.loads(env_headers)] except Exception as ex: raise ValueError(f'Invalid Download headers for {key} - must be JSON formatted as [["header1","foo"],["header2","bar"]]: {ex}') return environ.get(key, self._download_headers) def download_file(self, url, target, cwd=None): """ (internal) Download an ``url`` to a ``target``. """ if not url: return info('Downloading {} from {}'.format(self.name, url)) if cwd: target = join(cwd, target) parsed_url = urlparse(url) if parsed_url.scheme in ('http', 'https'): def report_hook(index, blksize, size): if size <= 0: progression = '{0} bytes'.format(index * blksize) else: progression = '{0:.2f}%'.format( index * blksize * 100. / float(size)) if "CI" not in environ: stdout.write('- Download {}\r'.format(progression)) stdout.flush() if exists(target): unlink(target) # Download item with multiple attempts (for bad connections): attempts = 0 seconds = 1 while True: try: # jqueryui.com returns a 403 w/ the default user agent # Mozilla/5.0 does not handle redirection for liblzma url_opener.addheaders = [('User-agent', 'Wget/1.0')] if self.download_headers: url_opener.addheaders += self.download_headers urlretrieve(url, target, report_hook) except OSError as e: attempts += 1 if attempts >= 5: raise stdout.write('Download failed: {}; retrying in {} second(s)...'.format(e, seconds)) time.sleep(seconds) seconds *= 2 continue finally: url_opener.addheaders = url_orig_headers break return target elif parsed_url.scheme in ('git', 'git+file', 'git+ssh', 'git+http', 'git+https'): if not isdir(target): if url.startswith('git+'): url = url[4:] # if 'version' is specified, do a shallow clone if self.version: ensure_dir(target) with current_directory(target): shprint(sh.git, 'init') shprint(sh.git, 'remote', 'add', 'origin', url) else: shprint(sh.git, 'clone', '--recursive', url, target) with current_directory(target): if self.version: shprint(sh.git, 'fetch', '--tags', '--depth', '1') shprint(sh.git, 'checkout', self.version) branch = sh.git('branch', '--show-current') if branch: shprint(sh.git, 'pull') shprint(sh.git, 'pull', '--recurse-submodules') shprint(sh.git, 'submodule', 'update', '--recursive', '--init', '--depth', '1') return target def apply_patch(self, filename, arch, build_dir=None): """ Apply a patch from the current recipe directory into the current build directory. .. versionchanged:: 0.6.0 Add ability to apply patch from any dir via kwarg `build_dir`''' """ info("Applying patch {}".format(filename)) build_dir = build_dir if build_dir else self.get_build_dir(arch) filename = join(self.get_recipe_dir(), filename) shprint(sh.patch, "-t", "-d", build_dir, "-p1", "-i", filename, _tail=10) def copy_file(self, filename, dest): info("Copy {} to {}".format(filename, dest)) filename = join(self.get_recipe_dir(), filename) dest = join(self.build_dir, dest) shutil.copy(filename, dest) def append_file(self, filename, dest): info("Append {} to {}".format(filename, dest)) filename = join(self.get_recipe_dir(), filename) dest = join(self.build_dir, dest) with open(filename, "rb") as fd: data = fd.read() with open(dest, "ab") as fd: fd.write(data) @property def name(self): '''The name of the recipe, the same as the folder containing it.''' modname = self.__class__.__module__ return modname.split(".", 2)[-1] @property def filtered_archs(self): '''Return archs of self.ctx that are valid build archs for the Recipe.''' result = [] for arch in self.ctx.archs: if not self.archs or (arch.arch in self.archs): result.append(arch) return result def check_recipe_choices(self): '''Checks what recipes are being built to see which of the alternative and optional dependencies are being used, and returns a list of these.''' recipes = [] built_recipes = self.ctx.recipe_build_order for recipe in self.depends: if isinstance(recipe, (tuple, list)): for alternative in recipe: if alternative in built_recipes: recipes.append(alternative) break for recipe in self.opt_depends: if recipe in built_recipes: recipes.append(recipe) return sorted(recipes) def get_opt_depends_in_list(self, recipes): '''Given a list of recipe names, returns those that are also in self.opt_depends. ''' return [recipe for recipe in recipes if recipe in self.opt_depends] def get_build_container_dir(self, arch): '''Given the arch name, returns the directory where it will be built. This returns a different directory depending on what alternative or optional dependencies are being built. ''' dir_name = self.get_dir_name() return join(self.ctx.build_dir, 'other_builds', dir_name, '{}__ndk_target_{}'.format(arch, self.ctx.ndk_api)) def get_dir_name(self): choices = self.check_recipe_choices() dir_name = '-'.join([self.name] + choices) return dir_name def get_build_dir(self, arch): '''Given the arch name, returns the directory where the downloaded/copied package will be built.''' return join(self.get_build_container_dir(arch), self.name) def get_recipe_dir(self): """ Returns the local recipe directory or defaults to the core recipe directory. """ if self.ctx.local_recipes is not None: local_recipe_dir = join(self.ctx.local_recipes, self.name) if exists(local_recipe_dir): return local_recipe_dir return join(self.ctx.root_dir, 'recipes', self.name) # Public Recipe API to be subclassed if needed def download_if_necessary(self): if self.ctx.ndk_api < self.min_ndk_api_support: error(f"In order to build '{self.name}', you must set minimum ndk api (minapi) to `{self.min_ndk_api_support}`.\n") exit(1) info_main('Downloading {}'.format(self.name)) user_dir = environ.get('P4A_{}_DIR'.format(self.name.lower())) if user_dir is not None: info('P4A_{}_DIR is set, skipping download for {}'.format( self.name, self.name)) return self.download() def download(self): if self.url is None: info('Skipping {} download as no URL is set'.format(self.name)) return url = self.versioned_url expected_digests = {} for alg in set(hashlib.algorithms_guaranteed) | set(('md5', 'sha512', 'blake2b')): expected_digest = getattr(self, alg + 'sum') if hasattr(self, alg + 'sum') else None ma = match(u'^(.+)#' + alg + u'=([0-9a-f]{32,})$', url) if ma: # fragmented URL? if expected_digest: raise ValueError( ('Received {}sum from both the {} recipe ' 'and its url').format(alg, self.name)) url = ma.group(1) expected_digest = ma.group(2) if expected_digest: expected_digests[alg] = expected_digest ensure_dir(join(self.ctx.packages_path, self.name)) with current_directory(join(self.ctx.packages_path, self.name)): filename = shprint(sh.basename, url).stdout[:-1].decode('utf-8') do_download = True marker_filename = '.mark-{}'.format(filename) if exists(filename) and isfile(filename): if not exists(marker_filename): shprint(sh.rm, filename) else: for alg, expected_digest in expected_digests.items(): current_digest = algsum(alg, filename) if current_digest != expected_digest: debug('* Generated {}sum: {}'.format(alg, current_digest)) debug('* Expected {}sum: {}'.format(alg, expected_digest)) raise ValueError( ('Generated {0}sum does not match expected {0}sum ' 'for {1} recipe').format(alg, self.name)) do_download = False # If we got this far, we will download if do_download: debug('Downloading {} from {}'.format(self.name, url)) shprint(sh.rm, '-f', marker_filename) self.download_file(self.versioned_url, filename) touch(marker_filename) if exists(filename) and isfile(filename): for alg, expected_digest in expected_digests.items(): current_digest = algsum(alg, filename) if current_digest != expected_digest: debug('* Generated {}sum: {}'.format(alg, current_digest)) debug('* Expected {}sum: {}'.format(alg, expected_digest)) raise ValueError( ('Generated {0}sum does not match expected {0}sum ' 'for {1} recipe').format(alg, self.name)) else: info('{} download already cached, skipping'.format(self.name)) def unpack(self, arch): info_main('Unpacking {} for {}'.format(self.name, arch)) build_dir = self.get_build_container_dir(arch) user_dir = environ.get('P4A_{}_DIR'.format(self.name.lower())) if user_dir is not None: info('P4A_{}_DIR exists, symlinking instead'.format( self.name.lower())) if exists(self.get_build_dir(arch)): return rmdir(build_dir) ensure_dir(build_dir) shprint(sh.cp, '-a', user_dir, self.get_build_dir(arch)) return if self.url is None: info('Skipping {} unpack as no URL is set'.format(self.name)) return filename = shprint( sh.basename, self.versioned_url).stdout[:-1].decode('utf-8') ma = match(u'^(.+)#[a-z0-9_]{3,}=([0-9a-f]{32,})$', filename) if ma: # fragmented URL? filename = ma.group(1) with current_directory(build_dir): directory_name = self.get_build_dir(arch) if not exists(directory_name) or not isdir(directory_name): extraction_filename = join( self.ctx.packages_path, self.name, filename) if isfile(extraction_filename): if extraction_filename.endswith(('.zip', '.whl')): try: sh.unzip(extraction_filename) except (sh.ErrorReturnCode_1, sh.ErrorReturnCode_2): # return code 1 means unzipping had # warnings but did complete, # apparently happens sometimes with # github zips pass fileh = zipfile.ZipFile(extraction_filename, 'r') root_directory = fileh.filelist[0].filename.split('/')[0] if root_directory != basename(directory_name): move(root_directory, directory_name) elif extraction_filename.endswith( ('.tar.gz', '.tgz', '.tar.bz2', '.tbz2', '.tar.xz', '.txz')): sh.tar('xf', extraction_filename) root_directory = sh.tar('tf', extraction_filename).split('\n')[0].split('/')[0] if root_directory != basename(directory_name): move(root_directory, directory_name) else: raise Exception( 'Could not extract {} download, it must be .zip, ' '.tar.gz or .tar.bz2 or .tar.xz'.format(extraction_filename)) elif isdir(extraction_filename): ensure_dir(directory_name) for entry in listdir(extraction_filename): # Previously we filtered out the .git folder, but during the build process for some recipes # (e.g. when version is parsed by `setuptools_scm`) that may be needed. shprint(sh.cp, '-R', join(extraction_filename, entry), directory_name) else: raise Exception( 'Given path is neither a file nor a directory: {}' .format(extraction_filename)) else: info('{} is already unpacked, skipping'.format(self.name)) def get_recipe_env(self, arch=None, with_flags_in_cc=True): """Return the env specialized for the recipe """ if arch is None: arch = self.filtered_archs[0] env = arch.get_env(with_flags_in_cc=with_flags_in_cc) for proxy_key in ['HTTP_PROXY', 'http_proxy', 'HTTPS_PROXY', 'https_proxy']: if proxy_key in environ: env[proxy_key] = environ[proxy_key] return env def prebuild_arch(self, arch): '''Run any pre-build tasks for the Recipe. By default, this checks if any prebuild_archname methods exist for the archname of the current architecture, and runs them if so.''' prebuild = "prebuild_{}".format(arch.arch.replace('-', '_')) if hasattr(self, prebuild): getattr(self, prebuild)() else: info('{} has no {}, skipping'.format(self.name, prebuild)) def is_patched(self, arch): build_dir = self.get_build_dir(arch.arch) return exists(join(build_dir, '.patched')) def apply_patches(self, arch, build_dir=None): '''Apply any patches for the Recipe. .. versionchanged:: 0.6.0 Add ability to apply patches from any dir via kwarg `build_dir`''' if self.patches: info_main('Applying patches for {}[{}]' .format(self.name, arch.arch)) if self.is_patched(arch): info_main('{} already patched, skipping'.format(self.name)) return build_dir = build_dir if build_dir else self.get_build_dir(arch.arch) for patch in self.patches: if isinstance(patch, (tuple, list)): patch, patch_check = patch if not patch_check(arch=arch, recipe=self): continue self.apply_patch( patch.format(version=self.version, arch=arch.arch), arch.arch, build_dir=build_dir) touch(join(build_dir, '.patched')) def should_build(self, arch): '''Should perform any necessary test and return True only if it needs building again. Per default we implement a library test, in case that we detect so. ''' if self.built_libraries: return not all( exists(lib) for lib in self.get_libraries(arch.arch) ) return True def build_arch(self, arch): '''Run any build tasks for the Recipe. By default, this checks if any build_archname methods exist for the archname of the current architecture, and runs them if so.''' build = "build_{}".format(arch.arch) if hasattr(self, build): getattr(self, build)() def install_libraries(self, arch): '''This method is always called after `build_arch`. In case that we detect a library recipe, defined by the class attribute `built_libraries`, we will copy all defined libraries into the right location. ''' if not self.built_libraries: return shared_libs = [ lib for lib in self.get_libraries(arch) if lib.endswith(".so") ] self.install_libs(arch, *shared_libs) def postbuild_arch(self, arch): '''Run any post-build tasks for the Recipe. By default, this checks if any postbuild_archname methods exist for the archname of the current architecture, and runs them if so. ''' postbuild = "postbuild_{}".format(arch.arch) if hasattr(self, postbuild): getattr(self, postbuild)() if self.need_stl_shared: self.install_stl_lib(arch) def prepare_build_dir(self, arch): '''Copies the recipe data into a build dir for the given arch. By default, this unpacks a downloaded recipe. You should override it (or use a Recipe subclass with different behaviour) if you want to do something else. ''' self.unpack(arch) def clean_build(self, arch=None): '''Deletes all the build information of the recipe. If arch is not None, only this arch dir is deleted. Otherwise (the default) all builds for all archs are deleted. By default, this just deletes the main build dir. If the recipe has e.g. object files biglinked, or .so files stored elsewhere, you should override this method. This method is intended for testing purposes, it may have strange results. Rebuild everything if this seems to happen. ''' if arch is None: base_dir = join(self.ctx.build_dir, 'other_builds', self.name) else: base_dir = self.get_build_container_dir(arch) dirs = glob.glob(base_dir + '-*') if exists(base_dir): dirs.append(base_dir) if not dirs: warning('Attempted to clean build for {} but found no existing ' 'build dirs'.format(self.name)) for directory in dirs: rmdir(directory) # Delete any Python distributions to ensure the recipe build # doesn't persist in site-packages rmdir(self.ctx.python_installs_dir) def install_libs(self, arch, *libs): libs_dir = self.ctx.get_libs_dir(arch.arch) if not libs: warning('install_libs called with no libraries to install!') return args = libs + (libs_dir,) shprint(sh.cp, *args) def has_libs(self, arch, *libs): return all(map(lambda lib: self.ctx.has_lib(arch.arch, lib), libs)) def get_libraries(self, arch_name, in_context=False): """Return the full path of the library depending on the architecture. Per default, the build library path it will be returned, unless `get_libraries` has been called with kwarg `in_context` set to True. .. note:: this method should be used for library recipes only """ recipe_libs = set() if not self.built_libraries: return recipe_libs for lib, rel_path in self.built_libraries.items(): if not in_context: abs_path = join(self.get_build_dir(arch_name), rel_path, lib) if rel_path in {".", "", None}: abs_path = join(self.get_build_dir(arch_name), lib) else: abs_path = join(self.ctx.get_libs_dir(arch_name), lib) recipe_libs.add(abs_path) return recipe_libs @classmethod def recipe_dirs(cls, ctx): recipe_dirs = [] if ctx.local_recipes is not None: recipe_dirs.append(realpath(ctx.local_recipes)) if ctx.storage_dir: recipe_dirs.append(join(ctx.storage_dir, 'recipes')) recipe_dirs.append(join(ctx.root_dir, "recipes")) return recipe_dirs @classmethod def list_recipes(cls, ctx): forbidden_dirs = ('__pycache__', ) for recipes_dir in cls.recipe_dirs(ctx): if recipes_dir and exists(recipes_dir): for name in listdir(recipes_dir): if name in forbidden_dirs: continue fn = join(recipes_dir, name) if isdir(fn): yield name @classmethod def get_recipe(cls, name, ctx): '''Returns the Recipe with the given name, if it exists.''' name = name.lower() if not hasattr(cls, "recipes"): cls.recipes = {} if name in cls.recipes: return cls.recipes[name] recipe_file = None for recipes_dir in cls.recipe_dirs(ctx): if not exists(recipes_dir): continue # Find matching folder (may differ in case): for subfolder in listdir(recipes_dir): if subfolder.lower() == name: recipe_file = join(recipes_dir, subfolder, '__init__.py') if exists(recipe_file): name = subfolder # adapt to actual spelling break recipe_file = None if recipe_file is not None: break else: raise ValueError('Recipe does not exist: {}'.format(name)) mod = import_recipe('pythonforandroid.recipes.{}'.format(name), recipe_file) if len(logger.handlers) > 1: logger.removeHandler(logger.handlers[1]) recipe = mod.recipe recipe.ctx = ctx cls.recipes[name.lower()] = recipe return recipe class IncludedFilesBehaviour(object): '''Recipe mixin class that will automatically unpack files included in the recipe directory.''' src_filename = None def prepare_build_dir(self, arch): if self.src_filename is None: raise BuildInterruptingException( 'IncludedFilesBehaviour failed: no src_filename specified') rmdir(self.get_build_dir(arch)) shprint(sh.cp, '-a', join(self.get_recipe_dir(), self.src_filename), self.get_build_dir(arch)) class BootstrapNDKRecipe(Recipe): '''A recipe class for recipes built in an Android project jni dir with an Android.mk. These are not cached separately, but built in the bootstrap's own building directory. To build an NDK project which is not part of the bootstrap, see :class:`~pythonforandroid.recipe.NDKRecipe`. To link with python, call the method :meth:`get_recipe_env` with the kwarg *with_python=True*. ''' dir_name = None # The name of the recipe build folder in the jni dir def get_build_container_dir(self, arch): return self.get_jni_dir() def get_build_dir(self, arch): if self.dir_name is None: raise ValueError('{} recipe doesn\'t define a dir_name, but ' 'this is necessary'.format(self.name)) return join(self.get_build_container_dir(arch), self.dir_name) 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().get_recipe_env(arch, with_flags_in_cc) if not with_python: return env env['PYTHON_INCLUDE_ROOT'] = self.ctx.python_recipe.include_root(arch.arch) env['PYTHON_LINK_ROOT'] = self.ctx.python_recipe.link_root(arch.arch) env['EXTRA_LDLIBS'] = ' -lpython{}'.format( self.ctx.python_recipe.link_version) return env class NDKRecipe(Recipe): '''A recipe class for any NDK project not included in the bootstrap.''' generated_libraries = [] def should_build(self, arch): lib_dir = self.get_lib_dir(arch) for lib in self.generated_libraries: if not exists(join(lib_dir, lib)): return True return False def get_lib_dir(self, arch): return join(self.get_build_dir(arch.arch), 'obj', 'local', arch.arch) def get_jni_dir(self, arch): return join(self.get_build_dir(arch.arch), 'jni') def build_arch(self, arch, *extra_args): super().build_arch(arch) env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): shprint( sh.Command(join(self.ctx.ndk_dir, "ndk-build")), 'V=1', "-j", str(cpu_count()), '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 ) class PythonRecipe(Recipe): site_packages_name = None '''The name of the module's folder when installed in the Python site-packages (e.g. for pyjnius it is 'jnius')''' call_hostpython_via_targetpython = True '''If True, tries to install the module using the hostpython binary copied to the target (normally arm) python build dir. However, this will fail if the module tries to import e.g. _io.so. Set this to False to call hostpython from its own build dir, installing the module in the right place via arguments to setup.py. However, this may not set the environment correctly and so False is not the default.''' install_in_hostpython = False '''If True, additionally installs the module in the hostpython build dir. This will make it available to other recipes if call_hostpython_via_targetpython is False. ''' install_in_targetpython = True '''If True, installs the module in the targetpython installation dir. This is almost always what you want to do.''' setup_extra_args = [] '''List of extra arguments to pass to setup.py''' 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 `__init__` call won't be called and the deps would be missing (which breaks the dependency graph computation) .. warning:: don't forget to call `super().__init__()` in any recipe's `__init__`, or otherwise it may not be ensured that it depends on python2 or python3 which can break the dependency graph ''' hostpython_prerequisites = ['setuptools'] '''List of hostpython packages required to build a recipe''' _host_recipe = None def __init__(self, *args, **kwargs): 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('python3') depends = list(set(depends)) self.depends = depends def prebuild_arch(self, arch): self._host_recipe = Recipe.get_recipe("hostpython3", self.ctx) return super().prebuild_arch(arch) def clean_build(self, arch=None): 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: site_packages_dir = glob.glob(join(python_install, 'lib', 'python*', 'site-packages')) if site_packages_dir: build_dir = join(site_packages_dir[0], name) if exists(build_dir): info('Deleted {}'.format(build_dir)) rmdir(build_dir) @property def real_hostpython_location(self): host_name = 'host{}'.format(self.ctx.python_recipe.name) if host_name == 'hostpython3': return self._host_recipe.python_exe else: python_recipe = self.ctx.python_recipe return 'python{}'.format(python_recipe.version) @property def hostpython_location(self): if not self.call_hostpython_via_targetpython: return self.real_hostpython_location return self.ctx.hostpython @property def folder_name(self): '''The name of the build folders containing this recipe.''' name = self.site_packages_name if name is None: name = self.name return name def patch_shebang(self, _file, original_bin): _file_des = open(_file, "r") try: data = _file_des.readlines() except UnicodeDecodeError: return if "#!" in (line := data[0]): if line.split("#!")[-1].strip() == original_bin: return info(f"Fixing shebang for '{_file}'") data.pop(0) data.insert(0, "#!" + original_bin + "\n") _file_des.close() _file_des = open(_file, "w") _file_des.write("".join(data)) _file_des.close() def patch_shebangs(self, path, original_bin): if not isdir(path): warning(f"Shebang patch skipped: '{path}' does not exist.") return # set correct shebang for file in listdir(path): _file = join(path, file) self.patch_shebang(_file, original_bin) def get_recipe_env(self, arch=None, with_flags_in_cc=True): if self._host_recipe is None: self._host_recipe = Recipe.get_recipe("hostpython3", self.ctx) env = super().get_recipe_env(arch, with_flags_in_cc) # Set the LANG, this isn't usually important but is a better default # as it occasionally matters how Python e.g. reads files env['LANG'] = "en_GB.UTF-8" # Binaries made by packages installed by pip self.patch_shebangs(self._host_recipe.local_bin, self._host_recipe.python_exe) env["PATH"] = self._host_recipe.local_bin + ":" + self._host_recipe.site_bin + ":" + env["PATH"] host_env = self.get_hostrecipe_env(arch) env['PYTHONPATH'] = host_env["PYTHONPATH"] if not self.call_hostpython_via_targetpython: env['CFLAGS'] += ' -I{}'.format( self.ctx.python_recipe.include_root(arch.arch) ) env['LDFLAGS'] += ' -L{} -lpython{}'.format( self.ctx.python_recipe.link_root(arch.arch), self.ctx.python_recipe.link_version, ) return env def should_build(self, arch): name = self.folder_name if self.ctx.has_package(name, arch): info('Python package already exists in site-packages') return False info('{} apparently isn\'t already in site-packages'.format(name)) return True def build_arch(self, arch): '''Install the Python module by calling setup.py install with the target Python dir.''' self.install_hostpython_prerequisites() super().build_arch(arch) self.install_python_package(arch) def install_python_package(self, arch, name=None, env=None, is_dir=True): '''Automate the installation of a Python package (or a cython package where the cython components are pre-built).''' # arch = self.filtered_archs[0] # old kivy-ios way if name is None: name = self.name if env is None: env = self.get_recipe_env(arch) info('Installing {} into site-packages'.format(self.name)) hpenv = env.copy() with current_directory(self.get_build_dir(arch.arch)): shprint(self._host_recipe.pip, 'install', '.', '--compile', '--target', self.ctx.get_python_install_dir(arch.arch), _env=hpenv, *self.setup_extra_args ) def get_hostrecipe_env(self, arch=None): env = environ.copy() _python_path = self._host_recipe.get_path_to_python() libdir = glob.glob(join(_python_path, "build", "lib*")) env['PYTHONPATH'] = self._host_recipe.site_dir + ":" + join( _python_path, "Modules") + ":" + (libdir[0] if libdir else "") return env @property def hostpython_site_dir(self): return join(dirname(self.real_hostpython_location), 'Lib', 'site-packages') def install_hostpython_package(self, arch): env = self.get_hostrecipe_env(arch) shprint(self._host_recipe.pip, 'install', '.', '--compile', '--root={}'.format(self._host_recipe.site_root), _env=env, *self.setup_extra_args) @property def python_major_minor_version(self): parsed_version = packaging.version.parse(self.ctx.python_recipe.version) return f"{parsed_version.major}.{parsed_version.minor}" def install_hostpython_prerequisites(self, packages=None, force_upgrade=True): if not packages: packages = self.hostpython_prerequisites if len(packages) == 0: return pip_options = [ "install", *packages, "--target", self._host_recipe.site_dir, "--python-version", self.ctx.python_recipe.version, # Don't use sources, instead wheels "--only-binary=:all:", ] if force_upgrade: pip_options.append("--upgrade") # Use system's pip pip_env = self.get_hostrecipe_env() shprint(self._host_recipe.pip, *pip_options, _env=pip_env) def restore_hostpython_prerequisites(self, packages): _packages = [] for package in packages: original_version = Recipe.get_recipe(package, self.ctx).version _packages.append(package + "==" + original_version) self.install_hostpython_prerequisites(packages=_packages) class CompiledComponentsPythonRecipe(PythonRecipe): pre_build_ext = False build_cmd = 'build_ext' def build_arch(self, arch): '''Build any cython components, then install the Python module by calling pip install with the target Python dir. ''' Recipe.build_arch(self, arch) self.install_hostpython_prerequisites() self.build_compiled_components(arch) self.install_python_package(arch) def build_compiled_components(self, arch): info('Building compiled components in {}'.format(self.name)) env = self.get_recipe_env(arch) hostpython = sh.Command(self.hostpython_location) with current_directory(self.get_build_dir(arch.arch)): if self.install_in_hostpython: shprint(hostpython, 'setup.py', 'clean', '--all', _env=env) shprint(hostpython, 'setup.py', self.build_cmd, '-v', _env=env, *self.setup_extra_args) build_dir = glob.glob('build/lib.*')[0] shprint(sh.find, build_dir, '-name', '"*.o"', '-exec', env['STRIP'], '{}', ';', _env=env) def install_hostpython_package(self, arch): env = self.get_hostrecipe_env(arch) self.rebuild_compiled_components(arch, env) super().install_hostpython_package(arch) def rebuild_compiled_components(self, arch, env): info('Rebuilding compiled components in {}'.format(self.name)) hostpython = sh.Command(self.real_hostpython_location) shprint(hostpython, 'setup.py', 'clean', '--all', _env=env) shprint(hostpython, 'setup.py', self.build_cmd, '-v', _env=env, *self.setup_extra_args) class CppCompiledComponentsPythonRecipe(CompiledComponentsPythonRecipe): """ Extensions that require the cxx-stl """ call_hostpython_via_targetpython = False need_stl_shared = True class CythonRecipe(PythonRecipe): pre_build_ext = False cythonize = True cython_args = [] call_hostpython_via_targetpython = False def build_arch(self, arch): '''Build any cython components, then install the Python module by calling pip install with the target Python dir. ''' Recipe.build_arch(self, arch) self.build_cython_components(arch) self.install_python_package(arch) def build_cython_components(self, arch): info('Cythonizing anything necessary in {}'.format(self.name)) env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): hostpython = sh.Command(self.ctx.hostpython) shprint(hostpython, '-c', 'import sys; print(sys.path)', _env=env) debug('cwd is {}'.format(realpath(curdir))) info('Trying first build of {} to get cython files: this is ' 'expected to fail'.format(self.name)) manually_cythonise = False try: shprint(hostpython, 'setup.py', 'build_ext', '-v', _env=env, *self.setup_extra_args) except sh.ErrorReturnCode_1: print() info('{} first build failed (as expected)'.format(self.name)) manually_cythonise = True if manually_cythonise: self.cythonize_build(env=env) shprint(hostpython, 'setup.py', 'build_ext', '-v', _env=env, _tail=20, _critical=True, *self.setup_extra_args) else: info('First build appeared to complete correctly, skipping manual' 'cythonising.') if not self.ctx.with_debug_symbols: self.strip_object_files(arch, env) def strip_object_files(self, arch, env, build_dir=None): if build_dir is None: build_dir = self.get_build_dir(arch.arch) with current_directory(build_dir): info('Stripping object files') shprint(sh.find, '.', '-iname', '*.so', '-exec', '/usr/bin/echo', '{}', ';', _env=env) shprint(sh.find, '.', '-iname', '*.so', '-exec', env['STRIP'].split(' ')[0], '--strip-unneeded', # '/usr/bin/strip', '--strip-unneeded', '{}', ';', _env=env) def cythonize_file(self, env, build_dir, filename): short_filename = filename if filename.startswith(build_dir): short_filename = filename[len(build_dir) + 1:] info(u"Cythonize {}".format(short_filename)) cyenv = env.copy() if 'CYTHONPATH' in cyenv: cyenv['PYTHONPATH'] = cyenv['CYTHONPATH'] elif 'PYTHONPATH' in cyenv: del cyenv['PYTHONPATH'] if 'PYTHONNOUSERSITE' in cyenv: cyenv.pop('PYTHONNOUSERSITE') python_command = sh.Command("python{}".format( self.ctx.python_recipe.major_minor_version_string.split(".")[0] )) shprint(python_command, "-c" "import sys; from Cython.Compiler.Main import setuptools_main; sys.exit(setuptools_main());", filename, *self.cython_args, _env=cyenv) def cythonize_build(self, env, build_dir="."): if not self.cythonize: info('Running cython cancelled per recipe setting') return info('Running cython where appropriate') for root, dirnames, filenames in walk("."): for filename in fnmatch.filter(filenames, "*.pyx"): self.cythonize_file(env, build_dir, join(root, filename)) def get_recipe_env(self, arch, with_flags_in_cc=True): 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) + ' -L{}'.format(join(self.ctx.bootstrap.build_dir, 'obj', 'local', arch.arch))) env['LDSHARED'] = env['CC'] + ' -shared' # shprint(sh.whereis, env['LDSHARED'], _env=env) env['LIBLINK'] = 'NOTNONE' if self.ctx.copy_libs: env['COPYLIBS'] = '1' # Every recipe uses its own liblink path, object files are # collected and biglinked later liblink_path = join(self.get_build_container_dir(arch.arch), 'objects_{}'.format(self.name)) env['LIBLINK_PATH'] = liblink_path ensure_dir(liblink_path) return env class PyProjectRecipe(PythonRecipe): """Recipe for projects which contain `pyproject.toml`""" # Extra args to pass to `python -m build ...` extra_build_args = [] call_hostpython_via_targetpython = False def get_recipe_env(self, arch, **kwargs): # Custom hostpython self.ctx.python_recipe.python_exe = join( self.ctx.python_recipe.get_build_dir(arch), "android-build", "python3") env = super().get_recipe_env(arch, **kwargs) build_dir = self.get_build_dir(arch) ensure_dir(build_dir) build_opts = join(build_dir, "build-opts.cfg") with open(build_opts, "w") as file: file.write("[bdist_wheel]\nplat_name={}".format( self.get_wheel_platform_tag(arch) )) file.close() env["DIST_EXTRA_CONFIG"] = build_opts return env def get_wheel_platform_tag(self, arch): # https://peps.python.org/pep-0738/#packaging # official python only supports 64 bit: # android_21_arm64_v8a # android_21_x86_64 return f"android_{self.ctx.ndk_api}_" + { "arm64-v8a": "arm64_v8a", "x86_64": "x86_64", "armeabi-v7a": "arm", "x86": "i686", }[arch.arch] def install_wheel(self, arch, built_wheels): with patch_wheel_setuptools_logging(): from wheel.cli.tags import tags as wheel_tags from wheel.wheelfile import WheelFile _wheel = built_wheels[0] built_wheel_dir = dirname(_wheel) # Fix wheel platform tag wheel_tag = wheel_tags( _wheel, platform_tags=self.get_wheel_platform_tag(arch), remove=True, ) selected_wheel = join(built_wheel_dir, wheel_tag) _dev_wheel_dir = environ.get("P4A_WHEEL_DIR", False) if _dev_wheel_dir: ensure_dir(_dev_wheel_dir) shprint(sh.cp, selected_wheel, _dev_wheel_dir) info(f"Installing built wheel: {wheel_tag}") destination = self.ctx.get_python_install_dir(arch.arch) with WheelFile(selected_wheel) as wf: for zinfo in wf.filelist: wf.extract(zinfo, destination) wf.close() def build_arch(self, arch): build_dir = self.get_build_dir(arch.arch) if not (isfile(join(build_dir, "pyproject.toml")) or isfile(join(build_dir, "setup.py"))): warning("Skipping build because it does not appear to be a Python project.") return self.install_hostpython_prerequisites( packages=["build[virtualenv]", "pip", "setuptools", "patchelf"] + self.hostpython_prerequisites ) self.patch_shebangs(self._host_recipe.site_bin, self.real_hostpython_location) env = self.get_recipe_env(arch, with_flags_in_cc=True) # make build dir separately sub_build_dir = join(build_dir, "p4a_android_build") ensure_dir(sub_build_dir) # copy hostpython to built python to ensure correct selection of libs and includes shprint(sh.cp, self.real_hostpython_location, self.ctx.python_recipe.python_exe) build_args = [ "-m", "build", "--wheel", "--config-setting", "builddir={}".format(sub_build_dir), ] + self.extra_build_args built_wheels = [] with current_directory(build_dir): shprint( sh.Command(self.ctx.python_recipe.python_exe), *build_args, _env=env ) built_wheels = [realpath(whl) for whl in glob.glob("dist/*.whl")] self.install_wheel(arch, built_wheels) class MesonRecipe(PyProjectRecipe): '''Recipe for projects which uses meson as build system''' meson_version = "1.4.0" ninja_version = "1.11.1.1" skip_python = False '''If true, skips all Python build and installation steps. Useful for Meson projects written purely in C/C++ without Python bindings.''' def sanitize_flags(self, *flag_strings): return " ".join(flag_strings).strip().split(" ") def get_recipe_meson_options(self, arch): env = self.get_recipe_env(arch, with_flags_in_cc=True) return { "binaries": { "c": arch.get_clang_exe(with_target=True), "cpp": arch.get_clang_exe(with_target=True, plus_plus=True), "ar": self.ctx.ndk.llvm_ar, "strip": self.ctx.ndk.llvm_strip, }, "built-in options": { "c_args": self.sanitize_flags(env["CFLAGS"], env["CPPFLAGS"]), "cpp_args": self.sanitize_flags(env["CXXFLAGS"], env["CPPFLAGS"]), "c_link_args": self.sanitize_flags(env["LDFLAGS"]), "cpp_link_args": self.sanitize_flags(env["LDFLAGS"]), "fortran_link_args": self.sanitize_flags(env["LDFLAGS"]), }, "properties": { "needs_exe_wrapper": True, "sys_root": self.ctx.ndk.sysroot }, "host_machine": { "cpu_family": { "arm64-v8a": "aarch64", "armeabi-v7a": "arm", "x86_64": "x86_64", "x86": "x86" }[arch.arch], "cpu": { "arm64-v8a": "aarch64", "armeabi-v7a": "armv7", "x86_64": "x86_64", "x86": "i686" }[arch.arch], "endian": "little", "system": "android", } } def write_build_options(self, arch): """Writes python dict to meson config file""" option_data = "" build_options = self.get_recipe_meson_options(arch) for key in build_options.keys(): data_chunk = "[{}]".format(key) for subkey in build_options[key].keys(): value = build_options[key][subkey] if isinstance(value, int): value = str(value) elif isinstance(value, str): value = "'{}'".format(value) elif isinstance(value, bool): value = "true" if value else "false" elif isinstance(value, list): value = "['" + "', '".join(value) + "']" data_chunk += "\n" + subkey + " = " + value option_data += data_chunk + "\n\n" return option_data def ensure_args(self, *args): for arg in args: if arg not in self.extra_build_args: self.extra_build_args.append(arg) def build_arch(self, arch): cross_file = join("/tmp", "android.meson.cross") info("Writing cross file at: {}".format(cross_file)) # write cross config file with open(cross_file, "w") as file: file.write(self.write_build_options(arch)) file.close() # set cross file self.ensure_args('-Csetup-args=--cross-file', '-Csetup-args={}'.format(cross_file)) # ensure ninja and meson for dep in [ "ninja=={}".format(self.ninja_version), "meson=={}".format(self.meson_version), ]: if dep not in self.hostpython_prerequisites: self.hostpython_prerequisites.append(dep) if not self.skip_python: super().build_arch(arch) class RustCompiledComponentsRecipe(PyProjectRecipe): # Rust toolchain codes # https://doc.rust-lang.org/nightly/rustc/platform-support.html RUST_ARCH_CODES = { "arm64-v8a": "aarch64-linux-android", "armeabi-v7a": "armv7-linux-androideabi", "x86_64": "x86_64-linux-android", "x86": "i686-linux-android", } call_hostpython_via_targetpython = False def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) # Set rust build target build_target = self.RUST_ARCH_CODES[arch.arch] cargo_linker_name = "CARGO_TARGET_{}_LINKER".format( build_target.upper().replace("-", "_") ) env["CARGO_BUILD_TARGET"] = build_target env[cargo_linker_name] = join( self.ctx.ndk.llvm_prebuilt_dir, "bin", "{}{}-clang".format( # NDK's Clang format build_target.replace("7", "7a") if build_target.startswith("armv7") else build_target, self.ctx.ndk_api, ), ) realpython_dir = self.ctx.python_recipe.get_build_dir(arch.arch) env["RUSTFLAGS"] = "-Clink-args=-L{} -L{}".format( self.ctx.get_libs_dir(arch.arch), join(realpython_dir, "android-build") ) env["PYO3_CROSS_LIB_DIR"] = realpath(glob.glob(join( realpython_dir, "android-build", "build", "lib.*{}/".format(self.python_major_minor_version), ))[0]) info_main("Ensuring rust build toolchain") shprint(sh.rustup, "target", "add", build_target) # Add host python to PATH env["PATH"] = ("{hostpython_dir}:{old_path}").format( hostpython_dir=Recipe.get_recipe( "hostpython3", self.ctx ).get_path_to_python(), old_path=env["PATH"], ) return env def check_host_deps(self): if not hasattr(sh, "rustup"): error( "`rustup` was not found on host system." "Please install it using :" "\n`curl https://sh.rustup.rs -sSf | sh`\n" ) exit(1) def build_arch(self, arch): self.check_host_deps() super().build_arch(arch) class TargetPythonRecipe(Recipe): '''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.''' def __init__(self, *args, **kwargs): self._ctx = None super().__init__(*args, **kwargs) def prebuild_arch(self, arch): super().prebuild_arch(arch) self.ctx.python_recipe = self def include_root(self, arch): '''The root directory from which to include headers.''' raise NotImplementedError('Not implemented in TargetPythonRecipe') def link_root(self): raise NotImplementedError('Not implemented in TargetPythonRecipe') @property def major_minor_version_string(self): parsed_version = packaging.version.parse(self.version) return f"{parsed_version.major}.{parsed_version.minor}" 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. """ raise NotImplementedError('{} does not implement create_python_bundle'.format(self)) def reduce_object_file_names(self, dirn): """Recursively renames all files named XXX.cpython-...-linux-gnu.so" to "XXX.so", i.e. removing the erroneous architecture name coming from the local system. """ py_so_files = shprint(sh.find, dirn, '-iname', '*.so') filens = py_so_files.stdout.decode('utf-8').split('\n')[:-1] for filen in filens: file_dirname, file_basename = split(filen) parts = file_basename.split('.') if len(parts) <= 2: continue # PySide6 libraries end with .abi3.so if parts[1] == "abi3": continue move(filen, join(file_dirname, parts[0] + '.so')) def algsum(alg, filen): '''Calculate the digest of a file. ''' with open(filen, 'rb') as fileh: digest = getattr(hashlib, alg)(fileh.read()) return digest.hexdigest() ================================================ FILE: pythonforandroid/recipes/Pillow/__init__.py ================================================ from os.path import join from pythonforandroid.recipe import PyProjectRecipe class PillowRecipe(PyProjectRecipe): """ A recipe for Pillow (previously known as Pil). This recipe allow us to build the Pillow recipe with support for different types of images and fonts. But you should be aware, that in order to use some of the features of Pillow, we must build some libraries. By default we automatically trigger the build of below libraries:: - freetype: rendering fonts support. - harfbuzz: a text shaping library. - jpeg: reading and writing JPEG image files. - png: support for PNG images. But you also could enable the build of some extra image types by requesting the build of some libraries via argument `requirements`:: - libwebp: library to encode and decode images in WebP format. """ version = '11.3.0' url = 'https://github.com/python-pillow/Pillow/archive/{version}.tar.gz' site_packages_name = 'PIL' patches = ["setup.py.patch"] depends = ['png', 'jpeg', 'freetype'] hostpython_prerequisites = ["setuptools>=77"] opt_depends = ['libwebp'] def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) # Add math library linkage env["LDFLAGS"] = env.get("LDFLAGS", "") + " -lm" jpeg = self.get_recipe('jpeg', self.ctx) jpeg_inc_dir = jpeg_lib_dir = jpeg.get_build_dir(arch.arch) env["JPEG_ROOT"] = "{}:{}".format(jpeg_lib_dir, jpeg_inc_dir) freetype = self.get_recipe('freetype', self.ctx) free_lib_dir = join(freetype.get_build_dir(arch.arch), 'objs', '.libs') free_inc_dir = join(freetype.get_build_dir(arch.arch), 'include') env["FREETYPE_ROOT"] = "{}:{}".format(free_lib_dir, free_inc_dir) # harfbuzz is a direct dependency of freetype and we need the proper # flags to successfully build the Pillow recipe, so we add them here. harfbuzz = self.get_recipe('harfbuzz', self.ctx) harf_lib_dir = join(harfbuzz.get_build_dir(arch.arch), 'src', '.libs') harf_inc_dir = harfbuzz.get_build_dir(arch.arch) env["HARFBUZZ_ROOT"] = "{}:{}".format(harf_lib_dir, harf_inc_dir) env["ZLIB_ROOT"] = f"{arch.ndk_lib_dir_versioned}:{self.ctx.ndk.sysroot_include_dir}" # libwebp is an optional dependency, so we add the # flags if we have it in our `ctx.recipe_build_order` if 'libwebp' in self.ctx.recipe_build_order: webp = self.get_recipe('libwebp', self.ctx) webp_install = join( webp.get_build_dir(arch.arch), 'installation' ) env["WEBP_ROOT"] = f"{join(webp_install, 'lib')}:{join(webp_install, 'include')}" return env recipe = PillowRecipe() ================================================ FILE: pythonforandroid/recipes/Pillow/setup.py.patch ================================================ diff '--color=auto' -uNr Pillow-11.3.0/setup.py Pillow-11.3.0.mod/setup.py --- Pillow-11.3.0/setup.py 2025-07-01 13:11:24.000000000 +0530 +++ Pillow-11.3.0.mod/setup.py 2025-09-17 01:50:35.498105827 +0530 @@ -156,6 +156,7 @@ def _find_library_dirs_ldconfig() -> list[str]: + return [] # Based on ctypes.util from Python 2 ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" @@ -514,12 +515,10 @@ if root is None and root_name in os.environ: root_prefix = os.environ[root_name] - root = ( - os.path.join(root_prefix, "lib"), - os.path.join(root_prefix, "include"), - ) + root = tuple(os.environ[root_name].split(":")) if root is None and pkg_config: + continue if isinstance(lib_name, str): _dbg("Looking for `%s` using pkg-config.", lib_name) root = pkg_config(lib_name) @@ -565,13 +564,11 @@ for d in os.environ[k].split(os.path.pathsep): _add_directory(library_dirs, d) - _add_directory(library_dirs, os.path.join(sys.prefix, "lib")) - _add_directory(include_dirs, os.path.join(sys.prefix, "include")) # # add platform directories - if self.disable_platform_guessing: + if True: # self.disable_platform_guessing: pass elif sys.platform == "cygwin": @@ -674,7 +671,7 @@ # FIXME: check /opt/stuff directories here? # standard locations - if not self.disable_platform_guessing: + if False: # not self.disable_platform_guessing: _add_directory(library_dirs, "/usr/local/lib") _add_directory(include_dirs, "/usr/local/include") ================================================ FILE: pythonforandroid/recipes/__init__.py ================================================ ================================================ FILE: pythonforandroid/recipes/aiohttp/__init__.py ================================================ """Build AIOHTTP""" from typing import List from pythonforandroid.recipe import CppCompiledComponentsPythonRecipe class AIOHTTPRecipe(CppCompiledComponentsPythonRecipe): # type: ignore # pylint: disable=R0903 version = "3.8.3" url = "https://pypi.python.org/packages/source/a/aiohttp/aiohttp-{version}.tar.gz" name = "aiohttp" depends: List[str] = ["setuptools"] call_hostpython_via_targetpython = False install_in_hostpython = True def get_recipe_env(self, arch): env = super().get_recipe_env(arch) env['LDFLAGS'] += ' -lc++_shared' return env recipe = AIOHTTPRecipe() ================================================ FILE: pythonforandroid/recipes/android/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe, IncludedFilesBehaviour from pythonforandroid.util import current_directory from pythonforandroid import logger from os.path import join class AndroidRecipe(IncludedFilesBehaviour, PyProjectRecipe): # name = 'android' version = None url = None src_filename = 'src' depends = [('sdl3', 'sdl2', 'genericndkbuild'), 'pyjnius'] hostpython_prerequisites = ["Cython>=0.29,<3.1"] config_env = {} def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) env.update(self.config_env) return env def prebuild_arch(self, arch): super().prebuild_arch(arch) ctx_bootstrap = self.ctx.bootstrap.name # define macros for Cython, C, Python tpxi = 'DEF {} = {}\n' th = '#define {} {}\n' tpy = '{} = {}\n' # make sure bootstrap name is in unicode if isinstance(ctx_bootstrap, bytes): ctx_bootstrap = ctx_bootstrap.decode('utf-8') bootstrap = bootstrap_name = ctx_bootstrap if bootstrap_name in ["sdl2", "sdl3", "webview", "service_only", "service_library", "qt"]: java_ns = u'org.kivy.android' jni_ns = u'org/kivy/android' else: logger.error(( 'unsupported bootstrap for android recipe: {}' ''.format(bootstrap_name) )) exit(1) config = { 'BOOTSTRAP': bootstrap, 'IS_SDL2': int(bootstrap_name == "sdl2"), 'IS_SDL3': int(bootstrap_name == "sdl3"), 'PY2': 0, 'ANDROID_LIBS_DIR': "{}:{}".format( self.ctx.get_libs_dir(arch.arch), join(self.ctx.bootstrap.build_dir, 'obj', 'local', arch.arch) ), 'JAVA_NAMESPACE': java_ns, 'JNI_NAMESPACE': jni_ns, 'ACTIVITY_CLASS_NAME': self.ctx.activity_class_name, 'ACTIVITY_CLASS_NAMESPACE': self.ctx.activity_class_name.replace('.', '/'), 'SERVICE_CLASS_NAME': self.ctx.service_class_name, } # create config files for Cython, C and Python with ( current_directory(self.get_build_dir(arch.arch))), ( open(join('android', 'config.pxi'), 'w')) as fpxi, ( open(join('android', 'config.h'), 'w')) as fh, ( open(join('android', 'config.py'), 'w')) as fpy: for key, value in config.items(): fpxi.write(tpxi.format(key, repr(value))) fpy.write(tpy.format(key, repr(value))) fh.write(th.format( key, value if isinstance(value, int) else '"{}"'.format(value) )) self.config_env[key] = str(value) if bootstrap_name == "sdl2": fh.write('JNIEnv *SDL_AndroidGetJNIEnv(void);\n') fh.write( '#define SDL_ANDROID_GetJNIEnv SDL_AndroidGetJNIEnv\n' ) elif bootstrap_name == "sdl3": fh.write('JNIEnv *SDL_GetAndroidJNIEnv(void);\n') fh.write( '#define SDL_ANDROID_GetJNIEnv SDL_GetAndroidJNIEnv\n' ) else: fh.write('JNIEnv *WebView_AndroidGetJNIEnv(void);\n') fh.write( '#define SDL_ANDROID_GetJNIEnv WebView_AndroidGetJNIEnv\n' ) recipe = AndroidRecipe() ================================================ FILE: pythonforandroid/recipes/android/src/android/__init__.py ================================================ ''' Android module ============== ''' # legacy import from android._android import * # noqa: F401, F403 ================================================ FILE: pythonforandroid/recipes/android/src/android/_android.pyx ================================================ # Android-specific python services. include "config.pxi" # Android keycodes. KEYCODE_UNKNOWN = 0 KEYCODE_SOFT_LEFT = 1 KEYCODE_SOFT_RIGHT = 2 KEYCODE_HOME = 3 KEYCODE_BACK = 4 KEYCODE_CALL = 5 KEYCODE_ENDCALL = 6 KEYCODE_0 = 7 KEYCODE_1 = 8 KEYCODE_2 = 9 KEYCODE_3 = 10 KEYCODE_4 = 11 KEYCODE_5 = 12 KEYCODE_6 = 13 KEYCODE_7 = 14 KEYCODE_8 = 15 KEYCODE_9 = 16 KEYCODE_STAR = 17 KEYCODE_POUND = 18 KEYCODE_DPAD_UP = 19 KEYCODE_DPAD_DOWN = 20 KEYCODE_DPAD_LEFT = 21 KEYCODE_DPAD_RIGHT = 22 KEYCODE_DPAD_CENTER = 23 KEYCODE_VOLUME_UP = 24 KEYCODE_VOLUME_DOWN = 25 KEYCODE_POWER = 26 KEYCODE_CAMERA = 27 KEYCODE_CLEAR = 28 KEYCODE_A = 29 KEYCODE_B = 30 KEYCODE_C = 31 KEYCODE_D = 32 KEYCODE_E = 33 KEYCODE_F = 34 KEYCODE_G = 35 KEYCODE_H = 36 KEYCODE_I = 37 KEYCODE_J = 38 KEYCODE_K = 39 KEYCODE_L = 40 KEYCODE_M = 41 KEYCODE_N = 42 KEYCODE_O = 43 KEYCODE_P = 44 KEYCODE_Q = 45 KEYCODE_R = 46 KEYCODE_S = 47 KEYCODE_T = 48 KEYCODE_U = 49 KEYCODE_V = 50 KEYCODE_W = 51 KEYCODE_X = 52 KEYCODE_Y = 53 KEYCODE_Z = 54 KEYCODE_COMMA = 55 KEYCODE_PERIOD = 56 KEYCODE_ALT_LEFT = 57 KEYCODE_ALT_RIGHT = 58 KEYCODE_SHIFT_LEFT = 59 KEYCODE_SHIFT_RIGHT = 60 KEYCODE_TAB = 61 KEYCODE_SPACE = 62 KEYCODE_SYM = 63 KEYCODE_EXPLORER = 64 KEYCODE_ENVELOPE = 65 KEYCODE_ENTER = 66 KEYCODE_DEL = 67 KEYCODE_GRAVE = 68 KEYCODE_MINUS = 69 KEYCODE_EQUALS = 70 KEYCODE_LEFT_BRACKET = 71 KEYCODE_RIGHT_BRACKET = 72 KEYCODE_BACKSLASH = 73 KEYCODE_SEMICOLON = 74 KEYCODE_APOSTROPHE = 75 KEYCODE_SLASH = 76 KEYCODE_AT = 77 KEYCODE_NUM = 78 KEYCODE_HEADSETHOOK = 79 KEYCODE_FOCUS = 80 KEYCODE_PLUS = 81 KEYCODE_MENU = 82 KEYCODE_NOTIFICATION = 83 KEYCODE_SEARCH = 84 KEYCODE_MEDIA_PLAY_PAUSE= 85 KEYCODE_MEDIA_STOP = 86 KEYCODE_MEDIA_NEXT = 87 KEYCODE_MEDIA_PREVIOUS = 88 KEYCODE_MEDIA_REWIND = 89 KEYCODE_MEDIA_FAST_FORWARD = 90 KEYCODE_MUTE = 91 # Vibration support. cdef extern void android_vibrate(double) def vibrate(s): android_vibrate(s) # Accelerometer support. cdef extern void android_accelerometer_enable(int) cdef extern void android_accelerometer_reading(float *) accelerometer_enabled = False def accelerometer_enable(p): global accelerometer_enabled android_accelerometer_enable(p) accelerometer_enabled = p def accelerometer_reading(): cdef float rv[3] android_accelerometer_reading(rv) return (rv[0], rv[1], rv[2]) # Wifi reading support cdef extern void android_wifi_scanner_enable() cdef extern char * android_wifi_scan() def wifi_scanner_enable(): android_wifi_scanner_enable() def wifi_scan(): cdef char * reading reading = android_wifi_scan() reading_list = [] for line in filter(lambda l: l, reading.split('\n')): [ssid, mac, level] = line.split('\t') reading_list.append((ssid.strip(), mac.upper().strip(), int(level))) return reading_list # DisplayMetrics information. cdef extern int android_get_dpi() def get_dpi(): return android_get_dpi() # Soft keyboard. cdef extern void android_show_keyboard(int) cdef extern void android_hide_keyboard() from jnius import autoclass, PythonJavaClass, java_method, cast # API versions api_version = autoclass('android.os.Build$VERSION').SDK_INT version_codes = autoclass('android.os.Build$VERSION_CODES') python_act = autoclass(ACTIVITY_CLASS_NAME) Rect = autoclass(u'android.graphics.Rect') mActivity = python_act.mActivity if mActivity: # SDL2 now does not need the listener so there is # no point adding a processor intensive layout listenere here. height = 0 def get_keyboard_height(): rctx = Rect() mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rctx) # NOTE top should always be zero rctx.top = 0 height = mActivity.getWindowManager().getDefaultDisplay().getHeight() - (rctx.bottom - rctx.top) return height else: def get_keyboard_height(): return 0 # Flags for input_type, for requesting a particular type of keyboard #android FLAGS TYPE_CLASS_DATETIME = 4 TYPE_CLASS_NUMBER = 2 TYPE_NUMBER_VARIATION_NORMAL = 0 TYPE_NUMBER_VARIATION_PASSWORD = 16 TYPE_CLASS_TEXT = 1 TYPE_TEXT_FLAG_AUTO_COMPLETE = 65536 TYPE_TEXT_FLAG_AUTO_CORRECT = 32768 TYPE_TEXT_FLAG_NO_SUGGESTIONS = 524288 TYPE_TEXT_VARIATION_EMAIL_ADDRESS = 32 TYPE_TEXT_VARIATION_NORMAL = 0 TYPE_TEXT_VARIATION_PASSWORD = 128 TYPE_TEXT_VARIATION_POSTAL_ADDRESS = 112 TYPE_TEXT_VARIATION_URI = 16 TYPE_CLASS_PHONE = 3 IF BOOTSTRAP in ['sdl2', 'sdl3']: def remove_presplash(): # Remove android presplash in SDL2 bootstrap. mActivity.removeLoadingScreen() def show_keyboard(target, input_type): if input_type == 'text': _input_type = TYPE_CLASS_TEXT elif input_type == 'number': _input_type = TYPE_CLASS_NUMBER elif input_type == 'url': _input_type = \ TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_URI elif input_type == 'mail': _input_type = \ TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_EMAIL_ADDRESS elif input_type == 'datetime': _input_type = TYPE_CLASS_DATETIME elif input_type == 'tel': _input_type = TYPE_CLASS_PHONE elif input_type == 'address': _input_type = TYPE_TEXT_VARIATION_POSTAL_ADDRESS if hasattr(target, 'password') and target.password: if _input_type == TYPE_CLASS_TEXT: _input_type |= TYPE_TEXT_VARIATION_PASSWORD elif _input_type == TYPE_CLASS_NUMBER: _input_type |= TYPE_NUMBER_VARIATION_PASSWORD if hasattr(target, 'keyboard_suggestions') and not target.keyboard_suggestions: if _input_type == TYPE_CLASS_TEXT: _input_type = TYPE_CLASS_TEXT | \ TYPE_TEXT_FLAG_NO_SUGGESTIONS android_show_keyboard(_input_type) def hide_keyboard(): android_hide_keyboard() # Build info. cdef extern char* BUILD_MANUFACTURER cdef extern char* BUILD_MODEL cdef extern char* BUILD_PRODUCT cdef extern char* BUILD_VERSION_RELEASE cdef extern void android_get_buildinfo() class BuildInfo: MANUFACTURER = None MODEL = None PRODUCT = None VERSION_RELEASE = None def get_buildinfo(): android_get_buildinfo() binfo = BuildInfo() binfo.MANUFACTURER = BUILD_MANUFACTURER binfo.MODEL = BUILD_MODEL binfo.PRODUCT = BUILD_PRODUCT binfo.VERSION_RELEASE = BUILD_VERSION_RELEASE return binfo # ------------------------------------------------------------------- # URL Opening. def open_url(url): Intent = autoclass('android.content.Intent') Uri = autoclass('android.net.Uri') browserIntent = Intent() browserIntent.setAction(Intent.ACTION_VIEW) browserIntent.setData(Uri.parse(url)) currentActivity = cast('android.app.Activity', mActivity) currentActivity.startActivity(browserIntent) return True # Web browser support. class AndroidBrowser(object): def open(self, url, new=0, autoraise=True): return open_url(url) def open_new(self, url): return open_url(url) def open_new_tab(self, url): return open_url(url) import webbrowser webbrowser.register('android', AndroidBrowser) def start_service(title="Background Service", description="", arg="", as_foreground=True): # Legacy None value support (for old function signature style): if title is None: title = "Background Service" if description is None: description = "" if arg is None: arg = "" # Start service: mActivity = autoclass(ACTIVITY_CLASS_NAME).mActivity if as_foreground: mActivity.start_service( title, description, arg ) else: mActivity.start_service_not_as_foreground( title, description, arg ) cdef extern void android_stop_service() def stop_service(): android_stop_service() class AndroidService(object): '''Android service class. Run ``service/main.py`` from application directory as a service. :Parameters: `title`: str, default to 'Python service' Notification title. `description`: str, default to 'Kivy Python service started' Notification text. ''' def __init__(self, title='Python service', description='Kivy Python service started'): self.title = title self.description = description def start(self, arg=''): '''Start the service. :Parameters: `arg`: str, default to '' Argument to pass to a service, through environment variable ``PYTHON_SERVICE_ARGUMENT``. ''' start_service(self.title, self.description, arg) def stop(self): '''Stop the service. ''' stop_service() ================================================ FILE: pythonforandroid/recipes/android/src/android/_android_billing.pyx ================================================ # ------------------------------------------------------------------- # Billing cdef extern void android_billing_service_start() cdef extern void android_billing_service_stop() cdef extern void android_billing_buy(char *sku) cdef extern char *android_billing_get_purchased_items() cdef extern char *android_billing_get_pending_message() class BillingService(object): BILLING_ACTION_SUPPORTED = 'billingsupported' BILLING_ACTION_ITEMSCHANGED = 'itemschanged' BILLING_TYPE_INAPP = 'inapp' BILLING_TYPE_SUBSCRIPTION = 'subs' def __init__(self, callback): super().__init__() self.callback = callback self.purchased_items = None android_billing_service_start() def _stop(self): android_billing_service_stop() def buy(self, sku): cdef char *j_sku = sku android_billing_buy(j_sku) def get_purchased_items(self): cdef char *items = NULL cdef bytes pitem items = android_billing_get_purchased_items() if items == NULL: return [] pitems = items ret = {} for item in pitems.split('\n'): if not item: continue sku, qt = item.split(',') ret[sku] = {'qt': int(qt)} return ret def check(self, *largs): cdef char *message cdef bytes pymessage while True: message = android_billing_get_pending_message() if message == NULL: break pymessage = message self._handle_message(pymessage) if self.purchased_items is None: self._check_new_items() def _handle_message(self, message): action, data = message.split('|', 1) #print "HANDLE MESSAGE-----", (action, data) if action == 'billingSupported': tp, value = data.split('|') value = True if value == '1' else False self.callback(BillingService.BILLING_ACTION_SUPPORTED, tp, value) elif action == 'requestPurchaseResponse': self._check_new_items() elif action == 'purchaseStateChange': self._check_new_items() elif action == 'restoreTransaction': self._check_new_items() def _check_new_items(self): items = self.get_purchased_items() if self.purchased_items != items: self.purchased_items = items self.callback(BillingService.BILLING_ACTION_ITEMSCHANGED, self.purchased_items) ================================================ FILE: pythonforandroid/recipes/android/src/android/_android_billing_jni.c ================================================ #include #include #include #include #include #include "config.h" #define aassert(x) { if (!x) { __android_log_print(ANDROID_LOG_ERROR, "android_jni", "Assertion failed. %s:%d", __FILE__, __LINE__); abort(); }} #define PUSH_FRAME { (*env)->PushLocalFrame(env, 16); } #define POP_FRAME { (*env)->PopLocalFrame(env, NULL); } void android_billing_service_start() { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, JNI_NAMESPACE "/PythonActivity"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "billingServiceStart", "()V"); aassert(mid); } PUSH_FRAME; (*env)->CallStaticVoidMethod(env, cls, mid); POP_FRAME; } void android_billing_service_stop() { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, JNI_NAMESPACE "/PythonActivity"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "billingServiceStop", "()V"); aassert(mid); } PUSH_FRAME; (*env)->CallStaticVoidMethod(env, cls, mid); POP_FRAME; } void android_billing_buy(char *sku) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, JNI_NAMESPACE "/PythonActivity"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "billingBuy", "(Ljava/lang/String;)V"); aassert(mid); } PUSH_FRAME; (*env)->CallStaticVoidMethod( env, cls, mid, (*env)->NewStringUTF(env, sku) ); POP_FRAME; } char *android_billing_get_purchased_items() { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; jobject jreading; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, JNI_NAMESPACE "/PythonActivity"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "billingGetPurchasedItems", "()Ljava/lang/String;"); aassert(mid); } PUSH_FRAME; jreading = (*env)->CallStaticObjectMethod(env, cls, mid); const char * reading = (*env)->GetStringUTFChars(env, jreading, 0); POP_FRAME; return reading; } char *android_billing_get_pending_message() { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; jobject jreading; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, JNI_NAMESPACE "/PythonActivity"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "billingGetPendingMessage", "()Ljava/lang/String;"); aassert(mid); } PUSH_FRAME; jreading = (*env)->CallStaticObjectMethod(env, cls, mid); const char * reading = (*env)->GetStringUTFChars(env, jreading, 0); POP_FRAME; return reading; } ================================================ FILE: pythonforandroid/recipes/android/src/android/_android_jni.c ================================================ #include #include #include #include #include #include "config.h" #define aassert(x) { if (!x) { __android_log_print(ANDROID_LOG_ERROR, "android_jni", "Assertion failed. %s:%d", __FILE__, __LINE__); abort(); }} #define PUSH_FRAME { (*env)->PushLocalFrame(env, 16); } #define POP_FRAME { (*env)->PopLocalFrame(env, NULL); } void android_vibrate(double seconds) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/Hardware"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "vibrate", "(D)V"); aassert(mid); } (*env)->CallStaticVoidMethod( env, cls, mid, (jdouble) seconds); } void android_accelerometer_enable(int enable) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/Hardware"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "accelerometerEnable", "(Z)V"); aassert(mid); } (*env)->CallStaticVoidMethod( env, cls, mid, (jboolean) enable); } void android_wifi_scanner_enable(void){ static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/Hardware"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "enableWifiScanner", "()V"); aassert(mid); } (*env)->CallStaticVoidMethod(env, cls, mid); } char * android_wifi_scan() { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; jobject jreading; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/Hardware"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "scanWifi", "()Ljava/lang/String;"); aassert(mid); } PUSH_FRAME; jreading = (*env)->CallStaticObjectMethod(env, cls, mid); const char * reading = (*env)->GetStringUTFChars(env, jreading, 0); POP_FRAME; return reading; } void android_accelerometer_reading(float *values) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; jobject jvalues; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/Hardware"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "accelerometerReading", "()[F"); aassert(mid); } PUSH_FRAME; jvalues = (*env)->CallStaticObjectMethod(env, cls, mid); (*env)->GetFloatArrayRegion(env, jvalues, 0, 3, values); POP_FRAME; } int android_get_dpi(void) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/Hardware"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "getDPI", "()I"); aassert(mid); } return (*env)->CallStaticIntMethod(env, cls, mid); } void android_show_keyboard(int input_type) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/Hardware"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "showKeyboard", "(I)V"); aassert(mid); } (*env)->CallStaticVoidMethod(env, cls, mid, (jint) input_type); } void android_hide_keyboard(void) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/Hardware"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "hideKeyboard", "()V"); aassert(mid); } (*env)->CallStaticVoidMethod(env, cls, mid); } char* BUILD_MANUFACTURER = NULL; char* BUILD_MODEL = NULL; char* BUILD_PRODUCT = NULL; char* BUILD_VERSION_RELEASE = NULL; void android_get_buildinfo() { static JNIEnv *env = NULL; if (env == NULL) { jclass *cls = NULL; jfieldID fid; jstring sval; env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "android/os/Build"); fid = (*env)->GetStaticFieldID(env, cls, "MANUFACTURER", "Ljava/lang/String;"); sval = (jstring) (*env)->GetStaticObjectField(env, cls, fid); BUILD_MANUFACTURER = (*env)->GetStringUTFChars(env, sval, 0); fid = (*env)->GetStaticFieldID(env, cls, "MODEL", "Ljava/lang/String;"); sval = (jstring) (*env)->GetStaticObjectField(env, cls, fid); BUILD_MODEL = (*env)->GetStringUTFChars(env, sval, 0); fid = (*env)->GetStaticFieldID(env, cls, "PRODUCT", "Ljava/lang/String;"); sval = (jstring) (*env)->GetStaticObjectField(env, cls, fid); BUILD_PRODUCT = (*env)->GetStringUTFChars(env, sval, 0); cls = (*env)->FindClass(env, "android/os/Build$VERSION"); fid = (*env)->GetStaticFieldID(env, cls, "RELEASE", "Ljava/lang/String;"); sval = (jstring) (*env)->GetStaticObjectField(env, cls, fid); BUILD_VERSION_RELEASE = (*env)->GetStringUTFChars(env, sval, 0); } } void android_stop_service() { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); cls = (*env)->FindClass(env, JNI_NAMESPACE "/PythonActivity"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "stop_service", "()V"); aassert(mid); } (*env)->CallStaticVoidMethod(env, cls, mid); } ================================================ FILE: pythonforandroid/recipes/android/src/android/_android_sound.pyx ================================================ cdef extern void android_sound_queue(int, char *, char *, long long, long long) cdef extern void android_sound_play(int, char *, char *, long long, long long) cdef extern void android_sound_stop(int) cdef extern void android_sound_seek(int, float) cdef extern void android_sound_dequeue(int) cdef extern void android_sound_playing_name(int, char *, int) cdef extern void android_sound_pause(int) cdef extern void android_sound_unpause(int) cdef extern void android_sound_set_volume(int, float) cdef extern void android_sound_set_secondary_volume(int, float) cdef extern void android_sound_set_pan(int, float) cdef extern int android_sound_queue_depth(int) cdef extern int android_sound_get_pos(int) cdef extern int android_sound_get_length(int) channels = set() volumes = { } def queue(channel, file, name, fadein=0, tight=False): channels.add(channel) real_fn = file.name base = getattr(file, "base", -1) length = getattr(file, "length", -1) android_sound_queue(channel, name, real_fn, base, length) def play(channel, file, name, paused=False, fadein=0, tight=False): channels.add(channel) real_fn = file.name base = getattr(file, "base", -1) length = getattr(file, "length", -1) android_sound_play(channel, name, real_fn, base, length) def seek(channel, position): android_sound_seek(channel, position) def stop(channel): android_sound_stop(channel) def dequeue(channel, even_tight=False): android_sound_dequeue(channel) def queue_depth(channel): return android_sound_queue_depth(channel) def playing_name(channel): cdef char buf[1024] android_sound_playing_name(channel, buf, 1024) rv = buf if not len(rv): return None return rv def pause(channel): android_sound_pause(channel) return def unpause(channel): android_sound_unpause(channel) return def unpause_all(): for i in channels: unpause(i) def pause_all(): for i in channels: pause(i) def fadeout(channel, ms): stop(channel) def busy(channel): return playing_name(channel) != None def get_pos(channel): return android_sound_get_pos(channel) def get_length(channel): return android_sound_get_length(channel) def set_volume(channel, volume): android_sound_set_volume(channel, volume) volumes[channel] = volume def set_secondary_volume(channel, volume): android_sound_set_secondary_volume(channel, volume) def set_pan(channel, pan): android_sound_set_pan(channel, pan) def set_end_event(channel, event): return def get_volume(channel): return volumes.get(channel, 1.0) def init(freq, stereo, samples, status=False): return def quit(): for i in channels: stop(i) def periodic(): return def alloc_event(surf): return def refresh_event(): return def check_version(version): return ================================================ FILE: pythonforandroid/recipes/android/src/android/_android_sound_jni.c ================================================ #include #include #include #include JNIEnv *SDL_ANDROID_GetJNIEnv(); #define aassert(x) { if (!x) { __android_log_print(ANDROID_LOG_ERROR, "android_sound_jni", "Assertion failed. %s:%d", __FILE__, __LINE__); abort(); }} #define PUSH_FRAME { (*env)->PushLocalFrame(env, 16); } #define POP_FRAME { (*env)->PopLocalFrame(env, NULL); } void android_sound_queue(int channel, char *filename, char *real_fn, long long base, long long length) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/RenPySound"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "queue", "(ILjava/lang/String;Ljava/lang/String;JJ)V"); aassert(mid); } PUSH_FRAME; (*env)->CallStaticVoidMethod( env, cls, mid, channel, (*env)->NewStringUTF(env, filename), (*env)->NewStringUTF(env, real_fn), (jlong) base, (jlong) length); POP_FRAME; } void android_sound_play(int channel, char *filename, char *real_fn, long long base, long long length) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/RenPySound"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "play", "(ILjava/lang/String;Ljava/lang/String;JJ)V"); aassert(mid); } PUSH_FRAME; (*env)->CallStaticVoidMethod( env, cls, mid, channel, (*env)->NewStringUTF(env, filename), (*env)->NewStringUTF(env, real_fn), (jlong) base, (jlong) length); POP_FRAME; } void android_sound_seek(int channel, float position){ static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/RenPySound"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "seek", "(IF)V"); aassert(mid); } (*env)->CallStaticVoidMethod( env, cls, mid, channel, (jfloat) position); } void android_sound_stop(int channel) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/RenPySound"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "stop", "(I)V"); aassert(mid); } (*env)->CallStaticVoidMethod( env, cls, mid, channel); } void android_sound_dequeue(int channel) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/RenPySound"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "dequeue", "(I)V"); aassert(mid); } (*env)->CallStaticVoidMethod( env, cls, mid, channel); } int android_sound_queue_depth(int channel) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/RenPySound"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "queue_depth", "(I)I"); aassert(mid); } (*env)->CallStaticIntMethod( env, cls, mid, channel); } void android_sound_playing_name(int channel, char *buf, int buflen) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; jobject s = NULL; char *jbuf; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/RenPySound"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "playing_name", "(I)Ljava/lang/String;"); aassert(mid); } PUSH_FRAME; s = (*env)->CallStaticObjectMethod( env, cls, mid, channel); jbuf = (*env)->GetStringUTFChars(env, s, NULL); strncpy(buf, jbuf, buflen); (*env)->ReleaseStringUTFChars(env, s, jbuf); POP_FRAME; } void android_sound_set_volume(int channel, float value) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/RenPySound"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "set_volume", "(IF)V"); aassert(mid); } (*env)->CallStaticVoidMethod( env, cls, mid, channel, (jfloat) value); } void android_sound_set_secondary_volume(int channel, float value) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/RenPySound"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "set_secondary_volume", "(IF)V"); aassert(mid); } (*env)->CallStaticVoidMethod( env, cls, mid, channel, (jfloat) value); } void android_sound_set_pan(int channel, float value) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/RenPySound"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "set_pan", "(IF)V"); aassert(mid); } (*env)->CallStaticVoidMethod( env, cls, mid, channel, (jfloat) value); } void android_sound_pause(int channel) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/RenPySound"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "pause", "(I)V"); aassert(mid); } (*env)->CallStaticVoidMethod( env, cls, mid, channel); } void android_sound_unpause(int channel) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/RenPySound"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "unpause", "(I)V"); aassert(mid); } (*env)->CallStaticVoidMethod( env, cls, mid, channel); } int android_sound_get_pos(int channel) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/RenPySound"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "get_pos", "(I)I"); aassert(mid); } return (*env)->CallStaticIntMethod( env, cls, mid, channel); } int android_sound_get_length(int channel) { static JNIEnv *env = NULL; static jclass *cls = NULL; static jmethodID mid = NULL; if (env == NULL) { env = SDL_ANDROID_GetJNIEnv(); aassert(env); cls = (*env)->FindClass(env, "org/renpy/android/RenPySound"); aassert(cls); mid = (*env)->GetStaticMethodID(env, cls, "get_length", "(I)I"); aassert(mid); } return (*env)->CallStaticIntMethod( env, cls, mid, channel); } ================================================ FILE: pythonforandroid/recipes/android/src/android/_ctypes_library_finder.py ================================================ import sys import os def get_activity_lib_dir(activity_name): from jnius import autoclass # Get the actual activity instance: activity_class = autoclass(activity_name) if activity_class is None: return None activity = None if hasattr(activity_class, "mActivity") and \ activity_class.mActivity is not None: activity = activity_class.mActivity elif hasattr(activity_class, "mService") and \ activity_class.mService is not None: activity = activity_class.mService if activity is None: return None # Extract the native lib dir from the activity instance: package_name = activity.getApplicationContext().getPackageName() manager = activity.getApplicationContext().getPackageManager() manager_class = autoclass("android.content.pm.PackageManager") native_lib_dir = manager.getApplicationInfo( package_name, manager_class.GET_SHARED_LIBRARY_FILES ).nativeLibraryDir return native_lib_dir def does_libname_match_filename(search_name, file_path): # Filter file names so given search_name="mymodule" we match one of: # mymodule.so (direct name + .so) # libmymodule.so (added lib prefix) # mymodule.arm64.so (added dot-separated middle parts) # mymodule.so.1.3.4 (added dot-separated version tail) # and all above (all possible combinations) import re file_name = os.path.basename(file_path) return (re.match(r"^(lib)?" + re.escape(search_name) + r"\.(.*\.)?so(\.[0-9]+)*$", file_name) is not None) def find_library(name): # Obtain all places for native libraries: if sys.maxsize > 2**32: # 64bit-build lib_search_dirs = ["/system/lib64", "/system/lib"] else: lib_search_dirs = ["/system/lib"] lib_dir_1 = get_activity_lib_dir("org.kivy.android.PythonActivity") if lib_dir_1 is not None: lib_search_dirs.insert(0, lib_dir_1) lib_dir_2 = get_activity_lib_dir("org.kivy.android.PythonService") if lib_dir_2 is not None and lib_dir_2 not in lib_search_dirs: lib_search_dirs.insert(0, lib_dir_2) # Now scan the lib dirs: for lib_dir in [ldir for ldir in lib_search_dirs if os.path.exists(ldir)]: filelist = [ f for f in os.listdir(lib_dir) if does_libname_match_filename(name, f) ] if len(filelist) > 0: return os.path.join(lib_dir, filelist[0]) return None ================================================ FILE: pythonforandroid/recipes/android/src/android/activity.py ================================================ from jnius import PythonJavaClass, autoclass, java_method from android.config import ACTIVITY_CLASS_NAME, ACTIVITY_CLASS_NAMESPACE _activity = autoclass(ACTIVITY_CLASS_NAME).mActivity _callbacks = { 'on_new_intent': [], 'on_activity_result': [], } class NewIntentListener(PythonJavaClass): __javainterfaces__ = [ACTIVITY_CLASS_NAMESPACE + '$NewIntentListener'] __javacontext__ = 'app' def __init__(self, callback, **kwargs): super().__init__(**kwargs) self.callback = callback @java_method('(Landroid/content/Intent;)V') def onNewIntent(self, intent): self.callback(intent) class ActivityResultListener(PythonJavaClass): __javainterfaces__ = [ACTIVITY_CLASS_NAMESPACE + '$ActivityResultListener'] __javacontext__ = 'app' def __init__(self, callback): super().__init__() self.callback = callback @java_method('(IILandroid/content/Intent;)V') def onActivityResult(self, requestCode, resultCode, intent): self.callback(requestCode, resultCode, intent) def bind(**kwargs): for event, callback in kwargs.items(): if event not in _callbacks: raise Exception('Unknown {!r} event'.format(event)) elif event == 'on_new_intent': listener = NewIntentListener(callback) _activity.registerNewIntentListener(listener) _callbacks[event].append(listener) elif event == 'on_activity_result': listener = ActivityResultListener(callback) _activity.registerActivityResultListener(listener) _callbacks[event].append(listener) def unbind(**kwargs): for event, callback in kwargs.items(): if event not in _callbacks: raise Exception('Unknown {!r} event'.format(event)) else: for listener in _callbacks[event][:]: if listener.callback == callback: _callbacks[event].remove(listener) if event == 'on_new_intent': _activity.unregisterNewIntentListener(listener) elif event == 'on_activity_result': _activity.unregisterActivityResultListener(listener) # Keep a reference to all the registered classes so that python doesn't # garbage collect them. _lifecycle_callbacks = set() class ActivityLifecycleCallbacks(PythonJavaClass): """Callback class for handling PythonActivity lifecycle transitions""" __javainterfaces__ = ['android/app/Application$ActivityLifecycleCallbacks'] def __init__(self, callbacks): super().__init__() # It would be nice to use keyword arguments, but PythonJavaClass # doesn't allow that in its __cinit__ method. if not isinstance(callbacks, dict): raise ValueError('callbacks must be a dict instance') self.callbacks = callbacks def _callback(self, name, *args): func = self.callbacks.get(name) if func: return func(*args) @java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V') def onActivityCreated(self, activity, savedInstanceState): self._callback('onActivityCreated', activity, savedInstanceState) @java_method('(Landroid/app/Activity;)V') def onActivityDestroyed(self, activity): self._callback('onActivityDestroyed', activity) @java_method('(Landroid/app/Activity;)V') def onActivityPaused(self, activity): self._callback('onActivityPaused', activity) @java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V') def onActivityPostCreated(self, activity, savedInstanceState): self._callback('onActivityPostCreated', activity, savedInstanceState) @java_method('(Landroid/app/Activity;)V') def onActivityPostDestroyed(self, activity): self._callback('onActivityPostDestroyed', activity) @java_method('(Landroid/app/Activity;)V') def onActivityPostPaused(self, activity): self._callback('onActivityPostPaused', activity) @java_method('(Landroid/app/Activity;)V') def onActivityPostResumed(self, activity): self._callback('onActivityPostResumed', activity) @java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V') def onActivityPostSaveInstanceState(self, activity, outState): self._callback('onActivityPostSaveInstanceState', activity, outState) @java_method('(Landroid/app/Activity;)V') def onActivityPostStarted(self, activity): self._callback('onActivityPostStarted', activity) @java_method('(Landroid/app/Activity;)V') def onActivityPostStopped(self, activity): self._callback('onActivityPostStopped', activity) @java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V') def onActivityPreCreated(self, activity, savedInstanceState): self._callback('onActivityPreCreated', activity, savedInstanceState) @java_method('(Landroid/app/Activity;)V') def onActivityPreDestroyed(self, activity): self._callback('onActivityPreDestroyed', activity) @java_method('(Landroid/app/Activity;)V') def onActivityPrePaused(self, activity): self._callback('onActivityPrePaused', activity) @java_method('(Landroid/app/Activity;)V') def onActivityPreResumed(self, activity): self._callback('onActivityPreResumed', activity) @java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V') def onActivityPreSaveInstanceState(self, activity, outState): self._callback('onActivityPreSaveInstanceState', activity, outState) @java_method('(Landroid/app/Activity;)V') def onActivityPreStarted(self, activity): self._callback('onActivityPreStarted', activity) @java_method('(Landroid/app/Activity;)V') def onActivityPreStopped(self, activity): self._callback('onActivityPreStopped', activity) @java_method('(Landroid/app/Activity;)V') def onActivityResumed(self, activity): self._callback('onActivityResumed', activity) @java_method('(Landroid/app/Activity;Landroid/os/Bundle;)V') def onActivitySaveInstanceState(self, activity, outState): self._callback('onActivitySaveInstanceState', activity, outState) @java_method('(Landroid/app/Activity;)V') def onActivityStarted(self, activity): self._callback('onActivityStarted', activity) @java_method('(Landroid/app/Activity;)V') def onActivityStopped(self, activity): self._callback('onActivityStopped', activity) def register_activity_lifecycle_callbacks(**callbacks): """Register ActivityLifecycleCallbacks instance The callbacks are supplied as keyword arguments corresponding to the Application.ActivityLifecycleCallbacks methods such as onActivityStarted. See the ActivityLifecycleCallbacks documentation for the signature of each method. The ActivityLifecycleCallbacks instance is returned so it can be supplied to unregister_activity_lifecycle_callbacks if needed. """ instance = ActivityLifecycleCallbacks(callbacks) _lifecycle_callbacks.add(instance) # Use the registerActivityLifecycleCallbacks method from the # Activity class if it's available (API 29) since it guarantees the # callbacks will only be run for that activity. Otherwise, fallback # to the method on the Application class (API 14). In practice there # should be no difference since p4a applications only have a single # activity. if hasattr(_activity, 'registerActivityLifecycleCallbacks'): _activity.registerActivityLifecycleCallbacks(instance) else: app = _activity.getApplication() app.registerActivityLifecycleCallbacks(instance) return instance def unregister_activity_lifecycle_callbacks(instance): """Unregister ActivityLifecycleCallbacks instance""" if hasattr(_activity, 'unregisterActivityLifecycleCallbacks'): _activity.unregisterActivityLifecycleCallbacks(instance) else: app = _activity.getApplication() app.unregisterActivityLifecycleCallbacks(instance) try: _lifecycle_callbacks.remove(instance) except KeyError: pass ================================================ FILE: pythonforandroid/recipes/android/src/android/billing.py ================================================ ''' Android Billing API =================== ''' ================================================ FILE: pythonforandroid/recipes/android/src/android/broadcast.py ================================================ # ------------------------------------------------------------------- # Broadcast receiver bridge import logging from jnius import autoclass, PythonJavaClass, java_method from android.config import JAVA_NAMESPACE, JNI_NAMESPACE, ACTIVITY_CLASS_NAME, SERVICE_CLASS_NAME logger = logging.getLogger("BroadcastReceiver") logger.setLevel(logging.DEBUG) class BroadcastReceiver(object): class Callback(PythonJavaClass): __javainterfaces__ = [JNI_NAMESPACE + '/GenericBroadcastReceiverCallback'] __javacontext__ = 'app' def __init__(self, callback, *args, **kwargs): self.callback = callback PythonJavaClass.__init__(self, *args, **kwargs) @java_method('(Landroid/content/Context;Landroid/content/Intent;)V') def onReceive(self, context, intent): self.callback(context, intent) def __init__(self, callback, actions=None, categories=None): super().__init__() self.callback = callback self._is_registered = False if not actions and not categories: raise Exception('You need to define at least actions or categories') def _expand_partial_name(partial_name): if '.' in partial_name: return partial_name # Its actually a full dotted name else: name = 'ACTION_{}'.format(partial_name.upper()) if not hasattr(Intent, name): raise Exception('The intent {} does not exist'.format(name)) return getattr(Intent, name) # resolve actions/categories first Intent = autoclass('android.content.Intent') resolved_actions = [_expand_partial_name(x) for x in actions or []] resolved_categories = [_expand_partial_name(x) for x in categories or []] # resolve android API GenericBroadcastReceiver = autoclass(JAVA_NAMESPACE + '.GenericBroadcastReceiver') IntentFilter = autoclass('android.content.IntentFilter') HandlerThread = autoclass('android.os.HandlerThread') # create a thread for handling events from the receiver self.handlerthread = HandlerThread('handlerthread') # create a listener self.listener = BroadcastReceiver.Callback(self.callback) self.receiver = GenericBroadcastReceiver(self.listener) self.receiver_filter = IntentFilter() for x in resolved_actions: self.receiver_filter.addAction(x) for x in resolved_categories: self.receiver_filter.addCategory(x) def start(self): if hasattr(self, 'handlerthread') and self.handlerthread.isAlive(): logger.debug("HandlerThread already running, skipping start") return HandlerThread = autoclass('android.os.HandlerThread') self.handlerthread = HandlerThread('handlerthread') self.handlerthread.start() if self._is_registered: logger.info("Already registered.") return Handler = autoclass('android.os.Handler') self.handler = Handler(self.handlerthread.getLooper()) self.context.registerReceiver( self.receiver, self.receiver_filter, None, self.handler) self._is_registered = True def stop(self): try: self.context.unregisterReceiver(self.receiver) self._is_registered = False except Exception as e: logger.error("unregisterReceiver failed: %s", e) if hasattr(self, 'handlerthread'): self.handlerthread.quitSafely() self.handlerthread = None self.handler = None @property def context(self): from os import environ if 'PYTHON_SERVICE_ARGUMENT' in environ: PythonService = autoclass(SERVICE_CLASS_NAME) return PythonService.mService PythonActivity = autoclass(ACTIVITY_CLASS_NAME) return PythonActivity.mActivity ================================================ FILE: pythonforandroid/recipes/android/src/android/display_cutout.py ================================================ from jnius import autoclass from kivy.core.window import Window from android import mActivity __all__ = ('get_cutout_pos', 'get_cutout_size', 'get_width_of_bar', 'get_height_of_bar', 'get_size_of_bar', 'get_width_of_bar', 'get_cutout_mode') def _core_cutout(): decorview = mActivity.getWindow().getDecorView() cutout = decorview.rootWindowInsets.displayCutout return cutout.boundingRects.get(0) def get_cutout_pos(): """Get position of the display-cutout. Returns integer for each positions (xy) """ try: cutout = _core_cutout() return int(cutout.left), int(Window.height - cutout.height()) except Exception: # Doesn't have a camera builtin with the display return 0, 0 def get_cutout_size(): """Get the size (xy) of the front camera. Returns size with float values """ try: cutout = _core_cutout() return float(cutout.width()), float(cutout.height()) except Exception: # Doesn't have a camera builtin with the display return 0., 0. def get_height_of_bar(bar_target=None): """Get the height of either statusbar or navigationbar bar_target = status or navigation and defaults to status """ bar_target = bar_target or 'status' if bar_target not in ('status', 'navigation'): raise Exception("bar_target must be 'status' or 'navigation'") try: displayMetrics = autoclass('android.util.DisplayMetrics') mActivity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics()) resources = mActivity.getResources() resourceId = resources.getIdentifier(f'{bar_target}_bar_height', 'dimen', 'android') return float(max(resources.getDimensionPixelSize(resourceId), 0)) except Exception: # Getting the size is not supported on older Androids return 0. def get_width_of_bar(bar_target=None): """Get the width of the bar""" return Window.width def get_size_of_bar(bar_target=None): """Get the size of either statusbar or navigationbar bar_target = status or navigation and defaults to status """ return get_width_of_bar(), get_height_of_bar(bar_target) def get_heights_of_both_bars(): """Return heights of both bars""" return get_height_of_bar('status'), get_height_of_bar('navigation') def get_cutout_mode(): """Return mode for cutout supported applications""" BuildVersion = autoclass('android.os.Build$VERSION') cutout_modes = {} if BuildVersion.SDK_INT >= 28: LayoutParams = autoclass('android.view.WindowManager$LayoutParams') window = mActivity.getWindow() layout_params = window.getAttributes() cutout_mode = layout_params.layoutInDisplayCutoutMode cutout_modes.update({LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT: 'default', LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES: 'shortEdges'}) if BuildVersion.SDK_INT >= 30: cutout_modes[LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS] = 'always' return cutout_modes.get(cutout_mode, 'never') return None ================================================ FILE: pythonforandroid/recipes/android/src/android/loadingscreen.py ================================================ from jnius import autoclass from android.config import ACTIVITY_CLASS_NAME def hide_loading_screen(): mActivity = autoclass(ACTIVITY_CLASS_NAME).mActivity mActivity.removeLoadingScreen() ================================================ FILE: pythonforandroid/recipes/android/src/android/mixer.py ================================================ # This module is, as much a possible, a clone of the pygame # mixer api. import android._android_sound as sound import time import threading import os condition = threading.Condition() def periodic(): for i in range(0, num_channels): if i in channels: channels[i].periodic() num_channels = 8 reserved_channels = 0 def init(frequency=22050, size=-16, channels=2, buffer=4096): return None def pre_init(frequency=22050, size=-16, channels=2, buffersize=4096): return None def quit(): stop() return None def stop(): for i in range(0, num_channels): sound.stop(i) def pause(): for i in range(0, num_channels): sound.pause(i) def unpause(): for i in range(0, num_channels): sound.unpause(i) def get_busy(): for i in range(0, num_channels): if sound.busy(i): return True return False def fadeout(time): # Fadeout doesn't work - it just immediately stops playback. stop() # A map from channel number to Channel object. channels = {} def set_num_channels(count): global num_channels num_channels = count def get_num_channels(count): return num_channels def set_reserved(count): global reserved_channels reserved_channels = count def find_channel(force=False): busy = [] for i in range(reserved_channels, num_channels): c = Channel(i) if not c.get_busy(): return c busy.append(c) if not force: return None busy.sort(key=lambda x: x.play_time) return busy[0] class ChannelImpl(object): def __init__(self, id): self.id = id self.loop = None self.queued = None self.play_time = time.time() def periodic(self): qd = sound.queue_depth(self.id) if qd < 2: self.queued = None if self.loop is not None and sound.queue_depth(self.id) < 2: self.queue(self.loop, loops=1) def play(self, s, loops=0, maxtime=0, fade_ms=0): if loops: self.loop = s sound.play(self.id, s.file, s.serial) self.play_time = time.time() with condition: condition.notify() def seek(self, position): sound.seek(self.id, position) def stop(self): self.loop = None sound.stop(self.id) def pause(self): sound.pause(self.id) def unpause(self): sound.pause(self.id) def fadeout(self, time): # No fadeout self.stop() def set_volume(self, left, right=None): sound.set_volume(self.id, left) def get_volume(self): return sound.get_volume(self.id) def get_busy(self): return sound.busy(self.id) def get_sound(self): is_busy = sound.busy(self.id) if not is_busy: return serial = sound.playing_name(self.id) if not serial: return return sounds.get(serial, None) def queue(self, s): self.loop = None self.queued = s sound.queue(self.id, s.what, s.serial) with condition: condition.notify() def get_queue(self): return self.queued def get_pos(self): return sound.get_pos(self.id)/1000. def get_length(self): return sound.get_length(self.id)/1000. def Channel(n): """ Gets the channel with the given number. """ rv = channels.get(n, None) if rv is None: rv = ChannelImpl(n) channels[n] = rv return rv sound_serial = 0 sounds = {} class Sound(object): def __init__(self, what): # Doesn't support buffers. global sound_serial self._channel = None self._volume = 1. self.serial = str(sound_serial) sound_serial += 1 if isinstance(what, file): # noqa F821 self.file = what else: self.file = file(os.path.abspath(what), "rb") # noqa F821 sounds[self.serial] = self def play(self, loops=0, maxtime=0, fade_ms=0): # avoid new play if the sound is already playing # -> same behavior as standard pygame. if self._channel is not None: if self._channel.get_sound() is self: return self._channel = channel = find_channel(True) channel.set_volume(self._volume) channel.play(self, loops=loops) return channel def stop(self): for i in range(0, num_channels): if Channel(i).get_sound() is self: Channel(i).stop() def fadeout(self, time): self.stop() def set_volume(self, left, right=None): self._volume = left if self._channel: if self._channel.get_sound() is self: self._channel.set_volume(self._volume) def get_volume(self): return self._volume def get_num_channels(self): rv = 0 for i in range(0, num_channels): if Channel(i).get_sound() is self: rv += 1 return rv def get_length(self): return 1.0 music_channel = Channel(256) music_sound = None class music(object): @staticmethod def load(filename): music_channel.stop() global music_sound music_sound = Sound(filename) @staticmethod def play(loops=0, start=0.0): # No start. music_channel.play(music_sound, loops=loops) @staticmethod def rewind(): music_channel.play(music_sound) @staticmethod def seek(position): music_channel.seek(position) @staticmethod def stop(): music_channel.stop() @staticmethod def pause(): music_channel.pause() @staticmethod def unpause(): music_channel.unpause() @staticmethod def fadeout(time): music_channel.fadeout(time) @staticmethod def set_volume(value): music_channel.set_volume(value) @staticmethod def get_volume(): return music_channel.get_volume() @staticmethod def get_busy(): return music_channel.get_busy() @staticmethod def get_pos(): return music_channel.get_pos() @staticmethod def queue(filename): return music_channel.queue(Sound(filename)) ================================================ FILE: pythonforandroid/recipes/android/src/android/permissions.py ================================================ import threading try: from jnius import autoclass, PythonJavaClass, java_method except ImportError: # To allow importing by build/manifest-creating code without # pyjnius being present: def autoclass(item): raise RuntimeError("pyjnius not available") from android.config import ACTIVITY_CLASS_NAME, ACTIVITY_CLASS_NAMESPACE class Permission: ACCEPT_HANDOVER = "android.permission.ACCEPT_HANDOVER" ACCESS_BACKGROUND_LOCATION = "android.permission.ACCESS_BACKGROUND_LOCATION" ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION" ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION" ACCESS_LOCATION_EXTRA_COMMANDS = ( "android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" ) ACCESS_NETWORK_STATE = "android.permission.ACCESS_NETWORK_STATE" ACCESS_NOTIFICATION_POLICY = ( "android.permission.ACCESS_NOTIFICATION_POLICY" ) ACCESS_WIFI_STATE = "android.permission.ACCESS_WIFI_STATE" ADD_VOICEMAIL = "com.android.voicemail.permission.ADD_VOICEMAIL" ANSWER_PHONE_CALLS = "android.permission.ANSWER_PHONE_CALLS" BATTERY_STATS = "android.permission.BATTERY_STATS" BIND_ACCESSIBILITY_SERVICE = ( "android.permission.BIND_ACCESSIBILITY_SERVICE" ) BIND_AUTOFILL_SERVICE = "android.permission.BIND_AUTOFILL_SERVICE" BIND_CARRIER_MESSAGING_SERVICE = ( # note: deprecated in api 23+ "android.permission.BIND_CARRIER_MESSAGING_SERVICE" ) BIND_CARRIER_SERVICES = ( # replaces BIND_CARRIER_MESSAGING_SERVICE "android.permission.BIND_CARRIER_SERVICES" ) BIND_CHOOSER_TARGET_SERVICE = ( "android.permission.BIND_CHOOSER_TARGET_SERVICE" ) BIND_CONDITION_PROVIDER_SERVICE = ( "android.permission.BIND_CONDITION_PROVIDER_SERVICE" ) BIND_DEVICE_ADMIN = "android.permission.BIND_DEVICE_ADMIN" BIND_DREAM_SERVICE = "android.permission.BIND_DREAM_SERVICE" BIND_INCALL_SERVICE = "android.permission.BIND_INCALL_SERVICE" BIND_INPUT_METHOD = ( "android.permission.BIND_INPUT_METHOD" ) BIND_MIDI_DEVICE_SERVICE = ( "android.permission.BIND_MIDI_DEVICE_SERVICE" ) BIND_NFC_SERVICE = ( "android.permission.BIND_NFC_SERVICE" ) BIND_NOTIFICATION_LISTENER_SERVICE = ( "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" ) BIND_PRINT_SERVICE = ( "android.permission.BIND_PRINT_SERVICE" ) BIND_QUICK_SETTINGS_TILE = ( "android.permission.BIND_QUICK_SETTINGS_TILE" ) BIND_REMOTEVIEWS = ( "android.permission.BIND_REMOTEVIEWS" ) BIND_SCREENING_SERVICE = ( "android.permission.BIND_SCREENING_SERVICE" ) BIND_TELECOM_CONNECTION_SERVICE = ( "android.permission.BIND_TELECOM_CONNECTION_SERVICE" ) BIND_TEXT_SERVICE = ( "android.permission.BIND_TEXT_SERVICE" ) BIND_TV_INPUT = ( "android.permission.BIND_TV_INPUT" ) BIND_VISUAL_VOICEMAIL_SERVICE = ( "android.permission.BIND_VISUAL_VOICEMAIL_SERVICE" ) BIND_VOICE_INTERACTION = ( "android.permission.BIND_VOICE_INTERACTION" ) BIND_VPN_SERVICE = ( "android.permission.BIND_VPN_SERVICE" ) BIND_VR_LISTENER_SERVICE = ( "android.permission.BIND_VR_LISTENER_SERVICE" ) BIND_WALLPAPER = ( "android.permission.BIND_WALLPAPER" ) BLUETOOTH = ( "android.permission.BLUETOOTH" ) BLUETOOTH_ADVERTISE = ( "android.permission.BLUETOOTH_ADVERTISE" ) BLUETOOTH_CONNECT = ( "android.permission.BLUETOOTH_CONNECT" ) BLUETOOTH_SCAN = ( "android.permission.BLUETOOTH_SCAN" ) BLUETOOTH_ADMIN = ( "android.permission.BLUETOOTH_ADMIN" ) BODY_SENSORS = ( "android.permission.BODY_SENSORS" ) BROADCAST_PACKAGE_REMOVED = ( "android.permission.BROADCAST_PACKAGE_REMOVED" ) BROADCAST_STICKY = ( "android.permission.BROADCAST_STICKY" ) CALL_PHONE = ( "android.permission.CALL_PHONE" ) CALL_PRIVILEGED = ( "android.permission.CALL_PRIVILEGED" ) CAMERA = ( "android.permission.CAMERA" ) CAPTURE_AUDIO_OUTPUT = ( "android.permission.CAPTURE_AUDIO_OUTPUT" ) CAPTURE_SECURE_VIDEO_OUTPUT = ( "android.permission.CAPTURE_SECURE_VIDEO_OUTPUT" ) CAPTURE_VIDEO_OUTPUT = ( "android.permission.CAPTURE_VIDEO_OUTPUT" ) CHANGE_COMPONENT_ENABLED_STATE = ( "android.permission.CHANGE_COMPONENT_ENABLED_STATE" ) CHANGE_CONFIGURATION = ( "android.permission.CHANGE_CONFIGURATION" ) CHANGE_NETWORK_STATE = ( "android.permission.CHANGE_NETWORK_STATE" ) CHANGE_WIFI_MULTICAST_STATE = ( "android.permission.CHANGE_WIFI_MULTICAST_STATE" ) CHANGE_WIFI_STATE = ( "android.permission.CHANGE_WIFI_STATE" ) CLEAR_APP_CACHE = ( "android.permission.CLEAR_APP_CACHE" ) CONTROL_LOCATION_UPDATES = ( "android.permission.CONTROL_LOCATION_UPDATES" ) DELETE_CACHE_FILES = ( "android.permission.DELETE_CACHE_FILES" ) DELETE_PACKAGES = ( "android.permission.DELETE_PACKAGES" ) DIAGNOSTIC = ( "android.permission.DIAGNOSTIC" ) DISABLE_KEYGUARD = ( "android.permission.DISABLE_KEYGUARD" ) DUMP = ( "android.permission.DUMP" ) EXPAND_STATUS_BAR = ( "android.permission.EXPAND_STATUS_BAR" ) FACTORY_TEST = ( "android.permission.FACTORY_TEST" ) FOREGROUND_SERVICE = ( "android.permission.FOREGROUND_SERVICE" ) GET_ACCOUNTS = ( "android.permission.GET_ACCOUNTS" ) GET_ACCOUNTS_PRIVILEGED = ( "android.permission.GET_ACCOUNTS_PRIVILEGED" ) GET_PACKAGE_SIZE = ( "android.permission.GET_PACKAGE_SIZE" ) GET_TASKS = ( "android.permission.GET_TASKS" ) GLOBAL_SEARCH = ( "android.permission.GLOBAL_SEARCH" ) INSTALL_LOCATION_PROVIDER = ( "android.permission.INSTALL_LOCATION_PROVIDER" ) INSTALL_PACKAGES = ( "android.permission.INSTALL_PACKAGES" ) INSTALL_SHORTCUT = ( "com.android.launcher.permission.INSTALL_SHORTCUT" ) INSTANT_APP_FOREGROUND_SERVICE = ( "android.permission.INSTANT_APP_FOREGROUND_SERVICE" ) INTERNET = ( "android.permission.INTERNET" ) KILL_BACKGROUND_PROCESSES = ( "android.permission.KILL_BACKGROUND_PROCESSES" ) LOCATION_HARDWARE = ( "android.permission.LOCATION_HARDWARE" ) MANAGE_DOCUMENTS = ( "android.permission.MANAGE_DOCUMENTS" ) MANAGE_OWN_CALLS = ( "android.permission.MANAGE_OWN_CALLS" ) MASTER_CLEAR = ( "android.permission.MASTER_CLEAR" ) MEDIA_CONTENT_CONTROL = ( "android.permission.MEDIA_CONTENT_CONTROL" ) MODIFY_AUDIO_SETTINGS = ( "android.permission.MODIFY_AUDIO_SETTINGS" ) MODIFY_PHONE_STATE = ( "android.permission.MODIFY_PHONE_STATE" ) MOUNT_FORMAT_FILESYSTEMS = ( "android.permission.MOUNT_FORMAT_FILESYSTEMS" ) MOUNT_UNMOUNT_FILESYSTEMS = ( "android.permission.MOUNT_UNMOUNT_FILESYSTEMS" ) NEARBY_WIFI_DEVICES = ( "android.permission.NEARBY_WIFI_DEVICES" ) NFC = ( "android.permission.NFC" ) NFC_TRANSACTION_EVENT = ( "android.permission.NFC_TRANSACTION_EVENT" ) PACKAGE_USAGE_STATS = ( "android.permission.PACKAGE_USAGE_STATS" ) PERSISTENT_ACTIVITY = ( "android.permission.PERSISTENT_ACTIVITY" ) POST_NOTIFICATIONS = ( "android.permission.POST_NOTIFICATIONS" ) PROCESS_OUTGOING_CALLS = ( "android.permission.PROCESS_OUTGOING_CALLS" ) READ_CALENDAR = ( "android.permission.READ_CALENDAR" ) READ_CALL_LOG = ( "android.permission.READ_CALL_LOG" ) READ_CONTACTS = ( "android.permission.READ_CONTACTS" ) READ_EXTERNAL_STORAGE = ( "android.permission.READ_EXTERNAL_STORAGE" ) READ_FRAME_BUFFER = ( "android.permission.READ_FRAME_BUFFER" ) READ_INPUT_STATE = ( "android.permission.READ_INPUT_STATE" ) READ_LOGS = ( "android.permission.READ_LOGS" ) READ_MEDIA_AUDIO = ( "android.permission.READ_MEDIA_AUDIO" ) READ_MEDIA_IMAGES = ( "android.permission.READ_MEDIA_IMAGES" ) READ_MEDIA_VIDEO = ( "android.permission.READ_MEDIA_VIDEO" ) READ_PHONE_NUMBERS = ( "android.permission.READ_PHONE_NUMBERS" ) READ_PHONE_STATE = ( "android.permission.READ_PHONE_STATE" ) READ_SMS = ( "android.permission.READ_SMS" ) READ_SYNC_SETTINGS = ( "android.permission.READ_SYNC_SETTINGS" ) READ_SYNC_STATS = ( "android.permission.READ_SYNC_STATS" ) READ_VOICEMAIL = ( "com.android.voicemail.permission.READ_VOICEMAIL" ) REBOOT = ( "android.permission.REBOOT" ) RECEIVE_BOOT_COMPLETED = ( "android.permission.RECEIVE_BOOT_COMPLETED" ) RECEIVE_MMS = ( "android.permission.RECEIVE_MMS" ) RECEIVE_SMS = ( "android.permission.RECEIVE_SMS" ) RECEIVE_WAP_PUSH = ( "android.permission.RECEIVE_WAP_PUSH" ) RECORD_AUDIO = ( "android.permission.RECORD_AUDIO" ) REORDER_TASKS = ( "android.permission.REORDER_TASKS" ) REQUEST_COMPANION_RUN_IN_BACKGROUND = ( "android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" ) REQUEST_COMPANION_USE_DATA_IN_BACKGROUND = ( "android.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND" ) REQUEST_DELETE_PACKAGES = ( "android.permission.REQUEST_DELETE_PACKAGES" ) REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = ( "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" ) REQUEST_INSTALL_PACKAGES = ( "android.permission.REQUEST_INSTALL_PACKAGES" ) RESTART_PACKAGES = ( "android.permission.RESTART_PACKAGES" ) SEND_RESPOND_VIA_MESSAGE = ( "android.permission.SEND_RESPOND_VIA_MESSAGE" ) SEND_SMS = ( "android.permission.SEND_SMS" ) SET_ALARM = ( "com.android.alarm.permission.SET_ALARM" ) SET_ALWAYS_FINISH = ( "android.permission.SET_ALWAYS_FINISH" ) SET_ANIMATION_SCALE = ( "android.permission.SET_ANIMATION_SCALE" ) SET_DEBUG_APP = ( "android.permission.SET_DEBUG_APP" ) SET_PREFERRED_APPLICATIONS = ( "android.permission.SET_PREFERRED_APPLICATIONS" ) SET_PROCESS_LIMIT = ( "android.permission.SET_PROCESS_LIMIT" ) SET_TIME = ( "android.permission.SET_TIME" ) SET_TIME_ZONE = ( "android.permission.SET_TIME_ZONE" ) SET_WALLPAPER = ( "android.permission.SET_WALLPAPER" ) SET_WALLPAPER_HINTS = ( "android.permission.SET_WALLPAPER_HINTS" ) SIGNAL_PERSISTENT_PROCESSES = ( "android.permission.SIGNAL_PERSISTENT_PROCESSES" ) STATUS_BAR = ( "android.permission.STATUS_BAR" ) SYSTEM_ALERT_WINDOW = ( "android.permission.SYSTEM_ALERT_WINDOW" ) TRANSMIT_IR = ( "android.permission.TRANSMIT_IR" ) UNINSTALL_SHORTCUT = ( "com.android.launcher.permission.UNINSTALL_SHORTCUT" ) UPDATE_DEVICE_STATS = ( "android.permission.UPDATE_DEVICE_STATS" ) USE_BIOMETRIC = ( "android.permission.USE_BIOMETRIC" ) USE_FINGERPRINT = ( "android.permission.USE_FINGERPRINT" ) USE_SIP = ( "android.permission.USE_SIP" ) VIBRATE = ( "android.permission.VIBRATE" ) WAKE_LOCK = ( "android.permission.WAKE_LOCK" ) WRITE_APN_SETTINGS = ( "android.permission.WRITE_APN_SETTINGS" ) WRITE_CALENDAR = ( "android.permission.WRITE_CALENDAR" ) WRITE_CALL_LOG = ( "android.permission.WRITE_CALL_LOG" ) WRITE_CONTACTS = ( "android.permission.WRITE_CONTACTS" ) WRITE_EXTERNAL_STORAGE = ( "android.permission.WRITE_EXTERNAL_STORAGE" ) WRITE_GSERVICES = ( "android.permission.WRITE_GSERVICES" ) WRITE_SECURE_SETTINGS = ( "android.permission.WRITE_SECURE_SETTINGS" ) WRITE_SETTINGS = ( "android.permission.WRITE_SETTINGS" ) WRITE_SYNC_SETTINGS = ( "android.permission.WRITE_SYNC_SETTINGS" ) WRITE_VOICEMAIL = ( "com.android.voicemail.permission.WRITE_VOICEMAIL" ) MANAGE_EXTERNAL_STORAGE = ( # Convenient use of paths to manage files "android.permission.MANAGE_EXTERNAL_STORAGE" ) PERMISSION_GRANTED = 0 PERMISSION_DENIED = -1 class _onRequestPermissionsCallback(PythonJavaClass): """Callback class for registering a Python callback from onRequestPermissionsResult in PythonActivity. """ __javainterfaces__ = [ACTIVITY_CLASS_NAMESPACE + '$PermissionsCallback'] __javacontext__ = 'app' def __init__(self, func): self.func = func super().__init__() @java_method('(I[Ljava/lang/String;[I)V') def onRequestPermissionsResult(self, requestCode, permissions, grantResults): self.func(requestCode, permissions, grantResults) class _RequestPermissionsManager: """Internal class for requesting Android permissions. Permissions are requested through the method 'request_permissions' which accepts a list of permissions and an optional callback. Any callback will asynchronously receive arguments from onRequestPermissionsResult on PythonActivity after requestPermissions is called. The callback supplied must accept two arguments: 'permissions' and 'grantResults' (as supplied to onPermissionsCallbackResult). Note that for SDK_INT < 23, run-time permissions are not required, and so the callback will be called immediately. The attribute '_java_callback' is initially None, but is set when the first permissions request is made. It is set to an instance of onRequestPermissionsCallback, which allows the Java callback to be propagated to the class method 'python_callback'. This is then, in turn, used to call an application callback if provided to request_permissions. The attribute '_callback_id' is incremented with each call to request_permissions which has a callback (the value '1' is used for any call which does not pass a callback). This is passed to requestCode in the Java call, and used to identify (via the _callbacks dictionary) the matching call. """ _SDK_INT = None _java_callback = None _callbacks = {1: None} _callback_id = 1 # Lock to prevent multiple calls to request_permissions being handled # simultaneously (as incrementing _callback_id is not atomic) _lock = threading.Lock() @classmethod def register_callback(cls): """Register Java callback for requestPermissions.""" cls._java_callback = _onRequestPermissionsCallback(cls.python_callback) mActivity = autoclass(ACTIVITY_CLASS_NAME).mActivity mActivity.addPermissionsCallback(cls._java_callback) @classmethod def request_permissions(cls, permissions, callback=None): """Requests Android permissions from PythonActivity. If 'callback' is supplied, the request is made with a new requestCode and the callback is stored in the _callbacks dict. When a Java callback with the matching requestCode is received, callback will be called with arguments of 'permissions' and 'grant_results'. """ if not cls._SDK_INT: # Get the Android build version and store it VERSION = autoclass('android.os.Build$VERSION') cls.SDK_INT = VERSION.SDK_INT if cls.SDK_INT < 23: # No run-time permissions needed, return immediately. if callback: callback(permissions, [True for x in permissions]) return # Request permissions with cls._lock: if not cls._java_callback: cls.register_callback() mActivity = autoclass(ACTIVITY_CLASS_NAME).mActivity if not callback: mActivity.requestPermissions(permissions) else: cls._callback_id += 1 mActivity.requestPermissionsWithRequestCode( permissions, cls._callback_id) cls._callbacks[cls._callback_id] = callback @classmethod def python_callback(cls, requestCode, permissions, grantResults): """Calls the relevant callback with arguments of 'permissions' and 'grantResults'.""" # Convert from Android codes to True/False grant_results = [x == PERMISSION_GRANTED for x in grantResults] if cls._callbacks.get(requestCode): cls._callbacks[requestCode](permissions, grant_results) # Public API methods for requesting permissions def request_permissions(permissions, callback=None): """Requests Android permissions. Args: permissions (str): A list of permissions to requests (str) callback (callable, optional): A function to call when the request is completed (callable) Returns: None Notes: Permission strings can be imported from the 'Permission' class in this module. For example: from android import Permission permissions_list = [Permission.CAMERA, Permission.WRITE_EXTERNAL_STORAGE] See the p4a source file 'permissions.py' for a list of valid permission strings (pythonforandroid/recipes/android/src/android/permissions.py). Any callback supplied must accept two arguments: permissions (list of str): A list of permission strings grant_results (list of bool): A list of bools indicating whether the respective permission was granted. See Android documentation for onPermissionsCallbackResult for further information. Note that if the request is interrupted the callback may contain an empty list of permissions, without permissions being granted; the App should check that each permission requested has been granted. Also note that when calling request_permission on SDK_INT < 23, the callback will be returned immediately as requesting permissions is not required. """ _RequestPermissionsManager.request_permissions(permissions, callback) def request_permission(permission, callback=None): request_permissions([permission], callback) def check_permission(permission): """Checks if an app holds the passed permission. Args: - permission An Android permission (str) Returns: bool: True if the app holds the permission given, False otherwise. """ mActivity = autoclass(ACTIVITY_CLASS_NAME).mActivity result = bool(mActivity.checkCurrentPermission( permission + "" )) return result ================================================ FILE: pythonforandroid/recipes/android/src/android/runnable.py ================================================ ''' Runnable ======== ''' from jnius import PythonJavaClass, java_method, autoclass from android.config import ACTIVITY_CLASS_NAME # Reference to the activity _PythonActivity = autoclass(ACTIVITY_CLASS_NAME) # 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 call of a Python function into the PythonActivity thread. ''' __javainterfaces__ = ['java/lang/Runnable'] __runnables__ = [] def __init__(self, func): super().__init__() self.func = func def __call__(self, *args, **kwargs): self.args = args self.kwargs = kwargs Runnable.__runnables__.append(self) _PythonActivity.mActivity.runOnUiThread(self) @java_method('()V') def run(self): try: self.func(*self.args, **self.kwargs) except: # noqa E722 import traceback traceback.print_exc() Runnable.__runnables__.remove(self) 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): rfunction(*args, **kwargs) return f2 ================================================ FILE: pythonforandroid/recipes/android/src/android/storage.py ================================================ from jnius import autoclass, cast import os from android.config import ACTIVITY_CLASS_NAME, SERVICE_CLASS_NAME Environment = autoclass('android.os.Environment') File = autoclass('java.io.File') def _android_has_is_removable_func(): VERSION = autoclass('android.os.Build$VERSION') return (VERSION.SDK_INT >= 24) def _get_sdcard_path(): """ Internal function to return getExternalStorageDirectory() path. This is internal because it may either return the internal, or an external sd card, depending on the device. Use primary_external_storage_path() or secondary_external_storage_path() instead which try to distinguish this properly. """ return ( Environment.getExternalStorageDirectory().getAbsolutePath() ) def _get_activity(): """ Retrieves the activity from `PythonActivity` fallback to `PythonService`. """ PythonActivity = autoclass(ACTIVITY_CLASS_NAME) activity = PythonActivity.mActivity if activity is None: # assume we're running from the background service PythonService = autoclass(SERVICE_CLASS_NAME) activity = PythonService.mService return activity def app_storage_path(): """ Locate the built-in device storage used for this app only. This storage is APP-SPECIFIC, and not visible to other apps. It will be wiped when your app is uninstalled. Returns directory path to storage. """ activity = _get_activity() currentActivity = cast('android.app.Activity', activity) context = cast('android.content.ContextWrapper', currentActivity.getApplicationContext()) file_p = cast('java.io.File', context.getFilesDir()) return os.path.normpath(os.path.abspath( file_p.getAbsolutePath().replace("/", os.path.sep))) def primary_external_storage_path(): """ Locate the built-in device storage that user can see via file browser. Often found at: /sdcard/ This is storage is SHARED, and visible to other apps and the user. It will remain untouched when your app is uninstalled. Returns directory path to storage. WARNING: You need storage permissions to access this storage. """ if _android_has_is_removable_func(): sdpath = _get_sdcard_path() # Apparently this can both return primary (built-in) or # secondary (removable) external storage depending on the device, # therefore check that we got what we wanted: if not Environment.isExternalStorageRemovable(File(sdpath)): return sdpath if "EXTERNAL_STORAGE" in os.environ: return os.environ["EXTERNAL_STORAGE"] raise RuntimeError( "unexpectedly failed to determine " + "primary external storage path" ) def secondary_external_storage_path(): """ Locate the external SD Card storage, which may not be present. Often found at: /sdcard/External_SD/ This storage is SHARED, visible to other apps, and may not be be available if the user didn't put in an external SD card. It will remain untouched when your app is uninstalled. Returns None if not found, otherwise path to storage. WARNING: You need storage permissions to access this storage. If it is not writable and presents as empty even with permissions, then the external sd card may not be present. """ if _android_has_is_removable_func: # See if getExternalStorageDirectory() returns secondary ext storage: sdpath = _get_sdcard_path() # Apparently this can both return primary (built-in) or # secondary (removable) external storage depending on the device, # therefore check that we got what we wanted: if Environment.isExternalStorageRemovable(File(sdpath)): if os.path.exists(sdpath): return sdpath # See if we can take a guess based on environment variables: p = None if "SECONDARY_STORAGE" in os.environ: p = os.environ["SECONDARY_STORAGE"] elif "EXTERNAL_SDCARD_STORAGE" in os.environ: p = os.environ["EXTERNAL_SDCARD_STORAGE"] if p is not None and os.path.exists(p): return p return None ================================================ FILE: pythonforandroid/recipes/android/src/android/touch.py ================================================ """Touch interception helpers for Python for Android. This module exposes two utilities to hook into the Android SDL surface's intercept touch mechanism via pyjnius: - `OnInterceptTouchListener`: a thin bridge class that implements the Java interface `SDLSurface.OnInterceptTouchListener` and delegates to a provided Python callable. - `TouchListener`: a convenience class with helpers to register/unregister the intercept listener and a hit-testing routine against the Kivy `Window` to decide whether a touch should be consumed. - `TouchListener.register_listener` requires a `target_widget` argument, which is used for hit-testing to decide whether to consume touches. - Touch coordinates are taken from pointer index 0 and converted to Kivy's coordinate system by inverting Y relative to `Window.height`. Dependencies: pyjnius for bridging to Android, and Kivy for window and widget traversal used in hit-testing. """ from jnius import PythonJavaClass, java_method, autoclass from android.config import ACTIVITY_CLASS_NAME __all__ = ('OnInterceptTouchListener', 'TouchListener') class OnInterceptTouchListener(PythonJavaClass): """Bridge for Android's `SDLSurface.OnInterceptTouchListener`. Instances of this class can be passed to the SDL surface so that touch events can be intercepted before they reach the normal Android/Kivy dispatch pipeline. The Python callable provided at construction time is invoked for each `MotionEvent` and should return a boolean indicating whether the touch was consumed. """ __javacontext__ = 'app' __javainterfaces__ = [ 'org/libsdl/app/SDLSurface$OnInterceptTouchListener'] def __init__(self, listener): """Create a new intercept touch listener. Parameters: listener (Callable[[object], bool]): A callable that receives the Android `MotionEvent` instance and returns `True` if the touch should be consumed (intercepted), or `False` to let it propagate normally. """ self.listener = listener @java_method('(Landroid/view/MotionEvent;)Z') def onTouch(self, event): """Handle an incoming `MotionEvent`. Parameters: event: The Android `MotionEvent` object delivered by the SDL surface. Returns: bool: The boolean returned by the user-provided `listener`, where `True` indicates the event was consumed and should not propagate further; `False` lets normal processing continue. """ return self.listener(event) class TouchListener: """Convenience API to register a global Android intercept touch listener. This class manages a singleton instance of `OnInterceptTouchListener` that is attached to the app's `PythonActivity.mSurface`. It also stores a reference to a specific `target_widget` used during hit-testing to decide whether touches should be consumed. A small hit-testing helper walks the Kivy `Window` widget tree to determine whether a touch should be intercepted (consumed) or allowed to propagate. Notes: - The intercept listener affects the entire SDL surface and thus the whole app; use with care. - The internal `__listener` attribute stores the active listener instance when registered, or `None` when not set. - The internal `__target_widget` holds the widget against which the hit-test is compared and is cleared on `unregister_listener()`. """ __listener = None __target_widget = None @classmethod def register_listener(cls, target_widget): """Register the global intercept touch listener if not already set. This creates a singleton `OnInterceptTouchListener` that delegates to `TouchListener._on_touch_listener` and installs it on `PythonActivity.mSurface` via pyjnius. Parameters: target_widget: The widget used as the reference during hit-testing. If the touch lands on this widget and no other widget is found under the touch, the event will be consumed by the intercept listener. """ if cls.__listener: return cls.__target_widget = target_widget cls.__listener = OnInterceptTouchListener(cls._on_touch_listener) PythonActivity = autoclass(ACTIVITY_CLASS_NAME) PythonActivity.mSurface.setInterceptTouchListener(cls.__listener) @classmethod def unregister_listener(cls): """Unregister the global intercept touch listener, if any. Removes the previously installed listener from `PythonActivity.mSurface` by setting it to `None`. This does not modify the stored reference in `__listener`. """ PythonActivity = autoclass(ACTIVITY_CLASS_NAME) PythonActivity.mSurface.setInterceptTouchListener(None) cls.__target_widget = None @classmethod def is_listener_set(cls): """Report whether the intercept listener reference is set. Returns: bool: `False` if a listener instance is currently stored in `__listener` (i.e. registered), `True` if no listener is stored. Note: this method reflects the current implementation which returns the negation of the internal reference. """ return not cls.__listener @classmethod def _on_touch_listener(cls, event): """Default callback used by the installed intercept listener. What it does now (current behavior): - Reads touch coordinates from pointer index 0 using `event.getX(0)` and `event.getY(0)`. - Converts Android coordinates to Kivy coordinates by inverting the Y axis relative to `Window.height`. - Iterates over `Window.children` in reverse (front-to-back) and uses `TouchListener._pick` to select the deepest widget under the touch for each top-level child. - Compares the picked widget with the internally stored `__target_widget` that was provided to `register_listener(...)`. - Returns `True` (consume/intercept) only when the picked widget is exactly `__target_widget` and no other widget was found under the touch. Otherwise returns `False`. Important notes and limitations: - There is no filtering by MotionEvent action; all actions reaching this callback are evaluated the same way. - Only pointer index 0 is considered; multi-touch pointers other than index 0 are ignored. - The check is identity-based (`is`) against `__target_widget`. - If another widget (other than `__target_widget`) is hit, the event is not intercepted and will propagate normally. Parameters: event: The Android `MotionEvent` that triggered the listener. Returns: bool: `True` to consume the touch when the hit-test selects the `__target_widget` and no other widget is found; otherwise `False` to allow normal dispatch. """ from kivy.core.window import Window x = event.getX(0) y = event.getY(0) # invert Y ! y = Window.height - y # x, y are in Window coordinate. Try to select the widget under the # touch. me = None for child in reversed(Window.children): widget = cls._pick(child, x, y) if not widget: continue if cls.__target_widget is widget: me = widget # keep scanning to ensure no other widget is hit continue # any non-target hit means we should not intercept return False return cls.__target_widget is me @classmethod def _pick(cls, widget, x, y): """Pick the deepest child widget at coordinates. Parameters: widget: The root widget from which to start the search. x (float): X coordinate in the local space of `widget`. y (float): Y coordinate in the local space of `widget`. Returns: The deepest child that collides with the given point, or the highest-level `widget` itself if it collides and no deeper child does; otherwise `None` if no collision. """ # Fast exit if the root doesn't collide if not widget.collide_point(x, y): return None # Always descend through the first colliding child in z-order current = widget lx, ly = x, y while True: # Transform coordinates once per level nlx, nly = current.to_local(lx, ly) hit_child = None for child in reversed(current.children): if child.collide_point(nlx, nly): # keep the last colliding child in this order, matching # the original recursive implementation's semantics hit_child = child if hit_child is None: # No deeper child collides; current is the deepest hit return current # Prepare for next level using parent's local coords; we'll # convert again at the next iteration relative to the new # current widget. lx, ly = nlx, nly # Continue descent into the chosen child current = hit_child ================================================ FILE: pythonforandroid/recipes/android/src/setup.py ================================================ from setuptools import setup, Extension from Cython.Build import cythonize import os library_dirs = os.environ['ANDROID_LIBS_DIR'].split(":") lib_dict = { 'sdl2': ['SDL2', 'SDL2_image', 'SDL2_mixer', 'SDL2_ttf'], 'sdl3': ['SDL3', 'SDL3_image', 'SDL3_mixer', 'SDL3_ttf'], } sdl_libs = lib_dict.get(os.environ['BOOTSTRAP'], ['main']) modules = [ Extension('android._android', ['android/_android.pyx', 'android/_android_jni.c'], libraries=sdl_libs + ['log'], library_dirs=library_dirs), Extension('android._android_billing', ['android/_android_billing.pyx', 'android/_android_billing_jni.c'], libraries=['log'], library_dirs=library_dirs), Extension('android._android_sound', ['android/_android_sound.pyx', 'android/_android_sound_jni.c'], libraries=['log'], library_dirs=library_dirs, extra_compile_args=['-include', 'stdlib.h']) ] cythonized_modules = cythonize(modules, compiler_directives={'language_level': "3"}) setup(name='android', version='1.0', packages=['android'], package_dir={'android': 'android'}, ext_modules=cythonized_modules ) ================================================ FILE: pythonforandroid/recipes/apsw/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe class ApswRecipe(PyProjectRecipe): version = '3.50.4.0' url = 'https://github.com/rogerbinns/apsw/releases/download/{version}/apsw-{version}.tar.gz' depends = ['sqlite3'] site_packages_name = 'apsw' def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) sqlite_recipe = self.get_recipe('sqlite3', self.ctx) env['CFLAGS'] += ' -I' + sqlite_recipe.get_build_dir(arch.arch) env['LIBS'] = env.get('LIBS', '') + ' -lsqlite3' return env recipe = ApswRecipe() ================================================ FILE: pythonforandroid/recipes/argon2-cffi/__init__.py ================================================ from pythonforandroid.recipe import CompiledComponentsPythonRecipe class Argon2Recipe(CompiledComponentsPythonRecipe): version = '20.1.0' url = 'git+https://github.com/hynek/argon2-cffi' depends = ['setuptools', 'cffi'] call_hostpython_via_targetpython = False build_cmd = 'build' def get_recipe_env(self, arch): env = super().get_recipe_env(arch) env['ARGON2_CFFI_USE_SSE2'] = '0' return env recipe = Argon2Recipe() ================================================ FILE: pythonforandroid/recipes/atom/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe class AtomRecipe(PyProjectRecipe): site_packages_name = "atom" version = "0.11.0" url = "https://files.pythonhosted.org/packages/source/a/atom/atom-{version}.tar.gz" depends = ["setuptools"] patches = ["pyproject.toml.patch"] recipe = AtomRecipe() ================================================ FILE: pythonforandroid/recipes/atom/pyproject.toml.patch ================================================ diff --git a/pyproject.toml b/pyproject.toml index d41287f..c83b053 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ [tool.setuptools] include-package-data = false package-data = { atom = ["py.typed", "*.pyi"] } + packages = ["atom"] [tool.setuptools_scm] write_to = "atom/version.py" ================================================ FILE: pythonforandroid/recipes/aubio/__init__.py ================================================ """ Aubio recipe. Note that this hasn't been ported to cross compile from macOS yet, the error on 0.4.9 was: src/aubio_priv.h:95:10: fatal error: 'Accelerate/Accelerate.h' file not found #include """ from pythonforandroid.recipe import PyProjectRecipe class AubioRecipe(PyProjectRecipe): version = "0.4.9" url = "https://aubio.org/pub/aubio-{version}.tar.bz2" depends = ["numpy", "setuptools"] recipe = AubioRecipe() ================================================ FILE: pythonforandroid/recipes/audiostream/__init__.py ================================================ from pythonforandroid.recipe import CythonRecipe from pythonforandroid.toolchain import shprint, current_directory, info import sh from os.path import join class AudiostreamRecipe(CythonRecipe): # audiostream has no tagged versions; this is the latest commit to master 2020-12-22 # it includes a fix for the dyload issue on android that was preventing use version = '69f6b100f1ea4e3982a1acf6bbb0804e31a2cd50' url = 'https://github.com/kivy/audiostream/archive/{version}.zip' sha256sum = '4d415c91706fd76865d0d22f1945f87900dc42125ff5a6c8d77898ccdf613c21' name = 'audiostream' depends = ['python3', 'sdl2', 'pyjnius'] def get_recipe_env(self, arch): env = super().get_recipe_env(arch) sdl_include = 'SDL2' env['USE_SDL2'] = 'True' env['SDL2_INCLUDE_DIR'] = join(self.ctx.bootstrap.build_dir, 'jni', 'SDL', 'include') env['CFLAGS'] += ' -I{jni_path}/{sdl_include}/include'.format( jni_path=join(self.ctx.bootstrap.build_dir, 'jni'), sdl_include=sdl_include) sdl2_mixer_recipe = self.get_recipe('sdl2_mixer', self.ctx) for include_dir in sdl2_mixer_recipe.get_include_dirs(arch): env['CFLAGS'] += ' -I{include_dir}'.format(include_dir=include_dir) # NDKPLATFORM is our switch for detecting Android platform, so can't be None env['NDKPLATFORM'] = "NOTNONE" env['LIBLINK'] = 'NOTNONE' # Hacky fix. Needed by audiostream setup.py return env def postbuild_arch(self, arch): # TODO: This code was copied from pyjnius, but judging by the # audiostream history, it looks like this step might have # happened automatically in the past. # Given the goal of migrating off of recipes, it would # be good to repair or build infrastructure for doing this # automatically, for when including a java class is # the best solution to a problem. super().postbuild_arch(arch) info('Copying audiostream java files to classes build dir') with current_directory(self.get_build_dir(arch.arch)): shprint(sh.cp, '-a', join('audiostream', 'platform', 'android'), self.ctx.javaclass_dir) recipe = AudiostreamRecipe() ================================================ FILE: pythonforandroid/recipes/av/__init__.py ================================================ from pythonforandroid.toolchain import Recipe from pythonforandroid.recipe import CythonRecipe class PyAVRecipe(CythonRecipe): name = "av" version = "13.1.0" url = "https://github.com/PyAV-Org/PyAV/archive/v{version}.zip" depends = ["python3", "cython", "ffmpeg", "av_codecs"] opt_depends = ["openssl"] patches = ['patches/compilation_syntax_errors.patch'] def get_recipe_env(self, arch, with_flags_in_cc=True): env = super().get_recipe_env(arch) build_dir = Recipe.get_recipe("ffmpeg", self.ctx).get_build_dir( arch.arch ) self.setup_extra_args = ["--ffmpeg-dir={}".format(build_dir)] return env recipe = PyAVRecipe() ================================================ FILE: pythonforandroid/recipes/av/patches/compilation_syntax_errors.patch ================================================ diff --git a/av/container/streams.pyx b/av/container/streams.pyx index 17e4992..502ac5a 100644 --- a/av/container/streams.pyx +++ b/av/container/streams.pyx @@ -144,7 +144,7 @@ cdef class StreamContainer: return stream_index - def best(self, str type, /, Stream related = None): + def best(self, str type, Stream related=None): """best(type: Literal["video", "audio", "subtitle", "attachment", "data"], /, related: Stream | None) Finds the "best" stream in the file. Wraps :ffmpeg:`av_find_best_stream`. Example:: diff --git a/av/filter/context.pyx b/av/filter/context.pyx index b820d3d..8908b56 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -77,7 +77,8 @@ cdef class FilterContext: @property def graph(self): - if (graph := self._graph()): + graph = self._graph() + if graph: return graph else: raise RuntimeError("graph is unallocated") ================================================ FILE: pythonforandroid/recipes/av_codecs/__init__.py ================================================ from pythonforandroid.toolchain import Recipe class PyAVCodecsRecipe(Recipe): depends = ["libx264", "libshine", "libvpx"] def build_arch(self, arch): pass recipe = PyAVCodecsRecipe() ================================================ FILE: pythonforandroid/recipes/bcrypt/__init__.py ================================================ from pythonforandroid.recipe import CompiledComponentsPythonRecipe, Recipe class BCryptRecipe(CompiledComponentsPythonRecipe): name = 'bcrypt' version = '3.1.7' url = 'https://github.com/pyca/bcrypt/archive/{version}.tar.gz' depends = ['openssl', 'cffi'] call_hostpython_via_targetpython = False def get_recipe_env(self, arch): env = super().get_recipe_env(arch) openssl_recipe = Recipe.get_recipe('openssl', self.ctx) env['CFLAGS'] += openssl_recipe.include_flags(arch) env['LDFLAGS'] += openssl_recipe.link_dirs_flags(arch) env['LIBS'] = openssl_recipe.link_libs_flags() return env recipe = BCryptRecipe() ================================================ FILE: pythonforandroid/recipes/bitarray/__init__.py ================================================ from pythonforandroid.recipe import CppCompiledComponentsPythonRecipe class BitarrayRecipe(CppCompiledComponentsPythonRecipe): stl_lib_name = "c++_shared" version = "3.0.0" url = "https://github.com/ilanschnell/bitarray/archive/refs/tags/{version}.tar.gz" depends = ["setuptools"] recipe = BitarrayRecipe() ================================================ FILE: pythonforandroid/recipes/boost/__init__.py ================================================ from pythonforandroid.util import current_directory from pythonforandroid.recipe import Recipe from pythonforandroid.logger import shprint from os.path import join, exists from os import environ import shutil import sh """ This recipe bootstraps Boost from source to build Boost.Build including python bindings """ class BoostRecipe(Recipe): # Todo: make recipe compatible with all p4a architectures ''' .. note:: This recipe can be built only against API 21+ and an android ndk >= r19 .. versionchanged:: 0.6.0 Rewrote recipe to support clang's build. The following changes has been made: - Bumped version number to 1.68.0 - Better version handling for url - Added python 3 compatibility - Default compiler for ndk's toolchain set to clang - Python version will be detected via user-config.jam - Changed stl's lib from ``gnustl_shared`` to ``c++_shared`` .. versionchanged:: 2019.08.09.1.dev0 - Bumped version number to 1.68.0 - Adapted to work with ndk-r19+ ''' version = '1.69.0' url = ( 'https://downloads.sourceforge.net/project/boost/' 'boost/{version}/boost_{version_underscore}.tar.bz2' ) depends = ['python3'] patches = [ 'disable-so-version.patch', 'use-android-libs.patch', 'fix-android-issues.patch', ] need_stl_shared = True @property def versioned_url(self): if self.url is None: return None return self.url.format( version=self.version, version_underscore=self.version.replace('.', '_'), ) def should_build(self, arch): return not exists(join(self.get_build_dir(arch.arch), 'b2')) def prebuild_arch(self, arch): super().prebuild_arch(arch) env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): # Set custom configuration shutil.copyfile( join(self.get_recipe_dir(), 'user-config.jam'), join(env['BOOST_BUILD_PATH'], 'user-config.jam'), ) def build_arch(self, 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)): if not exists('b2'): # Compile Boost.Build engine with this custom toolchain bash = sh.Command('bash') shprint(bash, 'bootstrap.sh') # Do not pass env def get_recipe_env(self, arch): # We don't use the normal env because we # are building with a standalone toolchain env = environ.copy() # find user-config.jam env['BOOST_BUILD_PATH'] = self.get_build_dir(arch.arch) # find boost source env['BOOST_ROOT'] = env['BOOST_BUILD_PATH'] env['PYTHON_ROOT'] = self.ctx.python_recipe.link_root(arch.arch) env['PYTHON_INCLUDE'] = self.ctx.python_recipe.include_root(arch.arch) env['PYTHON_MAJOR_MINOR'] = self.ctx.python_recipe.version[:3] env['PYTHON_LINK_VERSION'] = self.ctx.python_recipe.link_version env['ARCH'] = arch.arch.replace('-', '') env['TARGET_TRIPLET'] = arch.target env['CROSSHOST'] = arch.command_prefix env['CROSSHOME'] = self.ctx.ndk.llvm_prebuilt_dir return env recipe = BoostRecipe() ================================================ FILE: pythonforandroid/recipes/boost/disable-so-version.patch ================================================ --- boost/boostcpp.jam 2015-12-14 03:30:09.000000000 +0100 +++ boost-patch/boostcpp.jam 2016-02-08 16:38:40.510859612 +0100 @@ -155,8 +155,9 @@ if $(type) = SHARED_LIB && ! [ $(property-set).get ] in windows cygwin darwin aix && ! [ $(property-set).get ] in pgi { + return $(result) ; # disable version suffix for android result = $(result).$(BOOST_VERSION) ; } return $(result) ; ================================================ FILE: pythonforandroid/recipes/boost/fix-android-issues.patch ================================================ diff -u -r boost_1_69_0.orig/boost/asio/detail/config.hpp boost_1_69_0/boost/asio/detail/config.hpp --- boost_1_69_0.orig/boost/asio/detail/config.hpp 2018-12-05 20:58:15.000000000 +0100 +++ boost_1_69_0/boost/asio/detail/config.hpp 2018-12-13 14:52:06.000000000 +0100 @@ -815,7 +815,11 @@ # if (_LIBCPP_VERSION < 7000) # if (__cplusplus >= 201402) # if __has_include() -# define BOOST_ASIO_HAS_STD_EXPERIMENTAL_STRING_VIEW 1 +# if __clang_major__ >= 7 +# undef BOOST_ASIO_HAS_STD_EXPERIMENTAL_STRING_VIEW +# else +# define BOOST_ASIO_HAS_STD_EXPERIMENTAL_STRING_VIEW 1 +# endif // __clang_major__ >= 7 # endif // __has_include() # endif // (__cplusplus >= 201402) # endif // (_LIBCPP_VERSION < 7000) diff -u -r boost_1_69_0.orig/boost/config/user.hpp boost_1_69_0/boost/config/user.hpp --- boost_1_69_0.orig/boost/config/user.hpp 2018-12-05 20:58:16.000000000 +0100 +++ boost_1_69_0/boost/config/user.hpp 2018-12-13 14:35:29.000000000 +0100 @@ -13,6 +13,12 @@ // configuration policy: // +// Android defines +// There is problem with std::atomic on android (and some other platforms). +// See this link for more info: +// https://code.google.com/p/android/issues/detail?id=42735#makechanges +#define BOOST_ASIO_DISABLE_STD_ATOMIC 1 + // define this to locate a compiler config file: // #define BOOST_COMPILER_CONFIG diff -u -r boost_1_69_0.orig/boost/system/error_code.hpp boost_1_69_0/boost/system/error_code.hpp --- boost_1_69_0.orig/boost/system/error_code.hpp 2018-12-05 20:58:23.000000000 +0100 +++ boost_1_69_0/boost/system/error_code.hpp 2018-12-13 14:53:33.000000000 +0100 @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include diff -u -r boost_1_69_0.orig/libs/filesystem/src/operations.cpp boost_1_69_0/libs/filesystem/src/operations.cpp --- boost_1_69_0.orig/libs/filesystem/src/operations.cpp 2018-12-05 20:58:17.000000000 +0100 +++ boost_1_69_0/libs/filesystem/src/operations.cpp 2018-12-13 14:55:41.000000000 +0100 @@ -232,6 +232,21 @@ # if defined(BOOST_POSIX_API) +# if defined(__ANDROID__) +# define truncate libboost_truncate_wrapper +// truncate() is present in Android libc only starting from ABI 21, so here's a simple wrapper +static int libboost_truncate_wrapper(const char *path, off_t length) +{ + int fd = open(path, O_WRONLY); + if (fd == -1) { + return -1; + } + int status = ftruncate(fd, length); + close(fd); + return status; +} +# endif + typedef int err_t; // POSIX uses a 0 return to indicate success diff -u -r boost_1_69_0.orig/tools/build/src/tools/common.jam boost_1_69_0/tools/build/src/tools/common.jam --- boost_1_69_0.orig/tools/build/src/tools/common.jam 2019-01-25 23:18:34.544755629 +0200 +++ boost_1_69_0/tools/build/src/tools/common.jam 2019-01-25 23:20:42.309047754 +0200 @@ -976,10 +976,10 @@ } # Ditto, from Clang 4 - if $(tag) in clang clangw && [ numbers.less 3 $(version[1]) ] - { - version = $(version[1]) ; - } + #if $(tag) in clang clangw && [ numbers.less 3 $(version[1]) ] + #{ + # version = $(version[1]) ; + #} # On intel, version is not added, because it does not matter and it is the # version of vc used as backend that matters. Ideally, we should encode the ================================================ FILE: pythonforandroid/recipes/boost/use-android-libs.patch ================================================ --- boost/tools/build/src/tools/python.jam 2015-10-16 20:55:36.000000000 +0200 +++ boost-patch/tools/build/src/tools/python.jam 2016-02-09 13:16:09.519261546 +0100 @@ -646,6 +646,7 @@ case aix : return pthread dl ; + case * : return ; # use Android builtin libs case * : return pthread dl gcc:util linux:util ; } ================================================ FILE: pythonforandroid/recipes/boost/user-config.jam ================================================ import os ; local ARCH = [ os.environ ARCH ] ; local TARGET_TRIPLET = [ os.environ TARGET_TRIPLET ] ; local CROSSHOME = [ os.environ CROSSHOME ] ; local PYTHON_HOST = [ os.environ PYTHON_HOST ] ; local PYTHON_ROOT = [ os.environ PYTHON_ROOT ] ; local PYTHON_INCLUDE = [ os.environ PYTHON_INCLUDE ] ; local PYTHON_LINK_VERSION = [ os.environ PYTHON_LINK_VERSION ] ; local PYTHON_MAJOR_MINOR = [ os.environ PYTHON_MAJOR_MINOR ] ; using clang : $(ARCH) : $(CROSSHOME)/bin/$(TARGET_TRIPLET)-clang++ : $(CROSSHOME)/bin/llvm-ar -fPIC -ffunction-sections -fdata-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -Wformat -Werror=format-security -frtti -fexceptions -DNDEBUG -g -Oz -mthumb -Wl,-z,relro -Wl,-z,now -lc++_shared -L$(PYTHON_ROOT) -lpython$(PYTHON_LINK_VERSION) -Wl,-O1 -Wl,-Bsymbolic-functions ; using python : $(PYTHON_MAJOR_MINOR) : $(PYTHON_host) : $(PYTHON_ROOT) $(PYTHON_INCLUDE) : $(PYTHON_ROOT)/libpython$(PYTHON_LINK_VERSION).so : #BOOST_ALL_DYN_LINK ; ================================================ FILE: pythonforandroid/recipes/brokenrecipe/__init__.py ================================================ from pythonforandroid.toolchain import Recipe class BrokenRecipe(Recipe): def __init__(self): print('This is a broken recipe, not a real one!') recipe = BrokenRecipe() ================================================ FILE: pythonforandroid/recipes/cffi/__init__.py ================================================ import os from pythonforandroid.recipe import PyProjectRecipe class CffiRecipe(PyProjectRecipe): """ Extra system dependencies: autoconf, automake and libtool. """ name = 'cffi' version = '2.0.0' url = 'https://github.com/python-cffi/cffi/archive/refs/tags/v{version}.tar.gz' depends = ['pycparser', 'libffi'] patches = ['disable-pkg-config.patch'] def get_hostrecipe_env(self, arch=None): # fixes missing ffi.h on some host systems (e.g. gentoo) 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, **kwargs): env = super().get_recipe_env(arch, **kwargs) libffi = self.get_recipe('libffi', self.ctx) includes = libffi.get_include_dirs(arch) env['CFLAGS'] = ' -I'.join([env.get('CFLAGS', '')] + includes) env['CFLAGS'] += ' -I{}'.format(self.ctx.python_recipe.include_root(arch.arch)) env['LDFLAGS'] = (env.get('CFLAGS', '') + ' -L' + self.ctx.get_libs_dir(arch.arch)) env['LDFLAGS'] += ' -L{}'.format(os.path.join(self.ctx.bootstrap.build_dir, 'libs', arch.arch)) # required for libc and libdl env['LDFLAGS'] += ' -L{}'.format(arch.ndk_lib_dir_versioned) env['LDFLAGS'] += ' -L{}'.format(self.ctx.python_recipe.link_root(arch.arch)) env['LDFLAGS'] += ' -lpython{}'.format(self.ctx.python_recipe.link_version) return env recipe = CffiRecipe() ================================================ FILE: pythonforandroid/recipes/cffi/disable-pkg-config.patch ================================================ diff --git a/setup.py b/setup copy.py index 4ce0007..9be4a6d 100644 --- a/setup.py +++ b/setup @@ -9,8 +9,7 @@ if sys.platform == "win32": sources = ['c/_cffi_backend.c'] libraries = ['ffi'] -include_dirs = ['/usr/include/ffi', - '/usr/include/libffi'] # may be changed by pkg-config +include_dirs = os.environ['FFI_INC'].split(',') if 'FFI_INC' in os.environ else [] define_macros = [('FFI_BUILDING', '1')] # for linking with libffi static library library_dirs = [] extra_compile_args = [] @@ -105,14 +104,7 @@ def uses_msvc(): return config.try_compile('#ifndef _MSC_VER\n#error "not MSVC"\n#endif') def use_pkg_config(): - if sys.platform == 'darwin' and os.path.exists('/usr/local/bin/brew'): - use_homebrew_for_libffi() - - _ask_pkg_config(include_dirs, '--cflags-only-I', '-I', sysroot=True) - _ask_pkg_config(extra_compile_args, '--cflags-only-other') - _ask_pkg_config(library_dirs, '--libs-only-L', '-L', sysroot=True) - _ask_pkg_config(extra_link_args, '--libs-only-other') - _ask_pkg_config(libraries, '--libs-only-l', '-l') + pass ================================================ FILE: pythonforandroid/recipes/coincurve/__init__.py ================================================ import os from pythonforandroid.recipe import PythonRecipe class CoincurveRecipe(PythonRecipe): version = "19.0.1" url = "https://github.com/ofek/coincurve/archive/v{version}.tar.gz" call_hostpython_via_targetpython = False depends = ["setuptools", "libffi", "cffi", "libsecp256k1", "asn1crypto"] patches = ["coincurve.patch"] def get_recipe_env(self, arch=None, with_flags_in_cc=True): env = super(CoincurveRecipe, self).get_recipe_env(arch, with_flags_in_cc) libsecp256k1 = self.get_recipe("libsecp256k1", self.ctx) libsecp256k1_dir = libsecp256k1.get_build_dir(arch.arch) env["CFLAGS"] += " -I" + os.path.join(libsecp256k1_dir, "include") env["LDFLAGS"] += " -lsecp256k1" return env recipe = CoincurveRecipe() ================================================ FILE: pythonforandroid/recipes/coincurve/coincurve.patch ================================================ diff '--color=auto' -uNr coincurve-19.0.1/setup.py coincurve-19.0.1.patch/setup.py --- coincurve-19.0.1/setup.py 2024-03-02 10:40:59.000000000 +0530 +++ coincurve-19.0.1.patch/setup.py 2024-03-10 09:51:58.034737104 +0530 @@ -47,6 +47,7 @@ def download_library(command): + return if command.dry_run: return libdir = absolute('libsecp256k1') @@ -189,6 +190,7 @@ absolute('libsecp256k1/configure'), '--disable-shared', '--enable-static', + '--host=%s' % os.environ['TOOLCHAIN_PREFIX'], '--disable-dependency-tracking', '--with-pic', '--enable-module-extrakeys', @@ -269,13 +271,7 @@ # ABI?: py_limited_api=True, ) - extension.extra_compile_args = [ - subprocess.check_output(['pkg-config', '--cflags-only-I', 'libsecp256k1']).strip().decode('utf-8') # noqa S603 - ] - extension.extra_link_args = [ - subprocess.check_output(['pkg-config', '--libs-only-L', 'libsecp256k1']).strip().decode('utf-8'), # noqa S603 - subprocess.check_output(['pkg-config', '--libs-only-l', 'libsecp256k1']).strip().decode('utf-8'), # noqa S603 - ] + extension.extra_link_args = ["-lsecp256k1"] if os.name == 'nt' or sys.platform == 'win32': # Apparently, the linker on Windows interprets -lxxx as xxx.lib, not libxxx.lib @@ -340,7 +336,7 @@ license='MIT OR Apache-2.0', python_requires='>=3.8', - install_requires=['asn1crypto', 'cffi>=1.3.0'], + install_requires=[], packages=find_packages(exclude=('_cffi_build', '_cffi_build.*', 'libsecp256k1', 'tests')), package_data=package_data, diff '--color=auto' -uNr coincurve-19.0.1/setup_support.py coincurve-19.0.1.patch/setup_support.py --- coincurve-19.0.1/setup_support.py 2024-03-02 10:40:59.000000000 +0530 +++ coincurve-19.0.1.patch/setup_support.py 2024-03-10 08:53:45.650056659 +0530 @@ -56,6 +56,7 @@ def _find_lib(): + return True if 'COINCURVE_IGNORE_SYSTEM_LIB' in os.environ: return False ================================================ FILE: pythonforandroid/recipes/coverage/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class CoverageRecipe(PythonRecipe): version = '4.1' url = 'https://pypi.python.org/packages/2d/10/6136c8e10644c16906edf4d9f7c782c0f2e7ed47ff2f41f067384e432088/coverage-{version}.tar.gz' depends = ['hostpython3', 'setuptools'] patches = ['fallback-utf8.patch'] site_packages_name = 'coverage' call_hostpython_via_targetpython = False recipe = CoverageRecipe() ================================================ FILE: pythonforandroid/recipes/coverage/fallback-utf8.patch ================================================ --- coverage-4.1/coverage/misc.py 2016-02-13 20:04:35.000000000 +0100 +++ patch/coverage/misc.py 2016-07-11 17:07:22.656603295 +0200 @@ -166,7 +166,8 @@ encoding = ( getattr(outfile, "encoding", None) or getattr(sys.__stdout__, "encoding", None) or - locale.getpreferredencoding() + locale.getpreferredencoding() or + 'utf-8' ) return encoding ================================================ FILE: pythonforandroid/recipes/cryptography/__init__.py ================================================ from pythonforandroid.recipe import RustCompiledComponentsRecipe from os.path import join class CryptographyRecipe(RustCompiledComponentsRecipe): name = 'cryptography' version = '46.0.3' url = 'https://github.com/pyca/cryptography/archive/refs/tags/{version}.tar.gz' depends = ['openssl', 'cffi'] def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) openssl_build_dir = self.get_recipe('openssl', self.ctx).get_build_dir(arch.arch) build_target = self.RUST_ARCH_CODES[arch.arch].upper().replace("-", "_") openssl_include = "{}_OPENSSL_INCLUDE_DIR".format(build_target) openssl_libs = "{}_OPENSSL_LIB_DIR".format(build_target) env[openssl_include] = join(openssl_build_dir, 'include') env[openssl_libs] = join(openssl_build_dir) return env recipe = CryptographyRecipe() ================================================ FILE: pythonforandroid/recipes/cymunk/__init__.py ================================================ from pythonforandroid.recipe import CythonRecipe class CymunkRecipe(CythonRecipe): version = 'master' url = 'https://github.com/tito/cymunk/archive/{version}.zip' name = 'cymunk' recipe = CymunkRecipe() ================================================ FILE: pythonforandroid/recipes/cython/__init__.py ================================================ from pythonforandroid.recipe import CompiledComponentsPythonRecipe class CythonRecipe(CompiledComponentsPythonRecipe): version = '0.29.36' url = 'https://github.com/cython/cython/archive/{version}.tar.gz' site_packages_name = 'cython' depends = ['setuptools'] call_hostpython_via_targetpython = False install_in_hostpython = True recipe = CythonRecipe() ================================================ FILE: pythonforandroid/recipes/decorator/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class DecoratorPyRecipe(PythonRecipe): version = '4.2.1' url = 'https://pypi.python.org/packages/source/d/decorator/decorator-{version}.tar.gz' url = 'https://github.com/micheles/decorator/archive/{version}.tar.gz' depends = ['setuptools'] site_packages_name = 'decorator' call_hostpython_via_targetpython = False recipe = DecoratorPyRecipe() ================================================ FILE: pythonforandroid/recipes/enaml/0001-Update-setup.py.patch ================================================ From 156a0426f7350bf49bdfae1aad555e13c9494b9a Mon Sep 17 00:00:00 2001 From: frmdstryr Date: Thu, 23 Jun 2016 22:04:32 -0400 Subject: [PATCH] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3bfd2a2..99817e5 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ setup( url='https://github.com/nucleic/enaml', description='Declarative DSL for building rich user interfaces in Python', long_description=open('README.rst').read(), - requires=['atom', 'PyQt', 'ply', 'kiwisolver'], + requires=['atom', 'ply', 'kiwisolver'], install_requires=['distribute', 'atom >= 0.3.8', 'kiwisolver >= 0.1.2', 'ply >= 3.4'], packages=find_packages(), package_data={ -- 2.7.4 ================================================ FILE: pythonforandroid/recipes/enaml/__init__.py ================================================ from pythonforandroid.recipe import CppCompiledComponentsPythonRecipe class EnamlRecipe(CppCompiledComponentsPythonRecipe): site_packages_name = 'enaml' version = '0.9.8' url = 'https://github.com/nucleic/enaml/archive/{version}.zip' patches = ['0001-Update-setup.py.patch'] # Remove PyQt dependency depends = ['setuptools', 'atom', 'kiwisolver'] recipe = EnamlRecipe() ================================================ FILE: pythonforandroid/recipes/ethash/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class EthashRecipe(PythonRecipe): url = 'https://github.com/ethereum/ethash/archive/master.zip' depends = ['setuptools'] recipe = EthashRecipe() ================================================ FILE: pythonforandroid/recipes/evdev/__init__.py ================================================ from pythonforandroid.recipe import CompiledComponentsPythonRecipe class EvdevRecipe(CompiledComponentsPythonRecipe): name = 'evdev' version = 'v0.4.7' url = 'https://github.com/gvalkov/python-evdev/archive/{version}.zip' call_hostpython_via_targetpython = False depends = [] build_cmd = 'build' patches = ['evcnt.patch', 'keycnt.patch', 'remove-uinput.patch', 'include-dir.patch', 'evdev-permissions.patch'] def get_recipe_env(self, arch=None): env = super().get_recipe_env(arch) env['SYSROOT'] = self.ctx.ndk.sysroot return env recipe = EvdevRecipe() ================================================ FILE: pythonforandroid/recipes/evdev/evcnt.patch ================================================ diff -Naur orig/evdev/input.c v0.4.7/evdev/input.c --- orig/evdev/input.c 2015-06-11 13:56:43.483891914 -0500 +++ v0.4.7/evdev/input.c 2015-06-11 13:57:29.079529095 -0500 @@ -24,6 +24,8 @@ #include #endif +#define EV_CNT (EV_MAX+1) + #define MAX_NAME_SIZE 256 extern char* EV_NAME[EV_CNT]; @@ -190,7 +192,7 @@ absinfo.maximum, absinfo.fuzz, absinfo.flat, - absinfo.resolution); + 0); evlong = PyLong_FromLong(ev_code); absitem = Py_BuildValue("(OO)", evlong, py_absinfo); ================================================ FILE: pythonforandroid/recipes/evdev/evdev-permissions.patch ================================================ diff -Naur orig/evdev/util.py v0.4.7/evdev/util.py --- orig/evdev/util.py 2015-06-12 16:31:46.532994729 -0500 +++ v0.4.7/evdev/util.py 2015-06-12 16:32:59.489933840 -0500 @@ -3,15 +3,53 @@ import os import stat import glob +import subprocess from evdev import ecodes from evdev.events import event_factory +su = False + + +def get_su_binary(): + global su + if su is not False: + return su + + su_files = ['/sbin/su', '/system/bin/su', '/system/xbin/su', '/data/local/xbin/su', + '/data/local/bin/su', '/system/sd/xbin/su', '/system/bin/failsafe/su', + '/data/local/su'] + su = None + + for fn in su_files: + if os.path.exists(fn): + try: + cmd = [fn, '-c', 'id'] + output = subprocess.check_output(cmd) + except Exception: + pass + else: + if 'uid=0' in output: + su = fn + break + + return su + + +def fix_permissions(nodes): + su = get_su_binary() + if su: + cmd = 'chmod 666 ' + ' '.join(nodes) + print cmd + subprocess.check_call(['su', '-c', cmd]) + + def list_devices(input_device_dir='/dev/input'): '''List readable character devices in ``input_device_dir``.''' fns = glob.glob('{}/event*'.format(input_device_dir)) + fix_permissions(fns) fns = list(filter(is_device, fns)) return fns ================================================ FILE: pythonforandroid/recipes/evdev/include-dir.patch ================================================ diff -Naur orig/setup.py v0.4.7/setup.py --- orig/setup.py 2015-06-11 14:16:31.315765908 -0500 +++ v0.4.7/setup.py 2015-06-11 14:17:05.800263536 -0500 @@ -64,7 +64,7 @@ #----------------------------------------------------------------------------- def create_ecodes(): - header = '/usr/include/linux/input.h' + header = os.environ['SYSROOT'] + '/usr/include/linux/input.h' if not os.path.isfile(header): msg = '''\ ================================================ FILE: pythonforandroid/recipes/evdev/keycnt.patch ================================================ diff -Naur orig/evdev/genecodes.py v0.4.7/evdev/genecodes.py --- orig/evdev/genecodes.py 2015-06-12 11:18:39.460538902 -0500 +++ v0.4.7/evdev/genecodes.py 2015-06-12 11:20:49.004337615 -0500 @@ -17,6 +17,8 @@ #include #endif +#define KEY_CNT (KEY_MAX+1) + /* Automatically generated by evdev.genecodes */ /* Generated on %s */ @@ -88,6 +88,7 @@ macro = regex.search(line) if macro: yield ' PyModule_AddIntMacro(m, %s);' % macro.group(1) + yield ' PyModule_AddIntMacro(m, KEY_CNT);' uname = list(os.uname()); del uname[1] uname = ' '.join(uname) ================================================ FILE: pythonforandroid/recipes/evdev/remove-uinput.patch ================================================ diff -Naur orig/evdev/device.py v0.4.7/evdev/device.py --- orig/evdev/device.py 2015-06-11 14:05:00.452884781 -0500 +++ v0.4.7/evdev/device.py 2015-06-11 14:05:47.606553546 -0500 @@ -4,7 +4,7 @@ from select import select from collections import namedtuple -from evdev import _input, _uinput, ecodes, util +from evdev import _input, ecodes, util from evdev.events import InputEvent @@ -203,7 +203,7 @@ .. ''' - _uinput.write(self.fd, ecodes.EV_LED, led_num, value) + pass def __eq__(self, other): '''Two devices are equal if their :data:`info` attributes are equal.''' diff -Naur orig/evdev/__init__.py v0.4.7/evdev/__init__.py --- orig/evdev/__init__.py 2015-06-11 14:05:00.452884781 -0500 +++ v0.4.7/evdev/__init__.py 2015-06-11 14:05:22.973204070 -0500 @@ -6,7 +6,6 @@ from evdev.device import DeviceInfo, InputDevice, AbsInfo from evdev.events import InputEvent, KeyEvent, RelEvent, SynEvent, AbsEvent, event_factory -from evdev.uinput import UInput, UInputError from evdev.util import list_devices, categorize, resolve_ecodes from evdev import ecodes from evdev import ff diff -Naur orig/evdev/uinput.c v0.4.7/evdev/uinput.c --- orig/evdev/uinput.c 2015-06-11 14:05:00.453884795 -0500 +++ v0.4.7/evdev/uinput.c 1969-12-31 18:00:00.000000000 -0600 @@ -1,255 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include -#include - -#ifdef __FreeBSD__ -#include -#include -#else -#include -#include -#endif - -int _uinput_close(int fd) -{ - if (ioctl(fd, UI_DEV_DESTROY) < 0) { - int oerrno = errno; - close(fd); - errno = oerrno; - return -1; - } - - return close(fd); -} - - -static PyObject * -uinput_open(PyObject *self, PyObject *args) -{ - const char* devnode; - - int ret = PyArg_ParseTuple(args, "s", &devnode); - if (!ret) return NULL; - - int fd = open(devnode, O_WRONLY | O_NONBLOCK); - if (fd < 0) { - PyErr_SetString(PyExc_IOError, "could not open uinput device in write mode"); - return NULL; - } - - return Py_BuildValue("i", fd); -} - - -static PyObject * -uinput_create(PyObject *self, PyObject *args) { - int fd, len, i, abscode; - uint16_t vendor, product, version, bustype; - - PyObject *absinfo = NULL, *item = NULL; - - struct uinput_user_dev uidev; - const char* name; - - int ret = PyArg_ParseTuple(args, "ishhhhO", &fd, &name, &vendor, - &product, &version, &bustype, &absinfo); - if (!ret) return NULL; - - memset(&uidev, 0, sizeof(uidev)); - strncpy(uidev.name, name, UINPUT_MAX_NAME_SIZE); - uidev.id.vendor = vendor; - uidev.id.product = product; - uidev.id.version = version; - uidev.id.bustype = bustype; - - len = PyList_Size(absinfo); - for (i=0; i (ABS_X, 0, 255, 0, 0) - item = PyList_GetItem(absinfo, i); - abscode = (int)PyLong_AsLong(PyList_GetItem(item, 0)); - - uidev.absmin[abscode] = PyLong_AsLong(PyList_GetItem(item, 1)); - uidev.absmax[abscode] = PyLong_AsLong(PyList_GetItem(item, 2)); - uidev.absfuzz[abscode] = PyLong_AsLong(PyList_GetItem(item, 3)); - uidev.absflat[abscode] = PyLong_AsLong(PyList_GetItem(item, 4)); - } - - if (write(fd, &uidev, sizeof(uidev)) != sizeof(uidev)) - goto on_err; - - /* if (ioctl(fd, UI_SET_EVBIT, EV_KEY) < 0) */ - /* goto on_err; */ - /* int i; */ - /* for (i=0; i= 3 -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - MODULE_NAME, - MODULE_HELP, - -1, /* m_size */ - MethodTable, /* m_methods */ - NULL, /* m_reload */ - NULL, /* m_traverse */ - NULL, /* m_clear */ - NULL, /* m_free */ -}; - -static PyObject * -moduleinit(void) -{ - PyObject* m = PyModule_Create(&moduledef); - if (m == NULL) return NULL; - - PyModule_AddIntConstant(m, "maxnamelen", UINPUT_MAX_NAME_SIZE); - return m; -} - -PyMODINIT_FUNC -PyInit__uinput(void) -{ - return moduleinit(); -} - -#else -static PyObject * -moduleinit(void) -{ - PyObject* m = Py_InitModule3(MODULE_NAME, MethodTable, MODULE_HELP); - if (m == NULL) return NULL; - - PyModule_AddIntConstant(m, "maxnamelen", UINPUT_MAX_NAME_SIZE); - return m; -} - -PyMODINIT_FUNC -init_uinput(void) -{ - moduleinit(); -} -#endif diff -Naur orig/evdev/uinput.py v0.4.7/evdev/uinput.py --- orig/evdev/uinput.py 2015-06-11 14:05:00.453884795 -0500 +++ v0.4.7/evdev/uinput.py 1969-12-31 18:00:00.000000000 -0600 @@ -1,208 +0,0 @@ -# encoding: utf-8 - -import os -import stat -import time - -from evdev import _uinput -from evdev import ecodes, util, device - - -class UInputError(Exception): - pass - - -class UInput(object): - ''' - A userland input device and that can inject input events into the - linux input subsystem. - ''' - - __slots__ = ( - 'name', 'vendor', 'product', 'version', 'bustype', - 'events', 'devnode', 'fd', 'device', - ) - - def __init__(self, - events=None, - name='py-evdev-uinput', - vendor=0x1, product=0x1, version=0x1, bustype=0x3, - devnode='/dev/uinput'): - ''' - :param events: the event types and codes that the uinput - device will be able to inject - defaults to all - key codes. - - :type events: dictionary of event types mapping to lists of - event codes. - - :param name: the name of the input device. - :param vendor: vendor identifier. - :param product: product identifier. - :param version: version identifier. - :param bustype: bustype identifier. - - .. note:: If you do not specify any events, the uinput device - will be able to inject only ``KEY_*`` and ``BTN_*`` - event codes. - ''' - - self.name = name #: Uinput device name. - self.vendor = vendor #: Device vendor identifier. - self.product = product #: Device product identifier. - self.version = version #: Device version identifier. - self.bustype = bustype #: Device bustype - eg. ``BUS_USB``. - self.devnode = devnode #: Uinput device node - eg. ``/dev/uinput/``. - - if not events: - events = {ecodes.EV_KEY: ecodes.keys.keys()} - - # the min, max, fuzz and flat values for the absolute axis for - # a given code - absinfo = [] - - self._verify() - - #: Write-only, non-blocking file descriptor to the uinput device node. - self.fd = _uinput.open(devnode) - - # set device capabilities - for etype, codes in events.items(): - for code in codes: - # handle max, min, fuzz, flat - if isinstance(code, (tuple, list, device.AbsInfo)): - # flatten (ABS_Y, (0, 255, 0, 0)) to (ABS_Y, 0, 255, 0, 0) - f = [code[0]]; f += code[1] - absinfo.append(f) - code = code[0] - - #:todo: a lot of unnecessary packing/unpacking - _uinput.enable(self.fd, etype, code) - - # create uinput device - _uinput.create(self.fd, name, vendor, product, version, bustype, absinfo) - - #: An :class:`InputDevice ` instance - #: for the fake input device. ``None`` if the device cannot be - #: opened for reading and writing. - self.device = self._find_device() - - def __enter__(self): - return self - - def __exit__(self, type, value, tb): - if hasattr(self, 'fd'): - self.close() - - def __repr__(self): - # :todo: - v = (repr(getattr(self, i)) for i in - ('name', 'bustype', 'vendor', 'product', 'version')) - return '{}({})'.format(self.__class__.__name__, ', '.join(v)) - - def __str__(self): - msg = ('name "{}", bus "{}", vendor "{:04x}", product "{:04x}", version "{:04x}"\n' - 'event types: {}') - - evtypes = [i[0] for i in self.capabilities(True).keys()] - msg = msg.format(self.name, ecodes.BUS[self.bustype], - self.vendor, self.product, - self.version, ' '.join(evtypes)) - - return msg - - def close(self): - # close the associated InputDevice, if it was previously opened - if self.device is not None: - self.device.close() - - # destroy the uinput device - if self.fd > -1: - _uinput.close(self.fd) - self.fd = -1 - - def write_event(self, event): - ''' - Inject an input event into the input subsystem. Events are - queued until a synchronization event is received. - - :param event: InputEvent instance or an object with an - ``event`` attribute (:class:`KeyEvent - `, :class:`RelEvent - ` etc). - - Example:: - - ev = InputEvent(1334414993, 274296, ecodes.EV_KEY, ecodes.KEY_A, 1) - ui.write_event(ev) - ''' - - if hasattr(event, 'event'): - event = event.event - - self.write(event.type, event.code, event.value) - - def write(self, etype, code, value): - ''' - Inject an input event into the input subsystem. Events are - queued until a synchronization event is received. - - :param etype: event type (eg. ``EV_KEY``). - :param code: event code (eg. ``KEY_A``). - :param value: event value (eg. 0 1 2 - depends on event type). - - Example:: - - ui.write(e.EV_KEY, e.KEY_A, 1) # key A - down - ui.write(e.EV_KEY, e.KEY_A, 0) # key A - up - ''' - - _uinput.write(self.fd, etype, code, value) - - def syn(self): - ''' - Inject a ``SYN_REPORT`` event into the input subsystem. Events - queued by :func:`write()` will be fired. If possible, events - will be merged into an 'atomic' event. - ''' - - _uinput.write(self.fd, ecodes.EV_SYN, ecodes.SYN_REPORT, 0) - - def capabilities(self, verbose=False, absinfo=True): - '''See :func:`capabilities `.''' - if self.device is None: - raise UInputError('input device not opened - cannot read capabilites') - - return self.device.capabilities(verbose, absinfo) - - def _verify(self): - ''' - Verify that an uinput device exists and is readable and writable - by the current process. - ''' - - try: - m = os.stat(self.devnode)[stat.ST_MODE] - if not stat.S_ISCHR(m): - raise - except (IndexError, OSError): - msg = '"{}" does not exist or is not a character device file '\ - '- verify that the uinput module is loaded' - raise UInputError(msg.format(self.devnode)) - - if not os.access(self.devnode, os.W_OK): - msg = '"{}" cannot be opened for writing' - raise UInputError(msg.format(self.devnode)) - - if len(self.name) > _uinput.maxnamelen: - msg = 'uinput device name must not be longer than {} characters' - raise UInputError(msg.format(_uinput.maxnamelen)) - - def _find_device(self): - #:bug: the device node might not be immediately available - time.sleep(0.1) - - for fn in util.list_devices('/dev/input/'): - d = device.InputDevice(fn) - if d.name == self.name: - return d diff -Naur orig/setup.py v0.4.7/setup.py --- orig/setup.py 2015-06-11 14:05:00.450884753 -0500 +++ v0.4.7/setup.py 2015-06-11 14:06:13.050914776 -0500 @@ -37,7 +37,6 @@ #----------------------------------------------------------------------------- cflags = ['-std=c99', '-Wno-error=declaration-after-statement'] input_c = Extension('evdev._input', sources=['evdev/input.c'], extra_compile_args=cflags) -uinput_c = Extension('evdev._uinput', sources=['evdev/uinput.c'], extra_compile_args=cflags) ecodes_c = Extension('evdev._ecodes', sources=['evdev/ecodes.c'], extra_compile_args=cflags) #----------------------------------------------------------------------------- @@ -56,7 +55,7 @@ 'classifiers': classifiers, 'packages': ['evdev'], - 'ext_modules': [input_c, uinput_c, ecodes_c], + 'ext_modules': [input_c, ecodes_c], 'include_package_data': False, 'zip_safe': True, 'cmdclass': {}, ================================================ FILE: pythonforandroid/recipes/feedparser/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class FeedparserPyRecipe(PythonRecipe): version = '5.2.1' url = 'https://github.com/kurtmckee/feedparser/archive/{version}.tar.gz' depends = ['setuptools'] site_packages_name = 'feedparser' call_hostpython_via_targetpython = False recipe = FeedparserPyRecipe() ================================================ FILE: pythonforandroid/recipes/ffmpeg/__init__.py ================================================ from pythonforandroid.toolchain import Recipe, current_directory, shprint from os.path import exists, join, realpath import sh from multiprocessing import cpu_count class FFMpegRecipe(Recipe): version = '8.0.1' # Moved to github.com instead of ffmpeg.org to improve download speed url = 'https://www.ffmpeg.org/releases/ffmpeg-{version}.tar.xz' depends = [('sdl2', 'sdl3')] # Need this to build correct recipe order opts_depends = ['openssl', 'ffpyplayer_codecs', 'av_codecs'] patches = ['patches/configure.patch', 'patches/backport-Android15-MediaCodec-fix.patch'] _libs = [ "libavcodec.so", "libavfilter.so", "libavutil.so", "libswscale.so", "libavdevice.so", "libavformat.so", "libswresample.so", "libffmpegbin.so", ] built_libraries = dict.fromkeys(_libs, "./lib") def should_build(self, arch): build_dir = self.get_build_dir(arch.arch) return not exists(join(build_dir, 'lib', 'libavcodec.so')) def get_recipe_env(self, arch): env = super().get_recipe_env(arch) env['NDK'] = self.ctx.ndk_dir return env def build_arch(self, arch): with current_directory(self.get_build_dir(arch.arch)): env = arch.get_env() flags = ['--disable-everything'] cflags = [] ldflags = [] # enable hardware acceleration codecs flags = [ '--enable-jni', '--enable-mediacodec' ] if 'openssl' in self.ctx.recipe_build_order: flags += [ '--enable-version3', '--enable-openssl', '--enable-nonfree', '--enable-protocol=https,tls_openssl', ] build_dir = Recipe.get_recipe( 'openssl', self.ctx).get_build_dir(arch.arch) cflags += ['-I' + build_dir + '/include/'] ldflags += ['-L' + build_dir, '-lssl', '-lcrypto'] codecs_opts = {"ffpyplayer_codecs", "av_codecs"} if codecs_opts.intersection(self.ctx.recipe_build_order): # Enable GPL flags += ['--enable-gpl'] # libx264 flags += ['--enable-libx264'] build_dir = Recipe.get_recipe( 'libx264', self.ctx).get_build_dir(arch.arch) cflags += ['-I' + build_dir + '/include/'] # Newer versions of FFmpeg prioritize the dynamic library and ignore # the static one, unless the static library path is explicitly set. ldflags += [build_dir + '/lib/' + 'libx264.a'] # libshine flags += ['--enable-libshine'] build_dir = Recipe.get_recipe('libshine', self.ctx).get_build_dir(arch.arch) cflags += ['-I' + build_dir + '/include/'] ldflags += ['-lshine', '-L' + build_dir + '/lib/'] ldflags += ['-lm'] # libvpx flags += ['--enable-libvpx'] build_dir = Recipe.get_recipe( 'libvpx', self.ctx).get_build_dir(arch.arch) cflags += ['-I' + build_dir + '/include/'] ldflags += ['-lvpx', '-L' + build_dir + '/lib/'] # Enable all codecs: flags += [ '--enable-parsers', '--enable-decoders', '--enable-encoders', '--enable-muxers', '--enable-demuxers', ] else: # Enable codecs only for .mp4: flags += [ '--enable-parser=aac,ac3,h261,h264,mpegaudio,mpeg4video,mpegvideo,vc1', '--enable-decoder=aac,h264,mpeg4,mpegvideo', '--enable-muxer=h264,mov,mp4,mpeg2video', '--enable-demuxer=aac,h264,m4v,mov,mpegvideo,vc1,rtsp', ] # needed to prevent _ffmpeg.so: version node not found for symbol av_init_packet@LIBAVFORMAT_52 # /usr/bin/ld: failed to set dynamic section sizes: Bad value flags += [ '--disable-symver', ] # disable doc flags += [ '--disable-doc', ] # other flags: flags += [ '--enable-filter=aresample,resample,crop,adelay,volume,scale', '--enable-protocol=file,http,hls,udp,tcp', '--enable-small', '--enable-hwaccels', '--enable-pic', '--disable-static', '--disable-debug', '--enable-shared', ] if 'arm64' in arch.arch: arch_flag = 'aarch64' elif 'x86' in arch.arch: arch_flag = 'x86' flags += ['--disable-asm'] else: arch_flag = 'arm' # android: flags += [ '--target-os=android', '--enable-cross-compile', '--cross-prefix={}-'.format(arch.target), '--arch={}'.format(arch_flag), '--strip={}'.format(self.ctx.ndk.llvm_strip), '--nm={}'.format(self.ctx.ndk.llvm_nm), '--sysroot={}'.format(self.ctx.ndk.sysroot), '--enable-neon', '--prefix={}'.format(realpath('.')), ] if arch_flag == 'arm': cflags += [ '-Wno-error=incompatible-pointer-types', '-mfpu=vfpv3-d16', '-mfloat-abi=softfp', '-fPIC', ] env['CFLAGS'] += ' ' + ' '.join(cflags) env['LDFLAGS'] += ' ' + ' '.join(ldflags) configure = sh.Command('./configure') shprint(configure, *flags, _env=env) shprint(sh.make, '-j', f"{cpu_count()}", _env=env) shprint(sh.make, 'install', _env=env) shprint(sh.cp, "ffmpeg", "./lib/libffmpegbin.so") recipe = FFMpegRecipe() ================================================ FILE: pythonforandroid/recipes/ffmpeg/patches/backport-Android15-MediaCodec-fix.patch ================================================ From 4531cc1b325b50a77fa22981f44a25fbce025a5e Mon Sep 17 00:00:00 2001 From: Dmitrii Okunev Date: Sat, 22 Nov 2025 19:57:19 +0000 Subject: [PATCH] fftools: Fix MediaCodec on Android15+ On Android15+ MediaCodec HAL backend was switched from HIDL to AIDL. As a result, MediaCodec operations started to hang, see: https://trac.ffmpeg.org/ticket/11363 https://github.com/termux/termux-packages/issues/21264 https://issuetracker.google.com/issues/382831999 To fix that it is necessary to initialize binder thread pool. Signed-off-by: Dmitrii Okunev --- compat/android/binder.c | 114 ++++++++++++++++++++++++++++++++++++++++ compat/android/binder.h | 31 +++++++++++ configure | 3 +- fftools/Makefile | 1 + fftools/ffmpeg.c | 7 +++ 5 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 compat/android/binder.c create mode 100644 compat/android/binder.h diff --git a/compat/android/binder.c b/compat/android/binder.c new file mode 100644 index 0000000000..a214d977cc --- /dev/null +++ b/compat/android/binder.c @@ -0,0 +1,114 @@ +/* + * Android Binder handler + * + * Copyright (c) 2025 Dmitrii Okunev + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +#if defined(__ANDROID__) + +#include +#include +#include + +#include "libavutil/log.h" +#include "binder.h" + +#define THREAD_POOL_SIZE 1 + +static void *dlopen_libbinder_ndk(void) +{ + /* + * libbinder_ndk.so often does not contain the functions we need, so making + * this dependency optional, thus using dlopen/dlsym instead of linking. + * + * See also: https://source.android.com/docs/core/architecture/aidl/aidl-backends + */ + + void *h = dlopen("libbinder_ndk.so", RTLD_NOW | RTLD_LOCAL); + if (h != NULL) + return h; + + av_log(NULL, AV_LOG_WARNING, + "android/binder: unable to load libbinder_ndk.so: '%s'; skipping binder threadpool init (MediaCodec likely won't work)\n", + dlerror()); + return NULL; +} + +static void android_binder_threadpool_init(void) +{ + typedef int (*set_thread_pool_max_fn)(uint32_t); + typedef void (*start_thread_pool_fn)(void); + + set_thread_pool_max_fn set_thread_pool_max = NULL; + start_thread_pool_fn start_thread_pool = NULL; + + void *h = dlopen_libbinder_ndk(); + if (h == NULL) + return; + + unsigned thead_pool_size = THREAD_POOL_SIZE; + + set_thread_pool_max = + (set_thread_pool_max_fn) dlsym(h, + "ABinderProcess_setThreadPoolMaxThreadCount"); + start_thread_pool = + (start_thread_pool_fn) dlsym(h, "ABinderProcess_startThreadPool"); + + if (start_thread_pool == NULL) { + av_log(NULL, AV_LOG_WARNING, + "android/binder: ABinderProcess_startThreadPool not found; skipping threadpool init (MediaCodec likely won't work)\n"); + return; + } + + if (set_thread_pool_max != NULL) { + int ok = set_thread_pool_max(thead_pool_size); + av_log(NULL, AV_LOG_DEBUG, + "android/binder: ABinderProcess_setThreadPoolMaxThreadCount(%u) => %s\n", + thead_pool_size, ok ? "ok" : "fail"); + } else { + av_log(NULL, AV_LOG_DEBUG, + "android/binder: ABinderProcess_setThreadPoolMaxThreadCount is unavailable; using the library default\n"); + } + + start_thread_pool(); + av_log(NULL, AV_LOG_DEBUG, + "android/binder: ABinderProcess_startThreadPool() called\n"); +} + +void android_binder_threadpool_init_if_required(void) +{ +#if __ANDROID_API__ >= 24 + if (android_get_device_api_level() < 35) { + // the issue with the thread pool was introduced in Android 15 (API 35) + av_log(NULL, AV_LOG_DEBUG, + "android/binder: API<35, thus no need to initialize a thread pool\n"); + return; + } + android_binder_threadpool_init(); +#else + // android_get_device_api_level was introduced in API 24, so we cannot use it + // to detect the API level in API<24. For simplicity we just assume + // libbinder_ndk.so on the system running this code would have API level < 35; + av_log(NULL, AV_LOG_DEBUG, + "android/binder: is built with API<24, assuming this is not Android 15+\n"); +#endif +} + +#endif /* __ANDROID__ */ diff --git a/compat/android/binder.h b/compat/android/binder.h new file mode 100644 index 0000000000..2b1ca53fe8 --- /dev/null +++ b/compat/android/binder.h @@ -0,0 +1,31 @@ +/* + * Android Binder handler + * + * Copyright (c) 2025 Dmitrii Okunev + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef COMPAT_ANDROID_BINDER_H +#define COMPAT_ANDROID_BINDER_H + +/** + * Initialize Android Binder thread pool. + */ +void android_binder_threadpool_init_if_required(void); + +#endif // COMPAT_ANDROID_BINDER_H diff --git a/configure b/configure index af125bc115..098fe12f39 100755 --- a/configure +++ b/configure @@ -7312,7 +7312,8 @@ enabled mbedtls && { check_pkg_config mbedtls mbedtls mbedtls/x509_crt check_pkg_config mbedtls mbedtls mbedtls/ssl.h mbedtls_ssl_init || check_lib mbedtls mbedtls/ssl.h mbedtls_ssl_init -lmbedtls -lmbedx509 -lmbedcrypto || die "ERROR: mbedTLS not found"; } -enabled mediacodec && { enabled jni || die "ERROR: mediacodec requires --enable-jni"; } +enabled mediacodec && { enabled jni || die "ERROR: mediacodec requires --enable-jni"; } && + add_compat android/binder.o enabled mmal && { check_lib mmal interface/mmal/mmal.h mmal_port_connect -lmmal_core -lmmal_util -lmmal_vc_client -lbcm_host || { ! enabled cross_compile && add_cflags -isystem/opt/vc/include/ -isystem/opt/vc/include/interface/vmcs_host/linux -isystem/opt/vc/include/interface/vcos/pthreads -fgnu89-inline && diff --git a/fftools/Makefile b/fftools/Makefile index bdb44fc5ce..01b16fa8f4 100644 --- a/fftools/Makefile +++ b/fftools/Makefile @@ -51,6 +51,7 @@ OBJS-ffprobe += \ fftools/textformat/tw_buffer.o \ fftools/textformat/tw_stdout.o \ +OBJS-ffmpeg += $(COMPAT_OBJS:%=compat/%) OBJS-ffplay += fftools/ffplay_renderer.o define DOFFTOOL diff --git a/fftools/ffmpeg.c b/fftools/ffmpeg.c index 444d027c15..c2c85d46bd 100644 --- a/fftools/ffmpeg.c +++ b/fftools/ffmpeg.c @@ -78,6 +78,9 @@ #include "libavdevice/avdevice.h" #include "cmdutils.h" +#if CONFIG_MEDIACODEC +#include "compat/android/binder.h" +#endif #include "ffmpeg.h" #include "ffmpeg_sched.h" #include "ffmpeg_utils.h" @@ -1019,6 +1022,10 @@ int main(int argc, char **argv) goto finish; } +#if CONFIG_MEDIACODEC + android_binder_threadpool_init_if_required(); +#endif + current_time = ti = get_benchmark_time_stamps(); ret = transcode(sch); if (ret >= 0 && do_benchmark) { -- 2.49.1 ================================================ FILE: pythonforandroid/recipes/ffmpeg/patches/configure.patch ================================================ --- ffmpeg-8.0/configure 2025-08-22 14:54:23.000000000 +0530 +++ ffmpeg-8.0.mod/configure 2026-01-03 13:40:42.264702438 +0530 @@ -7141,7 +7141,7 @@ enabled librtmp && require_pkg_config librtmp librtmp librtmp/rtmp.h RTMP_Socket enabled librubberband && require_pkg_config librubberband "rubberband >= 1.8.1" rubberband/rubberband-c.h rubberband_new -lstdc++ && append librubberband_extralibs "-lstdc++" enabled libshaderc && require_pkg_config spirv_compiler "shaderc >= 2019.1" shaderc/shaderc.h shaderc_compiler_initialize -enabled libshine && require_pkg_config libshine shine shine/layer3.h shine_encode_buffer +enabled libshine && require "shine" shine/layer3.h shine_encode_buffer -lshine -lm enabled libsmbclient && { check_pkg_config libsmbclient smbclient libsmbclient.h smbc_init || require libsmbclient libsmbclient.h smbc_init -lsmbclient; } enabled libsnappy && require libsnappy snappy-c.h snappy_compress -lsnappy -lstdc++ @@ -7195,7 +7195,7 @@ enabled libwebp && { enabled libwebp_encoder && require_pkg_config libwebp "libwebp >= 0.2.0" webp/encode.h WebPGetEncoderVersion enabled libwebp_anim_encoder && check_pkg_config libwebp_anim_encoder "libwebpmux >= 0.4.0" webp/mux.h WebPAnimEncoderOptionsInit; } -enabled libx264 && require_pkg_config libx264 x264 "stdint.h x264.h" x264_encoder_encode && +enabled libx264 && require "x264" "stdint.h x264.h" x264_encoder_encode && require_cpp_condition libx264 x264.h "X264_BUILD >= 155" && { [ "$toolchain" != "msvc" ] || require_cpp_condition libx264 x264.h "X264_BUILD >= 158"; } && @@ -7258,13 +7258,7 @@ enabled omx && require_headers OMX_Core.h && \ warn "The OpenMAX encoders are deprecated and will be removed in future versions" -enabled openssl && { { check_pkg_config openssl "openssl >= 3.0.0" openssl/ssl.h OPENSSL_init_ssl && - { enabled gplv3 || ! enabled gpl || enabled nonfree || die "ERROR: OpenSSL >=3.0.0 requires --enable-version3"; }; } || - { enabled gpl && ! enabled nonfree && die "ERROR: OpenSSL <3.0.0 is incompatible with the gpl"; } || - check_pkg_config openssl "openssl >= 1.1.0" openssl/ssl.h OPENSSL_init_ssl || - check_lib openssl openssl/ssl.h OPENSSL_init_ssl -lssl -lcrypto || - check_lib openssl openssl/ssl.h OPENSSL_init_ssl -lssl -lcrypto -lws2_32 -lgdi32 || - die "ERROR: openssl (>= 1.1.0) not found"; } +enabled openssl && true enabled pocketsphinx && require_pkg_config pocketsphinx pocketsphinx pocketsphinx/pocketsphinx.h ps_init enabled rkmpp && { require_pkg_config rkmpp rockchip_mpp rockchip/rk_mpi.h mpp_create && require_pkg_config rockchip_mpp "rockchip_mpp >= 1.3.7" rockchip/rk_mpi.h mpp_create && @@ -7273,14 +7267,6 @@ } enabled vapoursynth && require_headers "vapoursynth/VSScript4.h vapoursynth/VapourSynth4.h" -enabled openssl && { - enabled whip_muxer && { - $pkg_config --exists --print-errors "openssl >= 1.0.1k" || - require_pkg_config openssl "openssl >= 1.0.1k" openssl/ssl.h SSL_library_init || - require_pkg_config openssl "openssl >= 1.0.1k" openssl/ssl.h OPENSSL_init_ssl - } -} - if enabled gcrypt; then GCRYPT_CONFIG="${cross_prefix}libgcrypt-config" ================================================ FILE: pythonforandroid/recipes/ffpyplayer/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe, Recipe from os.path import join class FFPyPlayerRecipe(PyProjectRecipe): version = 'v4.5.1' url = 'https://github.com/matham/ffpyplayer/archive/{version}.zip' depends = ['python3', 'sdl2', 'ffmpeg'] patches = ["setup.py.patch"] opt_depends = ['openssl', 'ffpyplayer_codecs'] def get_recipe_env(self, arch, with_flags_in_cc=True): 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") env["FFMPEG_LIB_DIR"] = join(build_dir, "lib") env["SDL_INCLUDE_DIR"] = join(self.ctx.bootstrap.build_dir, 'jni', 'SDL', 'include') env["SDL_LIB_DIR"] = join(self.ctx.bootstrap.build_dir, 'libs', arch.arch) env["USE_SDL2_MIXER"] = '1' # ffpyplayer does not allow to pass more than one include dir for sdl2_mixer (and ATM is # not needed), so we only pass the first one. sdl2_mixer_recipe = self.get_recipe('sdl2_mixer', self.ctx) env["SDL2_MIXER_INCLUDE_DIR"] = sdl2_mixer_recipe.get_include_dirs(arch)[0] # NDKPLATFORM and LIBLINK are our switches for detecting Android platform, so can't be empty # FIXME: We may want to introduce a cleaner approach to this? env['NDKPLATFORM'] = "NOTNONE" env['LIBLINK'] = 'NOTNONE' # ffmpeg recipe enables GPL components only if ffpyplayer_codecs recipe used. # Therefore we need to disable libpostproc if skipped. if 'ffpyplayer_codecs' not in self.ctx.recipe_build_order: env["CONFIG_POSTPROC"] = '0' return env recipe = FFPyPlayerRecipe() ================================================ FILE: pythonforandroid/recipes/ffpyplayer/setup.py.patch ================================================ --- ffpyplayer/setup.py 2024-06-02 11:10:49.691183467 +0530 +++ ffpyplayer.mod/setup.py 2024-06-02 11:20:16.220966873 +0530 @@ -27,12 +27,6 @@ # This sets whether or not Cython gets added to setup_requires. declare_cython = False -if platform in ('ios', 'android'): - # NEVER use or declare cython on these platforms - print('Not using cython on %s' % platform) - can_use_cython = False -else: - declare_cython = True src_path = build_path = dirname(__file__) print(f'Source/build path: {src_path}') ================================================ FILE: pythonforandroid/recipes/ffpyplayer_codecs/__init__.py ================================================ from pythonforandroid.toolchain import Recipe class FFPyPlayerCodecsRecipe(Recipe): depends = ['libx264', 'libshine', 'libvpx'] def build_arch(self, arch): pass recipe = FFPyPlayerCodecsRecipe() ================================================ FILE: pythonforandroid/recipes/flask/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe class FlaskRecipe(PyProjectRecipe): version = '3.1.1' url = 'https://github.com/pallets/flask/archive/{version}.zip' python_depends = ['jinja2', 'werkzeug', 'markupsafe', 'itsdangerous', 'click', 'blinker'] recipe = FlaskRecipe() ================================================ FILE: pythonforandroid/recipes/fontconfig/__init__.py ================================================ from os.path import join from pythonforandroid.recipe import BootstrapNDKRecipe from pythonforandroid.toolchain import current_directory, shprint import sh class FontconfigRecipe(BootstrapNDKRecipe): version = "really_old" url = 'https://github.com/vault/fontconfig/archive/androidbuild.zip' depends = ['sdl2'] dir_name = 'fontconfig' def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_jni_dir()): shprint( sh.Command(join(self.ctx.ndk_dir, "ndk-build")), "V=1", "APP_ALLOW_MISSING_DEPS=true", "fontconfig", _env=env, ) recipe = FontconfigRecipe() ================================================ FILE: pythonforandroid/recipes/fortran/__init__.py ================================================ import os import subprocess import shutil import sh from pathlib import Path from os.path import join from pythonforandroid.recipe import Recipe from pythonforandroid.recommendations import read_ndk_version from pythonforandroid.logger import info, shprint, info_main from pythonforandroid.util import ensure_dir import hashlib FLANG_FILES = { "package-flang-aarch64.tar.bz2": "bf01399513e3b435224d9a9f656b72a0965a23fdd8c3c26af0f7c32f2a5f3403", "package-flang-host.tar.bz2": "3ea2c0e8125ededddf9b3f23c767b8e37816e140ac934c76ace19a168fefdf83", "package-flang-x86_64.tar.bz2": "afe7e391355c71e7b0c8ee71a3002e83e2e524ad61810238815facf3030be6e6", "package-install.tar.bz2": "169b75f6125dc7b95e1d30416147a05d135da6cbe9cc8432d48f5b8633ac38db", } class GFortranRecipe(Recipe): # flang support in NDK by @termux (on github) name = "fortran" toolchain_ver = 0 url = "https://github.com/termux/ndk-toolchain-clang-with-flang/releases/download/" def match_sha256(self, file_path, expected_hash): sha256 = hashlib.sha256() with open(file_path, "rb") as f: for chunk in iter(lambda: f.read(8192), b""): sha256.update(chunk) file_hash = sha256.hexdigest() return file_hash == expected_hash @property def ndk_version(self): ndk_version = read_ndk_version(self.ctx.ndk_dir) minor_to_letter = {0: ""} minor_to_letter.update( {n + 1: chr(i) for n, i in enumerate(range(ord("b"), ord("b") + 25))} ) return f"{ndk_version.major}{minor_to_letter[ndk_version.minor]}" def get_cache_dir(self): dir_name = self.get_dir_name() return join(self.ctx.build_dir, "other_builds", dir_name) def get_fortran_dir(self): toolchain_name = f"android-r{self.ndk_version}-api-{self.ctx.ndk_api}" return join( self.get_cache_dir(), f"{toolchain_name}-flang-v{self.toolchain_ver}" ) def get_incomplete_files(self): incomplete_files = [] cache_dir = self.get_cache_dir() for file, sha256sum in FLANG_FILES.items(): _file = join(cache_dir, file) if not (os.path.exists(_file) and self.match_sha256(_file, sha256sum)): incomplete_files.append(file) return incomplete_files def download_if_necessary(self): assert self.ndk_version == "28c" if len(self.get_incomplete_files()) == 0: return self.download() def download(self): cache_dir = self.get_cache_dir() ensure_dir(cache_dir) for file in self.get_incomplete_files(): _file = join(cache_dir, file) if os.path.exists(_file): os.remove(_file) self.download_file(f"{self.url}r{join(self.ndk_version, file)}", _file) def extract_tar(self, file_path: Path, dest: Path, strip=1): shprint( sh.tar, "xf", str(file_path), "--strip-components", str(strip), "-C", str(dest) if dest else ".", ) def create_flang_wrapper(self, path: Path, target: str): script = f"""#!/usr/bin/env bash if [ "$1" != "-cpp" ] && [ "$1" != "-fc1" ]; then `dirname $0`/flang-new --target={target}{self.ctx.ndk_api} -D__ANDROID_API__={self.ctx.ndk_api} "$@" else `dirname $0`/flang-new "$@" fi """ path.write_text(script) path.chmod(0o755) def unpack(self, arch): info_main("Unpacking fortran") flang_folder = self.get_fortran_dir() if os.path.exists(flang_folder): info("{} is already unpacked, skipping".format(self.name)) return toolchain_path = Path( join(self.ctx.ndk_dir, "toolchains/llvm/prebuilt/linux-x86_64") ) cache_dir = Path(os.path.abspath(self.get_cache_dir())) # clean tmp folder tmp_folder = Path(os.path.abspath(f"{flang_folder}-tmp")) shutil.rmtree(tmp_folder, ignore_errors=True) tmp_folder.mkdir(parents=True) os.chdir(tmp_folder) self.extract_tar(cache_dir / "package-install.tar.bz2", None, strip=4) self.extract_tar(cache_dir / "package-flang-host.tar.bz2", None) sysroot_path = tmp_folder / "sysroot" shutil.copytree(toolchain_path / "sysroot", sysroot_path) self.extract_tar( cache_dir / "package-flang-aarch64.tar.bz2", sysroot_path / "usr/lib/aarch64-linux-android", ) self.extract_tar( cache_dir / "package-flang-x86_64.tar.bz2", sysroot_path / "usr/lib/x86_64-linux-android", ) # Fix lib/clang paths version_output = subprocess.check_output( [str(tmp_folder / "bin/clang"), "--version"], text=True ) clang_version = next( (line for line in version_output.splitlines() if "clang version" in line), "", ) major_ver = clang_version.split("clang version ")[-1].split(".")[0] lib_path = tmp_folder / f"lib/clang/{major_ver}/lib" src_lib_path = toolchain_path / f"lib/clang/{major_ver}/lib" shutil.rmtree(lib_path, ignore_errors=True) lib_path.mkdir(parents=True) for item in src_lib_path.iterdir(): shprint(sh.cp, "-r", str(item), str(lib_path)) # Create flang wrappers targets = [ "aarch64-linux-android", "armv7a-linux-androideabi", "i686-linux-android", "x86_64-linux-android", ] for target in targets: wrapper_path = tmp_folder / f"bin/{target}-flang" self.create_flang_wrapper(wrapper_path, target) shutil.copy( wrapper_path, tmp_folder / f"bin/{target}{self.ctx.ndk_api}-flang" ) tmp_folder.rename(flang_folder) @property def bin_path(self): return f"{self.get_fortran_dir()}/bin" def get_host_platform(self, arch): return { "arm64-v8a": "aarch64-linux-android", "armeabi-v7a": "armv7a-linux-androideabi", "x86_64": "x86_64-linux-android", "x86": "i686-linux-android", }[arch] def get_fortran_bin(self, arch): return join(self.bin_path, f"{self.get_host_platform(arch)}-flang") def get_fortran_flags(self, arch): return f"--target={self.get_host_platform(arch)}{self.ctx.ndk_api} -D__ANDROID_API__={self.ctx.ndk_api}" recipe = GFortranRecipe() ================================================ FILE: pythonforandroid/recipes/freetype/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.logger import shprint, info from pythonforandroid.util import current_directory from os.path import join, exists from multiprocessing import cpu_count import sh class FreetypeRecipe(Recipe): """The freetype library it's special, because has cyclic dependencies with harfbuzz library, so freetype can be build with harfbuzz support, and harfbuzz can be build with freetype support. This complicates the build of both recipes because in order to get the full set we need to compile those recipes several times: - build freetype without harfbuzz - build harfbuzz with freetype - build freetype with harfbuzz support .. note:: To build freetype with harfbuzz support you must add `harfbuzz` to your requirements, otherwise freetype will be build without harfbuzz .. seealso:: https://sourceforge.net/projects/freetype/files/freetype2/2.5.3/ """ version = '2.14.1' url = 'https://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().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['HARFBUZZ_CFLAGS'] = '-I{harfbuzz} -I{harfbuzz}/src'.format( harfbuzz=harfbuzz_build ) env['HARFBUZZ_LIBS'] = ( '-L{freetype}/lib -lfreetype ' '-L{harfbuzz}/src/.libs -lharfbuzz'.format( freetype=freetype_install, harfbuzz=harfbuzz_build ) ) # android's zlib support zlib_lib_path = arch.ndk_lib_dir_versioned zlib_includes = self.ctx.ndk.sysroot_include_dir 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): env = self.get_recipe_env(arch, with_harfbuzz=with_harfbuzz) harfbuzz_in_recipes = 'harfbuzz' in self.ctx.recipe_build_order prefix_path = self.get_build_dir(arch.arch) if harfbuzz_in_recipes and not with_harfbuzz: # This is the first time we build freetype and we modify `prefix`, # because we will install the compiled library so later we can # build harfbuzz (with freetype support) using this freetype # installation prefix_path = join(prefix_path, 'install') # Configure freetype library config_args = { '--host={}'.format(arch.command_prefix), '--prefix={}'.format(prefix_path), '--without-bzip2', '--without-brotli', '--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', '--with-zlib=yes', } ) elif not with_harfbuzz: info('Build freetype for First time (without harfbuzz)') # This time we will build our freetype library as static because we # want that the harfbuzz library to have the necessary freetype # 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', '--with-zlib=no'} ) else: info('Build freetype for Second time (with harfbuzz)') config_args = config_args.union( {'--disable-static', '--enable-shared', '--with-harfbuzz=yes', '--with-zlib=yes', } ) info('Configure args are:\n\t-{}'.format('\n\t-'.join(config_args))) # Build freetype library with current_directory(self.get_build_dir(arch.arch)): configure = sh.Command('./configure') shprint(configure, *config_args, _env=env) shprint(sh.make, '-j', str(cpu_count()), _env=env) if not with_harfbuzz and harfbuzz_in_recipes: info('Installing freetype (first time build without harfbuzz)') # First build, install the compiled lib, and clean build env shprint(sh.make, 'install', _env=env) shprint(sh.make, 'distclean', _env=env) def install_libraries(self, arch): # This library it's special because the first time we built it may not # generate the expected library, because it can depend on harfbuzz, so # we will make sure to only install it when the library exists if not exists(list(self.get_libraries(arch))[0]): return self.install_libs(arch, *self.get_libraries(arch)) recipe = FreetypeRecipe() ================================================ FILE: pythonforandroid/recipes/freetype-py/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe class FreetypePyRecipe(PyProjectRecipe): version = '2.5.1' url = 'https://github.com/rougier/freetype-py/archive/refs/tags/v{version}.tar.gz' patches = ["fix_import.patch"] depends = ['freetype'] site_packages_name = 'freetype' def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) env["SETUPTOOLS_SCM_PRETEND_VERSION_FOR_freetype_py"] = self.version return env recipe = FreetypePyRecipe() ================================================ FILE: pythonforandroid/recipes/freetype-py/fix_import.patch ================================================ diff '--color=auto' -uNr freetype-py-2.5.1/MANIFEST.in freetype-py-2.5.1.mod/MANIFEST.in --- freetype-py-2.5.1/MANIFEST.in 2024-08-29 23:12:30.000000000 +0530 +++ freetype-py-2.5.1.mod/MANIFEST.in 2025-10-26 11:54:45.052025521 +0530 @@ -9,3 +9,4 @@ include LICENSE.txt include README.rst include setup-build-freetype.py +recursive-include _custom_build *.py ================================================ FILE: pythonforandroid/recipes/genericndkbuild/__init__.py ================================================ from os.path import join from pythonforandroid.recipe import BootstrapNDKRecipe from pythonforandroid.toolchain import current_directory, shprint import sh class GenericNDKBuildRecipe(BootstrapNDKRecipe): version = None url = None depends = ['python3'] conflicts = ['sdl2', 'sdl3'] def should_build(self, arch): return True def get_recipe_env(self, arch=None, with_flags_in_cc=True, with_python=True): 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' # required for Qt bootstrap env['PREFERRED_ABI'] = arch.arch return env def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_jni_dir()): shprint(sh.Command(join(self.ctx.ndk_dir, "ndk-build")), "V=1", _env=env) recipe = GenericNDKBuildRecipe() ================================================ FILE: pythonforandroid/recipes/gevent/__init__.py ================================================ """ Note that this recipe doesn't yet build on macOS, the error is: ``` deps/libuv/src/unix/bsd-ifaddrs.c:31:10: fatal error: 'net/if_dl.h' file not found #include ^~~~~~~~~~~~~ 1 error generated. error: command '/Users/runner/.android/android-ndk/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang' failed with exit code 1 ``` """ import re from pythonforandroid.logger import info from pythonforandroid.recipe import PyProjectRecipe class GeventRecipe(PyProjectRecipe): version = '24.11.1' url = 'https://github.com/gevent/gevent/archive/refs/tags/{version}.tar.gz' depends = ['librt', 'setuptools'] patches = ["cross_compiling.patch"] def get_recipe_env(self, arch, **kwargs): """ - Moves all -I -D from CFLAGS to CPPFLAGS environment. - Moves all -l from LDFLAGS to LIBS environment. - Copies all -l from LDLIBS to LIBS environment. - Fixes linker name (use cross compiler) and flags (appends LIBS). - Feds the command prefix for the configure --host flag. """ env = super().get_recipe_env(arch, **kwargs) # CFLAGS may only be used to specify C compiler flags, for macro definitions use CPPFLAGS regex = re.compile(r'(?:\s|^)-[DI][\S]+') env['CPPFLAGS'] = ''.join(re.findall(regex, env['CFLAGS'])).strip() env['CFLAGS'] = re.sub(regex, '', env['CFLAGS']) info('Moved "{}" from CFLAGS to CPPFLAGS.'.format(env['CPPFLAGS'])) # LDFLAGS may only be used to specify linker flags, for libraries use LIBS regex = re.compile(r'(?:\s|^)-l[\w\.]+') env['LIBS'] = ''.join(re.findall(regex, env['LDFLAGS'])).strip() env['LIBS'] += ' {}'.format(''.join(re.findall(regex, env['LDLIBS'])).strip()) env['LDFLAGS'] = re.sub(regex, '', env['LDFLAGS']) info('Moved "{}" from LDFLAGS to LIBS.'.format(env['LIBS'])) # used with the `./configure --host` flag for cross compiling, refs #2805 env['COMMAND_PREFIX'] = arch.command_prefix return env recipe = GeventRecipe() ================================================ FILE: pythonforandroid/recipes/gevent/cross_compiling.patch ================================================ diff --git a/_setupares.py b/_setupares.py index c42fe369..cd8854df 100644 --- a/_setupares.py +++ b/_setupares.py @@ -42,7 +42,7 @@ cflags = ('CFLAGS="%s"' % (cflags,)) if cflags else '' ares_configure_command = ' '.join([ "(cd ", quoted_dep_abspath('c-ares'), " && if [ -r include/ares_build.h ]; then cp include/ares_build.h include/ares_build.h.orig; fi ", - " && sh ./configure --disable-dependency-tracking --disable-tests -C " + cflags, + " && sh ./configure --host={} --disable-dependency-tracking --disable-tests -C ".format(os.environ['COMMAND_PREFIX']) + cflags, " && cp src/lib/ares_config.h include/ares_build.h \"$OLDPWD\" ", " && cat include/ares_build.h ", " && if [ -r include/ares_build.h.orig ]; then mv include/ares_build.h.orig include/ares_build.h; fi)", diff --git a/_setuplibev.py b/_setuplibev.py index f05c2fe9..32f9bd81 100644 --- a/_setuplibev.py +++ b/_setuplibev.py @@ -28,7 +28,7 @@ LIBEV_EMBED = should_embed('libev') # Configure libev in place libev_configure_command = ' '.join([ "(cd ", quoted_dep_abspath('libev'), - " && sh ./configure -C > configure-output.txt", + " && sh ./configure --host={} -C > configure-output.txt".format(os.environ['COMMAND_PREFIX']), ")", ]) ================================================ FILE: pythonforandroid/recipes/greenlet/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe class GreenletRecipe(PyProjectRecipe): version = '3.1.1' url = 'https://pypi.python.org/packages/source/g/greenlet/greenlet-{version}.tar.gz' depends = ['setuptools'] call_hostpython_via_targetpython = False recipe = GreenletRecipe() ================================================ FILE: pythonforandroid/recipes/groestlcoin_hash/__init__.py ================================================ from pythonforandroid.recipe import CythonRecipe class GroestlcoinHashRecipe(CythonRecipe): version = '1.0.3' url = 'https://github.com/Groestlcoin/groestlcoin-hash-python/archive/{version}.tar.gz' depends = ['setuptools'] cythonize = False recipe = GroestlcoinHashRecipe() ================================================ FILE: pythonforandroid/recipes/grpcio/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe, Recipe class GrpcioRecipe(PyProjectRecipe): version = '1.64.0' url = 'https://files.pythonhosted.org/packages/source/g/grpcio/grpcio-{version}.tar.gz' depends = ["setuptools", "librt", "libpthread"] patches = [ "comment-getserverbyport-r-args.patch", "remove-android-log-write.patch", "use-ndk-zlib-and-openssl-recipe-include.patch" ] def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) env["NDKPLATFORM"] = "NOTNONE" env["GRPC_PYTHON_BUILD_SYSTEM_OPENSSL"] = "1" env["GRPC_PYTHON_BUILD_SYSTEM_ZLIB"] = "1" env["ZLIB_INCLUDE"] = self.ctx.ndk.sysroot_include_dir # replace -I with a space openssl_recipe = Recipe.get_recipe('openssl', self.ctx) env["SSL_INCLUDE"] = openssl_recipe.include_flags(arch).strip().replace("-I", "") env["CFLAGS"] += " -U__ANDROID_API__" env["CFLAGS"] += " -D__ANDROID_API__={}".format(self.ctx.ndk_api) # turn off c++11 warning error of "invalid suffix on literal" env["CFLAGS"] += " -Wno-reserved-user-defined-literal" env["PLATFORM"] = "android" env["LDFLAGS"] += " -llog -landroid" env["LDFLAGS"] += openssl_recipe.link_flags(arch) return env recipe = GrpcioRecipe() ================================================ FILE: pythonforandroid/recipes/grpcio/comment-getserverbyport-r-args.patch ================================================ diff --git a/third_party/cares/config_darwin/ares_config.h b/third_party/cares/config_darwin/ares_config.h --- a/third_party/cares/config_darwin/ares_config.h 2024-07-16 20:46:22.000000000 +0100 +++ b/third_party/cares/config_darwin/ares_config.h 2024-07-29 00:18:30.096755745 +0100 @@ -43,7 +43,7 @@ #define GETNAMEINFO_TYPE_ARG7 int /* Specifies the number of arguments to getservbyport_r */ -#define GETSERVBYPORT_R_ARGS +/* #define GETSERVBYPORT_R_ARGS */ /* Define to 1 if you have AF_INET6. */ #define HAVE_AF_INET6 diff --git a/third_party/cares/config_linux/ares_config.h b/third_party/cares/config_linux/ares_config.h --- a/third_party/cares/config_linux/ares_config.h 2024-07-16 20:46:22.000000000 +0100 +++ b/third_party/cares/config_linux/ares_config.h 2024-07-29 00:19:39.479166654 +0100 @@ -43,7 +43,7 @@ #define GETNAMEINFO_TYPE_ARG7 int /* Specifies the number of arguments to getservbyport_r */ -#define GETSERVBYPORT_R_ARGS 6 +/* #define GETSERVBYPORT_R_ARGS 6 */ /* Define to 1 if you have AF_INET6. */ #define HAVE_AF_INET6 @@ -121,7 +121,7 @@ #define HAVE_GETNAMEINFO /* Define to 1 if you have the getservbyport_r function. */ -#define HAVE_GETSERVBYPORT_R +/* #define HAVE_GETSERVBYPORT_R */ /* Define to 1 if you have the `gettimeofday' function. */ #define HAVE_GETTIMEOFDAY ================================================ FILE: pythonforandroid/recipes/grpcio/remove-android-log-write.patch ================================================ Index: log.cc IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/core/lib/gpr/android/log.cc b/src/core/lib/gpr/android/log.cc --- a/src/core/lib/gpr/android/log.cc +++ b/src/core/lib/gpr/android/log.cc (date 1716778822204) @@ -30,18 +30,6 @@ #include "src/core/lib/gprpp/crash.h" -static android_LogPriority severity_to_log_priority(gpr_log_severity severity) { - switch (severity) { - case GPR_LOG_SEVERITY_DEBUG: - return ANDROID_LOG_DEBUG; - case GPR_LOG_SEVERITY_INFO: - return ANDROID_LOG_INFO; - case GPR_LOG_SEVERITY_ERROR: - return ANDROID_LOG_ERROR; - } - return ANDROID_LOG_DEFAULT; -} - void gpr_log(const char* file, int line, gpr_log_severity severity, const char* format, ...) { // Avoid message construction if gpr_log_message won't log @@ -70,8 +58,6 @@ asprintf(&output, "%s:%d] %s", display_file, args->line, args->message); - __android_log_write(severity_to_log_priority(args->severity), "GRPC", output); - // allocated by asprintf => use free, not gpr_free free(output); } ================================================ FILE: pythonforandroid/recipes/grpcio/use-ndk-zlib-and-openssl-recipe-include.patch ================================================ --- a/setup.py 2024-05-31 11:20:56.824695569 +0100 +++ b/setup.py 2024-05-31 23:13:40.324392463 +0100 @@ -299,11 +299,11 @@ lambda x: "third_party/boringssl" not in x, CORE_C_FILES ) CORE_C_FILES = filter(lambda x: "src/boringssl" not in x, CORE_C_FILES) - SSL_INCLUDE = (os.path.join("/usr", "include", "openssl"),) + SSL_INCLUDE = tuple(os.environ["SSL_INCLUDE"].split(" ")) if BUILD_WITH_SYSTEM_ZLIB: CORE_C_FILES = filter(lambda x: "third_party/zlib" not in x, CORE_C_FILES) - ZLIB_INCLUDE = (os.path.join("/usr", "include"),) + ZLIB_INCLUDE = tuple(os.environ["ZLIB_INCLUDE"].split(" ")) if BUILD_WITH_SYSTEM_CARES: CORE_C_FILES = filter(lambda x: "third_party/cares" not in x, CORE_C_FILES) ================================================ FILE: pythonforandroid/recipes/harfbuzz/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.util import current_directory from pythonforandroid.logger import shprint from multiprocessing import cpu_count from os.path import join import sh class HarfbuzzRecipe(Recipe): """The harfbuzz library it's special, because has cyclic dependencies with freetype library, so freetype can be build with harfbuzz support, and harfbuzz can be build with freetype support. This complicates the build of both recipes because in order to get the full set we need to compile those recipes several times: - build freetype without harfbuzz - build harfbuzz with freetype - build freetype with harfbuzz support .. seealso:: https://sourceforge.net/projects/freetype/files/freetype2/2.5.3/ """ 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().get_recipe_env(arch) if 'freetype' in self.ctx.recipe_build_order: freetype = self.get_recipe('freetype', self.ctx) freetype_install = join( freetype.get_build_dir(arch.arch), 'install' ) # Explicitly tell harfbuzz's configure script that we want to # use our freetype library or it won't be correctly detected env['FREETYPE_CFLAGS'] = '-I{}/include/freetype2'.format( freetype_install ) env['FREETYPE_LIBS'] = ' '.join( ['-L{}/lib'.format(freetype_install), '-lfreetype'] ) return env def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): configure = sh.Command('./configure') shprint( configure, '--host={}'.format(arch.command_prefix), '--prefix={}'.format(self.get_build_dir(arch.arch)), '--with-freetype={}'.format( 'yes' if 'freetype' in self.ctx.recipe_build_order else 'no' ), '--with-icu=no', '--with-cairo=no', '--with-fontconfig=no', '--with-glib=no', _env=env, ) shprint(sh.make, '-j', str(cpu_count()), _env=env) if 'freetype' in self.ctx.recipe_build_order: # Rebuild/install freetype with harfbuzz support freetype = self.get_recipe('freetype', self.ctx) freetype.build_arch(arch, with_harfbuzz=True) freetype.install_libraries(arch) recipe = HarfbuzzRecipe() ================================================ FILE: pythonforandroid/recipes/hostpython3/__init__.py ================================================ import sh import os from multiprocessing import cpu_count from pathlib import Path from os.path import join from packaging.version import Version from pythonforandroid.logger import shprint from pythonforandroid.recipe import Recipe from pythonforandroid.util import ( BuildInterruptingException, current_directory, ensure_dir, ) from pythonforandroid.prerequisites import OpenSSLPrerequisite HOSTPYTHON_VERSION_UNSET_MESSAGE = ( 'The hostpython recipe must have set version' ) SETUP_DIST_NOT_FIND_MESSAGE = ( 'Could not find Setup.dist or Setup in Python build' ) 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.14.2' url = 'https://github.com/python/cpython/archive/refs/tags/v{version}.tar.gz' '''The default url to download our host python recipe. This url will change depending on the python version set in attribute :attr:`version`.''' build_subdir = 'native-build' '''Specify the sub build directory for the hostpython3 recipe. Defaults to ``native-build``.''' patches = ["fix_ensurepip.patch"] @property def _exe_name(self): ''' Returns the name of the python executable depending on the version. ''' if not self.version: raise BuildInterruptingException(HOSTPYTHON_VERSION_UNSET_MESSAGE) return f'python{self.version.split(".")[0]}' @property def python_exe(self): '''Returns the full path of the hostpython executable.''' return join(self.get_path_to_python(), self._exe_name) def get_recipe_env(self, arch=None): env = os.environ.copy() openssl_prereq = OpenSSLPrerequisite() if env.get("PKG_CONFIG_PATH", ""): env["PKG_CONFIG_PATH"] = os.pathsep.join( [openssl_prereq.pkg_config_location, env["PKG_CONFIG_PATH"]] ) else: env["PKG_CONFIG_PATH"] = openssl_prereq.pkg_config_location return env def should_build(self, arch): if Path(self.python_exe).exists(): # 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) @property def site_root(self): return join(self.get_path_to_python(), "root") @property def site_bin(self): return join(self.site_root, self.site_dir, "bin") @property def local_bin(self): return join(self.site_root, "usr/local/bin/") @property def site_dir(self): p_version = Version(self.version) return join( self.site_root, f"usr/local/lib/python{p_version.major}.{p_version.minor}/site-packages/" ) @property def _pip(self): return join(self.local_bin, "pip3") @property def pip(self): return sh.Command(self._pip) def fix_pip_shebangs(self): if not os.path.exists(self.local_bin): return for filename in os.listdir(self.local_bin): if not filename.startswith("pip"): continue pip_path = os.path.join(self.local_bin, filename) with open(pip_path, "rb") as file: file_lines = file.read().splitlines() file_lines[0] = f"#!{self.python_exe}".encode() with open(pip_path, "wb") as file: file.write(b"\n".join(file_lines) + b"\n") def build_arch(self, arch): env = self.get_recipe_env(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) # Configure the build build_configured = False with current_directory(build_dir): if not Path('config.status').exists(): shprint(sh.Command(join(recipe_build_dir, 'configure')), _env=env) build_configured = True with current_directory(recipe_build_dir): # 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 Path(setup_dist_location).exists(): 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 Path(setup_location).exists(): raise BuildInterruptingException( SETUP_DIST_NOT_FIND_MESSAGE ) shprint(sh.make, '-j', str(cpu_count()), '-C', build_dir, _env=env) # 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 Path(exe).is_file(): shprint(sh.cp, exe, self.python_exe) break ensure_dir(self.site_root) self.ctx.hostpython = self.python_exe if build_configured: shprint( sh.Command(self.python_exe), "-m", "ensurepip", "--root", self.site_root, "-U", _env={"HOME": "/tmp", "PATH": self.local_bin} ) self.fix_pip_shebangs() recipe = HostPython3Recipe() ================================================ FILE: pythonforandroid/recipes/hostpython3/fix_ensurepip.patch ================================================ diff '--color=auto' -uNr cpython-3.14.0/Lib/ensurepip/__init__.py cpython-3.14.0.mod/Lib/ensurepip/__init__.py --- cpython-3.14.0/Lib/ensurepip/__init__.py 2025-10-07 15:04:52.000000000 +0530 +++ cpython-3.14.0.mod/Lib/ensurepip/__init__.py 2025-12-20 18:18:13.884914683 +0530 @@ -69,7 +69,15 @@ code = f""" import runpy import sys -sys.path = {additional_paths or []} + sys.path + +# tell ensurepip to ignore site-packages + +paths = [] +for path in sys.path: + if "site-packages" not in path: + paths.append(path) + +sys.path = {additional_paths or []} + paths sys.argv[1:] = {args} runpy.run_module("pip", run_name="__main__", alter_sys=True) """ ================================================ FILE: pythonforandroid/recipes/httpx/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe class HttpxRecipe(PyProjectRecipe): name = "httpx" version = "0.28.1" url = ( "https://pypi.python.org/packages/source/h/httpx/httpx-{version}.tar.gz" ) depends = ["httpcore", "h11", "certifi", "idna", "sniffio"] recipe = HttpxRecipe() ================================================ FILE: pythonforandroid/recipes/icu/__init__.py ================================================ import sh import os import platform from os.path import join, isdir, exists from multiprocessing import cpu_count from pythonforandroid.recipe import Recipe from pythonforandroid.toolchain import shprint from pythonforandroid.util import current_directory, ensure_dir class ICURecipe(Recipe): name = 'icu4c' version = '57.1' major_version = version.split('.')[0] url = ( "https://github.com/unicode-org/icu/releases/download/" "release-{version_hyphen}/icu4c-{version_underscore}-src.tgz" ) depends = ['hostpython3'] # installs in python patches = ['disable-libs-version.patch'] built_libraries = { 'libicui18n{}.so'.format(major_version): 'build_icu_android/lib', 'libicuuc{}.so'.format(major_version): 'build_icu_android/lib', 'libicudata{}.so'.format(major_version): 'build_icu_android/lib', 'libicule{}.so'.format(major_version): 'build_icu_android/lib', 'libicuio{}.so'.format(major_version): 'build_icu_android/lib', 'libicutu{}.so'.format(major_version): 'build_icu_android/lib', 'libiculx{}.so'.format(major_version): 'build_icu_android/lib', } @property def versioned_url(self): if self.url is None: return None return self.url.format( version=self.version, version_underscore=self.version.replace('.', '_'), version_hyphen=self.version.replace('.', '-')) def get_recipe_dir(self): """ .. note:: We need to overwrite `Recipe.get_recipe_dir` due to the mismatch name between the recipe's folder (icu) and the value of `ICURecipe.name` (icu4c). """ if self.ctx.local_recipes is not None: local_recipe_dir = join(self.ctx.local_recipes, 'icu') if exists(local_recipe_dir): return local_recipe_dir return join(self.ctx.root_dir, 'recipes', 'icu') def build_arch(self, arch): env = self.get_recipe_env(arch).copy() build_root = self.get_build_dir(arch.arch) def make_build_dest(dest): build_dest = join(build_root, dest) if not isdir(build_dest): ensure_dir(build_dest) return build_dest, False return build_dest, True icu_build = join(build_root, "icu_build") build_host, exists = make_build_dest("build_icu_host") host_env = os.environ.copy() # reduce the function set host_env["CPPFLAGS"] = ( "-O3 -fno-short-wchar -DU_USING_ICU_NAMESPACE=1 -fno-short-enums " "-DU_HAVE_NL_LANGINFO_CODESET=0 -D__STDC_INT64__ -DU_TIMEZONE=0 " "-DUCONFIG_NO_LEGACY_CONVERSION=1 " "-DUCONFIG_NO_TRANSLITERATION=0 ") if not exists: icu4c_host_platform = platform.system() if icu4c_host_platform == "Darwin": icu4c_host_platform = "MacOSX" configure = sh.Command( join(build_root, "source", "runConfigureICU")) with current_directory(build_host): shprint( configure, icu4c_host_platform, "--prefix="+icu_build, "--enable-extras=no", "--enable-strict=no", "--enable-static=no", "--enable-tests=no", "--enable-samples=no", _env=host_env) shprint(sh.make, "-j", str(cpu_count()), _env=host_env) shprint(sh.make, "install", _env=host_env) build_android, exists = make_build_dest("build_icu_android") if not exists: configure = sh.Command(join(build_root, "source", "configure")) with current_directory(build_android): shprint( configure, "--with-cross-build="+build_host, "--enable-extras=no", "--enable-strict=no", "--enable-static=no", "--enable-tests=no", "--enable-samples=no", "--host="+arch.command_prefix, "--prefix="+icu_build, _env=env) shprint(sh.make, "-j", str(cpu_count()), _env=env) shprint(sh.make, "install", _env=env) def install_libraries(self, arch): super().install_libraries(arch) src_include = join( self.get_build_dir(arch.arch), "icu_build", "include") dst_include = join( self.ctx.get_python_install_dir(arch.arch), "include", "icu") ensure_dir(dst_include) shprint(sh.cp, "-r", join(src_include, "layout"), dst_include) shprint(sh.cp, "-r", join(src_include, "unicode"), dst_include) recipe = ICURecipe() ================================================ FILE: pythonforandroid/recipes/icu/disable-libs-version.patch ================================================ diff -aur icu4c-org/source/config/Makefile.inc.in icu4c/source/config/Makefile.inc.in --- icu/source/config/Makefile.inc.in.orig 2016-03-23 21:50:50.000000000 +0100 +++ icu/source/config/Makefile.inc.in 2019-02-15 17:59:28.331749766 +0100 @@ -142,8 +142,8 @@ LDLIBRARYPATH_ENVVAR = LD_LIBRARY_PATH # Versioned target for a shared library -FINAL_SO_TARGET = $(SO_TARGET).$(SO_TARGET_VERSION) -MIDDLE_SO_TARGET = $(SO_TARGET).$(SO_TARGET_VERSION_MAJOR) +FINAL_SO_TARGET = $(SO_TARGET).$(SO_TARGET_VERSION) +MIDDLE_SO_TARGET = $(SO_TARGET) # Access to important ICU tools. # Use as follows: $(INVOKE) $(GENRB) arguments .. diff -aur icu4c-org/source/config/mh-linux icu4c/source/config/mh-linux --- icu4c-org/source/config/mh-linux 2017-07-05 13:23:06.000000000 +0200 +++ icu4c/source/config/mh-linux 2017-07-06 14:02:52.275016858 +0200 @@ -24,9 +24,17 @@ ## Compiler switch to embed a library name # The initial tab in the next line is to prevent icu-config from reading it. - LD_SONAME = -Wl,-soname -Wl,$(notdir $(MIDDLE_SO_TARGET)) + LD_SONAME = -Wl,-soname -Wl,$(notdir $(SO_TARGET)) + DATA_STUBNAME = data$(SO_TARGET_VERSION_MAJOR) + COMMON_STUBNAME = uc$(SO_TARGET_VERSION_MAJOR) + I18N_STUBNAME = i18n$(SO_TARGET_VERSION_MAJOR) + LAYOUT_STUBNAME = le$(SO_TARGET_VERSION_MAJOR) + LAYOUTEX_STUBNAME = lx$(SO_TARGET_VERSION_MAJOR) + IO_STUBNAME = io$(SO_TARGET_VERSION_MAJOR) + TOOLUTIL_STUBNAME = tu$(SO_TARGET_VERSION_MAJOR) + CTESTFW_STUBNAME = test$(SO_TARGET_VERSION_MAJOR) #SH# # We can't depend on MIDDLE_SO_TARGET being set. -#SH# LD_SONAME= +#SH# LD_SONAME=$(SO_TARGET) ## Shared library options LD_SOOPTIONS= -Wl,-Bsymbolic @@ -64,10 +64,10 @@ ## Versioned libraries rules -%.$(SO).$(SO_TARGET_VERSION_MAJOR): %.$(SO).$(SO_TARGET_VERSION) - $(RM) $@ && ln -s ${ use libifaddrs instead +if not hasattr(libc, 'getifaddrs'): + libc = ctypes.CDLL(ctypes.util.find_library('ifaddrs'), use_errno=True) + def get_adapters(): addr0 = addr = ctypes.POINTER(ifaddrs)() ================================================ FILE: pythonforandroid/recipes/ifaddrs/__init__.py ================================================ """ ifaddrs for Android """ from os.path import join import sh from pythonforandroid.logger import shprint from pythonforandroid.recipe import CompiledComponentsPythonRecipe from pythonforandroid.toolchain import current_directory from pythonforandroid.util import ensure_dir class IFAddrRecipe(CompiledComponentsPythonRecipe): version = '8f9a87c' url = 'https://github.com/morristech/android-ifaddrs/archive/{version}.zip' depends = ['hostpython3'] call_hostpython_via_targetpython = False site_packages_name = 'ifaddrs' generated_libraries = ['libifaddrs.so'] def prebuild_arch(self, arch): """Make the build and target directories""" path = self.get_build_dir(arch.arch) ensure_dir(path) def build_arch(self, arch): """simple shared compile""" env = self.get_recipe_env(arch, with_flags_in_cc=False) for path in ( self.get_build_dir(arch.arch), join(self.ctx.python_recipe.get_build_dir(arch.arch), 'Lib'), join(self.ctx.python_recipe.get_build_dir(arch.arch), 'Include')): ensure_dir(path) cli = env['CC'].split()[0] # makes sure first CC command is the compiler rather than ccache, refs: # https://github.com/kivy/python-for-android/issues/1398 if 'ccache' in cli: cli = env['CC'].split()[1] cc = sh.Command(cli) with current_directory(self.get_build_dir(arch.arch)): cflags = env['CFLAGS'].split() cflags.extend(['-I.', '-c', '-l.', 'ifaddrs.c', '-I.']) shprint(cc, *cflags, _env=env) cflags = env['CFLAGS'].split() cflags.extend(['-shared', '-I.', 'ifaddrs.o', '-o', 'libifaddrs.so']) cflags.extend(env['LDFLAGS'].split()) shprint(cc, *cflags, _env=env) shprint(sh.cp, 'libifaddrs.so', self.ctx.get_libs_dir(arch.arch)) recipe = IFAddrRecipe() ================================================ FILE: pythonforandroid/recipes/jedi/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class JediRecipe(PythonRecipe): version = 'v0.9.0' url = 'https://github.com/davidhalter/jedi/archive/{version}.tar.gz' patches = ['fix_MergedNamesDict_get.patch'] # This apparently should be fixed in jedi 0.10 (not released to # pypi yet), but it still occurs on Android, I could not reproduce # on desktop. call_hostpython_via_targetpython = False recipe = JediRecipe() ================================================ FILE: pythonforandroid/recipes/jedi/fix_MergedNamesDict_get.patch ================================================ diff --git a/jedi/parser/fast.py b/jedi/parser/fast.py index 35bb855..bc43359 100644 --- a/jedi/parser/fast.py +++ b/jedi/parser/fast.py @@ -75,7 +75,8 @@ class MergedNamesDict(object): return iter(set(key for dct in self.dicts for key in dct)) def __getitem__(self, value): - return list(chain.from_iterable(dct.get(value, []) for dct in self.dicts)) + return list(chain.from_iterable((dct[value] if value in dct else []) for dct in self.dicts)) + # return list(chain.from_iterable(dct.get(value, []) for dct in self.dicts)) def items(self): dct = {} ================================================ FILE: pythonforandroid/recipes/jpeg/Application.mk ================================================ APP_OPTIM := release APP_ABI := all # or armeabi APP_MODULES := libjpeg APP_ALLOW_MISSING_DEPS := true ================================================ FILE: pythonforandroid/recipes/jpeg/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.logger import shprint from pythonforandroid.util import current_directory from os.path import join import sh class JpegRecipe(Recipe): ''' .. versionchanged:: 0.6.0 rewrote recipe to be build with clang and updated libraries to latest version of the official git repo. ''' name = 'jpeg' version = '2.0.1' url = 'https://github.com/libjpeg-turbo/libjpeg-turbo/archive/{version}.tar.gz' # noqa built_libraries = {'libjpeg.a': '.', 'libturbojpeg.a': '.'} # we will require this below patch to build the shared library # patches = ['remove-version.patch'] def build_arch(self, arch): build_dir = self.get_build_dir(arch.arch) # TODO: Fix simd/neon with current_directory(build_dir): env = self.get_recipe_env(arch) toolchain_file = join(self.ctx.ndk_dir, 'build/cmake/android.toolchain.cmake') shprint(sh.rm, '-rf', 'CMakeCache.txt', 'CMakeFiles/') shprint(sh.cmake, '-G', 'Unix Makefiles', '-DCMAKE_SYSTEM_NAME=Android', '-DCMAKE_POSITION_INDEPENDENT_CODE=1', '-DCMAKE_ANDROID_ARCH_ABI={arch}'.format(arch=arch.arch), '-DCMAKE_ANDROID_NDK=' + self.ctx.ndk_dir, '-DCMAKE_C_COMPILER={cc}'.format(cc=arch.get_clang_exe()), '-DCMAKE_CXX_COMPILER={cc_plus}'.format( cc_plus=arch.get_clang_exe(plus_plus=True)), '-DCMAKE_BUILD_TYPE=Release', '-DCMAKE_INSTALL_PREFIX=./install', '-DCMAKE_TOOLCHAIN_FILE=' + toolchain_file, '-DANDROID_ABI={arch}'.format(arch=arch.arch), '-DANDROID_ARM_NEON=ON', '-DENABLE_NEON=ON', # '-DREQUIRE_SIMD=1', # Force disable shared, with the static ones is enough '-DENABLE_SHARED=0', '-DENABLE_STATIC=1', # Fix cmake compatibility issue '-DCMAKE_POLICY_VERSION_MINIMUM=3.5', _env=env) shprint(sh.make, _env=env) recipe = JpegRecipe() ================================================ FILE: pythonforandroid/recipes/jpeg/build-static.patch ================================================ diff -Naur jpeg/Android.mk b/Android.mk --- jpeg/Android.mk 2015-12-14 11:37:25.900190235 -0600 +++ b/Android.mk 2015-12-14 11:41:27.532182210 -0600 @@ -54,8 +54,7 @@ LOCAL_SRC_FILES:= $(libjpeg_SOURCES_DIST) -LOCAL_SHARED_LIBRARIES := libcutils -LOCAL_STATIC_LIBRARIES := libsimd +LOCAL_STATIC_LIBRARIES := libsimd libcutils LOCAL_C_INCLUDES := $(LOCAL_PATH) @@ -68,7 +67,7 @@ LOCAL_MODULE := libjpeg -include $(BUILD_SHARED_LIBRARY) +include $(BUILD_STATIC_LIBRARY) ###################################################### ### cjpeg ### @@ -82,7 +81,7 @@ LOCAL_SRC_FILES:= $(cjpeg_SOURCES) -LOCAL_SHARED_LIBRARIES := libjpeg +LOCAL_STATIC_LIBRARIES := libjpeg LOCAL_C_INCLUDES := $(LOCAL_PATH) \ $(LOCAL_PATH)/android @@ -110,7 +109,7 @@ LOCAL_SRC_FILES:= $(djpeg_SOURCES) -LOCAL_SHARED_LIBRARIES := libjpeg +LOCAL_STATIC_LIBRARIES := libjpeg LOCAL_C_INCLUDES := $(LOCAL_PATH) \ $(LOCAL_PATH)/android @@ -137,7 +136,7 @@ LOCAL_SRC_FILES:= $(jpegtran_SOURCES) -LOCAL_SHARED_LIBRARIES := libjpeg +LOCAL_STATIC_LIBRARIES := libjpeg LOCAL_C_INCLUDES := $(LOCAL_PATH) \ $(LOCAL_PATH)/android @@ -163,7 +162,7 @@ LOCAL_SRC_FILES:= $(tjunittest_SOURCES) -LOCAL_SHARED_LIBRARIES := libjpeg +LOCAL_STATIC_LIBRARIES := libjpeg LOCAL_C_INCLUDES := $(LOCAL_PATH) @@ -189,7 +188,7 @@ LOCAL_SRC_FILES:= $(tjbench_SOURCES) -LOCAL_SHARED_LIBRARIES := libjpeg +LOCAL_STATIC_LIBRARIES := libjpeg LOCAL_C_INCLUDES := $(LOCAL_PATH) @@ -215,7 +214,7 @@ LOCAL_SRC_FILES:= $(rdjpgcom_SOURCES) -LOCAL_SHARED_LIBRARIES := libjpeg +LOCAL_STATIC_LIBRARIES := libjpeg LOCAL_C_INCLUDES := $(LOCAL_PATH) @@ -240,7 +239,7 @@ LOCAL_SRC_FILES:= $(wrjpgcom_SOURCES) -LOCAL_SHARED_LIBRARIES := libjpeg +LOCAL_STATIC_LIBRARIES := libjpeg LOCAL_C_INCLUDES := $(LOCAL_PATH) ================================================ FILE: pythonforandroid/recipes/jpeg/remove-version.patch ================================================ --- jpeg/CMakeLists.txt.orig 2018-11-12 20:20:28.000000000 +0100 +++ jpeg/CMakeLists.txt 2018-12-14 12:43:45.338704504 +0100 @@ -573,6 +573,9 @@ add_library(turbojpeg SHARED ${TURBOJPEG_SOURCES}) set_property(TARGET turbojpeg PROPERTY COMPILE_FLAGS "-DBMP_SUPPORTED -DPPM_SUPPORTED") + set_property(TARGET jpeg PROPERTY NO_SONAME 1) + set_property(TARGET turbojpeg PROPERTY NO_SONAME 1) + set(CMAKE_SHARED_LIBRARY_SONAME_C_FLAG "") if(WIN32) set_target_properties(turbojpeg PROPERTIES DEFINE_SYMBOL DLLDEFINE) endif() ================================================ FILE: pythonforandroid/recipes/kivy/__init__.py ================================================ from os.path import join import sys import packaging.version import sh from pythonforandroid.recipe import PyProjectRecipe from pythonforandroid.toolchain import current_directory, shprint def get_kivy_version(recipe, arch): with current_directory(join(recipe.get_build_dir(arch.arch), "kivy")): return shprint( sh.Command(sys.executable), "-c", "import _version; print(_version.__version__)", ) def is_kivy_affected_by_deadlock_issue(recipe=None, arch=None): return packaging.version.parse( str(get_kivy_version(recipe, arch)) ) < packaging.version.Version("2.2.0.dev0") def is_kivy_less_than_3(recipe=None, arch=None): return packaging.version.parse( str(get_kivy_version(recipe, arch)) ) < packaging.version.Version("3.0.0") class KivyRecipe(PyProjectRecipe): version = '2.3.1' url = 'https://github.com/kivy/kivy/archive/{version}.zip' name = 'kivy' depends = [('sdl2', 'sdl3'), 'pyjnius', 'setuptools', 'android'] python_depends = ['certifi', 'chardet', 'idna', 'requests', 'urllib3', 'filetype'] hostpython_prerequisites = ["cython>=0.29.1,<=3.0.12"] # sdl-gl-swapwindow-nogil.patch is needed to avoid a deadlock. # See: https://github.com/kivy/kivy/pull/8025 # WARNING: Remove this patch when a new Kivy version is released. patches = [ ("sdl-gl-swapwindow-nogil.patch", is_kivy_affected_by_deadlock_issue), ("use_cython.patch", is_kivy_less_than_3), "no-ast-str.patch" ] @property def need_stl_shared(self): if "sdl3" in self.ctx.recipe_build_order: return True else: return False def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) # Taken from CythonRecipe env['LDFLAGS'] = env['LDFLAGS'] + ' -L{} '.format( self.ctx.get_libs_dir(arch.arch) + ' -L{} '.format(self.ctx.libs_dir) + ' -L{}'.format(join(self.ctx.bootstrap.build_dir, 'obj', 'local', arch.arch))) env['LDSHARED'] = env['CC'] + ' -shared' env['LIBLINK'] = 'NOTNONE' # NDKPLATFORM is our switch for detecting Android platform, so can't be None env['NDKPLATFORM'] = "NOTNONE" if not is_kivy_less_than_3(self, arch): env['KIVY_CROSS_PLATFORM'] = 'android' if 'sdl2' in self.ctx.recipe_build_order: env['USE_SDL2'] = '1' env['KIVY_SPLIT_EXAMPLES'] = '1' sdl2_mixer_recipe = self.get_recipe('sdl2_mixer', self.ctx) sdl2_image_recipe = self.get_recipe('sdl2_image', self.ctx) env['KIVY_SDL2_PATH'] = ':'.join([ join(self.ctx.bootstrap.build_dir, 'jni', 'SDL', 'include'), *sdl2_image_recipe.get_include_dirs(arch), *sdl2_mixer_recipe.get_include_dirs(arch), join(self.ctx.bootstrap.build_dir, 'jni', 'SDL2_ttf'), ]) if "sdl3" in self.ctx.recipe_build_order: sdl3_mixer_recipe = self.get_recipe("sdl3_mixer", self.ctx) sdl3_image_recipe = self.get_recipe("sdl3_image", self.ctx) sdl3_ttf_recipe = self.get_recipe("sdl3_ttf", self.ctx) sdl3_recipe = self.get_recipe("sdl3", self.ctx) env["USE_SDL3"] = "1" env["KIVY_SPLIT_EXAMPLES"] = "1" env["KIVY_SDL3_PATH"] = ":".join( [ *sdl3_mixer_recipe.get_include_dirs(arch), *sdl3_image_recipe.get_include_dirs(arch), *sdl3_ttf_recipe.get_include_dirs(arch), *sdl3_recipe.get_include_dirs(arch), ] ) return env recipe = KivyRecipe() ================================================ FILE: pythonforandroid/recipes/kivy/no-ast-str.patch ================================================ diff -ur kivy-2.3.1b/kivy/lang/parser.py kivy-2.3.1/kivy/lang/parser.py --- kivy-2.3.1b/kivy/lang/parser.py 2025-10-19 13:04:51.542798827 +1300 +++ kivy-2.3.1/kivy/lang/parser.py 2025-10-19 13:05:16.007104601 +1300 @@ -230,11 +230,7 @@ if isinstance(node, (ast.JoinedStr, ast.BoolOp)): for n in node.values: - if isinstance(n, ast.Str): - # NOTE: required for python3.6 - yield from cls.get_names_from_expression(n.s) - else: - yield from cls.get_names_from_expression(n.value) + yield from cls.get_names_from_expression(n.value) if isinstance(node, ast.BinOp): yield from cls.get_names_from_expression(node.right) ================================================ FILE: pythonforandroid/recipes/kivy/sdl-gl-swapwindow-nogil.patch ================================================ diff --git a/kivy/core/window/_window_sdl2.pyx b/kivy/core/window/_window_sdl2.pyx index 46e15ec63..5002cd0f9 100644 --- a/kivy/core/window/_window_sdl2.pyx +++ b/kivy/core/window/_window_sdl2.pyx @@ -746,7 +746,13 @@ cdef class _WindowSDL2Storage: pass def flip(self): - SDL_GL_SwapWindow(self.win) + # On Android (and potentially other platforms), SDL_GL_SwapWindow may + # lock the thread waiting for a mutex from another thread to be + # released. Calling SDL_GL_SwapWindow with the GIL released allow the + # other thread to run (e.g. to process the event filter callback) and + # release the mutex SDL_GL_SwapWindow is waiting for. + with nogil: + SDL_GL_SwapWindow(self.win) def save_bytes_in_png(self, filename, data, int width, int height): cdef SDL_Surface *surface = SDL_CreateRGBSurfaceFrom( diff --git a/kivy/lib/sdl2.pxi b/kivy/lib/sdl2.pxi index 6a539de6d..3a5a69d23 100644 --- a/kivy/lib/sdl2.pxi +++ b/kivy/lib/sdl2.pxi @@ -627,7 +627,7 @@ cdef extern from "SDL.h": cdef SDL_GLContext SDL_GL_GetCurrentContext() cdef int SDL_GL_SetSwapInterval(int interval) cdef int SDL_GL_GetSwapInterval() - cdef void SDL_GL_SwapWindow(SDL_Window * window) + cdef void SDL_GL_SwapWindow(SDL_Window * window) nogil cdef void SDL_GL_DeleteContext(SDL_GLContext context) cdef int SDL_NumJoysticks() ================================================ FILE: pythonforandroid/recipes/kivy/use_cython.patch ================================================ --- kivy-master/setup.py 2025-02-25 03:08:18.000000000 +0530 +++ kivy-master.mod/setup.py 2025-03-01 13:10:24.227808612 +0530 @@ -249,7 +249,7 @@ # This determines whether Cython specific functionality may be used. can_use_cython = True -if platform in ('ios', 'android'): +if platform in ('ios'): # NEVER use or declare cython on these platforms print('Not using cython on %s' % platform) can_use_cython = False ================================================ FILE: pythonforandroid/recipes/kivy3/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe import shutil class Kivy3Recipe(PythonRecipe): version = 'master' url = 'https://github.com/kivy/kivy3/archive/{version}.zip' depends = ['kivy'] site_packages_name = 'kivy3' '''Due to setuptools.''' call_hostpython_via_targetpython = False def build_arch(self, arch): super().build_arch(arch) suffix = '/kivy3/default.glsl' shutil.copyfile(self.get_build_dir(arch.arch) + suffix, self.ctx.get_python_install_dir(arch.arch) + suffix) recipe = Kivy3Recipe() ================================================ FILE: pythonforandroid/recipes/kiwisolver/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe class KiwiSolverRecipe(PyProjectRecipe): site_packages_name = 'kiwisolver' version = '1.4.5' url = 'git+https://github.com/nucleic/kiwi' depends = ['cppy'] need_stl_shared = True def get_recipe_env(self, arch, **kwargs): """Override compile and linker flags, refs: #3115 and #3122""" env = super().get_recipe_env(arch, **kwargs) flags = " -I" + self.ctx.python_recipe.include_root(arch.arch) env["CFLAGS"] += flags env["CPPFLAGS"] += flags env["LDFLAGS"] += " -shared" return env recipe = KiwiSolverRecipe() ================================================ FILE: pythonforandroid/recipes/leveldb/__init__.py ================================================ from pythonforandroid.logger import shprint from pythonforandroid.util import current_directory from pythonforandroid.recipe import Recipe from multiprocessing import cpu_count from os.path import join import sh class LevelDBRecipe(Recipe): version = '1.22' url = 'https://github.com/google/leveldb/archive/{version}.tar.gz' depends = ['snappy'] built_libraries = {'libleveldb.so': '.'} need_stl_shared = True def build_arch(self, arch): env = self.get_recipe_env(arch) source_dir = self.get_build_dir(arch.arch) with current_directory(source_dir): snappy_recipe = self.get_recipe('snappy', self.ctx) snappy_build = snappy_recipe.get_build_dir(arch.arch) shprint(sh.cmake, source_dir, '-DANDROID_ABI={}'.format(arch.arch), '-DANDROID_NATIVE_API_LEVEL={}'.format(self.ctx.ndk_api), '-DANDROID_STL=' + self.stl_lib_name, '-DCMAKE_TOOLCHAIN_FILE={}'.format( join(self.ctx.ndk_dir, 'build', 'cmake', 'android.toolchain.cmake')), '-DCMAKE_BUILD_TYPE=Release', '-DBUILD_SHARED_LIBS=1', '-DHAVE_SNAPPY=1', '-DCMAKE_CXX_FLAGS=-I{path}'.format(path=snappy_build), '-DCMAKE_SHARED_LINKER_FLAGS=-L{path} -lsnappy'.format( path=snappy_build), '-DCMAKE_EXE_LINKER_FLAGS=-L{path} -lsnappy'.format( path=snappy_build), _env=env) shprint(sh.make, '-j' + str(cpu_count()), _env=env) recipe = LevelDBRecipe() ================================================ FILE: pythonforandroid/recipes/libbz2/__init__.py ================================================ import sh from multiprocessing import cpu_count from pythonforandroid.archs import Arch from pythonforandroid.logger import shprint from pythonforandroid.recipe import Recipe from pythonforandroid.util import current_directory class LibBz2Recipe(Recipe): version = "1.0.8" url = "https://sourceware.org/pub/bzip2/bzip2-{version}.tar.gz" built_libraries = {"libbz2.so": ""} patches = ["lib_android.patch"] def build_arch(self, arch: Arch) -> None: env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): shprint( sh.make, "-j", str(cpu_count()), f'CC={env["CC"]}', "-f", "Makefile-libbz2_so", _env=env, ) def get_library_includes(self, arch: Arch) -> str: """ Returns a string with the appropriate `-I` to link with the bz2 lib. This string is usually added to the environment variable `CPPFLAGS`. """ return " -I" + self.get_build_dir(arch.arch) def get_library_ldflags(self, arch: Arch) -> str: """ Returns a string with the appropriate `-L` to link with the bz2 lib. This string is usually added to the environment variable `LDFLAGS`. """ return " -L" + self.get_build_dir(arch.arch) @staticmethod def get_library_libs_flag() -> str: """ Returns a string with the appropriate `-l` flags to link with the bz2 lib. This string is usually added to the environment variable `LIBS`. """ return " -lbz2" recipe = LibBz2Recipe() ================================================ FILE: pythonforandroid/recipes/libbz2/lib_android.patch ================================================ Set default compiler to `clang` and disable versioned shared library --- bzip2-1.0.8/Makefile-libbz2_so.orig 2019-07-13 19:50:05.000000000 +0200 +++ bzip2-1.0.8/Makefile-libbz2_so 2020-03-13 20:10:32.336990786 +0100 @@ -22,7 +22,7 @@ SHELL=/bin/sh -CC=gcc +CC=clang BIGFILES=-D_FILE_OFFSET_BITS=64 CFLAGS=-fpic -fPIC -Wall -Winline -O2 -g $(BIGFILES) @@ -35,13 +35,11 @@ OBJS= blocksort.o \ bzlib.o all: $(OBJS) - $(CC) -shared -Wl,-soname -Wl,libbz2.so.1.0 -o libbz2.so.1.0.8 $(OBJS) - $(CC) $(CFLAGS) -o bzip2-shared bzip2.c libbz2.so.1.0.8 - rm -f libbz2.so.1.0 - ln -s libbz2.so.1.0.8 libbz2.so.1.0 + $(CC) -shared -Wl,-soname=libbz2.so -o libbz2.so $(OBJS) + $(CC) $(CFLAGS) -o bzip2-shared bzip2.c libbz2.so clean: - rm -f $(OBJS) bzip2.o libbz2.so.1.0.8 libbz2.so.1.0 bzip2-shared + rm -f $(OBJS) bzip2.o libbz2.so bzip2-shared blocksort.o: blocksort.c $(CC) $(CFLAGS) -c blocksort.c ================================================ FILE: pythonforandroid/recipes/libcairo/__init__.py ================================================ from pythonforandroid.recipe import Recipe, MesonRecipe from os.path import join, exists from pythonforandroid.util import ensure_dir, current_directory from pythonforandroid.logger import shprint from multiprocessing import cpu_count import sh class LibCairoRecipe(MesonRecipe): name = 'libcairo' version = '1.18.4' url = 'https://gitlab.freedesktop.org/cairo/cairo/-/archive/{version}/cairo-{version}.tar.bz2' skip_python = True depends = ["png", "freetype"] patches = ["meson.patch"] built_libraries = { 'libcairo.so': 'install/lib', 'libpixman-1.so': 'install/lib', 'libcairo-script-interpreter.so': 'install/lib' } def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) cpufeatures = join(self.ctx.ndk.ndk_dir, "sources/android/cpufeatures") lib_dir = join(cpufeatures, "obj", "local", arch.arch) env["CFLAGS"] += f" -I{cpufeatures}" env["LDFLAGS"] += f" -L{lib_dir} -lcpufeatures" return env def should_build(self, arch): return Recipe.should_build(self, arch) def build_arch(self, arch): super().build_arch(arch) build_dir = self.get_build_dir(arch.arch) install_dir = join(build_dir, 'install') ensure_dir(install_dir) env = self.get_recipe_env(arch) lib_dir = self.ctx.get_libs_dir(arch.arch) png_include = self.get_recipe('png', self.ctx).get_build_dir(arch.arch) freetype_inc = join(self.get_recipe('freetype', self.ctx).get_build_dir(arch), "include") with current_directory(build_dir): cpufeatures_dir = join(self.ctx.ndk.ndk_dir, "sources/android/cpufeatures") lib_file = join(cpufeatures_dir, "obj", "local", arch.arch, "libcpufeatures.a") if not exists(lib_file): shprint( sh.Command(join(self.ctx.ndk_dir, "ndk-build")), f"NDK_PROJECT_PATH={cpufeatures_dir}", f"APP_BUILD_SCRIPT={cpufeatures_dir}/Android.mk", f"APP_ABI={arch.arch}", "APP_PLATFORM=latest", _env=env ) shprint(sh.meson, 'setup', 'builddir', '--cross-file', join("/tmp", "android.meson.cross"), f'--prefix={install_dir}', '-Dpng=enabled', '-Dzlib=enabled', '-Dglib=disabled', '-Dgtk_doc=false', '-Dsymbol-lookup=disabled', # deps f'-Dpng_include_dir={png_include}', f'-Dpng_lib_dir={lib_dir}', f'-Dfreetype_include_dir={freetype_inc}', f'-Dfreetype_lib_dir={lib_dir}', _env=env) shprint(sh.ninja, '-C', 'builddir', '-j', str(cpu_count()), _env=env) # macOS fix: sometimes Ninja creates a dummy 'lib' file instead of a directory. # So we remove and recreate the install directory using shell commands, # since os.remove/os.makedirs behave inconsistently in this build env. shprint(sh.rm, '-rf', install_dir) shprint(sh.mkdir, install_dir) shprint(sh.ninja, '-C', 'builddir', 'install', _env=env) recipe = LibCairoRecipe() ================================================ FILE: pythonforandroid/recipes/libcairo/meson.patch ================================================ diff '--color=auto' -uNr cairo-1.18.4/meson.build cairo-1.18.4.mod/meson.build --- cairo-1.18.4/meson.build 2025-03-08 18:53:25.000000000 +0530 +++ cairo-1.18.4.mod/meson.build 2025-07-14 20:42:56.226164648 +0530 @@ -235,11 +235,13 @@ conf.set('HAVE_ZLIB', 1) endif -png_dep = dependency('libpng', - required: get_option('png'), - version: libpng_required_version, - fallback: ['libpng', 'libpng_dep'] +png_inc = include_directories(get_option('png_include_dir')) +png_lib = cc.find_library('png16', dirs: [get_option('png_lib_dir')], required: true) +png_dep = declare_dependency( + include_directories: png_inc, + dependencies: [png_lib] ) + if png_dep.found() feature_conf.set('CAIRO_HAS_SVG_SURFACE', 1) feature_conf.set('CAIRO_HAS_PNG_FUNCTIONS', 1) @@ -265,7 +267,7 @@ # Disable fontconfig by default on platforms where it is optional fontconfig_option = get_option('fontconfig') -fontconfig_required = host_machine.system() not in ['windows', 'darwin'] +fontconfig_required = false fontconfig_option = fontconfig_option.disable_auto_if(not fontconfig_required) fontconfig_dep = dependency('fontconfig', @@ -304,11 +306,14 @@ freetype_required = host_machine.system() not in ['windows', 'darwin'] freetype_option = freetype_option.disable_auto_if(not freetype_required) -freetype_dep = dependency('freetype2', - required: freetype_option, - version: freetype_required_version, - fallback: ['freetype2', 'freetype_dep'], +freetype_inc = include_directories(get_option('freetype_include_dir')) +freetype_lib = cc.find_library('freetype', dirs: [get_option('freetype_lib_dir')], required: true) + +freetype_dep = declare_dependency( + include_directories: freetype_inc, + dependencies: [freetype_lib] ) + if freetype_dep.found() feature_conf.set('CAIRO_HAS_FT_FONT', 1) built_features += [{ diff '--color=auto' -uNr cairo-1.18.4/meson.options cairo-1.18.4.mod/meson.options --- cairo-1.18.4/meson.options 2025-03-08 18:53:25.000000000 +0530 +++ cairo-1.18.4.mod/meson.options 2025-07-14 20:43:00.473191452 +0530 @@ -28,3 +28,11 @@ # Documentation option('gtk_doc', type : 'boolean', value : false, description: 'Build the Cairo API reference (depends on gtk-doc)') + +# Deps + +option('png_include_dir', type: 'string', value: '', description: 'Path to PNG headers') +option('png_lib_dir', type: 'string', value: '', description: 'Path to PNG library') +option('freetype_include_dir', type: 'string', value: '', description: 'Path to FreeType headers') +option('freetype_lib_dir', type: 'string', value: '', description: 'Path to FreeType library') + ================================================ FILE: pythonforandroid/recipes/libcurl/__init__.py ================================================ import sh from pythonforandroid.recipe import Recipe from pythonforandroid.util import current_directory from pythonforandroid.logger import shprint from os.path import join from multiprocessing import cpu_count class LibcurlRecipe(Recipe): version = '7.55.1' url = 'https://curl.haxx.se/download/curl-7.55.1.tar.gz' built_libraries = {'libcurl.so': 'dist/lib'} depends = ['openssl'] def build_arch(self, arch): env = self.get_recipe_env(arch) openssl_recipe = self.get_recipe('openssl', self.ctx) openssl_dir = openssl_recipe.get_build_dir(arch.arch) env['LDFLAGS'] += openssl_recipe.link_dirs_flags(arch) env['LIBS'] = env.get('LIBS', '') + openssl_recipe.link_libs_flags() with current_directory(self.get_build_dir(arch.arch)): dst_dir = join(self.get_build_dir(arch.arch), 'dist') shprint( sh.Command('./configure'), '--host={}'.format(arch.command_prefix), '--enable-shared', '--with-ssl={}'.format(openssl_dir), '--prefix={}'.format(dst_dir), _env=env) shprint(sh.make, '-j', str(cpu_count()), _env=env) shprint(sh.make, 'install', _env=env) recipe = LibcurlRecipe() ================================================ FILE: pythonforandroid/recipes/libexpat/__init__.py ================================================ import sh from pythonforandroid.recipe import Recipe from pythonforandroid.util import current_directory from pythonforandroid.logger import shprint from os.path import join from multiprocessing import cpu_count class LibexpatRecipe(Recipe): version = 'master' url = 'https://github.com/libexpat/libexpat/archive/{version}.zip' built_libraries = {'libexpat.so': 'dist/lib'} depends = [] def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(join(self.get_build_dir(arch.arch), 'expat')): dst_dir = join(self.get_build_dir(arch.arch), 'dist') shprint(sh.Command('./buildconf.sh'), _env=env) shprint( sh.Command('./configure'), '--host={}'.format(arch.command_prefix), '--enable-shared', '--without-xmlwf', '--prefix={}'.format(dst_dir), _env=env) shprint(sh.make, '-j', str(cpu_count()), _env=env) shprint(sh.make, 'install', _env=env) recipe = LibexpatRecipe() ================================================ FILE: pythonforandroid/recipes/libffi/Application.mk ================================================ APP_OPTIM := release APP_ABI := all # or armeabi APP_MODULES := libffi ================================================ FILE: pythonforandroid/recipes/libffi/__init__.py ================================================ from os.path import exists, join from multiprocessing import cpu_count from pythonforandroid.recipe import Recipe from pythonforandroid.logger import shprint from pythonforandroid.util import current_directory import sh class LibffiRecipe(Recipe): """ Requires additional system dependencies on Ubuntu: - `automake` for the `aclocal` binary - `autoconf` for the `autoreconf` binary - `libltdl-dev` which defines the `LT_SYS_SYMBOL_USCORE` macro """ name = 'libffi' version = 'v3.4.2' url = 'https://github.com/libffi/libffi/archive/{version}.tar.gz' patches = ['remove-version-info.patch'] built_libraries = {'libffi.so': '.libs'} def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): if not exists('configure'): shprint(sh.Command('./autogen.sh'), _env=env) shprint(sh.Command('autoreconf'), '-vif', _env=env) shprint(sh.Command('./configure'), '--host=' + arch.command_prefix, '--prefix=' + self.get_build_dir(arch.arch), '--disable-builddir', '--enable-shared', _env=env) shprint(sh.make, '-j', str(cpu_count()), 'libffi.la', _env=env) def get_include_dirs(self, arch): return [join(self.get_build_dir(arch), 'include')] recipe = LibffiRecipe() ================================================ FILE: pythonforandroid/recipes/libffi/disable-mips-check.patch ================================================ diff -Naur libffi/Android.mk b/Android.mk --- libffi/Android.mk 2015-12-22 17:00:48.025478556 -0600 +++ b/Android.mk 2015-12-22 17:02:23.999249390 -0600 @@ -23,23 +23,20 @@ # Build rules for the target. # -# We only build ffi for mips. -ifeq ($(TARGET_ARCH),mips) - include $(CLEAR_VARS) +include $(CLEAR_VARS) - ffi_arch := $(TARGET_ARCH) - ffi_os := $(TARGET_OS) +ffi_arch := $(TARGET_ARCH) +ffi_os := $(TARGET_OS) - # This include just keeps the nesting a bit saner. - include $(LOCAL_PATH)/Libffi.mk +# This include just keeps the nesting a bit saner. +include $(LOCAL_PATH)/Libffi.mk - LOCAL_MODULE_TAGS := optional - LOCAL_MODULE := libffi +LOCAL_MODULE_TAGS := optional +LOCAL_MODULE := libffi - include $(BUILD_SHARED_LIBRARY) +include $(BUILD_SHARED_LIBRARY) -endif # Also include the rules for the test suite. include external/libffi/testsuite/Android.mk ================================================ FILE: pythonforandroid/recipes/libffi/remove-version-info.patch ================================================ --- libffi/Makefile.am.orig 2018-12-21 16:11:26.159181262 +0100 +++ libffi/Makefile.am 2018-12-21 16:14:44.075179374 +0100 @@ -156,7 +156,7 @@ libffi.map: $(top_srcdir)/libffi.map.in $(COMPILE) -D$(TARGET) -E -x assembler-with-cpp -o $@ $< -libffi_la_LDFLAGS = -no-undefined $(libffi_version_info) $(libffi_version_script) $(LTLDFLAGS) $(AM_LTLDFLAGS) +libffi_la_LDFLAGS = -no-undefined -avoid-version $(LTLDFLAGS) $(AM_LTLDFLAGS) libffi_la_DEPENDENCIES = $(libffi_la_LIBADD) $(libffi_version_dep) AM_CPPFLAGS = -I. -I$(top_srcdir)/include -Iinclude -I$(top_srcdir)/src ================================================ FILE: pythonforandroid/recipes/libgeos/__init__.py ================================================ from pythonforandroid.util import current_directory, ensure_dir from pythonforandroid.toolchain import shprint from pythonforandroid.recipe import Recipe from multiprocessing import cpu_count from os.path import join import sh class LibgeosRecipe(Recipe): version = '3.7.1' url = 'https://github.com/libgeos/libgeos/archive/{version}.zip' depends = [] built_libraries = { 'libgeos.so': 'install_target/lib', 'libgeos_c.so': 'install_target/lib' } need_stl_shared = True def build_arch(self, arch): source_dir = self.get_build_dir(arch.arch) build_target = join(source_dir, 'build_target') install_target = join(source_dir, 'install_target') ensure_dir(build_target) with current_directory(build_target): env = self.get_recipe_env(arch) shprint(sh.cmake, source_dir, '-DANDROID_ABI={}'.format(arch.arch), '-DANDROID_NATIVE_API_LEVEL={}'.format(self.ctx.ndk_api), '-DANDROID_STL=' + self.stl_lib_name, '-DCMAKE_TOOLCHAIN_FILE={}'.format( join(self.ctx.ndk_dir, 'build', 'cmake', 'android.toolchain.cmake')), '-DCMAKE_INSTALL_PREFIX={}'.format(install_target), '-DCMAKE_BUILD_TYPE=Release', '-DGEOS_ENABLE_TESTS=OFF', '-DBUILD_SHARED_LIBS=1', _env=env) shprint(sh.make, '-j' + str(cpu_count()), _env=env) # We make the install because this way we will have all the # includes in one place (mostly we are interested in `geos_c.h`, # which is not in the include folder, so this way we make easier to # link with this library...case of shapely's recipe) shprint(sh.make, 'install', _env=env) recipe = LibgeosRecipe() ================================================ FILE: pythonforandroid/recipes/libglob/__init__.py ================================================ """ android libglob available via '-lglob' LDFLAG """ from os.path import join import sh from pythonforandroid.logger import shprint from pythonforandroid.recipe import Recipe from pythonforandroid.toolchain import current_directory from pythonforandroid.util import ensure_dir class LibGlobRecipe(Recipe): """Make a glob.h and glob.so for the python_install_dir()""" version = '0.0.1' url = None # # glob.h and glob.c extracted from # https://github.com/white-gecko/TokyoCabinet, e.g.: # https://raw.githubusercontent.com/white-gecko/TokyoCabinet/master/glob.h # https://raw.githubusercontent.com/white-gecko/TokyoCabinet/master/glob.c # and pushed in via patch name = 'libglob' built_libraries = {'libglob.so': '.'} depends = ['hostpython3'] patches = ['glob.patch'] def should_build(self, arch): """It's faster to build than check""" return True def prebuild_arch(self, arch): """Make the build and target directories""" path = self.get_build_dir(arch.arch) ensure_dir(path) def build_arch(self, arch): """simple shared compile""" env = self.get_recipe_env(arch, with_flags_in_cc=False) for path in ( self.get_build_dir(arch.arch), join(self.ctx.python_recipe.get_build_dir(arch.arch), 'Lib'), join(self.ctx.python_recipe.get_build_dir(arch.arch), 'Include')): ensure_dir(path) cli = env['CC'].split()[0] # makes sure first CC command is the compiler rather than ccache, refs: # https://github.com/kivy/python-for-android/issues/1399 if 'ccache' in cli: cli = env['CC'].split()[1] cc = sh.Command(cli) with current_directory(self.get_build_dir(arch.arch)): cflags = env['CFLAGS'].split() cflags.extend(['-I.', '-c', '-l.', 'glob.c', '-I.']) shprint(cc, *cflags, _env=env) cflags = env['CFLAGS'].split() cflags.extend(['-shared', '-I.', 'glob.o', '-o', 'libglob.so']) cflags.extend(env['LDFLAGS'].split()) shprint(cc, *cflags, _env=env) recipe = LibGlobRecipe() ================================================ FILE: pythonforandroid/recipes/libglob/glob.patch ================================================ diff -Nur /tmp/x/glob.c libglob/glob.c --- /tmp/x/glob.c 1969-12-31 19:00:00.000000000 -0500 +++ libglob/glob.c 2017-08-19 15:23:19.910414868 -0400 @@ -0,0 +1,906 @@ +/* + * Natanael Arndt, 2011: removed collate.h dependencies + * (my changes are trivial) + * + * Copyright (c) 1989, 1993 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Guido van Rossum. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 4. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#if defined(LIBC_SCCS) && !defined(lint) +static char sccsid[] = "@(#)glob.c 8.3 (Berkeley) 10/13/93"; +#endif /* LIBC_SCCS and not lint */ +#include +__FBSDID("$FreeBSD$"); + +/* + * glob(3) -- a superset of the one defined in POSIX 1003.2. + * + * The [!...] convention to negate a range is supported (SysV, Posix, ksh). + * + * Optional extra services, controlled by flags not defined by POSIX: + * + * GLOB_QUOTE: + * Escaping convention: \ inhibits any special meaning the following + * character might have (except \ at end of string is retained). + * GLOB_MAGCHAR: + * Set in gl_flags if pattern contained a globbing character. + * GLOB_NOMAGIC: + * Same as GLOB_NOCHECK, but it will only append pattern if it did + * not contain any magic characters. [Used in csh style globbing] + * GLOB_ALTDIRFUNC: + * Use alternately specified directory access functions. + * GLOB_TILDE: + * expand ~user/foo to the /home/dir/of/user/foo + * GLOB_BRACE: + * expand {1,2}{a,b} to 1a 1b 2a 2b + * gl_matchc: + * Number of matches in the current invocation of glob. + */ + +/* + * Some notes on multibyte character support: + * 1. Patterns with illegal byte sequences match nothing - even if + * GLOB_NOCHECK is specified. + * 2. Illegal byte sequences in filenames are handled by treating them as + * single-byte characters with a value of the first byte of the sequence + * cast to wchar_t. + * 3. State-dependent encodings are not currently supported. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DOLLAR '$' +#define DOT '.' +#define EOS '\0' +#define LBRACKET '[' +#define NOT '!' +#define QUESTION '?' +#define QUOTE '\\' +#define RANGE '-' +#define RBRACKET ']' +#define SEP '/' +#define STAR '*' +#define TILDE '~' +#define UNDERSCORE '_' +#define LBRACE '{' +#define RBRACE '}' +#define SLASH '/' +#define COMMA ',' + +#ifndef DEBUG + +#define M_QUOTE 0x8000000000ULL +#define M_PROTECT 0x4000000000ULL +#define M_MASK 0xffffffffffULL +#define M_CHAR 0x00ffffffffULL + +typedef uint_fast64_t Char; + +#else + +#define M_QUOTE 0x80 +#define M_PROTECT 0x40 +#define M_MASK 0xff +#define M_CHAR 0x7f + +typedef char Char; + +#endif + + +#define CHAR(c) ((Char)((c)&M_CHAR)) +#define META(c) ((Char)((c)|M_QUOTE)) +#define M_ALL META('*') +#define M_END META(']') +#define M_NOT META('!') +#define M_ONE META('?') +#define M_RNG META('-') +#define M_SET META('[') +#define ismeta(c) (((c)&M_QUOTE) != 0) + + +static int compare(const void *, const void *); +static int g_Ctoc(const Char *, char *, size_t); +static int g_lstat(Char *, struct stat *, glob_t *); +static DIR *g_opendir(Char *, glob_t *); +static const Char *g_strchr(const Char *, wchar_t); +#ifdef notdef +static Char *g_strcat(Char *, const Char *); +#endif +static int g_stat(Char *, struct stat *, glob_t *); +static int glob0(const Char *, glob_t *, size_t *); +static int glob1(Char *, glob_t *, size_t *); +static int glob2(Char *, Char *, Char *, Char *, glob_t *, size_t *); +static int glob3(Char *, Char *, Char *, Char *, Char *, glob_t *, size_t *); +static int globextend(const Char *, glob_t *, size_t *); +static const Char * + globtilde(const Char *, Char *, size_t, glob_t *); +static int globexp1(const Char *, glob_t *, size_t *); +static int globexp2(const Char *, const Char *, glob_t *, int *, size_t *); +static int match(Char *, Char *, Char *); +#ifdef DEBUG +static void qprintf(const char *, Char *); +#endif + +int +glob(const char *pattern, int flags, int (*errfunc)(const char *, int), glob_t *pglob) +{ + const char *patnext; + size_t limit; + Char *bufnext, *bufend, patbuf[MAXPATHLEN], prot; + mbstate_t mbs; + wchar_t wc; + size_t clen; + + patnext = pattern; + if (!(flags & GLOB_APPEND)) { + pglob->gl_pathc = 0; + pglob->gl_pathv = NULL; + if (!(flags & GLOB_DOOFFS)) + pglob->gl_offs = 0; + } + if (flags & GLOB_LIMIT) { + limit = pglob->gl_matchc; + if (limit == 0) + limit = ARG_MAX; + } else + limit = 0; + pglob->gl_flags = flags & ~GLOB_MAGCHAR; + pglob->gl_errfunc = errfunc; + pglob->gl_matchc = 0; + + bufnext = patbuf; + bufend = bufnext + MAXPATHLEN - 1; + if (flags & GLOB_NOESCAPE) { + memset(&mbs, 0, sizeof(mbs)); + while (bufend - bufnext >= MB_CUR_MAX) { + clen = mbrtowc(&wc, patnext, MB_LEN_MAX, &mbs); + if (clen == (size_t)-1 || clen == (size_t)-2) + return (GLOB_NOMATCH); + else if (clen == 0) + break; + *bufnext++ = wc; + patnext += clen; + } + } else { + /* Protect the quoted characters. */ + memset(&mbs, 0, sizeof(mbs)); + while (bufend - bufnext >= MB_CUR_MAX) { + if (*patnext == QUOTE) { + if (*++patnext == EOS) { + *bufnext++ = QUOTE | M_PROTECT; + continue; + } + prot = M_PROTECT; + } else + prot = 0; + clen = mbrtowc(&wc, patnext, MB_LEN_MAX, &mbs); + if (clen == (size_t)-1 || clen == (size_t)-2) + return (GLOB_NOMATCH); + else if (clen == 0) + break; + *bufnext++ = wc | prot; + patnext += clen; + } + } + *bufnext = EOS; + + if (flags & GLOB_BRACE) + return globexp1(patbuf, pglob, &limit); + else + return glob0(patbuf, pglob, &limit); +} + +/* + * Expand recursively a glob {} pattern. When there is no more expansion + * invoke the standard globbing routine to glob the rest of the magic + * characters + */ +static int +globexp1(const Char *pattern, glob_t *pglob, size_t *limit) +{ + const Char* ptr = pattern; + int rv; + + /* Protect a single {}, for find(1), like csh */ + if (pattern[0] == LBRACE && pattern[1] == RBRACE && pattern[2] == EOS) + return glob0(pattern, pglob, limit); + + while ((ptr = g_strchr(ptr, LBRACE)) != NULL) + if (!globexp2(ptr, pattern, pglob, &rv, limit)) + return rv; + + return glob0(pattern, pglob, limit); +} + + +/* + * Recursive brace globbing helper. Tries to expand a single brace. + * If it succeeds then it invokes globexp1 with the new pattern. + * If it fails then it tries to glob the rest of the pattern and returns. + */ +static int +globexp2(const Char *ptr, const Char *pattern, glob_t *pglob, int *rv, size_t *limit) +{ + int i; + Char *lm, *ls; + const Char *pe, *pm, *pm1, *pl; + Char patbuf[MAXPATHLEN]; + + /* copy part up to the brace */ + for (lm = patbuf, pm = pattern; pm != ptr; *lm++ = *pm++) + continue; + *lm = EOS; + ls = lm; + + /* Find the balanced brace */ + for (i = 0, pe = ++ptr; *pe; pe++) + if (*pe == LBRACKET) { + /* Ignore everything between [] */ + for (pm = pe++; *pe != RBRACKET && *pe != EOS; pe++) + continue; + if (*pe == EOS) { + /* + * We could not find a matching RBRACKET. + * Ignore and just look for RBRACE + */ + pe = pm; + } + } + else if (*pe == LBRACE) + i++; + else if (*pe == RBRACE) { + if (i == 0) + break; + i--; + } + + /* Non matching braces; just glob the pattern */ + if (i != 0 || *pe == EOS) { + *rv = glob0(patbuf, pglob, limit); + return 0; + } + + for (i = 0, pl = pm = ptr; pm <= pe; pm++) + switch (*pm) { + case LBRACKET: + /* Ignore everything between [] */ + for (pm1 = pm++; *pm != RBRACKET && *pm != EOS; pm++) + continue; + if (*pm == EOS) { + /* + * We could not find a matching RBRACKET. + * Ignore and just look for RBRACE + */ + pm = pm1; + } + break; + + case LBRACE: + i++; + break; + + case RBRACE: + if (i) { + i--; + break; + } + /* FALLTHROUGH */ + case COMMA: + if (i && *pm == COMMA) + break; + else { + /* Append the current string */ + for (lm = ls; (pl < pm); *lm++ = *pl++) + continue; + /* + * Append the rest of the pattern after the + * closing brace + */ + for (pl = pe + 1; (*lm++ = *pl++) != EOS;) + continue; + + /* Expand the current pattern */ +#ifdef DEBUG + qprintf("globexp2:", patbuf); +#endif + *rv = globexp1(patbuf, pglob, limit); + + /* move after the comma, to the next string */ + pl = pm + 1; + } + break; + + default: + break; + } + *rv = 0; + return 0; +} + + + +/* + * expand tilde from the passwd file. + */ +static const Char * +globtilde(const Char *pattern, Char *patbuf, size_t patbuf_len, glob_t *pglob) +{ + struct passwd *pwd; + char *h; + const Char *p; + Char *b, *eb; + + if (*pattern != TILDE || !(pglob->gl_flags & GLOB_TILDE)) + return pattern; + + /* + * Copy up to the end of the string or / + */ + eb = &patbuf[patbuf_len - 1]; + for (p = pattern + 1, h = (char *) patbuf; + h < (char *)eb && *p && *p != SLASH; *h++ = *p++) + continue; + + *h = EOS; + + if (((char *) patbuf)[0] == EOS) { + /* + * handle a plain ~ or ~/ by expanding $HOME first (iff + * we're not running setuid or setgid) and then trying + * the password file + */ + if (issetugid() != 0 || + (h = getenv("HOME")) == NULL) { + if (((h = getlogin()) != NULL && + (pwd = getpwnam(h)) != NULL) || + (pwd = getpwuid(getuid())) != NULL) + h = pwd->pw_dir; + else + return pattern; + } + } + else { + /* + * Expand a ~user + */ + if ((pwd = getpwnam((char*) patbuf)) == NULL) + return pattern; + else + h = pwd->pw_dir; + } + + /* Copy the home directory */ + for (b = patbuf; b < eb && *h; *b++ = *h++) + continue; + + /* Append the rest of the pattern */ + while (b < eb && (*b++ = *p++) != EOS) + continue; + *b = EOS; + + return patbuf; +} + + +/* + * The main glob() routine: compiles the pattern (optionally processing + * quotes), calls glob1() to do the real pattern matching, and finally + * sorts the list (unless unsorted operation is requested). Returns 0 + * if things went well, nonzero if errors occurred. + */ +static int +glob0(const Char *pattern, glob_t *pglob, size_t *limit) +{ + const Char *qpatnext; + int err; + size_t oldpathc; + Char *bufnext, c, patbuf[MAXPATHLEN]; + + qpatnext = globtilde(pattern, patbuf, MAXPATHLEN, pglob); + oldpathc = pglob->gl_pathc; + bufnext = patbuf; + + /* We don't need to check for buffer overflow any more. */ + while ((c = *qpatnext++) != EOS) { + switch (c) { + case LBRACKET: + c = *qpatnext; + if (c == NOT) + ++qpatnext; + if (*qpatnext == EOS || + g_strchr(qpatnext+1, RBRACKET) == NULL) { + *bufnext++ = LBRACKET; + if (c == NOT) + --qpatnext; + break; + } + *bufnext++ = M_SET; + if (c == NOT) + *bufnext++ = M_NOT; + c = *qpatnext++; + do { + *bufnext++ = CHAR(c); + if (*qpatnext == RANGE && + (c = qpatnext[1]) != RBRACKET) { + *bufnext++ = M_RNG; + *bufnext++ = CHAR(c); + qpatnext += 2; + } + } while ((c = *qpatnext++) != RBRACKET); + pglob->gl_flags |= GLOB_MAGCHAR; + *bufnext++ = M_END; + break; + case QUESTION: + pglob->gl_flags |= GLOB_MAGCHAR; + *bufnext++ = M_ONE; + break; + case STAR: + pglob->gl_flags |= GLOB_MAGCHAR; + /* collapse adjacent stars to one, + * to avoid exponential behavior + */ + if (bufnext == patbuf || bufnext[-1] != M_ALL) + *bufnext++ = M_ALL; + break; + default: + *bufnext++ = CHAR(c); + break; + } + } + *bufnext = EOS; +#ifdef DEBUG + qprintf("glob0:", patbuf); +#endif + + if ((err = glob1(patbuf, pglob, limit)) != 0) + return(err); + + /* + * If there was no match we are going to append the pattern + * if GLOB_NOCHECK was specified or if GLOB_NOMAGIC was specified + * and the pattern did not contain any magic characters + * GLOB_NOMAGIC is there just for compatibility with csh. + */ + if (pglob->gl_pathc == oldpathc) { + if (((pglob->gl_flags & GLOB_NOCHECK) || + ((pglob->gl_flags & GLOB_NOMAGIC) && + !(pglob->gl_flags & GLOB_MAGCHAR)))) + return(globextend(pattern, pglob, limit)); + else + return(GLOB_NOMATCH); + } + if (!(pglob->gl_flags & GLOB_NOSORT)) + qsort(pglob->gl_pathv + pglob->gl_offs + oldpathc, + pglob->gl_pathc - oldpathc, sizeof(char *), compare); + return(0); +} + +static int +compare(const void *p, const void *q) +{ + return(strcmp(*(char **)p, *(char **)q)); +} + +static int +glob1(Char *pattern, glob_t *pglob, size_t *limit) +{ + Char pathbuf[MAXPATHLEN]; + + /* A null pathname is invalid -- POSIX 1003.1 sect. 2.4. */ + if (*pattern == EOS) + return(0); + return(glob2(pathbuf, pathbuf, pathbuf + MAXPATHLEN - 1, + pattern, pglob, limit)); +} + +/* + * The functions glob2 and glob3 are mutually recursive; there is one level + * of recursion for each segment in the pattern that contains one or more + * meta characters. + */ +static int +glob2(Char *pathbuf, Char *pathend, Char *pathend_last, Char *pattern, + glob_t *pglob, size_t *limit) +{ + struct stat sb; + Char *p, *q; + int anymeta; + + /* + * Loop over pattern segments until end of pattern or until + * segment with meta character found. + */ + for (anymeta = 0;;) { + if (*pattern == EOS) { /* End of pattern? */ + *pathend = EOS; + if (g_lstat(pathbuf, &sb, pglob)) + return(0); + + if (((pglob->gl_flags & GLOB_MARK) && + pathend[-1] != SEP) && (S_ISDIR(sb.st_mode) + || (S_ISLNK(sb.st_mode) && + (g_stat(pathbuf, &sb, pglob) == 0) && + S_ISDIR(sb.st_mode)))) { + if (pathend + 1 > pathend_last) + return (GLOB_ABORTED); + *pathend++ = SEP; + *pathend = EOS; + } + ++pglob->gl_matchc; + return(globextend(pathbuf, pglob, limit)); + } + + /* Find end of next segment, copy tentatively to pathend. */ + q = pathend; + p = pattern; + while (*p != EOS && *p != SEP) { + if (ismeta(*p)) + anymeta = 1; + if (q + 1 > pathend_last) + return (GLOB_ABORTED); + *q++ = *p++; + } + + if (!anymeta) { /* No expansion, do next segment. */ + pathend = q; + pattern = p; + while (*pattern == SEP) { + if (pathend + 1 > pathend_last) + return (GLOB_ABORTED); + *pathend++ = *pattern++; + } + } else /* Need expansion, recurse. */ + return(glob3(pathbuf, pathend, pathend_last, pattern, p, + pglob, limit)); + } + /* NOTREACHED */ +} + +static int +glob3(Char *pathbuf, Char *pathend, Char *pathend_last, + Char *pattern, Char *restpattern, + glob_t *pglob, size_t *limit) +{ + struct dirent *dp; + DIR *dirp; + int err; + char buf[MAXPATHLEN]; + + /* + * The readdirfunc declaration can't be prototyped, because it is + * assigned, below, to two functions which are prototyped in glob.h + * and dirent.h as taking pointers to differently typed opaque + * structures. + */ + struct dirent *(*readdirfunc)(); + + if (pathend > pathend_last) + return (GLOB_ABORTED); + *pathend = EOS; + errno = 0; + + if ((dirp = g_opendir(pathbuf, pglob)) == NULL) { + /* TODO: don't call for ENOENT or ENOTDIR? */ + if (pglob->gl_errfunc) { + if (g_Ctoc(pathbuf, buf, sizeof(buf))) + return (GLOB_ABORTED); + if (pglob->gl_errfunc(buf, errno) || + pglob->gl_flags & GLOB_ERR) + return (GLOB_ABORTED); + } + return(0); + } + + err = 0; + + /* Search directory for matching names. */ + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + readdirfunc = pglob->gl_readdir; + else + readdirfunc = readdir; + while ((dp = (*readdirfunc)(dirp))) { + char *sc; + Char *dc; + wchar_t wc; + size_t clen; + mbstate_t mbs; + + /* Initial DOT must be matched literally. */ + if (dp->d_name[0] == DOT && *pattern != DOT) + continue; + memset(&mbs, 0, sizeof(mbs)); + dc = pathend; + sc = dp->d_name; + while (dc < pathend_last) { + clen = mbrtowc(&wc, sc, MB_LEN_MAX, &mbs); + if (clen == (size_t)-1 || clen == (size_t)-2) { + wc = *sc; + clen = 1; + memset(&mbs, 0, sizeof(mbs)); + } + if ((*dc++ = wc) == EOS) + break; + sc += clen; + } + if (!match(pathend, pattern, restpattern)) { + *pathend = EOS; + continue; + } + err = glob2(pathbuf, --dc, pathend_last, restpattern, + pglob, limit); + if (err) + break; + } + + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + (*pglob->gl_closedir)(dirp); + else + closedir(dirp); + return(err); +} + + +/* + * Extend the gl_pathv member of a glob_t structure to accomodate a new item, + * add the new item, and update gl_pathc. + * + * This assumes the BSD realloc, which only copies the block when its size + * crosses a power-of-two boundary; for v7 realloc, this would cause quadratic + * behavior. + * + * Return 0 if new item added, error code if memory couldn't be allocated. + * + * Invariant of the glob_t structure: + * Either gl_pathc is zero and gl_pathv is NULL; or gl_pathc > 0 and + * gl_pathv points to (gl_offs + gl_pathc + 1) items. + */ +static int +globextend(const Char *path, glob_t *pglob, size_t *limit) +{ + char **pathv; + size_t i, newsize, len; + char *copy; + const Char *p; + + if (*limit && pglob->gl_pathc > *limit) { + errno = 0; + return (GLOB_NOSPACE); + } + + newsize = sizeof(*pathv) * (2 + pglob->gl_pathc + pglob->gl_offs); + pathv = pglob->gl_pathv ? + realloc((char *)pglob->gl_pathv, newsize) : + malloc(newsize); + if (pathv == NULL) { + if (pglob->gl_pathv) { + free(pglob->gl_pathv); + pglob->gl_pathv = NULL; + } + return(GLOB_NOSPACE); + } + + if (pglob->gl_pathv == NULL && pglob->gl_offs > 0) { + /* first time around -- clear initial gl_offs items */ + pathv += pglob->gl_offs; + for (i = pglob->gl_offs + 1; --i > 0; ) + *--pathv = NULL; + } + pglob->gl_pathv = pathv; + + for (p = path; *p++;) + continue; + len = MB_CUR_MAX * (size_t)(p - path); /* XXX overallocation */ + if ((copy = malloc(len)) != NULL) { + if (g_Ctoc(path, copy, len)) { + free(copy); + return (GLOB_NOSPACE); + } + pathv[pglob->gl_offs + pglob->gl_pathc++] = copy; + } + pathv[pglob->gl_offs + pglob->gl_pathc] = NULL; + return(copy == NULL ? GLOB_NOSPACE : 0); +} + +/* + * pattern matching function for filenames. Each occurrence of the * + * pattern causes a recursion level. + */ +static int +match(Char *name, Char *pat, Char *patend) +{ + int ok, negate_range; + Char c, k; + + while (pat < patend) { + c = *pat++; + switch (c & M_MASK) { + case M_ALL: + if (pat == patend) + return(1); + do + if (match(name, pat, patend)) + return(1); + while (*name++ != EOS); + return(0); + case M_ONE: + if (*name++ == EOS) + return(0); + break; + case M_SET: + ok = 0; + if ((k = *name++) == EOS) + return(0); + if ((negate_range = ((*pat & M_MASK) == M_NOT)) != EOS) + ++pat; + while (((c = *pat++) & M_MASK) != M_END) + if ((*pat & M_MASK) == M_RNG) { + if (CHAR(c) <= CHAR(k) && CHAR(k) <= CHAR(pat[1])) ok = 1; + pat += 2; + } else if (c == k) + ok = 1; + if (ok == negate_range) + return(0); + break; + default: + if (*name++ != c) + return(0); + break; + } + } + return(*name == EOS); +} + +/* Free allocated data belonging to a glob_t structure. */ +void +globfree(glob_t *pglob) +{ + size_t i; + char **pp; + + if (pglob->gl_pathv != NULL) { + pp = pglob->gl_pathv + pglob->gl_offs; + for (i = pglob->gl_pathc; i--; ++pp) + if (*pp) + free(*pp); + free(pglob->gl_pathv); + pglob->gl_pathv = NULL; + } +} + +static DIR * +g_opendir(Char *str, glob_t *pglob) +{ + char buf[MAXPATHLEN]; + + if (!*str) + strcpy(buf, "."); + else { + if (g_Ctoc(str, buf, sizeof(buf))) + return (NULL); + } + + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + return((*pglob->gl_opendir)(buf)); + + return(opendir(buf)); +} + +static int +g_lstat(Char *fn, struct stat *sb, glob_t *pglob) +{ + char buf[MAXPATHLEN]; + + if (g_Ctoc(fn, buf, sizeof(buf))) { + errno = ENAMETOOLONG; + return (-1); + } + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + return((*pglob->gl_lstat)(buf, sb)); + return(lstat(buf, sb)); +} + +static int +g_stat(Char *fn, struct stat *sb, glob_t *pglob) +{ + char buf[MAXPATHLEN]; + + if (g_Ctoc(fn, buf, sizeof(buf))) { + errno = ENAMETOOLONG; + return (-1); + } + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + return((*pglob->gl_stat)(buf, sb)); + return(stat(buf, sb)); +} + +static const Char * +g_strchr(const Char *str, wchar_t ch) +{ + + do { + if (*str == ch) + return (str); + } while (*str++); + return (NULL); +} + +static int +g_Ctoc(const Char *str, char *buf, size_t len) +{ + mbstate_t mbs; + size_t clen; + + memset(&mbs, 0, sizeof(mbs)); + while (len >= MB_CUR_MAX) { + clen = wcrtomb(buf, *str, &mbs); + if (clen == (size_t)-1) + return (1); + if (*str == L'\0') + return (0); + str++; + buf += clen; + len -= clen; + } + return (1); +} + +#ifdef DEBUG +static void +qprintf(const char *str, Char *s) +{ + Char *p; + + (void)printf("%s:\n", str); + for (p = s; *p; p++) + (void)printf("%c", CHAR(*p)); + (void)printf("\n"); + for (p = s; *p; p++) + (void)printf("%c", *p & M_PROTECT ? '"' : ' '); + (void)printf("\n"); + for (p = s; *p; p++) + (void)printf("%c", ismeta(*p) ? '_' : ' '); + (void)printf("\n"); +} +#endif diff -Nur /tmp/x/glob.h libglob/glob.h --- /tmp/x/glob.h 1969-12-31 19:00:00.000000000 -0500 +++ libglob/glob.h 2017-08-19 15:22:18.367109399 -0400 @@ -0,0 +1,104 @@ +/* + * Copyright (c) 1989, 1993 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Guido van Rossum. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * @(#)glob.h 8.1 (Berkeley) 6/2/93 + * $FreeBSD$ + */ + +#ifndef _GLOB_H_ +#define _GLOB_H_ + +#include +#include +#ifndef ARG_MAX +#define ARG_MAX 6553 +#endif + +#ifndef _SIZE_T_DECLARED +#include +#define _SIZE_T_DECLARED +#endif + +struct stat; +typedef struct { + size_t gl_pathc; /* Count of total paths so far. */ + size_t gl_matchc; /* Count of paths matching pattern. */ + size_t gl_offs; /* Reserved at beginning of gl_pathv. */ + int gl_flags; /* Copy of flags parameter to glob. */ + char **gl_pathv; /* List of paths matching pattern. */ + /* Copy of errfunc parameter to glob. */ + int (*gl_errfunc)(const char *, int); + + /* + * Alternate filesystem access methods for glob; replacement + * versions of closedir(3), readdir(3), opendir(3), stat(2) + * and lstat(2). + */ + void (*gl_closedir)(void *); + struct dirent *(*gl_readdir)(void *); + void *(*gl_opendir)(const char *); + int (*gl_lstat)(const char *, struct stat *); + int (*gl_stat)(const char *, struct stat *); +} glob_t; + +/* Believed to have been introduced in 1003.2-1992 */ +#define GLOB_APPEND 0x0001 /* Append to output from previous call. */ +#define GLOB_DOOFFS 0x0002 /* Use gl_offs. */ +#define GLOB_ERR 0x0004 /* Return on error. */ +#define GLOB_MARK 0x0008 /* Append / to matching directories. */ +#define GLOB_NOCHECK 0x0010 /* Return pattern itself if nothing matches. */ +#define GLOB_NOSORT 0x0020 /* Don't sort. */ +#define GLOB_NOESCAPE 0x2000 /* Disable backslash escaping. */ + +/* Error values returned by glob(3) */ +#define GLOB_NOSPACE (-1) /* Malloc call failed. */ +#define GLOB_ABORTED (-2) /* Unignored error. */ +#define GLOB_NOMATCH (-3) /* No match and GLOB_NOCHECK was not set. */ +#define GLOB_NOSYS (-4) /* Obsolete: source comptability only. */ + +#define GLOB_ALTDIRFUNC 0x0040 /* Use alternately specified directory funcs. */ +#define GLOB_BRACE 0x0080 /* Expand braces ala csh. */ +#define GLOB_MAGCHAR 0x0100 /* Pattern had globbing characters. */ +#define GLOB_NOMAGIC 0x0200 /* GLOB_NOCHECK without magic chars (csh). */ +#define GLOB_QUOTE 0x0400 /* Quote special chars with \. */ +#define GLOB_TILDE 0x0800 /* Expand tilde names from the passwd file. */ +#define GLOB_LIMIT 0x1000 /* limit number of returned paths */ + +/* source compatibility, these are the old names */ +#define GLOB_MAXPATH GLOB_LIMIT +#define GLOB_ABEND GLOB_ABORTED + +__BEGIN_DECLS +int glob(const char *, int, int (*)(const char *, int), glob_t *); +void globfree(glob_t *); +__END_DECLS + +#endif /* !_GLOB_H_ */ ================================================ FILE: pythonforandroid/recipes/libiconv/__init__.py ================================================ from pythonforandroid.logger import shprint from pythonforandroid.util import current_directory from pythonforandroid.recipe import Recipe from multiprocessing import cpu_count import sh class LibIconvRecipe(Recipe): version = '1.16' url = 'https://ftp.gnu.org/pub/gnu/libiconv/libiconv-{version}.tar.gz' built_libraries = {'libiconv.so': 'lib/.libs'} def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): shprint( sh.Command('./configure'), '--host=' + arch.command_prefix, '--prefix=' + self.ctx.get_python_install_dir(arch.arch), _env=env) shprint(sh.make, '-j' + str(cpu_count()), _env=env) recipe = LibIconvRecipe() ================================================ FILE: pythonforandroid/recipes/liblzma/__init__.py ================================================ import sh from multiprocessing import cpu_count from os.path import exists, join from pythonforandroid.archs import Arch from pythonforandroid.logger import shprint from pythonforandroid.recipe import Recipe from pythonforandroid.util import current_directory class LibLzmaRecipe(Recipe): version = '5.2.4' url = 'https://tukaani.org/xz/xz-{version}.tar.gz' built_libraries = {'liblzma.so': 'p4a_install/lib'} def build_arch(self, arch: Arch) -> None: env = self.get_recipe_env(arch) install_dir = join(self.get_build_dir(arch.arch), 'p4a_install') with current_directory(self.get_build_dir(arch.arch)): if not exists('configure'): shprint(sh.Command('./autogen.sh'), _env=env) shprint(sh.Command('autoreconf'), '-vif', _env=env) shprint(sh.Command('./configure'), '--host=' + arch.command_prefix, '--prefix=' + install_dir, '--disable-builddir', '--disable-static', '--enable-shared', '--disable-xz', '--disable-xzdec', '--disable-lzmadec', '--disable-lzmainfo', '--disable-scripts', '--disable-doc', _env=env) shprint( sh.make, '-j', str(cpu_count()), _env=env ) shprint(sh.make, 'install', _env=env) def get_library_includes(self, arch: Arch) -> str: """ Returns a string with the appropriate `-I` to link with the lzma lib. This string is usually added to the environment variable `CPPFLAGS`. """ return " -I" + join( self.get_build_dir(arch.arch), 'p4a_install', 'include', ) def get_library_ldflags(self, arch: Arch) -> str: """ Returns a string with the appropriate `-L` to link with the lzma lib. This string is usually added to the environment variable `LDFLAGS`. """ return " -L" + join( self.get_build_dir(arch.arch), self.built_libraries['liblzma.so'], ) @staticmethod def get_library_libs_flag() -> str: """ Returns a string with the appropriate `-l` flags to link with the lzma lib. This string is usually added to the environment variable `LIBS`. """ return " -llzma" recipe = LibLzmaRecipe() ================================================ FILE: pythonforandroid/recipes/libogg/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.toolchain import current_directory, shprint import sh class OggRecipe(Recipe): version = '1.3.3' url = 'http://downloads.xiph.org/releases/ogg/libogg-{version}.tar.gz' built_libraries = {'libogg.so': 'src/.libs'} def build_arch(self, arch): with current_directory(self.get_build_dir(arch.arch)): env = self.get_recipe_env(arch) flags = [ '--host=' + arch.command_prefix, ] configure = sh.Command('./configure') shprint(configure, *flags, _env=env) shprint(sh.make, _env=env) recipe = OggRecipe() ================================================ FILE: pythonforandroid/recipes/libopenblas/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.logger import shprint from pythonforandroid.util import current_directory, ensure_dir from multiprocessing import cpu_count from os.path import join import sh from pythonforandroid.util import rmdir class LibOpenBlasRecipe(Recipe): version = "0.3.29" url = "https://github.com/OpenMathLib/OpenBLAS/archive/refs/tags/v{version}.tar.gz" built_libraries = {"libopenblas.so": "build/lib"} min_ndk_api_support = 24 # complex math functions support def build_arch(self, arch): source_dir = self.get_build_dir(arch.arch) build_target = join(source_dir, "build") ensure_dir(build_target) with current_directory(build_target): env = self.get_recipe_env(arch) rmdir("CMakeFiles") shprint(sh.rm, "-f", "CMakeCache.txt", _env=env) opts = [ # default cmake options "-DCMAKE_SYSTEM_NAME=Android", "-DCMAKE_ANDROID_ARCH_ABI={arch}".format(arch=arch.arch), "-DCMAKE_ANDROID_NDK=" + self.ctx.ndk_dir, "-DCMAKE_ANDROID_API={api}".format(api=self.ctx.ndk_api), "-DCMAKE_BUILD_TYPE=Release", "-DBUILD_SHARED_LIBS=ON", "-DC_LAPACK=ON", "-DTARGET={target}".format( target={ "arm64-v8a": "ARMV8", "armeabi-v7a": "ARMV7", "x86_64": "CORE2", "x86": "CORE2", }[arch.arch] ), ] shprint(sh.cmake, source_dir, *opts, _env=env) shprint(sh.make, "-j" + str(cpu_count()), _env=env) recipe = LibOpenBlasRecipe() ================================================ FILE: pythonforandroid/recipes/libpcre/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.util import current_directory from pythonforandroid.logger import shprint import sh from multiprocessing import cpu_count from os.path import join class LibpcreRecipe(Recipe): version = '8.44' url = 'https://ftp.pcre.org/pub/pcre/pcre-{version}.tar.bz2' built_libraries = {'libpcre.so': '.libs'} def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): shprint( sh.Command('./configure'), *'''--host=arm-linux-androideabi --disable-cpp --enable-jit --enable-utf8 --enable-unicode-properties'''.split(), _env=env) shprint(sh.make, '-j', str(cpu_count()), _env=env) def get_lib_dir(self, arch): return join(self.get_build_dir(arch), '.libs') recipe = LibpcreRecipe() ================================================ FILE: pythonforandroid/recipes/libpq/__init__.py ================================================ from pythonforandroid.toolchain import Recipe, current_directory, shprint import sh import os.path class LibpqRecipe(Recipe): version = '10.12' url = 'http://ftp.postgresql.org/pub/source/v{version}/postgresql-{version}.tar.bz2' depends = [] def get_recipe_env(self, arch): env = super().get_recipe_env(arch) env['USE_DEV_URANDOM'] = '1' return env def should_build(self, arch): return not os.path.isfile('{}/libpq.a'.format(self.ctx.get_libs_dir(arch.arch))) def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): configure = sh.Command('./configure') shprint(configure, '--without-readline', '--host=arm-linux', _env=env) shprint(sh.make, 'submake-libpq', _env=env) shprint(sh.cp, '-a', 'src/interfaces/libpq/libpq.a', self.ctx.get_libs_dir(arch.arch)) recipe = LibpqRecipe() ================================================ FILE: pythonforandroid/recipes/libpthread/__init__.py ================================================ from os import makedirs, remove from os.path import exists, join import sh from pythonforandroid.recipe import Recipe from pythonforandroid.logger import shprint class LibPthread(Recipe): ''' This is a dumb recipe. We may need this because some recipes inserted some flags `-lpthread` without our control, case of: - :class:`~pythonforandroid.recipes.uvloop.UvloopRecipe` .. note:: the libpthread doesn't exist in android but it is integrated into libc, so we create a symbolic link which we will remove when our build finishes''' def build_arch(self, arch): libc_path = join(arch.ndk_lib_dir_versioned, 'libc') # Create a temporary folder to add to link path with a fake libpthread.so: fake_libpthread_temp_folder = join( self.get_build_dir(arch.arch), "p4a-libpthread-recipe-tempdir" ) if not exists(fake_libpthread_temp_folder): makedirs(fake_libpthread_temp_folder) # Set symlinks, and make sure to update them on every build run: if exists(join(fake_libpthread_temp_folder, "libpthread.so")): remove(join(fake_libpthread_temp_folder, "libpthread.so")) shprint(sh.ln, '-sf', libc_path + '.so', join(fake_libpthread_temp_folder, "libpthread.so"), ) if exists(join(fake_libpthread_temp_folder, "libpthread.a")): remove(join(fake_libpthread_temp_folder, "libpthread.a")) shprint(sh.ln, '-sf', libc_path + '.a', join(fake_libpthread_temp_folder, "libpthread.a"), ) # Add folder as -L link option for all recipes if not done yet: if fake_libpthread_temp_folder not in arch.extra_global_link_paths: arch.extra_global_link_paths.append( fake_libpthread_temp_folder ) recipe = LibPthread() ================================================ FILE: pythonforandroid/recipes/librt/__init__.py ================================================ from os import makedirs, remove from os.path import exists, join import sh from pythonforandroid.recipe import Recipe from pythonforandroid.logger import shprint class LibRt(Recipe): ''' This is a dumb recipe. We may need this because some recipes inserted some flags `-lrt` without our control, case of: - :class:`~pythonforandroid.recipes.gevent.GeventRecipe` - :class:`~pythonforandroid.recipes.lxml.LXMLRecipe` .. note:: the librt doesn't exist in android but it is integrated into libc, so we create a symbolic link which we will remove when our build finishes''' def build_arch(self, arch): libc_path = join(arch.ndk_lib_dir_versioned, 'libc') # Create a temporary folder to add to link path with a fake librt.so: fake_librt_temp_folder = join( self.get_build_dir(arch.arch), "p4a-librt-recipe-tempdir" ) if not exists(fake_librt_temp_folder): makedirs(fake_librt_temp_folder) # Set symlinks, and make sure to update them on every build run: if exists(join(fake_librt_temp_folder, "librt.so")): remove(join(fake_librt_temp_folder, "librt.so")) shprint(sh.ln, '-sf', libc_path + '.so', join(fake_librt_temp_folder, "librt.so"), ) if exists(join(fake_librt_temp_folder, "librt.a")): remove(join(fake_librt_temp_folder, "librt.a")) shprint(sh.ln, '-sf', libc_path + '.a', join(fake_librt_temp_folder, "librt.a"), ) # Add folder as -L link option for all recipes if not done yet: if fake_librt_temp_folder not in arch.extra_global_link_paths: arch.extra_global_link_paths.append( fake_librt_temp_folder ) recipe = LibRt() ================================================ FILE: pythonforandroid/recipes/libsecp256k1/__init__.py ================================================ from pythonforandroid.logger import shprint from pythonforandroid.util import current_directory from pythonforandroid.recipe import Recipe from multiprocessing import cpu_count from os.path import exists import sh class LibSecp256k1Recipe(Recipe): built_libraries = {'libsecp256k1.so': '.libs'} version = '0.4.1' url = 'https://github.com/bitcoin-core/secp256k1/archive/refs/tags/v{version}.tar.gz' def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): if not exists('configure'): shprint(sh.Command('./autogen.sh'), _env=env) shprint( sh.Command('./configure'), '--host=' + arch.command_prefix, '--prefix=' + self.ctx.get_python_install_dir(arch.arch), '--enable-shared', '--enable-module-recovery', '--enable-experimental', '--enable-module-ecdh', _env=env) shprint(sh.make, '-j' + str(cpu_count()), _env=env) recipe = LibSecp256k1Recipe() ================================================ FILE: pythonforandroid/recipes/libshine/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.util import current_directory from pythonforandroid.logger import shprint from multiprocessing import cpu_count from os.path import realpath import sh class LibShineRecipe(Recipe): version = 'c72aba9031bde18a0995e7c01c9b53f2e08a0e46' url = 'https://github.com/toots/shine/archive/{version}.zip' built_libraries = {'libshine.so': 'lib'} def get_recipe_env(self, arch=None, with_flags_in_cc=True): env = super().get_recipe_env(arch, with_flags_in_cc) # technically, libraries should go to `LDLIBS`, but it seems # that libshine doesn't like so, and it will fail on linking stage env['LDLIBS'] = env['LDLIBS'].replace(' -lm', '') env['LDFLAGS'] += ' -lm' return env def build_arch(self, arch): with current_directory(self.get_build_dir(arch.arch)): env = self.get_recipe_env(arch) shprint(sh.Command('./bootstrap')) configure = sh.Command('./configure') shprint(configure, f'--host={arch.command_prefix}', '--enable-pic', '--disable-static', '--enable-shared', f'--prefix={realpath(".")}', _env=env) shprint(sh.make, '-j', str(cpu_count()), _env=env) shprint(sh.make, 'install', _env=env) recipe = LibShineRecipe() ================================================ FILE: pythonforandroid/recipes/libsodium/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.util import current_directory from pythonforandroid.logger import shprint from multiprocessing import cpu_count import sh from packaging import version as packaging_version class LibsodiumRecipe(Recipe): version = '1.0.16' url = 'https://github.com/jedisct1/libsodium/releases/download/{}/libsodium-{}.tar.gz' depends = [] patches = ['size_max_fix.patch'] built_libraries = {'libsodium.so': 'src/libsodium/.libs'} @property def versioned_url(self): asked_version = packaging_version.parse(self.version) if asked_version > packaging_version.parse('1.0.16'): return self._url.format(self.version + '-RELEASE', self.version) else: return self._url.format(self.version, self.version) def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): bash = sh.Command('bash') shprint( bash, 'configure', '--disable-soname-versions', '--host={}'.format(arch.command_prefix), '--enable-shared', _env=env, ) shprint(sh.make, '-j', str(cpu_count()), _env=env) def get_recipe_env(self, arch): env = super().get_recipe_env(arch) env['CFLAGS'] += ' -Os' return env recipe = LibsodiumRecipe() ================================================ FILE: pythonforandroid/recipes/libsodium/size_max_fix.patch ================================================ diff -urN libsodium-1.0.16.ori/src/libsodium/include/sodium/export.h libsodium-1.0.16/src/libsodium/include/sodium/export.h --- libsodium-1.0.16.ori/src/libsodium/include/sodium/export.h 2017-12-12 00:03:07.000000000 +0100 +++ libsodium-1.0.16/src/libsodium/include/sodium/export.h 2018-10-31 09:46:06.051189444 +0100 @@ -47,6 +47,8 @@ # endif #endif +#include + #define SODIUM_MIN(A, B) ((A) < (B) ? (A) : (B)) #define SODIUM_SIZE_MAX SODIUM_MIN(UINT64_MAX, SIZE_MAX) ================================================ FILE: pythonforandroid/recipes/libtorrent/__init__.py ================================================ from multiprocessing import cpu_count from os import listdir, walk from os.path import join, basename import shutil import sh from pythonforandroid.toolchain import Recipe, shprint, current_directory # This recipe builds libtorrent with Python bindings # It depends on Boost.Build and the source of several Boost libraries present # in BOOST_ROOT, which is all provided by the boost recipe def get_lib_from(search_directory, lib_extension='.so'): '''Scan directories recursively until find any file with the given extension. The default extension to search is ``.so``.''' for root, dirs, files in walk(search_directory): for file in files: if file.endswith(lib_extension): print('get_lib_from: {}\n\t- {}'.format( search_directory, join(root, file))) return join(root, file) return None class LibtorrentRecipe(Recipe): # Todo: make recipe compatible with all p4a architectures ''' .. note:: This recipe can be built only against API 21+ and an android ndk >= r19 .. versionchanged:: 0.6.0 Rewrote recipe to support clang's build and boost 1.68. The following changes has been made: - Bumped version number to 1.2.0 - added python 3 compatibility - new system to detect/copy generated libraries .. versionchanged:: 2019.08.09.1.dev0 - Bumped version number to 1.2.1 - Adapted to work with ndk-r19+ ''' version = '1_2_1' url = 'https://github.com/arvidn/libtorrent/archive/libtorrent-{version}.tar.gz' depends = ['boost'] opt_depends = ['openssl'] patches = ['disable-so-version.patch', 'use-soname-python.patch', 'setup-lib-name.patch'] # libtorrent.so is not included because is not a system library generated_libraries = [ 'boost_system', 'boost_python{py_version}', 'torrent_rasterbar'] def should_build(self, arch): python_version = self.ctx.python_recipe.version[:3].replace('.', '') libs = ['lib' + lib_name.format(py_version=python_version) + '.so' for lib_name in self.generated_libraries] return not (self.has_libs(arch, *libs) and self.ctx.has_package('libtorrent', arch.arch)) def prebuild_arch(self, 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().build_arch(arch) env = self.get_recipe_env(arch) env['PYTHON_HOST'] = self.ctx.hostpython # Define build variables build_dir = self.get_build_dir(arch.arch) ctx_libs_dir = self.ctx.get_libs_dir(arch.arch) encryption = 'openssl' if 'openssl' in recipe.ctx.recipe_build_order else 'built-in' build_args = [ '-q', # '-a', # force build, useful to debug the build '-j' + str(cpu_count()), '--debug-configuration', # so we know if our python is detected # '--deprecated-functions=off', 'toolset=clang-{arch}'.format(arch=env['ARCH']), 'abi=aapcs', 'binary-format=elf', 'cxxflags=-std=c++11', 'target-os=android', 'threading=multi', 'link=shared', 'boost-link=shared', 'libtorrent-link=shared', 'runtime-link=shared', 'encryption={}'.format('on' if encryption == 'openssl' else 'off'), 'crypto=' + encryption ] crypto_folder = 'encryption-off' if encryption == 'openssl': crypto_folder = 'crypto-openssl' build_args.extend(['openssl-lib=' + env['OPENSSL_BUILD_PATH'], 'openssl-include=' + env['OPENSSL_INCLUDE'] ]) build_args.append('release') # Compile libtorrent with boost libraries and python bindings with current_directory(join(build_dir, 'bindings/python')): b2 = sh.Command(join(env['BOOST_ROOT'], 'b2')) shprint(b2, *build_args, _env=env) # Copy only the boost shared libraries into the libs folder. Because # boost build two boost_python libraries, we force to search the lib # into the corresponding build path. b2_build_dir = ( 'build/clang-linux-{arch}/release/{encryption}/' 'lt-visibility-hidden/'.format( arch=env['ARCH'], encryption=crypto_folder ) ) boost_libs_dir = join(env['BOOST_BUILD_PATH'], 'bin.v2/libs') for boost_lib in listdir(boost_libs_dir): lib_path = get_lib_from(join(boost_libs_dir, boost_lib, b2_build_dir)) if lib_path: lib_name = basename(lib_path) shutil.copyfile(lib_path, join(ctx_libs_dir, lib_name)) # Copy libtorrent shared libraries into the right places system_libtorrent = get_lib_from(join(build_dir, 'bin')) if system_libtorrent: shutil.copyfile(system_libtorrent, join(ctx_libs_dir, 'libtorrent_rasterbar.so')) python_libtorrent = get_lib_from(join(build_dir, 'bindings/python/bin')) shutil.copyfile(python_libtorrent, join(self.ctx.get_site_packages_dir(arch), 'libtorrent.so')) def get_recipe_env(self, arch): # Use environment from boost recipe, cause we use b2 tool from boost env = self.get_recipe('boost', self.ctx).get_recipe_env(arch) if 'openssl' in recipe.ctx.recipe_build_order: r = self.get_recipe('openssl', self.ctx) env['OPENSSL_BUILD_PATH'] = r.get_build_dir(arch.arch) env['OPENSSL_INCLUDE'] = join(r.get_build_dir(arch.arch), 'include') env['OPENSSL_VERSION'] = r.version return env recipe = LibtorrentRecipe() ================================================ FILE: pythonforandroid/recipes/libtorrent/disable-so-version.patch ================================================ --- libtorrent/Jamfile 2016-01-17 23:52:45.000000000 +0100 +++ libtorrent-patch/Jamfile 2016-02-09 13:37:57.499561750 +0100 @@ -325,6 +325,7 @@ if $(type) = SHARED_LIB && ( ! ( [ $(property-set).get ] in windows cygwin ) ) { + return "libtorrent_rasterbar.so" ; # linked by python bindings .so name = $(name).$(VERSION) ; } ================================================ FILE: pythonforandroid/recipes/libtorrent/setup-lib-name.patch ================================================ --- libtorrent/bindings/python/setup.py.orig 2018-11-26 22:21:48.772142135 +0100 +++ libtorrent/bindings/python/setup.py 2018-11-26 22:23:23.092141235 +0100 @@ -167,7 +167,7 @@ extra_compile = flags.parse(extra_cmd) ext = [Extension( - 'libtorrent', + 'libtorrent_rasterbar', sources=sorted(source_list), language='c++', include_dirs=flags.include_dirs, @@ -178,7 +178,7 @@ ] setup( - name='python-libtorrent', + name='libtorrent', version='1.2.1', author='Arvid Norberg', author_email='arvid@libtorrent.org', ================================================ FILE: pythonforandroid/recipes/libtorrent/use-soname-python.patch ================================================ --- libtorrent/bindings/python/Jamfile.orig 2018-12-07 16:46:50.851838981 +0100 +++ libtorrent/bindings/python/Jamfile 2018-12-07 16:49:09.099837663 +0100 @@ -113,7 +113,7 @@ if ( gcc in $(properties) ) { - result += -Wl,-Bsymbolic ; + result += -Wl,-soname=libtorrent.so,-Bsymbolic ; } } ================================================ FILE: pythonforandroid/recipes/libtorrent/user-config-openssl.patch ================================================ --- boost/user-config.jam.orig 2018-12-07 14:16:45.911924859 +0100 +++ boost/user-config.jam 2018-12-07 14:20:16.243922853 +0100 @@ -9,6 +9,8 @@ local PYTHON_INCLUDE = [ os.environ PYTHON_INCLUDE ] ; local PYTHON_LINK_VERSION = [ os.environ PYTHON_LINK_VERSION ] ; local PYTHON_MAJOR_MINOR = [ os.environ PYTHON_MAJOR_MINOR ] ; +local OPENSSL_BUILD_PATH = [ os.environ OPENSSL_BUILD_PATH ] ; +local OPENSSL_VERSION = [ os.environ OPENSSL_VERSION ] ; #using clang : $(ARCH) : $(ANDROID_BINARIES_PATH)/clang++ : #$(ANDROID_BINARIES_PATH)/llvm-ar @@ -56,6 +58,9 @@ -Wl,-z,relro -Wl,-z,now -lc++_shared +-L$(OPENSSL_BUILD_PATH) +-lcrypto$(OPENSSL_VERSION) +-lssl$(OPENSSL_VERSION) -L$(PYTHON_ROOT) -lpython$(PYTHON_LINK_VERSION) -Wl,-O1 ================================================ FILE: pythonforandroid/recipes/libtribler/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe """ Privacy with BitTorrent and resilient to shut down http://www.tribler.org """ class LibTriblerRecipe(PythonRecipe): version = 'devel' url = 'git+https://github.com/Tribler/tribler.git' depends = ['apsw', 'cryptography', 'ffmpeg', 'libsodium', 'libtorrent', 'm2crypto', 'netifaces', 'openssl', 'pil', 'pycrypto', 'pyleveldb', 'twisted', ] conflicts = ['python3'] python_depends = ['chardet', 'cherrypy', 'configobj', 'decorator', 'feedparser', 'libnacl', 'pyasn1', 'requests', 'six', ] site_packages_name = 'Tribler' recipe = LibTriblerRecipe() ================================================ FILE: pythonforandroid/recipes/libvorbis/__init__.py ================================================ from pythonforandroid.recipe import NDKRecipe from pythonforandroid.toolchain import current_directory, shprint from os.path import join import sh class VorbisRecipe(NDKRecipe): version = '1.3.6' url = 'http://downloads.xiph.org/releases/vorbis/libvorbis-{version}.tar.gz' opt_depends = ['libogg'] generated_libraries = ['libvorbis.so', 'libvorbisfile.so', 'libvorbisenc.so'] def get_recipe_env(self, arch=None): 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 def build_arch(self, arch): with current_directory(self.get_build_dir(arch.arch)): env = self.get_recipe_env(arch) flags = [ '--host=' + arch.command_prefix, ] configure = sh.Command('./configure') shprint(configure, *flags, _env=env) shprint(sh.make, _env=env) self.install_libs( arch, join('lib', '.libs', 'libvorbis.so'), join('lib', '.libs', 'libvorbisfile.so'), join('lib', '.libs', 'libvorbisenc.so')) recipe = VorbisRecipe() ================================================ FILE: pythonforandroid/recipes/libvpx/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.toolchain import current_directory, shprint from os.path import join, realpath from multiprocessing import cpu_count import sh TARGETS = { 'armeabi-v7a': 'armv7-android-gcc', 'arm64-v8a': 'arm64-android-gcc', 'x86': 'x86-android-gcc', 'x86_64': 'x86_64-android-gcc', } class VPXRecipe(Recipe): version = '1.11.0' url = 'https://github.com/webmproject/libvpx/archive/v{version}.tar.gz' patches = [ # See https://git.io/Jq50q join('patches', '0001-android-force-neon-runtime.patch'), ] def get_recipe_env(self, arch=None): env = super().get_recipe_env(arch) env['CXXFLAGS'] += f' -I{self.ctx.ndk.libcxx_include_dir}' return env def build_arch(self, arch): with current_directory(self.get_build_dir(arch.arch)): env = self.get_recipe_env(arch) flags = [ '--target=' + TARGETS[arch.arch], '--enable-pic', '--enable-vp8', '--enable-vp9', '--enable-static', '--enable-small', '--disable-shared', '--disable-examples', '--disable-unit-tests', '--disable-tools', '--disable-docs', '--disable-install-docs', '--disable-realtime-only', f'--prefix={realpath(".")}', ] if arch.arch == 'armeabi-v7a': flags.append('--disable-neon-asm') configure = sh.Command('./configure') shprint(configure, *flags, _env=env) shprint(sh.make, '-j', str(cpu_count()), _env=env) shprint(sh.make, 'install', _env=env) recipe = VPXRecipe() ================================================ FILE: pythonforandroid/recipes/libvpx/patches/0001-android-force-neon-runtime.patch ================================================ diff -u -r ../libvpx-1.6.1/vpx_ports/arm_cpudetect.c ./vpx_ports/arm_cpudetect.c --- ../libvpx-1.6.1/vpx_ports/arm_cpudetect.c 2017-01-12 21:27:27.000000000 +0100 +++ ./vpx_ports/arm_cpudetect.c 2017-01-29 23:55:05.399283897 +0100 @@ -92,20 +92,17 @@ } #elif defined(__ANDROID__) /* end _MSC_VER */ -#include int arm_cpu_caps(void) { int flags; int mask; - uint64_t features; if (!arm_cpu_env_flags(&flags)) { return flags; } mask = arm_cpu_env_mask(); - features = android_getCpuFeatures(); #if HAVE_NEON || HAVE_NEON_ASM - if (features & ANDROID_CPU_ARM_FEATURE_NEON) flags |= HAS_NEON; + flags |= HAS_NEON; #endif /* HAVE_NEON || HAVE_NEON_ASM */ return flags & mask; } ================================================ FILE: pythonforandroid/recipes/libwebp/__init__.py ================================================ from multiprocessing import cpu_count from os.path import join import sh from pythonforandroid.util import current_directory, ensure_dir from pythonforandroid.toolchain import shprint from pythonforandroid.recipe import Recipe class LibwebpRecipe(Recipe): version = '1.1.0' url = 'https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-{version}.tar.gz' # noqa depends = [] built_libraries = { 'libwebp.so': 'installation/lib', 'libwebpdecoder.so': 'installation/lib', 'libwebpdemux.so': 'installation/lib', 'libwebpmux.so': 'installation/lib', } def build_arch(self, arch): source_dir = self.get_build_dir(arch.arch) build_dir = join(source_dir, 'build') install_dir = join(source_dir, 'installation') toolchain_file = join( self.ctx.ndk_dir, 'build', 'cmake', 'android.toolchain.cmake', ) ensure_dir(build_dir) with current_directory(build_dir): env = self.get_recipe_env(arch) shprint(sh.cmake, source_dir, f'-DANDROID_ABI={arch.arch}', f'-DANDROID_NATIVE_API_LEVEL={self.ctx.ndk_api}', f'-DCMAKE_TOOLCHAIN_FILE={toolchain_file}', f'-DCMAKE_INSTALL_PREFIX={install_dir}', '-DCMAKE_BUILD_TYPE=Release', '-DBUILD_SHARED_LIBS=1', _env=env) shprint(sh.make, '-j' + str(cpu_count()), _env=env) # We make the install because this way we will have # all the includes and libraries in one place shprint(sh.make, 'install', _env=env) recipe = LibwebpRecipe() ================================================ FILE: pythonforandroid/recipes/libx264/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.util import current_directory from pythonforandroid.logger import shprint from multiprocessing import cpu_count from os.path import realpath import sh class LibX264Recipe(Recipe): version = '5db6aa6cab1b146e07b60cc1736a01f21da01154' # commit of latest known stable version url = 'https://code.videolan.org/videolan/x264/-/archive/{version}/x264-{version}.zip' built_libraries = {'libx264.a': 'lib'} def build_arch(self, arch): with current_directory(self.get_build_dir(arch.arch)): env = self.get_recipe_env(arch) configure = sh.Command('./configure') shprint(configure, f'--host={arch.command_prefix}', '--disable-asm', '--disable-cli', '--enable-pic', '--enable-static', '--prefix={}'.format(realpath('.')), _env=env) shprint(sh.make, '-j', str(cpu_count()), _env=env) shprint(sh.make, 'install', _env=env) recipe = LibX264Recipe() ================================================ FILE: pythonforandroid/recipes/libxml2/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.util import current_directory from pythonforandroid.logger import shprint from os.path import exists import sh class Libxml2Recipe(Recipe): version = '2.9.12' url = 'http://xmlsoft.org/sources/libxml2-{version}.tar.gz' depends = [] patches = ['add-glob.c.patch'] built_libraries = {'libxml2.a': '.libs'} def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): if not exists('configure'): shprint(sh.Command('./autogen.sh'), _env=env) shprint(sh.Command('autoreconf'), '-vif', _env=env) build_arch = shprint( sh.gcc, '-dumpmachine').stdout.decode('utf-8').split('\n')[0] shprint(sh.Command('./configure'), '--build=' + build_arch, '--host=' + arch.command_prefix, '--target=' + arch.command_prefix, '--without-modules', '--without-legacy', '--without-history', '--without-debug', '--without-docbook', '--without-python', '--without-threads', '--without-iconv', '--without-lzma', '--disable-shared', '--enable-static', _env=env) # Ensure we only build libxml2.la as if we do everything # we'll need the glob dependency which is a big headache shprint(sh.make, "libxml2.la", _env=env) def get_recipe_env(self, 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) return env recipe = Libxml2Recipe() ================================================ FILE: pythonforandroid/recipes/libxml2/add-glob.c.patch ================================================ From c97da18834aa41637e3e550bccb70bd2dd0ca3b9 Mon Sep 17 00:00:00 2001 From: Zachary Goldberg Date: Wed, 20 Apr 2016 21:21:52 -0700 Subject: [PATCH] Add glob --- glob.c | 906 ++++++++++++++++++++++++++++++++ glob.h | 105 ++++ 2 files changed, 1011 insertions(+) create mode 100644 glob.c create mode 100644 glob.h diff --git a/glob.c b/glob.c new file mode 100644 index 0000000..cec80ed --- /dev/null +++ b/glob.c @@ -0,0 +1,906 @@ +/* + * Natanael Arndt, 2011: removed collate.h dependencies + * (my changes are trivial) + * + * Copyright (c) 1989, 1993 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Guido van Rossum. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 4. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#if defined(LIBC_SCCS) && !defined(lint) +static char sccsid[] = "@(#)glob.c 8.3 (Berkeley) 10/13/93"; +#endif /* LIBC_SCCS and not lint */ +#include +__FBSDID("$FreeBSD$"); + +/* + * glob(3) -- a superset of the one defined in POSIX 1003.2. + * + * The [!...] convention to negate a range is supported (SysV, Posix, ksh). + * + * Optional extra services, controlled by flags not defined by POSIX: + * + * GLOB_QUOTE: + * Escaping convention: \ inhibits any special meaning the following + * character might have (except \ at end of string is retained). + * GLOB_MAGCHAR: + * Set in gl_flags if pattern contained a globbing character. + * GLOB_NOMAGIC: + * Same as GLOB_NOCHECK, but it will only append pattern if it did + * not contain any magic characters. [Used in csh style globbing] + * GLOB_ALTDIRFUNC: + * Use alternately specified directory access functions. + * GLOB_TILDE: + * expand ~user/foo to the /home/dir/of/user/foo + * GLOB_BRACE: + * expand {1,2}{a,b} to 1a 1b 2a 2b + * gl_matchc: + * Number of matches in the current invocation of glob. + */ + +/* + * Some notes on multibyte character support: + * 1. Patterns with illegal byte sequences match nothing - even if + * GLOB_NOCHECK is specified. + * 2. Illegal byte sequences in filenames are handled by treating them as + * single-byte characters with a value of the first byte of the sequence + * cast to wchar_t. + * 3. State-dependent encodings are not currently supported. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DOLLAR '$' +#define DOT '.' +#define EOS '\0' +#define LBRACKET '[' +#define NOT '!' +#define QUESTION '?' +#define QUOTE '\\' +#define RANGE '-' +#define RBRACKET ']' +#define SEP '/' +#define STAR '*' +#define TILDE '~' +#define UNDERSCORE '_' +#define LBRACE '{' +#define RBRACE '}' +#define SLASH '/' +#define COMMA ',' + +#ifndef DEBUG + +#define M_QUOTE 0x8000000000ULL +#define M_PROTECT 0x4000000000ULL +#define M_MASK 0xffffffffffULL +#define M_CHAR 0x00ffffffffULL + +typedef uint_fast64_t Char; + +#else + +#define M_QUOTE 0x80 +#define M_PROTECT 0x40 +#define M_MASK 0xff +#define M_CHAR 0x7f + +typedef char Char; + +#endif + + +#define CHAR(c) ((Char)((c)&M_CHAR)) +#define META(c) ((Char)((c)|M_QUOTE)) +#define M_ALL META('*') +#define M_END META(']') +#define M_NOT META('!') +#define M_ONE META('?') +#define M_RNG META('-') +#define M_SET META('[') +#define ismeta(c) (((c)&M_QUOTE) != 0) + + +static int compare(const void *, const void *); +static int g_Ctoc(const Char *, char *, size_t); +static int g_lstat(Char *, struct stat *, glob_t *); +static DIR *g_opendir(Char *, glob_t *); +static const Char *g_strchr(const Char *, wchar_t); +#ifdef notdef +static Char *g_strcat(Char *, const Char *); +#endif +static int g_stat(Char *, struct stat *, glob_t *); +static int glob0(const Char *, glob_t *, size_t *); +static int glob1(Char *, glob_t *, size_t *); +static int glob2(Char *, Char *, Char *, Char *, glob_t *, size_t *); +static int glob3(Char *, Char *, Char *, Char *, Char *, glob_t *, size_t *); +static int globextend(const Char *, glob_t *, size_t *); +static const Char * + globtilde(const Char *, Char *, size_t, glob_t *); +static int globexp1(const Char *, glob_t *, size_t *); +static int globexp2(const Char *, const Char *, glob_t *, int *, size_t *); +static int match(Char *, Char *, Char *); +#ifdef DEBUG +static void qprintf(const char *, Char *); +#endif + +int +glob(const char *pattern, int flags, int (*errfunc)(const char *, int), glob_t *pglob) +{ + const char *patnext; + size_t limit; + Char *bufnext, *bufend, patbuf[MAXPATHLEN], prot; + mbstate_t mbs; + wchar_t wc; + size_t clen; + + patnext = pattern; + if (!(flags & GLOB_APPEND)) { + pglob->gl_pathc = 0; + pglob->gl_pathv = NULL; + if (!(flags & GLOB_DOOFFS)) + pglob->gl_offs = 0; + } + if (flags & GLOB_LIMIT) { + limit = pglob->gl_matchc; + if (limit == 0) + limit = ARG_MAX; + } else + limit = 0; + pglob->gl_flags = flags & ~GLOB_MAGCHAR; + pglob->gl_errfunc = errfunc; + pglob->gl_matchc = 0; + + bufnext = patbuf; + bufend = bufnext + MAXPATHLEN - 1; + if (flags & GLOB_NOESCAPE) { + memset(&mbs, 0, sizeof(mbs)); + while (bufend - bufnext >= MB_CUR_MAX) { + clen = mbrtowc(&wc, patnext, MB_LEN_MAX, &mbs); + if (clen == (size_t)-1 || clen == (size_t)-2) + return (GLOB_NOMATCH); + else if (clen == 0) + break; + *bufnext++ = wc; + patnext += clen; + } + } else { + /* Protect the quoted characters. */ + memset(&mbs, 0, sizeof(mbs)); + while (bufend - bufnext >= MB_CUR_MAX) { + if (*patnext == QUOTE) { + if (*++patnext == EOS) { + *bufnext++ = QUOTE | M_PROTECT; + continue; + } + prot = M_PROTECT; + } else + prot = 0; + clen = mbrtowc(&wc, patnext, MB_LEN_MAX, &mbs); + if (clen == (size_t)-1 || clen == (size_t)-2) + return (GLOB_NOMATCH); + else if (clen == 0) + break; + *bufnext++ = wc | prot; + patnext += clen; + } + } + *bufnext = EOS; + + if (flags & GLOB_BRACE) + return globexp1(patbuf, pglob, &limit); + else + return glob0(patbuf, pglob, &limit); +} + +/* + * Expand recursively a glob {} pattern. When there is no more expansion + * invoke the standard globbing routine to glob the rest of the magic + * characters + */ +static int +globexp1(const Char *pattern, glob_t *pglob, size_t *limit) +{ + const Char* ptr = pattern; + int rv; + + /* Protect a single {}, for find(1), like csh */ + if (pattern[0] == LBRACE && pattern[1] == RBRACE && pattern[2] == EOS) + return glob0(pattern, pglob, limit); + + while ((ptr = g_strchr(ptr, LBRACE)) != NULL) + if (!globexp2(ptr, pattern, pglob, &rv, limit)) + return rv; + + return glob0(pattern, pglob, limit); +} + + +/* + * Recursive brace globbing helper. Tries to expand a single brace. + * If it succeeds then it invokes globexp1 with the new pattern. + * If it fails then it tries to glob the rest of the pattern and returns. + */ +static int +globexp2(const Char *ptr, const Char *pattern, glob_t *pglob, int *rv, size_t *limit) +{ + int i; + Char *lm, *ls; + const Char *pe, *pm, *pm1, *pl; + Char patbuf[MAXPATHLEN]; + + /* copy part up to the brace */ + for (lm = patbuf, pm = pattern; pm != ptr; *lm++ = *pm++) + continue; + *lm = EOS; + ls = lm; + + /* Find the balanced brace */ + for (i = 0, pe = ++ptr; *pe; pe++) + if (*pe == LBRACKET) { + /* Ignore everything between [] */ + for (pm = pe++; *pe != RBRACKET && *pe != EOS; pe++) + continue; + if (*pe == EOS) { + /* + * We could not find a matching RBRACKET. + * Ignore and just look for RBRACE + */ + pe = pm; + } + } + else if (*pe == LBRACE) + i++; + else if (*pe == RBRACE) { + if (i == 0) + break; + i--; + } + + /* Non matching braces; just glob the pattern */ + if (i != 0 || *pe == EOS) { + *rv = glob0(patbuf, pglob, limit); + return 0; + } + + for (i = 0, pl = pm = ptr; pm <= pe; pm++) + switch (*pm) { + case LBRACKET: + /* Ignore everything between [] */ + for (pm1 = pm++; *pm != RBRACKET && *pm != EOS; pm++) + continue; + if (*pm == EOS) { + /* + * We could not find a matching RBRACKET. + * Ignore and just look for RBRACE + */ + pm = pm1; + } + break; + + case LBRACE: + i++; + break; + + case RBRACE: + if (i) { + i--; + break; + } + /* FALLTHROUGH */ + case COMMA: + if (i && *pm == COMMA) + break; + else { + /* Append the current string */ + for (lm = ls; (pl < pm); *lm++ = *pl++) + continue; + /* + * Append the rest of the pattern after the + * closing brace + */ + for (pl = pe + 1; (*lm++ = *pl++) != EOS;) + continue; + + /* Expand the current pattern */ +#ifdef DEBUG + qprintf("globexp2:", patbuf); +#endif + *rv = globexp1(patbuf, pglob, limit); + + /* move after the comma, to the next string */ + pl = pm + 1; + } + break; + + default: + break; + } + *rv = 0; + return 0; +} + + + +/* + * expand tilde from the passwd file. + */ +static const Char * +globtilde(const Char *pattern, Char *patbuf, size_t patbuf_len, glob_t *pglob) +{ + struct passwd *pwd; + char *h; + const Char *p; + Char *b, *eb; + + if (*pattern != TILDE || !(pglob->gl_flags & GLOB_TILDE)) + return pattern; + + /* + * Copy up to the end of the string or / + */ + eb = &patbuf[patbuf_len - 1]; + for (p = pattern + 1, h = (char *) patbuf; + h < (char *)eb && *p && *p != SLASH; *h++ = *p++) + continue; + + *h = EOS; + + if (((char *) patbuf)[0] == EOS) { + /* + * handle a plain ~ or ~/ by expanding $HOME first (iff + * we're not running setuid or setgid) and then trying + * the password file + */ + if (issetugid() != 0 || + (h = getenv("HOME")) == NULL) { + if (((h = getlogin()) != NULL && + (pwd = getpwnam(h)) != NULL) || + (pwd = getpwuid(getuid())) != NULL) + h = pwd->pw_dir; + else + return pattern; + } + } + else { + /* + * Expand a ~user + */ + if ((pwd = getpwnam((char*) patbuf)) == NULL) + return pattern; + else + h = pwd->pw_dir; + } + + /* Copy the home directory */ + for (b = patbuf; b < eb && *h; *b++ = *h++) + continue; + + /* Append the rest of the pattern */ + while (b < eb && (*b++ = *p++) != EOS) + continue; + *b = EOS; + + return patbuf; +} + + +/* + * The main glob() routine: compiles the pattern (optionally processing + * quotes), calls glob1() to do the real pattern matching, and finally + * sorts the list (unless unsorted operation is requested). Returns 0 + * if things went well, nonzero if errors occurred. + */ +static int +glob0(const Char *pattern, glob_t *pglob, size_t *limit) +{ + const Char *qpatnext; + int err; + size_t oldpathc; + Char *bufnext, c, patbuf[MAXPATHLEN]; + + qpatnext = globtilde(pattern, patbuf, MAXPATHLEN, pglob); + oldpathc = pglob->gl_pathc; + bufnext = patbuf; + + /* We don't need to check for buffer overflow any more. */ + while ((c = *qpatnext++) != EOS) { + switch (c) { + case LBRACKET: + c = *qpatnext; + if (c == NOT) + ++qpatnext; + if (*qpatnext == EOS || + g_strchr(qpatnext+1, RBRACKET) == NULL) { + *bufnext++ = LBRACKET; + if (c == NOT) + --qpatnext; + break; + } + *bufnext++ = M_SET; + if (c == NOT) + *bufnext++ = M_NOT; + c = *qpatnext++; + do { + *bufnext++ = CHAR(c); + if (*qpatnext == RANGE && + (c = qpatnext[1]) != RBRACKET) { + *bufnext++ = M_RNG; + *bufnext++ = CHAR(c); + qpatnext += 2; + } + } while ((c = *qpatnext++) != RBRACKET); + pglob->gl_flags |= GLOB_MAGCHAR; + *bufnext++ = M_END; + break; + case QUESTION: + pglob->gl_flags |= GLOB_MAGCHAR; + *bufnext++ = M_ONE; + break; + case STAR: + pglob->gl_flags |= GLOB_MAGCHAR; + /* collapse adjacent stars to one, + * to avoid exponential behavior + */ + if (bufnext == patbuf || bufnext[-1] != M_ALL) + *bufnext++ = M_ALL; + break; + default: + *bufnext++ = CHAR(c); + break; + } + } + *bufnext = EOS; +#ifdef DEBUG + qprintf("glob0:", patbuf); +#endif + + if ((err = glob1(patbuf, pglob, limit)) != 0) + return(err); + + /* + * If there was no match we are going to append the pattern + * if GLOB_NOCHECK was specified or if GLOB_NOMAGIC was specified + * and the pattern did not contain any magic characters + * GLOB_NOMAGIC is there just for compatibility with csh. + */ + if (pglob->gl_pathc == oldpathc) { + if (((pglob->gl_flags & GLOB_NOCHECK) || + ((pglob->gl_flags & GLOB_NOMAGIC) && + !(pglob->gl_flags & GLOB_MAGCHAR)))) + return(globextend(pattern, pglob, limit)); + else + return(GLOB_NOMATCH); + } + if (!(pglob->gl_flags & GLOB_NOSORT)) + qsort(pglob->gl_pathv + pglob->gl_offs + oldpathc, + pglob->gl_pathc - oldpathc, sizeof(char *), compare); + return(0); +} + +static int +compare(const void *p, const void *q) +{ + return(strcmp(*(char **)p, *(char **)q)); +} + +static int +glob1(Char *pattern, glob_t *pglob, size_t *limit) +{ + Char pathbuf[MAXPATHLEN]; + + /* A null pathname is invalid -- POSIX 1003.1 sect. 2.4. */ + if (*pattern == EOS) + return(0); + return(glob2(pathbuf, pathbuf, pathbuf + MAXPATHLEN - 1, + pattern, pglob, limit)); +} + +/* + * The functions glob2 and glob3 are mutually recursive; there is one level + * of recursion for each segment in the pattern that contains one or more + * meta characters. + */ +static int +glob2(Char *pathbuf, Char *pathend, Char *pathend_last, Char *pattern, + glob_t *pglob, size_t *limit) +{ + struct stat sb; + Char *p, *q; + int anymeta; + + /* + * Loop over pattern segments until end of pattern or until + * segment with meta character found. + */ + for (anymeta = 0;;) { + if (*pattern == EOS) { /* End of pattern? */ + *pathend = EOS; + if (g_lstat(pathbuf, &sb, pglob)) + return(0); + + if (((pglob->gl_flags & GLOB_MARK) && + pathend[-1] != SEP) && (S_ISDIR(sb.st_mode) + || (S_ISLNK(sb.st_mode) && + (g_stat(pathbuf, &sb, pglob) == 0) && + S_ISDIR(sb.st_mode)))) { + if (pathend + 1 > pathend_last) + return (GLOB_ABORTED); + *pathend++ = SEP; + *pathend = EOS; + } + ++pglob->gl_matchc; + return(globextend(pathbuf, pglob, limit)); + } + + /* Find end of next segment, copy tentatively to pathend. */ + q = pathend; + p = pattern; + while (*p != EOS && *p != SEP) { + if (ismeta(*p)) + anymeta = 1; + if (q + 1 > pathend_last) + return (GLOB_ABORTED); + *q++ = *p++; + } + + if (!anymeta) { /* No expansion, do next segment. */ + pathend = q; + pattern = p; + while (*pattern == SEP) { + if (pathend + 1 > pathend_last) + return (GLOB_ABORTED); + *pathend++ = *pattern++; + } + } else /* Need expansion, recurse. */ + return(glob3(pathbuf, pathend, pathend_last, pattern, p, + pglob, limit)); + } + /* NOTREACHED */ +} + +static int +glob3(Char *pathbuf, Char *pathend, Char *pathend_last, + Char *pattern, Char *restpattern, + glob_t *pglob, size_t *limit) +{ + struct dirent *dp; + DIR *dirp; + int err; + char buf[MAXPATHLEN]; + + /* + * The readdirfunc declaration can't be prototyped, because it is + * assigned, below, to two functions which are prototyped in glob.h + * and dirent.h as taking pointers to differently typed opaque + * structures. + */ + struct dirent *(*readdirfunc)(); + + if (pathend > pathend_last) + return (GLOB_ABORTED); + *pathend = EOS; + errno = 0; + + if ((dirp = g_opendir(pathbuf, pglob)) == NULL) { + /* TODO: don't call for ENOENT or ENOTDIR? */ + if (pglob->gl_errfunc) { + if (g_Ctoc(pathbuf, buf, sizeof(buf))) + return (GLOB_ABORTED); + if (pglob->gl_errfunc(buf, errno) || + pglob->gl_flags & GLOB_ERR) + return (GLOB_ABORTED); + } + return(0); + } + + err = 0; + + /* Search directory for matching names. */ + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + readdirfunc = pglob->gl_readdir; + else + readdirfunc = readdir; + while ((dp = (*readdirfunc)(dirp))) { + char *sc; + Char *dc; + wchar_t wc; + size_t clen; + mbstate_t mbs; + + /* Initial DOT must be matched literally. */ + if (dp->d_name[0] == DOT && *pattern != DOT) + continue; + memset(&mbs, 0, sizeof(mbs)); + dc = pathend; + sc = dp->d_name; + while (dc < pathend_last) { + clen = mbrtowc(&wc, sc, MB_LEN_MAX, &mbs); + if (clen == (size_t)-1 || clen == (size_t)-2) { + wc = *sc; + clen = 1; + memset(&mbs, 0, sizeof(mbs)); + } + if ((*dc++ = wc) == EOS) + break; + sc += clen; + } + if (!match(pathend, pattern, restpattern)) { + *pathend = EOS; + continue; + } + err = glob2(pathbuf, --dc, pathend_last, restpattern, + pglob, limit); + if (err) + break; + } + + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + (*pglob->gl_closedir)(dirp); + else + closedir(dirp); + return(err); +} + + +/* + * Extend the gl_pathv member of a glob_t structure to accomodate a new item, + * add the new item, and update gl_pathc. + * + * This assumes the BSD realloc, which only copies the block when its size + * crosses a power-of-two boundary; for v7 realloc, this would cause quadratic + * behavior. + * + * Return 0 if new item added, error code if memory couldn't be allocated. + * + * Invariant of the glob_t structure: + * Either gl_pathc is zero and gl_pathv is NULL; or gl_pathc > 0 and + * gl_pathv points to (gl_offs + gl_pathc + 1) items. + */ +static int +globextend(const Char *path, glob_t *pglob, size_t *limit) +{ + char **pathv; + size_t i, newsize, len; + char *copy; + const Char *p; + + if (*limit && pglob->gl_pathc > *limit) { + errno = 0; + return (GLOB_NOSPACE); + } + + newsize = sizeof(*pathv) * (2 + pglob->gl_pathc + pglob->gl_offs); + pathv = pglob->gl_pathv ? + realloc((char *)pglob->gl_pathv, newsize) : + malloc(newsize); + if (pathv == NULL) { + if (pglob->gl_pathv) { + free(pglob->gl_pathv); + pglob->gl_pathv = NULL; + } + return(GLOB_NOSPACE); + } + + if (pglob->gl_pathv == NULL && pglob->gl_offs > 0) { + /* first time around -- clear initial gl_offs items */ + pathv += pglob->gl_offs; + for (i = pglob->gl_offs + 1; --i > 0; ) + *--pathv = NULL; + } + pglob->gl_pathv = pathv; + + for (p = path; *p++;) + continue; + len = MB_CUR_MAX * (size_t)(p - path); /* XXX overallocation */ + if ((copy = malloc(len)) != NULL) { + if (g_Ctoc(path, copy, len)) { + free(copy); + return (GLOB_NOSPACE); + } + pathv[pglob->gl_offs + pglob->gl_pathc++] = copy; + } + pathv[pglob->gl_offs + pglob->gl_pathc] = NULL; + return(copy == NULL ? GLOB_NOSPACE : 0); +} + +/* + * pattern matching function for filenames. Each occurrence of the * + * pattern causes a recursion level. + */ +static int +match(Char *name, Char *pat, Char *patend) +{ + int ok, negate_range; + Char c, k; + + while (pat < patend) { + c = *pat++; + switch (c & M_MASK) { + case M_ALL: + if (pat == patend) + return(1); + do + if (match(name, pat, patend)) + return(1); + while (*name++ != EOS); + return(0); + case M_ONE: + if (*name++ == EOS) + return(0); + break; + case M_SET: + ok = 0; + if ((k = *name++) == EOS) + return(0); + if ((negate_range = ((*pat & M_MASK) == M_NOT)) != EOS) + ++pat; + while (((c = *pat++) & M_MASK) != M_END) + if ((*pat & M_MASK) == M_RNG) { + if (CHAR(c) <= CHAR(k) && CHAR(k) <= CHAR(pat[1])) ok = 1; + pat += 2; + } else if (c == k) + ok = 1; + if (ok == negate_range) + return(0); + break; + default: + if (*name++ != c) + return(0); + break; + } + } + return(*name == EOS); +} + +/* Free allocated data belonging to a glob_t structure. */ +void +globfree(glob_t *pglob) +{ + size_t i; + char **pp; + + if (pglob->gl_pathv != NULL) { + pp = pglob->gl_pathv + pglob->gl_offs; + for (i = pglob->gl_pathc; i--; ++pp) + if (*pp) + free(*pp); + free(pglob->gl_pathv); + pglob->gl_pathv = NULL; + } +} + +static DIR * +g_opendir(Char *str, glob_t *pglob) +{ + char buf[MAXPATHLEN]; + + if (!*str) + strcpy(buf, "."); + else { + if (g_Ctoc(str, buf, sizeof(buf))) + return (NULL); + } + + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + return((*pglob->gl_opendir)(buf)); + + return(opendir(buf)); +} + +static int +g_lstat(Char *fn, struct stat *sb, glob_t *pglob) +{ + char buf[MAXPATHLEN]; + + if (g_Ctoc(fn, buf, sizeof(buf))) { + errno = ENAMETOOLONG; + return (-1); + } + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + return((*pglob->gl_lstat)(buf, sb)); + return(lstat(buf, sb)); +} + +static int +g_stat(Char *fn, struct stat *sb, glob_t *pglob) +{ + char buf[MAXPATHLEN]; + + if (g_Ctoc(fn, buf, sizeof(buf))) { + errno = ENAMETOOLONG; + return (-1); + } + if (pglob->gl_flags & GLOB_ALTDIRFUNC) + return((*pglob->gl_stat)(buf, sb)); + return(stat(buf, sb)); +} + +static const Char * +g_strchr(const Char *str, wchar_t ch) +{ + + do { + if (*str == ch) + return (str); + } while (*str++); + return (NULL); +} + +static int +g_Ctoc(const Char *str, char *buf, size_t len) +{ + mbstate_t mbs; + size_t clen; + + memset(&mbs, 0, sizeof(mbs)); + while (len >= MB_CUR_MAX) { + clen = wcrtomb(buf, *str, &mbs); + if (clen == (size_t)-1) + return (1); + if (*str == L'\0') + return (0); + str++; + buf += clen; + len -= clen; + } + return (1); +} + +#ifdef DEBUG +static void +qprintf(const char *str, Char *s) +{ + Char *p; + + (void)printf("%s:\n", str); + for (p = s; *p; p++) + (void)printf("%c", CHAR(*p)); + (void)printf("\n"); + for (p = s; *p; p++) + (void)printf("%c", *p & M_PROTECT ? '"' : ' '); + (void)printf("\n"); + for (p = s; *p; p++) + (void)printf("%c", ismeta(*p) ? '_' : ' '); + (void)printf("\n"); +} +#endif diff --git a/glob.h b/glob.h new file mode 100644 index 0000000..351b6c4 --- /dev/null +++ b/glob.h @@ -0,0 +1,105 @@ +/* + * Copyright (c) 1989, 1993 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Guido van Rossum. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * @(#)glob.h 8.1 (Berkeley) 6/2/93 + * $FreeBSD$ + */ + +#ifndef _GLOB_H_ +#define _GLOB_H_ + +#include +#include + +#ifndef _SIZE_T_DECLARED +typedef __size_t size_t; +#define _SIZE_T_DECLARED +#endif + +struct stat; +typedef struct { + size_t gl_pathc; /* Count of total paths so far. */ + size_t gl_matchc; /* Count of paths matching pattern. */ + size_t gl_offs; /* Reserved at beginning of gl_pathv. */ + int gl_flags; /* Copy of flags parameter to glob. */ + char **gl_pathv; /* List of paths matching pattern. */ + /* Copy of errfunc parameter to glob. */ + int (*gl_errfunc)(const char *, int); + + /* + * Alternate filesystem access methods for glob; replacement + * versions of closedir(3), readdir(3), opendir(3), stat(2) + * and lstat(2). + */ + void (*gl_closedir)(void *); + struct dirent *(*gl_readdir)(void *); + void *(*gl_opendir)(const char *); + int (*gl_lstat)(const char *, struct stat *); + int (*gl_stat)(const char *, struct stat *); +} glob_t; + +#if __POSIX_VISIBLE >= 199209 +/* Believed to have been introduced in 1003.2-1992 */ +#define GLOB_APPEND 0x0001 /* Append to output from previous call. */ +#define GLOB_DOOFFS 0x0002 /* Use gl_offs. */ +#define GLOB_ERR 0x0004 /* Return on error. */ +#define GLOB_MARK 0x0008 /* Append / to matching directories. */ +#define GLOB_NOCHECK 0x0010 /* Return pattern itself if nothing matches. */ +#define GLOB_NOSORT 0x0020 /* Don't sort. */ +#define GLOB_NOESCAPE 0x2000 /* Disable backslash escaping. */ + +/* Error values returned by glob(3) */ +#define GLOB_NOSPACE (-1) /* Malloc call failed. */ +#define GLOB_ABORTED (-2) /* Unignored error. */ +#define GLOB_NOMATCH (-3) /* No match and GLOB_NOCHECK was not set. */ +#define GLOB_NOSYS (-4) /* Obsolete: source comptability only. */ +#endif /* __POSIX_VISIBLE >= 199209 */ + +#if __BSD_VISIBLE +#define GLOB_ALTDIRFUNC 0x0040 /* Use alternately specified directory funcs. */ +#define GLOB_BRACE 0x0080 /* Expand braces ala csh. */ +#define GLOB_MAGCHAR 0x0100 /* Pattern had globbing characters. */ +#define GLOB_NOMAGIC 0x0200 /* GLOB_NOCHECK without magic chars (csh). */ +#define GLOB_QUOTE 0x0400 /* Quote special chars with \. */ +#define GLOB_TILDE 0x0800 /* Expand tilde names from the passwd file. */ +#define GLOB_LIMIT 0x1000 /* limit number of returned paths */ + +/* source compatibility, these are the old names */ +#define GLOB_MAXPATH GLOB_LIMIT +#define GLOB_ABEND GLOB_ABORTED +#endif /* __BSD_VISIBLE */ + +__BEGIN_DECLS +int glob(const char *, int, int (*)(const char *, int), glob_t *); +void globfree(glob_t *); +__END_DECLS + +#endif /* !_GLOB_H_ */ -- 1.9.1 ================================================ FILE: pythonforandroid/recipes/libxml2/glob.c ================================================ /* * Natanael Arndt, 2011: removed collate.h dependencies * (my changes are trivial) * * Copyright (c) 1989, 1993 * The Regents of the University of California. All rights reserved. * * This code is derived from software contributed to Berkeley by * Guido van Rossum. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 4. Neither the name of the University nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #if defined(LIBC_SCCS) && !defined(lint) static char sccsid[] = "@(#)glob.c 8.3 (Berkeley) 10/13/93"; #endif /* LIBC_SCCS and not lint */ #include __FBSDID("$FreeBSD$"); /* * glob(3) -- a superset of the one defined in POSIX 1003.2. * * The [!...] convention to negate a range is supported (SysV, Posix, ksh). * * Optional extra services, controlled by flags not defined by POSIX: * * GLOB_QUOTE: * Escaping convention: \ inhibits any special meaning the following * character might have (except \ at end of string is retained). * GLOB_MAGCHAR: * Set in gl_flags if pattern contained a globbing character. * GLOB_NOMAGIC: * Same as GLOB_NOCHECK, but it will only append pattern if it did * not contain any magic characters. [Used in csh style globbing] * GLOB_ALTDIRFUNC: * Use alternately specified directory access functions. * GLOB_TILDE: * expand ~user/foo to the /home/dir/of/user/foo * GLOB_BRACE: * expand {1,2}{a,b} to 1a 1b 2a 2b * gl_matchc: * Number of matches in the current invocation of glob. */ /* * Some notes on multibyte character support: * 1. Patterns with illegal byte sequences match nothing - even if * GLOB_NOCHECK is specified. * 2. Illegal byte sequences in filenames are handled by treating them as * single-byte characters with a value of the first byte of the sequence * cast to wchar_t. * 3. State-dependent encodings are not currently supported. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define DOLLAR '$' #define DOT '.' #define EOS '\0' #define LBRACKET '[' #define NOT '!' #define QUESTION '?' #define QUOTE '\\' #define RANGE '-' #define RBRACKET ']' #define SEP '/' #define STAR '*' #define TILDE '~' #define UNDERSCORE '_' #define LBRACE '{' #define RBRACE '}' #define SLASH '/' #define COMMA ',' #ifndef DEBUG #define M_QUOTE 0x8000000000ULL #define M_PROTECT 0x4000000000ULL #define M_MASK 0xffffffffffULL #define M_CHAR 0x00ffffffffULL typedef uint_fast64_t Char; #else #define M_QUOTE 0x80 #define M_PROTECT 0x40 #define M_MASK 0xff #define M_CHAR 0x7f typedef char Char; #endif #define CHAR(c) ((Char)((c)&M_CHAR)) #define META(c) ((Char)((c)|M_QUOTE)) #define M_ALL META('*') #define M_END META(']') #define M_NOT META('!') #define M_ONE META('?') #define M_RNG META('-') #define M_SET META('[') #define ismeta(c) (((c)&M_QUOTE) != 0) static int compare(const void *, const void *); static int g_Ctoc(const Char *, char *, size_t); static int g_lstat(Char *, struct stat *, glob_t *); static DIR *g_opendir(Char *, glob_t *); static const Char *g_strchr(const Char *, wchar_t); #ifdef notdef static Char *g_strcat(Char *, const Char *); #endif static int g_stat(Char *, struct stat *, glob_t *); static int glob0(const Char *, glob_t *, size_t *); static int glob1(Char *, glob_t *, size_t *); static int glob2(Char *, Char *, Char *, Char *, glob_t *, size_t *); static int glob3(Char *, Char *, Char *, Char *, Char *, glob_t *, size_t *); static int globextend(const Char *, glob_t *, size_t *); static const Char * globtilde(const Char *, Char *, size_t, glob_t *); static int globexp1(const Char *, glob_t *, size_t *); static int globexp2(const Char *, const Char *, glob_t *, int *, size_t *); static int match(Char *, Char *, Char *); #ifdef DEBUG static void qprintf(const char *, Char *); #endif int glob(const char *pattern, int flags, int (*errfunc)(const char *, int), glob_t *pglob) { const char *patnext; size_t limit; Char *bufnext, *bufend, patbuf[MAXPATHLEN], prot; mbstate_t mbs; wchar_t wc; size_t clen; patnext = pattern; if (!(flags & GLOB_APPEND)) { pglob->gl_pathc = 0; pglob->gl_pathv = NULL; if (!(flags & GLOB_DOOFFS)) pglob->gl_offs = 0; } if (flags & GLOB_LIMIT) { limit = pglob->gl_matchc; if (limit == 0) limit = ARG_MAX; } else limit = 0; pglob->gl_flags = flags & ~GLOB_MAGCHAR; pglob->gl_errfunc = errfunc; pglob->gl_matchc = 0; bufnext = patbuf; bufend = bufnext + MAXPATHLEN - 1; if (flags & GLOB_NOESCAPE) { memset(&mbs, 0, sizeof(mbs)); while (bufend - bufnext >= MB_CUR_MAX) { clen = mbrtowc(&wc, patnext, MB_LEN_MAX, &mbs); if (clen == (size_t)-1 || clen == (size_t)-2) return (GLOB_NOMATCH); else if (clen == 0) break; *bufnext++ = wc; patnext += clen; } } else { /* Protect the quoted characters. */ memset(&mbs, 0, sizeof(mbs)); while (bufend - bufnext >= MB_CUR_MAX) { if (*patnext == QUOTE) { if (*++patnext == EOS) { *bufnext++ = QUOTE | M_PROTECT; continue; } prot = M_PROTECT; } else prot = 0; clen = mbrtowc(&wc, patnext, MB_LEN_MAX, &mbs); if (clen == (size_t)-1 || clen == (size_t)-2) return (GLOB_NOMATCH); else if (clen == 0) break; *bufnext++ = wc | prot; patnext += clen; } } *bufnext = EOS; if (flags & GLOB_BRACE) return globexp1(patbuf, pglob, &limit); else return glob0(patbuf, pglob, &limit); } /* * Expand recursively a glob {} pattern. When there is no more expansion * invoke the standard globbing routine to glob the rest of the magic * characters */ static int globexp1(const Char *pattern, glob_t *pglob, size_t *limit) { const Char* ptr = pattern; int rv; /* Protect a single {}, for find(1), like csh */ if (pattern[0] == LBRACE && pattern[1] == RBRACE && pattern[2] == EOS) return glob0(pattern, pglob, limit); while ((ptr = g_strchr(ptr, LBRACE)) != NULL) if (!globexp2(ptr, pattern, pglob, &rv, limit)) return rv; return glob0(pattern, pglob, limit); } /* * Recursive brace globbing helper. Tries to expand a single brace. * If it succeeds then it invokes globexp1 with the new pattern. * If it fails then it tries to glob the rest of the pattern and returns. */ static int globexp2(const Char *ptr, const Char *pattern, glob_t *pglob, int *rv, size_t *limit) { int i; Char *lm, *ls; const Char *pe, *pm, *pm1, *pl; Char patbuf[MAXPATHLEN]; /* copy part up to the brace */ for (lm = patbuf, pm = pattern; pm != ptr; *lm++ = *pm++) continue; *lm = EOS; ls = lm; /* Find the balanced brace */ for (i = 0, pe = ++ptr; *pe; pe++) if (*pe == LBRACKET) { /* Ignore everything between [] */ for (pm = pe++; *pe != RBRACKET && *pe != EOS; pe++) continue; if (*pe == EOS) { /* * We could not find a matching RBRACKET. * Ignore and just look for RBRACE */ pe = pm; } } else if (*pe == LBRACE) i++; else if (*pe == RBRACE) { if (i == 0) break; i--; } /* Non matching braces; just glob the pattern */ if (i != 0 || *pe == EOS) { *rv = glob0(patbuf, pglob, limit); return 0; } for (i = 0, pl = pm = ptr; pm <= pe; pm++) switch (*pm) { case LBRACKET: /* Ignore everything between [] */ for (pm1 = pm++; *pm != RBRACKET && *pm != EOS; pm++) continue; if (*pm == EOS) { /* * We could not find a matching RBRACKET. * Ignore and just look for RBRACE */ pm = pm1; } break; case LBRACE: i++; break; case RBRACE: if (i) { i--; break; } /* FALLTHROUGH */ case COMMA: if (i && *pm == COMMA) break; else { /* Append the current string */ for (lm = ls; (pl < pm); *lm++ = *pl++) continue; /* * Append the rest of the pattern after the * closing brace */ for (pl = pe + 1; (*lm++ = *pl++) != EOS;) continue; /* Expand the current pattern */ #ifdef DEBUG qprintf("globexp2:", patbuf); #endif *rv = globexp1(patbuf, pglob, limit); /* move after the comma, to the next string */ pl = pm + 1; } break; default: break; } *rv = 0; return 0; } /* * expand tilde from the passwd file. */ static const Char * globtilde(const Char *pattern, Char *patbuf, size_t patbuf_len, glob_t *pglob) { struct passwd *pwd; char *h; const Char *p; Char *b, *eb; if (*pattern != TILDE || !(pglob->gl_flags & GLOB_TILDE)) return pattern; /* * Copy up to the end of the string or / */ eb = &patbuf[patbuf_len - 1]; for (p = pattern + 1, h = (char *) patbuf; h < (char *)eb && *p && *p != SLASH; *h++ = *p++) continue; *h = EOS; if (((char *) patbuf)[0] == EOS) { /* * handle a plain ~ or ~/ by expanding $HOME first (iff * we're not running setuid or setgid) and then trying * the password file */ if (issetugid() != 0 || (h = getenv("HOME")) == NULL) { if (((h = getlogin()) != NULL && (pwd = getpwnam(h)) != NULL) || (pwd = getpwuid(getuid())) != NULL) h = pwd->pw_dir; else return pattern; } } else { /* * Expand a ~user */ if ((pwd = getpwnam((char*) patbuf)) == NULL) return pattern; else h = pwd->pw_dir; } /* Copy the home directory */ for (b = patbuf; b < eb && *h; *b++ = *h++) continue; /* Append the rest of the pattern */ while (b < eb && (*b++ = *p++) != EOS) continue; *b = EOS; return patbuf; } /* * The main glob() routine: compiles the pattern (optionally processing * quotes), calls glob1() to do the real pattern matching, and finally * sorts the list (unless unsorted operation is requested). Returns 0 * if things went well, nonzero if errors occurred. */ static int glob0(const Char *pattern, glob_t *pglob, size_t *limit) { const Char *qpatnext; int err; size_t oldpathc; Char *bufnext, c, patbuf[MAXPATHLEN]; qpatnext = globtilde(pattern, patbuf, MAXPATHLEN, pglob); oldpathc = pglob->gl_pathc; bufnext = patbuf; /* We don't need to check for buffer overflow any more. */ while ((c = *qpatnext++) != EOS) { switch (c) { case LBRACKET: c = *qpatnext; if (c == NOT) ++qpatnext; if (*qpatnext == EOS || g_strchr(qpatnext+1, RBRACKET) == NULL) { *bufnext++ = LBRACKET; if (c == NOT) --qpatnext; break; } *bufnext++ = M_SET; if (c == NOT) *bufnext++ = M_NOT; c = *qpatnext++; do { *bufnext++ = CHAR(c); if (*qpatnext == RANGE && (c = qpatnext[1]) != RBRACKET) { *bufnext++ = M_RNG; *bufnext++ = CHAR(c); qpatnext += 2; } } while ((c = *qpatnext++) != RBRACKET); pglob->gl_flags |= GLOB_MAGCHAR; *bufnext++ = M_END; break; case QUESTION: pglob->gl_flags |= GLOB_MAGCHAR; *bufnext++ = M_ONE; break; case STAR: pglob->gl_flags |= GLOB_MAGCHAR; /* collapse adjacent stars to one, * to avoid exponential behavior */ if (bufnext == patbuf || bufnext[-1] != M_ALL) *bufnext++ = M_ALL; break; default: *bufnext++ = CHAR(c); break; } } *bufnext = EOS; #ifdef DEBUG qprintf("glob0:", patbuf); #endif if ((err = glob1(patbuf, pglob, limit)) != 0) return(err); /* * If there was no match we are going to append the pattern * if GLOB_NOCHECK was specified or if GLOB_NOMAGIC was specified * and the pattern did not contain any magic characters * GLOB_NOMAGIC is there just for compatibility with csh. */ if (pglob->gl_pathc == oldpathc) { if (((pglob->gl_flags & GLOB_NOCHECK) || ((pglob->gl_flags & GLOB_NOMAGIC) && !(pglob->gl_flags & GLOB_MAGCHAR)))) return(globextend(pattern, pglob, limit)); else return(GLOB_NOMATCH); } if (!(pglob->gl_flags & GLOB_NOSORT)) qsort(pglob->gl_pathv + pglob->gl_offs + oldpathc, pglob->gl_pathc - oldpathc, sizeof(char *), compare); return(0); } static int compare(const void *p, const void *q) { return(strcmp(*(char **)p, *(char **)q)); } static int glob1(Char *pattern, glob_t *pglob, size_t *limit) { Char pathbuf[MAXPATHLEN]; /* A null pathname is invalid -- POSIX 1003.1 sect. 2.4. */ if (*pattern == EOS) return(0); return(glob2(pathbuf, pathbuf, pathbuf + MAXPATHLEN - 1, pattern, pglob, limit)); } /* * The functions glob2 and glob3 are mutually recursive; there is one level * of recursion for each segment in the pattern that contains one or more * meta characters. */ static int glob2(Char *pathbuf, Char *pathend, Char *pathend_last, Char *pattern, glob_t *pglob, size_t *limit) { struct stat sb; Char *p, *q; int anymeta; /* * Loop over pattern segments until end of pattern or until * segment with meta character found. */ for (anymeta = 0;;) { if (*pattern == EOS) { /* End of pattern? */ *pathend = EOS; if (g_lstat(pathbuf, &sb, pglob)) return(0); if (((pglob->gl_flags & GLOB_MARK) && pathend[-1] != SEP) && (S_ISDIR(sb.st_mode) || (S_ISLNK(sb.st_mode) && (g_stat(pathbuf, &sb, pglob) == 0) && S_ISDIR(sb.st_mode)))) { if (pathend + 1 > pathend_last) return (GLOB_ABORTED); *pathend++ = SEP; *pathend = EOS; } ++pglob->gl_matchc; return(globextend(pathbuf, pglob, limit)); } /* Find end of next segment, copy tentatively to pathend. */ q = pathend; p = pattern; while (*p != EOS && *p != SEP) { if (ismeta(*p)) anymeta = 1; if (q + 1 > pathend_last) return (GLOB_ABORTED); *q++ = *p++; } if (!anymeta) { /* No expansion, do next segment. */ pathend = q; pattern = p; while (*pattern == SEP) { if (pathend + 1 > pathend_last) return (GLOB_ABORTED); *pathend++ = *pattern++; } } else /* Need expansion, recurse. */ return(glob3(pathbuf, pathend, pathend_last, pattern, p, pglob, limit)); } /* NOTREACHED */ } static int glob3(Char *pathbuf, Char *pathend, Char *pathend_last, Char *pattern, Char *restpattern, glob_t *pglob, size_t *limit) { struct dirent *dp; DIR *dirp; int err; char buf[MAXPATHLEN]; /* * The readdirfunc declaration can't be prototyped, because it is * assigned, below, to two functions which are prototyped in glob.h * and dirent.h as taking pointers to differently typed opaque * structures. */ struct dirent *(*readdirfunc)(); if (pathend > pathend_last) return (GLOB_ABORTED); *pathend = EOS; errno = 0; if ((dirp = g_opendir(pathbuf, pglob)) == NULL) { /* TODO: don't call for ENOENT or ENOTDIR? */ if (pglob->gl_errfunc) { if (g_Ctoc(pathbuf, buf, sizeof(buf))) return (GLOB_ABORTED); if (pglob->gl_errfunc(buf, errno) || pglob->gl_flags & GLOB_ERR) return (GLOB_ABORTED); } return(0); } err = 0; /* Search directory for matching names. */ if (pglob->gl_flags & GLOB_ALTDIRFUNC) readdirfunc = pglob->gl_readdir; else readdirfunc = readdir; while ((dp = (*readdirfunc)(dirp))) { char *sc; Char *dc; wchar_t wc; size_t clen; mbstate_t mbs; /* Initial DOT must be matched literally. */ if (dp->d_name[0] == DOT && *pattern != DOT) continue; memset(&mbs, 0, sizeof(mbs)); dc = pathend; sc = dp->d_name; while (dc < pathend_last) { clen = mbrtowc(&wc, sc, MB_LEN_MAX, &mbs); if (clen == (size_t)-1 || clen == (size_t)-2) { wc = *sc; clen = 1; memset(&mbs, 0, sizeof(mbs)); } if ((*dc++ = wc) == EOS) break; sc += clen; } if (!match(pathend, pattern, restpattern)) { *pathend = EOS; continue; } err = glob2(pathbuf, --dc, pathend_last, restpattern, pglob, limit); if (err) break; } if (pglob->gl_flags & GLOB_ALTDIRFUNC) (*pglob->gl_closedir)(dirp); else closedir(dirp); return(err); } /* * Extend the gl_pathv member of a glob_t structure to accommodate a new item, * add the new item, and update gl_pathc. * * This assumes the BSD realloc, which only copies the block when its size * crosses a power-of-two boundary; for v7 realloc, this would cause quadratic * behavior. * * Return 0 if new item added, error code if memory couldn't be allocated. * * Invariant of the glob_t structure: * Either gl_pathc is zero and gl_pathv is NULL; or gl_pathc > 0 and * gl_pathv points to (gl_offs + gl_pathc + 1) items. */ static int globextend(const Char *path, glob_t *pglob, size_t *limit) { char **pathv; size_t i, newsize, len; char *copy; const Char *p; if (*limit && pglob->gl_pathc > *limit) { errno = 0; return (GLOB_NOSPACE); } newsize = sizeof(*pathv) * (2 + pglob->gl_pathc + pglob->gl_offs); pathv = pglob->gl_pathv ? realloc((char *)pglob->gl_pathv, newsize) : malloc(newsize); if (pathv == NULL) { if (pglob->gl_pathv) { free(pglob->gl_pathv); pglob->gl_pathv = NULL; } return(GLOB_NOSPACE); } if (pglob->gl_pathv == NULL && pglob->gl_offs > 0) { /* first time around -- clear initial gl_offs items */ pathv += pglob->gl_offs; for (i = pglob->gl_offs + 1; --i > 0; ) *--pathv = NULL; } pglob->gl_pathv = pathv; for (p = path; *p++;) continue; len = MB_CUR_MAX * (size_t)(p - path); /* XXX overallocation */ if ((copy = malloc(len)) != NULL) { if (g_Ctoc(path, copy, len)) { free(copy); return (GLOB_NOSPACE); } pathv[pglob->gl_offs + pglob->gl_pathc++] = copy; } pathv[pglob->gl_offs + pglob->gl_pathc] = NULL; return(copy == NULL ? GLOB_NOSPACE : 0); } /* * pattern matching function for filenames. Each occurrence of the * * pattern causes a recursion level. */ static int match(Char *name, Char *pat, Char *patend) { int ok, negate_range; Char c, k; while (pat < patend) { c = *pat++; switch (c & M_MASK) { case M_ALL: if (pat == patend) return(1); do if (match(name, pat, patend)) return(1); while (*name++ != EOS); return(0); case M_ONE: if (*name++ == EOS) return(0); break; case M_SET: ok = 0; if ((k = *name++) == EOS) return(0); if ((negate_range = ((*pat & M_MASK) == M_NOT)) != EOS) ++pat; while (((c = *pat++) & M_MASK) != M_END) if ((*pat & M_MASK) == M_RNG) { if (CHAR(c) <= CHAR(k) && CHAR(k) <= CHAR(pat[1])) ok = 1; pat += 2; } else if (c == k) ok = 1; if (ok == negate_range) return(0); break; default: if (*name++ != c) return(0); break; } } return(*name == EOS); } /* Free allocated data belonging to a glob_t structure. */ void globfree(glob_t *pglob) { size_t i; char **pp; if (pglob->gl_pathv != NULL) { pp = pglob->gl_pathv + pglob->gl_offs; for (i = pglob->gl_pathc; i--; ++pp) if (*pp) free(*pp); free(pglob->gl_pathv); pglob->gl_pathv = NULL; } } static DIR * g_opendir(Char *str, glob_t *pglob) { char buf[MAXPATHLEN]; if (!*str) strcpy(buf, "."); else { if (g_Ctoc(str, buf, sizeof(buf))) return (NULL); } if (pglob->gl_flags & GLOB_ALTDIRFUNC) return((*pglob->gl_opendir)(buf)); return(opendir(buf)); } static int g_lstat(Char *fn, struct stat *sb, glob_t *pglob) { char buf[MAXPATHLEN]; if (g_Ctoc(fn, buf, sizeof(buf))) { errno = ENAMETOOLONG; return (-1); } if (pglob->gl_flags & GLOB_ALTDIRFUNC) return((*pglob->gl_lstat)(buf, sb)); return(lstat(buf, sb)); } static int g_stat(Char *fn, struct stat *sb, glob_t *pglob) { char buf[MAXPATHLEN]; if (g_Ctoc(fn, buf, sizeof(buf))) { errno = ENAMETOOLONG; return (-1); } if (pglob->gl_flags & GLOB_ALTDIRFUNC) return((*pglob->gl_stat)(buf, sb)); return(stat(buf, sb)); } static const Char * g_strchr(const Char *str, wchar_t ch) { do { if (*str == ch) return (str); } while (*str++); return (NULL); } static int g_Ctoc(const Char *str, char *buf, size_t len) { mbstate_t mbs; size_t clen; memset(&mbs, 0, sizeof(mbs)); while (len >= MB_CUR_MAX) { clen = wcrtomb(buf, *str, &mbs); if (clen == (size_t)-1) return (1); if (*str == L'\0') return (0); str++; buf += clen; len -= clen; } return (1); } #ifdef DEBUG static void qprintf(const char *str, Char *s) { Char *p; (void)printf("%s:\n", str); for (p = s; *p; p++) (void)printf("%c", CHAR(*p)); (void)printf("\n"); for (p = s; *p; p++) (void)printf("%c", *p & M_PROTECT ? '"' : ' '); (void)printf("\n"); for (p = s; *p; p++) (void)printf("%c", ismeta(*p) ? '_' : ' '); (void)printf("\n"); } #endif ================================================ FILE: pythonforandroid/recipes/libxml2/glob.h ================================================ /* * Copyright (c) 1989, 1993 * The Regents of the University of California. All rights reserved. * * This code is derived from software contributed to Berkeley by * Guido van Rossum. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of the University nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * @(#)glob.h 8.1 (Berkeley) 6/2/93 * $FreeBSD$ */ #ifndef _GLOB_H_ #define _GLOB_H_ #include #include #ifndef _SIZE_T_DECLARED typedef __size_t size_t; #define _SIZE_T_DECLARED #endif struct stat; typedef struct { size_t gl_pathc; /* Count of total paths so far. */ size_t gl_matchc; /* Count of paths matching pattern. */ size_t gl_offs; /* Reserved at beginning of gl_pathv. */ int gl_flags; /* Copy of flags parameter to glob. */ char **gl_pathv; /* List of paths matching pattern. */ /* Copy of errfunc parameter to glob. */ int (*gl_errfunc)(const char *, int); /* * Alternate filesystem access methods for glob; replacement * versions of closedir(3), readdir(3), opendir(3), stat(2) * and lstat(2). */ void (*gl_closedir)(void *); struct dirent *(*gl_readdir)(void *); void *(*gl_opendir)(const char *); int (*gl_lstat)(const char *, struct stat *); int (*gl_stat)(const char *, struct stat *); } glob_t; #if __POSIX_VISIBLE >= 199209 /* Believed to have been introduced in 1003.2-1992 */ #define GLOB_APPEND 0x0001 /* Append to output from previous call. */ #define GLOB_DOOFFS 0x0002 /* Use gl_offs. */ #define GLOB_ERR 0x0004 /* Return on error. */ #define GLOB_MARK 0x0008 /* Append / to matching directories. */ #define GLOB_NOCHECK 0x0010 /* Return pattern itself if nothing matches. */ #define GLOB_NOSORT 0x0020 /* Don't sort. */ #define GLOB_NOESCAPE 0x2000 /* Disable backslash escaping. */ /* Error values returned by glob(3) */ #define GLOB_NOSPACE (-1) /* Malloc call failed. */ #define GLOB_ABORTED (-2) /* Unignored error. */ #define GLOB_NOMATCH (-3) /* No match and GLOB_NOCHECK was not set. */ #define GLOB_NOSYS (-4) /* Obsolete: source compatibility only. */ #endif /* __POSIX_VISIBLE >= 199209 */ #if __BSD_VISIBLE #define GLOB_ALTDIRFUNC 0x0040 /* Use alternately specified directory funcs. */ #define GLOB_BRACE 0x0080 /* Expand braces ala csh. */ #define GLOB_MAGCHAR 0x0100 /* Pattern had globbing characters. */ #define GLOB_NOMAGIC 0x0200 /* GLOB_NOCHECK without magic chars (csh). */ #define GLOB_QUOTE 0x0400 /* Quote special chars with \. */ #define GLOB_TILDE 0x0800 /* Expand tilde names from the passwd file. */ #define GLOB_LIMIT 0x1000 /* limit number of returned paths */ /* source compatibility, these are the old names */ #define GLOB_MAXPATH GLOB_LIMIT #define GLOB_ABEND GLOB_ABORTED #endif /* __BSD_VISIBLE */ __BEGIN_DECLS int glob(const char *, int, int (*)(const char *, int), glob_t *); void globfree(glob_t *); __END_DECLS #endif /* !_GLOB_H_ */ ================================================ FILE: pythonforandroid/recipes/libxslt/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.util import current_directory from pythonforandroid.logger import shprint from os.path import exists, join import sh class LibxsltRecipe(Recipe): version = '1.1.34' url = 'http://xmlsoft.org/sources/libxslt-{version}.tar.gz' depends = ['libxml2'] patches = ['fix-dlopen.patch'] built_libraries = { 'libxslt.a': 'libxslt/.libs', 'libexslt.a': 'libexslt/.libs' } call_hostpython_via_targetpython = False def build_arch(self, arch): env = self.get_recipe_env(arch) build_dir = self.get_build_dir(arch.arch) with current_directory(build_dir): # If the build is done with /bin/sh things blow up, # try really hard to use bash libxml2_recipe = Recipe.get_recipe('libxml2', self.ctx) libxml2_build_dir = libxml2_recipe.get_build_dir(arch.arch) build_arch = shprint(sh.gcc, '-dumpmachine').stdout.decode( 'utf-8').split('\n')[0] if not exists('configure'): shprint(sh.Command('./autogen.sh'), _env=env) shprint(sh.Command('autoreconf'), '-vif', _env=env) shprint(sh.Command('./configure'), '--build=' + build_arch, '--host=' + arch.command_prefix, '--target=' + arch.command_prefix, '--without-plugins', '--without-debug', '--without-python', '--without-crypto', '--with-libxml-src=' + libxml2_build_dir, '--disable-shared', _env=env) shprint(sh.make, "V=1", _env=env) shprint(sh.Command('chmod'), '+x', 'xslt-config') def get_recipe_env(self, arch): env = super().get_recipe_env(arch) env['CONFIG_SHELL'] = '/bin/bash' env['SHELL'] = '/bin/bash' libxml2_recipe = Recipe.get_recipe('libxml2', self.ctx) libxml2_build_dir = libxml2_recipe.get_build_dir(arch.arch) libxml2_libs_dir = join(libxml2_build_dir, '.libs') env['CFLAGS'] = ' '.join([ env['CFLAGS'], '-I' + libxml2_build_dir, '-I' + join(libxml2_build_dir, 'include', 'libxml'), '-I' + self.get_build_dir(arch.arch), ]) env['LDFLAGS'] += ' -L' + libxml2_libs_dir env['LIBS'] = '-lxml2 -lz -lm' return env recipe = LibxsltRecipe() ================================================ FILE: pythonforandroid/recipes/libxslt/fix-dlopen.patch ================================================ --- libxslt-1.1.27.orig/python/libxsl.py 2012-09-04 16:26:23.000000000 +0200 +++ libxslt-1.1.27/python/libxsl.py 2013-07-29 15:11:04.182227378 +0200 @@ -4,7 +4,7 @@ # loader to work in that mode if feasible # import sys -if not hasattr(sys,'getdlopenflags'): +if True: import libxml2mod import libxsltmod import libxml2 ================================================ FILE: pythonforandroid/recipes/libzbar/__init__.py ================================================ import os from pythonforandroid.recipe import Recipe from pythonforandroid.util import current_directory from pythonforandroid.logger import shprint from multiprocessing import cpu_count import sh class LibZBarRecipe(Recipe): version = '0.10' url = 'https://github.com/ZBar/ZBar/archive/{version}.zip' depends = ['libiconv'] patches = ["werror.patch"] built_libraries = {'libzbar.so': 'zbar/.libs'} def get_recipe_env(self, arch=None, with_flags_in_cc=True): 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') env['LIBS'] = env.get('LIBS', '') + ' -landroid -liconv' return env def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): shprint(sh.Command('autoreconf'), '-vif', _env=env) shprint( sh.Command('./configure'), '--host=' + arch.command_prefix, '--target=' + arch.command_prefix, '--prefix=' + self.ctx.get_python_install_dir(arch.arch), # Python bindings are compiled in a separated recipe '--with-python=no', '--with-gtk=no', '--with-qt=no', '--with-x=no', '--with-jpeg=no', '--with-imagemagick=no', '--enable-pthread=no', '--enable-video=no', '--enable-shared=yes', '--enable-static=no', _env=env) shprint(sh.make, '-j' + str(cpu_count()), _env=env) recipe = LibZBarRecipe() ================================================ FILE: pythonforandroid/recipes/libzbar/werror.patch ================================================ diff --git a/configure.ac b/configure.ac index 256aedb..727caba 100644 --- a/configure.ac +++ b/configure.ac @@ -3,7 +3,7 @@ AC_PREREQ([2.61]) AC_INIT([zbar], [0.10], [spadix@users.sourceforge.net]) AC_CONFIG_AUX_DIR(config) AC_CONFIG_MACRO_DIR(config) -AM_INIT_AUTOMAKE([1.10 -Wall -Werror foreign subdir-objects std-options dist-bzip2]) +AM_INIT_AUTOMAKE([1.10 -Wall foreign subdir-objects std-options dist-bzip2]) AC_CONFIG_HEADERS([include/config.h]) AC_CONFIG_SRCDIR(zbar/scanner.c) LT_PREREQ([2.2]) ================================================ FILE: pythonforandroid/recipes/libzmq/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.logger import shprint from pythonforandroid.util import current_directory from os.path import join import sh class LibZMQRecipe(Recipe): version = '4.3.4' url = 'https://github.com/zeromq/libzmq/releases/download/v{version}/zeromq-{version}.zip' depends = [] built_libraries = {'libzmq.so': 'src/.libs'} need_stl_shared = True def build_arch(self, arch): env = self.get_recipe_env(arch) # # libsodium_recipe = Recipe.get_recipe('libsodium', self.ctx) # libsodium_dir = libsodium_recipe.get_build_dir(arch.arch) # env['sodium_CFLAGS'] = '-I{}'.format(join( # libsodium_dir, 'src')) # env['sodium_LDLAGS'] = '-L{}'.format(join( # libsodium_dir, 'src', 'libsodium', '.libs')) curdir = self.get_build_dir(arch.arch) prefix = join(curdir, "install") with current_directory(curdir): bash = sh.Command('sh') shprint( bash, './configure', '--host={}'.format(arch.command_prefix), '--without-documentation', '--prefix={}'.format(prefix), '--with-libsodium=no', '--disable-libunwind', '--disable-Werror', _env=env) shprint(sh.make, _env=env) shprint(sh.make, 'install', _env=env) recipe = LibZMQRecipe() ================================================ FILE: pythonforandroid/recipes/lxml/__init__.py ================================================ from pythonforandroid.recipe import Recipe, CompiledComponentsPythonRecipe from os.path import exists, join from os import uname class LXMLRecipe(CompiledComponentsPythonRecipe): version = '4.8.0' url = 'https://pypi.python.org/packages/source/l/lxml/lxml-{version}.tar.gz' # noqa depends = ['librt', 'libxml2', 'libxslt', 'setuptools'] name = 'lxml' call_hostpython_via_targetpython = False # Due to setuptools def should_build(self, arch): super().should_build(arch) py_ver = self.ctx.python_recipe.major_minor_version_string build_platform = "{system}-{machine}".format( system=uname()[0], machine=uname()[-1] ).lower() build_dir = join( self.get_build_dir(arch.arch), "build", "lib." + build_platform + "-" + py_ver, "lxml", ) py_libs = ["_elementpath.so", "builder.so", "etree.so", "objectify.so"] return not all([exists(join(build_dir, lib)) for lib in py_libs]) def get_recipe_env(self, arch): env = super().get_recipe_env(arch) # libxslt flags libxslt_recipe = Recipe.get_recipe('libxslt', self.ctx) libxslt_build_dir = libxslt_recipe.get_build_dir(arch.arch) # libxml2 flags libxml2_recipe = Recipe.get_recipe('libxml2', self.ctx) libxml2_build_dir = libxml2_recipe.get_build_dir(arch.arch) env["STATIC"] = "true" env["LXML_STATIC_INCLUDE_DIRS"] = "{}:{}".format( join(libxml2_build_dir, "include"), join(libxslt_build_dir) ) env["LXML_STATIC_LIBRARY_DIRS"] = "{}:{}:{}".format( join(libxml2_build_dir, ".libs"), join(libxslt_build_dir, "libxslt", ".libs"), join(libxslt_build_dir, "libexslt", ".libs"), ) env["WITH_XML2_CONFIG"] = join(libxml2_build_dir, "xml2-config") env["WITH_XSLT_CONFIG"] = join(libxslt_build_dir, "xslt-config") env["LXML_STATIC_BINARIES"] = "{}:{}:{}".format( join(libxml2_build_dir, ".libs", "libxml2.a"), join(libxslt_build_dir, "libxslt", ".libs", "libxslt.a"), join(libxslt_build_dir, "libexslt", ".libs", "libexslt.a"), ) return env recipe = LXMLRecipe() ================================================ FILE: pythonforandroid/recipes/m2crypto/__init__.py ================================================ from pythonforandroid.recipe import CompiledComponentsPythonRecipe from pythonforandroid.toolchain import current_directory from pythonforandroid.logger import shprint, info import glob import sh class M2CryptoRecipe(CompiledComponentsPythonRecipe): version = '0.30.1' url = 'https://pypi.python.org/packages/source/M/M2Crypto/M2Crypto-{version}.tar.gz' depends = ['openssl', 'setuptools'] site_packages_name = 'M2Crypto' call_hostpython_via_targetpython = False def build_compiled_components(self, arch): info('Building compiled components in {}'.format(self.name)) env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): # Build M2Crypto hostpython = sh.Command(self.hostpython_location) if self.install_in_hostpython: shprint(hostpython, 'setup.py', 'clean', '--all', _env=env) shprint(hostpython, 'setup.py', self.build_cmd, '-p' + arch.arch, '-c' + 'unix', '-o' + env['OPENSSL_BUILD_PATH'], '-L' + env['OPENSSL_BUILD_PATH'], _env=env, *self.setup_extra_args) build_dir = glob.glob('build/lib.*')[0] shprint(sh.find, build_dir, '-name', '"*.o"', '-exec', env['STRIP'], '{}', ';', _env=env) def get_recipe_env(self, arch): env = super().get_recipe_env(arch) env['OPENSSL_BUILD_PATH'] = self.get_recipe('openssl', self.ctx).get_build_dir(arch.arch) return env recipe = M2CryptoRecipe() ================================================ FILE: pythonforandroid/recipes/materialyoucolor/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe class MaterialyoucolorRecipe(PyProjectRecipe): stl_lib_name = "c++_shared" version = "2.0.10" url = "https://github.com/T-Dynamos/materialyoucolor-python/releases/download/v{version}/materialyoucolor-{version}.tar.gz" def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) env['LDCXXSHARED'] = env['CXX'] + ' -shared' return env recipe = MaterialyoucolorRecipe() ================================================ FILE: pythonforandroid/recipes/matplotlib/__init__.py ================================================ from pythonforandroid.recipe import MesonRecipe from pythonforandroid.logger import shprint from os.path import join import sh class MatplotlibRecipe(MesonRecipe): version = '3.10.7' url = 'https://github.com/matplotlib/matplotlib/archive/v{version}.zip' depends = ['kiwisolver', 'numpy', 'pillow'] python_depends = ['cycler', 'fonttools', 'packaging', 'pyparsing', 'python-dateutil'] hostpython_prerequisites = ["setuptools_scm>=7"] patches = ["meson.patch"] need_stl_shared = True def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) env['CXXFLAGS'] += ' -Wno-c++11-narrowing' return env def build_arch(self, arch): python_path = join(self.ctx.python_recipe.get_build_dir(arch), "android-build", "python3") self.extra_build_args += [f'-Csetup-args=-Dpython3_program={python_path}'] shprint(sh.cp, self.real_hostpython_location, python_path) super().build_arch(arch) recipe = MatplotlibRecipe() ================================================ FILE: pythonforandroid/recipes/matplotlib/meson.patch ================================================ diff '--color=auto' -uNr matplotlib-3.10.7/meson.build matplotlib-3.10.7.mod/meson.build --- matplotlib-3.10.7/meson.build 2025-10-09 04:16:31.000000000 +0530 +++ matplotlib-3.10.7.mod/meson.build 2025-10-12 10:19:29.664280049 +0530 @@ -36,7 +36,7 @@ # https://mesonbuild.com/Python-module.html py_mod = import('python') -py3 = py_mod.find_installation(pure: false) +py3 = py_mod.find_installation(get_option('python3_program'), pure: false) py3_dep = py3.dependency() pybind11_dep = dependency('pybind11', version: '>=2.13.2') diff '--color=auto' -uNr matplotlib-3.10.7/meson.options matplotlib-3.10.7.mod/meson.options --- matplotlib-3.10.7/meson.options 2025-10-09 04:16:31.000000000 +0530 +++ matplotlib-3.10.7.mod/meson.options 2025-10-12 10:19:23.762042521 +0530 @@ -28,3 +28,5 @@ # default is determined by fallback. option('rcParams-backend', type: 'string', value: 'auto', description: 'Set default backend at runtime') + +option('python3_program', type : 'string', value : '', description : 'Path to Python 3 executable') ================================================ FILE: pythonforandroid/recipes/moderngl/__init__.py ================================================ from pythonforandroid.recipe import CppCompiledComponentsPythonRecipe class ModernGLRecipe(CppCompiledComponentsPythonRecipe): version = '5.10.0' url = 'https://github.com/moderngl/moderngl/archive/refs/tags/{version}.tar.gz' site_packages_name = 'moderngl' name = 'moderngl' def get_recipe_env(self, arch): env = super().get_recipe_env(arch) env['LDFLAGS'] += ' -lstdc++' return env recipe = ModernGLRecipe() ================================================ FILE: pythonforandroid/recipes/msgpack-python/__init__.py ================================================ from pythonforandroid.recipe import CythonRecipe class MsgPackRecipe(CythonRecipe): version = '0.4.7' url = 'https://pypi.python.org/packages/source/m/msgpack-python/msgpack-python-{version}.tar.gz' depends = ["setuptools"] call_hostpython_via_targetpython = False recipe = MsgPackRecipe() ================================================ FILE: pythonforandroid/recipes/ndghttpsclient ================================================ from pythonforandroid.recipe import PythonRecipe class NdgHttpsClientRecipe(PythonRecipe): version = '0.5.1' url = 'https://pypi.python.org/packages/source/n/ndg-httpsclient/ndg_httpsclient-{version}.tar.gz' depends = ['python3', 'pyopenssl', 'cryptography'] call_hostpython_via_targetpython = False recipe = NdgHttpsClientRecipe() ================================================ FILE: pythonforandroid/recipes/netifaces/__init__.py ================================================ from pythonforandroid.recipe import CompiledComponentsPythonRecipe class NetifacesRecipe(CompiledComponentsPythonRecipe): version = '0.10.9' url = 'https://files.pythonhosted.org/packages/source/n/netifaces/netifaces-{version}.tar.gz' depends = ['setuptools'] patches = ['fix-build.patch'] site_packages_name = 'netifaces' call_hostpython_via_targetpython = False recipe = NetifacesRecipe() ================================================ FILE: pythonforandroid/recipes/netifaces/fix-build.patch ================================================ --- netifaces/setup.py.orig 2018-05-02 09:45:09.000000000 +0200 +++ netifaces/setup.py 2018-12-11 14:12:02.785808692 +0100 @@ -55,7 +55,7 @@ self.check_requirements() build_ext.build_extensions(self) - def test_build(self, contents, link=True, execute=False, libraries=None, + def test_build(self, contents, link=False, execute=False, libraries=None, include_dirs=None, library_dirs=None): name = os.path.join(self.build_temp, 'conftest-%s.c' % self.conftestidx) self.conftestidx += 1 ================================================ FILE: pythonforandroid/recipes/numpy/__init__.py ================================================ from pythonforandroid.recipe import Recipe, MesonRecipe from os.path import join import shutil NUMPY_NDK_MESSAGE = "In order to build numpy, you must set minimum ndk api (minapi) to `24`.\n" class NumpyRecipe(MesonRecipe): version = 'v2.3.0' url = 'git+https://github.com/numpy/numpy' hostpython_prerequisites = ["Cython>=3.0.6", "numpy"] # meson does not detects venv's cython extra_build_args = ['-Csetup-args=-Dblas=none', '-Csetup-args=-Dlapack=none'] need_stl_shared = True min_ndk_api_support = 24 def get_recipe_meson_options(self, arch): options = super().get_recipe_meson_options(arch) # Custom python is required, so that meson # gets libs and config files properly options["binaries"]["python"] = self.ctx.python_recipe.python_exe options["binaries"]["python3"] = self.ctx.python_recipe.python_exe options["properties"]["longdouble_format"] = "IEEE_DOUBLE_LE" if arch.arch in ["armeabi-v7a", "x86"] else "IEEE_QUAD_LE" return options def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) # _PYTHON_HOST_PLATFORM declares that we're cross-compiling # and avoids issues when building on macOS for Android targets. env["_PYTHON_HOST_PLATFORM"] = arch.command_prefix # NPY_DISABLE_SVML=1 allows numpy to build for non-AVX512 CPUs # See: https://github.com/numpy/numpy/issues/21196 env["NPY_DISABLE_SVML"] = "1" env["TARGET_PYTHON_EXE"] = join(Recipe.get_recipe( "python3", self.ctx).get_build_dir(arch.arch), "android-build", "python") return env def build_arch(self, arch): super().build_arch(arch) self.restore_hostpython_prerequisites(["cython"]) def get_hostrecipe_env(self, arch=None): env = super().get_hostrecipe_env(arch=arch) env['RANLIB'] = shutil.which('ranlib') return env recipe = NumpyRecipe() ================================================ FILE: pythonforandroid/recipes/omemo/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class OmemoRecipe(PythonRecipe): name = 'omemo' version = '0.11.0' url = 'https://pypi.python.org/packages/source/O/OMEMO/OMEMO-{version}.tar.gz' site_packages_name = 'omemo' depends = [ 'setuptools', 'x3dh', 'cryptography', ] call_hostpython_via_targetpython = False recipe = OmemoRecipe() ================================================ FILE: pythonforandroid/recipes/omemo-backend-signal/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class OmemoBackendSignalRecipe(PythonRecipe): name = 'omemo-backend-signal' version = '0.2.5' url = 'https://pypi.python.org/packages/source/o/omemo-backend-signal/omemo-backend-signal-{version}.tar.gz' site_packages_name = 'omemo-backend-signal' depends = [ 'setuptools', 'protobuf_cpp', 'x3dh', 'DoubleRatchet', 'hkdf==0.0.3', 'cryptography', 'omemo', ] call_hostpython_via_targetpython = False recipe = OmemoBackendSignalRecipe() ================================================ FILE: pythonforandroid/recipes/openal/__init__.py ================================================ from pythonforandroid.recipe import NDKRecipe from pythonforandroid.toolchain import current_directory, shprint from os.path import join import sh class OpenALRecipe(NDKRecipe): version = '1.21.1' url = 'https://github.com/kcat/openal-soft/archive/refs/tags/{version}.tar.gz' generated_libraries = ['libopenal.so'] def build_arch(self, arch): with current_directory(self.get_build_dir(arch.arch)): env = self.get_recipe_env(arch) cmake_args = [ "-DANDROID_STL=" + self.stl_lib_name, "-DCMAKE_TOOLCHAIN_FILE={}".format( join(self.ctx.ndk_dir, "build", "cmake", "android.toolchain.cmake") ), ] shprint( sh.cmake, '.', *cmake_args, _env=env ) shprint(sh.make, _env=env) self.install_libs(arch, 'libopenal.so') recipe = OpenALRecipe() ================================================ FILE: pythonforandroid/recipes/opencv/__init__.py ================================================ from multiprocessing import cpu_count from os.path import join import sh from pythonforandroid.logger import shprint from pythonforandroid.recipe import NDKRecipe from pythonforandroid.util import current_directory, ensure_dir class OpenCVRecipe(NDKRecipe): ''' .. versionchanged:: 0.7.1 rewrote recipe to support the python bindings (cv2.so) and enable the build of most of the libraries of the opencv's package, so we can process images, videos, objects, photos... ''' version = '4.12.0' url = 'https://github.com/opencv/opencv/archive/{version}.zip' depends = ['numpy'] patches = ['patches/p4a_build.patch'] generated_libraries = [ 'libopencv_features2d.so', 'libopencv_imgproc.so', 'libopencv_stitching.so', 'libopencv_calib3d.so', 'libopencv_flann.so', 'libopencv_ml.so', 'libopencv_videoio.so', 'libopencv_core.so', 'libopencv_highgui.so', 'libopencv_objdetect.so', 'libopencv_video.so', 'libopencv_dnn.so', 'libopencv_imgcodecs.so', 'libopencv_photo.so', ] 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().get_recipe_env(arch) env['ANDROID_NDK'] = self.ctx.ndk_dir env['ANDROID_SDK'] = self.ctx.sdk_dir return env def build_arch(self, arch): build_dir = join(self.get_build_dir(arch.arch), 'build') ensure_dir(build_dir) opencv_extras = [] if 'opencv_extras' in self.ctx.recipe_build_order: opencv_extras_dir = self.get_recipe( 'opencv_extras', self.ctx).get_build_dir(arch.arch) opencv_extras = [ f'-DOPENCV_EXTRA_MODULES_PATH={opencv_extras_dir}/modules', '-DBUILD_opencv_legacy=OFF', ] with current_directory(build_dir): env = self.get_recipe_env(arch) python_major = self.ctx.python_recipe.version[0] python_include_root = self.ctx.python_recipe.include_root(arch.arch) python_site_packages = self.ctx.get_site_packages_dir(arch) python_link_root = self.ctx.python_recipe.link_root(arch.arch) python_link_version = self.ctx.python_recipe.link_version python_library = join(python_link_root, 'libpython{}.so'.format(python_link_version)) python_include_numpy = join( self.ctx.get_python_install_dir(arch.arch), "numpy/_core/include", ) shprint(sh.cmake, '-DP4A=ON', '-DANDROID_ABI={}'.format(arch.arch), '-DANDROID_STANDALONE_TOOLCHAIN={}'.format(self.ctx.ndk_dir), '-DANDROID_NATIVE_API_LEVEL={}'.format(self.ctx.ndk_api), '-DANDROID_EXECUTABLE={}/tools/android'.format(env['ANDROID_SDK']), '-DANDROID_SDK_TOOLS_VERSION=6514223', '-DANDROID_PROJECTS_SUPPORT_GRADLE=ON', '-DCMAKE_TOOLCHAIN_FILE={}'.format( join(self.ctx.ndk_dir, 'build', 'cmake', 'android.toolchain.cmake')), # Make the linkage with our python library, otherwise we # will get dlopen error when trying to import cv2's module. '-DCMAKE_SHARED_LINKER_FLAGS=-L{path} -lpython{version}'.format( path=python_link_root, version=python_link_version), '-DBUILD_WITH_STANDALONE_TOOLCHAIN=ON', # Force to build as shared libraries the cv2's dependent # libs or we will not be able to link with our python '-DBUILD_SHARED_LIBS=ON', '-DBUILD_STATIC_LIBS=OFF', # Disable some opencv's features '-DBUILD_opencv_java=OFF', '-DBUILD_opencv_java_bindings_generator=OFF', # '-DBUILD_opencv_highgui=OFF', # '-DBUILD_opencv_imgproc=OFF', # '-DBUILD_opencv_flann=OFF', '-DBUILD_TESTS=OFF', '-DBUILD_PERF_TESTS=OFF', '-DENABLE_TESTING=OFF', '-DBUILD_EXAMPLES=OFF', '-DBUILD_ANDROID_EXAMPLES=OFF', # Force to only build our version of python '-DBUILD_OPENCV_PYTHON{major}=ON'.format(major=python_major), '-DBUILD_OPENCV_PYTHON{major}=OFF'.format( major='2' if python_major == '3' else '3'), # Force to install the `cv2.so` library directly into # python's site packages (otherwise the cv2's loader fails # on finding the cv2.so library) '-DOPENCV_SKIP_PYTHON_LOADER=ON', '-DOPENCV_PYTHON{major}_INSTALL_PATH={site_packages}'.format( major=python_major, site_packages=python_site_packages), # Define python's paths for: exe, lib, includes, numpy... '-DPYTHON_DEFAULT_EXECUTABLE={}'.format(self.ctx.hostpython), '-DPYTHON{major}_EXECUTABLE={host_python}'.format( major=python_major, host_python=self.ctx.hostpython), '-DPYTHON{major}_INCLUDE_PATH={include_path}'.format( major=python_major, include_path=python_include_root), '-DPYTHON{major}_LIBRARIES={python_lib}'.format( major=python_major, python_lib=python_library), '-DPYTHON{major}_NUMPY_INCLUDE_DIRS={numpy_include}'.format( major=python_major, numpy_include=python_include_numpy), '-DPYTHON{major}_PACKAGES_PATH={site_packages}'.format( major=python_major, site_packages=python_site_packages), *opencv_extras, self.get_build_dir(arch.arch), _env=env) # patch link.txt for unsupported flag link_txt = 'modules/python3/CMakeFiles/opencv_python3.dir/link.txt' with open(link_txt, 'r+') as f: content = f.read().replace('-version', ' ') f.seek(0) f.write(content) f.truncate() shprint(sh.make, '-j' + str(cpu_count()), 'opencv_python' + python_major) # Install python bindings (cv2.so) shprint(sh.cmake, '-DCOMPONENT=python', '-P', './cmake_install.cmake') # Copy third party shared libs that we need in our final apk sh.cp('-a', sh.glob('./lib/{}/lib*.so'.format(arch.arch)), self.ctx.get_libs_dir(arch.arch)) recipe = OpenCVRecipe() ================================================ FILE: pythonforandroid/recipes/opencv/patches/p4a_build.patch ================================================ diff '--color=auto' -uNr opencv-4.12.0/cmake/OpenCVDetectPython.cmake opencv-4.12.0.mod/cmake/OpenCVDetectPython.cmake --- opencv-4.12.0/cmake/OpenCVDetectPython.cmake 2025-07-02 13:24:13.000000000 +0530 +++ opencv-4.12.0.mod/cmake/OpenCVDetectPython.cmake 2025-09-20 22:22:14.961944470 +0530 @@ -175,7 +175,7 @@ endif() endif() - if(NOT ANDROID AND NOT IOS AND NOT XROS) + if(P4A OR NOT ANDROID AND NOT IOS AND NOT XROS) if(CMAKE_HOST_UNIX) execute_process(COMMAND ${_executable} -c "from sysconfig import *; print(get_path('purelib'))" RESULT_VARIABLE _cvpy_process diff '--color=auto' -uNr opencv-4.12.0/modules/python/CMakeLists.txt opencv-4.12.0.mod/modules/python/CMakeLists.txt --- opencv-4.12.0/modules/python/CMakeLists.txt 2025-07-02 13:24:13.000000000 +0530 +++ opencv-4.12.0.mod/modules/python/CMakeLists.txt 2025-09-20 22:23:15.124356524 +0530 @@ -3,7 +3,7 @@ # ---------------------------------------------------------------------------- if(DEFINED OPENCV_INITIAL_PASS) # OpenCV build -if(ANDROID OR APPLE_FRAMEWORK OR WINRT) +if(ANDROID AND NOT P4A OR APPLE_FRAMEWORK OR WINRT) ocv_module_disable_(python2) ocv_module_disable_(python3) return() ================================================ FILE: pythonforandroid/recipes/opencv_extras/__init__.py ================================================ from pythonforandroid.recipe import Recipe class OpenCVExtrasRecipe(Recipe): """ OpenCV extras recipe allows us to build extra modules from the `opencv_contrib` repository. It depends on opencv recipe and all the build of the modules will be performed inside opencv recipe build directory. .. note:: the version of this recipe should be the same than opencv recipe. .. warning:: Be aware that these modules are experimental, some of them maybe included in opencv future releases and removed from extras. .. seealso:: https://github.com/opencv/opencv_contrib """ version = '4.5.1' url = 'https://github.com/opencv/opencv_contrib/archive/{version}.zip' depends = ['opencv'] recipe = OpenCVExtrasRecipe() ================================================ FILE: pythonforandroid/recipes/openssl/__init__.py ================================================ from os.path import join from multiprocessing import cpu_count from pythonforandroid.recipe import Recipe from pythonforandroid.util import current_directory from pythonforandroid.logger import shprint import sh class OpenSSLRecipe(Recipe): ''' The OpenSSL libraries for python-for-android. This recipe will generate the following libraries as shared libraries (*.so): - crypto - ssl The generated openssl libraries are versioned, where the version is the recipe attribute :attr:`version` e.g.: ``libcrypto1.1.so``, ``libssl1.1.so``...so...to link your recipe with the openssl libs, remember to add the version at the end, e.g.: ``-lcrypto1.1 -lssl1.1``. Or better, you could do it dynamically using the methods: :meth:`include_flags`, :meth:`link_dirs_flags` and :meth:`link_libs_flags`. .. warning:: This recipe is very sensitive because is used for our core recipes, the python recipes. The used API should match with the one used in our python build, otherwise we will be unable to build the _ssl.so python module. .. versionchanged:: 0.6.0 - The gcc compiler has been deprecated in favour of clang and libraries updated to version 1.1.1 (LTS - supported until 11th September 2023) - Added two new methods to make easier to link with openssl: :meth:`include_flags` and :meth:`link_flags` - subclassed versioned_url - Adapted method :meth:`select_build_arch` to API 21+ - Add ability to build a legacy version of the openssl libs when using python2legacy or python3crystax. .. versionchanged:: 2019.06.06.1.dev0 - Removed legacy version of openssl libraries ''' version = '3.3.1' url = 'https://www.openssl.org/source/openssl-{version}.tar.gz' built_libraries = { 'libcrypto.so': '.', 'libssl.so': '.', } def get_build_dir(self, arch): return join( self.get_build_container_dir(arch), self.name + self.version[0] ) def include_flags(self, arch): '''Returns a string with the include folders''' openssl_includes = join(self.get_build_dir(arch.arch), 'include') return (' -I' + openssl_includes + ' -I' + join(openssl_includes, 'openssl')) def link_dirs_flags(self, arch): '''Returns a string with the appropriate `-L` to link with the openssl libs. This string is usually added to the environment variable `LDFLAGS`''' return ' -L' + self.get_build_dir(arch.arch) def link_libs_flags(self): '''Returns a string with the appropriate `-l` flags to link with the openssl libs. This string is usually added to the environment variable `LIBS`''' return ' -lcrypto -lssl' def link_flags(self, arch): '''Returns a string with the flags to link with the openssl libraries in the format: `-L -l`''' return self.link_dirs_flags(arch) + self.link_libs_flags() def get_recipe_env(self, arch=None): env = super().get_recipe_env(arch) env['OPENSSL_VERSION'] = self.version[0] env['CC'] = 'clang' env['ANDROID_NDK_ROOT'] = self.ctx.ndk_dir env["PATH"] = f"{self.ctx.ndk.llvm_bin_dir}:{env['PATH']}" env["CFLAGS"] += " -Wno-macro-redefined" env["MAKE"] = "make" return env def select_build_arch(self, arch): aname = arch.arch if 'arm64' in aname: return 'android-arm64' if 'v7a' in aname: return 'android-arm' if 'arm' in aname: return 'android' if 'x86_64' in aname: return 'android-x86_64' if 'x86' in aname: return 'android-x86' return 'linux-armv4' def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): # sh fails with code 255 trying to execute ./Configure # so instead we manually run perl passing in Configure perl = sh.Command('perl') buildarch = self.select_build_arch(arch) config_args = [ 'shared', 'no-dso', 'no-asm', 'no-tests', buildarch, '-D__ANDROID_API__={}'.format(self.ctx.ndk_api), ] shprint(perl, 'Configure', *config_args, _env=env) shprint(sh.make, '-j', str(cpu_count()), _env=env) recipe = OpenSSLRecipe() ================================================ FILE: pythonforandroid/recipes/pandas/__init__.py ================================================ from os.path import join from pythonforandroid.recipe import MesonRecipe class PandasRecipe(MesonRecipe): version = 'v2.3.0' url = 'git+https://github.com/pandas-dev/pandas' depends = ['numpy', 'libbz2', 'liblzma'] hostpython_prerequisites = ["Cython<4.0.0a0", "versioneer", "numpy"] # meson does not detects venv's cython patches = ['fix_numpy_includes.patch'] python_depends = ['python-dateutil', 'pytz'] need_stl_shared = True def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) # 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( self.ctx.get_python_install_dir(arch.arch), "numpy/_core/include", ) env["PYTHON_INCLUDE_DIR"] = self.ctx.python_recipe.include_root(arch) # this flag below is to fix a runtime error: # ImportError: dlopen failed: cannot locate symbol # "_ZTVSt12length_error" referenced by # "/data/data/org.test.matplotlib_testapp/files/app/_python_bundle # /site-packages/pandas/_libs/window/aggregations.so"... env['LDFLAGS'] += f' -landroid -l{self.stl_lib_name}' return env def build_arch(self, arch): super().build_arch(arch) self.restore_hostpython_prerequisites(["cython"]) recipe = PandasRecipe() ================================================ FILE: pythonforandroid/recipes/pandas/fix_numpy_includes.patch ================================================ diff '--color=auto' -uNr pandas/pandas/_libs/meson.build pandas.mod/pandas/_libs/meson.build --- pandas/pandas/_libs/meson.build 2024-04-24 07:24:46.009296003 +0530 +++ pandas.mod/pandas/_libs/meson.build 2024-04-24 07:45:15.221534571 +0530 @@ -115,7 +115,7 @@ ext_name, ext_dict.get('sources'), cython_args: cython_args, - include_directories: [inc_np, inc_pd], + include_directories: [inc_android, inc_np, inc_pd], dependencies: ext_dict.get('deps', ''), subdir: 'pandas/_libs', install: true diff '--color=auto' -uNr pandas/pandas/_libs/tslibs/meson.build pandas.mod/pandas/_libs/tslibs/meson.build --- pandas/pandas/_libs/tslibs/meson.build 2024-04-24 07:24:46.019296090 +0530 +++ pandas.mod/pandas/_libs/tslibs/meson.build 2024-04-24 07:45:53.528798309 +0530 @@ -33,7 +33,7 @@ ext_name, ext_dict.get('sources'), cython_args: cython_args, - include_directories: [inc_np, inc_pd], + include_directories: [inc_android, inc_np, inc_pd], dependencies: ext_dict.get('deps', ''), subdir: 'pandas/_libs/tslibs', install: true diff '--color=auto' -uNr pandas/pandas/_libs/window/meson.build pandas.mod/pandas/_libs/window/meson.build --- pandas/pandas/_libs/window/meson.build 2024-04-24 07:24:46.029296177 +0530 +++ pandas.mod/pandas/_libs/window/meson.build 2024-04-28 10:47:16.915307381 +0530 @@ -2,7 +2,7 @@ 'aggregations', ['aggregations.pyx'], cython_args: ['-X always_allow_keywords=true'], - include_directories: [inc_np, inc_pd], + include_directories: [inc_android, inc_np, inc_pd], subdir: 'pandas/_libs/window', override_options : ['cython_language=cpp'], install: true @@ -12,7 +12,7 @@ 'indexers', ['indexers.pyx'], cython_args: ['-X always_allow_keywords=true'], - include_directories: [inc_np, inc_pd], + include_directories: [inc_android, inc_np, inc_pd], subdir: 'pandas/_libs/window', install: true ) diff '--color=auto' -uNr pandas/pandas/meson.build pandas.mod/pandas/meson.build --- pandas/pandas/meson.build 2024-04-24 07:24:46.232297943 +0530 +++ pandas.mod/pandas/meson.build 2024-04-24 07:46:12.508929590 +0530 @@ -3,20 +3,23 @@ '-c', ''' import os -import numpy as np -try: - # Check if include directory is inside the pandas dir - # e.g. a venv created inside the pandas dir - # If so, convert it to a relative path - incdir = os.path.relpath(np.get_include()) -except Exception: - incdir = np.get_include() -print(incdir) - ''' +print(os.environ["NUMPY_INCLUDES"]) + ''' + ], + check: true +).stdout().strip() +incdir_android = run_command(py, + [ + '-c', + ''' +import os +print(os.environ["PYTHON_INCLUDE_DIR"]) + ''' ], check: true ).stdout().strip() +inc_android = include_directories(incdir_android) inc_np = include_directories(incdir_numpy) inc_pd = include_directories('_libs/include') ================================================ FILE: pythonforandroid/recipes/pil/__init__.py ================================================ from pythonforandroid.recipes.Pillow import PillowRecipe from pythonforandroid.logger import warning class PilRecipe(PillowRecipe): """A transparent wrapper around the Pillow recipe, it should build Pillow automatically as if "pillow" were specified in the requirements. """ name = 'Pillow' # ensures the Pillow recipe directory is used where necessary conflicts = ['pillow'] def build_arch(self, arch): warning('PIL is no longer supported, building Pillow instead. ' 'This should be a drop-in replacement.') warning('It is recommended to change "pil" to "pillow" in your requirements, ' 'to ensure future compatibility') super().build_arch(arch) recipe = PilRecipe() ================================================ FILE: pythonforandroid/recipes/png/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.logger import shprint from pythonforandroid.util import current_directory from multiprocessing import cpu_count import sh class PngRecipe(Recipe): name = 'png' version = '1.6.37' url = 'https://github.com/glennrp/libpng/archive/v{version}.zip' built_libraries = {'libpng16.so': '.libs'} def build_arch(self, arch): build_dir = self.get_build_dir(arch.arch) with current_directory(build_dir): env = self.get_recipe_env(arch) shprint( sh.Command('./configure'), '--host=' + arch.command_prefix, '--target=' + arch.command_prefix, '--disable-static', '--enable-shared', '--prefix={}/install'.format(self.get_build_dir(arch.arch)), _env=env, ) shprint(sh.make, '-j', str(cpu_count()), _env=env) recipe = PngRecipe() ================================================ FILE: pythonforandroid/recipes/png/build_shared_library.patch ================================================ diff --git a/jni/Android.mk b/jni/Android.mk index df2ff1a..2f70985 100644 --- a/jni/Android.mk +++ b/jni/Android.mk @@ -26,8 +26,9 @@ LOCAL_SRC_FILES :=\ arm/filter_neon_intrinsics.c #LOCAL_SHARED_LIBRARIES := -lz -LOCAL_EXPORT_LDLIBS := -lz +# LOCAL_EXPORT_LDLIBS := -lz +LOCAL_LDLIBS := -lz LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/. -#include $(BUILD_SHARED_LIBRARY) -include $(BUILD_STATIC_LIBRARY) +include $(BUILD_SHARED_LIBRARY) +# include $(BUILD_STATIC_LIBRARY) ================================================ FILE: pythonforandroid/recipes/preppy/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class PreppyRecipe(PythonRecipe): version = '27b7085' url = 'https://bitbucket.org/rptlab/preppy/get/{version}.tar.gz' depends = [] patches = ['fix-setup.patch'] call_hostpython_via_targetpython = False recipe = PreppyRecipe() ================================================ FILE: pythonforandroid/recipes/preppy/fix-setup.patch ================================================ --- a/setup.py 2017-11-20 13:53:42.000000000 +0000 +++ b/setup.py 2017-11-20 14:00:44.862203526 +0000 @@ -15,35 +15,6 @@ import preppy version = preppy.VERSION - scriptsPath=os.path.join(pkgDir,'build','scripts') - - def makeScript(modName): - try: - bat=sys.platform in ('win32','amd64') - scriptPath=os.path.join(scriptsPath,modName+(bat and '.bat' or '')) - exePath=sys.executable - f = open(scriptPath,'w') - try: - if bat: - text = '@echo off\nrem startup script for %s-%s\n"%s" -m "%s" %%*\n' % (modName,version,exePath,modName) - else: - text = '#!/bin/sh\n#startup script for %s-%s\nexec "%s" -m "%s" $*\n' % (modName,version,exePath,modName) - f.write(text) - finally: - f.close() - except: - print('script for %s not created or erroneous' % modName) - import traceback - traceback.print_exc(file=sys.stdout) - return None - print('Created "%s"' % scriptPath) - return scriptPath - - scripts = [] - if not os.path.isdir(scriptsPath): os.makedirs(scriptsPath) - scripts.extend(filter(None,[ - makeScript('preppy'), - ])) setup(name='preppy', version=version, @@ -52,5 +23,4 @@ author_email='andy@reportlab.com', url='http://bitbucket.org/rptlab/preppy', py_modules=['preppy'], - scripts=scripts, ) ================================================ FILE: pythonforandroid/recipes/primp/__init__.py ================================================ from pythonforandroid.logger import info from pythonforandroid.recipe import RustCompiledComponentsRecipe class PrimpRecipe(RustCompiledComponentsRecipe): version = "v0.14.0" url = "https://github.com/deedy5/primp/archive/refs/tags/{version}.tar.gz" def get_recipe_env_post(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) env["ANDROID_NDK_HOME"] = self.ctx.ndk.llvm_prebuilt_dir return env def get_recipe_env_pre(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) env["ANDROID_NDK_HOME"] = self.ctx.ndk_dir return env def build_arch(self, arch): # Why need of two env? # Because there are two dependencies which accepts # different ANDROID_NDK_HOME self.get_recipe_env = self.get_recipe_env_pre prebuild_ = super().build_arch try: prebuild_(arch) except Exception: info("pyreqwest_impersonate first build failed, as expected") self.get_recipe_env = self.get_recipe_env_post prebuild_(arch) recipe = PrimpRecipe() ================================================ FILE: pythonforandroid/recipes/protobuf_cpp/__init__.py ================================================ from multiprocessing import cpu_count import os from os.path import exists, join from pythonforandroid.toolchain import info import sh import sys from pythonforandroid.recipe import CppCompiledComponentsPythonRecipe from pythonforandroid.logger import shprint, info_notify from pythonforandroid.util import current_directory, touch class ProtobufCppRecipe(CppCompiledComponentsPythonRecipe): """This is a two-in-one recipe: - build labraru `libprotobuf.so` - build and install python binding for protobuf_cpp """ name = 'protobuf_cpp' version = '3.6.1' url = 'https://github.com/google/protobuf/releases/download/v{version}/protobuf-python-{version}.tar.gz' call_hostpython_via_targetpython = False depends = ['cffi', 'setuptools'] site_packages_name = 'google/protobuf/pyext' setup_extra_args = ['--cpp_implementation'] built_libraries = {'libprotobuf.so': 'src/.libs'} protoc_dir = None def prebuild_arch(self, 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): self.apply_patch('fix-python3-compatibility.patch', arch.arch) touch(patch_mark) # During building, host needs to transpile .proto files to .py # ideally with the same version as protobuf runtime, or with an older one. # Because protoc is compiled for target (i.e. Android), we need an other binary # which can be run by host. # To make it easier, we download prebuild protoc binary adapted to the platform info_notify("Downloading protoc compiler for your platform") url_prefix = "https://github.com/protocolbuffers/protobuf/releases/download/v{version}".format(version=self.version) if sys.platform.startswith('linux'): info_notify("GNU/Linux detected") filename = "protoc-{version}-linux-x86_64.zip".format(version=self.version) elif sys.platform.startswith('darwin'): info_notify("Mac OS X detected") filename = "protoc-{version}-osx-x86_64.zip".format(version=self.version) else: info_notify("Your platform is not supported, but recipe can still " "be built if you have a valid protoc (<={version}) in " "your path".format(version=self.version)) return protoc_url = join(url_prefix, filename) self.protoc_dir = join(self.ctx.build_dir, "tools", "protoc") if os.path.exists(join(self.protoc_dir, "bin", "protoc")): info_notify("protoc found, no download needed") return try: os.makedirs(self.protoc_dir) except OSError as e: # if dir already exists (errno 17), we ignore the error if e.errno != 17: raise e info_notify("Will download into {dest_dir}".format(dest_dir=self.protoc_dir)) self.download_file(protoc_url, join(self.protoc_dir, filename)) with current_directory(self.protoc_dir): shprint(sh.unzip, join(self.protoc_dir, filename)) def build_arch(self, arch): env = self.get_recipe_env(arch) # Build libproto.so with current_directory(self.get_build_dir(arch.arch)): build_arch = ( shprint(sh.gcc, '-dumpmachine') .stdout.decode('utf-8') .split('\n')[0] ) if not exists('configure'): shprint(sh.Command('./autogen.sh'), _env=env) shprint(sh.Command('./configure'), '--build={}'.format(build_arch), '--host={}'.format(arch.command_prefix), '--target={}'.format(arch.command_prefix), '--disable-static', '--enable-shared', _env=env) with current_directory(join(self.get_build_dir(arch.arch), 'src')): shprint(sh.make, 'libprotobuf.la', '-j'+str(cpu_count()), _env=env) self.install_python_package(arch) def build_compiled_components(self, arch): # Build python bindings and _message.so env = self.get_recipe_env(arch) with current_directory(join(self.get_build_dir(arch.arch), 'python')): hostpython = sh.Command(self.hostpython_location) shprint(hostpython, 'setup.py', 'build_ext', _env=env, *self.setup_extra_args) def install_python_package(self, arch): env = self.get_recipe_env(arch) info('Installing {} into site-packages'.format(self.name)) with current_directory(join(self.get_build_dir(arch.arch), 'python')): hostpython = sh.Command(self.hostpython_location) hpenv = env.copy() shprint(hostpython, 'setup.py', 'install', '-O2', '--root={}'.format(self.ctx.get_python_install_dir(arch.arch)), '--install-lib=.', _env=hpenv, *self.setup_extra_args) # Create __init__.py which is missing, see also: # - https://github.com/protocolbuffers/protobuf/issues/1296 # - https://stackoverflow.com/questions/13862562/ # google-protocol-buffers-not-found-when-trying-to-freeze-python-app open( join(self.ctx.get_site_packages_dir(arch), 'google', '__init__.py'), 'a', ).close() def get_recipe_env(self, 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') env['TARGET_OS'] = 'OS_ANDROID_CROSSCOMPILE' env['CXXFLAGS'] += ' -std=c++11' env['LDFLAGS'] += ' -lm -landroid -llog' return env recipe = ProtobufCppRecipe() ================================================ FILE: pythonforandroid/recipes/protobuf_cpp/fix-python3-compatibility.patch ================================================ From 539bc017a62f91bdf7c547b58948cb5a2f59d918 Mon Sep 17 00:00:00 2001 From: Ben Webb Date: Thu, 12 Jul 2018 10:58:10 -0700 Subject: [PATCH] Add Python 3.7 compatibility (#4862) Compilation of Python wrappers fails with Python 3.7 because the Python folks changed their C API such that PyUnicode_AsUTF8AndSize() now returns a const char* rather than a char*. Add a patch to work around. Relates #4086. --- python/google/protobuf/pyext/descriptor.cc | 2 +- python/google/protobuf/pyext/descriptor_containers.cc | 2 +- python/google/protobuf/pyext/descriptor_pool.cc | 2 +- python/google/protobuf/pyext/extension_dict.cc | 2 +- python/google/protobuf/pyext/message.cc | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/python/google/protobuf/pyext/descriptor.cc b/python/google/protobuf/pyext/descriptor.cc index 8af0cb1289..19a1c38a62 100644 --- a/python/google/protobuf/pyext/descriptor.cc +++ b/python/google/protobuf/pyext/descriptor.cc @@ -56,7 +56,7 @@ #endif #define PyString_AsStringAndSize(ob, charpp, sizep) \ (PyUnicode_Check(ob)? \ - ((*(charpp) = PyUnicode_AsUTF8AndSize(ob, (sizep))) == NULL? -1: 0): \ + ((*(charpp) = const_cast(PyUnicode_AsUTF8AndSize(ob, (sizep)))) == NULL? -1: 0): \ PyBytes_AsStringAndSize(ob, (charpp), (sizep))) #endif diff --git a/python/google/protobuf/pyext/descriptor_containers.cc b/python/google/protobuf/pyext/descriptor_containers.cc index bc007f7efa..0153664f50 100644 --- a/python/google/protobuf/pyext/descriptor_containers.cc +++ b/python/google/protobuf/pyext/descriptor_containers.cc @@ -66,7 +66,7 @@ #endif #define PyString_AsStringAndSize(ob, charpp, sizep) \ (PyUnicode_Check(ob)? \ - ((*(charpp) = PyUnicode_AsUTF8AndSize(ob, (sizep))) == NULL? -1: 0): \ + ((*(charpp) = const_cast(PyUnicode_AsUTF8AndSize(ob, (sizep)))) == NULL? -1: 0): \ PyBytes_AsStringAndSize(ob, (charpp), (sizep))) #endif diff --git a/python/google/protobuf/pyext/descriptor_pool.cc b/python/google/protobuf/pyext/descriptor_pool.cc index 95882aeb35..962accc6e9 100644 --- a/python/google/protobuf/pyext/descriptor_pool.cc +++ b/python/google/protobuf/pyext/descriptor_pool.cc @@ -48,7 +48,7 @@ #endif #define PyString_AsStringAndSize(ob, charpp, sizep) \ (PyUnicode_Check(ob)? \ - ((*(charpp) = PyUnicode_AsUTF8AndSize(ob, (sizep))) == NULL? -1: 0): \ + ((*(charpp) = const_cast(PyUnicode_AsUTF8AndSize(ob, (sizep)))) == NULL? -1: 0): \ PyBytes_AsStringAndSize(ob, (charpp), (sizep))) #endif diff --git a/python/google/protobuf/pyext/extension_dict.cc b/python/google/protobuf/pyext/extension_dict.cc index 018b5c2c49..174c5470c2 100644 --- a/python/google/protobuf/pyext/extension_dict.cc +++ b/python/google/protobuf/pyext/extension_dict.cc @@ -53,7 +53,7 @@ #endif #define PyString_AsStringAndSize(ob, charpp, sizep) \ (PyUnicode_Check(ob)? \ - ((*(charpp) = PyUnicode_AsUTF8AndSize(ob, (sizep))) == NULL? -1: 0): \ + ((*(charpp) = const_cast(PyUnicode_AsUTF8AndSize(ob, (sizep)))) == NULL? -1: 0): \ PyBytes_AsStringAndSize(ob, (charpp), (sizep))) #endif diff --git a/python/google/protobuf/pyext/message.cc b/python/google/protobuf/pyext/message.cc index 5893533adf..31094b7e10 100644 --- a/python/google/protobuf/pyext/message.cc +++ b/python/google/protobuf/pyext/message.cc @@ -79,7 +79,7 @@ (PyUnicode_Check(ob)? PyUnicode_AsUTF8(ob): PyBytes_AsString(ob)) #define PyString_AsStringAndSize(ob, charpp, sizep) \ (PyUnicode_Check(ob)? \ - ((*(charpp) = PyUnicode_AsUTF8AndSize(ob, (sizep))) == NULL? -1: 0): \ + ((*(charpp) = const_cast(PyUnicode_AsUTF8AndSize(ob, (sizep)))) == NULL? -1: 0): \ PyBytes_AsStringAndSize(ob, (charpp), (sizep))) #endif #endif @@ -1529,7 +1529,7 @@ PyObject* HasField(CMessage* self, PyObject* arg) { return NULL; } #else - field_name = PyUnicode_AsUTF8AndSize(arg, &size); + field_name = const_cast(PyUnicode_AsUTF8AndSize(arg, &size)); if (!field_name) { return NULL; } ================================================ FILE: pythonforandroid/recipes/psycopg2/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe from pythonforandroid.toolchain import current_directory, shprint import sh class Psycopg2Recipe(PythonRecipe): """ Requires `libpq-dev` system dependency e.g. for `pg_config` binary. If you get `nl_langinfo` symbol runtime error, make sure you're running on `ANDROID_API` (`ndk-api`) >= 26, see: https://github.com/kivy/python-for-android/issues/1711#issuecomment-465747557 """ version = '2.8.5' url = 'https://pypi.python.org/packages/source/p/psycopg2/psycopg2-{version}.tar.gz' depends = ['libpq', 'setuptools'] site_packages_name = 'psycopg2' call_hostpython_via_targetpython = False def prebuild_arch(self, arch): libdir = self.ctx.get_libs_dir(arch.arch) with current_directory(self.get_build_dir(arch.arch)): # pg_config_helper will return the system installed libpq, but we # need the one we just cross-compiled shprint(sh.sed, '-i', "s|pg_config_helper.query(.libdir.)|'{}'|".format(libdir), 'setup.py') def get_recipe_env(self, 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 def install_python_package(self, arch, name=None, env=None, is_dir=True): '''Automate the installation of a Python package (or a cython package where the cython components are pre-built).''' if env is None: env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): hostpython = sh.Command(self.ctx.hostpython) shprint(hostpython, 'setup.py', 'build_ext', '--static-libpq', _env=env) shprint(hostpython, 'setup.py', 'install', '-O2', '--root={}'.format(self.ctx.get_python_install_dir(arch.arch)), '--install-lib=.', _env=env) recipe = Psycopg2Recipe() ================================================ FILE: pythonforandroid/recipes/py3dns/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class Py3DNSRecipe(PythonRecipe): site_packages_name = 'DNS' version = '3.2.1' url = 'https://launchpad.net/py3dns/trunk/{version}/+download/py3dns-{version}.tar.gz' depends = ['setuptools'] patches = ['patches/android.patch'] call_hostpython_via_targetpython = False recipe = Py3DNSRecipe() ================================================ FILE: pythonforandroid/recipes/py3dns/patches/android.patch ================================================ diff --git a/DNS/Base.py b/DNS/Base.py index 34a6da7..a558889 100644 --- a/DNS/Base.py +++ b/DNS/Base.py @@ -15,6 +15,7 @@ import socket, string, types, time, select import errno from . import Type,Class,Opcode import asyncore +import os # # This random generator is used for transaction ids and port selection. This # is important to prevent spurious results from lost packets, and malicious @@ -50,8 +51,12 @@ defaults= { 'protocol':'udp', 'port':53, 'opcode':Opcode.QUERY, def ParseResolvConf(resolv_path="/etc/resolv.conf"): "parses the /etc/resolv.conf file and sets defaults for name servers" - with open(resolv_path, 'r') as stream: - return ParseResolvConfFromIterable(stream) + if os.path.exists(resolv_path): + with open(resolv_path, 'r') as stream: + return ParseResolvConfFromIterable(stream) + else: + defaults['server'].append('127.0.0.1') + return def ParseResolvConfFromIterable(lines): "parses a resolv.conf formatted stream and sets defaults for name servers" ================================================ FILE: pythonforandroid/recipes/pyaml/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class PyamlRecipe(PythonRecipe): version = "15.8.2" url = 'https://pypi.python.org/packages/source/p/pyaml/pyaml-{version}.tar.gz' depends = ["setuptools"] site_packages_name = 'yaml' call_hostpython_via_targetpython = False recipe = PyamlRecipe() ================================================ FILE: pythonforandroid/recipes/pybind11/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe from os.path import join class Pybind11Recipe(PythonRecipe): version = '2.11.1' url = 'https://github.com/pybind/pybind11/archive/refs/tags/v{version}.zip' depends = ['setuptools'] call_hostpython_via_targetpython = False install_in_hostpython = True def get_include_dir(self, arch): return join(self.get_build_dir(arch.arch), 'include') recipe = Pybind11Recipe() ================================================ FILE: pythonforandroid/recipes/pycairo/__init__.py ================================================ from pythonforandroid.recipe import MesonRecipe from os.path import join class PyCairoRecipe(MesonRecipe): version = '1.28.0' url = 'https://github.com/pygobject/pycairo/releases/download/v{version}/pycairo-{version}.tar.gz' name = 'pycairo' site_packages_name = 'cairo' depends = ['libcairo'] patches = ["meson.patch"] def build_arch(self, arch): include_path = join(self.get_recipe('libcairo', self.ctx).get_build_dir(arch), "install", "include", "cairo") lib_path = self.ctx.get_libs_dir(arch.arch) self.extra_build_args += [ f'-Csetup-args=-Dcairo_include={include_path}', f'-Csetup-args=-Dcairo_lib={lib_path}', ] super().build_arch(arch) recipe = PyCairoRecipe() ================================================ FILE: pythonforandroid/recipes/pycairo/meson.patch ================================================ diff '--color=auto' -uNr pycairo-1.28.0/cairo/meson.build pycairo-1.28.0.mod/cairo/meson.build --- pycairo-1.28.0/cairo/meson.build 2025-04-15 00:22:30.000000000 +0530 +++ pycairo-1.28.0.mod/cairo/meson.build 2025-07-14 21:56:34.782983845 +0530 @@ -28,7 +28,10 @@ fs.copyfile(python_file, python_file) endforeach -cairo_dep = dependency('cairo', version: cair_version_req, required: cc.get_id() != 'msvc') +cairo_dep = declare_dependency( + include_directories: include_directories(get_option('cairo_include')), + link_args: ['-L' + get_option('cairo_lib'), '-lcairo'] +) if cc.get_id() == 'msvc' and not cairo_dep.found() if cc.has_header('cairo.h') diff '--color=auto' -uNr pycairo-1.28.0/meson_options.txt pycairo-1.28.0.mod/meson_options.txt --- pycairo-1.28.0/meson_options.txt 2025-04-15 00:22:30.000000000 +0530 +++ pycairo-1.28.0.mod/meson_options.txt 2025-07-14 21:56:52.824191314 +0530 @@ -1,3 +1,5 @@ option('python', type : 'string', value : 'python3') option('tests', type : 'boolean', value : true, description : 'build unit tests') option('wheel', type : 'boolean', value : false, description : 'build for a Python wheel') +option('cairo_include', type: 'string', value: '', description: 'Path to cairo headers') +option('cairo_lib', type: 'string', value: '', description: 'Path to cairo libraries') ================================================ FILE: pythonforandroid/recipes/pycparser/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class PycparserRecipe(PythonRecipe): name = 'pycparser' version = '2.14' url = 'https://pypi.python.org/packages/source/p/pycparser/pycparser-{version}.tar.gz' depends = ['setuptools'] call_hostpython_via_targetpython = False install_in_hostpython = True recipe = PycparserRecipe() ================================================ FILE: pythonforandroid/recipes/pycrypto/__init__.py ================================================ from pythonforandroid.recipe import CompiledComponentsPythonRecipe, Recipe from pythonforandroid.toolchain import ( current_directory, info, shprint, ) import sh class PyCryptoRecipe(CompiledComponentsPythonRecipe): version = '2.7a1' url = 'https://github.com/dlitz/pycrypto/archive/v{version}.zip' 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().get_recipe_env(arch) openssl_recipe = Recipe.get_recipe('openssl', self.ctx) env['CC'] = env['CC'] + openssl_recipe.include_flags(arch) env['LDFLAGS'] += ' -L{}'.format(self.ctx.get_libs_dir(arch.arch)) env['LDFLAGS'] += ' -L{}'.format(self.ctx.libs_dir) env['LDFLAGS'] += openssl_recipe.link_dirs_flags(arch) env['LIBS'] = env.get('LIBS', '') + openssl_recipe.link_libs_flags() env['EXTRA_CFLAGS'] = '--host linux-armv' env['ac_cv_func_malloc_0_nonnull'] = 'yes' return env def build_compiled_components(self, arch): info('Configuring compiled components in {}'.format(self.name)) env = self.get_recipe_env(arch) with current_directory(self.get_build_dir(arch.arch)): configure = sh.Command('./configure') shprint(configure, '--host=arm-eabi', '--prefix={}'.format(self.ctx.get_python_install_dir(arch.arch)), '--enable-shared', _env=env) super().build_compiled_components(arch) recipe = PyCryptoRecipe() ================================================ FILE: pythonforandroid/recipes/pycrypto/add_length.patch ================================================ --- pycrypto-2.6.1/src/hash_SHA2_template.c.orig 2013-10-14 14:38:10.000000000 -0700 +++ pycrypto-2.6.1/src/hash_SHA2_template.c 2014-05-19 10:15:51.000000000 -0700 @@ -87,7 +87,7 @@ * return 1 on success * return 0 if the length overflows */ -int add_length(hash_state *hs, sha2_word_t inc) { +static int add_length(hash_state *hs, sha2_word_t inc) { sha2_word_t overflow_detector; overflow_detector = hs->length_lower; hs->length_lower += inc; ================================================ FILE: pythonforandroid/recipes/pycryptodome/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe class PycryptodomeRecipe(PyProjectRecipe): version = '3.23.0' url = 'https://github.com/Legrandin/pycryptodome/archive/refs/tags/v{version}.tar.gz' depends = ['cffi'] recipe = PycryptodomeRecipe() ================================================ FILE: pythonforandroid/recipes/pydantic-core/__init__.py ================================================ from pythonforandroid.recipe import RustCompiledComponentsRecipe class PydanticcoreRecipe(RustCompiledComponentsRecipe): version = "2.41.4" url = "https://github.com/pydantic/pydantic-core/archive/refs/tags/v{version}.tar.gz" site_packages_name = "pydantic_core" recipe = PydanticcoreRecipe() ================================================ FILE: pythonforandroid/recipes/pygame/__init__.py ================================================ from os.path import join from pythonforandroid.recipe import CompiledComponentsPythonRecipe from pythonforandroid.toolchain import current_directory class Pygame2Recipe(CompiledComponentsPythonRecipe): """ Recipe to build apps based on SDL2-based pygame. .. warning:: Some pygame functionality is still untested, and some dependencies like freetype, postmidi and libjpeg are currently not part of the build. It's usable, but not complete. """ version = '2.1.0' url = 'https://github.com/pygame/pygame/archive/{version}.tar.gz' site_packages_name = 'pygame' name = 'pygame' depends = ['sdl2', 'sdl2_image', 'sdl2_mixer', 'sdl2_ttf', 'setuptools', 'jpeg', 'png'] call_hostpython_via_targetpython = False # Due to setuptools install_in_hostpython = False def prebuild_arch(self, arch): super().prebuild_arch(arch) with current_directory(self.get_build_dir(arch.arch)): setup_template = open(join("buildconfig", "Setup.Android.SDL2.in")).read() env = self.get_recipe_env(arch) env['ANDROID_ROOT'] = join(self.ctx.ndk.sysroot, 'usr') png = self.get_recipe('png', self.ctx) png_lib_dir = join(png.get_build_dir(arch.arch), '.libs') png_inc_dir = png.get_build_dir(arch) jpeg = self.get_recipe('jpeg', self.ctx) jpeg_inc_dir = jpeg_lib_dir = jpeg.get_build_dir(arch.arch) sdl_mixer_includes = "" sdl2_mixer_recipe = self.get_recipe('sdl2_mixer', self.ctx) for include_dir in sdl2_mixer_recipe.get_include_dirs(arch): sdl_mixer_includes += f"-I{include_dir} " sdl2_image_includes = "" sdl2_image_recipe = self.get_recipe('sdl2_image', self.ctx) for include_dir in sdl2_image_recipe.get_include_dirs(arch): sdl2_image_includes += f"-I{include_dir} " setup_file = setup_template.format( sdl_includes=( " -I" + join(self.ctx.bootstrap.build_dir, 'jni', 'SDL', 'include') + " -L" + join(self.ctx.bootstrap.build_dir, "libs", str(arch)) + " -L" + png_lib_dir + " -L" + jpeg_lib_dir + " -L" + arch.ndk_lib_dir_versioned), sdl_ttf_includes="-I"+join(self.ctx.bootstrap.build_dir, 'jni', 'SDL2_ttf'), sdl_image_includes=sdl2_image_includes, sdl_mixer_includes=sdl_mixer_includes, jpeg_includes="-I"+jpeg_inc_dir, png_includes="-I"+png_inc_dir, freetype_includes="" ) open("Setup", "w").write(setup_file) def get_recipe_env(self, arch): env = super().get_recipe_env(arch) env['USE_SDL2'] = '1' env["PYGAME_CROSS_COMPILE"] = "TRUE" env["PYGAME_ANDROID"] = "TRUE" return env recipe = Pygame2Recipe() ================================================ FILE: pythonforandroid/recipes/pyicu/__init__.py ================================================ from os.path import join from pythonforandroid.recipe import CppCompiledComponentsPythonRecipe class PyICURecipe(CppCompiledComponentsPythonRecipe): version = '1.9.2' url = ('https://pypi.python.org/packages/source/P/PyICU/' 'PyICU-{version}.tar.gz') depends = ["icu"] patches = ['locale.patch'] def get_recipe_env(self, arch): env = super().get_recipe_env(arch) icu_include = join( self.ctx.get_python_install_dir(arch.arch), "include", "icu") icu_recipe = self.get_recipe('icu', self.ctx) icu_link_libs = icu_recipe.built_libraries.keys() env["PYICU_LIBRARIES"] = ":".join(lib[3:-3] for lib in icu_link_libs) env["CPPFLAGS"] += " -I" + icu_include env["LDFLAGS"] += " -L" + join( icu_recipe.get_build_dir(arch.arch), "icu_build", "lib" ) return env recipe = PyICURecipe() ================================================ FILE: pythonforandroid/recipes/pyicu/locale.patch ================================================ diff -Naur locale.cpp locale1.cpp --- pyicu/locale.cpp 2015-04-29 07:32:39.000000000 +0200 +++ locale1.cpp 2016-05-12 17:13:08.990059346 +0200 @@ -27,7 +27,7 @@ #if defined(_MSC_VER) || defined(__WIN32) #include #else -#include +#include #include #include #endif ================================================ FILE: pythonforandroid/recipes/pyjnius/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe from pythonforandroid.toolchain import shprint, current_directory, info from pythonforandroid.patching import will_build import sh from os.path import join class PyjniusRecipe(PyProjectRecipe): version = '1.7.0' url = 'https://github.com/kivy/pyjnius/archive/{version}.zip' name = 'pyjnius' depends = [('genericndkbuild', 'sdl2', 'sdl3'), 'six'] site_packages_name = 'jnius' hostpython_prerequisites = ["Cython<3.2"] patches = [ "use_cython.patch", ('genericndkbuild_jnienv_getter.patch', will_build('genericndkbuild')), ('sdl3_jnienv_getter.patch', will_build('sdl3')), ] def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) # Taken from CythonRecipe env['LDFLAGS'] = env['LDFLAGS'] + ' -L{} '.format( self.ctx.get_libs_dir(arch.arch) + ' -L{} '.format(self.ctx.libs_dir) + ' -L{}'.format(join(self.ctx.bootstrap.build_dir, 'obj', 'local', arch.arch))) env['LDSHARED'] = env['CC'] + ' -shared' env['LIBLINK'] = 'NOTNONE' # NDKPLATFORM is our switch for detecting Android platform, so can't be None env['NDKPLATFORM'] = "NOTNONE" return env def postbuild_arch(self, 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) recipe = PyjniusRecipe() ================================================ FILE: pythonforandroid/recipes/pyjnius/genericndkbuild_jnienv_getter.patch ================================================ diff -Naur pyjnius.orig/jnius/env.py pyjnius/jnius/env.py --- pyjnius.orig/jnius/env.py 2022-05-28 11:16:02.000000000 +0200 +++ pyjnius/jnius/env.py 2022-05-28 11:18:30.000000000 +0200 @@ -268,7 +268,7 @@ class AndroidJavaLocation(UnixJavaLocation): def get_libraries(self): - return ['SDL2', 'log'] + return ['main', 'log'] def get_include_dirs(self): # When cross-compiling for Android, we should not use the include dirs diff -Naur pyjnius.orig/jnius/jnius_jvm_android.pxi pyjnius/jnius/jnius_jvm_android.pxi --- pyjnius.orig/jnius/jnius_jvm_android.pxi 2022-05-28 11:16:02.000000000 +0200 +++ pyjnius/jnius/jnius_jvm_android.pxi 2022-05-28 11:17:17.000000000 +0200 @@ -1,6 +1,6 @@ # on android, rely on SDL to get the JNI env -cdef extern JNIEnv *SDL_AndroidGetJNIEnv() +cdef extern JNIEnv *WebView_AndroidGetJNIEnv() cdef JNIEnv *get_platform_jnienv() except NULL: - return SDL_AndroidGetJNIEnv() + return WebView_AndroidGetJNIEnv() ================================================ FILE: pythonforandroid/recipes/pyjnius/sdl3_jnienv_getter.patch ================================================ diff -Naur pyjnius.orig/jnius/env.py pyjnius/jnius/env.py --- pyjnius.orig/jnius/env.py 2022-05-28 11:16:02.000000000 +0200 +++ pyjnius/jnius/env.py 2022-05-28 11:18:30.000000000 +0200 @@ -268,7 +268,7 @@ class AndroidJavaLocation(UnixJavaLocation): def get_libraries(self): - return ['SDL2', 'log'] + return ['SDL3', 'log'] def get_include_dirs(self): # When cross-compiling for Android, we should not use the include dirs diff -Naur pyjnius.orig/jnius/jnius_jvm_android.pxi pyjnius/jnius/jnius_jvm_android.pxi --- pyjnius.orig/jnius/jnius_jvm_android.pxi 2022-05-28 11:16:02.000000000 +0200 +++ pyjnius/jnius/jnius_jvm_android.pxi 2022-05-28 11:17:17.000000000 +0200 @@ -1,6 +1,6 @@ # on android, rely on SDL to get the JNI env -cdef extern JNIEnv *SDL_AndroidGetJNIEnv() +cdef extern JNIEnv *SDL_GetAndroidJNIEnv() cdef JNIEnv *get_platform_jnienv() except NULL: - return SDL_AndroidGetJNIEnv() + return SDL_GetAndroidJNIEnv() ================================================ FILE: pythonforandroid/recipes/pyjnius/use_cython.patch ================================================ --- pyjnius-1.6.1/setup.py 2023-11-05 21:07:43.000000000 +0530 +++ pyjnius-1.6.1.mod/setup.py 2025-03-01 14:47:11.964847337 +0530 @@ -59,10 +59,6 @@ if NDKPLATFORM is not None and getenv('LIBLINK'): PLATFORM = 'android' -# detect platform -if PLATFORM == 'android': - PYX_FILES = [fn[:-3] + 'c' for fn in PYX_FILES] - JAVA=get_java_setup(PLATFORM) assert JAVA.is_jdk(), "You need a JDK, we only found a JRE. Try setting JAVA_HOME" ================================================ FILE: pythonforandroid/recipes/pyleveldb/__init__.py ================================================ from pythonforandroid.recipe import CppCompiledComponentsPythonRecipe class PyLevelDBRecipe(CppCompiledComponentsPythonRecipe): version = '0.194' url = ('https://pypi.python.org/packages/source/l/leveldb/' 'leveldb-{version}.tar.gz') depends = ['snappy', 'leveldb', 'setuptools'] patches = ['bindings-only.patch'] site_packages_name = 'leveldb' def get_recipe_env(self, arch): env = super().get_recipe_env(arch) snappy_recipe = self.get_recipe('snappy', self.ctx) leveldb_recipe = self.get_recipe('leveldb', self.ctx) env["LDFLAGS"] += " -L" + snappy_recipe.get_build_dir(arch.arch) env["LDFLAGS"] += " -L" + leveldb_recipe.get_build_dir(arch.arch) env["SNAPPY_BUILD_PATH"] = snappy_recipe.get_build_dir(arch.arch) env["LEVELDB_BUILD_PATH"] = leveldb_recipe.get_build_dir(arch.arch) return env recipe = PyLevelDBRecipe() ================================================ FILE: pythonforandroid/recipes/pyleveldb/bindings-only.patch ================================================ This patch force to only build the python bindings, and to do so, we modify the setup.py file in oder that finds our compiled libraries (libleveldb.so and libsnappy.so) --- leveldb-0.194/setup.py.orig 2016-09-17 02:05:55.000000000 +0200 +++ leveldb-0.194/setup.py 2019-02-26 16:57:40.997435911 +0100 @@ -11,44 +11,25 @@ import platform import sys from setuptools import setup, Extension +from os import environ system, node, release, version, machine, processor = platform.uname() -common_flags = [ - '-I./leveldb/include', - '-I./leveldb', - '-I./snappy', +extra_compile_args = [ + '-I{}/include'.format(environ.get('LEVELDB_BUILD_PATH')), + '-I{}'.format(environ.get('LEVELDB_BUILD_PATH')), + '-I{}'.format(environ.get('SNAPPY_BUILD_PATH')), + '-I.', '-I.', - '-fno-builtin-memcmp', '-O2', '-fPIC', '-DNDEBUG', '-DSNAPPY', + '-pthread', + '-Wall', + '-D_REENTRANT', + '-DOS_ANDROID', ] -if system == 'Darwin': - extra_compile_args = common_flags + [ - '-DOS_MACOSX', - '-DLEVELDB_PLATFORM_POSIX', - '-Wno-error=unused-command-line-argument-hard-error-in-future', - ] -elif system == 'Linux': - extra_compile_args = common_flags + [ - '-pthread', - '-Wall', - '-DOS_LINUX', - '-DLEVELDB_PLATFORM_POSIX', - ] -elif system == 'SunOS': - extra_compile_args = common_flags + [ - '-pthread', - '-Wall', - '-DOS_SOLARIS', - '-DLEVELDB_PLATFORM_POSIX', - ] -else: - sys.stderr.write("Don't know how to compile leveldb for %s!\n" % system) - sys.exit(1) - setup( name = 'leveldb', version = '0.194', @@ -81,57 +62,11 @@ setup( ext_modules = [ Extension('leveldb', sources = [ - # snappy - './snappy/snappy.cc', - './snappy/snappy-stubs-internal.cc', - './snappy/snappy-sinksource.cc', - './snappy/snappy-c.cc', - - #leveldb - 'leveldb/db/builder.cc', - 'leveldb/db/c.cc', - 'leveldb/db/db_impl.cc', - 'leveldb/db/db_iter.cc', - 'leveldb/db/dbformat.cc', - 'leveldb/db/filename.cc', - 'leveldb/db/log_reader.cc', - 'leveldb/db/log_writer.cc', - 'leveldb/db/memtable.cc', - 'leveldb/db/repair.cc', - 'leveldb/db/table_cache.cc', - 'leveldb/db/version_edit.cc', - 'leveldb/db/version_set.cc', - 'leveldb/db/write_batch.cc', - 'leveldb/table/block.cc', - 'leveldb/table/block_builder.cc', - 'leveldb/table/filter_block.cc', - 'leveldb/table/format.cc', - 'leveldb/table/iterator.cc', - 'leveldb/table/merger.cc', - 'leveldb/table/table.cc', - 'leveldb/table/table_builder.cc', - 'leveldb/table/two_level_iterator.cc', - 'leveldb/util/arena.cc', - 'leveldb/util/bloom.cc', - 'leveldb/util/cache.cc', - 'leveldb/util/coding.cc', - 'leveldb/util/comparator.cc', - 'leveldb/util/crc32c.cc', - 'leveldb/util/env.cc', - 'leveldb/util/env_posix.cc', - 'leveldb/util/filter_policy.cc', - 'leveldb/util/hash.cc', - 'leveldb/util/histogram.cc', - 'leveldb/util/logging.cc', - 'leveldb/util/options.cc', - 'leveldb/util/status.cc', - 'leveldb/port/port_posix.cc', - # python stuff 'leveldb_ext.cc', 'leveldb_object.cc', ], - libraries = ['stdc++'], + libraries = ['snappy', 'leveldb', 'stdc++', 'c++_shared'], extra_compile_args = extra_compile_args, ) ] ================================================ FILE: pythonforandroid/recipes/pymunk/__init__.py ================================================ from pythonforandroid.recipe import CompiledComponentsPythonRecipe class PymunkRecipe(CompiledComponentsPythonRecipe): name = "pymunk" version = "6.0.0" url = "https://pypi.python.org/packages/source/p/pymunk/pymunk-{version}.zip" depends = ["cffi", "setuptools"] call_hostpython_via_targetpython = False def get_recipe_env(self, arch): env = super().get_recipe_env(arch) env["LDFLAGS"] += " -llog" # Used by Chipmunk cpMessage env["LDFLAGS"] += " -lm" # For older versions of Android return env recipe = PymunkRecipe() ================================================ FILE: pythonforandroid/recipes/pynacl/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe import os class PyNaCLRecipe(PyProjectRecipe): name = 'pynacl' version = '1.3.0' url = 'https://github.com/pyca/pynacl/archive/refs/tags/{version}.tar.gz' depends = ['hostpython3', 'six', 'setuptools', 'cffi', 'libsodium'] call_hostpython_via_targetpython = False hostpython_prerequisites = ["cffi>=2.0.0"] def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) env['SODIUM_INSTALL'] = 'system' libsodium_build_dir = self.get_recipe( 'libsodium', self.ctx ).get_build_dir(arch.arch) env['CFLAGS'] += ' -I{}'.format( os.path.join(libsodium_build_dir, 'src/libsodium/include') ) for ldflag in [ self.ctx.get_libs_dir(arch.arch), self.ctx.libs_dir, libsodium_build_dir ]: env['LDFLAGS'] += ' -L{}'.format(ldflag) return env recipe = PyNaCLRecipe() ================================================ FILE: pythonforandroid/recipes/pyogg/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe from os.path import join class PyOggRecipe(PythonRecipe): version = '0.6.4a1' url = 'https://files.pythonhosted.org/packages/source/p/pyogg/PyOgg-{version}.tar.gz' depends = ['libogg', 'libvorbis', 'setuptools'] patches = [join('patches', 'fix-find-lib.patch')] call_hostpython_via_targetpython = False recipe = PyOggRecipe() ================================================ FILE: pythonforandroid/recipes/pyogg/patches/fix-find-lib.patch ================================================ diff --git a/pyogg/library_loader.py b/pyogg/library_loader.py index c2ba36c..383331a 100644 --- a/pyogg/library_loader.py +++ b/pyogg/library_loader.py @@ -54,7 +54,7 @@ def load_other(name, paths = None): except: pass else: - for path in [os.getcwd(), _here]: + for path in [os.path.join(os.environ['ANDROID_PRIVATE'], '..', 'lib')]: for style in other_styles: candidate = os.path.join(path, style.format(name)) if os.path.exists(candidate): ================================================ FILE: pythonforandroid/recipes/pyopenal/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe from os.path import join class PyOpenALRecipe(PythonRecipe): version = '0.7.3a1' url = 'https://files.pythonhosted.org/packages/source/p/pyopenal/PyOpenAL-{version}.tar.gz' depends = ['openal', 'numpy', 'setuptools'] patches = [join('patches', 'fix-find-lib.patch')] call_hostpython_via_targetpython = False recipe = PyOpenALRecipe() ================================================ FILE: pythonforandroid/recipes/pyopenal/patches/fix-find-lib.patch ================================================ diff --git a/openal/library_loader.py b/openal/library_loader.py index be2485c..e8c6cd2 100644 --- a/openal/library_loader.py +++ b/openal/library_loader.py @@ -56,7 +56,7 @@ class ExternalLibrary: except: pass else: - for path in [os.getcwd(), _here]: + for path in [os.path.join(os.environ['ANDROID_PRIVATE'], '..', 'lib')]: for style in ExternalLibrary.other_styles: candidate = os.path.join(path, style.format(name)) if os.path.exists(candidate) and os.path.isfile(candidate): ================================================ FILE: pythonforandroid/recipes/pyopenssl/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class PyOpenSSLRecipe(PythonRecipe): version = '24.1.0' url = 'https://pypi.python.org/packages/source/p/pyOpenSSL/pyOpenSSL-{version}.tar.gz' depends = ['cffi', 'openssl', 'setuptools'] site_packages_name = 'OpenSSL' call_hostpython_via_targetpython = False recipe = PyOpenSSLRecipe() ================================================ FILE: pythonforandroid/recipes/pyopenssl/fix-dlfcn.patch ================================================ --- pyOpenSSL-0.13.orig/OpenSSL/__init__.py 2011-09-02 17:46:13.000000000 +0200 +++ pyOpenSSL-0.13/OpenSSL/__init__.py 2013-07-29 17:20:15.750079894 +0200 @@ -12,6 +12,11 @@ except AttributeError: from OpenSSL import crypto else: + # XXX android fix + # linux: RTLD_NOW (0x2) | RTLD_GLOBAL (0x100 / 256) + # android: RTLD_NOW (0x0) | RTLD_GLOBAL (0x2) + flags = 0x2 + ''' try: import DLFCN except ImportError: @@ -31,6 +36,7 @@ else: flags = DLFCN.RTLD_NOW | DLFCN.RTLD_GLOBAL del DLFCN + ''' sys.setdlopenflags(flags) from OpenSSL import crypto ================================================ FILE: pythonforandroid/recipes/pyproj/__init__.py ================================================ from pythonforandroid.recipe import CythonRecipe class PyProjRecipe(CythonRecipe): version = '1.9.6' url = 'https://github.com/pyproj4/pyproj/archive/v{version}rel.zip' depends = ['setuptools'] call_hostpython_via_targetpython = False recipe = PyProjRecipe() ================================================ FILE: pythonforandroid/recipes/pyrxp/__init__.py ================================================ from pythonforandroid.recipe import CompiledComponentsPythonRecipe class PyRXPURecipe(CompiledComponentsPythonRecipe): version = '2a02cecc87b9' url = 'https://bitbucket.org/rptlab/pyrxp/get/{version}.tar.gz' depends = [] patches = [] recipe = PyRXPURecipe() ================================================ FILE: pythonforandroid/recipes/pysdl2/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class PySDL2Recipe(PythonRecipe): version = '0.9.6' url = 'https://files.pythonhosted.org/packages/source/P/PySDL2/PySDL2-{version}.tar.gz' depends = ['sdl2'] recipe = PySDL2Recipe() ================================================ FILE: pythonforandroid/recipes/pysha3/__init__.py ================================================ import os from pythonforandroid.recipe import PythonRecipe # TODO: CompiledComponentsPythonRecipe class Pysha3Recipe(PythonRecipe): version = '1.0.2' url = 'https://github.com/tiran/pysha3/archive/{version}.tar.gz' depends = ['setuptools'] call_hostpython_via_targetpython = False def get_recipe_env(self, arch=None, with_flags_in_cc=True): 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'] = '' # LDFLAGS may only be used to specify linker flags, for libraries use LIBS env['LDFLAGS'] = env['LDFLAGS'].replace('-lm', '') env['LDFLAGS'] += ' -L{}'.format(os.path.join(self.ctx.bootstrap.build_dir, 'libs', arch.arch)) env['LIBS'] = ' -lm' env['LDSHARED'] += env['LIBS'] return env recipe = Pysha3Recipe() ================================================ FILE: pythonforandroid/recipes/python3/__init__.py ================================================ import glob import sh import subprocess from os import environ, utime from os.path import dirname, exists, join, isfile import shutil from packaging.version import Version from pythonforandroid.logger import info, shprint, warning from pythonforandroid.recipe import Recipe, TargetPythonRecipe from pythonforandroid.util import ( current_directory, ensure_dir, walk_valid_filens, BuildInterruptingException, ) NDK_API_LOWER_THAN_SUPPORTED_MESSAGE = ( 'Target ndk-api is {ndk_api}, ' 'but the python3 recipe supports only {min_ndk_api}+' ) class Python3Recipe(TargetPythonRecipe): ''' The python3's recipe ^^^^^^^^^^^^^^^^^^^^ The python 3 recipe can be built with some extra python modules, but to do so, we need some libraries. By default, we ship the python3 recipe with some common libraries, defined in ``depends``. We also support some optional libraries, which are less common that the ones defined in ``depends``, so we added them as optional dependencies (``opt_depends``). Below you have a relationship between the python modules and the recipe libraries:: - _ctypes: you must add the recipe for ``libffi``. - _sqlite3: you must add the recipe for ``sqlite3``. - _ssl: you must add the recipe for ``openssl``. - _bz2: you must add the recipe for ``libbz2`` (optional). - _lzma: you must add the recipe for ``liblzma`` (optional). .. note:: This recipe can be built only against API 21+. .. versionchanged:: 2019.10.06.post0 - 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` ''' version = '3.14.2' url = 'https://github.com/python/cpython/archive/refs/tags/v{version}.tar.gz' name = 'python3' patches = [ 'patches/pyconfig_detection.patch', 'patches/reproducible-buildinfo.diff', ] depends = ['hostpython3', 'sqlite3', 'openssl', 'libffi'] # those optional depends allow us to build python compression modules: # - _bz2.so # - _lzma.so opt_depends = ['libbz2', 'liblzma'] '''The optional libraries which we would like to get our python linked''' configure_args = [ '--host={android_host}', '--build={android_build}', '--enable-shared', '--enable-ipv6', '--enable-loadable-sqlite-extensions', '--without-static-libpython', '--without-readline', '--without-ensurepip', # Android prefix '--prefix={prefix}', '--exec-prefix={exec_prefix}', '--enable-loadable-sqlite-extensions', # Special cross compile args 'ac_cv_file__dev_ptmx=yes', 'ac_cv_file__dev_ptc=no', 'ac_cv_header_sys_eventfd_h=no', 'ac_cv_little_endian_double=yes', 'ac_cv_header_bzlib_h=no', ] '''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_excluded_dir_exceptions = [ # 'numpy' is excluded here because importing with `import numpy as np` # can fail if the `tests` directory inside the numpy package is excluded. 'numpy', ] '''Directories from `site_packages_dir_blacklist` will not be excluded if the full path contains any of these exceptions.''' 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 ''' disable_gil = False '''python3.13 experimental free-threading build''' built_libraries = {"libpythonbin.so": "./android-build/"} def __init__(self, *args, **kwargs): self._ctx = None super().__init__(*args, **kwargs) @property def _libpython(self): '''return the python's library name (with extension)''' return 'libpython{link_version}.so'.format( link_version=self.link_version ) @property def link_version(self): '''return the python's library link version e.g. 3.7m, 3.8''' major, minor = self.major_minor_version_string.split('.') flags = '' if major == '3' and int(minor) < 8: flags += 'm' return '{major}.{minor}{flags}'.format( major=major, minor=minor, flags=flags ) def apply_patches(self, arch, build_dir=None): _p_version = Version(self.version) if _p_version.major == 3 and _p_version.minor == 7: self.patches += [ 'patches/py3.7.1_fix-ctypes-util-find-library.patch', 'patches/py3.7.1_fix-zlib-version.patch', ] if 8 <= _p_version.minor <= 10: self.patches.append('patches/py3.8.1.patch') if _p_version.minor >= 11: self.patches.append('patches/cpython-311-ctypes-find-library.patch') if _p_version.minor >= 14: self.patches.append('patches/3.14_armv7l_fix.patch') self.patches.append('patches/3.14_fix_remote_debug.patch') if shutil.which('lld') is not None: if _p_version.minor == 7: self.patches.append("patches/py3.7.1_fix_cortex_a8.patch") elif _p_version.minor >= 8: self.patches.append("patches/py3.8.1_fix_cortex_a8.patch") self.patches = list(set(self.patches)) super().apply_patches(arch, build_dir) 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 = super().get_recipe_env(arch) 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' ] ) env['LDFLAGS'] = env.get('LDFLAGS', '') if shutil.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 info('Activating flags for sqlite3') recipe = Recipe.get_recipe('sqlite3', self.ctx) add_flags(' -I' + recipe.get_build_dir(arch.arch), ' -L' + recipe.get_build_dir(arch.arch), ' -lsqlite3') 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_LIBDIR'] = 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') info('Activating flags for openssl') recipe = Recipe.get_recipe('openssl', self.ctx) self.configure_args.append('--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 = arch.ndk_lib_dir_versioned zlib_includes = self.ctx.ndk.sysroot_include_dir 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') _p_version = Version(self.version) if _p_version.minor >= 11: self.configure_args.append('--with-build-python={python_host_bin}') if _p_version.minor >= 13 and self.disable_gil: self.configure_args.append("--disable-gil") self.configure_args = list(set(self.configure_args)) return env def build_arch(self, arch): if self.ctx.ndk_api < self.MIN_NDK_API: raise BuildInterruptingException( NDK_API_LOWER_THAN_SUPPORTED_MESSAGE.format( ndk_api=self.ctx.ndk_api, min_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' env = self.get_recipe_env(arch) env = self.set_libs_flags(env, arch) android_build = sh.Command( join(recipe_build_dir, 'config.guess'))().strip() with current_directory(build_dir): 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, python_host_bin=join(self.get_recipe( 'host' + self.name, self.ctx ).get_path_to_python(), "python3"), prefix=sys_prefix, exec_prefix=sys_exec_prefix)).split(' '), _env=env) shprint( sh.make, 'all', 'INSTSONAME={lib_name}'.format(lib_name=self._libpython), _env=env ) # rename executable if isfile("python"): sh.cp('python', 'libpythonbin.so') elif isfile("python.exe"): # for macos sh.cp('python.exe', 'libpythonbin.so') # 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. """ modules_build_dir = glob.glob(join( self.get_build_dir(arch.arch), 'android-build', 'build', 'lib.*' ))[0] # Compile to *.pyc the python modules self.compile_python_files(modules_build_dir) # Compile to *.pyc the standard python library self.compile_python_files(join(self.get_build_dir(arch.arch), 'Lib')) # Compile to *.pyc the other python packages (site-packages) self.compile_python_files(self.ctx.get_python_install_dir(arch.arch)) # 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)) shutil.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)) if 'SOURCE_DATE_EPOCH' in environ: # for reproducible builds stdlib_filens.sort() timestamp = int(environ['SOURCE_DATE_EPOCH']) for filen in stdlib_filens: utime(filen, (timestamp, timestamp)) info("Zip {} files into the bundle".format(len(stdlib_filens))) shprint(sh.zip, '-X', stdlib_zip, *stdlib_filens) # copy the site-packages into place ensure_dir(join(dirn, 'site-packages')) ensure_dir(self.ctx.get_python_install_dir(arch.arch)) # TODO: Improve the API around walking and copying the files with current_directory(self.ctx.get_python_install_dir(arch.arch)): filens = list(walk_valid_filens( '.', self.site_packages_dir_blacklist, self.site_packages_filen_blacklist, excluded_dir_exceptions=self.site_packages_excluded_dir_exceptions)) 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))) shutil.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.link_version 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() ================================================ FILE: pythonforandroid/recipes/python3/patches/3.14_armv7l_fix.patch ================================================ diff '--color=auto' -uNr cpython-3.14.0/Lib/sysconfig/__init__.py cpython-3.14.0.mod/Lib/sysconfig/__init__.py --- cpython-3.14.0/Lib/sysconfig/__init__.py 2025-10-07 21:45:41.236149298 +0530 +++ cpython-3.14.0.mod/Lib/sysconfig/__init__.py 2025-10-07 21:45:54.650245131 +0530 @@ -702,7 +702,7 @@ "x86_64": "x86_64", "i686": "x86", "aarch64": "arm64_v8a", - "armv7l": "armeabi_v7a", + "arm": "armeabi_v7a", }[machine] elif osname == "linux": # At least on Linux/Intel, 'machine' is the processor -- ================================================ FILE: pythonforandroid/recipes/python3/patches/3.14_fix_remote_debug.patch ================================================ diff '--color=auto' -uNr cpython-3.14.2/Modules/_remote_debugging_module.c cpython-3.14.2.mod/Modules/_remote_debugging_module.c --- cpython-3.14.2/Modules/_remote_debugging_module.c 2025-12-05 22:19:16.000000000 +0530 +++ cpython-3.14.2.mod/Modules/_remote_debugging_module.c 2025-12-13 20:22:44.011497868 +0530 @@ -812,7 +812,9 @@ PyErr_SetString(PyExc_RuntimeError, "Failed to find the AsyncioDebug section in the process."); _PyErr_ChainExceptions1(exc); } -#elif defined(__linux__) + +// https://github.com/python/cpython/commit/1963e701001839389cfb1b11d803b0743f4705d7 +#elif defined(__linux__) && HAVE_PROCESS_VM_READV // On Linux, search for asyncio debug in executable or DLL address = search_linux_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython"); if (address == 0) { diff '--color=auto' -uNr cpython-3.14.2/Python/remote_debug.h cpython-3.14.2.mod/Python/remote_debug.h --- cpython-3.14.2/Python/remote_debug.h 2025-12-05 22:19:16.000000000 +0530 +++ cpython-3.14.2.mod/Python/remote_debug.h 2025-12-13 20:23:27.917518543 +0530 @@ -881,7 +881,9 @@ handle->pid); _PyErr_ChainExceptions1(exc); } -#elif defined(__linux__) + +// https://github.com/python/cpython/commit/1963e701001839389cfb1b11d803b0743f4705d7 +#elif defined(__linux__) && HAVE_PROCESS_VM_READV // On Linux, search for 'python' in executable or DLL address = search_linux_map_for_section(handle, "PyRuntime", "python"); if (address == 0) { ================================================ FILE: pythonforandroid/recipes/python3/patches/cpython-311-ctypes-find-library.patch ================================================ --- Python-3.11.5/Lib/ctypes/util.py 2023-08-24 17:39:18.000000000 +0530 +++ Python-3.11.5.mod/Lib/ctypes/util.py 2023-11-18 22:12:17.356160615 +0530 @@ -4,7 +4,15 @@ import sys # find_library(name) returns the pathname of a library, or None. -if os.name == "nt": + +# 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 == "nt": def _get_build_version(): """Return the version of MSVC that was used to build Python. ================================================ FILE: pythonforandroid/recipes/python3/patches/py3.7.1_fix-ctypes-util-find-library.patch ================================================ diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py @@ -67,4 +67,11 @@ return fname return None +# 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": ================================================ FILE: pythonforandroid/recipes/python3/patches/py3.7.1_fix-zlib-version.patch ================================================ --- 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 @@ -1410,7 +1410,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: ================================================ FILE: pythonforandroid/recipes/python3/patches/py3.7.1_fix_cortex_a8.patch ================================================ This patch removes --fix-cortex-a8 from the linker flags in order to support linking with lld, as lld does not support this flag (https://github.com/android-ndk/ndk/issues/766). diff --git a/configure b/configure --- a/configure +++ b/configure @@ -5671,7 +5671,7 @@ $as_echo_n "checking for the Android arm ABI... " >&6; } $as_echo "$_arm_arch" >&6; } if test "$_arm_arch" = 7; then BASECFLAGS="${BASECFLAGS} -mfloat-abi=softfp -mfpu=vfpv3-d16" - LDFLAGS="${LDFLAGS} -march=armv7-a -Wl,--fix-cortex-a8" + LDFLAGS="${LDFLAGS} -march=armv7-a" fi else { $as_echo "$as_me:${as_lineno-$LINENO}: result: not Android" >&5 ================================================ FILE: pythonforandroid/recipes/python3/patches/py3.8.1.patch ================================================ diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py index 97973bc..053c231 100644 --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py @@ -67,6 +67,13 @@ if os.name == "nt": return fname return None +# 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): diff --git a/configure b/configure index 0914e24..dd00812 100755 --- a/configure +++ b/configure @@ -18673,4 +18673,3 @@ if test "$Py_OPT" = 'false' -a "$Py_DEBUG" != 'true'; then echo "" >&6 echo "" >&6 fi - diff --git a/setup.py b/setup.py index 20d7f35..af15cc2 100644 --- a/setup.py +++ b/setup.py @@ -1501,7 +1501,9 @@ 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 = '"1.1.3"' + version_req = '"{}"'.format( + os.environ.get('ZLIB_VERSION', '1.1.3')) if MACOS and is_macosx_sdk_path(zlib_h): zlib_h = os.path.join(macosx_sdk_root(), zlib_h[1:]) with open(zlib_h) as fp: ================================================ FILE: pythonforandroid/recipes/python3/patches/py3.8.1_fix_cortex_a8.patch ================================================ This patch removes --fix-cortex-a8 from the linker flags in order to support linking with lld, as lld does not support this flag (https://github.com/android-ndk/ndk/issues/766). diff --git a/configure b/configure index 0914e24..7517168 100755 --- a/configure +++ b/configure @@ -5642,7 +5642,7 @@ $as_echo_n "checking for the Android arm ABI... " >&6; } $as_echo "$_arm_arch" >&6; } if test "$_arm_arch" = 7; then BASECFLAGS="${BASECFLAGS} -mfloat-abi=softfp -mfpu=vfpv3-d16" - LDFLAGS="${LDFLAGS} -march=armv7-a -Wl,--fix-cortex-a8" + LDFLAGS="${LDFLAGS} -march=armv7-a" fi else { $as_echo "$as_me:${as_lineno-$LINENO}: result: not Android" >&5 ================================================ FILE: pythonforandroid/recipes/python3/patches/pyconfig_detection.patch ================================================ diff -Nru Python-3.8.2/Lib/site.py Python-3.8.2-new/Lib/site.py --- Python-3.8.2/Lib/site.py 2020-04-28 12:48:38.000000000 -0700 +++ Python-3.8.2-new/Lib/site.py 2020-04-28 12:52:46.000000000 -0700 @@ -487,7 +487,8 @@ if key == 'include-system-site-packages': system_site = value.lower() elif key == 'home': - sys._home = value + # this is breaking pyconfig.h path detection with venv + print('Ignoring "sys._home = value" override') sys.prefix = sys.exec_prefix = site_prefix ================================================ FILE: pythonforandroid/recipes/python3/patches/reproducible-buildinfo.diff ================================================ # DP: Build getbuildinfo.o with DATE/TIME values when defined --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -785,6 +785,8 @@ Modules/getbuildinfo.o: $(PARSER_OBJS) \ -DGITVERSION="\"`LC_ALL=C $(GITVERSION)`\"" \ -DGITTAG="\"`LC_ALL=C $(GITTAG)`\"" \ -DGITBRANCH="\"`LC_ALL=C $(GITBRANCH)`\"" \ + $(if $(BUILD_DATE),-DDATE='"$(BUILD_DATE)"') \ + $(if $(BUILD_TIME),-DTIME='"$(BUILD_TIME)"') \ -o $@ $(srcdir)/Modules/getbuildinfo.c Modules/getpath.o: $(srcdir)/Modules/getpath.c Makefile ================================================ FILE: pythonforandroid/recipes/pyusb/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class PyusbRecipe(PythonRecipe): name = 'pyusb' version = '1.0.0b1' url = 'https://pypi.python.org/packages/source/p/pyusb/pyusb-{version}.tar.gz' depends = [] site_packages_name = 'usb' patches = ['fix-android.patch'] recipe = PyusbRecipe() ================================================ FILE: pythonforandroid/recipes/pyusb/fix-android.patch ================================================ --- pyusb-1.0.0b1.orig/usb/backend/libusb1.py 2013-10-21 12:56:10.000000000 -0500 +++ pyusb-1.0.0b1/usb/backend/libusb1.py 2014-12-08 16:49:07.141514148 -0600 @@ -265,13 +265,7 @@ def _load_library(): if sys.platform != 'cygwin': - candidates = ('usb-1.0', 'libusb-1.0', 'usb') - for candidate in candidates: - if sys.platform == 'win32': - candidate = candidate + '.dll' - - libname = ctypes.util.find_library(candidate) - if libname is not None: break + libname = '/system/lib/libusb1.0.so' else: # corner cases # cygwin predefines library names with 'cyg' instead of 'lib' @@ -672,16 +666,21 @@ # implementation of libusb 1.0 backend class _LibUSB(usb.backend.IBackend): + + ran_init = False + @methodtrace(_logger) def __init__(self, lib): usb.backend.IBackend.__init__(self) self.lib = lib self.ctx = c_void_p() _check(self.lib.libusb_init(byref(self.ctx))) + self.ran_init = True @methodtrace(_logger) def __del__(self): - self.lib.libusb_exit(self.ctx) + if self.ran_init is True: + self.lib.libusb_exit(self.ctx) @methodtrace(_logger) ================================================ FILE: pythonforandroid/recipes/pyzbar/__init__.py ================================================ from os.path import join from pythonforandroid.recipe import PythonRecipe class PyZBarRecipe(PythonRecipe): version = '0.1.9' url = 'https://github.com/NaturalHistoryMuseum/pyzbar/archive/v{version}.tar.gz' # noqa call_hostpython_via_targetpython = False depends = ['setuptools', 'libzbar'] def get_recipe_env(self, arch=None, with_flags_in_cc=True): 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(arch.arch) env['CFLAGS'] += ' -I' + join(libzbar_dir, 'include') env['LDFLAGS'] += ' -L' + join(libzbar_dir, 'zbar', '.libs') env['LIBS'] = env.get('LIBS', '') + ' -landroid -lzbar' return env recipe = PyZBarRecipe() ================================================ FILE: pythonforandroid/recipes/pyzmq/__init__.py ================================================ # coding=utf-8 from pythonforandroid.recipe import CythonRecipe, Recipe from os.path import join from pythonforandroid.util import current_directory import sh from pythonforandroid.logger import shprint import glob class PyZMQRecipe(CythonRecipe): name = 'pyzmq' version = '20.0.0' url = 'https://github.com/zeromq/pyzmq/archive/v{version}.zip' site_packages_name = 'zmq' depends = ['setuptools', 'libzmq'] cython_args = ['-Izmq/utils', '-Izmq/backend/cython', '-Izmq/devices'] def get_recipe_env(self, arch=None): 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) # env['PYTHONPATH'] = ( # join(hostpython.get_build_dir(arch.arch), 'build', # 'lib.linux-x86_64-2.7') + ':' + env.get('PYTHONPATH', '') # ) # env["LDSHARED"] = env["CC"] + ' -shared' return env def build_cython_components(self, arch): libzmq_recipe = Recipe.get_recipe('libzmq', self.ctx) libzmq_prefix = join(libzmq_recipe.get_build_dir(arch.arch), "install") self.setup_extra_args = ["--zmq={}".format(libzmq_prefix)] self.build_cmd = "configure" env = self.get_recipe_env(arch) setup_cfg = join(self.get_build_dir(arch.arch), "setup.cfg") with open(setup_cfg, "wb") as fd: fd.write(""" [global] zmq_prefix = {} skip_check_zmq = True """.format(libzmq_prefix).encode()) return super().build_cython_components(arch) with current_directory(self.get_build_dir(arch.arch)): hostpython = sh.Command(self.hostpython_location) shprint(hostpython, 'setup.py', 'configure', '-v', _env=env) shprint(hostpython, 'setup.py', 'build_ext', '-v', _env=env) build_dir = glob.glob('build/lib.*')[0] shprint(sh.find, build_dir, '-name', '"*.o"', '-exec', env['STRIP'], '{}', ';', _env=env) recipe = PyZMQRecipe() ================================================ FILE: pythonforandroid/recipes/regex/__init__.py ================================================ from pythonforandroid.recipe import CompiledComponentsPythonRecipe class RegexRecipe(CompiledComponentsPythonRecipe): name = 'regex' version = '2019.06.08' url = 'https://pypi.python.org/packages/source/r/regex/regex-{version}.tar.gz' # noqa depends = ['setuptools'] call_hostpython_via_targetpython = False recipe = RegexRecipe() ================================================ FILE: pythonforandroid/recipes/reportlab/__init__.py ================================================ import os import sh from pythonforandroid.logger import info from pythonforandroid.recipe import CompiledComponentsPythonRecipe from pythonforandroid.util import current_directory, ensure_dir, touch class ReportLabRecipe(CompiledComponentsPythonRecipe): version = 'fe660f227cac' url = 'https://hg.reportlab.com/hg-public/reportlab/archive/{version}.tar.gz' depends = ['freetype'] call_hostpython_via_targetpython = False def prebuild_arch(self, arch): if not self.is_patched(arch): super().prebuild_arch(arch) recipe_dir = self.get_build_dir(arch.arch) # Some versions of reportlab ship with a GPL-licensed font. # Remove it, since this is problematic in .apks unless the # entire app is GPL: font_dir = os.path.join(recipe_dir, "src", "reportlab", "fonts") if os.path.exists(font_dir): for file in os.listdir(font_dir): if file.lower().startswith('darkgarden'): os.remove(os.path.join(font_dir, file)) # Apply patches: self.apply_patch('patches/fix-setup.patch', arch.arch) touch(os.path.join(recipe_dir, '.patched')) ft = self.get_recipe('freetype', self.ctx) ft_dir = ft.get_build_dir(arch.arch) ft_lib_dir = os.environ.get('_FT_LIB_', os.path.join(ft_dir, 'objs', '.libs')) ft_inc_dir = os.environ.get('_FT_INC_', os.path.join(ft_dir, 'include')) tmp_dir = os.path.normpath(os.path.join(recipe_dir, "..", "..", "tmp")) info('reportlab recipe: recipe_dir={}'.format(recipe_dir)) info('reportlab recipe: tmp_dir={}'.format(tmp_dir)) info('reportlab recipe: ft_dir={}'.format(ft_dir)) info('reportlab recipe: ft_lib_dir={}'.format(ft_lib_dir)) info('reportlab recipe: ft_inc_dir={}'.format(ft_inc_dir)) with current_directory(recipe_dir): ensure_dir(tmp_dir) pfbfile = os.path.join(tmp_dir, "pfbfer-20070710.zip") if not os.path.isfile(pfbfile): sh.wget("http://www.reportlab.com/ftp/pfbfer-20070710.zip", "-O", pfbfile) sh.unzip("-u", "-d", os.path.join(recipe_dir, "src", "reportlab", "fonts"), pfbfile) if os.path.isfile("setup.py"): with open('setup.py', 'r') as f: text = f.read().replace('_FT_LIB_', ft_lib_dir).replace('_FT_INC_', ft_inc_dir) with open('setup.py', 'w') as f: f.write(text) recipe = ReportLabRecipe() ================================================ FILE: pythonforandroid/recipes/reportlab/patches/fix-setup.patch ================================================ diff -r 9ecdf084933c setup.py --- a/setup.py Wed May 13 14:09:03 2015 +0100 +++ b/setup.py Fri May 22 10:14:29 2015 +0100 @@ -14,8 +14,8 @@ #no-download-t1-files=yes #ignore-system-libart=yes # if used on command line the config values are not used -dlt1 = not specialOption('--no-download-t1-files') -isla = specialOption('--ignore-system-libart') +dlt1 = False +isla = True try: import configparser @@ -121,39 +121,6 @@ else: P.insert(x, d) -class inc_lib_dirs: - L = None - I = None - def __call__(self): - if self.L is None: - L = [] - I = [] - if platform == "cygwin": - aDir(L, os.path.join("/usr/lib", "python%s" % sys.version[:3], "config")) - elif platform == "darwin": - # attempt to make sure we pick freetype2 over other versions - aDir(I, "/sw/include/freetype2") - aDir(I, "/sw/lib/freetype2/include") - # fink installation directories - aDir(L, "/sw/lib") - aDir(I, "/sw/include") - # darwin ports installation directories - aDir(L, "/opt/local/lib") - aDir(I, "/opt/local/include") - aDir(I, "/usr/local/include") - aDir(L, "/usr/local/lib") - aDir(I, "/usr/include") - aDir(L, "/usr/lib") - aDir(I, "/usr/include/freetype2") - prefix = sysconfig.get_config_var("prefix") - if prefix: - aDir(L, pjoin(prefix, "lib")) - aDir(I, pjoin(prefix, "include")) - self.L=L - self.I=I - return self.I,self.L -inc_lib_dirs=inc_lib_dirs() - def getVersionFromCCode(fn): import re tag = re.search(r'^#define\s+VERSION\s+"([^"]*)"',open(fn,'r').read(),re.M) @@ -244,11 +211,7 @@ ] def get_fonts(PACKAGE_DIR, reportlab_files): - import sys, os, os.path, zipfile, io - if isPy3: - import urllib.request as ureq - else: - import urllib2 as ureq + import os, os.path rl_dir = PACKAGE_DIR['reportlab'] if not [x for x in reportlab_files if not os.path.isfile(pjoin(rl_dir,x))]: infoline("Standard T1 font curves already downloaded") @@ -257,6 +220,11 @@ infoline('not downloading T1 font curve files') return try: + if isPy3: + import urllib.request as ureq + else: + import urllib2 as ureq + import zipfile, io infoline("Downloading standard T1 font curves") remotehandle = ureq.urlopen("http://www.reportlab.com/ftp/pfbfer-20070710.zip") @@ -448,7 +416,8 @@ FT_LIB_DIR=[FT_LIB_DIR] if FT_LIB_DIR else [] FT_INC_DIR=config('FREETYPE_PATHS','inc') FT_INC_DIR=[FT_INC_DIR] if FT_INC_DIR else [] - I,L=inc_lib_dirs() + I=["_FT_INC_"] + L=["_FT_LIB_"] ftv = None for d in I: if isfile(pjoin(d, "ft2build.h")): ================================================ FILE: pythonforandroid/recipes/ruamel.yaml/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class RuamelYamlRecipe(PythonRecipe): version = '0.15.77' url = 'https://pypi.python.org/packages/source/r/ruamel.yaml/ruamel.yaml-{version}.tar.gz' depends = ['setuptools'] site_packages_name = 'ruamel' call_hostpython_via_targetpython = False patches = ['disable-pip-req.patch'] recipe = RuamelYamlRecipe() ================================================ FILE: pythonforandroid/recipes/ruamel.yaml/disable-pip-req.patch ================================================ --- setup.py 2018-11-11 18:27:31.936424140 +0100 +++ b/setup.py 2018-11-11 18:28:19.873507071 +0100 @@ -396,7 +396,7 @@ sys.exit(0) if not os.environ.get('RUAMEL_NO_PIP_INSTALL_CHECK', False): print('error: you have to install with "pip install ."') - sys.exit(1) + # sys.exit(1) # If you only support an extension module on Linux, Windows thinks it # is pure. That way you would get pure python .whl files that take # precedence for downloading on Linux over source with compilable C code ================================================ FILE: pythonforandroid/recipes/scipy/__init__.py ================================================ import os from os.path import join, dirname, basename from pythonforandroid.recipe import MesonRecipe, Recipe from pythonforandroid.logger import warning from pathlib import Path class ScipyRecipe(MesonRecipe): version = "v1.16.2" url = "git+https://github.com/scipy/scipy.git" depends = ["numpy", "libopenblas", "fortran"] need_stl_shared = True meson_version = "1.5.0" hostpython_prerequisites = ["numpy", "Cython>=3.0.8"] patches = ["meson.patch"] def get_recipe_meson_options(self, arch): options = super().get_recipe_meson_options(arch) options["binaries"]["python"] = self.ctx.python_recipe.python_exe options["binaries"]["fortran"] = self.place_wrapper(arch) options["properties"]["numpy-include-dir"] = join( self.ctx.get_python_install_dir(arch.arch), "numpy/_core/include" ) self.ensure_args( "-Csetup-args=-Dblas=openblas", "-Csetup-args=-Dlapack=openblas", f"-Csetup-args=-Dopenblas_libdir={self.ctx.get_libs_dir(arch.arch)}", f'-Csetup-args=-Dopenblas_incldir={join(Recipe.get_recipe("libopenblas", self.ctx).get_build_dir(arch.arch), "build")}', "-Csetup-args=-Duse-pythran=false", ) return options def place_wrapper(self, arch): compiler = Recipe.get_recipe("fortran", self.ctx).get_fortran_bin(arch.arch) file = join(self.get_recipe_dir(), "wrapper.py") with open(file, "r") as _file: data = _file.read() _file.close() data = data.replace("@COMPILER@", compiler) # custom compiler # taken from: https://github.com/termux/termux-packages/blob/master/packages/python-scipy/ m_compiler = Path(join(dirname(compiler), basename(compiler) + "-scipy")) m_compiler.write_text(data) m_compiler.chmod(0o755) self.patch_shebang(str(m_compiler), self.real_hostpython_location) return str(m_compiler) def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) arch_env = arch.get_env() env["LDFLAGS"] = arch_env["LDFLAGS"] env["LDFLAGS"] += " -L{} -lpython{}".format( self.ctx.python_recipe.link_root(arch.arch), self.ctx.python_recipe.link_version, ) return env def build_arch(self, arch): if arch.arch not in ["arm64-v8a", "x86_64"]: warning( "SciPy supports only 64-bit Android architectures: arm64-v8a and x86_64; skipping build." ) return if os.name != "posix": warning("Building SciPy is only supported on Linux; skipping.") return super().build_arch(arch) recipe = ScipyRecipe() ================================================ FILE: pythonforandroid/recipes/scipy/meson.patch ================================================ diff '--color=auto' -uNr scipy.git/meson.options scipy.git.patch/meson.options --- scipy.git/meson.options 2025-03-27 02:55:14.586853766 +0530 +++ scipy.git.patch/meson.options 2025-03-27 02:07:29.736674085 +0530 @@ -2,6 +2,8 @@ description: 'option for BLAS library switching') option('lapack', type: 'string', value: 'openblas', description: 'option for LAPACK library switching') +option('openblas_incldir', type: 'string', value: '', description: 'OpenBLAS include directory') +option('openblas_libdir', type: 'string', value: '', description: 'OpenBLAS library directory') option('use-g77-abi', type: 'boolean', value: false, description: 'If set to true, forces using g77 compatibility wrappers ' + 'for LAPACK functions. The default is to use gfortran ' + diff '--color=auto' -uNr scipy.git/scipy/meson.build scipy.git.patch/scipy/meson.build --- scipy.git/scipy/meson.build 2025-03-27 02:55:14.632428649 +0530 +++ scipy.git.patch/scipy/meson.build 2025-03-27 11:25:33.756445056 +0530 @@ -268,10 +268,18 @@ endif endif +openblas_inc = get_option('openblas_incldir') +openblas_lib = get_option('openblas_libdir') + +openblas_dep = declare_dependency( + include_directories: include_directories(openblas_inc), + link_args: ['-L' + openblas_lib, '-lopenblas'] +) + # pkg-config uses a lower-case name while CMake uses a capitalized name, so try # that too to make the fallback detection with CMake work if blas_name == 'openblas' - blas = dependency(['openblas', 'OpenBLAS']) + blas = openblas_dep elif blas_name != 'scipy-openblas' # if so, we found it already blas = dependency(blas_name) endif @@ -295,7 +303,7 @@ # use that - no need to run the full detection twice. lapack = blas elif lapack_name == 'openblas' - lapack = dependency(['openblas', 'OpenBLAS']) + lapack = openblas_dep else lapack = dependency(lapack_name) endif ================================================ FILE: pythonforandroid/recipes/scipy/wrapper.py ================================================ #!/usr/bin/python3 # Taken from https://github.com/termux/termux-packages/blob/master/packages/python-scipy/wrapper.py.in import os import subprocess import sys import typing """ This wrapper is used to ignore or replace some unsupported flags for flang-new. It will operate as follows: 1. Ignore `-Minform=inform` and `-fdiagnostics-color`. They are added by meson automatically, but are not supported by flang-new yet. 2. Remove `-lflang` and `-lpgmath`. It exists in classic-flang but doesn't exist in flang-new. 3. Replace `-Oz` to `-O2`. `-Oz` is not supported by flang-new. 4. Replace `-module` to `-J`. See https://github.com/llvm/llvm-project/issues/66969 5. Ignore `-MD`, `-MQ file` and `-MF file`. They generates files used by GNU make but we're using ninja. 6. Ignore `-fvisibility=hidden`. It is not supported by flang-new, and ignoring it will not break the functionality, as scipy also uses version script for shared libraries. """ COMPLIER_PATH = "@COMPILER@" def main(argv: typing.List[str]): cwd = os.getcwd() argv_new = [] i = 0 while i < len(argv): arg = argv[i] if arg in [ "-Minform=inform", "-lflang", "-lpgmath", "-MD", "-fvisibility=hidden", ] or arg.startswith("-fdiagnostics-color"): pass elif arg == "-Oz": argv_new.append("-O2") elif arg == "-module": argv_new.append("-J") elif arg in ["-MQ", "-MF"]: i += 1 else: argv_new.append(arg) i += 1 args = [COMPLIER_PATH] + argv_new subprocess.check_call(args, env=os.environ, cwd=cwd, text=True) if __name__ == "__main__": main(sys.argv[1:]) ================================================ FILE: pythonforandroid/recipes/scrypt/__init__.py ================================================ from pythonforandroid.recipe import CythonRecipe class ScryptRecipe(CythonRecipe): version = '0.8.20' url = 'https://github.com/holgern/py-scrypt/archive/refs/tags/v{version}.zip' depends = ['setuptools', 'openssl'] call_hostpython_via_targetpython = False patches = ["remove_librt.patch"] def get_recipe_env(self, arch, with_flags_in_cc=True): """ Adds openssl recipe to include and library path. """ 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)) env['LDFLAGS'] += ' -L{}'.format(self.ctx.libs_dir) env['LDFLAGS'] += openssl_recipe.link_dirs_flags(arch) env['LIBS'] = env.get('LIBS', '') + openssl_recipe.link_libs_flags() return env recipe = ScryptRecipe() ================================================ FILE: pythonforandroid/recipes/scrypt/remove_librt.patch ================================================ --- a/setup.py 2018-05-06 23:25:08.757522119 +0200 +++ b/setup.py 2018-05-06 23:25:30.269797365 +0200 @@ -15,7 +15,6 @@ if sys.platform.startswith('linux'): define_macros = [('HAVE_CLOCK_GETTIME', '1'), - ('HAVE_LIBRT', '1'), ('HAVE_POSIX_MEMALIGN', '1'), ('HAVE_STRUCT_SYSINFO', '1'), ('HAVE_STRUCT_SYSINFO_MEM_UNIT', '1'), @@ -23,8 +22,7 @@ ('HAVE_SYSINFO', '1'), ('HAVE_SYS_SYSINFO_H', '1'), ('_FILE_OFFSET_BITS', '64')] - libraries = ['crypto', 'rt'] - includes = ['/usr/local/include', '/usr/include'] + libraries = ['crypto'] CFLAGS.append('-O2') elif sys.platform.startswith('win32'): define_macros = [('inline', '__inline')] ================================================ FILE: pythonforandroid/recipes/sdl2/__init__.py ================================================ from os.path import exists, join from pythonforandroid.recipe import BootstrapNDKRecipe from pythonforandroid.toolchain import current_directory, shprint import sh class LibSDL2Recipe(BootstrapNDKRecipe): version = "2.30.11" url = "https://github.com/libsdl-org/SDL/releases/download/release-{version}/SDL2-{version}.tar.gz" md5sum = 'bea190b480f6df249db29eb3bacfe41e' conflicts = ['sdl3'] dir_name = 'SDL' depends = ['sdl2_image', 'sdl2_mixer', 'sdl2_ttf'] def get_recipe_env(self, arch=None, with_flags_in_cc=True, with_python=True): 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 def should_build(self, arch): libdir = join(self.get_build_dir(arch.arch), "../..", "libs", arch.arch) libs = ['libmain.so', 'libSDL2.so', 'libSDL2_image.so', 'libSDL2_mixer.so', 'libSDL2_ttf.so'] return not all(exists(join(libdir, x)) for x in libs) def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_jni_dir()): shprint( sh.Command(join(self.ctx.ndk_dir, "ndk-build")), "V=1", "NDK_DEBUG=" + ("1" if self.ctx.build_as_debuggable else "0"), _env=env ) recipe = LibSDL2Recipe() ================================================ FILE: pythonforandroid/recipes/sdl2_image/__init__.py ================================================ import os import sh from pythonforandroid.logger import shprint from pythonforandroid.recipe import BootstrapNDKRecipe class LibSDL2Image(BootstrapNDKRecipe): version = '2.8.2' url = 'https://github.com/libsdl-org/SDL_image/releases/download/release-{version}/SDL2_image-{version}.tar.gz' dir_name = 'SDL2_image' patches = ['enable-webp.patch'] def get_include_dirs(self, arch): return [ os.path.join(self.ctx.bootstrap.build_dir, "jni", "SDL2_image", "include") ] def prebuild_arch(self, arch): # We do not have a folder for each arch on BootstrapNDKRecipe, so we # need to skip the external deps download if we already have done it. build_dir = self.get_build_dir(arch.arch) with open(os.path.join(build_dir, ".gitmodules"), "r") as file: for section in file.read().split('[submodule "')[1:]: line_split = section.split(" = ") # Parse .gitmodule section clone_path, url, branch = ( os.path.join(build_dir, line_split[1].split("\n")[0].strip()), line_split[2].split("\n")[0].strip(), line_split[-1].strip() ) # Clone if needed if not os.path.exists(clone_path) or not os.listdir(clone_path): shprint( sh.git, "clone", url, "--depth", "1", "-b", branch, clone_path, "--recursive" ) super().prebuild_arch(arch) recipe = LibSDL2Image() ================================================ FILE: pythonforandroid/recipes/sdl2_image/enable-webp.patch ================================================ diff -Naur SDL2_image.orig/Android.mk SDL2_image/Android.mk --- SDL2_image.orig/Android.mk 2022-10-03 20:51:52.000000000 +0200 +++ SDL2_image/Android.mk 2022-10-03 20:52:48.000000000 +0200 @@ -32,7 +32,7 @@ # Enable this if you want to support loading WebP images # The library path should be a relative path to this directory. -SUPPORT_WEBP ?= false +SUPPORT_WEBP := true WEBP_LIBRARY_PATH := external/libwebp ================================================ FILE: pythonforandroid/recipes/sdl2_mixer/__init__.py ================================================ import os from pythonforandroid.recipe import BootstrapNDKRecipe class LibSDL2Mixer(BootstrapNDKRecipe): version = '2.6.3' url = 'https://github.com/libsdl-org/SDL_mixer/releases/download/release-{version}/SDL2_mixer-{version}.tar.gz' dir_name = 'SDL2_mixer' def get_include_dirs(self, arch): return [ os.path.join(self.ctx.bootstrap.build_dir, "jni", "SDL2_mixer", "include") ] recipe = LibSDL2Mixer() ================================================ FILE: pythonforandroid/recipes/sdl2_ttf/__init__.py ================================================ from pythonforandroid.recipe import BootstrapNDKRecipe class LibSDL2TTF(BootstrapNDKRecipe): version = '2.22.0' url = 'https://github.com/libsdl-org/SDL_ttf/releases/download/release-{version}/SDL2_ttf-{version}.tar.gz' dir_name = 'SDL2_ttf' recipe = LibSDL2TTF() ================================================ FILE: pythonforandroid/recipes/sdl3/__init__.py ================================================ from os.path import exists, join from pythonforandroid.recipe import BootstrapNDKRecipe from pythonforandroid.toolchain import current_directory, shprint import sh class LibSDL3Recipe(BootstrapNDKRecipe): version = "3.4.2" url = "https://github.com/libsdl-org/SDL/releases/download/release-{version}/SDL3-{version}.tar.gz" md5sum = "b488ea1ede947c06855588314effe905" conflicts = ["sdl2"] dir_name = "SDL" depends = ["sdl3_image", "sdl3_mixer", "sdl3_ttf"] def get_recipe_env( self, arch=None, with_flags_in_cc=True, with_python=True ): 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 def get_include_dirs(self, arch): return [ join(self.ctx.bootstrap.build_dir, "jni", "SDL", "include"), join(self.ctx.bootstrap.build_dir, "jni", "SDL", "include", "SDL3"), ] def should_build(self, arch): libdir = join(self.get_build_dir(arch.arch), "../..", "libs", arch.arch) libs = [ "libmain.so", "libSDL3.so", "libSDL3_image.so", "libSDL3_mixer.so", "libSDL3_ttf.so", ] return not all(exists(join(libdir, x)) for x in libs) def build_arch(self, arch): env = self.get_recipe_env(arch) with current_directory(self.get_jni_dir()): shprint( sh.Command(join(self.ctx.ndk_dir, "ndk-build")), "V=1", "NDK_DEBUG=" + ("1" if self.ctx.build_as_debuggable else "0"), _env=env, ) recipe = LibSDL3Recipe() ================================================ FILE: pythonforandroid/recipes/sdl3_image/__init__.py ================================================ import os import sh from pythonforandroid.logger import shprint from pythonforandroid.recipe import BootstrapNDKRecipe from pythonforandroid.util import current_directory class LibSDL3Image(BootstrapNDKRecipe): version = "3.4.0" url = "https://github.com/libsdl-org/SDL_image/releases/download/release-{version}/SDL3_image-{version}.tar.gz" dir_name = "SDL3_image" patches = ["enable-webp.patch"] def get_include_dirs(self, arch): return [ os.path.join( self.ctx.bootstrap.build_dir, "jni", "SDL3_image", "include" ), os.path.join( self.ctx.bootstrap.build_dir, "jni", "SDL3_image", "include", "SDL3_image", ), ] def prebuild_arch(self, arch): # We do not have a folder for each arch on BootstrapNDKRecipe, so we # need to skip the external deps download if we already have done it. external_deps_dir = os.path.join( self.get_build_dir(arch.arch), "external" ) if not os.path.exists(os.path.join(external_deps_dir, "libwebp")): with current_directory(external_deps_dir): shprint(sh.Command("./download.sh")) super().prebuild_arch(arch) recipe = LibSDL3Image() ================================================ FILE: pythonforandroid/recipes/sdl3_image/enable-webp.patch ================================================ --- a/Android.mk +++ b/Android.mk @@ -33,8 +33,8 @@ # Enable this if you want to support loading WebP images # The library path should be a relative path to this directory. -SUPPORT_WEBP ?= false -SUPPORT_SAVE_WEBP ?= true +SUPPORT_WEBP := true +SUPPORT_SAVE_WEBP := true WEBP_LIBRARY_PATH := external/libwebp @@ -160,7 +160,7 @@ ifeq ($(SUPPORT_WEBP),true) LOCAL_C_INCLUDES += $(LOCAL_PATH)/$(WEBP_LIBRARY_PATH)/src LOCAL_CFLAGS += -DLOAD_WEBP - LOCAL_STATIC_LIBRARIES += webpdemux + LOCAL_STATIC_LIBRARIES += webpmux webpdemux LOCAL_STATIC_LIBRARIES += webp ifeq ($(SUPPORT_SAVE_WEBP),true) LOCAL_CFLAGS += -DSAVE_WEBP=1 ================================================ FILE: pythonforandroid/recipes/sdl3_mixer/__init__.py ================================================ import os import sh from pythonforandroid.logger import shprint from pythonforandroid.recipe import BootstrapNDKRecipe from pythonforandroid.util import current_directory class LibSDL3Mixer(BootstrapNDKRecipe): version = "72a73339731a12c1002f9caca64f1ab924938102" url = "https://github.com/libsdl-org/SDL_mixer/archive/{version}.tar.gz" dir_name = "SDL3_mixer" patches = ["disable-libgme.patch"] def get_include_dirs(self, arch): return [ os.path.join( self.ctx.bootstrap.build_dir, "jni", "SDL3_mixer", "include" ), os.path.join( self.ctx.bootstrap.build_dir, "jni", "SDL3_mixer", "include", "SDL3_mixer", ), ] def prebuild_arch(self, arch): # We do not have a folder for each arch on BootstrapNDKRecipe, so we # need to skip the external deps download if we already have done it. external_deps_dir = os.path.join( self.get_build_dir(arch.arch), "external" ) if not os.path.exists( os.path.join(external_deps_dir, "libgme", "Android.mk") ): with current_directory(external_deps_dir): shprint(sh.Command("./download.sh")) super().prebuild_arch(arch) recipe = LibSDL3Mixer() ================================================ FILE: pythonforandroid/recipes/sdl3_mixer/disable-libgme.patch ================================================ diff -Naur SDL3_mixer.orig/Android.mk SDL3_mixer/Android.mk --- SDL3_mixer.orig/Android.mk 2025-03-16 21:05:19 +++ SDL3_mixer/Android.mk 2025-03-16 21:06:33 @@ -31,7 +31,7 @@ WAVPACK_LIBRARY_PATH := external/wavpack # Enable this if you want to support loading music via libgme -SUPPORT_GME ?= true +SUPPORT_GME ?= false GME_LIBRARY_PATH := external/libgme # Enable this if you want to support loading MOD music via XMP-lite ================================================ FILE: pythonforandroid/recipes/sdl3_ttf/__init__.py ================================================ import os import sh from pythonforandroid.logger import shprint from pythonforandroid.recipe import BootstrapNDKRecipe from pythonforandroid.util import current_directory class LibSDL3TTF(BootstrapNDKRecipe): version = "3.2.2" url = "https://github.com/libsdl-org/SDL_ttf/releases/download/release-{version}/SDL3_ttf-{version}.tar.gz" dir_name = "SDL3_ttf" def get_include_dirs(self, arch): return [ os.path.join( self.ctx.bootstrap.build_dir, "jni", "SDL3_ttf", "include" ), os.path.join( self.ctx.bootstrap.build_dir, "jni", "SDL3_ttf", "include", "SDL3_ttf", ), ] def prebuild_arch(self, arch): # We do not have a folder for each arch on BootstrapNDKRecipe, so we # need to skip the external deps download if we already have done it. external_deps_dir = os.path.join( self.get_build_dir(arch.arch), "external" ) if not os.path.exists(os.path.join(external_deps_dir, "harfbuzz")): with current_directory(external_deps_dir): shprint(sh.Command("./download.sh")) super().prebuild_arch(arch) recipe = LibSDL3TTF() ================================================ FILE: pythonforandroid/recipes/secp256k1/__init__.py ================================================ import os from pythonforandroid.recipe import CppCompiledComponentsPythonRecipe class Secp256k1Recipe(CppCompiledComponentsPythonRecipe): version = '0.13.2.4' url = 'https://github.com/ludbb/secp256k1-py/archive/{version}.tar.gz' call_hostpython_via_targetpython = False depends = [ 'openssl', 'hostpython3', 'python3', 'setuptools', 'libffi', 'cffi', 'libsecp256k1' ] patches = [ "cross_compile.patch", "drop_setup_requires.patch", "pkg-config.patch", "find_lib.patch", "no-download.patch"] def get_recipe_env(self, arch=None): 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') env['LDFLAGS'] += ' -L{} -lsecp256k1'.format(libsecp256k1_dir) return env recipe = Secp256k1Recipe() ================================================ FILE: pythonforandroid/recipes/secp256k1/cross_compile.patch ================================================ diff --git a/setup.py b/setup.py index bba4bce..b86b369 100644 --- a/setup.py +++ b/setup.py @@ -191,6 +192,7 @@ class build_clib(_build_clib): "--disable-dependency-tracking", "--with-pic", "--enable-module-recovery", + "--host=" + arch.command_prefix, "--prefix", os.path.abspath(self.build_clib), ] ================================================ FILE: pythonforandroid/recipes/secp256k1/drop_setup_requires.patch ================================================ diff --git a/setup.py b/setup.py index bba4bce..bfffbbc 100644 --- a/setup.py +++ b/setup.py @@ -263,7 +263,6 @@ setup( author_email='lud@tutanota.com', license='MIT', - setup_requires=['cffi>=1.3.0', 'pytest-runner==2.6.2'], install_requires=['cffi>=1.3.0'], tests_require=['pytest==2.8.7'], ================================================ FILE: pythonforandroid/recipes/secp256k1/find_lib.patch ================================================ diff --git a/setup_support.py b/setup_support.py index 68a2a7f..b84f420 100644 --- a/setup_support.py +++ b/setup_support.py @@ -68,6 +68,8 @@ def build_flags(library, type_, path): def _find_lib(): + # we're picking up the recipe one + return True from cffi import FFI ffi = FFI() try: ================================================ FILE: pythonforandroid/recipes/secp256k1/no-download.patch ================================================ diff --git a/setup.py b/setup.py index bba4bce..5ea0228 100644 --- a/setup.py +++ b/setup.py @@ -55,6 +55,8 @@ except OSError: def download_library(command): + # we will use the custom libsecp256k1 recipe + return if command.dry_run: return libdir = absolute("libsecp256k1") ================================================ FILE: pythonforandroid/recipes/secp256k1/pkg-config.patch ================================================ diff --git a/setup.py b/setup.py index bba4bce..609481c 100644 --- a/setup.py +++ b/setup.py @@ -48,10 +48,7 @@ if [int(i) for i in setuptools_version.split('.')] < [3, 3]: try: subprocess.check_call(['pkg-config', '--version']) except OSError: - raise SystemExit( - "'pkg-config' is required to install this package. " - "Please see the README for details." - ) + pass def download_library(command): diff --git a/setup_support.py b/setup_support.py index 68a2a7f..ccbafac 100644 --- a/setup_support.py +++ b/setup_support.py @@ -40,6 +40,7 @@ def absolute(*paths): def build_flags(library, type_, path): """Return separated build flags from pkg-config output""" + return [] pkg_config_path = [path] if "PKG_CONFIG_PATH" in os.environ: ================================================ FILE: pythonforandroid/recipes/setuptools/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe class SetuptoolsRecipe(PyProjectRecipe): hostpython_prerequisites = ['setuptools'] recipe = SetuptoolsRecipe() ================================================ FILE: pythonforandroid/recipes/shapely/__init__.py ================================================ from pythonforandroid.recipe import CythonRecipe from os.path import join class ShapelyRecipe(CythonRecipe): version = '1.7a1' url = 'https://github.com/Toblerity/Shapely/archive/{version}.tar.gz' depends = ['setuptools', 'libgeos'] call_hostpython_via_targetpython = False # Patch to avoid libgeos check (because it fails), insert environment # variables for our libgeos build (includes, lib paths...) and force # the cython's compilation to raise an error in case that it fails patches = ['setup.patch'] # Don't Force Cython # setup_extra_args = ['sdist'] def get_recipe_env(self, arch=None, with_flags_in_cc=True): env = super().get_recipe_env(arch) libgeos_install = join(self.get_recipe( 'libgeos', self.ctx).get_build_dir(arch.arch), 'install_target') # All this `GEOS_X` variables should be string types, separated # by commas in case that we need to pass more than one value env['GEOS_INCLUDE_DIRS'] = join(libgeos_install, 'include') env['GEOS_LIBRARY_DIRS'] = join(libgeos_install, 'lib') env['GEOS_LIBRARIES'] = 'geos_c,geos' return env recipe = ShapelyRecipe() ================================================ FILE: pythonforandroid/recipes/shapely/setup.patch ================================================ This patch does three things: - disable the libgeos check, because, even setting the proper env variables, it fails to load our libgeos library, so we skip that because it's not mandatory for the cythonizing. - sets some environment variables into the setup.py file, so we can pass our libgeos information (includes, lib path and libraries) - force to raise an error when cython file to compile (our current build system relies on this failure to do the proper `cythonizing`, if we don't raise the error, we will end up with the package installed without the speed optimizations. --- Shapely-1.7a1/setup.py.orig 2018-07-29 22:53:13.000000000 +0200 +++ Shapely-1.7a1/setup.py 2019-02-24 14:26:19.178610660 +0100 @@ -82,8 +82,8 @@ if not (py_version == (2, 7) or py_versi # Get geos_version from GEOS dynamic library, which depends on # GEOS_LIBRARY_PATH and/or GEOS_CONFIG environment variables -from shapely._buildcfg import geos_version_string, geos_version, \ - geos_config, get_geos_config +# from shapely._buildcfg import geos_version_string, geos_version, \ +# geos_config, get_geos_config logging.basicConfig() log = logging.getLogger(__file__) @@ -248,9 +248,9 @@ if sys.platform == 'win32': setup_args['package_data']['shapely'].append('shapely/DLLs/*.dll') # Prepare build opts and args for the speedups extension module. -include_dirs = [] -library_dirs = [] -libraries = [] +include_dirs = os.environ.get('GEOS_INCLUDE_DIRS', '').split(',') +library_dirs = os.environ.get('GEOS_LIBRARY_DIRS', '').split(',') +libraries = os.environ.get('GEOS_LIBRARIES', '').split(',') extra_link_args = [] # If NO_GEOS_CONFIG is set in the environment, geos-config will not @@ -375,6 +375,7 @@ try: construct_build_ext(existing_build_ext) setup(ext_modules=ext_modules, **setup_args) except BuildFailed as ex: + raise # Force python only build to fail BUILD_EXT_WARNING = "The C extension could not be compiled, " \ "speedups are not enabled." log.warn(ex) ================================================ FILE: pythonforandroid/recipes/snappy/__init__.py ================================================ from pythonforandroid.recipe import Recipe from pythonforandroid.logger import shprint from pythonforandroid.util import current_directory from os.path import join import sh class SnappyRecipe(Recipe): version = '1.1.7' url = 'https://github.com/google/snappy/archive/{version}.tar.gz' built_libraries = {'libsnappy.so': '.'} def build_arch(self, arch): env = self.get_recipe_env(arch) source_dir = self.get_build_dir(arch.arch) with current_directory(source_dir): shprint(sh.cmake, source_dir, '-DANDROID_ABI={}'.format(arch.arch), '-DANDROID_NATIVE_API_LEVEL={}'.format(self.ctx.ndk_api), '-DCMAKE_TOOLCHAIN_FILE={}'.format( join(self.ctx.ndk_dir, 'build', 'cmake', 'android.toolchain.cmake')), '-DBUILD_SHARED_LIBS=1', _env=env) shprint(sh.make, _env=env) recipe = SnappyRecipe() ================================================ FILE: pythonforandroid/recipes/spine/__init__.py ================================================ from pythonforandroid.recipe import CythonRecipe class SpineCython(CythonRecipe): version = '0.5.1' url = 'https://github.com/tileworks/spine-cython/archive/{version}.zip' name = 'spine' depends = ['setuptools'] site_packages_name = 'spine' call_hostpython_via_targetpython = False recipe = SpineCython() ================================================ FILE: pythonforandroid/recipes/sqlalchemy/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe class SQLAlchemyRecipe(PyProjectRecipe): name = 'sqlalchemy' version = '2.0.30' url = 'https://github.com/sqlalchemy/sqlalchemy/archive/refs/tags/rel_{}.tar.gz' depends = ['setuptools'] @property def versioned_url(self): return self.url.format(self.version.replace(".", "_")) recipe = SQLAlchemyRecipe() ================================================ FILE: pythonforandroid/recipes/sqlite3/__init__.py ================================================ import sh from pythonforandroid.logger import shprint from pythonforandroid.util import current_directory from pythonforandroid.recipe import Recipe from multiprocessing import cpu_count class Sqlite3Recipe(Recipe): version = '3.50.4' url = 'https://github.com/sqlite/sqlite/archive/refs/tags/version-{version}.tar.gz' built_libraries = {'libsqlite3.so': '.'} def build_arch(self, arch): env = self.get_recipe_env(arch) build_dir = self.get_build_dir(arch.arch) config_args = { '--host={}'.format(arch.command_prefix), '--prefix={}'.format(build_dir), '--disable-tcl', } with current_directory(build_dir): configure = sh.Command('./configure') shprint(configure, *config_args, _env=env) shprint(sh.make, '-j', str(cpu_count()), _env=env) recipe = Sqlite3Recipe() ================================================ FILE: pythonforandroid/recipes/storm/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe, current_directory, shprint import sh class StormRecipe(PythonRecipe): version = '0.20' url = 'https://launchpad.net/storm/trunk/{version}/+download/storm-{version}.tar.bz2' depends = [] site_packages_name = 'storm' call_hostpython_via_targetpython = False def prebuild_arch(self, arch): with current_directory(self.get_build_dir(arch.arch)): # Cross compiling for 32 bits in 64 bit ubuntu before precise is # failing. See # https://bugs.launchpad.net/ubuntu/+source/python2.7/+bug/873007 shprint(sh.sed, '-i', "s|BUILD_CEXTENSIONS = True|BUILD_CEXTENSIONS = False|", 'setup.py') recipe = StormRecipe() ================================================ FILE: pythonforandroid/recipes/tflite-runtime/CMakeLists.patch ================================================ --- tflite-runtime/tensorflow/lite/CMakeLists.txt 2022-01-27 17:29:49.460000000 -1000 +++ CMakeLists.txt 2022-02-21 15:03:09.568367300 -1000 @@ -220,6 +220,9 @@ if(NOT "${CMAKE_SYSTEM_NAME}" STREQUAL "iOS") list(FILTER TFLITE_SRCS EXCLUDE REGEX ".*minimal_logging_ios\\.cc$") endif() +if("${CMAKE_SYSTEM_NAME}" STREQUAL "Android") + list(FILTER TFLITE_SRCS EXCLUDE REGEX ".*minimal_logging_default\\.cc$") +endif() populate_tflite_source_vars("core" TFLITE_CORE_SRCS) populate_tflite_source_vars("core/api" TFLITE_CORE_API_SRCS) populate_tflite_source_vars("c" TFLITE_C_SRCS) @@ -505,6 +508,7 @@ ruy ${CMAKE_DL_LIBS} ${TFLITE_TARGET_DEPENDENCIES} + ${ANDROID_LOG_LIB} ) if (NOT BUILD_SHARED_LIBS) @@ -550,6 +554,7 @@ tensorflow-lite ${CMAKE_DL_LIBS} ) + target_compile_options(_pywrap_tensorflow_interpreter_wrapper PUBLIC ${TFLITE_TARGET_PUBLIC_OPTIONS} PRIVATE ${TFLITE_TARGET_PRIVATE_OPTIONS} ================================================ FILE: pythonforandroid/recipes/tflite-runtime/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe, current_directory, \ shprint, info_main, warning from pythonforandroid.logger import error from os.path import join import sh class TFLiteRuntimeRecipe(PythonRecipe): ############################################################### # # tflite-runtime README: # https://github.com/Android-for-Python/c4k_tflite_example/blob/main/README.md # # Recipe build references: # https://developer.android.com/ndk/guides/cmake # https://developer.android.com/ndk/guides/cpu-arm-neon#cmake # https://www.tensorflow.org/lite/guide/build_cmake # https://www.tensorflow.org/lite/guide/build_cmake_arm # # Tested using cmake 3.16.3 probably requires cmake >= 3.13 # # THIS RECIPE DOES NOT BUILD x86_64, USE X86 FOR AN EMULATOR # ############################################################### version = '2.8.0' url = 'https://github.com/tensorflow/tensorflow/archive/refs/tags/v{version}.zip' depends = ['pybind11', 'numpy'] patches = ['CMakeLists.patch', 'build_with_cmake.patch'] site_packages_name = 'tflite-runtime' call_hostpython_via_targetpython = False def should_build(self, arch): name = self.folder_name.replace('-', '_') if self.ctx.has_package(name, arch): info_main('Python package already exists in site-packages') return False info_main('{} apparently isn\'t already in site-packages'.format(name)) return True def build_arch(self, arch): if arch.arch == 'x86_64': warning("******** tflite-runtime x86_64 will not be built *******") warning("Expect one of these app run time error messages:") warning("ModuleNotFoundError: No module named 'tensorflow'") warning("ModuleNotFoundError: No module named 'tflite_runtime'") warning("Use x86 not x86_64") return env = self.get_recipe_env(arch) # Directories root_dir = self.get_build_dir(arch.arch) script_dir = join(root_dir, 'tensorflow', 'lite', 'tools', 'pip_package') build_dir = join(script_dir, 'gen', 'tflite_pip', 'python3') # Includes python_include_dir = self.ctx.python_recipe.include_root(arch.arch) pybind11_recipe = self.get_recipe('pybind11', self.ctx) pybind11_include_dir = pybind11_recipe.get_include_dir(arch) numpy_include_dir = join(self.ctx.get_site_packages_dir(arch), 'numpy', 'core', 'include') includes = ' -I' + python_include_dir + \ ' -I' + numpy_include_dir + \ ' -I' + pybind11_include_dir # Scripts build_script = join(script_dir, 'build_pip_package_with_cmake.sh') toolchain = join(self.ctx.ndk_dir, 'build', 'cmake', 'android.toolchain.cmake') # Build ######## with current_directory(root_dir): env.update({ 'TENSORFLOW_TARGET': 'android', 'CMAKE_TOOLCHAIN_FILE': toolchain, 'ANDROID_PLATFORM': str(self.ctx.ndk_api), 'ANDROID_ABI': arch.arch, 'WRAPPER_INCLUDES': includes, 'CMAKE_SHARED_LINKER_FLAGS': env['LDFLAGS'], }) try: info_main('tflite-runtime is building...') info_main('Expect this to take at least 5 minutes...') cmd = sh.Command(build_script) cmd(_env=env) except sh.ErrorReturnCode as e: error(str(e.stderr)) exit(1) # Install ########## info_main('Installing tflite-runtime into site-packages') with current_directory(build_dir): hostpython = sh.Command(self.hostpython_location) install_dir = self.ctx.get_python_install_dir(arch.arch) env['PACKAGE_VERSION'] = self.version shprint(hostpython, 'setup.py', 'install', '-O2', '--root={}'.format(install_dir), '--install-lib=.', _env=env) recipe = TFLiteRuntimeRecipe() ================================================ FILE: pythonforandroid/recipes/tflite-runtime/build_with_cmake.patch ================================================ --- tflite-runtime/tensorflow/lite/tools/pip_package/build_pip_package_with_cmake.sh 2022-01-22 08:57:16.000000000 -1000 +++ build_pip_package_with_cmake.sh 2022-03-02 18:19:05.185550500 -1000 @@ -28,7 +28,7 @@ export TENSORFLOW_TARGET="armhf" fi PYTHON_INCLUDE=$(${PYTHON} -c "from sysconfig import get_paths as gp; print(gp()['include'])") -PYBIND11_INCLUDE=$(${PYTHON} -c "import pybind11; print (pybind11.get_include())") +# PYBIND11_INCLUDE=$(${PYTHON} -c "import pybind11; print (pybind11.get_include())") export CROSSTOOL_PYTHON_INCLUDE_PATH=${PYTHON_INCLUDE} # Fix container image for cross build. @@ -58,7 +58,7 @@ "${TENSORFLOW_LITE_DIR}/python/metrics/metrics_portable.py" \ "${BUILD_DIR}/tflite_runtime" echo "__version__ = '${PACKAGE_VERSION}'" >> "${BUILD_DIR}/tflite_runtime/__init__.py" -echo "__git_version__ = '$(git -C "${TENSORFLOW_DIR}" describe)'" >> "${BUILD_DIR}/tflite_runtime/__init__.py" +echo "__git_version__ = '${PACKAGE_VERSION}'" >> "${BUILD_DIR}/tflite_runtime/__init__.py" # Build python interpreter_wrapper. mkdir -p "${BUILD_DIR}/cmake_build" @@ -111,6 +111,18 @@ -DCMAKE_CXX_FLAGS="${BUILD_FLAGS}" \ "${TENSORFLOW_LITE_DIR}" ;; + android) + BUILD_FLAGS=${BUILD_FLAGS:-"${WRAPPER_INCLUDES}"} + cmake \ + -DCMAKE_SYSTEM_NAME=Android \ + -DANDROID_ARM_NEON=ON \ + -DCMAKE_CXX_FLAGS="${BUILD_FLAGS}" \ + -DCMAKE_SHARED_LINKER_FLAGS="${CMAKE_SHARED_LINKER_FLAGS}" \ + -DCMAKE_TOOLCHAIN_FILE="${CMAKE_TOOLCHAIN_FILE}" \ + -DANDROID_PLATFORM="${ANDROID_PLATFORM}" \ + -DANDROID_ABI="${ANDROID_ABI}" \ + "${TENSORFLOW_LITE_DIR}" + ;; *) BUILD_FLAGS=${BUILD_FLAGS:-"-I${PYTHON_INCLUDE} -I${PYBIND11_INCLUDE}"} cmake \ @@ -162,7 +174,7 @@ ${PYTHON} setup.py bdist --plat-name=${WHEEL_PLATFORM_NAME} \ bdist_wheel --plat-name=${WHEEL_PLATFORM_NAME} else - ${PYTHON} setup.py bdist bdist_wheel + ${PYTHON} setup.py bdist fi ;; esac ================================================ FILE: pythonforandroid/recipes/tiktoken/__init__.py ================================================ from pythonforandroid.recipe import RustCompiledComponentsRecipe class TiktokenRecipe(RustCompiledComponentsRecipe): name = 'tiktoken' version = '0.7.0' url = 'https://github.com/openai/tiktoken/archive/refs/tags/{version}.tar.gz' sha512sum = "bb2d8fd5acd660d60e690769e46cf29b06361343ea30e35613d27d55f44acf9834e51aef28f4ff316ef66f2130042079718cea04b2353301aef334cd7bd6d221" depends = ['regex', 'requests'] recipe = TiktokenRecipe() ================================================ FILE: pythonforandroid/recipes/twisted/__init__.py ================================================ import os from pythonforandroid.recipe import CythonRecipe from pythonforandroid.util import rmdir class TwistedRecipe(CythonRecipe): version = '20.3.0' url = 'https://github.com/twisted/twisted/archive/twisted-{version}.tar.gz' depends = ['setuptools', 'zope_interface', 'incremental', 'constantly'] patches = ['incremental.patch', 'remove_tests.patch'] call_hostpython_via_targetpython = False install_in_hostpython = False def prebuild_arch(self, arch): super().prebuild_arch(arch) # TODO Need to whitelist tty.pyo and termios.so here # 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]) rmdir(full_path, ignore_errors=True) def get_recipe_env(self, 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(arch), env['BUILDLIB_PATH'], ]) return env recipe = TwistedRecipe() ================================================ FILE: pythonforandroid/recipes/twisted/incremental.patch ================================================ diff -Naur twisted-twisted-19.7.0/src/twisted/python/_setup.py twisted-twisted-19.7.0_patched/src/twisted/python/_setup.py --- twisted-twisted-19.7.0/src/twisted/python/_setup.py 2019-07-28 11:17:29.000000000 +0200 +++ twisted-twisted-19.7.0_patched/src/twisted/python/_setup.py 2019-10-21 22:10:03.643068863 +0200 @@ -282,7 +282,6 @@ requirements = [ "zope.interface >= 4.4.2", "constantly >= 15.1", - "incremental >= 16.10.1", "Automat >= 0.3.0", "hyperlink >= 17.1.1", "PyHamcrest >= 1.9.0", @@ -291,8 +290,6 @@ arguments.update(dict( packages=find_packages("src"), - use_incremental=True, - setup_requires=["incremental >= 16.10.1"], install_requires=requirements, entry_points={ 'console_scripts': _CONSOLE_SCRIPTS ================================================ FILE: pythonforandroid/recipes/twisted/remove_tests.patch ================================================ 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=[ ================================================ FILE: pythonforandroid/recipes/ujson/__init__.py ================================================ from pythonforandroid.recipe import CompiledComponentsPythonRecipe class UJsonRecipe(CompiledComponentsPythonRecipe): version = '1.35' url = 'https://pypi.python.org/packages/source/u/ujson/ujson-{version}.tar.gz' depends = [] recipe = UJsonRecipe() ================================================ FILE: pythonforandroid/recipes/uvloop/__init__.py ================================================ from pythonforandroid.recipe import PyProjectRecipe class UvloopRecipe(PyProjectRecipe): version = 'v0.19.0' url = 'git+https://github.com/MagicStack/uvloop' depends = ['librt', 'libpthread'] def get_recipe_env(self, arch, **kwargs): env = super().get_recipe_env(arch, **kwargs) env["LIBUV_CONFIGURE_HOST"] = arch.command_prefix env["PLATFORM"] = "android" return env recipe = UvloopRecipe() ================================================ FILE: pythonforandroid/recipes/vispy/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class VispyRecipe(PythonRecipe): version = '0.4.0' url = 'https://github.com/vispy/vispy/archive/v{version}.tar.gz' depends = ['numpy', 'pysdl2'] patches = ['disable_freetype.patch', 'disable_font_triage.patch', 'use_es2.patch', 'remove_ati_check.patch'] recipe = VispyRecipe() ================================================ FILE: pythonforandroid/recipes/vispy/disable_font_triage.patch ================================================ diff --git a/vispy/util/fonts/_triage.py b/vispy/util/fonts/_triage.py index ddbc93d..324c161 100644 --- a/vispy/util/fonts/_triage.py +++ b/vispy/util/fonts/_triage.py @@ -9,14 +9,14 @@ import sys from ._vispy_fonts import _vispy_fonts if sys.platform.startswith('linux'): from ._freetype import _load_glyph - from ...ext.fontconfig import _list_fonts -elif sys.platform == 'darwin': - from ._quartz import _load_glyph, _list_fonts -elif sys.platform.startswith('win'): - from ._freetype import _load_glyph # noqa, analysis:ignore - from ._win32 import _list_fonts # noqa, analysis:ignore -else: - raise NotImplementedError('unknown system %s' % sys.platform) + # from ...ext.fontconfig import _list_fonts +# elif sys.platform == 'darwin': +# from ._quartz import _load_glyph, _list_fonts +# elif sys.platform.startswith('win'): +# from ._freetype import _load_glyph # noqa, analysis:ignore +# from ._win32 import _list_fonts # noqa, analysis:ignore +# else: +# raise NotImplementedError('unknown system %s' % sys.platform) _fonts = {} ================================================ FILE: pythonforandroid/recipes/vispy/disable_freetype.patch ================================================ diff --git a/vispy/util/fonts/_freetype.py b/vispy/util/fonts/_freetype.py index 3b33d0b..229d559 100644 --- a/vispy/util/fonts/_freetype.py +++ b/vispy/util/fonts/_freetype.py @@ -12,12 +12,12 @@ import numpy as np # Convert face to filename from ._vispy_fonts import _vispy_fonts, _get_vispy_font_filename -if sys.platform.startswith('linux'): - from ...ext.fontconfig import find_font -elif sys.platform.startswith('win'): - from ._win32 import find_font # noqa, analysis:ignore -else: - raise NotImplementedError +# if sys.platform.startswith('linux'): +# from ...ext.fontconfig import find_font +# elif sys.platform.startswith('win'): +# from ._win32 import find_font # noqa, analysis:ignore +# else: +# raise NotImplementedError _font_dict = {} @@ -41,6 +41,7 @@ def _load_font(face, bold, italic): def _load_glyph(f, char, glyphs_dict): """Load glyph from font into dict""" + return from ...ext.freetype import (FT_LOAD_RENDER, FT_LOAD_NO_HINTING, FT_LOAD_NO_AUTOHINT) flags = FT_LOAD_RENDER | FT_LOAD_NO_HINTING | FT_LOAD_NO_AUTOHINT ================================================ FILE: pythonforandroid/recipes/vispy/disable_freetype.patch_backup ================================================ diff --git a/vispy/util/fonts/_freetype.py b/vispy/util/fonts/_freetype.py index 3b33d0b..229d559 100644 --- a/vispy/util/fonts/_freetype.py +++ b/vispy/util/fonts/_freetype.py @@ -12,12 +12,12 @@ import numpy as np # Convert face to filename from ._vispy_fonts import _vispy_fonts, _get_vispy_font_filename -if sys.platform.startswith('linux'): - from ...ext.fontconfig import find_font -elif sys.platform.startswith('win'): - from ._win32 import find_font # noqa, analysis:ignore -else: - raise NotImplementedError +# if sys.platform.startswith('linux'): +# from ...ext.fontconfig import find_font +# elif sys.platform.startswith('win'): +# from ._win32 import find_font # noqa, analysis:ignore +# else: +# raise NotImplementedError _font_dict = {} @@ -41,6 +41,7 @@ def _load_font(face, bold, italic): def _load_glyph(f, char, glyphs_dict): """Load glyph from font into dict""" + return from ...ext.freetype import (FT_LOAD_RENDER, FT_LOAD_NO_HINTING, FT_LOAD_NO_AUTOHINT) flags = FT_LOAD_RENDER | FT_LOAD_NO_HINTING | FT_LOAD_NO_AUTOHINT ================================================ FILE: pythonforandroid/recipes/vispy/remove_ati_check.patch ================================================ diff --git a/vispy/gloo/glir.py b/vispy/gloo/glir.py index 67419b5..341c13d 100644 --- a/vispy/gloo/glir.py +++ b/vispy/gloo/glir.py @@ -878,19 +878,19 @@ class GlirBuffer(GlirObject): self.activate() nbytes = data.nbytes - # Determine whether to check errors to try handling the ATI bug - check_ati_bug = ((not self._bufferSubDataOk) and - (gl.current_backend is gl.gl2) and - sys.platform.startswith('win')) - - # flush any pending errors - if check_ati_bug: - gl.check_error('periodic check') + # # Determine whether to check errors to try handling the ATI bug + # check_ati_bug = ((not self._bufferSubDataOk) and + # (gl.current_backend is gl.gl2) and + # sys.platform.startswith('win')) + + # # flush any pending errors + # if check_ati_bug: + # gl.check_error('periodic check') try: gl.glBufferSubData(self._target, offset, data) - if check_ati_bug: - gl.check_error('glBufferSubData') + # if check_ati_bug: + # gl.check_error('glBufferSubData') self._bufferSubDataOk = True # glBufferSubData seems to work except Exception: # This might be due to a driver error (seen on ATI), issue #64. ================================================ FILE: pythonforandroid/recipes/vispy/use_es2.patch ================================================ diff --git a/vispy/gloo/gl/__init__.py b/vispy/gloo/gl/__init__.py index 93813fa..c41859c 100644 --- a/vispy/gloo/gl/__init__.py +++ b/vispy/gloo/gl/__init__.py @@ -210,7 +210,7 @@ def check_error(when='periodic check'): # Load default gl backend -from . import gl2 as default_backend # noqa +from . import es2 as default_backend # noqa # Call use to start using our default backend -use_gl() +use_gl('es2') ================================================ FILE: pythonforandroid/recipes/vlc/__init__.py ================================================ from pythonforandroid.toolchain import Recipe, current_directory from pythonforandroid.logger import info, debug, shprint, warning from os.path import join, isdir, isfile from os import environ import sh class VlcRecipe(Recipe): version = '3.0.18' url = None name = 'vlc' depends = [] port_git = 'http://git.videolan.org/git/vlc-ports/android.git' # vlc_git = 'http://git.videolan.org/git/vlc.git' ENV_LIBVLC_AAR = 'LIBVLC_AAR' aars = {} # for future use of multiple arch def prebuild_arch(self, 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: aar = environ.get(self.ENV_LIBVLC_AAR) if isdir(aar): aar = join(aar, 'libvlc-{}.aar'.format(self.version)) if not isfile(aar): warning("Error: {} is not valid libvlc-.aar bundle".format(aar)) info("check {} environment!".format(self.ENV_LIBVLC_AAR)) exit(1) self.aars[arch] = aar else: aar_path = join(port_dir, 'libvlc', 'build', 'outputs', 'aar') self.aars[arch] = aar = join(aar_path, 'libvlc-{}.aar'.format(self.version)) warning("HINT: set path to precompiled libvlc-.aar bundle " "in {} environment!".format(self.ENV_LIBVLC_AAR)) info("libvlc-.aar should build " "from sources at {}".format(port_dir)) if not isfile(join(port_dir, 'compile.sh')): info("clone vlc port for android sources from {}".format( self.port_git)) shprint(sh.git, 'clone', self.port_git, port_dir, _tail=20, _critical=True) # now "git clone ..." is a part of compile.sh # vlc_dir = join(port_dir, 'vlc') # if not isfile(join(vlc_dir, 'Makefile.am')): # info("clone vlc sources from {}".format(self.vlc_git)) # shprint(sh.git, 'clone', self.vlc_git, vlc_dir, # _tail=20, _critical=True) def build_arch(self, arch): super().build_arch(arch) build_dir = self.get_build_dir(arch.arch) port_dir = join(build_dir, 'vlc-port-android', 'buildsystem') aar = self.aars[arch] if not isfile(aar): with current_directory(port_dir): env = dict(environ) env.update({ 'ANDROID_ABI': arch.arch, 'ANDROID_NDK': self.ctx.ndk_dir, 'ANDROID_SDK': self.ctx.sdk_dir, }) info("compiling vlc from sources") debug("environment: {}".format(env)) if not isfile(join('bin', 'VLC-debug.apk')): shprint(sh.Command('./compile.sh'), _env=env, _tail=50, _critical=True) shprint(sh.Command('./compile-medialibrary.sh'), _env=env, _tail=50, _critical=True) shprint(sh.cp, '-a', aar, self.ctx.aars_dir) recipe = VlcRecipe() ================================================ FILE: pythonforandroid/recipes/wsaccel/__init__.py ================================================ from pythonforandroid.recipe import CythonRecipe class WSAccellRecipe(CythonRecipe): version = '0.6.2' url = 'https://pypi.python.org/packages/source/w/wsaccel/wsaccel-{version}.tar.gz' depends = [] call_hostpython_via_targetpython = False recipe = WSAccellRecipe() ================================================ FILE: pythonforandroid/recipes/x3dh/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class X3DHRecipe(PythonRecipe): name = 'x3dh' version = '0.5.3' url = 'https://pypi.python.org/packages/source/X/X3DH/X3DH-{version}.tar.gz' site_packages_name = 'x3dh' depends = [ 'setuptools', 'cryptography', 'xeddsa', ] patches = ['requires_fix.patch'] call_hostpython_via_targetpython = False recipe = X3DHRecipe() ================================================ FILE: pythonforandroid/recipes/x3dh/requires_fix.patch ================================================ diff -urN X3DH-0.5.3.ori/setup.py X3DH-0.5.3/setup.py --- X3DH-0.5.3.ori/setup.py 2018-10-28 19:15:16.444766623 +0100 +++ X3DH-0.5.3/setup.py 2018-10-28 19:15:38.028060948 +0100 @@ -24,7 +24,7 @@ author_email = "tim@cifg.io", license = "MIT", packages = find_packages(), - install_requires = [ "cryptography>=1.7.1", "XEdDSA>=0.4.2" ], + install_requires = [], python_requires = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4", zip_safe = True, classifiers = [ ================================================ FILE: pythonforandroid/recipes/xeddsa/__init__.py ================================================ from pythonforandroid.recipe import CythonRecipe from pythonforandroid.toolchain import current_directory, shprint from os.path import join import sh class XedDSARecipe(CythonRecipe): name = 'xeddsa' version = '0.4.4' url = 'https://pypi.python.org/packages/source/X/XEdDSA/XEdDSA-{version}.tar.gz' depends = [ 'setuptools', 'cffi', 'pynacl', ] patches = ['remove_dependencies.patch'] call_hostpython_via_targetpython = False def build_arch(self, arch): with current_directory(join(self.get_build_dir(arch.arch))): env = self.get_recipe_env(arch) hostpython = sh.Command(self.ctx.hostpython) shprint( hostpython, 'ref10/build.py', _env=env ) # the library could be `_crypto_sign.cpython-37m-x86_64-linux-gnu.so` # or simply `_crypto_sign.so` depending on the platform/distribution sh.cp('-a', sh.glob('_crypto_sign*.so'), self.ctx.get_site_packages_dir(arch)) self.install_python_package(arch) recipe = XedDSARecipe() ================================================ FILE: pythonforandroid/recipes/xeddsa/remove_dependencies.patch ================================================ diff -urN XEdDSA-0.4.4.ori/setup.py XEdDSA-0.4.4/setup.py --- XEdDSA-0.4.4.ori/setup.py 2018-09-23 16:08:35.000000000 +0200 +++ XEdDSA-0.4.4/setup.py 2018-10-30 08:21:23.338790184 +0100 @@ -22,9 +22,8 @@ author_email = "tim@cifg.io", license = "MIT", packages = find_packages(), - install_requires = [ "cffi>=1.9.1", "pynacl>=1.0.1" ], - setup_requires = [ "cffi>=1.9.1" ], - cffi_modules = [ os.path.join("ref10", "build.py") + ":ffibuilder" ], + install_requires = ["pynacl>=1.0.1" ], + setup_requires = [], python_requires = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4", include_package_data = True, zip_safe = False, ================================================ FILE: pythonforandroid/recipes/zbar/__init__.py ================================================ from os.path import join from pythonforandroid.recipe import PythonRecipe class ZBarRecipe(PythonRecipe): version = '0.10' # For some reason the version 0.10 on PyPI is not the same as the ones # in sourceforge and GitHub. The one in PyPI has a setup.py. # url = 'https://github.com/ZBar/ZBar/archive/{version}.zip' url = 'https://pypi.python.org/packages/e0/5c/' + \ 'bd2a96a9f2adacffceb4482cdd56831735ab5a67ea6a60c0a8757c17b62e' + \ '/zbar-{version}.tar.gz' call_hostpython_via_targetpython = False depends = ['setuptools', 'libzbar'] patches = ["zbar-0.10-python-crash.patch"] def get_recipe_env(self, arch=None, with_flags_in_cc=True): 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(arch.arch) env['CFLAGS'] += ' -I' + join(libzbar_dir, 'include') env['LDFLAGS'] += ' -L' + join(libzbar_dir, 'zbar', '.libs') env['LIBS'] = env.get('LIBS', '') + ' -landroid -lzbar' return env recipe = ZBarRecipe() ================================================ FILE: pythonforandroid/recipes/zbar/zbar-0.10-python-crash.patch ================================================ https://sourceforge.net/p/zbar/patches/37/ fix from Debian for crashes when importing the python module. http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=702499 this doesn't happen on some arches as the data naturally ends up with zero data after the structure, but on some (like arm), it isn't so we crash when python walks the list. --- a/imagescanner.c +++ b/imagescanner.c @@ -68,6 +68,7 @@ imagescanner_get_results (zbarImageScanner *self, static PyGetSetDef imagescanner_getset[] = { { "results", (getter)imagescanner_get_results, }, + { NULL }, }; static PyObject* ================================================ FILE: pythonforandroid/recipes/zbarlight/__init__.py ================================================ from os.path import join from pythonforandroid.recipe import PythonRecipe class ZBarLightRecipe(PythonRecipe): version = '2.1' url = 'https://github.com/Polyconseil/zbarlight/archive/{version}.tar.gz' # noqa call_hostpython_via_targetpython = False depends = ['setuptools', 'libzbar'] def get_recipe_env(self, arch=None, with_flags_in_cc=True): 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(arch.arch) env['CFLAGS'] += ' -I' + join(libzbar_dir, 'include') env['LDFLAGS'] += ' -L' + join(libzbar_dir, 'zbar', '.libs') env['LIBS'] = env.get('LIBS', '') + ' -landroid -lzbar' return env recipe = ZBarLightRecipe() ================================================ FILE: pythonforandroid/recipes/zeroconf/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe class ZeroconfRecipe(PythonRecipe): name = 'zeroconf' version = '0.24.5' url = 'https://pypi.python.org/packages/source/z/zeroconf/zeroconf-{version}.tar.gz' depends = ['setuptools', 'ifaddr', 'typing;python_version<"3.5"'] call_hostpython_via_targetpython = False recipe = ZeroconfRecipe() ================================================ FILE: pythonforandroid/recipes/zeroconf/patches/setup.patch ================================================ --- zeroconf.orig/setup.py 2015-07-11 21:55:09.000000000 +0200 +++ zeroconf/setup.py 2017-02-23 01:04:13.370018716 +0100 @@ -55,12 +55,5 @@ 'mDNS', ], install_requires=[ - 'enum-compat', - # netifaces 0.10.5 has a bug that results in all interfaces' netmasks - # to be 255.255.255.255 on Windows which breaks things. See: - # * https://github.com/jstasiak/python-zeroconf/issues/84 - # * https://bitbucket.org/al45tair/netifaces/issues/39/netmask-is-always-255255255255 - 'netifaces<=0.10.4', - 'six', ], ) ================================================ FILE: pythonforandroid/recipes/zope/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe from os.path import join class ZopeRecipe(PythonRecipe): name = 'zope' version = '4.1.3' url = 'https://pypi.python.org/packages/source/z/zope.interface/zope.interface-{version}.tar.gz' depends = [] def get_recipe_env(self, 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( self.ctx.get_libs_dir(arch.arch)) env['LDSHARED'] = join(self.ctx.root_dir, 'tools', 'liblink') return env def postbuild_arch(self, arch): super().postbuild_arch(arch) # Should do some deleting here recipe = ZopeRecipe() # FIXME: @mirko liblink & LD ================================================ FILE: pythonforandroid/recipes/zope_interface/__init__.py ================================================ from os.path import join from pythonforandroid.recipe import PythonRecipe from pythonforandroid.toolchain import current_directory from pythonforandroid.util import rmdir class ZopeInterfaceRecipe(PythonRecipe): call_hostpython_via_targetpython = False name = 'zope_interface' version = '4.1.3' url = 'https://pypi.python.org/packages/source/z/zope.interface/zope.interface-{version}.tar.gz' site_packages_name = 'zope.interface' depends = ['setuptools'] patches = ['no_tests.patch'] def build_arch(self, 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 # this is solved in the future an error will be triggered zope_install = join(self.ctx.get_site_packages_dir(arch), 'zope') self.apply_patch('fix-init.patch', arch.arch, build_dir=zope_install) def prebuild_arch(self, arch): super().prebuild_arch(arch) with current_directory(self.get_build_dir(arch.arch)): rmdir('src/zope/interface/tests') rmdir('src/zope/interface/common/tests') recipe = ZopeInterfaceRecipe() ================================================ FILE: pythonforandroid/recipes/zope_interface/fix-init.patch ================================================ The zope.interface module lacks of the __init__.py file in `zope` folder (once is installed), this patch creates that missing file. This seems to be caused during the installation process because that file exists in source files. diff -Naurp zope.orig/__init__.py zope/__init__.py --- zope.orig/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ zope/__init__.py 2019-02-05 11:29:22.666757227 +0100 @@ -0,0 +1 @@ + ================================================ FILE: pythonforandroid/recipes/zope_interface/no_tests.patch ================================================ --- zope_interface/setup.py 2015-10-05 09:35:14.000000000 +0200 +++ b/setup.py 2016-06-15 17:44:35.108263993 +0200 @@ -139,9 +139,8 @@ "Topic :: Software Development :: Libraries :: Python Modules", ], - packages = ['zope', 'zope.interface', 'zope.interface.tests'], + packages = ['zope', 'zope.interface'], package_dir = {'': 'src'}, cmdclass = {'build_ext': optional_build_ext, }, - test_suite = 'zope.interface.tests', **extra) ================================================ FILE: pythonforandroid/recommendations.py ================================================ """Simple functions for checking dependency versions.""" import sys from os.path import join import packaging.version from pythonforandroid.logger import info, warning from pythonforandroid.util import BuildInterruptingException # We only check the NDK major version MIN_NDK_VERSION = 25 MAX_NDK_VERSION = 25 # DO NOT CHANGE LINE FORMAT: buildozer parses the existence of a RECOMMENDED_NDK_VERSION RECOMMENDED_NDK_VERSION = "28c" NDK_DOWNLOAD_URL = "https://developer.android.com/ndk/downloads/" # Important log messages NEW_NDK_MESSAGE = 'Newer NDKs may not be fully supported by p4a.' UNKNOWN_NDK_MESSAGE = ( 'Could not determine NDK version, no source.properties in the NDK dir.' ) PARSE_ERROR_NDK_MESSAGE = ( 'Could not parse $NDK_DIR/source.properties, not checking NDK version.' ) READ_ERROR_NDK_MESSAGE = ( 'Unable to read the NDK version from the given directory {ndk_dir}.' ) ENSURE_RIGHT_NDK_MESSAGE = ( 'Make sure your NDK version is greater than {min_supported}. If you get ' 'build errors, download the recommended NDK {rec_version} from {ndk_url}.' ) NDK_LOWER_THAN_SUPPORTED_MESSAGE = ( 'The minimum supported NDK version is {min_supported}. ' 'You can download it from {ndk_url}.' ) UNSUPPORTED_NDK_API_FOR_ARMEABI_MESSAGE = ( 'Asked to build for armeabi architecture with API ' '{req_ndk_api}, but API {max_ndk_api} or greater does not support armeabi.' ) CURRENT_NDK_VERSION_MESSAGE = ( 'Found NDK version {ndk_version}' ) RECOMMENDED_NDK_VERSION_MESSAGE = ( 'Maximum recommended NDK version is {recommended_ndk_version}, but newer versions may work.' ) def check_ndk_version(ndk_dir): """ Check the NDK version against what is currently recommended and raise an exception of :class:`~pythonforandroid.util.BuildInterruptingException` in case that the user tries to use an NDK lower than minimum supported, specified via attribute `MIN_NDK_VERSION`. .. versionchanged:: 2019.06.06.1.dev0 Added the ability to get android's NDK `letter version` and also rewrote to raise an exception in case that an NDK version lower than the minimum supported is detected. """ ndk_version = read_ndk_version(ndk_dir) if ndk_version is None: warning(READ_ERROR_NDK_MESSAGE.format(ndk_dir=ndk_dir)) warning( ENSURE_RIGHT_NDK_MESSAGE.format( min_supported=MIN_NDK_VERSION, rec_version=RECOMMENDED_NDK_VERSION, ndk_url=NDK_DOWNLOAD_URL, ) ) return # create a dictionary which will describe the relationship of the android's # NDK minor version with the `human readable` letter version, egs: # Pkg.Revision = 17.1.4828580 => ndk-17b # Pkg.Revision = 17.2.4988734 => ndk-17c # Pkg.Revision = 19.0.5232133 => ndk-19 (No letter) minor_to_letter = {0: ''} minor_to_letter.update( {n + 1: chr(i) for n, i in enumerate(range(ord('b'), ord('b') + 25))} ) string_version = f"{ndk_version.major}{minor_to_letter[ndk_version.minor]}" info(CURRENT_NDK_VERSION_MESSAGE.format(ndk_version=string_version)) if ndk_version.major < MIN_NDK_VERSION: raise BuildInterruptingException( NDK_LOWER_THAN_SUPPORTED_MESSAGE.format( min_supported=MIN_NDK_VERSION, ndk_url=NDK_DOWNLOAD_URL ), instructions=( 'Please, go to the android NDK page ({ndk_url}) and download a' ' supported version.\n*** The currently recommended NDK' ' version is {rec_version} ***'.format( ndk_url=NDK_DOWNLOAD_URL, rec_version=RECOMMENDED_NDK_VERSION, ) ), ) elif ndk_version.major > MAX_NDK_VERSION: warning( RECOMMENDED_NDK_VERSION_MESSAGE.format( recommended_ndk_version=RECOMMENDED_NDK_VERSION ) ) warning(NEW_NDK_MESSAGE) def read_ndk_version(ndk_dir): """Read the NDK version from the NDK dir, if possible""" try: with open(join(ndk_dir, 'source.properties')) as fileh: ndk_data = fileh.read() except IOError: info(UNKNOWN_NDK_MESSAGE) return for line in ndk_data.split('\n'): if line.startswith('Pkg.Revision'): break else: info(PARSE_ERROR_NDK_MESSAGE) return # Line should have the form "Pkg.Revision = ..." unparsed_ndk_version = line.split('=')[-1].strip() return packaging.version.parse(unparsed_ndk_version) MIN_TARGET_API = 30 # highest version tested to work fine with SDL2 # should be a good default for other bootstraps too RECOMMENDED_TARGET_API = 33 ARMEABI_MAX_TARGET_API = 21 OLD_API_MESSAGE = ( 'Target APIs lower than 30 are no longer supported on Google Play, ' 'and are not recommended. Note that the Target API can be higher than ' 'your device Android version, and should usually be as high as possible.') def check_target_api(api, arch): """Warn if the user's target API is less than the current minimum recommendation """ # FIXME: Should We remove support for armeabi (ARMv5)? if api >= ARMEABI_MAX_TARGET_API and arch == 'armeabi': raise BuildInterruptingException( UNSUPPORTED_NDK_API_FOR_ARMEABI_MESSAGE.format( req_ndk_api=api, max_ndk_api=ARMEABI_MAX_TARGET_API ), instructions='You probably want to build with --arch=armeabi-v7a instead') if api < MIN_TARGET_API: warning('Target API {} < {}'.format(api, MIN_TARGET_API)) warning(OLD_API_MESSAGE) MIN_NDK_API = 21 RECOMMENDED_NDK_API = 24 OLD_NDK_API_MESSAGE = ('NDK API less than {} is not supported'.format(MIN_NDK_API)) TARGET_NDK_API_GREATER_THAN_TARGET_API_MESSAGE = ( 'Target NDK API is {ndk_api}, ' 'higher than the target Android API {android_api}.' ) def check_ndk_api(ndk_api, android_api): """Warn if the user's NDK is too high or low.""" if ndk_api > android_api: raise BuildInterruptingException( TARGET_NDK_API_GREATER_THAN_TARGET_API_MESSAGE.format( ndk_api=ndk_api, android_api=android_api ), instructions=('The NDK API is a minimum supported API number and must be lower ' 'than the target Android API')) if ndk_api < MIN_NDK_API: warning(OLD_NDK_API_MESSAGE) MIN_PYTHON_MAJOR_VERSION = 3 MIN_PYTHON_MINOR_VERSION = 6 MIN_PYTHON_VERSION = packaging.version.Version( f"{MIN_PYTHON_MAJOR_VERSION}.{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.' ).format(min_version=MIN_PYTHON_VERSION) PY_VERSION_ERROR_TEXT = ( 'Your Python version {user_major}.{user_minor} is not supported by python-for-android, ' 'please upgrade to {min_version} or higher.' ).format( user_major=sys.version_info.major, user_minor=sys.version_info.minor, min_version=MIN_PYTHON_VERSION) def check_python_version(): # Python 2 special cased because it's a major transition. In the # future the major or minor versions can increment more quietly. if sys.version_info.major == 2: raise BuildInterruptingException(PY2_ERROR_TEXT) if ( sys.version_info.major < MIN_PYTHON_MAJOR_VERSION or sys.version_info.minor < MIN_PYTHON_MINOR_VERSION ): raise BuildInterruptingException(PY_VERSION_ERROR_TEXT) def print_recommendations(): """ Print the main recommended dependency versions as simple key-value pairs. """ print('Min supported NDK version: {}'.format(MIN_NDK_VERSION)) print('Recommended NDK version: {}'.format(RECOMMENDED_NDK_VERSION)) print('Min target API: {}'.format(MIN_TARGET_API)) print('Recommended target API: {}'.format(RECOMMENDED_TARGET_API)) print('Min NDK API: {}'.format(MIN_NDK_API)) print('Recommended NDK API: {}'.format(RECOMMENDED_NDK_API)) ================================================ FILE: pythonforandroid/toolchain.py ================================================ #!/usr/bin/env python """ Tool for packaging Python apps for Android ========================================== This module defines the entry point for command line and programmatic use. """ from appdirs import user_data_dir import argparse from functools import wraps import glob import logging import os from os import environ from os.path import (join, dirname, realpath, exists, expanduser, basename) import re import shlex import sys from sys import platform # This must be imported and run before other third-party or p4a # packages. from pythonforandroid.checkdependencies import check check() from packaging.version import Version import sh from pythonforandroid import __version__ from pythonforandroid.bootstrap import Bootstrap from pythonforandroid.build import Context, build_recipes, project_has_setup_py from pythonforandroid.distribution import Distribution, pretty_log_dists from pythonforandroid.entrypoints import main from pythonforandroid.graph import get_recipe_order_and_bootstrap from pythonforandroid.logger import (logger, info, warning, setup_color, Out_Style, Out_Fore, info_notify, info_main, shprint) from pythonforandroid.pythonpackage import get_dep_names_of_package from pythonforandroid.recipe import Recipe from pythonforandroid.recommendations import ( RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API, print_recommendations) from pythonforandroid.util import ( current_directory, BuildInterruptingException, load_source, rmdir, max_build_tool_version, ) user_dir = dirname(realpath(os.path.curdir)) toolchain_dir = dirname(__file__) sys.path.insert(0, join(toolchain_dir, "tools", "external")) def add_boolean_option(parser, names, no_names=None, default=True, dest=None, description=None): group = parser.add_argument_group(description=description) if not isinstance(names, (list, tuple)): names = [names] if dest is None: dest = names[0].strip("-").replace("-", "_") def add_dashes(x): return x if x.startswith("-") else "--"+x opts = [add_dashes(x) for x in names] group.add_argument( *opts, help=("(this is the default)" if default else None), dest=dest, action='store_true') if no_names is None: def add_no(x): x = x.lstrip("-") return ("no_"+x) if "_" in x else ("no-"+x) no_names = [add_no(x) for x in names] opts = [add_dashes(x) for x in no_names] group.add_argument( *opts, help=(None if default else "(this is the default)"), dest=dest, action='store_false') parser.set_defaults(**{dest: default}) def require_prebuilt_dist(func): """Decorator for ToolchainCL methods. If present, the method will automatically make sure a dist has been built before continuing or, if no dists are present or can be obtained, will raise an error. """ @wraps(func) def wrapper_func(self, args, **kw): ctx = self.ctx ctx.set_archs(self._archs) ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir, user_ndk_dir=self.ndk_dir, user_android_api=self.android_api, user_ndk_api=self.ndk_api) dist = self._dist if dist.needs_build: if dist.folder_exists(): # possible if the dist is being replaced dist.delete() info_notify('No dist exists that meets your requirements, ' 'so one will be built.') build_dist_from_args(ctx, dist, args) func(self, args, **kw) return wrapper_func def dist_from_args(ctx, args): """Parses out any distribution-related arguments, and uses them to obtain a Distribution class instance for the build. """ return Distribution.get_distribution( ctx, name=args.dist_name, recipes=split_argument_list(args.requirements), archs=args.arch, ndk_api=args.ndk_api, force_build=args.force_build, require_perfect_match=args.require_perfect_match, allow_replace_dist=args.allow_replace_dist) def build_dist_from_args(ctx, dist, args): """Parses out any bootstrap related arguments, and uses them to build a dist.""" bs = Bootstrap.get_bootstrap(args.bootstrap, ctx) blacklist = getattr(args, "blacklist_requirements", "").split(",") if len(blacklist) == 1 and blacklist[0] == "": blacklist = [] build_order, python_modules, bs = ( get_recipe_order_and_bootstrap( ctx, dist.recipes, bs, blacklist=blacklist )) assert set(build_order).intersection(set(python_modules)) == set() ctx.recipe_build_order = build_order ctx.python_modules = python_modules info('The selected bootstrap is {}'.format(bs.name)) info_main('# Creating dist with {} bootstrap'.format(bs.name)) bs.distribution = dist info_notify('Dist will have name {} and requirements ({})'.format( dist.name, ', '.join(dist.recipes))) info('Dist contains the following requirements as recipes: {}'.format( ctx.recipe_build_order)) info('Dist will also contain modules ({}) installed from pip'.format( ', '.join(ctx.python_modules))) info( 'Dist will be build in mode {build_mode}{with_debug_symbols}'.format( build_mode='debug' if ctx.build_as_debuggable else 'release', with_debug_symbols=' (with debug symbols)' if ctx.with_debug_symbols else '', ) ) ctx.distribution = dist ctx.prepare_bootstrap(bs) if dist.needs_build: ctx.prepare_dist() build_recipes(build_order, python_modules, ctx, getattr(args, "private", None), ignore_project_setup_py=getattr( args, "ignore_setup_py", False ), ) ctx.bootstrap.assemble_distribution() info_main('# Your distribution was created successfully, exiting.') info('Dist can be found at (for now) {}' .format(join(ctx.dist_dir, ctx.distribution.dist_dir))) def split_argument_list(arg_list): if not len(arg_list): return [] return re.split(r'[ ,]+', arg_list) class NoAbbrevParser(argparse.ArgumentParser): """We want to disable argument abbreviation so as not to interfere with passing through arguments to build.py, but in python2 argparse doesn't have this option. This subclass alternative is follows the suggestion at https://bugs.python.org/issue14910. """ def _get_option_tuples(self, option_string): return [] class ToolchainCL: def __init__(self): argv = sys.argv self.warn_on_carriage_return_args(argv) # Buildozer used to pass these arguments in a now-invalid order # If that happens, apply this fix # This fix will be removed once a fixed buildozer is released if (len(argv) > 2 and argv[1].startswith('--color') and argv[2].startswith('--storage-dir')): argv.append(argv.pop(1)) # the --color arg argv.append(argv.pop(1)) # the --storage-dir arg parser = NoAbbrevParser( description='A packaging tool for turning Python scripts and apps ' 'into Android APKs') generic_parser = argparse.ArgumentParser( add_help=False, description='Generic arguments applied to all commands') argparse.ArgumentParser( add_help=False, description='Arguments for dist building') generic_parser.add_argument( '--debug', dest='debug', action='store_true', default=False, help='Display debug output and all build info') generic_parser.add_argument( '--color', dest='color', choices=['always', 'never', 'auto'], help='Enable or disable color output (default enabled on tty)') generic_parser.add_argument( '--sdk-dir', '--sdk_dir', dest='sdk_dir', default='', help='The filepath where the Android SDK is installed') generic_parser.add_argument( '--ndk-dir', '--ndk_dir', dest='ndk_dir', default='', help='The filepath where the Android NDK is installed') generic_parser.add_argument( '--android-api', '--android_api', dest='android_api', default=0, type=int, help=('The Android API level to build against defaults to {} if ' 'not specified.').format(RECOMMENDED_TARGET_API)) generic_parser.add_argument( '--ndk-version', '--ndk_version', dest='ndk_version', default=None, help=('DEPRECATED: the NDK version is now found automatically or ' 'not at all.')) generic_parser.add_argument( '--ndk-api', type=int, default=None, help=('The Android API level to compile against. This should be your ' '*minimal supported* API, not normally the same as your --android-api. ' 'Defaults to min(ANDROID_API, {}) if not specified.').format(RECOMMENDED_NDK_API)) generic_parser.add_argument( '--symlink-bootstrap-files', '--ssymlink_bootstrap_files', action='store_true', dest='symlink_bootstrap_files', default=False, help=('If True, symlinks the bootstrap files ' 'creation. This is useful for development only, it could also' ' cause weird problems.')) default_storage_dir = user_data_dir('python-for-android') if ' ' in default_storage_dir: default_storage_dir = '~/.python-for-android' generic_parser.add_argument( '--storage-dir', dest='storage_dir', default=default_storage_dir, help=('Primary storage directory for downloads and builds ' '(default: {})'.format(default_storage_dir))) generic_parser.add_argument( '--arch', help='The archs to build for.', action='append', default=[]) # Options for specifying the Distribution generic_parser.add_argument( '--dist-name', '--dist_name', help='The name of the distribution to use or create', default='') generic_parser.add_argument( '--requirements', help=('Dependencies of your app, should be recipe names or ' 'Python modules. NOT NECESSARY if you are using ' 'Python 3 with --use-setup-py'), default='') generic_parser.add_argument( '--recipe-blacklist', help=('Blacklist an internal recipe from use. Allows ' 'disabling Python 3 core modules to save size'), dest="recipe_blacklist", default='') generic_parser.add_argument( '--blacklist-requirements', help=('Blacklist an internal recipe from use. Allows ' 'disabling Python 3 core modules to save size'), dest="blacklist_requirements", default='') generic_parser.add_argument( '--bootstrap', help='The bootstrap to build with. Leave unset to choose ' 'automatically.', default=None) generic_parser.add_argument( '--hook', help='Filename to a module that contains python-for-android hooks', default=None) add_boolean_option( generic_parser, ["force-build"], default=False, description='Whether to force compilation of a new distribution') add_boolean_option( generic_parser, ["require-perfect-match"], default=False, description=('Whether the dist recipes must perfectly match ' 'those requested')) add_boolean_option( generic_parser, ["allow-replace-dist"], default=True, description='Whether existing dist names can be automatically replaced' ) generic_parser.add_argument( '--local-recipes', '--local_recipes', dest='local_recipes', default='./p4a-recipes', help='Directory to look for local recipes') generic_parser.add_argument( '--activity-class-name', dest='activity_class_name', default='org.kivy.android.PythonActivity', help='The full java class name of the main activity') generic_parser.add_argument( '--service-class-name', dest='service_class_name', default='org.kivy.android.PythonService', help='Full java package name of the PythonService class') generic_parser.add_argument( '--java-build-tool', dest='java_build_tool', default='auto', choices=['auto', 'ant', 'gradle'], help=('The java build tool to use when packaging the APK, defaults ' 'to automatically selecting an appropriate tool.')) add_boolean_option( generic_parser, ['copy-libs'], default=False, description='Copy libraries instead of using biglink (Android 4.3+)' ) self._read_configuration() subparsers = parser.add_subparsers(dest='subparser_name', help='The command to run') def add_parser(subparsers, *args, **kwargs): """ argparse in python2 doesn't support the aliases option, so we just don't provide the aliases there. """ if 'aliases' in kwargs and sys.version_info.major < 3: kwargs.pop('aliases') return subparsers.add_parser(*args, **kwargs) add_parser( subparsers, 'recommendations', parents=[generic_parser], help='List recommended p4a dependencies') parser_recipes = add_parser( subparsers, 'recipes', parents=[generic_parser], help='List the available recipes') parser_recipes.add_argument( "--compact", action="store_true", default=False, help="Produce a compact list suitable for scripting") add_parser( subparsers, 'bootstraps', help='List the available bootstraps', parents=[generic_parser]) add_parser( subparsers, 'clean_all', aliases=['clean-all'], help='Delete all builds, dists and caches', parents=[generic_parser]) add_parser( subparsers, 'clean_dists', aliases=['clean-dists'], help='Delete all dists', parents=[generic_parser]) add_parser( subparsers, 'clean_bootstrap_builds', aliases=['clean-bootstrap-builds'], help='Delete all bootstrap builds', parents=[generic_parser]) add_parser( subparsers, 'clean_builds', aliases=['clean-builds'], help='Delete all builds', parents=[generic_parser]) parser_clean = add_parser( subparsers, 'clean', help='Delete build components.', parents=[generic_parser]) parser_clean.add_argument( 'component', nargs='+', help=('The build component(s) to delete. You can pass any ' 'number of arguments from "all", "builds", "dists", ' '"distributions", "bootstrap_builds", "downloads".')) parser_clean_recipe_build = add_parser( subparsers, 'clean_recipe_build', aliases=['clean-recipe-build'], help=('Delete the build components of the given recipe. ' 'By default this will also delete built dists'), parents=[generic_parser]) parser_clean_recipe_build.add_argument( 'recipe', help='The recipe name') parser_clean_recipe_build.add_argument( '--no-clean-dists', default=False, dest='no_clean_dists', action='store_true', help='If passed, do not delete existing dists') parser_clean_download_cache = add_parser( subparsers, 'clean_download_cache', aliases=['clean-download-cache'], help='Delete cached downloads for requirement builds', parents=[generic_parser]) parser_clean_download_cache.add_argument( 'recipes', nargs='*', help='The recipes to clean (space-separated). If no recipe name is' ' provided, the entire cache is cleared.') parser_export_dist = add_parser( subparsers, 'export_dist', aliases=['export-dist'], help='Copy the named dist to the given path', parents=[generic_parser]) parser_export_dist.add_argument('output_dir', help='The output dir to copy to') parser_export_dist.add_argument( '--symlink', action='store_true', help='Symlink the dist instead of copying') 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_packaging.add_argument( '--add-asset', dest='assets', action="append", default=[], help='Put this in the assets folder in the apk.') parser_packaging.add_argument( '--add-resource', dest='resources', action="append", default=[], help='Put this in the res folder in the apk.') 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_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_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. " + "This may be required if the setup.py is not " + "designed to work inside p4a (e.g. by installing " + "dependencies that won't work or aren't desired " + "on Android") 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( '--with-debug-symbols', dest='with_debug_symbols', action='store_const', const=True, default=False, help='Will keep debug symbols from `.so` files.') 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_packaging.add_argument( '--signkey', dest='signkey', action='store', default=None, help='Key alias to sign PARSER_APK. with (release build only)') parser_packaging.add_argument( '--keystorepw', dest='keystorepw', action='store', default=None, help='Password for keystore') 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, 'aab', help='Build an AAB', parents=[parser_packaging]) add_parser( subparsers, 'create', help='Compile a set of requirements into a dist', parents=[generic_parser]) add_parser( subparsers, 'archs', help='List the available target architectures', parents=[generic_parser]) add_parser( subparsers, 'distributions', aliases=['dists'], help='List the currently available (compiled) dists', parents=[generic_parser]) add_parser( subparsers, 'delete_dist', aliases=['delete-dist'], help='Delete a compiled dist', parents=[generic_parser]) parser_sdk_tools = add_parser( subparsers, 'sdk_tools', aliases=['sdk-tools'], help='Run the given binary from the SDK tools dis', parents=[generic_parser]) parser_sdk_tools.add_argument( 'tool', help='The binary tool name to run') add_parser( subparsers, 'adb', help='Run adb from the given SDK', parents=[generic_parser]) add_parser( subparsers, 'logcat', help='Run logcat from the given SDK', parents=[generic_parser]) add_parser( subparsers, 'build_status', aliases=['build-status'], help='Print some debug information about current built components', parents=[generic_parser]) parser.add_argument('-v', '--version', action='version', version=__version__) args, unknown = parser.parse_known_args(sys.argv[1:]) args.unknown_args = unknown if getattr(args, "private", None) is not None: # Pass this value on to the internal bootstrap build.py: args.unknown_args += ["--private", args.private] if getattr(args, "build_mode", None) == "release": args.unknown_args += ["--release"] if getattr(args, "with_debug_symbols", False): args.unknown_args += ["--with-debug-symbols"] if getattr(args, "ignore_setup_py", False): args.use_setup_py = False if getattr(args, "activity_class_name", "org.kivy.android.PythonActivity") != 'org.kivy.android.PythonActivity': args.unknown_args += ["--activity-class-name", args.activity_class_name] if getattr(args, "service_class_name", "org.kivy.android.PythonService") != 'org.kivy.android.PythonService': args.unknown_args += ["--service-class-name", args.service_class_name] self.args = args if args.subparser_name is None: parser.print_help() exit(1) setup_color(args.color) if args.debug: logger.setLevel(logging.DEBUG) 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" self.ctx.with_debug_symbols = getattr( args, "with_debug_symbols", False ) # Process requirements and put version in environ if hasattr(args, 'requirements'): requirements = [] # Add dependencies from setup.py, but only if they are recipes # (because otherwise, setup.py itself will install them later) if (project_has_setup_py(getattr(args, "private", None)) and getattr(args, "use_setup_py", False)): try: info("Analyzing package dependencies. MAY TAKE A WHILE.") # Get all the dependencies corresponding to a recipe: dependencies = [ dep.lower() for dep in get_dep_names_of_package( args.private, keep_version_pins=True, recursive=True, verbose=True, ) ] info("Dependencies obtained: " + str(dependencies)) all_recipes = [ recipe.lower() for recipe in set(Recipe.list_recipes(self.ctx)) ] dependencies = set(dependencies).intersection( set(all_recipes) ) # Add dependencies to argument list: if len(dependencies) > 0: if len(args.requirements) > 0: args.requirements += u"," args.requirements += u",".join(dependencies) except ValueError: # Not a python package, apparently. warning( "Processing failed, is this project a valid " "package? Will continue WITHOUT setup.py deps." ) # Parse --requirements argument list: for requirement in split_argument_list(args.requirements): if "==" in requirement: requirement, version = requirement.split(u"==", 1) os.environ["VERSION_{}".format(requirement)] = version info('Recipe {}: version "{}" requested'.format( requirement, version)) requirements.append(requirement) args.requirements = u",".join(requirements) self.warn_on_deprecated_args(args) self.storage_dir = args.storage_dir self.ctx.setup_dirs(self.storage_dir) self.sdk_dir = args.sdk_dir self.ndk_dir = args.ndk_dir self.android_api = args.android_api self.ndk_api = args.ndk_api self.ctx.symlink_bootstrap_files = args.symlink_bootstrap_files self.ctx.java_build_tool = args.java_build_tool self._archs = args.arch self.ctx.local_recipes = realpath(args.local_recipes) self.ctx.copy_libs = args.copy_libs self.ctx.activity_class_name = args.activity_class_name self.ctx.service_class_name = args.service_class_name # Each subparser corresponds to a method command = args.subparser_name.replace('-', '_') getattr(self, command)(args) @staticmethod def warn_on_carriage_return_args(args): for check_arg in args: if '\r' in check_arg: warning("Argument '{}' contains a carriage return (\\r).".format(str(check_arg.replace('\r', '')))) warning("Invoking this program via scripts which use CRLF instead of LF line endings will have undefined behaviour.") def warn_on_deprecated_args(self, args): """ Print warning messages for any deprecated arguments that were passed. """ # Output warning if setup.py is present and neither --ignore-setup-py # nor --use-setup-py was specified. if project_has_setup_py(getattr(args, "private", None)): if not getattr(args, "use_setup_py", False) and \ not getattr(args, "ignore_setup_py", False): warning(" **** FUTURE BEHAVIOR CHANGE WARNING ****") warning("Your project appears to contain a setup.py file.") warning("Currently, these are ignored by default.") warning("This will CHANGE in an upcoming version!") warning("") warning("To ensure your setup.py is ignored, please specify:") warning(" --ignore-setup-py") warning("") warning("To enable what will some day be the default, specify:") warning(" --use-setup-py") # NDK version is now determined automatically if args.ndk_version is not None: warning('--ndk-version is deprecated and no longer necessary, ' 'the value you passed is ignored') if 'ANDROIDNDKVER' in environ: warning('$ANDROIDNDKVER is deprecated and no longer necessary, ' 'the value you set is ignored') def hook(self, name): if not self.args.hook: return if not hasattr(self, "hook_module"): # first time, try to load the hook module self.hook_module = load_source( "pythonforandroid.hook", self.args.hook) if hasattr(self.hook_module, name): info("Hook: execute {}".format(name)) getattr(self.hook_module, name)(self) else: info("Hook: ignore {}".format(name)) @property def default_storage_dir(self): udd = user_data_dir('python-for-android') if ' ' in udd: udd = '~/.python-for-android' return udd @staticmethod def _read_configuration(): # search for a .p4a configuration file in the current directory if not exists(".p4a"): return info("Reading .p4a configuration") with open(".p4a") as fd: lines = fd.readlines() lines = [shlex.split(line) for line in lines if not line.startswith("#")] for line in lines: for arg in line: sys.argv.append(arg) def recipes(self, args): """ Prints recipes basic info, e.g. .. code-block:: bash python3 3.7.1 depends: ['hostpython3', 'sqlite3', 'openssl', 'libffi'] conflicts: [] optional depends: ['sqlite3', 'libffi', 'openssl'] """ ctx = self.ctx if args.compact: print(" ".join(set(Recipe.list_recipes(ctx)))) else: for name in sorted(Recipe.list_recipes(ctx)): try: recipe = Recipe.get_recipe(name, ctx) except (IOError, ValueError): warning('Recipe "{}" could not be loaded'.format(name)) except SyntaxError: import traceback traceback.print_exc() warning(('Recipe "{}" could not be loaded due to a ' 'syntax error').format(name)) version = str(recipe.version) print('{Fore.BLUE}{Style.BRIGHT}{recipe.name:<12} ' '{Style.RESET_ALL}{Fore.LIGHTBLUE_EX}' '{version:<8}{Style.RESET_ALL}'.format( recipe=recipe, Fore=Out_Fore, Style=Out_Style, version=version)) print(' {Fore.GREEN}depends: {recipe.depends}' '{Fore.RESET}'.format(recipe=recipe, Fore=Out_Fore)) if recipe.conflicts: print(' {Fore.RED}conflicts: {recipe.conflicts}' '{Fore.RESET}' .format(recipe=recipe, Fore=Out_Fore)) if recipe.opt_depends: print(' {Fore.YELLOW}optional depends: ' '{recipe.opt_depends}{Fore.RESET}' .format(recipe=recipe, Fore=Out_Fore)) def bootstraps(self, _args): """List all the bootstraps available to build with.""" for bs in Bootstrap.all_bootstraps(): bs = Bootstrap.get_bootstrap(bs, self.ctx) print('{Fore.BLUE}{Style.BRIGHT}{bs.name}{Style.RESET_ALL}' .format(bs=bs, Fore=Out_Fore, Style=Out_Style)) print(' {Fore.GREEN}depends: {bs.recipe_depends}{Fore.RESET}' .format(bs=bs, Fore=Out_Fore)) def clean(self, args): components = args.component component_clean_methods = { 'all': self.clean_all, 'dists': self.clean_dists, 'distributions': self.clean_dists, 'builds': self.clean_builds, 'bootstrap_builds': self.clean_bootstrap_builds, 'downloads': self.clean_download_cache} for component in components: if component not in component_clean_methods: raise BuildInterruptingException(( 'Asked to clean "{}" but this argument is not ' 'recognised'.format(component))) component_clean_methods[component](args) def clean_all(self, args): """Delete all build components; the package cache, package builds, bootstrap builds and distributions.""" self.clean_dists(args) self.clean_builds(args) self.clean_download_cache(args) def clean_dists(self, _args): """Delete all compiled distributions in the internal distribution directory.""" ctx = self.ctx rmdir(ctx.dist_dir) def clean_bootstrap_builds(self, _args): """Delete all the bootstrap builds.""" rmdir(join(self.ctx.build_dir, 'bootstrap_builds')) # for bs in Bootstrap.all_bootstraps(): # bs = Bootstrap.get_bootstrap(bs, self.ctx) # if bs.build_dir and exists(bs.build_dir): # info('Cleaning build for {} bootstrap.'.format(bs.name)) # rmdir(bs.build_dir) def clean_builds(self, _args): """Delete all build caches for each recipe, python-install, java code and compiled libs collection. This does *not* delete the package download cache or the final distributions. You can also use clean_recipe_build to delete the build of a specific recipe. """ ctx = self.ctx rmdir(ctx.build_dir) rmdir(ctx.python_installs_dir) libs_dir = join(self.ctx.build_dir, 'libs_collections') rmdir(libs_dir) def clean_recipe_build(self, args): """Deletes the build files of the given recipe. This is intended for debug purposes. You may experience strange behaviour or problems with some recipes if their build has made unexpected state changes. If this happens, run clean_builds, or attempt to clean other recipes until things work again. """ recipe = Recipe.get_recipe(args.recipe, self.ctx) info('Cleaning build for {} recipe.'.format(recipe.name)) recipe.clean_build() if not args.no_clean_dists: self.clean_dists(args) def clean_download_cache(self, args): """ Deletes a download cache for recipes passed as arguments. If no argument is passed, it'll delete *all* downloaded caches. :: p4a clean_download_cache kivy,pyjnius This does *not* delete the build caches or final distributions. """ ctx = self.ctx if hasattr(args, 'recipes') and args.recipes: for package in args.recipes: remove_path = join(ctx.packages_path, package) if exists(remove_path): rmdir(remove_path) info('Download cache removed for: "{}"'.format(package)) else: warning('No download cache found for "{}", skipping'.format( package)) else: if exists(ctx.packages_path): rmdir(ctx.packages_path) info('Download cache removed.') else: print('No cache found at "{}"'.format(ctx.packages_path)) @require_prebuilt_dist def export_dist(self, args): """Copies a created dist to an output dir. This makes it easy to navigate to the dist to investigate it or call build.py, though you do not in general need to do this and can use the apk command instead. """ ctx = self.ctx dist = dist_from_args(ctx, args) if dist.needs_build: raise BuildInterruptingException( 'You asked to export a dist, but there is no dist ' 'with suitable recipes available. For now, you must ' ' create one first with the create argument.') if args.symlink: shprint(sh.ln, '-s', dist.dist_dir, args.output_dir) else: shprint(sh.cp, '-r', dist.dist_dir, args.output_dir) @property def _dist(self): ctx = self.ctx dist = dist_from_args(ctx, self.args) ctx.distribution = dist return 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 """ fix_args = ('--dir', '--private', '--add-jar', '--add-source', '--whitelist', '--blacklist', '--presplash', '--icon', '--icon-bg', '--icon-fg') unknown_args = args.unknown_args for asset in args.assets: if ":" in asset: asset_src, asset_dest = asset.split(":") else: asset_src = asset_dest = asset # take abspath now, because build.py will be run in bootstrap dir unknown_args += ["--asset", os.path.abspath(asset_src)+":"+asset_dest] for resource in args.resources: if ":" in resource: resource_src, resource_dest = resource.split(":") else: resource_src = resource resource_dest = "" # take abspath now, because build.py will be run in bootstrap dir unknown_args += ["--resource", os.path.abspath(resource_src)+":"+resource_dest] for i, arg in enumerate(unknown_args): argx = arg.split('=') if argx[0] in fix_args: if len(argx) > 1: unknown_args[i] = '='.join( (argx[0], realpath(expanduser(argx[1])))) 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: env['P4A_RELEASE_KEYSTORE'] = realpath(expanduser(args.keystore)) if args.signkey: env['P4A_RELEASE_KEYALIAS'] = args.signkey if args.keystorepw: env['P4A_RELEASE_KEYSTORE_PASSWD'] = args.keystorepw if args.signkeypw: env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.signkeypw elif args.keystorepw and 'P4A_RELEASE_KEYALIAS_PASSWD' not in env: env['P4A_RELEASE_KEYALIAS_PASSWD'] = args.keystorepw 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', 'aab' :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 = 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_version = max_build_tool_version(build_tools_versions) info(('Detected highest available build tools ' 'version to be {}').format(build_tools_version)) if Version(build_tools_version.replace(" ", "")) < 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, _tail=20, _critical=True, _env=env ) if args.build_mode == "debug": if package_type == "aab": raise BuildInterruptingException( "aab is meant only for distribution and is not available in debug mode. " "Instead, you can use apk while building for debugging purposes." ) gradle_task = "assembleDebug" elif args.build_mode == "release": if package_type in ["apk", "aar"]: gradle_task = "assembleRelease" elif package_type == "aab": gradle_task = "bundleRelease" else: raise BuildInterruptingException( "Unknown build mode {} for apk()".format(args.build_mode)) # WARNING: We should make sure to clean the build directory before building. # See PR: kivy/python-for-android#2705 output = shprint(gradlew, "clean", gradle_task, _tail=20, _critical=True, _env=env) return output, build_args 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', 'aab' :param output_dir: where to put the package file """ 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 = package_re.match(line) if m: package_file = m.groups()[0] break 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: 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(package_files[-1])) package_file = package_files[-1] break else: raise BuildInterruptingException('Couldn\'t find the built APK') info_main('# Found android package file: {}'.format(package_file)) package_extension = f".{package_type}" if package_add_version: info('# Add version number to android package') package_name = basename(package_file)[:-len(package_extension)] package_file_dest = "{}-{}{}".format( package_name, build_args.version, package_extension) info('# Android package renamed to {}'.format(package_file_dest)) shprint(sh.cp, package_file, package_file_dest) else: 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 aab(self, args): output, build_args = self._build_package(args, package_type='aab') output_dir = join(self._dist.dist_dir, "build", "outputs", 'bundle', args.build_mode) self._finish_package(args, output, build_args, 'aab', output_dir) @require_prebuilt_dist def create(self, args): """Create a distribution directory if it doesn't already exist, run any recipes if necessary, and build the apk. """ pass # The decorator does everything def archs(self, _args): """List the target architectures available to be built for.""" print('{Style.BRIGHT}Available target architectures are:' '{Style.RESET_ALL}'.format(Style=Out_Style)) for arch in self.ctx.archs: print(' {}'.format(arch.arch)) def dists(self, args): """The same as :meth:`distributions`.""" self.distributions(args) def distributions(self, _args): """Lists all distributions currently available (i.e. that have already been built).""" ctx = self.ctx dists = Distribution.get_distributions(ctx) if dists: print('{Style.BRIGHT}Distributions currently installed are:' '{Style.RESET_ALL}'.format(Style=Out_Style)) pretty_log_dists(dists, print) else: print('{Style.BRIGHT}There are no dists currently built.' '{Style.RESET_ALL}'.format(Style=Out_Style)) def delete_dist(self, _args): dist = self._dist if not dist.folder_exists(): info('No dist exists that matches your specifications, ' 'exiting without deleting.') return dist.delete() def sdk_tools(self, args): """Runs the android binary from the detected SDK directory, passing all arguments straight to it. This binary is used to install e.g. platform-tools for different API level targets. This is intended as a convenience function if android is not in your $PATH. """ ctx = self.ctx ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir, user_ndk_dir=self.ndk_dir, user_android_api=self.android_api, user_ndk_api=self.ndk_api) android = sh.Command(join(ctx.sdk_dir, 'tools', args.tool)) output = android( *args.unknown_args, _iter=True, _out_bufsize=1, _err_to_out=True) for line in output: sys.stdout.write(line) sys.stdout.flush() def adb(self, args): """Runs the adb binary from the detected SDK directory, passing all arguments straight to it. This is intended as a convenience function if adb is not in your $PATH. """ self._adb(args.unknown_args) def logcat(self, args): """Runs ``adb logcat`` using the adb binary from the detected SDK directory. All extra args are passed as arguments to logcat.""" self._adb(['logcat'] + args.unknown_args) def _adb(self, commands): """Call the adb executable from the SDK, passing the given commands as arguments.""" ctx = self.ctx ctx.prepare_build_environment(user_sdk_dir=self.sdk_dir, user_ndk_dir=self.ndk_dir, user_android_api=self.android_api, user_ndk_api=self.ndk_api) if platform in ('win32', 'cygwin'): adb = sh.Command(join(ctx.sdk_dir, 'platform-tools', 'adb.exe')) else: adb = sh.Command(join(ctx.sdk_dir, 'platform-tools', 'adb')) info_notify('Starting adb...') output = adb(*commands, _iter=True, _out_bufsize=1, _err_to_out=True) for line in output: sys.stdout.write(line) sys.stdout.flush() def recommendations(self, args): print_recommendations() def build_status(self, _args): """Print the status of the specified build. """ print('{Style.BRIGHT}Bootstraps whose core components are probably ' 'already built:{Style.RESET_ALL}'.format(Style=Out_Style)) bootstrap_dir = join(self.ctx.build_dir, 'bootstrap_builds') if exists(bootstrap_dir): for filen in os.listdir(bootstrap_dir): print(' {Fore.GREEN}{Style.BRIGHT}{filen}{Style.RESET_ALL}' .format(filen=filen, Fore=Out_Fore, Style=Out_Style)) print('{Style.BRIGHT}Recipes that are probably already built:' '{Style.RESET_ALL}'.format(Style=Out_Style)) other_builds_dir = join(self.ctx.build_dir, 'other_builds') if exists(other_builds_dir): for filen in sorted(os.listdir(other_builds_dir)): name = filen.split('-')[0] dependencies = filen.split('-')[1:] recipe_str = (' {Style.BRIGHT}{Fore.GREEN}{name}' '{Style.RESET_ALL}'.format( Style=Out_Style, name=name, Fore=Out_Fore)) if dependencies: recipe_str += ( ' ({Fore.BLUE}with ' + ', '.join(dependencies) + '{Fore.RESET})').format(Fore=Out_Fore) recipe_str += '{Style.RESET_ALL}'.format(Style=Out_Style) print(recipe_str) if __name__ == "__main__": main() ================================================ FILE: pythonforandroid/tools/biglink ================================================ #!/usr/bin/env python import os import sys import subprocess sofiles = [] for directory in sys.argv[2:]: for fn in os.listdir(directory): fn = os.path.join(directory, fn) if not fn.endswith(".so.o"): continue if not os.path.exists(fn[:-2] + ".libs"): continue sofiles.append(fn[:-2]) # The raw argument list. args = [] for fn in sofiles: afn = fn + ".o" libsfn = fn + ".libs" args.append(afn) with open(libsfn) as fd: data = fd.read() args.extend(data.split(" ")) unique_args = [] while args: a = args.pop() if a in ('-L', ): continue if a not in unique_args: unique_args.insert(0, a) unique_args = [x for x in unique_args if x] print('Biglink create %s library' % sys.argv[1]) print('Biglink arguments:') for arg in unique_args: print(' %s' % arg) args = os.environ['CC'].split() + \ ['-shared', '-O3', '-o', sys.argv[1]] + \ unique_args sys.exit(subprocess.call(args)) ================================================ FILE: pythonforandroid/tools/liblink ================================================ #!/usr/bin/env python import sys import subprocess from os import environ from os.path import basename, join libs = [ ] objects = [ ] output = None copylibs = environ.get('COPYLIBS', '0') == '1' i = 1 while i < len(sys.argv): opt = sys.argv[i] i += 1 if opt == "-o": output = sys.argv[i] i += 1 continue if opt.startswith(("-l", "-L")): libs.append(opt) continue if opt in ("-r", "-pipe", "-no-cpp-precomp"): continue if opt in ("--sysroot", "-isysroot", "-framework", "-undefined", "-macosx_version_min"): i += 1 continue if opt.startswith( ("-I", "-isystem", "-m", "-f", "-O", "-g", "-D", "-R")): continue if opt.startswith("-"): print(sys.argv) print("Unknown option: %s" % opt) sys.exit(1) if not opt.endswith('.o'): continue objects.append(opt) print('liblink path is', str(environ.get('LIBLINK_PATH'))) abs_output = join(environ.get('LIBLINK_PATH'), basename(output)) if not copylibs: f = open(output, "w") f.close() output = abs_output f = open(output + ".libs", "w") f.write(" ".join(libs)) f.close() sys.exit(subprocess.call([ environ.get('LD'), '-r', '-o', output + '.o' #, '-arch', environ.get('ARCH') ] + objects)) else: with open(abs_output + '.libs', 'w') as f_libs: with open(abs_output + '.libdirs', 'w') as f_libdirs: for l in libs: if l[1] == 'l': f_libs.write(l[2:]) f_libs.write(' ') else: f_libdirs.write(l[2:]) f_libdirs.write(' ') libargs = ' '.join(["'%s'" % arg for arg in sys.argv[1:]]) cmd = '%s -shared %s %s' % (environ['CC'], environ['LDFLAGS'], libargs) sys.exit(subprocess.call(cmd, shell=True)) ================================================ FILE: pythonforandroid/tools/liblink.sh ================================================ #!/bin/sh PYTHONPATH= python `dirname $0`/liblink "$@" ================================================ FILE: pythonforandroid/util.py ================================================ import contextlib from unittest import mock from fnmatch import fnmatch import logging from os.path import exists, join from os import getcwd, chdir, makedirs, walk from pathlib import Path from platform import uname import shutil from tempfile import mkdtemp import packaging.version from pythonforandroid.logger import (logger, Err_Fore, error, info) LOGGER = logging.getLogger("p4a.util") build_platform = "{system}-{machine}".format( system=uname().system, machine=uname().machine ).lower() """the build platform in the format `system-machine`. We use this string to define the right build system when compiling some recipes or to get the right path for clang compiler""" @contextlib.contextmanager def current_directory(new_dir): cur_dir = getcwd() logger.info(''.join((Err_Fore.CYAN, '-> directory context ', new_dir, Err_Fore.RESET))) chdir(new_dir) yield logger.info(''.join((Err_Fore.CYAN, '<- directory context ', cur_dir, Err_Fore.RESET))) chdir(cur_dir) @contextlib.contextmanager def temp_directory(): temp_dir = mkdtemp() try: logger.debug(''.join((Err_Fore.CYAN, ' + temp directory used ', temp_dir, Err_Fore.RESET))) yield temp_dir finally: shutil.rmtree(temp_dir) logger.debug(''.join((Err_Fore.CYAN, ' - temp directory deleted ', temp_dir, Err_Fore.RESET))) def walk_valid_filens(base_dir, invalid_dir_names, invalid_file_patterns, excluded_dir_exceptions=None): """Recursively walks all the files and directories in ``dirn``, ignoring directories that match any pattern in ``invalid_dirns`` and files that patch any pattern in ``invalid_filens``. ``invalid_dirns`` and ``invalid_filens`` should both be lists of strings to match. ``invalid_dir_patterns`` expects a list of invalid directory names, while ``invalid_file_patterns`` expects a list of glob patterns compared against the full filepath. File and directory paths are evaluated as full paths relative to ``dirn``. If ``excluded_dir_exceptions`` is given, any directory path that contains any of those strings will *not* exclude subdirectories matching ``invalid_dir_names``. """ excluded_dir_exceptions = [] if excluded_dir_exceptions is None else excluded_dir_exceptions for dirn, subdirs, filens in walk(base_dir): allow_invalid_dirs = any(ex in dirn for ex in excluded_dir_exceptions) # Remove invalid subdirs so that they will not be walked if not allow_invalid_dirs: for i in reversed(range(len(subdirs))): subdir = subdirs[i] if subdir in invalid_dir_names: subdirs.pop(i) for filen in filens: for pattern in invalid_file_patterns: if fnmatch(filen, pattern): break else: yield join(dirn, filen) def 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.3 and 3.4: from importlib.machinery import SourceFileLoader return SourceFileLoader(module, filename).load_module() class BuildInterruptingException(Exception): def __init__(self, message, instructions=None): super().__init__(message, instructions) self.message = message self.instructions = instructions def handle_build_exception(exception): """ Handles a raised BuildInterruptingException by printing its error message and associated instructions, if any, then exiting. """ error('Build failed: {}'.format(exception.message)) if exception.instructions is not None: info('Instructions: {}'.format(exception.instructions)) exit(1) def rmdir(dn, ignore_errors=False): if not exists(dn): return LOGGER.debug("Remove directory and subdirectory {}".format(dn)) shutil.rmtree(dn, ignore_errors) def ensure_dir(dn): if exists(dn): return LOGGER.debug("Create directory {0}".format(dn)) makedirs(dn) def move(source, destination): LOGGER.debug("Moving {} to {}".format(source, destination)) shutil.move(source, destination) def touch(filename): Path(filename).touch() def build_tools_version_sort_key( version_string: str, ) -> packaging.version.Version: """ Returns a packaging.version.Version object for comparison purposes. It includes canonicalization of the version string to allow for comparison of versions with spaces in them (historically, RC candidates) If the version string is invalid, it returns a version object with version 0, which will be sorted at worst position. """ try: # Historically, Android build release candidates have had # spaces in the version number. return packaging.version.Version(version_string.replace(" ", "")) except packaging.version.InvalidVersion: # Put badly named versions at worst position. return packaging.version.Version("0") def max_build_tool_version( build_tools_versions: list, ) -> str: """ Returns the maximum build tools version from a list of build tools versions. It uses the :meth:`build_tools_version_sort_key` function to canonicalize the version strings and then returns the maximum version. """ return max(build_tools_versions, key=build_tools_version_sort_key) def patch_wheel_setuptools_logging(): """ When setuptools is not present and the root logger has no handlers, Wheels would configure the root logger with DEBUG level, refs: - https://github.com/pypa/wheel/blob/0.44.0/src/wheel/util.py - https://github.com/pypa/wheel/blob/0.44.0/src/wheel/_setuptools_logging.py Both of these conditions are met in our CI, leading to very verbose and unreadable `sh` logs. Patching it prevents that. """ return mock.patch("wheel._setuptools_logging.configure") ================================================ FILE: setup.py ================================================ import glob from io import open # for open(..,encoding=...) parameter in python 2 from os import walk from os.path import join, dirname, sep import re from setuptools import setup, find_packages # NOTE: All package data should also be set in MANIFEST.in packages = find_packages() package_data = {'': ['*.tmpl', '*.patch', '*.diff', ], } 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', 'sh>=2, <3.0; sys_platform!="win32"', 'meson', 'ninja', 'build', 'toml', 'packaging', 'setuptools', 'wheel~=0.43.0' ] # (build 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 # everything as a 'pythonforandroid' rule, using '' apparently doesn't # work. 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): continue filename = join(root, fn) directory = 'pythonforandroid' if directory not in results: results[directory] = [] results[directory].append(join(*filename.split(sep)[1:])) recursively_include(package_data, 'pythonforandroid/recipes', ['*.patch', 'Setup*', '*.pyx', '*.py', '*.c', '*.h', '*.mk', '*.jam', '*.diff', ]) recursively_include(package_data, 'pythonforandroid/bootstraps', [ '*.properties', '*.xml', '*.java', '*.tmpl', '*.txt', '*.png', '*.mk', '*.c', '*.h', '*.py', '*.sh', '*.jpg', '*.aidl', '*.gradle', '.gitkeep', 'gradlew*', '*.jar', '*.patch', ]) recursively_include(package_data, 'pythonforandroid/bootstraps', ['sdl-config', ]) recursively_include(package_data, 'pythonforandroid/bootstraps/webview', ['*.html', ]) recursively_include(package_data, 'pythonforandroid', ['liblink', 'biglink', 'liblink.sh']) with open(join(dirname(__file__), 'README.md'), encoding="utf-8", errors="replace", ) as fileh: long_description = fileh.read() init_filen = join(dirname(__file__), 'pythonforandroid', '__init__.py') version = None try: with open(init_filen, encoding="utf-8", errors="replace") as fileh: lines = fileh.readlines() except IOError: pass else: for line in lines: line = line.strip() if line.startswith('__version__ = '): matches = re.findall(r'["\'].+["\']', line) if matches: version = matches[0].strip("'").strip('"') break if version is None: raise Exception( 'Error: version could not be loaded from {}'.format(init_filen)) setup(name='python-for-android', version=version, description=( 'A development tool that packages Python apps into ' 'binaries that can run on Android devices.' ), long_description=long_description, long_description_content_type='text/markdown', python_requires=">=3.7.0", author='Kivy Team and other contributors', author_email='kivy-dev@googlegroups.com', url='https://github.com/kivy/python-for-android', license='MIT', install_requires=install_reqs, entry_points={ 'console_scripts': [ 'python-for-android = pythonforandroid.entrypoints:main', 'p4a = pythonforandroid.entrypoints:main', ], 'distutils.commands': [ 'apk = pythonforandroid.bdistapk:BdistAPK', 'aar = pythonforandroid.bdistapk:BdistAAR', 'aab = pythonforandroid.bdistapk:BdistAAB', ], }, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: Microsoft :: Windows', 'Operating System :: OS Independent', 'Operating System :: POSIX :: Linux', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Android', 'Programming Language :: C', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Topic :: Software Development', 'Topic :: Utilities', ], packages=packages, package_data=package_data, project_urls={ 'Documentation': "https://python-for-android.readthedocs.io", 'Source': "https://github.com/kivy/python-for-android", 'Bug Reports': "https://github.com/kivy/python-for-android/issues", }, ) ================================================ FILE: testapps/on_device_unit_tests/README.rst ================================================ 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 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 with buildozer =============================== This app can be built using buildozer, which it also serves as a test for:: $ buildozer android debug Install on an Android device:: $ adb install -r adb install -r bin/p4aunittests-0.1-debug.apk # or $ buildozer android deploy Run the app and check in logcat that all the tests pass:: $ adb logcat | grep python # or look up the adb syntax for this ================================================ FILE: testapps/on_device_unit_tests/buildozer.spec ================================================ [app] # (str) Title of your application title = p4a unit tests # (str) Package name package.name = p4aunittests # (str) Package domain (needed for android/ios packaging) package.domain = org.kivy # (str) Source code where the main.py live source.dir = test_app # (list) Source files to include (let empty to include all the files) 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 # (list) Source files to exclude (let empty to not exclude anything) #source.exclude_exts = spec # (list) List of directory to exclude (let empty to not exclude anything) #source.exclude_dirs = tests, bin # (list) List of exclusions using pattern matching #source.exclude_patterns = license,images/*/*.jpg # (str) Application versioning (method 1) version = 0.1 # (str) Application versioning (method 2) # version.regex = __version__ = ['"](.*)['"] # version.filename = %(source.dir)s/main.py # (list) Application requirements # comma separated e.g. requirements = sqlite3,kivy requirements = python3,kivy,libffi,openssl,numpy,sqlite3 # (str) Custom source folders for requirements # Sets custom source for any requirements with recipes # requirements.source.kivy = ../../kivy # (list) Garden requirements #garden_requirements = # (str) Presplash of the application #presplash.filename = %(source.dir)s/data/presplash.png # (str) Icon of the application #icon.filename = %(source.dir)s/data/icon.png # (str) Supported orientation (one of landscape, sensorLandscape, portrait or all) orientation = all # (list) List of service to declare #services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY # # OSX Specific # # # author = © Copyright Info # change the major version of python used by the app osx.python_version = 3 # Kivy version to use osx.kivy_version = 1.9.1 # # Android specific # # (bool) Indicate if the application should be fullscreen or not fullscreen = 0 # (string) Presplash background color (for new android toolchain) # 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. #android.presplash_color = #FFFFFF # (list) Permissions #android.permissions = INTERNET # (int) Target Android API, should be as high as possible. android.api = 35 # (int) Minimum API your APK will support. #android.minapi = 21 # (int) Android SDK version to use #android.sdk = 20 # (str) Android NDK version to use #android.ndk = 17c # (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi. #android.ndk_api = 21 # (bool) Use --private data storage (True) or --dir public storage (False) #android.private_storage = True # (str) Android NDK directory (if empty, it will be automatically downloaded.) #android.ndk_path = # (str) Android SDK directory (if empty, it will be automatically downloaded.) #android.sdk_path = # (str) ANT directory (if empty, it will be automatically downloaded.) #android.ant_path = # (bool) If True, then skip trying to update the Android sdk # This can be useful to avoid excess Internet downloads or save time # when an update is due and you just want to test/build your package # android.skip_update = False # (str) Android entry point, default is ok for Kivy-based app #android.entrypoint = org.renpy.android.PythonActivity # (list) Pattern to whitelist for the whole project android.whitelist = unittest/* # (str) Path to a custom whitelist file #android.whitelist_src = # (str) Path to a custom blacklist file #android.blacklist_src = # (list) List of Java .jar files to add to the libs so that pyjnius can access # their classes. Don't add jars that you do not need, since extra jars can slow # down the build process. Allows wildcards matching, for example: # OUYA-ODK/libs/*.jar #android.add_jars = foo.jar,bar.jar,path/to/more/*.jar # (list) List of Java files to add to the android project (can be java or a # directory containing the files) #android.add_src = # (list) Android AAR archives to add (currently works only with sdl2_gradle # bootstrap) #android.add_aars = # (list) Gradle dependencies to add (currently works only with sdl2_gradle # bootstrap) #android.gradle_dependencies = # (list) Java classes to add as activities to the manifest. #android.add_activites = com.example.ExampleActivity # (str) python-for-android branch to use, defaults to master p4a.branch = develop # (str) OUYA Console category. Should be one of GAME or APP # If you leave this blank, OUYA support will not be enabled #android.ouya.category = GAME # (str) Filename of OUYA Console icon. It must be a 732x412 png image. #android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png # (str) XML file to include as an intent filters in tag #android.manifest.intent_filters = # (str) launchMode to set for the main activity #android.manifest.launch_mode = standard # (list) Android additional libraries to copy into libs/armeabi #android.add_libs_armeabi = libs/android/*.so #android.add_libs_armeabi_v7a = libs/android-v7/*.so #android.add_libs_x86 = libs/android-x86/*.so #android.add_libs_mips = libs/android-mips/*.so # (bool) Indicate whether the screen should stay on # Don't forget to add the WAKE_LOCK permission if you set this to True #android.wakelock = False # (list) Android application meta-data to set (key=value format) #android.meta_data = # (list) Android library project to add (will be added in the # project.properties automatically.) #android.library_references = # (str) Android logcat filters to use #android.logcat_filters = *:S python:D # (bool) Copy library instead of making a libpymodules.so #android.copy_libs = 1 # (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86 android.arch = armeabi-v7a # # Python for android (p4a) specific # # (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) #p4a.source_dir = # (str) The directory in which python-for-android should look for your own build recipes (if any) #p4a.local_recipes = # (str) Filename to the hook for p4a #p4a.hook = # (str) Bootstrap to use for android builds # p4a.bootstrap = sdl2 # (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask) #p4a.port = # # iOS specific # # (str) Path to a custom kivy-ios folder #ios.kivy_ios_dir = ../kivy-ios # (str) Name of the certificate to use for signing the debug version # Get a list of available identities: buildozer ios list_identities #ios.codesign.debug = "iPhone Developer: ()" # (str) Name of the certificate to use for signing the release version #ios.codesign.release = %(ios.codesign.debug)s [buildozer] # (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) log_level = 2 # (int) Display warning if buildozer is run as root (0 = False, 1 = True) warn_on_root = 1 # (str) Path to build artifact storage, absolute or relative to spec file # build_dir = ./.buildozer # (str) Path to build output (i.e. .apk, .ipa) storage # bin_dir = ./bin # ----------------------------------------------------------------------------- # List as sections # # You can define all the "list" as [section:key]. # Each line will be considered as a option to the list. # Let's take [app] / source.exclude_patterns. # Instead of doing: # #[app] #source.exclude_patterns = license,data/audio/*.wav,data/images/original/* # # This can be translated into: # #[app:source.exclude_patterns] #license #data/audio/*.wav #data/images/original/* # # ----------------------------------------------------------------------------- # Profiles # # You can extend section / key with a profile # For example, you want to deploy a demo version of your application without # HD content. You could first change the title to add "(demo)" in the name # and extend the excluded directories to remove the HD content. # #[app@demo] #title = My Application (demo) # #[app:source.exclude_patterns@demo] #images/hd/* # # Then, invoke the command line with the "demo" profile: # #buildozer --profile demo android debug ================================================ FILE: testapps/on_device_unit_tests/setup.py ================================================ """ 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, urllib3,chardet,idna` - 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 setuptools import setup, 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,urllib3,' 'chardet,idna', 'android-api': 36, 'ndk-api': 24, 'dist-name': 'bdist_unit_tests_app', 'arch': 'armeabi-v7a', 'bootstrap' : 'sdl2', 'permissions': ['INTERNET', 'VIBRATE'], 'orientation': ['portrait', 'landscape'], 'service': 'P4a_test_service:app_service.py', }, 'aab': { 'requirements': 'sqlite3,libffi,openssl,pyjnius,kivy,python3,requests,urllib3,' 'chardet,idna', 'android-api': 36, 'ndk-api': 24, 'dist-name': 'bdist_unit_tests_app', 'arch': 'armeabi-v7a', 'bootstrap' : 'sdl2', 'permissions': ['INTERNET', 'VIBRATE'], 'orientation': ['portrait', 'landscape'], 'service': 'P4a_test_service:app_service.py', }, 'aar': { 'requirements' : 'python3', 'android-api': 36, 'ndk-api': 24, 'dist-name': 'bdist_unit_tests_app', 'arch': 'arm64-v8a', 'bootstrap' : 'service_library', 'permissions': ['INTERNET', 'VIBRATE'], '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'], } ) ================================================ FILE: testapps/on_device_unit_tests/test_app/app_flask.py ================================================ 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, setup_lifecycle_callbacks, ) app = Flask(__name__) setup_lifecycle_callbacks() service_running = False 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) def get_test_service(): from jnius import autoclass return autoclass('org.test.unit_tests_app.ServiceP4a_test_service') def start_service(): global service_running activity = get_android_python_activity() test_service = get_test_service() test_service.start(activity, 'Some argument') service_running = True def stop_service(): global service_running activity = get_android_python_activity() test_service = get_test_service() test_service.stop(activity) service_running = False @app.route('/') def index(): return render_template( 'index.html', platform='Android' if RUNNING_ON_ANDROID else 'Desktop', service_running=service_running, ) @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']) return ('', 204) @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']) vibrate_with_pyjnius(int(float(args['time']) * 1000)) return ('', 204) @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'] set_device_orientation(direction) return ('', 204) @app.route('/service') def service(): if not RUNNING_ON_ANDROID: print(NON_ANDROID_DEVICE_MSG, '...cancelled service.') return (NON_ANDROID_DEVICE_MSG, 400) args = request.args if 'action' not in args: print('ERROR: asked to manage service but no action specified') return ('No action specified', 400) action = args['action'] if action == 'start': start_service() else: stop_service() return ('', 204) ================================================ FILE: testapps/on_device_unit_tests/test_app/app_kivy.py ================================================ # -*- 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, setup_lifecycle_callbacks, 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 on_start(self): setup_lifecycle_callbacks() 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) ================================================ FILE: testapps/on_device_unit_tests/test_app/app_service.py ================================================ #!/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() ================================================ FILE: testapps/on_device_unit_tests/test_app/constants.py ================================================ # -*- 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 ================================================ FILE: testapps/on_device_unit_tests/test_app/main.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- """ 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. For now, we contemplate three possibilities: - A kivy unittest app (sdl2 bootstrap) - A unittest app (webview bootstrap) - A non-gui unittests app - A non-gui Qt app (qt bootstrap) 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. .. 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. .. 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. .. 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. """ import sys import unittest from os import curdir from os.path import isfile, realpath print('Imported unittest') sys.path.append('./') # 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)) flask_debug = not realpath(curdir).startswith('/data') # Flask is run non-threaded since it tries to resolve app classes # through pyjnius from request handlers. That doesn't work since the # JNI ends up using the Java system class loader in new native # threads. # # https://github.com/kivy/python-for-android/issues/2533 app_flask.app.run(threaded=False, debug=flask_debug) 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.values())) unittest.TextTestRunner().run(suite) ================================================ FILE: testapps/on_device_unit_tests/test_app/screen_keyboard.kv ================================================ #: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' ================================================ FILE: testapps/on_device_unit_tests/test_app/screen_orientation.kv ================================================ #: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' ================================================ FILE: testapps/on_device_unit_tests/test_app/screen_service.kv ================================================ #: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) ================================================ FILE: testapps/on_device_unit_tests/test_app/screen_unittests.kv ================================================ #: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) ================================================ FILE: testapps/on_device_unit_tests/test_app/static/flask.css ================================================ @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; } } ================================================ FILE: testapps/on_device_unit_tests/test_app/templates/base.html ================================================ {% block title %} Flask on {{ platform }} {% endblock %}

Flask on {{ platform }}!

{% block body %} {% endblock %}
================================================ FILE: testapps/on_device_unit_tests/test_app/templates/index.html ================================================ {% extends "base.html" %} {% block body %}

Main Page

Perform unittests

Test navigation

Android tests

{{ 'Service started' if service_running else 'Service stopped' }}


{% endblock %} ================================================ FILE: testapps/on_device_unit_tests/test_app/templates/page2.html ================================================ {% extends "base.html" %} {% block body %}

Page two


Yeah, it seems to work, I would suggest to go to:

...far more interesting ;)
{% endblock %} ================================================ FILE: testapps/on_device_unit_tests/test_app/templates/unittests.html ================================================ {% extends "base.html" %} {% block body %}

Unittests Page


Unittest recipes: {{ tested_recipes }}
{% for line in unittests_output %} {{ line }}
{% endfor %}
{% endblock %} ================================================ FILE: testapps/on_device_unit_tests/test_app/tests/__init__.py ================================================ ================================================ FILE: testapps/on_device_unit_tests/test_app/tests/mixin.py ================================================ 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') ================================================ FILE: testapps/on_device_unit_tests/test_app/tests/test_requirements.py ================================================ from unittest import TestCase from .mixin import PythonTestMixIn class NumpyTestCase(PythonTestMixIn, TestCase): module_import = 'numpy' def test_run_module(self): import numpy as np arr = np.random.random((3, 3)) det = np.linalg.det(arr) class ScipyTestCase(PythonTestMixIn, TestCase): module_import = 'scipy' def test_run_module(self): import numpy as np from scipy.cluster.vq import vq, kmeans, whiten features = np.array([[ 1.9,2.3], [ 1.5,2.5], [ 0.8,0.6], [ 0.4,1.8], [ 0.1,0.1], [ 0.2,1.8], [ 2.0,0.5], [ 0.3,1.5], [ 1.0,1.0]]) whitened = whiten(features) book = np.array((whitened[0],whitened[2])) print('kmeans', kmeans(whitened,book)) class OpensslTestCase(PythonTestMixIn, TestCase): module_import = '_ssl' def test_run_module(self): import ssl ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ctx.options &= ~ssl.OP_NO_SSLv3 class Sqlite3TestCase(PythonTestMixIn, TestCase): module_import = 'sqlite3' def test_run_module(self): import sqlite3 conn = sqlite3.connect('example.db') conn.cursor() class KivyTestCase(PythonTestMixIn, TestCase): module_import = 'kivy' def test_run_module(self): # This import has side effects, if it works then it's an # indication that Kivy is okay from kivy.core.window import Window class PyjniusTestCase(PythonTestMixIn, TestCase): module_import = 'jnius' 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)) class Pyside6TestCase(PythonTestMixIn, TestCase): module_import = 'PySide6' def test_run_module(self): import PySide6 from PySide6.QtCore import QDateTime from PySide6 import QtWidgets print(f"Imported PySide6 version {PySide6.__version__}") print(f"Current date and time obtained from PySide6 : {QDateTime.currentDateTime().toString()}") class Shiboken6TestCase(PythonTestMixIn, TestCase): module_import = 'shiboken6' def test_run_module(self): import shiboken6 from shiboken6 import Shiboken print('Imported shiboken6 version {}'.format(shiboken6.__version__)) ================================================ FILE: testapps/on_device_unit_tests/test_app/tools.py ================================================ # -*- 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) @skip_if_not_running_from_android_device def setup_lifecycle_callbacks(): """ Register example ActivityLifecycleCallbacks """ from android.activity import register_activity_lifecycle_callbacks register_activity_lifecycle_callbacks( onActivityStarted=lambda activity: print('onActivityStarted'), onActivityPaused=lambda activity: print('onActivityPaused'), onActivityResumed=lambda activity: print('onActivityResumed'), onActivityStopped=lambda activity: print('onActivityStopped'), onActivityDestroyed=lambda activity: print('onActivityDestroyed'), ) ================================================ FILE: testapps/on_device_unit_tests/test_app/widgets.kv ================================================ #: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 ================================================ FILE: testapps/on_device_unit_tests/test_app/widgets.py ================================================ # -*- 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('') ================================================ FILE: testapps/on_device_unit_tests/test_qt/recipes/PySide6/__init__.py ================================================ # Copyright (C) 2023 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only import shutil import zipfile from os.path import join from pathlib import Path from pythonforandroid.logger import info from pythonforandroid.recipe import PythonRecipe class PySideRecipe(PythonRecipe): version = '6.6.0a1' # This will download the aarch64 wheel from the Qt servers. # This wheel is only for testing purposes. This test will be update when PySide releases # official PySide6 Android wheels. url = ("https://download.qt.io/snapshots/ci/pyside/test/Android/aarch64/" "PySide6-6.6.0a1-6.6.0-cp37-abi3-android_aarch64.whl") wheel_name = 'PySide6-6.6.0a1-6.6.0-cp37-abi3-android_aarch64.whl' depends = ["shiboken6"] call_hostpython_via_targetpython = False install_in_hostpython = False def build_arch(self, arch): """Unzip the wheel and copy into site-packages of target""" self.wheel_path = join(self.ctx.packages_path, self.name, self.wheel_name) info("Copying libc++_shared.so from SDK to be loaded on startup") libcpp_path = f"{self.ctx.ndk.sysroot_lib_dir}/{arch.command_prefix}/libc++_shared.so" shutil.copyfile(libcpp_path, Path(self.ctx.get_libs_dir(arch.arch)) / "libc++_shared.so") info(f"Installing {self.name} into site-packages") with zipfile.ZipFile(self.wheel_path, "r") as zip_ref: info("Unzip wheels and copy into {}".format(self.ctx.get_python_install_dir(arch.arch))) zip_ref.extractall(self.ctx.get_python_install_dir(arch.arch)) lib_dir = Path(f"{self.ctx.get_python_install_dir(arch.arch)}/PySide6/Qt/lib") info("Copying Qt libraries to be loaded on startup") shutil.copytree(lib_dir, self.ctx.get_libs_dir(arch.arch), dirs_exist_ok=True) shutil.copyfile(lib_dir.parent.parent / "libpyside6.abi3.so", Path(self.ctx.get_libs_dir(arch.arch)) / "libpyside6.abi3.so") shutil.copyfile(lib_dir.parent.parent / "QtCore.abi3.so", Path(self.ctx.get_libs_dir(arch.arch)) / "QtCore.abi3.so") shutil.copyfile(lib_dir.parent.parent / "QtWidgets.abi3.so", Path(self.ctx.get_libs_dir(arch.arch)) / "QtWidgets.abi3.so") shutil.copyfile(lib_dir.parent.parent / "QtGui.abi3.so", Path(self.ctx.get_libs_dir(arch.arch)) / "QtGui.abi3.so") plugin_path = (lib_dir.parent / "plugins" / "platforms" / f"libplugins_platforms_qtforandroid_{arch.arch}.so") if plugin_path.exists(): shutil.copyfile(plugin_path, (Path(self.ctx.get_libs_dir(arch.arch)) / f"libplugins_platforms_qtforandroid_{arch.arch}.so")) recipe = PySideRecipe() ================================================ FILE: testapps/on_device_unit_tests/test_qt/recipes/shiboken6/__init__.py ================================================ # Copyright (C) 2023 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only import shutil import zipfile from os.path import join from pathlib import Path from pythonforandroid.logger import info from pythonforandroid.recipe import PythonRecipe class ShibokenRecipe(PythonRecipe): version = '6.6.0a1' # This will download the aarch64 wheel from the Qt servers. # This wheel is only for testing purposes. This test will be update when PySide releases # official shiboken6 Android wheels. url = ("https://download.qt.io/snapshots/ci/pyside/test/Android/aarch64/" "shiboken6-6.6.0a1-6.6.0-cp37-abi3-android_aarch64.whl") wheel_name = 'shiboken6-6.6.0a1-6.6.0-cp37-abi3-android_aarch64.whl' call_hostpython_via_targetpython = False install_in_hostpython = False def build_arch(self, arch): ''' Unzip the wheel and copy into site-packages of target''' self.wheel_path = join(self.ctx.packages_path, self.name, self.wheel_name) info('Installing {} into site-packages'.format(self.name)) with zipfile.ZipFile(self.wheel_path, 'r') as zip_ref: info('Unzip wheels and copy into {}'.format(self.ctx.get_python_install_dir(arch.arch))) zip_ref.extractall(self.ctx.get_python_install_dir(arch.arch)) lib_dir = Path(f"{self.ctx.get_python_install_dir(arch.arch)}/shiboken6") shutil.copyfile(lib_dir / "libshiboken6.abi3.so", Path(self.ctx.get_libs_dir(arch.arch)) / "libshiboken6.abi3.so") recipe = ShibokenRecipe() ================================================ FILE: testapps/setup_testapp_python3_sqlite_openssl.py ================================================ from setuptools import setup, find_packages options = {'apk': {'requirements': 'requests,peewee,sdl2,pyjnius,kivy,python3', 'android-api': 36, 'ndk-api': 21, 'bootstrap': 'sdl2', 'dist-name': 'bdisttest_python3_sqlite_openssl_googlendk', 'ndk-version': '10.3.2', 'arch': 'armeabi-v7a', 'permissions': ['INTERNET', 'VIBRATE'], }} setup( name='testapp_python3_sqlite_openssl_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_sqlite_openssl': ['*.py', '*.png']} ) ================================================ FILE: testapps/setup_vispy.py ================================================ from setuptools import setup, find_packages options = {'apk': {'debug': None, 'requirements': 'python3,vispy', 'blacklist-requirements': 'openssl,sqlite3', 'android-api': 33, 'ndk-api': 21, 'bootstrap': 'empty', '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_vispy', version='1.1', description='p4a setup.py test', author='Alexander Taylor', author_email='alexanderjohntaylor@gmail.com', packages=find_packages(), options=options, package_data={'testapp_vispy': ['*.py', '*.png']} ) ================================================ FILE: testapps/testapp_sqlite_openssl/main.py ================================================ 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) import peewee import requests import sqlite3 try: inclemnet = requests.get('http://inclem.net/') print('got inclem.net request') except: inclemnet = 'failed inclemnet' try: kivy = requests.get('https://kivy.org/') print('got kivy request (https)') except: kivy = 'failed kivy' from peewee import * db = SqliteDatabase('test.db') class Person(Model): name = CharField() birthday = DateField() is_relative = BooleanField() class Meta: database = db def __repr__(self): return ''.format(self.name, self.birthday) def __str__(self): return repr(self) db.connect() try: db.create_tables([Person]) except: import traceback traceback.print_exc() import random from datetime import date test_person = Person(name='person{}'.format(random.randint(0, 1000)), birthday=date(random.randint(1900, 2000), random.randint(1, 9), random.randint(1, 20)), is_relative=False) test_person.save() 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() Label: height: self.texture_size[1] size_hint_y: None text_size: self.size[0], None markup: True text: 'kivy request: {}\\ninclemnet request: {}'.format(app.kivy_request, app.inclemnet_request) halign: 'center' Label: height: self.texture_size[1] size_hint_y: None text_size: self.size[0], None markup: True text: 'people: {}'.format(app.people) halign: 'center' 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): kivy_request = kivy inclemnet_request = inclemnet people = ', '.join(map(str, list(Person.select()))) 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): pass def test_numpy(self, *args): import numpy print(numpy.zeros(5)) print(numpy.arange(5)) print(numpy.random.random((3, 3))) TestApp().run() ================================================ FILE: testapps/testapp_vispy/main.py ================================================ """ Demonstration of Tube """ print('testing!') import ctypes print('imported ctypes') print(ctypes.__dict__) print('dict done') import ctypes.util print('imported util') print(ctypes.util.find_library) import vispy # vispy.set_log_level('debug') import sys from vispy import scene from vispy.geometry.torusknot import TorusKnot from colorsys import hsv_to_rgb import numpy as np canvas = scene.SceneCanvas(keys='interactive', bgcolor='white') canvas.unfreeze() canvas.view = canvas.central_widget.add_view() points1 = TorusKnot(5, 3).first_component[:-1] points1[:, 0] -= 20. points1[:, 2] -= 15. points2 = points1.copy() points2[:, 2] += 30. points3 = points1.copy() points3[:, 0] += 41. points3[:, 2] += 30 points4 = points1.copy() points4[:, 0] += 41. colors = np.linspace(0, 1, len(points1)) colors = np.array([hsv_to_rgb(c, 1, 1) for c in colors]) vertex_colors = np.random.random(8 * len(points1)) vertex_colors = np.array([hsv_to_rgb(c, 1, 1) for c in vertex_colors]) l1 = scene.visuals.Tube(points1, shading='flat', color=colors, # this is overridden by # the vertex_colors argument vertex_colors=vertex_colors, tube_points=8) l2 = scene.visuals.Tube(points2, color=['red', 'green', 'blue'], shading='smooth', tube_points=8) l3 = scene.visuals.Tube(points3, color=colors, shading='flat', tube_points=8, closed=True) l4 = scene.visuals.Tube(points4, color=colors, shading='flat', tube_points=8, mode='lines') canvas.view.add(l1) canvas.view.add(l2) canvas.view.add(l3) canvas.view.add(l4) canvas.view.camera = scene.TurntableCamera() # tube does not expose its limits yet canvas.view.camera.set_range((-20, 20), (-20, 20), (-20, 20)) canvas.show() if __name__ == '__main__': if sys.flags.interactive != 1: canvas.app.run() ================================================ FILE: testapps/testlauncher_setup/sdl2.py ================================================ from setuptools import setup options = {'apk': {'debug': None, 'bootstrap': 'sdl2', 'launcher': None, 'requirements': ( 'python3,sdl2,android,' 'sqlite3,docutils,pygments,kivy,pyjnius,plyer,' 'cymunk,lxml,pil,openssl,pyopenssl,' 'twisted'), # audiostream, ffmpeg, numpy 'android-api': 14, 'dist-name': 'launchertest_sdl2', 'name': 'TestLauncher-sdl2', 'package': 'org.kivy.testlauncher_sdl2', 'permissions': [ 'ACCESS_COARSE_LOCATION', 'ACCESS_FINE_LOCATION', 'BLUETOOTH', 'BODY_SENSORS', 'CAMERA', 'INTERNET', 'NFC', 'READ_EXTERNAL_STORAGE', 'RECORD_AUDIO', 'USE_FINGERPRINT', 'VIBRATE', 'WAKE_LOCK', 'WRITE_EXTERNAL_STORAGE'] }} setup( name='testlauncher_sdl2', version='1.0', description='p4a sdl2.py apk', author='Peter Badida', options=options ) ================================================ FILE: testapps/testlauncherreboot_setup/sdl2.py ================================================ ''' Clone Python implementation of Kivy Launcher from kivy/kivy-launcher repo, install deps specified in the OPTIONS['apk']['requirements'] and put it to a dist named OPTIONS['apk']['dist-name']. Tested with P4A Dockerfile at 5fc5241e01fbbc2b23b3749f53ab48f22239f4fc, kivy-launcher at ad5c5c6e886a310bf6dd187e992df972864d1148 on Windows 8.1 with Docker for Windows and running on Samsung Galaxy Note 9, Android 8.1. docker run \ --interactive \ --tty \ -v "/c/Users/.../python-for-android/testapps":/home/user/testapps \ -v ".../python-for-android/pythonforandroid":/home/user/pythonforandroid \ p4a sh -c '\ . venv/bin/activate \ && cd testapps/testlauncherreboot_setup \ && python sdl2.py apk \ --sdk-dir $ANDROID_SDK_HOME \ --ndk-dir $ANDROID_NDK_HOME' ''' # pylint: disable=import-error,no-name-in-module from subprocess import Popen from os import listdir from os.path import join, dirname, abspath, exists from pprint import pprint from setuptools import setup, find_packages ROOT = dirname(abspath(__file__)) LAUNCHER = join(ROOT, 'launcherapp') if not exists(LAUNCHER): PROC = Popen([ 'git', 'clone', 'https://github.com/kivy/kivy-launcher', LAUNCHER ]) PROC.communicate() assert PROC.returncode == 0, PROC.returncode pprint(listdir(LAUNCHER)) pprint(listdir(ROOT)) OPTIONS = { 'apk': { 'debug': None, 'bootstrap': 'sdl2', 'requirements': ( 'python3,sdl2,kivy,android,pyjnius,plyer' ), # 'sqlite3,docutils,pygments,' # 'cymunk,lxml,pil,openssl,pyopenssl,' # 'twisted,audiostream,ffmpeg,numpy' 'android-api': 36, 'ndk-api': 21, 'dist-name': 'bdisttest_python3launcher_sdl2_googlendk', 'name': 'TestLauncherPy3-sdl2', 'package': 'org.kivy.testlauncherpy3_sdl2_googlendk', 'ndk-version': '10.3.2', 'arch': 'armeabi-v7a', 'permissions': [ 'ACCESS_COARSE_LOCATION', 'ACCESS_FINE_LOCATION', 'BLUETOOTH', 'BODY_SENSORS', 'CAMERA', 'INTERNET', 'NFC', 'READ_EXTERNAL_STORAGE', 'RECORD_AUDIO', 'USE_FINGERPRINT', 'VIBRATE', 'WAKE_LOCK', 'WRITE_EXTERNAL_STORAGE' ] } } PACKAGE_DATA = { 'launcherapp': [ '*.py', '*.png', '*.ttf', '*.eot', '*.svg', '*.woff', ], 'launcherapp/art': [ '*.py', '*.png', '*.ttf', '*.eot', '*.svg', '*.woff', ], 'launcherapp/art/fontello': [ '*.py', '*.png', '*.ttf', '*.eot', '*.svg', '*.woff', ], 'launcherapp/data': [ '*.py', '*.png', '*.ttf', '*.eot', '*.svg', '*.woff', ], 'launcherapp/launcher': [ '*.py', '*.png', '*.ttf', '*.eot', '*.svg', '*.woff', ] } PACKAGES = find_packages() print('packages are', PACKAGES) setup( name='testlauncherpy3_sdl2_googlendk', version='1.0', description='p4a sdl2.py apk', author='Peter Badida', author_email='keyweeusr@gmail.com', packages=find_packages(), options=OPTIONS, package_data=PACKAGE_DATA ) ================================================ FILE: tests/recipes/recipe_ctx.py ================================================ import os from pythonforandroid.bootstrap import Bootstrap from pythonforandroid.distribution import Distribution from pythonforandroid.recipe import Recipe from pythonforandroid.build import Context from pythonforandroid.archs import ArchAarch_64 from pythonforandroid.androidndk import AndroidNDK class RecipeCtx: """ An base class for unit testing a recipe. This will create a context so we can test any recipe using this context. Implement `setUp` and `tearDown` methods used by unit testing. """ ctx = None arch = None recipe = None recipe_name = "" "The name of the recipe to test." recipes = [] """A List of recipes to pass to `Distribution.get_distribution`. Should contain the target recipe to test as well as a python recipe.""" recipe_build_order = [] """A recipe_build_order which should take into account the recipe we want to test as well as the possible dependent recipes""" TEST_ARCH = 'arm64-v8a' def setUp(self): self.ctx = Context() self.ctx.ndk_api = 21 self.ctx.android_api = 27 self.ctx._sdk_dir = "/opt/android/android-sdk" self.ctx._ndk_dir = "/opt/android/android-ndk" self.ctx.ndk = AndroidNDK(self.ctx._ndk_dir) self.ctx.setup_dirs(os.getcwd()) self.ctx.bootstrap = Bootstrap().get_bootstrap("sdl2", self.ctx) self.ctx.bootstrap.distribution = Distribution.get_distribution( self.ctx, name="sdl2", recipes=self.recipes, archs=[self.TEST_ARCH], ) self.ctx.recipe_build_order = self.recipe_build_order self.ctx.python_recipe = Recipe.get_recipe("python3", self.ctx) self.arch = ArchAarch_64(self.ctx) self.ctx.ndk_sysroot = f'{self.ctx._ndk_dir}/sysroot' self.ctx.ndk_include_dir = f'{self.ctx.ndk_sysroot}/usr/include' self.recipe = Recipe.get_recipe(self.recipe_name, self.ctx) def tearDown(self): self.ctx = None ================================================ FILE: tests/recipes/recipe_lib_test.py ================================================ from unittest import mock from platform import system from tests.recipes.recipe_ctx import RecipeCtx class BaseTestForMakeRecipe(RecipeCtx): """ An unittest for testing any recipe using the standard build commands (`configure/make`). .. note:: Note that Some cmake recipe may need some more specific testing ...but this should cover the basics. """ recipe_name = None expected_compiler = ( "{android_ndk}/toolchains/llvm/prebuilt/{system}-x86_64/bin/clang" ) sh_command_calls = ["./configure"] """The expected commands that the recipe runs via `sh.command`.""" extra_env_flags = {} """ This must be a dictionary containing pairs of key (env var) and value. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.recipes = ["python3", "kivy", self.recipe_name] self.recipe_build_order = [ "hostpython3", self.recipe_name, "python3", "sdl2", "kivy" ] print(f"We are testing recipe: {self.recipe_name}") @mock.patch("pythonforandroid.recipe.Recipe.check_recipe_choices") @mock.patch("pythonforandroid.build.ensure_dir") @mock.patch("shutil.which") def test_get_recipe_env( self, mock_shutil_which, mock_ensure_dir, mock_check_recipe_choices, ): """ Test that get_recipe_env contains some expected arch flags and that some internal methods has been called. """ mock_shutil_which.return_value = self.expected_compiler.format( android_ndk=self.ctx._ndk_dir, system=system().lower() ) mock_check_recipe_choices.return_value = sorted( self.ctx.recipe_build_order ) # make sure the arch flags are in env env = self.recipe.get_recipe_env(self.arch) for flag in self.arch.arch_cflags: self.assertIn(flag, env["CFLAGS"]) self.assertIn( f"-target {self.arch.target}", env["CFLAGS"], ) for flag, value in self.extra_env_flags.items(): self.assertIn(value, env[flag]) # make sure that the mocked methods are actually called mock_ensure_dir.assert_called() mock_shutil_which.assert_called() mock_check_recipe_choices.assert_called() @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.build.ensure_dir") @mock.patch("shutil.which") def test_build_arch( self, mock_shutil_which, mock_ensure_dir, mock_current_directory, ): mock_shutil_which.return_value = self.expected_compiler.format( android_ndk=self.ctx._ndk_dir, system=system().lower() ) # Since the following mocks are dynamic, # we mock it inside a Context Manager with mock.patch( f"pythonforandroid.recipes.{self.recipe_name}.sh.Command" ) as mock_sh_command, mock.patch( f"pythonforandroid.recipes.{self.recipe_name}.sh.make", create=True ) as mock_make: self.recipe.build_arch(self.arch) # make sure that the mocked methods are actually called for command in self.sh_command_calls: self.assertIn( mock.call(command), mock_sh_command.mock_calls, ) mock_make.assert_called() mock_ensure_dir.assert_called() mock_current_directory.assert_called() mock_shutil_which.assert_called() class BaseTestForCmakeRecipe(BaseTestForMakeRecipe): """ An unittest for testing any recipe using `cmake`. It inherits from `BaseTestForMakeRecipe` but we override the build method to match the cmake build method. .. note:: Note that Some cmake recipe may need some more specific testing ...but this should cover the basics. """ @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.build.ensure_dir") @mock.patch("shutil.which") def test_build_arch( self, mock_shutil_which, mock_ensure_dir, mock_current_directory, ): mock_shutil_which.return_value = self.expected_compiler.format( android_ndk=self.ctx._ndk_dir, system=system().lower() ) # Since the following mocks are dynamic, # we mock it inside a Context Manager with mock.patch( f"pythonforandroid.recipes.{self.recipe_name}.sh.make", create=True ) as mock_make, mock.patch( f"pythonforandroid.recipes.{self.recipe_name}.sh.cmake", create=True ) as mock_cmake: self.recipe.build_arch(self.arch) # make sure that the mocked methods are actually called mock_cmake.assert_called() mock_make.assert_called() mock_ensure_dir.assert_called() mock_current_directory.assert_called() mock_shutil_which.assert_called() ================================================ FILE: tests/recipes/test_freetype.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestFreetypeRecipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.freetype` """ recipe_name = "freetype" sh_command_calls = ["./configure"] ================================================ FILE: tests/recipes/test_gevent.py ================================================ import unittest from unittest.mock import patch from tests.recipes.recipe_ctx import RecipeCtx class TestGeventRecipe(RecipeCtx, unittest.TestCase): recipe_name = "gevent" def test_get_recipe_env(self): """ Makes sure `get_recipe_env()` sets compilation flags properly. """ mocked_cflags = ( '-DANDROID -fomit-frame-pointer -D__ANDROID_API__=27 -mandroid ' '-isystem /path/to/isystem ' '-I/path/to/include1 ' '-isysroot /path/to/sysroot ' '-I/path/to/include2 ' '-march=armv7-a -mfloat-abi=softfp -mfpu=vfp -mthumb ' '-I/path/to/python3-libffi-openssl/include' ) mocked_ldflags = ( ' --sysroot /path/to/sysroot ' '-lm ' '-L/path/to/library1 ' '-L/path/to/library2 ' '-lpython3.7m ' # checks the regex doesn't parse `python3-libffi-openssl` as a `-libffi` '-L/path/to/python3-libffi-openssl/library3 ' ) mocked_ldlibs = ' -lm' mocked_env = { 'CFLAGS': mocked_cflags, 'LDFLAGS': mocked_ldflags, 'LDLIBS': mocked_ldlibs, } with patch('pythonforandroid.recipe.PyProjectRecipe.get_recipe_env') as m_get_recipe_env: m_get_recipe_env.return_value = mocked_env env = self.recipe.get_recipe_env(self.arch) expected_cflags = ( ' -fomit-frame-pointer -mandroid -isystem /path/to/isystem' ' -isysroot /path/to/sysroot' ' -march=armv7-a -mfloat-abi=softfp -mfpu=vfp -mthumb' ) expected_cppflags = ( '-DANDROID -D__ANDROID_API__=27 ' '-I/path/to/include1 ' '-I/path/to/include2 ' '-I/path/to/python3-libffi-openssl/include' ) expected_ldflags = ( ' --sysroot /path/to/sysroot' ' -L/path/to/library1' ' -L/path/to/library2' ' -L/path/to/python3-libffi-openssl/library3 ' ) expected_ldlibs = mocked_ldlibs expected_libs = '-lm -lpython3.7m -lm' expected_command_prefix = 'aarch64-linux-android' expected_env = { 'CFLAGS': expected_cflags, 'CPPFLAGS': expected_cppflags, 'LDFLAGS': expected_ldflags, 'LDLIBS': expected_ldlibs, 'LIBS': expected_libs, 'COMMAND_PREFIX': expected_command_prefix, } self.assertEqual(expected_env, env) ================================================ FILE: tests/recipes/test_harfbuzz.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestHarfbuzzRecipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.harfbuzz` """ recipe_name = "harfbuzz" sh_command_calls = ["./configure"] ================================================ FILE: tests/recipes/test_hostpython3.py ================================================ import unittest from os.path import join from unittest import mock from pythonforandroid.recipes.hostpython3 import ( HOSTPYTHON_VERSION_UNSET_MESSAGE, SETUP_DIST_NOT_FIND_MESSAGE, ) from pythonforandroid.util import BuildInterruptingException from tests.recipes.recipe_lib_test import RecipeCtx class TestHostPython3Recipe(RecipeCtx, unittest.TestCase): """ TestCase for recipe :mod:`~pythonforandroid.recipes.hostpython3` """ recipe_name = "hostpython3" def test_property__exe_name_no_version(self): hostpython_version = self.recipe.version self.recipe._version = None with self.assertRaises(BuildInterruptingException) as e: py_exe = self.recipe._exe_name # noqa: F841 self.assertEqual(e.exception.args[0], HOSTPYTHON_VERSION_UNSET_MESSAGE) # restore recipe's version or we will get failures with other test, # since we share `self.recipe with all the tests of the class self.recipe._version = hostpython_version def test_property__exe_name(self): self.assertEqual(self.recipe._exe_name, 'python3') def test_property_python_exe(self): self.assertEqual( self.recipe.python_exe, join(self.recipe.get_path_to_python(), 'python3') ) @mock.patch("pythonforandroid.recipes.hostpython3.Path.exists") def test_should_build(self, mock_exists): # test case for existing python exe which shouldn't trigger the build mock_exists.return_value = True self.assertFalse(self.recipe.should_build(self.arch)) # test case for existing python exe which should trigger the build mock_exists.return_value = False self.assertTrue(self.recipe.should_build(self.arch)) @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.util.makedirs") def test_build_arch(self, mock_makedirs, mock_chdir): """ Test case for :meth:`~pythonforandroid.recipes.python3.HostPython3Recipe.build_arch`, where we simulate the build for Python 3.8+. """ with mock.patch( "pythonforandroid.recipes.hostpython3.Path.exists" ) as mock_path_exists, mock.patch( "pythonforandroid.recipes.hostpython3.sh.Command" ) as mock_sh_command, mock.patch( "pythonforandroid.recipes.hostpython3.sh.make" ) as mock_make, mock.patch( "pythonforandroid.recipes.hostpython3.Path.is_file" ) as mock_path_isfile, mock.patch( "pythonforandroid.recipes.hostpython3.sh.cp" ) as mock_sh_cp: # here we simulate the expected behaviour for Python 3.8+ mock_path_exists.side_effect = [ False, # "config.status" not exists, so we trigger.configure False, # "Modules/Setup.dist" shouldn't exist (3.8+ case) True, # "Modules/Setup" exists, so we skip raise exception ] self.recipe.build_arch(self.arch) # make sure that the mocked methods are actually called mock_path_exists.assert_called() recipe_src = self.recipe.get_build_dir(self.arch.arch) self.assertIn( mock.call(f"{recipe_src}/configure"), mock_sh_command.mock_calls, ) mock_make.assert_called() exe = join(self.recipe.get_path_to_python(), 'python.exe') mock_path_isfile.assert_called() self.assertEqual(mock_sh_cp.call_count, 1) mock_call_args, mock_call_kwargs = mock_sh_cp.call_args_list[0] self.assertEqual(mock_call_args[0], exe) self.assertEqual(mock_call_args[1], self.recipe.python_exe) mock_makedirs.assert_called() mock_chdir.assert_called() @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.util.makedirs") def test_build_arch_python_lower_than_3_8(self, mock_makedirs, mock_chdir): """ Test case for :meth:`~pythonforandroid.recipes.python3.HostPython3Recipe.build_arch`, where we simulate a Python 3.7 build. Here we copy an extra file: - Modules/Setup.dist -> Modules/Setup. .. note:: We omit some checks because we already dit that at `test_build_arch`. Also we skip configure command for the same reason. """ with mock.patch( "pythonforandroid.recipes.hostpython3.Path.exists" ) as mock_path_exists, mock.patch( "pythonforandroid.recipes.hostpython3.sh.make" ) as mock_make, mock.patch( "pythonforandroid.recipes.hostpython3.Path.is_file" ) as mock_path_isfile, mock.patch( "pythonforandroid.recipes.hostpython3.sh.cp" ) as mock_sh_cp: mock_path_exists.side_effect = [ True, # simulate that "config.status" exists to skip configure True, # "Modules/Setup.dist" exist for Python 3.7 True, # "Modules/Setup" exists...we skip raise exception ] self.recipe.build_arch(self.arch) build_dir = join( self.recipe.get_build_dir(self.arch.arch), self.recipe.build_subdir ) # we expect two calls to copy command, The one described below and the # copy of the python binary which already chechet at `test_build_arch`. self.assertEqual(mock_sh_cp.call_count, 2) mock_call_args, mock_call_kwargs = mock_sh_cp.call_args_list[0] self.assertEqual(mock_call_args[0], "Modules/Setup.dist") self.assertEqual(mock_call_args[1], join(build_dir, "Modules/Setup")) mock_path_exists.assert_called() mock_make.assert_called() mock_path_isfile.assert_called() mock_makedirs.assert_called() mock_chdir.assert_called() @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.util.makedirs") def test_build_arch_setup_dist_exception(self, mock_makedirs, mock_chdir): """ Test case for :meth:`~pythonforandroid.recipes.python3.HostPython3Recipe.build_arch`, where we simulate that the sources hasn't Setup.dist file, which should raise an exception. .. note:: We skip configure command because already tested at `test_build_arch`. """ with mock.patch( "pythonforandroid.recipes.hostpython3.Path.exists" ) as mock_path_exists: mock_path_exists.side_effect = [ True, # simulate that "config.status" exists to skip configure False, # "Modules/Setup.dist" shouldn't exist (3.8+ case) False, # "Modules/Setup" doesn't exists...raise exception ] with self.assertRaises(BuildInterruptingException) as e: self.recipe.build_arch(self.arch) self.assertEqual(e.exception.args[0], SETUP_DIST_NOT_FIND_MESSAGE) mock_makedirs.assert_called() mock_chdir.assert_called() ================================================ FILE: tests/recipes/test_icu.py ================================================ import os import unittest from unittest import mock from tests.recipes.recipe_ctx import RecipeCtx from pythonforandroid.recipes.icu import ICURecipe class TestIcuRecipe(RecipeCtx, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.icu` """ recipe_name = "icu" def test_url(self): self.assertTrue(self.recipe.versioned_url.startswith("http")) self.assertIn(self.recipe.version.replace('.', '-'), self.recipe.versioned_url) @mock.patch( "pythonforandroid.recipe.Recipe.url", new_callable=mock.PropertyMock ) def test_url_none(self, mock_url): mock_url.return_value = None self.assertIsNone(self.recipe.versioned_url) def test_get_recipe_dir(self): expected_dir = os.path.join(self.ctx.root_dir, "recipes", "icu") self.assertEqual(self.recipe.get_recipe_dir(), expected_dir) @mock.patch("pythonforandroid.util.makedirs") @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.bootstrap.sh.Command") @mock.patch("pythonforandroid.recipes.icu.sh.make") @mock.patch("pythonforandroid.build.ensure_dir") @mock.patch("shutil.which") def test_build_arch( self, mock_shutil_which, mock_ensure_dir, mock_sh_make, mock_sh_command, mock_chdir, mock_makedirs, ): mock_shutil_which.return_value = os.path.join( self.ctx._ndk_dir, f"toolchains/llvm/prebuilt/{self.ctx.ndk.host_tag}/bin/clang", ) self.ctx.toolchain_version = "4.9" self.recipe.build_arch(self.arch) # We expect some calls to `sh.Command` build_root = self.recipe.get_build_dir(self.arch.arch) mock_sh_command.assert_has_calls( [ mock.call( os.path.join(build_root, "source", "runConfigureICU") ), mock.call(os.path.join(build_root, "source", "configure")), ], any_order=True ) mock_ensure_dir.assert_called() mock_chdir.assert_called() # we expect multiple calls to sh.make command expected_host_cppflags = ( "-O3 -fno-short-wchar -DU_USING_ICU_NAMESPACE=1 -fno-short-enums " "-DU_HAVE_NL_LANGINFO_CODESET=0 -D__STDC_INT64__ -DU_TIMEZONE=0 " "-DUCONFIG_NO_LEGACY_CONVERSION=1 " "-DUCONFIG_NO_TRANSLITERATION=0 " ) for call_number, call in enumerate(mock_sh_make.call_args_list): # here we expect to find the compile command `make -j`in first and # third calls, the others should be the `make install` commands is_host_build = call_number in [0, 1] is_compile = call_number in [0, 2] call_args, call_kwargs = call self.assertTrue( call_args[0].startswith("-j" if is_compile else "install") ) self.assertIn("_env", call_kwargs) if is_host_build: self.assertIn( expected_host_cppflags, call_kwargs["_env"]["CPPFLAGS"] ) else: self.assertNotIn( expected_host_cppflags, call_kwargs["_env"]["CPPFLAGS"] ) mock_makedirs.assert_called() mock_shutil_which.assert_called_once() self.assertEqual( mock_shutil_which.call_args[0][0], mock_shutil_which.return_value, ) @mock.patch("pythonforandroid.recipes.icu.sh.cp") @mock.patch("pythonforandroid.util.makedirs") def test_install_libraries(self, mock_makedirs, mock_sh_cp): self.recipe.install_libraries(self.arch) mock_makedirs.assert_called() mock_sh_cp.assert_called() @mock.patch("pythonforandroid.recipes.icu.exists") def test_get_recipe_dir_with_local_recipes(self, mock_exists): self.ctx.local_recipes = "/home/user/p4a_local_recipes" # we don't use `self.recipe` because, somehow, the modified variable # above is not updated in the `ctx` and makes the test fail... recipe = ICURecipe() recipe.ctx = self.ctx recipe_dir = recipe.get_recipe_dir() expected_dir = os.path.join(self.ctx.local_recipes, "icu") self.assertEqual(recipe_dir, expected_dir) mock_exists.assert_called_once_with(expected_dir) ================================================ FILE: tests/recipes/test_jpeg.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForCmakeRecipe class TestJpegRecipe(BaseTestForCmakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.jpeg` """ recipe_name = "jpeg" ================================================ FILE: tests/recipes/test_leveldb.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForCmakeRecipe class TestLeveldbRecipe(BaseTestForCmakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.leveldb` """ recipe_name = "leveldb" ================================================ FILE: tests/recipes/test_libbz2.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLibBz2Recipe(BaseTestForMakeRecipe, unittest.TestCase): """TestCase for recipe :mod:`~pythonforandroid.recipes.libbz2`.""" recipe_name = "libbz2" sh_command_calls = [] def test_get_library_includes(self): """ Test :meth:`~pythonforandroid.recipes.libbz2.get_library_includes`. """ self.assertEqual( self.recipe.get_library_includes(self.arch), f" -I{self.recipe.get_build_dir(self.arch.arch)}", ) def test_get_library_ldflags(self): """ Test :meth:`~pythonforandroid.recipes.libbz2.get_library_ldflags`. """ self.assertEqual( self.recipe.get_library_ldflags(self.arch), f" -L{self.recipe.get_build_dir(self.arch.arch)}", ) def test_link_libs_flags(self): """ Test :meth:`~pythonforandroid.recipes.libbz2.get_library_ldflags`. """ self.assertEqual(self.recipe.get_library_libs_flag(), " -lbz2") ================================================ FILE: tests/recipes/test_libcurl.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLibcurlRecipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.libcurl` """ recipe_name = "libcurl" sh_command_calls = ["./configure"] ================================================ FILE: tests/recipes/test_libexpat.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLibexpatRecipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.libexpat` """ recipe_name = "libexpat" sh_command_calls = ["./buildconf.sh", "./configure"] ================================================ FILE: tests/recipes/test_libffi.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLibffiRecipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.libffi` """ recipe_name = "libffi" sh_command_calls = ["./autogen.sh", "autoreconf", "./configure"] def test_get_include_dirs(self): list_of_includes = self.recipe.get_include_dirs(self.arch) self.assertIsInstance(list_of_includes, list) self.assertTrue(list_of_includes[0].endswith("include")) ================================================ FILE: tests/recipes/test_libgeos.py ================================================ import unittest from unittest import mock from tests.recipes.recipe_lib_test import BaseTestForCmakeRecipe class TestLibgeosRecipe(BaseTestForCmakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.libgeos` """ recipe_name = "libgeos" @mock.patch("pythonforandroid.util.makedirs") @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.build.ensure_dir") @mock.patch("shutil.which") def test_build_arch( self, mock_shutil_which, mock_ensure_dir, mock_current_directory, mock_makedirs, ): # We overwrite the base test method because we # want to avoid any file/directory creation super().test_build_arch() # make sure that the mocked methods are actually called mock_makedirs.assert_called() ================================================ FILE: tests/recipes/test_libiconv.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLibiconvRecipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.libiconv` """ recipe_name = "libiconv" sh_command_calls = ["./configure"] ================================================ FILE: tests/recipes/test_liblzma.py ================================================ import unittest from os.path import join from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLibLzmaRecipe(BaseTestForMakeRecipe, unittest.TestCase): """TestCase for recipe :mod:`~pythonforandroid.recipes.liblzma`.""" recipe_name = "liblzma" sh_command_calls = ["./autogen.sh", "autoreconf", "./configure"] def test_get_library_includes(self): """ Test :meth:`~pythonforandroid.recipes.liblzma.get_library_includes`. """ recipe_build_dir = self.recipe.get_build_dir(self.arch.arch) self.assertEqual( self.recipe.get_library_includes(self.arch), f" -I{join(recipe_build_dir, 'p4a_install/include')}", ) def test_get_library_ldflags(self): """ Test :meth:`~pythonforandroid.recipes.liblzma.get_library_ldflags`. """ recipe_build_dir = self.recipe.get_build_dir(self.arch.arch) self.assertEqual( self.recipe.get_library_ldflags(self.arch), f" -L{join(recipe_build_dir, 'p4a_install/lib')}", ) def test_link_libs_flags(self): """ Test :meth:`~pythonforandroid.recipes.liblzma.get_library_libs_flag`. """ self.assertEqual(self.recipe.get_library_libs_flag(), " -llzma") def test_install_dir_not_named_install(self): """ Tests that the install directory is not named ``install``. liblzma already have a file named ``INSTALL`` in its source directory. On case-insensitive filesystems, using a folder named ``install`` will cause a conflict. (See issue: #2343). WARNING: This test is quite flaky, but should be enough to ensure that someone in the future will not accidentally rename the install directory without seeing this test to fail. """ liblzma_install_dir = self.recipe.built_libraries["liblzma.so"] self.assertNotIn("install", liblzma_install_dir.split("/")) ================================================ FILE: tests/recipes/test_libogg.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLiboggRecipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.libogg` """ recipe_name = "libogg" sh_command_calls = ["./configure"] ================================================ FILE: tests/recipes/test_libpq.py ================================================ import unittest from unittest import mock from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLibpqRecipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.libpq` """ recipe_name = "libpq" sh_command_calls = ["./configure"] @mock.patch("pythonforandroid.recipes.libpq.sh.cp") @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.build.ensure_dir") @mock.patch("shutil.which") def test_build_arch( self, mock_shutil_which, mock_ensure_dir, mock_current_directory, mock_sh_cp, ): # We overwrite the base test method because we need to mock a little # more with this recipe (`sh.cp`) super().test_build_arch() # make sure that the mocked methods are actually called mock_sh_cp.assert_called() ================================================ FILE: tests/recipes/test_libsecp256k1.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLibsecp256k1Recipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.libsecp256k1` """ recipe_name = "libsecp256k1" sh_command_calls = ["./autogen.sh", "./configure"] ================================================ FILE: tests/recipes/test_libshine.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLibshineRecipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.libshine` """ recipe_name = "libshine" sh_command_calls = ["./bootstrap", "./configure"] ================================================ FILE: tests/recipes/test_libvorbis.py ================================================ import unittest from unittest import mock from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLibvorbisRecipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.libvorbis` """ recipe_name = "libvorbis" sh_command_calls = ["./configure"] extra_env_flags = {'CFLAGS': 'libogg/include'} @mock.patch("pythonforandroid.recipes.libvorbis.sh.cp") @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.build.ensure_dir") @mock.patch("shutil.which") def test_build_arch( self, mock_shutil_which, mock_ensure_dir, mock_current_directory, mock_sh_cp, ): # We overwrite the base test method because we need to mock a little # more with this recipe (`sh.cp`) super().test_build_arch() # make sure that the mocked methods are actually called mock_sh_cp.assert_called() ================================================ FILE: tests/recipes/test_libvpx.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLibVPXRecipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.libvpx` """ recipe_name = "libvpx" sh_command_calls = ["./configure"] ================================================ FILE: tests/recipes/test_libx264.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLibx264Recipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.libx264` """ recipe_name = "libx264" sh_command_calls = ["./configure"] ================================================ FILE: tests/recipes/test_libxml2.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLibxml2Recipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.libxml2` """ recipe_name = "libxml2" sh_command_calls = ["./autogen.sh", "autoreconf", "./configure"] extra_env_flags = { "CONFIG_SHELL": "/bin/bash", "SHELL": "/bin/bash", } ================================================ FILE: tests/recipes/test_libxslt.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestLibxsltRecipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.libxslt` """ recipe_name = "libxslt" sh_command_calls = ["./autogen.sh", "autoreconf", "./configure"] extra_env_flags = { "CONFIG_SHELL": "/bin/bash", "SHELL": "/bin/bash", "LIBS": "-lxml2 -lz -lm", } ================================================ FILE: tests/recipes/test_openal.py ================================================ import unittest from unittest import mock from tests.recipes.recipe_lib_test import BaseTestForCmakeRecipe class TestOpenalRecipe(BaseTestForCmakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.openal` """ recipe_name = "openal" @mock.patch("pythonforandroid.recipes.openal.sh.cmake", create=True) @mock.patch("pythonforandroid.recipes.openal.sh.make", create=True) @mock.patch("pythonforandroid.recipes.openal.sh.cp") @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.build.ensure_dir") @mock.patch("shutil.which") def test_prebuild_arch( self, mock_shutil_which, mock_ensure_dir, mock_current_directory, mock_sh_cp, mock_sh_make, mock_sh_cmake, ): mock_shutil_which.return_value = ( "/opt/android/android-ndk/toolchains/" "llvm/prebuilt/linux-x86_64/bin/clang" ) self.recipe.build_arch(self.arch) # make sure that the mocked methods are actually called mock_ensure_dir.assert_called() mock_current_directory.assert_called() mock_shutil_which.assert_called() mock_sh_cp.assert_called() mock_sh_make.assert_called() mock_sh_cmake.assert_called() @mock.patch("pythonforandroid.recipes.openal.sh.cp") @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.build.ensure_dir") @mock.patch("shutil.which") def test_build_arch( self, mock_shutil_which, mock_ensure_dir, mock_current_directory, mock_sh_cp, ): # We overwrite the base test method because we need to mock a little # more with this recipe. super().test_build_arch() # make sure that the mocked methods are actually called mock_sh_cp.assert_called() ================================================ FILE: tests/recipes/test_openssl.py ================================================ import unittest from unittest import mock from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestOpensslRecipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.openssl` """ recipe_name = "openssl" sh_command_calls = ["perl"] @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.build.ensure_dir") @mock.patch("shutil.which") def test_build_arch( self, mock_shutil_which, mock_ensure_dir, mock_current_directory, ): # We overwrite the base test method because we need to mock a little # more with this recipe. super().test_build_arch() def test_include_flags(self): inc = self.recipe.include_flags(self.arch) build_dir = self.recipe.get_build_dir(self.arch) for i in {"include", "include/openssl"}: self.assertIn(f"-I{build_dir}/{i}", inc) def test_link_flags(self): build_dir = self.recipe.get_build_dir(self.arch) self.assertEqual( f" -L{build_dir} -lcrypto -lssl", self.recipe.link_flags(self.arch), ) def test_select_build_arch(self): expected_build_archs = { "armeabi": "android", "armeabi-v7a": "android-arm", "arm64-v8a": "android-arm64", "x86": "android-x86", "x86_64": "android-x86_64", } for arch in self.ctx.archs: self.assertEqual( expected_build_archs[arch.arch], self.recipe.select_build_arch(arch), ) ================================================ FILE: tests/recipes/test_pandas.py ================================================ import unittest from os.path import join from unittest import mock from tests.recipes.recipe_lib_test import RecipeCtx class TestPandasRecipe(RecipeCtx, unittest.TestCase): """ TestCase for recipe :mod:`~pythonforandroid.recipes.pandas` """ recipe_name = "pandas" @mock.patch("pythonforandroid.recipe.Recipe.check_recipe_choices") @mock.patch("pythonforandroid.build.ensure_dir") @mock.patch("shutil.which") def test_get_recipe_env( self, mock_shutil_which, mock_ensure_dir, mock_check_recipe_choices, ): """ Test that method :meth:`~pythonforandroid.recipes.pandas.PandasRecipe.get_recipe_env` returns the expected flags """ mock_shutil_which.return_value = ( "/opt/android/android-ndk/toolchains/" "llvm/prebuilt/linux-x86_64/bin/clang" ) mock_check_recipe_choices.return_value = sorted( self.ctx.recipe_build_order ) numpy_includes = join( self.ctx.get_python_install_dir(self.arch.arch), "numpy/_core/include", ) env = self.recipe.get_recipe_env(self.arch) self.assertIn(numpy_includes, env["NUMPY_INCLUDES"]) self.assertIn(" -landroid", env["LDFLAGS"]) # make sure that the mocked methods are actually called mock_ensure_dir.assert_called() mock_shutil_which.assert_called() mock_check_recipe_choices.assert_called() ================================================ FILE: tests/recipes/test_png.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe class TestPngRecipe(BaseTestForMakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.png` """ recipe_name = "png" sh_command_calls = ["./configure"] ================================================ FILE: tests/recipes/test_pyicu.py ================================================ import unittest from unittest import mock from tests.recipes.recipe_ctx import RecipeCtx from pythonforandroid.recipe import Recipe class TestPyIcuRecipe(RecipeCtx, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.pyicu` """ recipe_name = "pyicu" @mock.patch("pythonforandroid.recipe.Recipe.check_recipe_choices") @mock.patch("pythonforandroid.build.ensure_dir") @mock.patch("shutil.which") def test_get_recipe_env( self, mock_shutil_which, mock_ensure_dir, mock_check_recipe_choices, ): """ Test that method :meth:`~pythonforandroid.recipes.pyicu.PyICURecipe.get_recipe_env` returns the expected flags """ icu_recipe = Recipe.get_recipe("icu", self.ctx) mock_shutil_which.return_value = ( "/opt/android/android-ndk/toolchains/" "llvm/prebuilt/linux-x86_64/bin/clang" ) mock_check_recipe_choices.return_value = sorted( self.ctx.recipe_build_order ) expected_pyicu_libs = [ lib[3:-3] for lib in icu_recipe.built_libraries.keys() ] env = self.recipe.get_recipe_env(self.arch) self.assertEqual(":".join(expected_pyicu_libs), env["PYICU_LIBRARIES"]) self.assertIn("include/icu", env["CPPFLAGS"]) self.assertIn("icu4c/icu_build/lib", env["LDFLAGS"]) # make sure that the mocked methods are actually called mock_ensure_dir.assert_called() mock_shutil_which.assert_called() mock_check_recipe_choices.assert_called() ================================================ FILE: tests/recipes/test_python3.py ================================================ import unittest from os.path import join from unittest import mock from pythonforandroid.recipes.python3 import ( NDK_API_LOWER_THAN_SUPPORTED_MESSAGE, ) from pythonforandroid.util import BuildInterruptingException, build_platform from tests.recipes.recipe_lib_test import RecipeCtx class TestPython3Recipe(RecipeCtx, unittest.TestCase): """ TestCase for recipe :mod:`~pythonforandroid.recipes.python3` """ recipe_name = "python3" expected_compiler = ( f"/opt/android/android-ndk/toolchains/" f"llvm/prebuilt/{build_platform}/bin/clang" ) def test_property__libpython(self): self.assertEqual( self.recipe._libpython, f'libpython{self.recipe.link_version}.so' ) def test_include_root(self): expected_include_dir = join( self.recipe.get_build_dir(self.arch.arch), 'Include', ) self.assertEqual( expected_include_dir, self.recipe.include_root(self.arch.arch) ) def test_link_root(self): expected_link_root = join( self.recipe.get_build_dir(self.arch.arch), 'android-build', ) self.assertEqual( expected_link_root, self.recipe.link_root(self.arch.arch) ) @mock.patch("pythonforandroid.recipes.python3.subprocess.call") def test_compile_python_files(self, mock_subprocess): fake_compile_dir = '/fake/compile/dir' hostpy = self.recipe.ctx.hostpython = '/fake/hostpython3' self.recipe.compile_python_files(fake_compile_dir) mock_subprocess.assert_called_once_with( [hostpy, '-OO', '-m', 'compileall', '-b', '-f', fake_compile_dir], ) @mock.patch("pythonforandroid.recipe.Recipe.check_recipe_choices") @mock.patch("shutil.which") def test_get_recipe_env( self, mock_shutil_which, mock_check_recipe_choices, ): """ Test that method :meth:`~pythonforandroid.recipes.python3.Python3Recipe.get_recipe_env` returns the expected flags """ mock_shutil_which.return_value = self.expected_compiler mock_check_recipe_choices.return_value = sorted( self.ctx.recipe_build_order ) env = self.recipe.get_recipe_env(self.arch) self.assertIn('-fPIC -DANDROID', env["CFLAGS"]) self.assertEqual(env["CC"], self.arch.get_clang_exe(with_target=True)) # make sure that the mocked methods are actually called mock_check_recipe_choices.assert_called() def test_set_libs_flags(self): # todo: properly check `Python3Recipe.set_lib_flags` pass # These decorators are to mock calls to `get_recipe_env` # and `set_libs_flags`, since these calls are tested separately @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.util.makedirs") @mock.patch("shutil.which") def test_build_arch( self, mock_shutil_which, mock_makedirs, mock_chdir): mock_shutil_which.return_value = self.expected_compiler # specific `build_arch` mocks with mock.patch( "builtins.open", mock.mock_open(read_data="#define ZLIB_VERSION 1.1\nfoo") ) as mock_open_zlib, mock.patch( "pythonforandroid.recipes.python3.sh.Command" ) as mock_sh_command, mock.patch( "pythonforandroid.recipes.python3.sh.make" ) as mock_make, mock.patch( "pythonforandroid.recipes.python3.sh.cp" ) as mock_cp: self.recipe.build_arch(self.arch) # make sure that the mocked methods are actually called recipe_build_dir = self.recipe.get_build_dir(self.arch.arch) sh_command_calls = { f"{recipe_build_dir}/config.guess", f"{recipe_build_dir}/configure", } for command in sh_command_calls: self.assertIn( mock.call(command), mock_sh_command.mock_calls, ) mock_open_zlib.assert_called() self.assertEqual(mock_make.call_count, 1) for make_call, kw in mock_make.call_args_list: self.assertIn( f'INSTSONAME={self.recipe._libpython}', make_call ) mock_cp.assert_called_with( "pyconfig.h", join(recipe_build_dir, 'Include'), ) mock_makedirs.assert_called() mock_chdir.assert_called() def test_build_arch_wrong_ndk_api(self): # we check ndk_api using recipe's ctx self.recipe.ctx.ndk_api = 20 with self.assertRaises(BuildInterruptingException) as e: self.recipe.build_arch(self.arch) self.assertEqual( e.exception.args[0], NDK_API_LOWER_THAN_SUPPORTED_MESSAGE.format( ndk_api=self.recipe.ctx.ndk_api, min_ndk_api=self.recipe.MIN_NDK_API, ), ) # restore recipe's ctx or we could get failures with other test, # since we share `self.recipe with all the tests of the class self.recipe.ctx.ndk_api = self.ctx.ndk_api ================================================ FILE: tests/recipes/test_reportlab.py ================================================ import os import unittest from unittest.mock import patch from tests.recipes.recipe_ctx import RecipeCtx from pythonforandroid.util import ensure_dir class TestReportLabRecipe(RecipeCtx, unittest.TestCase): recipe_name = "reportlab" def setUp(self): """ Setups recipe and context. """ super().setUp() self.recipe_dir = self.recipe.get_build_dir(self.arch.arch) ensure_dir(self.recipe_dir) def test_prebuild_arch(self): """ Makes sure `prebuild_arch()` runs without error and patches `setup.py` as expected. """ # `prebuild_arch()` dynamically replaces strings in the `setup.py` file setup_path = os.path.join(self.recipe_dir, 'setup.py') with open(setup_path, 'w') as setup_file: setup_file.write('_FT_LIB_\n') setup_file.write('_FT_INC_\n') # these sh commands are not relevant for the test and need to be mocked with \ patch('sh.patch'), \ patch('pythonforandroid.recipe.touch'), \ patch('sh.unzip'), \ patch('pythonforandroid.recipe.Recipe.is_patched', lambda *a: False), \ patch('os.path.isfile'): self.recipe.prebuild_arch(self.arch) # makes sure placeholder got replaced with library and include paths with open(setup_path, 'r') as setup_file: lines = setup_file.readlines() self.assertTrue(lines[0].endswith('freetype/objs/.libs\n')) self.assertTrue(lines[1].endswith('freetype/include\n')) ================================================ FILE: tests/recipes/test_sdl2_mixer.py ================================================ import unittest from tests.recipes.recipe_ctx import RecipeCtx class TestSDL2MixerRecipe(RecipeCtx, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.sdl2_mixer` """ recipe_name = "sdl2_mixer" def setUp(self): """Setups bootstrap build_dir.""" super().setUp() bootstrap = self.ctx.bootstrap bootstrap.build_dir = bootstrap.get_build_dir() def test_get_include_dirs(self): list_of_includes = self.recipe.get_include_dirs(self.arch) self.assertIsInstance(list_of_includes, list) self.assertTrue(list_of_includes[0].endswith("include")) ================================================ FILE: tests/recipes/test_snappy.py ================================================ import unittest from tests.recipes.recipe_lib_test import BaseTestForCmakeRecipe class TestSnappyRecipe(BaseTestForCmakeRecipe, unittest.TestCase): """ An unittest for recipe :mod:`~pythonforandroid.recipes.snappy` """ recipe_name = "snappy" ================================================ FILE: tests/test_androidmodule_ctypes_finder.py ================================================ # This test is still expected to support Python 2, as it tests # on-Android functionality that we still maintain try: # Python 3+ from unittest import mock from unittest.mock import MagicMock except ImportError: # Python 2 import mock from mock import MagicMock import os import shutil import sys import tempfile # Import the tested android._ctypes_library_finder module, # making sure android._android won't crash us! # (since android._android is android-only / not compilable on desktop) android_module_folder = os.path.abspath(os.path.join( os.path.dirname(__file__), "..", "pythonforandroid", "recipes", "android", "src" )) sys.path.insert(0, android_module_folder) sys.modules['android._android'] = MagicMock() import android._ctypes_library_finder sys.path.remove(android_module_folder) @mock.patch.dict('sys.modules', jnius=MagicMock()) def test_get_activity_lib_dir(): import jnius # should get us our fake module # Short test that it works when activity doesn't exist: jnius.autoclass = MagicMock() jnius.autoclass.return_value = None assert android._ctypes_library_finder.get_activity_lib_dir( "JavaClass" ) is None assert mock.call("JavaClass") in jnius.autoclass.call_args_list # Comprehensive test that verifies getApplicationInfo() call: activity = MagicMock() app_context = activity.getApplicationContext() app_context.getPackageName.return_value = "test.package" app_info = app_context.getPackageManager().getApplicationInfo() app_info.nativeLibraryDir = '/testpath' def pick_class(name): cls = MagicMock() if name == "JavaClass": cls.mActivity = activity elif name == "android.content.pm.PackageManager": # Manager class: cls.GET_SHARED_LIBRARY_FILES = 1024 return cls jnius.autoclass = MagicMock(side_effect=pick_class) assert android._ctypes_library_finder.get_activity_lib_dir( "JavaClass" ) == "/testpath" assert mock.call("JavaClass") in jnius.autoclass.call_args_list assert mock.call("test.package", 1024) in ( app_context.getPackageManager().getApplicationInfo.call_args_list ) @mock.patch.dict('sys.modules', jnius=MagicMock()) def test_find_library(): test_d = tempfile.mkdtemp(prefix="p4a-android-ctypes-test-libdir-") try: with open(os.path.join(test_d, "mymadeuplib.so.5"), "w"): pass import jnius # should get us our fake module # Test with mActivity returned: jnius.autoclass = MagicMock() jnius.autoclass().mService = None app_context = jnius.autoclass().mActivity.getApplicationContext() app_info = app_context.getPackageManager().getApplicationInfo() app_info.nativeLibraryDir = '/doesnt-exist-testpath' assert android._ctypes_library_finder.find_library( "mymadeuplib" ) is None assert mock.call("org.kivy.android.PythonActivity") in ( jnius.autoclass.call_args_list ) app_info.nativeLibraryDir = test_d assert os.path.normpath(android._ctypes_library_finder.find_library( "mymadeuplib" )) == os.path.normpath(os.path.join(test_d, "mymadeuplib.so.5")) # Test with mService returned: jnius.autoclass = MagicMock() jnius.autoclass().mActivity = None app_context = jnius.autoclass().mService.getApplicationContext() app_info = app_context.getPackageManager().getApplicationInfo() app_info.nativeLibraryDir = '/doesnt-exist-testpath' assert android._ctypes_library_finder.find_library( "mymadeuplib" ) is None app_info.nativeLibraryDir = test_d assert os.path.normpath(android._ctypes_library_finder.find_library( "mymadeuplib" )) == os.path.normpath(os.path.join(test_d, "mymadeuplib.so.5")) finally: shutil.rmtree(test_d) def test_does_libname_match_filename(): assert android._ctypes_library_finder.does_libname_match_filename( "mylib", "mylib.so" ) assert not android._ctypes_library_finder.does_libname_match_filename( "mylib", "amylib.so" ) assert not android._ctypes_library_finder.does_libname_match_filename( "mylib", "mylib.txt" ) assert not android._ctypes_library_finder.does_libname_match_filename( "mylib", "mylib" ) assert android._ctypes_library_finder.does_libname_match_filename( "mylib", "libmylib.test.so.1.2.3" ) assert not android._ctypes_library_finder.does_libname_match_filename( "mylib", "libtest.mylib.so" ) assert android._ctypes_library_finder.does_libname_match_filename( "mylib", "mylib.so.5" ) ================================================ FILE: tests/test_androidndk.py ================================================ import unittest from unittest import mock from pythonforandroid.androidndk import AndroidNDK class TestAndroidNDK(unittest.TestCase): """ An inherited class of `unittest.TestCase`to test the module :mod:`~pythonforandroid.androidndk`. """ def setUp(self): """Configure a :class:`~pythonforandroid.androidndk.AndroidNDK` so we can perform our unittests""" self.ndk = AndroidNDK("/opt/android/android-ndk") @mock.patch("sys.platform", "linux") def test_host_tag_linux(self): """Test the `host_tag` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class when the host is Linux.""" self.assertEqual(self.ndk.host_tag, "linux-x86_64") @mock.patch("sys.platform", "darwin") def test_host_tag_darwin(self): """Test the `host_tag` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class when the host is Darwin.""" self.assertEqual(self.ndk.host_tag, "darwin-x86_64") def test_llvm_prebuilt_dir(self): """Test the `llvm_prebuilt_dir` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class.""" self.assertEqual( self.ndk.llvm_prebuilt_dir, f"/opt/android/android-ndk/toolchains/llvm/prebuilt/{self.ndk.host_tag}", ) def test_llvm_bin_dir(self): """Test the `llvm_bin_dir` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class.""" self.assertEqual( self.ndk.llvm_bin_dir, f"/opt/android/android-ndk/toolchains/llvm/prebuilt/{self.ndk.host_tag}/bin", ) def test_clang(self): """Test the `clang` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class.""" self.assertEqual( self.ndk.clang, f"/opt/android/android-ndk/toolchains/llvm/prebuilt/{self.ndk.host_tag}/bin/clang", ) def test_clang_cxx(self): """Test the `clang_cxx` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class.""" self.assertEqual( self.ndk.clang_cxx, f"/opt/android/android-ndk/toolchains/llvm/prebuilt/{self.ndk.host_tag}/bin/clang++", ) def test_llvm_ar(self): """Test the `llvm_ar` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class.""" self.assertEqual( self.ndk.llvm_ar, f"/opt/android/android-ndk/toolchains/llvm/prebuilt/{self.ndk.host_tag}/bin/llvm-ar", ) def test_llvm_ranlib(self): """Test the `llvm_ranlib` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class.""" self.assertEqual( self.ndk.llvm_ranlib, f"/opt/android/android-ndk/toolchains/llvm/prebuilt/{self.ndk.host_tag}/bin/llvm-ranlib", ) def test_llvm_objcopy(self): """Test the `llvm_objcopy` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class.""" self.assertEqual( self.ndk.llvm_objcopy, f"/opt/android/android-ndk/toolchains/llvm/prebuilt/{self.ndk.host_tag}/bin/llvm-objcopy", ) def test_llvm_objdump(self): """Test the `llvm_objdump` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class.""" self.assertEqual( self.ndk.llvm_objdump, f"/opt/android/android-ndk/toolchains/llvm/prebuilt/{self.ndk.host_tag}/bin/llvm-objdump", ) def test_llvm_readelf(self): """Test the `llvm_readelf` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class.""" self.assertEqual( self.ndk.llvm_readelf, f"/opt/android/android-ndk/toolchains/llvm/prebuilt/{self.ndk.host_tag}/bin/llvm-readelf", ) def test_llvm_strip(self): """Test the `llvm_strip` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class.""" self.assertEqual( self.ndk.llvm_strip, f"/opt/android/android-ndk/toolchains/llvm/prebuilt/{self.ndk.host_tag}/bin/llvm-strip", ) def test_sysroot(self): """Test the `sysroot` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class.""" self.assertEqual( self.ndk.sysroot, f"/opt/android/android-ndk/toolchains/llvm/prebuilt/{self.ndk.host_tag}/sysroot", ) def test_sysroot_include_dir(self): """Test the `sysroot_include_dir` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class.""" self.assertEqual( self.ndk.sysroot_include_dir, f"/opt/android/android-ndk/toolchains/llvm/prebuilt/{self.ndk.host_tag}/sysroot/usr/include", ) def test_sysroot_lib_dir(self): """Test the `sysroot_lib_dir` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class.""" self.assertEqual( self.ndk.sysroot_lib_dir, f"/opt/android/android-ndk/toolchains/llvm/prebuilt/{self.ndk.host_tag}/sysroot/usr/lib", ) def test_libcxx_include_dir(self): """Test the `libcxx_include_dir` property of the :class:`~pythonforandroid.androidndk.AndroidNDK` class.""" self.assertEqual( self.ndk.libcxx_include_dir, f"/opt/android/android-ndk/toolchains/llvm/prebuilt/{self.ndk.host_tag}/sysroot/usr/include/c++/v1", ) ================================================ FILE: tests/test_archs.py ================================================ import os import unittest from os import environ from unittest import mock from pythonforandroid.bootstrap import Bootstrap from pythonforandroid.distribution import Distribution from pythonforandroid.recipe import Recipe from pythonforandroid.build import Context from pythonforandroid.util import BuildInterruptingException from pythonforandroid.archs import ( Arch, ArchARM, ArchARMv7_a, ArchAarch_64, Archx86, Archx86_64, ) from pythonforandroid.androidndk import AndroidNDK expected_env_gcc_keys = { "CFLAGS", "LDFLAGS", "CXXFLAGS", "CC", "CXX", "LDSHARED", "STRIP", "MAKE", "READELF", "BUILDLIB_PATH", "PATH", "ARCH", "NDK_API", } class ArchSetUpBaseClass(object): """ An class object which is intended to be used as a base class to configure an inherited class of `unittest.TestCase`. This class will override the `setUp` method. """ ctx = None expected_compiler = "" TEST_ARCH = 'armeabi-v7a' def setUp(self): self.ctx = Context() self.ctx.ndk_api = 21 self.ctx.android_api = 27 self.ctx._sdk_dir = "/opt/android/android-sdk" self.ctx._ndk_dir = "/opt/android/android-ndk" self.ctx.ndk = AndroidNDK(self.ctx._ndk_dir) self.ctx.setup_dirs(os.getcwd()) self.ctx.bootstrap = Bootstrap().get_bootstrap("sdl2", self.ctx) self.ctx.bootstrap.distribution = Distribution.get_distribution( self.ctx, name="sdl2", recipes=["python3", "kivy"], archs=[self.TEST_ARCH], ) self.ctx.python_recipe = Recipe.get_recipe("python3", self.ctx) # Here we define the expected compiler, which, as per ndk >= r19, # should be the same for all the tests (no more gcc compiler) self.expected_compiler = ( f"/opt/android/android-ndk/toolchains/" f"llvm/prebuilt/{self.ctx.ndk.host_tag}/bin/clang" ) class TestArch(ArchSetUpBaseClass, unittest.TestCase): """ An inherited class of `ArchSetUpBaseClass` and `unittest.TestCase` which will be used to perform tests for the base class :class:`~pythonforandroid.archs.Arch`. """ def test_arch(self): arch = Arch(self.ctx) self.assertEqual(arch.__str__(), arch.arch) self.assertEqual(arch.target, "None21") self.assertIsNone(arch.command_prefix) self.assertIsInstance(arch.include_dirs, list) class TestArchARM(ArchSetUpBaseClass, unittest.TestCase): """ An inherited class of `ArchSetUpBaseClass` and `unittest.TestCase` which will be used to perform tests for :class:`~pythonforandroid.archs.ArchARM`. """ @mock.patch("shutil.which") @mock.patch("pythonforandroid.build.ensure_dir") def test_arch_arm(self, mock_ensure_dir, mock_shutil_which): """ Test that class :class:`~pythonforandroid.archs.ArchARM` returns some expected attributes and environment variables. .. note:: Here we mock two methods: - `ensure_dir` because we don't want to create any directory - `shutil.which` because otherwise we will get an error when trying to find the compiler (we are setting some fake paths for our android sdk and ndk so probably will not exist) """ mock_shutil_which.return_value = self.expected_compiler mock_ensure_dir.return_value = True arch = ArchARM(self.ctx) self.assertEqual(arch.arch, "armeabi") self.assertEqual(arch.__str__(), "armeabi") self.assertEqual(arch.command_prefix, "arm-linux-androideabi") self.assertEqual(arch.target, "armv7a-linux-androideabi21") arch = ArchARM(self.ctx) # Check environment flags env = arch.get_env() self.assertIsInstance(env, dict) self.assertEqual( expected_env_gcc_keys, set(env.keys()) & expected_env_gcc_keys ) # check shutil.which calls mock_shutil_which.assert_called_once_with( self.expected_compiler, path=environ["PATH"] ) # check gcc compilers self.assertEqual(env["CC"].split()[0], self.expected_compiler) self.assertEqual(env["CXX"].split()[0], self.expected_compiler + "++") # check android binaries self.assertEqual( env["STRIP"].split()[0], os.path.join( self.ctx._ndk_dir, f"toolchains/llvm/prebuilt/{self.ctx.ndk.host_tag}/bin", "llvm-strip", ) ) self.assertEqual( env["READELF"].split()[0], os.path.join( self.ctx._ndk_dir, f"toolchains/llvm/prebuilt/{self.ctx.ndk.host_tag}/bin", "llvm-readelf", ) ) # check that cflags are in gcc self.assertIn(env["CFLAGS"], env["CC"]) # check that flags aren't in gcc and also check ccache self.ctx.ccache = "/usr/bin/ccache" env = arch.get_env(with_flags_in_cc=False) self.assertNotIn(env["CFLAGS"], env["CC"]) self.assertEqual(env["USE_CCACHE"], "1") self.assertEqual(env["NDK_CCACHE"], "/usr/bin/ccache") # Check exception in case that CC is not found mock_shutil_which.return_value = None with self.assertRaises(BuildInterruptingException) as e: arch.get_env() self.assertEqual( e.exception.args[0], "Couldn't find executable for CC. This indicates a problem " "locating the {expected_compiler} executable in the Android " "NDK, not that you don't have a normal compiler installed. " "Exiting.".format(expected_compiler=self.expected_compiler), ) class TestArchARMv7a(ArchSetUpBaseClass, unittest.TestCase): """ An inherited class of `ArchSetUpBaseClass` and `unittest.TestCase` which will be used to perform tests for :class:`~pythonforandroid.archs.ArchARMv7_a`. """ @mock.patch("shutil.which") @mock.patch("pythonforandroid.build.ensure_dir") def test_arch_armv7a( self, mock_ensure_dir, mock_shutil_which ): """ Test that class :class:`~pythonforandroid.archs.ArchARMv7_a` returns some expected attributes and environment variables. .. note:: Here we mock the same functions than :meth:`TestArchARM.test_arch_arm`. This has to be done because here we tests the `get_env` with clang """ mock_shutil_which.return_value = self.expected_compiler mock_ensure_dir.return_value = True arch = ArchARMv7_a(self.ctx) self.assertEqual(arch.arch, "armeabi-v7a") self.assertEqual(arch.__str__(), "armeabi-v7a") self.assertEqual(arch.command_prefix, "arm-linux-androideabi") self.assertEqual(arch.target, "armv7a-linux-androideabi21") env = arch.get_env() # check shutil.which calls mock_shutil_which.assert_called_once_with( self.expected_compiler, path=environ["PATH"] ) # check clang self.assertEqual( env["CC"].split()[0], "{ndk_dir}/toolchains/llvm/prebuilt/" "{host_tag}/bin/clang".format( ndk_dir=self.ctx._ndk_dir, host_tag=self.ctx.ndk.host_tag ), ) self.assertEqual( env["CXX"].split()[0], "{ndk_dir}/toolchains/llvm/prebuilt/" "{host_tag}/bin/clang++".format( ndk_dir=self.ctx._ndk_dir, host_tag=self.ctx.ndk.host_tag ), ) # For armeabi-v7a we expect some extra cflags self.assertIn( " -march=armv7-a -mfloat-abi=softfp -mfpu=vfp -mthumb", env["CFLAGS"], ) class TestArchX86(ArchSetUpBaseClass, unittest.TestCase): """ An inherited class of `ArchSetUpBaseClass` and `unittest.TestCase` which will be used to perform tests for :class:`~pythonforandroid.archs.Archx86`. """ @mock.patch("shutil.which") @mock.patch("pythonforandroid.build.ensure_dir") def test_arch_x86(self, mock_ensure_dir, mock_shutil_which): """ Test that class :class:`~pythonforandroid.archs.Archx86` returns some expected attributes and environment variables. .. note:: Here we mock the same functions than :meth:`TestArchARM.test_arch_arm` plus `glob`, so we make sure that the glob result is the expected even if the folder doesn't exist, which is probably the case. This has to be done because here we tests the `get_env` with clang """ mock_shutil_which.return_value = self.expected_compiler mock_ensure_dir.return_value = True arch = Archx86(self.ctx) self.assertEqual(arch.arch, "x86") self.assertEqual(arch.__str__(), "x86") self.assertEqual(arch.command_prefix, "i686-linux-android") self.assertEqual(arch.target, "i686-linux-android21") env = arch.get_env() # check shutil.which calls mock_shutil_which.assert_called_once_with( self.expected_compiler, path=environ["PATH"] ) # For x86 we expect some extra cflags in our `environment` self.assertIn( " -march=i686 -mssse3 -mfpmath=sse -m32", env["CFLAGS"], ) class TestArchX86_64(ArchSetUpBaseClass, unittest.TestCase): """ An inherited class of `ArchSetUpBaseClass` and `unittest.TestCase` which will be used to perform tests for :class:`~pythonforandroid.archs.Archx86_64`. """ @mock.patch("shutil.which") @mock.patch("pythonforandroid.build.ensure_dir") def test_arch_x86_64( self, mock_ensure_dir, mock_shutil_which ): """ Test that class :class:`~pythonforandroid.archs.Archx86_64` returns some expected attributes and environment variables. .. note:: Here we mock the same functions than :meth:`TestArchARM.test_arch_arm` plus `glob`, so we make sure that the glob result is the expected even if the folder doesn't exist, which is probably the case. This has to be done because here we tests the `get_env` with clang """ mock_shutil_which.return_value = self.expected_compiler mock_ensure_dir.return_value = True arch = Archx86_64(self.ctx) self.assertEqual(arch.arch, "x86_64") self.assertEqual(arch.__str__(), "x86_64") self.assertEqual(arch.command_prefix, "x86_64-linux-android") self.assertEqual(arch.target, "x86_64-linux-android21") env = arch.get_env() # check shutil.which calls mock_shutil_which.assert_called_once_with( self.expected_compiler, path=environ["PATH"] ) # For x86_64 we expect some extra cflags in our `environment` mock_shutil_which.assert_called_once() self.assertIn( " -march=x86-64 -msse4.2 -mpopcnt -m64", env["CFLAGS"] ) class TestArchAArch64(ArchSetUpBaseClass, unittest.TestCase): """ An inherited class of `ArchSetUpBaseClass` and `unittest.TestCase` which will be used to perform tests for :class:`~pythonforandroid.archs.ArchAarch_64`. """ @mock.patch("shutil.which") @mock.patch("pythonforandroid.build.ensure_dir") def test_arch_aarch_64( self, mock_ensure_dir, mock_shutil_which ): """ Test that class :class:`~pythonforandroid.archs.ArchAarch_64` returns some expected attributes and environment variables. .. note:: Here we mock the same functions than :meth:`TestArchARM.test_arch_arm` plus `glob`, so we make sure that the glob result is the expected even if the folder doesn't exist, which is probably the case. This has to be done because here we tests the `get_env` with clang """ mock_shutil_which.return_value = self.expected_compiler mock_ensure_dir.return_value = True arch = ArchAarch_64(self.ctx) self.assertEqual(arch.arch, "arm64-v8a") self.assertEqual(arch.__str__(), "arm64-v8a") self.assertEqual(arch.command_prefix, "aarch64-linux-android") self.assertEqual(arch.target, "aarch64-linux-android21") env = arch.get_env() # check shutil.which calls mock_shutil_which.assert_called_once_with( self.expected_compiler, path=environ["PATH"] ) # For x86_64 we expect to find an extra key in`environment` for flag in {"CFLAGS", "CXXFLAGS", "CC", "CXX"}: self.assertIn("-march=armv8-a", env[flag]) ================================================ FILE: tests/test_bdistapk.py ================================================ import sys from unittest import mock from setuptools.dist import Distribution from pythonforandroid.bdistapk import ( argv_contains, BdistAPK, BdistAAR, BdistAAB, ) class TestArgvContains: """Test argv_contains helper function.""" def test_argv_contains_present(self): """Test argv_contains returns True when argument is present.""" with mock.patch.object(sys, 'argv', ['prog', '--name=test', '--version=1.0']): assert argv_contains('--name') assert argv_contains('--version') def test_argv_contains_partial_match(self): """Test argv_contains returns True for partial matches.""" with mock.patch.object(sys, 'argv', ['prog', '--name=test']): assert argv_contains('--name') assert argv_contains('--nam') def test_argv_contains_not_present(self): """Test argv_contains returns False when argument is not present.""" with mock.patch.object(sys, 'argv', ['prog', '--name=test']): assert not argv_contains('--package') assert not argv_contains('--arch') class TestBdist: """Test Bdist base class.""" def setup_method(self): """Set up test fixtures.""" self.distribution = Distribution({ 'name': 'TestApp', 'version': '1.0.0', }) self.distribution.package_data = {'testapp': ['*.py', '*.kv']} @mock.patch('pythonforandroid.bdistapk.ensure_dir') @mock.patch('pythonforandroid.bdistapk.rmdir') def test_initialize_options(self, mock_rmdir, mock_ensure_dir): """Test initialize_options sets attributes from user_options.""" bdist = BdistAPK(self.distribution) bdist.user_options = [('name=', None, None), ('version=', None, None)] bdist.initialize_options() assert hasattr(bdist, 'name') assert hasattr(bdist, 'version') @mock.patch('pythonforandroid.bdistapk.argv_contains') @mock.patch('pythonforandroid.bdistapk.ensure_dir') @mock.patch('pythonforandroid.bdistapk.rmdir') def test_finalize_options_injects_defaults( self, mock_rmdir, mock_ensure_dir, mock_argv_contains ): """Test finalize_options injects default name, package, version, arch.""" mock_argv_contains.return_value = False with mock.patch.object(sys, 'argv', ['setup.py', 'apk']): bdist = BdistAPK(self.distribution) bdist.finalize_options() # Check that defaults were added to sys.argv argv_str = ' '.join(sys.argv) assert '--name=' in argv_str or any('--name' in arg for arg in sys.argv) @mock.patch('pythonforandroid.bdistapk.argv_contains') @mock.patch('pythonforandroid.bdistapk.ensure_dir') @mock.patch('pythonforandroid.bdistapk.rmdir') def test_finalize_options_permissions_handling( self, mock_rmdir, mock_ensure_dir, mock_argv_contains ): """Test finalize_options handles permissions list correctly.""" mock_argv_contains.side_effect = lambda x: x != '--permissions' # Set up permissions in the distribution command options self.distribution.command_options['apk'] = { 'permissions': ('setup.py', ['INTERNET', 'CAMERA']) } with mock.patch.object(sys, 'argv', ['setup.py', 'apk']): bdist = BdistAPK(self.distribution) bdist.package_type = 'apk' bdist.finalize_options() # Check permissions were added assert any('--permission=INTERNET' in arg for arg in sys.argv) assert any('--permission=CAMERA' in arg for arg in sys.argv) @mock.patch('pythonforandroid.entrypoints.main') @mock.patch('pythonforandroid.bdistapk.argv_contains') @mock.patch('pythonforandroid.bdistapk.ensure_dir') @mock.patch('pythonforandroid.bdistapk.rmdir') @mock.patch('pythonforandroid.bdistapk.copyfile') @mock.patch('pythonforandroid.bdistapk.glob') def test_run_calls_main( self, mock_glob, mock_copyfile, mock_rmdir, mock_ensure_dir, mock_argv_contains, mock_main ): """Test run() calls prepare_build_dir and then main().""" mock_glob.return_value = ['testapp/main.py'] mock_argv_contains.return_value = False # Not using --launcher or --private with mock.patch.object(sys, 'argv', ['setup.py', 'apk']): bdist = BdistAPK(self.distribution) bdist.arch = 'armeabi-v7a' bdist.run() mock_rmdir.assert_called() mock_ensure_dir.assert_called() mock_main.assert_called_once() assert sys.argv[1] == 'apk' @mock.patch('pythonforandroid.bdistapk.argv_contains') @mock.patch('pythonforandroid.bdistapk.ensure_dir') @mock.patch('pythonforandroid.bdistapk.rmdir') @mock.patch('pythonforandroid.bdistapk.copyfile') @mock.patch('pythonforandroid.bdistapk.glob') @mock.patch('builtins.exit', side_effect=SystemExit(1)) def test_prepare_build_dir_no_main_py( self, mock_exit, mock_glob, mock_copyfile, mock_rmdir, mock_ensure_dir, mock_argv_contains ): """Test prepare_build_dir exits if no main.py found and not using launcher.""" mock_glob.return_value = ['testapp/helper.py'] mock_argv_contains.return_value = False # Not using --launcher bdist = BdistAPK(self.distribution) bdist.arch = 'armeabi-v7a' # Expect SystemExit to be raised try: bdist.prepare_build_dir() assert False, "Expected SystemExit to be raised" except SystemExit: pass mock_exit.assert_called_once_with(1) @mock.patch('pythonforandroid.bdistapk.argv_contains') @mock.patch('pythonforandroid.bdistapk.ensure_dir') @mock.patch('pythonforandroid.bdistapk.rmdir') @mock.patch('pythonforandroid.bdistapk.copyfile') @mock.patch('pythonforandroid.bdistapk.glob') def test_prepare_build_dir_with_main_py( self, mock_glob, mock_copyfile, mock_rmdir, mock_ensure_dir, mock_argv_contains ): """Test prepare_build_dir succeeds when main.py is found.""" mock_glob.return_value = ['testapp/main.py', 'testapp/helper.py'] # Return False for all argv_contains checks (no --launcher, no --private) mock_argv_contains.return_value = False with mock.patch.object(sys, 'argv', ['setup.py', 'apk']): bdist = BdistAPK(self.distribution) bdist.arch = 'armeabi-v7a' bdist.prepare_build_dir() # Should have copied files (glob might return duplicates) assert mock_copyfile.call_count >= 2 # Should have added --private argument assert any('--private=' in arg for arg in sys.argv) class TestBdistSubclasses: """Test BdistAPK, BdistAAR, BdistAAB subclasses.""" def setup_method(self): """Set up test fixtures.""" self.distribution = Distribution({ 'name': 'TestApp', 'version': '1.0.0', }) self.distribution.package_data = {} def test_bdist_apk_package_type(self): """Test BdistAPK has correct package_type.""" bdist = BdistAPK(self.distribution) assert bdist.package_type == 'apk' assert bdist.description == 'Create an APK with python-for-android' def test_bdist_aar_package_type(self): """Test BdistAAR has correct package_type.""" bdist = BdistAAR(self.distribution) assert bdist.package_type == 'aar' assert bdist.description == 'Create an AAR with python-for-android' def test_bdist_aab_package_type(self): """Test BdistAAB has correct package_type.""" bdist = BdistAAB(self.distribution) assert bdist.package_type == 'aab' assert bdist.description == 'Create an AAB with python-for-android' ================================================ FILE: tests/test_bootstrap.py ================================================ import os import sh import unittest from unittest import mock from pythonforandroid.bootstrap import ( _cmp_bootstraps_by_priority, Bootstrap, expand_dependencies, ) from pythonforandroid.distribution import Distribution from pythonforandroid.recipe import Recipe from pythonforandroid.archs import ArchARMv7_a from pythonforandroid.build import Context from pythonforandroid.util import BuildInterruptingException from pythonforandroid.androidndk import AndroidNDK from tests.test_graph import get_fake_recipe class BaseClassSetupBootstrap: """ An class which is intended to be used as a base class to configure an inherited class of `unittest.TestCase`. This class will override the `setUp` and `tearDown` methods. """ TEST_ARCH = 'armeabi-v7a' def setUp(self): Recipe.recipes = {} # clear Recipe class cache self.ctx = Context() self.ctx.ndk_api = 21 self.ctx.android_api = 27 self.ctx._sdk_dir = "/opt/android/android-sdk" self.ctx._ndk_dir = "/opt/android/android-ndk" self.ctx.ndk = AndroidNDK(self.ctx._ndk_dir) self.ctx.setup_dirs(os.getcwd()) self.ctx.recipe_build_order = [ "hostpython3", "python3", "sdl2", "kivy", ] def setUp_distribution_with_bootstrap(self, bs): """ Extend the setUp by configuring a distribution, because some test needs a distribution to be set to be properly tested """ self.ctx.bootstrap = bs self.ctx.bootstrap.distribution = Distribution.get_distribution( self.ctx, name="test_prj", recipes=["python3", "kivy"], archs=[self.TEST_ARCH], ) def tearDown(self): """ Extend the `tearDown` by configuring a distribution, because some test needs a distribution to be set to be properly tested """ self.ctx.bootstrap = None class TestBootstrapBasic(BaseClassSetupBootstrap, unittest.TestCase): """ An inherited class of `BaseClassSetupBootstrap` and `unittest.TestCase` which will be used to perform tests for the methods/attributes shared between all bootstraps which inherits from class :class:`~pythonforandroid.bootstrap.Bootstrap`. """ def test_attributes(self): """A test which will initialize a bootstrap and will check if the values are the expected. """ 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") # bs.dist_dir should raise an error if there is no distribution to query bs.distribution = None with self.assertRaises(BuildInterruptingException): bs.dist_dir # test dist_dir success self.setUp_distribution_with_bootstrap(bs) expected_folder_name = 'test_prj' self.assertTrue( bs.dist_dir.endswith(f"dists/{expected_folder_name}")) def test_build_dist_dirs(self): """A test which will initialize a bootstrap and will check if the directories we set has the values that we expect. Here we test methods: - :meth:`~pythonforandroid.bootstrap.Bootstrap.get_build_dir` - :meth:`~pythonforandroid.bootstrap.Bootstrap.get_dist_dir` - :meth:`~pythonforandroid.bootstrap.Bootstrap.get_common_dir` """ bs = Bootstrap.get_bootstrap("sdl2", self.ctx) self.assertTrue( bs.get_build_dir().endswith("build/bootstrap_builds/sdl2") ) self.assertTrue(bs.get_dist_dir("test_prj").endswith("dists/test_prj")) def test__cmp_bootstraps_by_priority(self): # Test service_only has higher priority than sdl2: # (higher priority = smaller number/comes first) self.assertTrue(_cmp_bootstraps_by_priority( Bootstrap.get_bootstrap("service_only", self.ctx), Bootstrap.get_bootstrap("sdl2", self.ctx) ) < 0) # Test a random bootstrap is always lower priority than sdl2: class _FakeBootstrap: def __init__(self, name): self.name = name bs1 = _FakeBootstrap("alpha") bs2 = _FakeBootstrap("zeta") self.assertTrue(_cmp_bootstraps_by_priority( bs1, Bootstrap.get_bootstrap("sdl2", self.ctx) ) > 0) self.assertTrue(_cmp_bootstraps_by_priority( bs2, Bootstrap.get_bootstrap("sdl2", self.ctx) ) > 0) # Test bootstraps that aren't otherwise recognized are ranked # alphabetically: self.assertTrue(_cmp_bootstraps_by_priority( bs2, bs1, ) > 0) self.assertTrue(_cmp_bootstraps_by_priority( bs1, bs2, ) < 0) def test_all_bootstraps(self): """A test which will initialize a bootstrap and will check if the method :meth:`~pythonforandroid.bootstrap.Bootstrap.all_bootstraps ` returns the expected values, which should be: `empty", `service_only`, `webview`, `sdl2`, `sdl3` and `qt` """ expected_bootstraps = { "empty", "service_only", "service_library", "webview", "sdl2", "sdl3", "qt", } set_of_bootstraps = Bootstrap.all_bootstraps() self.assertEqual( expected_bootstraps, expected_bootstraps & set_of_bootstraps ) self.assertEqual(len(expected_bootstraps), len(set_of_bootstraps)) def test_expand_dependencies(self): # Test dependency expansion of a recipe with no alternatives: expanded_result_1 = expand_dependencies(["pysdl2"], self.ctx) self.assertTrue( {"sdl2", "pysdl2", "python3"} in [set(s) for s in expanded_result_1] ) # Test expansion of a single element but as tuple: expanded_result_1 = expand_dependencies([("pysdl2",)], self.ctx) self.assertTrue( {"sdl2", "pysdl2", "python3"} in [set(s) for s in expanded_result_1] ) # Test all alternatives are listed (they won't have dependencies # expanded since expand_dependencies() is too simplistic): expanded_result_2 = expand_dependencies([("pysdl2", "kivy")], self.ctx) self.assertEqual([["pysdl2"], ["kivy"]], expanded_result_2) def test_expand_dependencies_with_pure_python_package(self): """Check that `expanded_dependencies`, with a pure python package as one of the dependencies, returns a list of dependencies """ expanded_result = expand_dependencies( ["python3", "kivy", "peewee"], self.ctx ) # we expect to 2 results for python3 # (python3, sdl2/sdl3 [one is blacklisted]) self.assertEqual(len(expanded_result), 2) self.assertIsInstance(expanded_result, list) for i in expanded_result: self.assertIsInstance(i, list) def test_get_bootstraps_from_recipes(self): """A test which will initialize a bootstrap and will check if the method :meth:`~pythonforandroid.bootstrap.Bootstrap. get_bootstraps_from_recipes` returns the expected values """ import pythonforandroid.recipe original_get_recipe = pythonforandroid.recipe.Recipe.get_recipe # Test that SDL2 works with kivy: recipes_sdl2 = {"sdl2", "python3", "kivy"} bs = Bootstrap.get_bootstrap_from_recipes(recipes_sdl2, self.ctx) self.assertEqual(bs.name, "sdl2") # Test that pysdl2 or kivy alone will also yield SDL2 (dependency): recipes_pysdl2_only = {"pysdl2"} bs = Bootstrap.get_bootstrap_from_recipes( recipes_pysdl2_only, self.ctx ) self.assertEqual(bs.name, "sdl2") recipes_kivy_only = {"kivy"} bs = Bootstrap.get_bootstrap_from_recipes( recipes_kivy_only, self.ctx ) self.assertEqual(bs.name, "sdl2") with mock.patch("pythonforandroid.recipe.Recipe.get_recipe") as \ mock_get_recipe: # Test that something conflicting with sdl2 won't give sdl2: def _add_sdl2_conflicting_recipe(name, ctx): if name == "conflictswithsdl2": if name not in pythonforandroid.recipe.Recipe.recipes: pythonforandroid.recipe.Recipe.recipes[name] = ( get_fake_recipe("sdl2", conflicts=["sdl2"]) ) return original_get_recipe(name, ctx) mock_get_recipe.side_effect = _add_sdl2_conflicting_recipe recipes_with_sdl2_conflict = {"python3", "conflictswithsdl2"} bs = Bootstrap.get_bootstrap_from_recipes( recipes_with_sdl2_conflict, self.ctx ) self.assertNotEqual(bs.name, "sdl2") # Test using flask will default to webview: recipes_with_flask = {"python3", "flask"} bs = Bootstrap.get_bootstrap_from_recipes( recipes_with_flask, self.ctx ) self.assertEqual(bs.name, "webview") # Test using random packages will default to service_only: recipes_with_no_sdl2_or_web = {"python3", "numpy"} bs = Bootstrap.get_bootstrap_from_recipes( recipes_with_no_sdl2_or_web, self.ctx ) self.assertEqual(bs.name, "service_only") @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 method :meth:`~pythonforandroid.bootstrap.Bootstrap.prepare_dist_dir` successfully calls once the method `endure_dir` """ bs = Bootstrap().get_bootstrap("sdl2", self.ctx) bs.prepare_dist_dir() mock_ensure_dir.assert_called_once() @mock.patch("pythonforandroid.bootstrap.open", create=True) @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.bootstrap.shutil.copy") @mock.patch("pythonforandroid.bootstrap.os.makedirs") def test_bootstrap_prepare_build_dir( self, mock_os_makedirs, mock_shutil_copy, mock_chdir, mock_open ): """A test which will initialize a bootstrap and will check if the method :meth:`~pythonforandroid.bootstrap.Bootstrap.prepare_build_dir` successfully calls the methods that we need to prepare a build dir. """ # prepare bootstrap bs = Bootstrap().get_bootstrap("service_only", self.ctx) self.ctx.bootstrap = bs # test that prepare_build_dir runs (notice that we mock # any file/dir creation so we can speed up the tests) bs.prepare_build_dir() # make sure that the open command has been called only once mock_open.assert_called_once_with("project.properties", "w") # check that the other mocks we made are actually called mock_os_makedirs.assert_called() mock_shutil_copy.assert_called() mock_chdir.assert_called() @mock.patch("pythonforandroid.bootstrap.os.path.isfile") @mock.patch("pythonforandroid.bootstrap.os.path.exists") @mock.patch("pythonforandroid.bootstrap.os.unlink") @mock.patch("pythonforandroid.bootstrap.open", create=True) @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.bootstrap.listdir") def test_bootstrap_prepare_build_dir_with_java_src( self, mock_listdir, mock_chdir, mock_open, mock_os_unlink, mock_os_path_exists, mock_os_path_isfile, ): """A test which will initialize a bootstrap and will check perform another test for method :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_bootstrap_files = True mock_listdir.return_value = [ "jnius", "kivy", "Kivy-1.11.0.dev0-py3.7.egg-info", "pyjnius-1.2.1.dev0-py3.7.egg", ] # prepare bootstrap bs = Bootstrap().get_bootstrap("sdl2", self.ctx) self.ctx.bootstrap = bs # test that prepare_build_dir runs (notice that we mock # any file/dir creation so we can speed up the tests) bs.prepare_build_dir() # make sure that the open command has been called only once mock_open.assert_called_with("project.properties", "w") # check that the other mocks we made are actually called mock_chdir.assert_called() mock_os_unlink.assert_called() mock_os_path_exists.assert_called() mock_os_path_isfile.assert_called() class GenericBootstrapTest(BaseClassSetupBootstrap): """ An inherited class of `BaseClassSetupBootstrap` which will extends his functionality by adding some generic bootstrap tests, so this way we can test all our sub modules of :mod:`~pythonforandroid.bootstraps` from within this module. .. warning:: This is supposed to be used as a base class, so please, don't use this directly. """ @property def bootstrap_name(self): """Subclasses must have property 'bootstrap_name'. It should be the name of the bootstrap to test""" raise NotImplementedError("Not implemented in GenericBootstrapTest") @mock.patch("pythonforandroid.bootstraps.qt.shprint") @mock.patch("pythonforandroid.bootstraps.qt.rmdir") @mock.patch("pythonforandroid.bootstraps.qt.open", create=True) @mock.patch("pythonforandroid.bootstrap.open", create=True) @mock.patch("pythonforandroid.distribution.open", create=True) @mock.patch("pythonforandroid.bootstrap.Bootstrap.strip_libraries") @mock.patch("pythonforandroid.util.exists") @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.bootstrap.listdir") @mock.patch("pythonforandroid.bootstrap.rmdir") @mock.patch("pythonforandroid.bootstrap.shprint") def test_assemble_distribution( self, mock_shprint, mock_rmdir, mock_listdir, mock_chdir, mock_ensure_dir, mock_strip_libraries, mock_open_dist_files, mock_open_bootstrap_files, mock_open_qt_files, mock_qt_rmdir, mock_qt_shprint ): """ A test for any overwritten method of `~pythonforandroid.bootstrap.Bootstrap.assemble_distribution`. Here we mock any file/dir operation that it could slow down our tests, and there is a lot to mock, because the `assemble_distribution` method it should take care of prepare all compiled files to generate the final `apk`. The targets of this test will be: - :meth:`~pythonforandroid.bootstraps.sdl2.BootstrapSdl2 .assemble_distribution` - :meth:`~pythonforandroid.bootstraps.service_only .ServiceOnlyBootstrap.assemble_distribution` - :meth:`~pythonforandroid.bootstraps.webview.WebViewBootstrap .assemble_distribution` - :meth:`~pythonforandroid.bootstraps.empty.EmptyBootstrap. assemble_distribution` Here we will tests all those methods that are specific for each class. """ # prepare bootstrap and distribution bs = Bootstrap.get_bootstrap(self.bootstrap_name, self.ctx) self.assertNotEqual(bs.ctx, None) bs.build_dir = bs.get_build_dir() self.setUp_distribution_with_bootstrap(bs) 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)] self.ctx.bootstrap = bs bs.assemble_distribution() mock_open_dist_files.assert_called_once_with("dist_info.json", "w") # Qt bootstrap has its own assemble_distribution, others use base class if self.bootstrap_name == "qt": mock_open_bs = mock_open_qt_files else: mock_open_bs = mock_open_bootstrap_files expected_open_calls = { "sdl2": [ mock.call("local.properties", "w"), mock.call("blacklist.txt", "a"), ], "sdl3": [ mock.call("local.properties", "w"), mock.call("blacklist.txt", "a"), ], "webview": [ mock.call("local.properties", "w"), mock.call("blacklist.txt", "a"), ], "service_only": [ mock.call("local.properties", "w"), mock.call("blacklist.txt", "a"), ], "qt": [mock.call("local.properties", "w")] } # test that the expected calls has been called for expected_call in expected_open_calls[self.bootstrap_name]: self.assertIn(expected_call, mock_open_bs.call_args_list) # test that the write function has been called with the expected args self.assertIn( mock.call().__enter__().write("sdk.dir=/opt/android/android-sdk"), mock_open_bs.mock_calls, ) if self.bootstrap_name in ["sdl2", "sdl3", "webview", "service_only"]: self.assertIn( mock.call() .__enter__() .write("\nsqlite3/*\nlib-dynload/_sqlite3.so\n"), mock_open_bs.mock_calls, ) # check that the other mocks we made are actually called mock_shprint.assert_called() mock_chdir.assert_called() mock_listdir.assert_called() mock_strip_libraries.assert_called() expected__python_bundle = os.path.join( self.ctx.dist_dir, self.ctx.bootstrap.distribution.name, f"_python_bundle__{self.TEST_ARCH}", "_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") @mock.patch("pythonforandroid.bootstrap.ensure_dir") @mock.patch("pythonforandroid.build.ensure_dir") def test_distribute_methods( self, mock_build_dir, mock_bs_dir, mock_glob, mock_shprint ): # prepare arch, bootstrap and distribution arch = ArchARMv7_a(self.ctx) bs = Bootstrap().get_bootstrap(self.bootstrap_name, self.ctx) self.setUp_distribution_with_bootstrap(bs) # a convenient method to reset mocks in one shot def reset_mocks(): mock_glob.reset_mock() mock_shprint.reset_mock() mock_build_dir.reset_mock() mock_bs_dir.reset_mock() # test distribute_libs mock_glob.return_value = [ "/fake_dir/libsqlite3.so", "/fake_dir/libpng16.so", ] bs.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)]) 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), 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() # test distribute_javaclasses mock_glob.return_value = ["/fakedir/java_file.java"] bs.distribute_javaclasses(self.ctx.javaclass_dir) mock_glob.assert_called_once_with(self.ctx.javaclass_dir) mock_build_dir.assert_called_with(self.ctx.javaclass_dir) mock_bs_dir.assert_called_once_with("src") self.assertEqual( mock_shprint.call_args, mock.call(sh.cp, "-a", "/fakedir/java_file.java", "src"), ) reset_mocks() # test distribute_aars mock_glob.return_value = ["/fakedir/file.aar"] bs.distribute_aars(arch) mock_build_dir.assert_called_with(self.ctx.aars_dir) # We expect three calls to shprint: unzip, cp, cp zip_call, kw = mock_shprint.call_args_list[0] self.assertEqual(zip_call[0], sh.unzip) self.assertEqual(zip_call[2], "/fakedir/file.aar") cp_java_call, kw = mock_shprint.call_args_list[1] self.assertEqual(cp_java_call[0], sh.cp) self.assertTrue(cp_java_call[2].endswith("classes.jar")) self.assertEqual(cp_java_call[3], "libs/file.jar") cp_libs_call, kw = mock_shprint.call_args_list[2] self.assertEqual(cp_libs_call[0], sh.cp) self.assertEqual(cp_libs_call[2], "/fakedir/file.aar") self.assertEqual(cp_libs_call[3], libs_dir) mock_bs_dir.assert_has_calls([mock.call("libs"), mock.call(libs_dir)]) mock_glob.assert_called() @mock.patch("pythonforandroid.bootstrap.shprint") @mock.patch("pythonforandroid.bootstrap.sh.Command") @mock.patch("pythonforandroid.build.ensure_dir") @mock.patch("shutil.which") def test_bootstrap_strip( self, mock_shutil_which, mock_ensure_dir, mock_sh_command, mock_sh_print, ): mock_shutil_which.return_value = os.path.join( self.ctx._ndk_dir, f"toolchains/llvm/prebuilt/{self.ctx.ndk.host_tag}/bin/clang", ) # prepare arch, bootstrap, distribution and PythonRecipe arch = ArchARMv7_a(self.ctx) bs = Bootstrap().get_bootstrap(self.bootstrap_name, self.ctx) self.setUp_distribution_with_bootstrap(bs) self.ctx.python_recipe = Recipe.get_recipe("python3", self.ctx) # test that strip_libraries runs with a fake distribution bs.strip_libraries(arch) self.assertEqual( mock_shutil_which.call_args[0][0], mock_shutil_which.return_value, ) mock_sh_command.assert_called_once_with( os.path.join( self.ctx._ndk_dir, f"toolchains/llvm/prebuilt/{self.ctx.ndk.host_tag}/bin", "llvm-strip", ) ) # check that the other mocks we made are actually called mock_ensure_dir.assert_called() mock_sh_print.assert_called() @mock.patch("pythonforandroid.bootstrap.listdir") @mock.patch("pythonforandroid.bootstrap.rmdir") @mock.patch("pythonforandroid.bootstrap.move") @mock.patch("pythonforandroid.bootstrap.isdir") def test_bootstrap_fry_eggs( self, mock_isdir, mock_move, mock_rmdir, mock_listdir ): mock_listdir.return_value = [ "jnius", "kivy", "Kivy-1.11.0.dev0-py3.7.egg-info", "pyjnius-1.2.1.dev0-py3.7.egg", ] # prepare bootstrap, context and distribution bs = Bootstrap().get_bootstrap(self.bootstrap_name, self.ctx) self.setUp_distribution_with_bootstrap(bs) # test that fry_eggs runs with a fake distribution site_packages = os.path.join( bs.dist_dir, "_python_bundle", "_python_bundle" ) bs.fry_eggs(site_packages) mock_listdir.assert_has_calls( [ mock.call(site_packages), mock.call( os.path.join(site_packages, "pyjnius-1.2.1.dev0-py3.7.egg") ), ] ) self.assertEqual( mock_rmdir.call_args[0][0], "pyjnius-1.2.1.dev0-py3.7.egg" ) # check that the other mocks we made are actually called mock_isdir.assert_called() mock_move.assert_called() class TestBootstrapSdl2(GenericBootstrapTest, unittest.TestCase): """ An inherited class of `GenericBootstrapTest` and `unittest.TestCase` which will be used to perform tests for :class:`~pythonforandroid.bootstraps.sdl2.BootstrapSdl2`. """ @property def bootstrap_name(self): return "sdl2" class TestBootstrapSdl3(GenericBootstrapTest, unittest.TestCase): """ An inherited class of `GenericBootstrapTest` and `unittest.TestCase` which will be used to perform tests for :class:`~pythonforandroid.bootstraps.sdl3.BootstrapSdl3`. """ @property def bootstrap_name(self): return "sdl3" class TestBootstrapServiceOnly(GenericBootstrapTest, unittest.TestCase): """ An inherited class of `GenericBootstrapTest` and `unittest.TestCase` which will be used to perform tests for :class:`~pythonforandroid.bootstraps.service_only.ServiceOnlyBootstrap`. """ @property def bootstrap_name(self): return "service_only" class TestBootstrapWebview(GenericBootstrapTest, unittest.TestCase): """ An inherited class of `GenericBootstrapTest` and `unittest.TestCase` which will be used to perform tests for :class:`~pythonforandroid.bootstraps.webview.WebViewBootstrap`. """ @property def bootstrap_name(self): return "webview" class TestBootstrapEmpty(GenericBootstrapTest, unittest.TestCase): """ An inherited class of `GenericBootstrapTest` and `unittest.TestCase` which will be used to perform tests for :class:`~pythonforandroid.bootstraps.empty.EmptyBootstrap`. .. note:: here will test most of the base class methods, because we only overwrite :meth:`~pythonforandroid.bootstraps.empty. EmptyBootstrap.assemble_distribution` """ @property def bootstrap_name(self): return "empty" def test_assemble_distribution(self, *args): # prepare bootstrap bs = Bootstrap().get_bootstrap(self.bootstrap_name, self.ctx) self.ctx.bootstrap = bs # test dist_dir error with self.assertRaises(SystemExit) as e: bs.assemble_distribution() self.assertEqual(e.exception.args[0], 1) class TestBootstrapQt(GenericBootstrapTest, unittest.TestCase): """ An inherited class of `GenericBootstrapTest` and `unittest.TestCase` which will be used to perform tests for :class:`~pythonforandroid.bootstraps.qt.BootstrapQt`. """ @property def bootstrap_name(self): return "qt" ================================================ FILE: tests/test_bootstrap_build.py ================================================ import unittest from unittest import mock import pytest import os from pythonforandroid.util import load_source class TestBootstrapBuild(unittest.TestCase): def setUp(self): os.environ["P4A_BUILD_IS_RUNNING_UNITTESTS"] = "1" build_src = os.path.join( os.path.dirname(os.path.abspath(__file__)), "../pythonforandroid/bootstraps/common/build/build.py", ) self.buildpy = load_source("buildpy", build_src) self.buildpy.get_bootstrap_name = mock.Mock(return_value="sdl2") self.ap = self.buildpy.create_argument_parser() self.common_args = [ "--package", "org.test.app", "--name", "TestApp", "--version", "0.1", ] class TestParsePermissions(TestBootstrapBuild): def test_parse_permissions_with_migrations(self): # Test that permissions declared in the old format are migrated to the # new format. # (Users can new declare permissions in both formats, even a mix) self.ap = self.buildpy.create_argument_parser() args = [ *self.common_args, "--permission", "INTERNET", "--permission", "com.android.voicemail.permission.ADD_VOICEMAIL", "--permission", "(name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=18)", "--permission", "(name=android.permission.BLUETOOTH_SCAN;usesPermissionFlags=neverForLocation)", ] args = self.ap.parse_args(args) parsed_permissions = self.buildpy.parse_permissions(args.permissions) assert parsed_permissions == [ dict(name="android.permission.INTERNET"), dict(name="com.android.voicemail.permission.ADD_VOICEMAIL"), dict(name="android.permission.WRITE_EXTERNAL_STORAGE", maxSdkVersion="18"), dict( name="android.permission.BLUETOOTH_SCAN", usesPermissionFlags="neverForLocation", ), ] def test_parse_permissions_invalid_property(self): self.ap = self.buildpy.create_argument_parser() args = [ *self.common_args, "--permission", "(name=android.permission.BLUETOOTH_SCAN;propertyThatFails=neverForLocation)", ] args = self.ap.parse_args(args) with pytest.raises( ValueError, match="Property 'propertyThatFails' is not supported." ): self.buildpy.parse_permissions(args.permissions) class TestOrientationArg(TestBootstrapBuild): def test_no_orientation_args(self): args = self.common_args args = self.ap.parse_args(args) assert ( self.buildpy.get_manifest_orientation( args.orientation, args.manifest_orientation ) == "unspecified" ) assert self.buildpy.get_sdl_orientation_hint(args.orientation) == "" def test_manifest_orientation_present(self): args = [ *self.common_args, "--orientation", "landscape", "--orientation", "portrait", "--manifest-orientation", "fullSensor", ] args = self.ap.parse_args(args) assert ( self.buildpy.get_manifest_orientation( args.orientation, manifest_orientation=args.manifest_orientation ) == "fullSensor" ) def test_manifest_orientation_supported(self): args = [*self.common_args, "--orientation", "landscape"] args = self.ap.parse_args(args) assert ( self.buildpy.get_manifest_orientation( args.orientation, manifest_orientation=args.manifest_orientation ) == "landscape" ) def test_android_manifest_multiple_orientation_supported(self): args = [ *self.common_args, "--orientation", "landscape", "--orientation", "portrait", ] args = self.ap.parse_args(args) assert ( self.buildpy.get_manifest_orientation( args.orientation, manifest_orientation=args.manifest_orientation ) == "unspecified" ) def test_sdl_orientation_hint_single(self): args = [*self.common_args, "--orientation", "landscape"] args = self.ap.parse_args(args) assert ( self.buildpy.get_sdl_orientation_hint(args.orientation) == "LandscapeLeft" ) def test_sdl_orientation_hint_multiple(self): args = [ *self.common_args, "--orientation", "landscape", "--orientation", "portrait", ] args = self.ap.parse_args(args) sdl_orientation_hint = self.buildpy.get_sdl_orientation_hint( args.orientation ).split(" ") assert "LandscapeLeft" in sdl_orientation_hint assert "Portrait" in sdl_orientation_hint ================================================ FILE: tests/test_build.py ================================================ import os import unittest from unittest import mock import jinja2 from pythonforandroid.build import ( Context, RECOMMENDED_TARGET_API, run_pymodules_install, process_python_modules ) from pythonforandroid.archs import ArchARMv7_a, ArchAarch_64 class TestBuildBasic(unittest.TestCase): def test_run_pymodules_install_optional_project_dir(self): """ Makes sure the `run_pymodules_install()` doesn't crash when the `project_dir` optional parameter is None, refs #1898 """ ctx = mock.Mock(recipe_build_order=[]) ctx.archs = [ArchARMv7_a(ctx), ArchAarch_64(ctx)] modules = [] project_dir = None with mock.patch('pythonforandroid.build.info') as m_info: assert run_pymodules_install(ctx, ctx.archs[0], 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_python_module_parser(self): ctx = mock.Mock(recipe_build_order=[]) ctx.archs = [ArchARMv7_a(ctx), ArchAarch_64(ctx)] # should not alter original module name (like with adding version number) assert "kivy_garden.frostedglass" in process_python_modules(ctx, ["kivy_garden.frostedglass"]) # should skip urls and other unsupported format modules = ["https://example.com/some.zip", "git+https://github.com/kivy/python-for-android@develop"] result = process_python_modules(ctx, modules) assert modules == result def test_strip_if_with_debug_symbols(self): ctx = mock.Mock(recipe_build_order=[]) ctx.python_recipe.major_minor_version_string = "3.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 `with_debug_symbols` is true: ctx.with_debug_symbols = True assert run_pymodules_install(ctx, ctx.archs[0], modules, project_dir) is None assert m_CythonRecipe().strip_object_files.called is False # Make sure strip object files IS called when # `with_debug_symbols` is false: ctx.with_debug_symbols = False assert run_pymodules_install(ctx, ctx.archs[0], modules, project_dir) is None assert m_CythonRecipe().strip_object_files.called is True class TestTemplates(unittest.TestCase): def test_android_manifest_xml(self): args = mock.Mock() args.min_sdk_version = 12 args.build_mode = 'debug' args.native_services = ['abcd', ] args.permissions = [ dict(name="android.permission.INTERNET"), dict(name="android.permission.WRITE_EXTERNAL_STORAGE", maxSdkVersion=18), dict(name="android.permission.BLUETOOTH_SCAN", usesPermissionFlags="neverForLocation")] args.add_activity = [] args.android_used_libs = [] args.meta_data = [] args.extra_manifest_xml = '' args.extra_manifest_application_arguments = 'android:someParameter="true" android:anotherParameter="false"' render_args = { "args": args, "service": False, "service_names": [], "android_api": 1234, "debug": "debug" in args.build_mode, "native_services": args.native_services } environment = jinja2.Environment( loader=jinja2.FileSystemLoader('pythonforandroid/bootstraps/_sdl_common/build/templates/') ) template = environment.get_template('AndroidManifest.tmpl.xml') xml = template.render(**render_args) assert xml.count('android:minSdkVersion="12"') == 1 assert xml.count('android:anotherParameter="false"') == 1 assert xml.count('android:someParameter="true"') == 1 assert xml.count('') == 1 assert xml.count('android:process=":service_') == 0 assert xml.count('targetSdkVersion="1234"') == 1 assert xml.count('android:debuggable="true"') == 1 assert xml.count('') == 1 assert xml.count('') == 1 assert xml.count('') == 1 assert xml.count('') == 1 # TODO: potentially some other checks to be added here to cover other "logic" (flags and loops) in the template class TestContext(unittest.TestCase): @mock.patch.dict('pythonforandroid.build.Context.env') @mock.patch('pythonforandroid.build.get_available_apis') @mock.patch('pythonforandroid.build.ensure_dir') def test_sdk_ndk_paths( self, mock_ensure_dir, mock_get_available_apis, ): mock_get_available_apis.return_value = [RECOMMENDED_TARGET_API] context = Context() context.setup_dirs(os.getcwd()) context.prepare_build_environment( user_sdk_dir='sdk', user_ndk_dir='ndk', user_android_api=None, user_ndk_api=None, ) # The context was supplied with relative SDK and NDK dirs. Check # that it resolved them to absolute paths. real_sdk_dir = os.path.join(os.getcwd(), 'sdk') real_ndk_dir = os.path.join(os.getcwd(), 'ndk') assert context.sdk_dir == real_sdk_dir assert context.ndk_dir == real_ndk_dir context_paths = context.env['PATH'].split(':') assert context_paths[0:3] == [ f'{real_ndk_dir}/toolchains/llvm/prebuilt/{context.ndk.host_tag}/bin', real_ndk_dir, f'{real_sdk_dir}/tools' ] ================================================ FILE: tests/test_checkdependencies.py ================================================ import sys from unittest import mock from pythonforandroid import checkdependencies class TestCheckPythonDependencies: """Test check_python_dependencies function.""" @mock.patch('pythonforandroid.checkdependencies.import_module') def test_all_modules_present(self, mock_import): """Test that check_python_dependencies completes when all modules are present.""" # Mock all required modules mock_colorama = mock.Mock() mock_colorama.__version__ = '0.4.0' mock_sh = mock.Mock() mock_sh.__version__ = '1.12' mock_appdirs = mock.Mock() mock_jinja2 = mock.Mock() def import_side_effect(name): if name == 'colorama': return mock_colorama elif name == 'sh': return mock_sh elif name == 'appdirs': return mock_appdirs elif name == 'jinja2': return mock_jinja2 raise ImportError(f"No module named '{name}'") mock_import.side_effect = import_side_effect with mock.patch.object(sys, 'modules', { 'colorama': mock_colorama, 'sh': mock_sh, 'appdirs': mock_appdirs, 'jinja2': mock_jinja2 }): checkdependencies.check_python_dependencies() @mock.patch('builtins.exit') @mock.patch('builtins.print') @mock.patch('pythonforandroid.checkdependencies.import_module') def test_missing_module_without_version(self, mock_import, mock_print, mock_exit): """Test error message when module without version requirement is missing.""" modules_dict = {} def import_side_effect(name): if name == 'appdirs': raise ImportError(f"No module named '{name}'") mock_mod = mock.Mock() mock_mod.__version__ = '1.0' modules_dict[name] = mock_mod return mock_mod mock_import.side_effect = import_side_effect with mock.patch.object(sys, 'modules', modules_dict): checkdependencies.check_python_dependencies() # Verify error message was printed error_calls = [str(call) for call in mock_print.call_args_list] assert any('appdirs' in call and 'ERROR' in call for call in error_calls) mock_exit.assert_called_once_with(1) @mock.patch('builtins.exit') @mock.patch('builtins.print') @mock.patch('pythonforandroid.checkdependencies.import_module') def test_missing_module_with_version(self, mock_import, mock_print, mock_exit): """Test error message when module with version requirement is missing.""" modules_dict = {} def import_side_effect(name): if name == 'colorama': raise ImportError(f"No module named '{name}'") mock_mod = mock.Mock() mock_mod.__version__ = '1.0' modules_dict[name] = mock_mod return mock_mod mock_import.side_effect = import_side_effect with mock.patch.object(sys, 'modules', modules_dict): checkdependencies.check_python_dependencies() # Verify error message includes version requirement error_calls = [str(call) for call in mock_print.call_args_list] assert any('colorama' in call and '0.3.3' in call for call in error_calls) mock_exit.assert_called_once_with(1) @mock.patch('builtins.exit') @mock.patch('builtins.print') @mock.patch('pythonforandroid.checkdependencies.import_module') def test_module_version_too_old(self, mock_import, mock_print, mock_exit): """Test error when module version is too old.""" mock_colorama = mock.Mock() mock_colorama.__version__ = '0.2.0' # Too old, needs 0.3.3 modules_dict = {'colorama': mock_colorama} def import_side_effect(name): if name == 'colorama': return mock_colorama mock_mod = mock.Mock() mock_mod.__version__ = '1.0' modules_dict[name] = mock_mod return mock_mod mock_import.side_effect = import_side_effect with mock.patch.object(sys, 'modules', modules_dict): checkdependencies.check_python_dependencies() # Verify error message about version error_calls = [str(call) for call in mock_print.call_args_list] assert any('version' in call.lower() and 'colorama' in call for call in error_calls) mock_exit.assert_called_once_with(1) @mock.patch('pythonforandroid.checkdependencies.import_module') def test_module_version_acceptable(self, mock_import): """Test that acceptable versions pass.""" mock_colorama = mock.Mock() mock_colorama.__version__ = '0.4.0' # Newer than 0.3.3 mock_sh = mock.Mock() mock_sh.__version__ = '1.12' # Newer than 1.10 def import_side_effect(name): if name == 'colorama': return mock_colorama elif name == 'sh': return mock_sh mock_mod = mock.Mock() return mock_mod mock_import.side_effect = import_side_effect with mock.patch.object(sys, 'modules', { 'colorama': mock_colorama, 'sh': mock_sh }): # Should complete without error checkdependencies.check_python_dependencies() @mock.patch('pythonforandroid.checkdependencies.import_module') def test_module_without_version_attribute(self, mock_import): """Test handling of modules that don't have __version__.""" mock_colorama = mock.Mock(spec=[]) # No __version__ attribute modules_dict = {'colorama': mock_colorama} def import_side_effect(name): if name == 'colorama': return mock_colorama mock_mod = mock.Mock() modules_dict[name] = mock_mod return mock_mod mock_import.side_effect = import_side_effect with mock.patch.object(sys, 'modules', modules_dict): # Should complete without error (version check is skipped) checkdependencies.check_python_dependencies() class TestCheck: """Test the main check() function.""" @mock.patch('pythonforandroid.checkdependencies.check_python_dependencies') @mock.patch('pythonforandroid.checkdependencies.check_and_install_default_prerequisites') def test_check_with_skip_prerequisites(self, mock_prereqs, mock_python_deps): """Test check() skips prerequisites when SKIP_PREREQUISITES_CHECK=1.""" with mock.patch.dict('os.environ', {'SKIP_PREREQUISITES_CHECK': '1'}): checkdependencies.check() mock_prereqs.assert_not_called() mock_python_deps.assert_called_once() @mock.patch('pythonforandroid.checkdependencies.check_python_dependencies') @mock.patch('pythonforandroid.checkdependencies.check_and_install_default_prerequisites') def test_check_without_skip(self, mock_prereqs, mock_python_deps): """Test check() runs prerequisites when SKIP_PREREQUISITES_CHECK is not set.""" with mock.patch.dict('os.environ', {}, clear=True): checkdependencies.check() mock_prereqs.assert_called_once() mock_python_deps.assert_called_once() @mock.patch('pythonforandroid.checkdependencies.check_python_dependencies') @mock.patch('pythonforandroid.checkdependencies.check_and_install_default_prerequisites') def test_check_with_skip_set_to_zero(self, mock_prereqs, mock_python_deps): """Test check() runs prerequisites when SKIP_PREREQUISITES_CHECK=0.""" with mock.patch.dict('os.environ', {'SKIP_PREREQUISITES_CHECK': '0'}): checkdependencies.check() mock_prereqs.assert_called_once() mock_python_deps.assert_called_once() ================================================ FILE: tests/test_distribution.py ================================================ import os import json import unittest from unittest import mock from pythonforandroid.bootstrap import Bootstrap from pythonforandroid.distribution import Distribution from pythonforandroid.recipe import Recipe from pythonforandroid.util import BuildInterruptingException from pythonforandroid.build import Context dist_info_data = { "dist_name": "sdl2_dist", "bootstrap": "sdl2", "archs": ["armeabi", "armeabi-v7a", "x86", "x86_64", "arm64-v8a"], "ndk_api": 21, "use_setup_py": False, "recipes": ["hostpython3", "python3", "sdl2", "kivy", "requests"], "hostpython": "/some/fake/hostpython3", "python_version": "3.7", } class TestDistribution(unittest.TestCase): """ An inherited class of `unittest.TestCase`to test the module :mod:`~pythonforandroid.distribution`. """ TEST_ARCH = 'armeabi-v7a' def setUp(self): """Configure a :class:`~pythonforandroid.build.Context` so we can perform our unittests""" self.ctx = Context() self.ctx.ndk_api = 21 self.ctx.android_api = 27 self.ctx._sdk_dir = "/opt/android/android-sdk" self.ctx._ndk_dir = "/opt/android/android-ndk" self.ctx.setup_dirs(os.getcwd()) self.ctx.recipe_build_order = [ "hostpython3", "python3", "sdl2", "kivy", ] def setUp_distribution_with_bootstrap(self, bs, **kwargs): """Extend the setUp by configuring a distribution, because some test needs a distribution to be set to be properly tested""" self.ctx.bootstrap = bs self.ctx.bootstrap.distribution = Distribution.get_distribution( self.ctx, name=kwargs.pop("name", "test_prj"), recipes=kwargs.pop("recipes", ["python3", "kivy"]), archs=[self.TEST_ARCH], **kwargs ) def tearDown(self): """Here we make sure that we reset a possible bootstrap created in `setUp_distribution_with_bootstrap`""" self.ctx.bootstrap = None def test_properties(self): """Test that some attributes has the expected result (for now, we check that `__repr__` and `__str__` return the proper values""" self.setUp_distribution_with_bootstrap( Bootstrap().get_bootstrap("sdl2", self.ctx) ) distribution = self.ctx.bootstrap.distribution self.assertEqual(self.ctx, distribution.ctx) expected_repr = ( "" ) self.assertEqual(distribution.__str__(), expected_repr) self.assertEqual(distribution.__repr__(), expected_repr) @mock.patch("pythonforandroid.distribution.exists") def test_folder_exist(self, mock_exists): """Test that method :meth:`~pythonforandroid.distribution.Distribution.folder_exist` is called once with the proper arguments.""" mock_exists.return_value = False self.setUp_distribution_with_bootstrap( Bootstrap.get_bootstrap("sdl2", self.ctx) ) self.ctx.bootstrap.distribution.folder_exists() mock_exists.assert_called_with( self.ctx.bootstrap.distribution.dist_dir ) @mock.patch("pythonforandroid.distribution.rmdir") def test_delete(self, mock_rmdir): """Test that method :meth:`~pythonforandroid.distribution.Distribution.delete` is called once with the proper arguments.""" self.setUp_distribution_with_bootstrap( Bootstrap().get_bootstrap("sdl2", self.ctx) ) self.ctx.bootstrap.distribution.delete() mock_rmdir.assert_called_once_with( self.ctx.bootstrap.distribution.dist_dir ) @mock.patch("pythonforandroid.distribution.exists") def test_get_distribution_no_name(self, mock_exists): """Test that method :meth:`~pythonforandroid.distribution.Distribution.get_distribution` returns the proper result which should `unnamed_dist_1`.""" mock_exists.return_value = False self.ctx.bootstrap = Bootstrap().get_bootstrap("sdl2", self.ctx) dist = Distribution.get_distribution(self.ctx, archs=[self.TEST_ARCH]) self.assertEqual(dist.name, "unnamed_dist_1") @mock.patch("pythonforandroid.util.chdir") @mock.patch("pythonforandroid.distribution.open", create=True) def test_save_info(self, mock_open_dist_info, mock_chdir): """Test that method :meth:`~pythonforandroid.distribution.Distribution.save_info` is called once with the proper arguments.""" self.setUp_distribution_with_bootstrap( Bootstrap().get_bootstrap("sdl2", self.ctx) ) self.ctx.hostpython = "/some/fake/hostpython3" self.ctx.python_recipe = Recipe.get_recipe("python3", self.ctx) self.ctx.python_modules = ["requests"] mock_open_dist_info.side_effect = [ mock.mock_open(read_data=json.dumps(dist_info_data)).return_value ] self.ctx.bootstrap.distribution.save_info("/fake_dir") mock_open_dist_info.assert_called_once_with("dist_info.json", "w") mock_open_dist_info.reset_mock() @mock.patch("pythonforandroid.distribution.open", create=True) @mock.patch("pythonforandroid.distribution.exists") @mock.patch("pythonforandroid.distribution.glob.glob") def test_get_distributions( self, mock_glob, mock_exists, mock_open_dist_info ): """Test that method :meth:`~pythonforandroid.distribution.Distribution.get_distributions` returns some expected values: - A list of instances of class `~pythonforandroid.distribution.Distribution - That one of the distributions returned in the result has the proper values (`name`, `ndk_api` and `recipes`) """ self.setUp_distribution_with_bootstrap( Bootstrap().get_bootstrap("sdl2", self.ctx) ) mock_glob.return_value = ["sdl2-python3"] mock_open_dist_info.side_effect = [ mock.mock_open(read_data=json.dumps(dist_info_data)).return_value ] dists = self.ctx.bootstrap.distribution.get_distributions(self.ctx) self.assertIsInstance(dists, list) self.assertEqual(len(dists), 1) self.assertIsInstance(dists[0], Distribution) self.assertEqual(dists[0].name, "sdl2_dist") self.assertEqual(dists[0].dist_dir, "sdl2-python3") self.assertEqual(dists[0].ndk_api, 21) self.assertEqual( dists[0].recipes, ["hostpython3", "python3", "sdl2", "kivy", "requests"], ) mock_open_dist_info.assert_called_with("sdl2-python3/dist_info.json") mock_open_dist_info.reset_mock() @mock.patch("pythonforandroid.distribution.open", create=True) @mock.patch("pythonforandroid.distribution.exists") @mock.patch("pythonforandroid.distribution.glob.glob") def test_get_distributions_error_ndk_api( self, mock_glob, mock_exists, mock_open_dist_info ): """Test method :meth:`~pythonforandroid.distribution.Distribution.get_distributions` in case that `ndk_api` is not set..which should return a `None`. """ dist_info_data_no_ndk_api = dist_info_data.copy() dist_info_data_no_ndk_api.pop("ndk_api") self.setUp_distribution_with_bootstrap( Bootstrap().get_bootstrap("sdl2", self.ctx) ) mock_glob.return_value = ["sdl2-python3"] mock_open_dist_info.side_effect = [ mock.mock_open( read_data=json.dumps(dist_info_data_no_ndk_api) ).return_value ] dists = self.ctx.bootstrap.distribution.get_distributions(self.ctx) self.assertEqual(dists[0].ndk_api, None) mock_open_dist_info.assert_called_with("sdl2-python3/dist_info.json") mock_open_dist_info.reset_mock() @mock.patch("pythonforandroid.distribution.Distribution.get_distributions") @mock.patch("pythonforandroid.distribution.exists") @mock.patch("pythonforandroid.distribution.glob.glob") def test_get_distributions_error_ndk_api_mismatch( self, mock_glob, mock_exists, mock_get_dists ): """Test that method :meth:`~pythonforandroid.distribution.Distribution.get_distribution` raises an error in case that we have some distribution already build, with a given `name` and `ndk_api`, and we try to get another distribution with the same `name` but different `ndk_api`. """ expected_dist = Distribution.get_distribution( self.ctx, name="test_prj", recipes=["python3", "kivy"], archs=[self.TEST_ARCH], ) mock_get_dists.return_value = [expected_dist] mock_glob.return_value = ["sdl2-python3"] with self.assertRaises(BuildInterruptingException) as e: self.setUp_distribution_with_bootstrap( Bootstrap().get_bootstrap("sdl2", self.ctx), allow_replace_dist=False, ndk_api=22, ) self.assertEqual( e.exception.args[0], "Asked for dist with name test_prj with recipes (python3, kivy)" " and NDK API 22, but a dist with this name already exists and has" " either incompatible recipes (python3, kivy) or NDK API 21", ) def test_get_distributions_error_extra_dist_dirs(self): """Test that method :meth:`~pythonforandroid.distribution.Distribution.get_distributions` raises an exception of :class:`~pythonforandroid.util.BuildInterruptingException` in case that we supply the kwargs `extra_dist_dirs`. """ self.setUp_distribution_with_bootstrap( Bootstrap().get_bootstrap("sdl2", self.ctx) ) with self.assertRaises(BuildInterruptingException) as e: self.ctx.bootstrap.distribution.get_distributions( self.ctx, extra_dist_dirs=["/fake/extra/dist_dirs"] ) self.assertEqual( e.exception.args[0], "extra_dist_dirs argument to get" "_distributions is not yet implemented", ) @mock.patch("pythonforandroid.distribution.Distribution.get_distributions") def test_get_distributions_possible_dists(self, mock_get_dists): """Test that method :meth:`~pythonforandroid.distribution.Distribution.get_distributions` returns the proper `:class:`~pythonforandroid.distribution.Distribution` in case that we already have it build and we request the same `:class:`~pythonforandroid.distribution.Distribution`. """ expected_dist = Distribution.get_distribution( self.ctx, name="test_prj", recipes=["python3", "kivy"], archs=[self.TEST_ARCH], ) mock_get_dists.return_value = [expected_dist] self.setUp_distribution_with_bootstrap( Bootstrap().get_bootstrap("sdl2", self.ctx), name="test_prj" ) dists = self.ctx.bootstrap.distribution.get_distributions(self.ctx) self.assertEqual(dists[0], expected_dist) ================================================ FILE: tests/test_entrypoints.py ================================================ from unittest import mock from pythonforandroid.entrypoints import main from pythonforandroid.util import BuildInterruptingException class TestMain: """Test the main entry point function.""" @mock.patch('pythonforandroid.toolchain.ToolchainCL') @mock.patch('pythonforandroid.entrypoints.check_python_version') def test_main_success(self, mock_check_version, mock_toolchain): """Test main() executes successfully with valid Python version.""" main() mock_check_version.assert_called_once() mock_toolchain.assert_called_once() @mock.patch('pythonforandroid.entrypoints.handle_build_exception') @mock.patch('pythonforandroid.toolchain.ToolchainCL') @mock.patch('pythonforandroid.entrypoints.check_python_version') def test_main_build_interrupting_exception( self, mock_check_version, mock_toolchain, mock_handler ): """Test main() catches BuildInterruptingException and handles it.""" exc = BuildInterruptingException("Build failed", "Try reinstalling") mock_toolchain.side_effect = exc main() mock_check_version.assert_called_once() mock_toolchain.assert_called_once() mock_handler.assert_called_once_with(exc) @mock.patch('pythonforandroid.toolchain.ToolchainCL') @mock.patch('pythonforandroid.entrypoints.check_python_version') def test_main_other_exception_propagates( self, mock_check_version, mock_toolchain ): """Test main() allows non-BuildInterruptingException to propagate.""" mock_toolchain.side_effect = RuntimeError("Unexpected error") try: main() assert False, "Expected RuntimeError to be raised" except RuntimeError as e: assert str(e) == "Unexpected error" mock_check_version.assert_called_once() mock_toolchain.assert_called_once() @mock.patch('pythonforandroid.entrypoints.check_python_version') def test_main_python_version_check_fails(self, mock_check_version): """Test main() allows Python version check failure to propagate.""" mock_check_version.side_effect = SystemExit(1) try: main() assert False, "Expected SystemExit to be raised" except SystemExit as e: assert e.code == 1 mock_check_version.assert_called_once() ================================================ FILE: tests/test_graph.py ================================================ from pythonforandroid.build import Context from pythonforandroid.graph import ( fix_deplist, get_dependency_tuple_list_for_recipe, get_recipe_order_and_bootstrap, obvious_conflict_checker, ) from pythonforandroid.bootstrap import Bootstrap from pythonforandroid.recipe import Recipe from pythonforandroid.util import BuildInterruptingException from itertools import product from unittest import mock import pytest ctx = Context() name_sets = [['python3'], ['kivy']] bootstraps = [None, Bootstrap.get_bootstrap('sdl2', ctx)] valid_combinations = list(product(name_sets, bootstraps)) valid_combinations.extend( [(['python3'], Bootstrap.get_bootstrap('sdl2', ctx)), (['kivy', 'python3'], Bootstrap.get_bootstrap('sdl2', ctx)), (['flask'], Bootstrap.get_bootstrap('webview', ctx)), (['pysdl2'], None), # auto-detect bootstrap! important corner case ] ) invalid_combinations = [ [['pil', 'pillow'], None], [['pysdl2', 'genericndkbuild'], None], ] invalid_combinations_simple = list(invalid_combinations) # NOTE !! keep in mind when setting invalid_combinations_simple: # # This is used to test obvious_conflict_checker(), which only # catches CERTAIN conflicts: # # This must be a list of conflicts where the conflict is ONLY in # non-tuple/non-ambiguous dependencies, e.g.: # # dependencies_1st = ["python2", "pillow"] # dependencies_2nd = ["python3", "pillow"] # # This however won't work: # # dependencies_1st = [("python2", "python3"), "pillow"] # # (This is simply because the conflict checker doesn't resolve this to # keep the code ismple enough) def get_fake_recipe(name, depends=None, conflicts=None): recipe = mock.Mock() recipe.name = name recipe.get_opt_depends_in_list = lambda: [] recipe.get_dir_name = lambda: name recipe.depends = list(depends or []) recipe.conflicts = list(conflicts or []) return recipe def register_fake_recipes_for_test(monkeypatch, recipe_list): _orig_get_recipe = Recipe.get_recipe def mock_get_recipe(name, ctx): for recipe in recipe_list: if recipe.name == name: return recipe return _orig_get_recipe(name, ctx) # Note: staticmethod() needed for python ONLY, don't ask me why: monkeypatch.setattr(Recipe, 'get_recipe', staticmethod(mock_get_recipe)) @pytest.mark.parametrize('names,bootstrap', valid_combinations) def test_valid_recipe_order_and_bootstrap(names, bootstrap): get_recipe_order_and_bootstrap(ctx, names, bootstrap) @pytest.mark.parametrize('names,bootstrap', invalid_combinations) def test_invalid_recipe_order_and_bootstrap(names, bootstrap): with pytest.raises(BuildInterruptingException) as e_info: get_recipe_order_and_bootstrap(ctx, names, bootstrap) assert "conflict" in e_info.value.message.lower() def test_blacklist(): # First, get order without blacklist: build_order, python_modules, bs = get_recipe_order_and_bootstrap( ctx, ["python3", "kivy"], None ) # Now, obtain again with blacklist: build_order_2, python_modules_2, bs_2 = get_recipe_order_and_bootstrap( ctx, ["python3", "kivy"], None, blacklist=["libffi"] ) assert "libffi" not in build_order_2 assert set(build_order_2).union({"libffi"}) == set(build_order) # Check that we get a conflict when using webview and kivy combined: wbootstrap = Bootstrap.get_bootstrap('webview', ctx) with pytest.raises(BuildInterruptingException) as e_info: get_recipe_order_and_bootstrap(ctx, ["flask", "kivy"], wbootstrap) assert "conflict" in e_info.value.message.lower() # We should no longer get a conflict blacklisting sdl2 and sdl3 get_recipe_order_and_bootstrap( ctx, ["flask", "kivy"], wbootstrap, blacklist=["sdl2", "sdl3"] ) def test_get_dependency_tuple_list_for_recipe(monkeypatch): r = get_fake_recipe("recipe1", depends=[ "libffi", ("libffi", "Pillow") ]) dep_list = get_dependency_tuple_list_for_recipe( r, blacklist={"libffi"} ) assert dep_list == [("pillow",)] @pytest.mark.parametrize('names,bootstrap', valid_combinations) def test_valid_obvious_conflict_checker(names, bootstrap): # Note: obvious_conflict_checker is stricter on input # (needs fix_deplist) than get_recipe_order_and_bootstrap! obvious_conflict_checker(ctx, fix_deplist(names)) @pytest.mark.parametrize('names,bootstrap', invalid_combinations_simple # see above for why this ) # is a separate list def test_invalid_obvious_conflict_checker(names, bootstrap): # Note: obvious_conflict_checker is stricter on input # (needs fix_deplist) than get_recipe_order_and_bootstrap! with pytest.raises(BuildInterruptingException) as e_info: obvious_conflict_checker(ctx, fix_deplist(names)) assert "conflict" in e_info.value.message.lower() def test_misc_obvious_conflict_checker(monkeypatch): # Check that the assert about wrong input data is hit: with pytest.raises(AssertionError) as e_info: obvious_conflict_checker( ctx, ["this_is_invalid"] # (invalid because it isn't properly nested as tuple) ) # Test that non-recipe dependencies work in overall: obvious_conflict_checker( ctx, fix_deplist(["python3", "notarecipelibrary"]) ) # Test that a conflict with a non-recipe dependency works: # This is currently not used, so we need a custom test recipe: # To get that, we simply modify one! with monkeypatch.context() as m: register_fake_recipes_for_test(m, [ get_fake_recipe("recipe1", conflicts=[("fakelib")]), ]) with pytest.raises(BuildInterruptingException) as e_info: obvious_conflict_checker(ctx, fix_deplist(["recipe1", "fakelib"])) assert "conflict" in e_info.value.message.lower() # Test a case where a recipe pulls in a conditional tuple # of additional dependencies. This is e.g. done for ('python3', # 'python2', ...) but most recipes don't depend on this anymore, # so we need to add a manual test for this case: with monkeypatch.context() as m: register_fake_recipes_for_test(m, [ get_fake_recipe("recipe1", depends=[("libffi", "Pillow")]), ]) obvious_conflict_checker(ctx, fix_deplist(["recipe1"])) def test_indirectconflict_obvious_conflict_checker(monkeypatch): # Test a case where there's an indirect conflict, which also # makes sure the error message correctly blames the OUTER recipes # as original conflict source: with monkeypatch.context() as m: register_fake_recipes_for_test(m, [ get_fake_recipe("outerrecipe1", depends=["innerrecipe1"]), get_fake_recipe("outerrecipe2", depends=["innerrecipe2"]), get_fake_recipe("innerrecipe1"), get_fake_recipe("innerrecipe2", conflicts=["innerrecipe1"]), ]) with pytest.raises(BuildInterruptingException) as e_info: obvious_conflict_checker( ctx, fix_deplist(["outerrecipe1", "outerrecipe2"]) ) assert ("conflict" in e_info.value.message.lower() and "outerrecipe1" in e_info.value.message.lower() and "outerrecipe2" in e_info.value.message.lower()) def test_multichoice_obvious_conflict_checker(monkeypatch): # Test a case where there's a conflict with a multi-choice tuple: with monkeypatch.context() as m: register_fake_recipes_for_test(m, [ get_fake_recipe("recipe1", conflicts=["lib1", "lib2"]), get_fake_recipe("recipe2", depends=[("lib1", "lib2")]), ]) with pytest.raises(BuildInterruptingException) as e_info: obvious_conflict_checker( ctx, fix_deplist([("lib1", "lib2"), "recipe1"]) ) assert "conflict" in e_info.value.message.lower() def test_bootstrap_dependency_addition(): build_order, python_modules, bs = get_recipe_order_and_bootstrap( ctx, ['kivy'], None) assert ('hostpython3' in build_order) def test_graph_deplist_transformation(): test_pairs = [ (["Pillow", ('python2', 'python3')], [('pillow',), ('python2', 'python3')]), (["Pillow", ('python2',)], [('pillow',), ('python2',)]), ] for (before_list, after_list) in test_pairs: assert fix_deplist(before_list) == after_list def test_bootstrap_dependency_addition2(): build_order, python_modules, bs = get_recipe_order_and_bootstrap( ctx, ['kivy', 'python3'], None) assert 'hostpython3' in build_order if __name__ == "__main__": get_recipe_order_and_bootstrap(ctx, ['python3'], Bootstrap.get_bootstrap('sdl2', ctx)) ================================================ FILE: tests/test_logger.py ================================================ import logging import sh import pytest import unittest from unittest.mock import MagicMock, Mock, patch from pythonforandroid import logger class TestColorSetup: """Test color setup and configuration.""" def teardown_method(self): """Reset color state after each test to avoid affecting other tests.""" logger.setup_color('never') def test_setup_color_never(self): """Test color disabled when set to 'never'.""" logger.setup_color('never') assert not logger.Out_Style._enabled assert not logger.Out_Fore._enabled assert not logger.Err_Style._enabled assert not logger.Err_Fore._enabled def test_setup_color_always(self): """Test color enabled when set to 'always'.""" logger.setup_color('always') assert logger.Out_Style._enabled assert logger.Out_Fore._enabled assert logger.Err_Style._enabled assert logger.Err_Fore._enabled @patch('pythonforandroid.logger.stdout') @patch('pythonforandroid.logger.stderr') def test_setup_color_auto_with_tty(self, mock_stderr, mock_stdout): """Test color enabled when auto and isatty() returns True.""" mock_stdout.isatty.return_value = True mock_stderr.isatty.return_value = True logger.setup_color('auto') assert logger.Out_Style._enabled assert logger.Err_Style._enabled class TestUtilityFunctions: """Test logger utility functions.""" def test_shorten_string_short(self): """Test shorten_string returns string unchanged when under limit.""" result = logger.shorten_string("short", 50) assert result == "short" def test_shorten_string_long(self): """Test shorten_string truncates long strings correctly.""" long_string = "a" * 100 result = logger.shorten_string(long_string, 50) assert "...(and" in result assert "more)" in result assert len(result) <= 50 def test_shorten_string_bytes(self): """Test shorten_string handles bytes input.""" byte_string = b"test" * 50 result = logger.shorten_string(byte_string, 50) assert "...(and" in result @patch.dict('os.environ', {'COLUMNS': '120'}) def test_get_console_width_from_env(self): """Test get_console_width reads from COLUMNS env var.""" width = logger.get_console_width() assert width == 120 @patch.dict('os.environ', {}, clear=True) @patch('os.popen') def test_get_console_width_from_stty(self, mock_popen): """Test get_console_width falls back to stty command.""" mock_popen.return_value.read.return_value = "40 80" width = logger.get_console_width() assert width == 80 mock_popen.assert_called_once_with('stty size', 'r') @patch.dict('os.environ', {}, clear=True) @patch('os.popen') def test_get_console_width_default(self, mock_popen): """Test get_console_width returns default when stty fails.""" mock_popen.return_value.read.side_effect = Exception("stty failed") width = logger.get_console_width() assert width == 100 class TestLevelDifferentiatingFormatter: """Test custom log message formatter.""" def test_format_error_level(self): """Test formatter adds [ERROR] prefix for ERROR level.""" formatter = logger.LevelDifferentiatingFormatter('%(message)s') record = logging.LogRecord( name='test', level=40, pathname='', lineno=0, msg='test error', args=(), exc_info=None ) formatted = formatter.format(record) assert '[ERROR]' in formatted def test_format_warning_level(self): """Test formatter adds [WARNING] prefix for WARNING level.""" formatter = logger.LevelDifferentiatingFormatter('%(message)s') record = logging.LogRecord( name='test', level=30, pathname='', lineno=0, msg='test warning', args=(), exc_info=None ) formatted = formatter.format(record) assert '[WARNING]' in formatted def test_format_info_level(self): """Test formatter adds [INFO] prefix for INFO level.""" formatter = logger.LevelDifferentiatingFormatter('%(message)s') record = logging.LogRecord( name='test', level=20, pathname='', lineno=0, msg='test info', args=(), exc_info=None ) formatted = formatter.format(record) assert '[INFO]' in formatted def test_format_debug_level(self): """Test formatter adds [DEBUG] prefix for DEBUG level.""" formatter = logger.LevelDifferentiatingFormatter('%(message)s') record = logging.LogRecord( name='test', level=10, pathname='', lineno=0, msg='test debug', args=(), exc_info=None ) formatted = formatter.format(record) assert '[DEBUG]' in formatted class TestShprintErrorHandling: """Test shprint error handling and edge cases.""" @patch('pythonforandroid.logger.get_console_width') def test_shprint_with_filter(self, mock_width): """Test shprint filters output with _filter parameter.""" mock_width.return_value = 100 command = MagicMock() # Create a mock error with required attributes error = Mock(spec=sh.ErrorReturnCode) error.stdout = b'line1\nfiltered_line\nline3' error.stderr = b'' command.side_effect = error with pytest.raises(TypeError): logger.shprint(command, _filter='filtered', _tail=10) @patch('pythonforandroid.logger.get_console_width') def test_shprint_with_filterout(self, mock_width): """Test shprint excludes output with _filterout parameter.""" mock_width.return_value = 100 command = MagicMock() error = Mock(spec=sh.ErrorReturnCode) error.stdout = b'keep1\nexclude_line\nkeep2' error.stderr = b'' command.side_effect = error with pytest.raises(TypeError): logger.shprint(command, _filterout='exclude', _tail=10) @patch('pythonforandroid.logger.get_console_width') @patch('pythonforandroid.logger.stdout') @patch.dict('os.environ', {'P4A_FULL_DEBUG': '1'}) def test_shprint_full_debug_mode(self, mock_stdout, mock_width): """Test shprint in P4A_FULL_DEBUG mode shows all output.""" mock_width.return_value = 100 command = MagicMock() command.return_value = iter(['debug line 1\n', 'debug line 2\n']) logger.shprint(command) # In full debug mode, output is written directly to stdout assert mock_stdout.write.called @patch('pythonforandroid.logger.get_console_width') @patch.dict('os.environ', {}, clear=True) def test_shprint_critical_failure_exits(self, mock_width): """Test shprint exits on critical command failure.""" mock_width.return_value = 100 command = MagicMock() # Create a proper exception class that mimics sh.ErrorReturnCode class MockErrorReturnCode(sh.ErrorReturnCode): def __init__(self): self.full_cmd = 'test' self.stdout = b'output' self.stderr = b'error' self.exit_code = 1 error = MockErrorReturnCode() command.side_effect = error with patch('pythonforandroid.logger.exit', side_effect=SystemExit) as mock_exit: with pytest.raises(SystemExit): logger.shprint(command, _critical=True, _tail=5) mock_exit.assert_called_once_with(1) class TestLoggingHelpers: """Test logging helper functions.""" @patch('pythonforandroid.logger.logger') def test_info_main(self, mock_logger): """Test info_main logs with bright green formatting.""" logger.info_main('test', 'message') mock_logger.info.assert_called_once() # Verify the call contains color codes and text call_args = mock_logger.info.call_args[0][0] assert 'test' in call_args assert 'message' in call_args @patch('pythonforandroid.logger.info') def test_info_notify(self, mock_info): """Test info_notify logs with blue formatting.""" logger.info_notify('notification') mock_info.assert_called_once() call_args = mock_info.call_args[0][0] assert 'notification' in call_args class TestShprint(unittest.TestCase): def test_unicode_encode(self): """ Makes sure `shprint()` can handle unicode command output. Running the test with PYTHONIOENCODING=ASCII env would fail, refs: https://github.com/kivy/python-for-android/issues/1654 """ expected_command_output = ["foo\xa0bar"] command = MagicMock() command.return_value = expected_command_output output = logger.shprint(command, 'a1', k1='k1') self.assertEqual(output, expected_command_output) ================================================ FILE: tests/test_patching.py ================================================ from unittest import mock from pythonforandroid.patching import ( is_platform, is_linux, is_darwin, is_windows, is_arch, is_api, is_api_gt, is_api_gte, is_api_lt, is_api_lte, is_ndk, is_version_gt, is_version_lt, version_starts_with, will_build, check_all, check_any, ) class TestPlatformChecks: """Test platform detection functions.""" @mock.patch('pythonforandroid.patching.uname') def test_is_platform_linux(self, mock_uname): """Test is_platform returns check function for Linux.""" mock_uname.return_value = mock.Mock(system='Linux') check_fn = is_platform('Linux') assert check_fn(None, None) @mock.patch('pythonforandroid.patching.uname') def test_is_platform_darwin(self, mock_uname): """Test is_platform returns check function for Darwin.""" mock_uname.return_value = mock.Mock(system='Darwin') check_fn = is_platform('Darwin') assert check_fn(None, None) @mock.patch('pythonforandroid.patching.uname') def test_is_platform_case_insensitive(self, mock_uname): """Test is_platform is case insensitive.""" mock_uname.return_value = mock.Mock(system='LINUX') check_fn = is_platform('linux') assert check_fn(None, None) @mock.patch('pythonforandroid.patching.uname') def test_is_platform_mismatch(self, mock_uname): """Test is_platform returns False for mismatched platform.""" mock_uname.return_value = mock.Mock(system='Linux') check_fn = is_platform('Windows') assert not check_fn(None, None) def test_is_linux(self): """Test is_linux constant function is defined.""" # is_linux is defined at module import time based on real platform # We can only verify it's callable assert callable(is_linux) def test_is_darwin(self): """Test is_darwin constant function is defined.""" # is_darwin is defined at module import time based on real platform # We can only verify it's callable assert callable(is_darwin) def test_is_windows(self): """Test is_windows constant function is defined.""" # is_windows is defined at module import time based on real platform # We can only verify it's callable assert callable(is_windows) class TestArchChecks: """Test architecture check functions.""" def test_is_arch_match(self): """Test is_arch returns True for matching architecture.""" mock_arch = mock.Mock(arch='armeabi-v7a') check_fn = is_arch('armeabi-v7a') assert check_fn(mock_arch) def test_is_arch_mismatch(self): """Test is_arch returns False for mismatched architecture.""" mock_arch = mock.Mock(arch='armeabi-v7a') check_fn = is_arch('arm64-v8a') assert not check_fn(mock_arch) class TestAndroidAPIChecks: """Test Android API level comparison functions.""" def test_is_api_equal(self): """Test is_api for equal API level.""" mock_recipe = mock.Mock() mock_recipe.ctx.android_api = 21 check_fn = is_api(21) assert check_fn(None, mock_recipe) def test_is_api_not_equal(self): """Test is_api for unequal API level.""" mock_recipe = mock.Mock() mock_recipe.ctx.android_api = 21 check_fn = is_api(27) assert not check_fn(None, mock_recipe) def test_is_api_gt(self): """Test is_api_gt for greater than comparison.""" mock_recipe = mock.Mock() mock_recipe.ctx.android_api = 27 check_fn = is_api_gt(21) assert check_fn(None, mock_recipe) mock_recipe.ctx.android_api = 21 assert not check_fn(None, mock_recipe) def test_is_api_gte(self): """Test is_api_gte for greater than or equal comparison.""" mock_recipe = mock.Mock() mock_recipe.ctx.android_api = 27 check_fn = is_api_gte(21) assert check_fn(None, mock_recipe) mock_recipe.ctx.android_api = 21 check_fn = is_api_gte(21) assert check_fn(None, mock_recipe) mock_recipe.ctx.android_api = 19 assert not check_fn(None, mock_recipe) def test_is_api_lt(self): """Test is_api_lt for less than comparison.""" mock_recipe = mock.Mock() mock_recipe.ctx.android_api = 19 check_fn = is_api_lt(21) assert check_fn(None, mock_recipe) mock_recipe.ctx.android_api = 21 assert not check_fn(None, mock_recipe) def test_is_api_lte(self): """Test is_api_lte for less than or equal comparison.""" mock_recipe = mock.Mock() mock_recipe.ctx.android_api = 19 check_fn = is_api_lte(21) assert check_fn(None, mock_recipe) mock_recipe.ctx.android_api = 21 check_fn = is_api_lte(21) assert check_fn(None, mock_recipe) mock_recipe.ctx.android_api = 27 assert not check_fn(None, mock_recipe) class TestNDKChecks: """Test NDK version check functions.""" def test_is_ndk_equal(self): """Test is_ndk for equal NDK version.""" mock_ndk = mock.Mock(name='ndk_r21e') mock_recipe = mock.Mock() mock_recipe.ctx.ndk = mock_ndk check_fn = is_ndk(mock_ndk) assert check_fn(None, mock_recipe) def test_is_ndk_not_equal(self): """Test is_ndk for unequal NDK version.""" mock_ndk1 = mock.Mock(name='ndk_r21e') mock_ndk2 = mock.Mock(name='ndk_r25c') mock_recipe = mock.Mock() mock_recipe.ctx.ndk = mock_ndk1 check_fn = is_ndk(mock_ndk2) assert not check_fn(None, mock_recipe) class TestVersionChecks: """Test recipe version comparison functions.""" def test_is_version_gt(self): """Test is_version_gt for version comparison.""" mock_recipe = mock.Mock(version='2.0.0') check_fn = is_version_gt('1.0.0') assert check_fn(None, mock_recipe) mock_recipe.version = '1.0.0' assert not check_fn(None, mock_recipe) def test_is_version_lt(self): """Test is_version_lt for version comparison.""" mock_recipe = mock.Mock(version='1.0.0') check_fn = is_version_lt('2.0.0') assert check_fn(None, mock_recipe) mock_recipe.version = '2.0.0' assert not check_fn(None, mock_recipe) def test_version_starts_with(self): """Test version_starts_with for version prefix matching.""" mock_recipe = mock.Mock(version='1.15.2') check_fn = version_starts_with('1.15') assert check_fn(None, mock_recipe) check_fn = version_starts_with('1.14') assert not check_fn(None, mock_recipe) check_fn = version_starts_with('2') assert not check_fn(None, mock_recipe) class TestWillBuild: """Test will_build function.""" def test_will_build_present(self): """Test will_build returns True when recipe is in build order.""" mock_recipe = mock.Mock() mock_recipe.ctx.recipe_build_order = ['python3', 'numpy', 'kivy'] check_fn = will_build('numpy') assert check_fn(None, mock_recipe) def test_will_build_absent(self): """Test will_build returns False when recipe is not in build order.""" mock_recipe = mock.Mock() mock_recipe.ctx.recipe_build_order = ['python3', 'numpy', 'kivy'] check_fn = will_build('scipy') assert not check_fn(None, mock_recipe) class TestConjunctions: """Test logical conjunction functions.""" def test_check_all_all_true(self): """Test check_all returns True when all checks pass.""" def check1(_arch, _recipe): return True def check2(_arch, _recipe): return True def check3(_arch, _recipe): return True check_fn = check_all(check1, check2, check3) assert check_fn(None, None) def test_check_all_one_false(self): """Test check_all returns False when one check fails.""" def check1(_arch, _recipe): return True def check2(_arch, _recipe): return False def check3(_arch, _recipe): return True check_fn = check_all(check1, check2, check3) assert not check_fn(None, None) def test_check_all_all_false(self): """Test check_all returns False when all checks fail.""" def check1(_arch, _recipe): return False def check2(_arch, _recipe): return False check_fn = check_all(check1, check2) assert not check_fn(None, None) def test_check_any_one_true(self): """Test check_any returns True when one check passes.""" def check1(_arch, _recipe): return False def check2(_arch, _recipe): return True def check3(_arch, _recipe): return False check_fn = check_any(check1, check2, check3) assert check_fn(None, None) def test_check_any_all_false(self): """Test check_any returns False when all checks fail.""" def check1(_arch, _recipe): return False def check2(_arch, _recipe): return False check_fn = check_any(check1, check2) assert not check_fn(None, None) def test_check_any_all_true(self): """Test check_any returns True when all checks pass.""" def check1(_arch, _recipe): return True def check2(_arch, _recipe): return True check_fn = check_any(check1, check2) assert check_fn(None, None) @mock.patch('pythonforandroid.patching.uname') def test_combined_checks(self, mock_uname): """Test combining multiple check functions with check_all and check_any.""" # Test check_all with is_platform and is_version_gt mock_uname.return_value = mock.Mock(system='Linux') mock_recipe = mock.Mock(version='2.0.0') check_fn = check_all( is_platform('Linux'), is_version_gt('1.0.0') ) assert check_fn(None, mock_recipe) # Test check_any with is_platform and is_version_gt mock_uname.return_value = mock.Mock(system='Windows') check_fn = check_any( is_platform('Linux'), is_version_gt('1.0.0') ) assert check_fn(None, mock_recipe) ================================================ FILE: tests/test_prerequisites.py ================================================ import unittest from unittest import mock, skipIf import sys import pytest from pythonforandroid.prerequisites import ( Prerequisite, JDKPrerequisite, HomebrewPrerequisite, OpenSSLPrerequisite, AutoconfPrerequisite, AutomakePrerequisite, LibtoolPrerequisite, PkgConfigPrerequisite, CmakePrerequisite, get_required_prerequisites, check_and_install_default_prerequisites, ) class PrerequisiteSetUpBaseClass: def setUp(self): self.mandatory = dict(linux=False, darwin=False) self.installer_is_supported = dict(linux=False, darwin=False) self.expected_homebrew_formula_name = "" def test_is_mandatory_on_darwin(self): assert self.prerequisite.mandatory["darwin"] == self.mandatory["darwin"] def test_is_mandatory_on_linux(self): assert self.prerequisite.mandatory["linux"] == self.mandatory["linux"] def test_installer_is_supported_on_darwin(self): assert ( self.prerequisite.installer_is_supported["darwin"] == self.installer_is_supported["darwin"] ) def test_installer_is_supported_on_linux(self): assert ( self.prerequisite.installer_is_supported["linux"] == self.installer_is_supported["linux"] ) def test_darwin_pkg_config_location(self): self.assertEqual(self.prerequisite.darwin_pkg_config_location(), "") def test_linux_pkg_config_location(self): self.assertEqual(self.prerequisite.linux_pkg_config_location(), "") @skipIf(sys.platform != "darwin", "Only run on macOS") def test_pkg_config_location_property__darwin(self): self.assertEqual( self.prerequisite.pkg_config_location, self.prerequisite.darwin_pkg_config_location(), ) @skipIf(sys.platform != "linux", "Only run on Linux") def test_pkg_config_location_property__linux(self): self.assertEqual( self.prerequisite.pkg_config_location, self.prerequisite.linux_pkg_config_location(), ) class TestJDKPrerequisite(PrerequisiteSetUpBaseClass, unittest.TestCase): def setUp(self): super().setUp() self.mandatory = dict(linux=False, darwin=True) self.installer_is_supported = dict(linux=False, darwin=True) self.prerequisite = JDKPrerequisite() class TestBrewPrerequisite(PrerequisiteSetUpBaseClass, unittest.TestCase): def setUp(self): super().setUp() self.mandatory = dict(linux=False, darwin=True) self.installer_is_supported = dict(linux=False, darwin=False) self.prerequisite = HomebrewPrerequisite() @mock.patch("shutil.which") def test_darwin_checker(self, shutil_which): shutil_which.return_value = None self.assertFalse(self.prerequisite.darwin_checker()) shutil_which.return_value = "/opt/homebrew/bin/brew" self.assertTrue(self.prerequisite.darwin_checker()) @mock.patch("pythonforandroid.prerequisites.info") def test_darwin_helper(self, info): self.prerequisite.darwin_helper() info.assert_called_once_with( "Installer for homebrew is not yet supported on macOS," "the nice news is that the installation process is easy!" "See: https://brew.sh for further instructions." ) class TestOpenSSLPrerequisite(PrerequisiteSetUpBaseClass, unittest.TestCase): def setUp(self): super().setUp() self.mandatory = dict(linux=False, darwin=True) self.installer_is_supported = dict(linux=False, darwin=True) self.prerequisite = OpenSSLPrerequisite() self.expected_homebrew_formula_name = "openssl@3" self.expected_homebrew_location_prefix = "/opt/homebrew/opt/openssl@3" @mock.patch( "pythonforandroid.prerequisites.Prerequisite._darwin_get_brew_formula_location_prefix" ) def test_darwin_checker(self, _darwin_get_brew_formula_location_prefix): _darwin_get_brew_formula_location_prefix.return_value = None self.assertFalse(self.prerequisite.darwin_checker()) _darwin_get_brew_formula_location_prefix.return_value = ( self.expected_homebrew_location_prefix ) self.assertTrue(self.prerequisite.darwin_checker()) _darwin_get_brew_formula_location_prefix.assert_called_with( self.expected_homebrew_formula_name, installed=True ) @mock.patch("pythonforandroid.prerequisites.subprocess.check_output") def test_darwin_installer(self, check_output): self.prerequisite.darwin_installer() check_output.assert_called_once_with( ["brew", "install", self.expected_homebrew_formula_name] ) @mock.patch( "pythonforandroid.prerequisites.Prerequisite._darwin_get_brew_formula_location_prefix" ) def test_darwin_pkg_config_location(self, _darwin_get_brew_formula_location_prefix): _darwin_get_brew_formula_location_prefix.return_value = ( self.expected_homebrew_location_prefix ) self.assertEqual( self.prerequisite.darwin_pkg_config_location(), f"{self.expected_homebrew_location_prefix}/lib/pkgconfig", ) class TestAutoconfPrerequisite(PrerequisiteSetUpBaseClass, unittest.TestCase): def setUp(self): super().setUp() self.mandatory = dict(linux=False, darwin=True) self.installer_is_supported = dict(linux=False, darwin=True) self.prerequisite = AutoconfPrerequisite() @mock.patch( "pythonforandroid.prerequisites.Prerequisite._darwin_get_brew_formula_location_prefix" ) def test_darwin_checker(self, _darwin_get_brew_formula_location_prefix): _darwin_get_brew_formula_location_prefix.return_value = None self.assertFalse(self.prerequisite.darwin_checker()) _darwin_get_brew_formula_location_prefix.return_value = ( "/opt/homebrew/opt/autoconf" ) self.assertTrue(self.prerequisite.darwin_checker()) _darwin_get_brew_formula_location_prefix.assert_called_with( "autoconf", installed=True ) @mock.patch("pythonforandroid.prerequisites.subprocess.check_output") def test_darwin_installer(self, check_output): self.prerequisite.darwin_installer() check_output.assert_called_once_with(["brew", "install", "autoconf"]) class TestAutomakePrerequisite(PrerequisiteSetUpBaseClass, unittest.TestCase): def setUp(self): super().setUp() self.mandatory = dict(linux=False, darwin=True) self.installer_is_supported = dict(linux=False, darwin=True) self.prerequisite = AutomakePrerequisite() @mock.patch( "pythonforandroid.prerequisites.Prerequisite._darwin_get_brew_formula_location_prefix" ) def test_darwin_checker(self, _darwin_get_brew_formula_location_prefix): _darwin_get_brew_formula_location_prefix.return_value = None self.assertFalse(self.prerequisite.darwin_checker()) _darwin_get_brew_formula_location_prefix.return_value = ( "/opt/homebrew/opt/automake" ) self.assertTrue(self.prerequisite.darwin_checker()) _darwin_get_brew_formula_location_prefix.assert_called_with( "automake", installed=True ) @mock.patch("pythonforandroid.prerequisites.subprocess.check_output") def test_darwin_installer(self, check_output): self.prerequisite.darwin_installer() check_output.assert_called_once_with(["brew", "install", "automake"]) class TestLibtoolPrerequisite(PrerequisiteSetUpBaseClass, unittest.TestCase): def setUp(self): super().setUp() self.mandatory = dict(linux=False, darwin=True) self.installer_is_supported = dict(linux=False, darwin=True) self.prerequisite = LibtoolPrerequisite() @mock.patch( "pythonforandroid.prerequisites.Prerequisite._darwin_get_brew_formula_location_prefix" ) def test_darwin_checker(self, _darwin_get_brew_formula_location_prefix): _darwin_get_brew_formula_location_prefix.return_value = None self.assertFalse(self.prerequisite.darwin_checker()) _darwin_get_brew_formula_location_prefix.return_value = ( "/opt/homebrew/opt/libtool" ) self.assertTrue(self.prerequisite.darwin_checker()) _darwin_get_brew_formula_location_prefix.assert_called_with( "libtool", installed=True ) @mock.patch("pythonforandroid.prerequisites.subprocess.check_output") def test_darwin_installer(self, check_output): self.prerequisite.darwin_installer() check_output.assert_called_once_with(["brew", "install", "libtool"]) class TestPkgConfigPrerequisite(PrerequisiteSetUpBaseClass, unittest.TestCase): def setUp(self): super().setUp() self.mandatory = dict(linux=False, darwin=True) self.installer_is_supported = dict(linux=False, darwin=True) self.prerequisite = PkgConfigPrerequisite() @mock.patch( "pythonforandroid.prerequisites.Prerequisite._darwin_get_brew_formula_location_prefix" ) def test_darwin_checker(self, _darwin_get_brew_formula_location_prefix): _darwin_get_brew_formula_location_prefix.return_value = None self.assertFalse(self.prerequisite.darwin_checker()) _darwin_get_brew_formula_location_prefix.return_value = ( "/opt/homebrew/opt/pkg-config" ) self.assertTrue(self.prerequisite.darwin_checker()) _darwin_get_brew_formula_location_prefix.assert_called_with( "pkg-config", installed=True ) @mock.patch("pythonforandroid.prerequisites.subprocess.check_output") def test_darwin_installer(self, check_output): self.prerequisite.darwin_installer() check_output.assert_called_once_with(["brew", "install", "pkg-config"]) class TestCmakePrerequisite(PrerequisiteSetUpBaseClass, unittest.TestCase): def setUp(self): super().setUp() self.mandatory = dict(linux=False, darwin=True) self.installer_is_supported = dict(linux=False, darwin=True) self.prerequisite = CmakePrerequisite() @mock.patch( "pythonforandroid.prerequisites.Prerequisite._darwin_get_brew_formula_location_prefix" ) def test_darwin_checker(self, _darwin_get_brew_formula_location_prefix): _darwin_get_brew_formula_location_prefix.return_value = None self.assertFalse(self.prerequisite.darwin_checker()) _darwin_get_brew_formula_location_prefix.return_value = ( "/opt/homebrew/opt/cmake" ) self.assertTrue(self.prerequisite.darwin_checker()) _darwin_get_brew_formula_location_prefix.assert_called_with( "cmake", installed=True ) @mock.patch("pythonforandroid.prerequisites.subprocess.check_output") def test_darwin_installer(self, check_output): self.prerequisite.darwin_installer() check_output.assert_called_once_with(["brew", "install", "cmake"]) class TestDefaultPrerequisitesCheckandInstall(unittest.TestCase): def test_default_darwin_prerequisites_set(self): self.assertListEqual( [ p.__class__.__name__ for p in get_required_prerequisites(platform="darwin") ], [ "HomebrewPrerequisite", "AutoconfPrerequisite", "AutomakePrerequisite", "LibtoolPrerequisite", "PkgConfigPrerequisite", "CmakePrerequisite", "OpenSSLPrerequisite", "JDKPrerequisite", ], ) def test_default_linux_prerequisites_set(self): self.assertListEqual( [ p.__class__.__name__ for p in get_required_prerequisites(platform="linux") ], [ ], ) class TestPrerequisiteBaseClass: """Test base Prerequisite class methods.""" @mock.patch('pythonforandroid.prerequisites.info') @mock.patch.object(Prerequisite, 'checker') def test_is_valid_when_met(self, mock_checker, mock_info): """Test is_valid returns True when prerequisite is met.""" mock_checker.return_value = True prerequisite = Prerequisite() result = prerequisite.is_valid() assert result == (True, "") mock_info.assert_called() assert "is met" in mock_info.call_args[0][0] @mock.patch('pythonforandroid.prerequisites.warning') @mock.patch.object(Prerequisite, 'checker') def test_is_valid_when_not_met_non_mandatory(self, mock_checker, mock_warning): """Test is_valid warns when non-mandatory prerequisite not met.""" mock_checker.return_value = False prerequisite = Prerequisite() prerequisite.mandatory = dict(linux=False, darwin=False) result = prerequisite.is_valid() assert result is None mock_warning.assert_called() assert "not met" in mock_warning.call_args[0][0] @mock.patch('pythonforandroid.prerequisites.error') @mock.patch.object(Prerequisite, 'checker') @mock.patch('sys.platform', 'linux') def test_is_valid_when_not_met_mandatory(self, mock_checker, mock_error): """Test is_valid errors when mandatory prerequisite not met.""" mock_checker.return_value = False prerequisite = Prerequisite() prerequisite.mandatory = dict(linux=True, darwin=False) result = prerequisite.is_valid() assert result is None mock_error.assert_called() assert "not met" in mock_error.call_args[0][0] @mock.patch('sys.platform', 'linux') @mock.patch.object(Prerequisite, 'linux_checker') def test_checker_calls_linux_checker(self, mock_linux_checker): """Test checker dispatches to linux_checker on Linux.""" mock_linux_checker.return_value = True prerequisite = Prerequisite() result = prerequisite.checker() assert result is True mock_linux_checker.assert_called_once() @mock.patch('sys.platform', 'darwin') @mock.patch.object(Prerequisite, 'darwin_checker') def test_checker_calls_darwin_checker(self, mock_darwin_checker): """Test checker dispatches to darwin_checker on macOS.""" mock_darwin_checker.return_value = True prerequisite = Prerequisite() result = prerequisite.checker() assert result is True mock_darwin_checker.assert_called_once() @mock.patch('sys.platform', 'win32') def test_checker_raises_on_unsupported_platform(self): """Test checker raises exception on unsupported platform.""" prerequisite = Prerequisite() with pytest.raises(Exception, match="Unsupported platform"): prerequisite.checker() class TestPrerequisiteInstallation: """Test prerequisite installation workflow.""" @mock.patch.dict('os.environ', {'PYTHONFORANDROID_PREREQUISITES_INSTALL_INTERACTIVE': '1'}) @mock.patch('builtins.input') def test_ask_to_install_user_accepts(self, mock_input): """Test ask_to_install returns True when user enters 'y'.""" prerequisite = Prerequisite() prerequisite.name = "TestPrerequisite" mock_input.return_value = 'y' result = prerequisite.ask_to_install() assert result is True @mock.patch.dict('os.environ', {'PYTHONFORANDROID_PREREQUISITES_INSTALL_INTERACTIVE': '1'}) @mock.patch('builtins.input') def test_ask_to_install_user_declines(self, mock_input): """Test ask_to_install returns False when user enters 'n'.""" prerequisite = Prerequisite() prerequisite.name = "TestPrerequisite" mock_input.return_value = 'n' result = prerequisite.ask_to_install() assert result is False @mock.patch.dict('os.environ', {'PYTHONFORANDROID_PREREQUISITES_INSTALL_INTERACTIVE': '0'}) @mock.patch('pythonforandroid.prerequisites.info') def test_ask_to_install_non_interactive(self, mock_info): """Test ask_to_install returns True in non-interactive mode (CI).""" prerequisite = Prerequisite() prerequisite.name = "TestPrerequisite" result = prerequisite.ask_to_install() assert result is True mock_info.assert_called() assert "not interactive" in mock_info.call_args[0][0] @mock.patch('sys.platform', 'linux') @mock.patch.object(Prerequisite, 'ask_to_install') @mock.patch.object(Prerequisite, 'linux_installer') @mock.patch('pythonforandroid.prerequisites.info') def test_install_when_user_accepts_linux(self, mock_info, mock_installer, mock_ask): """Test install calls linux_installer when user accepts on Linux.""" prerequisite = Prerequisite() prerequisite.installer_is_supported = dict(linux=True, darwin=True) mock_ask.return_value = True prerequisite.install() mock_installer.assert_called_once() @mock.patch('sys.platform', 'darwin') @mock.patch.object(Prerequisite, 'ask_to_install') @mock.patch.object(Prerequisite, 'darwin_installer') def test_install_when_user_accepts_darwin(self, mock_installer, mock_ask): """Test install calls darwin_installer when user accepts on macOS.""" prerequisite = Prerequisite() prerequisite.installer_is_supported = dict(linux=True, darwin=True) mock_ask.return_value = True prerequisite.install() mock_installer.assert_called_once() @mock.patch.object(Prerequisite, 'ask_to_install') @mock.patch('pythonforandroid.prerequisites.info') def test_install_when_user_declines(self, mock_info, mock_ask): """Test install skips installation when user declines.""" prerequisite = Prerequisite() prerequisite.name = "TestPrerequisite" mock_ask.return_value = False prerequisite.install() mock_info.assert_called() assert "Skipping" in mock_info.call_args[0][0] def test_install_is_supported(self): """Test install_is_supported returns correct platform support.""" prerequisite = Prerequisite() prerequisite.installer_is_supported = dict(linux=True, darwin=False) with mock.patch('sys.platform', 'linux'): assert prerequisite.install_is_supported() is True class TestJDKPrerequisiteVersionChecking: """Test JDK version checking logic.""" @mock.patch('pythonforandroid.prerequisites.subprocess.Popen') @mock.patch('os.path.exists') def test_darwin_jdk_is_supported_valid_version(self, mock_exists, mock_popen): """Test _darwin_jdk_is_supported returns True for valid JDK 17.""" prerequisite = JDKPrerequisite() mock_exists.return_value = True # Mock javac version output mock_process = mock.Mock() mock_process.returncode = 0 mock_process.communicate.return_value = (b'javac 17.0.2\n', b'') mock_popen.return_value = mock_process result = prerequisite._darwin_jdk_is_supported('/path/to/jdk') assert result is True @mock.patch('pythonforandroid.prerequisites.subprocess.Popen') @mock.patch('os.path.exists') def test_darwin_jdk_is_supported_invalid_version(self, mock_exists, mock_popen): """Test _darwin_jdk_is_supported returns False for wrong JDK version.""" prerequisite = JDKPrerequisite() mock_exists.return_value = True mock_process = mock.Mock() mock_process.returncode = 0 mock_process.communicate.return_value = (b'javac 11.0.1\n', b'') mock_popen.return_value = mock_process result = prerequisite._darwin_jdk_is_supported('/path/to/jdk') assert result is False @mock.patch('os.path.exists') def test_darwin_jdk_is_supported_no_javac(self, mock_exists): """Test _darwin_jdk_is_supported returns False when javac doesn't exist.""" prerequisite = JDKPrerequisite() mock_exists.return_value = False result = prerequisite._darwin_jdk_is_supported('/path/to/jdk') assert result is False @mock.patch('pythonforandroid.prerequisites.subprocess.run') def test_darwin_get_libexec_jdk_path(self, mock_run): """Test _darwin_get_libexec_jdk_path calls java_home correctly.""" prerequisite = JDKPrerequisite() mock_run.return_value = mock.Mock(stdout=b'/Library/Java/JDK/17\n') result = prerequisite._darwin_get_libexec_jdk_path(version='17') assert result == '/Library/Java/JDK/17' mock_run.assert_called_once() assert '-v' in mock_run.call_args[0][0] assert '17' in mock_run.call_args[0][0] @mock.patch.dict('os.environ', {'JAVA_HOME': '/custom/jdk'}) @mock.patch.object(JDKPrerequisite, '_darwin_jdk_is_supported') def test_darwin_checker_uses_java_home_env(self, mock_is_supported): """Test darwin_checker uses JAVA_HOME env var if set.""" prerequisite = JDKPrerequisite() mock_is_supported.return_value = True result = prerequisite.darwin_checker() assert result is True mock_is_supported.assert_called_with('/custom/jdk') class TestHomebrewHelpers: """Test Homebrew helper methods.""" @mock.patch('pythonforandroid.prerequisites.subprocess.Popen') def test_darwin_get_brew_formula_location_prefix_success(self, mock_popen): """Test _darwin_get_brew_formula_location_prefix returns path on success.""" prerequisite = Prerequisite() mock_process = mock.Mock() mock_process.returncode = 0 mock_process.communicate.return_value = (b'/opt/homebrew/opt/openssl@3\n', b'') mock_popen.return_value = mock_process result = prerequisite._darwin_get_brew_formula_location_prefix('openssl@3') assert result == '/opt/homebrew/opt/openssl@3' mock_popen.assert_called_once() assert 'brew' in mock_popen.call_args[0][0] assert '--prefix' in mock_popen.call_args[0][0] @mock.patch('pythonforandroid.prerequisites.subprocess.Popen') @mock.patch('pythonforandroid.prerequisites.error') def test_darwin_get_brew_formula_location_prefix_failure(self, mock_error, mock_popen): """Test _darwin_get_brew_formula_location_prefix returns None on failure.""" prerequisite = Prerequisite() mock_process = mock.Mock() mock_process.returncode = 1 mock_process.communicate.return_value = (b'', b'Formula not found\n') mock_popen.return_value = mock_process result = prerequisite._darwin_get_brew_formula_location_prefix('nonexistent') assert result is None mock_error.assert_called() @mock.patch('pythonforandroid.prerequisites.subprocess.Popen') def test_darwin_get_brew_formula_location_prefix_with_installed_flag(self, mock_popen): """Test _darwin_get_brew_formula_location_prefix uses --installed flag.""" prerequisite = Prerequisite() mock_process = mock.Mock() mock_process.returncode = 0 mock_process.communicate.return_value = (b'/opt/homebrew/opt/cmake\n', b'') mock_popen.return_value = mock_process prerequisite._darwin_get_brew_formula_location_prefix('cmake', installed=True) assert '--installed' in mock_popen.call_args[0][0] class TestCheckAndInstallPrerequisites: """Test main prerequisite checking workflow.""" @mock.patch('pythonforandroid.prerequisites.get_required_prerequisites') def test_check_and_install_all_met(self, mock_get_prereqs): """Test check_and_install when all prerequisites are met.""" # Create mock prerequisites that are all valid mock_prereq1 = mock.Mock() mock_prereq1.is_valid.return_value = True mock_prereq2 = mock.Mock() mock_prereq2.is_valid.return_value = True mock_get_prereqs.return_value = [mock_prereq1, mock_prereq2] check_and_install_default_prerequisites() # Verify prerequisites were checked mock_prereq1.is_valid.assert_called_once() mock_prereq2.is_valid.assert_called_once() # Verify no installation attempted mock_prereq1.install.assert_not_called() mock_prereq2.install.assert_not_called() @mock.patch('pythonforandroid.prerequisites.get_required_prerequisites') def test_check_and_install_some_not_met(self, mock_get_prereqs): """Test check_and_install when some prerequisites are not met.""" # First prerequisite valid, second not valid but has installer mock_prereq1 = mock.Mock() mock_prereq1.is_valid.return_value = True mock_prereq2 = mock.Mock() mock_prereq2.is_valid.return_value = False mock_prereq2.install_is_supported.return_value = True mock_get_prereqs.return_value = [mock_prereq1, mock_prereq2] check_and_install_default_prerequisites() # Verify second prerequisite triggers installation workflow mock_prereq2.show_helper.assert_called_once() mock_prereq2.install.assert_called_once() ================================================ FILE: tests/test_pythonpackage.py ================================================ """ THESE TESTS DON'T RUN IN GITHUB-ACTIONS (takes too long!!) ONLY THE BASIC ONES IN test_pythonpackage_basic.py DO. (This file basically covers all tests for any of the functions that aren't already part of the basic test set) """ import os import shutil import tempfile from pythonforandroid.pythonpackage import ( _extract_info_from_package, extract_metainfo_files_from_package, get_package_as_folder, get_package_dependencies, ) def local_repo_folder(): return os.path.abspath(os.path.join( os.path.dirname(__file__), ".." )) def test_get_package_dependencies(): # TEST 1 from source code folder: deps_nonrecursive = get_package_dependencies( local_repo_folder(), recursive=False ) deps_recursive = get_package_dependencies( local_repo_folder(), recursive=True ) # Check that jinja2 is returned as direct dep: assert len([dep for dep in deps_nonrecursive if "jinja2" in dep]) > 0 # Check that MarkupSafe is returned as indirect dep of jinja2: assert [ dep for dep in deps_recursive if "MarkupSafe" in dep ] # Check setuptools not being in non-recursive deps: # (It will be in recursive ones due to p4a's build dependency) assert "setuptools" not in deps_nonrecursive # Check setuptools is present in non-recursive deps, # if we also add build requirements: assert "setuptools" in get_package_dependencies( local_repo_folder(), recursive=False, include_build_requirements=True, ) # TEST 2 from external ref: # Check that jinja2 is returned as direct dep: assert len([dep for dep in get_package_dependencies("python-for-android") if "jinja2" in dep]) > 0 # Check that MarkupSafe is returned as indirect dep of jinja2: assert [ dep for dep in get_package_dependencies( "python-for-android", recursive=True ) if "MarkupSafe" in dep ] def test_extract_metainfo_files_from_package(): # TEST 1 from external ref: files_dir = tempfile.mkdtemp() try: extract_metainfo_files_from_package("python-for-android", files_dir, debug=True) assert os.path.exists(os.path.join(files_dir, "METADATA")) finally: shutil.rmtree(files_dir) # TEST 2 from local folder: files_dir = tempfile.mkdtemp() try: extract_metainfo_files_from_package(local_repo_folder(), files_dir, debug=True) assert os.path.exists(os.path.join(files_dir, "METADATA")) finally: shutil.rmtree(files_dir) def test_get_package_as_folder(): # WARNING !!! This function behaves DIFFERENTLY if the requested package # has a wheel available vs a source package. What we're getting is # essentially what pip also would fetch, but this can obviously CHANGE # depending on what is happening/available on PyPI. # # Therefore, this test doesn't really go in-depth. (obtained_type, obtained_path) = \ get_package_as_folder("python-for-android") try: assert obtained_type in {"source", "wheel"} assert os.path.isdir(obtained_path) finally: # Try to ensure cleanup: shutil.rmtree(obtained_path) def test__extract_info_from_package(): # This is indirectly already tested a lot through get_package_name() # and get_package_dependencies(), so we'll just do one basic test: assert _extract_info_from_package( local_repo_folder(), extract_type="name" ) == "python-for-android" ================================================ FILE: tests/test_pythonpackage_basic.py ================================================ """ ONLY BASIC TEST SET. The additional ones are in test_pythonpackage.py. These are in a separate file because these were picked to run in github-actions, while the other additional ones aren't (for build time reasons). """ import os import shutil import sys import subprocess import tempfile import textwrap from unittest import mock import pytest from build import BuildBackendException from pythonforandroid.pythonpackage import ( _extract_info_from_package, get_dep_names_of_package, get_package_name, _get_system_python_executable, is_filesystem_path, parse_as_folder_reference, transform_dep_for_pip, ) def local_repo_folder(): return os.path.abspath(os.path.join( os.path.dirname(__file__), ".." )) def fake_metadata_extract(dep_name, output_folder, debug=False): # Helper function to write out fake metadata. with open(os.path.join(output_folder, "METADATA"), "w") as f: f.write(textwrap.dedent("""\ Metadata-Version: 2.1 Name: testpackage Version: 0.1 Requires-Dist: testpkg Requires-Dist: testpkg2 Lorem Ipsum""" )) with open(os.path.join(output_folder, "metadata_source"), "w") as f: f.write(u"wheel") # since we have no pyproject.toml def test__extract_info_from_package(): import pythonforandroid.pythonpackage # noqa with mock.patch("pythonforandroid.pythonpackage." "extract_metainfo_files_from_package", fake_metadata_extract): assert _extract_info_from_package( "whatever", extract_type="name" ) == "testpackage" assert set(_extract_info_from_package( "whatever", extract_type="dependencies" )) == {"testpkg", "testpkg2"} def test_get_package_name(): # TEST 1 from external ref with mock.patch("pythonforandroid.pythonpackage." "extract_metainfo_files_from_package", fake_metadata_extract): assert get_package_name("TeStPackaGe") == "testpackage" # TEST 2 from a local folder, for which we'll create a fake package: temp_d = tempfile.mkdtemp(prefix="p4a-pythonpackage-test-tmp-") try: with open(os.path.join(temp_d, "setup.py"), "w") as f: f.write(textwrap.dedent("""\ from setuptools import setup setup(name="testpackage") """ )) pkg_name = get_package_name(temp_d) assert pkg_name == "testpackage" finally: shutil.rmtree(temp_d) def test_get_dep_names_of_package(): # TEST 1 from external ref: # Check that colorama is returned without the install condition when # just getting the names (it has a "; ..." conditional originally): dep_names = get_dep_names_of_package("python-for-android==2023.9.16") assert "colorama" in dep_names assert "setuptools" not in dep_names try: dep_names = get_dep_names_of_package( "python-for-android", include_build_requirements=True, verbose=True, ) except NotImplementedError as e: # If python-for-android was fetched as wheel then build requirements # cannot be obtained (since that is not implemented for wheels). # Check for the correct error message: assert "wheel" in str(e) # (And yes it would be better to do a local test with something # that is guaranteed to be a wheel and not remote on pypi, # but that might require setting up a full local pypiserver. # Not worth the test complexity for now, but if anyone has an # idea in the future feel free to replace this subtest.) else: # We managed to obtain build requirements! # Check setuptools is in here: assert "setuptools" in dep_names # TEST 2 from local folder: assert "colorama" in get_dep_names_of_package(local_repo_folder()) # Now test that exact version pins are kept, but others aren't: test_fake_package = tempfile.mkdtemp() try: with open(os.path.join(test_fake_package, "setup.py"), "w") as f: f.write(textwrap.dedent("""\ from setuptools import setup setup(name='fakeproject', description='fake for testing', install_requires=['buildozer==0.39', 'python-for-android>=0.5.1'], ) """)) # See that we get the deps with the exact version pin kept but # the other version restriction gone: assert set(get_dep_names_of_package( test_fake_package, recursive=False, keep_version_pins=True, verbose=True )) == {"buildozer==0.39", "python-for-android"} # Make sure we also can get the fully cleaned up variant: assert set(get_dep_names_of_package( test_fake_package, recursive=False, keep_version_pins=False, verbose=True )) == {"buildozer", "python-for-android"} # Test with build requirements included: dep_names = get_dep_names_of_package( test_fake_package, recursive=False, keep_version_pins=False, verbose=True, include_build_requirements=True ) assert len( {"buildozer", "python-for-android", "setuptools"}.intersection( dep_names ) ) == 3 # all three must be included finally: shutil.rmtree(test_fake_package) def test_transform_dep_for_pip(): # A reminder, this entire function we test here is just a workaround # for https://github.com/pypa/pip/issues/6097 (and not a nice one.) # As soon as upstream fixes it, we should throw it & this test out transformed = ( transform_dep_for_pip( "python-for-android @ https://github.com/kivy/" + "python-for-android/archive/master.zip" ), transform_dep_for_pip( "python-for-android @ https://github.com/kivy/" + "python-for-android/archive/master.zip" + "#egg=python-for-android-master" ), transform_dep_for_pip( "python-for-android @ https://github.com/kivy/" + "python-for-android/archive/master.zip" + "#" # common hack variant used by others to make pip parse it ), ) expected = ( "https://github.com/kivy/python-for-android/archive/master.zip" + "#egg=python-for-android" ) assert transformed == (expected, expected, expected) assert transform_dep_for_pip("https://a@b/") == "https://a@b/" def test_is_filesystem_path(): assert is_filesystem_path("/some/test") assert not is_filesystem_path("https://blubb") assert not is_filesystem_path("test @ bla") assert is_filesystem_path("/abc/c@d") assert not is_filesystem_path("https://user:pw@host/") assert is_filesystem_path(".") assert is_filesystem_path("") def test_parse_as_folder_reference(): assert parse_as_folder_reference("file:///a%20test") == "/a test" assert parse_as_folder_reference("https://github.com") is None assert parse_as_folder_reference("/a/folder") == "/a/folder" assert parse_as_folder_reference("test @ /abc") == "/abc" assert parse_as_folder_reference("test @ https://bla") is None @pytest.mark.parametrize("input_ref,expected", [ # URL-encoded special characters ("file:///path/with%40special", "/path/with@special"), ("file:///path/with%23hash", "/path/with#hash"), # Mixed @ syntax ("pkg @ file:///path/to/pkg", "/path/to/pkg"), # Empty and relative paths ("", ""), ("./relative", "./relative"), ]) def test_parse_as_folder_reference_edge_cases(input_ref, expected): """Test edge cases in folder reference parsing.""" assert parse_as_folder_reference(input_ref) == expected @pytest.mark.parametrize("path,expected", [ # Relative paths (should be filesystem paths) ("../parent", True), ("~/home/path", True), ("./current", True), # Git URLs (should not be filesystem paths) ("git+https://github.com/user/repo.git", False), ("git+ssh://git@github.com/user/repo.git", False), # Version specifiers (should not be filesystem paths) ("package>=1.0,<2.0", False), ("package[extra]>=1.0", False), ]) def test_is_filesystem_path_edge_cases(path, expected): """Test additional edge cases for filesystem path detection.""" assert is_filesystem_path(path) == expected @pytest.mark.parametrize("input_dep,expected", [ # Query parameters ("pkg @ https://example.com/pkg.zip?token=abc123", "https://example.com/pkg.zip?token=abc123#egg=pkg"), # Fragments ("pkg @ https://example.com/pkg.zip#sha256=abc", "https://example.com/pkg.zip#sha256=abc#egg=pkg"), ]) def test_transform_dep_for_pip_with_special_urls(input_dep, expected): """Test dependency transformation with query parameters and fragments.""" assert transform_dep_for_pip(input_dep) == expected def test_transform_dep_for_pip_passthrough(): """Test passthrough for already-transformed URLs.""" url = "https://example.com/package.zip#egg=package" assert transform_dep_for_pip(url) == url def test_get_package_name_with_error(): """Test get_package_name handles errors gracefully.""" # Test with invalid package that doesn't exist with mock.patch("pythonforandroid.pythonpackage." "extract_metainfo_files_from_package") as mock_extract: exception_message = "Package not found" mock_extract.side_effect = Exception(exception_message) with pytest.raises(Exception, match=exception_message): get_package_name("nonexistent-package-xyz-123") def test_get_dep_names_error_handling(): """Test error handling in dependency extraction.""" # Use context manager to ensure cleanup even if test fails with tempfile.TemporaryDirectory(prefix="p4a-error-test-") as temp_d: # Create a setup.py that will fail with open(os.path.join(temp_d, "setup.py"), "w") as f: f.write("raise RuntimeError('Invalid setup.py')") with pytest.raises(BuildBackendException, match="Backend subprocess exited when trying to invoke get_requires_for_build_wheel"): get_dep_names_of_package(temp_d, recursive=False, verbose=True) def test_extract_info_from_package_missing_metadata(): """Test _extract_info_from_package raises error when metadata is missing.""" def fake_empty_metadata(dep_name, output_folder, debug=False): # Don't create any metadata files pass with mock.patch("pythonforandroid.pythonpackage." "extract_metainfo_files_from_package", fake_empty_metadata): # Should raise an exception when metadata is missing with pytest.raises(FileNotFoundError): _extract_info_from_package("test", extract_type="name") class TestGetSystemPythonExecutable(): """ This contains all tests for _get_system_python_executable(). ULTRA IMPORTANT THING TO UNDERSTAND: (if you touch this) This code runs things with other python interpreters NOT in the tox environment/virtualenv. E.g. _get_system_python_executable() is outside in the regular host environment! That also means all dependencies can be possibly not present! This is kind of absurd that we need this to run the test at all, but we can't test this inside tox's virtualenv: """ def test_basic(self): # Tests function inside tox env with no further special setup. # Get system-wide python bin: pybin = _get_system_python_executable() # The python binary needs to match our major version to be correct: pyversion = subprocess.check_output([ pybin, "-c", "import sys; print(sys.version)" ], stderr=subprocess.STDOUT).decode("utf-8", "replace") assert pyversion.strip() == sys.version.strip() def run__get_system_python_executable(self, pybin): """ Helper function to run our function. We want to see what _get_system_python_executable() does given a specific python, so we need to make it import it and run it, with that TARGET python, which this function does. """ cmd = [ pybin, "-c", "import importlib\n" "import build.util\n" "import os\n" "import sys\n" "sys.path = [os.path.dirname(sys.argv[1])] + sys.path\n" "m = importlib.import_module(\n" " os.path.basename(sys.argv[1]).partition('.')[0]\n" ")\n" "print(m._get_system_python_executable())", os.path.join(os.path.dirname(__file__), "..", "pythonforandroid", "pythonpackage.py"), ] # Actual call to python: try: return subprocess.check_output( cmd, stderr=subprocess.STDOUT ).decode("utf-8", "replace").strip() except subprocess.CalledProcessError as e: raise RuntimeError("call failed, with output: " + str(e.output)) def test_systemwide_python(self): # 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) # Check that in system-wide python, the system-wide python is returned: # IMPORTANT: understand that this runs OUTSIDE of any virtualenv. try: p1 = os.path.normpath( self.run__get_system_python_executable(pybin) ) p2 = os.path.normpath(pybin) assert p1 == p2 except RuntimeError as e: # (remember this is not in a virtualenv) # Some deps may not be installed, so we just avoid to raise # an exception here, as a missing dep should not make the test # fail. if "build" in str(e.args): # System python probably doesn't have build available! pass elif "toml" in str(e.args): # System python probably doesn't have toml available! pass else: raise def test_venv(self): """ Verifies that _get_system_python_executable() works correctly in a 'venv' (Python 3 only feature). """ # 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 venv/pyvenv, the system-wide python is returned: subprocess.check_output([ pybin, "-m", "venv", "--", os.path.join(test_dir, "venv") ]) subprocess.check_output([ os.path.join(test_dir, "venv", "bin", "pip"), "install", "-U", "pip" ]) subprocess.check_output([ os.path.join(test_dir, "venv", "bin", "pip"), "install", "-U", "build", "toml", "sh<2.0", "colorama", "appdirs", "jinja2", "packaging" ]) sys_python_path = self.run__get_system_python_executable( os.path.join(test_dir, "venv", "bin", "python") ) assert os.path.normpath(sys_python_path).startswith( os.path.normpath(pybin) ) finally: shutil.rmtree(test_dir) ================================================ FILE: tests/test_recipe.py ================================================ import os import pytest import tempfile import types import unittest import warnings from unittest import mock from pythonforandroid.build import Context from pythonforandroid.recipe import Recipe, TargetPythonRecipe, import_recipe from pythonforandroid.archs import ArchAarch_64 from pythonforandroid.bootstrap import Bootstrap from tests.test_bootstrap import BaseClassSetupBootstrap def patch_logger(level): return mock.patch('pythonforandroid.recipe.{}'.format(level)) def patch_logger_info(): return patch_logger('info') def patch_logger_debug(): return patch_logger('debug') def patch_urlretrieve(): return mock.patch('pythonforandroid.recipe.urlretrieve') class DummyRecipe(Recipe): pass class TestRecipe(unittest.TestCase): def test_recipe_dirs(self): """ Trivial `recipe_dirs()` test. Makes sure the list is not empty and has the root directory. """ ctx = Context() recipes_dir = Recipe.recipe_dirs(ctx) # by default only the root dir `recipes` directory self.assertEqual(len(recipes_dir), 1) self.assertTrue(recipes_dir[0].startswith(ctx.root_dir)) def test_list_recipes(self): """ Trivial test verifying list_recipes returns a generator with some recipes. """ ctx = Context() recipes = Recipe.list_recipes(ctx) self.assertTrue(isinstance(recipes, types.GeneratorType)) recipes = list(recipes) self.assertIn('python3', recipes) def test_get_recipe(self): """ Makes sure `get_recipe()` returns a `Recipe` object when possible. """ ctx = Context() recipe_name = 'python3' recipe = Recipe.get_recipe(recipe_name, ctx) self.assertTrue(isinstance(recipe, Recipe)) self.assertEqual(recipe.name, recipe_name) recipe_name = 'does_not_exist' with self.assertRaises(ValueError) as e: Recipe.get_recipe(recipe_name, ctx) self.assertEqual( e.exception.args[0], 'Recipe does not exist: {}'.format(recipe_name)) def test_import_recipe(self): """ Verifies we can dynamically import a recipe without warnings. """ p4a_root_dir = os.path.dirname(os.path.dirname(__file__)) name = 'pythonforandroid.recipes.python3' pathname = os.path.join( *([p4a_root_dir] + name.split('.') + ['__init__.py']) ) with warnings.catch_warnings(record=True) as recorded_warnings: warnings.simplefilter("always") module = import_recipe(name, pathname) assert module is not None assert recorded_warnings == [] def test_download_if_necessary(self): """ Download should happen via `Recipe.download()` only if the recipe specific environment variable is not set. """ # download should happen as the environment variable is not set recipe = DummyRecipe() recipe.ctx = Context() recipe.ctx._ndk_api = 36 with mock.patch.object(Recipe, 'download') as m_download: recipe.download_if_necessary() assert m_download.call_args_list == [mock.call()] # after setting it the download should be skipped env_var = 'P4A_test_recipe_DIR' env_dict = {env_var: '1'} with mock.patch.object(Recipe, 'download') as m_download, mock.patch.dict(os.environ, env_dict): recipe.download_if_necessary() assert m_download.call_args_list == [] def test_download_url_not_set(self): """ Verifies that no download happens when URL is not set. """ recipe = DummyRecipe() with patch_logger_info() as m_info: recipe.download() assert m_info.call_args_list == [ mock.call('Skipping test_recipe download as no URL is set')] @staticmethod def get_dummy_python_recipe_for_download_tests(): """ Helper method for creating a test recipe used in download tests. """ recipe = DummyRecipe() filename = 'Python-3.7.4.tgz' url = 'https://www.python.org/ftp/python/3.7.4/{}'.format(filename) recipe._url = url recipe.ctx = Context() return recipe, filename def test_download_url_is_set(self): """ Verifies the actual download gets triggered when the URL is set. """ recipe, filename = self.get_dummy_python_recipe_for_download_tests() url = recipe.url with ( patch_logger_debug()) as m_debug, ( mock.patch.object(Recipe, 'download_file')) as m_download_file, ( mock.patch('pythonforandroid.recipe.touch')) as m_touch, ( tempfile.TemporaryDirectory()) as temp_dir: recipe.ctx.setup_dirs(temp_dir) recipe.download() assert m_download_file.call_args_list == [mock.call(url, filename)] assert m_debug.call_args_list == [ mock.call( 'Downloading test_recipe from ' 'https://www.python.org/ftp/python/3.7.4/Python-3.7.4.tgz')] assert m_touch.call_count == 1 def test_download_file_scheme_https(self): """ Verifies `urlretrieve()` is being called on https downloads. """ recipe, filename = self.get_dummy_python_recipe_for_download_tests() url = recipe.url with ( patch_urlretrieve()) as m_urlretrieve, ( tempfile.TemporaryDirectory()) as temp_dir: recipe.ctx.setup_dirs(temp_dir) assert recipe.download_file(url, filename) == filename assert m_urlretrieve.call_args_list == [ mock.call(url, filename, mock.ANY) ] def test_download_file_scheme_https_oserror(self): """ Checks `urlretrieve()` is being retried on `OSError`. After a number of retries the exception is re-reaised. """ recipe, filename = self.get_dummy_python_recipe_for_download_tests() url = recipe.url with ( patch_urlretrieve()) as m_urlretrieve, ( mock.patch('pythonforandroid.recipe.time.sleep')) as m_sleep, ( pytest.raises(OSError)), ( tempfile.TemporaryDirectory()) as temp_dir: recipe.ctx.setup_dirs(temp_dir) m_urlretrieve.side_effect = OSError assert recipe.download_file(url, filename) == filename retry = 5 expected_call_args_list = [mock.call(url, filename, mock.ANY)] * retry assert m_urlretrieve.call_args_list == expected_call_args_list expected_call_args_list = [mock.call(2**i) for i in range(retry - 1)] assert m_sleep.call_args_list == expected_call_args_list class TestTargetPythonRecipe(unittest.TestCase): def test_major_minor_version_string(self): """ Test that the major_minor_version_string property returns the correct string. """ class DummyTargetPythonRecipe(TargetPythonRecipe): version = '1.2.3' recipe = DummyTargetPythonRecipe() assert recipe.major_minor_version_string == '1.2' class TestLibraryRecipe(BaseClassSetupBootstrap, unittest.TestCase): 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().setUp() self.ctx.bootstrap = Bootstrap().get_bootstrap('sdl2', self.ctx) self.setUp_distribution_with_bootstrap(self.ctx.bootstrap) def test_built_libraries(self): """The openssl recipe is a library recipe, so it should have set the attribute `built_libraries`, but not the case of `pyopenssl` recipe. """ recipe = Recipe.get_recipe('openssl', self.ctx) self.assertTrue(recipe.built_libraries) recipe = Recipe.get_recipe('pyopenssl', self.ctx) self.assertFalse(recipe.built_libraries) @mock.patch('pythonforandroid.recipe.exists') def test_should_build(self, mock_exists): # avoid trying to find the recipe in a non-existing storage directory self.ctx.storage_dir = None arch = ArchAarch_64(self.ctx) recipe = Recipe.get_recipe('openssl', self.ctx) recipe.ctx = self.ctx self.assertFalse(recipe.should_build(arch)) mock_exists.return_value = False self.assertTrue(recipe.should_build(arch)) @mock.patch('pythonforandroid.recipe.Recipe.get_libraries') @mock.patch('pythonforandroid.recipe.Recipe.install_libs') def test_install_libraries(self, mock_install_libs, mock_get_libraries): mock_get_libraries.return_value = { '/build_lib/libsample1.so', '/build_lib/libsample2.so', } self.ctx.recipe_build_order = [ "hostpython3", "openssl", "python3", "sdl2", "kivy", ] arch = ArchAarch_64(self.ctx) recipe = Recipe.get_recipe('openssl', self.ctx) recipe.install_libraries(arch) mock_install_libs.assert_called_once_with( arch, *mock_get_libraries.return_value ) class TesSTLRecipe(BaseClassSetupBootstrap, unittest.TestCase): def setUp(self): """ Initialize a Context with a Bootstrap and a Distribution to properly test a recipe which depends on android's STL library, to do so we reuse `BaseClassSetupBootstrap` """ 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) @mock.patch('shutil.which') @mock.patch('pythonforandroid.build.ensure_dir') def test_get_recipe_env_with( self, mock_ensure_dir, mock_shutil_which ): """ Test that :meth:`~pythonforandroid.recipe.STLRecipe.get_recipe_env` returns some expected keys and values. .. note:: We don't check all the env variables, only those one specific of :class:`~pythonforandroid.recipe.STLRecipe`, the others should be tested in the proper test. """ expected_compiler = ( f"/opt/android/android-ndk/toolchains/" f"llvm/prebuilt/{self.ctx.ndk.host_tag}/bin/clang" ) mock_shutil_which.return_value = expected_compiler arch = ArchAarch_64(self.ctx) recipe = Recipe.get_recipe('libgeos', self.ctx) assert recipe.need_stl_shared, True env = recipe.get_recipe_env(arch) # check that the mocks have been called mock_ensure_dir.assert_called() mock_shutil_which.assert_called_once_with( expected_compiler, path=self.ctx.env['PATH'] ) self.assertIsInstance(env, dict) @mock.patch('pythonforandroid.recipe.Recipe.install_libs') @mock.patch('pythonforandroid.recipe.isfile') @mock.patch('pythonforandroid.build.ensure_dir') def test_install_stl_lib( self, mock_ensure_dir, mock_isfile, mock_install_lib ): """ Test that :meth:`~pythonforandroid.recipe.STLRecipe.install_stl_lib`, calls the method :meth:`~pythonforandroid.recipe.Recipe.install_libs` with the proper arguments: a subclass of :class:`~pythonforandroid.archs.Arch` and our stl lib (:attr:`~pythonforandroid.recipe.STLRecipe.stl_lib_name`) """ mock_isfile.return_value = False arch = ArchAarch_64(self.ctx) recipe = Recipe.get_recipe('libgeos', self.ctx) recipe.ctx = self.ctx assert recipe.need_stl_shared, True recipe.install_stl_lib(arch) mock_install_lib.assert_called_once_with( arch, os.path.join(arch.ndk_lib_dir, f"lib{recipe.stl_lib_name}.so"), ) mock_ensure_dir.assert_called() @mock.patch('pythonforandroid.recipe.Recipe.install_stl_lib') def test_postarch_build(self, mock_install_stl_lib): arch = ArchAarch_64(self.ctx) recipe = Recipe.get_recipe('libgeos', self.ctx) assert recipe.need_stl_shared, True recipe.postbuild_arch(arch) mock_install_stl_lib.assert_called_once_with(arch) def test_recipe_download_headers(self): """Download header can be created on the fly using environment variables.""" recipe = DummyRecipe() with mock.patch.dict(os.environ, {f'DOWNLOAD_HEADERS_{recipe.name}': '[["header1","foo"],["header2", "bar"]]'}): download_headers = recipe.download_headers assert download_headers == [("header1", "foo"), ("header2", "bar")] ================================================ FILE: tests/test_recommendations.py ================================================ import unittest from os.path import join from sys import version as py_version import packaging.version from unittest import mock from pythonforandroid.recommendations import ( check_ndk_api, check_ndk_version, check_target_api, read_ndk_version, check_python_version, print_recommendations, MAX_NDK_VERSION, RECOMMENDED_NDK_VERSION, RECOMMENDED_TARGET_API, MIN_NDK_API, MIN_NDK_VERSION, NDK_DOWNLOAD_URL, ARMEABI_MAX_TARGET_API, MIN_TARGET_API, UNKNOWN_NDK_MESSAGE, PARSE_ERROR_NDK_MESSAGE, READ_ERROR_NDK_MESSAGE, ENSURE_RIGHT_NDK_MESSAGE, NDK_LOWER_THAN_SUPPORTED_MESSAGE, UNSUPPORTED_NDK_API_FOR_ARMEABI_MESSAGE, CURRENT_NDK_VERSION_MESSAGE, RECOMMENDED_NDK_VERSION_MESSAGE, TARGET_NDK_API_GREATER_THAN_TARGET_API_MESSAGE, OLD_NDK_API_MESSAGE, NEW_NDK_MESSAGE, OLD_API_MESSAGE, MIN_PYTHON_MAJOR_VERSION, MIN_PYTHON_MINOR_VERSION, PY2_ERROR_TEXT, PY_VERSION_ERROR_TEXT, ) from pythonforandroid.util import BuildInterruptingException running_in_py2 = int(py_version[0]) < 3 class TestRecommendations(unittest.TestCase): """ An inherited class of `unittest.TestCase`to test the module :mod:`~pythonforandroid.recommendations`. """ def setUp(self): self.ndk_dir = "/opt/android/android-ndk" @unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+") @mock.patch("pythonforandroid.recommendations.read_ndk_version") def test_check_ndk_version_greater_than_recommended(self, mock_read_ndk): _version_string = f"{MIN_NDK_VERSION + 1}.0.5232133" mock_read_ndk.return_value = packaging.version.Version(_version_string) with self.assertLogs(level="INFO") as cm: check_ndk_version(self.ndk_dir) mock_read_ndk.assert_called_once_with(self.ndk_dir) self.assertEqual( cm.output, [ "INFO:p4a:[INFO]: {}".format( CURRENT_NDK_VERSION_MESSAGE.format( ndk_version=MAX_NDK_VERSION + 1 ) ), "WARNING:p4a:[WARNING]: {}".format( RECOMMENDED_NDK_VERSION_MESSAGE.format( recommended_ndk_version=RECOMMENDED_NDK_VERSION ) ), "WARNING:p4a:[WARNING]: {}".format(NEW_NDK_MESSAGE), ], ) @mock.patch("pythonforandroid.recommendations.read_ndk_version") def test_check_ndk_version_lower_than_recommended(self, mock_read_ndk): _version_string = f"{MIN_NDK_VERSION - 1}.0.5232133" mock_read_ndk.return_value = packaging.version.Version(_version_string) with self.assertRaises(BuildInterruptingException) as e: check_ndk_version(self.ndk_dir) self.assertEqual( e.exception.args[0], NDK_LOWER_THAN_SUPPORTED_MESSAGE.format( min_supported=MIN_NDK_VERSION, ndk_url=NDK_DOWNLOAD_URL ), ) mock_read_ndk.assert_called_once_with(self.ndk_dir) @unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+") def test_check_ndk_version_error(self): """ Test that a fake ndk dir give us two messages: - first should be an `INFO` log - second should be an `WARNING` log """ with self.assertLogs(level="INFO") as cm: check_ndk_version(self.ndk_dir) self.assertEqual( cm.output, [ "INFO:p4a:[INFO]: {}".format(UNKNOWN_NDK_MESSAGE), "WARNING:p4a:[WARNING]: {}".format( READ_ERROR_NDK_MESSAGE.format(ndk_dir=self.ndk_dir) ), "WARNING:p4a:[WARNING]: {}".format( ENSURE_RIGHT_NDK_MESSAGE.format( min_supported=MIN_NDK_VERSION, rec_version=RECOMMENDED_NDK_VERSION, ndk_url=NDK_DOWNLOAD_URL, ) ), ], ) @mock.patch("pythonforandroid.recommendations.open") def test_read_ndk_version(self, mock_open_src_prop): mock_open_src_prop.side_effect = [ mock.mock_open( read_data="Pkg.Revision = 17.2.4988734" ).return_value ] version = read_ndk_version(self.ndk_dir) mock_open_src_prop.assert_called_once_with( join(self.ndk_dir, "source.properties") ) assert version.major == 17 assert version.minor == 2 assert version.micro == 4988734 @unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+") @mock.patch("pythonforandroid.recommendations.open") def test_read_ndk_version_error(self, mock_open_src_prop): mock_open_src_prop.side_effect = [ mock.mock_open(read_data="").return_value ] with self.assertLogs(level="INFO") as cm: version = read_ndk_version(self.ndk_dir) self.assertEqual( cm.output, ["INFO:p4a:[INFO]: {}".format(PARSE_ERROR_NDK_MESSAGE)], ) mock_open_src_prop.assert_called_once_with( join(self.ndk_dir, "source.properties") ) assert version is None def test_check_target_api_error_arch_armeabi(self): with self.assertRaises(BuildInterruptingException) as e: check_target_api(RECOMMENDED_TARGET_API, "armeabi") self.assertEqual( e.exception.args[0], UNSUPPORTED_NDK_API_FOR_ARMEABI_MESSAGE.format( req_ndk_api=RECOMMENDED_TARGET_API, max_ndk_api=ARMEABI_MAX_TARGET_API, ), ) @unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+") def test_check_target_api_warning_target_api(self): with self.assertLogs(level="INFO") as cm: check_target_api(MIN_TARGET_API - 1, MIN_TARGET_API) self.assertEqual( cm.output, [ "WARNING:p4a:[WARNING]: Target API 29 < 30", "WARNING:p4a:[WARNING]: {old_api_msg}".format( old_api_msg=OLD_API_MESSAGE ), ], ) def test_check_ndk_api_error_android_api(self): """ Given an `android api` greater than an `ndk_api`, we should get an `BuildInterruptingException`. """ ndk_api = MIN_NDK_API + 1 android_api = MIN_NDK_API with self.assertRaises(BuildInterruptingException) as e: check_ndk_api(ndk_api, android_api) self.assertEqual( e.exception.args[0], TARGET_NDK_API_GREATER_THAN_TARGET_API_MESSAGE.format( ndk_api=ndk_api, android_api=android_api ), ) @unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+") def test_check_ndk_api_warning_old_ndk(self): """ Given an `android api` lower than the supported by p4a, we should get an `BuildInterruptingException`. """ ndk_api = MIN_NDK_API - 1 android_api = RECOMMENDED_TARGET_API with self.assertLogs(level="INFO") as cm: check_ndk_api(ndk_api, android_api) self.assertEqual( cm.output, [ "WARNING:p4a:[WARNING]: {}".format( OLD_NDK_API_MESSAGE.format(MIN_NDK_API) ) ], ) def test_check_python_version(self): """With any version info lower than the minimum, we should get a BuildInterruptingException with an appropriate message. """ with mock.patch('sys.version_info') as fake_version_info: # Major version is Python 2 => exception fake_version_info.major = MIN_PYTHON_MAJOR_VERSION - 1 fake_version_info.minor = MIN_PYTHON_MINOR_VERSION with self.assertRaises(BuildInterruptingException) as context: check_python_version() assert context.exception.message == PY2_ERROR_TEXT # Major version too low => exception # Using a float valued major version just to test the logic and avoid # clashing with the Python 2 check fake_version_info.major = MIN_PYTHON_MAJOR_VERSION - 0.1 fake_version_info.minor = MIN_PYTHON_MINOR_VERSION with self.assertRaises(BuildInterruptingException) as context: check_python_version() assert context.exception.message == PY_VERSION_ERROR_TEXT # Minor version too low => exception fake_version_info.major = MIN_PYTHON_MAJOR_VERSION fake_version_info.minor = MIN_PYTHON_MINOR_VERSION - 1 with self.assertRaises(BuildInterruptingException) as context: check_python_version() assert context.exception.message == PY_VERSION_ERROR_TEXT # Version high enough => nothing interesting happens fake_version_info.major = MIN_PYTHON_MAJOR_VERSION fake_version_info.minor = MIN_PYTHON_MINOR_VERSION check_python_version() def test_print_recommendations(self): """ Simple test that the function actually runs. """ # The main failure mode is if the function tries to print a variable # that doesn't actually exist, so simply running to check all the # prints work is the most important test. print_recommendations() ================================================ FILE: tests/test_toolchain.py ================================================ import io import os import sys import pytest from unittest import mock from pythonforandroid.recipe import Recipe from pythonforandroid.toolchain import ToolchainCL from pythonforandroid.util import BuildInterruptingException def patch_sys_argv(argv): return mock.patch('sys.argv', argv) def patch_argparse_print_help(): return mock.patch('argparse.ArgumentParser.print_help') def patch_sys_stdout(): return mock.patch('sys.stdout', new_callable=io.StringIO) def raises_system_exit(): return pytest.raises(SystemExit) class TestToolchainCL: def test_help(self): """ Calling with `--help` should print help and exit 0. """ argv = ['toolchain.py', '--help', '--storage-dir=/tmp'] with patch_sys_argv(argv), raises_system_exit( ) as ex_info, patch_argparse_print_help() as m_print_help: ToolchainCL() assert ex_info.value.code == 0 assert m_print_help.call_args_list == [mock.call()] @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires python3") def test_unknown(self): """ Calling with unknown args should print help and exit 1. """ argv = ['toolchain.py', '--unknown'] with patch_sys_argv(argv), raises_system_exit( ) as ex_info, patch_argparse_print_help() as m_print_help: ToolchainCL() assert ex_info.value.code == 1 assert m_print_help.call_args_list == [mock.call()] def test_create(self): """ Basic `create` distribution test. """ argv = [ 'toolchain.py', 'create', '--sdk-dir=/tmp/android-sdk', '--ndk-dir=/tmp/android-ndk', '--bootstrap=service_only', '--requirements=python3', '--dist-name=test_toolchain', '--activity-class-name=abc.myapp.android.CustomPythonActivity', '--service-class-name=xyz.myapp.android.CustomPythonService', '--arch=arm64-v8a', '--arch=armeabi-v7a' ] with patch_sys_argv(argv), mock.patch( 'pythonforandroid.build.get_available_apis' ) as m_get_available_apis, mock.patch( 'pythonforandroid.toolchain.build_recipes' ) as m_build_recipes, mock.patch( 'pythonforandroid.bootstraps.service_only.' 'ServiceOnlyBootstrap.assemble_distribution' ) as m_run_distribute: m_get_available_apis.return_value = [33] tchain = ToolchainCL() assert tchain.ctx.activity_class_name == 'abc.myapp.android.CustomPythonActivity' assert tchain.ctx.service_class_name == 'xyz.myapp.android.CustomPythonService' assert m_get_available_apis.call_args_list in [ [mock.call('/tmp/android-sdk')], # linux case [mock.call('/private/tmp/android-sdk')] # macos case ] build_order = [ 'hostpython3', 'libffi', 'openssl', 'sqlite3', 'python3', 'genericndkbuild', 'pyjnius', 'android', ] python_modules = ['six'] context = mock.ANY project_dir = None assert m_build_recipes.call_args_list == [ mock.call( build_order, python_modules, context, project_dir, ignore_project_setup_py=False ) ] assert m_run_distribute.call_args_list == [mock.call()] @mock.patch( 'pythonforandroid.build.environ', # Make sure that no environ variable modifies `sdk_dir` {'ANDROIDSDK': None, 'ANDROID_HOME': None}) def test_create_no_sdk_dir(self): """ The `--sdk-dir` is mandatory to `create` a distribution. """ argv = ['toolchain.py', 'create', '--arch=arm64-v8a', '--arch=armeabi-v7a'] with patch_sys_argv(argv), pytest.raises( BuildInterruptingException ) as ex_info: ToolchainCL() assert ex_info.value.message == ( 'Android SDK dir was not specified, exiting.') @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires python3") def test_recipes(self): """ Checks the `recipes` command prints out recipes information without crashing. """ argv = ['toolchain.py', 'recipes'] with patch_sys_argv(argv), patch_sys_stdout() as m_stdout: ToolchainCL() # check if we have common patterns in the output expected_strings = ( 'conflicts:', 'depends:', 'kivy', 'optional depends:', 'python3', 'sdl2', ) for expected_string in expected_strings: assert expected_string in m_stdout.getvalue() # deletes static attribute to not mess with other tests del Recipe.recipes def test_local_recipes_dir(self): """ Checks the `local_recipes` attribute in the Context is absolute. """ cwd = os.path.realpath(os.getcwd()) common_args = [ 'toolchain.py', 'recommendations', ] # Check the default ./p4a-recipes becomes absolute. argv = common_args with patch_sys_argv(argv): toolchain = ToolchainCL() expected_local_recipes = os.path.join(cwd, 'p4a-recipes') assert toolchain.ctx.local_recipes == expected_local_recipes # Check a supplied relative directory becomes absolute. argv = common_args + ['--local-recipes=foo'] with patch_sys_argv(argv): toolchain = ToolchainCL() expected_local_recipes = os.path.join(cwd, 'foo') assert toolchain.ctx.local_recipes == expected_local_recipes # An absolute directory should remain unchanged. local_recipes = os.path.join(cwd, 'foo') argv = common_args + ['--local-recipes={}'.format(local_recipes)] with patch_sys_argv(argv): toolchain = ToolchainCL() assert toolchain.ctx.local_recipes == local_recipes ================================================ FILE: tests/test_util.py ================================================ import os from pathlib import Path from tempfile import TemporaryDirectory import types import unittest from unittest import mock from pythonforandroid import util class TestUtil(unittest.TestCase): """ An inherited class of `unittest.TestCase`to test the module :mod:`~pythonforandroid.util`. """ @mock.patch("pythonforandroid.util.makedirs") def test_ensure_dir(self, mock_makedirs): """ Basic test for method :meth:`~pythonforandroid.util.ensure_dir`. Here we make sure that the mentioned method is called only once. """ util.ensure_dir("fake_directory") mock_makedirs.assert_called_once_with("fake_directory") @mock.patch("shutil.rmtree") @mock.patch("pythonforandroid.util.mkdtemp") def test_temp_directory(self, mock_mkdtemp, mock_shutil_rmtree): """ Basic test for method :meth:`~pythonforandroid.util.temp_directory`. We perform this test by `mocking` the command `mkdtemp` and `shutil.rmtree` and we make sure that those functions are called in the proper place. """ mock_mkdtemp.return_value = "/temp/any_directory" with util.temp_directory(): mock_mkdtemp.assert_called_once() mock_shutil_rmtree.assert_not_called() mock_shutil_rmtree.assert_called_once_with("/temp/any_directory") @mock.patch("pythonforandroid.util.chdir") def test_current_directory(self, moch_chdir): """ Basic test for method :meth:`~pythonforandroid.util.current_directory`. We `mock` chdir and we check that the command is executed once we are inside a python's `with` statement. Then we check that `chdir has been called with the proper arguments inside this `with` statement and also that, once we leave the `with` statement, is called again with the current working path. """ chdir_dir = "/temp/any_directory" # test chdir to existing directory with util.current_directory(chdir_dir): moch_chdir.assert_called_once_with("/temp/any_directory") moch_chdir.assert_has_calls( [mock.call("/temp/any_directory"), mock.call(os.getcwd())] ) def test_current_directory_exception(self): """ Another test for method :meth:`~pythonforandroid.util.current_directory`, but here we check that using the method with a non-existing-directory raises an `OSError` exception. .. note:: test chdir to non-existing directory, should raise error, for py3 the exception is FileNotFoundError and IOError for py2, to avoid introduce conditions, we test with a more generic exception """ with self.assertRaises(OSError), util.current_directory( "/fake/directory" ): pass @mock.patch("pythonforandroid.util.walk") def test_walk_valid_filens(self, mock_walk): """ Test method :meth:`~pythonforandroid.util.walk_valid_filens` In here we simulate the following directory structure: /fake_dir |-- README |-- setup.py |-- __pycache__ |-- |__ |__Lib |-- abc.pyc |-- abc.py |__ ctypes |-- util.pyc |-- util.py Then we execute the method in order to check that we got the expected result, which should be: .. code-block:: python :emphasize-lines: 2-4 expected_result = { "/fake_dir/README", "/fake_dir/Lib/abc.pyc", "/fake_dir/Lib/ctypes/util.pyc", } """ simulated_walk_result = [ ["/fake_dir", ["__pycache__", "Lib"], ["README", "setup.py"]], ["/fake_dir/Lib", ["ctypes"], ["abc.pyc", "abc.py"]], ["/fake_dir/Lib/ctypes", [], ["util.pyc", "util.py"]], ] mock_walk.return_value = simulated_walk_result file_ens = util.walk_valid_filens( "/fake_dir", ["__pycache__"], ["*.py"] ) self.assertIsInstance(file_ens, types.GeneratorType) expected_result = { "/fake_dir/README", "/fake_dir/Lib/abc.pyc", "/fake_dir/Lib/ctypes/util.pyc", } result = set(file_ens) self.assertEqual(result, expected_result) def test_util_exceptions(self): """ Test exceptions for a couple of methods: - method :meth:`~pythonforandroid.util.BuildInterruptingException` - method :meth:`~pythonforandroid.util.handle_build_exception` Here we create an exception with method :meth:`~pythonforandroid.util.BuildInterruptingException` and we run it inside method :meth:`~pythonforandroid.util.handle_build_exception` to make sure that it raises an `SystemExit`. """ exc = util.BuildInterruptingException( "missing dependency xxx", instructions="pip install --user xxx" ) with self.assertRaises(SystemExit): util.handle_build_exception(exc) def test_move(self): with mock.patch( "pythonforandroid.util.LOGGER" ) as m_logger, TemporaryDirectory() as base_dir: new_path = Path(base_dir) / "new" # Set up source old_path = Path(base_dir) / "old" with open(old_path, "w") as outfile: outfile.write("Temporary content") # Non existent source with self.assertRaises(FileNotFoundError): util.move(new_path, new_path) m_logger.debug.assert_called() m_logger.error.assert_not_called() m_logger.reset_mock() assert old_path.exists() assert not new_path.exists() # Successful move util.move(old_path, new_path) assert not old_path.exists() assert new_path.exists() m_logger.debug.assert_called() m_logger.error.assert_not_called() m_logger.reset_mock() # Move over existing: existing_path = Path(base_dir) / "existing" existing_path.touch() util.move(new_path, existing_path) with open(existing_path, "r") as infile: assert infile.read() == "Temporary content" m_logger.debug.assert_called() m_logger.error.assert_not_called() m_logger.reset_mock() def test_touch(self): # Just checking the new file case. # Assume the existing file timestamp case will work if this does. with TemporaryDirectory() as base_dir: new_file_path = Path(base_dir) / "new_file" assert not new_file_path.exists() util.touch(new_file_path) assert new_file_path.exists() def test_build_tools_version_sort_key(self): build_tools_versions = [ "26.0.1", "26.0.0", "26.0.2", "32.0.0 rc1", "31.0.0", "999something", ] expected_result = [ "999something", # invalid version "26.0.0", "26.0.1", "26.0.2", "31.0.0", "32.0.0 rc1", ] result = sorted( build_tools_versions, key=util.build_tools_version_sort_key ) self.assertEqual(result, expected_result) def test_max_build_tool_version(self): build_tools_versions = [ "26.0.1", "26.0.0", "26.0.2", "32.0.0 rc1", "31.0.0", "999something", ] expected_result = "32.0.0 rc1" result = util.max_build_tool_version(build_tools_versions) self.assertEqual(result, expected_result) def test_load_source(self): """ Test method :meth:`~pythonforandroid.util.load_source`. We test loading a Python module from a file path using importlib. """ with TemporaryDirectory() as temp_dir: # Create a test module file test_module_path = Path(temp_dir) / "test_module.py" with open(test_module_path, "w") as f: f.write("TEST_VALUE = 42\n") f.write("def test_function():\n") f.write(" return 'hello'\n") # Load the module loaded_module = util.load_source("test_module", str(test_module_path)) # Verify the module was loaded correctly self.assertEqual(loaded_module.TEST_VALUE, 42) self.assertEqual(loaded_module.test_function(), 'hello') @mock.patch("pythonforandroid.util.exists") @mock.patch("shutil.rmtree") def test_rmdir_exists(self, mock_rmtree, mock_exists): """ Test method :meth:`~pythonforandroid.util.rmdir` when directory exists. We mock exists to return True and verify rmtree is called. """ mock_exists.return_value = True util.rmdir("/fake/directory") mock_rmtree.assert_called_once_with("/fake/directory", False) @mock.patch("pythonforandroid.util.exists") @mock.patch("shutil.rmtree") def test_rmdir_not_exists(self, mock_rmtree, mock_exists): """ Test method :meth:`~pythonforandroid.util.rmdir` when directory doesn't exist. We mock exists to return False and verify rmtree is not called. """ mock_exists.return_value = False util.rmdir("/fake/directory") mock_rmtree.assert_not_called() @mock.patch("pythonforandroid.util.exists") @mock.patch("shutil.rmtree") def test_rmdir_ignore_errors(self, mock_rmtree, mock_exists): """ Test method :meth:`~pythonforandroid.util.rmdir` with ignore_errors flag. We verify that the ignore_errors parameter is passed to rmtree. """ mock_exists.return_value = True util.rmdir("/fake/directory", ignore_errors=True) mock_rmtree.assert_called_once_with("/fake/directory", True) @mock.patch("pythonforandroid.util.mock") def test_patch_wheel_setuptools_logging(self, mock_mock): """ Test method :meth:`~pythonforandroid.util.patch_wheel_setuptools_logging`. We verify it returns a mock.patch object for the wheel logging module. """ mock_patch_obj = mock.Mock() mock_mock.patch.return_value = mock_patch_obj result = util.patch_wheel_setuptools_logging() mock_mock.patch.assert_called_once_with("wheel._setuptools_logging.configure") self.assertEqual(result, mock_patch_obj) ================================================ FILE: tox.ini ================================================ [tox] envlist = pep8,py3 basepython = python3 [testenv] deps = pytest py3: coveralls # posargs will be replaced by the tox args, so you can override pytest # args e.g. `tox -- tests/test_graph.py` commands = pytest {posargs:tests/} passenv = GITHUB_* setenv = PYTHONPATH={toxinidir} SKIP_PREREQUISITES_CHECK=1 [testenv:py3] # for py3 env we will get code coverage commands = coverage run --branch --source=pythonforandroid -m pytest {posargs:tests/} coverage report -m [testenv:pep8] deps = flake8 commands = flake8 pythonforandroid/ tests/ ci/ setup.py [flake8] ignore = # Closing bracket does not match indentation of opening bracket's line E123, # Closing bracket does not match visual indentation E124, # Continuation line over-indented for hanging indent E126, # Missing whitespace around arithmetic operator E226, # Module level import not at top of file E402, # Line too long (82 > 79 characters) E501, # Line break occurred before a binary operator W503, # Line break occurred after a binary operator W504