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