diff --git a/anaconda/pyanaconda/bootloader.py b/anaconda/pyanaconda/bootloader.py index 9717a83..37f147d 100644 --- a/anaconda/pyanaconda/bootloader.py +++ b/anaconda/pyanaconda/bootloader.py @@ -1908,16 +1908,208 @@ class XenEFI(EFIGRUB): write_config = BootLoader.write_config -class MacEFIGRUB(EFIGRUB): +class MacEFIGRUB(XenEFI): + """Special EFI handling for macOS HFS+ ESP partition. + + Typical GRUB2 installations would execute the script + located at /usr/libexec/mactel-boot-setup which would + modify the HFS+ ESP files and bless the specified efi. + + However, we are not using GRUB at this time which would + cause that script to exit earlier. + + In this class, we will execute the relevant commands + to symlink the efi file in the /System directory as well + the cfg file. Lastly, macOS requires the bootable efi + file to be blessed. + """ + + def __init__(self): + super(MacEFIGRUB, self).__init__() + self._mountpoint = "/boot/efi" # fixme: extract from writeBootLoader() + self._system_label = "Qubes OS" + self._mactel_sys_dir = "{}/{}".format(self._mountpoint, "/System/Library/Coreservices") + self._mactel_artwork_dir = "{}/{}".format("/usr/share/pixmaps/bootloader/apple", self.efi_dir) + def mactel_config(self): - if os.path.exists(iutil.getSysroot() + "/usr/libexec/mactel-boot-setup"): - rc = iutil.execInSysroot("/usr/libexec/mactel-boot-setup", []) - if rc: - log.error("failed to configure Mac boot loader") + """Modifies the HFS+ ESP partition to be bootable. + + Based on the /usr/libexec/mactel-boot-setup script, + we will create two symlinks pointing to the Qubes + Xen version that is installed and configured for + the system. We also need to run hfs-bless on + the specific EFI file we allow to be bootable. + """ + + log.info("mactel configure MacEFI boot partition") + + not_ready = False + xen_efi_file = "{}/{}".format(iutil.getSysroot() + self.config_dir, "xen.efi") + if not os.path.exists(xen_efi_file): + log.waring("mactel efi file not found: %s", xen_efi_file) + not_ready = True + xen_cfg_file = "{}/{}".format(iutil.getSysroot() + self.config_dir, "xen.cfg") + if not os.path.exists(xen_cfg_file): + log.warning("mactel efi cfg not found: %s", xen_cfg_file) + not_ready = True + if not_ready: + log.error("mactel cannot continue with mactel_config. see log for details.") + return + + sys_dir = iutil.getSysroot() + self._mactel_sys_dir + if not os.path.exists(sys_dir): + try: + os.makedirs(sys_dir) + except Exception as error: + log.warning("mactel tried to create sys_dir %s but received error: %s", sys_dir, error) + + src_efi = "{}/{}".format("../../../EFI/" + self.efi_dir, "xen.efi") + sys_efi = "{}/{}".format(sys_dir, "boot.efi") + self._symlink(src_efi, sys_efi) + + src_cfg = "{}/{}".format("../../../EFI/" + self.efi_dir, "xen.cfg") + sys_cfg = "{}/{}".format(sys_dir, "xen.cfg") # convention for Xen's cfg lookup + self._symlink(src_cfg, sys_cfg) + + result_code = iutil.execInSysroot("touch", ["{}/{}".format(self._mountpoint, "mach_kernel")]) + if result_code: + log.error("mactel failed to touch: %s", "{}/{}".format(self._mountpoint, "mach_kernel")) + + text = """ + + + + ProductBuildVersion + + ProductName + Linux + ProductVersion + {} + +""".format(self._system_label) + sys_ver_file_name = "{}/{}".format(sys_dir, "SystemVersion.plist") + try: + with open(sys_ver_file_name, "w") as sys_version_file: + sys_version_file.write(text) + except IOError as error: + log.error("mactel failed to open %s for write: %s", sys_ver_file_name, error) + cfg_ver_file_name = "{}/{}".format(iutil.getSysroot() + self.config_dir, "SystemVersion.plist") + self._copy_file(sys_ver_file_name, cfg_ver_file_name) + + bless_file = "{}/{}".format(self.config_dir, "xen.efi") + result_code = iutil.execInSysroot("hfs-bless", [bless_file]) + if result_code: + log.error("mactel failed to run 'hfs-bless %s'", bless_file) + + # make the partition macOS friendly (e.g. to set rescue mode from macOS) + fseventsd = "{}/{}".format(iutil.getSysroot() + self._mountpoint, ".fseventsd") + try: + os.makedirs(fseventsd) + except Exception as error: + log.error("mactel could not make directory %s: %s", fseventsd, error) + touch_files = [ + "{}/{}/{}".format(self._mountpoint, ".fseventsd", "no_log"), + "{}/{}".format(self._mountpoint, ".metadata_never_index"), + "{}/{}".format(self._mountpoint, ".Trashes")] + result_code = iutil.execInSysroot("touch", touch_files) + if result_code: + log.error("mactel failed to touch: %s", touch_files) + + text = """Qubes OS + +CAUTION +-- +This partition is used to boot Qubes OS on a macOS machine. +Modifying the contents could render your installation unbootable. + +RESCUE / RECOVERY MODE +-- +In the event that you need to boot into Rescue mode, you will need +to change the default Xen configuration. + +0. Backup your xen.cfg. + + cp /EFI/qubes/xen.cfg /EFI/qubes/xen.cfg~ + +1. Open /EFI/qubes/xen.cfg +2. Look at the sections with the named headers. One may already be + named "qubes-rescue", which we will use in Step 4 below. + + If not, we will need to make one. Copy your current configuration + to another entry. But change the kernel line to only have "rescue" + at the end. As an example only: + + [qubes-rescue] + options=loglvl=all dom0_mem=min:1024M dom0_mem=max:4096M iommu=no-igfx + kernel=vmlinuz-4.14.13-2.pvops.qubes.x86_64 rescue + ramdisk=initramfs-4.14.13-2.pvops.qubes.x86_64.img + + Do not simply copy this section above. You will need to make sure + all of your existing parameters, vmlinuz and initramfs versions match + your current kernel(s) you are booting. + +3. At the top of the file, edit the default link to use your new entry. + + [global] + default=qubes-rescue + +Now when you reboot using Boot Camp and select "Qubes OS", it will boot +into Rescue mode. + +To revert, change the default entry back to the original first entry name.""" + readme = "{}/{}".format(iutil.getSysroot() + self._mountpoint, "00-README.txt") + try: + with open(readme, "w") as readme_file: + readme_file.write(text) + except IOError as error: + log.error("mactel failed to open %s for write: %s", readme, error) + + def mactel_install_qubes_artwork(self): + """Configures the Qubes logo and label for macOS boot selector. + + Shows during boot selection options. + + .disk_label is defined as a text representation of the volume + label converted to a special image. See this link for + more details: http://refit.sourceforge.net/info/vollabel.html + + .VolumeIcon.icns is defined as an ICNS image format of 512x512. + """ + + log.info("mactel creating Qubes Artwork") + + artwork_dir = self._mactel_artwork_dir + if not os.path.exists(artwork_dir): + log.debug("mactel using sysroot for artwork prefix") + artwork_dir = iutil.getSysroot() + artwork_dir + if not os.path.exists(artwork_dir): + log.warning("mactel artwork missing from: %s", artwork_dir) + return + + icon_src = artwork_dir + ".icns" + if os.path.exists(icon_src): + icon_dst_fil = "{}/{}".format(iutil.getSysroot() + self._mountpoint, ".VolumeIcon.icns") + self._copy_file(icon_src, icon_dst_fil) + else: + log.warning("mactel volume icon not found: %s", icon_src) + + src_files = os.listdir(artwork_dir) + for file_name in src_files: + full_file_name = "{}/{}".format(artwork_dir, file_name) + if os.path.isfile(full_file_name): + sys_dir_file_name = "{}/{}".format(iutil.getSysroot() + self._mactel_sys_dir, "." + file_name) + self._copy_file(full_file_name, sys_dir_file_name) + config_dir = iutil.getSysroot() + self.config_dir + if os.path.exists(config_dir): + dest = "{}/{}".format(config_dir, "." + file_name) + self._copy_file(full_file_name, dest) + def install(self, args=None): super(MacEFIGRUB, self).install() + log.info("Installing mactel MacEFI") self.mactel_config() + self.mactel_install_qubes_artwork() def is_valid_stage1_device(self, device, early=False): valid = super(MacEFIGRUB, self).is_valid_stage1_device(device, early) @@ -1932,6 +2124,24 @@ class MacEFIGRUB(EFIGRUB): log.debug("MacEFIGRUB.is_valid_stage1_device(%s) returning %s", device.name, valid) return valid + @staticmethod + def _symlink(source="", target=""): + """Creates a symlink between source and target.""" + try: + os.symlink(source, target) + except OSError as error: + log.error("mactel failed to symlink %s -> %s : %s", source, target, error) + + @staticmethod + def _copy_file(source="", target=""): + """Copies source to target using shutil. + + Also chmod to 0644.""" + try: + shutil.copy(source, target) + os.chmod(target, 0o644) + except OSError as error: + log.error("mactel failed to copy %s to %s: %s", source, target, error) # Inherit abstract methods from BootLoader # pylint: disable=abstract-method