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