Skip to content

Commit 23bf987

Browse files
committed
Move the refactored part of hospython3 and python3 recipes into his own module
In order to be neatest as suggested by the creator of the python3's recipe @inclement ;)
1 parent 702f208 commit 23bf987

File tree

6 files changed

+369
-312
lines changed

6 files changed

+369
-312
lines changed

pythonforandroid/python.py

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
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

Comments
 (0)