You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
438 lines
20 KiB
Python
438 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*
|
|
# Name: grub.py
|
|
# Purpose: This module contain many functions used for updating grub.cfg file to provide support for EFI/UEFI booting
|
|
# Authors: Sundar
|
|
# Licence: This file is a part of multibootusb package. You can redistribute it or modify
|
|
# under the terms of GNU General Public License, v.2 or above
|
|
import os
|
|
import re
|
|
from . import config
|
|
from . import iso
|
|
from . import _7zip
|
|
from . import gen
|
|
from .usb import bytes2human
|
|
from . import menus
|
|
|
|
|
|
def mbusb_update_grub_cfg():
|
|
"""
|
|
Function to update grub.cfg file to support UEFI/EFI systems
|
|
:return:
|
|
"""
|
|
|
|
install_dir = os.path.join(config.usb_mount, 'multibootusb',
|
|
iso.iso_basename(config.image_path))
|
|
grub_cfg_path = None
|
|
syslinux_menu = None
|
|
mbus_grub_cfg_path = os.path.join(config.usb_mount, 'multibootusb',
|
|
'grub', 'grub.cfg')
|
|
isobin_dir = iso.isolinux_bin_dir(config.image_path)
|
|
if isobin_dir is not False:
|
|
for name in ['syslinux.cfg', 'isolinux.cfg']:
|
|
cfg_path = os.path.join(isobin_dir, name)
|
|
cfg_fullpath = os.path.join(install_dir, cfg_path)
|
|
if os.path.exists(cfg_fullpath):
|
|
syslinux_menu = cfg_path.replace('\\', '/')
|
|
break
|
|
|
|
# Decide which grub config file to boot by.
|
|
loopback_cfg_list = iso.get_file_list(
|
|
config.image_path,
|
|
lambda x: os.path.basename(x).lower()=='loopback.cfg')
|
|
grub_cfg_list = iso.get_file_list(
|
|
config.image_path,
|
|
lambda x: os.path.basename(x).lower().startswith('grub') and
|
|
os.path.basename(x).lower().endswith('.cfg'))
|
|
# favour 'grub.cfg' over variants.
|
|
flagged = [(f, os.path.basename(f).lower()=='grub.cfg')
|
|
for f in grub_cfg_list]
|
|
grub_cfg_list = [ x[0] for x in flagged if x[1] ] + \
|
|
[ x[0] for x in flagged if not x[1] ]
|
|
candidates = []
|
|
for src_list, predicate in [
|
|
# List in the order of decreasing preference.
|
|
(loopback_cfg_list, lambda x: 'efi' in x.lower()),
|
|
(loopback_cfg_list, lambda x: 'boot' in x.lower()),
|
|
(grub_cfg_list, lambda x: 'efi' in x.lower()),
|
|
(grub_cfg_list, lambda x: 'boot' in x.lower() and 'efi' not in x.lower()),
|
|
(loopback_cfg_list,
|
|
lambda x: 'efi' not in x.lower() and 'boot' not in x.lower()),
|
|
(grub_cfg_list,
|
|
lambda x: 'efi' not in x.lower() and 'boot' not in x.lower())]:
|
|
sub_candidates = [x for x in src_list if predicate(x)]
|
|
if len(sub_candidates):
|
|
candidates.append(sub_candidates[0])
|
|
# We could 'break' here but will let the iteration continue
|
|
# in order to lower the chance of keeping latent bugs.
|
|
|
|
if config.distro == 'mageialive' and 1<len(candidates):
|
|
grub_cfg_path = candidates[1].replace('\\', '/')
|
|
elif 0<len(candidates):
|
|
grub_cfg_path = candidates[0].replace('\\', '/')
|
|
else :
|
|
# No suitable grub configuration file is provided by distro.
|
|
# Lets convert syslinux config files to grub2 accepted file format.
|
|
new_loopback_here = 'loopback.cfg'
|
|
try:
|
|
# First write custom loopback.cfg file so as to be detected
|
|
# by iso2grub2 function later.
|
|
write_custom_grub_cfg(install_dir, new_loopback_here)
|
|
gen.log('Trying to create loopback.cfg')
|
|
iso2grub2(install_dir, new_loopback_here)
|
|
except Exception as e:
|
|
new_loopback_here = None
|
|
gen.log(e)
|
|
gen.log('Error converting syslinux cfg to grub2 cfg', error=True)
|
|
if new_loopback_here:
|
|
grub_cfg_path = new_loopback_here.replace('\\', '/')
|
|
#elif bootx_64_cfg is not False:
|
|
# grub_cfg_path = bootx_64_cfg.replace('\\', '/')
|
|
gen.log("Using %s to boot this distro." % grub_cfg_path)
|
|
|
|
if os.path.exists(mbus_grub_cfg_path):
|
|
gen.log('Updating grub.cfg file...')
|
|
if grub_custom_menu(mbus_grub_cfg_path, config.distro) is False:
|
|
with open(mbus_grub_cfg_path, 'a') as f:
|
|
f.write("#start " + iso.iso_basename(config.image_path) + "\n")
|
|
if grub_cfg_path is not None:
|
|
if config.distro == 'grub2only':
|
|
f.write(' menuentry ' + iso.iso_basename(config.image_path) + ' {configfile '
|
|
+ '/' + grub_cfg_path.replace('\\', '/') + '}' + "\n")
|
|
else:
|
|
f.write(' menuentry ' + iso.iso_basename(config.image_path) + ' {configfile '
|
|
+ '/multibootusb/' + iso.iso_basename(config.image_path) + '/' + grub_cfg_path + '}' + "\n")
|
|
elif config.distro == 'f4ubcd':
|
|
f.write(' menuentry ' + iso.iso_basename(config.image_path) +
|
|
' {linux /multibootusb/grub.exe --config-file=/multibootusb' +
|
|
iso.iso_basename(config.image_path) + '/menu.lst}'"\n")
|
|
elif config.distro == 'pc-unlocker':
|
|
f.write(' menuentry ' + iso.iso_basename(config.image_path) +
|
|
' {\n linux /ldntldr\n ntldr /ntldr }' + "\n")
|
|
elif config.distro == 'ReactOS':
|
|
f.write(' menuentry ' + iso.iso_basename(config.image_path) +
|
|
' {multiboot /loader/setupldr.sys}' + "\n")
|
|
elif config.distro == 'memdisk_img':
|
|
f.write(menus.memdisk_img_cfg(syslinux=False, grub=True))
|
|
elif config.distro == 'memdisk_iso':
|
|
f.write(menus.memdisk_iso_cfg(syslinux=False, grub=True))
|
|
elif config.distro == 'memtest':
|
|
f.write(' menuentry ' + iso.iso_basename(config.image_path) +
|
|
' {linux16 ' + '/multibootusb/' + iso.iso_basename(config.image_path) + '/BISOLINUX/MEMTEST}' + "\n")
|
|
elif syslinux_menu is not None:
|
|
f.write(' menuentry ' + iso.iso_basename(config.image_path) + ' {syslinux_configfile '
|
|
+ '/multibootusb/' + iso.iso_basename(config.image_path) + '/' + syslinux_menu + '}' + "\n")
|
|
f.write("#end " + iso.iso_basename(config.image_path) + "\n")
|
|
|
|
# Ascertain if the entry is made..
|
|
if gen.check_text_in_file(mbus_grub_cfg_path, iso.iso_basename(config.image_path)):
|
|
gen.log('Updated entry in grub.cfg...')
|
|
else:
|
|
gen.log('Unable to update entry in grub.cfg...')
|
|
|
|
|
|
def write_custom_grub_cfg(install_dir, loopback_cfg_path):
|
|
"""
|
|
Create custom grub loopback.cfg file for known distros. Custom menu entries are stored on munus.py module
|
|
:return:
|
|
"""
|
|
loopback_cfg_path = os.path.join(
|
|
install_dir, loopback_cfg_path.lstrip(r'\/'))
|
|
menu = False
|
|
if config.distro == 'pc-tool':
|
|
menu = menus.pc_tool_config(syslinux=False, grub=True)
|
|
elif config.distro == 'rising-av':
|
|
menu = menus.rising(syslinux=False, grub=True)
|
|
|
|
if menu is not False:
|
|
gen.log('Writing custom loopback.cfg file...')
|
|
write_to_file(loopback_cfg_path, menu)
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def grub_custom_menu(mbus_grub_cfg_path, distro):
|
|
iso_size_mb = bytes2human(iso.iso_size(config.image_path))
|
|
gen.log('size of the ISO is ' + str(iso_size_mb))
|
|
if distro in ['sgrubd2', 'raw_iso']:
|
|
grub_raw_iso(mbus_grub_cfg_path)
|
|
|
|
# with open(mbus_grub_cfg_path, 'a') as f:
|
|
# f.write("#start " + iso.iso_basename(config.image_path) + "\n")
|
|
# f.write(grub_raw_iso())
|
|
# f.write("#end " + iso.iso_basename(config.image_path) + "\n")
|
|
#
|
|
#
|
|
# elif iso_size_mb < 750.0:
|
|
# grub_raw_iso(mbus_grub_cfg_path)
|
|
|
|
else:
|
|
return False
|
|
|
|
|
|
def grub_raw_iso(mbus_grub_cfg_path):
|
|
"""
|
|
Generic menu entry for booting ISO files directly using memdisk. Should have enough memory to load to RAM
|
|
:return:
|
|
"""
|
|
menu_entry = ' search --set -f /multibootusb/' + iso.iso_basename(config.image_path) + '/' + iso.iso_name(config.image_path) + '\n' \
|
|
' menuentry ' + iso.iso_basename(config.image_path) + ' {\n' \
|
|
' linux16 /multibootusb/memdisk iso raw vmalloc=750M\n' \
|
|
' initrd16 /multibootusb/' + iso.iso_basename(config.image_path) + '/' + iso.iso_name(config.image_path) + '\n' \
|
|
'}\n'
|
|
with open(mbus_grub_cfg_path, 'a') as f:
|
|
f.write("#start " + iso.iso_basename(config.image_path) + "\n")
|
|
f.write(menu_entry)
|
|
f.write("#end " + iso.iso_basename(config.image_path) + "\n")
|
|
return menu_entry
|
|
|
|
|
|
def write_to_file(file_path, _strings):
|
|
|
|
try:
|
|
with open(file_path, 'a') as f:
|
|
f.write(_strings + '\n')
|
|
except:
|
|
gen.log('Error writing to %s...' % file_path)
|
|
|
|
|
|
def locate_kernel_file(subpath, isolinux_dir):
|
|
subpath_original = subpath
|
|
# Looks like relative paths don't work in grub.
|
|
#if subpath[0] != '/':
|
|
# gen.log("Accepting a relative kernel/initrd path '%s' as is."
|
|
# % subpath)
|
|
# return subpath
|
|
if subpath[:1] != '/':
|
|
subpath = '/' + subpath
|
|
if os.path.exists(os.path.join(config.usb_mount, subpath[1:])):
|
|
gen.log("Accepting kernel/initrd path '%s' as it exists." % subpath)
|
|
return subpath
|
|
_iso_basename = iso.iso_basename(config.image_path)
|
|
subpath = subpath[1:] # strip off the leading '/'
|
|
drive_relative_prefix = 'multibootusb/' + _iso_basename + '/'
|
|
if subpath.startswith(drive_relative_prefix):
|
|
# Paths is already drive-relative make it install-dir-relative.
|
|
subpath = subpath[len(drive_relative_prefix):]
|
|
gen.log("Trying to locate kernel/initrd file '%s'" % subpath)
|
|
for d in [
|
|
os.path.join('multibootusb', _iso_basename, isolinux_dir or ''),
|
|
# Down below are dire attemps to find.
|
|
os.path.join('multibootusb', _iso_basename),
|
|
os.path.join('multibootusb', _iso_basename, 'arch'),
|
|
]:
|
|
fullpath = os.path.join(config.usb_mount, d, subpath)
|
|
if os.path.exists(fullpath):
|
|
gen.log("Digged out '%s' at '%s'" % (subpath, fullpath))
|
|
unix_style_path = os.path.join(d, subpath).\
|
|
replace('\\', '/').\
|
|
lstrip('/')
|
|
return ('/' + unix_style_path)
|
|
return subpath_original
|
|
|
|
|
|
def tweak_bootfile_path(img_file_spec, isolinux_dir):
|
|
"""
|
|
Function to find image files to boot and return them concatinated
|
|
with a space. Return the spec untouched if no locations are found.
|
|
:param kernel_file_spec: Image path specification lead by kernel/linux keyword within isolinux supported .cfg files.
|
|
:param isolinux_dir: Path to isolinux directory of an ISO
|
|
:return: Converted file paths joined by a space. If no files can be located, img_file_spec is returned unmodified.
|
|
"""
|
|
kernel_line = ''
|
|
|
|
raw_paths = img_file_spec.split(',')
|
|
converted_paths = [locate_kernel_file(p, isolinux_dir) for p in raw_paths]
|
|
if raw_paths != converted_paths: # Tweaked the paths successfully?
|
|
return ' '.join(converted_paths)
|
|
|
|
if 'z,' in img_file_spec:
|
|
# Fallback to legacy code.
|
|
# "... I found this only in dban"
|
|
iso_dir = iso.isolinux_bin_dir(config.image_path)
|
|
replacement = ' /multibootusb/' + iso.iso_basename(config.image_path) \
|
|
+ '/' \
|
|
+ iso_dir.replace('\\', '/') + '/'
|
|
return img_file_spec.replace('z,', replacement)
|
|
# Give up and return the original with replaced delimeters.
|
|
return ' '.join(img_file_spec.split(','))
|
|
|
|
|
|
def extract_initrd_params_and_fix_kernel(value, isolinux_dir):
|
|
initrd_line, others = '', []
|
|
tokens = value.split(' ')
|
|
tokens.reverse()
|
|
while 0<len(tokens):
|
|
token = tokens.pop()
|
|
if token=='linux':
|
|
# deal with 'append linux /boot/bzImage' in salitaz-rolling
|
|
if 0<len(tokens):
|
|
kernel_file = locate_kernel_file(tokens.pop(), isolinux_dir)
|
|
others.extend(['linux', kernel_file])
|
|
else:
|
|
others.append('linux')
|
|
elif token.startswith('initrd='):
|
|
paths = [locate_kernel_file(s, isolinux_dir) for s
|
|
in token[len('initrd='):].split(',')]
|
|
initrd_line = 'initrd ' + ' '.join(paths)
|
|
else:
|
|
others.append(token)
|
|
return initrd_line, ' '.join(others),
|
|
|
|
|
|
def iso2grub2(install_dir, loopback_cfg_path):
|
|
"""
|
|
Function to convert syslinux configuration to grub2 accepted configuration format. Features implemented are similar
|
|
to that of grub2 'loopback.cfg'. This 'loopback.cfg' file can be later on caled directly from grub2. The main
|
|
advantage of this function is to generate the 'loopback.cfg' file automatically without manual involvement.
|
|
:param install_dir: Path to distro install directory for looping through '.cfg' files.
|
|
:param loopback_cfg_path: Path to 'loopback.cfg' to be updated
|
|
:param file_out: Path to 'loopback.cfg' file. By default it is set to root of distro install directory.
|
|
:return:
|
|
"""
|
|
loopback_cfg_path = os.path.join(
|
|
install_dir, loopback_cfg_path.lstrip(r'\/'))
|
|
|
|
gen.log('loopback.cfg file is set to ' + loopback_cfg_path)
|
|
|
|
iso_bin_dir = iso.isolinux_bin_dir(config.image_path)
|
|
seen_menu_entries = []
|
|
# Loop though the distro installed directory for finding config files
|
|
for dirpath, dirnames, filenames in os.walk(install_dir):
|
|
for f in filenames:
|
|
# We will strict to only files ending with '.cfg' extension. This is the file extension isolinux or syslinux
|
|
# recommends for writing configurations
|
|
if not f.endswith((".cfg", ".CFG")):
|
|
continue
|
|
cfg_file_path = os.path.join(dirpath, f)
|
|
# We will omit the grub directory
|
|
if 'grub' in cfg_file_path:
|
|
continue
|
|
# we will use only files containing strings which can be converted to grub2 cfg style
|
|
with open(cfg_file_path, "r", errors='ignore') as f:
|
|
data = f.read()
|
|
|
|
# Make sure that lines with 'label' available for processing.
|
|
# Do nothing otherwise.
|
|
matching_blocks_re = [m for m in re.finditer(
|
|
'^(label)(.*?)(?=(^label))',
|
|
data, re.I|re.DOTALL|re.MULTILINE)]
|
|
|
|
if matching_blocks_re:
|
|
matching_blocks = [m.group() for m in matching_blocks_re]
|
|
# Append the block after the last matching position
|
|
matching_blocks.append(data[matching_blocks_re[-1].span()[1]:])
|
|
else:
|
|
m = re.search('^(label)(.*?)',
|
|
data, re.I|re.DOTALL|re.MULTILINE)
|
|
matching_blocks = m and [data[m.start():]] or []
|
|
|
|
if not matching_blocks:
|
|
continue
|
|
|
|
#if not cfg_file_path.endswith('archiso_pxe64.cfg'):
|
|
# continue
|
|
gen.log("Probing '%s'" % cfg_file_path)
|
|
out_lines = []
|
|
for matching_block in matching_blocks:
|
|
#print ('------------ block begins here ------------')
|
|
#print (matching_block)
|
|
#print ('------------ block ends here ------------')
|
|
# Extract 'menu label or 'label' line.
|
|
matches = re.findall(
|
|
r'^\s*(menu label|label)\s+(.*)$',
|
|
matching_block, re.I|re.MULTILINE)
|
|
labels = [v for v in matches if v[0].lower()=='label']
|
|
menu_labels = [v for v in matches if v[0].lower()=='menu label']
|
|
if 0 == len(labels) + len(menu_labels):
|
|
gen.log('Warning: found a block without menu-entry.')
|
|
menu_entry = 'Anonymous'
|
|
menu_label = 'Unlabeled'
|
|
else:
|
|
for vec, name in [ (labels, 'label'),
|
|
(menu_labels, 'menu label') ]:
|
|
if 2 <= len(vec):
|
|
gen.log("warning: found a block with more than "
|
|
"one '%s' entries." % name)
|
|
# Prefer 'menu label' over 'label'.
|
|
if 0<len(menu_labels):
|
|
value = menu_labels[0][1].replace('^', '')
|
|
else:
|
|
value = labels[0][1]
|
|
menu_entry = menu_label = value
|
|
|
|
# Extract lines containing 'kernel','linux','initrd'
|
|
# or 'append' to convert them into grub2 compatible ones.
|
|
linux_line = initrd_line = None
|
|
appends = []
|
|
sought_archs = ['x86_64', 'i686', 'i386']
|
|
arch = []
|
|
for keyword, value in re.findall(
|
|
r'^\s*(kernel|linux|initrd|append)[= ](.*)$',
|
|
matching_block, re.I|re.MULTILINE):
|
|
kw = keyword.lower()
|
|
if kw in ['kernel', 'linux']:
|
|
if linux_line:
|
|
gen.log("Warning: found more than one "
|
|
"'kernel/linux' lines in block '%s'."
|
|
% menu_label)
|
|
continue
|
|
arch = [(value.find(a), a) for a in sought_archs
|
|
if 0 <= value.find(a)]
|
|
linux_line = 'linux ' + \
|
|
tweak_bootfile_path(value, iso_bin_dir)
|
|
elif kw == 'initrd':
|
|
if initrd_line:
|
|
gen.log("Warning: found more than one "
|
|
"'initrd' specifications in block '%s'."
|
|
% menu_label)
|
|
continue
|
|
initrd_line = 'initrd ' + \
|
|
tweak_bootfile_path(value, iso_bin_dir)
|
|
elif kw== 'append':
|
|
new_initrd_line, new_value \
|
|
= extract_initrd_params_and_fix_kernel(
|
|
value, iso_bin_dir)
|
|
appends.append(new_value)
|
|
if new_initrd_line:
|
|
if initrd_line:
|
|
gen.log("Warning: found more than one initrd "
|
|
"specifications in block '%s'."
|
|
% menu_label)
|
|
initrd_line = new_initrd_line
|
|
if arch: # utilize left most arch.
|
|
menu_entry += (" (%s)" % sorted(arch)[-1][1])
|
|
menu_line = 'menuentry ' + gen.quote(menu_entry)
|
|
if menu_entry in seen_menu_entries:
|
|
out_lines.append( "# '%s' is superceded by the previous "
|
|
"definition." % menu_label)
|
|
else:
|
|
if linux_line or initrd_line:
|
|
seen_menu_entries.append(menu_entry)
|
|
out_lines.append(menu_line + ' {')
|
|
for starter, value in [
|
|
(linux_line, ' '.join(appends)),
|
|
(initrd_line, '')]:
|
|
vec = [x for x in [starter, value] if x]
|
|
if vec:
|
|
out_lines.append(' ' + ' '.join(vec))
|
|
out_lines.append( '}' )
|
|
else:
|
|
out_lines.append("# Avoided emitting an empty "
|
|
"menu item '%s'." % menu_label)
|
|
|
|
with open(loopback_cfg_path, 'a') as f:
|
|
f.write('# Extracted from %s\n' %
|
|
cfg_file_path.replace('\\', '/'))
|
|
f.write('\n'.join(out_lines) + '\n')
|
|
f.write('\n')
|
|
|
|
if os.path.exists(loopback_cfg_path):
|
|
gen.log(
|
|
'loopback.cfg file is successfully created.\nYou must send this file for debugging if something goes wrong.')
|
|
return loopback_cfg_path
|
|
else:
|
|
gen.log('Failed to convert syslinux config file to loopback.cfg')
|
|
return False
|