#!/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 <http://www.gnu.org/licenses/>.
#
# Red Hat Author(s):  Will Woods <wwoods@redhat.com>

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('''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ProductBuildVersion</key>
<string></string>
<key>ProductName</key>
<string>Linux</string>
<key>ProductVersion</key>
<string>%s</string>
</dict>
</plist>
''' % (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")
    # 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()
