Skip to content

Commit 146bab2

Browse files
authored
Merge pull request #1412 from inclement/python3_recipe
Python 3 recipe
2 parents 6369041 + b076cd9 commit 146bab2

33 files changed

+746
-318
lines changed

.travis.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@ env:
1919
- ANDROID_NDK_HOME=/opt/android/android-ndk
2020
- CRYSTAX_NDK_HOME=/opt/android/crystax-ndk
2121
matrix:
22-
- COMMAND='. venv/bin/activate && cd testapps/ && python setup_testapp_python2.py apk --sdk-dir $ANDROID_SDK_HOME --ndk-dir $ANDROID_NDK_HOME'
2322
# overrides requirements to skip `peewee` pure python module, see:
2423
# https://github.com/kivy/python-for-android/issues/1263#issuecomment-390421054
2524
- COMMAND='. venv/bin/activate && cd testapps/ && python setup_testapp_python2_sqlite_openssl.py apk --sdk-dir $ANDROID_SDK_HOME --ndk-dir $ANDROID_NDK_HOME --requirements sdl2,pyjnius,kivy,python2,openssl,requests,sqlite3,setuptools'
2625
- COMMAND='. venv/bin/activate && cd testapps/ && python setup_testapp_python2.py apk --sdk-dir $ANDROID_SDK_HOME --ndk-dir $ANDROID_NDK_HOME --bootstrap sdl2 --requirements python2,numpy'
27-
- COMMAND='. venv/bin/activate && cd testapps/ && python setup_testapp_python3.py apk --sdk-dir $ANDROID_SDK_HOME --ndk-dir $CRYSTAX_NDK_HOME --requirements python3crystax,setuptools,android,sdl2,pyjnius,kivy'
26+
- COMMAND='. venv/bin/activate && cd testapps/ && python setup_testapp_python3crystax.py apk --sdk-dir $ANDROID_SDK_HOME --ndk-dir $CRYSTAX_NDK_HOME --requirements python3crystax,setuptools,android,sdl2,pyjnius,kivy'
2827
# builds only the recipes that moved
2928
- COMMAND='. venv/bin/activate && ./ci/rebuild_updated_recipes.py'
3029

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ RUN mkdir --parents "${ANDROID_SDK_HOME}/.android/" && \
8484
echo '### User Sources for Android SDK Manager' > "${ANDROID_SDK_HOME}/.android/repositories.cfg"
8585
RUN yes | "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" --licenses
8686
RUN "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "platforms;android-19" && \
87+
"${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "platforms;android-27" && \
8788
"${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "build-tools;26.0.2" && \
8889
chmod +x "${ANDROID_SDK_HOME}/tools/bin/avdmanager"
8990

doc/source/buildoptions.rst

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,23 @@ This option builds Python 2.7.2 for your selected Android
1717
architecture. There are no special requirements, all the building is
1818
done locally.
1919

20-
The python2 build is also the way python-for-android originally
21-
worked, even in the old toolchain.
22-
2320

2421
python3
2522
~~~~~~~
2623

27-
.. warning::
28-
Python3 support is experimental, and some of these details
29-
may change as it is improved and fully stabilised.
24+
Python3 is supported in two ways. The default method uses CPython 3.7+
25+
and works with any recent version of the Android NDK.
26+
27+
Select Python 3 by adding it to your requirements,
28+
e.g. ``--requirements=python3``.
29+
30+
31+
CrystaX python3
32+
###############
33+
34+
.. warning:: python-for-android originally supported Python 3 using the CrystaX
35+
NDK. This support is now being phased out as CrystaX is no longer
36+
actively developed.
3037

3138
.. note:: You must manually download the `CrystaX NDK
3239
<https://www.crystax.net/android/ndk>`__ and tell
@@ -42,11 +49,6 @@ Google's official NDK which includes many improvements. You
4249
python3. You can get it `here
4350
<https://www.crystax.net/en/download>`__.
4451

45-
The python3crystax build is handled quite differently to python2 so
46-
there may be bugs or surprising behaviours. If you come across any,
47-
feel free to `open an issue
48-
<https://github.com/kivy/python-for-android>`__.
49-
5052
.. _bootstrap_build_options:
5153

5254
Bootstrap options

doc/source/quickstart.rst

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,17 @@ Then, you can edit your ``~/.bashrc`` or other favorite shell to include new env
114114
# Adjust the paths!
115115
export ANDROIDSDK="$HOME/Documents/android-sdk-21"
116116
export ANDROIDNDK="$HOME/Documents/android-ndk-r10e"
117-
export ANDROIDAPI="19" # Target API version of your application
117+
export ANDROIDAPI="26" # Target API version of your application
118+
export NDKAPI="19" # Minimum supported API version of your application
118119
export ANDROIDNDKVER="r10e" # Version of the NDK you installed
119120

120121
You have the possibility to configure on any command the PATH to the SDK, NDK and Android API using:
121122

122-
- :code:`--sdk_dir PATH` as an equivalent of `$ANDROIDSDK`
123-
- :code:`--ndk_dir PATH` as an equivalent of `$ANDROIDNDK`
124-
- :code:`--android_api VERSION` as an equivalent of `$ANDROIDAPI`
125-
- :code:`--ndk_version VERSION` as an equivalent of `$ANDROIDNDKVER`
123+
- :code:`--sdk-dir PATH` as an equivalent of `$ANDROIDSDK`
124+
- :code:`--ndk-dir PATH` as an equivalent of `$ANDROIDNDK`
125+
- :code:`--android-api VERSION` as an equivalent of `$ANDROIDAPI`
126+
- :code:`--ndk-api VERSION` as an equivalent of `$NDKAPI`
127+
- :code:`--ndk-version VERSION` as an equivalent of `$ANDROIDNDKVER`
126128

127129

128130
Usage

pythonforandroid/archs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def get_env(self, with_flags_in_cc=True):
3535

3636
env['CFLAGS'] = ' '.join([
3737
'-DANDROID', '-mandroid', '-fomit-frame-pointer'
38-
' -D__ANDROID_API__={}'.format(self.ctx._android_api),
38+
' -D__ANDROID_API__={}'.format(self.ctx.ndk_api),
3939
])
4040
env['LDFLAGS'] = ' '
4141

@@ -133,6 +133,7 @@ def get_env(self, with_flags_in_cc=True):
133133
env['PATH'] = environ['PATH']
134134

135135
env['ARCH'] = self.arch
136+
env['NDK_API'] = 'android-{}'.format(str(self.ctx.ndk_api))
136137

137138
if self.ctx.python_recipe and self.ctx.python_recipe.from_crystax:
138139
env['CRYSTAX_PYTHON_VERSION'] = self.ctx.python_recipe.version

pythonforandroid/bootstrap.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from os import listdir
33
import sh
44
import glob
5-
import json
65
import importlib
76

87
from pythonforandroid.logger import (warning, shprint, info, logger,
@@ -102,21 +101,10 @@ def prepare_build_dir(self):
102101
fileh.write('target=android-{}'.format(self.ctx.android_api))
103102

104103
def prepare_dist_dir(self, name):
105-
# self.dist_dir = self.get_dist_dir(name)
106104
ensure_dir(self.dist_dir)
107105

108106
def run_distribute(self):
109-
# print('Default bootstrap being used doesn\'t know how '
110-
# 'to distribute...failing.')
111-
# exit(1)
112-
with current_directory(self.dist_dir):
113-
info('Saving distribution info')
114-
with open('dist_info.json', 'w') as fileh:
115-
json.dump({'dist_name': self.ctx.dist_name,
116-
'bootstrap': self.ctx.bootstrap.name,
117-
'archs': [arch.arch for arch in self.ctx.archs],
118-
'recipes': self.ctx.recipe_build_order + self.ctx.python_modules},
119-
fileh)
107+
self.distribution.save_info(self.dist_dir)
120108

121109
@classmethod
122110
def list_bootstraps(cls):
@@ -254,9 +242,15 @@ def strip_libraries(self, arch):
254242
warning('Can\'t find strip in PATH...')
255243
return
256244
strip = sh.Command(strip)
257-
filens = shprint(sh.find, join(self.dist_dir, 'private'),
258-
join(self.dist_dir, 'libs'),
259-
'-iname', '*.so', _env=env).stdout.decode('utf-8')
245+
246+
if self.ctx.python_recipe.name == 'python2':
247+
filens = shprint(sh.find, join(self.dist_dir, 'private'),
248+
join(self.dist_dir, 'libs'),
249+
'-iname', '*.so', _env=env).stdout.decode('utf-8')
250+
else:
251+
filens = shprint(sh.find, join(self.dist_dir, '_python_bundle', '_python_bundle', 'modules'),
252+
join(self.dist_dir, 'libs'),
253+
'-iname', '*.so', _env=env).stdout.decode('utf-8')
260254
logger.info('Stripping libraries in private dir')
261255
for filen in filens.split('\n'):
262256
try:

pythonforandroid/bootstraps/sdl2/__init__.py

Lines changed: 15 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,21 @@
11
from pythonforandroid.toolchain import (
22
Bootstrap, shprint, current_directory, info, info_main)
33
from pythonforandroid.util import ensure_dir
4-
from os.path import join, exists, curdir, abspath
5-
from os import walk
6-
import glob
4+
from os.path import join, exists
75
import sh
86

97

10-
EXCLUDE_EXTS = (".py", ".pyc", ".so.o", ".so.a", ".so.libs", ".pyx")
11-
12-
138
class SDL2GradleBootstrap(Bootstrap):
14-
name = 'sdl2_gradle'
9+
name = 'sdl2'
1510

16-
recipe_depends = ['sdl2', ('python2', 'python3crystax')]
11+
recipe_depends = ['sdl2', ('python2', 'python3', 'python3crystax')]
1712

1813
def run_distribute(self):
1914
info_main("# Creating Android project ({})".format(self.name))
2015

2116
arch = self.ctx.archs[0]
2217
python_install_dir = self.ctx.get_python_install_dir()
2318
from_crystax = self.ctx.python_recipe.from_crystax
24-
crystax_python_dir = join("crystax_python", "crystax_python")
2519

2620
if len(self.ctx.archs) > 1:
2721
raise ValueError("SDL2/gradle support only one arch")
@@ -30,7 +24,7 @@ def run_distribute(self):
3024
shprint(sh.rm, "-rf", self.dist_dir)
3125
shprint(sh.cp, "-r", self.build_dir, self.dist_dir)
3226

33-
# either the build use environemnt variable (ANDROID_HOME)
27+
# either the build use environment variable (ANDROID_HOME)
3428
# or the local.properties if exists
3529
with current_directory(self.dist_dir):
3630
with open('local.properties', 'w') as fileh:
@@ -39,91 +33,31 @@ def run_distribute(self):
3933
with current_directory(self.dist_dir):
4034
info("Copying Python distribution")
4135

42-
if not exists("private") and not from_crystax:
43-
ensure_dir("private")
44-
if not exists("crystax_python") and from_crystax:
45-
ensure_dir(crystax_python_dir)
46-
4736
hostpython = sh.Command(self.ctx.hostpython)
48-
if not from_crystax:
37+
if self.ctx.python_recipe.name == 'python2':
4938
try:
5039
shprint(hostpython, '-OO', '-m', 'compileall',
5140
python_install_dir,
5241
_tail=10, _filterout="^Listing")
5342
except sh.ErrorReturnCode:
5443
pass
55-
if not exists('python-install'):
44+
if 'python2' in self.ctx.recipe_build_order and not exists('python-install'):
5645
shprint(
5746
sh.cp, '-a', python_install_dir, './python-install')
5847

5948
self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)])
6049
self.distribute_javaclasses(self.ctx.javaclass_dir,
6150
dest_dir=join("src", "main", "java"))
6251

63-
if not from_crystax:
64-
info("Filling private directory")
65-
if not exists(join("private", "lib")):
66-
info("private/lib does not exist, making")
67-
shprint(sh.cp, "-a",
68-
join("python-install", "lib"), "private")
69-
shprint(sh.mkdir, "-p",
70-
join("private", "include", "python2.7"))
71-
72-
libpymodules_fn = join("libs", arch.arch, "libpymodules.so")
73-
if exists(libpymodules_fn):
74-
shprint(sh.mv, libpymodules_fn, 'private/')
75-
shprint(sh.cp,
76-
join('python-install', 'include',
77-
'python2.7', 'pyconfig.h'),
78-
join('private', 'include', 'python2.7/'))
79-
80-
info('Removing some unwanted files')
81-
shprint(sh.rm, '-f', join('private', 'lib', 'libpython2.7.so'))
82-
shprint(sh.rm, '-rf', join('private', 'lib', 'pkgconfig'))
83-
84-
libdir = join(self.dist_dir, 'private', 'lib', 'python2.7')
85-
site_packages_dir = join(libdir, 'site-packages')
86-
with current_directory(libdir):
87-
removes = []
88-
for dirname, root, filenames in walk("."):
89-
for filename in filenames:
90-
for suffix in EXCLUDE_EXTS:
91-
if filename.endswith(suffix):
92-
removes.append(filename)
93-
shprint(sh.rm, '-f', *removes)
94-
95-
info('Deleting some other stuff not used on android')
96-
# To quote the original distribute.sh, 'well...'
97-
shprint(sh.rm, '-rf', 'lib2to3')
98-
shprint(sh.rm, '-rf', 'idlelib')
99-
for filename in glob.glob('config/libpython*.a'):
100-
shprint(sh.rm, '-f', filename)
101-
shprint(sh.rm, '-rf', 'config/python.o')
102-
103-
else: # Python *is* loaded from crystax
104-
ndk_dir = self.ctx.ndk_dir
105-
py_recipe = self.ctx.python_recipe
106-
python_dir = join(ndk_dir, 'sources', 'python',
107-
py_recipe.version, 'libs', arch.arch)
108-
shprint(sh.cp, '-r', join(python_dir,
109-
'stdlib.zip'), crystax_python_dir)
110-
shprint(sh.cp, '-r', join(python_dir,
111-
'modules'), crystax_python_dir)
112-
shprint(sh.cp, '-r', self.ctx.get_python_install_dir(),
113-
join(crystax_python_dir, 'site-packages'))
114-
115-
info('Renaming .so files to reflect cross-compile')
116-
site_packages_dir = join(crystax_python_dir, "site-packages")
117-
find_ret = shprint(
118-
sh.find, site_packages_dir, '-iname', '*.so')
119-
filenames = find_ret.stdout.decode('utf-8').split('\n')[:-1]
120-
for filename in filenames:
121-
parts = filename.split('.')
122-
if len(parts) <= 2:
123-
continue
124-
shprint(sh.mv, filename, filename.split('.')[0] + '.so')
125-
site_packages_dir = join(abspath(curdir),
126-
site_packages_dir)
52+
python_bundle_dir = join('_python_bundle', '_python_bundle')
53+
if 'python2' in self.ctx.recipe_build_order:
54+
# Python 2 is a special case with its own packaging location
55+
python_bundle_dir = 'private'
56+
ensure_dir(python_bundle_dir)
57+
58+
site_packages_dir = self.ctx.python_recipe.create_python_bundle(
59+
join(self.dist_dir, python_bundle_dir), arch)
60+
12761
if 'sqlite3' not in self.ctx.recipe_build_order:
12862
with open('blacklist.txt', 'a') as fileh:
12963
fileh.write('\nsqlite3/*\nlib-dynload/_sqlite3.so\n')

pythonforandroid/bootstraps/sdl2/build/build.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import os
99
import tarfile
1010
import time
11+
import json
1112
import subprocess
1213
import shutil
1314
from zipfile import ZipFile
@@ -125,7 +126,7 @@ def make_python_zip():
125126

126127
if not exists('private'):
127128
print('No compiled python is present to zip, skipping.')
128-
print('this should only be the case if you are using the CrystaX python')
129+
print('this should only be the case if you are using the CrystaX python or python3')
129130
return
130131

131132
global python_files
@@ -239,10 +240,9 @@ def make_package(args):
239240

240241
# Package up the private data (public not supported).
241242
tar_dirs = [args.private]
242-
if exists('private'):
243-
tar_dirs.append('private')
244-
if exists('crystax_python'):
245-
tar_dirs.append('crystax_python')
243+
for python_bundle_dir in ('private', 'crystax_python', '_python_bundle'):
244+
if exists(python_bundle_dir):
245+
tar_dirs.append(python_bundle_dir)
246246

247247
if args.private:
248248
make_tar('src/main/assets/private.mp3', tar_dirs, args.ignore_path)
@@ -409,7 +409,17 @@ def make_package(args):
409409

410410
def parse_args(args=None):
411411
global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON
412-
default_android_api = 12
412+
413+
# Get the default minsdk, equal to the NDK API that this dist is built against
414+
with open('dist_info.json', 'r') as fileh:
415+
info = json.load(fileh)
416+
if 'ndk_api' not in info:
417+
print('WARNING: Failed to read ndk_api from dist info, defaulting to 12')
418+
default_min_api = 12 # The old default before ndk_api was introduced
419+
else:
420+
default_min_api = info['ndk_api']
421+
ndk_api = info['ndk_api']
422+
413423
import argparse
414424
ap = argparse.ArgumentParser(description='''\
415425
Package a Python application for Android.
@@ -491,10 +501,13 @@ def parse_args(args=None):
491501
ap.add_argument('--sdk', dest='sdk_version', default=-1,
492502
type=int, help=('Deprecated argument, does nothing'))
493503
ap.add_argument('--minsdk', dest='min_sdk_version',
494-
default=default_android_api, type=int,
495-
help=('Minimum Android SDK version to use. Default to '
496-
'the value of ANDROIDAPI, or {} if not set'
497-
.format(default_android_api)))
504+
default=default_min_api, type=int,
505+
help=('Minimum Android SDK version that the app supports. '
506+
'Defaults to {}.'.format(default_min_api)))
507+
ap.add_argument('--allow-minsdk-ndkapi-mismatch', default=False,
508+
action='store_true',
509+
help=('Allow the --minsdk argument to be different from '
510+
'the discovered ndk_api in the dist'))
498511
ap.add_argument('--intent-filters', dest='intent_filters',
499512
help=('Add intent-filters xml rules to the '
500513
'AndroidManifest.xml file. The argument is a '
@@ -529,8 +542,18 @@ def parse_args(args=None):
529542
if args.name and args.name[0] == '"' and args.name[-1] == '"':
530543
args.name = args.name[1:-1]
531544

532-
# if args.sdk_version == -1:
533-
# args.sdk_version = args.min_sdk_version
545+
if ndk_api != args.min_sdk_version:
546+
print(('WARNING: --minsdk argument does not match the api that is '
547+
'compiled against. Only proceed if you know what you are '
548+
'doing, otherwise use --minsdk={} or recompile against api '
549+
'{}').format(ndk_api, args.min_sdk_version))
550+
if not args.allow_minsdk_ndkapi_mismatch:
551+
print('You must pass --allow-minsdk-ndkapi-mismatch to build '
552+
'with --minsdk different to the target NDK api from the '
553+
'build step')
554+
exit(1)
555+
else:
556+
print('Proceeding with --minsdk not matching build target api')
534557

535558
if args.sdk_version != -1:
536559
print('WARNING: Received a --sdk argument, but this argument is '

pythonforandroid/bootstraps/sdl2/build/jni/Application.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55

66
# APP_ABI := armeabi armeabi-v7a x86
77
APP_ABI := $(ARCH)
8+
APP_PLATFORM := $(NDK_API)

0 commit comments

Comments
 (0)