Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/archinstall
diff options
context:
space:
mode:
authoradvaithm <advaith.madhukar@gmail.com>2021-04-02 09:48:41 +0530
committeradvaithm <advaith.madhukar@gmail.com>2021-04-02 09:48:41 +0530
commitf4e616cd9e1328a83fdf6f1541fd495292474642 (patch)
tree605a932306254bec6a232888791897a1eb099ef2 /archinstall
parent72284e2b2a96f4c8c6603f3a900b956121253b92 (diff)
parent2c90f02b6b26cc489e90f7536fe8931a4b2b9195 (diff)
updated to latest commits
Diffstat (limited to 'archinstall')
-rw-r--r--archinstall/lib/disk.py105
-rw-r--r--archinstall/lib/exceptions.py5
-rw-r--r--archinstall/lib/general.py11
-rw-r--r--archinstall/lib/installer.py168
-rw-r--r--archinstall/lib/luks.py35
-rw-r--r--archinstall/lib/output.py12
-rw-r--r--archinstall/lib/profiles.py21
-rw-r--r--archinstall/lib/user_interaction.py84
8 files changed, 319 insertions, 122 deletions
diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py
index d05588a6..fbc11ca3 100644
--- a/archinstall/lib/disk.py
+++ b/archinstall/lib/disk.py
@@ -1,5 +1,5 @@
import glob, re, os, json, time, hashlib
-import pathlib
+import pathlib, traceback
from collections import OrderedDict
from .exceptions import DiskError
from .general import *
@@ -108,7 +108,7 @@ class BlockDevice():
if part_id not in self.part_cache:
## TODO: Force over-write even if in cache?
if part_id not in self.part_cache or self.part_cache[part_id].size != part['size']:
- self.part_cache[part_id] = Partition(root_path + part_id, part_id=part_id, size=part['size'])
+ self.part_cache[part_id] = Partition(root_path + part_id, self, part_id=part_id, size=part['size'])
return {k: self.part_cache[k] for k in sorted(self.part_cache)}
@@ -130,16 +130,22 @@ class BlockDevice():
return True
return False
+ def flush_cache(self):
+ self.part_cache = OrderedDict()
+
class Partition():
- def __init__(self, path, part_id=None, size=-1, filesystem=None, mountpoint=None, encrypted=False, autodetect_filesystem=True):
+ def __init__(self, path :str, block_device :BlockDevice, part_id=None, size=-1, filesystem=None, mountpoint=None, encrypted=False, autodetect_filesystem=True):
if not part_id:
part_id = os.path.basename(path)
+
+ self.block_device = block_device
self.path = path
self.part_id = part_id
self.mountpoint = mountpoint
self.target_mountpoint = mountpoint
self.filesystem = filesystem
self.size = size # TODO: Refresh?
+ self._encrypted = None
self.encrypted = encrypted
self.allow_formatting = False # A fail-safe for unconfigured partitions, such as windows NTFS partitions.
@@ -175,28 +181,48 @@ class Partition():
elif self.target_mountpoint:
mount_repr = f", rel_mountpoint={self.target_mountpoint}"
- if self.encrypted:
+ if self._encrypted:
return f'Partition(path={self.path}, real_device={self.real_device}, fs={self.filesystem}{mount_repr})'
else:
return f'Partition(path={self.path}, fs={self.filesystem}{mount_repr})'
@property
+ def encrypted(self):
+ return self._encrypted
+
+ @encrypted.setter
+ def encrypted(self, value :bool):
+ if value:
+ log(f'Marking {self} as encrypted: {value}', level=LOG_LEVELS.Debug)
+ log(f"Callstrack when marking the partition: {''.join(traceback.format_stack())}", level=LOG_LEVELS.Debug)
+
+ self._encrypted = value
+
+ @property
def real_device(self):
- if not self.encrypted:
+ if not self._encrypted:
return self.path
else:
for blockdevice in json.loads(b''.join(sys_command('lsblk -J')).decode('UTF-8'))['blockdevices']:
if (parent := self.find_parent_of(blockdevice, os.path.basename(self.path))):
return f"/dev/{parent}"
- raise DiskError(f'Could not find appropriate parent for encrypted partition {self}')
+ # raise DiskError(f'Could not find appropriate parent for encrypted partition {self}')
+ return self.path
def detect_inner_filesystem(self, password):
log(f'Trying to detect inner filesystem format on {self} (This might take a while)', level=LOG_LEVELS.Info)
from .luks import luks2
- with luks2(self, 'luksloop', password, auto_unmount=True) as unlocked_device:
- return unlocked_device.filesystem
+
+ try:
+ with luks2(self, 'luksloop', password, auto_unmount=True) as unlocked_device:
+ return unlocked_device.filesystem
+ except SysCallError:
+ return None
def has_content(self):
+ if not get_filesystem_type(self.path):
+ return False
+
temporary_mountpoint = '/tmp/'+hashlib.md5(bytes(f"{time.time()}", 'UTF-8')+os.urandom(12)).hexdigest()
temporary_path = pathlib.Path(temporary_mountpoint)
@@ -213,8 +239,10 @@ class Partition():
def safe_to_format(self):
if self.allow_formatting is False:
+ log(f"Partition {self} is not marked for formatting.", level=LOG_LEVELS.Debug)
return False
elif self.target_mountpoint == '/boot' and self.has_content():
+ log(f"Partition {self} is a boot partition and has content inside.", level=LOG_LEVELS.Debug)
return False
return True
@@ -225,10 +253,11 @@ class Partition():
"""
from .luks import luks2
- if not self.encrypted:
+ if not self._encrypted:
raise DiskError(f"Attempting to encrypt a partition that was not marked for encryption: {self}")
if not self.safe_to_format():
+ log(f"Partition {self} was marked as protected but encrypt() was called on it!", level=LOG_LEVELS.Error, fg="red")
return False
handle = luks2(self, None, None)
@@ -247,6 +276,11 @@ class Partition():
if allow_formatting is None:
allow_formatting = self.allow_formatting
+ # To avoid "unable to open /dev/x: No such file or directory"
+ start_wait = time.time()
+ while pathlib.Path(path).exists() is False and time.time() - start_wait < 10:
+ time.sleep(0.025)
+
if not allow_formatting:
raise PermissionError(f"{self} is not formatable either because instance is locked ({self.allow_formatting}) or a blocking flag was given ({allow_formatting})")
@@ -288,6 +322,12 @@ class Partition():
else:
raise UnknownFilesystemFormat(f"Fileformat '{filesystem}' is not yet implemented.")
+
+ if get_filesystem_type(path) == 'crypto_LUKS' or get_filesystem_type(self.real_device) == 'crypto_LUKS':
+ self.encrypted = True
+ else:
+ self.encrypted = False
+
return True
def find_parent_of(self, data, name, parent=None):
@@ -313,6 +353,24 @@ class Partition():
self.mountpoint = target
return True
+ def unmount(self):
+ try:
+ exit_code = sys_command(f'/usr/bin/umount {self.path}').exit_code
+ except SysCallError as err:
+ exit_code = err.exit_code
+
+ # Without to much research, it seams that low error codes are errors.
+ # And above 8k is indicators such as "/dev/x not mounted.".
+ # So anything in between 0 and 8k are errors (?).
+ if exit_code > 0 and exit_code < 8000:
+ raise err
+
+ self.mountpoint = None
+ return True
+
+ def umount(self):
+ return self.unmount()
+
def filesystem_supported(self):
"""
The support for a filesystem (this partition) is tested by calling
@@ -343,7 +401,8 @@ class Filesystem():
if self.blockdevice.keep_partitions is False:
log(f'Wiping {self.blockdevice} by using partition format {self.mode}', level=LOG_LEVELS.Debug)
if self.mode == GPT:
- if sys_command(f'/usr/bin/parted -s {self.blockdevice.device} mklabel gpt',).exit_code == 0:
+ if self.raw_parted(f'{self.blockdevice.device} mklabel gpt').exit_code == 0:
+ self.blockdevice.flush_cache()
return self
else:
raise DiskError(f'Problem setting the partition format to GPT:', f'/usr/bin/parted -s {self.blockdevice.device} mklabel gpt')
@@ -380,7 +439,7 @@ class Filesystem():
def raw_parted(self, string:str):
x = sys_command(f'/usr/bin/parted -s {string}')
- o = b''.join(x)
+ log(f"'parted -s {string}' returned: {b''.join(x)}", level=LOG_LEVELS.Debug)
return x
def parted(self, string:str):
@@ -392,25 +451,33 @@ class Filesystem():
"""
return self.raw_parted(string).exit_code
- def use_entire_disk(self, root_filesystem_type='ext4', encrypt_root_partition=True):
- self.add_partition('primary', start='1MiB', end='513MiB', format='vfat')
- #TODO: figure out what do for bios, we don't need a seprate partion for the bootloader
+ def use_entire_disk(self, root_filesystem_type='ext4'):
+ log(f"Using and formatting the entire {self.blockdevice}.", level=LOG_LEVELS.Debug)
if hasUEFI():
+ self.add_partition('primary', start='1MiB', end='513MiB', format='fat32')
self.set_name(0, 'EFI')
self.set(0, 'boot on')
- # TODO: Probably redundant because in GPT mode 'esp on' is an alias for "boot on"?
- # https://www.gnu.org/software/parted/manual/html_node/set.html
+ # TODO: Probably redundant because in GPT mode 'esp on' is an alias for "boot on"?
+ # https://www.gnu.org/software/parted/manual/html_node/set.html
self.set(0, 'esp on')
self.add_partition('primary', start='513MiB', end='100%')
self.blockdevice.partition[0].filesystem = 'vfat'
self.blockdevice.partition[1].filesystem = root_filesystem_type
+ log(f"Set the root partition {self.blockdevice.partition[1]} to use filesystem {root_filesystem_type}.", level=LOG_LEVELS.Debug)
self.blockdevice.partition[0].target_mountpoint = '/boot'
self.blockdevice.partition[1].target_mountpoint = '/'
- if encrypt_root_partition:
- self.blockdevice.partition[1].encrypted = True
+ self.blockdevice.partition[0].allow_formatting = True
+ self.blockdevice.partition[1].allow_formatting = True
+ else:
+ #we don't need a seprate boot partition it would be a waste of space
+ self.add_partition('primary', start='1MB', end='100%')
+ self.blockdevice.partition[0].filesystem=root_filesystem_type
+ log(f"Set the root partition {self.blockdevice.partition[0]} to use filesystem {root_filesystem_type}.", level=LOG_LEVELS.Debug)
+ self.blockdevice.partition[0].target_mountpoint = '/'
+ self.blockdevice.partition[0].allow_formatting = True
def add_partition(self, type, start, end, format=None):
log(f'Adding partition to {self.blockdevice}', level=LOG_LEVELS.Info)
@@ -507,4 +574,4 @@ def get_filesystem_type(path):
handle = sys_command(f"blkid -o value -s TYPE {path}")
return b''.join(handle).strip().decode('UTF-8')
except SysCallError:
- return None \ No newline at end of file
+ return None
diff --git a/archinstall/lib/exceptions.py b/archinstall/lib/exceptions.py
index 5a5d47c6..a320eef6 100644
--- a/archinstall/lib/exceptions.py
+++ b/archinstall/lib/exceptions.py
@@ -7,7 +7,10 @@ class UnknownFilesystemFormat(BaseException):
class ProfileError(BaseException):
pass
class SysCallError(BaseException):
- pass
+ def __init__(self, message, exit_code):
+ super(SysCallError, self).__init__(message)
+ self.message = message
+ self.exit_code = exit_code
class ProfileNotFound(BaseException):
pass
class HardwareIncompatibilityError(BaseException):
diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py
index e87e4102..f2a714e7 100644
--- a/archinstall/lib/general.py
+++ b/archinstall/lib/general.py
@@ -105,8 +105,13 @@ class sys_command():#Thread):
self.status = 'starting'
user_catalogue = os.path.expanduser('~')
- self.cwd = f"{user_catalogue}/.cache/archinstall/workers/{kwargs['worker_id']}/"
- self.exec_dir = f'{self.cwd}/{os.path.basename(self.cmd[0])}_workingdir'
+
+ if (workdir := kwargs.get('workdir', None)):
+ self.cwd = workdir
+ self.exec_dir = workdir
+ else:
+ self.cwd = f"{user_catalogue}/.cache/archinstall/workers/{kwargs['worker_id']}/"
+ self.exec_dir = f'{self.cwd}/{os.path.basename(self.cmd[0])}_workingdir'
if not self.cmd[0][0] == '/':
# "which" doesn't work as it's a builtin to bash.
@@ -251,7 +256,7 @@ class sys_command():#Thread):
if self.exit_code != 0 and not self.kwargs['suppress_errors']:
#self.log(self.trace_log.decode('UTF-8'), level=LOG_LEVELS.Debug)
#self.log(f"'{self.raw_cmd}' did not exit gracefully, exit code {self.exit_code}.", level=LOG_LEVELS.Error)
- raise SysCallError(f"{self.trace_log.decode('UTF-8')}\n'{self.raw_cmd}' did not exit gracefully (trace log above), exit code: {self.exit_code}")
+ raise SysCallError(message=f"{self.trace_log.decode('UTF-8')}\n'{self.raw_cmd}' did not exit gracefully (trace log above), exit code: {self.exit_code}", exit_code=self.exit_code)
self.ended = time.time()
with open(f'{self.cwd}/trace.log', 'wb') as fh:
diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py
index e8829296..d161c3b7 100644
--- a/archinstall/lib/installer.py
+++ b/archinstall/lib/installer.py
@@ -1,4 +1,4 @@
-import os, stat, time, shutil, subprocess
+import os, stat, time, shutil, pathlib
from .exceptions import *
from .disk import *
@@ -9,7 +9,6 @@ from .mirrors import *
from .systemd import Networkd
from .output import log, LOG_LEVELS
from .storage import storage
-from .hardware import *
class Installer():
"""
@@ -54,8 +53,6 @@ class Installer():
}
self.base_packages = base_packages.split(' ')
- if not hasUEFI():
- base_packages.append('grub') # if it isn't uefi is must be bios therefore we need grub as systemd-boot is uefi only
self.post_base_install = []
storage['session'] = self
@@ -172,10 +169,19 @@ class Installer():
return True if sys_command(f'/usr/bin/arch-chroot {self.mountpoint} locale-gen').exit_code == 0 else False
def set_timezone(self, zone, *args, **kwargs):
- if not len(zone): return True
+ if not zone: return True
+ if not len(zone): return True # Redundant
- o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} ln -s /usr/share/zoneinfo/{zone} /etc/localtime'))
- return True
+ if (pathlib.Path("/usr")/"share"/"zoneinfo"/zone).exists():
+ (pathlib.Path(self.mountpoint)/"etc"/"localtime").unlink(missing_ok=True)
+ sys_command(f'/usr/bin/arch-chroot {self.mountpoint} ln -s /usr/share/zoneinfo/{zone} /etc/localtime')
+ return True
+ else:
+ self.log(
+ f"Time zone {zone} does not exist, continuing with system default.",
+ level=LOG_LEVELS.Warning,
+ fg='red'
+ )
def activate_ntp(self):
self.log(f'Installing and activating NTP.', level=LOG_LEVELS.Info)
@@ -292,13 +298,22 @@ class Installer():
# TODO: Use python functions for this
sys_command(f'/usr/bin/arch-chroot {self.mountpoint} chmod 700 /root')
+ # Configure mkinitcpio to handle some specific use cases.
+ # TODO: Yes, we should not overwrite the entire thing, but for now this should be fine
+ # since we just installed the base system.
if self.partition.filesystem == 'btrfs':
with open(f'{self.mountpoint}/etc/mkinitcpio.conf', 'w') as mkinit:
- ## TODO: Don't replace it, in case some update in the future actually adds something.
mkinit.write('MODULES=(btrfs)\n')
mkinit.write('BINARIES=(/usr/bin/btrfs)\n')
mkinit.write('FILES=()\n')
- mkinit.write('HOOKS=(base udev autodetect modconf block encrypt filesystems keyboard fsck)\n')
+ mkinit.write('HOOKS=(base udev autodetect modconf block encrypt filesystems keymap keyboard fsck)\n')
+ sys_command(f'/usr/bin/arch-chroot {self.mountpoint} mkinitcpio -p linux')
+ elif self.partition.encrypted:
+ with open(f'{self.mountpoint}/etc/mkinitcpio.conf', 'w') as mkinit:
+ mkinit.write('MODULES=()\n')
+ mkinit.write('BINARIES=()\n')
+ mkinit.write('FILES=()\n')
+ mkinit.write('HOOKS=(base udev autodetect modconf block encrypt filesystems keymap keyboard fsck)\n')
sys_command(f'/usr/bin/arch-chroot {self.mountpoint} mkinitcpio -p linux')
self.helper_flags['base'] = True
@@ -314,62 +329,75 @@ class Installer():
self.log(f'Adding bootloader {bootloader} to {self.boot_partition}', level=LOG_LEVELS.Info)
if bootloader == 'systemd-bootctl':
- if hasUEFI():
- o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} bootctl --no-variables --path=/boot install'))
- with open(f'{self.mountpoint}/boot/loader/loader.conf', 'w') as loader:
- loader.write('default arch\n')
- loader.write('timeout 5\n')
-
- ## For some reason, blkid and /dev/disk/by-uuid are not getting along well.
- ## And blkid is wrong in terms of LUKS.
- #UUID = sys_command('blkid -s PARTUUID -o value {drive}{partition_2}'.format(**args)).decode('UTF-8').strip()
- with open(f'{self.mountpoint}/boot/loader/entries/arch.conf', 'w') as entry:
- entry.write('title Arch Linux\n')
- entry.write('linux /vmlinuz-linux\n')
- entry.write('initrd /initramfs-linux.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 self.partition.encrypted:
- for root, folders, uids in os.walk('/dev/disk/by-uuid'):
- for uid in uids:
- real_path = os.path.realpath(os.path.join(root, uid))
- if not os.path.basename(real_path) == os.path.basename(self.partition.real_device): continue
- if hasAMDCPU(): # intel_paste is intel only, it's redudant on AMD systens
- entry.write(f'options cryptdevice=UUID={uid}:luksdev root=/dev/mapper/luksdev rw\n')
- else:
- entry.write(f'options cryptdevice=UUID={uid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp\n')
-
- self.helper_flags['bootloader'] = bootloader
- return True
- break
- else:
- for root, folders, uids in os.walk('/dev/disk/by-partuuid'):
- for uid in uids:
- real_path = os.path.realpath(os.path.join(root, uid))
- if not os.path.basename(real_path) == os.path.basename(self.partition.path): continue
- if hasAMDCPU():
- entry.write(f'options root=PARTUUID={uid} rw\n')
- else:
- entry.write(f'options root=PARTUUID={uid} rw intel_pstate=no_hwp\n')
-
- self.helper_flags['bootloader'] = bootloader
- return True
- break
- raise RequirementError(f"Could not identify the UUID of {self.partition}, there for {self.mountpoint}/boot/loader/entries/arch.conf will be broken until fixed.")
- else:
- raise RequirementError("Systemd-boot is UEFI only it can not be installed or used on bios")
- elif bootloader == 'grub-install':
- if hasUEFI():
- o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB'))
- sys_command('/usr/bin/arch-chroot grub-mkconfig -o /boot/grub/grub.cfg')
+ # TODO: Ideally we would want to check if another config
+ # points towards the same disk and/or partition.
+ # And in which case we should do some clean up.
+
+ # Install the boot loader
+ sys_command(f'/usr/bin/arch-chroot {self.mountpoint} bootctl --no-variables --path=/boot install')
+
+ # Modify or create a loader.conf
+ if os.path.isfile(f'{self.mountpoint}/boot/loader/loader.conf'):
+ with open(f'{self.mountpoint}/boot/loader/loader.conf', 'r') as loader:
+ loader_data = loader.read().split('\n')
else:
- root_device = subprocess.check_output(f'basename "$(readlink -f "/sys/class/block/{self.partition.path.strip("/dev/")}/..")',shell=True).decode().strip()
- if root_device == "block":
- root_device = f"{self.partition.path}"
- o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} grub-install --target=--target=i386-pc /dev/{root_device}'))
- sys_command('/usr/bin/arch-chroot grub-mkconfig -o /boot/grub/grub.cfg')
+ loader_data = [
+ f"default {self.init_time}",
+ f"timeout 5"
+ ]
+
+ with open(f'{self.mountpoint}/boot/loader/loader.conf', 'w') as loader:
+ for line in loader_data:
+ if line[:8] == 'default ':
+ loader.write(f'default {self.init_time}\n')
+ else:
+ loader.write(f"{line}")
+
+ ## For some reason, blkid and /dev/disk/by-uuid are not getting along well.
+ ## And blkid is wrong in terms of LUKS.
+ #UUID = sys_command('blkid -s PARTUUID -o value {drive}{partition_2}'.format(**args)).decode('UTF-8').strip()
+
+ # Setup the loader entry
+ with open(f'{self.mountpoint}/boot/loader/entries/{self.init_time}.conf', 'w') as entry:
+ entry.write(f'# Created by: archinstall\n')
+ entry.write(f'# Created on: {self.init_time}\n')
+ entry.write(f'title Arch Linux\n')
+ entry.write(f'linux /vmlinuz-linux\n')
+ entry.write(f'initrd /initramfs-linux.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 self.partition.encrypted:
+ log(f"Identifying root partition by DISK-UUID on {self.partition}, looking for '{os.path.basename(self.partition.real_device)}'.", level=LOG_LEVELS.Debug)
+ for root, folders, uids in os.walk('/dev/disk/by-uuid'):
+ for uid in uids:
+ real_path = os.path.realpath(os.path.join(root, uid))
+
+ log(f"Checking root partition match {os.path.basename(real_path)} against {os.path.basename(self.partition.real_device)}: {os.path.basename(real_path) == os.path.basename(self.partition.real_device)}", level=LOG_LEVELS.Debug)
+ if not os.path.basename(real_path) == os.path.basename(self.partition.real_device): continue
+
+ entry.write(f'options cryptdevice=UUID={uid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp\n')
+
+ self.helper_flags['bootloader'] = bootloader
+ return True
+ break
+ else:
+ log(f"Identifying root partition by PART-UUID on {self.partition}, looking for '{os.path.basename(self.partition.path)}'.", level=LOG_LEVELS.Debug)
+ for root, folders, uids in os.walk('/dev/disk/by-partuuid'):
+ for uid in uids:
+ real_path = os.path.realpath(os.path.join(root, uid))
+
+ log(f"Checking root partition match {os.path.basename(real_path)} against {os.path.basename(self.partition.path)}: {os.path.basename(real_path) == os.path.basename(self.partition.path)}", level=LOG_LEVELS.Debug)
+ if not os.path.basename(real_path) == os.path.basename(self.partition.path): continue
+
+ entry.write(f'options root=PARTUUID={uid} rw intel_pstate=no_hwp\n')
+
+ self.helper_flags['bootloader'] = bootloader
+ return True
+ break
+
+ raise RequirementError(f"Could not identify the UUID of {self.partition}, there for {self.mountpoint}/boot/loader/entries/arch.conf will be broken until fixed.")
else:
raise RequirementError(f"Unknown (or not yet implemented) bootloader added to add_bootloader(): {bootloader}")
@@ -377,7 +405,17 @@ class Installer():
return self.pacstrap(*packages)
def install_profile(self, profile):
- profile = Profile(self, profile)
+ # TODO: Replace this with a import archinstall.session instead in the profiles.
+ # The tricky thing with doing the import archinstall.session instead is that
+ # profiles might be run from a different chroot, and there's no way we can
+ # guarantee file-path safety when accessing the installer object that way.
+ # Doing the __builtins__ replacement, ensures that the global vriable "installation"
+ # is always kept up to date. It's considered a nasty hack - but it's a safe way
+ # of ensuring 100% accuracy of archinstall session variables.
+ __builtins__['installation'] = self
+
+ if type(profile) == str:
+ profile = Profile(self, profile)
self.log(f'Installing network profile {profile}', level=LOG_LEVELS.Info)
return profile.install()
diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py
index e54641b8..19c21795 100644
--- a/archinstall/lib/luks.py
+++ b/archinstall/lib/luks.py
@@ -64,8 +64,37 @@ class luks2():
with open(key_file, 'wb') as fh:
fh.write(password)
- o = b''.join(sys_command(f'/usr/bin/cryptsetup -q -v --type luks2 --pbkdf argon2i --hash {hash_type} --key-size {key_size} --iter-time {iter_time} --key-file {os.path.abspath(key_file)} --use-urandom luksFormat {partition.path}'))
- if b'Command successful.' not in o:
+ try:
+ # Try to setup the crypt-device
+ cmd_handle = sys_command(f'/usr/bin/cryptsetup -q -v --type luks2 --pbkdf argon2i --hash {hash_type} --key-size {key_size} --iter-time {iter_time} --key-file {os.path.abspath(key_file)} --use-urandom luksFormat {partition.path}')
+ except SysCallError as err:
+ if err.exit_code == 256:
+ log(f'{partition} is being used, trying to unmount and crypt-close the device and running one more attempt at encrypting the device.', level=LOG_LEVELS.Debug)
+ # Partition was in use, unmount it and try again
+ partition.unmount()
+
+ # Get crypt-information about the device by doing a reverse lookup starting with the partition path
+ # For instance: /dev/sda
+ devinfo = json.loads(b''.join(sys_command(f"lsblk --fs -J {partition.path}")).decode('UTF-8'))['blockdevices'][0]
+
+ # For each child (sub-partition/sub-device)
+ if len(children := devinfo.get('children', [])):
+ for child in children:
+ # Unmount the child location
+ if child_mountpoint := child.get('mountpoint', None):
+ log(f'Unmounting {child_mountpoint}', level=LOG_LEVELS.Debug)
+ sys_command(f"umount -R {child_mountpoint}")
+
+ # And close it if possible.
+ log(f"Closing crypt device {child['name']}", level=LOG_LEVELS.Debug)
+ sys_command(f"cryptsetup close {child['name']}")
+
+ # Then try again to set up the crypt-device
+ cmd_handle = sys_command(f'/usr/bin/cryptsetup -q -v --type luks2 --pbkdf argon2i --hash {hash_type} --key-size {key_size} --iter-time {iter_time} --key-file {os.path.abspath(key_file)} --use-urandom luksFormat {partition.path}')
+ else:
+ raise err
+
+ if b'Command successful.' not in b''.join(cmd_handle):
raise DiskError(f'Could not encrypt volume "{partition.path}": {o}')
return key_file
@@ -84,7 +113,7 @@ class luks2():
sys_command(f'/usr/bin/cryptsetup open {partition.path} {mountpoint} --key-file {os.path.abspath(key_file)} --type luks2')
if os.path.islink(f'/dev/mapper/{mountpoint}'):
self.mapdev = f'/dev/mapper/{mountpoint}'
- unlocked_partition = Partition(self.mapdev, encrypted=True, filesystem=get_filesystem_type(self.mapdev), autodetect_filesystem=False)
+ unlocked_partition = Partition(self.mapdev, None, encrypted=True, filesystem=get_filesystem_type(self.mapdev), autodetect_filesystem=False)
unlocked_partition.allow_formatting = self.partition.allow_formatting
return unlocked_partition
diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py
index 0e0a295b..537fb695 100644
--- a/archinstall/lib/output.py
+++ b/archinstall/lib/output.py
@@ -96,7 +96,17 @@ def log(*args, **kwargs):
if (filename := storage.get('LOG_FILE', None)):
absolute_logfile = os.path.join(storage.get('LOG_PATH', './'), filename)
if not os.path.isfile(absolute_logfile):
- os.makedirs(os.path.dirname(absolute_logfile))
+ try:
+ Path(absolute_logfile).parents[0].mkdir(exist_ok=True, parents=True)
+ except PermissionError:
+ # Fallback to creating the log file in the current folder
+ err_string = f"Not enough permission to place log file at {absolute_logfile}, creating it in {Path('./').absolute()/filename} instead."
+ absolute_logfile = Path('./').absolute()/filename
+ absolute_logfile.parents[0].mkdir(exist_ok=True)
+ absolute_logfile = str(absolute_logfile)
+ storage['LOG_PATH'] = './'
+ log(err_string, fg="red")
+
Path(absolute_logfile).touch() # Overkill?
with open(absolute_logfile, 'a') as log_file:
diff --git a/archinstall/lib/profiles.py b/archinstall/lib/profiles.py
index 01c3288c..08b1d618 100644
--- a/archinstall/lib/profiles.py
+++ b/archinstall/lib/profiles.py
@@ -1,7 +1,7 @@
import os, urllib.request, urllib.parse, ssl, json, re
import importlib.util, sys, glob, hashlib
from collections import OrderedDict
-from .general import multisplit, sys_command, log
+from .general import multisplit, sys_command
from .exceptions import *
from .networking import *
from .output import log, LOG_LEVELS
@@ -76,6 +76,8 @@ class Script():
self.spec = None
self.examples = None
self.namespace = os.path.splitext(os.path.basename(self.path))[0]
+ self.original_namespace = self.namespace
+ log(f"Script {self} has been loaded with namespace '{self.namespace}'", level=LOG_LEVELS.Debug)
def __enter__(self, *args, **kwargs):
self.execute()
@@ -131,14 +133,13 @@ class Script():
self.spec = importlib.util.spec_from_file_location(self.namespace, self.path)
imported = importlib.util.module_from_spec(self.spec)
sys.modules[self.namespace] = imported
-
+
return self
def execute(self):
if not self.namespace in sys.modules or self.spec is None:
self.load_instructions()
- __builtins__['installation'] = self.installer # TODO: Replace this with a import archinstall.session instead
self.spec.loader.exec_module(sys.modules[self.namespace])
return sys.modules[self.namespace]
@@ -146,7 +147,6 @@ class Script():
class Profile(Script):
def __init__(self, installer, path, args={}):
super(Profile, self).__init__(path, installer)
- self._cache = None
def __dump__(self, *args, **kwargs):
return {'path' : self.path}
@@ -155,6 +155,10 @@ class Profile(Script):
return f'Profile({os.path.basename(self.profile)})'
def install(self):
+ # Before installing, revert any temporary changes to the namespace.
+ # This ensures that the namespace during installation is the original initation namespace.
+ # (For instance awesome instead of aweosme.py or app-awesome.py)
+ self.namespace = self.original_namespace
return self.execute()
def has_prep_function(self):
@@ -202,4 +206,11 @@ class Application(Profile):
elif parsed_url.scheme in ('https', 'http'):
return self.localize_path(self.profile)
else:
- raise ProfileNotFound(f"Application cannot handle scheme {parsed_url.scheme}") \ No newline at end of file
+ raise ProfileNotFound(f"Application cannot handle scheme {parsed_url.scheme}")
+
+ def install(self):
+ # Before installing, revert any temporary changes to the namespace.
+ # This ensures that the namespace during installation is the original initation namespace.
+ # (For instance awesome instead of aweosme.py or app-awesome.py)
+ self.namespace = self.original_namespace
+ return self.execute() \ No newline at end of file
diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py
index 7e7f5873..80db7be1 100644
--- a/archinstall/lib/user_interaction.py
+++ b/archinstall/lib/user_interaction.py
@@ -1,4 +1,4 @@
-import getpass
+import getpass, pathlib, os, shutil
from .exceptions import *
from .profiles import Profile
from .locale_helpers import search_keyboard_layout
@@ -9,15 +9,44 @@ from .networking import list_interfaces
## TODO: Some inconsistencies between the selection processes.
## Some return the keys from the options, some the values?
+def get_terminal_height():
+ return shutil.get_terminal_size().lines
+
+def get_terminal_width():
+ return shutil.get_terminal_size().columns
+
+def get_longest_option(options):
+ return max([len(x) for x in options])
+
def get_password(prompt="Enter a password: "):
while (passwd := getpass.getpass(prompt)):
passwd_verification = getpass.getpass(prompt='And one more time for verification: ')
if passwd != passwd_verification:
log(' * Passwords did not match * ', bg='black', fg='red')
continue
+
+ if len(passwd.strip()) <= 0:
+ break
+
return passwd
return None
+def print_large_list(options, padding=5, margin_bottom=0, separator=': '):
+ highest_index_number_length = len(str(len(options)))
+ longest_line = highest_index_number_length + len(separator) + get_longest_option(options) + padding
+ max_num_of_columns = get_terminal_width() // longest_line
+ max_options_in_cells = max_num_of_columns * (get_terminal_height()-margin_bottom)
+
+ if (len(options) > max_options_in_cells):
+ for index, option in enumerate(options):
+ print(f"{index}: {option}")
+ else:
+ for row in range(0, (get_terminal_height()-margin_bottom)):
+ for column in range(row, len(options), (get_terminal_height()-margin_bottom)):
+ spaces = " "*(longest_line - len(options[column]))
+ print(f"{str(column): >{highest_index_number_length}}{separator}{options[column]}", end = spaces)
+ print()
+
def ask_for_superuser_account(prompt='Create a required super-user with sudo privileges: ', forced=False):
while 1:
new_user = input(prompt).strip(' ')
@@ -31,7 +60,7 @@ def ask_for_superuser_account(prompt='Create a required super-user with sudo pri
raise UserError("No superuser was created.")
password = get_password(prompt=f'Password for user {new_user}: ')
- return {new_user: password}
+ return {new_user: {"!password" : password}}
def ask_for_additional_users(prompt='Any additional users to install (leave blank for no users): '):
users = {}
@@ -44,12 +73,23 @@ def ask_for_additional_users(prompt='Any additional users to install (leave blan
password = get_password(prompt=f'Password for user {new_user}: ')
if input("Should this user be a sudo (super) user (y/N): ").strip(' ').lower() in ('y', 'yes'):
- super_users[new_user] = password
+ super_users[new_user] = {"!password" : password}
else:
- users[new_user] = password
+ users[new_user] = {"!password" : password}
return users, super_users
+def ask_for_a_timezone():
+ timezone = input('Enter a valid timezone (Example: Europe/Stockholm): ').strip()
+ if (pathlib.Path("/usr")/"share"/"zoneinfo"/timezone).exists():
+ return timezone
+ else:
+ log(
+ f"Time zone {timezone} does not exist, continuing with system default.",
+ level=LOG_LEVELS.Warning,
+ fg='red'
+ )
+
def ask_to_configure_network():
# Optionally configure one network interface.
#while 1:
@@ -102,11 +142,10 @@ def ask_for_main_filesystem_format():
'btrfs' : 'btrfs',
'ext4' : 'ext4',
'xfs' : 'xfs',
- 'f2fs' : 'f2fs',
- 'vfat' : 'vfat'
+ 'f2fs' : 'f2fs'
}
- value = generic_select(options.values(), "Select your main partitions filesystem by number or free-text: ")
+ value = generic_select(options.values(), "Select which filesystem your main partition should use (by number or name): ")
return next((key for key, val in options.items() if val == value), None)
def generic_select(options, input_text="Select one of the above by index or absolute value: ", sort=True):
@@ -131,7 +170,10 @@ def generic_select(options, input_text="Select one of the above by index or abso
if len(selected_option.strip()) <= 0:
return None
elif selected_option.isdigit():
- selected_option = options[int(selected_option)]
+ selected_option = int(selected_option)
+ if selected_option >= len(options):
+ raise RequirementError(f'Selected option "{selected_option}" is out of range')
+ selected_option = options[selected_option]
elif selected_option in options:
pass # We gave a correct absolute value
else:
@@ -156,7 +198,10 @@ def select_disk(dict_o_disks):
print(f"{index}: {drive} ({dict_o_disks[drive]['size'], dict_o_disks[drive].device, dict_o_disks[drive]['label']})")
drive = input('Select one of the above disks (by number or full path): ')
if drive.isdigit():
- drive = dict_o_disks[drives[int(drive)]]
+ drive = int(drive)
+ if drive >= len(drives):
+ raise DiskError(f'Selected option "{drive}" is out of range')
+ drive = dict_o_disks[drives[drive]]
elif drive in dict_o_disks:
drive = dict_o_disks[drive]
else:
@@ -182,10 +227,10 @@ def select_profile(options):
for index, profile in enumerate(profiles):
print(f"{index}: {profile}")
- print(' -- The above list is pre-programmed profiles. --')
+ print(' -- The above list is a set of pre-programmed profiles. --')
print(' -- They might make it easier to install things like desktop environments. --')
- print(' -- (Leave blank to skip this next optional step) --')
- selected_profile = input('Any particular pre-programmed profile you want to install: ')
+ print(' -- (Leave blank and hit enter to skip this step and continue) --')
+ selected_profile = input('Enter a pre-programmed profile name if you want to install one: ')
if len(selected_profile.strip()) <= 0:
return None
@@ -265,24 +310,13 @@ def select_mirror_regions(mirrors, show_top_mirrors=True):
selected_mirrors = {}
if len(regions) >= 1:
- for index, region in enumerate(regions):
- print(f"{index}: {region}")
+ print_large_list(regions, margin_bottom=2)
- print(' -- You can enter ? or help to search for more regions --')
print(' -- You can skip this step by leaving the option blank --')
- print(' -- (You can use Shift + PageUp to scroll in the list --')
selected_mirror = input('Select one of the above regions to download packages from (by number or full name): ')
if len(selected_mirror.strip()) == 0:
return {}
- elif selected_mirror.lower() in ('?', 'help'):
- filter_string = input('Search for a region containing (example: "united"): ').strip().lower()
- for region in mirrors:
- if filter_string in region.lower():
- selected_mirrors[region] = mirrors[region]
-
- return selected_mirrors
-
elif selected_mirror.isdigit() and (pos := int(selected_mirror)) <= len(regions)-1:
region = regions[int(selected_mirror)]
selected_mirrors[region] = mirrors[region]
@@ -298,4 +332,4 @@ def select_mirror_regions(mirrors, show_top_mirrors=True):
return selected_mirrors
- raise RequirementError("Selecting mirror region require a least one region to be given as an option.") \ No newline at end of file
+ raise RequirementError("Selecting mirror region require a least one region to be given as an option.")