blob: f9e9bfd5873bb7968e1a8777f9aa6282d61db5fa [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001from oe.utils import execute_pre_post_process
2import os
3import subprocess
4import multiprocessing
5
6
7def generate_image(arg):
8 (type, subimages, create_img_cmd) = arg
9
10 bb.note("Running image creation script for %s: %s ..." %
11 (type, create_img_cmd))
12
13 try:
14 output = subprocess.check_output(create_img_cmd,
15 stderr=subprocess.STDOUT)
16 except subprocess.CalledProcessError as e:
17 return("Error: The image creation script '%s' returned %d:\n%s" %
18 (e.cmd, e.returncode, e.output))
19
20 bb.note("Script output:\n%s" % output)
21
22 return None
23
24
25"""
26This class will help compute IMAGE_FSTYPE dependencies and group them in batches
27that can be executed in parallel.
28
29The next example is for illustration purposes, highly unlikely to happen in real life.
30It's just one of the test cases I used to test the algorithm:
31
32For:
33IMAGE_FSTYPES = "i1 i2 i3 i4 i5"
34IMAGE_TYPEDEP_i4 = "i2"
35IMAGE_TYPEDEP_i5 = "i6 i4"
36IMAGE_TYPEDEP_i6 = "i7"
37IMAGE_TYPEDEP_i7 = "i2"
38
39We get the following list of batches that can be executed in parallel, having the
40dependencies satisfied:
41
42[['i1', 'i3', 'i2'], ['i4', 'i7'], ['i6'], ['i5']]
43"""
44class ImageDepGraph(object):
45 def __init__(self, d):
46 self.d = d
47 self.graph = dict()
48 self.deps_array = dict()
49
50 def _construct_dep_graph(self, image_fstypes):
51 graph = dict()
52
53 def add_node(node):
54 base_type = self._image_base_type(node)
55 deps = (self.d.getVar('IMAGE_TYPEDEP_' + node, True) or "")
56 base_deps = (self.d.getVar('IMAGE_TYPEDEP_' + base_type, True) or "")
57 if deps != "" or base_deps != "":
58 graph[node] = deps
59
60 for dep in deps.split() + base_deps.split():
61 if not dep in graph:
62 add_node(dep)
63 else:
64 graph[node] = ""
65
66 for fstype in image_fstypes:
67 add_node(fstype)
68
69 return graph
70
71 def _clean_graph(self):
72 # Live and VMDK/VDI images will be processed via inheriting
73 # bbclass and does not get processed here. Remove them from the fstypes
74 # graph. Their dependencies are already added, so no worries here.
75 remove_list = (self.d.getVar('IMAGE_TYPES_MASKED', True) or "").split()
76
77 for item in remove_list:
78 self.graph.pop(item, None)
79
80 def _image_base_type(self, type):
81 ctypes = self.d.getVar('COMPRESSIONTYPES', True).split()
82 if type in ["vmdk", "vdi", "qcow2", "live", "iso", "hddimg"]:
83 type = "ext4"
84 basetype = type
85 for ctype in ctypes:
86 if type.endswith("." + ctype):
87 basetype = type[:-len("." + ctype)]
88 break
89
90 return basetype
91
92 def _compute_dependencies(self):
93 """
94 returns dict object of nodes with [no_of_depends_on, no_of_depended_by]
95 for each node
96 """
97 deps_array = dict()
98 for node in self.graph:
99 deps_array[node] = [0, 0]
100
101 for node in self.graph:
102 deps = self.graph[node].split()
103 deps_array[node][0] += len(deps)
104 for dep in deps:
105 deps_array[dep][1] += 1
106
107 return deps_array
108
109 def _sort_graph(self):
110 sorted_list = []
111 group = []
112 for node in self.graph:
113 if node not in self.deps_array:
114 continue
115
116 depends_on = self.deps_array[node][0]
117
118 if depends_on == 0:
119 group.append(node)
120
121 if len(group) == 0 and len(self.deps_array) != 0:
122 bb.fatal("possible fstype circular dependency...")
123
124 sorted_list.append(group)
125
126 # remove added nodes from deps_array
127 for item in group:
128 for node in self.graph:
129 if item in self.graph[node].split():
130 self.deps_array[node][0] -= 1
131
132 self.deps_array.pop(item, None)
133
134 if len(self.deps_array):
135 # recursive call, to find the next group
136 sorted_list += self._sort_graph()
137
138 return sorted_list
139
140 def group_fstypes(self, image_fstypes):
141 self.graph = self._construct_dep_graph(image_fstypes)
142
143 self._clean_graph()
144
145 self.deps_array = self._compute_dependencies()
146
147 alltypes = [node for node in self.graph]
148
149 return (alltypes, self._sort_graph())
150
151
152class Image(ImageDepGraph):
153 def __init__(self, d):
154 self.d = d
155
156 super(Image, self).__init__(d)
157
158 def _get_rootfs_size(self):
159 """compute the rootfs size"""
160 rootfs_alignment = int(self.d.getVar('IMAGE_ROOTFS_ALIGNMENT', True))
161 overhead_factor = float(self.d.getVar('IMAGE_OVERHEAD_FACTOR', True))
162 rootfs_req_size = int(self.d.getVar('IMAGE_ROOTFS_SIZE', True))
163 rootfs_extra_space = eval(self.d.getVar('IMAGE_ROOTFS_EXTRA_SPACE', True))
164 rootfs_maxsize = self.d.getVar('IMAGE_ROOTFS_MAXSIZE', True)
165
166 output = subprocess.check_output(['du', '-ks',
167 self.d.getVar('IMAGE_ROOTFS', True)])
168 size_kb = int(output.split()[0])
169 base_size = size_kb * overhead_factor
170 base_size = (base_size, rootfs_req_size)[base_size < rootfs_req_size] + \
171 rootfs_extra_space
172
173 if base_size != int(base_size):
174 base_size = int(base_size + 1)
Patrick Williamsd7e96312015-09-22 08:09:05 -0500175 else:
176 base_size = int(base_size)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500177
178 base_size += rootfs_alignment - 1
179 base_size -= base_size % rootfs_alignment
180
181 # Check the rootfs size against IMAGE_ROOTFS_MAXSIZE (if set)
182 if rootfs_maxsize:
183 rootfs_maxsize_int = int(rootfs_maxsize)
184 if base_size > rootfs_maxsize_int:
185 bb.fatal("The rootfs size %d(K) overrides the max size %d(K)" % \
186 (base_size, rootfs_maxsize_int))
187
188 return base_size
189
190 def _create_symlinks(self, subimages):
191 """create symlinks to the newly created image"""
192 deploy_dir = self.d.getVar('DEPLOY_DIR_IMAGE', True)
193 img_name = self.d.getVar('IMAGE_NAME', True)
194 link_name = self.d.getVar('IMAGE_LINK_NAME', True)
195 manifest_name = self.d.getVar('IMAGE_MANIFEST', True)
196
197 os.chdir(deploy_dir)
198
199 if link_name:
200 for type in subimages:
201 if os.path.exists(img_name + ".rootfs." + type):
202 dst = link_name + "." + type
203 src = img_name + ".rootfs." + type
204 bb.note("Creating symlink: %s -> %s" % (dst, src))
205 os.symlink(src, dst)
206
207 if manifest_name is not None and \
208 os.path.exists(manifest_name) and \
209 not os.path.exists(link_name + ".manifest"):
210 os.symlink(os.path.basename(manifest_name),
211 link_name + ".manifest")
212
213 def _remove_old_symlinks(self):
214 """remove the symlinks to old binaries"""
215
216 if self.d.getVar('IMAGE_LINK_NAME', True):
217 deploy_dir = self.d.getVar('DEPLOY_DIR_IMAGE', True)
218 for img in os.listdir(deploy_dir):
219 if img.find(self.d.getVar('IMAGE_LINK_NAME', True)) == 0:
220 img = os.path.join(deploy_dir, img)
221 if os.path.islink(img):
222 if self.d.getVar('RM_OLD_IMAGE', True) == "1" and \
223 os.path.exists(os.path.realpath(img)):
224 os.remove(os.path.realpath(img))
225
226 os.remove(img)
227
228 """
229 This function will just filter out the compressed image types from the
230 fstype groups returning a (filtered_fstype_groups, cimages) tuple.
231 """
232 def _filter_out_commpressed(self, fstype_groups):
233 ctypes = self.d.getVar('COMPRESSIONTYPES', True).split()
234 cimages = {}
235
236 filtered_groups = []
237 for group in fstype_groups:
238 filtered_group = []
239 for type in group:
240 basetype = None
241 for ctype in ctypes:
242 if type.endswith("." + ctype):
243 basetype = type[:-len("." + ctype)]
244 if basetype not in filtered_group:
245 filtered_group.append(basetype)
246 if basetype not in cimages:
247 cimages[basetype] = []
248 if ctype not in cimages[basetype]:
249 cimages[basetype].append(ctype)
250 break
251 if not basetype and type not in filtered_group:
252 filtered_group.append(type)
253
254 filtered_groups.append(filtered_group)
255
256 return (filtered_groups, cimages)
257
258 def _get_image_types(self):
259 """returns a (types, cimages) tuple"""
260
261 alltypes, fstype_groups = self.group_fstypes(self.d.getVar('IMAGE_FSTYPES', True).split())
262
263 filtered_groups, cimages = self._filter_out_commpressed(fstype_groups)
264
265 return (alltypes, filtered_groups, cimages)
266
267 def _write_script(self, type, cmds):
268 tempdir = self.d.getVar('T', True)
269 script_name = os.path.join(tempdir, "create_image." + type)
270 rootfs_size = self._get_rootfs_size()
271
272 self.d.setVar('img_creation_func', '\n'.join(cmds))
273 self.d.setVarFlag('img_creation_func', 'func', 1)
274 self.d.setVarFlag('img_creation_func', 'fakeroot', 1)
275 self.d.setVar('ROOTFS_SIZE', str(rootfs_size))
276
277 with open(script_name, "w+") as script:
278 script.write("%s" % bb.build.shell_trap_code())
279 script.write("export ROOTFS_SIZE=%d\n" % rootfs_size)
280 bb.data.emit_func('img_creation_func', script, self.d)
281 script.write("img_creation_func\n")
282
283 os.chmod(script_name, 0775)
284
285 return script_name
286
287 def _get_imagecmds(self):
288 old_overrides = self.d.getVar('OVERRIDES', 0)
289
290 alltypes, fstype_groups, cimages = self._get_image_types()
291
292 image_cmd_groups = []
293
294 bb.note("The image creation groups are: %s" % str(fstype_groups))
295 for fstype_group in fstype_groups:
296 image_cmds = []
297 for type in fstype_group:
298 cmds = []
299 subimages = []
300
301 localdata = bb.data.createCopy(self.d)
302 localdata.setVar('OVERRIDES', '%s:%s' % (type, old_overrides))
303 bb.data.update_data(localdata)
304 localdata.setVar('type', type)
305
306 image_cmd = localdata.getVar("IMAGE_CMD", True)
307 if image_cmd:
308 cmds.append("\t" + image_cmd)
309 else:
310 bb.fatal("No IMAGE_CMD defined for IMAGE_FSTYPES entry '%s' - possibly invalid type name or missing support class" % type)
311 cmds.append(localdata.expand("\tcd ${DEPLOY_DIR_IMAGE}"))
312
313 if type in cimages:
314 for ctype in cimages[type]:
315 cmds.append("\t" + localdata.getVar("COMPRESS_CMD_" + ctype, True))
316 subimages.append(type + "." + ctype)
317
318 if type not in alltypes:
319 cmds.append(localdata.expand("\trm ${IMAGE_NAME}.rootfs.${type}"))
320 else:
321 subimages.append(type)
322
323 script_name = self._write_script(type, cmds)
324
325 image_cmds.append((type, subimages, script_name))
326
327 image_cmd_groups.append(image_cmds)
328
329 return image_cmd_groups
330
331 def _write_wic_env(self):
332 """
333 Write environment variables used by wic
334 to tmp/sysroots/<machine>/imgdata/<image>.env
335 """
336 stdir = self.d.getVar('STAGING_DIR_TARGET', True)
337 outdir = os.path.join(stdir, 'imgdata')
338 if not os.path.exists(outdir):
339 os.makedirs(outdir)
340 basename = self.d.getVar('IMAGE_BASENAME', True)
341 with open(os.path.join(outdir, basename) + '.env', 'w') as envf:
342 for var in self.d.getVar('WICVARS', True).split():
343 value = self.d.getVar(var, True)
344 if value:
345 envf.write('%s="%s"\n' % (var, value.strip()))
346
347 def create(self):
348 bb.note("###### Generate images #######")
349 pre_process_cmds = self.d.getVar("IMAGE_PREPROCESS_COMMAND", True)
350 post_process_cmds = self.d.getVar("IMAGE_POSTPROCESS_COMMAND", True)
351
352 execute_pre_post_process(self.d, pre_process_cmds)
353
354 self._remove_old_symlinks()
355
356 image_cmd_groups = self._get_imagecmds()
357
358 self._write_wic_env()
359
360 for image_cmds in image_cmd_groups:
361 # create the images in parallel
362 nproc = multiprocessing.cpu_count()
363 pool = bb.utils.multiprocessingpool(nproc)
364 results = list(pool.imap(generate_image, image_cmds))
365 pool.close()
366 pool.join()
367
368 for result in results:
369 if result is not None:
370 bb.fatal(result)
371
372 for image_type, subimages, script in image_cmds:
373 bb.note("Creating symlinks for %s image ..." % image_type)
374 self._create_symlinks(subimages)
375
376 execute_pre_post_process(self.d, post_process_cmds)
377
378
379def create_image(d):
380 Image(d).create()
381
382if __name__ == "__main__":
383 """
384 Image creation can be called independent from bitbake environment.
385 """
386 """
387 TBD
388 """