Merge branch 'master' into torxed-v2.2.0

This commit is contained in:
Anton Hvornum 2021-04-12 00:09:55 +02:00
commit 398f95ee56
29 changed files with 691 additions and 313 deletions

View File

@ -21,6 +21,18 @@ Assuming you are on a Arch Linux live-ISO and booted into EFI mode.
# python -m archinstall guided
# Mission Statement
Archinstall promises to ship a [guided installer](https://github.com/archlinux/archinstall/blob/master/examples/guided.py) that follows the [Arch Principles](https://wiki.archlinux.org/index.php/Arch_Linux#Principles) as well as a library to manage services, packages and other Arch Linux aspects.
The guided installer will provide user friendly options along the way, but the keyword here is options, they are optional and will never be forced upon anyone. The guided installer itself is also optional to use if so desired and not forced upon anyone.
---
Archinstall has one fundamental function which is to be a flexible library to manage services, packages and other aspects inside the installed system. This library is in turn used by the provided guided installer but is also for anyone who wants to script their own installations.
Therefore, Archinstall will try its best to not introduce any breaking changes except for major releases which may break backwards compability after notifying about such changes.
# Scripting your own installation
You could just copy [guided.py](examples/guided.py) as a starting point.
@ -35,23 +47,44 @@ import archinstall, getpass
harddrive = archinstall.select_disk(archinstall.all_disks())
disk_password = getpass.getpass(prompt='Disk password (won\'t echo): ')
with archinstall.Filesystem(harddrive, archinstall.GPT) as fs:
# use_entire_disk() is a helper to not have to format manually
fs.use_entire_disk('luks2')
# We disable safety precautions in the library that protects the partitions
harddrive.keep_partitions = False
harddrive.partition[0].format('fat32')
with archinstall.luks2(harddrive.partition[1], 'luksloop', disk_password) as unlocked_device:
unlocked_device.format('btrfs')
# First, we configure the basic filesystem layout
with archinstall.Filesystem(archinstall.arguments['harddrive'], archinstall.GPT) as fs:
# We create a filesystem layout that will use the entire drive
# (this is a helper function, you can partition manually as well)
fs.use_entire_disk(root_filesystem_type='btrfs')
with archinstall.Installer(unlocked_device, hostname='testmachine') as installation:
if installation.minimal_installation():
installation.add_bootloader(harddrive.partition[0])
boot = fs.find_partition('/boot')
root = fs.find_partition('/')
installation.add_additional_packages(['nano', 'wget', 'git'])
installation.install_profile('awesome')
boot.format('vfat')
installation.user_create('anton', 'test')
installation.user_set_pw('root', 'toor')
# Set the flat for encrypted to allow for encryption and then encrypt
root.encrypted = True
root.encrypt(password=archinstall.arguments.get('!encryption-password', None))
with archinstall.luks2(root, 'luksloop', disk_password) as unlocked_root:
unlocked_root.format(root.filesystem)
unlocked_root.mount('/mnt')
boot.mount('/mnt/boot')
with archinstall.Installer('/mnt') as installation:
if installation.minimal_installation():
installation.set_hostname('minimal-arch')
installation.add_bootloader()
installation.add_additional_packages(['nano', 'wget', 'git'])
# Optionally, install a profile of choice.
# In this case, we install a minimal profile that is empty
installation.install_profile('minimal')
installation.user_create('devel', 'devel')
installation.user_set_pw('root', 'airoot')
```
This installer will perform the following:

View File

@ -127,6 +127,18 @@ class BlockDevice():
def partition_table_type(self):
return GPT
@property
def uuid(self):
log(f'BlockDevice().uuid is untested!', level=LOG_LEVELS.Warning, fg='yellow')
"""
Returns the disk UUID 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.
"""
lsblk = b''.join(sys_command(f'lsblk -J -o+UUID {self.path}'))
for partition in json.loads(lsblk.decode('UTF-8'))['blockdevices']:
return partition.get('uuid', None)
def has_partitions(self):
return len(self.partitions)
@ -167,7 +179,7 @@ class Partition():
self.mountpoint = target
if not self.filesystem and autodetect_filesystem:
if (fstype := mount_information.get('fstype', get_filesystem_type(self.real_device))):
if (fstype := mount_information.get('fstype', get_filesystem_type(path))):
self.filesystem = fstype
if self.filesystem == 'crypto_LUKS':
@ -188,9 +200,9 @@ class Partition():
mount_repr = f", rel_mountpoint={self.target_mountpoint}"
if self._encrypted:
return f'Partition(path={self.path}, real_device={self.real_device}, fs={self.filesystem}{mount_repr})'
return f'Partition(path={self.path}, size={self.size}, real_device={self.real_device}, fs={self.filesystem}{mount_repr})'
else:
return f'Partition(path={self.path}, fs={self.filesystem}{mount_repr})'
return f'Partition(path={self.path}, size={self.size}, fs={self.filesystem}{mount_repr})'
@property
def uuid(self) -> str:
@ -215,14 +227,15 @@ class Partition():
self._encrypted = value
@property
def parent(self):
return self.real_device
@property
def real_device(self):
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}"
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}')
return self.path
@ -367,14 +380,16 @@ class Partition():
if not fs:
if not self.filesystem: raise DiskError(f'Need to format (or define) the filesystem on {self} before mounting.')
fs = self.filesystem
## libc has some issues with loop devices, defaulting back to sys calls
# ret = libc.mount(self.path.encode(), target.encode(), fs.encode(), 0, options.encode())
# if ret < 0:
# errno = ctypes.get_errno()
# raise OSError(errno, f"Error mounting {self.path} ({fs}) on {target} with options '{options}': {os.strerror(errno)}")
if sys_command(f'/usr/bin/mount {self.path} {target}').exit_code == 0:
self.mountpoint = target
return True
pathlib.Path(target).mkdir(parents=True, exist_ok=True)
try:
sys_command(f'/usr/bin/mount {self.path} {target}')
except SysCallError as err:
raise err
self.mountpoint = target
return True
def unmount(self):
try:
@ -589,6 +604,24 @@ def get_mount_info(path):
return output['filesystems'][0]
def get_partitions_in_use(mountpoint):
try:
output = b''.join(sys_command(f'/usr/bin/findmnt --json -R {mountpoint}'))
except SysCallError:
return {}
mounts = []
output = output.decode('UTF-8')
output = json.loads(output)
for target in output.get('filesystems', []):
mounts.append(Partition(target['source'], None, filesystem=target.get('fstype', None), mountpoint=target['target']))
for child in target.get('children', []):
mounts.append(Partition(child['source'], None, filesystem=child.get('fstype', None), mountpoint=child['target']))
return mounts
def get_filesystem_type(path):
try:
handle = sys_command(f"blkid -o value -s TYPE {path}")

View File

@ -18,4 +18,6 @@ class HardwareIncompatibilityError(BaseException):
class PermissionError(BaseException):
pass
class UserError(BaseException):
pass
class ServiceException(BaseException):
pass

View File

@ -39,30 +39,21 @@ class Installer():
:type hostname: str, optional
"""
def __init__(self, partition, boot_partition, *, base_packages=__base_packages__, profile=None, mountpoint='/mnt', hostname='ArchInstalled', logdir=None, logfile=None):
self.profile = profile
self.hostname = hostname
self.mountpoint = mountpoint
def __init__(self, target, *, base_packages='base base-devel linux linux-firmware efibootmgr'):
self.target = target
self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S')
self.milliseconds = int(str(time.time()).split('.')[1])
if logdir:
storage['LOG_PATH'] = logdir
if logfile:
storage['LOG_FILE'] = logfile
self.helper_flags = {
'bootloader' : False,
'base' : False,
'user' : False # Root counts as a user, if additional users are skipped.
'bootloader' : False
}
self.base_packages = base_packages.split(' ') if type(base_packages) is str else base_packages
self.post_base_install = []
storage['session'] = self
self.partition = partition
self.boot_partition = boot_partition
storage['session'] = self
self.partitions = get_partitions_in_use(self.target)
def log(self, *args, level=LOG_LEVELS.Debug, **kwargs):
"""
@ -72,11 +63,6 @@ class Installer():
log(*args, level=level, **kwargs)
def __enter__(self, *args, **kwargs):
if hasUEFI():
# on bios we don't have a boot partition
self.partition.mount(self.mountpoint)
os.makedirs(f'{self.mountpoint}/boot', exist_ok=True)
self.boot_partition.mount(f'{self.mountpoint}/boot')
return self
def __exit__(self, *args, **kwargs):
@ -119,18 +105,18 @@ class Installer():
if (filename := storage.get('LOG_FILE', None)):
absolute_logfile = os.path.join(storage.get('LOG_PATH', './'), filename)
if not os.path.isdir(f"{self.mountpoint}/{os.path.dirname(absolute_logfile)}"):
os.makedirs(f"{self.mountpoint}/{os.path.dirname(absolute_logfile)}")
if not os.path.isdir(f"{self.target}/{os.path.dirname(absolute_logfile)}"):
os.makedirs(f"{self.target}/{os.path.dirname(absolute_logfile)}")
shutil.copy2(absolute_logfile, f"{self.mountpoint}/{absolute_logfile}")
shutil.copy2(absolute_logfile, f"{self.target}/{absolute_logfile}")
return True
def mount(self, partition, mountpoint, create_mountpoint=True):
if create_mountpoint and not os.path.isdir(f'{self.mountpoint}{mountpoint}'):
os.makedirs(f'{self.mountpoint}{mountpoint}')
if create_mountpoint and not os.path.isdir(f'{self.target}{mountpoint}'):
os.makedirs(f'{self.target}{mountpoint}')
partition.mount(f'{self.mountpoint}{mountpoint}')
partition.mount(f'{self.target}{mountpoint}')
def post_install_check(self, *args, **kwargs):
return [step for step, flag in self.helper_flags.items() if flag is False]
@ -140,7 +126,7 @@ class Installer():
self.log(f'Installing packages: {packages}', level=LOG_LEVELS.Info)
if (sync_mirrors := sys_command('/usr/bin/pacman -Syy')).exit_code == 0:
if (pacstrap := sys_command(f'/usr/bin/pacstrap {self.mountpoint} {" ".join(packages)}', peak_output=True, **kwargs)).exit_code == 0:
if (pacstrap := sys_command(f'/usr/bin/pacstrap {self.target} {" ".join(packages)}', **kwargs)).exit_code == 0:
return True
else:
self.log(f'Could not strap in packages: {pacstrap.exit_code}', level=LOG_LEVELS.Info)
@ -148,42 +134,41 @@ class Installer():
self.log(f'Could not sync mirrors: {sync_mirrors.exit_code}', level=LOG_LEVELS.Info)
def set_mirrors(self, mirrors):
return use_mirrors(mirrors, destination=f'{self.mountpoint}/etc/pacman.d/mirrorlist')
return use_mirrors(mirrors, destination=f'{self.target}/etc/pacman.d/mirrorlist')
def genfstab(self, flags='-pU'):
self.log(f"Updating {self.mountpoint}/etc/fstab", level=LOG_LEVELS.Info)
self.log(f"Updating {self.target}/etc/fstab", level=LOG_LEVELS.Info)
fstab = sys_command(f'/usr/bin/genfstab {flags} {self.mountpoint}').trace_log
with open(f"{self.mountpoint}/etc/fstab", 'ab') as fstab_fh:
fstab = sys_command(f'/usr/bin/genfstab {flags} {self.target}').trace_log
with open(f"{self.target}/etc/fstab", 'ab') as fstab_fh:
fstab_fh.write(fstab)
if not os.path.isfile(f'{self.mountpoint}/etc/fstab'):
if not os.path.isfile(f'{self.target}/etc/fstab'):
raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n{fstab}')
return True
def set_hostname(self, hostname=None, *args, **kwargs):
if not hostname: hostname = self.hostname
with open(f'{self.mountpoint}/etc/hostname', 'w') as fh:
fh.write(self.hostname + '\n')
def set_hostname(self, hostname :str, *args, **kwargs):
with open(f'{self.target}/etc/hostname', 'w') as fh:
fh.write(hostname + '\n')
def set_locale(self, locale, encoding='UTF-8', *args, **kwargs):
if not len(locale): return True
with open(f'{self.mountpoint}/etc/locale.gen', 'a') as fh:
with open(f'{self.target}/etc/locale.gen', 'a') as fh:
fh.write(f'{locale}.{encoding} {encoding}\n')
with open(f'{self.mountpoint}/etc/locale.conf', 'w') as fh:
with open(f'{self.target}/etc/locale.conf', 'w') as fh:
fh.write(f'LANG={locale}.{encoding}\n')
return True if sys_command(f'/usr/bin/arch-chroot {self.mountpoint} locale-gen').exit_code == 0 else False
return True if sys_command(f'/usr/bin/arch-chroot {self.target} locale-gen').exit_code == 0 else False
def set_timezone(self, zone, *args, **kwargs):
if not zone: return True
if not len(zone): return True # Redundant
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')
(pathlib.Path(self.target)/"etc"/"localtime").unlink(missing_ok=True)
sys_command(f'/usr/bin/arch-chroot {self.target} ln -s /usr/share/zoneinfo/{zone} /etc/localtime')
return True
else:
self.log(
@ -198,12 +183,14 @@ class Installer():
if self.enable_service('ntpd'):
return True
def enable_service(self, service):
self.log(f'Enabling service {service}', level=LOG_LEVELS.Info)
return self.arch_chroot(f'systemctl enable {service}').exit_code == 0
def enable_service(self, *services):
for service in services:
self.log(f'Enabling service {service}', level=LOG_LEVELS.Info)
if (output := self.arch_chroot(f'systemctl enable {service}')).exit_code != 0:
raise ServiceException(f"Unable to start service {service}: {output}")
def run_command(self, cmd, *args, **kwargs):
return sys_command(f'/usr/bin/arch-chroot {self.mountpoint} {cmd}')
return sys_command(f'/usr/bin/arch-chroot {self.target} {cmd}')
def arch_chroot(self, cmd, *args, **kwargs):
return self.run_command(cmd)
@ -223,15 +210,15 @@ class Installer():
conf = Networkd(Match={"Name": nic}, Network=network)
with open(f"{self.mountpoint}/etc/systemd/network/10-{nic}.network", "a") as netconf:
with open(f"{self.target}/etc/systemd/network/10-{nic}.network", "a") as netconf:
netconf.write(str(conf))
def copy_ISO_network_config(self, enable_services=False):
# Copy (if any) iwd password and config files
if os.path.isdir('/var/lib/iwd/'):
if (psk_files := glob.glob('/var/lib/iwd/*.psk')):
if not os.path.isdir(f"{self.mountpoint}/var/lib/iwd"):
os.makedirs(f"{self.mountpoint}/var/lib/iwd")
if not os.path.isdir(f"{self.target}/var/lib/iwd"):
os.makedirs(f"{self.target}/var/lib/iwd")
if enable_services:
# If we haven't installed the base yet (function called pre-maturely)
@ -251,43 +238,67 @@ class Installer():
self.enable_service('iwd')
for psk in psk_files:
shutil.copy2(psk, f"{self.mountpoint}/var/lib/iwd/{os.path.basename(psk)}")
shutil.copy2(psk, f"{self.target}/var/lib/iwd/{os.path.basename(psk)}")
# Copy (if any) systemd-networkd config files
if (netconfigurations := glob.glob('/etc/systemd/network/*')):
if not os.path.isdir(f"{self.mountpoint}/etc/systemd/network/"):
os.makedirs(f"{self.mountpoint}/etc/systemd/network/")
if not os.path.isdir(f"{self.target}/etc/systemd/network/"):
os.makedirs(f"{self.target}/etc/systemd/network/")
for netconf_file in netconfigurations:
shutil.copy2(netconf_file, f"{self.mountpoint}/etc/systemd/network/{os.path.basename(netconf_file)}")
shutil.copy2(netconf_file, f"{self.target}/etc/systemd/network/{os.path.basename(netconf_file)}")
if enable_services:
# If we haven't installed the base yet (function called pre-maturely)
if self.helper_flags.get('base', False) is False:
def post_install_enable_networkd_resolved(*args, **kwargs):
self.enable_service('systemd-networkd')
self.enable_service('systemd-resolved')
self.enable_service('systemd-networkd', 'systemd-resolved')
self.post_base_install.append(post_install_enable_networkd_resolved)
# Otherwise, we can go ahead and enable the services
else:
self.enable_service('systemd-networkd')
self.enable_service('systemd-resolved')
self.enable_service('systemd-networkd', 'systemd-resolved')
return True
def detect_encryption(self, partition):
if partition.encrypted:
return partition
elif partition.parent not in partition.path and Partition(partition.parent, None, autodetect_filesystem=True).filesystem == 'crypto_LUKS':
return Partition(partition.parent, None, autodetect_filesystem=True)
return False
def minimal_installation(self):
## Add necessary packages if encrypting the drive
## (encrypted partitions default to btrfs for now, so we need btrfs-progs)
## TODO: Perhaps this should be living in the function which dictates
## the partitioning. Leaving here for now.
if self.partition.filesystem == 'btrfs':
#if self.partition.encrypted:
self.base_packages.append('btrfs-progs')
if self.partition.filesystem == 'xfs':
self.base_packages.append('xfsprogs')
if self.partition.filesystem == 'f2fs':
self.base_packages.append('f2fs-tools')
MODULES = []
BINARIES = []
FILES = []
HOOKS = ["base", "udev", "autodetect", "keyboard", "keymap", "modconf", "block", "filesystems", "fsck"]
for partition in self.partitions:
if partition.filesystem == 'btrfs':
#if partition.encrypted:
self.base_packages.append('btrfs-progs')
if partition.filesystem == 'xfs':
self.base_packages.append('xfsprogs')
if partition.filesystem == 'f2fs':
self.base_packages.append('f2fs-tools')
# Configure mkinitcpio to handle some specific use cases.
if partition.filesystem == 'btrfs':
if 'btrfs' not in MODULES:
MODULES.append('btrfs')
if '/usr/bin/btrfs-progs' not in BINARIES:
BINARIES.append('/usr/bin/btrfs')
if self.detect_encryption(partition):
if 'encrypt' not in HOOKS:
HOOKS.insert(HOOKS.index('filesystems'), 'encrypt')
self.pacstrap(self.base_packages)
self.helper_flags['base-strapped'] = True
#self.genfstab()
@ -298,39 +309,28 @@ class Installer():
elif vendor == "GenuineIntel":
self.base_packages.append("intel-ucode")
else:
self.log("unknown cpu vendor not installing ucode")
with open(f"{self.mountpoint}/etc/fstab", "a") as fstab:
self.log("Unknown cpu vendor not installing ucode")
with open(f"{self.target}/etc/fstab", "a") as fstab:
fstab.write(
"\ntmpfs /tmp tmpfs defaults,noatime,mode=1777 0 0\n"
) # Redundant \n at the start? who knows?
## TODO: Support locale and timezone
#os.remove(f'{self.mountpoint}/etc/localtime')
#sys_command(f'/usr/bin/arch-chroot {self.mountpoint} ln -s /usr/share/zoneinfo/{localtime} /etc/localtime')
#os.remove(f'{self.target}/etc/localtime')
#sys_command(f'/usr/bin/arch-chroot {self.target} ln -s /usr/share/zoneinfo/{localtime} /etc/localtime')
#sys_command('/usr/bin/arch-chroot /mnt hwclock --hctosys --localtime')
self.set_hostname()
self.set_hostname('archinstall')
self.set_locale('en_US')
# TODO: Use python functions for this
sys_command(f'/usr/bin/arch-chroot {self.mountpoint} chmod 700 /root')
sys_command(f'/usr/bin/arch-chroot {self.target} 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:
mkinit.write('MODULES=(btrfs)\n')
mkinit.write('BINARIES=(/usr/bin/btrfs)\n')
mkinit.write('FILES=()\n')
mkinit.write('HOOKS=(base udev autodetect keyboard keymap modconf block encrypt filesystems 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 keyboard keymap modconf block encrypt filesystems fsck)\n')
sys_command(f'/usr/bin/arch-chroot {self.mountpoint} mkinitcpio -p linux')
with open(f'{self.target}/etc/mkinitcpio.conf', 'w') as mkinit:
mkinit.write(f"MODULES=({' '.join(MODULES)})\n")
mkinit.write(f"BINARIES=({' '.join(BINARIES)})\n")
mkinit.write(f"FILES=({' '.join(FILES)})\n")
mkinit.write(f"HOOKS=({' '.join(HOOKS)})\n")
sys_command(f'/usr/bin/arch-chroot {self.target} mkinitcpio -p linux')
self.helper_flags['base'] = True
@ -342,7 +342,15 @@ class Installer():
return True
def add_bootloader(self, bootloader='systemd-bootctl'):
self.log(f'Adding bootloader {bootloader} to {self.boot_partition}', level=LOG_LEVELS.Info)
boot_partition = None
root_partition = None
for partition in self.partitions:
if partition.mountpoint == self.target+'/boot':
boot_partition = partition
elif partition.mountpoint == self.target:
root_partition = partition
self.log(f'Adding bootloader {bootloader} to {boot_partition}', level=LOG_LEVELS.Info)
if bootloader == 'systemd-bootctl':
if not hasUEFI():
@ -352,11 +360,11 @@ class Installer():
# 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')
sys_command(f'/usr/bin/arch-chroot {self.target} 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:
if os.path.isfile(f'{self.target}/boot/loader/loader.conf'):
with open(f'{self.target}/boot/loader/loader.conf', 'r') as loader:
loader_data = loader.read().split('\n')
else:
loader_data = [
@ -364,7 +372,7 @@ class Installer():
f"timeout 5"
]
with open(f'{self.mountpoint}/boot/loader/loader.conf', 'w') as loader:
with open(f'{self.target}/boot/loader/loader.conf', 'w') as loader:
for line in loader_data:
if line[:8] == 'default ':
loader.write(f'default {self.init_time}\n')
@ -375,7 +383,7 @@ class Installer():
## 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:
with open(f'{self.target}/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')
@ -393,37 +401,28 @@ class Installer():
## 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
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=LOG_LEVELS.Debug)
entry.write(f'options cryptdevice=PARTUUID={real_device.uuid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp\n')
else:
log(f"Identifying root partition by PART-UUID on {self.partition}, looking for '{os.path.basename(self.partition.path)}'.", level=LOG_LEVELS.Debug)
entry.write(f'options root=PARTUUID={self.partition.uuid} rw intel_pstate=no_hwp\n')
log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=LOG_LEVELS.Debug)
entry.write(f'options root=PARTUUID={root_partition.uuid} rw intel_pstate=no_hwp\n')
self.helper_flags['bootloader'] = bootloader
return True
self.helper_flags['bootloader'] = bootloader
return True
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.")
raise RequirementError(f"Could not identify the UUID of {self.partition}, there for {self.target}/boot/loader/entries/arch.conf will be broken until fixed.")
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'))
o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.target} 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')
else:
root_device = subprocess.check_output(f'basename "$(readlink -f "/sys/class/block/{self.partition.path.strip("/dev/")}/..")',shell=True).decode().strip()
root_device = subprocess.check_output(f'basename "$(readlink -f "/sys/class/block/{root_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}'))
root_device = f"{root_partition.path}"
o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.target} grub-install --target=--target=i386-pc /dev/{root_device}'))
sys_command('/usr/bin/arch-chroot grub-mkconfig -o /boot/grub/grub.cfg')
else:
raise RequirementError(f"Unknown (or not yet implemented) bootloader added to add_bootloader(): {bootloader}")
@ -449,19 +448,19 @@ class Installer():
def enable_sudo(self, entity :str, group=False):
self.log(f'Enabling sudo permissions for {entity}.', level=LOG_LEVELS.Info)
with open(f'{self.mountpoint}/etc/sudoers', 'a') as sudoers:
with open(f'{self.target}/etc/sudoers', 'a') as sudoers:
sudoers.write(f'{"%" if group else ""}{entity} ALL=(ALL) ALL\n')
return True
def user_create(self, user :str, password=None, groups=[], sudo=False):
self.log(f'Creating user {user}', level=LOG_LEVELS.Info)
o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} useradd -m -G wheel {user}'))
o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.target} useradd -m -G wheel {user}'))
if password:
self.user_set_pw(user, password)
if groups:
for group in groups:
o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} gpasswd -a {user} {group}'))
o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.target} gpasswd -a {user} {group}'))
if sudo and self.enable_sudo(user):
self.helper_flags['user'] = True
@ -473,12 +472,12 @@ class Installer():
# This means the root account isn't locked/disabled with * in /etc/passwd
self.helper_flags['user'] = True
o = b''.join(sys_command(f"/usr/bin/arch-chroot {self.mountpoint} sh -c \"echo '{user}:{password}' | chpasswd\""))
o = b''.join(sys_command(f"/usr/bin/arch-chroot {self.target} sh -c \"echo '{user}:{password}' | chpasswd\""))
pass
def set_keyboard_language(self, language):
if len(language.strip()):
with open(f'{self.mountpoint}/etc/vconsole.conf', 'w') as vconsole:
with open(f'{self.target}/etc/vconsole.conf', 'w') as vconsole:
vconsole.write(f'KEYMAP={language}\n')
vconsole.write(f'FONT=lat9w-16\n')
return True

View File

@ -1,5 +1,7 @@
import os
import shlex
import time
import pathlib
from .exceptions import *
from .general import *
from .disk import Partition
@ -114,7 +116,7 @@ class luks2():
def unlock(self, partition, mountpoint, key_file):
"""
Mounts a lukts2 compatible partition to a certain mountpoint.
Mounts a luks2 compatible partition to a certain mountpoint.
Keyfile must be specified as there's no way to interact with the pw-prompt atm.
:param mountpoint: The name without absolute path, for instance "luksdev" will point to /dev/mapper/luksdev
@ -123,6 +125,11 @@ class luks2():
from .disk import get_filesystem_type
if '/' in mountpoint:
os.path.basename(mountpoint) # TODO: Raise exception instead?
wait_timer = time.time()
while pathlib.Path(partition.path).exists() is False and time.time() - wait_timer < 10:
time.sleep(0.025)
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}'

View File

@ -177,6 +177,52 @@ class Profile(Script):
if hasattr(imported, '_prep_function'):
return True
return False
"""
def has_post_install(self):
with open(self.path, 'r') as source:
source_data = source.read()
# Some crude safety checks, make sure the imported profile has
# a __name__ check and if so, check if it's got a _prep_function()
# we can call to ask for more user input.
#
# If the requirements are met, import with .py in the namespace to not
# trigger a traditional:
# if __name__ == 'moduleName'
if '__name__' in source_data and '_post_install' in source_data:
with self.load_instructions(namespace=f"{self.namespace}.py") as imported:
if hasattr(imported, '_post_install'):
return True
"""
def is_top_level_profile(self):
with open(self.path, 'r') as source:
source_data = source.read()
# TODO: I imagine that there is probably a better way to write this.
return 'top_level_profile = True' in source_data
@property
def packages(self) -> list:
"""
Returns a list of packages baked into the profile definition.
If no package definition has been done, .packages() will return None.
"""
with open(self.path, 'r') as source:
source_data = source.read()
# Some crude safety checks, make sure the imported profile has
# a __name__ check before importing.
#
# If the requirements are met, import with .py in the namespace to not
# trigger a traditional:
# if __name__ == 'moduleName'
if '__name__' in source_data and '__packages__' in source_data:
with self.load_instructions(namespace=f"{self.namespace}.py") as imported:
if hasattr(imported, '__packages__'):
return imported.__packages__
return None
def has_post_install(self):
with open(self.path, 'r') as source:

View File

@ -1,4 +1,5 @@
import getpass, pathlib, os, shutil, re
import sys, time, signal
from .exceptions import *
from .profiles import Profile
from .locale_helpers import search_keyboard_layout
@ -21,14 +22,49 @@ def get_longest_option(options):
return max([len(x) for x in options])
def check_for_correct_username(username):
if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32:
return True
log(
if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32:
return True
log(
"The username you entered is invalid. Try again",
level=LOG_LEVELS.Warning,
fg='red'
)
return False
return False
def do_countdown():
SIG_TRIGGER = False
def kill_handler(sig, frame):
print()
exit(0)
def sig_handler(sig, frame):
global SIG_TRIGGER
SIG_TRIGGER = True
signal.signal(signal.SIGINT, kill_handler)
original_sigint_handler = signal.getsignal(signal.SIGINT)
signal.signal(signal.SIGINT, sig_handler)
for i in range(5, 0, -1):
print(f"{i}", end='')
for x in range(4):
sys.stdout.flush()
time.sleep(0.25)
print(".", end='')
if SIG_TRIGGER:
abort = input('\nDo you really want to abort (y/n)? ')
if abort.strip() != 'n':
exit(0)
if SIG_TRIGGER is False:
sys.stdin.read()
SIG_TRIGGER = False
signal.signal(signal.SIGINT, sig_handler)
print()
signal.signal(signal.SIGINT, original_sigint_handler)
return True
def get_password(prompt="Enter a password: "):
while (passwd := getpass.getpass(prompt)):
@ -196,7 +232,7 @@ def generic_select(options, input_text="Select one of the above by index or abso
return None
elif selected_option.isdigit():
selected_option = int(selected_option)
if selected_option >= len(options):
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:
@ -221,8 +257,10 @@ def select_disk(dict_o_disks):
if len(drives) >= 1:
for index, drive in enumerate(drives):
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 = input('Select one of the above disks (by number or full path) or write /mnt to skip partitioning: ')
if drive.strip() == '/mnt':
return None
elif drive.isdigit():
drive = int(drive)
if drive >= len(drives):
raise DiskError(f'Selected option "{drive}" is out of range')

View File

@ -24,6 +24,9 @@ Locale related
.. autofunction:: archinstall.set_keyboard_language
..
autofunction:: archinstall.Installer.set_keyboard_layout
Services
========
@ -88,3 +91,13 @@ Exceptions
.. autofunction:: archinstall.ProfileError
.. autofunction:: archinstall.SysCallError
.. autofunction:: archinstall.ProfileNotFound
.. autofunction:: archinstall.HardwareIncompatibilityError
.. autofunction:: archinstall.PermissionError
.. autofunction:: archinstall.UserError
.. autofunction:: archinstall.ServiceException

View File

@ -2,7 +2,7 @@ python-archinstall Documentation
================================
| **python-archinstall** *(or, archinstall for short)* is a helper library to install Arch Linux and manage services, packages and other things.
| It comes packaged with different pre-configured installers, such as the :ref:`guided <installing.guided>` installer.
| It comes packaged with different pre-configured installers, such as the `Guided installation`_ installer.
|
| A demo can be viewed here: `https://www.youtube.com/watch?v=9Xt7X_Iqg6E <https://www.youtube.com/watch?v=9Xt7X_Iqg6E>`_ which uses the default guided installer.

View File

@ -1,4 +1,4 @@
import getpass, time, json, sys, signal, os
import getpass, time, json, os
import archinstall
from archinstall.lib.hardware import hasUEFI
from archinstall.lib.profiles import Profile
@ -52,12 +52,14 @@ def ask_user_questions():
archinstall.arguments['harddrive'] = archinstall.BlockDevice(archinstall.arguments['harddrive'])
else:
archinstall.arguments['harddrive'] = archinstall.select_disk(archinstall.all_disks())
if archinstall.arguments['harddrive'] is None:
archinstall.arguments['target-mount'] = '/mnt'
# Perform a quick sanity check on the selected harddrive.
# 1. Check if it has partitions
# 3. Check that we support the current partitions
# 2. If so, ask if we should keep them or wipe everything
if archinstall.arguments['harddrive'].has_partitions():
if archinstall.arguments['harddrive'] and archinstall.arguments['harddrive'].has_partitions():
archinstall.log(f"{archinstall.arguments['harddrive']} contains the following partitions:", fg='yellow')
# We curate a list pf supported partitions
@ -136,14 +138,14 @@ def ask_user_questions():
elif option == 'format-all':
archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format()
archinstall.arguments['harddrive'].keep_partitions = False
else:
elif archinstall.arguments['harddrive']:
# If the drive doesn't have any partitions, safely mark the disk with keep_partitions = False
# and ask the user for a root filesystem.
archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format()
archinstall.arguments['harddrive'].keep_partitions = False
# Get disk encryption password (or skip if blank)
if not archinstall.arguments.get('!encryption-password', None):
if archinstall.arguments['harddrive'] and archinstall.arguments.get('!encryption-password', None) is None:
if (passwd := archinstall.get_password(prompt='Enter disk encryption password (leave blank for no encryption): ')):
archinstall.arguments['!encryption-password'] = passwd
archinstall.arguments['harddrive'].encryption_password = archinstall.arguments['!encryption-password']
@ -199,12 +201,14 @@ def ask_user_questions():
print("If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.")
archinstall.arguments['packages'] = [package for package in input('Write additional packages to install (space separated, leave blank to skip): ').split(' ') if len(package)]
# Verify packages that were given
try:
archinstall.validate_package_list(archinstall.arguments['packages'])
except archinstall.RequirementError as e:
archinstall.log(e, fg='red')
exit(1)
if len(archinstall.arguments['packages']):
# Verify packages that were given
try:
archinstall.log(f"Verifying that additional packages exist (this might take a few seconds)")
archinstall.validate_package_list(archinstall.arguments['packages'])
except archinstall.RequirementError as e:
archinstall.log(e, fg='red')
exit(1)
# Ask or Call the helper function that asks the user to optionally configure a network.
if not archinstall.arguments.get('nic', None):
@ -217,8 +221,6 @@ def ask_user_questions():
def perform_installation_steps():
global SIG_TRIGGER
print()
print('This is your chosen configuration:')
archinstall.log("-- Guided template chosen (with below config) --", level=archinstall.LOG_LEVELS.Debug)
@ -232,89 +234,67 @@ def perform_installation_steps():
We mention the drive one last time, and count from 5 to 0.
"""
print(f" ! Formatting {archinstall.arguments['harddrive']} in ", end='')
if archinstall.arguments.get('harddrive', None):
print(f" ! Formatting {archinstall.arguments['harddrive']} in ", end='')
archinstall.do_countdown()
for i in range(5, 0, -1):
print(f"{i}", end='')
for x in range(4):
sys.stdout.flush()
time.sleep(0.25)
print(".", end='')
if SIG_TRIGGER:
abort = input('\nDo you really want to abort (y/n)? ')
if abort.strip() != 'n':
exit(0)
if SIG_TRIGGER is False:
sys.stdin.read()
SIG_TRIGGER = False
signal.signal(signal.SIGINT, sig_handler)
# Put back the default/original signal handler now that we're done catching
# and interrupting SIGINT with "Do you really want to abort".
print()
signal.signal(signal.SIGINT, original_sigint_handler)
"""
Setup the blockdevice, filesystem (and optionally encryption).
Once that's done, we'll hand over to perform_installation()
"""
# maybe we can ask the user what they would prefer on uefi systems?
if hasUEFI():
"""
Setup the blockdevice, filesystem (and optionally encryption).
Once that's done, we'll hand over to perform_installation()
"""
mode = archinstall.GPT
else:
mode = archinstall.MBR
with archinstall.Filesystem(archinstall.arguments['harddrive'],mode) as fs:
# Wipe the entire drive if the disk flag `keep_partitions`is False.
if archinstall.arguments['harddrive'].keep_partitions is False:
fs.use_entire_disk(root_filesystem_type=archinstall.arguments.get('filesystem', 'btrfs'))
# Check if encryption is desired and mark the root partition as encrypted.
if archinstall.arguments.get('!encryption-password', None):
root_partition = fs.find_partition('/')
root_partition.encrypted = True
# After the disk is ready, iterate the partitions and check
# which ones are safe to format, and format those.
for partition in archinstall.arguments['harddrive']:
if partition.safe_to_format():
# Partition might be marked as encrypted due to the filesystem type crypt_LUKS
# But we might have omitted the encryption password question to skip encryption.
# In which case partition.encrypted will be true, but passwd will be false.
if partition.encrypted and (passwd := archinstall.arguments.get('!encryption-password', None)):
partition.encrypt(password=passwd)
if hasUEFI() is False:
mode = archinstall.MBR
with archinstall.Filesystem(archinstall.arguments['harddrive'], mode) as fs:
# Wipe the entire drive if the disk flag `keep_partitions`is False.
if archinstall.arguments['harddrive'].keep_partitions is False:
fs.use_entire_disk(root_filesystem_type=archinstall.arguments.get('filesystem', 'btrfs'))
# Check if encryption is desired and mark the root partition as encrypted.
if archinstall.arguments.get('!encryption-password', None):
root_partition = fs.find_partition('/')
root_partition.encrypted = True
# After the disk is ready, iterate the partitions and check
# which ones are safe to format, and format those.
for partition in archinstall.arguments['harddrive']:
if partition.safe_to_format():
# Partition might be marked as encrypted due to the filesystem type crypt_LUKS
# But we might have omitted the encryption password question to skip encryption.
# In which case partition.encrypted will be true, but passwd will be false.
if partition.encrypted and (passwd := archinstall.arguments.get('!encryption-password', None)):
partition.encrypt(password=passwd)
else:
partition.format()
else:
partition.format()
archinstall.log(f"Did not format {partition} because .safe_to_format() returned False or .allow_formatting was False.", level=archinstall.LOG_LEVELS.Debug)
fs.find_partition('/boot').format('vfat')
if archinstall.arguments.get('!encryption-password', None):
# First encrypt and unlock, then format the desired partition inside the encrypted part.
# archinstall.luks2() encrypts the partition when entering the with context manager, and
# unlocks the drive so that it can be used as a normal block-device within archinstall.
with archinstall.luks2(fs.find_partition('/'), 'luksloop', archinstall.arguments.get('!encryption-password', None)) as unlocked_device:
unlocked_device.format(fs.find_partition('/').filesystem)
unlocked_device.mount('/mnt')
else:
archinstall.log(f"Did not format {partition} because .safe_to_format() returned False or .allow_formatting was False.", level=archinstall.LOG_LEVELS.Debug)
fs.find_partition('/').format(fs.find_partition('/').filesystem)
fs.find_partition('/').mount('/mnt')
if archinstall.arguments.get('!encryption-password', None):
# First encrypt and unlock, then format the desired partition inside the encrypted part.
# archinstall.luks2() encrypts the partition when entering the with context manager, and
# unlocks the drive so that it can be used as a normal block-device within archinstall.
with archinstall.luks2(fs.find_partition('/'), 'luksloop', archinstall.arguments.get('!encryption-password', None)) as unlocked_device:
unlocked_device.format(fs.find_partition('/').filesystem)
perform_installation(device=unlocked_device,
boot_partition=fs.find_partition('/boot'),
language=archinstall.arguments['keyboard-language'],
mirrors=archinstall.arguments['mirror-region'])
else:
perform_installation(device=fs.find_partition('/'),
boot_partition=fs.find_partition('/boot'),
language=archinstall.arguments['keyboard-language'],
mirrors=archinstall.arguments['mirror-region'])
fs.find_partition('/boot').mount('/mnt/boot')
perform_installation('/mnt')
def perform_installation(device, boot_partition, language, mirrors):
def perform_installation(mountpoint):
"""
Performs the installation steps on a block device.
Only requirement is that the block devices are
formatted and setup prior to entering this function.
"""
with archinstall.Installer(device, boot_partition=boot_partition, hostname=archinstall.arguments.get('hostname', 'Archinstall')) as installation:
with archinstall.Installer(mountpoint) as installation:
## if len(mirrors):
# Certain services might be running that affects the system during installation.
# Currently, only one such service is "reflector.service" which updates /etc/pacman.d/mirrorlist
@ -323,10 +303,11 @@ def perform_installation(device, boot_partition, language, mirrors):
while 'dead' not in (status := archinstall.service_state('reflector')):
time.sleep(1)
archinstall.use_mirrors(mirrors) # Set the mirrors for the live medium
archinstall.use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium
if installation.minimal_installation():
installation.set_mirrors(mirrors) # Set the mirrors in the installation medium
installation.set_keyboard_language(language)
installation.set_hostname(archinstall.arguments['hostname'])
installation.set_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium
installation.set_keyboard_language(archinstall.arguments['keyboard-language'])
installation.add_bootloader()
# If user selected to copy the current ISO network configuration
@ -346,7 +327,8 @@ def perform_installation(device, boot_partition, language, mirrors):
installation.log(f"The {archinstall.arguments.get('audio', None)} audio server will be used.", level=archinstall.LOG_LEVELS.Info)
if archinstall.arguments.get('audio', None) == 'pipewire':
print('Installing pipewire ...')
installation.add_additional_packages(["pipewire", "pipewire-alsa", "pipewire-docs", "pipewire-jack", "pipewire-media-session", "pipewire-pulse", "gst-plugin-pipewire", "libpulse"])
installation.add_additional_packages(["pipewire", "pipewire-alsa", "pipewire-jack", "pipewire-media-session", "pipewire-pulse", "gst-plugin-pipewire", "libpulse"])
elif archinstall.arguments.get('audio', None) == 'pulseaudio':
print('Installing pulseaudio ...')
installation.add_additional_packages("pulseaudio")

View File

@ -1,30 +1,71 @@
import archinstall, getpass
# Unmount and close previous runs
archinstall.sys_command(f'umount -R /mnt', suppress_errors=True)
archinstall.sys_command(f'cryptsetup close /dev/mapper/luksloop', suppress_errors=True)
import archinstall
# Select a harddrive and a disk password
harddrive = archinstall.select_disk(archinstall.all_disks())
disk_password = getpass.getpass(prompt='Disk password (won\'t echo): ')
archinstall.log(f"Minimal only supports:")
archinstall.log(f" * Being installed to a single disk")
with archinstall.Filesystem(harddrive) as fs:
# Use the entire disk instead of setting up partitions on your own
fs.use_entire_disk('luks2')
if archinstall.arguments.get('help', None):
archinstall.log(f" - Optional disk encryption via --!encryption-password=<password>")
archinstall.log(f" - Optional filesystem type via --filesystem=<fs type>")
archinstall.log(f" - Optional systemd network via --network")
if harddrive.partition[1].size == '512M':
raise OSError('Trying to encrypt the boot partition for petes sake..')
harddrive.partition[0].format('fat32')
archinstall.arguments['harddrive'] = archinstall.select_disk(archinstall.all_disks())
with archinstall.luks2(harddrive.partition[1], 'luksloop', disk_password) as unlocked_device:
unlocked_device.format('btrfs')
with archinstall.Installer(unlocked_device, boot_partition=harddrive.partition[0], hostname='testmachine') as installation:
if installation.minimal_installation():
installation.add_bootloader()
def install_on(mountpoint):
# We kick off the installer by telling it where the
with archinstall.Installer(mountpoint) as installation:
# Strap in the base system, add a boot loader and configure
# some other minor details as specified by this profile and user.
if installation.minimal_installation():
installation.set_hostname('minimal-arch')
installation.add_bootloader()
installation.add_additional_packages(['nano', 'wget', 'git'])
installation.install_profile('minimal')
# Optionally enable networking:
if archinstall.arguments.get('network', None):
installation.copy_ISO_network_config(enable_services=True)
installation.user_create('devel', 'devel')
installation.user_set_pw('root', 'toor')
installation.add_additional_packages(['nano', 'wget', 'git'])
installation.install_profile('minimal')
installation.user_create('devel', 'devel')
installation.user_set_pw('root', 'airoot')
# Once this is done, we output some useful information to the user
# And the installation is complete.
archinstall.log(f"There are two new accounts in your installation after reboot:")
archinstall.log(f" * root (password: airoot)")
archinstall.log(f" * devel (password: devel)")
if archinstall.arguments['harddrive']:
archinstall.arguments['harddrive'].keep_partitions = False
print(f" ! Formatting {archinstall.arguments['harddrive']} in ", end='')
archinstall.do_countdown()
# First, we configure the basic filesystem layout
with archinstall.Filesystem(archinstall.arguments['harddrive'], archinstall.GPT) as fs:
# We use the entire disk instead of setting up partitions on your own
if archinstall.arguments['harddrive'].keep_partitions is False:
fs.use_entire_disk(root_filesystem_type=archinstall.arguments.get('filesystem', 'btrfs'))
boot = fs.find_partition('/boot')
root = fs.find_partition('/')
boot.format('vfat')
# We encrypt the root partition if we got a password to do so with,
# Otherwise we just skip straight to formatting and installation
if archinstall.arguments.get('!encryption-password', None):
root.encrypted = True
root.encrypt(password=archinstall.arguments.get('!encryption-password', None))
with archinstall.luks2(root, 'luksloop', archinstall.arguments.get('!encryption-password', None)) as unlocked_root:
unlocked_root.format(root.filesystem)
unlocked_root.mount('/mnt')
else:
root.format(root.filesystem)
root.mount('/mnt')
boot.mount('/mnt/boot')
install_on('/mnt')

View File

@ -0,0 +1,4 @@
import archinstall
# "It is recommended also to install the gnome group, which contains applications required for the standard GNOME experience." - Arch Wiki
installation.add_additional_packages("budgie-desktop lightdm lightdm-gtk-greeter gnome")

View File

@ -1,4 +1,3 @@
import archinstall
installation.add_additional_packages("cinnamon system-config-printer gnome-keyring gnome-terminal blueberry metacity lightdm lightdm-gtk-greeter lightdm-gtk-greeter-settings")
# We'll create a cinnamon-minimal later, but for now, we'll avoid issues by giving more than we need.
installation.add_additional_packages("cinnamon system-config-printer gnome-keyring gnome-terminal blueberry metacity lightdm lightdm-gtk-greeter")

View File

@ -1,2 +1,2 @@
import archinstall
installation.add_additional_packages("i3lock i3status i3blocks i3-gaps")
installation.add_additional_packages("i3-gaps")

View File

@ -1,2 +1,2 @@
import archinstall
installation.add_additional_packages("i3lock i3status i3blocks i3-wm")
installation.add_additional_packages("i3-wm")

View File

@ -0,0 +1,3 @@
import archinstall
installation.add_additional_packages("lxqt breeze-icons oxygen-icons xdg-utils ttf-freefont leafpad slock archlinux-wallpaper sddm")

View File

@ -0,0 +1,3 @@
import archinstall
installation.add_additional_packages("mate mate-extra lightdm lightdm-gtk-greeter")

View File

@ -1,3 +1,3 @@
import archinstall
packages = "sway swaylock swayidle dmenu alacritty"
installation.add_additional_packages(packages)
__packages__ = "sway swaylock swayidle waybar dmenu light grim slurp pavucontrol alacritty"
installation.add_additional_packages(__packages__)

View File

@ -1,4 +1,3 @@
import archinstall
installation.add_additional_packages("xfce4 xfce4-goodies lightdm lightdm-gtk-greeter lightdm-gtk-greeter-settings")
# We'll create a xfce4-minimal later, but for now, we'll avoid issues by giving more than we need.
__packages__ = "xfce4 xfce4-goodies lightdm lightdm-gtk-greeter"
installation.add_additional_packages(__packages__)

View File

@ -6,7 +6,7 @@ is_top_level_profile = False
# New way of defining packages for a profile, which is iterable and can be used out side
# of the profile to get a list of "what packages will be installed".
__packages__ = ['nano', 'nemo', 'gpicview-gtk3', 'openssh', 'sshfs', 'htop', 'scrot', 'wget']
__packages__ = ['nemo', 'gpicview-gtk3', 'scrot']
def _prep_function(*args, **kwargs):
"""

56
profiles/budgie.py Normal file
View File

@ -0,0 +1,56 @@
<<<<<<< HEAD:profiles/cinnamon.py
# A desktop environment using "Cinnamon"
import archinstall
=======
# A desktop environment using "budgie"
import archinstall
>>>>>>> master:profiles/budgie.py
is_top_level_profile = False
def _prep_function(*args, **kwargs):
"""
Magic function called by the importing installer
before continuing any further. It also avoids executing any
other code in this stage. So it's a safe way to ask the user
for more input before any other installer steps start.
"""
<<<<<<< HEAD:profiles/cinnamon.py
# Cinnamon requires a functioning Xorg installation.
=======
# budgie requires a functioning Xorg installation.
>>>>>>> master:profiles/budgie.py
profile = archinstall.Profile(None, 'xorg')
with profile.load_instructions(namespace='xorg.py') as imported:
if hasattr(imported, '_prep_function'):
return imported._prep_function()
else:
print('Deprecated (??): xorg profile has no _prep_function() anymore')
# Ensures that this code only gets executed if executed
<<<<<<< HEAD:profiles/cinnamon.py
# through importlib.util.spec_from_file_location("cinnamon", "/somewhere/cinnamon.py")
# or through conventional import cinnamon
if __name__ == 'cinnamon':
# Install dependency profiles
installation.install_profile('xorg')
# Install the application cinnamon from the template under /applications/
cinnamon = archinstall.Application(installation, 'cinnamon')
cinnamon.install()
=======
# through importlib.util.spec_from_file_location("budgie", "/somewhere/budgie.py")
# or through conventional import budgie
if __name__ == 'budgie':
# Install dependency profiles
installation.install_profile('xorg')
# Install the application budgie from the template under /applications/
budgie = archinstall.Application(installation, 'budgie')
budgie.install()
>>>>>>> master:profiles/budgie.py
installation.enable_service('lightdm') # Light Display Manager

View File

@ -1,4 +1,5 @@
# A desktop environment using "Cinnamon"
import archinstall
is_top_level_profile = False

View File

@ -4,6 +4,10 @@ import archinstall, os
is_top_level_profile = True
# New way of defining packages for a profile, which is iterable and can be used out side
# of the profile to get a list of "what packages will be installed".
__packages__ = ['nano', 'vim', 'openssh', 'htop', 'wget', 'iwd', 'wireless_tools', 'wpa_supplicant', 'smartmontools', 'xdg-utils']
def _prep_function(*args, **kwargs):
"""
Magic function called by the importing installer
@ -12,9 +16,9 @@ def _prep_function(*args, **kwargs):
for more input before any other installer steps start.
"""
supported_desktops = ['gnome', 'kde', 'awesome', 'sway', 'cinnamon', 'xfce4', 'lxqt', 'i3', 'budgie']
supported_desktops = ['gnome', 'kde', 'awesome', 'sway', 'cinnamon', 'xfce4', 'lxqt', 'i3', 'budgie', 'mate']
desktop = archinstall.generic_select(supported_desktops, 'Select your desired desktop environment: ')
# Temporarily store the selected desktop profile
# in a session-safe location, since this module will get reloaded
# the next time it gets executed.
@ -41,7 +45,11 @@ if __name__ == 'desktop':
There are plenty of desktop-turn-key-solutions based on Arch Linux,
this is therefore just a helper to get started
"""
# Install common packages for all desktop environments
installation.add_additional_packages(__packages__)
# TODO: Remove magic variable 'installation' and place it
# in archinstall.storage or archinstall.session/archinstall.installation
installation.install_profile(archinstall.storage['_desktop_profile'])

63
profiles/i3.py Normal file
View File

@ -0,0 +1,63 @@
# Common package for i3, lets user select which i3 configuration they want.
import archinstall, os
is_top_level_profile = False
# New way of defining packages for a profile, which is iterable and can be used out side
# of the profile to get a list of "what packages will be installed".
__packages__ = ['i3lock', 'i3status', 'i3blocks', 'xterm']
def _prep_function(*args, **kwargs):
"""
Magic function called by the importing installer
before continuing any further. It also avoids executing any
other code in this stage. So it's a safe way to ask the user
for more input before any other installer steps start.
"""
supported_configurations = ['i3-wm', 'i3-gaps']
desktop = archinstall.generic_select(supported_configurations, 'Select your desired configuration: ')
# Temporarily store the selected desktop profile
# in a session-safe location, since this module will get reloaded
# the next time it gets executed.
archinstall.storage['_i3_configuration'] = desktop
# i3 requires a functioning Xorg installation.
profile = archinstall.Profile(None, 'xorg')
with profile.load_instructions(namespace='xorg.py') as imported:
if hasattr(imported, '_prep_function'):
return imported._prep_function()
else:
print('Deprecated (??): xorg profile has no _prep_function() anymore')
if __name__ == 'i3':
"""
This "profile" is a meta-profile.
There are no desktop-specific steps, it simply routes
the installer to whichever desktop environment/window manager was chosen.
Maybe in the future, a network manager or similar things *could* be added here.
We should honor that Arch Linux does not officially endorse a desktop-setup, nor is
it trying to be a turn-key desktop distribution.
There are plenty of desktop-turn-key-solutions based on Arch Linux,
this is therefore just a helper to get started
"""
# Install common packages for all i3 configurations
installation.add_additional_packages(__packages__)
# Install dependency profiles
installation.install_profile('xorg')
# gaps is installed by deafult so we are overriding it here
installation.add_additional_packages("lightdm-gtk-greeter lightdm")
# Auto start lightdm for all users
installation.enable_service('lightdm')
# install the i3 group now
i3 = archinstall.Application(installation, archinstall.storage['_i3_configuration'])
i3.install()

View File

@ -22,11 +22,13 @@ def _prep_function(*args, **kwargs):
else:
print('Deprecated (??): xorg profile has no _prep_function() anymore')
"""
def _post_install(*args, **kwargs):
if "nvidia" in _gfx_driver_packages:
print("Plasma Wayland has known compatibility issues with the proprietary Nvidia driver")
print("After booting, you can choose between Wayland and Xorg using the drop-down menu")
return True
"""
# Ensures that this code only gets executed if executed
# through importlib.util.spec_from_file_location("kde", "/somewhere/kde.py")

35
profiles/lxqt.py Normal file
View File

@ -0,0 +1,35 @@
# A desktop environment using "LXQt"
import archinstall
is_top_level_profile = False
def _prep_function(*args, **kwargs):
"""
Magic function called by the importing installer
before continuing any further. It also avoids executing any
other code in this stage. So it's a safe way to ask the user
for more input before any other installer steps start.
"""
# LXQt requires a functional xorg installation.
profile = archinstall.Profile(None, 'xorg')
with profile.load_instructions(namespace='xorg.py') as imported:
if hasattr(imported, '_prep_function'):
return imported._prep_function()
else:
print('Deprecated (??): xorg profile has no _prep_function() anymore')
# Ensures that this code only gets executed if executed
# through importlib.util.spec_from_file_location("lxqt", "/somewhere/lxqt.py")
# or through conventional import lxqt
if __name__ == 'lxqt':
# Install dependency profiles
installation.install_profile('xorg')
# Install the application xfce4 from the template under /applications/
xfce = archinstall.Application(installation, 'lxqt')
xfce.install()
installation.enable_service('sddm') # SDDM Display Manager

34
profiles/mate.py Normal file
View File

@ -0,0 +1,34 @@
# A desktop environment using "MATE"
import archinstall
is_top_level_profile = False
def _prep_function(*args, **kwargs):
"""
Magic function called by the importing installer
before continuing any further. It also avoids executing any
other code in this stage. So it's a safe way to ask the user
for more input before any other installer steps start.
"""
# MATE requires a functional xorg installation.
profile = archinstall.Profile(None, 'xorg')
with profile.load_instructions(namespace='xorg.py') as imported:
if hasattr(imported, '_prep_function'):
return imported._prep_function()
else:
print('Deprecated (??): xorg profile has no _prep_function() anymore')
# Ensures that this code only gets executed if executed
# through importlib.util.spec_from_file_location("mate", "/somewhere/mate.py")
# or through conventional import mate
if __name__ == 'mate':
# Install dependency profiles
installation.install_profile('xorg')
# Install the application mate from the template under /applications/
mate = archinstall.Application(installation, 'mate')
mate.install()
installation.enable_service('lightdm') # Light Display Manager

View File

@ -1,6 +1,8 @@
import archinstall, os
# A desktop environment using "Sway"
# TODO: Remove hard dependency of bash (due to .bash_profile)
import archinstall
is_top_level_profile = False
def _prep_function(*args, **kwargs):
"""
@ -9,37 +11,12 @@ def _prep_function(*args, **kwargs):
other code in this stage. So it's a safe way to ask the user
for more input before any other installer steps start.
"""
__builtins__['_gfx_driver_packages'] = archinstall.select_driver()
return True
def _post_install(*args, **kwargs):
choice = input("Would you like to autostart sway on login [Y/n]: ")
if choice.lower == "y":
with open(f"{installation.mountpoint}/etc/profile", "a") as f:
x = """
if [ -z $DISPLAY ] && [ "$(tty)" == "/dev/tty1" ]; then
exec sway
fi
"""
f.write(x)
f.close()
else:
installation.log("To start Sway, run the 'sway' command after logging in.")
# Ensures that this code only gets executed if executed
# through importlib.util.spec_from_file_location("kde", "/somewhere/kde.py")
# or through conventional import kde
# through importlib.util.spec_from_file_location("sway", "/somewhere/sway.py")
# or through conventional import sway
if __name__ == 'sway':
installation.add_additional_packages(_gfx_driver_packages)
# Install dependency profiles
if _gfx_driver_packages == 'nvidia':
# NOTE: This is technically runnable with the --my-next-gpu-wont-be-nvidia option
raise archinstall.lib.exceptions.HardwareIncompatibilityError("Sway does not officially support the proprietary Nvidia driver, you may have to use nouveau.")
# Install the application kde from the template under /applications/
# Install the application sway from the template under /applications/
sway = archinstall.Application(installation, 'sway')
sway.install()

View File

@ -1,10 +1,10 @@
# A desktop environment using "Xfce4"
is_top_level_profile = False
import archinstall
is_top_level_profile = False
def _prep_function(*args, **kwargs):
"""
Magic function called by the importing installer