1# 2# Copyright OpenEmbedded Contributors 3# 4# SPDX-License-Identifier: GPL-2.0-only 5# 6# DESCRIPTION 7# This implements the 'isoimage-isohybrid' source plugin class for 'wic' 8# 9# AUTHORS 10# Mihaly Varga <mihaly.varga (at] ni.com> 11 12import glob 13import logging 14import os 15import re 16import shutil 17 18from wic import WicError 19from wic.engine import get_custom_config 20from wic.pluginbase import SourcePlugin 21from wic.misc import exec_cmd, exec_native_cmd, get_bitbake_var 22 23logger = logging.getLogger('wic') 24 25class IsoImagePlugin(SourcePlugin): 26 """ 27 Create a bootable ISO image 28 29 This plugin creates a hybrid, legacy and EFI bootable ISO image. The 30 generated image can be used on optical media as well as USB media. 31 32 Legacy boot uses syslinux and EFI boot uses grub or gummiboot (not 33 implemented yet) as bootloader. The plugin creates the directories required 34 by bootloaders and populates them by creating and configuring the 35 bootloader files. 36 37 Example kickstart file: 38 part /boot --source isoimage-isohybrid --sourceparams="loader=grub-efi, \\ 39 image_name= IsoImage" --ondisk cd --label LIVECD 40 bootloader --timeout=10 --append=" " 41 42 In --sourceparams "loader" specifies the bootloader used for booting in EFI 43 mode, while "image_name" specifies the name of the generated image. In the 44 example above, wic creates an ISO image named IsoImage-cd.direct (default 45 extension added by direct imeger plugin) and a file named IsoImage-cd.iso 46 """ 47 48 name = 'isoimage-isohybrid' 49 50 @classmethod 51 def do_configure_syslinux(cls, creator, cr_workdir): 52 """ 53 Create loader-specific (syslinux) config 54 """ 55 splash = os.path.join(cr_workdir, "ISO/boot/splash.jpg") 56 if os.path.exists(splash): 57 splashline = "menu background splash.jpg" 58 else: 59 splashline = "" 60 61 bootloader = creator.ks.bootloader 62 63 syslinux_conf = "" 64 syslinux_conf += "PROMPT 0\n" 65 syslinux_conf += "TIMEOUT %s \n" % (bootloader.timeout or 10) 66 syslinux_conf += "\n" 67 syslinux_conf += "ALLOWOPTIONS 1\n" 68 syslinux_conf += "SERIAL 0 115200\n" 69 syslinux_conf += "\n" 70 if splashline: 71 syslinux_conf += "%s\n" % splashline 72 syslinux_conf += "DEFAULT boot\n" 73 syslinux_conf += "LABEL boot\n" 74 75 kernel = get_bitbake_var("KERNEL_IMAGETYPE") 76 if get_bitbake_var("INITRAMFS_IMAGE_BUNDLE") == "1": 77 if get_bitbake_var("INITRAMFS_IMAGE"): 78 kernel = "%s-%s.bin" % \ 79 (get_bitbake_var("KERNEL_IMAGETYPE"), get_bitbake_var("INITRAMFS_LINK_NAME")) 80 81 syslinux_conf += "KERNEL /" + kernel + "\n" 82 syslinux_conf += "APPEND initrd=/initrd LABEL=boot %s\n" \ 83 % bootloader.append 84 85 logger.debug("Writing syslinux config %s/ISO/isolinux/isolinux.cfg", 86 cr_workdir) 87 88 with open("%s/ISO/isolinux/isolinux.cfg" % cr_workdir, "w") as cfg: 89 cfg.write(syslinux_conf) 90 91 @classmethod 92 def do_configure_grubefi(cls, part, creator, target_dir): 93 """ 94 Create loader-specific (grub-efi) config 95 """ 96 configfile = creator.ks.bootloader.configfile 97 if configfile: 98 grubefi_conf = get_custom_config(configfile) 99 if grubefi_conf: 100 logger.debug("Using custom configuration file %s for grub.cfg", 101 configfile) 102 else: 103 raise WicError("configfile is specified " 104 "but failed to get it from %s", configfile) 105 else: 106 splash = os.path.join(target_dir, "splash.jpg") 107 if os.path.exists(splash): 108 splashline = "menu background splash.jpg" 109 else: 110 splashline = "" 111 112 bootloader = creator.ks.bootloader 113 114 grubefi_conf = "" 115 grubefi_conf += "serial --unit=0 --speed=115200 --word=8 " 116 grubefi_conf += "--parity=no --stop=1\n" 117 grubefi_conf += "default=boot\n" 118 grubefi_conf += "timeout=%s\n" % (bootloader.timeout or 10) 119 grubefi_conf += "\n" 120 grubefi_conf += "search --set=root --label %s " % part.label 121 grubefi_conf += "\n" 122 grubefi_conf += "menuentry 'boot'{\n" 123 124 kernel = get_bitbake_var("KERNEL_IMAGETYPE") 125 if get_bitbake_var("INITRAMFS_IMAGE_BUNDLE") == "1": 126 if get_bitbake_var("INITRAMFS_IMAGE"): 127 kernel = "%s-%s.bin" % \ 128 (get_bitbake_var("KERNEL_IMAGETYPE"), get_bitbake_var("INITRAMFS_LINK_NAME")) 129 130 grubefi_conf += "linux /%s rootwait %s\n" \ 131 % (kernel, bootloader.append) 132 grubefi_conf += "initrd /initrd \n" 133 grubefi_conf += "}\n" 134 135 if splashline: 136 grubefi_conf += "%s\n" % splashline 137 138 cfg_path = os.path.join(target_dir, "grub.cfg") 139 logger.debug("Writing grubefi config %s", cfg_path) 140 141 with open(cfg_path, "w") as cfg: 142 cfg.write(grubefi_conf) 143 144 @staticmethod 145 def _build_initramfs_path(rootfs_dir, cr_workdir): 146 """ 147 Create path for initramfs image 148 """ 149 150 initrd = get_bitbake_var("INITRD_LIVE") or get_bitbake_var("INITRD") 151 if not initrd: 152 initrd_dir = get_bitbake_var("DEPLOY_DIR_IMAGE") 153 if not initrd_dir: 154 raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting.") 155 156 image_name = get_bitbake_var("IMAGE_BASENAME") 157 if not image_name: 158 raise WicError("Couldn't find IMAGE_BASENAME, exiting.") 159 160 image_type = get_bitbake_var("INITRAMFS_FSTYPES") 161 if not image_type: 162 raise WicError("Couldn't find INITRAMFS_FSTYPES, exiting.") 163 164 machine = os.path.basename(initrd_dir) 165 166 pattern = '%s/%s*%s.%s' % (initrd_dir, image_name, machine, image_type) 167 files = glob.glob(pattern) 168 if files: 169 initrd = files[0] 170 171 if not initrd or not os.path.exists(initrd): 172 # Create initrd from rootfs directory 173 initrd = "%s/initrd.cpio.gz" % cr_workdir 174 initrd_dir = "%s/INITRD" % cr_workdir 175 shutil.copytree("%s" % rootfs_dir, \ 176 "%s" % initrd_dir, symlinks=True) 177 178 if os.path.isfile("%s/init" % rootfs_dir): 179 shutil.copy2("%s/init" % rootfs_dir, "%s/init" % initrd_dir) 180 elif os.path.lexists("%s/init" % rootfs_dir): 181 os.symlink(os.readlink("%s/init" % rootfs_dir), \ 182 "%s/init" % initrd_dir) 183 elif os.path.isfile("%s/sbin/init" % rootfs_dir): 184 shutil.copy2("%s/sbin/init" % rootfs_dir, \ 185 "%s" % initrd_dir) 186 elif os.path.lexists("%s/sbin/init" % rootfs_dir): 187 os.symlink(os.readlink("%s/sbin/init" % rootfs_dir), \ 188 "%s/init" % initrd_dir) 189 else: 190 raise WicError("Couldn't find or build initrd, exiting.") 191 192 exec_cmd("cd %s && find . | cpio -o -H newc -R root:root >%s/initrd.cpio " \ 193 % (initrd_dir, cr_workdir), as_shell=True) 194 exec_cmd("gzip -f -9 %s/initrd.cpio" % cr_workdir, as_shell=True) 195 shutil.rmtree(initrd_dir) 196 197 return initrd 198 199 @classmethod 200 def do_configure_partition(cls, part, source_params, creator, cr_workdir, 201 oe_builddir, bootimg_dir, kernel_dir, 202 native_sysroot): 203 """ 204 Called before do_prepare_partition(), creates loader-specific config 205 """ 206 isodir = "%s/ISO/" % cr_workdir 207 208 if os.path.exists(isodir): 209 shutil.rmtree(isodir) 210 211 install_cmd = "install -d %s " % isodir 212 exec_cmd(install_cmd) 213 214 # Overwrite the name of the created image 215 logger.debug(source_params) 216 if 'image_name' in source_params and \ 217 source_params['image_name'].strip(): 218 creator.name = source_params['image_name'].strip() 219 logger.debug("The name of the image is: %s", creator.name) 220 221 @staticmethod 222 def _install_payload(source_params, iso_dir): 223 """ 224 Copies contents of payload directory (as specified in 'payload_dir' param) into iso_dir 225 """ 226 227 if source_params.get('payload_dir'): 228 payload_dir = source_params['payload_dir'] 229 230 logger.debug("Payload directory: %s", payload_dir) 231 shutil.copytree(payload_dir, iso_dir, symlinks=True, dirs_exist_ok=True) 232 233 @classmethod 234 def do_prepare_partition(cls, part, source_params, creator, cr_workdir, 235 oe_builddir, bootimg_dir, kernel_dir, 236 rootfs_dir, native_sysroot): 237 """ 238 Called to do the actual content population for a partition i.e. it 239 'prepares' the partition to be incorporated into the image. 240 In this case, prepare content for a bootable ISO image. 241 """ 242 243 isodir = "%s/ISO" % cr_workdir 244 245 cls._install_payload(source_params, isodir) 246 247 if part.rootfs_dir is None: 248 if not 'ROOTFS_DIR' in rootfs_dir: 249 raise WicError("Couldn't find --rootfs-dir, exiting.") 250 rootfs_dir = rootfs_dir['ROOTFS_DIR'] 251 else: 252 if part.rootfs_dir in rootfs_dir: 253 rootfs_dir = rootfs_dir[part.rootfs_dir] 254 elif part.rootfs_dir: 255 rootfs_dir = part.rootfs_dir 256 else: 257 raise WicError("Couldn't find --rootfs-dir=%s connection " 258 "or it is not a valid path, exiting." % 259 part.rootfs_dir) 260 261 if not os.path.isdir(rootfs_dir): 262 rootfs_dir = get_bitbake_var("IMAGE_ROOTFS") 263 if not os.path.isdir(rootfs_dir): 264 raise WicError("Couldn't find IMAGE_ROOTFS, exiting.") 265 266 part.rootfs_dir = rootfs_dir 267 deploy_dir = get_bitbake_var("DEPLOY_DIR_IMAGE") 268 img_iso_dir = get_bitbake_var("ISODIR") 269 270 # Remove the temporary file created by part.prepare_rootfs() 271 if os.path.isfile(part.source_file): 272 os.remove(part.source_file) 273 274 # Support using a different initrd other than default 275 if source_params.get('initrd'): 276 initrd = source_params['initrd'] 277 if not deploy_dir: 278 raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting") 279 cp_cmd = "cp %s/%s %s" % (deploy_dir, initrd, cr_workdir) 280 exec_cmd(cp_cmd) 281 else: 282 # Prepare initial ramdisk 283 initrd = "%s/initrd" % deploy_dir 284 if not os.path.isfile(initrd): 285 initrd = "%s/initrd" % img_iso_dir 286 if not os.path.isfile(initrd): 287 initrd = cls._build_initramfs_path(rootfs_dir, cr_workdir) 288 289 install_cmd = "install -m 0644 %s %s/initrd" % (initrd, isodir) 290 exec_cmd(install_cmd) 291 292 # Remove the temporary file created by _build_initramfs_path function 293 if os.path.isfile("%s/initrd.cpio.gz" % cr_workdir): 294 os.remove("%s/initrd.cpio.gz" % cr_workdir) 295 296 kernel = get_bitbake_var("KERNEL_IMAGETYPE") 297 if get_bitbake_var("INITRAMFS_IMAGE_BUNDLE") == "1": 298 if get_bitbake_var("INITRAMFS_IMAGE"): 299 kernel = "%s-%s.bin" % \ 300 (get_bitbake_var("KERNEL_IMAGETYPE"), get_bitbake_var("INITRAMFS_LINK_NAME")) 301 302 install_cmd = "install -m 0644 %s/%s %s/%s" % \ 303 (kernel_dir, kernel, isodir, kernel) 304 exec_cmd(install_cmd) 305 306 #Create bootloader for efi boot 307 try: 308 target_dir = "%s/EFI/BOOT" % isodir 309 if os.path.exists(target_dir): 310 shutil.rmtree(target_dir) 311 312 os.makedirs(target_dir) 313 314 if source_params['loader'] == 'grub-efi': 315 # Builds bootx64.efi/bootia32.efi if ISODIR didn't exist or 316 # didn't contains it 317 target_arch = get_bitbake_var("TARGET_SYS") 318 if not target_arch: 319 raise WicError("Coludn't find target architecture") 320 321 if re.match("x86_64", target_arch): 322 grub_src_image = "grub-efi-bootx64.efi" 323 grub_dest_image = "bootx64.efi" 324 elif re.match('i.86', target_arch): 325 grub_src_image = "grub-efi-bootia32.efi" 326 grub_dest_image = "bootia32.efi" 327 else: 328 raise WicError("grub-efi is incompatible with target %s" % 329 target_arch) 330 331 grub_target = os.path.join(target_dir, grub_dest_image) 332 if not os.path.isfile(grub_target): 333 grub_src = os.path.join(deploy_dir, grub_src_image) 334 if not os.path.exists(grub_src): 335 raise WicError("Grub loader %s is not found in %s. " 336 "Please build grub-efi first" % (grub_src_image, deploy_dir)) 337 shutil.copy(grub_src, grub_target) 338 339 if not os.path.isfile(os.path.join(target_dir, "boot.cfg")): 340 cls.do_configure_grubefi(part, creator, target_dir) 341 342 else: 343 raise WicError("unrecognized bootimg-efi loader: %s" % 344 source_params['loader']) 345 except KeyError: 346 raise WicError("bootimg-efi requires a loader, none specified") 347 348 # Create efi.img that contains bootloader files for EFI booting 349 # if ISODIR didn't exist or didn't contains it 350 if os.path.isfile("%s/efi.img" % img_iso_dir): 351 install_cmd = "install -m 0644 %s/efi.img %s/efi.img" % \ 352 (img_iso_dir, isodir) 353 exec_cmd(install_cmd) 354 else: 355 # Default to 100 blocks of extra space for file system overhead 356 esp_extra_blocks = int(source_params.get('esp_extra_blocks', '100')) 357 358 du_cmd = "du -bks %s/EFI" % isodir 359 out = exec_cmd(du_cmd) 360 blocks = int(out.split()[0]) 361 blocks += esp_extra_blocks 362 logger.debug("Added 100 extra blocks to %s to get to %d " 363 "total blocks", part.mountpoint, blocks) 364 365 # dosfs image for EFI boot 366 bootimg = "%s/efi.img" % isodir 367 368 esp_label = source_params.get('esp_label', 'EFIimg') 369 370 dosfs_cmd = 'mkfs.vfat -n \'%s\' -S 512 -C %s %d' \ 371 % (esp_label, bootimg, blocks) 372 exec_native_cmd(dosfs_cmd, native_sysroot) 373 374 mmd_cmd = "mmd -i %s ::/EFI" % bootimg 375 exec_native_cmd(mmd_cmd, native_sysroot) 376 377 mcopy_cmd = "mcopy -i %s -s %s/EFI/* ::/EFI/" \ 378 % (bootimg, isodir) 379 exec_native_cmd(mcopy_cmd, native_sysroot) 380 381 chmod_cmd = "chmod 644 %s" % bootimg 382 exec_cmd(chmod_cmd) 383 384 # Prepare files for legacy boot 385 syslinux_dir = get_bitbake_var("STAGING_DATADIR") 386 if not syslinux_dir: 387 raise WicError("Couldn't find STAGING_DATADIR, exiting.") 388 389 if os.path.exists("%s/isolinux" % isodir): 390 shutil.rmtree("%s/isolinux" % isodir) 391 392 install_cmd = "install -d %s/isolinux" % isodir 393 exec_cmd(install_cmd) 394 395 cls.do_configure_syslinux(creator, cr_workdir) 396 397 install_cmd = "install -m 444 %s/syslinux/ldlinux.sys " % syslinux_dir 398 install_cmd += "%s/isolinux/ldlinux.sys" % isodir 399 exec_cmd(install_cmd) 400 401 install_cmd = "install -m 444 %s/syslinux/isohdpfx.bin " % syslinux_dir 402 install_cmd += "%s/isolinux/isohdpfx.bin" % isodir 403 exec_cmd(install_cmd) 404 405 install_cmd = "install -m 644 %s/syslinux/isolinux.bin " % syslinux_dir 406 install_cmd += "%s/isolinux/isolinux.bin" % isodir 407 exec_cmd(install_cmd) 408 409 install_cmd = "install -m 644 %s/syslinux/ldlinux.c32 " % syslinux_dir 410 install_cmd += "%s/isolinux/ldlinux.c32" % isodir 411 exec_cmd(install_cmd) 412 413 #create ISO image 414 iso_img = "%s/tempiso_img.iso" % cr_workdir 415 iso_bootimg = "isolinux/isolinux.bin" 416 iso_bootcat = "isolinux/boot.cat" 417 efi_img = "efi.img" 418 419 mkisofs_cmd = "mkisofs -V %s " % part.label 420 mkisofs_cmd += "-o %s -U " % iso_img 421 mkisofs_cmd += "-J -joliet-long -r -iso-level 2 -b %s " % iso_bootimg 422 mkisofs_cmd += "-c %s -no-emul-boot -boot-load-size 4 " % iso_bootcat 423 mkisofs_cmd += "-boot-info-table -eltorito-alt-boot " 424 mkisofs_cmd += "-eltorito-platform 0xEF -eltorito-boot %s " % efi_img 425 mkisofs_cmd += "-no-emul-boot %s " % isodir 426 427 logger.debug("running command: %s", mkisofs_cmd) 428 exec_native_cmd(mkisofs_cmd, native_sysroot) 429 430 shutil.rmtree(isodir) 431 432 du_cmd = "du -Lbks %s" % iso_img 433 out = exec_cmd(du_cmd) 434 isoimg_size = int(out.split()[0]) 435 436 part.size = isoimg_size 437 part.source_file = iso_img 438 439 @classmethod 440 def do_install_disk(cls, disk, disk_name, creator, workdir, oe_builddir, 441 bootimg_dir, kernel_dir, native_sysroot): 442 """ 443 Called after all partitions have been prepared and assembled into a 444 disk image. In this case, we insert/modify the MBR using isohybrid 445 utility for booting via BIOS from disk storage devices. 446 """ 447 448 iso_img = "%s.p1" % disk.path 449 full_path = creator._full_path(workdir, disk_name, "direct") 450 full_path_iso = creator._full_path(workdir, disk_name, "iso") 451 452 isohybrid_cmd = "isohybrid -u %s" % iso_img 453 logger.debug("running command: %s", isohybrid_cmd) 454 exec_native_cmd(isohybrid_cmd, native_sysroot) 455 456 # Replace the image created by direct plugin with the one created by 457 # mkisofs command. This is necessary because the iso image created by 458 # mkisofs has a very specific MBR is system area of the ISO image, and 459 # direct plugin adds and configures an another MBR. 460 logger.debug("Replaceing the image created by direct plugin\n") 461 os.remove(disk.path) 462 shutil.copy2(iso_img, full_path_iso) 463 shutil.copy2(full_path_iso, full_path) 464