blob: 2361955971bf3fb7956923df0272281c846cdee6 [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)
175
176 base_size += rootfs_alignment - 1
177 base_size -= base_size % rootfs_alignment
178
179 # Check the rootfs size against IMAGE_ROOTFS_MAXSIZE (if set)
180 if rootfs_maxsize:
181 rootfs_maxsize_int = int(rootfs_maxsize)
182 if base_size > rootfs_maxsize_int:
183 bb.fatal("The rootfs size %d(K) overrides the max size %d(K)" % \
184 (base_size, rootfs_maxsize_int))
185
186 return base_size
187
188 def _create_symlinks(self, subimages):
189 """create symlinks to the newly created image"""
190 deploy_dir = self.d.getVar('DEPLOY_DIR_IMAGE', True)
191 img_name = self.d.getVar('IMAGE_NAME', True)
192 link_name = self.d.getVar('IMAGE_LINK_NAME', True)
193 manifest_name = self.d.getVar('IMAGE_MANIFEST', True)
194
195 os.chdir(deploy_dir)
196
197 if link_name:
198 for type in subimages:
199 if os.path.exists(img_name + ".rootfs." + type):
200 dst = link_name + "." + type
201 src = img_name + ".rootfs." + type
202 bb.note("Creating symlink: %s -> %s" % (dst, src))
203 os.symlink(src, dst)
204
205 if manifest_name is not None and \
206 os.path.exists(manifest_name) and \
207 not os.path.exists(link_name + ".manifest"):
208 os.symlink(os.path.basename(manifest_name),
209 link_name + ".manifest")
210
211 def _remove_old_symlinks(self):
212 """remove the symlinks to old binaries"""
213
214 if self.d.getVar('IMAGE_LINK_NAME', True):
215 deploy_dir = self.d.getVar('DEPLOY_DIR_IMAGE', True)
216 for img in os.listdir(deploy_dir):
217 if img.find(self.d.getVar('IMAGE_LINK_NAME', True)) == 0:
218 img = os.path.join(deploy_dir, img)
219 if os.path.islink(img):
220 if self.d.getVar('RM_OLD_IMAGE', True) == "1" and \
221 os.path.exists(os.path.realpath(img)):
222 os.remove(os.path.realpath(img))
223
224 os.remove(img)
225
226 """
227 This function will just filter out the compressed image types from the
228 fstype groups returning a (filtered_fstype_groups, cimages) tuple.
229 """
230 def _filter_out_commpressed(self, fstype_groups):
231 ctypes = self.d.getVar('COMPRESSIONTYPES', True).split()
232 cimages = {}
233
234 filtered_groups = []
235 for group in fstype_groups:
236 filtered_group = []
237 for type in group:
238 basetype = None
239 for ctype in ctypes:
240 if type.endswith("." + ctype):
241 basetype = type[:-len("." + ctype)]
242 if basetype not in filtered_group:
243 filtered_group.append(basetype)
244 if basetype not in cimages:
245 cimages[basetype] = []
246 if ctype not in cimages[basetype]:
247 cimages[basetype].append(ctype)
248 break
249 if not basetype and type not in filtered_group:
250 filtered_group.append(type)
251
252 filtered_groups.append(filtered_group)
253
254 return (filtered_groups, cimages)
255
256 def _get_image_types(self):
257 """returns a (types, cimages) tuple"""
258
259 alltypes, fstype_groups = self.group_fstypes(self.d.getVar('IMAGE_FSTYPES', True).split())
260
261 filtered_groups, cimages = self._filter_out_commpressed(fstype_groups)
262
263 return (alltypes, filtered_groups, cimages)
264
265 def _write_script(self, type, cmds):
266 tempdir = self.d.getVar('T', True)
267 script_name = os.path.join(tempdir, "create_image." + type)
268 rootfs_size = self._get_rootfs_size()
269
270 self.d.setVar('img_creation_func', '\n'.join(cmds))
271 self.d.setVarFlag('img_creation_func', 'func', 1)
272 self.d.setVarFlag('img_creation_func', 'fakeroot', 1)
273 self.d.setVar('ROOTFS_SIZE', str(rootfs_size))
274
275 with open(script_name, "w+") as script:
276 script.write("%s" % bb.build.shell_trap_code())
277 script.write("export ROOTFS_SIZE=%d\n" % rootfs_size)
278 bb.data.emit_func('img_creation_func', script, self.d)
279 script.write("img_creation_func\n")
280
281 os.chmod(script_name, 0775)
282
283 return script_name
284
285 def _get_imagecmds(self):
286 old_overrides = self.d.getVar('OVERRIDES', 0)
287
288 alltypes, fstype_groups, cimages = self._get_image_types()
289
290 image_cmd_groups = []
291
292 bb.note("The image creation groups are: %s" % str(fstype_groups))
293 for fstype_group in fstype_groups:
294 image_cmds = []
295 for type in fstype_group:
296 cmds = []
297 subimages = []
298
299 localdata = bb.data.createCopy(self.d)
300 localdata.setVar('OVERRIDES', '%s:%s' % (type, old_overrides))
301 bb.data.update_data(localdata)
302 localdata.setVar('type', type)
303
304 image_cmd = localdata.getVar("IMAGE_CMD", True)
305 if image_cmd:
306 cmds.append("\t" + image_cmd)
307 else:
308 bb.fatal("No IMAGE_CMD defined for IMAGE_FSTYPES entry '%s' - possibly invalid type name or missing support class" % type)
309 cmds.append(localdata.expand("\tcd ${DEPLOY_DIR_IMAGE}"))
310
311 if type in cimages:
312 for ctype in cimages[type]:
313 cmds.append("\t" + localdata.getVar("COMPRESS_CMD_" + ctype, True))
314 subimages.append(type + "." + ctype)
315
316 if type not in alltypes:
317 cmds.append(localdata.expand("\trm ${IMAGE_NAME}.rootfs.${type}"))
318 else:
319 subimages.append(type)
320
321 script_name = self._write_script(type, cmds)
322
323 image_cmds.append((type, subimages, script_name))
324
325 image_cmd_groups.append(image_cmds)
326
327 return image_cmd_groups
328
329 def _write_wic_env(self):
330 """
331 Write environment variables used by wic
332 to tmp/sysroots/<machine>/imgdata/<image>.env
333 """
334 stdir = self.d.getVar('STAGING_DIR_TARGET', True)
335 outdir = os.path.join(stdir, 'imgdata')
336 if not os.path.exists(outdir):
337 os.makedirs(outdir)
338 basename = self.d.getVar('IMAGE_BASENAME', True)
339 with open(os.path.join(outdir, basename) + '.env', 'w') as envf:
340 for var in self.d.getVar('WICVARS', True).split():
341 value = self.d.getVar(var, True)
342 if value:
343 envf.write('%s="%s"\n' % (var, value.strip()))
344
345 def create(self):
346 bb.note("###### Generate images #######")
347 pre_process_cmds = self.d.getVar("IMAGE_PREPROCESS_COMMAND", True)
348 post_process_cmds = self.d.getVar("IMAGE_POSTPROCESS_COMMAND", True)
349
350 execute_pre_post_process(self.d, pre_process_cmds)
351
352 self._remove_old_symlinks()
353
354 image_cmd_groups = self._get_imagecmds()
355
356 self._write_wic_env()
357
358 for image_cmds in image_cmd_groups:
359 # create the images in parallel
360 nproc = multiprocessing.cpu_count()
361 pool = bb.utils.multiprocessingpool(nproc)
362 results = list(pool.imap(generate_image, image_cmds))
363 pool.close()
364 pool.join()
365
366 for result in results:
367 if result is not None:
368 bb.fatal(result)
369
370 for image_type, subimages, script in image_cmds:
371 bb.note("Creating symlinks for %s image ..." % image_type)
372 self._create_symlinks(subimages)
373
374 execute_pre_post_process(self.d, post_process_cmds)
375
376
377def create_image(d):
378 Image(d).create()
379
380if __name__ == "__main__":
381 """
382 Image creation can be called independent from bitbake environment.
383 """
384 """
385 TBD
386 """