2 # -*- coding: utf-8 -*-
6 # Copyright © 2013-2019 RebornOS
8 # This file is part of Cnchi.
10 # Cnchi is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
15 # Cnchi is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # The following additional terms are in effect as per Section 7 of the license:
22 # The preservation of all legal notices and author attributions in
23 # the material or in the Appropriate Legal Notices displayed
24 # by works containing it is required.
26 # You should have received a copy of the GNU General Public License
27 # along with Cnchi; If not, see <http://www.gnu.org/licenses/>.
30 """ GRUB2 bootloader installation """
41 import parted3.fs_module as fs
42 from installation import special_dirs
43 from misc.run_cmd import call, chroot_call
44 from misc.extra import random_generator
49 # When testing, no _() is available
52 except NameError as err:
58 """ Class to perform boot loader installation """
60 def __init__(self, dest_dir, settings, uuids):
61 self.dest_dir = dest_dir
62 self.settings = settings
66 """ Install Grub2 bootloader """
67 self.modify_grub_default()
70 if os.path.exists('/sys/firmware/efi'):
71 logging.debug("Cnchi will install the Grub2 (efi) loader")
74 logging.debug("Cnchi will install the Grub2 (bios) loader")
77 self.check_root_uuid_in_grub()
79 def check_root_uuid_in_grub(self):
80 """ Checks grub.cfg for correct root UUID """
81 if self.settings.get("zfs"):
82 # No root uuid checking if using zfs
85 if "/" not in self.uuids:
87 "Root uuid variable is not set. I can't check root UUID"
88 "in grub.cfg, let's hope it's ok")
91 ruuid_str = 'root=UUID={0}'.format(self.uuids["/"])
93 cmdline_linux = self.settings.get('GRUB_CMDLINE_LINUX')
94 if cmdline_linux is None:
97 cmdline_linux_default = self.settings.get('GRUB_CMDLINE_LINUX_DEFAULT')
98 if cmdline_linux_default is None:
99 cmdline_linux_default = ""
101 boot_command = 'linux /vmlinuz-linux {0} {1} {2}\n'.format(
104 cmdline_linux_default)
106 pattern = re.compile(
107 "menuentry 'RebornOS'[\s\S]*initramfs-linux.img\n}")
109 cfg = os.path.join(self.dest_dir, "boot/grub/grub.cfg")
110 with open(cfg) as grub_file:
111 parse = grub_file.read()
113 if not self.settings.get('use_luks') and ruuid_str not in parse:
114 entry = pattern.search(parse)
117 "Wrong uuid in grub.cfg, Cnchi will try to fix it.")
119 "linux\t/vmlinuz.*quiet\n",
122 parse = parse.replace(entry.group(), new_entry)
124 with open(cfg, 'w') as grub_file:
125 grub_file.write(parse)
127 def modify_grub_default(self):
128 """ If using LUKS as root, we need to modify GRUB_CMDLINE_LINUX
129 GRUB_CMDLINE_LINUX : Command-line arguments to add to menu entries
130 for the Linux kernel.
131 GRUB_CMDLINE_LINUX_DEFAULT : Unless ‘GRUB_DISABLE_RECOVERY’ is set
132 to ‘true’, two menu entries will be generated for each Linux kernel:
133 one default entry and one entry for recovery mode. This option lists
134 command-line arguments to add only to the default menu entry, after
135 those listed in ‘GRUB_CMDLINE_LINUX’. """
137 plymouth_bin = os.path.join(self.dest_dir, "usr/bin/plymouth")
138 cmd_linux_default = "quiet"
141 # https://www.kernel.org/doc/Documentation/kernel-parameters.txt
142 # cmd_linux_default : quiet splash resume=UUID=ABC zfs=ABC
144 if os.path.exists(plymouth_bin):
145 cmd_linux_default += " splash"
147 # resume does not work in zfs (or so it seems)
148 if "swap" in self.uuids and not self.settings.get("zfs"):
149 cmd_linux_default += " resume=UUID={0}".format(self.uuids["swap"])
151 if self.settings.get("zfs"):
152 zfs_pool_name = self.settings.get("zfs_pool_name")
153 cmd_linux += " zfs={0}".format(zfs_pool_name)
155 if self.settings.get('use_luks'):
156 # When using separate boot partition,
157 # add GRUB_ENABLE_CRYPTODISK to grub.cfg
158 if self.uuids["/"] != self.uuids["/boot"]:
159 self.set_grub_option("GRUB_ENABLE_CRYPTODISK", "y")
161 # Let GRUB automatically add the kernel parameters for
163 luks_root_volume = self.settings.get('luks_root_volume')
164 logging.debug("Luks Root Volume: %s", luks_root_volume)
166 if (self.settings.get("partition_mode") == "advanced" and
167 self.settings.get('use_luks_in_root')):
168 # In advanced, if using luks in root device,
169 # we store root device it in luks_root_device var
170 root_device = self.settings.get('luks_root_device')
171 self.uuids["/"] = fs.get_uuid(root_device)
173 cmd_linux += " cryptdevice=/dev/disk/by-uuid/{0}:{1}".format(
177 if self.settings.get("luks_root_password") == "":
178 # No luks password, so user wants to use a keyfile
179 cmd_linux += " cryptkey=/dev/disk/by-uuid/{0}:ext2:/.keyfile-root".format(
182 # Remove leading/ending spaces
183 cmd_linux_default = cmd_linux_default.strip()
184 cmd_linux = cmd_linux.strip()
186 # Modify /etc/default/grub
187 self.set_grub_option(
188 "GRUB_THEME", "/boot/grub/themes/Vimix/theme.txt")
189 self.set_grub_option("GRUB_DISTRIBUTOR", "RebornOS")
190 self.set_grub_option("GRUB_CMDLINE_LINUX_DEFAULT", cmd_linux_default)
191 self.set_grub_option("GRUB_CMDLINE_LINUX", cmd_linux)
193 # Also store grub line in settings, we'll use it later in check_root_uuid_in_grub()
195 self.settings.set('GRUB_CMDLINE_LINUX', cmd_linux)
196 except AttributeError:
199 logging.debug("Grub configuration completed successfully.")
201 def set_grub_option(self, option, cmd):
202 """ Changes a grub setup option in /etc/default/grub """
204 default_grub_path = os.path.join(
205 self.dest_dir, "etc/default", "grub")
206 default_grub_lines = []
208 with open(default_grub_path, 'r', newline='\n') as grub_file:
209 default_grub_lines = [x for x in grub_file.readlines()]
211 with open(default_grub_path, 'w', newline='\n') as grub_file:
212 param_in_file = False
213 param_to_look_for = option + '='
214 for line in default_grub_lines:
215 if param_to_look_for in line:
216 # Option was already in file, update it
217 line = '{0}="{1}"\n'.format(option, cmd)
219 grub_file.write(line)
221 if not param_in_file:
222 # Option was not found. Thus, append new option
223 grub_file.write('\n{0}="{1}"\n'.format(option, cmd))
225 logging.debug('Set %s="%s" in /etc/default/grub', option, cmd)
226 except FileNotFoundError as ex:
228 except Exception as ex:
229 tpl1 = "Can't modify {0}".format(default_grub_path)
230 tpl2 = "An exception of type {0} occured. Arguments:\n{1!r}"
231 template = '{0} {1}'.format(tpl1, tpl2)
232 message = template.format(type(ex).__name__, ex.args)
233 logging.error(message)
235 def prepare_grub_d(self):
236 """ Copies 10_antergos script into /etc/grub.d/ """
237 grub_d_dir = os.path.join(self.dest_dir, "etc/grub.d")
238 script_dir = os.path.join(self.settings.get("cnchi"), "scripts")
239 script = "10_antergos"
241 os.makedirs(grub_d_dir, mode=0o755, exist_ok=True)
243 script_path = os.path.join(script_dir, script)
244 if os.path.exists(script_path):
246 shutil.copy2(script_path, grub_d_dir)
247 os.chmod(os.path.join(grub_d_dir, script), 0o755)
248 except FileNotFoundError:
249 logging.debug("Could not copy %s to grub.d", script)
250 except FileExistsError:
253 logging.warning("Can't find script %s", script_path)
255 def grub_ripper(self):
259 ret = subprocess.check_output(
260 ['pidof', 'grub-mount']).decode().strip()
262 subprocess.check_output(['kill', '-9', ret.split()[0]])
265 except subprocess.CalledProcessError as err:
266 logging.warning("Error running %s: %s", err.cmd, err.output)
269 def run_mkconfig(self):
270 """ Create grub.cfg file using grub-mkconfig """
271 logging.debug("Generating grub.cfg...")
273 # Make sure that /dev and others are mounted (binded).
274 special_dirs.mount(self.dest_dir)
276 # Hack to kill grub-mount hanging
277 threading.Thread(target=self.grub_ripper).start()
279 # Add -l option to os-prober's umount call so that it does not hang
280 self.apply_osprober_patch()
281 logging.debug("Running grub-mkconfig...")
282 locale = self.settings.get("locale")
283 cmd = 'LANG={0} grub-mkconfig -o /boot/grub/grub.cfg'.format(locale)
284 cmd_sh = ['sh', '-c', cmd]
285 if not chroot_call(cmd_sh, self.dest_dir, timeout=300):
286 msg = ("grub-mkconfig does not respond. Killing grub-mount and"
287 "os-prober so we can continue.")
289 call(['killall', 'grub-mount'])
290 call(['killall', 'os-prober'])
292 def install_bios(self):
293 """ Install Grub2 bootloader in a BIOS system """
294 grub_location = self.settings.get('bootloader_device')
295 txt = _("Installing GRUB(2) BIOS boot loader in {0}").format(
299 # /dev and others need to be mounted (binded).
300 # We call mount_special_dirs here just to be sure
301 special_dirs.mount(self.dest_dir)
303 grub_install = ['grub-install',
304 '--directory=/usr/lib/grub/i386-pc',
306 '--boot-directory=/boot',
309 # Use --force when installing in /dev/sdXY or in /dev/mmcblk
310 if len(grub_location) > len("/dev/sdX"):
311 grub_install.append("--force")
313 grub_install.append(grub_location)
315 chroot_call(grub_install, self.dest_dir)
317 self.install_locales()
321 grub_cfg_path = os.path.join(self.dest_dir, "boot/grub/grub.cfg")
322 with open(grub_cfg_path) as grub_cfg:
323 if "Antergos" in grub_cfg.read():
324 txt = _("GRUB(2) BIOS has been successfully installed.")
326 self.settings.set('bootloader_installation_successful', True)
328 txt = _("ERROR installing GRUB(2) BIOS.")
330 self.settings.set('bootloader_installation_successful', False)
332 def install_efi(self):
333 """ Install Grub2 bootloader in a UEFI system """
335 spec_uefi_arch = "x64"
336 spec_uefi_arch_caps = "X64"
337 fpath = '/install/boot/efi/EFI/RebornOS'
338 bootloader_id = 'RebornOS' if not os.path.exists(fpath) else \
339 'RebornOS_{0}'.format(random_generator())
341 # grub2 in efi needs efibootmgr
342 if not os.path.exists("/usr/bin/efibootmgr"):
344 "Please install efibootmgr package to install Grub2 for x86_64-efi platform.")
346 txt = _("GRUB(2) will NOT be installed")
348 self.settings.set('bootloader_installation_successful', False)
351 txt = _("Installing GRUB(2) UEFI {0} boot loader").format(uefi_arch)
356 '--target={0}-efi'.format(uefi_arch),
357 '--efi-directory=/install/boot/efi',
358 '--bootloader-id={0}'.format(bootloader_id),
359 '--boot-directory=/install/boot',
361 load_module = ['modprobe', '-a', 'efivarfs']
363 call(load_module, timeout=15)
364 call(grub_install, timeout=120)
366 self.install_locales()
368 # Copy grub into dirs known to be used as default by some OEMs
369 # if they do not exist yet.
374 "BOOT{0}.efi".format(spec_uefi_arch_caps)),
377 "boot/efi/EFI/Microsoft/Boot",
380 grub_path = os.path.join(
382 "boot/efi/EFI/antergos_grub",
383 "grub{0}.efi".format(spec_uefi_arch))
385 for grub_default in grub_defaults:
386 path = grub_default.split()[0]
387 if not os.path.exists(path):
388 msg = _("No OEM loader found in %s. Copying Grub(2) into dir.")
389 logging.info(msg, path)
390 os.makedirs(path, mode=0o755)
391 msg_failed = _("Copying Grub(2) into OEM dir failed: %s")
393 shutil.copy(grub_path, grub_default)
394 except FileNotFoundError:
395 logging.warning(msg_failed, _("File not found."))
396 except FileExistsError:
397 logging.warning(msg_failed, _("File already exists."))
398 except Exception as ex:
399 template = "An exception of type {0} occured. Arguments:\n{1!r}"
400 message = template.format(type(ex).__name__, ex.args)
401 logging.error(message)
406 os.path.join(self.dest_dir, "boot/grub/x86_64-efi/core.efi"),
409 "boot/efi/EFI/{0}".format(bootloader_id),
410 "grub{0}.efi".format(spec_uefi_arch))]
414 if not os.path.exists(path):
416 logging.debug("Path '%s' doesn't exist, when it should", path)
419 logging.info("GRUB(2) UEFI install completed successfully")
420 self.settings.set('bootloader_installation_successful', True)
423 "GRUB(2) UEFI install may not have completed successfully.")
424 self.settings.set('bootloader_installation_successful', False)
426 def apply_osprober_patch(self):
427 """ Adds -l option to os-prober's umount call so that it does not hang """
428 osp_path = os.path.join(
430 "usr/lib/os-probes/50mounted-tests")
431 if os.path.exists(osp_path):
432 with open(osp_path) as osp:
433 text = osp.read().replace("umount", "umount -l")
434 with open(osp_path, 'w') as osp:
436 logging.debug("50mounted-tests file patched successfully")
438 logging.warning("Failed to patch 50mounted-tests, file not found.")
440 def install_locales(self):
441 """ Install Grub2 locales """
442 logging.debug("Installing Grub2 locales.")
443 dest_locale_dir = os.path.join(self.dest_dir, "boot/grub/locale")
445 os.makedirs(dest_locale_dir, mode=0o755, exist_ok=True)
447 grub_mo = os.path.join(
449 "usr/share/locale/en@quot/LC_MESSAGES/grub.mo")
452 shutil.copy2(grub_mo, os.path.join(dest_locale_dir, "en.mo"))
453 except FileNotFoundError:
454 logging.warning("Can't install GRUB(2) locale.")
455 except FileExistsError:
456 # Ignore if already exists
460 if __name__ == '__main__':
461 os.makedirs("/install/etc/default", mode=0o755, exist_ok=True)
462 shutil.copy2("/etc/default/grub", "/install/etc/default/grub")
463 dest_dir = "/install"
465 settings["zfs"] = True
466 settings["zfs_pool_name"] = "Reborn_d3sq"
467 settings["use_luks"] = True
470 uuids["/boot"] = "ZXCV"
471 grub2 = Grub2(dest_dir, settings, uuids)
472 grub2.modify_grub_default()