From 5b6ed287aee364777681cc5a68e1929dc738628e Mon Sep 17 00:00:00 2001 From: germn Date: Thu, 8 Dec 2016 21:22:17 +0300 Subject: [PATCH 0001/1138] Add ffpyplayer and dependencies recipes for new toolchain. --- pythonforandroid/recipes/ffmpeg/__init__.py | 147 ++++++++++++++---- .../patches/fix-libshine-configure.patch | 11 ++ .../recipes/ffmpeg/settings.patch | 19 --- .../recipes/ffpyplayer/__init__.py | 28 ++++ .../recipes/ffpyplayer_codecs/__init__.py | 10 ++ pythonforandroid/recipes/libshine/__init__.py | 29 ++++ pythonforandroid/recipes/libx264/__init__.py | 31 ++++ 7 files changed, 228 insertions(+), 47 deletions(-) create mode 100644 pythonforandroid/recipes/ffmpeg/patches/fix-libshine-configure.patch delete mode 100644 pythonforandroid/recipes/ffmpeg/settings.patch create mode 100644 pythonforandroid/recipes/ffpyplayer/__init__.py create mode 100644 pythonforandroid/recipes/ffpyplayer_codecs/__init__.py create mode 100644 pythonforandroid/recipes/libshine/__init__.py create mode 100644 pythonforandroid/recipes/libx264/__init__.py diff --git a/pythonforandroid/recipes/ffmpeg/__init__.py b/pythonforandroid/recipes/ffmpeg/__init__.py index 221a33af9e..eaca9a6f90 100644 --- a/pythonforandroid/recipes/ffmpeg/__init__.py +++ b/pythonforandroid/recipes/ffmpeg/__init__.py @@ -1,43 +1,134 @@ -from pythonforandroid.toolchain import Recipe, shprint, shutil, current_directory -from os.path import join, exists +from pythonforandroid.toolchain import Recipe, shprint, current_directory, ArchARM +from os.path import exists, join, realpath +from os import uname +import glob import sh +import os +import shutil -""" -FFmpeg for Android compiled with x264, libass, fontconfig, freetype, fribidi and lame (Supports Android 4.1+) -http://writingminds.github.io/ffmpeg-android/ -""" class FFMpegRecipe(Recipe): + version = '2.8.8' + url = 'http://ffmpeg.org/releases/ffmpeg-{version}.tar.bz2' + depends = ['openssl', 'ffpyplayer_codecs'] # TODO should be opts_depends + patches = ['patches/fix-libshine-configure.patch'] - version = 'master' - url = 'git+https://github.com/WritingMinds/ffmpeg-android.git' - patches = ['settings.patch'] + # TODO add should_build(self, arch) + def prebuild_arch(self, arch): + self.apply_patches(arch) - def should_build(self, arch): - return not exists(self.get_build_bin(arch)) - + def get_recipe_env(self,arch): + env = super(FFMpegRecipe, self).get_recipe_env(arch) + env['NDK'] = self.ctx.ndk_dir + return env def build_arch(self, arch): - super(FFMpegRecipe, self).build_arch(arch) - env = self.get_recipe_env(arch) - build_dir = self.get_build_dir(arch.arch) - with current_directory(build_dir): - bash = sh.Command('bash') - shprint(bash, 'init_update_libs.sh') - shprint(bash, 'android_build.sh', _env=env) + with current_directory(self.get_build_dir(arch.arch)): + env = arch.get_env() + flags = ['--disable-everything'] + cflags = [] + ldflags = [] - def get_build_bin(self, arch): - build_dir = self.get_build_dir(arch.arch) - return join(build_dir, 'build', arch.arch, 'bin', 'ffmpeg') + if 'openssl' in self.ctx.recipe_build_order: + flags += [ + '--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] + if 'ffpyplayer_codecs' in self.ctx.recipe_build_order: + # libx264 + flags += ['--enable-libx264'] + build_dir = Recipe.get_recipe('libx264', self.ctx).get_build_dir(arch.arch) + cflags += ['-I' + build_dir + '/include/'] + ldflags += ['-lx264', '-L' + build_dir + '/lib/'] - def get_recipe_env(self, arch): - env = super(FFMpegRecipe, self).get_recipe_env(arch) - env['ANDROID_NDK'] = self.ctx.ndk_dir - env['ANDROID_API'] = str(self.ctx.android_api) - return env + # 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/'] + + # Enable all codecs: + flags += [ + '--enable-parsers', + '--enable-decoders', + '--enable-encoders', + '--enable-muxers', + '--enable-demuxers', + ] + else: + # Enable codecs only for .mp4: + flags += [ + '--enable-parser=h264,aac', + '--enable-decoder=h263,h264,aac', + ] + + # disable some unused algo + # note: "golomb" are the one used in our video test, so don't use --disable-golomb + # note: and for aac decoding: "rdft", "mdct", and "fft" are needed + flags += [ + '--disable-dxva2 --disable-vdpau --disable-vaapi', + '--disable-dct', + ] + + # 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 binaries / doc + flags += [ + '--disable-ffmpeg', + '--disable-ffplay', + '--disable-ffprobe', + '--disable-ffserver', + '--disable-doc', + ] + + # other flags: + flags += [ + '--enable-filter=aresample,resample,crop,adelay,volume', + '--enable-protocol=file,http', + '--enable-small', + '--enable-hwaccels', + '--enable-gpl', + '--enable-pic', + '--disable-static', + '--enable-shared', + ] + + # android: + flags += [ + '--target-os=android', + '--cross-prefix=arm-linux-androideabi-', + '--arch=arm', + '--sysroot=' + self.ctx.ndk_platform, + '--enable-neon', + '--prefix={}'.format(realpath('.')), + ] + cflags = [ + '-march=armv7-a', + '-mfpu=vfpv3-d16', + '-mfloat-abi=softfp', + '-fPIC', + '-DANDROID', + ] + cflags + + env['CFLAGS'] += ' ' + ' '.join(cflags) + env['LDFLAGS'] += ' ' + ' '.join(ldflags) + configure = sh.Command('./configure') + shprint(configure, *flags, _env=env) + shprint(sh.make, '-j4', _env=env) + shprint(sh.make, 'install', _env=env) + # copy libs: + sh.cp('-a', sh.glob('./lib/lib*.so'), self.ctx.get_libs_dir(arch.arch)) -recipe = FFMpegRecipe() \ No newline at end of file +recipe = FFMpegRecipe() diff --git a/pythonforandroid/recipes/ffmpeg/patches/fix-libshine-configure.patch b/pythonforandroid/recipes/ffmpeg/patches/fix-libshine-configure.patch new file mode 100644 index 0000000000..4be8a231ba --- /dev/null +++ b/pythonforandroid/recipes/ffmpeg/patches/fix-libshine-configure.patch @@ -0,0 +1,11 @@ +--- ./configure.orig 2016-09-19 04:41:33.000000000 +0300 ++++ ./configure 2016-12-06 19:12:05.046025000 +0300 +@@ -5260,7 +5260,7 @@ + enabled libquvi && require_pkg_config libquvi quvi/quvi.h quvi_init + enabled librtmp && require_pkg_config librtmp librtmp/rtmp.h RTMP_Socket + enabled libschroedinger && require_pkg_config schroedinger-1.0 schroedinger/schro.h schro_init +-enabled libshine && require_pkg_config shine shine/layer3.h shine_encode_buffer ++enabled libshine && require "shine" shine/layer3.h shine_encode_buffer -lshine + enabled libsmbclient && { use_pkg_config smbclient libsmbclient.h smbc_init || + require smbclient libsmbclient.h smbc_init -lsmbclient; } + enabled libsnappy && require snappy snappy-c.h snappy_compress -lsnappy diff --git a/pythonforandroid/recipes/ffmpeg/settings.patch b/pythonforandroid/recipes/ffmpeg/settings.patch deleted file mode 100644 index 8e3181cf7f..0000000000 --- a/pythonforandroid/recipes/ffmpeg/settings.patch +++ /dev/null @@ -1,19 +0,0 @@ ---- ffmpeg/settings.sh 2016-04-27 18:50:16.294291099 +0200 -+++ ffmpeg-patch/settings.sh 2016-04-27 18:55:07.085398617 +0200 -@@ -1,13 +1,13 @@ - #!/bin/bash - --SUPPORTED_ARCHITECTURES=(armeabi-v7a armeabi-v7a-neon x86) -+SUPPORTED_ARCHITECTURES=(${ARCH}) - ANDROID_NDK_ROOT_PATH=${ANDROID_NDK} - if [[ -z "$ANDROID_NDK_ROOT_PATH" ]]; then - echo "You need to set ANDROID_NDK environment variable, please check instructions" - exit - fi --ANDROID_API_VERSION=9 --NDK_TOOLCHAIN_ABI_VERSION=4.8 -+ANDROID_API_VERSION=${ANDROID_API} -+NDK_TOOLCHAIN_ABI_VERSION=${TOOLCHAIN_VERSION} - - NUMBER_OF_CORES=$(nproc) - HOST_UNAME=$(uname -m) diff --git a/pythonforandroid/recipes/ffpyplayer/__init__.py b/pythonforandroid/recipes/ffpyplayer/__init__.py new file mode 100644 index 0000000000..2c1e7a368e --- /dev/null +++ b/pythonforandroid/recipes/ffpyplayer/__init__.py @@ -0,0 +1,28 @@ +from pythonforandroid.toolchain import Recipe, CythonRecipe, shprint, current_directory, ArchARM +from os.path import exists, join, realpath +from os import uname +import glob +import sh +import os + + +class FFPyPlayerRecipe(CythonRecipe): + version = 'v4.0.0' + url = 'https://github.com/matham/ffpyplayer/archive/{version}.zip' + depends = ['python2', 'sdl2', 'ffmpeg'] + opt_depends = ['openssl', 'ffpyplayer_codecs'] + + def get_recipe_env(self, arch, with_flags_in_cc=True): + env = super(FFPyPlayerRecipe, self).get_recipe_env(arch) + + # TODO cannot find -lsdl error + + 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) + + 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") + return env + +recipe = FFPyPlayerRecipe() diff --git a/pythonforandroid/recipes/ffpyplayer_codecs/__init__.py b/pythonforandroid/recipes/ffpyplayer_codecs/__init__.py new file mode 100644 index 0000000000..ee70a43f69 --- /dev/null +++ b/pythonforandroid/recipes/ffpyplayer_codecs/__init__.py @@ -0,0 +1,10 @@ +from pythonforandroid.toolchain import Recipe + + +class FFPyPlayerCodecsRecipe(Recipe): + depends = ['libshine', 'libx264'] + + def build_arch(self, arch): + pass + +recipe = FFPyPlayerCodecsRecipe() diff --git a/pythonforandroid/recipes/libshine/__init__.py b/pythonforandroid/recipes/libshine/__init__.py new file mode 100644 index 0000000000..680a66de3b --- /dev/null +++ b/pythonforandroid/recipes/libshine/__init__.py @@ -0,0 +1,29 @@ +from pythonforandroid.toolchain import Recipe, shprint, current_directory, ArchARM +from os.path import exists, join, realpath +from os import uname +import glob +import sh + + +class LibShineRecipe(Recipe): + version = 'master' + url = 'https://github.com/toots/shine/archive/{version}.zip' + + # TODO add should_build(self, arch) + + 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, + '--host=arm-linux', + '--enable-pic', + '--disable-shared', + '--enable-static', + '--prefix={}'.format(realpath('.')), + _env=env) + shprint(sh.make, '-j4', _env=env) + shprint(sh.make, 'install', _env=env) + +recipe = LibShineRecipe() diff --git a/pythonforandroid/recipes/libx264/__init__.py b/pythonforandroid/recipes/libx264/__init__.py new file mode 100644 index 0000000000..79b97fabc9 --- /dev/null +++ b/pythonforandroid/recipes/libx264/__init__.py @@ -0,0 +1,31 @@ +from pythonforandroid.toolchain import Recipe, shprint, current_directory, ArchARM +from os.path import exists, join, realpath +from os import uname +import glob +import sh + + +class LibX264Recipe(Recipe): + version = 'last_stable' + url = 'http://mirror.yandex.ru/mirrors/ftp.videolan.org/x264/snapshots/{version}_x264.tar.bz2' + + # TODO add should_build(self, arch) + + 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, + '--cross-prefix=arm-linux-androideabi-', + '--host=arm-linux', + '--disable-asm', + '--disable-cli', + '--enable-pic', + '--disable-shared', + '--enable-static', + '--prefix={}'.format(realpath('.')), + _env=env) + shprint(sh.make, '-j4', _env=env) + shprint(sh.make, 'install', _env=env) + +recipe = LibX264Recipe() From b2022a096849c2c6a001da9a96c8d86cb879140a Mon Sep 17 00:00:00 2001 From: germn Date: Sun, 11 Dec 2016 16:35:16 +0300 Subject: [PATCH 0002/1138] Fix ffpyplayer==4.0.0 compile with SDL2 --- .../recipes/ffpyplayer/__init__.py | 8 ++++++-- .../patches/fix-ffpyplayer-setup.patch | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 pythonforandroid/recipes/ffpyplayer/patches/fix-ffpyplayer-setup.patch diff --git a/pythonforandroid/recipes/ffpyplayer/__init__.py b/pythonforandroid/recipes/ffpyplayer/__init__.py index 2c1e7a368e..5c9dbfa9ef 100644 --- a/pythonforandroid/recipes/ffpyplayer/__init__.py +++ b/pythonforandroid/recipes/ffpyplayer/__init__.py @@ -9,20 +9,24 @@ class FFPyPlayerRecipe(CythonRecipe): version = 'v4.0.0' url = 'https://github.com/matham/ffpyplayer/archive/{version}.zip' + md5sum = '99f4c7103bce0ecb167510fc810db82f' depends = ['python2', 'sdl2', 'ffmpeg'] opt_depends = ['openssl', 'ffpyplayer_codecs'] + patches = ['patches/fix-ffpyplayer-setup.patch'] # need this to compile with SDL2 + + def prebuild_arch(self, arch): + self.apply_patches(arch) def get_recipe_env(self, arch, with_flags_in_cc=True): env = super(FFPyPlayerRecipe, self).get_recipe_env(arch) - # TODO cannot find -lsdl error - 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) 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") + return env recipe = FFPyPlayerRecipe() diff --git a/pythonforandroid/recipes/ffpyplayer/patches/fix-ffpyplayer-setup.patch b/pythonforandroid/recipes/ffpyplayer/patches/fix-ffpyplayer-setup.patch new file mode 100644 index 0000000000..0f8642f016 --- /dev/null +++ b/pythonforandroid/recipes/ffpyplayer/patches/fix-ffpyplayer-setup.patch @@ -0,0 +1,19 @@ +--- ./setup.py.orig 2016-11-28 23:59:57.000000000 +0300 ++++ ./setup.py 2016-12-11 14:50:15.938008000 +0300 +@@ -117,11 +117,13 @@ + include_dirs = [ + environ.get("SDL_INCLUDE_DIR"), + environ.get("FFMPEG_INCLUDE_DIR")] +- ffmpeg_libdir = environ.get("FFMPEG_LIB_DIR") +- sdl = "SDL" ++ library_dirs = [ ++ environ.get("SDL_LIB_DIR"), ++ environ.get("FFMPEG_LIB_DIR")] ++ sdl = "SDL2" + libraries = ['avcodec', 'avdevice', 'avfilter', 'avformat', + 'avutil', 'swscale', 'swresample', 'postproc', +- 'sdl'] ++ 'SDL2'] + + else: + From 3b201378e0857ed19b18f46fcd762afb084bd0da Mon Sep 17 00:00:00 2001 From: germn Date: Sun, 11 Dec 2016 16:36:57 +0300 Subject: [PATCH 0003/1138] Add md5sum for deps. --- pythonforandroid/recipes/ffmpeg/__init__.py | 1 + pythonforandroid/recipes/libshine/__init__.py | 3 ++- pythonforandroid/recipes/libx264/__init__.py | 5 +++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pythonforandroid/recipes/ffmpeg/__init__.py b/pythonforandroid/recipes/ffmpeg/__init__.py index eaca9a6f90..2bcc148e5b 100644 --- a/pythonforandroid/recipes/ffmpeg/__init__.py +++ b/pythonforandroid/recipes/ffmpeg/__init__.py @@ -10,6 +10,7 @@ class FFMpegRecipe(Recipe): version = '2.8.8' url = 'http://ffmpeg.org/releases/ffmpeg-{version}.tar.bz2' + md5sum = 'afeae3b80b7e7e03db957f33a7ef20d2' depends = ['openssl', 'ffpyplayer_codecs'] # TODO should be opts_depends patches = ['patches/fix-libshine-configure.patch'] diff --git a/pythonforandroid/recipes/libshine/__init__.py b/pythonforandroid/recipes/libshine/__init__.py index 680a66de3b..0957a2f556 100644 --- a/pythonforandroid/recipes/libshine/__init__.py +++ b/pythonforandroid/recipes/libshine/__init__.py @@ -6,8 +6,9 @@ class LibShineRecipe(Recipe): - version = 'master' + version = 'b403b3e8a41377e0576d834b179a5cc7096ff548' # we need master brnch url = 'https://github.com/toots/shine/archive/{version}.zip' + md5sum = '24cf9488d06f7acf0a0fbb162cc587ab' # TODO add should_build(self, arch) diff --git a/pythonforandroid/recipes/libx264/__init__.py b/pythonforandroid/recipes/libx264/__init__.py index 79b97fabc9..704076850a 100644 --- a/pythonforandroid/recipes/libx264/__init__.py +++ b/pythonforandroid/recipes/libx264/__init__.py @@ -6,8 +6,9 @@ class LibX264Recipe(Recipe): - version = 'last_stable' - url = 'http://mirror.yandex.ru/mirrors/ftp.videolan.org/x264/snapshots/{version}_x264.tar.bz2' + version = 'x264-snapshot-20161210-2245-stable' # using mirror url since can't use ftp + url = 'http://mirror.yandex.ru/mirrors/ftp.videolan.org/x264/snapshots/{version}.tar.bz2' + md5sum = '6bcca94ae1d81ee14236ba9af42135d9' # TODO add should_build(self, arch) From 7e16b3ca4951d2734d10ae8a74d83fbf87165211 Mon Sep 17 00:00:00 2001 From: germn Date: Sun, 11 Dec 2016 16:38:33 +0300 Subject: [PATCH 0004/1138] Add should_build for deps. --- pythonforandroid/recipes/ffmpeg/__init__.py | 4 +++- pythonforandroid/recipes/libshine/__init__.py | 4 +++- pythonforandroid/recipes/libx264/__init__.py | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pythonforandroid/recipes/ffmpeg/__init__.py b/pythonforandroid/recipes/ffmpeg/__init__.py index 2bcc148e5b..810beaf475 100644 --- a/pythonforandroid/recipes/ffmpeg/__init__.py +++ b/pythonforandroid/recipes/ffmpeg/__init__.py @@ -14,7 +14,9 @@ class FFMpegRecipe(Recipe): depends = ['openssl', 'ffpyplayer_codecs'] # TODO should be opts_depends patches = ['patches/fix-libshine-configure.patch'] - # TODO add should_build(self, arch) + def should_build(self, arch): + build_dir = self.get_build_dir(arch.arch) + return not exists(join(build_dir, 'lib', 'libavcodec.so')) def prebuild_arch(self, arch): self.apply_patches(arch) diff --git a/pythonforandroid/recipes/libshine/__init__.py b/pythonforandroid/recipes/libshine/__init__.py index 0957a2f556..9a1f88cb9d 100644 --- a/pythonforandroid/recipes/libshine/__init__.py +++ b/pythonforandroid/recipes/libshine/__init__.py @@ -10,7 +10,9 @@ class LibShineRecipe(Recipe): url = 'https://github.com/toots/shine/archive/{version}.zip' md5sum = '24cf9488d06f7acf0a0fbb162cc587ab' - # TODO add should_build(self, arch) + def should_build(self, arch): + build_dir = self.get_build_dir(arch.arch) + return not exists(join(build_dir, 'lib', 'libshine.a')) def build_arch(self, arch): with current_directory(self.get_build_dir(arch.arch)): diff --git a/pythonforandroid/recipes/libx264/__init__.py b/pythonforandroid/recipes/libx264/__init__.py index 704076850a..8027ef8836 100644 --- a/pythonforandroid/recipes/libx264/__init__.py +++ b/pythonforandroid/recipes/libx264/__init__.py @@ -10,7 +10,9 @@ class LibX264Recipe(Recipe): url = 'http://mirror.yandex.ru/mirrors/ftp.videolan.org/x264/snapshots/{version}.tar.bz2' md5sum = '6bcca94ae1d81ee14236ba9af42135d9' - # TODO add should_build(self, arch) + def should_build(self, arch): + build_dir = self.get_build_dir(arch.arch) + return not exists(join(build_dir, 'lib', 'libx264.a')) def build_arch(self, arch): with current_directory(self.get_build_dir(arch.arch)): From f32632496766b07f65e13039f7f92461b6eb5028 Mon Sep 17 00:00:00 2001 From: germn Date: Sun, 11 Dec 2016 16:39:49 +0300 Subject: [PATCH 0005/1138] Typo fix. --- pythonforandroid/recipes/libshine/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/recipes/libshine/__init__.py b/pythonforandroid/recipes/libshine/__init__.py index 9a1f88cb9d..7e80d5b69d 100644 --- a/pythonforandroid/recipes/libshine/__init__.py +++ b/pythonforandroid/recipes/libshine/__init__.py @@ -6,7 +6,7 @@ class LibShineRecipe(Recipe): - version = 'b403b3e8a41377e0576d834b179a5cc7096ff548' # we need master brnch + version = 'b403b3e8a41377e0576d834b179a5cc7096ff548' # we need master branch url = 'https://github.com/toots/shine/archive/{version}.zip' md5sum = '24cf9488d06f7acf0a0fbb162cc587ab' From 73f5ba80ff10b2f7a1459b81ae4cda60957c710c Mon Sep 17 00:00:00 2001 From: germn Date: Sun, 11 Dec 2016 21:06:17 +0300 Subject: [PATCH 0006/1138] Fix recipe build order for ffpyplayer. --- pythonforandroid/recipes/ffmpeg/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pythonforandroid/recipes/ffmpeg/__init__.py b/pythonforandroid/recipes/ffmpeg/__init__.py index 810beaf475..4d6c3f5782 100644 --- a/pythonforandroid/recipes/ffmpeg/__init__.py +++ b/pythonforandroid/recipes/ffmpeg/__init__.py @@ -11,7 +11,8 @@ class FFMpegRecipe(Recipe): version = '2.8.8' url = 'http://ffmpeg.org/releases/ffmpeg-{version}.tar.bz2' md5sum = 'afeae3b80b7e7e03db957f33a7ef20d2' - depends = ['openssl', 'ffpyplayer_codecs'] # TODO should be opts_depends + depends = ['sdl2'] # Actually no, but we need this to build correct recipe order + opts_depends = ['openssl', 'ffpyplayer_codecs'] patches = ['patches/fix-libshine-configure.patch'] def should_build(self, arch): From 46a9cb396925efe3e361bfecaf87395bdb38b354 Mon Sep 17 00:00:00 2001 From: germn Date: Sun, 15 Jan 2017 00:06:55 +0300 Subject: [PATCH 0007/1138] Update ffpyplayer to master. --- .../recipes/ffpyplayer/__init__.py | 7 +------ .../patches/fix-ffpyplayer-setup.patch | 19 ------------------- 2 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 pythonforandroid/recipes/ffpyplayer/patches/fix-ffpyplayer-setup.patch diff --git a/pythonforandroid/recipes/ffpyplayer/__init__.py b/pythonforandroid/recipes/ffpyplayer/__init__.py index 5c9dbfa9ef..3ebfed6fdb 100644 --- a/pythonforandroid/recipes/ffpyplayer/__init__.py +++ b/pythonforandroid/recipes/ffpyplayer/__init__.py @@ -7,15 +7,10 @@ class FFPyPlayerRecipe(CythonRecipe): - version = 'v4.0.0' + version = 'master' url = 'https://github.com/matham/ffpyplayer/archive/{version}.zip' - md5sum = '99f4c7103bce0ecb167510fc810db82f' depends = ['python2', 'sdl2', 'ffmpeg'] opt_depends = ['openssl', 'ffpyplayer_codecs'] - patches = ['patches/fix-ffpyplayer-setup.patch'] # need this to compile with SDL2 - - def prebuild_arch(self, arch): - self.apply_patches(arch) def get_recipe_env(self, arch, with_flags_in_cc=True): env = super(FFPyPlayerRecipe, self).get_recipe_env(arch) diff --git a/pythonforandroid/recipes/ffpyplayer/patches/fix-ffpyplayer-setup.patch b/pythonforandroid/recipes/ffpyplayer/patches/fix-ffpyplayer-setup.patch deleted file mode 100644 index 0f8642f016..0000000000 --- a/pythonforandroid/recipes/ffpyplayer/patches/fix-ffpyplayer-setup.patch +++ /dev/null @@ -1,19 +0,0 @@ ---- ./setup.py.orig 2016-11-28 23:59:57.000000000 +0300 -+++ ./setup.py 2016-12-11 14:50:15.938008000 +0300 -@@ -117,11 +117,13 @@ - include_dirs = [ - environ.get("SDL_INCLUDE_DIR"), - environ.get("FFMPEG_INCLUDE_DIR")] -- ffmpeg_libdir = environ.get("FFMPEG_LIB_DIR") -- sdl = "SDL" -+ library_dirs = [ -+ environ.get("SDL_LIB_DIR"), -+ environ.get("FFMPEG_LIB_DIR")] -+ sdl = "SDL2" - libraries = ['avcodec', 'avdevice', 'avfilter', 'avformat', - 'avutil', 'swscale', 'swresample', 'postproc', -- 'sdl'] -+ 'SDL2'] - - else: - From 03eb7963c05ef2dc448d280c83ca51f7b583d962 Mon Sep 17 00:00:00 2001 From: Peter Badida Date: Mon, 16 Jan 2017 01:27:33 +0100 Subject: [PATCH 0008/1138] Fix python2 for webview new env var ANDROID_UNPACK to target a folder '/data/data//files' and for sdl2 that + '/app' to get libpymodules.so --- .../sdl2/build/src/org/kivy/android/PythonActivity.java | 7 +------ .../recipes/python2/patches/custom-loader.patch | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java index 7da0f39a1c..5153f5b4b5 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java @@ -131,12 +131,6 @@ protected void onPostExecute(String result) { // 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(); - String app_root_dir = getAppRoot(); if (getIntent() != null && getIntent().getAction() != null && getIntent().getAction().equals("org.kivy.LAUNCH")) { @@ -172,6 +166,7 @@ protected void onPostExecute(String result) { 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", mFilesDirectory + "/app"); SDLActivity.nativeSetEnv("PYTHONHOME", app_root_dir); SDLActivity.nativeSetEnv("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib"); SDLActivity.nativeSetEnv("PYTHONOPTIMIZE", "2"); diff --git a/pythonforandroid/recipes/python2/patches/custom-loader.patch b/pythonforandroid/recipes/python2/patches/custom-loader.patch index 54af221e67..f4408a0a05 100644 --- a/pythonforandroid/recipes/python2/patches/custom-loader.patch +++ b/pythonforandroid/recipes/python2/patches/custom-loader.patch @@ -17,8 +17,8 @@ + + /* Ensure we have access to libpymodules. */ + if (libpymodules == -1) { -+ printf("ANDROID_APP_PATH = %s\n", getenv("ANDROID_APP_PATH")); -+ PyOS_snprintf(pathbuf, sizeof(pathbuf), "%s/libpymodules.so", getenv("ANDROID_APP_PATH")); ++ printf("ANDROID_UNPACK = %s\n", getenv("ANDROID_UNPACK")); ++ PyOS_snprintf(pathbuf, sizeof(pathbuf), "%s/libpymodules.so", getenv("ANDROID_UNPACK")); + libpymodules = dlopen(pathbuf, RTLD_NOW); + + if (libpymodules == NULL) { From 72d3ff456eadd003de18f04ec5316c545ab02fcb Mon Sep 17 00:00:00 2001 From: Peter Badida Date: Tue, 17 Jan 2017 01:27:54 +0100 Subject: [PATCH 0009/1138] Add ANDROID_UNPACK to other bootstraps --- .../pygame/build/src/org/renpy/android/SDLSurfaceView.java | 1 + .../build/src/org/kivy/android/PythonService.java | 6 ++++-- .../service_only/build/templates/Service.tmpl.java | 1 + .../webview/build/src/org/kivy/android/PythonActivity.java | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pythonforandroid/bootstraps/pygame/build/src/org/renpy/android/SDLSurfaceView.java b/pythonforandroid/bootstraps/pygame/build/src/org/renpy/android/SDLSurfaceView.java index da7e87e6cb..d88783b488 100644 --- a/pythonforandroid/bootstraps/pygame/build/src/org/renpy/android/SDLSurfaceView.java +++ b/pythonforandroid/bootstraps/pygame/build/src/org/renpy/android/SDLSurfaceView.java @@ -706,6 +706,7 @@ public void run() { nativeResize(mWidth, mHeight); nativeInitJavaCallbacks(); nativeSetEnv("ANDROID_PRIVATE", mFilesDirectory); + nativeSetEnv("ANDROID_UNPACK", mFilesDirectory); nativeSetEnv("ANDROID_ARGUMENT", mArgument); nativeSetEnv("ANDROID_APP_PATH", mArgument); nativeSetEnv("PYTHONOPTIMIZE", "2"); diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/PythonService.java b/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/PythonService.java index df4fe0d2c8..be4fbde509 100644 --- a/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/PythonService.java +++ b/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/PythonService.java @@ -26,6 +26,7 @@ public abstract class PythonService extends Service implements Runnable { private String pythonName; private String pythonHome; private String pythonPath; + private String ANDROID_UNPACK; private String serviceEntrypoint; private String pythonServiceArgument; @@ -70,6 +71,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { pythonName = extras.getString("pythonName"); pythonHome = extras.getString("pythonHome"); pythonPath = extras.getString("pythonPath"); + ANDROID_UNPACK = extras.getString("ANDROID_UNPACK"); pythonServiceArgument = extras.getString("pythonServiceArgument"); Log.v(TAG, "Starting Python thread"); @@ -127,7 +129,7 @@ public void onDestroy() { public void run() { PythonUtil.loadLibraries(getFilesDir()); nativeStart(androidPrivate, androidArgument, serviceEntrypoint, pythonName, pythonHome, - pythonPath, pythonServiceArgument); + pythonPath, pythonServiceArgument, ANDROID_UNPACK); stopSelf(); } @@ -143,5 +145,5 @@ public void run() { public static native void nativeStart(String androidPrivate, String androidArgument, String serviceEntrypoint, String pythonName, String pythonHome, String pythonPath, - String pythonServiceArgument); + String pythonServiceArgument, String ANDROID_UNPACK); } diff --git a/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java b/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java index 31c4183449..c4ad38c50c 100644 --- a/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java +++ b/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java @@ -40,6 +40,7 @@ public static void start(Context ctx, String pythonServiceArgument) { intent.putExtra("serviceDescription", ""); intent.putExtra("pythonName", "{{ name }}"); intent.putExtra("pythonHome", argument); + intent.putExtra("ANDROID_UNPACK", argument); intent.putExtra("pythonPath", argument + ":" + argument + "/lib"); intent.putExtra("pythonServiceArgument", pythonServiceArgument); ctx.startService(intent); diff --git a/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/PythonActivity.java index efc6043f3a..194bc90a6c 100644 --- a/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/PythonActivity.java +++ b/pythonforandroid/bootstraps/webview/build/src/org/kivy/android/PythonActivity.java @@ -158,6 +158,7 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { PythonActivity.nativeSetEnv("ANDROID_PRIVATE", mFilesDirectory); PythonActivity.nativeSetEnv("ANDROID_ARGUMENT", mFilesDirectory); PythonActivity.nativeSetEnv("ANDROID_APP_PATH", mFilesDirectory); + PythonActivity.nativeSetEnv("ANDROID_UNPACK", mFilesDirectory); PythonActivity.nativeSetEnv("ANDROID_ENTRYPOINT", "main.pyo"); PythonActivity.nativeSetEnv("PYTHONHOME", mFilesDirectory); PythonActivity.nativeSetEnv("PYTHONPATH", mFilesDirectory + ":" + mFilesDirectory + "/lib"); From c255b76a37d2154484ae6f184e4767f98d4be8fc Mon Sep 17 00:00:00 2001 From: Peter Badida Date: Thu, 19 Jan 2017 22:28:03 +0100 Subject: [PATCH 0010/1138] Match java style in service_only --- .../build/src/org/kivy/android/PythonService.java | 8 ++++---- .../service_only/build/templates/Service.tmpl.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/PythonService.java b/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/PythonService.java index be4fbde509..232c8553d0 100644 --- a/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/PythonService.java +++ b/pythonforandroid/bootstraps/service_only/build/src/org/kivy/android/PythonService.java @@ -26,7 +26,7 @@ public abstract class PythonService extends Service implements Runnable { private String pythonName; private String pythonHome; private String pythonPath; - private String ANDROID_UNPACK; + private String androidUnpack; private String serviceEntrypoint; private String pythonServiceArgument; @@ -71,7 +71,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { pythonName = extras.getString("pythonName"); pythonHome = extras.getString("pythonHome"); pythonPath = extras.getString("pythonPath"); - ANDROID_UNPACK = extras.getString("ANDROID_UNPACK"); + androidUnpack = extras.getString("androidUnpack"); pythonServiceArgument = extras.getString("pythonServiceArgument"); Log.v(TAG, "Starting Python thread"); @@ -129,7 +129,7 @@ public void onDestroy() { public void run() { PythonUtil.loadLibraries(getFilesDir()); nativeStart(androidPrivate, androidArgument, serviceEntrypoint, pythonName, pythonHome, - pythonPath, pythonServiceArgument, ANDROID_UNPACK); + pythonPath, pythonServiceArgument, androidUnpack); stopSelf(); } @@ -145,5 +145,5 @@ public void run() { public static native void nativeStart(String androidPrivate, String androidArgument, String serviceEntrypoint, String pythonName, String pythonHome, String pythonPath, - String pythonServiceArgument, String ANDROID_UNPACK); + String pythonServiceArgument, String androidUnpack); } diff --git a/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java b/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java index c4ad38c50c..137181a7f9 100644 --- a/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java +++ b/pythonforandroid/bootstraps/service_only/build/templates/Service.tmpl.java @@ -40,7 +40,7 @@ public static void start(Context ctx, String pythonServiceArgument) { intent.putExtra("serviceDescription", ""); intent.putExtra("pythonName", "{{ name }}"); intent.putExtra("pythonHome", argument); - intent.putExtra("ANDROID_UNPACK", argument); + intent.putExtra("androidUnpack", argument); intent.putExtra("pythonPath", argument + ":" + argument + "/lib"); intent.putExtra("pythonServiceArgument", pythonServiceArgument); ctx.startService(intent); From a7a878b42c7f14ba4495aa405481a837950acfc2 Mon Sep 17 00:00:00 2001 From: Peter Badida Date: Sun, 29 Jan 2017 19:03:34 +0100 Subject: [PATCH 0011/1138] Fix after rebase --- .../sdl2/build/src/org/kivy/android/PythonActivity.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java index 5153f5b4b5..672502a203 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java @@ -131,6 +131,12 @@ protected void onPostExecute(String result) { // 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(); + String app_root_dir = getAppRoot(); if (getIntent() != null && getIntent().getAction() != null && getIntent().getAction().equals("org.kivy.LAUNCH")) { From dc3764cb25b860931bfb4daec054dcfb8d5743d0 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sun, 9 Apr 2017 01:02:16 +0100 Subject: [PATCH 0012/1138] Updated Kivy icons to newer logo under sdl2 --- .../sdl2/build/templates/kivy-icon.png | Bin 16525 -> 3229 bytes .../sdl2/build/templates/kivy-presplash.jpg | Bin 18251 -> 11584 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/pythonforandroid/bootstraps/sdl2/build/templates/kivy-icon.png b/pythonforandroid/bootstraps/sdl2/build/templates/kivy-icon.png index 59a00ba6fff07cec43a4100fdf22f3f679df2349..6ecb013b4434585eef667ce48c97f55265eddeec 100644 GIT binary patch delta 3227 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 L>T(q_W}*KFob!<> diff --git a/pythonforandroid/bootstraps/sdl2/build/templates/kivy-presplash.jpg b/pythonforandroid/bootstraps/sdl2/build/templates/kivy-presplash.jpg index 161ebc09284183771507c3eeb338cd5a7fefcb9d..c61efa272e88d11f0984a20a436d76028360eeb4 100644 GIT binary patch literal 11584 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�?&S>mLfAa%CJdI`D!ndU) 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 literal 18251 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 Date: Wed, 12 Apr 2017 15:50:06 -0700 Subject: [PATCH 0013/1138] support ruamel.yaml --- pythonforandroid/recipes/ruamel.yaml/__init__.py | 14 ++++++++++++++ .../recipes/ruamel.yaml/disable-pip-req.patch | 11 +++++++++++ 2 files changed, 25 insertions(+) create mode 100644 pythonforandroid/recipes/ruamel.yaml/__init__.py create mode 100644 pythonforandroid/recipes/ruamel.yaml/disable-pip-req.patch diff --git a/pythonforandroid/recipes/ruamel.yaml/__init__.py b/pythonforandroid/recipes/ruamel.yaml/__init__.py new file mode 100644 index 0000000000..fda6ec0d94 --- /dev/null +++ b/pythonforandroid/recipes/ruamel.yaml/__init__.py @@ -0,0 +1,14 @@ +from pythonforandroid.toolchain import PythonRecipe + + +class RuamelYamlRecipe(PythonRecipe): + version = '0.14.5' + url = 'https://pypi.python.org/packages/5c/13/c120a06b3add0f9763ca9190e5f6edb9faf9d34b158dd3cff7cc9097be03/ruamel.yaml-{version}.tar.gz' + + depends = [ ('python2', 'python3crystax') ] + site_packages_name = 'ruamel' + call_hostpython_via_targetpython = False + + patches = ['disable-pip-req.patch'] + +recipe = RuamelYamlRecipe() diff --git a/pythonforandroid/recipes/ruamel.yaml/disable-pip-req.patch b/pythonforandroid/recipes/ruamel.yaml/disable-pip-req.patch new file mode 100644 index 0000000000..fd3cb365cf --- /dev/null +++ b/pythonforandroid/recipes/ruamel.yaml/disable-pip-req.patch @@ -0,0 +1,11 @@ +--- setup.py 2017-03-23 05:28:37.000000000 -0700 ++++ b/setup.py 2017-04-12 15:03:28.218529255 -0700 +@@ -316,7 +316,7 @@ + os.system('pip install .') + sys.exit(0) + 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 From 94e084ef139bbde89332f4803b5dff23ab7356cc Mon Sep 17 00:00:00 2001 From: debauchery1st Date: Sun, 16 Apr 2017 16:04:45 -0400 Subject: [PATCH 0014/1138] Recipe for websocket-client --- .../recipes/websocket-client/__init__.py | 12 +++++++ .../recipes/websocket-client/websocket.patch | 33 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 pythonforandroid/recipes/websocket-client/__init__.py create mode 100644 pythonforandroid/recipes/websocket-client/websocket.patch diff --git a/pythonforandroid/recipes/websocket-client/__init__.py b/pythonforandroid/recipes/websocket-client/__init__.py new file mode 100644 index 0000000000..e5d9061286 --- /dev/null +++ b/pythonforandroid/recipes/websocket-client/__init__.py @@ -0,0 +1,12 @@ +class WebSocketClient(Recipe): + + url = 'https://pypi.python.org/packages/a7/2b/0039154583cb0489c8e18313aa91ccd140ada103289c5c5d31d80fd6d186/websocket_client-0.40.0.tar.gz' + version = '0.40.0' + md5sum = 'f1cf4cc7869ef97a98e5f4be25c30986' + + patches = ['websocket.patch'] # Paths relative to the recipe dir + + depends = ['kivy', 'python2', 'android', 'pyjnius', 'backports.ssl-match-hostname', + 'cryptography', 'pyasn1', 'ndg_httpsclient', 'pyopenssl'] + +recipe = WebSocketClient() diff --git a/pythonforandroid/recipes/websocket-client/websocket.patch b/pythonforandroid/recipes/websocket-client/websocket.patch new file mode 100644 index 0000000000..da04b01ba5 --- /dev/null +++ b/pythonforandroid/recipes/websocket-client/websocket.patch @@ -0,0 +1,33 @@ +Binary files websocket/.DS_Store and websocket-patch/.DS_Store differ +diff -ruN websocket/_logging.py websocket-patch/_logging.py +--- websocket/_logging.py 2016-10-02 21:35:05.000000000 -0400 ++++ websocket-patch/_logging.py 2017-04-10 15:01:56.000000000 -0400 +@@ -19,9 +19,10 @@ + Boston, MA 02110-1335 USA + + """ +-import logging +- +-_logger = logging.getLogger('websocket') ++# import logging ++from kivy.logger import Logger ++# _logger = logging.getLogger('websocket') ++_logger = Logger + _traceEnabled = False + + __all__ = ["enableTrace", "dump", "error", "debug", "trace", +@@ -67,8 +68,10 @@ + + + def isEnabledForError(): +- return _logger.isEnabledFor(logging.ERROR) ++ return True ++ # return _logger.isEnabledFor(logging.ERROR) + + + def isEnabledForDebug(): +- return _logger.isEnabledFor(logging.DEBUG) ++ return True ++ # return _logger.isEnabledFor(logging.DEBUG) +Binary files websocket/tests/__init__.pyc and websocket-patch/tests/__init__.pyc differ +Binary files websocket/tests/test_websocket.pyc and websocket-patch/tests/test_websocket.pyc differ From b2297bc74d394531c1db6112bb3a3f0f9890dedb Mon Sep 17 00:00:00 2001 From: debauchery1st Date: Sun, 16 Apr 2017 16:16:29 -0400 Subject: [PATCH 0015/1138] Recipe for websocket-client --- pythonforandroid/recipes/websocket-client/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pythonforandroid/recipes/websocket-client/__init__.py b/pythonforandroid/recipes/websocket-client/__init__.py index e5d9061286..7814986848 100644 --- a/pythonforandroid/recipes/websocket-client/__init__.py +++ b/pythonforandroid/recipes/websocket-client/__init__.py @@ -1,3 +1,6 @@ +from pythonforandroid.toolchain import Recipe + + class WebSocketClient(Recipe): url = 'https://pypi.python.org/packages/a7/2b/0039154583cb0489c8e18313aa91ccd140ada103289c5c5d31d80fd6d186/websocket_client-0.40.0.tar.gz' From 1191d8f609a7d9567225d334de5d874ff9475a03 Mon Sep 17 00:00:00 2001 From: debauchery1st Date: Sun, 16 Apr 2017 16:20:24 -0400 Subject: [PATCH 0016/1138] Recipe for websocket-client --- pythonforandroid/recipes/websocket-client/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/recipes/websocket-client/__init__.py b/pythonforandroid/recipes/websocket-client/__init__.py index 7814986848..8c4ab9d41b 100644 --- a/pythonforandroid/recipes/websocket-client/__init__.py +++ b/pythonforandroid/recipes/websocket-client/__init__.py @@ -9,7 +9,7 @@ class WebSocketClient(Recipe): patches = ['websocket.patch'] # Paths relative to the recipe dir - depends = ['kivy', 'python2', 'android', 'pyjnius', 'backports.ssl-match-hostname', + depends = ['kivy', 'python2', 'android', 'pyjnius', 'cryptography', 'pyasn1', 'ndg_httpsclient', 'pyopenssl'] recipe = WebSocketClient() From 7850a937b7bc8e2bffa58903b21a06c286d3b990 Mon Sep 17 00:00:00 2001 From: debauchery1st Date: Sun, 16 Apr 2017 16:21:17 -0400 Subject: [PATCH 0017/1138] Recipe for websocket-client --- pythonforandroid/recipes/websocket-client/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/recipes/websocket-client/__init__.py b/pythonforandroid/recipes/websocket-client/__init__.py index 8c4ab9d41b..984eeff047 100644 --- a/pythonforandroid/recipes/websocket-client/__init__.py +++ b/pythonforandroid/recipes/websocket-client/__init__.py @@ -10,6 +10,6 @@ class WebSocketClient(Recipe): patches = ['websocket.patch'] # Paths relative to the recipe dir depends = ['kivy', 'python2', 'android', 'pyjnius', - 'cryptography', 'pyasn1', 'ndg_httpsclient', 'pyopenssl'] + 'cryptography', 'pyasn1', 'pyopenssl'] recipe = WebSocketClient() From f72769bc45e9547b036a27eba8263461a5bd88b7 Mon Sep 17 00:00:00 2001 From: Trevor Martin Date: Thu, 13 Apr 2017 21:38:51 -0400 Subject: [PATCH 0018/1138] websocket-client recipe --- .../recipes/websocket-client/websocket.patch | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pythonforandroid/recipes/websocket-client/websocket.patch b/pythonforandroid/recipes/websocket-client/websocket.patch index da04b01ba5..bf34fd4209 100644 --- a/pythonforandroid/recipes/websocket-client/websocket.patch +++ b/pythonforandroid/recipes/websocket-client/websocket.patch @@ -1,7 +1,5 @@ -Binary files websocket/.DS_Store and websocket-patch/.DS_Store differ -diff -ruN websocket/_logging.py websocket-patch/_logging.py ---- websocket/_logging.py 2016-10-02 21:35:05.000000000 -0400 -+++ websocket-patch/_logging.py 2017-04-10 15:01:56.000000000 -0400 +--- a/websocket/_logging.py 2016-10-02 21:35:05.000000000 -0400 ++++ b/websocket/_logging.py 2017-04-13 21:17:34.539598154 -0400 @@ -19,9 +19,10 @@ Boston, MA 02110-1335 USA @@ -9,25 +7,23 @@ diff -ruN websocket/_logging.py websocket-patch/_logging.py -import logging - -_logger = logging.getLogger('websocket') -+# import logging ++#import logging +from kivy.logger import Logger -+# _logger = logging.getLogger('websocket') ++#_logger = logging.getLogger('websocket') +_logger = Logger _traceEnabled = False __all__ = ["enableTrace", "dump", "error", "debug", "trace", -@@ -67,8 +68,10 @@ +@@ -67,8 +68,9 @@ def isEnabledForError(): - return _logger.isEnabledFor(logging.ERROR) +- ++# return _logger.isEnabledFor(logging.ERROR) + return True -+ # return _logger.isEnabledFor(logging.ERROR) - def isEnabledForDebug(): - return _logger.isEnabledFor(logging.DEBUG) ++# return _logger.isEnabledFor(logging.DEBUG) + return True -+ # return _logger.isEnabledFor(logging.DEBUG) -Binary files websocket/tests/__init__.pyc and websocket-patch/tests/__init__.pyc differ -Binary files websocket/tests/test_websocket.pyc and websocket-patch/tests/test_websocket.pyc differ From 4dd8927e8442b9989ddc36959af2ade04857c4fe Mon Sep 17 00:00:00 2001 From: debauchery1st Date: Sun, 16 Apr 2017 17:25:27 -0400 Subject: [PATCH 0019/1138] ssl-match-hostname required --- pythonforandroid/recipes/websocket-client/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythonforandroid/recipes/websocket-client/__init__.py b/pythonforandroid/recipes/websocket-client/__init__.py index 984eeff047..7814986848 100644 --- a/pythonforandroid/recipes/websocket-client/__init__.py +++ b/pythonforandroid/recipes/websocket-client/__init__.py @@ -9,7 +9,7 @@ class WebSocketClient(Recipe): patches = ['websocket.patch'] # Paths relative to the recipe dir - depends = ['kivy', 'python2', 'android', 'pyjnius', - 'cryptography', 'pyasn1', 'pyopenssl'] + depends = ['kivy', 'python2', 'android', 'pyjnius', 'backports.ssl-match-hostname', + 'cryptography', 'pyasn1', 'ndg_httpsclient', 'pyopenssl'] recipe = WebSocketClient() From 0ef145f1e5b563fb17a56ec0bd90f8914c151361 Mon Sep 17 00:00:00 2001 From: debauchery1st Date: Sun, 16 Apr 2017 17:31:28 -0400 Subject: [PATCH 0020/1138] ssl-match-hostname required --- pythonforandroid/recipes/websocket-client/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pythonforandroid/recipes/websocket-client/__init__.py b/pythonforandroid/recipes/websocket-client/__init__.py index 7814986848..d21e7465b3 100644 --- a/pythonforandroid/recipes/websocket-client/__init__.py +++ b/pythonforandroid/recipes/websocket-client/__init__.py @@ -9,7 +9,8 @@ class WebSocketClient(Recipe): patches = ['websocket.patch'] # Paths relative to the recipe dir - depends = ['kivy', 'python2', 'android', 'pyjnius', 'backports.ssl-match-hostname', - 'cryptography', 'pyasn1', 'ndg_httpsclient', 'pyopenssl'] + depends = ['kivy', 'python2', 'android', 'pyjnius', + 'backports.ssl-match-hostname', 'cryptography', + 'pyasn1', 'ndg_httpsclient', 'pyopenssl'] recipe = WebSocketClient() From 5a9414881e88f0ba2bc03fdceb1a2f9eb091b1e3 Mon Sep 17 00:00:00 2001 From: Trevor Martin Date: Mon, 17 Apr 2017 12:54:58 -0400 Subject: [PATCH 0021/1138] git --diff format patch --- .../recipes/websocket-client/__init__.py | 5 ++--- .../recipes/websocket-client/websocket.patch | 17 ++++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/pythonforandroid/recipes/websocket-client/__init__.py b/pythonforandroid/recipes/websocket-client/__init__.py index 7814986848..3390d8876e 100644 --- a/pythonforandroid/recipes/websocket-client/__init__.py +++ b/pythonforandroid/recipes/websocket-client/__init__.py @@ -2,14 +2,13 @@ class WebSocketClient(Recipe): - url = 'https://pypi.python.org/packages/a7/2b/0039154583cb0489c8e18313aa91ccd140ada103289c5c5d31d80fd6d186/websocket_client-0.40.0.tar.gz' version = '0.40.0' md5sum = 'f1cf4cc7869ef97a98e5f4be25c30986' patches = ['websocket.patch'] # Paths relative to the recipe dir - depends = ['kivy', 'python2', 'android', 'pyjnius', 'backports.ssl-match-hostname', - 'cryptography', 'pyasn1', 'ndg_httpsclient', 'pyopenssl'] + depends = ['kivy', 'python2', 'android', 'pyjnius', + 'cryptography', 'pyasn1', 'pyopenssl'] recipe = WebSocketClient() diff --git a/pythonforandroid/recipes/websocket-client/websocket.patch b/pythonforandroid/recipes/websocket-client/websocket.patch index bf34fd4209..694bcb653b 100644 --- a/pythonforandroid/recipes/websocket-client/websocket.patch +++ b/pythonforandroid/recipes/websocket-client/websocket.patch @@ -1,29 +1,28 @@ ---- a/websocket/_logging.py 2016-10-02 21:35:05.000000000 -0400 -+++ b/websocket/_logging.py 2017-04-13 21:17:34.539598154 -0400 -@@ -19,9 +19,10 @@ +diff --git a/websocket/_logging.py b/websocket/_logging.py +index 8a5f4a5..cebc23b 100644 +--- a/websocket/_logging.py ++++ b/websocket/_logging.py +@@ -19,9 +19,8 @@ Copyright (C) 2010 Hiroki Ohtani(liris) Boston, MA 02110-1335 USA """ -import logging - -_logger = logging.getLogger('websocket') -+#import logging +from kivy.logger import Logger -+#_logger = logging.getLogger('websocket') +_logger = Logger _traceEnabled = False __all__ = ["enableTrace", "dump", "error", "debug", "trace", -@@ -67,8 +68,9 @@ +@@ -67,8 +66,9 @@ def trace(msg): def isEnabledForError(): - return _logger.isEnabledFor(logging.ERROR) -- -+# return _logger.isEnabledFor(logging.ERROR) + return True + def isEnabledForDebug(): - return _logger.isEnabledFor(logging.DEBUG) -+# return _logger.isEnabledFor(logging.DEBUG) + return True ++ From 26c6f606fc9788ef4487a0265850863ead893c04 Mon Sep 17 00:00:00 2001 From: Trevor Martin Date: Mon, 17 Apr 2017 14:47:00 -0400 Subject: [PATCH 0022/1138] work-around for patch --- pythonforandroid/recipes/websocket-client/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pythonforandroid/recipes/websocket-client/__init__.py b/pythonforandroid/recipes/websocket-client/__init__.py index 3390d8876e..91b89f438f 100644 --- a/pythonforandroid/recipes/websocket-client/__init__.py +++ b/pythonforandroid/recipes/websocket-client/__init__.py @@ -2,11 +2,12 @@ class WebSocketClient(Recipe): - url = 'https://pypi.python.org/packages/a7/2b/0039154583cb0489c8e18313aa91ccd140ada103289c5c5d31d80fd6d186/websocket_client-0.40.0.tar.gz' + url = 'https://github.com/debauchery1st/websocket-client/blob/master/websocket_client-0.40.0.tar.gz?raw=true' + # url = 'https://pypi.python.org/packages/a7/2b/0039154583cb0489c8e18313aa91ccd140ada103289c5c5d31d80fd6d186/websocket_client-0.40.0.tar.gz' version = '0.40.0' - md5sum = 'f1cf4cc7869ef97a98e5f4be25c30986' + # md5sum = 'f1cf4cc7869ef97a98e5f4be25c30986' - patches = ['websocket.patch'] # Paths relative to the recipe dir + # patches = ['websocket.patch'] # Paths relative to the recipe dir depends = ['kivy', 'python2', 'android', 'pyjnius', 'cryptography', 'pyasn1', 'pyopenssl'] From cb59b5564a50cd5b50f60ccbc892d560ea21e6ea Mon Sep 17 00:00:00 2001 From: Trevor Martin Date: Mon, 17 Apr 2017 14:50:44 -0400 Subject: [PATCH 0023/1138] fixed url --- pythonforandroid/recipes/websocket-client/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/recipes/websocket-client/__init__.py b/pythonforandroid/recipes/websocket-client/__init__.py index 91b89f438f..e69dad5e58 100644 --- a/pythonforandroid/recipes/websocket-client/__init__.py +++ b/pythonforandroid/recipes/websocket-client/__init__.py @@ -2,7 +2,7 @@ class WebSocketClient(Recipe): - url = 'https://github.com/debauchery1st/websocket-client/blob/master/websocket_client-0.40.0.tar.gz?raw=true' + url = 'https://github.com/debauchery1st/websocket-client/raw/master/websocket_client-0.40.0.tar.gz' # url = 'https://pypi.python.org/packages/a7/2b/0039154583cb0489c8e18313aa91ccd140ada103289c5c5d31d80fd6d186/websocket_client-0.40.0.tar.gz' version = '0.40.0' # md5sum = 'f1cf4cc7869ef97a98e5f4be25c30986' From e0b69238b8e5933583a9c010f54b0229681d9f7f Mon Sep 17 00:00:00 2001 From: Trevor Martin Date: Mon, 17 Apr 2017 15:51:04 -0400 Subject: [PATCH 0024/1138] note about import error --- pythonforandroid/recipes/websocket-client/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pythonforandroid/recipes/websocket-client/__init__.py b/pythonforandroid/recipes/websocket-client/__init__.py index e69dad5e58..257ec00656 100644 --- a/pythonforandroid/recipes/websocket-client/__init__.py +++ b/pythonforandroid/recipes/websocket-client/__init__.py @@ -1,5 +1,11 @@ from pythonforandroid.toolchain import Recipe +# if android app crashes on start with "ImportError: No module named websocket" +# +# copy the 'websocket' directory into your app directory to force inclusion. +# +# see my example at https://github.com/debauchery1st/example_kivy_websocket-recipe + class WebSocketClient(Recipe): url = 'https://github.com/debauchery1st/websocket-client/raw/master/websocket_client-0.40.0.tar.gz' From ffdf43b28fa69aecf1f3c2b7358b8932acaa37ea Mon Sep 17 00:00:00 2001 From: Trevor Martin Date: Mon, 17 Apr 2017 15:52:37 -0400 Subject: [PATCH 0025/1138] clean up --- pythonforandroid/recipes/websocket-client/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pythonforandroid/recipes/websocket-client/__init__.py b/pythonforandroid/recipes/websocket-client/__init__.py index 257ec00656..aeb6b3b8a1 100644 --- a/pythonforandroid/recipes/websocket-client/__init__.py +++ b/pythonforandroid/recipes/websocket-client/__init__.py @@ -8,8 +8,9 @@ class WebSocketClient(Recipe): + url = 'https://github.com/debauchery1st/websocket-client/raw/master/websocket_client-0.40.0.tar.gz' - # url = 'https://pypi.python.org/packages/a7/2b/0039154583cb0489c8e18313aa91ccd140ada103289c5c5d31d80fd6d186/websocket_client-0.40.0.tar.gz' + version = '0.40.0' # md5sum = 'f1cf4cc7869ef97a98e5f4be25c30986' From 52d3526a6cbbc6b5eb35cfeabc45b4e8ca7cc063 Mon Sep 17 00:00:00 2001 From: germn Date: Thu, 20 Apr 2017 20:04:03 +0300 Subject: [PATCH 0026/1138] Add missing dependencies for ffpyplayer_codecs. --- doc/source/quickstart.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 5700372c8c..a5c4b1505e 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -61,13 +61,15 @@ p4a has several dependencies that must be installed: - unzip - virtualenv (can be installed via pip) - ccache (optional) +- autoconf (for ffpyplayer_codecs recipe) +- libtool (for ffpyplayer_codecs recipe) On recent versions of Ubuntu and its derivatives you may be able to install most of these with:: sudo dpkg --add-architecture i386 sudo apt-get update - sudo apt-get install -y build-essential ccache git zlib1g-dev python2.7 python2.7-dev libncurses5:i386 libstdc++6:i386 zlib1g:i386 openjdk-7-jdk unzip ant ccache + sudo apt-get install -y build-essential ccache git zlib1g-dev python2.7 python2.7-dev libncurses5:i386 libstdc++6:i386 zlib1g:i386 openjdk-7-jdk unzip ant ccache autoconf libtool On Arch Linux (64 bit) you should be able to run the following to install most of the dependencies (note: this list may not be From 049ce2fee55cbfb9b5c9f245419b7b6199c5e026 Mon Sep 17 00:00:00 2001 From: Hobbestigrou Date: Wed, 10 May 2017 09:38:33 +0200 Subject: [PATCH 0027/1138] [Core] Get the latest version of requests. --- pythonforandroid/recipes/requests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/recipes/requests/__init__.py b/pythonforandroid/recipes/requests/__init__.py index 753f918a85..c2a349af16 100644 --- a/pythonforandroid/recipes/requests/__init__.py +++ b/pythonforandroid/recipes/requests/__init__.py @@ -1,7 +1,7 @@ from pythonforandroid.toolchain import PythonRecipe class RequestsRecipe(PythonRecipe): - version = '2.9.1' + version = '2.13.0' url = 'https://github.com/kennethreitz/requests/archive/v{version}.tar.gz' depends = ['hostpython2', 'setuptools'] site_packages_name = 'requests' From bdf81ee709a827ac76f1f5584a0621c82a0744ba Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Fri, 2 Jun 2017 23:00:05 +0100 Subject: [PATCH 0028/1138] Delete the kivy-examples dir from the dist under python3 Unlike the python2 build, this dir is automatically included in site-packages. --- pythonforandroid/recipes/kivy/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pythonforandroid/recipes/kivy/__init__.py b/pythonforandroid/recipes/kivy/__init__.py index 6b5eaf5037..52cad36fc5 100644 --- a/pythonforandroid/recipes/kivy/__init__.py +++ b/pythonforandroid/recipes/kivy/__init__.py @@ -1,6 +1,7 @@ from pythonforandroid.toolchain import CythonRecipe, shprint, current_directory, ArchARM -from os.path import exists, join +from os.path import exists, join, isdir +from shutil import rmtree import sh import glob @@ -43,4 +44,12 @@ def get_recipe_env(self, arch): return env + def install_python_package(self, arch): + super(KivyRecipe, self).install_python_package(arch) + site_packages_dir = self.ctx.get_site_packages_dir(arch) + usr_dir = join(site_packages_dir, 'usr', 'share', 'kivy-examples') + print('usr_dir is', usr_dir) + if exists(usr_dir) and isdir(usr_dir): + rmtree(usr_dir) + recipe = KivyRecipe() From c0049ae1aa6f31eaf3421116bbb4c1ecedf4b04f Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Fri, 2 Jun 2017 23:07:45 +0100 Subject: [PATCH 0029/1138] Delete debug print statement --- pythonforandroid/recipes/kivy/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pythonforandroid/recipes/kivy/__init__.py b/pythonforandroid/recipes/kivy/__init__.py index 52cad36fc5..8d89cff0b9 100644 --- a/pythonforandroid/recipes/kivy/__init__.py +++ b/pythonforandroid/recipes/kivy/__init__.py @@ -48,7 +48,6 @@ def install_python_package(self, arch): super(KivyRecipe, self).install_python_package(arch) site_packages_dir = self.ctx.get_site_packages_dir(arch) usr_dir = join(site_packages_dir, 'usr', 'share', 'kivy-examples') - print('usr_dir is', usr_dir) if exists(usr_dir) and isdir(usr_dir): rmtree(usr_dir) From e8e4fb1285139f943568ac149861131433a2818c Mon Sep 17 00:00:00 2001 From: Peter Badida Date: Sat, 3 Jun 2017 01:15:01 +0200 Subject: [PATCH 0030/1138] Use Kivy setup.py flag instead of rmtree --- pythonforandroid/recipes/kivy/__init__.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pythonforandroid/recipes/kivy/__init__.py b/pythonforandroid/recipes/kivy/__init__.py index 8d89cff0b9..975c855334 100644 --- a/pythonforandroid/recipes/kivy/__init__.py +++ b/pythonforandroid/recipes/kivy/__init__.py @@ -1,7 +1,6 @@ from pythonforandroid.toolchain import CythonRecipe, shprint, current_directory, ArchARM -from os.path import exists, join, isdir -from shutil import rmtree +from os.path import exists, join import sh import glob @@ -35,6 +34,7 @@ def get_recipe_env(self, arch): env = super(KivyRecipe, self).get_recipe_env(arch) if 'sdl2' in self.ctx.recipe_build_order: env['USE_SDL2'] = '1' + env['KIVY_SPLIT_EXAMPLES'] = '1' env['KIVY_SDL2_PATH'] = ':'.join([ join(self.ctx.bootstrap.build_dir, 'jni', 'SDL', 'include'), join(self.ctx.bootstrap.build_dir, 'jni', 'SDL2_image'), @@ -44,11 +44,4 @@ def get_recipe_env(self, arch): return env - def install_python_package(self, arch): - super(KivyRecipe, self).install_python_package(arch) - site_packages_dir = self.ctx.get_site_packages_dir(arch) - usr_dir = join(site_packages_dir, 'usr', 'share', 'kivy-examples') - if exists(usr_dir) and isdir(usr_dir): - rmtree(usr_dir) - recipe = KivyRecipe() From 81fe4bf405f7245afb501aeb398a2631d00a75bf Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sun, 4 Jun 2017 00:59:33 +0100 Subject: [PATCH 0031/1138] Added new recipe graph creation method --- pythonforandroid/graph.py | 81 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/pythonforandroid/graph.py b/pythonforandroid/graph.py index fc491ca277..f886cd48fb 100644 --- a/pythonforandroid/graph.py +++ b/pythonforandroid/graph.py @@ -1,5 +1,6 @@ from copy import deepcopy +from itertools import product from pythonforandroid.logger import (info, info_notify, warning) from pythonforandroid.recipe import Recipe @@ -115,6 +116,84 @@ def find_order(self, index=0): bset.discard(result) +class RecipeOrder(dict): + + def __init__(self, ctx): + self.ctx = ctx + + def conflicts(self, name): + for name in self.keys(): + try: + recipe = Recipe.get_recipe(name, self.ctx) + conflicts = recipe.conflicts + except OSError: + conflicts = [] + + if any([c in self for c in recipe.conflicts]): + return True + return False + +def recursively_collect_orders(name, ctx, orders=[]): + '''For each possible recipe ordering we were passed, try to add the + new recipe name to that order. Recursively do the same thing with + all the dependencies of each recipe. + ''' + try: + recipe = Recipe.get_recipe(name, ctx) + if recipe.depends is None: + dependencies = [] + else: + # make all dependencies into lists so that product will work + dependencies = [([dependency] if not isinstance(dependency, (list, tuple)) + else dependency) for dependency in recipe.depends] + except OSError: + # The recipe does not exist, so we assume it can be installed + # via pip with no extra dependencies + dependencies = [] + + 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(name): + 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, dependency_new_orders) + + new_orders.extend(dependency_new_orders) + + return new_orders + + +def new_get_recipe_order_and_bootstrap(ctx, names, bs=None): + recipes_to_load = set(names) + # if bs is not None and bs.recipe_depends: + # recipes_to_load = recipes_to_load.union(set(bs.recipe_depends)) + + possible_orders = [RecipeOrder(ctx)] + + # get all possible recipe orders + for name in names: + possible_orders = recursively_collect_orders(name, ctx, orders=possible_orders) + + # prefer python2 and SDL2 if available + possible_orders = sorted(possible_orders, + key=lambda order: -('python2' in order) - ('sdl2' in order)) + + + + return possible_orders + + def get_recipe_order_and_bootstrap(ctx, names, bs=None): '''Takes a list of recipe names and (optionally) a bootstrap. Then works out the dependency graph (including bootstrap recipes if @@ -129,7 +208,9 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None): recipes_to_load = list(recipes_to_load) recipe_loaded = [] python_modules = [] + print('recipes_to_load', recipes_to_load) while recipes_to_load: + info('Current recipes to load: {}'.format(', '.join(map(str, recipes_to_load)))) name = recipes_to_load.pop(0) if name in recipe_loaded or isinstance(name, (list, tuple)): continue From bfd675a4e4dd604e455688544751d5ddbe9b1070 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sun, 4 Jun 2017 15:35:32 +0100 Subject: [PATCH 0032/1138] Made bootstrap selection from recipes work --- pythonforandroid/bootstrap.py | 2 +- pythonforandroid/graph.py | 73 +++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/pythonforandroid/bootstrap.py b/pythonforandroid/bootstrap.py index d407de1b59..008a74e542 100644 --- a/pythonforandroid/bootstrap.py +++ b/pythonforandroid/bootstrap.py @@ -27,7 +27,7 @@ class Bootstrap(object): dist_name = None distribution = None - recipe_depends = [] + recipe_depends = ['sdl2'] can_be_chosen_automatically = True '''Determines whether the bootstrap can be chosen as one that diff --git a/pythonforandroid/graph.py b/pythonforandroid/graph.py index f886cd48fb..a38d9768f8 100644 --- a/pythonforandroid/graph.py +++ b/pythonforandroid/graph.py @@ -129,7 +129,7 @@ def conflicts(self, name): except OSError: conflicts = [] - if any([c in self for c in recipe.conflicts]): + if any([c in self for c in conflicts]): return True return False @@ -151,6 +151,8 @@ def recursively_collect_orders(name, ctx, orders=[]): # via pip with no extra dependencies dependencies = [] + # TODO: Also check recipe conflicts + new_orders = [] # for each existing recipe order, see if we can add the new recipe name for order in orders: @@ -174,24 +176,72 @@ def recursively_collect_orders(name, ctx, 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 = [l for l, s in graph.items() if not s] + 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 new_get_recipe_order_and_bootstrap(ctx, names, bs=None): recipes_to_load = set(names) - # if bs is not None and bs.recipe_depends: - # recipes_to_load = recipes_to_load.union(set(bs.recipe_depends)) - - possible_orders = [RecipeOrder(ctx)] + if bs is not None and bs.recipe_depends: + recipes_to_load = recipes_to_load.union(set(bs.recipe_depends)) - # get all possible recipe orders - for name in names: - possible_orders = recursively_collect_orders(name, ctx, orders=possible_orders) + possible_orders = [] + + # get all possible recipe sets if names includes alternative + # dependencies + names = [([name] if not isinstance(name, (list, tuple)) else name) + for name in names] + for name_set in product(*names): + new_possible_orders = [RecipeOrder(ctx)] + for name in name_set: + new_possible_orders = recursively_collect_orders( + name, ctx, orders=new_possible_orders) + 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 {}'.format(possible_order)) + continue + orders.append(list(order)) # prefer python2 and SDL2 if available - possible_orders = sorted(possible_orders, - key=lambda order: -('python2' in order) - ('sdl2' in order)) + orders = sorted(orders, + key=lambda order: -('python2' in order) - ('sdl2' in order)) + # 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 + order = orders[0] + print('pre-bs order is', order) + + if bs is None: + bs = Bootstrap.get_bootstrap_from_recipes(order, ctx) + orders, bs = new_get_recipe_order_and_bootstrap(ctx, order, bs=bs) + order = orders[0] - return possible_orders + return order, bs + + def get_recipe_order_and_bootstrap(ctx, names, bs=None): @@ -210,7 +260,6 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None): python_modules = [] print('recipes_to_load', recipes_to_load) while recipes_to_load: - info('Current recipes to load: {}'.format(', '.join(map(str, recipes_to_load)))) name = recipes_to_load.pop(0) if name in recipe_loaded or isinstance(name, (list, tuple)): continue From 7339b279edd97d8ad45af1da6d98f5365aeeb0e8 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sun, 4 Jun 2017 15:50:42 +0100 Subject: [PATCH 0033/1138] Fixed bugs in new dependency graph resolution --- pythonforandroid/graph.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/pythonforandroid/graph.py b/pythonforandroid/graph.py index a38d9768f8..58803695c8 100644 --- a/pythonforandroid/graph.py +++ b/pythonforandroid/graph.py @@ -1,8 +1,9 @@ from copy import deepcopy from itertools import product +from sys import exit -from pythonforandroid.logger import (info, info_notify, warning) +from pythonforandroid.logger import (info, info_notify, warning, error) from pythonforandroid.recipe import Recipe from pythonforandroid.bootstrap import Bootstrap @@ -219,27 +220,41 @@ def new_get_recipe_order_and_bootstrap(ctx, names, bs=None): try: order = find_order(possible_order) except ValueError: # a circular dependency was found - info('Circular dependency found in graph {}'.format(possible_order)) + info('Circular dependency found in graph {}, skipping it.'.format(possible_order)) continue + except: + warning('Failed to import recipe named {}; the recipe exists ' + 'but appears broken.'.format(name)) + warning('Exception was:') + raise orders.append(list(order)) # prefer python2 and SDL2 if available orders = sorted(orders, key=lambda order: -('python2' in order) - ('sdl2' in order)) + if not orders: + error('Didn\'t find any valid dependency graphs.') + error('This means that some of your requirements pull in conflicting dependencies.') + error('Exiting.') + exit(1) # 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 - order = orders[0] - - print('pre-bs order is', order) + 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(order, ctx) - orders, bs = new_get_recipe_order_and_bootstrap(ctx, order, bs=bs) - order = orders[0] - - return order, bs + bs = Bootstrap.get_bootstrap_from_recipes(chosen_order, ctx) + chosen_order, bs = new_get_recipe_order_and_bootstrap(ctx, chosen_order, bs=bs) + + return chosen_order, bs From 13d5d206dafa5aec0d4aa4d93d52c3cfe7232831 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sun, 4 Jun 2017 15:54:46 +0100 Subject: [PATCH 0034/1138] Completely replaced old recipe graph calculation --- pythonforandroid/graph.py | 472 +++++++++++++++++++------------------- 1 file changed, 239 insertions(+), 233 deletions(-) diff --git a/pythonforandroid/graph.py b/pythonforandroid/graph.py index 58803695c8..21cf6a2d17 100644 --- a/pythonforandroid/graph.py +++ b/pythonforandroid/graph.py @@ -8,113 +8,113 @@ from pythonforandroid.bootstrap import Bootstrap -class Graph(object): - # Taken from the old python-for-android/depsort - # Modified to include alternative dependencies - def __init__(self): - # `graph`: dict that maps each package to a set of its dependencies. - self.graphs = [{}] - # self.graph = {} - - def remove_redundant_graphs(self): - '''Removes possible graphs if they are equivalent to others.''' - graphs = self.graphs - # Walk the list backwards so that popping elements doesn't - # mess up indexing. - - # n.b. no need to test graph 0 as it will have been tested against - # all others by the time we get to it - for i in range(len(graphs) - 1, 0, -1): - graph = graphs[i] - - # test graph i against all graphs 0 to i-1 - for j in range(0, i): - comparison_graph = graphs[j] - - if set(comparison_graph.keys()) == set(graph.keys()): - # graph[i] == graph[j] - # so remove graph[i] and continue on to testing graph[i-1] - graphs.pop(i) - break - - def add(self, dependent, dependency): - """Add a dependency relationship to the graph""" - if isinstance(dependency, (tuple, list)): - for graph in self.graphs[:]: - for dep in dependency[1:]: - new_graph = deepcopy(graph) - self._add(new_graph, dependent, dep) - self.graphs.append(new_graph) - self._add(graph, dependent, dependency[0]) - else: - for graph in self.graphs: - self._add(graph, dependent, dependency) - self.remove_redundant_graphs() - - def _add(self, graph, dependent, dependency): - '''Add a dependency relationship to a specific graph, where dependency - must be a single dependency, not a list or tuple. - ''' - graph.setdefault(dependent, set()) - graph.setdefault(dependency, set()) - if dependent != dependency: - graph[dependent].add(dependency) - - def conflicts(self, conflict): - graphs = self.graphs - initial_num = len(graphs) - for i in range(len(graphs)): - graph = graphs[initial_num - 1 - i] - if conflict in graph: - graphs.pop(initial_num - 1 - i) - return len(graphs) == 0 - - def remove_remaining_conflicts(self, ctx): - # It's unpleasant to have to pass ctx as an argument... - '''Checks all possible graphs for conflicts that have arisen during - the additon of alternative repice branches, as these are not checked - for conflicts at the time.''' - new_graphs = [] - for i, graph in enumerate(self.graphs): - for name in graph.keys(): - recipe = Recipe.get_recipe(name, ctx) - if any([c in graph for c in recipe.conflicts]): - break - else: - new_graphs.append(graph) - self.graphs = new_graphs - - def add_optional(self, dependent, dependency): - """Add an optional (ordering only) dependency relationship to the graph - - Only call this after all mandatory requirements are added - """ - for graph in self.graphs: - if dependent in graph and dependency in graph: - self._add(graph, dependent, dependency) - - def find_order(self, index=0): - """Do a topological sort on a dependency graph - - :Parameters: - :Returns: - iterator, sorted items form first to last - """ - graph = self.graphs[index] - graph = dict((k, set(v)) for k, v in graph.items()) - while graph: - # Find all items without a parent - leftmost = [l for l, s in graph.items() if not s] - 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) +# class Graph(object): +# # Taken from the old python-for-android/depsort +# # Modified to include alternative dependencies +# def __init__(self): +# # `graph`: dict that maps each package to a set of its dependencies. +# self.graphs = [{}] +# # self.graph = {} + +# def remove_redundant_graphs(self): +# '''Removes possible graphs if they are equivalent to others.''' +# graphs = self.graphs +# # Walk the list backwards so that popping elements doesn't +# # mess up indexing. + +# # n.b. no need to test graph 0 as it will have been tested against +# # all others by the time we get to it +# for i in range(len(graphs) - 1, 0, -1): +# graph = graphs[i] + +# # test graph i against all graphs 0 to i-1 +# for j in range(0, i): +# comparison_graph = graphs[j] + +# if set(comparison_graph.keys()) == set(graph.keys()): +# # graph[i] == graph[j] +# # so remove graph[i] and continue on to testing graph[i-1] +# graphs.pop(i) +# break + +# def add(self, dependent, dependency): +# """Add a dependency relationship to the graph""" +# if isinstance(dependency, (tuple, list)): +# for graph in self.graphs[:]: +# for dep in dependency[1:]: +# new_graph = deepcopy(graph) +# self._add(new_graph, dependent, dep) +# self.graphs.append(new_graph) +# self._add(graph, dependent, dependency[0]) +# else: +# for graph in self.graphs: +# self._add(graph, dependent, dependency) +# self.remove_redundant_graphs() + +# def _add(self, graph, dependent, dependency): +# '''Add a dependency relationship to a specific graph, where dependency +# must be a single dependency, not a list or tuple. +# ''' +# graph.setdefault(dependent, set()) +# graph.setdefault(dependency, set()) +# if dependent != dependency: +# graph[dependent].add(dependency) + +# def conflicts(self, conflict): +# graphs = self.graphs +# initial_num = len(graphs) +# for i in range(len(graphs)): +# graph = graphs[initial_num - 1 - i] +# if conflict in graph: +# graphs.pop(initial_num - 1 - i) +# return len(graphs) == 0 + +# def remove_remaining_conflicts(self, ctx): +# # It's unpleasant to have to pass ctx as an argument... +# '''Checks all possible graphs for conflicts that have arisen during +# the additon of alternative repice branches, as these are not checked +# for conflicts at the time.''' +# new_graphs = [] +# for i, graph in enumerate(self.graphs): +# for name in graph.keys(): +# recipe = Recipe.get_recipe(name, ctx) +# if any([c in graph for c in recipe.conflicts]): +# break +# else: +# new_graphs.append(graph) +# self.graphs = new_graphs + +# def add_optional(self, dependent, dependency): +# """Add an optional (ordering only) dependency relationship to the graph + +# Only call this after all mandatory requirements are added +# """ +# for graph in self.graphs: +# if dependent in graph and dependency in graph: +# self._add(graph, dependent, dependency) + +# def find_order(self, index=0): +# """Do a topological sort on a dependency graph + +# :Parameters: +# :Returns: +# iterator, sorted items form first to last +# """ +# graph = self.graphs[index] +# graph = dict((k, set(v)) for k, v in graph.items()) +# while graph: +# # Find all items without a parent +# leftmost = [l for l, s in graph.items() if not s] +# 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) class RecipeOrder(dict): @@ -135,9 +135,10 @@ def conflicts(self, name): return False def recursively_collect_orders(name, ctx, orders=[]): - '''For each possible recipe ordering we were passed, try to add the - new recipe name to that order. Recursively do the same thing with - all the dependencies of each recipe. + '''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. + ''' try: recipe = Recipe.get_recipe(name, ctx) @@ -147,12 +148,15 @@ def recursively_collect_orders(name, ctx, orders=[]): # make all dependencies into lists so that product will work dependencies = [([dependency] if not isinstance(dependency, (list, tuple)) else dependency) for dependency in recipe.depends] + if recipe.conflicts is None: + conflicts = [] + else: + conflicts = recipe.conflicts except OSError: # The recipe does not exist, so we assume it can be installed # via pip with no extra dependencies dependencies = [] - - # TODO: Also check recipe conflicts + conflicts = [] new_orders = [] # for each existing recipe order, see if we can add the new recipe name @@ -162,6 +166,8 @@ def recursively_collect_orders(name, ctx, orders=[]): continue if order.conflicts(name): continue + if any([conflict in order for conflict in conflicts]): + continue for dependency_set in product(*dependencies): new_order = deepcopy(order) @@ -196,7 +202,7 @@ def find_order(graph): bset.discard(result) -def new_get_recipe_order_and_bootstrap(ctx, names, bs=None): +def get_recipe_order_and_bootstrap(ctx, names, bs=None): recipes_to_load = set(names) if bs is not None and bs.recipe_depends: recipes_to_load = recipes_to_load.union(set(bs.recipe_depends)) @@ -252,129 +258,129 @@ def new_get_recipe_order_and_bootstrap(ctx, names, bs=None): if bs is None: bs = Bootstrap.get_bootstrap_from_recipes(chosen_order, ctx) - chosen_order, bs = new_get_recipe_order_and_bootstrap(ctx, chosen_order, bs=bs) + chosen_order, bs = get_recipe_order_and_bootstrap(ctx, chosen_order, bs=bs) return chosen_order, bs -def get_recipe_order_and_bootstrap(ctx, names, bs=None): - '''Takes a list of recipe names and (optionally) a bootstrap. Then - works out the dependency graph (including bootstrap recipes if - necessary). Finally, if no bootstrap was initially selected, - chooses one that supports all the recipes. - ''' - graph = Graph() - recipes_to_load = set(names) - if bs is not None and bs.recipe_depends: - info_notify('Bootstrap requires recipes {}'.format(bs.recipe_depends)) - recipes_to_load = recipes_to_load.union(set(bs.recipe_depends)) - recipes_to_load = list(recipes_to_load) - recipe_loaded = [] - python_modules = [] - print('recipes_to_load', recipes_to_load) - while recipes_to_load: - name = recipes_to_load.pop(0) - if name in recipe_loaded or isinstance(name, (list, tuple)): - continue - try: - recipe = Recipe.get_recipe(name, ctx) - except IOError: - info('No recipe named {}; will attempt to install with pip' - .format(name)) - python_modules.append(name) - continue - except (KeyboardInterrupt, SystemExit): - raise - except: - warning('Failed to import recipe named {}; the recipe exists ' - 'but appears broken.'.format(name)) - warning('Exception was:') - raise - graph.add(name, name) - info('Loaded recipe {} (depends on {}{})'.format( - name, recipe.depends, - ', conflicts {}'.format(recipe.conflicts) if recipe.conflicts - else '')) - for depend in recipe.depends: - graph.add(name, depend) - recipes_to_load += recipe.depends - for conflict in recipe.conflicts: - if graph.conflicts(conflict): - warning( - ('{} conflicts with {}, but both have been ' - 'included or pulled into the requirements.' - .format(recipe.name, conflict))) - warning( - 'Due to this conflict the build cannot continue, exiting.') - exit(1) - python_modules += recipe.python_depends - recipe_loaded.append(name) - graph.remove_remaining_conflicts(ctx) - if len(graph.graphs) > 1: - info('Found multiple valid recipe sets:') - for g in graph.graphs: - info(' {}'.format(g.keys())) - info_notify('Using the first of these: {}' - .format(graph.graphs[0].keys())) - elif len(graph.graphs) == 0: - warning('Didn\'t find any valid dependency graphs, exiting.') - exit(1) - else: - info('Found a single valid recipe set (this is good)') - - build_order = list(graph.find_order(0)) - if bs is None: # It would be better to check against possible - # orders other than the first one, but in practice - # there will rarely be clashes, and the user can - # specify more parameters if necessary to resolve - # them. - bs = Bootstrap.get_bootstrap_from_recipes(build_order, ctx) - if bs is None: - info('Could not find a bootstrap compatible with the ' - 'required recipes.') - info('If you think such a combination should exist, try ' - 'specifying the bootstrap manually with --bootstrap.') - exit(1) - info('{} bootstrap appears compatible with the required recipes.' - .format(bs.name)) - info('Checking this...') - recipes_to_load = bs.recipe_depends - # This code repeats the code from earlier! Should move to a function: - while recipes_to_load: - name = recipes_to_load.pop(0) - if name in recipe_loaded or isinstance(name, (list, tuple)): - continue - try: - recipe = Recipe.get_recipe(name, ctx) - except ImportError: - info('No recipe named {}; will attempt to install with pip' - .format(name)) - python_modules.append(name) - continue - graph.add(name, name) - info('Loaded recipe {} (depends on {}{})'.format( - name, recipe.depends, - ', conflicts {}'.format(recipe.conflicts) if recipe.conflicts - else '')) - for depend in recipe.depends: - graph.add(name, depend) - recipes_to_load += recipe.depends - for conflict in recipe.conflicts: - if graph.conflicts(conflict): - warning( - ('{} conflicts with {}, but both have been ' - 'included or pulled into the requirements.' - .format(recipe.name, conflict))) - warning('Due to this conflict the build cannot continue, ' - 'exiting.') - exit(1) - recipe_loaded.append(name) - graph.remove_remaining_conflicts(ctx) - build_order = list(graph.find_order(0)) - build_order, python_modules, bs = get_recipe_order_and_bootstrap( - ctx, build_order + python_modules, bs) - return build_order, python_modules, bs - - # Do a final check that the new bs doesn't pull in any conflicts +# def get_recipe_order_and_bootstrap(ctx, names, bs=None): +# '''Takes a list of recipe names and (optionally) a bootstrap. Then +# works out the dependency graph (including bootstrap recipes if +# necessary). Finally, if no bootstrap was initially selected, +# chooses one that supports all the recipes. +# ''' +# graph = Graph() +# recipes_to_load = set(names) +# if bs is not None and bs.recipe_depends: +# info_notify('Bootstrap requires recipes {}'.format(bs.recipe_depends)) +# recipes_to_load = recipes_to_load.union(set(bs.recipe_depends)) +# recipes_to_load = list(recipes_to_load) +# recipe_loaded = [] +# python_modules = [] +# print('recipes_to_load', recipes_to_load) +# while recipes_to_load: +# name = recipes_to_load.pop(0) +# if name in recipe_loaded or isinstance(name, (list, tuple)): +# continue +# try: +# recipe = Recipe.get_recipe(name, ctx) +# except IOError: +# info('No recipe named {}; will attempt to install with pip' +# .format(name)) +# python_modules.append(name) +# continue +# except (KeyboardInterrupt, SystemExit): +# raise +# except: +# warning('Failed to import recipe named {}; the recipe exists ' +# 'but appears broken.'.format(name)) +# warning('Exception was:') +# raise +# graph.add(name, name) +# info('Loaded recipe {} (depends on {}{})'.format( +# name, recipe.depends, +# ', conflicts {}'.format(recipe.conflicts) if recipe.conflicts +# else '')) +# for depend in recipe.depends: +# graph.add(name, depend) +# recipes_to_load += recipe.depends +# for conflict in recipe.conflicts: +# if graph.conflicts(conflict): +# warning( +# ('{} conflicts with {}, but both have been ' +# 'included or pulled into the requirements.' +# .format(recipe.name, conflict))) +# warning( +# 'Due to this conflict the build cannot continue, exiting.') +# exit(1) +# python_modules += recipe.python_depends +# recipe_loaded.append(name) +# graph.remove_remaining_conflicts(ctx) +# if len(graph.graphs) > 1: +# info('Found multiple valid recipe sets:') +# for g in graph.graphs: +# info(' {}'.format(g.keys())) +# info_notify('Using the first of these: {}' +# .format(graph.graphs[0].keys())) +# elif len(graph.graphs) == 0: +# warning('Didn\'t find any valid dependency graphs, exiting.') +# exit(1) +# else: +# info('Found a single valid recipe set (this is good)') + +# build_order = list(graph.find_order(0)) +# if bs is None: # It would be better to check against possible +# # orders other than the first one, but in practice +# # there will rarely be clashes, and the user can +# # specify more parameters if necessary to resolve +# # them. +# bs = Bootstrap.get_bootstrap_from_recipes(build_order, ctx) +# if bs is None: +# info('Could not find a bootstrap compatible with the ' +# 'required recipes.') +# info('If you think such a combination should exist, try ' +# 'specifying the bootstrap manually with --bootstrap.') +# exit(1) +# info('{} bootstrap appears compatible with the required recipes.' +# .format(bs.name)) +# info('Checking this...') +# recipes_to_load = bs.recipe_depends +# # This code repeats the code from earlier! Should move to a function: +# while recipes_to_load: +# name = recipes_to_load.pop(0) +# if name in recipe_loaded or isinstance(name, (list, tuple)): +# continue +# try: +# recipe = Recipe.get_recipe(name, ctx) +# except ImportError: +# info('No recipe named {}; will attempt to install with pip' +# .format(name)) +# python_modules.append(name) +# continue +# graph.add(name, name) +# info('Loaded recipe {} (depends on {}{})'.format( +# name, recipe.depends, +# ', conflicts {}'.format(recipe.conflicts) if recipe.conflicts +# else '')) +# for depend in recipe.depends: +# graph.add(name, depend) +# recipes_to_load += recipe.depends +# for conflict in recipe.conflicts: +# if graph.conflicts(conflict): +# warning( +# ('{} conflicts with {}, but both have been ' +# 'included or pulled into the requirements.' +# .format(recipe.name, conflict))) +# warning('Due to this conflict the build cannot continue, ' +# 'exiting.') +# exit(1) +# recipe_loaded.append(name) +# graph.remove_remaining_conflicts(ctx) +# build_order = list(graph.find_order(0)) +# build_order, python_modules, bs = get_recipe_order_and_bootstrap( +# ctx, build_order + python_modules, bs) +# return build_order, python_modules, bs + +# # Do a final check that the new bs doesn't pull in any conflicts From 37515da66bd8867ab837f0f0699fbd06b221192e Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sun, 4 Jun 2017 16:16:36 +0100 Subject: [PATCH 0035/1138] Separated pip install targets from local recipes in graph --- pythonforandroid/bootstrap.py | 9 +++++++-- pythonforandroid/graph.py | 25 +++++++++++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/pythonforandroid/bootstrap.py b/pythonforandroid/bootstrap.py index 008a74e542..c1b6633317 100644 --- a/pythonforandroid/bootstrap.py +++ b/pythonforandroid/bootstrap.py @@ -150,9 +150,14 @@ def get_bootstrap_from_recipes(cls, recipes, ctx): ok = False break for recipe in recipes: - recipe = Recipe.get_recipe(recipe, ctx) + try: + recipe = Recipe.get_recipe(recipe, ctx) + except IOError: + conflicts = [] + else: + conflicts = recipe.conflicts if any([conflict in possible_dependencies - for conflict in recipe.conflicts]): + for conflict in conflicts]): ok = False break if ok: diff --git a/pythonforandroid/graph.py b/pythonforandroid/graph.py index 21cf6a2d17..7e2357736a 100644 --- a/pythonforandroid/graph.py +++ b/pythonforandroid/graph.py @@ -127,7 +127,7 @@ def conflicts(self, name): try: recipe = Recipe.get_recipe(name, self.ctx) conflicts = recipe.conflicts - except OSError: + except IOError: conflicts = [] if any([c in self for c in conflicts]): @@ -152,7 +152,7 @@ def recursively_collect_orders(name, ctx, orders=[]): conflicts = [] else: conflicts = recipe.conflicts - except OSError: + except IOError: # The recipe does not exist, so we assume it can be installed # via pip with no extra dependencies dependencies = [] @@ -209,8 +209,8 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None): possible_orders = [] - # get all possible recipe sets if names includes alternative - # dependencies + # get all possible order graphs, as names may include tuples/lists + # of alternative dependencies names = [([name] if not isinstance(name, (list, tuple)) else name) for name in names] for name_set in product(*names): @@ -258,11 +258,20 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None): if bs is None: bs = Bootstrap.get_bootstrap_from_recipes(chosen_order, ctx) - chosen_order, bs = get_recipe_order_and_bootstrap(ctx, chosen_order, bs=bs) - - return chosen_order, bs - + recipes, python_modules, bs = get_recipe_order_and_bootstrap(ctx, chosen_order, bs=bs) + else: + # check if each requirement has a recipe + recipes = [] + python_modules = [] + for name in chosen_order: + try: + recipe = Recipe.get_recipe(name, ctx) + except IOError: + python_modules.append(name) + else: + recipes.append(name) + return recipes, python_modules, bs # def get_recipe_order_and_bootstrap(ctx, names, bs=None): From 3dd3fa790650d75851c26bd16f9e7ee07355c615 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sun, 4 Jun 2017 16:18:46 +0100 Subject: [PATCH 0036/1138] Removed debug comments and did pep8 fixes in graph --- pythonforandroid/graph.py | 251 ++------------------------------------ 1 file changed, 13 insertions(+), 238 deletions(-) diff --git a/pythonforandroid/graph.py b/pythonforandroid/graph.py index 7e2357736a..45f60cbb58 100644 --- a/pythonforandroid/graph.py +++ b/pythonforandroid/graph.py @@ -3,120 +3,11 @@ from itertools import product from sys import exit -from pythonforandroid.logger import (info, info_notify, warning, error) +from pythonforandroid.logger import (info, warning, error) from pythonforandroid.recipe import Recipe from pythonforandroid.bootstrap import Bootstrap -# class Graph(object): -# # Taken from the old python-for-android/depsort -# # Modified to include alternative dependencies -# def __init__(self): -# # `graph`: dict that maps each package to a set of its dependencies. -# self.graphs = [{}] -# # self.graph = {} - -# def remove_redundant_graphs(self): -# '''Removes possible graphs if they are equivalent to others.''' -# graphs = self.graphs -# # Walk the list backwards so that popping elements doesn't -# # mess up indexing. - -# # n.b. no need to test graph 0 as it will have been tested against -# # all others by the time we get to it -# for i in range(len(graphs) - 1, 0, -1): -# graph = graphs[i] - -# # test graph i against all graphs 0 to i-1 -# for j in range(0, i): -# comparison_graph = graphs[j] - -# if set(comparison_graph.keys()) == set(graph.keys()): -# # graph[i] == graph[j] -# # so remove graph[i] and continue on to testing graph[i-1] -# graphs.pop(i) -# break - -# def add(self, dependent, dependency): -# """Add a dependency relationship to the graph""" -# if isinstance(dependency, (tuple, list)): -# for graph in self.graphs[:]: -# for dep in dependency[1:]: -# new_graph = deepcopy(graph) -# self._add(new_graph, dependent, dep) -# self.graphs.append(new_graph) -# self._add(graph, dependent, dependency[0]) -# else: -# for graph in self.graphs: -# self._add(graph, dependent, dependency) -# self.remove_redundant_graphs() - -# def _add(self, graph, dependent, dependency): -# '''Add a dependency relationship to a specific graph, where dependency -# must be a single dependency, not a list or tuple. -# ''' -# graph.setdefault(dependent, set()) -# graph.setdefault(dependency, set()) -# if dependent != dependency: -# graph[dependent].add(dependency) - -# def conflicts(self, conflict): -# graphs = self.graphs -# initial_num = len(graphs) -# for i in range(len(graphs)): -# graph = graphs[initial_num - 1 - i] -# if conflict in graph: -# graphs.pop(initial_num - 1 - i) -# return len(graphs) == 0 - -# def remove_remaining_conflicts(self, ctx): -# # It's unpleasant to have to pass ctx as an argument... -# '''Checks all possible graphs for conflicts that have arisen during -# the additon of alternative repice branches, as these are not checked -# for conflicts at the time.''' -# new_graphs = [] -# for i, graph in enumerate(self.graphs): -# for name in graph.keys(): -# recipe = Recipe.get_recipe(name, ctx) -# if any([c in graph for c in recipe.conflicts]): -# break -# else: -# new_graphs.append(graph) -# self.graphs = new_graphs - -# def add_optional(self, dependent, dependency): -# """Add an optional (ordering only) dependency relationship to the graph - -# Only call this after all mandatory requirements are added -# """ -# for graph in self.graphs: -# if dependent in graph and dependency in graph: -# self._add(graph, dependent, dependency) - -# def find_order(self, index=0): -# """Do a topological sort on a dependency graph - -# :Parameters: -# :Returns: -# iterator, sorted items form first to last -# """ -# graph = self.graphs[index] -# graph = dict((k, set(v)) for k, v in graph.items()) -# while graph: -# # Find all items without a parent -# leftmost = [l for l, s in graph.items() if not s] -# 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) - - class RecipeOrder(dict): def __init__(self, ctx): @@ -129,11 +20,12 @@ def conflicts(self, name): conflicts = recipe.conflicts except IOError: conflicts = [] - + if any([c in self for c in conflicts]): return True return False + def recursively_collect_orders(name, ctx, orders=[]): '''For each possible recipe ordering, try to add the new recipe name to that order. Recursively do the same thing with all the @@ -146,7 +38,8 @@ def recursively_collect_orders(name, ctx, orders=[]): dependencies = [] else: # make all dependencies into lists so that product will work - dependencies = [([dependency] if not isinstance(dependency, (list, tuple)) + dependencies = [([dependency] if not isinstance( + dependency, (list, tuple)) else dependency) for dependency in recipe.depends] if recipe.conflicts is None: conflicts = [] @@ -200,7 +93,7 @@ def find_order(graph): graph.pop(result) for bset in graph.values(): bset.discard(result) - + def get_recipe_order_and_bootstrap(ctx, names, bs=None): recipes_to_load = set(names) @@ -226,7 +119,8 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None): try: order = find_order(possible_order) except ValueError: # a circular dependency was found - info('Circular dependency found in graph {}, skipping it.'.format(possible_order)) + info('Circular dependency found in graph {}, skipping it.'.format( + possible_order)) continue except: warning('Failed to import recipe named {}; the recipe exists ' @@ -241,7 +135,8 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None): if not orders: error('Didn\'t find any valid dependency graphs.') - error('This means that some of your requirements pull in conflicting dependencies.') + error('This means that some of your requirements pull in ' + 'conflicting dependencies.') error('Exiting.') exit(1) # It would be better to check against possible orders other @@ -258,138 +153,18 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None): if bs is None: bs = Bootstrap.get_bootstrap_from_recipes(chosen_order, ctx) - recipes, python_modules, bs = get_recipe_order_and_bootstrap(ctx, chosen_order, bs=bs) + recipes, python_modules, bs = get_recipe_order_and_bootstrap( + ctx, chosen_order, bs=bs) else: # check if each requirement has a recipe recipes = [] python_modules = [] for name in chosen_order: try: - recipe = Recipe.get_recipe(name, ctx) + Recipe.get_recipe(name, ctx) except IOError: python_modules.append(name) else: recipes.append(name) return recipes, python_modules, bs - - -# def get_recipe_order_and_bootstrap(ctx, names, bs=None): -# '''Takes a list of recipe names and (optionally) a bootstrap. Then -# works out the dependency graph (including bootstrap recipes if -# necessary). Finally, if no bootstrap was initially selected, -# chooses one that supports all the recipes. -# ''' -# graph = Graph() -# recipes_to_load = set(names) -# if bs is not None and bs.recipe_depends: -# info_notify('Bootstrap requires recipes {}'.format(bs.recipe_depends)) -# recipes_to_load = recipes_to_load.union(set(bs.recipe_depends)) -# recipes_to_load = list(recipes_to_load) -# recipe_loaded = [] -# python_modules = [] -# print('recipes_to_load', recipes_to_load) -# while recipes_to_load: -# name = recipes_to_load.pop(0) -# if name in recipe_loaded or isinstance(name, (list, tuple)): -# continue -# try: -# recipe = Recipe.get_recipe(name, ctx) -# except IOError: -# info('No recipe named {}; will attempt to install with pip' -# .format(name)) -# python_modules.append(name) -# continue -# except (KeyboardInterrupt, SystemExit): -# raise -# except: -# warning('Failed to import recipe named {}; the recipe exists ' -# 'but appears broken.'.format(name)) -# warning('Exception was:') -# raise -# graph.add(name, name) -# info('Loaded recipe {} (depends on {}{})'.format( -# name, recipe.depends, -# ', conflicts {}'.format(recipe.conflicts) if recipe.conflicts -# else '')) -# for depend in recipe.depends: -# graph.add(name, depend) -# recipes_to_load += recipe.depends -# for conflict in recipe.conflicts: -# if graph.conflicts(conflict): -# warning( -# ('{} conflicts with {}, but both have been ' -# 'included or pulled into the requirements.' -# .format(recipe.name, conflict))) -# warning( -# 'Due to this conflict the build cannot continue, exiting.') -# exit(1) -# python_modules += recipe.python_depends -# recipe_loaded.append(name) -# graph.remove_remaining_conflicts(ctx) -# if len(graph.graphs) > 1: -# info('Found multiple valid recipe sets:') -# for g in graph.graphs: -# info(' {}'.format(g.keys())) -# info_notify('Using the first of these: {}' -# .format(graph.graphs[0].keys())) -# elif len(graph.graphs) == 0: -# warning('Didn\'t find any valid dependency graphs, exiting.') -# exit(1) -# else: -# info('Found a single valid recipe set (this is good)') - -# build_order = list(graph.find_order(0)) -# if bs is None: # It would be better to check against possible -# # orders other than the first one, but in practice -# # there will rarely be clashes, and the user can -# # specify more parameters if necessary to resolve -# # them. -# bs = Bootstrap.get_bootstrap_from_recipes(build_order, ctx) -# if bs is None: -# info('Could not find a bootstrap compatible with the ' -# 'required recipes.') -# info('If you think such a combination should exist, try ' -# 'specifying the bootstrap manually with --bootstrap.') -# exit(1) -# info('{} bootstrap appears compatible with the required recipes.' -# .format(bs.name)) -# info('Checking this...') -# recipes_to_load = bs.recipe_depends -# # This code repeats the code from earlier! Should move to a function: -# while recipes_to_load: -# name = recipes_to_load.pop(0) -# if name in recipe_loaded or isinstance(name, (list, tuple)): -# continue -# try: -# recipe = Recipe.get_recipe(name, ctx) -# except ImportError: -# info('No recipe named {}; will attempt to install with pip' -# .format(name)) -# python_modules.append(name) -# continue -# graph.add(name, name) -# info('Loaded recipe {} (depends on {}{})'.format( -# name, recipe.depends, -# ', conflicts {}'.format(recipe.conflicts) if recipe.conflicts -# else '')) -# for depend in recipe.depends: -# graph.add(name, depend) -# recipes_to_load += recipe.depends -# for conflict in recipe.conflicts: -# if graph.conflicts(conflict): -# warning( -# ('{} conflicts with {}, but both have been ' -# 'included or pulled into the requirements.' -# .format(recipe.name, conflict))) -# warning('Due to this conflict the build cannot continue, ' -# 'exiting.') -# exit(1) -# recipe_loaded.append(name) -# graph.remove_remaining_conflicts(ctx) -# build_order = list(graph.find_order(0)) -# build_order, python_modules, bs = get_recipe_order_and_bootstrap( -# ctx, build_order + python_modules, bs) -# return build_order, python_modules, bs - -# # Do a final check that the new bs doesn't pull in any conflicts From a6c66e92e99658e4b40e2f4d472da5c345a753f8 Mon Sep 17 00:00:00 2001 From: Justin Partain Date: Mon, 5 Jun 2017 10:13:12 -0400 Subject: [PATCH 0037/1138] Fix typo in pythonforandroid/recipe.py --- pythonforandroid/recipe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/recipe.py b/pythonforandroid/recipe.py index ee8bffa41f..234a111a4d 100644 --- a/pythonforandroid/recipe.py +++ b/pythonforandroid/recipe.py @@ -986,7 +986,7 @@ def build_cython_components(self, arch): '-c', 'import site; print("\\n".join(site.getsitepackages()))') site_packages_dirs = site_packages_dirs.stdout.decode('utf-8').split('\n') if 'PYTHONPATH' in env: - env['PYTHONPATH'] = env + ':{}'.format(':'.join(site_packages_dirs)) + env['PYTHONPATH'] = env['PYTHONPATH'] + ':{}'.format(':'.join(site_packages_dirs)) else: env['PYTHONPATH'] = ':'.join(site_packages_dirs) From 7025e54bda6f7352d4d8e9ab771f41ea2114e415 Mon Sep 17 00:00:00 2001 From: Brent Picasso Date: Mon, 5 Jun 2017 14:47:24 -0700 Subject: [PATCH 0038/1138] implement wakelock for sdl2 --- .../src/org/kivy/android/PythonActivity.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java index 7da0f39a1c..7fcae698c8 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java @@ -183,7 +183,8 @@ protected void onPostExecute(String result) { 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 = 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."); @@ -452,5 +453,27 @@ protected void showLoadingScreen() { } } + + @Override + protected void onPause() { + // fooabc + if ( this.mWakeLock != null && mWakeLock.isHeld()){ + this.mWakeLock.release(); + } + + Log.v(TAG, "onPause()"); + super.onPause(); + } + + @Override + protected void onResume() { + if ( this.mWakeLock != null){ + this.mWakeLock.acquire(); + } + Log.v(TAG, "onResume()"); + super.onResume(); + } + + } From 370a8b763f726d8cea8e28f4af6258957985727c Mon Sep 17 00:00:00 2001 From: germn Date: Sat, 17 Jun 2017 23:11:40 +0300 Subject: [PATCH 0039/1138] Update ffmpeg and deps versions. --- pythonforandroid/recipes/ffmpeg/__init__.py | 6 +++--- pythonforandroid/recipes/libshine/__init__.py | 4 ++-- pythonforandroid/recipes/libx264/__init__.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pythonforandroid/recipes/ffmpeg/__init__.py b/pythonforandroid/recipes/ffmpeg/__init__.py index 4d6c3f5782..3ca7dac608 100644 --- a/pythonforandroid/recipes/ffmpeg/__init__.py +++ b/pythonforandroid/recipes/ffmpeg/__init__.py @@ -8,10 +8,10 @@ class FFMpegRecipe(Recipe): - version = '2.8.8' + version = '3.1.8' # 3.2+ works with bugs url = 'http://ffmpeg.org/releases/ffmpeg-{version}.tar.bz2' - md5sum = 'afeae3b80b7e7e03db957f33a7ef20d2' - depends = ['sdl2'] # Actually no, but we need this to build correct recipe order + md5sum = 'f25a0cdd7f731cfbd8c0f7842b0d15b9' + depends = ['sdl2'] # Need this to build correct recipe order opts_depends = ['openssl', 'ffpyplayer_codecs'] patches = ['patches/fix-libshine-configure.patch'] diff --git a/pythonforandroid/recipes/libshine/__init__.py b/pythonforandroid/recipes/libshine/__init__.py index 7e80d5b69d..26df8d8380 100644 --- a/pythonforandroid/recipes/libshine/__init__.py +++ b/pythonforandroid/recipes/libshine/__init__.py @@ -6,9 +6,9 @@ class LibShineRecipe(Recipe): - version = 'b403b3e8a41377e0576d834b179a5cc7096ff548' # we need master branch + version = '20aee967f67abefd065c196eec7ce21adbbe1549' url = 'https://github.com/toots/shine/archive/{version}.zip' - md5sum = '24cf9488d06f7acf0a0fbb162cc587ab' + md5sum = 'bbf1f657e6adccb5e79f59da9ecfac2d' def should_build(self, arch): build_dir = self.get_build_dir(arch.arch) diff --git a/pythonforandroid/recipes/libx264/__init__.py b/pythonforandroid/recipes/libx264/__init__.py index 8027ef8836..0ec3f2a1fe 100644 --- a/pythonforandroid/recipes/libx264/__init__.py +++ b/pythonforandroid/recipes/libx264/__init__.py @@ -6,9 +6,9 @@ class LibX264Recipe(Recipe): - version = 'x264-snapshot-20161210-2245-stable' # using mirror url since can't use ftp + version = 'x264-snapshot-20170608-2245-stable' # using mirror url since can't use ftp url = 'http://mirror.yandex.ru/mirrors/ftp.videolan.org/x264/snapshots/{version}.tar.bz2' - md5sum = '6bcca94ae1d81ee14236ba9af42135d9' + md5sum = 'adf3b87f759b5cc9f100f8cf99276f77' def should_build(self, arch): build_dir = self.get_build_dir(arch.arch) From 2dcd3cf649bba15b57067ad2be856ff4eb260f0d Mon Sep 17 00:00:00 2001 From: AndreMiras Date: Mon, 19 Jun 2017 17:26:56 +0200 Subject: [PATCH 0040/1138] Add Ethereum related recipes (#1068) * Ethereum related recipes * ethash * libsecp256k1 * pycryptodome * pyethereum * pysha3 * scrypt * secp256k1 See https://github.com/ethereum * Added explicit version For pycryptodome, pyethereum and pysha3 --- pythonforandroid/recipes/ethash/__init__.py | 11 +++++++ .../recipes/libsecp256k1/__init__.py | 32 +++++++++++++++++++ .../recipes/pycryptodome/__init__.py | 11 +++++++ .../recipes/pyethereum/__init__.py | 15 +++++++++ pythonforandroid/recipes/pysha3/__init__.py | 11 +++++++ pythonforandroid/recipes/scrypt/__init__.py | 31 ++++++++++++++++++ .../recipes/scrypt/remove_librt.patch | 20 ++++++++++++ .../recipes/secp256k1/__init__.py | 32 +++++++++++++++++++ .../recipes/secp256k1/cross_compile.patch | 12 +++++++ .../secp256k1/drop_setup_requires.patch | 12 +++++++ .../recipes/secp256k1/find_lib.patch | 13 ++++++++ .../recipes/secp256k1/pkg-config.patch | 12 +++++++ 12 files changed, 212 insertions(+) create mode 100644 pythonforandroid/recipes/ethash/__init__.py create mode 100644 pythonforandroid/recipes/libsecp256k1/__init__.py create mode 100644 pythonforandroid/recipes/pycryptodome/__init__.py create mode 100644 pythonforandroid/recipes/pyethereum/__init__.py create mode 100644 pythonforandroid/recipes/pysha3/__init__.py create mode 100644 pythonforandroid/recipes/scrypt/__init__.py create mode 100644 pythonforandroid/recipes/scrypt/remove_librt.patch create mode 100644 pythonforandroid/recipes/secp256k1/__init__.py create mode 100644 pythonforandroid/recipes/secp256k1/cross_compile.patch create mode 100644 pythonforandroid/recipes/secp256k1/drop_setup_requires.patch create mode 100644 pythonforandroid/recipes/secp256k1/find_lib.patch create mode 100644 pythonforandroid/recipes/secp256k1/pkg-config.patch diff --git a/pythonforandroid/recipes/ethash/__init__.py b/pythonforandroid/recipes/ethash/__init__.py new file mode 100644 index 0000000000..403513d89d --- /dev/null +++ b/pythonforandroid/recipes/ethash/__init__.py @@ -0,0 +1,11 @@ +from pythonforandroid.recipe import PythonRecipe + + +class EthashRecipe(PythonRecipe): + + url = 'https://github.com/ethereum/ethash/archive/master.zip' + + depends = ['python2', 'setuptools'] + + +recipe = EthashRecipe() diff --git a/pythonforandroid/recipes/libsecp256k1/__init__.py b/pythonforandroid/recipes/libsecp256k1/__init__.py new file mode 100644 index 0000000000..a8552577eb --- /dev/null +++ b/pythonforandroid/recipes/libsecp256k1/__init__.py @@ -0,0 +1,32 @@ +from pythonforandroid.toolchain import shprint, current_directory +from pythonforandroid.recipe import Recipe +from multiprocessing import cpu_count +from os.path import exists +import sh + + +class LibSecp256k1Recipe(Recipe): + + url = 'https://github.com/bitcoin-core/secp256k1/archive/master.zip' + + def build_arch(self, arch): + super(LibSecp256k1Recipe, self).build_arch(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.toolchain_prefix, + '--prefix=' + self.ctx.get_python_install_dir(), + '--enable-shared', + '--enable-module-recovery', + '--enable-experimental', + '--enable-module-ecdh', + _env=env) + shprint(sh.make, '-j' + str(cpu_count()), _env=env) + libs = ['.libs/libsecp256k1.so'] + self.install_libs(arch, *libs) + + +recipe = LibSecp256k1Recipe() diff --git a/pythonforandroid/recipes/pycryptodome/__init__.py b/pythonforandroid/recipes/pycryptodome/__init__.py new file mode 100644 index 0000000000..fe7ac40d51 --- /dev/null +++ b/pythonforandroid/recipes/pycryptodome/__init__.py @@ -0,0 +1,11 @@ +from pythonforandroid.recipe import PythonRecipe + + +class PycryptodomeRecipe(PythonRecipe): + version = 'v3.4.6' + url = 'https://github.com/Legrandin/pycryptodome/archive/{version}.tar.gz' + + depends = ['python2', 'setuptools'] + + +recipe = PycryptodomeRecipe() diff --git a/pythonforandroid/recipes/pyethereum/__init__.py b/pythonforandroid/recipes/pyethereum/__init__.py new file mode 100644 index 0000000000..f08c073308 --- /dev/null +++ b/pythonforandroid/recipes/pyethereum/__init__.py @@ -0,0 +1,15 @@ +from pythonforandroid.recipe import PythonRecipe + + +class PyethereumRecipe(PythonRecipe): + version = 'v1.6.1' + url = 'https://github.com/ethereum/pyethereum/archive/{version}.tar.gz' + + depends = [ + 'python2', 'setuptools', 'pycryptodome', 'pysha3', 'ethash', 'scrypt' + ] + + call_hostpython_via_targetpython = False + + +recipe = PyethereumRecipe() diff --git a/pythonforandroid/recipes/pysha3/__init__.py b/pythonforandroid/recipes/pysha3/__init__.py new file mode 100644 index 0000000000..3b98d59337 --- /dev/null +++ b/pythonforandroid/recipes/pysha3/__init__.py @@ -0,0 +1,11 @@ +from pythonforandroid.recipe import PythonRecipe + + +class Pysha3Recipe(PythonRecipe): + version = '1.0.2' + url = 'https://github.com/tiran/pysha3/archive/{version}.tar.gz' + + depends = ['python2', 'setuptools'] + + +recipe = Pysha3Recipe() diff --git a/pythonforandroid/recipes/scrypt/__init__.py b/pythonforandroid/recipes/scrypt/__init__.py new file mode 100644 index 0000000000..035b19abb4 --- /dev/null +++ b/pythonforandroid/recipes/scrypt/__init__.py @@ -0,0 +1,31 @@ +from pythonforandroid.toolchain import CythonRecipe +from os.path import join + + +class ScryptRecipe(CythonRecipe): + + url = 'https://bitbucket.org/mhallin/py-scrypt/get/default.zip' + + depends = ['python2', '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(ScryptRecipe, self).get_recipe_env(arch, with_flags_in_cc) + openssl_build_dir = self.get_recipe( + 'openssl', self.ctx).get_build_dir(arch.arch) + print("openssl_build_dir:", openssl_build_dir) + env['CC'] = '%s -I%s' % (env['CC'], join(openssl_build_dir, 'include')) + env['LDFLAGS'] = env['LDFLAGS'] + ' -L{}'.format( + self.ctx.get_libs_dir(arch.arch) + + '-L{}'.format(self.ctx.libs_dir)) + ' -L{}'.format( + openssl_build_dir) + return env + + +recipe = ScryptRecipe() diff --git a/pythonforandroid/recipes/scrypt/remove_librt.patch b/pythonforandroid/recipes/scrypt/remove_librt.patch new file mode 100644 index 0000000000..37d56a6355 --- /dev/null +++ b/pythonforandroid/recipes/scrypt/remove_librt.patch @@ -0,0 +1,20 @@ +diff -r 91d194b6a6bd setup.py +--- a/setup.py Sat Sep 17 15:29:49 2016 +0200 ++++ b/setup.py Mon May 29 07:30:24 2017 +0000 +@@ -13,7 +13,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'), +@@ -21,7 +20,7 @@ + ('HAVE_SYSINFO', '1'), + ('HAVE_SYS_SYSINFO_H', '1'), + ('_FILE_OFFSET_BITS', '64')] +- libraries = ['crypto', 'rt'] ++ libraries = ['crypto'] + CFLAGS.append('-O2') + elif sys.platform.startswith('win32'): + define_macros = [('inline', '__inline')] diff --git a/pythonforandroid/recipes/secp256k1/__init__.py b/pythonforandroid/recipes/secp256k1/__init__.py new file mode 100644 index 0000000000..53ffd497ae --- /dev/null +++ b/pythonforandroid/recipes/secp256k1/__init__.py @@ -0,0 +1,32 @@ +from os.path import join +from pythonforandroid.recipe import CompiledComponentsPythonRecipe + + +class Secp256k1Recipe(CompiledComponentsPythonRecipe): + + url = 'https://github.com/ludbb/secp256k1-py/archive/master.zip' + + call_hostpython_via_targetpython = False + + depends = [ + 'openssl', 'hostpython2', 'python2', 'setuptools', + 'libffi', 'cffi', 'libsecp256k1'] + + patches = ["cross_compile.patch", "pkg-config.patch", "find_lib.patch"] + + def get_recipe_env(self, arch=None): + env = super(Secp256k1Recipe, self).get_recipe_env(arch) + libsecp256k1 = self.get_recipe('libsecp256k1', self.ctx) + libsecp256k1_dir = libsecp256k1.get_build_dir(arch.arch) + env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() + env['CFLAGS'] = ' -I' + join(libsecp256k1_dir, 'include') + env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python2.7' + env['LDSHARED'] = env['CC'] + \ + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' + env['LDFLAGS'] += ' -L{}'.format(libsecp256k1_dir) + # TODO: hardcoded Python version + env['LDFLAGS'] += " -landroid -lpython2.7 -lsecp256k1" + return env + + +recipe = Secp256k1Recipe() diff --git a/pythonforandroid/recipes/secp256k1/cross_compile.patch b/pythonforandroid/recipes/secp256k1/cross_compile.patch new file mode 100644 index 0000000000..bfef228193 --- /dev/null +++ b/pythonforandroid/recipes/secp256k1/cross_compile.patch @@ -0,0 +1,12 @@ +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=%s" % os.environ['TOOLCHAIN_PREFIX'], + "--prefix", + os.path.abspath(self.build_clib), + ] diff --git a/pythonforandroid/recipes/secp256k1/drop_setup_requires.patch b/pythonforandroid/recipes/secp256k1/drop_setup_requires.patch new file mode 100644 index 0000000000..3be02934ba --- /dev/null +++ b/pythonforandroid/recipes/secp256k1/drop_setup_requires.patch @@ -0,0 +1,12 @@ +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'], + diff --git a/pythonforandroid/recipes/secp256k1/find_lib.patch b/pythonforandroid/recipes/secp256k1/find_lib.patch new file mode 100644 index 0000000000..87997d5d85 --- /dev/null +++ b/pythonforandroid/recipes/secp256k1/find_lib.patch @@ -0,0 +1,13 @@ +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: diff --git a/pythonforandroid/recipes/secp256k1/pkg-config.patch b/pythonforandroid/recipes/secp256k1/pkg-config.patch new file mode 100644 index 0000000000..f3acc4ee0d --- /dev/null +++ b/pythonforandroid/recipes/secp256k1/pkg-config.patch @@ -0,0 +1,12 @@ +diff --git a/setup_support.py b/setup_support.py +index 68a2a7f..42e85a3 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: From 909f5b227568570c4b7c356390148ba4a02cb608 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Thu, 20 Jul 2017 00:12:31 +0200 Subject: [PATCH 0041/1138] Various Ethereum related recipes fixes * pycryptodome * secp256k1 * pbkdf2 --- pythonforandroid/recipes/pbkdf2/__init__.py | 12 ++++++++++++ .../recipes/pycryptodome/__init__.py | 2 +- pythonforandroid/recipes/secp256k1/__init__.py | 10 ++++++---- .../recipes/secp256k1/no-download.patch | 13 +++++++++++++ .../recipes/secp256k1/pkg-config.patch | 18 +++++++++++++++++- 5 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 pythonforandroid/recipes/pbkdf2/__init__.py create mode 100644 pythonforandroid/recipes/secp256k1/no-download.patch diff --git a/pythonforandroid/recipes/pbkdf2/__init__.py b/pythonforandroid/recipes/pbkdf2/__init__.py new file mode 100644 index 0000000000..d1d5d7b442 --- /dev/null +++ b/pythonforandroid/recipes/pbkdf2/__init__.py @@ -0,0 +1,12 @@ +from pythonforandroid.recipe import PythonRecipe + + +class Pbkdf2Recipe(PythonRecipe): + + # TODO: version + url = 'https://github.com/dlitz/python-pbkdf2/archive/master.zip' + + depends = ['setuptools'] + + +recipe = Pbkdf2Recipe() diff --git a/pythonforandroid/recipes/pycryptodome/__init__.py b/pythonforandroid/recipes/pycryptodome/__init__.py index fe7ac40d51..3fa007be68 100644 --- a/pythonforandroid/recipes/pycryptodome/__init__.py +++ b/pythonforandroid/recipes/pycryptodome/__init__.py @@ -5,7 +5,7 @@ class PycryptodomeRecipe(PythonRecipe): version = 'v3.4.6' url = 'https://github.com/Legrandin/pycryptodome/archive/{version}.tar.gz' - depends = ['python2', 'setuptools'] + depends = ['python2', 'setuptools', 'cffi'] recipe = PycryptodomeRecipe() diff --git a/pythonforandroid/recipes/secp256k1/__init__.py b/pythonforandroid/recipes/secp256k1/__init__.py index 53ffd497ae..a4ef6211ac 100644 --- a/pythonforandroid/recipes/secp256k1/__init__.py +++ b/pythonforandroid/recipes/secp256k1/__init__.py @@ -1,8 +1,8 @@ from os.path import join -from pythonforandroid.recipe import CompiledComponentsPythonRecipe +from pythonforandroid.recipe import PythonRecipe -class Secp256k1Recipe(CompiledComponentsPythonRecipe): +class Secp256k1Recipe(PythonRecipe): url = 'https://github.com/ludbb/secp256k1-py/archive/master.zip' @@ -10,9 +10,11 @@ class Secp256k1Recipe(CompiledComponentsPythonRecipe): depends = [ 'openssl', 'hostpython2', 'python2', 'setuptools', - 'libffi', 'cffi', 'libsecp256k1'] + 'libffi', 'cffi', 'libffi', 'libsecp256k1'] - patches = ["cross_compile.patch", "pkg-config.patch", "find_lib.patch"] + 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(Secp256k1Recipe, self).get_recipe_env(arch) diff --git a/pythonforandroid/recipes/secp256k1/no-download.patch b/pythonforandroid/recipes/secp256k1/no-download.patch new file mode 100644 index 0000000000..e905a39a8a --- /dev/null +++ b/pythonforandroid/recipes/secp256k1/no-download.patch @@ -0,0 +1,13 @@ +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") diff --git a/pythonforandroid/recipes/secp256k1/pkg-config.patch b/pythonforandroid/recipes/secp256k1/pkg-config.patch index f3acc4ee0d..bb1e344eae 100644 --- a/pythonforandroid/recipes/secp256k1/pkg-config.patch +++ b/pythonforandroid/recipes/secp256k1/pkg-config.patch @@ -1,5 +1,21 @@ +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..42e85a3 100644 +index 68a2a7f..ccbafac 100644 --- a/setup_support.py +++ b/setup_support.py @@ -40,6 +40,7 @@ def absolute(*paths): From 04f51198230509c7b3fa61323cd3576355ca4150 Mon Sep 17 00:00:00 2001 From: Peter Badida Date: Fri, 11 Aug 2017 21:38:48 +0200 Subject: [PATCH 0042/1138] Update copyright year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index b336a3aa7b..d5d6b13c85 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010-2015 Kivy Team and other contributors +Copyright (c) 2010-2017 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 From a8ace86160e954f606f1108e34ad43f2205ece01 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sat, 12 Aug 2017 22:48:21 +0100 Subject: [PATCH 0043/1138] Fixed indentation inconsistency in protobuf_cpp recipe --- .../recipes/protobuf_cpp/__init__.py | 184 +++++++++--------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/pythonforandroid/recipes/protobuf_cpp/__init__.py b/pythonforandroid/recipes/protobuf_cpp/__init__.py index 53ac8fd080..c135c3d68e 100644 --- a/pythonforandroid/recipes/protobuf_cpp/__init__.py +++ b/pythonforandroid/recipes/protobuf_cpp/__init__.py @@ -11,99 +11,99 @@ class ProtobufCppRecipe(PythonRecipe): - name = 'protobuf_cpp' - version = '3.1.0' - 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' - - def build_arch(self, arch): - env = self.get_recipe_env(arch) - - # Build libproto.a - with current_directory(self.get_build_dir(arch.arch)): - env['HOSTARCH'] = 'arm-eabi' - env['BUILDARCH'] = 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'), - '--host={}'.format(env['HOSTARCH']), - '--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) - shprint(sh.cp, '.libs/libprotobuf.a', join(self.ctx.get_libs_dir(arch.arch), 'libprotobuf.a')) - - # Copy stl library + name = 'protobuf_cpp' + version = '3.1.0' + 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' + + def build_arch(self, arch): + env = self.get_recipe_env(arch) + + # Build libproto.a + with current_directory(self.get_build_dir(arch.arch)): + env['HOSTARCH'] = 'arm-eabi' + env['BUILDARCH'] = 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'), + '--host={}'.format(env['HOSTARCH']), + '--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) + shprint(sh.cp, '.libs/libprotobuf.a', join(self.ctx.get_libs_dir(arch.arch), 'libprotobuf.a')) + + # Copy stl library shutil.copyfile(self.ctx.ndk_dir + '/sources/cxx-stl/gnu-libstdc++/' + self.ctx.toolchain_version + '/libs/' + arch.arch + '/libgnustl_shared.so', - join(self.ctx.get_libs_dir(arch.arch), 'libgnustl_shared.so')) - - # Build python bindings and _message.so - with current_directory(join(self.get_build_dir(arch.arch), 'python')): - hostpython = sh.Command(self.hostpython_location) - shprint(hostpython, - 'setup.py', - 'build_ext', - '--cpp_implementation' - , _env=env) - - # Install python bindings - self.install_python_package(arch) - - - 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) - - if self.ctx.python_recipe.from_crystax: - hpenv = env.copy() - shprint(hostpython, 'setup.py', 'install', '-O2', - '--root={}'.format(self.ctx.get_python_install_dir()), - '--install-lib=.', - '--cpp_implementation', - _env=hpenv, *self.setup_extra_args) - else: - hppath = join(dirname(self.hostpython_location), 'Lib', - 'site-packages') - hpenv = env.copy() - if 'PYTHONPATH' in hpenv: - hpenv['PYTHONPATH'] = ':'.join([hppath] + - hpenv['PYTHONPATH'].split(':')) - else: - hpenv['PYTHONPATH'] = hppath - shprint(hostpython, 'setup.py', 'install', '-O2', - '--root={}'.format(self.ctx.get_python_install_dir()), - '--install-lib=lib/python2.7/site-packages', - '--cpp_implementation', - _env=hpenv, *self.setup_extra_args) - - - def get_recipe_env(self, arch): - env = super(ProtobufCppRecipe, self).get_recipe_env(arch) - env['PROTOC'] = '/home/fipo/soft/protobuf-3.1.0/src/protoc' - env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() - env['TARGET_OS'] = 'OS_ANDROID_CROSSCOMPILE' - env['CFLAGS'] += ' -I' + self.ctx.ndk_dir + '/platforms/android-' + str( - self.ctx.android_api) + '/arch-' + arch.arch.replace('eabi', '') + '/usr/include' + \ - ' -I' + self.ctx.ndk_dir + '/sources/cxx-stl/gnu-libstdc++/' + self.ctx.toolchain_version + '/include' + \ - ' -I' + self.ctx.ndk_dir + '/sources/cxx-stl/gnu-libstdc++/' + self.ctx.toolchain_version + '/libs/' + arch.arch + '/include' + \ - ' -I' + env['PYTHON_ROOT'] + '/include/python2.7' - env['CXXFLAGS'] = env['CFLAGS'] - env['CXXFLAGS'] += ' -frtti' - env['CXXFLAGS'] += ' -fexceptions' - env['LDFLAGS'] += ' -L' + self.ctx.ndk_dir + '/sources/cxx-stl/gnu-libstdc++/' + self.ctx.toolchain_version + '/libs/' + arch.arch + \ - ' -lgnustl_shared -lpython2.7' - - env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' - return env + join(self.ctx.get_libs_dir(arch.arch), 'libgnustl_shared.so')) + + # Build python bindings and _message.so + with current_directory(join(self.get_build_dir(arch.arch), 'python')): + hostpython = sh.Command(self.hostpython_location) + shprint(hostpython, + 'setup.py', + 'build_ext', + '--cpp_implementation' + , _env=env) + + # Install python bindings + self.install_python_package(arch) + + + 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) + + if self.ctx.python_recipe.from_crystax: + hpenv = env.copy() + shprint(hostpython, 'setup.py', 'install', '-O2', + '--root={}'.format(self.ctx.get_python_install_dir()), + '--install-lib=.', + '--cpp_implementation', + _env=hpenv, *self.setup_extra_args) + else: + hppath = join(dirname(self.hostpython_location), 'Lib', + 'site-packages') + hpenv = env.copy() + if 'PYTHONPATH' in hpenv: + hpenv['PYTHONPATH'] = ':'.join([hppath] + + hpenv['PYTHONPATH'].split(':')) + else: + hpenv['PYTHONPATH'] = hppath + shprint(hostpython, 'setup.py', 'install', '-O2', + '--root={}'.format(self.ctx.get_python_install_dir()), + '--install-lib=lib/python2.7/site-packages', + '--cpp_implementation', + _env=hpenv, *self.setup_extra_args) + + + def get_recipe_env(self, arch): + env = super(ProtobufCppRecipe, self).get_recipe_env(arch) + env['PROTOC'] = '/home/fipo/soft/protobuf-3.1.0/src/protoc' + env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() + env['TARGET_OS'] = 'OS_ANDROID_CROSSCOMPILE' + env['CFLAGS'] += ' -I' + self.ctx.ndk_dir + '/platforms/android-' + str( + self.ctx.android_api) + '/arch-' + arch.arch.replace('eabi', '') + '/usr/include' + \ + ' -I' + self.ctx.ndk_dir + '/sources/cxx-stl/gnu-libstdc++/' + self.ctx.toolchain_version + '/include' + \ + ' -I' + self.ctx.ndk_dir + '/sources/cxx-stl/gnu-libstdc++/' + self.ctx.toolchain_version + '/libs/' + arch.arch + '/include' + \ + ' -I' + env['PYTHON_ROOT'] + '/include/python2.7' + env['CXXFLAGS'] = env['CFLAGS'] + env['CXXFLAGS'] += ' -frtti' + env['CXXFLAGS'] += ' -fexceptions' + env['LDFLAGS'] += ' -L' + self.ctx.ndk_dir + '/sources/cxx-stl/gnu-libstdc++/' + self.ctx.toolchain_version + '/libs/' + arch.arch + \ + ' -lgnustl_shared -lpython2.7' + + env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' + return env recipe = ProtobufCppRecipe() From 5869eb23e5b040e4ab90434b9fec42b9b53f7c17 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sat, 12 Aug 2017 22:54:02 +0100 Subject: [PATCH 0044/1138] Made recipe list ignore broken recipes --- pythonforandroid/toolchain.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pythonforandroid/toolchain.py b/pythonforandroid/toolchain.py index ceb18970e6..90f55fad6c 100644 --- a/pythonforandroid/toolchain.py +++ b/pythonforandroid/toolchain.py @@ -553,6 +553,11 @@ def recipes(self, args): recipe = Recipe.get_recipe(name, ctx) except IOError: 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}' From cee77181d025ea2f72c37f61abaad0aa53252dc4 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sun, 13 Aug 2017 15:20:59 +0100 Subject: [PATCH 0045/1138] Made logger convert output to utf-8 including errors This prevents a regular error in the python2 build where the output is in byte format but is not valid utf-8, so the logger crashes because the format is wrong. --- pythonforandroid/logger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pythonforandroid/logger.py b/pythonforandroid/logger.py index 93c086f821..64d5f2c452 100644 --- a/pythonforandroid/logger.py +++ b/pythonforandroid/logger.py @@ -174,6 +174,8 @@ def shprint(command, *args, **kwargs): output = command(*args, **kwargs) for line in output: if logger.level > logging.DEBUG: + if isinstance(line, bytes): + line = line.decode('utf-8', errors='replace') msg = line.replace( '\n', ' ').replace( '\t', ' ').replace( From d086280a2bdbb28ab42d74cd96f6fef64fce105a Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sun, 13 Aug 2017 15:56:58 +0100 Subject: [PATCH 0046/1138] Moved logger bytes conversion to affect all lines --- pythonforandroid/logger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythonforandroid/logger.py b/pythonforandroid/logger.py index 64d5f2c452..75fee40d1a 100644 --- a/pythonforandroid/logger.py +++ b/pythonforandroid/logger.py @@ -173,9 +173,9 @@ def shprint(command, *args, **kwargs): 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 isinstance(line, bytes): - line = line.decode('utf-8', errors='replace') msg = line.replace( '\n', ' ').replace( '\t', ' ').replace( From e48bc2aa432a84bb4c86ca67a2c2236c98059973 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sun, 13 Aug 2017 16:19:29 +0100 Subject: [PATCH 0047/1138] Made graph recognise python_depends --- pythonforandroid/graph.py | 4 +++- pythonforandroid/toolchain.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pythonforandroid/graph.py b/pythonforandroid/graph.py index 45f60cbb58..2030718593 100644 --- a/pythonforandroid/graph.py +++ b/pythonforandroid/graph.py @@ -161,10 +161,12 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None): python_modules = [] for name in chosen_order: try: - Recipe.get_recipe(name, ctx) + recipe = Recipe.get_recipe(name, ctx) + python_modules += recipe.python_depends except IOError: python_modules.append(name) else: recipes.append(name) + python_modules = list(set(python_modules)) return recipes, python_modules, bs diff --git a/pythonforandroid/toolchain.py b/pythonforandroid/toolchain.py index 90f55fad6c..65908d3047 100644 --- a/pythonforandroid/toolchain.py +++ b/pythonforandroid/toolchain.py @@ -182,6 +182,8 @@ def build_dist_from_args(ctx, dist, args): bs.distribution = dist info_notify('Dist will have name {} and recipes ({})'.format( dist.name, ', '.join(dist.recipes))) + info('Dist will also contain modules ({}) installed from pip'.format( + ', '.join(ctx.python_modules))) ctx.dist_name = bs.distribution.name ctx.prepare_bootstrap(bs) From 149b76885052000a5395bc4d4a2efb179f5e92c4 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 20 Aug 2017 22:10:10 +0300 Subject: [PATCH 0048/1138] Deleted whitespace --- doc/source/buildoptions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/buildoptions.rst b/doc/source/buildoptions.rst index a8ff5f56f6..bb95d3969c 100644 --- a/doc/source/buildoptions.rst +++ b/doc/source/buildoptions.rst @@ -95,7 +95,7 @@ options (this list may not be exhaustive): ``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, +- ``--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. From 3a0388368795803cae58c8f7ad2f008591015b52 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sun, 20 Aug 2017 22:37:52 +0100 Subject: [PATCH 0049/1138] Improved some testapp setup.py files --- pythonforandroid/recipes/flask/__init__.py | 2 +- testapps/setup_pygame.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pythonforandroid/recipes/flask/__init__.py b/pythonforandroid/recipes/flask/__init__.py index 0a3e6fe0d8..c21b9d9128 100644 --- a/pythonforandroid/recipes/flask/__init__.py +++ b/pythonforandroid/recipes/flask/__init__.py @@ -9,7 +9,7 @@ class FlaskRecipe(PythonRecipe): # 0.10.1 at least for now url = 'https://github.com/pallets/flask/archive/{version}.zip' - depends = [('python2', 'python3crystax'), 'setuptools'] + depends = [('python2', 'python3crystax'), 'setuptools', 'genericndkbuild'] python_depends = ['jinja2', 'werkzeug', 'markupsafe', 'itsdangerous', 'click'] diff --git a/testapps/setup_pygame.py b/testapps/setup_pygame.py index dcfdb47aff..3f96942b29 100644 --- a/testapps/setup_pygame.py +++ b/testapps/setup_pygame.py @@ -3,7 +3,7 @@ from setuptools import find_packages options = {'apk': {'debug': None, - 'requirements': 'pygame,pyjnius,kivy,python2', + 'requirements': 'pygame,pyjnius,kivy,python2,android', 'android-api': 19, 'ndk-dir': '/home/asandy/android/crystax-ndk-10.3.2', 'dist-name': 'bdisttest_pygame', From 1d460e5e1c1f8f7fad5c7ce3f7623160edf6f9aa Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sun, 20 Aug 2017 22:38:23 +0100 Subject: [PATCH 0050/1138] Made ANDROID_UNPACK source explicitly the same as ANDROID_ARGUMENT (under sdl2) --- .../sdl2/build/src/org/kivy/android/PythonActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java index 79f080928f..6a0c4d3040 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java @@ -172,7 +172,7 @@ protected void onPostExecute(String result) { 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", mFilesDirectory + "/app"); + 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"); From 7aee0147511d0743b87263ffcdf754e54cba736e Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sun, 20 Aug 2017 22:40:50 +0100 Subject: [PATCH 0051/1138] Changed order of pyjnius optional dependencies genericndkbuild will now be preferred over sdl2 --- pythonforandroid/recipes/pyjnius/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/recipes/pyjnius/__init__.py b/pythonforandroid/recipes/pyjnius/__init__.py index 7aad8d46ee..81fc323d7d 100644 --- a/pythonforandroid/recipes/pyjnius/__init__.py +++ b/pythonforandroid/recipes/pyjnius/__init__.py @@ -9,7 +9,7 @@ class PyjniusRecipe(CythonRecipe): version = 'master' url = 'https://github.com/kivy/pyjnius/archive/{version}.zip' name = 'pyjnius' - depends = [('python2', 'python3crystax'), ('sdl2', 'sdl', 'genericndkbuild'), 'six'] + depends = [('python2', 'python3crystax'), ('genericndkbuild', 'sdl2', 'sdl'), 'six'] site_packages_name = 'jnius' patches = [('sdl2_jnienv_getter.patch', will_build('sdl2')), From a8248ea0e73a4caf51c45bbdb3e2151fe3c1d138 Mon Sep 17 00:00:00 2001 From: "Mark W. Alexander" Date: Mon, 21 Aug 2017 12:43:51 -0400 Subject: [PATCH 0052/1138] glob.so - glob is not in Android's libc --- pythonforandroid/recipes/libglob/__init__.py | 74 ++ pythonforandroid/recipes/libglob/glob.patch | 1016 ++++++++++++++++++ 2 files changed, 1090 insertions(+) create mode 100644 pythonforandroid/recipes/libglob/__init__.py create mode 100644 pythonforandroid/recipes/libglob/glob.patch diff --git a/pythonforandroid/recipes/libglob/__init__.py b/pythonforandroid/recipes/libglob/__init__.py new file mode 100644 index 0000000000..eca17f9bd3 --- /dev/null +++ b/pythonforandroid/recipes/libglob/__init__.py @@ -0,0 +1,74 @@ +""" + android libglob + available via '-lglob' LDFLAG +""" +from os.path import exists, join, dirname +from pythonforandroid.toolchain import (CompiledComponentsPythonRecipe, + current_directory) +from pythonforandroid.logger import shprint, info, warning, info_main +import sh + +class LibGlobRecipe(CompiledComponentsPythonRecipe): + """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' + + depends = [('hostpython2', 'hostpython3'), ('python2', 'python3crystax')] + 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) + if not exists(path): + info("creating {}".format(path)) + shprint(sh.mkdir, '-p', 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'), + ): + if not exists(path): + info("creating {}".format(path)) + shprint(sh.mkdir, '-p', path) + cli = env['CC'].split() + cc = sh.Command(cli[0]) + + with current_directory(self.get_build_dir(arch.arch)): + cflags = env['CFLAGS'].split() + cflags.extend(['-I.', '-c', '-l.', 'glob.c', '-I.']) # , '-o', 'glob.o']) + shprint(cc, *cflags, _env=env) + + cflags = env['CFLAGS'].split() + srindex = cflags.index('--sysroot') + if srindex: + cflags[srindex+1] = self.ctx.ndk_platform + cflags.extend(['-shared', '-I.', 'glob.o', '-o', 'libglob.so']) + shprint(cc, *cflags, _env=env) + + shprint(sh.cp, 'libglob.so', join(self.ctx.libs_dir, arch.arch)) + shprint(sh.cp, "libglob.so", join(self.ctx.get_python_install_dir(), 'lib')) + # drop header in to the Python include directory + shprint(sh.cp, "glob.h", join(self.ctx.get_python_install_dir(), + 'include/python{}'.format( + self.ctx.python_recipe.version[0:3] + ) + ) + ) + include_path = join(self.ctx.python_recipe.get_build_dir(arch.arch), 'Include') + shprint(sh.cp, "glob.h", include_path) + +recipe = LibGlobRecipe() diff --git a/pythonforandroid/recipes/libglob/glob.patch b/pythonforandroid/recipes/libglob/glob.patch new file mode 100644 index 0000000000..c7fe81738f --- /dev/null +++ b/pythonforandroid/recipes/libglob/glob.patch @@ -0,0 +1,1016 @@ +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,102 @@ ++/* ++ * 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 ++#define ARG_MAX 6553 ++ ++#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; ++ ++/* 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_ */ From 29ef2e1d10a1a601aba08f00ad855f5663662035 Mon Sep 17 00:00:00 2001 From: "Mark W. Alexander" Date: Mon, 21 Aug 2017 14:25:14 -0400 Subject: [PATCH 0053/1138] Support unified headers in NDK 15+ https://android.googlesource.com/platform/ndk/+/ndk-r15-release/docs/UnifiedHeaders.md - adds --isysroot and -isystem options for unified header includes - changes --sysroot to use the prebuilt NDK /sysroot --- pythonforandroid/archs.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pythonforandroid/archs.py b/pythonforandroid/archs.py index 8f19485a4f..00d8626b77 100644 --- a/pythonforandroid/archs.py +++ b/pythonforandroid/archs.py @@ -1,4 +1,4 @@ -from os.path import (join, dirname) +from os.path import (exists, join, dirname) from os import environ, uname import sys from distutils.spawn import find_executable @@ -33,13 +33,27 @@ def include_dirs(self): def get_env(self, with_flags_in_cc=True): env = {} - env["CFLAGS"] = " ".join([ - "-DANDROID", "-mandroid", "-fomit-frame-pointer", - "--sysroot", self.ctx.ndk_platform]) + env['CFLAGS'] = ' '.join([ + '-DANDROID', '-mandroid', '-fomit-frame-pointer' + ' -D__ANDROID_API__={}'.format(self.ctx._android_api), + ]) + env['LDFLAGS'] = ' ' + + sysroot = join(self.ctx._ndk_dir, 'sysroot') + if exists(sysroot): + # post-15 NDK per + # https://android.googlesource.com/platform/ndk/+/ndk-r15-release/docs/UnifiedHeaders.md + env['CFLAGS'] += ' -isystem {}/sysroot/usr/include/{}'.format( + self.ctx.ndk_dir, self.ctx.toolchain_prefix) + else: + sysroot = self.ctx.ndk_platform + env['CFLAGS'] += ' -I{}'.format(self.ctx.ndk_platform) + env['CFLAGS'] += ' -isysroot {} '.format(sysroot) + env['LDFLAGS'] += '--sysroot {} '.format(self.ctx.ndk_platform) env["CXXFLAGS"] = env["CFLAGS"] - env["LDFLAGS"] = " ".join(['-lm', '-L' + self.ctx.get_libs_dir(self.arch)]) + env["LDFLAGS"] += " ".join(['-lm', '-L' + self.ctx.get_libs_dir(self.arch)]) if self.ctx.ndk == 'crystax': env['LDFLAGS'] += ' -L{}/sources/crystax/libs/{} -lcrystax'.format(self.ctx.ndk_dir, self.arch) From b591cbb84db51b90530ce8b4ca1f74dec95fb16a Mon Sep 17 00:00:00 2001 From: "Mark W. Alexander" Date: Mon, 21 Aug 2017 14:30:10 -0400 Subject: [PATCH 0054/1138] - remove LIBS from environment to prevent user setting breaking linking --- pythonforandroid/recipes/hostpython2/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pythonforandroid/recipes/hostpython2/__init__.py b/pythonforandroid/recipes/hostpython2/__init__.py index 785c4b7387..ddc3f6e695 100644 --- a/pythonforandroid/recipes/hostpython2/__init__.py +++ b/pythonforandroid/recipes/hostpython2/__init__.py @@ -2,6 +2,7 @@ from pythonforandroid.toolchain import Recipe, shprint, current_directory, info, warning from os.path import join, exists from os import chdir +import os import sh @@ -36,6 +37,8 @@ def build_arch(self, arch): 'hostpgen') return + if 'LIBS' in os.environ: + os.environ.pop('LIBS') configure = sh.Command('./configure') shprint(configure) From bbad2057e4bb3a8fc404bec9548cfc056004c1dc Mon Sep 17 00:00:00 2001 From: "Mark W. Alexander" Date: Mon, 21 Aug 2017 14:31:55 -0400 Subject: [PATCH 0055/1138] This recipe uses libtool which doesn't handle --sysroot well and is completely clueless about -isysroot and -isystem so it can't link. It traps the sh.make failure and then manually links and installs libffi.so --- pythonforandroid/recipes/libffi/__init__.py | 136 +++++++++++--------- 1 file changed, 74 insertions(+), 62 deletions(-) diff --git a/pythonforandroid/recipes/libffi/__init__.py b/pythonforandroid/recipes/libffi/__init__.py index 1f216b960c..6d80840761 100644 --- a/pythonforandroid/recipes/libffi/__init__.py +++ b/pythonforandroid/recipes/libffi/__init__.py @@ -1,71 +1,83 @@ +from os.path import exists, join from pythonforandroid.recipe import Recipe -from pythonforandroid.logger import shprint +from pythonforandroid.logger import info, shprint from pythonforandroid.util import current_directory -from os.path import exists, join import sh -import glob class LibffiRecipe(Recipe): - name = 'libffi' - version = 'v3.2.1' - url = 'https://github.com/atgreen/libffi/archive/{version}.zip' - - patches = ['remove-version-info.patch'] - - def get_host(self, arch): - with current_directory(self.get_build_dir(arch.arch)): - host = None - with open('Makefile') as f: - for line in f: - if line.startswith('host = '): - host = line.strip()[7:] - break - - if not host or not exists(host): - raise RuntimeError('failed to find build output! ({})' - .format(host)) - - return host - - def should_build(self, arch): - # return not bool(glob.glob(join(self.ctx.get_libs_dir(arch.arch), - # 'libffi.so*'))) - return not exists(join(self.ctx.get_libs_dir(arch.arch), 'libffi.so')) - # return not exists(join(self.ctx.get_python_install_dir(), 'lib', - # 'libffi.so')) - - 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.toolchain_prefix, - '--prefix=' + self.ctx.get_python_install_dir(), - '--enable-shared', _env=env) - shprint(sh.make, '-j5', 'libffi.la', _env=env) - - - # dlname = None - # with open(join(host, 'libffi.la')) as f: - # for line in f: - # if line.startswith('dlname='): - # dlname = line.strip()[8:-1] - # break - # - # if not dlname or not exists(join(host, '.libs', dlname)): - # raise RuntimeError('failed to locate shared object! ({})' - # .format(dlname)) - - # shprint(sh.sed, '-i', 's/^dlname=.*$/dlname=\'libffi.so\'/', join(host, 'libffi.la')) - - shprint(sh.cp, '-t', self.ctx.get_libs_dir(arch.arch), - join(self.get_host(arch), '.libs', 'libffi.so')) #, - # join(host, 'libffi.la')) - - def get_include_dirs(self, arch): - return [join(self.get_build_dir(arch.arch), self.get_host(arch), 'include')] + name = 'libffi' + version = 'v3.2.1' + url = 'https://github.com/atgreen/libffi/archive/{version}.zip' + + patches = ['remove-version-info.patch'] + + def get_host(self, arch): + with current_directory(self.get_build_dir(arch.arch)): + host = None + with open('Makefile') as f: + for line in f: + if line.startswith('host = '): + host = line.strip()[7:] + break + + if not host or not exists(host): + raise RuntimeError('failed to find build output! ({})' + .format(host)) + + return host + + def should_build(self, arch): + return not exists(join(self.ctx.get_libs_dir(arch.arch), 'libffi.so')) + + 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.toolchain_prefix, + '--prefix=' + self.ctx.get_python_install_dir(), + '--enable-shared', _env=env) + #'--with-sysroot={}'.format(self.ctx.ndk_platform), + #'--target={}'.format(arch.toolchain_prefix), + + # ndk 15 introduces unified headers required --sysroot and + # -isysroot for libraries and headers. libtool's head explodes + # trying to weave them into it's own magic. The result is a link + # failure tryng to link libc. We call make to compile the bits + # and manually link... + + try: + shprint(sh.make, '-j5', 'libffi.la', _env=env) + except sh.ErrorReturnCode_2: + info("make libffi.la failed as expected") + cc = sh.Command(env['CC'].split()[0]) + cflags = env['CC'].split()[1:] + + cflags.extend(['-march=armv7-a', '-mfloat-abi=softfp', '-mfpu=vfp', + '-mthumb', '-shared', '-fPIC', '-DPIC', + 'src/.libs/prep_cif.o', 'src/.libs/types.o', + 'src/.libs/raw_api.o', 'src/.libs/java_raw_api.o', + 'src/.libs/closures.o', 'src/arm/.libs/sysv.o', + 'src/arm/.libs/ffi.o', ] + ) + + ldflags = env['LDFLAGS'].split() + cflags.extend(ldflags) + cflags.extend(['-Wl,-soname', '-Wl,libffi.so', '-o', + '.libs/libffi.so']) + + with current_directory(self.get_host(arch)): + shprint(cc, *cflags, _env=env) + + shprint(sh.cp, '-t', self.ctx.get_libs_dir(arch.arch), + join(self.get_host(arch), '.libs', 'libffi.so')) + + def get_include_dirs(self, arch): + return [join(self.get_build_dir(arch.arch), self.get_host(arch), + 'include')] recipe = LibffiRecipe() From 651a188e3eb014110ff20ae27ce0e6dba3dfc3ee Mon Sep 17 00:00:00 2001 From: "Mark W. Alexander" Date: Mon, 21 Aug 2017 14:36:10 -0400 Subject: [PATCH 0056/1138] NDK 15 (and 14) has langinfo.h but doesn't define nl_langinfo Discussed here regarding Python3 This patch stuffs the configure setting into the environment to disable the search for langinfo.h --- pythonforandroid/recipes/python2/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pythonforandroid/recipes/python2/__init__.py b/pythonforandroid/recipes/python2/__init__.py index c22c5f5221..3f03eae962 100644 --- a/pythonforandroid/recipes/python2/__init__.py +++ b/pythonforandroid/recipes/python2/__init__.py @@ -111,6 +111,8 @@ def do_python_build(self, arch): f = 'LDFLAGS' env[f] = env[f] + l if f in env else l + # NDK has langinfo.h but doesn't define nl_langinfo() + env['ac_cv_header_langinfo_h'] = 'no' configure = sh.Command('./configure') # AND: OFLAG isn't actually set, should it be? shprint(configure, From 1b88f09a4e9715b11b9c9087a1f8c55ad15f4248 Mon Sep 17 00:00:00 2001 From: "Mark W. Alexander" Date: Mon, 21 Aug 2017 18:55:49 -0400 Subject: [PATCH 0057/1138] NDK no longer includes getifaddr, make an ifaddr.h and ifaddr.so --- pythonforandroid/recipes/ifaddrs/__init__.py | 68 ++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 pythonforandroid/recipes/ifaddrs/__init__.py diff --git a/pythonforandroid/recipes/ifaddrs/__init__.py b/pythonforandroid/recipes/ifaddrs/__init__.py new file mode 100644 index 0000000000..d58fd604c9 --- /dev/null +++ b/pythonforandroid/recipes/ifaddrs/__init__.py @@ -0,0 +1,68 @@ +""" ifaddrs for Android +""" +from os.path import join, exists +import sh +from pythonforandroid.logger import info, shprint + +from pythonforandroid.toolchain import (CompiledComponentsPythonRecipe, + current_directory) + +class IFAddrRecipe(CompiledComponentsPythonRecipe): + version = 'master' + url = 'git+https://github.com/morristech/android-ifaddrs.git' + depends = [('hostpython2', 'hostpython3'), ('python2', 'python3crystax')] + + call_hostpython_via_targetpython = False + site_packages_name = 'ifaddrs' + generated_libraries = ['libifaddrs.so'] + + def should_build(self, arch): + """It's faster to build than check""" + return not (exists(join(self.ctx.libs_dir, arch.arch, 'libifaddrs.so')) + and exists(join(self.ctx.get_python_install_dir(), 'lib' + "libifaddrs.so")) + ) + + def prebuild_arch(self, arch): + """Make the build and target directories""" + path = self.get_build_dir(arch.arch) + if not exists(path): + info("creating {}".format(path)) + shprint(sh.mkdir, '-p', 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'), + ): + if not exists(path): + info("creating {}".format(path)) + shprint(sh.mkdir, '-p', path) + cli = env['CC'].split() + cc = sh.Command(cli[0]) + + 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)) + shprint(sh.cp, "libifaddrs.so", join(self.ctx.get_python_install_dir(), 'lib')) + # drop header in to the Python include directory + shprint(sh.cp, "ifaddrs.h", join(self.ctx.get_python_install_dir(), + 'include/python{}'.format( + self.ctx.python_recipe.version[0:3] + ) + ) + ) + include_path = join(self.ctx.python_recipe.get_build_dir(arch.arch), 'Include') + shprint(sh.cp, "ifaddrs.h", include_path) + +recipe = IFAddrRecipe() From d1eeab07f4053edc0598c843973f0bd16a79ce48 Mon Sep 17 00:00:00 2001 From: "Mark W. Alexander" Date: Mon, 21 Aug 2017 18:59:05 -0400 Subject: [PATCH 0058/1138] add -I python-install's include directory. This also provides a place to drop header files for linking with shared libraries. --- pythonforandroid/archs.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pythonforandroid/archs.py b/pythonforandroid/archs.py index 00d8626b77..109fbdb1df 100644 --- a/pythonforandroid/archs.py +++ b/pythonforandroid/archs.py @@ -49,6 +49,11 @@ def get_env(self, with_flags_in_cc=True): sysroot = self.ctx.ndk_platform env['CFLAGS'] += ' -I{}'.format(self.ctx.ndk_platform) env['CFLAGS'] += ' -isysroot {} '.format(sysroot) + env['CFLAGS'] += '-I' + join(self.ctx.get_python_install_dir(), + 'include/python{}'.format( + self.ctx.python_recipe.version[0:3]) + ) + env['LDFLAGS'] += '--sysroot {} '.format(self.ctx.ndk_platform) env["CXXFLAGS"] = env["CFLAGS"] From a8b2d9866bac802508711935af2b00825b549913 Mon Sep 17 00:00:00 2001 From: zed Date: Tue, 22 Aug 2017 21:38:51 +0300 Subject: [PATCH 0059/1138] Update README.md Add link to the documentation. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 59dd62bded..ce159f11fc 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ If you did this, to build an APK with SDL2 you can try e.g. p4a apk --requirements=kivy --private /home/asandy/devel/planewave_frozen/ --package=net.inclem.planewavessdl2 --name="planewavessdl2" --version=0.5 --bootstrap=sdl2 -For full instructions and parameter options, see the documentation. +For full instructions and parameter options, see [the documentation](https://python-for-android.readthedocs.io/en/latest/quickstart/#usage). # Support From 4cca0caec9cb6c3f4bf7e5d7d28b910fe87099d1 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sat, 26 Aug 2017 17:29:19 +0100 Subject: [PATCH 0060/1138] Changed README from markdown to rst for pypi compatibility --- README.md => README.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README.md => README.rst (100%) diff --git a/README.md b/README.rst similarity index 100% rename from README.md rename to README.rst From 3d8a7b27318563b73b568626ef7901339b6e6ab6 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sat, 26 Aug 2017 17:33:07 +0100 Subject: [PATCH 0061/1138] Fixed README --- README.rst | 82 +++++++++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/README.rst b/README.rst index ce159f11fc..fb109802a7 100644 --- a/README.rst +++ b/README.rst @@ -1,38 +1,40 @@ -# python-for-android +python-for-android +================== python-for-android is a packager for Python apps on Android. You can create your own Python distribution including the modules and -dependencies you want, and bundle it in an APK along with your own -code. +dependencies you want, and bundle it in an APK along with your own code. Features include: -- Support for building with both Python 2 and Python 3. -- Different app backends including Kivy, PySDL2, and a WebView with - Python webserver. -- Automatic support for most pure Python modules, and built in support - for many others, including popular dependencies such as numpy and - sqlalchemy. -- Multiple architecture targets, for APKs optimised on any given device. +- Support for building with both Python 2 and Python 3. +- Different app backends including Kivy, PySDL2, and a WebView with + Python webserver. +- Automatic support for most pure Python modules, and built in support + for many others, including popular dependencies such as numpy and + sqlalchemy. +- Multiple architecture targets, for APKs optimised on any given + device. For documentation and support, see: -- Website: http://python-for-android.readthedocs.io -- Mailing list: https://groups.google.com/forum/#!forum/kivy-users or - https://groups.google.com/forum/#!forum/python-android. +- Website: http://python-for-android.readthedocs.io +- Mailing list: https://groups.google.com/forum/#!forum/kivy-users or + https://groups.google.com/forum/#!forum/python-android. In 2015 these tools were rewritten to provide a new, easier to use and extend interface. If you are looking for the old toolchain with distribute.sh and build.py, it is still available at -https://github.com/kivy/python-for-android/tree/old_toolchain, and -issues and PRs relating to this branch are still accepted. However, -the new toolchain contains all the same functionality via the built in +https://github.com/kivy/python-for-android/tree/old\_toolchain, and +issues and PRs relating to this branch are still accepted. However, the +new toolchain contains all the same functionality via the built in pygame bootstrap. -# Documentation +Documentation +============= -Follow the -[quickstart instructions](https://python-for-android.readthedocs.org/en/latest/quickstart/) +Follow the `quickstart +instructions `__ to install and begin creating APKs. Quick instructions to start would be:: @@ -43,7 +45,7 @@ or to test the master branch:: pip install git+https://github.com/kivy/python-for-android.git -The executable is called `python-for-android` or `p4a` (both are +The executable is called ``python-for-android`` or ``p4a`` (both are equivalent). To test that the installation worked, try python-for-android recipes @@ -57,40 +59,44 @@ If you did this, to build an APK with SDL2 you can try e.g. p4a apk --requirements=kivy --private /home/asandy/devel/planewave_frozen/ --package=net.inclem.planewavessdl2 --name="planewavessdl2" --version=0.5 --bootstrap=sdl2 -For full instructions and parameter options, see [the documentation](https://python-for-android.readthedocs.io/en/latest/quickstart/#usage). +For full instructions and parameter options, see `the +documentation `__. -# Support +Support +======= If you need assistance, you can ask for help on our mailing list: -* User Group: https://groups.google.com/group/kivy-users -* Email: kivy-users@googlegroups.com +- User Group: https://groups.google.com/group/kivy-users +- Email: kivy-users@googlegroups.com We also have an IRC channel: -* Server: irc.freenode.net -* Port: 6667, 6697 (SSL only) -* Channel: #kivy +- Server: irc.freenode.net +- Port: 6667, 6697 (SSL only) +- Channel: #kivy -# Contributing +Contributing +============ We love pull requests and discussing novel ideas. Check out our -[contribution guide](http://kivy.org/docs/contribute.html) and -feel free to improve python-for-android. +`contribution guide `__ and feel +free to improve python-for-android. The following mailing list and IRC channel are used exclusively for discussions about developing the Kivy framework and its sister projects: -* Dev Group: https://groups.google.com/group/kivy-dev -* Email: kivy-dev@googlegroups.com +- Dev Group: https://groups.google.com/group/kivy-dev +- Email: kivy-dev@googlegroups.com IRC channel: -* Server: irc.freenode.net -* Port: 6667, 6697 (SSL only) -* Channel: #kivy or #kivy-dev +- Server: irc.freenode.net +- Port: 6667, 6697 (SSL only) +- Channel: #kivy or #kivy-dev -# License +License +======= -python-for-android is released under the terms of the MIT License. Please refer to the -LICENSE file. +python-for-android is released under the terms of the MIT License. +Please refer to the LICENSE file. From 84a8b8ec888716c621601e5a56868c0272a466e7 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sat, 26 Aug 2017 17:34:40 +0100 Subject: [PATCH 0062/1138] Updated version to 0.5.2 and added long_description --- pythonforandroid/__init__.py | 2 +- setup.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pythonforandroid/__init__.py b/pythonforandroid/__init__.py index 27f44935af..5381b5210a 100644 --- a/pythonforandroid/__init__.py +++ b/pythonforandroid/__init__.py @@ -1,2 +1,2 @@ -__version__ = '0.5' +__version__ = '0.5.2' diff --git a/setup.py b/setup.py index b2e6641af1..bb35dba75b 100644 --- a/setup.py +++ b/setup.py @@ -53,9 +53,13 @@ def recursively_include(results, directory, patterns): recursively_include(package_data, 'pythonforandroid', ['liblink', 'biglink', 'liblink.sh']) +with open(join(dirname(__file__), 'README.rst')) as fileh: + long_description = fileh.read() + setup(name='python-for-android', - version='0.5', + version='0.5.2', description='Android APK packager for Python scripts and apps', + long_description=long_description, author='The Kivy team', author_email='kivy-dev@googlegroups.com', url='https://github.com/kivy/python-for-android', From a6c927e51a38b4fabb91795c640bb80751e93e04 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sat, 26 Aug 2017 17:43:12 +0100 Subject: [PATCH 0063/1138] Fixed some rst syntax in README.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index fb109802a7..63505a4ca2 100644 --- a/README.rst +++ b/README.rst @@ -46,7 +46,7 @@ or to test the master branch:: pip install git+https://github.com/kivy/python-for-android.git The executable is called ``python-for-android`` or ``p4a`` (both are -equivalent). To test that the installation worked, try +equivalent). To test that the installation worked, try:: python-for-android recipes @@ -55,7 +55,7 @@ This should return a list of recipes available to be built. To build any distributions, you need to set up the Android SDK and NDK as described in the documentation linked above. -If you did this, to build an APK with SDL2 you can try e.g. +If you did this, to build an APK with SDL2 you can try e.g.:: p4a apk --requirements=kivy --private /home/asandy/devel/planewave_frozen/ --package=net.inclem.planewavessdl2 --name="planewavessdl2" --version=0.5 --bootstrap=sdl2 From 956a266c671e9cca37adfe3f2d3d0bd334a1f9c6 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sat, 26 Aug 2017 20:48:22 +0100 Subject: [PATCH 0064/1138] Updated version to 0.5.4 --- pythonforandroid/__init__.py | 2 +- setup.py | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/pythonforandroid/__init__.py b/pythonforandroid/__init__.py index 5381b5210a..04131071f2 100644 --- a/pythonforandroid/__init__.py +++ b/pythonforandroid/__init__.py @@ -1,2 +1,2 @@ -__version__ = '0.5.2' +__version__ = '0.5.4' diff --git a/setup.py b/setup.py index bb35dba75b..4db62a52a7 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ from os.path import join, dirname, sep import os import glob +import re # NOTE: All package data should also be set in MANIFEST.in @@ -56,8 +57,26 @@ def recursively_include(results, directory, patterns): with open(join(dirname(__file__), 'README.rst')) as fileh: long_description = fileh.read() +init_filen = join(dirname(__file__), 'pythonforandroid', '__init__.py') +version = None +try: + with open(init_filen) 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='0.5.2', + version=version, description='Android APK packager for Python scripts and apps', long_description=long_description, author='The Kivy team', From cbfbbbaf4119d7f4365287d2cf7a83cfe237c979 Mon Sep 17 00:00:00 2001 From: germn Date: Sun, 27 Aug 2017 20:34:32 +0300 Subject: [PATCH 0065/1138] Added "regex" module recipe. --- pythonforandroid/recipes/regex/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 pythonforandroid/recipes/regex/__init__.py diff --git a/pythonforandroid/recipes/regex/__init__.py b/pythonforandroid/recipes/regex/__init__.py new file mode 100644 index 0000000000..f2c36c76de --- /dev/null +++ b/pythonforandroid/recipes/regex/__init__.py @@ -0,0 +1,12 @@ +from pythonforandroid.toolchain import CompiledComponentsPythonRecipe + + +class RegexRecipe(CompiledComponentsPythonRecipe): + name = 'regex' + version = '2017.07.28' + url = 'https://pypi.python.org/packages/d1/23/5fa829706ee1d4452552eb32e0bfc1039553e01f50a8754c6f7152e85c1b/regex-{version}.tar.gz' + + depends = ['python2', 'setuptools'] + + +recipe = RegexRecipe() From 82ae80ad7733eb7d579163500a495b35534457ff Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 29 Aug 2017 14:05:14 +0300 Subject: [PATCH 0066/1138] Fix: only first line of note was displayed in the blue box --- doc/source/services.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/source/services.rst b/doc/source/services.rst index d2ea1eedb7..a250c7bc47 100644 --- a/doc/source/services.rst +++ b/doc/source/services.rst @@ -80,8 +80,10 @@ 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. +.. 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. From 223225fdb3b15fcb3746b88a38e4545f4e5c34ba Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Tue, 29 Aug 2017 19:02:49 +0100 Subject: [PATCH 0067/1138] Added patch to pass ndk platform includes to sdl2_image --- pythonforandroid/archs.py | 3 +++ pythonforandroid/recipes/sdl2_image/__init__.py | 3 ++- .../sdl2_image/add_ndk_platform_include_dir.patch | 13 +++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 pythonforandroid/recipes/sdl2_image/add_ndk_platform_include_dir.patch diff --git a/pythonforandroid/archs.py b/pythonforandroid/archs.py index 109fbdb1df..11b1d26c87 100644 --- a/pythonforandroid/archs.py +++ b/pythonforandroid/archs.py @@ -63,6 +63,9 @@ def get_env(self, with_flags_in_cc=True): if self.ctx.ndk == 'crystax': env['LDFLAGS'] += ' -L{}/sources/crystax/libs/{} -lcrystax'.format(self.ctx.ndk_dir, self.arch) + # Pass the ndk platform include dir to Android.mk files if necessary + env['NDK_PLATFORM_INCLUDE_DIR'] = join(self.ctx.ndk_platform, 'usr', 'include') + py_platform = sys.platform if py_platform in ['linux2', 'linux3']: py_platform = 'linux' diff --git a/pythonforandroid/recipes/sdl2_image/__init__.py b/pythonforandroid/recipes/sdl2_image/__init__.py index e4453603d4..c7c43475a2 100644 --- a/pythonforandroid/recipes/sdl2_image/__init__.py +++ b/pythonforandroid/recipes/sdl2_image/__init__.py @@ -9,6 +9,7 @@ class LibSDL2Image(BootstrapNDKRecipe): patches = ['toggle_jpg_png_webp.patch', ('disable_jpg.patch', is_arch('x86')), - 'extra_cflags.patch'] + 'extra_cflags.patch', + 'add_ndk_platform_include_dir.patch'] recipe = LibSDL2Image() diff --git a/pythonforandroid/recipes/sdl2_image/add_ndk_platform_include_dir.patch b/pythonforandroid/recipes/sdl2_image/add_ndk_platform_include_dir.patch new file mode 100644 index 0000000000..123e5c573a --- /dev/null +++ b/pythonforandroid/recipes/sdl2_image/add_ndk_platform_include_dir.patch @@ -0,0 +1,13 @@ +diff --git a/Android.mk b/Android.mk +index d48111b..3108b2c 100644 +--- a/Android.mk ++++ b/Android.mk +@@ -22,7 +22,7 @@ SUPPORT_WEBP := false + WEBP_LIBRARY_PATH := external/libwebp-0.3.0 + + +-LOCAL_C_INCLUDES := $(LOCAL_PATH) ++LOCAL_C_INCLUDES := $(LOCAL_PATH) $(NDK_PLATFORM_INCLUDE_DIR) + LOCAL_CFLAGS := -DLOAD_BMP -DLOAD_GIF -DLOAD_LBM -DLOAD_PCX -DLOAD_PNM \ + -DLOAD_TGA -DLOAD_XCF -DLOAD_XPM -DLOAD_XV + LOCAL_CFLAGS += -O3 -fstrict-aliasing -fprefetch-loop-arrays $(EXTRA_CFLAGS) From 02c93f8a5a07465240bf16919d961c097502cde6 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Tue, 29 Aug 2017 19:03:14 +0100 Subject: [PATCH 0068/1138] Made ndk builds allow missing deps This lets the python_shared dependency work. --- pythonforandroid/recipes/sdl2/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pythonforandroid/recipes/sdl2/__init__.py b/pythonforandroid/recipes/sdl2/__init__.py index 8eaebd68ae..7c2a5729f4 100644 --- a/pythonforandroid/recipes/sdl2/__init__.py +++ b/pythonforandroid/recipes/sdl2/__init__.py @@ -21,6 +21,8 @@ def get_recipe_env(self, arch=None): env['PYTHON2_NAME'] = py2.get_dir_name() if 'python2' in self.ctx.recipe_build_order: env['EXTRA_LDLIBS'] = ' -lpython2.7' + + env['APP_ALLOW_MISSING_DEPS'] = 'true' return env def build_arch(self, arch): From ae2cd93224c4448d7f39c872a0f359da8645d9e1 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Tue, 29 Aug 2017 19:03:29 +0100 Subject: [PATCH 0069/1138] Made liblink recognise -isystem alongside -I arguments --- pythonforandroid/tools/liblink | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/tools/liblink b/pythonforandroid/tools/liblink index 523eef9948..69f8ef23af 100755 --- a/pythonforandroid/tools/liblink +++ b/pythonforandroid/tools/liblink @@ -34,7 +34,7 @@ while i < len(sys.argv): i += 1 continue - if opt.startswith("-I"): + if opt.startswith("-I") or opt.startswith('-isystem'): continue if opt.startswith("-m"): From b3e79980980e6850b898932f465cb2a55e9ae3a6 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Tue, 29 Aug 2017 20:47:48 +0100 Subject: [PATCH 0070/1138] Added directory change during biglink This seems to be necessary with the ndk 15 changes, otherwise some object files can't be linked. It seems like this should be fixable with an LDFLAG, but I can't any changes here that would have broken it. --- pythonforandroid/build.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pythonforandroid/build.py b/pythonforandroid/build.py index ed406b0995..8a566ce788 100644 --- a/pythonforandroid/build.py +++ b/pythonforandroid/build.py @@ -666,12 +666,16 @@ def biglink(ctx, arch): info('target {}'.format(join(ctx.get_libs_dir(arch.arch), 'libpymodules.so'))) do_biglink = copylibs_function if ctx.copy_libs else biglink_function - 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)], - env=env) + + # Move to the directory containing crtstart_so.o and crtend_so.o + # This is necessary with newer NDKs? A gcc bug? + with current_directory(join(ctx.ndk_platform, 'usr', 'lib')): + 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)], + env=env) def biglink_function(soname, objs_paths, extra_link_dirs=[], env=None): From 9afdc77e3fa5079d5713b1e0c7dbf86ccf2616f1 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Thu, 31 Aug 2017 21:47:33 +0100 Subject: [PATCH 0071/1138] Improved logging of library loading in PythonUtil.java --- .../sdl2/build/src/org/kivy/android/PythonUtil.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java index 3c6f2865b4..1e6e15e585 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java @@ -27,6 +27,7 @@ public static void loadLibraries(File filesDir) { boolean foundPython = false; for (String lib : getLibraries()) { + Log.v("python", "Loading library: " + lib); try { System.loadLibrary(lib); if (lib.startsWith("python")) { @@ -36,10 +37,15 @@ public static void loadLibraries(File filesDir) { // If this is the last possible libpython // load, and it has failed, give a more // general error + Log.v("python", "Library loading error: " + e.getMessage()); if (lib.startsWith("python3.6") && !foundPython) { throw new java.lang.RuntimeException("Could not load any libpythonXXX.so"); + } else if (lib.startsWith("python")) { + continue; + } else { + Log.v("python", "An UnsatisfiedLinkError occurred loading " + lib); + throw e; } - continue; } } From c94f55312a36f9148383c84a402a4d772c9b811c Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Thu, 31 Aug 2017 23:58:15 +0100 Subject: [PATCH 0072/1138] Updated logging TAG in PythonUtil.java --- .../sdl2/build/src/org/kivy/android/PythonUtil.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java index 1e6e15e585..9174cd3afc 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java @@ -6,7 +6,7 @@ public class PythonUtil { - private static final String TAG = "PythonUtil"; + private static final String TAG = "pythonutil"; protected static String[] getLibraries() { return new String[] { @@ -27,7 +27,7 @@ public static void loadLibraries(File filesDir) { boolean foundPython = false; for (String lib : getLibraries()) { - Log.v("python", "Loading library: " + lib); + Log.v(TAG, "Loading library: " + lib); try { System.loadLibrary(lib); if (lib.startsWith("python")) { @@ -37,13 +37,13 @@ public static void loadLibraries(File filesDir) { // If this is the last possible libpython // load, and it has failed, give a more // general error - Log.v("python", "Library loading error: " + e.getMessage()); + Log.v(TAG, "Library loading error: " + e.getMessage()); if (lib.startsWith("python3.6") && !foundPython) { throw new java.lang.RuntimeException("Could not load any libpythonXXX.so"); } else if (lib.startsWith("python")) { continue; } else { - Log.v("python", "An UnsatisfiedLinkError occurred loading " + lib); + Log.v(TAG, "An UnsatisfiedLinkError occurred loading " + lib); throw e; } } From 1a42ce18e9b976919fc150b5fb4ce7129f22a6a4 Mon Sep 17 00:00:00 2001 From: akshayaurora Date: Sun, 9 Oct 2016 14:56:41 +0530 Subject: [PATCH 0073/1138] load `ssl` and `crypto` libs before loading python, for (4.1.2 or earlier devices) closes #866 --- .../bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java index 9174cd3afc..cd0d3f459f 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java @@ -14,6 +14,8 @@ protected static String[] getLibraries() { "SDL2_image", "SDL2_mixer", "SDL2_ttf", + "crypto1.0.2h", + "ssl1.0.2h", "python2.7", "python3.5m", "python3.6m", From 6f33b48f194eb0cff3d4171cb520882a6a343d2f Mon Sep 17 00:00:00 2001 From: Akshay Arora Date: Fri, 4 Nov 2016 02:45:52 +0530 Subject: [PATCH 0074/1138] dynamically load libcrypto ans libel if they exist --- .../src/org/kivy/android/PythonUtil.java | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java index cd0d3f459f..cfb376a451 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java @@ -3,32 +3,48 @@ import java.io.File; import android.util.Log; - +import java.util.ArrayList; +import java.io.FilenameFilter; public class PythonUtil { private static final String TAG = "pythonutil"; - protected static String[] getLibraries() { - return new String[] { - "SDL2", - "SDL2_image", - "SDL2_mixer", - "SDL2_ttf", - "crypto1.0.2h", - "ssl1.0.2h", - "python2.7", - "python3.5m", - "python3.6m", - "main" + protected static ArrayList getLibraries(File filesDir) { + + ArrayList MyList = new ArrayList(); + MyList.add("SDL2"); + MyList.add("SDL2_image"); + MyList.add("SDL2_mixer"); + MyList.add("SDL2_ttf"); + + String absPath = filesDir.getParentFile().getParentFile().getAbsolutePath() + "/lib/"; + filesDir = new File(absPath); + File [] files = filesDir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.matches(".*ssl.*") || name.matches(".*crypto.*"); + } + }); + + for (int i = 0; i < files.length; ++i) { + File mfl = files[i]; + String name = mfl.getName(); + name = name.substring(3, name.length() - 3); + MyList.add(name); }; + + MyList.add("python2.7"); + MyList.add("python3.5m"); + MyList.add("main"); + return MyList; } - public static void loadLibraries(File filesDir) { + public static void loadLibraries(File filesDir) { String filesDirPath = filesDir.getAbsolutePath(); boolean foundPython = false; - for (String lib : getLibraries()) { + for (String lib : getLibraries(filesDir)) { Log.v(TAG, "Loading library: " + lib); try { System.loadLibrary(lib); @@ -68,3 +84,4 @@ public static void loadLibraries(File filesDir) { Log.v(TAG, "Loaded everything!"); } } + From 6703ffd60b12f95b1a8a16c75450bee09126db12 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Fri, 1 Sep 2017 01:29:41 +0100 Subject: [PATCH 0075/1138] Restructured code for adding libs by regex --- .../src/org/kivy/android/PythonUtil.java | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java index cfb376a451..20a4a7b27b 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java @@ -5,38 +5,48 @@ import android.util.Log; import java.util.ArrayList; import java.io.FilenameFilter; +// import android.os.PatternMatcher; +import java.util.regex.Pattern; public class PythonUtil { private static final String TAG = "pythonutil"; - protected static ArrayList getLibraries(File filesDir) { - - ArrayList MyList = new ArrayList(); - MyList.add("SDL2"); - MyList.add("SDL2_image"); - MyList.add("SDL2_mixer"); - MyList.add("SDL2_ttf"); + 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(); - String absPath = filesDir.getParentFile().getParentFile().getAbsolutePath() + "/lib/"; - filesDir = new File(absPath); - File [] files = filesDir.listFiles(new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return name.matches(".*ssl.*") || name.matches(".*crypto.*"); + 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)); } - }); + } + } - for (int i = 0; i < files.length; ++i) { - File mfl = files[i]; - String name = mfl.getName(); - name = name.substring(3, name.length() - 3); - MyList.add(name); - }; + protected static ArrayList getLibraries(File filesDir) { + + String libsDirPath = filesDir.getParentFile().getParentFile().getAbsolutePath() + "/lib/"; + File libsDir = new File(libsDirPath); - MyList.add("python2.7"); - MyList.add("python3.5m"); - MyList.add("main"); - return MyList; + ArrayList libsList = new ArrayList(); + addLibraryIfExists(libsList, "crystax", libsDir); + libsList.add("SDL2"); + libsList.add("SDL2_image"); + libsList.add("SDL2_mixer"); + libsList.add("SDL2_ttf"); + addLibraryIfExists(libsList, "ssl.*", libsDir); + addLibraryIfExists(libsList, "crypto.*", libsDir); + libsList.add("python2.7"); + libsList.add("python3.5m"); + libsList.add("main"); + return libsList; } public static void loadLibraries(File filesDir) { From 0aac1947ba24c035ad45daa61db5646f521f1b28 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Fri, 1 Sep 2017 01:32:48 +0100 Subject: [PATCH 0076/1138] Added sqlite3 to conditional library loads --- .../bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java index 20a4a7b27b..6574dae787 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonUtil.java @@ -5,7 +5,6 @@ import android.util.Log; import java.util.ArrayList; import java.io.FilenameFilter; -// import android.os.PatternMatcher; import java.util.regex.Pattern; public class PythonUtil { @@ -37,6 +36,7 @@ protected static ArrayList getLibraries(File filesDir) { ArrayList libsList = new ArrayList(); addLibraryIfExists(libsList, "crystax", libsDir); + addLibraryIfExists(libsList, "sqlite3", libsDir); libsList.add("SDL2"); libsList.add("SDL2_image"); libsList.add("SDL2_mixer"); From dd965f8e511fba76153c466cceb34da9f18b11d2 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Tue, 5 Sep 2017 23:29:49 +0100 Subject: [PATCH 0077/1138] Added option to compileall with system python instead of hostpython --- pythonforandroid/bootstraps/sdl2/build/build.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pythonforandroid/bootstraps/sdl2/build/build.py b/pythonforandroid/bootstraps/sdl2/build/build.py index 4a7f4668f1..9ded97c817 100755 --- a/pythonforandroid/bootstraps/sdl2/build/build.py +++ b/pythonforandroid/bootstraps/sdl2/build/build.py @@ -491,6 +491,9 @@ def parse_args(args=None): 'NAME:PATH_TO_PY[:foreground]') ap.add_argument('--add-source', dest='extra_source_dirs', action='append', help='Include additional source dirs in Java build') + 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('--no-compile-pyo', dest='no_compile_pyo', action='store_true', help='Do not optimise .py files to .pyo.') ap.add_argument('--sign', action='store_true', @@ -524,6 +527,17 @@ def parse_args(args=None): if args.services is None: args.services = [] + 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.no_compile_pyo: PYTHON = None BLACKLIST_PATTERNS.remove('*.py') From 48eb6e8cc1e6ddcc1a6936badea13f39a7e1e038 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Tue, 5 Sep 2017 23:32:01 +0100 Subject: [PATCH 0078/1138] Made system python compileall option work only with python2 --- pythonforandroid/bootstraps/sdl2/build/build.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pythonforandroid/bootstraps/sdl2/build/build.py b/pythonforandroid/bootstraps/sdl2/build/build.py index 9ded97c817..59e294c0ae 100755 --- a/pythonforandroid/bootstraps/sdl2/build/build.py +++ b/pythonforandroid/bootstraps/sdl2/build/build.py @@ -530,13 +530,14 @@ def parse_args(args=None): 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 not exists('crystax_python'): + python_executable = 'python2.7' + try: + subprocess.call([python_executable, '--version']) + except (OSError, subprocess.CalledProcessError): + pass + else: + PYTHON = python_executable if args.no_compile_pyo: PYTHON = None From 65105ab4b886ddc87a60cfa4e6600d8996164a81 Mon Sep 17 00:00:00 2001 From: Brent Picasso Date: Sat, 9 Sep 2017 11:33:55 -0700 Subject: [PATCH 0079/1138] Update __init__.py Resolves #1107 --- pythonforandroid/recipes/websocket-client/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pythonforandroid/recipes/websocket-client/__init__.py b/pythonforandroid/recipes/websocket-client/__init__.py index aeb6b3b8a1..b0de3d0055 100644 --- a/pythonforandroid/recipes/websocket-client/__init__.py +++ b/pythonforandroid/recipes/websocket-client/__init__.py @@ -5,6 +5,12 @@ # copy the 'websocket' directory into your app directory to force inclusion. # # see my example at https://github.com/debauchery1st/example_kivy_websocket-recipe +# +# If you see errors relating to 'SSL not available' ensure you have the package backports.ssl-match-hostname +# in the buildozer requirements, since Kivy targets python 2.7.x +# +# You may also need sslopt={"cert_reqs": ssl.CERT_NONE} as a parameter to ws.run_forever() if you get an error relating to +# host verification class WebSocketClient(Recipe): From 01309f07ef839c0abe1219f07f36d64868315014 Mon Sep 17 00:00:00 2001 From: Brent Picasso Date: Sat, 9 Sep 2017 11:49:53 -0700 Subject: [PATCH 0080/1138] Add help regarding websocket-client & SSL --- doc/source/troubleshooting.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/source/troubleshooting.rst b/doc/source/troubleshooting.rst index dcd4788ffe..f1a2e7b19d 100644 --- a/doc/source/troubleshooting.rst +++ b/doc/source/troubleshooting.rst @@ -160,3 +160,10 @@ This error appears in the logcat log if you try to access ``org.renpy.android.PythonActivity`` from within the new toolchain. To fix it, change your code to reference ``org.kivy.android.PythonActivity`` instead. + +websocket-client: if you see errors relating to 'SSL not available' +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Ensure you have the package backports.ssl-match-hostname in the buildozer requirements, since Kivy targets python 2.7.x + +You may also need sslopt={"cert_reqs": ssl.CERT_NONE} as a parameter to ws.run_forever() if you get an error relating to host verification + From c60e02d2e32e31a3a754838c51e9242cbadcd9e8 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Sat, 16 Sep 2017 13:06:35 +0100 Subject: [PATCH 0081/1138] Removed python3cyrystax optional dependency from pil recipe PIL doesn't work with python3. pillow will, when a recipe is added for that. --- pythonforandroid/recipes/pil/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/recipes/pil/__init__.py b/pythonforandroid/recipes/pil/__init__.py index 3f79bace29..bd027457ef 100644 --- a/pythonforandroid/recipes/pil/__init__.py +++ b/pythonforandroid/recipes/pil/__init__.py @@ -7,7 +7,7 @@ class PILRecipe(CompiledComponentsPythonRecipe): name = 'pil' version = '1.1.7' url = 'http://effbot.org/downloads/Imaging-{version}.tar.gz' - depends = [('python2', 'python3crystax'), 'png', 'jpeg'] + depends = ['python2', 'png', 'jpeg'] site_packages_name = 'PIL' patches = ['disable-tk.patch', From 81205c8f6d0677fbe4945f47874080f064abe400 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Tue, 19 Sep 2017 23:29:05 +0100 Subject: [PATCH 0082/1138] Added version argument --- pythonforandroid/toolchain.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pythonforandroid/toolchain.py b/pythonforandroid/toolchain.py index 65908d3047..f603634198 100644 --- a/pythonforandroid/toolchain.py +++ b/pythonforandroid/toolchain.py @@ -7,6 +7,7 @@ """ from __future__ import print_function +from pythonforandroid import __version__ def check_python_dependencies(): # Check if the Python requirements are installed. This appears @@ -464,6 +465,8 @@ def add_parser(subparsers, *args, **kwargs): 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 From c1f83cb13cff5cbe032d3a1bfce8401d3757d603 Mon Sep 17 00:00:00 2001 From: Enoch Date: Wed, 20 Sep 2017 06:05:07 -0400 Subject: [PATCH 0083/1138] allow checkout from a local repository --- pythonforandroid/recipe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/recipe.py b/pythonforandroid/recipe.py index 234a111a4d..8e76dacaaf 100644 --- a/pythonforandroid/recipe.py +++ b/pythonforandroid/recipe.py @@ -149,7 +149,7 @@ def report_hook(index, blksize, size): urlretrieve(url, target, report_hook) return target - elif parsed_url.scheme in ('git', 'git+ssh', 'git+http', 'git+https'): + elif parsed_url.scheme in ('git', 'git+file', 'git+ssh', 'git+http', 'git+https'): if isdir(target): with current_directory(target): shprint(sh.git, 'fetch', '--tags') From d197e293cbb792470bba8291fc2ddd2e1be37e3e Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Thu, 28 Sep 2017 22:59:07 +0100 Subject: [PATCH 0084/1138] Switched SDL2_image to use its master branch --- pythonforandroid/archs.py | 3 --- pythonforandroid/recipes/sdl2_image/__init__.py | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pythonforandroid/archs.py b/pythonforandroid/archs.py index 11b1d26c87..109fbdb1df 100644 --- a/pythonforandroid/archs.py +++ b/pythonforandroid/archs.py @@ -63,9 +63,6 @@ def get_env(self, with_flags_in_cc=True): if self.ctx.ndk == 'crystax': env['LDFLAGS'] += ' -L{}/sources/crystax/libs/{} -lcrystax'.format(self.ctx.ndk_dir, self.arch) - # Pass the ndk platform include dir to Android.mk files if necessary - env['NDK_PLATFORM_INCLUDE_DIR'] = join(self.ctx.ndk_platform, 'usr', 'include') - py_platform = sys.platform if py_platform in ['linux2', 'linux3']: py_platform = 'linux' diff --git a/pythonforandroid/recipes/sdl2_image/__init__.py b/pythonforandroid/recipes/sdl2_image/__init__.py index c7c43475a2..930ed9b828 100644 --- a/pythonforandroid/recipes/sdl2_image/__init__.py +++ b/pythonforandroid/recipes/sdl2_image/__init__.py @@ -4,12 +4,12 @@ class LibSDL2Image(BootstrapNDKRecipe): version = '2.0.1' - url = 'https://www.libsdl.org/projects/SDL_image/release/SDL2_image-{version}.tar.gz' + # url = 'https://www.libsdl.org/projects/SDL_image/release/SDL2_image-{version}.tar.gz' + url = 'https://hg.libsdl.org/SDL_image/archive/tip.tar.gz' dir_name = 'SDL2_image' patches = ['toggle_jpg_png_webp.patch', ('disable_jpg.patch', is_arch('x86')), - 'extra_cflags.patch', - 'add_ndk_platform_include_dir.patch'] + 'extra_cflags.patch'] recipe = LibSDL2Image() From 779a58468baa7ad9ff4f85265da63a0c2b8ab4ae Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Thu, 28 Sep 2017 22:59:33 +0100 Subject: [PATCH 0085/1138] Fixed sqlite3 with NDK 15+ --- pythonforandroid/recipes/sqlite3/Android.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonforandroid/recipes/sqlite3/Android.mk b/pythonforandroid/recipes/sqlite3/Android.mk index fab4b67e2f..f52bc46f97 100644 --- a/pythonforandroid/recipes/sqlite3/Android.mk +++ b/pythonforandroid/recipes/sqlite3/Android.mk @@ -6,6 +6,6 @@ LOCAL_SRC_FILES := sqlite3.c LOCAL_MODULE := sqlite3 -LOCAL_CFLAGS := -DSQLITE_ENABLE_FTS4 +LOCAL_CFLAGS := -DSQLITE_ENABLE_FTS4 -D_FILE_OFFSET_BITS=32 include $(BUILD_SHARED_LIBRARY) From 144bff9f54117f706d7095bb6a4e2d353e546357 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Thu, 28 Sep 2017 23:07:40 +0100 Subject: [PATCH 0086/1138] Ported SDL2_image fix on Android to SDL2_image 2.0.1 The changes are the same as the patch at https://hg.libsdl.org/SDL_image/rev/7ad06019831d, but modified for version 2.0.1. --- .../recipes/sdl2_image/__init__.py | 6 +-- .../sdl2_image/fix_with_ndk_15_plus.patch | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 pythonforandroid/recipes/sdl2_image/fix_with_ndk_15_plus.patch diff --git a/pythonforandroid/recipes/sdl2_image/__init__.py b/pythonforandroid/recipes/sdl2_image/__init__.py index 930ed9b828..fbddff5cd2 100644 --- a/pythonforandroid/recipes/sdl2_image/__init__.py +++ b/pythonforandroid/recipes/sdl2_image/__init__.py @@ -4,12 +4,12 @@ class LibSDL2Image(BootstrapNDKRecipe): version = '2.0.1' - # url = 'https://www.libsdl.org/projects/SDL_image/release/SDL2_image-{version}.tar.gz' - url = 'https://hg.libsdl.org/SDL_image/archive/tip.tar.gz' + url = 'https://www.libsdl.org/projects/SDL_image/release/SDL2_image-{version}.tar.gz' dir_name = 'SDL2_image' patches = ['toggle_jpg_png_webp.patch', ('disable_jpg.patch', is_arch('x86')), - 'extra_cflags.patch'] + 'extra_cflags.patch', + 'fix_with_ndk_15_plus.patch'] recipe = LibSDL2Image() diff --git a/pythonforandroid/recipes/sdl2_image/fix_with_ndk_15_plus.patch b/pythonforandroid/recipes/sdl2_image/fix_with_ndk_15_plus.patch new file mode 100644 index 0000000000..a6d42b8dc4 --- /dev/null +++ b/pythonforandroid/recipes/sdl2_image/fix_with_ndk_15_plus.patch @@ -0,0 +1,50 @@ +diff --git a/Android.mk b/Android.mk +index 97a96c7..2e724c0 100644 +--- a/Android.mk ++++ b/Android.mk +@@ -79,6 +79,7 @@ ifeq ($(SUPPORT_JPG),true) + $(JPG_LIBRARY_PATH)/jfdctfst.c \ + $(JPG_LIBRARY_PATH)/jfdctint.c \ + $(JPG_LIBRARY_PATH)/jidctflt.c \ ++ $(JPG_LIBRARY_PATH)/jidctfst.c \ + $(JPG_LIBRARY_PATH)/jidctint.c \ + $(JPG_LIBRARY_PATH)/jquant1.c \ + $(JPG_LIBRARY_PATH)/jquant2.c \ +@@ -86,12 +87,6 @@ ifeq ($(SUPPORT_JPG),true) + $(JPG_LIBRARY_PATH)/jmemmgr.c \ + $(JPG_LIBRARY_PATH)/jmem-android.c + +- # assembler support is available for arm +- ifeq ($(TARGET_ARCH),arm) +- LOCAL_SRC_FILES += $(JPG_LIBRARY_PATH)/jidctfst.S +- else +- LOCAL_SRC_FILES += $(JPG_LIBRARY_PATH)/jidctfst.c +- endif + endif + + ifeq ($(SUPPORT_PNG),true) +diff --git a/external/jpeg-9/Android.mk b/external/jpeg-9/Android.mk +index a5edbde..77f139c 100644 +--- a/external/jpeg-9/Android.mk ++++ b/external/jpeg-9/Android.mk +@@ -14,20 +14,6 @@ LOCAL_SRC_FILES := \ + jquant2.c jutils.c jmemmgr.c \ + jmem-android.c + +-# the assembler is only for the ARM version, don't break the Linux sim +-ifneq ($(TARGET_ARCH),arm) +-ANDROID_JPEG_NO_ASSEMBLER := true +-endif +- +-# temp fix until we understand why this broke cnn.com +-#ANDROID_JPEG_NO_ASSEMBLER := true +- +-ifeq ($(strip $(ANDROID_JPEG_NO_ASSEMBLER)),true) +-LOCAL_SRC_FILES += jidctint.c jidctfst.c +-else +-LOCAL_SRC_FILES += jidctint.c jidctfst.S +-endif +- + LOCAL_CFLAGS += -DAVOID_TABLES + LOCAL_CFLAGS += -O3 -fstrict-aliasing -fprefetch-loop-arrays + #LOCAL_CFLAGS += -march=armv6j From e3df8a812c24919c5f4f27748bb0c53060390558 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Sat, 6 May 2017 12:47:46 +0200 Subject: [PATCH 0087/1138] sdl2/gradle: first version (not yet compiled) of a sdl2 build based on gradle --- pythonforandroid/bootstrap.py | 9 +- .../bootstraps/sdl2_gradle/__init__.py | 154 ++ .../bootstraps/sdl2_gradle/build/.gitignore | 14 + .../sdl2_gradle/build/blacklist.txt | 83 + .../bootstraps/sdl2_gradle/build/build.py | 542 ++++++ .../build/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 51017 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + .../bootstraps/sdl2_gradle/build/gradlew | 164 ++ .../bootstraps/sdl2_gradle/build/gradlew.bat | 90 + .../sdl2_gradle/build/jni/Android.mk | 1 + .../sdl2_gradle/build/jni/Application.mk | 7 + .../sdl2_gradle/build/jni/src/Android.mk | 27 + .../build/jni/src/Android_static.mk | 12 + .../sdl2_gradle/build/jni/src/start.c | 320 ++++ .../build/src/main/assets/.gitkeep | 0 .../sdl2_gradle/build/src/main/java/.gitkeep | 0 .../main/java/org/kamranzafar/jtar/Octal.java | 141 ++ .../org/kamranzafar/jtar/TarConstants.java | 28 + .../java/org/kamranzafar/jtar/TarEntry.java | 284 +++ .../java/org/kamranzafar/jtar/TarHeader.java | 243 +++ .../org/kamranzafar/jtar/TarInputStream.java | 249 +++ .../org/kamranzafar/jtar/TarOutputStream.java | 163 ++ .../java/org/kamranzafar/jtar/TarUtils.java | 96 + .../android/GenericBroadcastReceiver.java | 19 + .../GenericBroadcastReceiverCallback.java | 8 + .../java/org/kivy/android/PythonActivity.java | 456 +++++ .../java/org/kivy/android/PythonService.java | 132 ++ .../java/org/kivy/android/PythonUtil.java | 62 + .../kivy/android/concurrency/PythonEvent.java | 45 + .../kivy/android/concurrency/PythonLock.java | 19 + .../org/kivy/android/launcher/Project.java | 99 ++ .../kivy/android/launcher/ProjectAdapter.java | 44 + .../kivy/android/launcher/ProjectChooser.java | 94 + .../main/java/org/libsdl/app/SDLActivity.java | 1579 +++++++++++++++++ .../java/org/renpy/android/AssetExtract.java | 115 ++ .../main/java/org/renpy/android/Hardware.java | 287 +++ .../org/renpy/android/PythonActivity.java | 12 + .../java/org/renpy/android/PythonService.java | 12 + .../org/renpy/android/ResourceManager.java | 54 + .../build/src/main/jniLibs/.gitkeep | 0 .../sdl2_gradle/build/src/main/libs/.gitkeep | 0 .../main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 2683 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 1698 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 3872 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 6874 bytes .../build/src/main/res/drawable/.gitkeep | 0 .../build/src/main/res/layout/main.xml | 13 + .../build/templates/AndroidManifest.tmpl.xml | 124 ++ .../build/templates/Service.tmpl.java | 56 + .../build/templates/build.tmlp.gradle | 66 + .../sdl2_gradle/build/templates/kivy-icon.png | Bin 0 -> 16525 bytes .../build/templates/kivy-presplash.jpg | Bin 0 -> 18251 bytes .../build/templates/strings.tmpl.xml | 7 + .../sdl2_gradle/build/whitelist.txt | 1 + 54 files changed, 5933 insertions(+), 4 deletions(-) create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/__init__.py create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/.gitignore create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/blacklist.txt create mode 100755 pythonforandroid/bootstraps/sdl2_gradle/build/build.py create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/gradle/wrapper/gradle-wrapper.jar create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/gradle/wrapper/gradle-wrapper.properties create mode 100755 pythonforandroid/bootstraps/sdl2_gradle/build/gradlew create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/gradlew.bat create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/jni/Android.mk create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/jni/Application.mk create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/jni/src/Android.mk create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/jni/src/Android_static.mk create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/jni/src/start.c create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/assets/.gitkeep create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/.gitkeep create mode 100755 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/Octal.java create mode 100755 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarConstants.java create mode 100755 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarEntry.java create mode 100755 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarHeader.java create mode 100755 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarInputStream.java create mode 100755 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarOutputStream.java create mode 100755 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarUtils.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/GenericBroadcastReceiver.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/GenericBroadcastReceiverCallback.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/PythonActivity.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/PythonService.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/PythonUtil.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/concurrency/PythonEvent.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/concurrency/PythonLock.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/launcher/Project.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/launcher/ProjectAdapter.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/launcher/ProjectChooser.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/libsdl/app/SDLActivity.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/renpy/android/AssetExtract.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/renpy/android/Hardware.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/renpy/android/PythonActivity.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/renpy/android/PythonService.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/renpy/android/ResourceManager.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/jniLibs/.gitkeep create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/libs/.gitkeep create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/res/drawable/.gitkeep create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/src/main/res/layout/main.xml create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/templates/AndroidManifest.tmpl.xml create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/templates/Service.tmpl.java create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/templates/build.tmlp.gradle create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/templates/kivy-icon.png create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/templates/kivy-presplash.jpg create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/templates/strings.tmpl.xml create mode 100644 pythonforandroid/bootstraps/sdl2_gradle/build/whitelist.txt diff --git a/pythonforandroid/bootstrap.py b/pythonforandroid/bootstrap.py index c1b6633317..1770a6ce52 100644 --- a/pythonforandroid/bootstrap.py +++ b/pythonforandroid/bootstrap.py @@ -195,20 +195,21 @@ def get_bootstrap(cls, name, ctx): bootstrap.ctx = ctx return bootstrap - def distribute_libs(self, arch, src_dirs, wildcard='*'): + 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('libs', arch.arch) + tgt_dir = join(dest_dir, arch.arch) ensure_dir(tgt_dir) for src_dir in src_dirs: for lib in glob.glob(join(src_dir, wildcard)): shprint(sh.cp, '-a', lib, tgt_dir) - def distribute_javaclasses(self, javaclass_dir): + def distribute_javaclasses(self, javaclass_dir, dest_dir="src"): '''Copy existing javaclasses from build dir to current dist dir.''' info('Copying java files') + ensure_dir(dest_dir) for filename in glob.glob(javaclass_dir): - shprint(sh.cp, '-a', filename, 'src') + shprint(sh.cp, '-a', filename, dest_dir) def distribute_aars(self, arch): '''Process existing .aar bundles and copy to current dist dir.''' diff --git a/pythonforandroid/bootstraps/sdl2_gradle/__init__.py b/pythonforandroid/bootstraps/sdl2_gradle/__init__.py new file mode 100644 index 0000000000..62dcb77ac6 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/__init__.py @@ -0,0 +1,154 @@ +# coding=utf-8 +""" +Bootstrap for SDL2, using gradlew for building +============================================== + +.. warning:: Experimental + +Good point: +- automatic dependencies management +- no need to unpack aar + +TODO: +- test with crystax + +""" + +from pythonforandroid.toolchain import ( + Bootstrap, shprint, current_directory, info, warning, ArchARM, info_main) +from pythonforandroid.utils import ensure_dir +from os.path import join, exists, curdir, abspath +from os import walk, makedirs +import glob +import sh + + +EXCLUDE_EXTS = (".py", ".pyc", ".so.o", ".so.a", ".so.libs", ".pyx") + + +class SDL2GradleBootstrap(Bootstrap): + name = 'sdl2_gradle' + + recipe_depends = ['sdl2', ('python2', 'python3crystax')] + + def run_distribute(self): + info_main("# Creating Android project ({})".format(self.name)) + + arch = self.ctx.archs[0] + python_install_dir = self.ctx.get_python_install_dir() + from_crystax = self.ctx.python_recipe.from_crystax + crystax_python_dir = join("crystax_python", "crystax_python") + + if len(self.ctx.archs) > 1: + raise ValueError("SDL2/gradle support only one arch") + + info("Copying SDL2/gradle build for {}".format(arch)) + shprint(sh.rm, "-rf", self.dist_dir) + shprint(sh.cp, "-r", self.build_dir, self.dist_dir) + + # either the build use environemnt variable (ANDROID_HOME) + # or the local.properties if exists + 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") + + if not exists("private") and not from_crystax: + ensure_dir("private") + if not exists("crystax_python") and from_crystax: + ensure_dir(crystax_python_dir) + + hostpython = sh.Command(self.ctx.hostpython) + if not from_crystax: + try: + shprint(hostpython, '-OO', '-m', 'compileall', + python_install_dir, + _tail=10, _filterout="^Listing") + except sh.ErrorReturnCode: + pass + if not exists('python-install'): + shprint( + sh.cp, '-a', python_install_dir, './python-install') + + self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)], + dest_dir=join("src", "main", "jniLibs")) + self.distribute_javaclasses(self.ctx.javaclass_dir, + dest_dir=join("src", "main", "java")) + + if not from_crystax: + info("Filling private directory") + if not exists(join("private", "lib")): + info("private/lib does not exist, making") + shprint(sh.cp, "-a", + join("python-install", "lib"), "private") + shprint(sh.mkdir, "-p", + join("private", "include", "python2.7")) + + # AND: Copylibs stuff should go here + libpymodules_fn = join("libs", arch.arch, "libpymodules.so") + if exists(libpymodules_fn): + shprint(sh.mv, libpymodules_fn, 'private/') + shprint(sh.cp, + join('python-install', 'include', + 'python2.7', 'pyconfig.h'), + join('private', 'include', 'python2.7/')) + + info('Removing some unwanted files') + shprint(sh.rm, '-f', join('private', 'lib', 'libpython2.7.so')) + shprint(sh.rm, '-rf', join('private', 'lib', 'pkgconfig')) + + libdir = join(self.dist_dir, 'private', 'lib', 'python2.7') + site_packages_dir = join(libdir, 'site-packages') + with current_directory(libdir): + removes = [] + for dirname, root, filenames in walk("."): + for filename in filenames: + for suffix in EXCLUDE_EXTS: + if filename.endswith(suffix): + removes.append(filename) + shprint(sh.rm, '-f', *removes) + + info('Deleting some other stuff not used on android') + # To quote the original distribute.sh, 'well...' + shprint(sh.rm, '-rf', 'lib2to3') + shprint(sh.rm, '-rf', 'idlelib') + for filename in glob.glob('config/libpython*.a'): + shprint(sh.rm, '-f', filename) + shprint(sh.rm, '-rf', 'config/python.o') + + else: # Python *is* loaded from crystax + ndk_dir = self.ctx.ndk_dir + py_recipe = self.ctx.python_recipe + python_dir = join(ndk_dir, 'sources', 'python', + py_recipe.version, 'libs', arch.arch) + shprint(sh.cp, '-r', join(python_dir, + 'stdlib.zip'), crystax_python_dir) + shprint(sh.cp, '-r', join(python_dir, + 'modules'), crystax_python_dir) + shprint(sh.cp, '-r', self.ctx.get_python_install_dir(), + crystax_python_dir) + + info('Renaming .so files to reflect cross-compile') + site_packages_dir = join(crystax_python_dir, "site-packages") + find_ret = shprint( + sh.find, site_packages_dir, '-iname', '*.so') + filenames = find_ret.stdout.decode('utf-8').split('\n')[:-1] + for filename in filenames: + parts = filename.split('.') + if len(parts) <= 2: + continue + shprint(sh.mv, filename, filename.split('.')[0] + '.so') + site_packages_dir = join(abspath(curdir), + site_packages_dir) + 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.strip_libraries(arch) + self.fry_eggs(site_packages_dir) + super(SDL2GradleBootstrap, self).run_distribute() + + +bootstrap = SDL2GradleBootstrap() diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/.gitignore b/pythonforandroid/bootstraps/sdl2_gradle/build/.gitignore new file mode 100644 index 0000000000..a1fc39c070 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/.gitignore @@ -0,0 +1,14 @@ +.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 diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/blacklist.txt b/pythonforandroid/bootstraps/sdl2_gradle/build/blacklist.txt new file mode 100644 index 0000000000..3d596e44cd --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/blacklist.txt @@ -0,0 +1,83 @@ +# prevent user to include invalid extensions +*.apk +*.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/mmap.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 diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/build.py b/pythonforandroid/bootstraps/sdl2_gradle/build/build.py new file mode 100755 index 0000000000..f35519c9a7 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/build.py @@ -0,0 +1,542 @@ +#!/usr/bin/env python2.7 +# coding: utf-8 + +from __future__ import print_function +from os.path import ( + dirname, join, isfile, realpath, relpath, split, exists, basename) +from os import makedirs +import os +import tarfile +import time +import subprocess +import shutil +from zipfile import ZipFile +import sys +import re + +from fnmatch import fnmatch + +import jinja2 + +if os.name == 'nt': + ANDROID = 'android.bat' + ANT = 'ant.bat' +else: + ANDROID = 'android' + ANT = 'ant' + +curdir = dirname(__file__) + +# Try to find a host version of Python that matches our ARM version. +PYTHON = join(curdir, 'python-install', 'bin', 'python.host') +if not exists(PYTHON): + print('Could not find hostpython, will not compile to .pyo (this is normal with python3)') + PYTHON = None + +BLACKLIST_PATTERNS = [ + # code versionning + '^*.hg/*', + '^*.git/*', + '^*.bzr/*', + '^*.svn/*', + + # pyc/py + '*.pyc', + + # temp files + '~', + '*.bak', + '*.swp', +] +if PYTHON is not None: + BLACKLIST_PATTERNS.append('*.py') + +WHITELIST_PATTERNS = ['pyconfig.h', ] + +python_files = [] + + +environment = jinja2.Environment(loader=jinja2.FileSystemLoader( + join(curdir, 'templates'))) + + +def try_unlink(fn): + if exists(fn): + os.unlink(fn) + + +def ensure_dir(path): + if not exists(path): + makedirs(path) + + +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_python_zip(): + ''' + Search for all the python related files, and construct the pythonXX.zip + According to + # http://randomsplat.com/id5-cross-compiling-python-for-embedded-linux.html + site-packages, config and lib-dynload will be not included. + ''' + + if not exists('private'): + print('No compiled python is present to zip, skipping.') + print('this should only be the case if you are using the CrystaX python') + return + + global python_files + d = realpath(join('private', 'lib', 'python2.7')) + + def select(fn): + if is_blacklist(fn): + return False + fn = realpath(fn) + assert(fn.startswith(d)) + fn = fn[len(d):] + if (fn.startswith('/site-packages/') or + fn.startswith('/config/') or + fn.startswith('/lib-dynload/') or + fn.startswith('/libpymodules.so')): + return False + return fn + + # get a list of all python file + python_files = [x for x in listfiles(d) if select(x)] + + # create the final zipfile + zfn = join('private', 'lib', 'python27.zip') + zf = ZipFile(zfn, 'w') + + # put all the python files in it + for fn in python_files: + afn = fn[len(d):] + zf.write(fn, afn) + zf.close() + + +def make_tar(tfn, source_dirs, ignore_path=[]): + ''' + Make a zip file `fn` from the contents of source_dis. + ''' + + # selector function + def select(fn): + rfn = realpath(fn) + for p in ignore_path: + if p.endswith('/'): + p = p[:-1] + if rfn.startswith(p): + return False + if rfn in python_files: + return False + return not is_blacklist(fn) + + # get the files and relpath file of all the directory we asked for + files = [] + for sd in source_dirs: + sd = realpath(sd) + compile_dir(sd) + files += [(x, relpath(realpath(x), sd)) for x in listfiles(sd) + if select(x)] + + # create tar.gz of thoses files + tf = tarfile.open(tfn, 'w:gz', format=tarfile.USTAR_FORMAT) + dirs = [] + for fn, afn in files: +# print('%s: %s' % (tfn, fn)) + 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 + tf.addfile(tinfo) + + # put the file + tf.add(fn, afn) + tf.close() + + +def compile_dir(dfn): + ''' + Compile *.py in directory `dfn` to *.pyo + ''' + # -OO = strip docstrings + if PYTHON is None: + return + subprocess.call([PYTHON, '-OO', '-m', 'compileall', '-f', dfn]) + + +def make_package(args): + # Ignore warning if the launcher is in args + if not args.launcher: + if not (exists(join(realpath(args.private), 'main.py')) or + exists(join(realpath(args.private), 'main.pyo'))): + print('''BUILD FAILURE: No main.py(o) 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.''') + exit(1) + + # Delete the old assets. + try_unlink('src/main/assets/public.mp3') + try_unlink('src/main/assets/private.mp3') + + # In order to speedup import and initial depack, + # construct a python27.zip + make_python_zip() + + # Package up the private and public data. + # AND: Just private for now + tar_dirs = [args.private] + if exists('private'): + tar_dirs.append('private') + if exists('crystax_python'): + tar_dirs.append('crystax_python') + + if args.private: + make_tar('src/main/assets/private.mp3', tar_dirs, args.ignore_path) + elif args.launcher: + # clean 'None's as a result of main.py path absence + tar_dirs = [tdir for tdir in tar_dirs if tdir] + make_tar('src/main/assets/private.mp3', tar_dirs, args.ignore_path) + + # folder name for launcher + url_scheme = 'kivy' + + # Prepare some variables for templating process + default_icon = 'templates/kivy-icon.png' + default_presplash = 'templates/kivy-presplash.jpg' + shutil.copy(args.icon or default_icon, 'res/drawable/icon.png') + shutil.copy(args.presplash or default_presplash, + 'res/drawable/presplash.jpg') + + # If extra Java jars were requested, copy them into the libs directory + 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') + + # 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).splitext()[0]) + + versioned_name = (args.name.replace(' ', '').replace('\'', '') + + '-' + args.version) + + version_code = 0 + if not args.numeric_version: + for i in args.version.split('.'): + version_code *= 100 + version_code += int(i) + args.numeric_version = str(version_code) + + if args.intent_filters: + with open(args.intent_filters) as fd: + args.intent_filters = fd.read() + + if args.extra_source_dirs: + esd = [] + for spec in args.extra_source_dirs: + if ':' in spec: + specdir, specincludes = spec.split(':') + 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_names = [] + 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 + + service_names.append(name) + render( + 'Service.tmpl.java', + 'src/{}/Service{}.java'.format(args.package.replace(".", "/"), name.capitalize()), + name=name, + entrypoint=entrypoint, + args=args, + foreground=foreground, + sticky=sticky, + service_id=sid + 1, + ) + + render( + 'AndroidManifest.tmpl.xml', + 'AndroidManifest.xml', + args=args, + service=service, + service_names=service_names, + url_scheme=url_scheme, + ) + + render( + 'build.tmpl.gradle', + 'build.gradle', + args=args, + aars=aars, + versioned_name=versioned_name) + + render( + 'strings.tmpl.xml', + 'src/main/res/values/strings.xml', + args=args, + url_scheme=url_scheme, + ) + + if args.sign: + render('build.properties', 'build.properties') + else: + if exists('build.properties'): + os.remove('build.properties') + + with open(join(dirname(__file__), 'res', + 'values', 'strings.xml')) as fileh: + lines = fileh.read() + + with open(join(dirname(__file__), 'res', + 'values', 'strings.xml'), 'w') as fileh: + fileh.write(re.sub(r'"private_version">[0-9\.]*<', + '"private_version">{}<'.format( + str(time.time())), lines)) + + +def parse_args(args=None): + global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON + default_android_api = 12 + import argparse + ap = argparse.ArgumentParser(description='''\ +Package a Python application for Android. + +For this to work, Java and Ant need to be in your path, as does the +tools directory of the Android SDK. +''') + + ap.add_argument('--private', dest='private', + help='the dir of user files') + # , required=True) for launcher, crashes in make_package + # if not mentioned (and the check is there anyway) + 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) + ap.add_argument('--orientation', dest='orientation', default='portrait', + help=('The orientation that the game will display in. ' + 'Usually one of "landscape", "portrait", ' + '"sensor", or "user" (the same as "sensor" but ' + 'obeying the user\'s Android rotation setting). ' + 'The full list of options is given under ' + 'android_screenOrientation at ' + 'https://developer.android.com/guide/topics/manifest/' + 'activity-element.html')) + 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('--icon', dest='icon', + help='A png file to use as the icon for the application.') + ap.add_argument('--permission', dest='permissions', action='append', + help='The permissions to give this app.', nargs='+') + ap.add_argument('--meta-data', dest='meta_data', action='append', + help='Custom key=value to add in application metadata') + ap.add_argument('--presplash', dest='presplash', + help=('A jpeg file to use as a screen 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('--wakelock', dest='wakelock', action='store_true', + help=('Indicate if the application needs the device ' + 'to stay on')) + ap.add_argument('--window', dest='window', action='store_true', + help='Indicate if the application will be windowed') + 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('--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)')) + ap.add_argument('--sdk', dest='sdk_version', default=-1, + type=int, help=('Android SDK version to use. Default to ' + 'the value of minsdk')) + ap.add_argument('--minsdk', dest='min_sdk_version', + default=default_android_api, type=int, + help=('Minimum Android SDK version to use. Default to ' + 'the value of ANDROIDAPI, or {} if not set' + .format(default_android_api))) + 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('--with-billing', dest='billing_pubkey', + help='If set, the billing service will be added (not implemented)') + ap.add_argument('--service', dest='services', action='append', + help='Declare a new service entrypoint: ' + 'NAME:PATH_TO_PY[:foreground]') + ap.add_argument('--add-source', dest='extra_source_dirs', action='append', + help='Include additional source dirs in Java build') + ap.add_argument('--no-compile-pyo', dest='no_compile_pyo', action='store_true', + help='Do not optimise .py files to .pyo.') + ap.add_argument('--sign', action='store_true', + help=('Try to sign the APK with your credentials. You must set ' + 'the appropriate environment variables.')) + + if args is None: + args = sys.argv[1:] + args = ap.parse_args(args) + args.ignore_path = [] + + if args.name and args.name[0] == '"' and args.name[-1] == '"': + args.name = args.name[1:-1] + + if args.billing_pubkey: + print('Billing not yet supported in sdl2 bootstrap!') + exit(1) + + if args.sdk_version == -1: + args.sdk_version = args.min_sdk_version + + if args.permissions is None: + args.permissions = [] + elif args.permissions: + if isinstance(args.permissions[0], list): + args.permissions = [p for perm in args.permissions for p in perm] + + if args.meta_data is None: + args.meta_data = [] + + if args.services is None: + args.services = [] + + if args.no_compile_pyo: + PYTHON = None + BLACKLIST_PATTERNS.remove('*.py') + + 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 + + make_package(args) + + return args + + +if __name__ == "__main__": + + parse_args() diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/gradle/wrapper/gradle-wrapper.jar b/pythonforandroid/bootstraps/sdl2_gradle/build/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..3d0dee6e8edfecc92e04653ec780de06f7b34f8b GIT binary patch literal 51017 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 H-~Rf4?cF@p literal 0 HcmV?d00001 diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/gradle/wrapper/gradle-wrapper.properties b/pythonforandroid/bootstraps/sdl2_gradle/build/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..ac1799fa2a --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#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-3.3-all.zip diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/gradlew b/pythonforandroid/bootstraps/sdl2_gradle/build/gradlew new file mode 100755 index 0000000000..91a7e269e1 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/gradlew @@ -0,0 +1,164 @@ +#!/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 "$@" diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/gradlew.bat b/pythonforandroid/bootstraps/sdl2_gradle/build/gradlew.bat new file mode 100644 index 0000000000..aec99730b4 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/gradlew.bat @@ -0,0 +1,90 @@ +@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 diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/jni/Android.mk b/pythonforandroid/bootstraps/sdl2_gradle/build/jni/Android.mk new file mode 100644 index 0000000000..5053e7d643 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/jni/Android.mk @@ -0,0 +1 @@ +include $(call all-subdir-makefiles) diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/jni/Application.mk b/pythonforandroid/bootstraps/sdl2_gradle/build/jni/Application.mk new file mode 100644 index 0000000000..e79e378f94 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/jni/Application.mk @@ -0,0 +1,7 @@ + +# 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) diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/jni/src/Android.mk b/pythonforandroid/bootstraps/sdl2_gradle/build/jni/src/Android.mk new file mode 100644 index 0000000000..16fadb73ed --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/jni/src/Android.mk @@ -0,0 +1,27 @@ +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 := $(SDL_PATH)/src/main/android/SDL_android_main.c \ + start.c + +LOCAL_CFLAGS += -I$(LOCAL_PATH)/../../python-install/include/python2.7 $(EXTRA_CFLAGS) + +LOCAL_SHARED_LIBRARIES := SDL2 + +LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog -lpython2.7 $(EXTRA_LDLIBS) + +LOCAL_LDFLAGS += -L$(LOCAL_PATH)/../../python-install/lib $(APPLICATION_ADDITIONAL_LDFLAGS) + +include $(BUILD_SHARED_LIBRARY) + +ifdef CRYSTAX_PYTHON_VERSION + $(call import-module,python/$(CRYSTAX_PYTHON_VERSION)) +endif diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/jni/src/Android_static.mk b/pythonforandroid/bootstraps/sdl2_gradle/build/jni/src/Android_static.mk new file mode 100644 index 0000000000..faed669c0e --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/jni/src/Android_static.mk @@ -0,0 +1,12 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := main + +LOCAL_SRC_FILES := YourSourceHere.c + +LOCAL_STATIC_LIBRARIES := SDL2_static + +include $(BUILD_SHARED_LIBRARY) +$(call import-module,SDL)LOCAL_PATH := $(call my-dir) diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/jni/src/start.c b/pythonforandroid/bootstraps/sdl2_gradle/build/jni/src/start.c new file mode 100644 index 0000000000..7582348cad --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/jni/src/start.c @@ -0,0 +1,320 @@ + +#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 "SDL.h" +#include "android/log.h" +#include "SDL_opengles2.h" + +#define ENTRYPOINT_MAXLEN 128 +#define LOG(n, x) __android_log_write(ANDROID_LOG_INFO, (n), (x)) +#define LOGP(x) LOG("python", (x)) + +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) { + FILE *file; + if (file = fopen(filename, "r")) { + fclose(file); + return 1; + } + return 0; +} + +/* 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; + + /* AND: Several filepaths are hardcoded here, these must be made + configurable */ + /* AND: P4A uses env vars...not sure what's best */ + LOGP("Initialize Python for Android"); + env_argument = getenv("ANDROID_ARGUMENT"); + setenv("ANDROID_APP_PATH", env_argument, 1); + env_entrypoint = getenv("ANDROID_ENTRYPOINT"); + env_logname = getenv("PYTHON_NAME"); + + if (env_logname == NULL) { + env_logname = "python"; + setenv("PYTHON_NAME", "python", 1); + } + + LOGP("Changing directory to the one provided by ANDROID_ARGUMENT"); + LOGP(env_argument); + chdir(env_argument); + + Py_SetProgramName(L"android_python"); + +#if PY_MAJOR_VERSION >= 3 + /* our logging module for android + */ + PyImport_AppendInittab("androidembed", initandroidembed); +#endif + + LOGP("Preparing to initialize python"); + + if (dir_exists("crystax_python/")) { + LOGP("crystax_python exists"); + char paths[256]; + snprintf(paths, 256, + "%s/crystax_python/stdlib.zip:%s/crystax_python/modules", + env_argument, env_argument); + /* snprintf(paths, 256, "%s/stdlib.zip:%s/modules", env_argument, + * env_argument); */ + LOGP("calculated paths to be..."); + LOGP(paths); + +#if PY_MAJOR_VERSION >= 3 + wchar_t *wchar_paths = Py_DecodeLocale(paths, NULL); + Py_SetPath(wchar_paths); +#else + char *wchar_paths = paths; + LOGP("Can't Py_SetPath in python2, so crystax python2 doesn't work yet"); + exit(1); +#endif + + LOGP("set wchar paths..."); + } else { + LOGP("crystax_python does not exist"); + } + + Py_Initialize(); + +#if PY_MAJOR_VERSION < 3 + PySys_SetArgv(argc, argv); +#endif + + LOGP("Initialized python"); + + /* ensure threads will work. + */ + LOGP("AND: Init threads"); + PyEval_InitThreads(); + +#if PY_MAJOR_VERSION < 3 + initandroidembed(); +#endif + + PyRun_SimpleString("import androidembed\nandroidembed.log('testing python " + "print redirection')"); + + /* inject our bootstrap code to redirect python stdin/stdout + * replace sys.path with our path + */ + PyRun_SimpleString("import sys, posix\n"); + if (dir_exists("lib")) { + /* If we built our own python, set up the paths correctly */ + LOGP("Setting up python from ANDROID_PRIVATE"); + PyRun_SimpleString("private = posix.environ['ANDROID_APP_PATH']\n" + "argument = posix.environ['ANDROID_ARGUMENT']\n" + "sys.path[:] = [ \n" + " private + '/lib/python27.zip', \n" + " private + '/lib/python2.7/', \n" + " private + '/lib/python2.7/lib-dynload/', \n" + " private + '/lib/python2.7/site-packages/', \n" + " argument ]\n"); + } + + if (dir_exists("crystax_python")) { + char add_site_packages_dir[256]; + snprintf(add_site_packages_dir, 256, + "sys.path.append('%s/crystax_python/site-packages')", + env_argument); + + PyRun_SimpleString("import sys\n" + "sys.argv = ['notaninterpreterreally']\n" + "from os.path import realpath, join, dirname"); + PyRun_SimpleString(add_site_packages_dir); + /* "sys.path.append(join(dirname(realpath(__file__)), 'site-packages'))") */ + PyRun_SimpleString("sys.path = ['.'] + sys.path"); + } + + PyRun_SimpleString( + "class LogFile(object):\n" + " def __init__(self):\n" + " self.buffer = ''\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)\n" + " self.buffer = lines[-1]\n" + " def flush(self):\n" + " return\n" + "sys.stdout = sys.stderr = LogFile()\n" + "print('Android path', sys.path)\n" + "import os\n" + "print('os.environ is', os.environ)\n" + "print('Android kivy bootstrap done. __name__ is', __name__)"); + +#if PY_MAJOR_VERSION < 3 + PyRun_SimpleString("import site; print site.getsitepackages()\n"); +#endif + + LOGP("AND: Ran string"); + + /* run it ! + */ + LOGP("Run user program, change dir and execute entrypoint"); + + /* Get the entrypoint, search the .pyo then .py + */ + char *dot = strrchr(env_entrypoint, '.'); + 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, ".pyo")) { + 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 (.pyo, fallback on .py), abort"); + return -1; + } + } else { + strcpy(entrypoint, env_entrypoint); + } + } else if (!strcmp(dot, ".py")) { + /* if .py is passed, check the pyo version first */ + strcpy(entrypoint, env_entrypoint); + entrypoint[strlen(env_entrypoint) + 1] = '\0'; + entrypoint[strlen(env_entrypoint)] = 'o'; + 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 .pyo), 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); + + 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)) /* python2 used Py_FlushLine, but this no longer exists */ + PyErr_Clear(); + } + + /* close everything + */ + Py_Finalize(); + fclose(fd); + + LOGP("Python for android ended."); + 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); + + char *argv[] = {"."}; + /* ANDROID_ARGUMENT points to service subdir, + * so main() will run main.py from this dir + */ + main(1, argv); +} + +#endif diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/assets/.gitkeep b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/.gitkeep b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/Octal.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/Octal.java new file mode 100755 index 0000000000..dd10624eab --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/Octal.java @@ -0,0 +1,141 @@ +/** + * 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; + } + +} diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarConstants.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarConstants.java new file mode 100755 index 0000000000..4611e20eaa --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarConstants.java @@ -0,0 +1,28 @@ +/** + * 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; +} diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarEntry.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarEntry.java new file mode 100755 index 0000000000..fe01db463a --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarEntry.java @@ -0,0 +1,284 @@ +/** + * 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); + } +} \ No newline at end of file diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarHeader.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarHeader.java new file mode 100755 index 0000000000..b9d3a86bef --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarHeader.java @@ -0,0 +1,243 @@ +/** + * 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'          Contigous
+ * 
+ * + * + * + * 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; + } +} \ No newline at end of file diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarInputStream.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarInputStream.java new file mode 100755 index 0000000000..ec50a1b688 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarInputStream.java @@ -0,0 +1,249 @@ +/** + * 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; + } +} diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarOutputStream.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarOutputStream.java new file mode 100755 index 0000000000..ffdfe87564 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarOutputStream.java @@ -0,0 +1,163 @@ +/** + * 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] ); + } + } + } +} diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarUtils.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarUtils.java new file mode 100755 index 0000000000..50165765c0 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kamranzafar/jtar/TarUtils.java @@ -0,0 +1,96 @@ +/** + * 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(); + } +} diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/GenericBroadcastReceiver.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/GenericBroadcastReceiver.java new file mode 100644 index 0000000000..58a1c5edf8 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/GenericBroadcastReceiver.java @@ -0,0 +1,19 @@ +package org.kivy.android; + +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.Context; + +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); + } +} diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/GenericBroadcastReceiverCallback.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/GenericBroadcastReceiverCallback.java new file mode 100644 index 0000000000..1a87c98b2d --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/GenericBroadcastReceiverCallback.java @@ -0,0 +1,8 @@ +package org.kivy.android; + +import android.content.Intent; +import android.content.Context; + +public interface GenericBroadcastReceiverCallback { + void onReceive(Context context, Intent intent); +}; diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/PythonActivity.java new file mode 100644 index 0000000000..7da0f39a1c --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/PythonActivity.java @@ -0,0 +1,456 @@ + +package org.kivy.android; + +import java.io.InputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ArrayList; + +import android.view.ViewGroup; +import android.view.SurfaceView; +import android.app.Activity; +import android.content.Intent; +import android.util.Log; +import android.widget.Toast; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.PowerManager; +import android.graphics.PixelFormat; +import android.view.SurfaceHolder; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ApplicationInfo; +import android.content.Intent; +import android.widget.ImageView; +import java.io.InputStream; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; + +import org.libsdl.app.SDLActivity; + +import org.kivy.android.PythonUtil; +import org.kivy.android.launcher.Project; + +import org.renpy.android.ResourceManager; +import org.renpy.android.AssetExtract; + + +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, "My 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(); + + 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); + } + + public void recursiveDelete(File f) { + if (f.isDirectory()) { + for (File r : f.listFiles()) { + recursiveDelete(r); + } + } + f.delete(); + } + + /** + * 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"); + unpackData("private", app_root_file); + 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(); + + 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); + SDLActivity.nativeSetEnv("ANDROID_ENTRYPOINT", p.dir + "/main.py"); + 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 { + SDLActivity.nativeSetEnv("ANDROID_ENTRYPOINT", "main.pyo"); + 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("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"); + } + 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) { + } + } + + @Override + protected void onPreExecute() { + } + + @Override + protected void onProgressUpdate(Void... values) { + } + } + + public void unpackData(final String resource, File target) { + + Log.v(TAG, "UNPACKING!!! " + resource + " " + target.getName()); + + // The version of data in memory and on disk. + String data_version = resourceManager.getString(resource + "_version"); + String disk_version = null; + + Log.v(TAG, "Data version is " + data_version); + + // If no version, no unpacking is necessary. + if (data_version == null) { + return; + } + + // Check the current disk version, if any. + String filesDir = target.getAbsolutePath(); + String disk_version_fn = filesDir + "/" + resource + ".version"; + + try { + byte buf[] = new byte[64]; + InputStream is = new FileInputStream(disk_version_fn); + int len = is.read(buf); + disk_version = new String(buf, 0, len); + is.close(); + } catch (Exception e) { + disk_version = ""; + } + + // If the disk data is out of date, extract it and write the + // version file. + // if (! data_version.equals(disk_version)) { + if (! data_version.equals(disk_version)) { + Log.v(TAG, "Extracting " + resource + " assets."); + + recursiveDelete(target); + target.mkdirs(); + + AssetExtract ae = new AssetExtract(this); + if (!ae.extractTar(resource + ".mp3", target.getAbsolutePath())) { + toastError("Could not extract " + resource + " data."); + } + + try { + // Write .nomedia. + new File(target, ".nomedia").createNewFile(); + + // Write version file. + FileOutputStream os = new FileOutputStream(disk_version_fn); + os.write(data_version.getBytes()); + os.close(); + } catch (Exception e) { + Log.w("python", e); + } + } + } + + 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) { + Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); + String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath(); + String filesDirectory = argument; + String app_root_dir = PythonActivity.mActivity.getAppRoot(); + serviceIntent.putExtra("androidPrivate", argument); + serviceIntent.putExtra("androidArgument", app_root_dir); + serviceIntent.putExtra("serviceEntrypoint", "service/main.pyo"); + serviceIntent.putExtra("pythonHome", app_root_dir); + serviceIntent.putExtra("pythonPath", app_root_dir + ":" + app_root_dir + "/lib"); + 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 implementation + * keepActive() is a method plugged in pollInputDevices in SDLActivity. + * Once it's called twice, the loading screen will be removed. + * The first call happen as soon as the window is created, but no image has been + * displayed first. My tests showed that we can wait one more. This might delay + * the real available of few hundred milliseconds. + * The real deal is to know if a rendering has already happen. The previous + * python-for-android and kivy was having something for that, but this new version + * is not compatible, and would require a new kivy version. + * In case of, the method PythonActivty.mActivity.removeLoadingScreen() can be called. + */ + public static ImageView mImageView = null; + int mLoadingCount = 2; + + @Override + public void keepActive() { + if (this.mLoadingCount > 0) { + this.mLoadingCount -= 1; + if (this.mLoadingCount == 0) { + this.removeLoadingScreen(); + } + } + } + + 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); + } + + } + +} diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/PythonService.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/PythonService.java new file mode 100644 index 0000000000..ea1c9855ae --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/PythonService.java @@ -0,0 +1,132 @@ +package org.kivy.android; + +import android.app.Service; +import android.os.IBinder; +import android.os.Bundle; +import android.content.Intent; +import android.content.Context; +import android.util.Log; +import android.app.Notification; +import android.app.PendingIntent; +import android.os.Process; +import java.io.File; + +import org.kivy.android.PythonUtil; + +import org.renpy.android.Hardware; + + +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 boolean canDisplayNotification() { + return true; + } + + 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 START_NOT_STICKY; + } + + 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"); + pythonServiceArgument = extras.getString("pythonServiceArgument"); + + pythonThread = new Thread(this); + pythonThread.start(); + + if (canDisplayNotification()) { + doStartForeground(extras); + } + + return startType(); + } + + protected void doStartForeground(Bundle extras) { + String serviceTitle = extras.getString("serviceTitle"); + String serviceDescription = extras.getString("serviceDescription"); + + Context context = getApplicationContext(); + Notification notification = new Notification(context.getApplicationInfo().icon, + serviceTitle, System.currentTimeMillis()); + Intent contextIntent = new Intent(context, PythonActivity.class); + PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + notification.setLatestEventInfo(context, serviceTitle, serviceDescription, pIntent); + startForeground(1, 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()); + } + + @Override + public void run(){ + String app_root = getFilesDir().getAbsolutePath() + "/app"; + File app_root_file = new File(app_root); + PythonUtil.loadLibraries(app_root_file); + 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); +} diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/PythonUtil.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/PythonUtil.java new file mode 100644 index 0000000000..3c6f2865b4 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/PythonUtil.java @@ -0,0 +1,62 @@ +package org.kivy.android; + +import java.io.File; + +import android.util.Log; + + +public class PythonUtil { + private static final String TAG = "PythonUtil"; + + protected static String[] getLibraries() { + return new String[] { + "SDL2", + "SDL2_image", + "SDL2_mixer", + "SDL2_ttf", + "python2.7", + "python3.5m", + "python3.6m", + "main" + }; + } + + public static void loadLibraries(File filesDir) { + + String filesDirPath = filesDir.getAbsolutePath(); + boolean foundPython = false; + + for (String lib : getLibraries()) { + 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 + if (lib.startsWith("python3.6") && !foundPython) { + throw new java.lang.RuntimeException("Could not load any libpythonXXX.so"); + } + continue; + } + } + + try { + System.load(filesDirPath + "/lib/python2.7/lib-dynload/_io.so"); + System.load(filesDirPath + "/lib/python2.7/lib-dynload/unicodedata.so"); + } catch(UnsatisfiedLinkError e) { + Log.v(TAG, "Failed to load _io.so or unicodedata.so...but that's okay."); + } + + try { + // System.loadLibrary("ctypes"); + System.load(filesDirPath + "/lib/python2.7/lib-dynload/_ctypes.so"); + } catch(UnsatisfiedLinkError e) { + Log.v(TAG, "Unsatisfied linker when loading ctypes"); + } + + Log.v(TAG, "Loaded everything!"); + } +} diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/concurrency/PythonEvent.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/concurrency/PythonEvent.java new file mode 100644 index 0000000000..9911356ba0 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/concurrency/PythonEvent.java @@ -0,0 +1,45 @@ +package org.kivy.android.concurrency; + +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Created by ryan on 3/28/14. + */ +public class PythonEvent { + private final Lock lock = new ReentrantLock(); + private final Condition cond = lock.newCondition(); + private boolean flag = false; + + public void set() { + lock.lock(); + try { + flag = true; + cond.signalAll(); + } finally { + lock.unlock(); + } + } + + public void wait_() throws InterruptedException { + lock.lock(); + try { + while (!flag) { + cond.await(); + } + } finally { + lock.unlock(); + } + } + + public void clear() { + lock.lock(); + try { + flag = false; + cond.signalAll(); + } finally { + lock.unlock(); + } + } +} diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/concurrency/PythonLock.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/concurrency/PythonLock.java new file mode 100644 index 0000000000..22f9d903e1 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/concurrency/PythonLock.java @@ -0,0 +1,19 @@ +package org.kivy.android.concurrency; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Created by ryan on 3/28/14. + */ +public class PythonLock { + private final Lock lock = new ReentrantLock(); + + public void acquire() { + lock.lock(); + } + + public void release() { + lock.unlock(); + } +} diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/launcher/Project.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/launcher/Project.java new file mode 100644 index 0000000000..9177b43bb7 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/launcher/Project.java @@ -0,0 +1,99 @@ +package org.kivy.android.launcher; + +import java.io.UnsupportedEncodingException; +import java.io.File; +import java.io.FileInputStream; +import java.util.Properties; + +import android.util.Log; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + + +/** + * 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; + + } +} diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/launcher/ProjectAdapter.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/launcher/ProjectAdapter.java new file mode 100644 index 0000000000..f66debfec8 --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/launcher/ProjectAdapter.java @@ -0,0 +1,44 @@ +package org.kivy.android.launcher; + +import android.app.Activity; +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.view.Gravity; +import android.widget.ArrayAdapter; +import android.widget.TextView; +import android.widget.LinearLayout; +import android.widget.ImageView; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; + +import org.renpy.android.ResourceManager; + +public class ProjectAdapter extends ArrayAdapter { + + private Activity mContext; + private ResourceManager resourceManager; + + public ProjectAdapter(Activity context) { + super(context, 0); + + mContext = context; + 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; + } +} diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/launcher/ProjectChooser.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/launcher/ProjectChooser.java new file mode 100644 index 0000000000..17eec32f0b --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/kivy/android/launcher/ProjectChooser.java @@ -0,0 +1,94 @@ +package org.kivy.android.launcher; + +import android.app.Activity; +import android.os.Bundle; + +import android.content.Intent; +import android.content.res.Resources; +import android.util.Log; +import android.view.View; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.AdapterView; +import android.os.Environment; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import android.net.Uri; + +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(); + } +} diff --git a/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/libsdl/app/SDLActivity.java b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/libsdl/app/SDLActivity.java new file mode 100644 index 0000000000..e1dc08468d --- /dev/null +++ b/pythonforandroid/bootstraps/sdl2_gradle/build/src/main/java/org/libsdl/app/SDLActivity.java @@ -0,0 +1,1579 @@ +package org.libsdl.app; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.lang.reflect.Method; + +import android.app.*; +import android.content.*; +import android.view.*; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.widget.AbsoluteLayout; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.os.*; +import android.util.Log; +import android.util.SparseArray; +import android.graphics.*; +import android.graphics.drawable.Drawable; +import android.media.*; +import android.hardware.*; + +/** + SDL Activity +*/ +public class SDLActivity extends Activity { + private static final String TAG = "SDL"; + + // Keep track of the paused state + public static boolean mIsPaused, mIsSurfaceReady, mHasFocus; + public static boolean mExitCalledFromJava; + + /** If shared libraries (e.g. SDL or the native application) could not be loaded. */ + public static boolean mBrokenLibraries; + + // If we want to separate mouse and touch events. + // This is only toggled in native code when a hint is set! + public static boolean mSeparateMouseAndTouch; + + // Main components + protected static SDLActivity mSingleton; + protected static SDLSurface mSurface; + protected static View mTextEdit; + protected static ViewGroup mLayout; + protected static SDLJoystickHandler mJoystickHandler; + + // This is what SDL runs in. It invokes SDL_main(), eventually + protected static Thread mSDLThread; + + // Audio + protected static AudioTrack mAudioTrack; + + /** + * This method is called by SDL before loading the native shared libraries. + * It can be overridden to provide names of shared libraries to be loaded. + * The default implementation returns the defaults. It never returns null. + * An array returned by a new implementation must at least contain "SDL2". + * Also keep in mind that the order the libraries are loaded may matter. + * @return names of shared libraries to be loaded (e.g. "SDL2", "main"). + */ + protected String[] getLibraries() { + return new String[] { + "SDL2", + // "SDL2_image", + // "SDL2_mixer", + // "SDL2_net", + // "SDL2_ttf", + "main" + }; + } + + // Load the .so + public void loadLibraries() { + for (String lib : getLibraries()) { + System.loadLibrary(lib); + } + } + + /** + * This method is called by SDL before starting the native application thread. + * It can be overridden to provide the arguments after the application name. + * The default implementation returns an empty array. It never returns null. + * @return arguments for the native application. + */ + protected String[] getArguments() { + return new String[0]; + } + + 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 + mSingleton = null; + mSurface = null; + mTextEdit = null; + mLayout = null; + mJoystickHandler = null; + mSDLThread = null; + mAudioTrack = null; + mExitCalledFromJava = false; + mBrokenLibraries = false; + mIsPaused = false; + mIsSurfaceReady = false; + mHasFocus = true; + } + + // Setup + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.v("SDL", "Device: " + android.os.Build.DEVICE); + Log.v("SDL", "Model: " + android.os.Build.MODEL); + Log.v("SDL", "onCreate():" + mSingleton); + 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() { + // 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 start the application. Please try again and/or reinstall." + + System.getProperty("line.separator") + + System.getProperty("line.separator") + + "Error: " + errorMsgBrokenLib); + dlgAlert.setTitle("SDL Error"); + dlgAlert.setPositiveButton("Exit", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog,int id) { + // if this button is clicked, close current activity + SDLActivity.mSingleton.finish(); + } + }); + dlgAlert.setCancelable(false); + dlgAlert.create().show(); + + return; + } + + // Set up the surface + mSurface = new SDLSurface(getApplication()); + + if(Build.VERSION.SDK_INT >= 12) { + mJoystickHandler = new SDLJoystickHandler_API12(); + } + else { + mJoystickHandler = new SDLJoystickHandler(); + } + + mLayout = new AbsoluteLayout(this); + mLayout.addView(mSurface); + + setContentView(mLayout); + } + + // Events + @Override + protected void onPause() { + Log.v("SDL", "onPause()"); + super.onPause(); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.handlePause(); + } + + @Override + protected void onResume() { + Log.v("SDL", "onResume()"); + super.onResume(); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.handleResume(); + } + + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + Log.v("SDL", "onWindowFocusChanged(): " + hasFocus); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.mHasFocus = hasFocus; + if (hasFocus) { + SDLActivity.handleResume(); + } + } + + @Override + public void onLowMemory() { + Log.v("SDL", "onLowMemory()"); + super.onLowMemory(); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.nativeLowMemory(); + } + + @Override + protected void onDestroy() { + Log.v("SDL", "onDestroy()"); + + if (SDLActivity.mBrokenLibraries) { + super.onDestroy(); + // Reset everything in case the user re opens the app + SDLActivity.initialize(); + return; + } + + // Send a quit message to the application + SDLActivity.mExitCalledFromJava = true; + SDLActivity.nativeQuit(); + + // Now wait for the SDL thread to quit + if (SDLActivity.mSDLThread != null) { + try { + SDLActivity.mSDLThread.join(); + } catch(Exception e) { + Log.v("SDL", "Problem stopping thread: " + e); + } + SDLActivity.mSDLThread = null; + + //Log.v("SDL", "Finished waiting for SDL thread"); + } + + super.onDestroy(); + // Reset everything in case the user re opens the app + SDLActivity.initialize(); + + // Completely closes application. + System.exit(0); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + + if (SDLActivity.mBrokenLibraries) { + return false; + } + + int keyCode = event.getKeyCode(); + // Ignore certain special keys so they're handled by Android + if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || + keyCode == KeyEvent.KEYCODE_VOLUME_UP || + keyCode == KeyEvent.KEYCODE_CAMERA || + keyCode == 168 || /* API 11: KeyEvent.KEYCODE_ZOOM_IN */ + keyCode == 169 /* API 11: KeyEvent.KEYCODE_ZOOM_OUT */ + ) { + return false; + } + return super.dispatchKeyEvent(event); + } + + /** Called by onPause or surfaceDestroyed. Even if surfaceDestroyed + * is the first to be called, mIsSurfaceReady should still be set + * to 'true' during the call to onPause (in a usual scenario). + */ + public static void handlePause() { + if (!SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady) { + SDLActivity.mIsPaused = true; + SDLActivity.nativePause(); + mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, false); + } + } + + /** Called by onResume or surfaceCreated. An actual resume should be done only when the surface is ready. + * Note: Some Android variants may send multiple surfaceChanged events, so we don't need to resume + * every time we get one of those events, only if it comes after surfaceDestroyed + */ + public static void handleResume() { + if (SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady && SDLActivity.mHasFocus) { + SDLActivity.mIsPaused = false; + SDLActivity.nativeResume(); + mSurface.handleResume(); + } + } + + /* The native thread has finished */ + public static void handleNativeExit() { + SDLActivity.mSDLThread = null; + mSingleton.finish(); + } + + + // Messages from the SDLMain thread + static final int COMMAND_CHANGE_TITLE = 1; + static final int COMMAND_UNUSED = 2; + static final int COMMAND_TEXTEDIT_HIDE = 3; + static final int COMMAND_SET_KEEP_SCREEN_ON = 5; + + protected static final int COMMAND_USER = 0x8000; + + /** + * This method is called by SDL if SDL did not handle a message itself. + * This happens if a received message contains an unsupported command. + * Method can be overwritten to handle Messages in a different class. + * @param command the command of the message. + * @param param the parameter of the message. May be null. + * @return if the message was handled in overridden method. + */ + protected boolean onUnhandledMessage(int command, Object param) { + return false; + } + + /** + * A Handler class for Messages from native SDL applications. + * It uses current Activities as target (e.g. for the title). + * static to prevent implicit references to enclosing object. + */ + protected static class SDLCommandHandler extends Handler { + @Override + public void handleMessage(Message msg) { + Context context = getContext(); + if (context == null) { + Log.e(TAG, "error handling message, getContext() returned null"); + return; + } + switch (msg.arg1) { + case COMMAND_CHANGE_TITLE: + if (context instanceof Activity) { + ((Activity) context).setTitle((String)msg.obj); + } else { + Log.e(TAG, "error handling message, getContext() returned no Activity"); + } + break; + case COMMAND_TEXTEDIT_HIDE: + if (mTextEdit != null) { + mTextEdit.setVisibility(View.GONE); + + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0); + } + break; + case COMMAND_SET_KEEP_SCREEN_ON: + { + Window window = ((Activity) context).getWindow(); + if (window != null) { + if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + break; + } + default: + if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) { + Log.e(TAG, "error handling message, command is " + msg.arg1); + } + } + } + } + + // Handler for the messages + Handler commandHandler = new SDLCommandHandler(); + + // Send a message from the SDLMain thread + boolean sendCommand(int command, Object data) { + Message msg = commandHandler.obtainMessage(); + msg.arg1 = command; + msg.obj = data; + return commandHandler.sendMessage(msg); + } + + // C functions we call + public static native int nativeInit(Object arguments); + public static native void nativeLowMemory(); + public static native void nativeQuit(); + public static native void nativePause(); + public static native void nativeResume(); + public static native void onNativeResize(int x, int y, int format, float rate); + public static native int onNativePadDown(int device_id, int keycode); + public static native int onNativePadUp(int device_id, int keycode); + public static native void onNativeJoy(int device_id, int axis, + float value); + public static native void onNativeHat(int device_id, int hat_id, + int x, int y); + public static native void nativeSetEnv(String j_name, String j_value); + public static native void onNativeKeyDown(int keycode); + public static native void onNativeKeyUp(int keycode); + public static native void onNativeKeyboardFocusLost(); + public static native void onNativeMouse(int button, int action, float x, float y); + public static native void onNativeTouch(int touchDevId, int pointerFingerId, + int action, float x, + float y, float p); + public static native void onNativeAccel(float x, float y, float z); + public static native void onNativeSurfaceChanged(); + public static native void onNativeSurfaceDestroyed(); + public static native void nativeFlipBuffers(); + public static native int nativeAddJoystick(int device_id, String name, + int is_accelerometer, int nbuttons, + int naxes, int nhats, int nballs); + public static native int nativeRemoveJoystick(int device_id); + public static native String nativeGetHint(String name); + + /** + * This method is called by SDL using JNI. + */ + public static void flipBuffers() { + SDLActivity.nativeFlipBuffers(); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean setActivityTitle(String title) { + // Called from SDLMain() thread and can't directly affect the view + return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean sendMessage(int command, int param) { + return mSingleton.sendCommand(command, Integer.valueOf(param)); + } + + /** + * This method is called by SDL using JNI. + */ + public static Context getContext() { + return mSingleton; + } + + /** + * This method is called by SDL using JNI. + * @return result of getSystemService(name) but executed on UI thread. + */ + public Object getSystemServiceFromUiThread(final String name) { + final Object lock = new Object(); + final Object[] results = new Object[2]; // array for writable variables + synchronized (lock) { + runOnUiThread(new Runnable() { + @Override + public void run() { + synchronized (lock) { + results[0] = getSystemService(name); + results[1] = Boolean.TRUE; + lock.notify(); + } + } + }); + if (results[1] == null) { + try { + lock.wait(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + } + return results[0]; + } + + static class ShowTextInputTask implements Runnable { + /* + * This is used to regulate the pan&scan method to have some offset from + * the bottom edge of the input region and the top edge of an input + * method (soft keyboard) + */ + static final int HEIGHT_PADDING = 15; + + public int x, y, w, h; + + public ShowTextInputTask(int x, int y, int w, int h) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + } + + @Override + public void run() { + AbsoluteLayout.LayoutParams params = new AbsoluteLayout.LayoutParams( + w, h + HEIGHT_PADDING, x, y); + + if (mTextEdit == null) { + mTextEdit = new DummyEdit(getContext()); + + mLayout.addView(mTextEdit, params); + } else { + mTextEdit.setLayoutParams(params); + } + + mTextEdit.setVisibility(View.VISIBLE); + mTextEdit.requestFocus(); + + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mTextEdit, 0); + } + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean showTextInput(int x, int y, int w, int h) { + // Transfer the task to the main thread as a Runnable + return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h)); + } + + /** + * This method is called by SDL using JNI. + */ + public static Surface getNativeSurface() { + return SDLActivity.mSurface.getNativeSurface(); + } + + // Audio + + /** + * This method is called by SDL using JNI. + */ + public static int audioInit(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames) { + int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO; + int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT; + int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1); + + Log.v("SDL", "SDL audio: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer"); + + // Let the user pick a larger buffer if they really want -- but ye + // gods they probably shouldn't, the minimums are horrifyingly high + // latency already + desiredFrames = Math.max(desiredFrames, (AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize); + + if (mAudioTrack == null) { + mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, + channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM); + + // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid + // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java + // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState() + + if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) { + Log.e("SDL", "Failed during initialization of Audio Track"); + mAudioTrack = null; + return -1; + } + + mAudioTrack.play(); + } + + Log.v("SDL", "SDL audio: got " + ((mAudioTrack.getChannelCount() >= 2) ? "stereo" : "mono") + " " + ((mAudioTrack.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) ? "16-bit" : "8-bit") + " " + (mAudioTrack.getSampleRate() / 1000f) + "kHz, " + desiredFrames + " frames buffer"); + + return 0; + } + + /** + * This method is called by SDL using JNI. + */ + public static void audioWriteShortBuffer(short[] buffer) { + for (int i = 0; i < buffer.length; ) { + int result = mAudioTrack.write(buffer, i, buffer.length - i); + if (result > 0) { + i += result; + } else if (result == 0) { + try { + Thread.sleep(1); + } catch(InterruptedException e) { + // Nom nom + } + } else { + Log.w("SDL", "SDL audio: error return from write(short)"); + return; + } + } + } + + /** + * This method is called by SDL using JNI. + */ + public static void audioWriteByteBuffer(byte[] buffer) { + for (int i = 0; i < buffer.length; ) { + int result = mAudioTrack.write(buffer, i, buffer.length - i); + if (result > 0) { + i += result; + } else if (result == 0) { + try { + Thread.sleep(1); + } catch(InterruptedException e) { + // Nom nom + } + } else { + Log.w("SDL", "SDL audio: error return from write(byte)"); + return; + } + } + } + + /** + * This method is called by SDL using JNI. + */ + public static void audioQuit() { + if (mAudioTrack != null) { + mAudioTrack.stop(); + mAudioTrack = null; + } + } + + // Input + + /** + * This method is called by SDL using JNI. + * @return an array which may be empty but is never null. + */ + public static int[] inputGetInputDeviceIds(int sources) { + int[] ids = InputDevice.getDeviceIds(); + int[] filtered = new int[ids.length]; + int used = 0; + for (int i = 0; i < ids.length; ++i) { + InputDevice device = InputDevice.getDevice(ids[i]); + if ((device != null) && ((device.getSources() & sources) != 0)) { + filtered[used++] = device.getId(); + } + } + return Arrays.copyOf(filtered, used); + } + + // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance + public static boolean handleJoystickMotionEvent(MotionEvent event) { + return mJoystickHandler.handleMotionEvent(event); + } + + /** + * This method is called by SDL using JNI. + */ + public static void pollInputDevices() { + if (SDLActivity.mSDLThread != null) { + mJoystickHandler.pollInputDevices(); + SDLActivity.mSingleton.keepActive(); + } + } + + /** + * Trick needed for loading screen + */ + public void keepActive() { + } + + // APK extension files support + + /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */ + private Object expansionFile; + + /** com.android.vending.expansion.zipfile.ZipResourceFile's getInputStream() or null. */ + private Method expansionFileMethod; + + /** + * This method is called by SDL using JNI. + */ + public InputStream openAPKExtensionInputStream(String fileName) throws IOException { + // Get a ZipResourceFile representing a merger of both the main and patch files + if (expansionFile == null) { + Integer mainVersion = Integer.valueOf(nativeGetHint("SDL_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION")); + Integer patchVersion = Integer.valueOf(nativeGetHint("SDL_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION")); + + try { + // To avoid direct dependency on Google APK extension library that is + // not a part of Android SDK we access it using reflection + expansionFile = Class.forName("com.android.vending.expansion.zipfile.APKExpansionSupport") + .getMethod("getAPKExpansionZipFile", Context.class, int.class, int.class) + .invoke(null, this, mainVersion, patchVersion); + + expansionFileMethod = expansionFile.getClass() + .getMethod("getInputStream", String.class); + } catch (Exception ex) { + ex.printStackTrace(); + expansionFile = null; + expansionFileMethod = null; + } + } + + // Get an input stream for a known file inside the expansion file ZIPs + InputStream fileStream; + try { + fileStream = (InputStream)expansionFileMethod.invoke(expansionFile, fileName); + } catch (Exception ex) { + ex.printStackTrace(); + fileStream = null; + } + + if (fileStream == null) { + throw new IOException(); + } + + return fileStream; + } + + // Messagebox + + /** Result of current messagebox. Also used for blocking the calling thread. */ + protected final int[] messageboxSelection = new int[1]; + + /** Id of current dialog. */ + protected int dialogs = 0; + + /** + * This method is called by SDL using JNI. + * Shows the messagebox from UI thread and block calling thread. + * buttonFlags, buttonIds and buttonTexts must have same length. + * @param buttonFlags array containing flags for every button. + * @param buttonIds array containing id for every button. + * @param buttonTexts array containing text for every button. + * @param colors null for default or array of length 5 containing colors. + * @return button id or -1. + */ + public int messageboxShowMessageBox( + final int flags, + final String title, + final String message, + final int[] buttonFlags, + final int[] buttonIds, + final String[] buttonTexts, + final int[] colors) { + + messageboxSelection[0] = -1; + + // sanity checks + + if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) { + return -1; // implementation broken + } + + // collect arguments for Dialog + + final Bundle args = new Bundle(); + args.putInt("flags", flags); + args.putString("title", title); + args.putString("message", message); + args.putIntArray("buttonFlags", buttonFlags); + args.putIntArray("buttonIds", buttonIds); + args.putStringArray("buttonTexts", buttonTexts); + args.putIntArray("colors", colors); + + // trigger Dialog creation on UI thread + + runOnUiThread(new Runnable() { + @Override + public void run() { + showDialog(dialogs++, args); + } + }); + + // block the calling thread + + synchronized (messageboxSelection) { + try { + messageboxSelection.wait(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + return -1; + } + } + + // return selected value + + return messageboxSelection[0]; + } + + @Override + protected Dialog onCreateDialog(int ignore, Bundle args) { + + // TODO set values from "flags" to messagebox dialog + + // get colors + + int[] colors = args.getIntArray("colors"); + int backgroundColor; + int textColor; + int buttonBorderColor; + int buttonBackgroundColor; + int buttonSelectedColor; + if (colors != null) { + int i = -1; + backgroundColor = colors[++i]; + textColor = colors[++i]; + buttonBorderColor = colors[++i]; + buttonBackgroundColor = colors[++i]; + buttonSelectedColor = colors[++i]; + } else { + backgroundColor = Color.TRANSPARENT; + textColor = Color.TRANSPARENT; + buttonBorderColor = Color.TRANSPARENT; + buttonBackgroundColor = Color.TRANSPARENT; + buttonSelectedColor = Color.TRANSPARENT; + } + + // create dialog with title and a listener to wake up calling thread + + final Dialog dialog = new Dialog(this); + dialog.setTitle(args.getString("title")); + dialog.setCancelable(false); + dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface unused) { + synchronized (messageboxSelection) { + messageboxSelection.notify(); + } + } + }); + + // create text + + TextView message = new TextView(this); + message.setGravity(Gravity.CENTER); + message.setText(args.getString("message")); + if (textColor != Color.TRANSPARENT) { + message.setTextColor(textColor); + } + + // create buttons + + int[] buttonFlags = args.getIntArray("buttonFlags"); + int[] buttonIds = args.getIntArray("buttonIds"); + String[] buttonTexts = args.getStringArray("buttonTexts"); + + final SparseArray