PR #271: (breaking changes) This fixes #124 and some more

(breaking changes) This fixes #124 and some more
This commit is contained in:
Anton Hvornum 2021-04-10 09:56:54 +00:00 committed by GitHub
commit afda647623
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 291 additions and 211 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 # 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 # Scripting your own installation
You could just copy [guided.py](examples/guided.py) as a starting point. 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()) harddrive = archinstall.select_disk(archinstall.all_disks())
disk_password = getpass.getpass(prompt='Disk password (won\'t echo): ') disk_password = getpass.getpass(prompt='Disk password (won\'t echo): ')
with archinstall.Filesystem(harddrive, archinstall.GPT) as fs: # We disable safety precautions in the library that protects the partitions
# use_entire_disk() is a helper to not have to format manually harddrive.keep_partitions = False
fs.use_entire_disk('luks2')
harddrive.partition[0].format('fat32') # First, we configure the basic filesystem layout
with archinstall.luks2(harddrive.partition[1], 'luksloop', disk_password) as unlocked_device: with archinstall.Filesystem(archinstall.arguments['harddrive'], archinstall.GPT) as fs:
unlocked_device.format('btrfs') # 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: boot = fs.find_partition('/boot')
if installation.minimal_installation(): root = fs.find_partition('/')
installation.add_bootloader(harddrive.partition[0])
installation.add_additional_packages(['nano', 'wget', 'git']) boot.format('vfat')
installation.install_profile('awesome')
# 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')
installation.user_create('anton', 'test')
installation.user_set_pw('root', 'toor')
``` ```
This installer will perform the following: This installer will perform the following:

View File

@ -126,6 +126,18 @@ class BlockDevice():
def partition_table_type(self): def partition_table_type(self):
return GPT 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): def has_partitions(self):
return len(self.partitions) return len(self.partitions)
@ -166,7 +178,7 @@ class Partition():
self.mountpoint = target self.mountpoint = target
if not self.filesystem and autodetect_filesystem: 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 self.filesystem = fstype
if self.filesystem == 'crypto_LUKS': if self.filesystem == 'crypto_LUKS':
@ -214,14 +226,15 @@ class Partition():
self._encrypted = value self._encrypted = value
@property
def parent(self):
return self.real_device
@property @property
def real_device(self): def real_device(self):
if not self._encrypted: for blockdevice in json.loads(b''.join(sys_command('lsblk -J')).decode('UTF-8'))['blockdevices']:
return self.path if (parent := self.find_parent_of(blockdevice, os.path.basename(self.path))):
else: 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}') # raise DiskError(f'Could not find appropriate parent for encrypted partition {self}')
return self.path return self.path
@ -366,14 +379,16 @@ class Partition():
if not fs: if not fs:
if not self.filesystem: raise DiskError(f'Need to format (or define) the filesystem on {self} before mounting.') if not self.filesystem: raise DiskError(f'Need to format (or define) the filesystem on {self} before mounting.')
fs = self.filesystem 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()) pathlib.Path(target).mkdir(parents=True, exist_ok=True)
# if ret < 0:
# errno = ctypes.get_errno() try:
# raise OSError(errno, f"Error mounting {self.path} ({fs}) on {target} with options '{options}': {os.strerror(errno)}") sys_command(f'/usr/bin/mount {self.path} {target}')
if sys_command(f'/usr/bin/mount {self.path} {target}').exit_code == 0: except SysCallError as err:
self.mountpoint = target raise err
return True
self.mountpoint = target
return True
def unmount(self): def unmount(self):
try: try:
@ -572,6 +587,24 @@ def get_mount_info(path):
return output['filesystems'][0] 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): def get_filesystem_type(path):
try: try:
handle = sys_command(f"blkid -o value -s TYPE {path}") handle = sys_command(f"blkid -o value -s TYPE {path}")

View File

@ -34,30 +34,21 @@ class Installer():
:type hostname: str, optional :type hostname: str, optional
""" """
def __init__(self, partition, boot_partition, *, base_packages='base base-devel linux linux-firmware efibootmgr', profile=None, mountpoint='/mnt', hostname='ArchInstalled', logdir=None, logfile=None): def __init__(self, target, *, base_packages='base base-devel linux linux-firmware efibootmgr'):
self.profile = profile self.target = target
self.hostname = hostname
self.mountpoint = mountpoint
self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S') self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S')
self.milliseconds = int(str(time.time()).split('.')[1]) self.milliseconds = int(str(time.time()).split('.')[1])
if logdir:
storage['LOG_PATH'] = logdir
if logfile:
storage['LOG_FILE'] = logfile
self.helper_flags = { self.helper_flags = {
'bootloader' : False,
'base' : 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.base_packages = base_packages.split(' ') if type(base_packages) is str else base_packages
self.post_base_install = [] self.post_base_install = []
storage['session'] = self
self.partition = partition storage['session'] = self
self.boot_partition = boot_partition self.partitions = get_partitions_in_use(self.target)
def log(self, *args, level=LOG_LEVELS.Debug, **kwargs): def log(self, *args, level=LOG_LEVELS.Debug, **kwargs):
""" """
@ -67,9 +58,6 @@ class Installer():
log(*args, level=level, **kwargs) log(*args, level=level, **kwargs)
def __enter__(self, *args, **kwargs): def __enter__(self, *args, **kwargs):
self.partition.mount(self.mountpoint)
os.makedirs(f'{self.mountpoint}/boot', exist_ok=True)
self.boot_partition.mount(f'{self.mountpoint}/boot')
return self return self
def __exit__(self, *args, **kwargs): def __exit__(self, *args, **kwargs):
@ -112,18 +100,18 @@ class Installer():
if (filename := storage.get('LOG_FILE', None)): if (filename := storage.get('LOG_FILE', None)):
absolute_logfile = os.path.join(storage.get('LOG_PATH', './'), filename) absolute_logfile = os.path.join(storage.get('LOG_PATH', './'), filename)
if not os.path.isdir(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.mountpoint}/{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 return True
def mount(self, partition, mountpoint, create_mountpoint=True): def mount(self, partition, mountpoint, create_mountpoint=True):
if create_mountpoint and not os.path.isdir(f'{self.mountpoint}{mountpoint}'): if create_mountpoint and not os.path.isdir(f'{self.target}{mountpoint}'):
os.makedirs(f'{self.mountpoint}{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): def post_install_check(self, *args, **kwargs):
return [step for step, flag in self.helper_flags.items() if flag is False] return [step for step, flag in self.helper_flags.items() if flag is False]
@ -133,7 +121,7 @@ class Installer():
self.log(f'Installing packages: {packages}', level=LOG_LEVELS.Info) self.log(f'Installing packages: {packages}', level=LOG_LEVELS.Info)
if (sync_mirrors := sys_command('/usr/bin/pacman -Syy')).exit_code == 0: if (sync_mirrors := sys_command('/usr/bin/pacman -Syy')).exit_code == 0:
if (pacstrap := sys_command(f'/usr/bin/pacstrap {self.mountpoint} {" ".join(packages)}', **kwargs)).exit_code == 0: if (pacstrap := sys_command(f'/usr/bin/pacstrap {self.target} {" ".join(packages)}', **kwargs)).exit_code == 0:
return True return True
else: else:
self.log(f'Could not strap in packages: {pacstrap.exit_code}', level=LOG_LEVELS.Info) self.log(f'Could not strap in packages: {pacstrap.exit_code}', level=LOG_LEVELS.Info)
@ -141,42 +129,41 @@ class Installer():
self.log(f'Could not sync mirrors: {sync_mirrors.exit_code}', level=LOG_LEVELS.Info) self.log(f'Could not sync mirrors: {sync_mirrors.exit_code}', level=LOG_LEVELS.Info)
def set_mirrors(self, mirrors): 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'): 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 fstab = sys_command(f'/usr/bin/genfstab {flags} {self.target}').trace_log
with open(f"{self.mountpoint}/etc/fstab", 'ab') as fstab_fh: with open(f"{self.target}/etc/fstab", 'ab') as fstab_fh:
fstab_fh.write(fstab) 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}') raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n{fstab}')
return True return True
def set_hostname(self, hostname=None, *args, **kwargs): def set_hostname(self, hostname :str, *args, **kwargs):
if not hostname: hostname = self.hostname with open(f'{self.target}/etc/hostname', 'w') as fh:
with open(f'{self.mountpoint}/etc/hostname', 'w') as fh: fh.write(hostname + '\n')
fh.write(self.hostname + '\n')
def set_locale(self, locale, encoding='UTF-8', *args, **kwargs): def set_locale(self, locale, encoding='UTF-8', *args, **kwargs):
if not len(locale): return True 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') 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') 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): def set_timezone(self, zone, *args, **kwargs):
if not zone: return True if not zone: return True
if not len(zone): return True # Redundant if not len(zone): return True # Redundant
if (pathlib.Path("/usr")/"share"/"zoneinfo"/zone).exists(): if (pathlib.Path("/usr")/"share"/"zoneinfo"/zone).exists():
(pathlib.Path(self.mountpoint)/"etc"/"localtime").unlink(missing_ok=True) (pathlib.Path(self.target)/"etc"/"localtime").unlink(missing_ok=True)
sys_command(f'/usr/bin/arch-chroot {self.mountpoint} ln -s /usr/share/zoneinfo/{zone} /etc/localtime') sys_command(f'/usr/bin/arch-chroot {self.target} ln -s /usr/share/zoneinfo/{zone} /etc/localtime')
return True return True
else: else:
self.log( self.log(
@ -196,7 +183,7 @@ class Installer():
return self.arch_chroot(f'systemctl enable {service}').exit_code == 0 return self.arch_chroot(f'systemctl enable {service}').exit_code == 0
def run_command(self, cmd, *args, **kwargs): 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): def arch_chroot(self, cmd, *args, **kwargs):
return self.run_command(cmd) return self.run_command(cmd)
@ -216,15 +203,15 @@ class Installer():
conf = Networkd(Match={"Name": nic}, Network=network) 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)) netconf.write(str(conf))
def copy_ISO_network_config(self, enable_services=False): def copy_ISO_network_config(self, enable_services=False):
# Copy (if any) iwd password and config files # Copy (if any) iwd password and config files
if os.path.isdir('/var/lib/iwd/'): if os.path.isdir('/var/lib/iwd/'):
if (psk_files := glob.glob('/var/lib/iwd/*.psk')): if (psk_files := glob.glob('/var/lib/iwd/*.psk')):
if not os.path.isdir(f"{self.mountpoint}/var/lib/iwd"): if not os.path.isdir(f"{self.target}/var/lib/iwd"):
os.makedirs(f"{self.mountpoint}/var/lib/iwd") os.makedirs(f"{self.target}/var/lib/iwd")
if enable_services: if enable_services:
# If we haven't installed the base yet (function called pre-maturely) # If we haven't installed the base yet (function called pre-maturely)
@ -244,15 +231,15 @@ class Installer():
self.enable_service('iwd') self.enable_service('iwd')
for psk in psk_files: 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 # Copy (if any) systemd-networkd config files
if (netconfigurations := glob.glob('/etc/systemd/network/*')): if (netconfigurations := glob.glob('/etc/systemd/network/*')):
if not os.path.isdir(f"{self.mountpoint}/etc/systemd/network/"): if not os.path.isdir(f"{self.target}/etc/systemd/network/"):
os.makedirs(f"{self.mountpoint}/etc/systemd/network/") os.makedirs(f"{self.target}/etc/systemd/network/")
for netconf_file in netconfigurations: 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 enable_services:
# If we haven't installed the base yet (function called pre-maturely) # If we haven't installed the base yet (function called pre-maturely)
@ -269,54 +256,69 @@ class Installer():
return True 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): def minimal_installation(self):
## Add necessary packages if encrypting the drive ## Add necessary packages if encrypting the drive
## (encrypted partitions default to btrfs for now, so we need btrfs-progs) ## (encrypted partitions default to btrfs for now, so we need btrfs-progs)
## TODO: Perhaps this should be living in the function which dictates ## TODO: Perhaps this should be living in the function which dictates
## the partitioning. Leaving here for now. ## the partitioning. Leaving here for now.
if self.partition.filesystem == 'btrfs': MODULES = []
#if self.partition.encrypted: BINARIES = []
self.base_packages.append('btrfs-progs') FILES = []
if self.partition.filesystem == 'xfs': HOOKS = ["base", "udev", "autodetect", "keyboard", "keymap", "modconf", "block", "filesystems", "fsck"]
self.base_packages.append('xfsprogs')
if self.partition.filesystem == 'f2fs': for partition in self.partitions:
self.base_packages.append('f2fs-tools') 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.pacstrap(self.base_packages)
self.helper_flags['base-strapped'] = True self.helper_flags['base-strapped'] = True
#self.genfstab() #self.genfstab()
with open(f"{self.mountpoint}/etc/fstab", "a") as fstab: with open(f"{self.target}/etc/fstab", "a") as fstab:
fstab.write( fstab.write(
"\ntmpfs /tmp tmpfs defaults,noatime,mode=1777 0 0\n" "\ntmpfs /tmp tmpfs defaults,noatime,mode=1777 0 0\n"
) # Redundant \n at the start? who knows? ) # Redundant \n at the start? who knows?
## TODO: Support locale and timezone ## TODO: Support locale and timezone
#os.remove(f'{self.mountpoint}/etc/localtime') #os.remove(f'{self.target}/etc/localtime')
#sys_command(f'/usr/bin/arch-chroot {self.mountpoint} ln -s /usr/share/zoneinfo/{localtime} /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') #sys_command('/usr/bin/arch-chroot /mnt hwclock --hctosys --localtime')
self.set_hostname() self.set_hostname('archinstall')
self.set_locale('en_US') self.set_locale('en_US')
# TODO: Use python functions for this # 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. with open(f'{self.target}/etc/mkinitcpio.conf', 'w') as mkinit:
# TODO: Yes, we should not overwrite the entire thing, but for now this should be fine mkinit.write(f"MODULES=({' '.join(MODULES)})\n")
# since we just installed the base system. mkinit.write(f"BINARIES=({' '.join(BINARIES)})\n")
if self.partition.filesystem == 'btrfs': mkinit.write(f"FILES=({' '.join(FILES)})\n")
with open(f'{self.mountpoint}/etc/mkinitcpio.conf', 'w') as mkinit: mkinit.write(f"HOOKS=({' '.join(HOOKS)})\n")
mkinit.write('MODULES=(btrfs)\n') sys_command(f'/usr/bin/arch-chroot {self.target} mkinitcpio -p linux')
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')
self.helper_flags['base'] = True self.helper_flags['base'] = True
@ -328,7 +330,15 @@ class Installer():
return True return True
def add_bootloader(self, bootloader='systemd-bootctl'): 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 bootloader == 'systemd-bootctl':
# TODO: Ideally we would want to check if another config # TODO: Ideally we would want to check if another config
@ -336,11 +346,11 @@ class Installer():
# And in which case we should do some clean up. # And in which case we should do some clean up.
# Install the boot loader # 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 # Modify or create a loader.conf
if os.path.isfile(f'{self.mountpoint}/boot/loader/loader.conf'): if os.path.isfile(f'{self.target}/boot/loader/loader.conf'):
with open(f'{self.mountpoint}/boot/loader/loader.conf', 'r') as loader: with open(f'{self.target}/boot/loader/loader.conf', 'r') as loader:
loader_data = loader.read().split('\n') loader_data = loader.read().split('\n')
else: else:
loader_data = [ loader_data = [
@ -348,7 +358,7 @@ class Installer():
f"timeout 5" 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: for line in loader_data:
if line[:8] == 'default ': if line[:8] == 'default ':
loader.write(f'default {self.init_time}\n') loader.write(f'default {self.init_time}\n')
@ -360,7 +370,7 @@ class Installer():
#UUID = sys_command('blkid -s PARTUUID -o value {drive}{partition_2}'.format(**args)).decode('UTF-8').strip() #UUID = sys_command('blkid -s PARTUUID -o value {drive}{partition_2}'.format(**args)).decode('UTF-8').strip()
# Setup the loader entry # 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 by: archinstall\n')
entry.write(f'# Created on: {self.init_time}\n') entry.write(f'# Created on: {self.init_time}\n')
entry.write(f'title Arch Linux\n') entry.write(f'title Arch Linux\n')
@ -370,28 +380,19 @@ class Installer():
## so we'll use the old manual method until we get that sorted out. ## so we'll use the old manual method until we get that sorted out.
if self.partition.encrypted: if (real_device := self.detect_encryption(root_partition)):
log(f"Identifying root partition by DISK-UUID on {self.partition}, looking for '{os.path.basename(self.partition.real_device)}'.", level=LOG_LEVELS.Debug) # TODO: We need to detect if the encrypted device is a whole disk encryption,
for root, folders, uids in os.walk('/dev/disk/by-uuid'): # or simply a partition encryption. Right now we assume it's a partition (and we always have)
for uid in uids: log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.uuid}'.", level=LOG_LEVELS.Debug)
real_path = os.path.realpath(os.path.join(root, uid)) entry.write(f'options cryptdevice=PARTUUID={real_device.uuid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp\n')
log(f"Checking root partition match {os.path.basename(real_path)} against {os.path.basename(self.partition.real_device)}: {os.path.basename(real_path) == os.path.basename(self.partition.real_device)}", level=LOG_LEVELS.Debug)
if not os.path.basename(real_path) == os.path.basename(self.partition.real_device): continue
entry.write(f'options cryptdevice=UUID={uid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp\n')
self.helper_flags['bootloader'] = bootloader
return True
break
else: else:
log(f"Identifying root partition by PART-UUID on {self.partition}, looking for '{os.path.basename(self.partition.path)}'.", level=LOG_LEVELS.Debug) 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={self.partition.uuid} rw intel_pstate=no_hwp\n') entry.write(f'options root=PARTUUID={root_partition.uuid} rw intel_pstate=no_hwp\n')
self.helper_flags['bootloader'] = bootloader self.helper_flags['bootloader'] = bootloader
return True 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 {root_partition}, there for {self.target}/boot/loader/entries/arch.conf will be broken until fixed.")
else: else:
raise RequirementError(f"Unknown (or not yet implemented) bootloader added to add_bootloader(): {bootloader}") raise RequirementError(f"Unknown (or not yet implemented) bootloader added to add_bootloader(): {bootloader}")
@ -416,19 +417,19 @@ class Installer():
def enable_sudo(self, entity :str, group=False): def enable_sudo(self, entity :str, group=False):
self.log(f'Enabling sudo permissions for {entity}.', level=LOG_LEVELS.Info) 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') sudoers.write(f'{"%" if group else ""}{entity} ALL=(ALL) ALL\n')
return True return True
def user_create(self, user :str, password=None, groups=[], sudo=False): def user_create(self, user :str, password=None, groups=[], sudo=False):
self.log(f'Creating user {user}', level=LOG_LEVELS.Info) 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: if password:
self.user_set_pw(user, password) self.user_set_pw(user, password)
if groups: if groups:
for group in 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): if sudo and self.enable_sudo(user):
self.helper_flags['user'] = True self.helper_flags['user'] = True
@ -440,12 +441,12 @@ class Installer():
# This means the root account isn't locked/disabled with * in /etc/passwd # This means the root account isn't locked/disabled with * in /etc/passwd
self.helper_flags['user'] = True 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 pass
def set_keyboard_language(self, language): def set_keyboard_language(self, language):
if len(language.strip()): 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'KEYMAP={language}\n')
vconsole.write(f'FONT=lat9w-16\n') vconsole.write(f'FONT=lat9w-16\n')
return True return True

View File

@ -255,8 +255,10 @@ def select_disk(dict_o_disks):
if len(drives) >= 1: if len(drives) >= 1:
for index, drive in enumerate(drives): 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']})") 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): ') drive = input('Select one of the above disks (by number or full path) or write /mnt to skip partitioning: ')
if drive.isdigit(): if drive.strip() == '/mnt':
return None
elif drive.isdigit():
drive = int(drive) drive = int(drive)
if drive >= len(drives): if drive >= len(drives):
raise DiskError(f'Selected option "{drive}" is out of range') raise DiskError(f'Selected option "{drive}" is out of range')

View File

@ -30,12 +30,14 @@ def ask_user_questions():
archinstall.arguments['harddrive'] = archinstall.BlockDevice(archinstall.arguments['harddrive']) archinstall.arguments['harddrive'] = archinstall.BlockDevice(archinstall.arguments['harddrive'])
else: else:
archinstall.arguments['harddrive'] = archinstall.select_disk(archinstall.all_disks()) 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. # Perform a quick sanity check on the selected harddrive.
# 1. Check if it has partitions # 1. Check if it has partitions
# 3. Check that we support the current partitions # 3. Check that we support the current partitions
# 2. If so, ask if we should keep them or wipe everything # 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') archinstall.log(f"{archinstall.arguments['harddrive']} contains the following partitions:", fg='yellow')
# We curate a list pf supported partitions # We curate a list pf supported partitions
@ -114,14 +116,14 @@ def ask_user_questions():
elif option == 'format-all': elif option == 'format-all':
archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format() archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format()
archinstall.arguments['harddrive'].keep_partitions = False 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 # If the drive doesn't have any partitions, safely mark the disk with keep_partitions = False
# and ask the user for a root filesystem. # and ask the user for a root filesystem.
archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format() archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format()
archinstall.arguments['harddrive'].keep_partitions = False archinstall.arguments['harddrive'].keep_partitions = False
# Get disk encryption password (or skip if blank) # 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): ')): if (passwd := archinstall.get_password(prompt='Enter disk encryption password (leave blank for no encryption): ')):
archinstall.arguments['!encryption-password'] = passwd archinstall.arguments['!encryption-password'] = passwd
archinstall.arguments['harddrive'].encryption_password = archinstall.arguments['!encryption-password'] archinstall.arguments['harddrive'].encryption_password = archinstall.arguments['!encryption-password']
@ -210,62 +212,63 @@ def perform_installation_steps():
We mention the drive one last time, and count from 5 to 0. 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):
archinstall.do_countdown() print(f" ! Formatting {archinstall.arguments['harddrive']} in ", end='')
archinstall.do_countdown()
""" """
Setup the blockdevice, filesystem (and optionally encryption). Setup the blockdevice, filesystem (and optionally encryption).
Once that's done, we'll hand over to perform_installation() Once that's done, we'll hand over to perform_installation()
""" """
with archinstall.Filesystem(archinstall.arguments['harddrive'], archinstall.GPT) as fs: with archinstall.Filesystem(archinstall.arguments['harddrive'], archinstall.GPT) as fs:
# Wipe the entire drive if the disk flag `keep_partitions`is False. # Wipe the entire drive if the disk flag `keep_partitions`is False.
if archinstall.arguments['harddrive'].keep_partitions is False: if archinstall.arguments['harddrive'].keep_partitions is False:
fs.use_entire_disk(root_filesystem_type=archinstall.arguments.get('filesystem', 'btrfs')) fs.use_entire_disk(root_filesystem_type=archinstall.arguments.get('filesystem', 'btrfs'))
# Check if encryption is desired and mark the root partition as encrypted. # Check if encryption is desired and mark the root partition as encrypted.
if archinstall.arguments.get('!encryption-password', None): if archinstall.arguments.get('!encryption-password', None):
root_partition = fs.find_partition('/') root_partition = fs.find_partition('/')
root_partition.encrypted = True root_partition.encrypted = True
# After the disk is ready, iterate the partitions and check # After the disk is ready, iterate the partitions and check
# which ones are safe to format, and format those. # which ones are safe to format, and format those.
for partition in archinstall.arguments['harddrive']: for partition in archinstall.arguments['harddrive']:
if partition.safe_to_format(): if partition.safe_to_format():
# Partition might be marked as encrypted due to the filesystem type crypt_LUKS # 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. # 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. # In which case partition.encrypted will be true, but passwd will be false.
if partition.encrypted and (passwd := archinstall.arguments.get('!encryption-password', None)): if partition.encrypted and (passwd := archinstall.arguments.get('!encryption-password', None)):
partition.encrypt(password=passwd) partition.encrypt(password=passwd)
else:
partition.format()
else: 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: 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): fs.find_partition('/boot').mount('/mnt/boot')
# 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, perform_installation('/mnt')
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'])
def perform_installation(device, boot_partition, language, mirrors): def perform_installation(mountpoint):
""" """
Performs the installation steps on a block device. Performs the installation steps on a block device.
Only requirement is that the block devices are Only requirement is that the block devices are
formatted and setup prior to entering this function. 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): ## if len(mirrors):
# Certain services might be running that affects the system during installation. # 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 # Currently, only one such service is "reflector.service" which updates /etc/pacman.d/mirrorlist
@ -274,10 +277,11 @@ def perform_installation(device, boot_partition, language, mirrors):
while 'dead' not in (status := archinstall.service_state('reflector')): while 'dead' not in (status := archinstall.service_state('reflector')):
time.sleep(1) 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(): if installation.minimal_installation():
installation.set_mirrors(mirrors) # Set the mirrors in the installation medium installation.set_hostname(archinstall.arguments['hostname'])
installation.set_keyboard_language(language) 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() installation.add_bootloader()
# If user selected to copy the current ISO network configuration # If user selected to copy the current ISO network configuration

View File

@ -1,4 +1,4 @@
import archinstall, getpass import archinstall
# Select a harddrive and a disk password # Select a harddrive and a disk password
archinstall.log(f"Minimal only supports:") archinstall.log(f"Minimal only supports:")
@ -10,14 +10,14 @@ if archinstall.arguments.get('help', None):
archinstall.log(f" - Optional systemd network via --network") archinstall.log(f" - Optional systemd network via --network")
archinstall.arguments['harddrive'] = archinstall.select_disk(archinstall.all_disks()) archinstall.arguments['harddrive'] = archinstall.select_disk(archinstall.all_disks())
archinstall.arguments['harddrive'].keep_partitions = False
def install_on(root, boot): def install_on(mountpoint):
# We kick off the installer by telling it where the root and boot lives # We kick off the installer by telling it where the
with archinstall.Installer(root, boot_partition=boot, hostname='minimal-arch') as installation: with archinstall.Installer(mountpoint) as installation:
# Strap in the base system, add a boot loader and configure # Strap in the base system, add a boot loader and configure
# some other minor details as specified by this profile and user. # some other minor details as specified by this profile and user.
if installation.minimal_installation(): if installation.minimal_installation():
installation.set_hostname('minimal-arch')
installation.add_bootloader() installation.add_bootloader()
# Optionally enable networking: # Optionally enable networking:
@ -36,29 +36,36 @@ def install_on(root, boot):
archinstall.log(f" * root (password: airoot)") archinstall.log(f" * root (password: airoot)")
archinstall.log(f" * devel (password: devel)") archinstall.log(f" * devel (password: devel)")
print(f" ! Formatting {archinstall.arguments['harddrive']} in ", end='') if archinstall.arguments['harddrive']:
archinstall.do_countdown() archinstall.arguments['harddrive'].keep_partitions = False
# First, we configure the basic filesystem layout print(f" ! Formatting {archinstall.arguments['harddrive']} in ", end='')
with archinstall.Filesystem(archinstall.arguments['harddrive'], archinstall.GPT) as fs: archinstall.do_countdown()
# 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') # First, we configure the basic filesystem layout
root = fs.find_partition('/') 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.format('vfat') boot = fs.find_partition('/boot')
root = fs.find_partition('/')
# We encrypt the root partition if we got a password to do so with, boot.format('vfat')
# Otherwise we just skip straight to formatting and installation
if archinstall.arguments.get('!encryption-password', None):
root.encrypt()
with archinstall.luks2(root, 'luksloop', archinstall.arguments.get('!encryption-password', None)) as unlocked_root: # We encrypt the root partition if we got a password to do so with,
unlocked_root.format(root.filesystem) # 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))
install_on(unlocked_root) with archinstall.luks2(root, 'luksloop', archinstall.arguments.get('!encryption-password', None)) as unlocked_root:
else: unlocked_root.format(root.filesystem)
root.format(root.filesystem) unlocked_root.mount('/mnt')
install_on(root, boot) else:
root.format(root.filesystem)
root.mount('/mnt')
boot.mount('/mnt/boot')
install_on('/mnt')