17
17
except ImportError :
18
18
from urllib .parse import urlparse
19
19
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 )
21
22
22
23
# this import is necessary to keep imp.load_source from complaining :)
23
24
if PY2 :
@@ -1037,17 +1038,147 @@ def get_recipe_env(self, arch, with_flags_in_cc=True):
1037
1038
1038
1039
1039
1040
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
+ '''
1042
1069
1043
1070
from_crystax = False
1044
1071
'''True if the python is used from CrystaX, False otherwise (i.e. if
1045
1072
it is built by p4a).'''
1046
1073
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
+
1047
1111
def __init__ (self , * args , ** kwargs ):
1048
1112
self ._ctx = None
1049
1113
super (TargetPythonRecipe , self ).__init__ (* args , ** kwargs )
1050
1114
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
+
1051
1182
def prebuild_arch (self , arch ):
1052
1183
super (TargetPythonRecipe , self ).prebuild_arch (arch )
1053
1184
if self .from_crystax and self .ctx .ndk != 'crystax' :
@@ -1056,12 +1187,14 @@ def prebuild_arch(self, arch):
1056
1187
exit (1 )
1057
1188
self .ctx .python_recipe = self
1058
1189
1059
- def include_root (self , arch ):
1190
+ def include_root (self , arch_name ):
1060
1191
'''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' )
1062
1194
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' )
1065
1198
1066
1199
@property
1067
1200
def major_minor_version_string (self ):
@@ -1074,7 +1207,55 @@ def create_python_bundle(self, dirn, arch):
1074
1207
copying all the modules and standard library to the right
1075
1208
place.
1076
1209
"""
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' )
1078
1259
1079
1260
def reduce_object_file_names (self , dirn ):
1080
1261
"""Recursively renames all files named XXX.cpython-...-linux-gnu.so"
@@ -1090,6 +1271,41 @@ def reduce_object_file_names(self, dirn):
1090
1271
continue
1091
1272
shprint (sh .mv , filen , join (file_dirname , parts [0 ] + '.so' ))
1092
1273
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
+
1093
1309
1094
1310
class TargetHostPythonRecipe (Recipe ):
1095
1311
'''
0 commit comments