Skip to content

Commit

Permalink
Add support for unified kernel image (archlinux#1519)
Browse files Browse the repository at this point in the history
  • Loading branch information
codefiles authored Oct 17, 2023
1 parent 332ec0d commit bc3b3a3
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 47 deletions.
10 changes: 10 additions & 0 deletions archinstall/lib/global_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from . import disk
from .general import secret
from .hardware import SysInfo
from .locale.locale_menu import LocaleConfiguration, LocaleMenu
from .menu import Selector, AbstractMenu
from .mirrors import MirrorConfiguration, MirrorMenu
Expand All @@ -20,6 +21,7 @@
from .interactions import ask_for_additional_users
from .interactions import ask_for_audio_selection
from .interactions import ask_for_bootloader
from .interactions import ask_for_uki
from .interactions import ask_for_swap
from .interactions import ask_hostname
from .interactions import ask_to_configure_network
Expand Down Expand Up @@ -85,6 +87,11 @@ def setup_selection_menu_options(self):
lambda preset: ask_for_bootloader(preset),
display_func=lambda x: x.value,
default=Bootloader.get_default())
self._menu_options['uki'] = \
Selector(
_('Unified kernel images'),
lambda preset: ask_for_uki(preset),
default=False)
self._menu_options['hostname'] = \
Selector(
_('Hostname'),
Expand Down Expand Up @@ -216,6 +223,9 @@ def _update_install_text(self, name: Optional[str] = None, value: Any = None):
self._menu_options['install'].update_description(text)

def post_callback(self, name: Optional[str] = None, value: Any = None):
if not SysInfo.has_uefi():
self._menu_options['uki'].set_enabled(False)

self._update_install_text(name, value)

def _install_text(self):
Expand Down
166 changes: 121 additions & 45 deletions archinstall/lib/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,18 +542,13 @@ def post_install_enable_networkd_resolved(*args :str, **kwargs :str):

return True

def mkinitcpio(self, flags: List[str], locale_config: LocaleConfiguration) -> bool:
def mkinitcpio(self, flags: List[str]) -> bool:
for plugin in plugins.values():
if hasattr(plugin, 'on_mkinitcpio'):
# Allow plugins to override the usage of mkinitcpio altogether.
if plugin.on_mkinitcpio(self):
return True

# mkinitcpio will error out if there's no vconsole.
if (vconsole := Path(f"{self.target}/etc/vconsole.conf")).exists() is False:
with vconsole.open('w') as fh:
fh.write(f"KEYMAP={locale_config.kb_layout}\n")

with open(f'{self.target}/etc/mkinitcpio.conf', 'w') as mkinit:
mkinit.write(f"MODULES=({' '.join(self.modules)})\n")
mkinit.write(f"BINARIES=({' '.join(self._binaries)})\n")
Expand Down Expand Up @@ -587,6 +582,7 @@ def minimal_installation(
self,
testing: bool = False,
multilib: bool = False,
mkinitcpio: bool = True,
hostname: str = 'archinstall',
locale_config: LocaleConfiguration = LocaleConfiguration.default()
):
Expand Down Expand Up @@ -674,7 +670,7 @@ def minimal_installation(
# TODO: Use python functions for this
SysCommand(f'/usr/bin/arch-chroot {self.target} chmod 700 /root')

if not self.mkinitcpio(['-P'], locale_config):
if mkinitcpio and not self.mkinitcpio(['-P']):
error(f"Error generating initramfs (continuing anyway)")

self.helper_flags['base'] = True
Expand Down Expand Up @@ -783,7 +779,8 @@ def _add_systemd_bootloader(
self,
boot_partition: disk.PartitionModification,
root_partition: disk.PartitionModification,
efi_partition: Optional[disk.PartitionModification]
efi_partition: Optional[disk.PartitionModification],
uki_enabled: bool = False
):
self.pacman.strap('efibootmgr')

Expand Down Expand Up @@ -815,11 +812,18 @@ def _add_systemd_bootloader(
loader_dir = self.target / 'boot/loader'
loader_dir.mkdir(parents=True, exist_ok=True)

default_kernel = self.kernels[0]
if uki_enabled:
default_entry = f'arch-{default_kernel}.efi'
else:
entry_name = self.init_time + '_{kernel}{variant}.conf'
default_entry = entry_name.format(kernel=default_kernel, variant='')

default = f'default {default_entry}'

# Modify or create a loader.conf
loader_conf = loader_dir / 'loader.conf'

default = f'default {self.init_time}_{self.kernels[0]}.conf'

try:
loader_data = loader_conf.read_text().splitlines()
except FileNotFoundError:
Expand All @@ -837,6 +841,9 @@ def _add_systemd_bootloader(

loader_conf.write_text('\n'.join(loader_data) + '\n')

if uki_enabled:
return

# Ensure that the $BOOT/loader/entries/ directory exists before we try to create files in it
entries_dir = loader_dir / 'entries'
entries_dir.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -867,7 +874,8 @@ def _add_systemd_bootloader(
options,
]

entry_conf = entries_dir / f'{self.init_time}_{kernel}{variant}.conf'
name = entry_name.format(kernel=kernel, variant=variant)
entry_conf = entries_dir / name
entry_conf.write_text('\n'.join(entry) + '\n')

self.helper_flags['bootloader'] = 'systemd'
Expand All @@ -876,17 +884,19 @@ def _add_grub_bootloader(
self,
boot_partition: disk.PartitionModification,
root_partition: disk.PartitionModification,
efi_partition: Optional[disk.PartitionModification]
efi_partition: Optional[disk.PartitionModification],
uki_enabled: bool = False
):
self.pacman.strap('grub') # no need?

grub_default = self.target / 'etc/default/grub'
config = grub_default.read_text()
if not uki_enabled:
grub_default = self.target / 'etc/default/grub'
config = grub_default.read_text()

kernel_parameters = ' '.join(self._get_kernel_params(root_partition, False, False))
config = re.sub(r'(GRUB_CMDLINE_LINUX=")("\n)', rf'\1{kernel_parameters}\2', config, 1)
kernel_parameters = ' '.join(self._get_kernel_params(root_partition, False, False))
config = re.sub(r'(GRUB_CMDLINE_LINUX=")("\n)', rf'\1{kernel_parameters}\2', config, 1)

grub_default.write_text(config)
grub_default.write_text(config)

info(f"GRUB boot partition: {boot_partition.dev_path}")

Expand Down Expand Up @@ -1067,7 +1077,8 @@ def create_pacman_hook(contents: str):
def _add_efistub_bootloader(
self,
boot_partition: disk.PartitionModification,
root_partition: disk.PartitionModification
root_partition: disk.PartitionModification,
uki_enabled: bool = False
):
self.pacman.strap('efibootmgr')

Expand All @@ -1078,41 +1089,103 @@ def _add_efistub_bootloader(
# points towards the same disk and/or partition.
# And in which case we should do some clean up.

microcode = []
if not uki_enabled:
loader = '/vmlinuz-{kernel}'

if ucode := self._get_microcode():
microcode.append(f'initrd=\\{ucode}')
else:
debug('Archinstall will not add any ucode to firmware boot entry.')
microcode = []

if ucode := self._get_microcode():
microcode.append(f'initrd=/{ucode}')
else:
debug('Archinstall will not add any ucode to firmware boot entry.')

kernel_parameters = self._get_kernel_params(root_partition)
entries = (
*microcode,
'initrd=/initramfs-{kernel}.img',
*self._get_kernel_params(root_partition)
)

cmdline = tuple(' '.join(entries))
else:
loader = '/EFI/Linux/arch-{kernel}.efi'
cmdline = tuple()

parent_dev_path = disk.device_handler.get_parent_device_path(boot_partition.safe_dev_path)

cmd_template = (
'efibootmgr',
'--create',
'--disk', str(parent_dev_path),
'--part', str(boot_partition.partn),
'--label', 'Arch Linux ({kernel})',
'--loader', loader,
'--unicode', *cmdline,
'--verbose'
)

for kernel in self.kernels:
# Setup the firmware entry
cmdline = [
*microcode,
f"initrd=\\initramfs-{kernel}.img",
*kernel_parameters,
]

cmd = [
'efibootmgr',
'--disk', str(parent_dev_path),
'--part', str(boot_partition.partn),
'--create',
'--label', f'Arch Linux ({kernel})',
'--loader', f"/vmlinuz-{kernel}",
'--unicode', ' '.join(cmdline),
'--verbose'
]

cmd = [arg.format(kernel=kernel) for arg in cmd_template]
SysCommand(cmd)

self.helper_flags['bootloader'] = "efistub"

def add_bootloader(self, bootloader: Bootloader):
def _config_uki(
self,
root_partition: disk.PartitionModification,
efi_partition: Optional[disk.PartitionModification]
):
if not efi_partition or not efi_partition.mountpoint:
raise ValueError(f'Could not detect ESP at mountpoint {self.target}')

# Set up kernel command line
with open(self.target / 'etc/kernel/cmdline', 'w') as cmdline:
kernel_parameters = self._get_kernel_params(root_partition)
cmdline.write(' '.join(kernel_parameters) + '\n')

ucode = self._get_microcode()

esp = efi_partition.mountpoint

diff_mountpoint = None
if esp != Path('/efi'):
diff_mountpoint = str(esp)

image_re = re.compile('(.+_image="/([^"]+).+\n)')
uki_re = re.compile('#((.+_uki=")/[^/]+(.+\n))')

# Modify .preset files
for kernel in self.kernels:
preset = self.target / 'etc/mkinitcpio.d' / (kernel + '.preset')
config = preset.read_text().splitlines(True)

for index, line in enumerate(config):
if not ucode and line.startswith('ALL_microcode='):
config[index] = '#' + line
# Avoid storing redundant image file
elif m := image_re.match(line):
image = self.target / m.group(2)
image.unlink(missing_ok=True)
config[index] = '#' + m.group(1)
elif m := uki_re.match(line):
if diff_mountpoint:
config[index] = m.group(2) + diff_mountpoint + m.group(3)
else:
config[index] = m.group(1)
elif line.startswith('#default_options='):
config[index] = line.removeprefix('#')

preset.write_text(''.join(config))

# Directory for the UKIs
uki_dir = self.target / esp.relative_to(Path('/')) / 'EFI/Linux'
uki_dir.mkdir(parents=True, exist_ok=True)

# Build the UKIs
if not self.mkinitcpio(['-P']):
error(f"Error generating initramfs (continuing anyway)")

def add_bootloader(self, bootloader: Bootloader, uki_enabled: bool = False):
"""
Adds a bootloader to the installation instance.
Archinstall supports one of three types:
Expand Down Expand Up @@ -1143,13 +1216,16 @@ def add_bootloader(self, bootloader: Bootloader):

info(f'Adding bootloader {bootloader.value} to {boot_partition.dev_path}')

if uki_enabled:
self._config_uki(root_partition, efi_partition)

match bootloader:
case Bootloader.Systemd:
self._add_systemd_bootloader(boot_partition, root_partition, efi_partition)
self._add_systemd_bootloader(boot_partition, root_partition, efi_partition, uki_enabled)
case Bootloader.Grub:
self._add_grub_bootloader(boot_partition, root_partition, efi_partition)
self._add_grub_bootloader(boot_partition, root_partition, efi_partition, uki_enabled)
case Bootloader.Efistub:
self._add_efistub_bootloader(boot_partition, root_partition)
self._add_efistub_bootloader(boot_partition, root_partition, uki_enabled)
case Bootloader.Limine:
self._add_limine_bootloader(boot_partition, root_partition)

Expand Down
2 changes: 1 addition & 1 deletion archinstall/lib/interactions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
)

from .system_conf import (
select_kernel, ask_for_bootloader, select_driver, ask_for_swap
select_kernel, ask_for_bootloader, ask_for_uki, select_driver, ask_for_swap
)
16 changes: 16 additions & 0 deletions archinstall/lib/interactions/system_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ def ask_for_bootloader(preset: Bootloader) -> Bootloader:
return preset


def ask_for_uki(preset: bool = True) -> bool:
if preset:
preset_val = Menu.yes()
else:
preset_val = Menu.no()

prompt = _('Would you like to use unified kernel images?')
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.no(), preset_values=preset_val).run()

match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection: return False if choice.value == Menu.no() else True

return preset


def select_driver(options: List[GfxDriver] = [], current_value: Optional[GfxDriver] = None) -> Optional[GfxDriver]:
"""
Some what convoluted function, whose job is simple.
Expand Down
9 changes: 8 additions & 1 deletion archinstall/scripts/guided.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ def ask_user_questions():
# Ask which boot-loader to use (will only ask if we're in UEFI mode, otherwise will default to GRUB)
global_menu.enable('bootloader')

global_menu.enable('uki')

global_menu.enable('swap')

# Get the hostname for the machine
Expand Down Expand Up @@ -111,6 +113,7 @@ def perform_installation(mountpoint: Path):
# Retrieve list of additional repositories and set boolean values appropriately
enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', [])
enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', [])
run_mkinitcpio = not archinstall.arguments.get('uki')
locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config']
disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None)

Expand Down Expand Up @@ -141,6 +144,7 @@ def perform_installation(mountpoint: Path):
installation.minimal_installation(
testing=enable_testing,
multilib=enable_multilib,
mkinitcpio=run_mkinitcpio,
hostname=archinstall.arguments.get('hostname', 'archlinux'),
locale_config=locale_config
)
Expand All @@ -154,7 +158,10 @@ def perform_installation(mountpoint: Path):
if archinstall.arguments.get("bootloader") == Bootloader.Grub and SysInfo.has_uefi():
installation.add_additional_packages("grub")

installation.add_bootloader(archinstall.arguments["bootloader"])
installation.add_bootloader(
archinstall.arguments["bootloader"],
archinstall.arguments["uki"]
)

# If user selected to copy the current ISO network configuration
# Perform a copy of the config
Expand Down
4 changes: 4 additions & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
"efistub"
]
},
"uki": {
"description": "Set to true to use a unified kernel images",
"type": "boolean"
},
"custom-commands": {
"description": "Custom commands to be run post install",
"type": "array",
Expand Down

0 comments on commit bc3b3a3

Please sign in to comment.