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): |
| 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 | """ |
| 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 "") |
| 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 | |
| 152 | class 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 Williams | d7e9631 | 2015-09-22 08:09:05 -0500 | [diff] [blame] | 175 | else: |
| 176 | base_size = int(base_size) |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 177 | |
| 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 | |
| 379 | def create_image(d): |
| 380 | Image(d).create() |
| 381 | |
| 382 | if __name__ == "__main__": |
| 383 | """ |
| 384 | Image creation can be called independent from bitbake environment. |
| 385 | """ |
| 386 | """ |
| 387 | TBD |
| 388 | """ |