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