forked from kivy/python-for-android
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbootstrap.py
More file actions
390 lines (342 loc) · 15.5 KB
/
bootstrap.py
File metadata and controls
390 lines (342 loc) · 15.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
from os.path import (join, dirname, isdir, normpath, splitext, basename)
from os import listdir, walk, sep
import sh
import glob
import importlib
import os
import shutil
from pythonforandroid.logger import (warning, shprint, info, logger,
debug)
from pythonforandroid.util import (current_directory, ensure_dir,
temp_directory, which)
from pythonforandroid.recipe import Recipe
def copy_files(src_root, dest_root, override=True):
for root, dirnames, filenames in walk(src_root):
for filename in filenames:
subdir = normpath(root.replace(src_root, ""))
if subdir.startswith(sep): # ensure it is relative
subdir = subdir[1:]
dest_dir = join(dest_root, subdir)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
src_file = join(root, filename)
dest_file = join(dest_dir, filename)
if os.path.isfile(src_file):
if override and os.path.exists(dest_file):
os.unlink(dest_file)
if not os.path.exists(dest_file):
shutil.copy(src_file, dest_file)
else:
os.makedirs(dest_file)
class Bootstrap(object):
'''An Android project template, containing recipe stuff for
compilation and templated fields for APK info.
.. versionchanged:: 0.6.0
Adds attribute :attr:`libraries_to_load` and method
:meth:`collect_libraries_to_load`.
'''
name = ''
jni_subdir = '/jni'
ctx = None
bootstrap_dir = None
build_dir = None
dist_dir = None
dist_name = None
distribution = None
recipe_depends = ['sdl2']
can_be_chosen_automatically = True
'''Determines whether the bootstrap can be chosen as one that
satisfies user requirements. If False, it will not be returned
from Bootstrap.get_bootstrap_from_recipes.
'''
libraries_to_load = []
'''The list of libraries that should be loaded by android on python's app
initialization. This list will be dynamically created at
:meth:`collect_libraries_to_load` depending on the recipes build order.'''
# Other things a Bootstrap might need to track (maybe separately):
# ndk_main.c
# whitelist.txt
# blacklist.txt
@property
def dist_dir(self):
'''The dist dir at which to place the finished distribution.'''
if self.distribution is None:
warning('Tried to access {}.dist_dir, but {}.distribution '
'is None'.format(self, self))
exit(1)
return self.distribution.dist_dir
@property
def jni_dir(self):
return self.name + self.jni_subdir
def check_recipe_choices(self):
'''Checks what recipes are being built to see which of the alternative
and optional dependencies are being used,
and returns a list of these.'''
recipes = []
built_recipes = self.ctx.recipe_build_order
for recipe in self.recipe_depends:
if isinstance(recipe, (tuple, list)):
for alternative in recipe:
if alternative in built_recipes:
recipes.append(alternative)
break
return sorted(recipes)
def get_build_dir_name(self):
choices = self.check_recipe_choices()
dir_name = '-'.join([self.name] + choices)
return dir_name
def get_build_dir(self):
return join(self.ctx.build_dir, 'bootstrap_builds', self.get_build_dir_name())
def get_dist_dir(self, name):
return join(self.ctx.dist_dir, name)
def get_common_dir(self):
return os.path.abspath(join(self.bootstrap_dir, "..", 'common'))
@property
def name(self):
modname = self.__class__.__module__
return modname.split(".", 2)[-1]
def prepare_build_dir(self):
'''Ensure that a build dir exists for the recipe. This same single
dir will be used for building all different archs.'''
self.build_dir = self.get_build_dir()
self.common_dir = self.get_common_dir()
copy_files(join(self.bootstrap_dir, 'build'), self.build_dir)
copy_files(join(self.common_dir, 'build'), self.build_dir,
override=False)
if self.ctx.symlink_java_src:
info('Symlinking java src instead of copying')
shprint(sh.rm, '-r', join(self.build_dir, 'src'))
shprint(sh.mkdir, join(self.build_dir, 'src'))
for dirn in listdir(join(self.bootstrap_dir, 'build', 'src')):
shprint(sh.ln, '-s', join(self.bootstrap_dir, 'build', 'src', dirn),
join(self.build_dir, 'src'))
with current_directory(self.build_dir):
with open('project.properties', 'w') as fileh:
fileh.write('target=android-{}'.format(self.ctx.android_api))
def prepare_dist_dir(self, name):
ensure_dir(self.dist_dir)
def run_distribute(self):
self.distribution.save_info(self.dist_dir)
@classmethod
def list_bootstraps(cls):
'''Find all the available bootstraps and return them.'''
forbidden_dirs = ('__pycache__', 'common')
bootstraps_dir = join(dirname(__file__), 'bootstraps')
for name in listdir(bootstraps_dir):
if name in forbidden_dirs:
continue
filen = join(bootstraps_dir, name)
if isdir(filen):
yield name
@classmethod
def get_bootstrap_from_recipes(cls, recipes, ctx):
'''Returns a bootstrap whose recipe requirements do not conflict with
the given recipes.'''
info('Trying to find a bootstrap that matches the given recipes.')
bootstraps = [cls.get_bootstrap(name, ctx)
for name in cls.list_bootstraps()]
acceptable_bootstraps = []
for bs in bootstraps:
if not bs.can_be_chosen_automatically:
continue
possible_dependency_lists = expand_dependencies(bs.recipe_depends)
for possible_dependencies in possible_dependency_lists:
ok = True
for recipe in possible_dependencies:
recipe = Recipe.get_recipe(recipe, ctx)
if any([conflict in recipes for conflict in recipe.conflicts]):
ok = False
break
for recipe in recipes:
try:
recipe = Recipe.get_recipe(recipe, ctx)
except IOError:
conflicts = []
else:
conflicts = recipe.conflicts
if any([conflict in possible_dependencies
for conflict in conflicts]):
ok = False
break
if ok:
acceptable_bootstraps.append(bs)
info('Found {} acceptable bootstraps: {}'.format(
len(acceptable_bootstraps),
[bs.name for bs in acceptable_bootstraps]))
if acceptable_bootstraps:
info('Using the first of these: {}'
.format(acceptable_bootstraps[0].name))
return acceptable_bootstraps[0]
return None
@classmethod
def get_bootstrap(cls, name, ctx):
'''Returns an instance of a bootstrap with the given name.
This is the only way you should access a bootstrap class, as
it sets the bootstrap directory correctly.
'''
if name is None:
return None
if not hasattr(cls, 'bootstraps'):
cls.bootstraps = {}
if name in cls.bootstraps:
return cls.bootstraps[name]
mod = importlib.import_module('pythonforandroid.bootstraps.{}'
.format(name))
if len(logger.handlers) > 1:
logger.removeHandler(logger.handlers[1])
bootstrap = mod.bootstrap
bootstrap.bootstrap_dir = join(ctx.root_dir, 'bootstraps', name)
bootstrap.ctx = ctx
return bootstrap
def distribute_libs(self, arch, src_dirs, wildcard='*', dest_dir="libs"):
'''Copy existing arch libs from build dirs to current dist dir.'''
info('Copying libs')
tgt_dir = join(dest_dir, arch.arch)
ensure_dir(tgt_dir)
for src_dir in src_dirs:
for lib in glob.glob(join(src_dir, wildcard)):
shprint(sh.cp, '-a', lib, tgt_dir)
def distribute_javaclasses(self, javaclass_dir, dest_dir="src"):
'''Copy existing javaclasses from build dir to current dist dir.'''
info('Copying java files')
ensure_dir(dest_dir)
for filename in glob.glob(javaclass_dir):
shprint(sh.cp, '-a', filename, dest_dir)
def distribute_aars(self, arch):
'''Process existing .aar bundles and copy to current dist dir.'''
info('Unpacking aars')
for aar in glob.glob(join(self.ctx.aars_dir, '*.aar')):
self._unpack_aar(aar, arch)
def _unpack_aar(self, aar, arch):
'''Unpack content of .aar bundle and copy to current dist dir.'''
with temp_directory() as temp_dir:
name = splitext(basename(aar))[0]
jar_name = name + '.jar'
info("unpack {} aar".format(name))
debug(" from {}".format(aar))
debug(" to {}".format(temp_dir))
shprint(sh.unzip, '-o', aar, '-d', temp_dir)
jar_src = join(temp_dir, 'classes.jar')
jar_tgt = join('libs', jar_name)
debug("copy {} jar".format(name))
debug(" from {}".format(jar_src))
debug(" to {}".format(jar_tgt))
ensure_dir('libs')
shprint(sh.cp, '-a', jar_src, jar_tgt)
so_src_dir = join(temp_dir, 'jni', arch.arch)
so_tgt_dir = join('libs', arch.arch)
debug("copy {} .so".format(name))
debug(" from {}".format(so_src_dir))
debug(" to {}".format(so_tgt_dir))
ensure_dir(so_tgt_dir)
so_files = glob.glob(join(so_src_dir, '*.so'))
for f in so_files:
shprint(sh.cp, '-a', f, so_tgt_dir)
def collect_libraries_to_load(self, arch):
'''
Collect the shared libraries needed to load when apk initialises and
returns a list. This list will be used to create a file in our
distribution's assets folder which will be used by our java loading
mechanism to load those shared libraries at runtime.
.. warning:: the loading order matters, this cannot be arbitrary,
for example, if some lib has been build against our python, then
needs to be loaded after the python library has been loaded.
'''
info('Collect compiled shared libraries to load at runtime')
libs_to_load = []
# Add libs that should be loaded before bootstrap libs (here goes some
# special shared libs, like stl libs used at compile time or libs that
# the bootstrap may be dependant).
# Todo: All the recipes using gnustl_shared should be migrated to use
# c++_shared because in ndk r18 it will be the only stl available:
# https://developer.android.com/ndk/guides/cpp-support
stl_gnu_shared_recipes = (
'icu', 'leveldb', 'libzmq', 'protobuf_cpp',
# CppCompiledPythonRecipes depends on gnustl_shared so...
'atom', 'kiwisolver',)
stl_cxx_shared_recipes = ('boost',)
for recipe_name in self.ctx.recipe_build_order:
if 'c++_shared' not in libs_to_load and recipe_name in stl_cxx_shared_recipes:
libs_to_load.append('c++_shared')
if 'gnustl_shared' not in libs_to_load and recipe_name in stl_gnu_shared_recipes:
libs_to_load.append('gnustl_shared')
if all(lib in ('c++_shared', 'gnustl_shared') for lib in libs_to_load):
break
# Add crystax if necessary
if self.ctx.python_recipe.from_crystax:
libs_to_load.append('crystax')
# Add libraries that python depends on
if 'sqlite3' in self.ctx.recipe_build_order:
libs_to_load.append('sqlite3')
if 'ffi' in self.ctx.recipe_build_order:
libs_to_load.append('ffi')
if 'openssl' in self.ctx.recipe_build_order:
recipe = Recipe.get_recipe('openssl', self.ctx)
libs_to_load.append('ssl' + recipe.version)
libs_to_load.append('crypto' + recipe.version)
# Add the bootstrap libs
[libs_to_load.append(i) for i in self.libraries_to_load]
# Add the corresponding python lib
python_version = self.ctx.python_recipe.major_minor_version_string
if python_version[0] == '3':
python_version += 'm'
libs_to_load.append('python' + python_version)
# Add libs that should be loaded after python because depends on it
if 'boost' in self.ctx.recipe_build_order:
# Todo: check boost loading dependencies and create a proper
# loading order. For now it works for libtorrent's recipe, the only
# recipe that depends on boost...so...for now it's fine, but should
# be enhanced, maybe using similar method than
# `pythonforandroid.build.copylibs_function`?
for l in listdir(self.ctx.get_libs_dir(arch.arch)):
if l.startswith('libboost_'):
libs_to_load.append(l[3:-3])
if 'libtorrent' in self.ctx.recipe_build_order:
libs_to_load.append('torrent_rasterbar')
return libs_to_load
def strip_libraries(self, arch):
info('Stripping libraries')
if self.ctx.python_recipe.from_crystax:
info('Python was loaded from CrystaX, skipping strip')
return
env = arch.get_env()
strip = which('arm-linux-androideabi-strip', env['PATH'])
if strip is None:
warning('Can\'t find strip in PATH...')
return
strip = sh.Command(strip)
filens = shprint(sh.find, join(self.dist_dir, '_python_bundle',
'_python_bundle', 'modules'),
join(self.dist_dir, 'libs'),
'-iname', '*.so', _env=env).stdout.decode('utf-8')
logger.info('Stripping libraries in private dir')
for filen in filens.split('\n'):
try:
strip(filen, _env=env)
except sh.ErrorReturnCode_1:
logger.debug('Failed to strip ' + filen)
def fry_eggs(self, sitepackages):
info('Frying eggs in {}'.format(sitepackages))
for d in listdir(sitepackages):
rd = join(sitepackages, d)
if isdir(rd) and d.endswith('.egg'):
info(' ' + d)
files = [join(rd, f) for f in listdir(rd) if f != 'EGG-INFO']
if files:
shprint(sh.mv, '-t', sitepackages, *files)
shprint(sh.rm, '-rf', d)
def expand_dependencies(recipes):
recipe_lists = [[]]
for recipe in recipes:
if isinstance(recipe, (tuple, list)):
new_recipe_lists = []
for alternative in recipe:
for old_list in recipe_lists:
new_list = [i for i in old_list]
new_list.append(alternative)
new_recipe_lists.append(new_list)
recipe_lists = new_recipe_lists
else:
for old_list in recipe_lists:
old_list.append(recipe)
return recipe_lists