| from oe.utils import execute_pre_post_process |
| import os |
| import subprocess |
| import multiprocessing |
| |
| |
| def generate_image(arg): |
| (type, subimages, create_img_cmd) = arg |
| |
| bb.note("Running image creation script for %s: %s ..." % |
| (type, create_img_cmd)) |
| |
| try: |
| output = subprocess.check_output(create_img_cmd, |
| stderr=subprocess.STDOUT) |
| except subprocess.CalledProcessError as e: |
| return("Error: The image creation script '%s' returned %d:\n%s" % |
| (e.cmd, e.returncode, e.output)) |
| |
| bb.note("Script output:\n%s" % output) |
| |
| return None |
| |
| |
| """ |
| This class will help compute IMAGE_FSTYPE dependencies and group them in batches |
| that can be executed in parallel. |
| |
| The next example is for illustration purposes, highly unlikely to happen in real life. |
| It's just one of the test cases I used to test the algorithm: |
| |
| For: |
| IMAGE_FSTYPES = "i1 i2 i3 i4 i5" |
| IMAGE_TYPEDEP_i4 = "i2" |
| IMAGE_TYPEDEP_i5 = "i6 i4" |
| IMAGE_TYPEDEP_i6 = "i7" |
| IMAGE_TYPEDEP_i7 = "i2" |
| |
| We get the following list of batches that can be executed in parallel, having the |
| dependencies satisfied: |
| |
| [['i1', 'i3', 'i2'], ['i4', 'i7'], ['i6'], ['i5']] |
| """ |
| class ImageDepGraph(object): |
| def __init__(self, d): |
| self.d = d |
| self.graph = dict() |
| self.deps_array = dict() |
| |
| def _construct_dep_graph(self, image_fstypes): |
| graph = dict() |
| |
| def add_node(node): |
| base_type = self._image_base_type(node) |
| deps = (self.d.getVar('IMAGE_TYPEDEP_' + node, True) or "") |
| base_deps = (self.d.getVar('IMAGE_TYPEDEP_' + base_type, True) or "") |
| if deps != "" or base_deps != "": |
| graph[node] = deps |
| |
| for dep in deps.split() + base_deps.split(): |
| if not dep in graph: |
| add_node(dep) |
| else: |
| graph[node] = "" |
| |
| for fstype in image_fstypes: |
| add_node(fstype) |
| |
| return graph |
| |
| def _clean_graph(self): |
| # Live and VMDK/VDI images will be processed via inheriting |
| # bbclass and does not get processed here. Remove them from the fstypes |
| # graph. Their dependencies are already added, so no worries here. |
| remove_list = (self.d.getVar('IMAGE_TYPES_MASKED', True) or "").split() |
| |
| for item in remove_list: |
| self.graph.pop(item, None) |
| |
| def _image_base_type(self, type): |
| ctypes = self.d.getVar('COMPRESSIONTYPES', True).split() |
| if type in ["vmdk", "vdi", "qcow2", "live", "iso", "hddimg"]: |
| type = "ext4" |
| basetype = type |
| for ctype in ctypes: |
| if type.endswith("." + ctype): |
| basetype = type[:-len("." + ctype)] |
| break |
| |
| return basetype |
| |
| def _compute_dependencies(self): |
| """ |
| returns dict object of nodes with [no_of_depends_on, no_of_depended_by] |
| for each node |
| """ |
| deps_array = dict() |
| for node in self.graph: |
| deps_array[node] = [0, 0] |
| |
| for node in self.graph: |
| deps = self.graph[node].split() |
| deps_array[node][0] += len(deps) |
| for dep in deps: |
| deps_array[dep][1] += 1 |
| |
| return deps_array |
| |
| def _sort_graph(self): |
| sorted_list = [] |
| group = [] |
| for node in self.graph: |
| if node not in self.deps_array: |
| continue |
| |
| depends_on = self.deps_array[node][0] |
| |
| if depends_on == 0: |
| group.append(node) |
| |
| if len(group) == 0 and len(self.deps_array) != 0: |
| bb.fatal("possible fstype circular dependency...") |
| |
| sorted_list.append(group) |
| |
| # remove added nodes from deps_array |
| for item in group: |
| for node in self.graph: |
| if item in self.graph[node].split(): |
| self.deps_array[node][0] -= 1 |
| |
| self.deps_array.pop(item, None) |
| |
| if len(self.deps_array): |
| # recursive call, to find the next group |
| sorted_list += self._sort_graph() |
| |
| return sorted_list |
| |
| def group_fstypes(self, image_fstypes): |
| self.graph = self._construct_dep_graph(image_fstypes) |
| |
| self._clean_graph() |
| |
| self.deps_array = self._compute_dependencies() |
| |
| alltypes = [node for node in self.graph] |
| |
| return (alltypes, self._sort_graph()) |
| |
| |
| class Image(ImageDepGraph): |
| def __init__(self, d): |
| self.d = d |
| |
| super(Image, self).__init__(d) |
| |
| def _get_rootfs_size(self): |
| """compute the rootfs size""" |
| rootfs_alignment = int(self.d.getVar('IMAGE_ROOTFS_ALIGNMENT', True)) |
| overhead_factor = float(self.d.getVar('IMAGE_OVERHEAD_FACTOR', True)) |
| rootfs_req_size = int(self.d.getVar('IMAGE_ROOTFS_SIZE', True)) |
| rootfs_extra_space = eval(self.d.getVar('IMAGE_ROOTFS_EXTRA_SPACE', True)) |
| rootfs_maxsize = self.d.getVar('IMAGE_ROOTFS_MAXSIZE', True) |
| |
| output = subprocess.check_output(['du', '-ks', |
| self.d.getVar('IMAGE_ROOTFS', True)]) |
| size_kb = int(output.split()[0]) |
| base_size = size_kb * overhead_factor |
| base_size = (base_size, rootfs_req_size)[base_size < rootfs_req_size] + \ |
| rootfs_extra_space |
| |
| if base_size != int(base_size): |
| base_size = int(base_size + 1) |
| else: |
| base_size = int(base_size) |
| |
| base_size += rootfs_alignment - 1 |
| base_size -= base_size % rootfs_alignment |
| |
| # Check the rootfs size against IMAGE_ROOTFS_MAXSIZE (if set) |
| if rootfs_maxsize: |
| rootfs_maxsize_int = int(rootfs_maxsize) |
| if base_size > rootfs_maxsize_int: |
| bb.fatal("The rootfs size %d(K) overrides the max size %d(K)" % \ |
| (base_size, rootfs_maxsize_int)) |
| |
| return base_size |
| |
| def _create_symlinks(self, subimages): |
| """create symlinks to the newly created image""" |
| deploy_dir = self.d.getVar('DEPLOY_DIR_IMAGE', True) |
| img_name = self.d.getVar('IMAGE_NAME', True) |
| link_name = self.d.getVar('IMAGE_LINK_NAME', True) |
| manifest_name = self.d.getVar('IMAGE_MANIFEST', True) |
| |
| os.chdir(deploy_dir) |
| |
| if link_name: |
| for type in subimages: |
| if os.path.exists(img_name + ".rootfs." + type): |
| dst = link_name + "." + type |
| src = img_name + ".rootfs." + type |
| bb.note("Creating symlink: %s -> %s" % (dst, src)) |
| os.symlink(src, dst) |
| |
| if manifest_name is not None and \ |
| os.path.exists(manifest_name) and \ |
| not os.path.exists(link_name + ".manifest"): |
| os.symlink(os.path.basename(manifest_name), |
| link_name + ".manifest") |
| |
| def _remove_old_symlinks(self): |
| """remove the symlinks to old binaries""" |
| |
| if self.d.getVar('IMAGE_LINK_NAME', True): |
| deploy_dir = self.d.getVar('DEPLOY_DIR_IMAGE', True) |
| for img in os.listdir(deploy_dir): |
| if img.find(self.d.getVar('IMAGE_LINK_NAME', True)) == 0: |
| img = os.path.join(deploy_dir, img) |
| if os.path.islink(img): |
| if self.d.getVar('RM_OLD_IMAGE', True) == "1" and \ |
| os.path.exists(os.path.realpath(img)): |
| os.remove(os.path.realpath(img)) |
| |
| os.remove(img) |
| |
| """ |
| This function will just filter out the compressed image types from the |
| fstype groups returning a (filtered_fstype_groups, cimages) tuple. |
| """ |
| def _filter_out_commpressed(self, fstype_groups): |
| ctypes = self.d.getVar('COMPRESSIONTYPES', True).split() |
| cimages = {} |
| |
| filtered_groups = [] |
| for group in fstype_groups: |
| filtered_group = [] |
| for type in group: |
| basetype = None |
| for ctype in ctypes: |
| if type.endswith("." + ctype): |
| basetype = type[:-len("." + ctype)] |
| if basetype not in filtered_group: |
| filtered_group.append(basetype) |
| if basetype not in cimages: |
| cimages[basetype] = [] |
| if ctype not in cimages[basetype]: |
| cimages[basetype].append(ctype) |
| break |
| if not basetype and type not in filtered_group: |
| filtered_group.append(type) |
| |
| filtered_groups.append(filtered_group) |
| |
| return (filtered_groups, cimages) |
| |
| def _get_image_types(self): |
| """returns a (types, cimages) tuple""" |
| |
| alltypes, fstype_groups = self.group_fstypes(self.d.getVar('IMAGE_FSTYPES', True).split()) |
| |
| filtered_groups, cimages = self._filter_out_commpressed(fstype_groups) |
| |
| return (alltypes, filtered_groups, cimages) |
| |
| def _write_script(self, type, cmds): |
| tempdir = self.d.getVar('T', True) |
| script_name = os.path.join(tempdir, "create_image." + type) |
| rootfs_size = self._get_rootfs_size() |
| |
| self.d.setVar('img_creation_func', '\n'.join(cmds)) |
| self.d.setVarFlag('img_creation_func', 'func', 1) |
| self.d.setVarFlag('img_creation_func', 'fakeroot', 1) |
| self.d.setVar('ROOTFS_SIZE', str(rootfs_size)) |
| |
| with open(script_name, "w+") as script: |
| script.write("%s" % bb.build.shell_trap_code()) |
| script.write("export ROOTFS_SIZE=%d\n" % rootfs_size) |
| bb.data.emit_func('img_creation_func', script, self.d) |
| script.write("img_creation_func\n") |
| |
| os.chmod(script_name, 0775) |
| |
| return script_name |
| |
| def _get_imagecmds(self): |
| old_overrides = self.d.getVar('OVERRIDES', 0) |
| |
| alltypes, fstype_groups, cimages = self._get_image_types() |
| |
| image_cmd_groups = [] |
| |
| bb.note("The image creation groups are: %s" % str(fstype_groups)) |
| for fstype_group in fstype_groups: |
| image_cmds = [] |
| for type in fstype_group: |
| cmds = [] |
| subimages = [] |
| |
| localdata = bb.data.createCopy(self.d) |
| localdata.setVar('OVERRIDES', '%s:%s' % (type, old_overrides)) |
| bb.data.update_data(localdata) |
| localdata.setVar('type', type) |
| |
| image_cmd = localdata.getVar("IMAGE_CMD", True) |
| if image_cmd: |
| cmds.append("\t" + image_cmd) |
| else: |
| bb.fatal("No IMAGE_CMD defined for IMAGE_FSTYPES entry '%s' - possibly invalid type name or missing support class" % type) |
| cmds.append(localdata.expand("\tcd ${DEPLOY_DIR_IMAGE}")) |
| |
| if type in cimages: |
| for ctype in cimages[type]: |
| cmds.append("\t" + localdata.getVar("COMPRESS_CMD_" + ctype, True)) |
| subimages.append(type + "." + ctype) |
| |
| if type not in alltypes: |
| cmds.append(localdata.expand("\trm ${IMAGE_NAME}.rootfs.${type}")) |
| else: |
| subimages.append(type) |
| |
| script_name = self._write_script(type, cmds) |
| |
| image_cmds.append((type, subimages, script_name)) |
| |
| image_cmd_groups.append(image_cmds) |
| |
| return image_cmd_groups |
| |
| def _write_wic_env(self): |
| """ |
| Write environment variables used by wic |
| to tmp/sysroots/<machine>/imgdata/<image>.env |
| """ |
| stdir = self.d.getVar('STAGING_DIR_TARGET', True) |
| outdir = os.path.join(stdir, 'imgdata') |
| if not os.path.exists(outdir): |
| os.makedirs(outdir) |
| basename = self.d.getVar('IMAGE_BASENAME', True) |
| with open(os.path.join(outdir, basename) + '.env', 'w') as envf: |
| for var in self.d.getVar('WICVARS', True).split(): |
| value = self.d.getVar(var, True) |
| if value: |
| envf.write('%s="%s"\n' % (var, value.strip())) |
| |
| def create(self): |
| bb.note("###### Generate images #######") |
| pre_process_cmds = self.d.getVar("IMAGE_PREPROCESS_COMMAND", True) |
| post_process_cmds = self.d.getVar("IMAGE_POSTPROCESS_COMMAND", True) |
| |
| execute_pre_post_process(self.d, pre_process_cmds) |
| |
| self._remove_old_symlinks() |
| |
| image_cmd_groups = self._get_imagecmds() |
| |
| self._write_wic_env() |
| |
| for image_cmds in image_cmd_groups: |
| # create the images in parallel |
| nproc = multiprocessing.cpu_count() |
| pool = bb.utils.multiprocessingpool(nproc) |
| results = list(pool.imap(generate_image, image_cmds)) |
| pool.close() |
| pool.join() |
| |
| for result in results: |
| if result is not None: |
| bb.fatal(result) |
| |
| for image_type, subimages, script in image_cmds: |
| bb.note("Creating symlinks for %s image ..." % image_type) |
| self._create_symlinks(subimages) |
| |
| execute_pre_post_process(self.d, post_process_cmds) |
| |
| |
| def create_image(d): |
| Image(d).create() |
| |
| if __name__ == "__main__": |
| """ |
| Image creation can be called independent from bitbake environment. |
| """ |
| """ |
| TBD |
| """ |