diff --git a/archinstall/lib/bootloader/bootloader_menu.py b/archinstall/lib/bootloader/bootloader_menu.py index 88fe8f7343..65f28add95 100644 --- a/archinstall/lib/bootloader/bootloader_menu.py +++ b/archinstall/lib/bootloader/bootloader_menu.py @@ -41,6 +41,11 @@ def _define_menu_options(self) -> list[MenuItem]: if not removable_enabled: self._bootloader_conf.removable = False + # os-prober availability + os_prober_enabled = bootloader.has_os_prober_support() + if not os_prober_enabled: + self._bootloader_conf.os_prober = False + return [ MenuItem( text=tr('Bootloader'), @@ -66,6 +71,14 @@ def _define_menu_options(self) -> list[MenuItem]: key='removable', enabled=removable_enabled, ), + MenuItem( + text=tr('Detect other operating systems'), + action=self._select_os_prober, + value=self._bootloader_conf.os_prober, + preview_action=self._prev_os_prober, + key='os_prober', + enabled=os_prober_enabled, + ), MenuItem( text=tr('Plymouth'), action=self._select_plymouth, @@ -92,6 +105,13 @@ def _prev_removable(self, item: MenuItem) -> str | None: return tr('Will install to /EFI/BOOT/ (removable location, safe default)') return tr('Will install to custom location with NVRAM entry') + def _prev_os_prober(self, item: MenuItem) -> str | None: + os_prober_text = f'{tr("Detect other operating systems")}' + if item.value: + return f'{os_prober_text}: {tr("Enabled")}' + else: + return f'{os_prober_text}: {tr("Disabled")}' + def _prev_plymouth(self, item: MenuItem) -> str | None: if item.value: return f'{tr("Plymouth")}: {item.value.value}' @@ -127,6 +147,15 @@ async def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | No self._bootloader_conf.removable = True removable_item.enabled = True + # Update os-prober option based on bootloader + os_prober_item = self._menu_item_group.find_by_key('os_prober') + if not bootloader.has_os_prober_support(): + os_prober_item.enabled = False + os_prober_item.value = False + self._bootloader_conf.os_prober = False + else: + os_prober_item.enabled = True + return bootloader async def _select_plymouth(self, preset: PlymouthTheme | None) -> PlymouthTheme | None: @@ -219,6 +248,19 @@ async def _select_removable(self, preset: bool) -> bool: case ResultType.Reset: raise ValueError('Unhandled result type') + async def _select_os_prober(self, preset: bool) -> bool: + prompt = tr('Would you like to enable os-prober to detect other operating systems (e.g. Windows)?') + '\n' + + result = await Confirmation(header=prompt, allow_skip=True, preset=preset).show() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Selection: + return result.item() == MenuItem.yes() + case ResultType.Reset: + raise ValueError('Unhandled result type') + async def select_bootloader( preset: Bootloader | None, diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index aeb5c6aea1..1adbcef4ad 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1331,6 +1331,7 @@ def _add_grub_bootloader( efi_partition: PartitionModification | None, uki_enabled: bool = False, bootloader_removable: bool = False, + os_prober: bool = False, ) -> None: debug('Installing grub bootloader') @@ -1430,6 +1431,31 @@ def _add_grub_bootloader( grub_default.write_text(config) + if os_prober: + debug('Enabling os-prober in GRUB configuration') + + # fuse3 enables grub-mount, which os-prober requires to inspect + # partitions that are not mounted (e.g. Windows on another disk) + self.pacman.strap(['os-prober', 'fuse3']) + + # grub-mkconfig only runs os-prober when GRUB_DISABLE_OS_PROBER is + # explicitly set to false; the stock config ships the option commented out + grub_default = self.target / 'etc/default/grub' + config = grub_default.read_text() + + config, count = re.subn( + r'^#?GRUB_DISABLE_OS_PROBER=.*$', + 'GRUB_DISABLE_OS_PROBER=false', + config, + count=1, + flags=re.MULTILINE, + ) + + if count == 0: + config += '\nGRUB_DISABLE_OS_PROBER=false\n' + + grub_default.write_text(config) + try: self.arch_chroot( f'grub-mkconfig -o {boot_dir}/grub/grub.cfg', @@ -1833,7 +1859,12 @@ def _config_uki( error('Error generating initramfs (continuing anyway)') def add_bootloader( - self, bootloader: Bootloader, uki_enabled: bool = False, bootloader_removable: bool = False, plymouth: PlymouthTheme | None = None + self, + bootloader: Bootloader, + uki_enabled: bool = False, + bootloader_removable: bool = False, + plymouth: PlymouthTheme | None = None, + os_prober: bool = False, ) -> None: """ Adds a bootloader to the installation instance. @@ -1848,6 +1879,7 @@ def add_bootloader( :param uki_enabled: Whether to use unified kernel images :param bootloader_removable: Whether to install to removable media location (UEFI only, for GRUB and Limine) :param plymouth: Optional Plymouth theme to install and configure + :param os_prober: Whether to enable os-prober so grub-mkconfig detects other operating systems (GRUB only) """ for plugin in plugins.values(): @@ -1883,6 +1915,11 @@ def add_bootloader( warn(f'Bootloader {bootloader.value} lacks removable support; disabling.') bootloader_removable = False + # validate os-prober option + if os_prober and not bootloader.has_os_prober_support(): + warn(f'Bootloader {bootloader.value} does not support os-prober; disabling.') + os_prober = False + if plymouth is not None: self._install_plymouth(plymouth) @@ -1899,7 +1936,7 @@ def add_bootloader( case Bootloader.Systemd: self._add_systemd_bootloader(boot_partition, root, efi_partition, uki_enabled) case Bootloader.Grub: - self._add_grub_bootloader(boot_partition, root, efi_partition, uki_enabled, bootloader_removable) + self._add_grub_bootloader(boot_partition, root, efi_partition, uki_enabled, bootloader_removable, os_prober) case Bootloader.Efistub: self._add_efistub_bootloader(boot_partition, root, uki_enabled) case Bootloader.Limine: diff --git a/archinstall/lib/models/bootloader.py b/archinstall/lib/models/bootloader.py index a4900e5bb6..84cc999278 100644 --- a/archinstall/lib/models/bootloader.py +++ b/archinstall/lib/models/bootloader.py @@ -26,6 +26,9 @@ def has_removable_support(self) -> bool: case _: return False + def has_os_prober_support(self) -> bool: + return self == Bootloader.Grub + def is_uefi_only(self) -> bool: match self: case Bootloader.Systemd | Bootloader.Efistub | Bootloader.Refind: @@ -94,10 +97,11 @@ class BootloaderConfiguration(SubConfig): uki: bool = False removable: bool = True plymouth: PlymouthTheme | None = None + os_prober: bool = False @override def json(self) -> dict[str, Any]: - data = {'bootloader': self.bootloader.json(), 'uki': self.uki, 'removable': self.removable} + data = {'bootloader': self.bootloader.json(), 'uki': self.uki, 'removable': self.removable, 'os_prober': self.os_prober} if self.plymouth is not None: data['plymouth'] = self.plymouth.value @@ -111,6 +115,8 @@ def summary(self) -> list[str]: out.append(tr('UKI enabled')) if self.removable: out.append(tr('Removable')) + if self.os_prober: + out.append(tr('os-prober enabled')) if self.plymouth is not None: out.append(tr('Plymouth "{}"').format(self.plymouth.value)) @@ -122,7 +128,8 @@ def parse_arg(cls, config: dict[str, Any], skip_boot: bool) -> Self: uki = config.get('uki', False) removable = config.get('removable', True) plymouth = PlymouthTheme.from_arg(config.get('plymouth', None)) - return cls(bootloader=bootloader, uki=uki, removable=removable, plymouth=plymouth) + os_prober = config.get('os_prober', False) + return cls(bootloader=bootloader, uki=uki, removable=removable, plymouth=plymouth, os_prober=os_prober) @classmethod def get_default(cls, uefi: bool, skip_boot: bool = False) -> Self: @@ -130,7 +137,8 @@ def get_default(cls, uefi: bool, skip_boot: bool = False) -> Self: removable = uefi and bootloader.has_removable_support() uki = uefi and bootloader.has_uki_support() plymouth = None - return cls(bootloader=bootloader, uki=uki, removable=removable, plymouth=plymouth) + os_prober = False + return cls(bootloader=bootloader, uki=uki, removable=removable, plymouth=plymouth, os_prober=os_prober) def preview(self, uefi: bool) -> str: text = f'{tr("Bootloader")}: {self.bootloader.value}' @@ -149,6 +157,13 @@ def preview(self, uefi: bool) -> str: removable_string = tr('Disabled') text += f'{tr("Removable")}: {removable_string}' text += '\n' + if self.bootloader.has_os_prober_support(): + if self.os_prober: + os_prober_string = tr('Enabled') + else: + os_prober_string = tr('Disabled') + text += f'{tr("Detect other operating systems")}: {os_prober_string}' + text += '\n' if self.plymouth is not None: text += f'{tr("Plymouth")}: {self.plymouth.value}' text += '\n' diff --git a/archinstall/locales/base.pot b/archinstall/locales/base.pot index a242cbaf24..b5ee0d7f90 100644 --- a/archinstall/locales/base.pot +++ b/archinstall/locales/base.pot @@ -209,6 +209,9 @@ msgstr "" msgid "Install to removable location" msgstr "" +msgid "Detect other operating systems" +msgstr "" + msgid "Plymouth" msgstr "" @@ -259,6 +262,11 @@ msgstr "" msgid "Systems where you want the disk to be bootable on any computer." msgstr "" +msgid "" +"Would you like to enable os-prober to detect other operating systems (e.g. " +"Windows)?" +msgstr "" + msgid "Select bootloader to install" msgstr "" @@ -906,6 +914,9 @@ msgstr "" msgid "Removable" msgstr "" +msgid "os-prober enabled" +msgstr "" + #, python-brace-format msgid "Plymouth \"{}\"" msgstr "" @@ -1122,6 +1133,15 @@ msgid "" "Select any packages from the below list that should be installed additionally" msgstr "" +msgid "" +"Pacman is already running, waiting maximum 10 minutes for it to terminate." +msgstr "" + +msgid "" +"Pre-existing pacman lock never exited. Please clean up any existing pacman " +"sessions before using archinstall." +msgstr "" + #, python-brace-format msgid "Enter the number of parallel downloads (1-{})" msgstr "" @@ -1133,15 +1153,6 @@ msgstr "" msgid "Enable colored output for pacman" msgstr "" -msgid "" -"Pacman is already running, waiting maximum 10 minutes for it to terminate." -msgstr "" - -msgid "" -"Pre-existing pacman lock never exited. Please clean up any existing pacman " -"sessions before using archinstall." -msgstr "" - msgid "The proprietary Nvidia driver is not supported by Sway." msgstr "" diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 627a3b7553..9e5371c801 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -120,6 +120,7 @@ def perform_installation( config.bootloader_config.uki, config.bootloader_config.removable, config.bootloader_config.plymouth, + config.bootloader_config.os_prober, ) if config.network_config: diff --git a/docs/cli_parameters/config/config_options.csv b/docs/cli_parameters/config/config_options.csv index a902d1d20e..bb3ce2ebba 100644 --- a/docs/cli_parameters/config/config_options.csv +++ b/docs/cli_parameters/config/config_options.csv @@ -2,7 +2,7 @@ Key,Value(s),Description,Required additional-repositories,[ `multilib `_!, `testing `_ ],Enables one or more of the testing and multilib repositories before proceeding with installation,No archinstall-language,`lang `__,Sets the TUI language used *(make sure to use the ``lang`` value not the ``abbr``)*,No audio_config,`pipewire `_!, `pulseaudio `_,Audioserver to be installed,No -bootloader_config,"{ bootloader: `Systemd-boot `_!, `grub `_!, `limine `_!, uki: ``true``/``false``!, removable: ``true``/``false`` }","Bootloader configuration. ``bootloader`` selects which bootloader to install *(grub/limine mandatory on BIOS)*. ``uki`` enables unified kernel images *(UEFI only!, systemd-boot/limine only)*. ``removable`` installs to default removable media path /EFI/BOOT/ instead of NVRAM *(UEFI only!, grub/limine only)*",Yes +bootloader_config,"{ bootloader: `Systemd-boot `_!, `grub `_!, `limine `_!, uki: ``true``/``false``!, removable: ``true``/``false``!, os_prober: ``true``/``false`` }","Bootloader configuration. ``bootloader`` selects which bootloader to install *(grub/limine mandatory on BIOS)*. ``uki`` enables unified kernel images *(UEFI only!, systemd-boot/limine only)*. ``removable`` installs to default removable media path /EFI/BOOT/ instead of NVRAM *(UEFI only!, grub/limine only)*. ``os_prober`` installs os-prober so GRUB can detect other operating systems such as Windows *(grub only)*",Yes debug,``true``!, ``false``,Enables debug output,No disk_config,*Read more under* :ref:`disk config`,Contains the desired disk setup to be used during installation,No disk_encryption,*Read more about under* :ref:`disk encryption`,Parameters for disk encryption applied on top of ``disk_config``,No diff --git a/docs/installing/guided.rst b/docs/installing/guided.rst index 562d2b2980..2b55c42ed9 100644 --- a/docs/installing/guided.rst +++ b/docs/installing/guided.rst @@ -69,7 +69,8 @@ The contents of :code:`https://domain.lan/config.json`: "bootloader_config": { "bootloader": "Systemd-boot", "uki": false, - "removable": false + "removable": false, + "os_prober": false }, "bootloader": "Systemd-boot", "debug": false, diff --git a/examples/config-sample.json b/examples/config-sample.json index ac366b6c9f..47d8da535f 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -6,7 +6,8 @@ "bootloader_config": { "bootloader": "Systemd-boot", "uki": false, - "removable": false + "removable": false, + "os_prober": false }, "debug": false, "disk_config": { diff --git a/tests/data/test_config.json b/tests/data/test_config.json index 618bd8e9c7..a2f01aaed3 100644 --- a/tests/data/test_config.json +++ b/tests/data/test_config.json @@ -24,7 +24,8 @@ "bootloader_config": { "bootloader": "Systemd-boot", "uki": false, - "removable": false + "removable": false, + "os_prober": true }, "services": [ "service_1", diff --git a/tests/test_args.py b/tests/test_args.py index 324f5a1173..7c6f3d8bdd 100644 --- a/tests/test_args.py +++ b/tests/test_args.py @@ -232,6 +232,7 @@ def test_config_file_parsing( bootloader=Bootloader.Systemd, uki=False, removable=False, + os_prober=True, ), hostname='archy', kernels=['linux-zen'],