From a1df2afec57825c08685aaf7fa487bcbc8357843 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 5 Aug 2026 11:29:16 -0300 Subject: [PATCH 1/9] Add os_prober option to bootloader configuration model Introduce an os_prober flag on BootloaderConfiguration, defaulting to false to match the upstream GRUB default of shipping os-prober disabled. Bootloader.has_os_prober_support() limits the option to GRUB, the only supported bootloader that consumes os-prober output. --- archinstall/lib/models/bootloader.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/archinstall/lib/models/bootloader.py b/archinstall/lib/models/bootloader.py index 70ced759ad..9e39ce9dd8 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: @@ -95,12 +98,13 @@ class BootloaderConfiguration(SubConfig): uki: bool = False removable: bool = True plymouth: PlymouthTheme | None = None + os_prober: bool = False NAME: str = tr('Bootloader') @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 @@ -116,6 +120,9 @@ def summary(self, _level: SummaryLevel = SummaryLevel.BASIC) -> list[str]: 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)) @@ -127,7 +134,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: @@ -135,7 +143,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}' @@ -154,6 +163,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'os-prober: {os_prober_string}' + text += '\n' if self.plymouth is not None: text += f'{tr("Plymouth")}: {self.plymouth.value}' text += '\n' From 10ab724c72dcffad713a53bb42a31066bcb85fd6 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 5 Aug 2026 11:29:20 -0300 Subject: [PATCH 2/9] Enable os-prober during GRUB installation When the os_prober option is set, install the os-prober package and set GRUB_DISABLE_OS_PROBER=false in /etc/default/grub so that grub-mkconfig detects other operating systems such as Windows. The edit runs for both the UKI and non-UKI paths and handles configs where the option is commented out, set to another value or missing entirely. Like the uki and removable options, an unsupported combination is downgraded with a warning instead of failing the installation. --- archinstall/lib/installer.py | 37 +++++++++++++++++++++++++++++++++-- archinstall/scripts/guided.py | 1 + 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 11b89a9cad..a4824a36f0 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1333,6 +1333,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') @@ -1432,6 +1433,27 @@ def _add_grub_bootloader( grub_default.write_text(config) + if os_prober: + self.pacman.strap('os-prober') + + # 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', @@ -1836,7 +1858,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. @@ -1851,6 +1878,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(): @@ -1886,6 +1914,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) @@ -1902,7 +1935,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/scripts/guided.py b/archinstall/scripts/guided.py index 4b4273ddc9..7dd2e8f9f1 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: From 08d4dac3e65e1b9a4d98060d0c9c64f07757da0e Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 5 Aug 2026 11:29:23 -0300 Subject: [PATCH 3/9] Add os-prober toggle to the bootloader menu Expose the os_prober option in the bootloader submenu. The item is only enabled when GRUB is selected and is reset when switching to a bootloader without os-prober support, mirroring the uki and removable handling. Regenerate the translation template for the new strings. --- archinstall/lib/bootloader/bootloader_menu.py | 42 +++++++++++++++++++ archinstall/locales/base.pot | 11 +++++ 2 files changed, 53 insertions(+) diff --git a/archinstall/lib/bootloader/bootloader_menu.py b/archinstall/lib/bootloader/bootloader_menu.py index 607a09ee19..d907846155 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/locales/base.pot b/archinstall/locales/base.pot index 61a063a9fb..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 "" From 80c79e29df935e218565c9a0ee662c2af93b5ecc Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 5 Aug 2026 11:29:25 -0300 Subject: [PATCH 4/9] Update tests, docs and samples for the os_prober option Cover os_prober in the config fixture and parsing assertions, and document the new key in the config options table and sample configs. --- docs/cli_parameters/config/config_options.csv | 2 +- docs/installing/guided.rst | 3 ++- examples/config-sample.json | 3 ++- tests/data/test_config.json | 3 ++- tests/test_args.py | 1 + 5 files changed, 8 insertions(+), 4 deletions(-) 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 905b8f88fb..f9f4b108e0 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 be2769ce58..9b823321cb 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 faf6656afe..0a697d436b 100644 --- a/tests/data/test_config.json +++ b/tests/data/test_config.json @@ -21,7 +21,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 0010e2dc91..8c226db767 100644 --- a/tests/test_args.py +++ b/tests/test_args.py @@ -233,6 +233,7 @@ def test_config_file_parsing( bootloader=Bootloader.Systemd, uki=False, removable=False, + os_prober=True, ), hostname='archy', kernels=['linux-zen'], From d5a2589247356a04dad1c2133e817f1c8eb95ce4 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 5 Aug 2026 11:48:33 -0300 Subject: [PATCH 5/9] Install fuse3 alongside os-prober Since version 1.77 os-prober probes unmounted partitions exclusively through grub-mount and silently skips them when it cannot run. The grub package ships grub-mount but only lists fuse3 as an optional dependency, so without it os-prober is limited to already-mounted partitions and misses the common dual-boot case of Windows living on another disk. Installing fuse3 also lets grub-mount read NTFS in userspace, keeping detection independent of kernel filesystem modules inside the installation chroot. --- archinstall/lib/installer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index a4824a36f0..a0711f8351 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1434,7 +1434,9 @@ def _add_grub_bootloader( grub_default.write_text(config) if os_prober: - self.pacman.strap('os-prober') + # 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 From adda583544acd7d62f433d2bfc96b84d32c7c6e4 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 5 Aug 2026 14:58:38 -0300 Subject: [PATCH 6/9] Log os-prober enablement during GRUB setup The os-prober block straps packages and edits /etc/default/grub without leaving any trace in the install log. Add a debug marker, matching the logging convention of the surrounding bootloader steps. --- archinstall/lib/installer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index a0711f8351..dd121dc25f 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1434,6 +1434,8 @@ 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']) From 49ea3e3434ca5d8df73e5e4e7b395b125458e0d6 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 5 Aug 2026 19:29:56 -0300 Subject: [PATCH 7/9] Translate the os-prober label in the bootloader preview Reuse the existing 'Detect other operating systems' msgid so the preview matches the menu item label and stays localizable, like the neighbouring Removable and Plymouth labels. Suggested in PR review. --- archinstall/lib/models/bootloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/archinstall/lib/models/bootloader.py b/archinstall/lib/models/bootloader.py index 9e39ce9dd8..baec3789a0 100644 --- a/archinstall/lib/models/bootloader.py +++ b/archinstall/lib/models/bootloader.py @@ -168,7 +168,7 @@ def preview(self, uefi: bool) -> str: os_prober_string = tr('Enabled') else: os_prober_string = tr('Disabled') - text += f'os-prober: {os_prober_string}' + 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}' From bb93c4a12cb80c6744e29099827c00b881285765 Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 19 Oct 2026 14:10:31 +0000 Subject: [PATCH 8/9] Drop the else after return in the os-prober preview Upstream now enables Ruff's flake8-return rules, and RET505 flags it. --- archinstall/lib/bootloader/bootloader_menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/archinstall/lib/bootloader/bootloader_menu.py b/archinstall/lib/bootloader/bootloader_menu.py index d907846155..9b7787968c 100644 --- a/archinstall/lib/bootloader/bootloader_menu.py +++ b/archinstall/lib/bootloader/bootloader_menu.py @@ -109,8 +109,8 @@ 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")}' + + return f'{os_prober_text}: {tr("Disabled")}' def _prev_plymouth(self, item: MenuItem) -> str | None: if item.value: From b891e20bd842b8765dbbcd90263022ed5bcbe463 Mon Sep 17 00:00:00 2001 From: AMMAAR-IC Date: Sun, 18 Oct 2026 00:25:09 +0530 Subject: [PATCH 9/9] Import datetime as dt in mirrors model Ruff 0.17 flags 'import datetime' under ICN001 (flake8-import-conventions), which breaks the ruff check linting workflow. (cherry picked from commit e143605bbfdd5b0b6ece222f05f30018a3234e3a) --- archinstall/lib/models/mirrors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/archinstall/lib/models/mirrors.py b/archinstall/lib/models/mirrors.py index 67811b2c4e..824505ed54 100644 --- a/archinstall/lib/models/mirrors.py +++ b/archinstall/lib/models/mirrors.py @@ -1,4 +1,4 @@ -import datetime +import datetime as dt import http.client import urllib.error import urllib.parse @@ -30,7 +30,7 @@ class MirrorStatusEntryV3(BaseModel): ipv6: bool details: str delay: int | None = None - last_sync: datetime.datetime | None = None + last_sync: dt.datetime | None = None duration_avg: float | None = None duration_stddev: float | None = None completion_pct: float | None = None @@ -120,7 +120,7 @@ def debug_output(self, info: ValidationInfo) -> Self: class MirrorStatusListV3(BaseModel): cutoff: int - last_check: datetime.datetime + last_check: dt.datetime num_checks: int urls: list[MirrorStatusEntryV3] version: int