Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/archinstall/lib
diff options
context:
space:
mode:
Diffstat (limited to 'archinstall/lib')
-rw-r--r--archinstall/lib/disk/btrfs.py104
-rw-r--r--archinstall/lib/disk/filesystem.py16
-rw-r--r--archinstall/lib/disk/helpers.py24
-rw-r--r--archinstall/lib/disk/partition.py45
-rw-r--r--archinstall/lib/installer.py72
-rw-r--r--archinstall/lib/luks.py2
6 files changed, 221 insertions, 42 deletions
diff --git a/archinstall/lib/disk/btrfs.py b/archinstall/lib/disk/btrfs.py
index 7ae4f6a6..fb9712f8 100644
--- a/archinstall/lib/disk/btrfs.py
+++ b/archinstall/lib/disk/btrfs.py
@@ -6,6 +6,8 @@ from .helpers import get_mount_info
from ..exceptions import DiskError
from ..general import SysCommand
from ..output import log
+from .partition import Partition
+
def mount_subvolume(installation, subvolume_location :Union[pathlib.Path, str], force=False) -> bool:
"""
@@ -72,3 +74,105 @@ def create_subvolume(installation, subvolume_location :Union[pathlib.Path, str])
log(f"Creating a subvolume on {target}", level=logging.INFO)
if (cmd := SysCommand(f"btrfs subvolume create {target}")).exit_code != 0:
raise DiskError(f"Could not create a subvolume at {target}: {cmd}")
+
+def manage_btrfs_subvolumes(installation, partition :dict, mountpoints :dict, subvolumes :dict, unlocked_device :dict = None):
+ """ we do the magic with subvolumes in a centralized place
+ parameters:
+ * the installation object
+ * the partition dictionary entry which represents the physical partition
+ * mountpoinst, the dictionary which contains all the partititon to be mounted
+ * subvolumes is the dictionary with the names of the subvolumes and its location
+ We expect the partition has been mounted as / , and it to be unmounted after the processing
+ Then we create all the subvolumes inside btrfs as demand
+ We clone then, both the partition dictionary and the object inside it and adapt it to the subvolume needs
+ Then we add it them to the mountpoints dictionary to be processed as "normal" partitions
+ # TODO For encrypted devices we need some special processing prior to it
+ """
+ # We process each of the pairs <subvolume name: mount point | None | mount info dict>
+ # th mount info dict has an entry for the path of the mountpoint (named 'mountpoint') and 'options' which is a list
+ # of mount options (or similar used by brtfs)
+ for name, right_hand in subvolumes.items():
+ try:
+ # we normalize the subvolume name (getting rid of slash at the start if exists. In our implemenation has no semantic load - every subvolume is created from the top of the hierarchy- and simplifies its further use
+ if name.startswith('/'):
+ name = name[1:]
+ # renormalize the right hand.
+ location = None
+ mount_options = []
+ # no contents, so it is not to be mounted
+ if not right_hand:
+ location = None
+ # just a string. per backward compatibility the mount point
+ elif isinstance(right_hand,str):
+ location = right_hand
+ # a dict. two elements 'mountpoint' (obvious) and and a mount options list ¿?
+ elif isinstance(right_hand,dict):
+ location = right_hand.get('mountpoint',None)
+ mount_options = right_hand.get('options',[])
+ # we create the subvolume
+ create_subvolume(installation,name)
+ # Make the nodatacow processing now
+ # It will be the main cause of creation of subvolumes which are not to be mounted
+ # it is not an options which can be established by subvolume (but for whole file systems), and can be
+ # set up via a simple attribute change in a directory (if empty). And here the directories are brand new
+ if 'nodatacow' in mount_options:
+ if (cmd := SysCommand(f"chattr +C {installation.target}/{name}")).exit_code != 0:
+ raise DiskError(f"Could not set nodatacow attribute at {installation.target}/{name}: {cmd}")
+ # entry is deleted so nodatacow doesn't propagate to the mount options
+ del mount_options[mount_options.index('nodatacow')]
+ # Make the compress processing now
+ # it is not an options which can be established by subvolume (but for whole file systems), and can be
+ # set up via a simple attribute change in a directory (if empty). And here the directories are brand new
+ # in this way only zstd compression is activaded
+ # TODO WARNING it is not clear if it should be a standard feature, so it might need to be deactivated
+ if 'compress' in mount_options:
+ if (cmd := SysCommand(f"chattr +c {installation.target}/{name}")).exit_code != 0:
+ raise DiskError(f"Could not set compress attribute at {installation.target}/{name}: {cmd}")
+ # entry is deleted so nodatacow doesn't propagate to the mount options
+ del mount_options[mount_options.index('compress')]
+ # END compress processing.
+ # we do not mount if THE basic partition will be mounted or if we exclude explicitly this subvolume
+ if not partition['mountpoint'] and location is not None:
+ # we begin to create a fake partition entry. First we copy the original -the one that corresponds to
+ # the primary partition
+ fake_partition = partition.copy()
+ # we start to modify entries in the "fake partition" to match the needs of the subvolumes
+ #
+ # to avoid any chance of entering in a loop (not expected) we delete the list of subvolumes in the copy
+ # and reset the encryption parameters
+ del fake_partition['btrfs']
+ fake_partition['encrypted'] = False
+ fake_partition['generate-encryption-key-file'] = False
+ # Mount destination. As of now the right hand part
+ fake_partition['mountpoint'] = location
+ # we load the name in an attribute called subvolume, but i think it is not needed anymore, 'cause the mount logic uses a different path.
+ fake_partition['subvolume'] = name
+ # here we add the mount options
+ fake_partition['options'] = mount_options
+ # Here comes the most exotic part. The dictionary attribute 'device_instance' contains an instance of Partition. This instance will be queried along the mount process at the installer.
+ # We instanciate a new object with following attributes coming / adapted from the instance which was in the primary partition entry (the one we are coping - partition['device_instance']
+ # * path, which will be expanded with the subvolume name to use the bind mount syntax the system uses for naming mounted subvolumes
+ # * size. When the OS queries all the subvolumes share the same size as the full partititon
+ # * uuid. All the subvolumes on a partition share the same uuid
+ if not unlocked_device:
+ fake_partition['device_instance'] = Partition(f"{partition['device_instance'].path}[/{name}]",partition['device_instance'].size,partition['device_instance'].uuid)
+ else:
+ # for subvolumes IN an encrypted partition we make our device instance from unlocked device instead of the raw partition.
+ # This time we make a copy (we should to the same above TODO) and alter the path by hand
+ from copy import copy
+ # KIDS DONT'T DO THIS AT HOME
+ fake_partition['device_instance'] = copy(unlocked_device)
+ fake_partition['device_instance'].path = f"{unlocked_device.path}[/{name}]"
+ # we reset this attribute, which holds where the partition is actually mounted. Remember, the physical partition is mounted at this moment and therefore has the value '/'.
+ # If i don't reset it, process will abort as "already mounted' .
+ # TODO It works for this purpose, but the fact that this bevahiour can happed, should make think twice
+ fake_partition['device_instance'].mountpoint = None
+ #
+ # Well, now that this "fake partition" is ready, we add it to the list of the ones which are to be mounted,
+ # as "normal" ones
+ mountpoints[fake_partition['mountpoint']] = fake_partition
+ except Exception as e:
+ raise e
+ # if the physical partition has been selected to be mounted, we include it at the list. Remmeber, all the above treatement won't happen except the creation of the subvolume
+ if partition['mountpoint']:
+ mountpoints[partition['mountpoint']] = partition
diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py
index 72be7e70..51ef949b 100644
--- a/archinstall/lib/disk/filesystem.py
+++ b/archinstall/lib/disk/filesystem.py
@@ -37,10 +37,10 @@ class Filesystem:
for i in range(storage['DISK_RETRY_ATTEMPTS']):
self.partprobe()
time.sleep(5)
-
+
# TODO: Convert to blkid (or something similar, but blkid doesn't support traversing to list sub-PARTUUIDs based on blockdevice path?)
output = json.loads(SysCommand(f"lsblk --json -o+PARTUUID {self.blockdevice.device}").decode('UTF-8'))
-
+
for device in output['blockdevices']:
for index, partition in enumerate(device['children']):
if (partuuid := partition.get('partuuid', None)) and partuuid.lower() == uuid:
@@ -93,8 +93,12 @@ class Filesystem:
storage['arguments']['!encryption-password'] = get_password(f"Enter a encryption password for {partition['device_instance']}")
partition['!password'] = storage['arguments']['!encryption-password']
-
- loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['mountpoint']).name}loop"
+ # to be able to generate an unique name in case the partition will not be mounted
+ if partition.get('mountpoint',None):
+ ppath = partition['mountpoint']
+ else:
+ ppath = partition['device_instance'].path
+ loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(ppath).name}loop"
partition['device_instance'].encrypt(password=partition['!password'])
@@ -207,9 +211,9 @@ class Filesystem:
SysCommand(f'bash -c "umount {device}?"')
except:
pass
-
+
self.partprobe()
worked = self.raw_parted(f'{device} mklabel {disk_label}').exit_code == 0
self.partprobe()
-
+
return worked
diff --git a/archinstall/lib/disk/helpers.py b/archinstall/lib/disk/helpers.py
index 8e6a79e4..ba29744f 100644
--- a/archinstall/lib/disk/helpers.py
+++ b/archinstall/lib/disk/helpers.py
@@ -123,9 +123,19 @@ def harddrive(size=None, model=None, fuzzy=False):
return collection[drive]
+def split_bind_name(path :Union[pathlib.Path, str]) -> list:
+ # we check for the bind notation. if exist we'll only use the "true" device path
+ if '[' in str(path) : # is a bind path (btrfs subvolume path)
+ device_path, bind_path = str(path).split('[')
+ bind_path = bind_path[:-1].strip() # remove the ]
+ else:
+ device_path = path
+ bind_path = None
+ return device_path,bind_path
def get_mount_info(path :Union[pathlib.Path, str], traverse=False, return_real_path=False) -> dict:
- for traversal in list(map(str, [str(path)] + list(pathlib.Path(str(path)).parents))):
+ device_path,bind_path = split_bind_name(path)
+ for traversal in list(map(str, [str(device_path)] + list(pathlib.Path(str(device_path)).parents))):
try:
log(f"Getting mount information for device path {traversal}", level=logging.INFO)
output = SysCommand(f'/usr/bin/findmnt --json {traversal}').decode('UTF-8')
@@ -141,6 +151,10 @@ def get_mount_info(path :Union[pathlib.Path, str], traverse=False, return_real_p
raise DiskError(f"Could not get mount information for device path {path}")
output = json.loads(output)
+ # for btrfs partitions we redice the filesystem list to the one with the source equals to the parameter
+ # i.e. the subvolume filesystem we're searching for
+ if 'filesystems' in output and len(output['filesystems']) > 1 and bind_path is not None:
+ output['filesystems'] = [entry for entry in output['filesystems'] if entry['source'] == str(path)]
if 'filesystems' in output:
if len(output['filesystems']) > 1:
raise DiskError(f"Path '{path}' contains multiple mountpoints: {output['filesystems']}")
@@ -180,8 +194,9 @@ def get_partitions_in_use(mountpoint) -> list:
def get_filesystem_type(path):
+ device_name, bind_name = split_bind_name(path)
try:
- return SysCommand(f"blkid -o value -s TYPE {path}").decode('UTF-8').strip()
+ return SysCommand(f"blkid -o value -s TYPE {device_name}").decode('UTF-8').strip()
except SysCallError:
return None
@@ -217,12 +232,13 @@ def partprobe():
time.sleep(5)
def convert_device_to_uuid(path :str) -> str:
+ device_name, bind_name = split_bind_name(path)
for i in range(storage['DISK_RETRY_ATTEMPTS']):
partprobe()
-
+
# TODO: Convert lsblk to blkid
# (lsblk supports BlockDev and Partition UUID grabbing, blkid requires you to pick PTUUID and PARTUUID)
- output = json.loads(SysCommand(f"lsblk --json -o+UUID {path}").decode('UTF-8'))
+ output = json.loads(SysCommand(f"lsblk --json -o+UUID {device_name}").decode('UTF-8'))
for device in output['blockdevices']:
if (dev_uuid := device.get('uuid', None)):
diff --git a/archinstall/lib/disk/partition.py b/archinstall/lib/disk/partition.py
index b696d9dd..bb6f2d53 100644
--- a/archinstall/lib/disk/partition.py
+++ b/archinstall/lib/disk/partition.py
@@ -7,7 +7,7 @@ import os
import hashlib
from typing import Optional
from .blockdevice import BlockDevice
-from .helpers import get_mount_info, get_filesystem_type, convert_size_to_gb
+from .helpers import get_mount_info, get_filesystem_type, convert_size_to_gb, split_bind_name
from ..storage import storage
from ..exceptions import DiskError, SysCallError, UnknownFilesystemFormat
from ..output import log
@@ -87,7 +87,7 @@ class Partition:
@property
def sector_size(self):
- output = json.loads(SysCommand(f"lsblk --json -o+LOG-SEC {self.path}").decode('UTF-8'))
+ output = json.loads(SysCommand(f"lsblk --json -o+LOG-SEC {self.device_path}").decode('UTF-8'))
for device in output['blockdevices']:
return device.get('log-sec', None)
@@ -114,7 +114,7 @@ class Partition:
for i in range(storage['DISK_RETRY_ATTEMPTS']):
self.partprobe()
- if (handle := SysCommand(f"lsblk --json -b -o+SIZE {self.path}")).exit_code == 0:
+ if (handle := SysCommand(f"lsblk --json -b -o+SIZE {self.device_path}")).exit_code == 0:
lsblk = json.loads(handle.decode('UTF-8'))
for device in lsblk['blockdevices']:
@@ -144,7 +144,7 @@ class Partition:
@property
def partition_type(self):
- lsblk = json.loads(SysCommand(f"lsblk --json -o+PTTYPE {self.path}").decode('UTF-8'))
+ lsblk = json.loads(SysCommand(f"lsblk --json -o+PTTYPE {self.device_path}").decode('UTF-8'))
for device in lsblk['blockdevices']:
return device['pttype']
@@ -155,6 +155,7 @@ class Partition:
Returns the PARTUUID as returned by lsblk.
This is more reliable than relying on /dev/disk/by-partuuid as
it doesn't seam to be able to detect md raid partitions.
+ For bind mounts all the subvolumes share the same uuid
"""
for i in range(storage['DISK_RETRY_ATTEMPTS']):
self.partprobe()
@@ -175,8 +176,7 @@ class Partition:
For instance when you want to get a __repr__ of the class.
"""
self.partprobe()
-
- return SysCommand(f'blkid -s PARTUUID -o value {self.path}').decode('UTF-8').strip()
+ return SysCommand(f'blkid -s PARTUUID -o value {self.device_path}').decode('UTF-8').strip()
@property
def encrypted(self):
@@ -184,7 +184,6 @@ class Partition:
@encrypted.setter
def encrypted(self, value: bool):
-
self._encrypted = value
@property
@@ -194,11 +193,26 @@ class Partition:
@property
def real_device(self):
for blockdevice in json.loads(SysCommand('lsblk -J').decode('UTF-8'))['blockdevices']:
- if parent := self.find_parent_of(blockdevice, os.path.basename(self.path)):
+ if parent := self.find_parent_of(blockdevice, os.path.basename(self.device_path)):
return f"/dev/{parent}"
# raise DiskError(f'Could not find appropriate parent for encrypted partition {self}')
return self.path
+ @property
+ def device_path(self):
+ """ for bind mounts returns the phisical path of the partition
+ """
+ device_path, bind_name = split_bind_name(self.path)
+ return device_path
+
+ @property
+ def bind_name(self):
+ """ for bind mounts returns the bind name (subvolume path).
+ Returns none if this property does not exist
+ """
+ device_path, bind_name = split_bind_name(self.path)
+ return bind_name
+
def partprobe(self):
SysCommand(f'bash -c "partprobe"')
time.sleep(1)
@@ -348,11 +362,20 @@ class Partition:
pathlib.Path(target).mkdir(parents=True, exist_ok=True)
+ if self.bind_name:
+ device_path = self.device_path
+ # TODO options should be better be a list than a string
+ if options:
+ options = f"{options},subvol={self.bind_name}"
+ else:
+ options = f"subvol={self.bind_name}"
+ else:
+ device_path = self.path
try:
if options:
- mnt_handle = SysCommand(f"/usr/bin/mount -t {fs_type} -o {options} {self.path} {target}")
+ mnt_handle = SysCommand(f"/usr/bin/mount -t {fs_type} -o {options} {device_path} {target}")
else:
- mnt_handle = SysCommand(f"/usr/bin/mount -t {fs_type} {self.path} {target}")
+ mnt_handle = SysCommand(f"/usr/bin/mount -t {fs_type} {device_path} {target}")
# TODO: Should be redundant to check for exit_code
if mnt_handle.exit_code != 0:
@@ -401,5 +424,5 @@ def get_mount_fs_type(fs):
if fs == 'ntfs':
return 'ntfs3' # Needed to use the Paragon R/W NTFS driver
elif fs == 'fat32':
- return 'vfat' # This is the actual type used for fat32 mounting.
+ return 'vfat' # This is the actual type used for fat32 mounting
return fs
diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py
index d2d30c85..f202404a 100644
--- a/archinstall/lib/installer.py
+++ b/archinstall/lib/installer.py
@@ -11,14 +11,14 @@ from .disk import get_partitions_in_use, Partition
from .general import SysCommand, generate_password
from .hardware import has_uefi, is_vm, cpu_vendor
from .locale_helpers import verify_keyboard_layout, verify_x11_keyboard_layout
-from .disk.helpers import get_mount_info
+from .disk.helpers import get_mount_info, split_bind_name
from .mirrors import use_mirrors
from .plugins import plugins
from .storage import storage
# from .user_interaction import *
from .output import log
from .profiles import Profile
-from .disk.btrfs import create_subvolume, mount_subvolume
+from .disk.btrfs import manage_btrfs_subvolumes
from .disk.partition import get_mount_fs_type
from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError
@@ -184,12 +184,38 @@ class Installer:
mountpoints = {}
for blockdevice in layouts:
for partition in layouts[blockdevice]['partitions']:
- mountpoints[partition['mountpoint']] = partition
-
+ if (subvolumes := partition.get('btrfs', {}).get('subvolumes', {})):
+ if partition.get('encrypted',False):
+ if partition.get('mountpoint',None):
+ ppath = partition['mountpoint']
+ else:
+ ppath = partition['device_instance'].path
+ loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(ppath).name}loop"
+ # Immediately unlock the encrypted device to format the inner volume
+ with luks2(partition['device_instance'], loopdev, partition['!password'], auto_unmount=False) as unlocked_device:
+ unlocked_device.mount(f"{self.target}/")
+ try:
+ manage_btrfs_subvolumes(self,partition,mountpoints,subvolumes,unlocked_device)
+ except Exception as e:
+ # every exception unmounts the physical volume. Otherwise we let the system in an unstable state
+ unlocked_device.unmount()
+ raise e
+ unlocked_device.unmount()
+ # TODO generate key
+ else:
+ self.mount(partition['device_instance'],"/")
+ try:
+ manage_btrfs_subvolumes(self,partition,mountpoints,subvolumes)
+ except Exception as e:
+ # every exception unmounts the physical volume. Otherwise we let the system in an unstable state
+ partition['device_instance'].unmount()
+ raise e
+ partition['device_instance'].unmount()
+ else:
+ mountpoints[partition['mountpoint']] = partition
for mountpoint in sorted([mnt_dest for mnt_dest in mountpoints.keys() if mnt_dest != None]):
partition = mountpoints[mountpoint]
-
- if partition.get('encrypted', False):
+ if partition.get('encrypted', False) and not partition.get('subvolume',None):
loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['mountpoint']).name}loop"
if not (password := partition.get('!password', None)):
raise RequirementError(f"Missing mountpoint {mountpoint} encryption password in layout: {partition}")
@@ -215,19 +241,17 @@ class Installer:
else:
log(f"Mounting {mountpoint} to {self.target}{mountpoint} using {partition['device_instance']}", level=logging.INFO)
- partition['device_instance'].mount(f"{self.target}{mountpoint}")
-
+ if partition.get('options',[]):
+ mount_options = ','.join(partition['options'])
+ partition['device_instance'].mount(f"{self.target}{mountpoint}",options=mount_options)
+ else:
+ partition['device_instance'].mount(f"{self.target}{mountpoint}")
time.sleep(1)
try:
get_mount_info(f"{self.target}{mountpoint}", traverse=False)
except DiskError:
raise DiskError(f"Target {self.target}{mountpoint} never got mounted properly (unable to get mount information using findmnt).")
- if (subvolumes := partition.get('btrfs', {}).get('subvolumes', {})):
- for name, location in subvolumes.items():
- create_subvolume(self, location)
- mount_subvolume(self, location)
-
def mount(self, partition, mountpoint, create_mountpoint=True):
if create_mountpoint and not os.path.isdir(f'{self.target}{mountpoint}'):
os.makedirs(f'{self.target}{mountpoint}')
@@ -468,11 +492,14 @@ class Installer:
for partition in self.partitions:
if partition.filesystem == 'btrfs':
# if partition.encrypted:
- self.base_packages.append('btrfs-progs')
+ if 'btrfs-progs' not in self.base_packages:
+ self.base_packages.append('btrfs-progs')
if partition.filesystem == 'xfs':
- self.base_packages.append('xfsprogs')
+ if 'xfs' not in self.base_packages:
+ self.base_packages.append('xfsprogs')
if partition.filesystem == 'f2fs':
- self.base_packages.append('f2fs-tools')
+ if 'f2fs' not in self.base_packages:
+ self.base_packages.append('f2fs-tools')
# Configure mkinitcpio to handle some specific use cases.
if partition.filesystem == 'btrfs':
@@ -480,7 +507,6 @@ class Installer:
self.MODULES.append('btrfs')
if '/usr/bin/btrfs' not in self.BINARIES:
self.BINARIES.append('/usr/bin/btrfs')
-
# There is not yet an fsck tool for NTFS. If it's being used for the root filesystem, the hook should be removed.
if partition.filesystem == 'ntfs3' and partition.mountpoint == self.target:
if 'fsck' in self.HOOKS:
@@ -634,15 +660,21 @@ class Installer:
entry.write(f"initrd /initramfs-{kernel}.img\n")
# blkid doesn't trigger on loopback devices really well,
# so we'll use the old manual method until we get that sorted out.
-
+ if root_fs_type is not None:
+ options_entry = f'rw intel_pstate=no_hwp rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}\n'
+ else:
+ options_entry = f'rw intel_pstate=no_hwp {" ".join(self.KERNEL_PARAMS)}\n'
+ base_path,bind_path = split_bind_name(str(root_partition.path))
+ if bind_path is not None: # and root_fs_type == 'btrfs':
+ options_entry = f"rootflags=subvol={bind_path} " + options_entry
if real_device := self.detect_encryption(root_partition):
# TODO: We need to detect if the encrypted device is a whole disk encryption,
# or simply a partition encryption. Right now we assume it's a partition (and we always have)
log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.uuid}'.", level=logging.DEBUG)
- entry.write(f'options cryptdevice=PARTUUID={real_device.uuid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}\n')
+ entry.write(f'options cryptdevice=PARTUUID={real_device.uuid}:luksdev root=/dev/mapper/luksdev {options_entry}')
else:
log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=logging.DEBUG)
- entry.write(f'options root=PARTUUID={root_partition.uuid} rw intel_pstate=no_hwp rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}\n')
+ entry.write(f'options root=PARTUUID={root_partition.uuid} {options_entry}')
self.helper_flags['bootloader'] = bootloader
diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py
index 55eaa62f..255c75d9 100644
--- a/archinstall/lib/luks.py
+++ b/archinstall/lib/luks.py
@@ -172,4 +172,4 @@ class luks2:
def crypttab(self, installation, key_path :str, options=["luks", "key-slot=1"]):
log(f'Adding a crypttab entry for key {key_path} in {installation}', level=logging.INFO)
with open(f"{installation.target}/etc/crypttab", "a") as crypttab:
- crypttab.write(f"{self.mountpoint} UUID={convert_device_to_uuid(self.partition.path)} {key_path} {','.join(options)}\n") \ No newline at end of file
+ crypttab.write(f"{self.mountpoint} UUID={convert_device_to_uuid(self.partition.path)} {key_path} {','.join(options)}\n")