Skip to content

Commit dd4025d

Browse files
committed
Refactor python3's recipe into TargetPythonRecipe
1 parent 6246b48 commit dd4025d

File tree

2 files changed

+244
-195
lines changed

2 files changed

+244
-195
lines changed

pythonforandroid/recipe.py

Lines changed: 224 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
except ImportError:
1818
from urllib.parse import urlparse
1919
from pythonforandroid.logger import (logger, info, warning, error, debug, shprint, info_main)
20-
from pythonforandroid.util import (urlretrieve, current_directory, ensure_dir)
20+
from pythonforandroid.util import (
21+
urlretrieve, current_directory, ensure_dir, walk_valid_filens)
2122

2223
# this import is necessary to keep imp.load_source from complaining :)
2324
if PY2:
@@ -1037,17 +1038,147 @@ def get_recipe_env(self, arch, with_flags_in_cc=True):
10371038

10381039

10391040
class TargetPythonRecipe(Recipe):
1040-
'''Class for target python recipes. Sets ctx.python_recipe to point to
1041-
itself, so as to know later what kind of Python was built or used.'''
1041+
'''
1042+
Class for target python recipes. Sets ctx.python_recipe to point to itself,
1043+
so as to know later what kind of Python was built or used.
1044+
1045+
This base class is used for our main python recipes (python2 and python3)
1046+
which shares most of the build process.
1047+
1048+
.. note:: The recipe for python3Crystax also inherits from this base class,
1049+
but overwrites most of the code used here because has some different
1050+
build process.
1051+
1052+
.. versionchanged:: 0.6.0
1053+
1054+
- Adds the python's build process, which has been taken from the
1055+
python3's recipe by `inclement` and refactored here, split
1056+
into several methods: :meth:`build_arch` and :meth:`get_recipe_env`.
1057+
- Adds the attribute :attr:`configure_args`, which has been moved from
1058+
the original python3's recipe method :meth:`build_arch` into a static
1059+
class variable.
1060+
- Changes the return value for methods: :meth:`include_root` and
1061+
:meth:`link_root`.
1062+
- Adds some static class variables used to create the python bundle and
1063+
modifies the method :meth:`create_python_bundle`, refactored from the
1064+
python3's recipe by inclement. The added variables, also taken from
1065+
the mentioned recipe, are: :attr:`stdlib_dir_blacklist`,
1066+
:attr:`stdlib_filen_blacklist`, :attr:`site_packages_dir_blacklist`
1067+
and :attr:`site_packages_filen_blacklist`.
1068+
'''
10421069

10431070
from_crystax = False
10441071
'''True if the python is used from CrystaX, False otherwise (i.e. if
10451072
it is built by p4a).'''
10461073

1074+
configure_args = ()
1075+
'''The configure arguments needed to build the python recipe. Those are
1076+
used in method :meth:`build_arch` (if not overwritten like python3crystax's
1077+
recipe does).
1078+
1079+
.. note:: This variable should be properly set in subclass.
1080+
'''
1081+
1082+
stdlib_dir_blacklist = {
1083+
'__pycache__',
1084+
'test',
1085+
'tests',
1086+
'lib2to3',
1087+
'ensurepip',
1088+
'idlelib',
1089+
'tkinter',
1090+
}
1091+
'''The directories that we want to omit for our python bundle'''
1092+
1093+
stdlib_filen_blacklist = [
1094+
'*.pyc',
1095+
'*.exe',
1096+
'*.whl',
1097+
]
1098+
'''The file extensions that we want to blacklist for our python bundle'''
1099+
1100+
site_packages_dir_blacklist = {
1101+
'__pycache__',
1102+
'tests'
1103+
}
1104+
'''The directories from site packages dir that we don't want to be included
1105+
in our python bundle.'''
1106+
1107+
site_packages_filen_blacklist = []
1108+
'''The file extensions from site packages dir that we don't want to be
1109+
included in our python bundle.'''
1110+
10471111
def __init__(self, *args, **kwargs):
10481112
self._ctx = None
10491113
super(TargetPythonRecipe, self).__init__(*args, **kwargs)
10501114

1115+
def get_recipe_env(self, arch=None, with_flags_in_cc=True):
1116+
if self.from_crystax:
1117+
return \
1118+
super(TargetPythonRecipe, self).get_recipe_env(
1119+
arch=arch, with_flags_in_cc=with_flags_in_cc)
1120+
1121+
env = environ.copy()
1122+
platform_name = 'android-{}'.format(self.ctx.ndk_api)
1123+
1124+
# TODO: Get this information from p4a's arch system
1125+
android_host = env['HOSTARCH'] = 'arm-linux-androideabi'
1126+
1127+
toolchain = '{android_host}-4.9'.format(android_host=android_host)
1128+
toolchain = join(self.ctx.ndk_dir, 'toolchains',
1129+
toolchain, 'prebuilt', 'linux-x86_64')
1130+
1131+
env['CC'] = \
1132+
'{clang} -target {target} -gcc-toolchain {toolchain}'.format(
1133+
clang=join(self.ctx.ndk_dir, 'toolchains', 'llvm', 'prebuilt',
1134+
'linux-x86_64', 'bin', 'clang'),
1135+
target='armv7-none-linux-androideabi',
1136+
toolchain=toolchain)
1137+
env['AR'] = join(toolchain, 'bin', android_host) + '-ar'
1138+
env['LD'] = join(toolchain, 'bin', android_host) + '-ld'
1139+
env['RANLIB'] = join(toolchain, 'bin', android_host) + '-ranlib'
1140+
env['READELF'] = join(toolchain, 'bin', android_host) + '-readelf'
1141+
env['STRIP'] = \
1142+
join(toolchain, 'bin', android_host) + \
1143+
'-strip --strip-debug --strip-unneeded'
1144+
1145+
env['PATH'] = \
1146+
'{hostpython_dir}:{old_path}'.format(
1147+
hostpython_dir=self.get_recipe(
1148+
'host' + self.name, self.ctx).get_path_to_python(),
1149+
old_path=env['PATH'])
1150+
1151+
ndk_flags = (
1152+
'--sysroot={ndk_sysroot} -D__ANDROID_API__={android_api} '
1153+
'-isystem {ndk_android_host}').format(
1154+
ndk_sysroot=join(self.ctx.ndk_dir, 'sysroot'),
1155+
android_api=self.ctx.ndk_api,
1156+
ndk_android_host=join(
1157+
self.ctx.ndk_dir, 'sysroot', 'usr', 'include',
1158+
android_host))
1159+
sysroot = join(self.ctx.ndk_dir, 'platforms',
1160+
platform_name, 'arch-arm')
1161+
env['CFLAGS'] = env.get('CFLAGS', '') + ' ' + ndk_flags
1162+
env['CPPFLAGS'] = env.get('CPPFLAGS', '') + ' ' + ndk_flags
1163+
env['LDFLAGS'] = env.get('LDFLAGS', '') + ' --sysroot={} -L{}'.format(
1164+
sysroot, join(sysroot, 'usr', 'lib'))
1165+
1166+
# Manually add the libs directory, and copy some object
1167+
# files to the current directory otherwise they aren't
1168+
# picked up. This seems necessary because the --sysroot
1169+
# setting in LDFLAGS is overridden by the other flags.
1170+
# TODO: Work out why this doesn't happen in the original
1171+
# bpo-30386 Makefile system.
1172+
logger.warning('Doing some hacky stuff to link properly')
1173+
lib_dir = join(sysroot, 'usr', 'lib')
1174+
env['LDFLAGS'] += ' -L{}'.format(lib_dir)
1175+
shprint(sh.cp, join(lib_dir, 'crtbegin_so.o'), './')
1176+
shprint(sh.cp, join(lib_dir, 'crtend_so.o'), './')
1177+
1178+
env['SYSROOT'] = sysroot
1179+
1180+
return env
1181+
10511182
def prebuild_arch(self, arch):
10521183
super(TargetPythonRecipe, self).prebuild_arch(arch)
10531184
if self.from_crystax and self.ctx.ndk != 'crystax':
@@ -1056,12 +1187,14 @@ def prebuild_arch(self, arch):
10561187
exit(1)
10571188
self.ctx.python_recipe = self
10581189

1059-
def include_root(self, arch):
1190+
def include_root(self, arch_name):
10601191
'''The root directory from which to include headers.'''
1061-
raise NotImplementedError('Not implemented in TargetPythonRecipe')
1192+
return join(self.get_build_dir(arch_name),
1193+
'Include')
10621194

1063-
def link_root(self):
1064-
raise NotImplementedError('Not implemented in TargetPythonRecipe')
1195+
def link_root(self, arch_name):
1196+
return join(self.get_build_dir(arch_name),
1197+
'android-build')
10651198

10661199
@property
10671200
def major_minor_version_string(self):
@@ -1074,7 +1207,55 @@ def create_python_bundle(self, dirn, arch):
10741207
copying all the modules and standard library to the right
10751208
place.
10761209
"""
1077-
raise NotImplementedError('{} does not implement create_python_bundle'.format(self))
1210+
# Bundle compiled python modules to a folder
1211+
modules_dir = join(dirn, 'modules')
1212+
ensure_dir(modules_dir)
1213+
# Todo: find a better way to find the build libs folder
1214+
modules_build_dir = join(
1215+
self.get_build_dir(arch.arch),
1216+
'android-build',
1217+
'build',
1218+
'lib.linux{}-arm-{}'.format(
1219+
'2' if self.version[0] == '2' else '',
1220+
self.major_minor_version_string
1221+
))
1222+
module_filens = (glob.glob(join(modules_build_dir, '*.so')) +
1223+
glob.glob(join(modules_build_dir, '*.py')))
1224+
for filen in module_filens:
1225+
shprint(sh.cp, filen, modules_dir)
1226+
1227+
# zip up the standard library
1228+
stdlib_zip = join(dirn, 'stdlib.zip')
1229+
with current_directory(join(self.get_build_dir(arch.arch), 'Lib')):
1230+
stdlib_filens = walk_valid_filens(
1231+
'.', self.stdlib_dir_blacklist, self.stdlib_filen_blacklist)
1232+
shprint(sh.zip, stdlib_zip, *stdlib_filens)
1233+
1234+
# copy the site-packages into place
1235+
ensure_dir(join(dirn, 'site-packages'))
1236+
# TODO: Improve the API around walking and copying the files
1237+
with current_directory(self.ctx.get_python_install_dir()):
1238+
filens = list(walk_valid_filens(
1239+
'.', self.site_packages_dir_blacklist,
1240+
self.site_packages_filen_blacklist))
1241+
for filen in filens:
1242+
ensure_dir(join(dirn, 'site-packages', dirname(filen)))
1243+
sh.cp(filen, join(dirn, 'site-packages', filen))
1244+
1245+
# copy the python .so files into place
1246+
python_build_dir = join(self.get_build_dir(arch.arch),
1247+
'android-build')
1248+
python_lib_name = 'libpython' + self.major_minor_version_string
1249+
if self.major_minor_version_string[0] == '3':
1250+
python_lib_name += 'm'
1251+
for lib in [python_lib_name + '.so', python_lib_name + '.so.1.0']:
1252+
shprint(sh.cp, join(python_build_dir, lib),
1253+
'libs/{}'.format(arch.arch))
1254+
1255+
info('Renaming .so files to reflect cross-compile')
1256+
self.reduce_object_file_names(join(dirn, 'site-packages'))
1257+
1258+
return join(dirn, 'site-packages')
10781259

10791260
def reduce_object_file_names(self, dirn):
10801261
"""Recursively renames all files named XXX.cpython-...-linux-gnu.so"
@@ -1090,6 +1271,41 @@ def reduce_object_file_names(self, dirn):
10901271
continue
10911272
shprint(sh.mv, filen, join(file_dirname, parts[0] + '.so'))
10921273

1274+
def build_arch(self, arch):
1275+
recipe_build_dir = self.get_build_dir(arch.arch)
1276+
1277+
# Create a subdirectory to actually perform the build
1278+
build_dir = join(recipe_build_dir, 'android-build')
1279+
ensure_dir(build_dir)
1280+
1281+
# TODO: Get these dynamically, like bpo-30386 does
1282+
sys_prefix = '/usr/local'
1283+
sys_exec_prefix = '/usr/local'
1284+
1285+
with current_directory(build_dir):
1286+
env = self.get_recipe_env(arch)
1287+
1288+
android_build = sh.Command(
1289+
join(recipe_build_dir,
1290+
'config.guess'))().stdout.strip().decode('utf-8')
1291+
1292+
if not exists('config.status'):
1293+
shprint(
1294+
sh.Command(join(recipe_build_dir, 'configure')),
1295+
*(' '.join(self.configure_args).format(
1296+
android_host=env['HOSTARCH'],
1297+
android_build=android_build,
1298+
prefix=sys_prefix,
1299+
exec_prefix=sys_exec_prefix)).split(' '),
1300+
_env=env)
1301+
1302+
if not exists('python'):
1303+
shprint(sh.make, 'all', _env=env)
1304+
1305+
# TODO: Look into passing the path to pyconfig.h in a
1306+
# better way, although this is probably acceptable
1307+
sh.cp('pyconfig.h', join(recipe_build_dir, 'Include'))
1308+
10931309

10941310
class TargetHostPythonRecipe(Recipe):
10951311
'''

0 commit comments

Comments
 (0)