#!/usr/bin/python3 # mkefiboot - a tool to make EFI boot images # # Copyright (C) 2011-2015 Red Hat, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Red Hat Author(s): Will Woods import logging logging.basicConfig() log = logging.getLogger() import os, tempfile, argparse from subprocess import check_call, PIPE from pylorax.imgutils import mkdosimg, round_to_blocks, LoopDev, DMDev, dm_detach from pylorax.imgutils import mkhfsimg, Mount, estimate_size import struct, shutil, glob def mkefiboot(bootdir, outfile, label): '''Make an EFI boot image with the contents of bootdir in EFI/BOOT''' mkdosimg(None, outfile, label=label, graft={'EFI/BOOT':bootdir}) def mkmacboot(bootdir, outfile, label, icon=None, product='Generic', diskname=None): '''Make an EFI boot image for Apple's EFI implementation''' graft = {'EFI/BOOT':bootdir} if icon and os.path.exists(icon): graft['.VolumeIcon.icns'] = icon if diskname and os.path.exists(diskname): graft['EFI/BOOT/.disk_label'] = diskname # shim and grub2 are in 2 places so double the estimate size = estimate_size(None, graft=graft) * 2 mkhfsimg(None, outfile, label=label, graft=graft, size=size) macmunge(outfile, product) # To make an HFS+ image bootable, we need to fill in parts of the # HFSPlusVolumeHeader structure - specifically, finderInfo[0,1,5]. # For details, see Technical Note TN1150: HFS Plus Volume Format # http://developer.apple.com/library/mac/#technotes/tn/tn1150.html # # Additionally, we want to do some fixups to make it play nicely with # the startup disk preferences panel. def macmunge(imgfile, product): '''"bless" the EFI bootloader inside the given Mac EFI boot image, by writing its inode info into the HFS+ volume header.''' # Get the inode number for the boot image and its parent directory with LoopDev(imgfile) as loopdev: with Mount(loopdev) as mnt: shim = glob.glob(os.path.join(mnt, 'EFI/BOOT/BOOT*.EFI'))[0] loader = glob.glob(os.path.join(mnt,'EFI/BOOT/grub*.efi'))[0] config = glob.glob(os.path.join(mnt,'EFI/*/grub*.cfg'))[0] blessnode = os.stat(loader).st_ino dirnode = os.stat(os.path.dirname(loader)).st_ino with open(os.path.join(mnt,'mach_kernel'), 'w') as kernel: kernel.write('Dummy kernel for booting') sysdir = os.path.join(mnt,'System/Library/CoreServices/') os.makedirs(sysdir) with open(os.path.join(sysdir,'SystemVersion.plist'), 'w') as plist: plist.write(''' ProductBuildVersion ProductName Linux ProductVersion %s ''' % (product,)) # NOTE: OSX won't boot if we hardlink to /EFI/BOOT and grub2 # can't read the config file if we hardlink the other direction # So copy the files. shutil.copy(shim, os.path.join(sysdir,'boot.efi')) shutil.copy(loader, sysdir) shutil.copy(config, sysdir) # format data properly (big-endian UInt32) nodedata = struct.pack(">i", blessnode) dirdata = struct.pack(">i", dirnode) # Write it to the volume header with open(imgfile, "r+b") as img: img.seek(0x450) # HFSPlusVolumeHeader->finderInfo img.write(dirdata) # finderInfo[0] img.write(nodedata) # finderInfo[1] img.seek(0x464) # img.write(dirdata) # finderInfo[5] def mkefidisk(efiboot, outfile): '''Make a bootable EFI disk image out of the given EFI boot image.''' # pjones sez: "17408 is the size of the GPT tables parted creates, # but there are two of them." partsize = os.path.getsize(efiboot) + 17408 * 2 disksize = round_to_blocks(2 * 17408 + partsize, 512) with LoopDev(outfile, disksize) as loopdev: with DMDev(loopdev, disksize) as dmdev: check_call(["parted", "--script", "/dev/mapper/%s" % dmdev, "mklabel", "gpt", "unit", "b", "mkpart", "'EFI System Partition'", "fat32", "34816", str(partsize), "set", "1", "boot", "on"], stdout=PIPE, stderr=PIPE) partdev = "/dev/mapper/{0}p1".format(dmdev) with open(efiboot, "rb") as infile: with open(partdev, "wb") as outfile: outfile.write(infile.read()) dm_detach(dmdev+"p1") def main(): parser = argparse.ArgumentParser(description="Make an EFI boot image from the given directory.") parser.add_argument("--debug", action="store_const", const=logging.DEBUG, dest="loglevel", default=log.getEffectiveLevel(), help="print debugging info") parser.add_argument("-d", "--disk", action="store_true", help="make a full EFI disk image (including partition table)") parser.add_argument("-a", "--apple", action="store_const", const="apple", dest="imgtype", default="default", help="make an Apple EFI image (use hfs+, bless bootloader)") parser.add_argument("-l", "--label", default="EFI", help="filesystem label to use (default: %(default)s)") parser.add_argument("-i", "--icon", metavar="ICONFILE", help="icon file to include (for Apple EFI image)") parser.add_argument("-n", "--diskname", metavar="DISKNAME", help="disk name image to include (for Apple EFI image)") parser.add_argument("-p", "--product", metavar="PRODUCT", help="product name to use (for Apple EFI image)") parser.add_argument("bootdir", metavar="EFIBOOTDIR", help="input directory (will become /EFI/BOOT in the image)") parser.add_argument("outfile", metavar="OUTPUTFILE", help="output file to write") opt = parser.parse_args() # logging log.setLevel(opt.loglevel) # sanity checks if not os.path.isdir(opt.bootdir): parser.error("%s is not a directory" % opt.bootdir) if os.getuid() > 0: parser.error("need root permissions") if opt.icon and not opt.imgtype == "apple": print("Warning: --icon is only useful for Apple EFI images") if opt.diskname and not opt.imgtype == "apple": print("Warning: --diskname is only useful for Apple EFI images") log.debug(opt) # do the thing! if opt.imgtype == "apple": mkmacboot(opt.bootdir, opt.outfile, opt.label, opt.icon, opt.product, opt.diskname) else: mkefiboot(opt.bootdir, opt.outfile, opt.label) if opt.disk: efiboot = tempfile.NamedTemporaryFile(prefix="mkefiboot.").name shutil.move(opt.outfile, efiboot) mkefidisk(efiboot, opt.outfile) if __name__ == '__main__': main()