-
Notifications
You must be signed in to change notification settings - Fork 1.9k
/
distribution.py
278 lines (243 loc) · 10.8 KB
/
distribution.py
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
import json
import glob
from os.path import exists, join
from pythonforandroid.logger import (
debug, info, info_notify, warning, Err_Style, Err_Fore)
from pythonforandroid.util import (
current_directory, BuildInterruptingException, rmdir)
class Distribution:
'''State container for information about a distribution (i.e. an
Android project).
This is separate from a Bootstrap because the Bootstrap is
concerned with building and populating the dist directory, whereas
the dist itself could also come from e.g. a binary download.
'''
ctx = None
name = None # A name identifying the dist. May not be None.
needs_build = False # Whether the dist needs compiling
url = None
dist_dir = None # Where the dist dir ultimately is. Should not be None.
ndk_api = None
archs = []
'''The names of the arch targets that the dist is built for.'''
recipes = []
description = '' # A long description
def __init__(self, ctx):
self.ctx = ctx
def __str__(self):
return '<Distribution: name {} with recipes ({})>'.format(
# self.name, ', '.join([recipe.name for recipe in self.recipes]))
self.name, ', '.join(self.recipes))
def __repr__(self):
return str(self)
@classmethod
def get_distribution(
cls,
ctx,
*,
archs, # required keyword argument: there is no sensible default
name=None,
recipes=[],
ndk_api=None,
force_build=False,
extra_dist_dirs=[],
require_perfect_match=False,
allow_replace_dist=True
):
'''Takes information about the distribution, and decides what kind of
distribution it will be.
If parameters conflict (e.g. a dist with that name already
exists, but doesn't have the right set of recipes),
an error is thrown.
Parameters
----------
name : str
The name of the distribution. If a dist with this name already '
exists, it will be used.
ndk_api : int
The NDK API to compile against, included in the dist because it cannot
be changed later during APK packaging.
archs : list
The target architectures list to compile against, included in the dist because
it cannot be changed later during APK packaging.
recipes : list
The recipes that the distribution must contain.
force_download: bool
If True, only downloaded dists are considered.
force_build : bool
If True, the dist is forced to be built locally.
extra_dist_dirs : list
Any extra directories in which to search for dists.
require_perfect_match : bool
If True, will only match distributions with precisely the
correct set of recipes.
allow_replace_dist : bool
If True, will allow an existing dist with the specified
name but incompatible requirements to be overwritten by
a new one with the current requirements.
'''
possible_dists = Distribution.get_distributions(ctx)
debug(f"All possible dists: {possible_dists}")
# Will hold dists that would be built in the same folder as an existing dist
folder_match_dist = None
# 0) Check if a dist with that name and architecture already exists
if name is not None and name:
possible_dists = [
d for d in possible_dists if
(d.name == name) and all(arch_name in d.archs for arch_name in archs)]
debug(f"Dist matching name and arch: {possible_dists}")
if possible_dists:
# There should only be one folder with a given dist name *and* arch.
# We could check that here, but for compatibility let's let it slide
# and just record the details of one of them. We only use this data to
# possibly fail the build later, so it doesn't really matter if there
# was more than one clash.
folder_match_dist = possible_dists[0]
# 1) Check if any existing dists meet the requirements
_possible_dists = []
for dist in possible_dists:
if (
ndk_api is not None and dist.ndk_api != ndk_api
) or dist.ndk_api is None:
debug(
f"dist {dist} failed to match ndk_api, target api {ndk_api}, dist api {dist.ndk_api}"
)
continue
for recipe in recipes:
if recipe not in dist.recipes:
debug(f"dist {dist} missing recipe {recipe}")
break
else:
_possible_dists.append(dist)
possible_dists = _possible_dists
debug(f"Dist matching ndk_api and recipe: {possible_dists}")
if possible_dists:
info('Of the existing distributions, the following meet '
'the given requirements:')
pretty_log_dists(possible_dists)
else:
info('No existing dists meet the given requirements!')
# If any dist has perfect recipes, arch and NDK API, return it
for dist in possible_dists:
if force_build:
debug("Skipping dist due to forced build")
continue
if ndk_api is not None and dist.ndk_api != ndk_api:
debug("Skipping dist due to ndk_api mismatch")
continue
if not all(arch_name in dist.archs for arch_name in archs):
debug("Skipping dist due to arch mismatch")
continue
if (set(dist.recipes) == set(recipes) or
(set(recipes).issubset(set(dist.recipes)) and
not require_perfect_match)):
info_notify('{} has compatible recipes, using this one'
.format(dist.name))
return dist
else:
debug(
f"Skipping dist due to recipes mismatch, expected {set(recipes)}, actual {set(dist.recipes)}"
)
# If there was a name match but we didn't already choose it,
# then the existing dist is incompatible with the requested
# configuration and the build cannot continue
if folder_match_dist is not None and not allow_replace_dist:
raise BuildInterruptingException(
'Asked for dist with name {name} with recipes ({req_recipes}) and '
'NDK API {req_ndk_api}, but a dist '
'with this name already exists and has either incompatible recipes '
'({dist_recipes}) or NDK API {dist_ndk_api}'.format(
name=name,
req_ndk_api=ndk_api,
dist_ndk_api=folder_match_dist.ndk_api,
req_recipes=', '.join(recipes),
dist_recipes=', '.join(folder_match_dist.recipes)))
assert len(possible_dists) < 2
# If we got this far, we need to build a new dist
dist = Distribution(ctx)
dist.needs_build = True
if not name:
filen = 'unnamed_dist_{}'
i = 1
while exists(join(ctx.dist_dir, filen.format(i))):
i += 1
name = filen.format(i)
dist.name = name
dist.dist_dir = join(
ctx.dist_dir,
name)
dist.recipes = recipes
dist.ndk_api = ctx.ndk_api
dist.archs = archs
return dist
def folder_exists(self):
return exists(self.dist_dir)
def delete(self):
rmdir(self.dist_dir)
@classmethod
def get_distributions(cls, ctx, extra_dist_dirs=[]):
'''Returns all the distributions found locally.'''
if extra_dist_dirs:
raise BuildInterruptingException(
'extra_dist_dirs argument to get_distributions '
'is not yet implemented')
dist_dir = ctx.dist_dir
folders = glob.glob(join(dist_dir, '*'))
for dir in extra_dist_dirs:
folders.extend(glob.glob(join(dir, '*')))
dists = []
for folder in folders:
if exists(join(folder, 'dist_info.json')):
with open(join(folder, 'dist_info.json')) as fileh:
dist_info = json.load(fileh)
dist = cls(ctx)
dist.name = dist_info['dist_name']
dist.dist_dir = folder
dist.needs_build = False
dist.recipes = dist_info['recipes']
if 'archs' in dist_info:
dist.archs = dist_info['archs']
if 'ndk_api' in dist_info:
dist.ndk_api = dist_info['ndk_api']
else:
dist.ndk_api = None
warning(
"Distribution {distname}: ({distdir}) has been "
"built with an unknown api target, ignoring it, "
"you might want to delete it".format(
distname=dist.name,
distdir=dist.dist_dir
)
)
dists.append(dist)
return dists
def save_info(self, dirn):
'''
Save information about the distribution in its dist_dir.
'''
with current_directory(dirn):
info('Saving distribution info')
with open('dist_info.json', 'w') as fileh:
json.dump({'dist_name': self.name,
'bootstrap': self.ctx.bootstrap.name,
'archs': [arch.arch for arch in self.ctx.archs],
'ndk_api': self.ctx.ndk_api,
'use_setup_py': self.ctx.use_setup_py,
'recipes': self.ctx.recipe_build_order + self.ctx.python_modules,
'hostpython': self.ctx.hostpython,
'python_version': self.ctx.python_recipe.major_minor_version_string},
fileh)
def pretty_log_dists(dists, log_func=info):
infos = []
for dist in dists:
ndk_api = 'unknown' if dist.ndk_api is None else dist.ndk_api
infos.append('{Fore.GREEN}{Style.BRIGHT}{name}{Style.RESET_ALL}: min API {ndk_api}, '
'includes recipes ({Fore.GREEN}{recipes}'
'{Style.RESET_ALL}), built for archs ({Fore.BLUE}'
'{archs}{Style.RESET_ALL})'.format(
ndk_api=ndk_api,
name=dist.name, recipes=', '.join(dist.recipes),
archs=', '.join(dist.archs) if dist.archs else 'UNKNOWN',
Fore=Err_Fore, Style=Err_Style))
for line in infos:
log_func('\t' + line)