Many more manual changes

This commit is contained in:
Dylan Taylor 2021-05-15 12:29:57 -04:00
parent 8eebc8ade3
commit 69d675f4aa
17 changed files with 232 additions and 150 deletions

View File

@ -1,12 +1,11 @@
from typing import Optional import glob
import glob, re, os, json, time, hashlib import pathlib
import pathlib, traceback, logging import re
from collections import OrderedDict from collections import OrderedDict
from .exceptions import DiskError
from .general import * from .general import *
from .output import log
from .storage import storage
from .hardware import hasUEFI from .hardware import hasUEFI
from .output import log
ROOT_DIR_PATTERN = re.compile('^.*?/devices') ROOT_DIR_PATTERN = re.compile('^.*?/devices')
GPT = 0b00000001 GPT = 0b00000001

View File

@ -1,23 +1,41 @@
class RequirementError(BaseException): class RequirementError(BaseException):
pass pass
class DiskError(BaseException): class DiskError(BaseException):
pass pass
class UnknownFilesystemFormat(BaseException): class UnknownFilesystemFormat(BaseException):
pass pass
class ProfileError(BaseException): class ProfileError(BaseException):
pass pass
class SysCallError(BaseException): class SysCallError(BaseException):
def __init__(self, message, exit_code): def __init__(self, message, exit_code):
super(SysCallError, self).__init__(message) super(SysCallError, self).__init__(message)
self.message = message self.message = message
self.exit_code = exit_code self.exit_code = exit_code
class ProfileNotFound(BaseException): class ProfileNotFound(BaseException):
pass pass
class HardwareIncompatibilityError(BaseException): class HardwareIncompatibilityError(BaseException):
pass pass
class PermissionError(BaseException): class PermissionError(BaseException):
pass pass
class UserError(BaseException): class UserError(BaseException):
pass pass
class ServiceException(BaseException): class ServiceException(BaseException):
pass pass

View File

@ -1,11 +1,18 @@
import os, json, hashlib, shlex, sys import hashlib
import time, pty, logging import json
import logging
import os
import pty
import shlex
import sys
import time
from datetime import datetime, date from datetime import datetime, date
from subprocess import Popen, STDOUT, PIPE, check_output
from select import epoll, EPOLLIN, EPOLLHUP from select import epoll, EPOLLIN, EPOLLHUP
from typing import Union
from .exceptions import * from .exceptions import *
from .output import log from .output import log
from typing import Optional, Union
def gen_uid(entropy_length=256): def gen_uid(entropy_length=256):
return hashlib.sha512(os.urandom(entropy_length)).hexdigest() return hashlib.sha512(os.urandom(entropy_length)).hexdigest()

View File

@ -1,8 +1,11 @@
import os, subprocess, json import json
from .general import sys_command import os
from .networking import list_interfaces, enrichIfaceTypes import subprocess
from typing import Optional from typing import Optional
from .general import sys_command
from .networking import list_interfaces, enrich_iface_types
__packages__ = [ __packages__ = [
"mesa", "mesa",
"xf86-video-amdgpu", "xf86-video-amdgpu",
@ -53,7 +56,7 @@ AVAILABLE_GFX_DRIVERS = {
} }
def hasWifi()->bool: def hasWifi()->bool:
return 'WIRELESS' in enrichIfaceTypes(list_interfaces().values()).values() return 'WIRELESS' in enrich_iface_types(list_interfaces().values()).values()
def hasAMDCPU()->bool: def hasAMDCPU()->bool:
if subprocess.check_output("lscpu | grep AMD", shell=True).strip().decode(): if subprocess.check_output("lscpu | grep AMD", shell=True).strip().decode():

View File

@ -1,15 +1,11 @@
import os, stat, time, shutil, pathlib
import subprocess, logging
from .exceptions import *
from .disk import * from .disk import *
from .general import *
from .user_interaction import *
from .profiles import Profile
from .mirrors import *
from .systemd import Networkd
from .output import log
from .storage import storage
from .hardware import * from .hardware import *
from .mirrors import *
from .output import log
from .profiles import Profile
from .storage import storage
from .systemd import Networkd
from .user_interaction import *
# Any package that the Installer() is responsible for (optional and the default ones) # Any package that the Installer() is responsible for (optional and the default ones)
__packages__ = ["base", "base-devel", "linux-firmware", "linux", "linux-lts", "linux-zen", "linux-hardened"] __packages__ = ["base", "base-devel", "linux-firmware", "linux", "linux-lts", "linux-zen", "linux-hardened"]

View File

@ -4,6 +4,7 @@ import os
from .exceptions import * from .exceptions import *
# from .general import sys_command # from .general import sys_command
def list_keyboard_languages(): def list_keyboard_languages():
locale_dir = '/usr/share/kbd/keymaps/' locale_dir = '/usr/share/kbd/keymaps/'
@ -16,16 +17,19 @@ def list_keyboard_languages():
if os.path.splitext(file)[1] == '.gz': if os.path.splitext(file)[1] == '.gz':
yield file.strip('.gz').strip('.map') yield file.strip('.gz').strip('.map')
def verify_keyboard_layout(layout): def verify_keyboard_layout(layout):
for language in list_keyboard_languages(): for language in list_keyboard_languages():
if layout.lower() == language.lower(): if layout.lower() == language.lower():
return True return True
return False return False
def search_keyboard_layout(filter): def search_keyboard_layout(filter):
for language in list_keyboard_languages(): for language in list_keyboard_languages():
if filter.lower() in language.lower(): if filter.lower() in language.lower():
yield language yield language
def set_keyboard_language(locale): def set_keyboard_language(locale):
return subprocess.call(['loadkeys', locale]) == 0 return subprocess.call(['loadkeys', locale]) == 0

View File

@ -1,13 +1,9 @@
import os
import shlex
import time
import pathlib import pathlib
import logging
from .exceptions import *
from .general import *
from .disk import Partition from .disk import Partition
from .general import *
from .output import log from .output import log
from .storage import storage
class luks2(): class luks2():
def __init__(self, partition, mountpoint, password, key_file=None, auto_unmount=False, *args, **kwargs): def __init__(self, partition, mountpoint, password, key_file=None, auto_unmount=False, *args, **kwargs):
@ -22,9 +18,9 @@ class luks2():
self.mapdev = None self.mapdev = None
def __enter__(self): def __enter__(self):
#if self.partition.allow_formatting: # if self.partition.allow_formatting:
# self.key_file = self.encrypt(self.partition, *self.args, **self.kwargs) # self.key_file = self.encrypt(self.partition, *self.args, **self.kwargs)
#else: # else:
if not self.key_file: if not self.key_file:
self.key_file = f"/tmp/{os.path.basename(self.partition.path)}.disk_pw" # TODO: Make disk-pw-file randomly unique? self.key_file = f"/tmp/{os.path.basename(self.partition.path)}.disk_pw" # TODO: Make disk-pw-file randomly unique?

View File

@ -1,9 +1,8 @@
import urllib.request, logging import urllib.request
from .exceptions import *
from .general import * from .general import *
from .output import log from .output import log
from .storage import storage
def filter_mirrors_by_region(regions, destination='/etc/pacman.d/mirrorlist', tmp_dir='/root', *args, **kwargs): def filter_mirrors_by_region(regions, destination='/etc/pacman.d/mirrorlist', tmp_dir='/root', *args, **kwargs):
""" """
@ -22,6 +21,7 @@ def filter_mirrors_by_region(regions, destination='/etc/pacman.d/mirrorlist', tm
return True return True
def add_custom_mirrors(mirrors:list, *args, **kwargs): def add_custom_mirrors(mirrors:list, *args, **kwargs):
""" """
This will append custom mirror definitions in pacman.conf This will append custom mirror definitions in pacman.conf
@ -37,6 +37,7 @@ def add_custom_mirrors(mirrors:list, *args, **kwargs):
return True return True
def insert_mirrors(mirrors, *args, **kwargs): def insert_mirrors(mirrors, *args, **kwargs):
""" """
This function will insert a given mirror-list at the top of `/etc/pacman.d/mirrorlist`. This function will insert a given mirror-list at the top of `/etc/pacman.d/mirrorlist`.
@ -58,6 +59,7 @@ def insert_mirrors(mirrors, *args, **kwargs):
return True return True
def use_mirrors(regions :dict, destination='/etc/pacman.d/mirrorlist'): def use_mirrors(regions :dict, destination='/etc/pacman.d/mirrorlist'):
log(f'A new package mirror-list has been created: {destination}', level=logging.INFO) log(f'A new package mirror-list has been created: {destination}', level=logging.INFO)
for region, mirrors in regions.items(): for region, mirrors in regions.items():
@ -67,11 +69,13 @@ def use_mirrors(regions :dict, destination='/etc/pacman.d/mirrorlist'):
mirrorlist.write(f'Server = {mirror}\n') mirrorlist.write(f'Server = {mirror}\n')
return True return True
def re_rank_mirrors(top=10, *positionals, **kwargs): def re_rank_mirrors(top=10, *positionals, **kwargs):
if sys_command((f'/usr/bin/rankmirrors -n {top} /etc/pacman.d/mirrorlist > /etc/pacman.d/mirrorlist')).exit_code == 0: if sys_command((f'/usr/bin/rankmirrors -n {top} /etc/pacman.d/mirrorlist > /etc/pacman.d/mirrorlist')).exit_code == 0:
return True return True
return False return False
def list_mirrors(): def list_mirrors():
url = f"https://archlinux.org/mirrorlist/?protocol=https&ip_version=4&ip_version=6&use_mirror_status=on" url = f"https://archlinux.org/mirrorlist/?protocol=https&ip_version=4&ip_version=6&use_mirror_status=on"
regions = {} regions = {}

View File

@ -7,22 +7,25 @@ from .exceptions import *
from .general import sys_command from .general import sys_command
from .storage import storage from .storage import storage
def getHwAddr(ifname):
def get_hw_addr(ifname):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', bytes(ifname, 'utf-8')[:15])) info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', bytes(ifname, 'utf-8')[:15]))
return ':'.join('%02x' % b for b in info[18:24]) return ':'.join('%02x' % b for b in info[18:24])
def list_interfaces(skip_loopback=True): def list_interfaces(skip_loopback=True):
interfaces = OrderedDict() interfaces = OrderedDict()
for index, iface in socket.if_nameindex(): for index, iface in socket.if_nameindex():
if skip_loopback and iface == "lo": if skip_loopback and iface == "lo":
continue continue
mac = getHwAddr(iface).replace(':', '-').lower() mac = get_hw_addr(iface).replace(':', '-').lower()
interfaces[mac] = iface interfaces[mac] = iface
return interfaces return interfaces
def enrichIfaceTypes(interfaces :dict):
def enrich_iface_types(interfaces :dict):
result = {} result = {}
for iface in interfaces: for iface in interfaces:
if os.path.isdir(f"/sys/class/net/{iface}/bridge/"): if os.path.isdir(f"/sys/class/net/{iface}/bridge/"):
@ -39,11 +42,13 @@ def enrichIfaceTypes(interfaces :dict):
result[iface] = 'UNKNOWN' result[iface] = 'UNKNOWN'
return result return result
def get_interface_from_mac(mac): def get_interface_from_mac(mac):
return list_interfaces().get(mac.lower(), None) return list_interfaces().get(mac.lower(), None)
def wirelessScan(interface):
interfaces = enrichIfaceTypes(list_interfaces().values()) def wireless_scan(interface):
interfaces = enrich_iface_types(list_interfaces().values())
if interfaces[interface] != 'WIRELESS': if interfaces[interface] != 'WIRELESS':
raise HardwareIncompatibilityError(f"Interface {interface} is not a wireless interface: {interfaces}") raise HardwareIncompatibilityError(f"Interface {interface} is not a wireless interface: {interfaces}")
@ -56,12 +61,13 @@ def wirelessScan(interface):
storage['_WIFI'][interface]['scanning'] = True storage['_WIFI'][interface]['scanning'] = True
# TODO: Full WiFi experience might get evolved in the future, pausing for now 2021-01-25 # TODO: Full WiFi experience might get evolved in the future, pausing for now 2021-01-25
def getWirelessNetworks(interface): def get_wireless_networks(interface):
# TODO: Make this oneliner pritter to check if the interface is scanning or not. # TODO: Make this oneliner pritter to check if the interface is scanning or not.
if not '_WIFI' in storage or interface not in storage['_WIFI'] or storage['_WIFI'][interface].get('scanning', False) is False: if not '_WIFI' in storage or interface not in storage['_WIFI'] or storage['_WIFI'][interface].get('scanning', False) is False:
import time import time
wirelessScan(interface) wireless_scan(interface)
time.sleep(5) time.sleep(5)
for line in sys_command(f"iwctl station {interface} get-networks"): for line in sys_command(f"iwctl station {interface} get-networks"):

View File

@ -5,16 +5,18 @@ import logging
from pathlib import Path from pathlib import Path
from .storage import storage from .storage import storage
# TODO: use logging's built in levels instead. # TODO: use logging's built in levels instead.
# Although logging is threaded and I wish to avoid that. # Although logging is threaded and I wish to avoid that.
# It's more Pythonistic or w/e you want to call it. # It's more Pythonistic or w/e you want to call it.
class LOG_LEVELS: class LogLevels:
Critical = 0b001 Critical = 0b001
Error = 0b010 Error = 0b010
Warning = 0b011 Warning = 0b011
Info = 0b101 Info = 0b101
Debug = 0b111 Debug = 0b111
class journald(dict): class journald(dict):
@abc.abstractmethod @abc.abstractmethod
def log(message, level=logging.DEBUG): def log(message, level=logging.DEBUG):
@ -27,19 +29,19 @@ class journald(dict):
# to logging levels (and warn about deprecated usage) # to logging levels (and warn about deprecated usage)
# There's some code re-usage here but that should be fine. # There's some code re-usage here but that should be fine.
# TODO: Remove these in a few versions: # TODO: Remove these in a few versions:
if level == LOG_LEVELS.Critical: if level == LogLevels.Critical:
log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True)
level = logging.CRITICAL level = logging.CRITICAL
elif level == LOG_LEVELS.Error: elif level == LogLevels.Error:
log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True)
level = logging.ERROR level = logging.ERROR
elif level == LOG_LEVELS.Warning: elif level == LogLevels.Warning:
log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True)
level = logging.WARNING level = logging.WARNING
elif level == LOG_LEVELS.Info: elif level == LogLevels.Info:
log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True)
level = logging.INFO level = logging.INFO
elif level == LOG_LEVELS.Debug: elif level == LogLevels.Debug:
log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True)
level = logging.DEBUG level = logging.DEBUG
@ -52,11 +54,13 @@ class journald(dict):
log_adapter.log(level, message) log_adapter.log(level, message)
# TODO: Replace log() for session based logging. # TODO: Replace log() for session based logging.
class SessionLogging(): class SessionLogging:
def __init__(self): def __init__(self):
pass pass
# Found first reference here: https://stackoverflow.com/questions/7445658/how-to-detect-if-the-console-does-support-ansi-escape-codes-in-python # Found first reference here: https://stackoverflow.com/questions/7445658/how-to-detect-if-the-console-does-support-ansi-escape-codes-in-python
# And re-used this: https://github.com/django/django/blob/master/django/core/management/color.py#L12 # And re-used this: https://github.com/django/django/blob/master/django/core/management/color.py#L12
def supports_color(): def supports_color():
@ -70,6 +74,7 @@ def supports_color():
is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
return supported_platform and is_a_tty return supported_platform and is_a_tty
# Heavily influenced by: https://github.com/django/django/blob/ae8338daf34fd746771e0678081999b656177bae/django/utils/termcolors.py#L13 # Heavily influenced by: https://github.com/django/django/blob/ae8338daf34fd746771e0678081999b656177bae/django/utils/termcolors.py#L13
# Color options here: https://askubuntu.com/questions/528928/how-to-do-underline-bold-italic-strikethrough-color-background-and-size-i # Color options here: https://askubuntu.com/questions/528928/how-to-do-underline-bold-italic-strikethrough-color-background-and-size-i
def stylize_output(text :str, *opts, **kwargs): def stylize_output(text :str, *opts, **kwargs):
@ -94,6 +99,7 @@ def stylize_output(text :str, *opts, **kwargs):
text = '%s\x1b[%sm' % (text or '', RESET) text = '%s\x1b[%sm' % (text or '', RESET)
return '%s%s' % (('\x1b[%sm' % ';'.join(code_list)), text or '') return '%s%s' % (('\x1b[%sm' % ';'.join(code_list)), text or '')
def log(*args, **kwargs): def log(*args, **kwargs):
string = orig_string = ' '.join([str(x) for x in args]) string = orig_string = ' '.join([str(x) for x in args])
@ -132,19 +138,19 @@ def log(*args, **kwargs):
# to logging levels (and warn about deprecated usage) # to logging levels (and warn about deprecated usage)
# There's some code re-usage here but that should be fine. # There's some code re-usage here but that should be fine.
# TODO: Remove these in a few versions: # TODO: Remove these in a few versions:
if kwargs['level'] == LOG_LEVELS.Critical: if kwargs['level'] == LogLevels.Critical:
log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True)
kwargs['level'] = logging.CRITICAL kwargs['level'] = logging.CRITICAL
elif kwargs['level'] == LOG_LEVELS.Error: elif kwargs['level'] == LogLevels.Error:
log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True)
kwargs['level'] = logging.ERROR kwargs['level'] = logging.ERROR
elif kwargs['level'] == LOG_LEVELS.Warning: elif kwargs['level'] == LogLevels.Warning:
log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True)
kwargs['level'] = logging.WARNING kwargs['level'] = logging.WARNING
elif kwargs['level'] == LOG_LEVELS.Info: elif kwargs['level'] == LogLevels.Info:
log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True)
kwargs['level'] = logging.INFO kwargs['level'] = logging.INFO
elif kwargs['level'] == LOG_LEVELS.Debug: elif kwargs['level'] == LogLevels.Debug:
log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True)
kwargs['level'] = logging.DEBUG kwargs['level'] = logging.DEBUG
@ -156,7 +162,7 @@ def log(*args, **kwargs):
try: try:
journald.log(string, level=kwargs.get('level', logging.INFO)) journald.log(string, level=kwargs.get('level', logging.INFO))
except ModuleNotFoundError: except ModuleNotFoundError:
pass # Ignore writing to journald pass # Ignore writing to journald
# Finally, print the log unless we skipped it based on level. # Finally, print the log unless we skipped it based on level.
# We use sys.stdout.write()+flush() instead of print() to try and # We use sys.stdout.write()+flush() instead of print() to try and

View File

@ -1,10 +1,14 @@
import urllib.request, urllib.parse import json
import ssl, json import ssl
import urllib.parse
import urllib.request
from .exceptions import * from .exceptions import *
BASE_URL = 'https://archlinux.org/packages/search/json/?name={package}' BASE_URL = 'https://archlinux.org/packages/search/json/?name={package}'
BASE_GROUP_URL = 'https://archlinux.org/groups/x86_64/{group}/' BASE_GROUP_URL = 'https://archlinux.org/groups/x86_64/{group}/'
def find_group(name): def find_group(name):
ssl_context = ssl.create_default_context() ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False ssl_context.check_hostname = False
@ -21,6 +25,7 @@ def find_group(name):
if response.code == 200: if response.code == 200:
return True return True
def find_package(name): def find_package(name):
""" """
Finds a specific package via the package database. Finds a specific package via the package database.
@ -33,6 +38,7 @@ def find_package(name):
data = response.read().decode('UTF-8') data = response.read().decode('UTF-8')
return json.loads(data) return json.loads(data)
def find_packages(*names): def find_packages(*names):
""" """
This function returns the search results for many packages. This function returns the search results for many packages.
@ -44,6 +50,7 @@ def find_packages(*names):
result[package] = find_package(package) result[package] = find_package(package)
return result return result
def validate_package_list(packages :list): def validate_package_list(packages :list):
""" """
Validates a list of given packages. Validates a list of given packages.

View File

@ -1,13 +1,17 @@
import hashlib
import importlib.util
import json
import re
import ssl
import sys
import urllib.parse
import urllib.request
from typing import Optional from typing import Optional
import os, urllib.request, urllib.parse, ssl, json, re from .general import multisplit
import importlib.util, sys, glob, hashlib, logging
from collections import OrderedDict
from .general import multisplit, sys_command
from .exceptions import *
from .networking import * from .networking import *
from .output import log
from .storage import storage from .storage import storage
def grab_url_data(path): def grab_url_data(path):
safe_path = path[:path.find(':')+1]+''.join([item if item in ('/', '?', '=', '&') else urllib.parse.quote(item) for item in multisplit(path[path.find(':')+1:], ('/', '?', '=', '&'))]) safe_path = path[:path.find(':')+1]+''.join([item if item in ('/', '?', '=', '&') else urllib.parse.quote(item) for item in multisplit(path[path.find(':')+1:], ('/', '?', '=', '&'))])
ssl_context = ssl.create_default_context() ssl_context = ssl.create_default_context()
@ -16,6 +20,7 @@ def grab_url_data(path):
response = urllib.request.urlopen(safe_path, context=ssl_context) response = urllib.request.urlopen(safe_path, context=ssl_context)
return response.read() return response.read()
def list_profiles(filter_irrelevant_macs=True, subpath='', filter_top_level_profiles=False): def list_profiles(filter_irrelevant_macs=True, subpath='', filter_top_level_profiles=False):
# TODO: Grab from github page as well, not just local static files # TODO: Grab from github page as well, not just local static files
if filter_irrelevant_macs: if filter_irrelevant_macs:
@ -73,7 +78,8 @@ def list_profiles(filter_irrelevant_macs=True, subpath='', filter_top_level_prof
return cache return cache
class Script():
class Script:
def __init__(self, profile, installer=None): def __init__(self, profile, installer=None):
# profile: https://hvornum.se/something.py # profile: https://hvornum.se/something.py
# profile: desktop # profile: desktop
@ -154,6 +160,7 @@ class Script():
return sys.modules[self.namespace] return sys.modules[self.namespace]
class Profile(Script): class Profile(Script):
def __init__(self, installer, path, args={}): def __init__(self, installer, path, args={}):
super(Profile, self).__init__(path, installer) super(Profile, self).__init__(path, installer)
@ -238,6 +245,7 @@ class Profile(Script):
return imported.__packages__ return imported.__packages__
return None return None
class Application(Profile): class Application(Profile):
def __repr__(self, *args, **kwargs): def __repr__(self, *args, **kwargs):
return f'Application({os.path.basename(self.profile)})' return f'Application({os.path.basename(self.profile)})'

View File

@ -1,8 +1,6 @@
import os
from .exceptions import *
from .general import * from .general import *
def service_state(service_name: str): def service_state(service_name: str):
if os.path.splitext(service_name)[1] != '.service': if os.path.splitext(service_name)[1] != '.service':
service_name += '.service' # Just to be safe service_name += '.service' # Just to be safe

View File

@ -12,7 +12,7 @@ storage = {
'./profiles', './profiles',
'~/.config/archinstall/profiles', '~/.config/archinstall/profiles',
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'profiles'), os.path.join(os.path.dirname(os.path.abspath(__file__)), 'profiles'),
#os.path.abspath(f'{os.path.dirname(__file__)}/../examples') # os.path.abspath(f'{os.path.dirname(__file__)}/../examples')
], ],
'UPSTREAM_URL' : 'https://raw.githubusercontent.com/archlinux/archinstall/master/profiles', 'UPSTREAM_URL' : 'https://raw.githubusercontent.com/archlinux/archinstall/master/profiles',
'PROFILE_DB' : None, # Used in cases when listing profiles is desired, not mandatory for direct profile grabing. 'PROFILE_DB' : None, # Used in cases when listing profiles is desired, not mandatory for direct profile grabing.

View File

@ -1,4 +1,4 @@
class Ini(): class Ini:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
""" """
Limited INI handler for now. Limited INI handler for now.
@ -25,11 +25,13 @@ class Ini():
return result return result
class Systemd(Ini): class Systemd(Ini):
""" """
Placeholder class to do systemd specific setups. Placeholder class to do systemd specific setups.
""" """
class Networkd(Systemd): class Networkd(Systemd):
""" """
Placeholder class to do systemd-network specific setups. Placeholder class to do systemd-network specific setups.

View File

@ -1,27 +1,40 @@
import getpass, pathlib, os, shutil, re, time import getpass
import sys, time, signal, ipaddress, logging import ipaddress
import termios, tty, select # Used for char by char polling of sys.stdin import logging
import pathlib
import re
import select # Used for char by char polling of sys.stdin
import shutil
import signal
import sys
import termios
import time
import tty
from .exceptions import * from .exceptions import *
from .profiles import Profile
from .locale_helpers import list_keyboard_languages, verify_keyboard_layout, search_keyboard_layout
from .output import log
from .storage import storage
from .networking import list_interfaces
from .general import sys_command from .general import sys_command
from .hardware import AVAILABLE_GFX_DRIVERS, hasUEFI from .hardware import AVAILABLE_GFX_DRIVERS, hasUEFI
from .locale_helpers import list_keyboard_languages, verify_keyboard_layout, search_keyboard_layout
from .networking import list_interfaces
from .output import log
from .profiles import Profile
## TODO: Some inconsistencies between the selection processes.
## Some return the keys from the options, some the values? # TODO: Some inconsistencies between the selection processes.
# Some return the keys from the options, some the values?
def get_terminal_height(): def get_terminal_height():
return shutil.get_terminal_size().lines return shutil.get_terminal_size().lines
def get_terminal_width(): def get_terminal_width():
return shutil.get_terminal_size().columns return shutil.get_terminal_size().columns
def get_longest_option(options): def get_longest_option(options):
return max([len(x) for x in options]) return max([len(x) for x in options])
def check_for_correct_username(username): def check_for_correct_username(username):
if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32: if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32:
return True return True
@ -32,6 +45,7 @@ def check_for_correct_username(username):
) )
return False return False
def do_countdown(): def do_countdown():
SIG_TRIGGER = False SIG_TRIGGER = False
def kill_handler(sig, frame): def kill_handler(sig, frame):
@ -67,6 +81,7 @@ def do_countdown():
signal.signal(signal.SIGINT, original_sigint_handler) signal.signal(signal.SIGINT, original_sigint_handler)
return True return True
def get_password(prompt="Enter a password: "): def get_password(prompt="Enter a password: "):
while (passwd := getpass.getpass(prompt)): while (passwd := getpass.getpass(prompt)):
passwd_verification = getpass.getpass(prompt='And one more time for verification: ') passwd_verification = getpass.getpass(prompt='And one more time for verification: ')
@ -80,6 +95,7 @@ def get_password(prompt="Enter a password: "):
return passwd return passwd
return None return None
def print_large_list(options, padding=5, margin_bottom=0, separator=': '): def print_large_list(options, padding=5, margin_bottom=0, separator=': '):
highest_index_number_length = len(str(len(options))) highest_index_number_length = len(str(len(options)))
longest_line = highest_index_number_length + len(separator) + get_longest_option(options) + padding longest_line = highest_index_number_length + len(separator) + get_longest_option(options) + padding
@ -173,7 +189,7 @@ def generic_multi_select(options, text="Select one or more of the options above
return selected_options return selected_options
class MiniCurses(): class MiniCurses:
def __init__(self, width, height): def __init__(self, width, height):
self.width = width self.width = width
self.height = height self.height = height
@ -200,10 +216,10 @@ class MiniCurses():
if x < 0: x = 0 if x < 0: x = 0
if y < 0: y = 0 if y < 0: y = 0
#import time # import time
#sys.stdout.write(f"Clearing from: {x, y}") # sys.stdout.write(f"Clearing from: {x, y}")
#sys.stdout.flush() # sys.stdout.flush()
#time.sleep(2) # time.sleep(2)
sys.stdout.flush() sys.stdout.flush()
sys.stdout.write('\033[%d;%df' % (y, x)) sys.stdout.write('\033[%d;%df' % (y, x))
@ -259,16 +275,16 @@ class MiniCurses():
poller.register(sys.stdin.fileno(), select.EPOLLIN) poller.register(sys.stdin.fileno(), select.EPOLLIN)
EOF = False eof = False
while EOF is False: while eof is False:
for fileno, event in poller.poll(0.025): for fileno, event in poller.poll(0.025):
char = sys.stdin.read(1) char = sys.stdin.read(1)
#sys.stdout.write(f"{[char]}") # sys.stdout.write(f"{[char]}")
#sys.stdout.flush() # sys.stdout.flush()
if (newline := (char in ('\n', '\r'))): if newline := (char in ('\n', '\r')):
EOF = True eof = True
if not newline or strip_rowbreaks is False: if not newline or strip_rowbreaks is False:
response += char response += char
@ -287,6 +303,7 @@ class MiniCurses():
if response: if response:
return response return response
def ask_for_superuser_account(prompt='Username for required superuser with sudo privileges: ', forced=False): def ask_for_superuser_account(prompt='Username for required superuser with sudo privileges: ', forced=False):
while 1: while 1:
new_user = input(prompt).strip(' ') new_user = input(prompt).strip(' ')
@ -304,6 +321,7 @@ def ask_for_superuser_account(prompt='Username for required superuser with sudo
password = get_password(prompt=f'Password for user {new_user}: ') password = get_password(prompt=f'Password for user {new_user}: ')
return {new_user: {"!password" : password}} return {new_user: {"!password" : password}}
def ask_for_additional_users(prompt='Any additional users to install (leave blank for no users): '): def ask_for_additional_users(prompt='Any additional users to install (leave blank for no users): '):
users = {} users = {}
superusers = {} superusers = {}
@ -323,6 +341,7 @@ def ask_for_additional_users(prompt='Any additional users to install (leave blan
return users, superusers return users, superusers
def ask_for_a_timezone(): def ask_for_a_timezone():
while True: while True:
timezone = input('Enter a valid timezone (examples: Europe/Stockholm, US/Eastern) or press enter to use UTC: ').strip().strip('*.') timezone = input('Enter a valid timezone (examples: Europe/Stockholm, US/Eastern) or press enter to use UTC: ').strip().strip('*.')
@ -337,6 +356,7 @@ def ask_for_a_timezone():
fg='red' fg='red'
) )
def ask_for_bootloader() -> str: def ask_for_bootloader() -> str:
bootloader = "systemd-bootctl" bootloader = "systemd-bootctl"
if hasUEFI()==False: if hasUEFI()==False:
@ -347,6 +367,7 @@ def ask_for_bootloader() -> str:
bootloader="grub-install" bootloader="grub-install"
return bootloader return bootloader
def ask_for_audio_selection(): def ask_for_audio_selection():
audio = "pulseaudio" # Default for most desktop environments audio = "pulseaudio" # Default for most desktop environments
pipewire_choice = input("Would you like to install pipewire instead of pulseaudio as the default audio server? [Y/n] ").lower() pipewire_choice = input("Would you like to install pipewire instead of pulseaudio as the default audio server? [Y/n] ").lower()
@ -355,6 +376,7 @@ def ask_for_audio_selection():
return audio return audio
def ask_to_configure_network(): def ask_to_configure_network():
# Optionally configure one network interface. # Optionally configure one network interface.
#while 1: #while 1:
@ -422,6 +444,7 @@ def ask_to_configure_network():
return {} return {}
def ask_for_disk_layout(): def ask_for_disk_layout():
options = { options = {
'keep-existing' : 'Keep existing partition layout and select which ones to use where', 'keep-existing' : 'Keep existing partition layout and select which ones to use where',
@ -433,6 +456,7 @@ def ask_for_disk_layout():
allow_empty_input=False, sort=True) allow_empty_input=False, sort=True)
return next((key for key, val in options.items() if val == value), None) return next((key for key, val in options.items() if val == value), None)
def ask_for_main_filesystem_format(): def ask_for_main_filesystem_format():
options = { options = {
'btrfs' : 'btrfs', 'btrfs' : 'btrfs',
@ -445,6 +469,7 @@ def ask_for_main_filesystem_format():
allow_empty_input=False) allow_empty_input=False)
return next((key for key, val in options.items() if val == value), None) 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: ", allow_empty_input=True, options_output=True, sort=False): def generic_select(options, input_text="Select one of the above by index or absolute value: ", allow_empty_input=True, options_output=True, sort=False):
""" """
A generic select function that does not output anything A generic select function that does not output anything
@ -477,7 +502,6 @@ def generic_select(options, input_text="Select one of the above by index or abso
# As we pass only list and dict (converted to list), we can skip converting to list # As we pass only list and dict (converted to list), we can skip converting to list
options = sorted(options) options = sorted(options)
# Added ability to disable the output of options items, # Added ability to disable the output of options items,
# if another function displays something different from this # if another function displays something different from this
if options_output: if options_output:
@ -510,6 +534,7 @@ def generic_select(options, input_text="Select one of the above by index or abso
return selected_option return selected_option
def select_disk(dict_o_disks): def select_disk(dict_o_disks):
""" """
Asks the user to select a harddrive from the `dict_o_disks` selection. Asks the user to select a harddrive from the `dict_o_disks` selection.
@ -527,8 +552,7 @@ 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']})") print(f"{index}: {drive} ({dict_o_disks[drive]['size'], dict_o_disks[drive].device, dict_o_disks[drive]['label']})")
log(f"You can skip selecting a drive and partitioning and use whatever drive-setup is mounted at /mnt (experimental)", fg="yellow") log(f"You can skip selecting a drive and partitioning and use whatever drive-setup is mounted at /mnt (experimental)", fg="yellow")
drive = generic_select(drives, 'Select one of the above disks (by name or number) or leave blank to use /mnt: ', drive = generic_select(drives, 'Select one of the above disks (by name or number) or leave blank to use /mnt: ', options_output=False)
options_output=False)
if not drive: if not drive:
return drive return drive
@ -537,6 +561,7 @@ def select_disk(dict_o_disks):
raise DiskError('select_disk() requires a non-empty dictionary of disks to select from.') raise DiskError('select_disk() requires a non-empty dictionary of disks to select from.')
def select_profile(options): def select_profile(options):
""" """
Asks the user to select a profile from the `options` dictionary parameter. Asks the user to select a profile from the `options` dictionary parameter.
@ -565,6 +590,7 @@ def select_profile(options):
else: else:
raise RequirementError("Selecting profiles require a least one profile to be given as an option.") raise RequirementError("Selecting profiles require a least one profile to be given as an option.")
def select_language(options, show_only_country_codes=True): def select_language(options, show_only_country_codes=True):
""" """
Asks the user to select a language from the `options` dictionary parameter. Asks the user to select a language from the `options` dictionary parameter.
@ -579,7 +605,7 @@ def select_language(options, show_only_country_codes=True):
:return: The language/dictionary key of the selected language :return: The language/dictionary key of the selected language
:rtype: str :rtype: str
""" """
DEFAULT_KEYBOARD_LANGUAGE = 'us' default_keyboard_language = 'us'
if show_only_country_codes: if show_only_country_codes:
languages = sorted([language for language in list(options) if len(language) == 2]) languages = sorted([language for language in list(options) if len(language) == 2])
@ -596,7 +622,7 @@ def select_language(options, show_only_country_codes=True):
while True: while True:
selected_language = input('Select one of the above keyboard languages (by name or full name): ') selected_language = input('Select one of the above keyboard languages (by name or full name): ')
if not selected_language: if not selected_language:
return DEFAULT_KEYBOARD_LANGUAGE return default_keyboard_language
elif selected_language.lower() in ('?', 'help'): elif selected_language.lower() in ('?', 'help'):
while True: while True:
filter_string = input("Search for layout containing (example: \"sv-\") or enter 'exit' to exit from search: ") filter_string = input("Search for layout containing (example: \"sv-\") or enter 'exit' to exit from search: ")
@ -624,6 +650,7 @@ def select_language(options, show_only_country_codes=True):
raise RequirementError("Selecting languages require a least one language to be given as an option.") raise RequirementError("Selecting languages require a least one language to be given as an option.")
def select_mirror_regions(mirrors, show_top_mirrors=True): def select_mirror_regions(mirrors, show_top_mirrors=True):
""" """
Asks the user to select a mirror or region from the `mirrors` dictionary parameter. Asks the user to select a mirror or region from the `mirrors` dictionary parameter.
@ -665,6 +692,7 @@ def select_mirror_regions(mirrors, show_top_mirrors=True):
raise RequirementError("Selecting mirror region require a least one region to be given as an option.") raise RequirementError("Selecting mirror region require a least one region to be given as an option.")
def select_driver(options=AVAILABLE_GFX_DRIVERS): def select_driver(options=AVAILABLE_GFX_DRIVERS):
""" """
Some what convoluted function, which's job is simple. Some what convoluted function, which's job is simple.
@ -696,8 +724,7 @@ def select_driver(options=AVAILABLE_GFX_DRIVERS):
if type(selected_driver) == dict: if type(selected_driver) == dict:
driver_options = sorted(list(selected_driver)) driver_options = sorted(list(selected_driver))
driver_package_group = generic_select(driver_options, f'Which driver-type do you want for {initial_option}: ', driver_package_group = generic_select(driver_options, f'Which driver-type do you want for {initial_option}: ', allow_empty_input=False)
allow_empty_input=False)
driver_package_group = selected_driver[driver_package_group] driver_package_group = selected_driver[driver_package_group]
return driver_package_group return driver_package_group
@ -706,6 +733,7 @@ def select_driver(options=AVAILABLE_GFX_DRIVERS):
raise RequirementError("Selecting drivers require a least one profile to be given as an option.") raise RequirementError("Selecting drivers require a least one profile to be given as an option.")
def select_kernel(options): def select_kernel(options):
""" """
Asks the user to select a kernel for system. Asks the user to select a kernel for system.
@ -717,11 +745,11 @@ def select_kernel(options):
:rtype: string :rtype: string
""" """
DEFAULT_KERNEL = "linux" default_kernel = "linux"
kernels = sorted(list(options)) kernels = sorted(list(options))
if kernels: if kernels:
return generic_multi_select(kernels, f"Choose which kernels to use (leave blank for default: {DEFAULT_KERNEL}): ", default=DEFAULT_KERNEL, sort=False) return generic_multi_select(kernels, f"Choose which kernels to use (leave blank for default: {default_kernel}): ", default=default_kernel, sort=False)
raise RequirementError("Selecting kernels require a least one kernel to be given as an option.") raise RequirementError("Selecting kernels require a least one kernel to be given as an option.")

View File

@ -10,7 +10,7 @@ if archinstall.arguments.get('help'):
exit(0) exit(0)
# For support reasons, we'll log the disk layout pre installation to match against post-installation layout # For support reasons, we'll log the disk layout pre installation to match against post-installation layout
archinstall.log(f"Disk states before installing: {archinstall.disk_layouts()}", level=archinstall.LOG_LEVELS.Debug) archinstall.log(f"Disk states before installing: {archinstall.disk_layouts()}", level=archinstall.LogLevels.Debug)
def ask_user_questions(): def ask_user_questions():
@ -387,7 +387,7 @@ def perform_installation(mountpoint):
pass pass
# For support reasons, we'll log the disk layout post installation (crash or no crash) # For support reasons, we'll log the disk layout post installation (crash or no crash)
archinstall.log(f"Disk states after installing: {archinstall.disk_layouts()}", level=archinstall.LOG_LEVELS.Debug) archinstall.log(f"Disk states after installing: {archinstall.disk_layouts()}", level=archinstall.LogLevels.Debug)
ask_user_questions() ask_user_questions()
perform_installation_steps() perform_installation_steps()