|
| 1 | +''' |
| 2 | +This module is kind of special because it contains the base classes used to |
| 3 | +build our python3 and python2 recipes and his corresponding hostpython recipes. |
| 4 | +''' |
| 5 | + |
| 6 | +from os.path import dirname, exists, join |
| 7 | +from os import environ |
| 8 | +import glob |
| 9 | +import sh |
| 10 | + |
| 11 | +from pythonforandroid.recipe import Recipe, TargetPythonRecipe |
| 12 | +from pythonforandroid.logger import logger, info, error, shprint |
| 13 | +from pythonforandroid.util import ( |
| 14 | + current_directory, ensure_dir, walk_valid_filens) |
| 15 | + |
| 16 | + |
| 17 | +class HostPythonRecipe(Recipe): |
| 18 | + ''' |
| 19 | + This is the base class for hostpython3 and hostpython2 recipes. This class |
| 20 | + will take care to do all the work to build a hostpython recipe but, be |
| 21 | + careful, it is intended to be subclassed because some of the vars needs to |
| 22 | + be set: |
| 23 | +
|
| 24 | + - :attr:`name` |
| 25 | + - :attr:`version` |
| 26 | +
|
| 27 | + .. versionadded:: 0.6.0 |
| 28 | + Refactored from the hostpython3's recipe by inclement |
| 29 | + ''' |
| 30 | + |
| 31 | + name = '' |
| 32 | + '''The hostpython's recipe name. This should be ``hostpython2`` or |
| 33 | + ``hostpython3`` |
| 34 | +
|
| 35 | + .. warning:: This must be set in inherited class.''' |
| 36 | + |
| 37 | + version = '' |
| 38 | + '''The hostpython's recipe version. |
| 39 | +
|
| 40 | + .. warning:: This must be set in inherited class.''' |
| 41 | + |
| 42 | + build_subdir = 'native-build' |
| 43 | + '''Specify the sub build directory for the hostpython recipe. Defaults |
| 44 | + to ``native-build``.''' |
| 45 | + |
| 46 | + url = 'https://www.python.org/ftp/python/{version}/Python-{version}.tgz' |
| 47 | + '''The default url to download our host python recipe. This url will |
| 48 | + change depending on the python version set in attribute :attr:`version`.''' |
| 49 | + |
| 50 | + def get_build_container_dir(self, arch=None): |
| 51 | + choices = self.check_recipe_choices() |
| 52 | + dir_name = '-'.join([self.name] + choices) |
| 53 | + return join(self.ctx.build_dir, 'other_builds', dir_name, 'desktop') |
| 54 | + |
| 55 | + def get_build_dir(self, arch=None): |
| 56 | + ''' |
| 57 | + .. note:: Unlike other recipes, the hostpython build dir doesn't |
| 58 | + depend on the target arch |
| 59 | + ''' |
| 60 | + return join(self.get_build_container_dir(), self.name) |
| 61 | + |
| 62 | + def get_path_to_python(self): |
| 63 | + return join(self.get_build_dir(), self.build_subdir) |
| 64 | + |
| 65 | + def build_arch(self, arch): |
| 66 | + recipe_build_dir = self.get_build_dir(arch.arch) |
| 67 | + |
| 68 | + # Create a subdirectory to actually perform the build |
| 69 | + build_dir = join(recipe_build_dir, self.build_subdir) |
| 70 | + ensure_dir(build_dir) |
| 71 | + |
| 72 | + if not exists(join(build_dir, 'python')): |
| 73 | + with current_directory(recipe_build_dir): |
| 74 | + # Configure the build |
| 75 | + with current_directory(build_dir): |
| 76 | + if not exists('config.status'): |
| 77 | + shprint( |
| 78 | + sh.Command(join(recipe_build_dir, 'configure'))) |
| 79 | + |
| 80 | + # Create the Setup file. This copying from Setup.dist |
| 81 | + # seems to be the normal and expected procedure. |
| 82 | + shprint(sh.cp, join('Modules', 'Setup.dist'), |
| 83 | + join(build_dir, 'Modules', 'Setup')) |
| 84 | + |
| 85 | + result = shprint(sh.make, '-C', build_dir) |
| 86 | + else: |
| 87 | + info('Skipping {name} ({version}) build, as it has already ' |
| 88 | + 'been completed'.format(name=self.name, version=self.version)) |
| 89 | + |
| 90 | + self.ctx.hostpython = join(build_dir, 'python') |
| 91 | + |
| 92 | + |
| 93 | +class GuestPythonRecipe(TargetPythonRecipe): |
| 94 | + ''' |
| 95 | + Class for target python recipes. Sets ctx.python_recipe to point to itself, |
| 96 | + so as to know later what kind of Python was built or used. |
| 97 | +
|
| 98 | + This base class is used for our main python recipes (python2 and python3) |
| 99 | + which shares most of the build process. |
| 100 | +
|
| 101 | + .. versionadded:: 0.6.0 |
| 102 | + Refactored from the inclement's python3 recipe with a few changes: |
| 103 | +
|
| 104 | + - Splits the python's build process several methods: :meth:`build_arch` |
| 105 | + and :meth:`get_recipe_env`. |
| 106 | + - Adds the attribute :attr:`configure_args`, which has been moved from |
| 107 | + the method :meth:`build_arch` into a static class variable. |
| 108 | + - Adds some static class variables used to create the python bundle and |
| 109 | + modifies the method :meth:`create_python_bundle`, to adapt to the new |
| 110 | + situation. The added static class variables are: |
| 111 | + :attr:`stdlib_dir_blacklist`, :attr:`stdlib_filen_blacklist`, |
| 112 | + :attr:`site_packages_dir_blacklist`and |
| 113 | + :attr:`site_packages_filen_blacklist`. |
| 114 | + ''' |
| 115 | + |
| 116 | + MIN_NDK_API = 21 |
| 117 | + '''Sets the minimal ndk api number needed to use the recipe. |
| 118 | +
|
| 119 | + .. warning:: This recipe can be built only against API 21+, so it means |
| 120 | + that any class which inherits from class:`GuestPythonRecipe` will have |
| 121 | + this limitation. |
| 122 | + ''' |
| 123 | + |
| 124 | + from_crystax = False |
| 125 | + '''True if the python is used from CrystaX, False otherwise (i.e. if |
| 126 | + it is built by p4a).''' |
| 127 | + |
| 128 | + configure_args = () |
| 129 | + '''The configure arguments needed to build the python recipe. Those are |
| 130 | + used in method :meth:`build_arch` (if not overwritten like python3crystax's |
| 131 | + recipe does). |
| 132 | +
|
| 133 | + .. note:: This variable should be properly set in subclass. |
| 134 | + ''' |
| 135 | + |
| 136 | + stdlib_dir_blacklist = { |
| 137 | + '__pycache__', |
| 138 | + 'test', |
| 139 | + 'tests', |
| 140 | + 'lib2to3', |
| 141 | + 'ensurepip', |
| 142 | + 'idlelib', |
| 143 | + 'tkinter', |
| 144 | + } |
| 145 | + '''The directories that we want to omit for our python bundle''' |
| 146 | + |
| 147 | + stdlib_filen_blacklist = [ |
| 148 | + '*.pyc', |
| 149 | + '*.exe', |
| 150 | + '*.whl', |
| 151 | + ] |
| 152 | + '''The file extensions that we want to blacklist for our python bundle''' |
| 153 | + |
| 154 | + site_packages_dir_blacklist = { |
| 155 | + '__pycache__', |
| 156 | + 'tests' |
| 157 | + } |
| 158 | + '''The directories from site packages dir that we don't want to be included |
| 159 | + in our python bundle.''' |
| 160 | + |
| 161 | + site_packages_filen_blacklist = [] |
| 162 | + '''The file extensions from site packages dir that we don't want to be |
| 163 | + included in our python bundle.''' |
| 164 | + |
| 165 | + def __init__(self, *args, **kwargs): |
| 166 | + self._ctx = None |
| 167 | + super(GuestPythonRecipe, self).__init__(*args, **kwargs) |
| 168 | + |
| 169 | + def get_recipe_env(self, arch=None, with_flags_in_cc=True): |
| 170 | + if self.from_crystax: |
| 171 | + return \ |
| 172 | + super(GuestPythonRecipe, self).get_recipe_env( |
| 173 | + arch=arch, with_flags_in_cc=with_flags_in_cc) |
| 174 | + |
| 175 | + env = environ.copy() |
| 176 | + platform_name = 'android-{}'.format(self.ctx.ndk_api) |
| 177 | + |
| 178 | + # TODO: Get this information from p4a's arch system |
| 179 | + android_host = env['HOSTARCH'] = 'arm-linux-androideabi' |
| 180 | + |
| 181 | + toolchain = '{android_host}-4.9'.format(android_host=android_host) |
| 182 | + toolchain = join(self.ctx.ndk_dir, 'toolchains', |
| 183 | + toolchain, 'prebuilt', 'linux-x86_64') |
| 184 | + |
| 185 | + env['CC'] = \ |
| 186 | + '{clang} -target {target} -gcc-toolchain {toolchain}'.format( |
| 187 | + clang=join(self.ctx.ndk_dir, 'toolchains', 'llvm', 'prebuilt', |
| 188 | + 'linux-x86_64', 'bin', 'clang'), |
| 189 | + target='armv7-none-linux-androideabi', |
| 190 | + toolchain=toolchain) |
| 191 | + env['AR'] = join(toolchain, 'bin', android_host) + '-ar' |
| 192 | + env['LD'] = join(toolchain, 'bin', android_host) + '-ld' |
| 193 | + env['RANLIB'] = join(toolchain, 'bin', android_host) + '-ranlib' |
| 194 | + env['READELF'] = join(toolchain, 'bin', android_host) + '-readelf' |
| 195 | + env['STRIP'] = \ |
| 196 | + join(toolchain, 'bin', android_host) + \ |
| 197 | + '-strip --strip-debug --strip-unneeded' |
| 198 | + |
| 199 | + env['PATH'] = \ |
| 200 | + '{hostpython_dir}:{old_path}'.format( |
| 201 | + hostpython_dir=self.get_recipe( |
| 202 | + 'host' + self.name, self.ctx).get_path_to_python(), |
| 203 | + old_path=env['PATH']) |
| 204 | + |
| 205 | + ndk_flags = ( |
| 206 | + '--sysroot={ndk_sysroot} -D__ANDROID_API__={android_api} ' |
| 207 | + '-isystem {ndk_android_host}').format( |
| 208 | + ndk_sysroot=join(self.ctx.ndk_dir, 'sysroot'), |
| 209 | + android_api=self.ctx.ndk_api, |
| 210 | + ndk_android_host=join( |
| 211 | + self.ctx.ndk_dir, 'sysroot', 'usr', 'include', |
| 212 | + android_host)) |
| 213 | + sysroot = join(self.ctx.ndk_dir, 'platforms', |
| 214 | + platform_name, 'arch-arm') |
| 215 | + env['CFLAGS'] = env.get('CFLAGS', '') + ' ' + ndk_flags |
| 216 | + env['CPPFLAGS'] = env.get('CPPFLAGS', '') + ' ' + ndk_flags |
| 217 | + env['LDFLAGS'] = env.get('LDFLAGS', '') + ' --sysroot={} -L{}'.format( |
| 218 | + sysroot, join(sysroot, 'usr', 'lib')) |
| 219 | + |
| 220 | + # Manually add the libs directory, and copy some object |
| 221 | + # files to the current directory otherwise they aren't |
| 222 | + # picked up. This seems necessary because the --sysroot |
| 223 | + # setting in LDFLAGS is overridden by the other flags. |
| 224 | + # TODO: Work out why this doesn't happen in the original |
| 225 | + # bpo-30386 Makefile system. |
| 226 | + logger.warning('Doing some hacky stuff to link properly') |
| 227 | + lib_dir = join(sysroot, 'usr', 'lib') |
| 228 | + env['LDFLAGS'] += ' -L{}'.format(lib_dir) |
| 229 | + shprint(sh.cp, join(lib_dir, 'crtbegin_so.o'), './') |
| 230 | + shprint(sh.cp, join(lib_dir, 'crtend_so.o'), './') |
| 231 | + |
| 232 | + env['SYSROOT'] = sysroot |
| 233 | + |
| 234 | + return env |
| 235 | + |
| 236 | + def prebuild_arch(self, arch): |
| 237 | + super(TargetPythonRecipe, self).prebuild_arch(arch) |
| 238 | + if self.from_crystax and self.ctx.ndk != 'crystax': |
| 239 | + error('The {} recipe can only be built when ' |
| 240 | + 'using the CrystaX NDK. Exiting.'.format(self.name)) |
| 241 | + exit(1) |
| 242 | + self.ctx.python_recipe = self |
| 243 | + |
| 244 | + def include_root(self, arch_name): |
| 245 | + return join(self.get_build_dir(arch_name), 'Include') |
| 246 | + |
| 247 | + def link_root(self, arch_name): |
| 248 | + return join(self.get_build_dir(arch_name), 'android-build') |
| 249 | + |
| 250 | + def create_python_bundle(self, dirn, arch): |
| 251 | + """ |
| 252 | + Create a packaged python bundle in the target directory, by |
| 253 | + copying all the modules and standard library to the right |
| 254 | + place. |
| 255 | + """ |
| 256 | + # Bundle compiled python modules to a folder |
| 257 | + modules_dir = join(dirn, 'modules') |
| 258 | + ensure_dir(modules_dir) |
| 259 | + # Todo: find a better way to find the build libs folder |
| 260 | + modules_build_dir = join( |
| 261 | + self.get_build_dir(arch.arch), |
| 262 | + 'android-build', |
| 263 | + 'build', |
| 264 | + 'lib.linux{}-arm-{}'.format( |
| 265 | + '2' if self.version[0] == '2' else '', |
| 266 | + self.major_minor_version_string |
| 267 | + )) |
| 268 | + module_filens = (glob.glob(join(modules_build_dir, '*.so')) + |
| 269 | + glob.glob(join(modules_build_dir, '*.py'))) |
| 270 | + for filen in module_filens: |
| 271 | + shprint(sh.cp, filen, modules_dir) |
| 272 | + |
| 273 | + # zip up the standard library |
| 274 | + stdlib_zip = join(dirn, 'stdlib.zip') |
| 275 | + with current_directory(join(self.get_build_dir(arch.arch), 'Lib')): |
| 276 | + stdlib_filens = walk_valid_filens( |
| 277 | + '.', self.stdlib_dir_blacklist, self.stdlib_filen_blacklist) |
| 278 | + shprint(sh.zip, stdlib_zip, *stdlib_filens) |
| 279 | + |
| 280 | + # copy the site-packages into place |
| 281 | + ensure_dir(join(dirn, 'site-packages')) |
| 282 | + # TODO: Improve the API around walking and copying the files |
| 283 | + with current_directory(self.ctx.get_python_install_dir()): |
| 284 | + filens = list(walk_valid_filens( |
| 285 | + '.', self.site_packages_dir_blacklist, |
| 286 | + self.site_packages_filen_blacklist)) |
| 287 | + for filen in filens: |
| 288 | + ensure_dir(join(dirn, 'site-packages', dirname(filen))) |
| 289 | + sh.cp(filen, join(dirn, 'site-packages', filen)) |
| 290 | + |
| 291 | + # copy the python .so files into place |
| 292 | + python_build_dir = join(self.get_build_dir(arch.arch), |
| 293 | + 'android-build') |
| 294 | + python_lib_name = 'libpython' + self.major_minor_version_string |
| 295 | + if self.major_minor_version_string[0] == '3': |
| 296 | + python_lib_name += 'm' |
| 297 | + for lib in [python_lib_name + '.so', python_lib_name + '.so.1.0']: |
| 298 | + shprint(sh.cp, join(python_build_dir, lib), |
| 299 | + 'libs/{}'.format(arch.arch)) |
| 300 | + |
| 301 | + info('Renaming .so files to reflect cross-compile') |
| 302 | + self.reduce_object_file_names(join(dirn, 'site-packages')) |
| 303 | + |
| 304 | + return join(dirn, 'site-packages') |
| 305 | + |
| 306 | + def build_arch(self, arch): |
| 307 | + if self.ctx.ndk_api < self.MIN_NDK_API: |
| 308 | + error('Target ndk-api is {}, but the {} recipe supports only {}+'.format( |
| 309 | + self.ctx.ndk_api, self.name, self.MIN_NDK_API)) |
| 310 | + exit(1) |
| 311 | + |
| 312 | + recipe_build_dir = self.get_build_dir(arch.arch) |
| 313 | + |
| 314 | + # Create a subdirectory to actually perform the build |
| 315 | + build_dir = join(recipe_build_dir, 'android-build') |
| 316 | + ensure_dir(build_dir) |
| 317 | + |
| 318 | + # TODO: Get these dynamically, like bpo-30386 does |
| 319 | + sys_prefix = '/usr/local' |
| 320 | + sys_exec_prefix = '/usr/local' |
| 321 | + |
| 322 | + with current_directory(build_dir): |
| 323 | + env = self.get_recipe_env(arch) |
| 324 | + |
| 325 | + android_build = sh.Command( |
| 326 | + join(recipe_build_dir, |
| 327 | + 'config.guess'))().stdout.strip().decode('utf-8') |
| 328 | + |
| 329 | + if not exists('config.status'): |
| 330 | + shprint( |
| 331 | + sh.Command(join(recipe_build_dir, 'configure')), |
| 332 | + *(' '.join(self.configure_args).format( |
| 333 | + android_host=env['HOSTARCH'], |
| 334 | + android_build=android_build, |
| 335 | + prefix=sys_prefix, |
| 336 | + exec_prefix=sys_exec_prefix)).split(' '), |
| 337 | + _env=env) |
| 338 | + |
| 339 | + if not exists('python'): |
| 340 | + shprint(sh.make, 'all', _env=env) |
| 341 | + |
| 342 | + # TODO: Look into passing the path to pyconfig.h in a |
| 343 | + # better way, although this is probably acceptable |
| 344 | + sh.cp('pyconfig.h', join(recipe_build_dir, 'Include')) |
0 commit comments