index : archinstall32 | |
Archlinux32 installer | gitolite user |
summaryrefslogtreecommitdiff |
diff --git a/.github/workflows/iso-build.yaml b/.github/workflows/iso-build.yaml index 2e4df13c..6b89a50c 100644 --- a/.github/workflows/iso-build.yaml +++ b/.github/workflows/iso-build.yaml @@ -3,17 +3,16 @@ name: Build Arch ISO with ArchInstall Commit on: + push: + branches: + - master + - main # In case we adopt this convention in the future pull_request: paths-ignore: - - '.github/**' - 'docs/**' - '**.editorconfig' - '**.gitignore' - - '**.yml' - - '**.yaml' - '**.md' - - '**.toml' - - '**.cfg' - 'LICENSE' - 'PKGBUILD' @@ -29,7 +28,11 @@ jobs: - run: find . - run: cat /etc/os-release - run: mkdir -p /tmp/archlive/airootfs/root/archinstall-git; cp -r . /tmp/archlive/airootfs/root/archinstall-git - - run: echo "pip uninstall archinstall -y; cd archinstall-git; python setup.py install; echo 'Type python -m archinstall to launch archinstall'" > /tmp/archlive/airootfs/root/.zprofile + - run: echo "pip uninstall archinstall -y; cd archinstall-git; python setup.py install" > /tmp/archlive/airootfs/root/.zprofile + - run: echo "echo \"This is an unofficial ISO for development and testing of archinstall. No support will be provided.\"" >> /tmp/archlive/airootfs/root/.zprofile + - run: echo "echo \"This ISO was built from Git SHA $GITHUB_SHA\"" >> /tmp/archlive/airootfs/root/.zprofile + - run: echo "echo \"Type archinstall to launch the installer.\"" >> /tmp/archlive/airootfs/root/.zprofile + - run: cat /tmp/archlive/airootfs/root/.zprofile - run: pacman -Sy; pacman --noconfirm -S git archiso - run: cp -r /usr/share/archiso/configs/releng/* /tmp/archlive - run: echo -e "git\npython\npython-pip\npython-setuptools" >> /tmp/archlive/packages.x86_64 diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 09bcba55..489077b7 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -5,7 +5,7 @@ name: Upload archinstall to PyPi on: release: - types: [created] + types: [created, published] jobs: deploy: @@ -21,11 +21,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine + pip install setuptools wheel flit - name: Build and publish env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + FLIT_USERNAME: ${{ secrets.PYPI_USERNAME }} + FLIT_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | - python setup.py sdist bdist_wheel - twine upload dist/* + flit publish @@ -21,3 +21,6 @@ SAFETY_LOCK **/**.target **/**.qcow2 **/test.py +**/archiso +/guided.py +/install.log diff --git a/.pypirc b/.pypirc new file mode 100644 index 00000000..7b926de7 --- /dev/null +++ b/.pypirc @@ -0,0 +1,6 @@ +[distutils] +index-servers = + pypi + +[pypi] +repository = https://upload.pypi.org/legacy/
\ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 48cc14d1..a72704bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,15 @@ # Contributing to archinstall -Any contributions through pull requests are welcome as this project aims to be a community based project to ease some Arch Linux installation steps.<br> -Bear in mind that the future of this repo might be transferred to the official [GitLab repo under Arch Linux](http://gitlab.archlinux.org/archlinux/) *(if they so choose to adopt the project)*. +Any contributions through pull requests are welcome as this project aims to be a community based project to ease some Arch Linux installation steps. Bear in mind that in the future this repo might be transferred to the official [GitLab repo under Arch Linux](http://gitlab.archlinux.org/archlinux/) *(if GitLab becomes open to the general public)*. + Therefore guidelines and style changes to the code might come into affect as well as guidelines surrounding bug reporting and discussions. +## Branches + +`master` is currently the default branch, and that's where all future feature work is being done, this means that `master` is a living entity and will most likely never be in a fully stable state. For stable releases, please see the tagged commits. + +Patch releases will be done against their own branches, branched from stable tagged releases and will be named according to the version it will become on release *(Patches to `v2.1.4` will be done on branch `v2.1.5` for instance)*. + ## Discussions Currently, questions, bugs and suggestions should be reported through [GitHub issue tracker](https://github.com/archlinux/archinstall/issues).<br> @@ -26,6 +26,11 @@ Assuming you are on a Arch Linux live-ISO and booted into EFI mode. # python -m archinstall guided +# Help? + +Submit an issue here on Github, or submit a post in the discord help channel.<br> +When doing so, attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you! + # 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. @@ -95,19 +100,18 @@ with archinstall.Installer('/mnt') as installation: This installer will perform the following: * Prompt the user to select a disk and disk-password - * Proceed to wipe the selected disk with a `GPT` partition table. + * Proceed to wipe the selected disk with a `GPT` partition table on a UEFI system and MBR on a bios system. * Sets up a default 100% used disk with encryption. * Installs a basic instance of Arch Linux *(base base-devel linux linux-firmware btrfs-progs efibootmgr)* - * Installs and configures a bootloader to partition 0. + * Installs and configures a bootloader to partition 0 on uefi. on bios it sets the root to partition 0. * Install additional packages *(nano, wget, git)* * Installs a profile with a window manager called [awesome](https://github.com/archlinux/archinstall/blob/master/profiles/awesome.py) *(more on profile installations in the [documentation](https://python-archinstall.readthedocs.io/en/latest/archinstall/Profile.html))*. > **Creating your own ISO with this script on it:** Follow [ArchISO](https://wiki.archlinux.org/index.php/archiso)'s guide on how to create your own ISO or use a pre-built [guided ISO](https://hvornum.se/archiso/) to skip the python installation step, or to create auto-installing ISO templates. Further down are examples and cheat sheets on how to create different live ISO's. -# Help +## Unattended installation based on MAC address -Submit an issue on Github, or submit a post in the discord help channel.<br> -When doing so, attach any `install-session_*.log` to the issue ticket which can be found under `~/.cache/archinstall/`. +Archinstall comes with a [unattended](examples/unattended.py) example which will look for a matching profile for the machine it is being run on, based on any local MAC address. For instance, if the machine that [unattended](examples/unattended.py) is run on has the MAC address `52:54:00:12:34:56` it will look for a profile called [profiles/52-54-00-12-34-56.py](profiles/52-54-00-12-34-56.py). If it's found, the unattended installation will commence and source that profile as it's installation proceedure. # Testing diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 84ba0ec0..e2c7ea62 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -1,8 +1,9 @@ +"""Arch Linux installer - guided, templates etc.""" from .lib.general import * from .lib.disk import * from .lib.user_interaction import * from .lib.exceptions import * -from .lib.installer import * +from .lib.installer import __packages__, Installer from .lib.profiles import * from .lib.luks import * from .lib.mirrors import * @@ -14,7 +15,7 @@ from .lib.output import * from .lib.storage import * from .lib.hardware import * -__version__ = "2.1.4" +__version__ = "2.2.0" ## Basic version of arg.parse() supporting: ## --key=value diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index c23bc6ac..44462a21 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -1,10 +1,11 @@ import glob, re, os, json, time, hashlib -import pathlib, traceback +import pathlib, traceback, logging from collections import OrderedDict from .exceptions import DiskError from .general import * -from .output import log, LOG_LEVELS +from .output import log from .storage import storage +from .hardware import hasUEFI ROOT_DIR_PATTERN = re.compile('^.*?/devices') GPT = 0b00000001 @@ -73,7 +74,7 @@ class BlockDevice(): raise DiskError(f'Could not locate backplane info for "{self.path}"') if self.info['type'] == 'loop': - for drive in json.loads(b''.join(sys_command(f'losetup --json', hide_from_log=True)).decode('UTF_8'))['loopdevices']: + for drive in json.loads(b''.join(sys_command(['losetup', '--json'], hide_from_log=True)).decode('UTF_8'))['loopdevices']: if not drive['name'] == self.path: continue return drive['back-file'] @@ -87,17 +88,17 @@ class BlockDevice(): raise DiskError(f'A crypt device ({self.path}) without a parent kernel device name.') return f"/dev/{self.info['pkname']}" else: - log(f"Unknown blockdevice type for {self.path}: {self.info['type']}", level=LOG_LEVELS.Debug) + log(f"Unknown blockdevice type for {self.path}: {self.info['type']}", level=logging.DEBUG) # if not stat.S_ISBLK(os.stat(full_path).st_mode): # raise DiskError(f'Selected disk "{full_path}" is not a block device.') @property def partitions(self): - o = b''.join(sys_command(f'partprobe {self.path}')) + o = b''.join(sys_command(['partprobe', self.path])) #o = b''.join(sys_command('/usr/bin/lsblk -o name -J -b {dev}'.format(dev=dev))) - o = b''.join(sys_command(f'/usr/bin/lsblk -J {self.path}')) + o = b''.join(sys_command(['/usr/bin/lsblk', '-J', self.path])) if b'not a block device' in o: raise DiskError(f'Can not read partitions off something that isn\'t a block device: {self.path}') @@ -128,7 +129,7 @@ class BlockDevice(): @property def uuid(self): - log(f'BlockDevice().uuid is untested!', level=LOG_LEVELS.Warning, fg='yellow') + log(f'BlockDevice().uuid is untested!', level=logging.WARNING, fg='yellow') """ Returns the disk UUID as returned by lsblk. This is more reliable than relying on /dev/disk/by-partuuid as @@ -220,9 +221,6 @@ class Partition(): @encrypted.setter def encrypted(self, value :bool): - if value: - log(f'Marking {self} as encrypted: {value}', level=LOG_LEVELS.Debug) - #log(f"Callstrack when marking the partition: {''.join(traceback.format_stack())}", level=LOG_LEVELS.Debug) self._encrypted = value @@ -239,7 +237,7 @@ class Partition(): return self.path def detect_inner_filesystem(self, password): - log(f'Trying to detect inner filesystem format on {self} (This might take a while)', level=LOG_LEVELS.Info) + log(f'Trying to detect inner filesystem format on {self} (This might take a while)', level=logging.INFO) from .luks import luks2 try: @@ -268,16 +266,16 @@ class Partition(): def safe_to_format(self): if self.allow_formatting is False: - log(f"Partition {self} is not marked for formatting.", level=LOG_LEVELS.Debug) + log(f"Partition {self} is not marked for formatting.", level=logging.DEBUG) return False elif self.target_mountpoint == '/boot': try: if self.has_content(): - log(f"Partition {self} is a boot partition and has content inside.", level=LOG_LEVELS.Debug) + log(f"Partition {self} is a boot partition and has content inside.", level=logging.DEBUG) return False except SysCallError as err: - log(err.message, LOG_LEVELS.Debug) - log(f"Partition {self} was identified as /boot but we could not mount to check for content, continuing!", level=LOG_LEVELS.Debug) + log(err.message, logging.DEBUG) + log(f"Partition {self} was identified as /boot but we could not mount to check for content, continuing!", level=logging.DEBUG) pass return True @@ -292,7 +290,7 @@ class Partition(): raise DiskError(f"Attempting to encrypt a partition that was not marked for encryption: {self}") if not self.safe_to_format(): - log(f"Partition {self} was marked as protected but encrypt() was called on it!", level=LOG_LEVELS.Error, fg="red") + log(f"Partition {self} was marked as protected but encrypt() was called on it!", level=logging.ERROR, fg="red") return False handle = luks2(self, None, None) @@ -320,7 +318,7 @@ class Partition(): raise PermissionError(f"{self} is not formatable either because instance is locked ({self.allow_formatting}) or a blocking flag was given ({allow_formatting})") if log_formatting: - log(f'Formatting {path} -> {filesystem}', level=LOG_LEVELS.Info) + log(f'Formatting {path} -> {filesystem}', level=logging.INFO) if filesystem == 'btrfs': o = b''.join(sys_command(f'/usr/bin/mkfs.btrfs -f {path}')) @@ -375,7 +373,7 @@ class Partition(): def mount(self, target, fs=None, options=''): if not self.mountpoint: - log(f'Mounting {self} to {target}', level=LOG_LEVELS.Info) + log(f'Mounting {self} to {target}', level=logging.INFO) if not fs: if not self.filesystem: raise DiskError(f'Need to format (or define) the filesystem on {self} before mounting.') fs = self.filesystem @@ -427,25 +425,30 @@ class Filesystem(): # TODO: # When instance of a HDD is selected, check all usages and gracefully unmount them # as well as close any crypto handles. - def __init__(self, blockdevice, mode=GPT): + def __init__(self, blockdevice,mode): self.blockdevice = blockdevice self.mode = mode def __enter__(self, *args, **kwargs): if self.blockdevice.keep_partitions is False: - log(f'Wiping {self.blockdevice} by using partition format {self.mode}', level=LOG_LEVELS.Debug) + log(f'Wiping {self.blockdevice} by using partition format {self.mode}', level=logging.DEBUG) if self.mode == GPT: if self.raw_parted(f'{self.blockdevice.device} mklabel gpt').exit_code == 0: self.blockdevice.flush_cache() return self else: raise DiskError(f'Problem setting the partition format to GPT:', f'/usr/bin/parted -s {self.blockdevice.device} mklabel gpt') + elif self.mode == MBR: + if sys_command(f'/usr/bin/parted -s {self.blockdevice.device} mklabel msdos').exit_code == 0: + return self + else: + raise DiskError(f'Problem setting the partition format to GPT:', f'/usr/bin/parted -s {self.blockdevice.device} mklabel msdos') else: raise DiskError(f'Unknown mode selected to format in: {self.mode}') # TODO: partition_table_type is hardcoded to GPT at the moment. This has to be changed. elif self.mode == self.blockdevice.partition_table_type: - log(f'Kept partition format {self.mode} for {self.blockdevice}', level=LOG_LEVELS.Debug) + log(f'Kept partition format {self.mode} for {self.blockdevice}', level=logging.DEBUG) else: raise DiskError(f'The selected partition table format {self.mode} does not match that of {self.blockdevice}.') @@ -468,7 +471,6 @@ class Filesystem(): def raw_parted(self, string:str): x = sys_command(f'/usr/bin/parted -s {string}') - log(f"'parted -s {string}' returned: {b''.join(x)}", level=LOG_LEVELS.Debug) return x def parted(self, string:str): @@ -481,29 +483,40 @@ class Filesystem(): return self.raw_parted(string).exit_code def use_entire_disk(self, root_filesystem_type='ext4'): - log(f"Using and formatting the entire {self.blockdevice}.", level=LOG_LEVELS.Debug) - self.add_partition('primary', start='1MiB', end='513MiB', format='fat32') - self.set_name(0, 'EFI') - self.set(0, 'boot on') - # TODO: Probably redundant because in GPT mode 'esp on' is an alias for "boot on"? - # https://www.gnu.org/software/parted/manual/html_node/set.html - self.set(0, 'esp on') - self.add_partition('primary', start='513MiB', end='100%') - - self.blockdevice.partition[0].filesystem = 'vfat' - self.blockdevice.partition[1].filesystem = root_filesystem_type - log(f"Set the root partition {self.blockdevice.partition[1]} to use filesystem {root_filesystem_type}.", level=LOG_LEVELS.Debug) - - self.blockdevice.partition[0].target_mountpoint = '/boot' - self.blockdevice.partition[1].target_mountpoint = '/' - - self.blockdevice.partition[0].allow_formatting = True - self.blockdevice.partition[1].allow_formatting = True + log(f"Using and formatting the entire {self.blockdevice}.", level=logging.DEBUG) + if hasUEFI(): + self.add_partition('primary', start='1MiB', end='513MiB', format='fat32') + self.set_name(0, 'EFI') + self.set(0, 'boot on') + # TODO: Probably redundant because in GPT mode 'esp on' is an alias for "boot on"? + # https://www.gnu.org/software/parted/manual/html_node/set.html + self.set(0, 'esp on') + self.add_partition('primary', start='513MiB', end='100%') + + self.blockdevice.partition[0].filesystem = 'vfat' + self.blockdevice.partition[1].filesystem = root_filesystem_type + log(f"Set the root partition {self.blockdevice.partition[1]} to use filesystem {root_filesystem_type}.", level=logging.DEBUG) + + self.blockdevice.partition[0].target_mountpoint = '/boot' + self.blockdevice.partition[1].target_mountpoint = '/' + + self.blockdevice.partition[0].allow_formatting = True + self.blockdevice.partition[1].allow_formatting = True + else: + #we don't need a seprate boot partition it would be a waste of space + self.add_partition('primary', start='1MB', end='100%') + self.blockdevice.partition[0].filesystem=root_filesystem_type + log(f"Set the root partition {self.blockdevice.partition[0]} to use filesystem {root_filesystem_type}.", level=logging.DEBUG) + self.blockdevice.partition[0].target_mountpoint = '/' + self.blockdevice.partition[0].allow_formatting = True def add_partition(self, type, start, end, format=None): - log(f'Adding partition to {self.blockdevice}', level=LOG_LEVELS.Info) + log(f'Adding partition to {self.blockdevice}', level=logging.INFO) previous_partitions = self.blockdevice.partitions + if self.mode == MBR: + if len(self.blockdevice.partitions)>3: + DiskError("Too many partitions on disk, MBR disks can only have 3 parimary partitions") if format: partitioning = self.parted(f'{self.blockdevice.device} mkpart {type} {format} {start} {end}') == 0 else: diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 6e3b66f1..eb0c5d14 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -1,10 +1,10 @@ import os, json, hashlib, shlex, sys -import time, pty +import time, pty, logging from datetime import datetime, date from subprocess import Popen, STDOUT, PIPE, check_output from select import epoll, EPOLLIN, EPOLLHUP from .exceptions import * -from .output import log, LOG_LEVELS +from .output import log def gen_uid(entropy_length=256): return hashlib.sha512(os.urandom(entropy_length)).hexdigest() @@ -76,7 +76,7 @@ class sys_command():#Thread): """ Stolen from archinstall_gui """ - def __init__(self, cmd, callback=None, start_callback=None, environment_vars={}, *args, **kwargs): + def __init__(self, cmd, callback=None, start_callback=None, peak_output=False, environment_vars={}, *args, **kwargs): kwargs.setdefault("worker_id", gen_uid()) kwargs.setdefault("emulate", False) kwargs.setdefault("suppress_errors", False) @@ -84,15 +84,24 @@ class sys_command():#Thread): self.log = kwargs.get('log', log) if kwargs['emulate']: - self.log(f"Starting command '{cmd}' in emulation mode.", level=LOG_LEVELS.Debug) + self.log(f"Starting command '{cmd}' in emulation mode.", level=logging.DEBUG) + + if type(cmd) is list: + # if we get a list of arguments + self.raw_cmd = shlex.join(cmd) + self.cmd = cmd + else: + # else consider it a single shell string + # this should only be used if really necessary + self.raw_cmd = cmd + try: + self.cmd = shlex.split(cmd) + except Exception as e: + raise ValueError(f'Incorrect string to split: {cmd}\n{e}') - self.raw_cmd = cmd - try: - self.cmd = shlex.split(cmd) - except Exception as e: - raise ValueError(f'Incorrect string to split: {cmd}\n{e}') self.args = args self.kwargs = kwargs + self.peak_output = peak_output self.environment_vars = environment_vars self.kwargs.setdefault("worker", None) @@ -151,6 +160,38 @@ class sys_command():#Thread): 'exit_code': self.exit_code } + def peak(self, output :str): + if type(output) == bytes: + try: + output = output.decode('UTF-8') + except UnicodeDecodeError: + return None + + output = output.strip('\r\n ') + if len(output) <= 0: + return None + + if self.peak_output: + from .user_interaction import get_terminal_width + + # Move back to the beginning of the terminal + sys.stdout.flush() + sys.stdout.write("\033[%dG" % 0) + sys.stdout.flush() + + # Clear the line + sys.stdout.write(" " * get_terminal_width()) + sys.stdout.flush() + + # Move back to the beginning again + sys.stdout.flush() + sys.stdout.write("\033[%dG" % 0) + sys.stdout.flush() + + # And print the new output we're peaking on: + sys.stdout.write(output) + sys.stdout.flush() + def run(self): self.status = 'running' old_dir = os.getcwd() @@ -163,7 +204,7 @@ class sys_command():#Thread): os.execve(self.cmd[0], self.cmd, {**os.environ, **self.environment_vars}) except FileNotFoundError: self.status = 'done' - self.log(f"{self.cmd[0]} does not exist.", level=LOG_LEVELS.Debug) + self.log(f"{self.cmd[0]} does not exist.", level=logging.DEBUG) self.exit_code = 1 return False @@ -173,8 +214,8 @@ class sys_command():#Thread): poller.register(child_fd, EPOLLIN | EPOLLHUP) if 'events' in self.kwargs and 'debug' in self.kwargs: - self.log(f'[D] Using triggers for command: {self.cmd}', level=LOG_LEVELS.Debug) - self.log(json.dumps(self.kwargs['events']), level=LOG_LEVELS.Debug) + self.log(f'[D] Using triggers for command: {self.cmd}', level=logging.DEBUG) + self.log(json.dumps(self.kwargs['events']), level=logging.DEBUG) alive = True last_trigger_pos = 0 @@ -182,13 +223,14 @@ class sys_command():#Thread): for fileno, event in poller.poll(0.1): try: output = os.read(child_fd, 8192) + self.peak(output) self.trace_log += output except OSError: alive = False break if 'debug' in self.kwargs and self.kwargs['debug'] and len(output): - self.log(self.cmd, 'gave:', output.decode('UTF-8'), level=LOG_LEVELS.Debug) + self.log(self.cmd, 'gave:', output.decode('UTF-8'), level=logging.DEBUG) if 'on_output' in self.kwargs: self.kwargs['on_output'](self.kwargs['worker'], output) @@ -209,8 +251,8 @@ class sys_command():#Thread): trigger_pos = self.trace_log[last_trigger_pos:].lower().find(trigger.lower()) if 'debug' in self.kwargs and self.kwargs['debug']: - self.log(f"Writing to subprocess {self.cmd[0]}: {self.kwargs['events'][trigger].decode('UTF-8')}", level=LOG_LEVELS.Debug) - self.log(f"Writing to subprocess {self.cmd[0]}: {self.kwargs['events'][trigger].decode('UTF-8')}", level=LOG_LEVELS.Debug) + self.log(f"Writing to subprocess {self.cmd[0]}: {self.kwargs['events'][trigger].decode('UTF-8')}", level=logging.DEBUG) + self.log(f"Writing to subprocess {self.cmd[0]}: {self.kwargs['events'][trigger].decode('UTF-8')}", level=logging.DEBUG) last_trigger_pos = trigger_pos os.write(child_fd, self.kwargs['events'][trigger]) @@ -224,18 +266,18 @@ class sys_command():#Thread): ## Adding a exit trigger: if len(self.kwargs['events']) == 0: if 'debug' in self.kwargs and self.kwargs['debug']: - self.log(f"Waiting for last command {self.cmd[0]} to finish.", level=LOG_LEVELS.Debug) + self.log(f"Waiting for last command {self.cmd[0]} to finish.", level=logging.DEBUG) if bytes(f']$'.lower(), 'UTF-8') in self.trace_log[0-len(f']$')-5:].lower(): if 'debug' in self.kwargs and self.kwargs['debug']: - self.log(f"{self.cmd[0]} has finished.", level=LOG_LEVELS.Debug) + self.log(f"{self.cmd[0]} has finished.", level=logging.DEBUG) alive = False break self.status = 'done' if 'debug' in self.kwargs and self.kwargs['debug']: - self.log(f"{self.cmd[0]} waiting for exit code.", level=LOG_LEVELS.Debug) + self.log(f"{self.cmd[0]} waiting for exit code.", level=logging.DEBUG) if not self.kwargs['emulate']: try: @@ -249,14 +291,14 @@ class sys_command():#Thread): self.exit_code = 0 if 'debug' in self.kwargs and self.kwargs['debug']: - self.log(f"{self.cmd[0]} got exit code: {self.exit_code}", level=LOG_LEVELS.Debug) + self.log(f"{self.cmd[0]} got exit code: {self.exit_code}", level=logging.DEBUG) if 'ignore_errors' in self.kwargs: self.exit_code = 0 if self.exit_code != 0 and not self.kwargs['suppress_errors']: - #self.log(self.trace_log.decode('UTF-8'), level=LOG_LEVELS.Debug) - #self.log(f"'{self.raw_cmd}' did not exit gracefully, exit code {self.exit_code}.", level=LOG_LEVELS.Error) + #self.log(self.trace_log.decode('UTF-8'), level=logging.DEBUG) + #self.log(f"'{self.raw_cmd}' did not exit gracefully, exit code {self.exit_code}.", level=logging.ERROR) raise SysCallError(message=f"{self.trace_log.decode('UTF-8')}\n'{self.raw_cmd}' did not exit gracefully (trace log above), exit code: {self.exit_code}", exit_code=self.exit_code) self.ended = time.time() diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index 5828fd09..f139dfe4 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -1,14 +1,44 @@ -import os +import os, subprocess, json from .general import sys_command from .networking import list_interfaces, enrichIfaceTypes +from typing import Optional -def hasWifi(): +__packages__ = ['xf86-video-amdgpu', 'xf86-video-ati', 'xf86-video-intel', 'xf86-video-nouveau', 'xf86-video-fbdev', 'xf86-video-vesa', 'xf86-video-vmware', 'nvidia', 'mesa'] + +AVAILABLE_GFX_DRIVERS = { + # Sub-dicts are layer-2 options to be selected + # and lists are a list of packages to be installed + 'AMD / ATI' : { + 'amd' : ['xf86-video-amdgpu'], + 'ati' : ['xf86-video-ati'] + }, + 'intel' : ['xf86-video-intel'], + 'nvidia' : { + 'open-source' : ['xf86-video-nouveau'], + 'proprietary' : ['nvidia'] + }, + 'mesa' : ['mesa'], + 'fbdev' : ['xf86-video-fbdev'], + 'vesa' : ['xf86-video-vesa'], + 'vmware / virtualbox' : ['xf86-video-vmware'] +} + +def hasWifi()->bool: return 'WIRELESS' in enrichIfaceTypes(list_interfaces().values()).values() -def hasUEFI(): +def hasAMDCPU()->bool: + if subprocess.check_output("lscpu | grep AMD", shell=True).strip().decode(): + return True + return False +def hasIntelCPU()->bool: + if subprocess.check_output("lscpu | grep Intel", shell=True).strip().decode(): + return True + return False + +def hasUEFI()->bool: return os.path.isdir('/sys/firmware/efi') -def graphicsDevices(): +def graphicsDevices()->dict: cards = {} for line in sys_command(f"lspci"): if b' VGA ' in line: @@ -16,13 +46,28 @@ def graphicsDevices(): cards[identifier.strip().lower().decode('UTF-8')] = line return cards -def hasNvidiaGraphics(): +def hasNvidiaGraphics()->bool: return any('nvidia' in x for x in graphicsDevices()) -def hasAmdGraphics(): +def hasAmdGraphics()->bool: return any('amd' in x for x in graphicsDevices()) -def hasIntelGraphics(): +def hasIntelGraphics()->bool: return any('intel' in x for x in graphicsDevices()) + +def cpuVendor()-> Optional[str]: + cpu_info = json.loads(subprocess.check_output("lscpu -J", shell=True).decode('utf-8'))['lscpu'] + for info in cpu_info: + if info.get('field',None): + if info.get('field',None) == "Vendor ID:": + return info.get('data',None) + +def isVM() -> bool: + try: + subprocess.check_call(["systemd-detect-virt"]) # systemd-detect-virt issues a non-zero exit code if it is not on a virtual machine + return True + except: + return False + # TODO: Add more identifiers diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 758033a7..331762b4 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1,5 +1,5 @@ -import os, stat, time, shutil, pathlib, subprocess - +import os, stat, time, shutil, pathlib +import subprocess, logging from .exceptions import * from .disk import * from .general import * @@ -7,8 +7,12 @@ from .user_interaction import * from .profiles import Profile from .mirrors import * from .systemd import Networkd -from .output import log, LOG_LEVELS +from .output import log from .storage import storage +from .hardware import * + +# Any package that the Installer() is responsible for (optional and the default ones) +__packages__ = ["base", "base-devel", "linux-firmware", "linux", "linux-lts", "linux-zen", "linux-hardened"] class Installer(): """ @@ -18,7 +22,7 @@ class Installer(): :param partition: Requires a partition as the first argument, this is so that the installer can mount to `mountpoint` and strap packages there. :type partition: class:`archinstall.Partition` - + :param boot_partition: There's two reasons for needing a boot partition argument, The first being so that `mkinitcpio` can place the `vmlinuz` kernel at the right place during the `pacstrap` or `linux` and the base packages for a minimal installation. @@ -29,12 +33,12 @@ class Installer(): :param profile: A profile to install, this is optional and can be called later manually. This just simplifies the process by not having to call :py:func:`~archinstall.Installer.install_profile` later on. :type profile: str, optional - + :param hostname: The given /etc/hostname for the machine. :type hostname: str, optional """ - def __init__(self, target, *, base_packages='base base-devel linux linux-firmware efibootmgr'): + def __init__(self, target, *, base_packages=__packages__[:3], kernels=['linux']): self.target = target self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S') self.milliseconds = int(str(time.time()).split('.')[1]) @@ -43,14 +47,17 @@ class Installer(): 'base' : False, 'bootloader' : False } - + self.base_packages = base_packages.split(' ') if type(base_packages) is str else base_packages + for kernel in kernels: + self.base_packages.append(kernel) + self.post_base_install = [] storage['session'] = self self.partitions = get_partitions_in_use(self.target) - def log(self, *args, level=LOG_LEVELS.Debug, **kwargs): + def log(self, *args, level=logging.DEBUG, **kwargs): """ installer.log() wraps output.log() mainly to set a default log-level for this install session. Any manual override can be done per log() call. @@ -65,8 +72,8 @@ class Installer(): # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager if len(args) >= 2 and args[1]: - #self.log(self.trace_log.decode('UTF-8'), level=LOG_LEVELS.Debug) - self.log(args[1], level=LOG_LEVELS.Error, fg='red') + #self.log(self.trace_log.decode('UTF-8'), level=logging.DEBUG) + self.log(args[1], level=logging.ERROR, fg='red') self.sync_log_to_install_medium() @@ -79,17 +86,17 @@ class Installer(): self.genfstab() if not (missing_steps := self.post_install_check()): - self.log('Installation completed without any errors. You may now reboot.', bg='black', fg='green', level=LOG_LEVELS.Info) + self.log('Installation completed without any errors. You may now reboot.', fg='green', level=logging.INFO) self.sync_log_to_install_medium() return True else: - self.log('Some required steps were not successfully installed/configured before leaving the installer:', bg='black', fg='red', level=LOG_LEVELS.Warning) + self.log('Some required steps were not successfully installed/configured before leaving the installer:', fg='red', level=logging.WARNING) for step in missing_steps: - self.log(f' - {step}', bg='black', fg='red', level=LOG_LEVELS.Warning) + self.log(f' - {step}', fg='red', level=logging.WARNING) - self.log(f"Detailed error logs can be found at: {storage['LOG_PATH']}", level=LOG_LEVELS.Warning) - self.log(f"Submit this zip file as an issue to https://github.com/archlinux/archinstall/issues", level=LOG_LEVELS.Warning) + self.log(f"Detailed error logs can be found at: {storage['LOG_PATH']}", level=logging.WARNING) + self.log(f"Submit this zip file as an issue to https://github.com/archlinux/archinstall/issues", level=logging.WARNING) self.sync_log_to_install_medium() return False @@ -119,21 +126,21 @@ class Installer(): def pacstrap(self, *packages, **kwargs): if type(packages[0]) in (list, tuple): packages = packages[0] - self.log(f'Installing packages: {packages}', level=LOG_LEVELS.Info) + self.log(f'Installing packages: {packages}', level=logging.INFO) if (sync_mirrors := sys_command('/usr/bin/pacman -Syy')).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) + self.log(f'Could not strap in packages: {pacstrap.exit_code}', level=logging.INFO) else: - 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=logging.INFO) def set_mirrors(self, mirrors): return use_mirrors(mirrors, destination=f'{self.target}/etc/pacman.d/mirrorlist') def genfstab(self, flags='-pU'): - self.log(f"Updating {self.target}/etc/fstab", level=LOG_LEVELS.Info) + self.log(f"Updating {self.target}/etc/fstab", level=logging.INFO) fstab = sys_command(f'/usr/bin/genfstab {flags} {self.target}').trace_log with open(f"{self.target}/etc/fstab", 'ab') as fstab_fh: @@ -141,7 +148,7 @@ class Installer(): 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 :str, *args, **kwargs): @@ -169,19 +176,19 @@ class Installer(): else: self.log( f"Time zone {zone} does not exist, continuing with system default.", - level=LOG_LEVELS.Warning, + level=logging.WARNING, fg='red' ) def activate_ntp(self): - self.log(f'Installing and activating NTP.', level=LOG_LEVELS.Info) + self.log(f'Installing and activating NTP.', level=logging.INFO) if self.pacstrap('ntp'): if self.enable_service('ntpd'): return True def enable_service(self, *services): for service in services: - self.log(f'Enabling service {service}', level=LOG_LEVELS.Info) + self.log(f'Enabling service {service}', level=logging.INFO) if (output := self.arch_chroot(f'systemctl enable {service}')).exit_code != 0: raise ServiceException(f"Unable to start service {service}: {output}") @@ -189,6 +196,9 @@ class Installer(): return sys_command(f'/usr/bin/arch-chroot {self.target} {cmd}') def arch_chroot(self, cmd, *args, **kwargs): + if 'runas' in kwargs: + cmd = f"su - {kwargs['runas']} -c \"{cmd}\"" + return self.run_command(cmd) def drop_to_shell(self): @@ -223,7 +233,7 @@ class Installer(): # If we haven't installed the base yet (function called pre-maturely) if self.helper_flags.get('base', False) is False: self.base_packages.append('iwd') - # This function will be called after minimal_installation() + # This function will be called after minimal_installation() # as a hook for post-installs. This hook is only needed if # base is not installed yet. def post_install_enable_iwd_service(*args, **kwargs): @@ -273,6 +283,7 @@ class Installer(): ## (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. + MODULES = [] BINARIES = [] FILES = [] @@ -298,10 +309,20 @@ class Installer(): if 'encrypt' not in HOOKS: HOOKS.insert(HOOKS.index('filesystems'), 'encrypt') + if not(hasUEFI()): # TODO: Allow for grub even on EFI + self.base_packages.append('grub') + self.pacstrap(self.base_packages) self.helper_flags['base-strapped'] = True #self.genfstab() - + if not isVM(): + vendor = cpuVendor() + if vendor == "AuthenticAMD": + self.base_packages.append("amd-ucode") + elif vendor == "GenuineIntel": + self.base_packages.append("intel-ucode") + else: + 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" @@ -322,13 +343,13 @@ class Installer(): 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') + sys_command(f'/usr/bin/arch-chroot {self.target} mkinitcpio -P') self.helper_flags['base'] = True # Run registered post-install hooks for function in self.post_base_install: - self.log(f"Running post-installation hook: {function}", level=LOG_LEVELS.Info) + self.log(f"Running post-installation hook: {function}", level=logging.INFO) function(self) return True @@ -342,9 +363,13 @@ class Installer(): elif partition.mountpoint == self.target: root_partition = partition - self.log(f'Adding bootloader {bootloader} to {boot_partition}', level=LOG_LEVELS.Info) + self.log(f'Adding bootloader {bootloader} to {boot_partition if boot_partition else root_partition}', level=logging.INFO) if bootloader == 'systemd-bootctl': + self.pacstrap('efibootmgr') + + if not hasUEFI(): + raise HardwareIncompatibilityError # TODO: Ideally we would want to check if another config # points towards the same disk and/or partition. # And in which case we should do some clean up. @@ -372,13 +397,20 @@ class Installer(): ## For some reason, blkid and /dev/disk/by-uuid are not getting along well. ## And blkid is wrong in terms of LUKS. #UUID = sys_command('blkid -s PARTUUID -o value {drive}{partition_2}'.format(**args)).decode('UTF-8').strip() - # Setup the loader entry with open(f'{self.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') entry.write(f'linux /vmlinuz-linux\n') + if not isVM(): + vendor = cpuVendor() + if vendor == "AuthenticAMD": + entry.write("initrd /amd-ucode.img\n") + elif vendor == "GenuineIntel": + entry.write("initrd /intel-ucode.img\n") + else: + self.log("unknow cpu vendor, not adding ucode to systemd-boot config") entry.write(f'initrd /initramfs-linux.img\n') ## blkid doesn't trigger on loopback devices really well, ## so we'll use the old manual method until we get that sorted out. @@ -387,18 +419,34 @@ class Installer(): 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) + log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.uuid}'.", level=logging.DEBUG) entry.write(f'options cryptdevice=PARTUUID={real_device.uuid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp\n') else: - log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=LOG_LEVELS.Debug) + log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=logging.DEBUG) entry.write(f'options root=PARTUUID={root_partition.uuid} rw intel_pstate=no_hwp\n') self.helper_flags['bootloader'] = bootloader return True - 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.") + 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": + self.pacstrap('grub') + + if hasUEFI(): + self.pacstrap('efibootmgr') + 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 /mnt grub-mkconfig -o /boot/grub/grub.cfg') + return True + else: + root_device = subprocess.check_output(f'basename "$(readlink -f /sys/class/block/{root_partition.path.replace("/dev/","")}/..)"', shell=True).decode().strip() + if root_device == "block": + root_device = f"{root_partition.path}" + o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.target} grub-install --target=i386-pc /dev/{root_device}')) + sys_command('/usr/bin/arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg') + self.helper_flags['bootloader'] = bootloader + return True else: - raise RequirementError(f"Unknown (or not yet implemented) bootloader added to add_bootloader(): {bootloader}") + raise RequirementError(f"Unknown (or not yet implemented) bootloader requested: {bootloader}") def add_additional_packages(self, *packages): return self.pacstrap(*packages) @@ -416,17 +464,17 @@ class Installer(): if type(profile) == str: profile = Profile(self, profile) - self.log(f'Installing network profile {profile}', level=LOG_LEVELS.Info) + self.log(f'Installing network profile {profile}', level=logging.INFO) return profile.install() 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=logging.INFO) 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) + self.log(f'Creating user {user}', level=logging.INFO) 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) @@ -439,7 +487,7 @@ class Installer(): self.helper_flags['user'] = True def user_set_pw(self, user, password): - self.log(f'Setting password for {user}', level=LOG_LEVELS.Info) + self.log(f'Setting password for {user}', level=logging.INFO) if user == 'root': # This means the root account isn't locked/disabled with * in /etc/passwd @@ -449,7 +497,7 @@ class Installer(): pass def user_set_shell(self, user, shell): - self.log(f'Setting shell for {user} to {shell}', level=LOG_LEVELS.Info) + self.log(f'Setting shell for {user} to {shell}', level=logging.INFO) o = b''.join(sys_command(f"/usr/bin/arch-chroot {self.target} sh -c \"chsh -s {shell} {user}\"")) pass @@ -460,5 +508,5 @@ class Installer(): vconsole.write(f'KEYMAP={language}\n') vconsole.write(f'FONT=lat9w-16\n') else: - self.log(f'Keyboard language was not changed from default (no language specified).', fg="yellow", level=LOG_LEVELS.Info) + self.log(f'Keyboard language was not changed from default (no language specified).', fg="yellow", level=logging.INFO) return True diff --git a/archinstall/lib/locale_helpers.py b/archinstall/lib/locale_helpers.py index 736bfc47..3c373bc6 100644 --- a/archinstall/lib/locale_helpers.py +++ b/archinstall/lib/locale_helpers.py @@ -16,6 +16,12 @@ def list_keyboard_languages(): if os.path.splitext(file)[1] == '.gz': yield file.strip('.gz').strip('.map') +def verify_keyboard_layout(layout): + for language in list_keyboard_languages(): + if layout.lower() == language.lower(): + return True + return False + def search_keyboard_layout(filter): for language in list_keyboard_languages(): if filter.lower() in language.lower(): diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py index ca077b3d..7f8485e6 100644 --- a/archinstall/lib/luks.py +++ b/archinstall/lib/luks.py @@ -2,10 +2,11 @@ import os import shlex import time import pathlib +import logging from .exceptions import * from .general import * from .disk import Partition -from .output import log, LOG_LEVELS +from .output import log from .storage import storage class luks2(): @@ -48,7 +49,7 @@ class luks2(): if not self.partition.allow_formatting: raise DiskError(f'Could not encrypt volume {self.partition} due to it having a formatting lock.') - log(f'Encrypting {partition} (This might take a while)', level=LOG_LEVELS.Info) + log(f'Encrypting {partition} (This might take a while)', level=logging.INFO) if not key_file: if self.key_file: @@ -70,7 +71,7 @@ class luks2(): '--batch-mode', '--verbose', '--type', 'luks2', - '--pbkdf', 'argon2i', + '--pbkdf', 'argon2id', '--hash', hash_type, '--key-size', str(key_size), '--iter-time', str(iter_time), @@ -84,7 +85,7 @@ class luks2(): cmd_handle = sys_command(cryptsetup_args) except SysCallError as err: if err.exit_code == 256: - log(f'{partition} is being used, trying to unmount and crypt-close the device and running one more attempt at encrypting the device.', level=LOG_LEVELS.Debug) + log(f'{partition} is being used, trying to unmount and crypt-close the device and running one more attempt at encrypting the device.', level=logging.DEBUG) # Partition was in use, unmount it and try again partition.unmount() @@ -97,11 +98,11 @@ class luks2(): for child in children: # Unmount the child location if child_mountpoint := child.get('mountpoint', None): - log(f'Unmounting {child_mountpoint}', level=LOG_LEVELS.Debug) + log(f'Unmounting {child_mountpoint}', level=logging.DEBUG) sys_command(f"umount -R {child_mountpoint}") # And close it if possible. - log(f"Closing crypt device {child['name']}", level=LOG_LEVELS.Debug) + log(f"Closing crypt device {child['name']}", level=logging.DEBUG) sys_command(f"cryptsetup close {child['name']}") # Then try again to set up the crypt-device diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 04f47c0d..ae6c6422 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -1,4 +1,4 @@ -import urllib.request +import urllib.request, logging from .exceptions import * from .general import * @@ -59,7 +59,7 @@ def insert_mirrors(mirrors, *args, **kwargs): return True def use_mirrors(regions :dict, destination='/etc/pacman.d/mirrorlist'): - log(f'A new package mirror-list has been created: {destination}', level=LOG_LEVELS.Info) + log(f'A new package mirror-list has been created: {destination}', level=logging.INFO) for region, mirrors in regions.items(): with open(destination, 'w') as mirrorlist: for mirror in mirrors: @@ -79,7 +79,7 @@ def list_mirrors(): try: response = urllib.request.urlopen(url) except urllib.error.URLError as err: - log(f'Could not fetch an active mirror-list: {err}', level=LOG_LEVELS.Warning, fg="yellow") + log(f'Could not fetch an active mirror-list: {err}', level=logging.WARNING, fg="yellow") return regions diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index 6b184b4b..06d99778 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -17,8 +17,32 @@ class LOG_LEVELS: class journald(dict): @abc.abstractmethod - def log(message, level=LOG_LEVELS.Debug): - import systemd.journal + def log(message, level=logging.DEBUG): + try: + import systemd.journal + except ModuleNotFoundError: + return False + + # For backwards compability, convert old style log-levels + # to logging levels (and warn about deprecated usage) + # There's some code re-usage here but that should be fine. + # TODO: Remove these in a few versions: + if level == LOG_LEVELS.Critical: + log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) + level = logging.CRITICAL + elif level == LOG_LEVELS.Error: + log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) + level = logging.ERROR + elif level == LOG_LEVELS.Warning: + log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) + level = logging.WARNING + elif level == LOG_LEVELS.Info: + log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) + level = logging.INFO + elif level == LOG_LEVELS.Debug: + log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) + level = logging.DEBUG + log_adapter = logging.getLogger('archinstall') log_fmt = logging.Formatter("[%(levelname)s]: %(message)s") log_ch = systemd.journal.JournalHandler() @@ -26,19 +50,7 @@ class journald(dict): log_adapter.addHandler(log_ch) log_adapter.setLevel(logging.DEBUG) - if level == LOG_LEVELS.Critical: - log_adapter.critical(message) - elif level == LOG_LEVELS.Error: - log_adapter.error(message) - elif level == LOG_LEVELS.Warning: - log_adapter.warning(message) - elif level == LOG_LEVELS.Info: - log_adapter.info(message) - elif level == LOG_LEVELS.Debug: - log_adapter.debug(message) - else: - # Fallback logger - log_adapter.debug(message) + log_adapter.log(level, message) # TODO: Replace log() for session based logging. class SessionLogging(): @@ -95,19 +107,19 @@ def log(*args, **kwargs): # we use that one to output everything if (filename := storage.get('LOG_FILE', None)): absolute_logfile = os.path.join(storage.get('LOG_PATH', './'), filename) - if not os.path.isfile(absolute_logfile): - try: - Path(absolute_logfile).parents[0].mkdir(exist_ok=True, parents=True) - except PermissionError: - # Fallback to creating the log file in the current folder - err_string = f"Not enough permission to place log file at {absolute_logfile}, creating it in {Path('./').absolute()/filename} instead." - absolute_logfile = Path('./').absolute()/filename - absolute_logfile.parents[0].mkdir(exist_ok=True) - absolute_logfile = str(absolute_logfile) - storage['LOG_PATH'] = './' - log(err_string, fg="red") - - Path(absolute_logfile).touch() # Overkill? + + try: + Path(absolute_logfile).parents[0].mkdir(exist_ok=True, parents=True) + with open(absolute_logfile, 'a') as log_file: + log_file.write("") + except PermissionError: + # Fallback to creating the log file in the current folder + err_string = f"Not enough permission to place log file at {absolute_logfile}, creating it in {Path('./').absolute()/filename} instead." + absolute_logfile = Path('./').absolute()/filename + absolute_logfile.parents[0].mkdir(exist_ok=True) + absolute_logfile = str(absolute_logfile) + storage['LOG_PATH'] = './' + log(err_string, fg="red") with open(absolute_logfile, 'a') as log_file: log_file.write(f"{orig_string}\n") @@ -116,13 +128,33 @@ def log(*args, **kwargs): # Unless the level is higher than we've decided to output interactively. # (Remember, log files still get *ALL* the output despite level restrictions) if 'level' in kwargs: - if kwargs['level'] > storage.get('LOG_LEVEL', LOG_LEVELS.Info): + # For backwards compability, convert old style log-levels + # to logging levels (and warn about deprecated usage) + # There's some code re-usage here but that should be fine. + # TODO: Remove these in a few versions: + if kwargs['level'] == LOG_LEVELS.Critical: + log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) + kwargs['level'] = logging.CRITICAL + elif kwargs['level'] == LOG_LEVELS.Error: + log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) + kwargs['level'] = logging.ERROR + elif kwargs['level'] == LOG_LEVELS.Warning: + log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) + kwargs['level'] = logging.WARNING + elif kwargs['level'] == LOG_LEVELS.Info: + log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) + kwargs['level'] = logging.INFO + elif kwargs['level'] == LOG_LEVELS.Debug: + log("Deprecated level detected in log message, please use new logging.<level> instead for the following log message:", fg="red", level=logging.ERROR, force=True) + kwargs['level'] = logging.DEBUG + + if kwargs['level'] > storage.get('LOG_LEVEL', logging.INFO) and not 'force' in kwargs: # Level on log message was Debug, but output level is set to Info. # In that case, we'll drop it. return None try: - journald.log(string, level=kwargs.get('level', LOG_LEVELS.Info)) + journald.log(string, level=kwargs.get('level', logging.INFO)) except ModuleNotFoundError: pass # Ignore writing to journald diff --git a/archinstall/lib/profiles.py b/archinstall/lib/profiles.py index 21ec5f6f..06237c1c 100644 --- a/archinstall/lib/profiles.py +++ b/archinstall/lib/profiles.py @@ -1,10 +1,10 @@ import os, urllib.request, urllib.parse, ssl, json, re -import importlib.util, sys, glob, hashlib +import importlib.util, sys, glob, hashlib, logging from collections import OrderedDict from .general import multisplit, sys_command from .exceptions import * from .networking import * -from .output import log, LOG_LEVELS +from .output import log from .storage import storage def grab_url_data(path): @@ -15,7 +15,7 @@ def grab_url_data(path): response = urllib.request.urlopen(safe_path, context=ssl_context) return response.read() -def list_profiles(filter_irrelevant_macs=True, subpath=''): +def list_profiles(filter_irrelevant_macs=True, subpath='', filter_top_level_profiles=False): # TODO: Grab from github page as well, not just local static files if filter_irrelevant_macs: local_macs = list_interfaces() @@ -63,6 +63,11 @@ def list_profiles(filter_irrelevant_macs=True, subpath=''): cache[profile[:-3]] = {'path' : os.path.join(storage["UPSTREAM_URL"]+subpath, profile), 'description' : profile_list[profile], 'tailored' : tailored} + if filter_top_level_profiles: + for profile in list(cache.keys()): + if Profile(None, profile).is_top_level_profile() is False: + del(cache[profile]) + return cache class Script(): @@ -77,7 +82,6 @@ class Script(): self.examples = None self.namespace = os.path.splitext(os.path.basename(self.path))[0] self.original_namespace = self.namespace - log(f"Script {self} has been loaded with namespace '{self.namespace}'", level=LOG_LEVELS.Debug) def __enter__(self, *args, **kwargs): self.execute() @@ -88,6 +92,9 @@ class Script(): if len(args) >= 2 and args[1]: raise args[1] + if self.original_namespace: + self.namespace = self.original_namespace + def localize_path(self, profile_path): if (url := urllib.parse.urlparse(profile_path)).scheme and url.scheme in ('https', 'http'): if not self.converted_path: @@ -177,7 +184,7 @@ 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() @@ -193,14 +200,19 @@ class Profile(Script): 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 + if '__name__' in source_data and 'is_top_level_profile' in source_data: + with self.load_instructions(namespace=f"{self.namespace}.py") as imported: + if hasattr(imported, 'is_top_level_profile'): + return imported.is_top_level_profile + + # Default to True if nothing is specified, + # since developers like less code - omitting it should assume they want to present it. + return True @property def packages(self) -> list: @@ -222,7 +234,6 @@ class Profile(Script): if hasattr(imported, '__packages__'): return imported.__packages__ return None - class Application(Profile): def __repr__(self, *args, **kwargs): diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py index df8668af..be01594e 100644 --- a/archinstall/lib/user_interaction.py +++ b/archinstall/lib/user_interaction.py @@ -1,11 +1,14 @@ -import getpass, pathlib, os, shutil, re -import sys, time, signal +import getpass, pathlib, os, shutil, re, time +import sys, time, signal, ipaddress, logging +import termios, tty, select # Used for char by char polling of sys.stdin from .exceptions import * from .profiles import Profile -from .locale_helpers import search_keyboard_layout -from .output import log, LOG_LEVELS +from .locale_helpers import list_keyboard_languages, verify_keyboard_layout, search_keyboard_layout +from .output import log from .storage import storage from .networking import list_interfaces +from .general import sys_command +from .hardware import AVAILABLE_GFX_DRIVERS, hasUEFI ## TODO: Some inconsistencies between the selection processes. ## Some return the keys from the options, some the values? @@ -24,7 +27,7 @@ def check_for_correct_username(username): return True log( "The username you entered is invalid. Try again", - level=LOG_LEVELS.Warning, + level=logging.WARNING, fg='red' ) return False @@ -93,14 +96,181 @@ def print_large_list(options, padding=5, margin_bottom=0, separator=': '): print(f"{str(column): >{highest_index_number_length}}{separator}{options[column]}", end = spaces) print() -def ask_for_superuser_account(prompt='Create a required super-user with sudo privileges: ', forced=False): + return column, row + + +def generic_multi_select(options, text="Select one or more of the options above (leave blank to continue): ", sort=True, default=None, allow_empty=False): + if sort: + options = sorted(options) + + section = MiniCurses(get_terminal_width(), len(options)) + + selected_options = [] + + while True: + if len(selected_options) <= 0 and default and default in options: + selected_options.append(default) + + printed_options = [] + for option in options: + if option in selected_options: + printed_options.append(f'>> {option}') + else: + printed_options.append(f'{option}') + + section.clear(0, get_terminal_height()-section._cursor_y-1) + x, y = print_large_list(printed_options, margin_bottom=2) + section._cursor_y = len(printed_options) + section._cursor_x = 0 + section.write_line(text) + section.input_pos = section._cursor_x + selected_option = section.get_keyboard_input(end=None) + + if selected_option is None: + if len(selected_options) <= 0 and default: + selected_options = [default] + + if len(selected_options) or allow_empty is True: + break + else: + log('* Need to select at least one option!', fg='red') + continue + + elif selected_option.isdigit(): + if (selected_option := int(selected_option)) >= len(options): + log('* Option is out of range, please select another one!', fg='red') + continue + selected_option = options[selected_option] + if selected_option in selected_options: + selected_options.remove(selected_option) + else: + selected_options.append(selected_option) + + return selected_options + + +class MiniCurses(): + def __init__(self, width, height): + self.width = width + self.height = height + + self._cursor_y = 0 + self._cursor_x = 0 + + self.input_pos = 0 + + def write_line(self, text, clear_line=True): + if clear_line: + sys.stdout.flush() + sys.stdout.write("\033[%dG" % 0) + sys.stdout.flush() + sys.stdout.write(" " * (get_terminal_width()-1)) + sys.stdout.flush() + sys.stdout.write("\033[%dG" % 0) + sys.stdout.flush() + sys.stdout.write(text) + sys.stdout.flush() + self._cursor_x += len(text) + + def clear(self, x, y): + if x < 0: x = 0 + if y < 0: y = 0 + + #import time + #sys.stdout.write(f"Clearing from: {x, y}") + #sys.stdout.flush() + #time.sleep(2) + + sys.stdout.flush() + sys.stdout.write('\033[%d;%df' % (y, x)) + for line in range(get_terminal_height()-y-1, y): + sys.stdout.write(" " * (get_terminal_width()-1)) + sys.stdout.flush() + sys.stdout.write('\033[%d;%df' % (y, x)) + sys.stdout.flush() + + def deal_with_control_characters(self, char): + mapper = { + '\x7f' : 'BACKSPACE', + '\r' : 'CR', + '\n' : 'NL' + } + + if (mapped_char := mapper.get(char, None)) == 'BACKSPACE': + if self._cursor_x <= self.input_pos: + # Don't backspace futher back than the cursor start position during input + return True + # Move back to the current known position (BACKSPACE doesn't updated x-pos) + sys.stdout.flush() + sys.stdout.write("\033[%dG" % (self._cursor_x)) + sys.stdout.flush() + + # Write a blank space + sys.stdout.flush() + sys.stdout.write(" ") + sys.stdout.flush() + + # And move back again + sys.stdout.flush() + sys.stdout.write("\033[%dG" % (self._cursor_x)) + sys.stdout.flush() + + self._cursor_x -= 1 + + return True + elif mapped_char in ('CR', 'NL'): + return True + + return None + + def get_keyboard_input(self, strip_rowbreaks=True, end='\n'): + assert end in ['\r', '\n', None] + + poller = select.epoll() + response = '' + + sys_fileno = sys.stdin.fileno() + old_settings = termios.tcgetattr(sys_fileno) + tty.setraw(sys_fileno) + + poller.register(sys.stdin.fileno(), select.EPOLLIN) + + EOF = False + while EOF is False: + for fileno, event in poller.poll(0.025): + char = sys.stdin.read(1) + + #sys.stdout.write(f"{[char]}") + #sys.stdout.flush() + + if (newline := (char in ('\n', '\r'))): + EOF = True + + if not newline or strip_rowbreaks is False: + response += char + + if self.deal_with_control_characters(char) is not True: + self.write_line(response[-1], clear_line=False) + + termios.tcsetattr(sys_fileno, termios.TCSADRAIN, old_settings) + + if end: + sys.stdout.write(end) + sys.stdout.flush() + self._cursor_x = 0 + self._cursor_y += 1 + + if response: + return response + +def ask_for_superuser_account(prompt='Username for required superuser with sudo privileges: ', forced=False): while 1: new_user = input(prompt).strip(' ') if not new_user and forced: # TODO: make this text more generic? # It's only used to create the first sudo user when root is disabled in guided.py - log(' * Since root is disabled, you need to create a least one (super) user!', fg='red') + log(' * Since root is disabled, you need to create a least one superuser!', fg='red') continue elif not new_user and not forced: raise UserError("No superuser was created.") @@ -112,7 +282,7 @@ def ask_for_superuser_account(prompt='Create a required super-user with sudo pri def ask_for_additional_users(prompt='Any additional users to install (leave blank for no users): '): users = {} - super_users = {} + superusers = {} while 1: new_user = input(prompt).strip(' ') @@ -122,26 +292,37 @@ def ask_for_additional_users(prompt='Any additional users to install (leave blan continue password = get_password(prompt=f'Password for user {new_user}: ') - if input("Should this user be a sudo (super) user (y/N): ").strip(' ').lower() in ('y', 'yes'): - super_users[new_user] = {"!password" : password} + if input("Should this user be a superuser (sudoer) [y/N]: ").strip(' ').lower() in ('y', 'yes'): + superusers[new_user] = {"!password" : password} else: users[new_user] = {"!password" : password} - return users, super_users + return users, superusers def ask_for_a_timezone(): - timezone = input('Enter a valid timezone (examples: Europe/Stockholm, US/Eastern) or press enter to use UTC: ').strip() - if timezone == '': - timezone = 'UTC' - if (pathlib.Path("/usr")/"share"/"zoneinfo"/timezone).exists(): - return timezone + while True: + timezone = input('Enter a valid timezone (examples: Europe/Stockholm, US/Eastern) or press enter to use UTC: ').strip().strip('*.') + if timezone == '': + timezone = 'UTC' + if (pathlib.Path("/usr")/"share"/"zoneinfo"/timezone).exists(): + return timezone + else: + log( + f"Specified timezone {timezone} does not exist.", + level=logging.WARNING, + fg='red' + ) + +def ask_for_bootloader() -> str: + bootloader = "systemd-bootctl" + if hasUEFI()==False: + bootloader="grub-install" else: - log( - f"Time zone {timezone} does not exist, continuing with system default.", - level=LOG_LEVELS.Warning, - fg='red' - ) - + bootloader_choice = input("Would you like to use GRUB as a bootloader instead of systemd-boot? [y/N] ").lower() + if bootloader_choice == "y": + bootloader="grub-install" + return bootloader + def ask_for_audio_selection(): audio = "pulseaudio" # Default for most desktop environments pipewire_choice = input("Would you like to install pipewire instead of pulseaudio as the default audio server? [Y/n] ").lower() @@ -154,27 +335,56 @@ def ask_to_configure_network(): # Optionally configure one network interface. #while 1: # {MAC: Ifname} - interfaces = {'ISO-CONFIG' : 'Copy ISO network configuration to installation','NetworkManager':'Use NetworkManager to control and manage your internet connection', **list_interfaces()} + interfaces = { + 'ISO-CONFIG' : 'Copy ISO network configuration to installation', + 'NetworkManager':'Use NetworkManager to control and manage your internet connection', + **list_interfaces() + } - nic = generic_select(interfaces.values(), "Select one network interface to configure (leave blank to skip): ") + nic = generic_select(interfaces, "Select one network interface to configure (leave blank to skip): ") if nic and nic != 'Copy ISO network configuration to installation': if nic == 'Use NetworkManager to control and manage your internet connection': return {'nic': nic,'NetworkManager':True} - mode = generic_select(['DHCP (auto detect)', 'IP (static)'], f"Select which mode to configure for {nic}: ") - if mode == 'IP (static)': + + # Current workaround: + # For selecting modes without entering text within brackets, + # printing out this part separate from options, passed in + # `generic_select` + modes = ['DHCP (auto detect)', 'IP (static)'] + for index, mode in enumerate(modes): + print(f"{index}: {mode}") + + mode = generic_select(['DHCP', 'IP'], f"Select which mode to configure for {nic} or leave blank for DHCP: ", + options_output=False) + if mode == 'IP': while 1: ip = input(f"Enter the IP and subnet for {nic} (example: 192.168.0.5/24): ").strip() - if ip: + # Implemented new check for correct IP/subnet input + try: + ipaddress.ip_interface(ip) break - else: + except ValueError: log( "You need to enter a valid IP in IP-config mode.", - level=LOG_LEVELS.Warning, + level=logging.WARNING, fg='red' ) - if not len(gateway := input('Enter your gateway (router) IP address or leave blank for none: ').strip()): - gateway = None + # Implemented new check for correct gateway IP address + while 1: + gateway = input('Enter your gateway (router) IP address or leave blank for none: ').strip() + try: + if len(gateway) == 0: + gateway = None + else: + ipaddress.ip_address(gateway) + break + except ValueError: + log( + "You need to enter a valid gateway (router) IP address.", + level=logging.WARNING, + fg='red' + ) dns = None if len(dns_input := input('Enter your DNS servers (space separated, blank for none): ').strip()): @@ -190,12 +400,13 @@ def ask_to_configure_network(): def ask_for_disk_layout(): options = { - 'keep-existing' : 'Keep existing partition layout and select which ones to use where.', - 'format-all' : 'Format entire drive and setup a basic partition scheme.', - 'abort' : 'Abort the installation.' + 'keep-existing' : 'Keep existing partition layout and select which ones to use where', + 'format-all' : 'Format entire drive and setup a basic partition scheme', + 'abort' : 'Abort the installation' } - value = generic_select(options.values(), "Found partitions on the selected drive, (select by number) what you want to do: ") + value = generic_select(options, "Found partitions on the selected drive, (select by number) what you want to do: ", + allow_empty_input=False, sort=True) return next((key for key, val in options.items() if val == value), None) def ask_for_main_filesystem_format(): @@ -206,40 +417,71 @@ def ask_for_main_filesystem_format(): 'f2fs' : 'f2fs' } - value = generic_select(options.values(), "Select which filesystem your main partition should use (by number or name): ") + value = generic_select(options, "Select which filesystem your main partition should use (by number or name): ", + allow_empty_input=False) return next((key for key, val in options.items() if val == value), None) -def generic_select(options, input_text="Select one of the above by index or absolute value: ", sort=True): +def generic_select(options, input_text="Select one of the above by index or absolute value: ", allow_empty_input=True, options_output=True, sort=False): """ A generic select function that does not output anything other than the options and their indexes. As an example: generic_select(["first", "second", "third option"]) - 1: first - 2: second - 3: third option + 0: first + 1: second + 2: third option + + When the user has entered the option correctly, + this function returns an item from list, a string, or None """ - if type(options) == dict: options = list(options) - if sort: options = sorted(list(options)) - if len(options) <= 0: raise RequirementError('generic_select() requires at least one option to operate.') + # Checking if options are different from `list` or `dict` + if type(options) not in [list, dict]: + log(f" * Generic select doesn't support ({type(options)}) as type of options * ", fg='red') + log(" * If problem persists, please create an issue on https://github.com/archlinux/archinstall/issues * ", fg='yellow') + raise RequirementError("generic_select() requires list or dictionary as options.") + # To allow only `list` and `dict`, converting values of options here. + # Therefore, now we can only provide the dictionary itself + if type(options) == dict: options = list(options.values()) + if sort: options = sorted(options) # As we pass only list and dict (converted to list), we can skip converting to list + if len(options) == 0: + log(f" * Generic select didn't find any options to choose from * ", fg='red') + log(" * If problem persists, please create an issue on https://github.com/archlinux/archinstall/issues * ", fg='yellow') + raise RequirementError('generic_select() requires at least one option to proceed.') + - for index, option in enumerate(options): - print(f"{index}: {option}") + # Added ability to disable the output of options items, + # if another function displays something different from this + if options_output: + for index, option in enumerate(options): + print(f"{index}: {option}") + + # The new changes introduce a single while loop for all inputs processed by this function + # Now the try...except block handles validation for invalid input from the user + while True: + try: + selected_option = input(input_text) + if len(selected_option.strip()) == 0: + # `allow_empty_input` parameter handles return of None on empty input, if necessary + # Otherwise raise `RequirementError` + if allow_empty_input: + return None + raise RequirementError('Please select an option to continue') + # Replaced `isdigit` with` isnumeric` to discard all negative numbers + elif selected_option.isnumeric(): + selected_option = int(selected_option) + if selected_option >= len(options): + raise RequirementError(f'Selected option "{selected_option}" is out of range') + selected_option = options[selected_option] + break + elif selected_option in options: + break # We gave a correct absolute value + else: + raise RequirementError(f'Selected option "{selected_option}" does not exist in available options') + except RequirementError as err: + log(f" * {err} * ", fg='red') + continue - selected_option = input(input_text) - if len(selected_option.strip()) <= 0: - return None - elif selected_option.isdigit(): - selected_option = int(selected_option) - if selected_option > len(options): - raise RequirementError(f'Selected option "{selected_option}" is out of range') - selected_option = options[selected_option] - elif selected_option in options: - pass # We gave a correct absolute value - else: - raise RequirementError(f'Selected option "{selected_option}" does not exist in available options: {options}') - return selected_option def select_disk(dict_o_disks): @@ -257,18 +499,14 @@ 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) 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') - drive = dict_o_disks[drives[drive]] - elif drive in dict_o_disks: - drive = dict_o_disks[drive] - else: - raise DiskError(f'Selected drive does not exist: "{drive}"') + + log(f"You can skip selecting a drive and partitioning and use whatever drive-setup is mounted at /mnt (experimental)", fg="yellow") + drive = generic_select(drives, 'Select one of the above disks (by name or number) or leave blank to use /mnt: ', + options_output=False) + if not drive: + return drive + + drive = dict_o_disks[drive] return drive raise DiskError('select_disk() requires a non-empty dictionary of disks to select from.') @@ -293,29 +531,21 @@ def select_profile(options): print(' -- The above list is a set of pre-programmed profiles. --') print(' -- They might make it easier to install things like desktop environments. --') print(' -- (Leave blank and hit enter to skip this step and continue) --') - selected_profile = input('Enter a pre-programmed profile name if you want to install one: ') - - if len(selected_profile.strip()) <= 0: - return None - - if selected_profile.isdigit() and (pos := int(selected_profile)) <= len(profiles)-1: - selected_profile = profiles[pos] - elif selected_profile in options: - selected_profile = options[options.index(selected_profile)] - else: - RequirementError("Selected profile does not exist.") - return Profile(None, selected_profile) - - raise RequirementError("Selecting profiles require a least one profile to be given as an option.") + selected_profile = generic_select(profiles, 'Enter a pre-programmed profile name if you want to install one: ', + options_output=False) + if selected_profile: + return Profile(None, selected_profile) + else: + raise RequirementError("Selecting profiles require a least one profile to be given as an option.") def select_language(options, show_only_country_codes=True): """ Asks the user to select a language from the `options` dictionary parameter. Usually this is combined with :ref:`archinstall.list_keyboard_languages`. - :param options: A `dict` where keys are the language name, value should be a dict containing language information. - :type options: dict + :param options: A `generator` or `list` where keys are the language name, value should be a dict containing language information. + :type options: generator or list :param show_only_country_codes: Filters out languages that are not len(lang) == 2. This to limit the number of results from stuff like dvorak and x-latin1 alternatives. :type show_only_country_codes: bool @@ -334,35 +564,37 @@ def select_language(options, show_only_country_codes=True): for index, language in enumerate(languages): print(f"{index}: {language}") - print(' -- You can enter ? or help to search for more languages, or skip to use US layout --') - selected_language = input('Select one of the above keyboard languages (by number or full name): ') - - if len(selected_language.strip()) == 0: - return DEFAULT_KEYBOARD_LANGUAGE - elif selected_language.lower() in ('?', 'help'): - while True: - filter_string = input('Search for layout containing (example: "sv-"): ') - new_options = list(search_keyboard_layout(filter_string)) - - if len(new_options) <= 0: - log(f"Search string '{filter_string}' yielded no results, please try another search or Ctrl+D to abort.", fg='yellow') - continue + print(" -- You can choose a layout that isn't in this list, but whose name you know --") + print(" -- Also, you can enter '?' or 'help' to search for more languages, or skip to use US layout --") - return select_language(new_options, show_only_country_codes=False) + while True: + selected_language = input('Select one of the above keyboard languages (by name or full name): ') + if not selected_language: + return DEFAULT_KEYBOARD_LANGUAGE + elif selected_language.lower() in ('?', 'help'): + while True: + filter_string = input("Search for layout containing (example: \"sv-\") or enter 'exit' to exit from search: ") - elif selected_language.isdigit() and (pos := int(selected_language)) <= len(languages)-1: - selected_language = languages[pos] - return selected_language - # I'm leaving "options" on purpose here. - # Since languages possibly contains a filtered version of - # all possible language layouts, and we might want to write - # for instance sv-latin1 (if we know that exists) without having to - # go through the search step. - elif selected_language in options: - selected_language = options[options.index(selected_language)] - return selected_language - else: - raise RequirementError("Selected language does not exist.") + if filter_string.lower() == 'exit': + return select_language(list_keyboard_languages()) + + new_options = list(search_keyboard_layout(filter_string)) + + if len(new_options) <= 0: + log(f"Search string '{filter_string}' yielded no results, please try another search.", fg='yellow') + continue + + return select_language(new_options, show_only_country_codes=False) + elif selected_language.isnumeric(): + selected_language = int(selected_language) + if selected_language >= len(languages): + log(' * Selected option is out of range * ', fg='red') + continue + return languages[selected_language] + elif verify_keyboard_layout(selected_language): + return selected_language + else: + log(" * Given language wasn't found * ", fg='red') raise RequirementError("Selecting languages require a least one language to be given as an option.") @@ -389,23 +621,76 @@ def select_mirror_regions(mirrors, show_top_mirrors=True): print_large_list(regions, margin_bottom=4) print(' -- You can skip this step by leaving the option blank --') - selected_mirror = input('Select one of the above regions to download packages from (by number or full name): ') - if len(selected_mirror.strip()) == 0: + selected_mirror = generic_select(regions, 'Select one of the above regions to download packages from (by number or full name): ', + options_output=False) + if not selected_mirror: # Returning back empty options which can be both used to # do "if x:" logic as well as do `x.get('mirror', {}).get('sub', None)` chaining return {} - elif selected_mirror.isdigit() and int(selected_mirror) <= len(regions)-1: - # I'm leaving "mirrors" on purpose here. - # Since region possibly contains a known region of - # all possible regions, and we might want to write - # for instance Sweden (if we know that exists) without having to - # go through the search step. - region = regions[int(selected_mirror)] - selected_mirrors[region] = mirrors[region] - elif selected_mirror in mirrors: - selected_mirrors[selected_mirror] = mirrors[selected_mirror] - else: - raise RequirementError("Selected region does not exist.") + # I'm leaving "mirrors" on purpose here. + # Since region possibly contains a known region of + # all possible regions, and we might want to write + # for instance Sweden (if we know that exists) without having to + # go through the search step. + selected_mirrors[selected_mirror] = mirrors[selected_mirror] return selected_mirrors + + raise RequirementError("Selecting mirror region require a least one region to be given as an option.") + +def select_driver(options=AVAILABLE_GFX_DRIVERS): + """ + Some what convoluted function, which's job is simple. + Select a graphics driver from a pre-defined set of popular options. + + (The template xorg is for beginner users, not advanced, and should + there for appeal to the general public first and edge cases later) + """ + + drivers = sorted(list(options)) + + if drivers: + lspci = sys_command(f'/usr/bin/lspci') + for line in lspci.trace_log.split(b'\r\n'): + if b' vga ' in line.lower(): + if b'nvidia' in line.lower(): + print(' ** nvidia card detected, suggested driver: nvidia **') + elif b'amd' in line.lower(): + print(' ** AMD card detected, suggested driver: AMD / ATI **') + + initial_option = generic_select(drivers, input_text="Select your graphics card driver: ") + selected_driver = options[initial_option] + + if type(selected_driver) == dict: + driver_options = sorted(list(selected_driver)) + + driver_package_group = generic_select(driver_options, f'Which driver-type do you want for {initial_option}: ', + allow_empty_input=False) + driver_package_group = selected_driver[driver_package_group] + + return driver_package_group + + return selected_driver + + raise RequirementError("Selecting drivers require a least one profile to be given as an option.") + +def select_kernel(options): + """ + Asks the user to select a kernel for system. + + :param options: A `list` with kernel options + :type options: list + + :return: The string as a selected kernel + :rtype: string + """ + + DEFAULT_KERNEL = "linux" + + kernels = sorted(list(options)) + + if kernels: + return generic_multi_select(kernels, f"Choose which kernel to use (leave blank for default: {DEFAULT_KERNEL}): ", default=DEFAULT_KERNEL) + + raise RequirementError("Selecting kernels require a least one kernel to be given as an option.") diff --git a/docs/archinstall/general.rst b/docs/archinstall/general.rst index c3913ea9..7319d244 100644 --- a/docs/archinstall/general.rst +++ b/docs/archinstall/general.rst @@ -12,7 +12,7 @@ Packages ======== .. autofunction:: archinstall.find_package - +Be .. autofunction:: archinstall.find_packages Locale related diff --git a/docs/pull_request_template.md b/docs/pull_request_template.md index c2f694ce..729c1aae 100644 --- a/docs/pull_request_template.md +++ b/docs/pull_request_template.md @@ -2,18 +2,8 @@ # New features *(v2.2.0)* -Merge new features in to `torxed-v2.2.0`.<br> -This branch is designated for potential breaking changes, added complexity and new functionality. - -# Bug fixes *(v2.1.4)* - -Merge against `master` for bug fixes and anything that improves stability and quality of life.<br> -This excludes: - * New functionality - * Added complexity - * Breaking changes - -Any changes to `master` automatically gets pulled in to `torxed-v2.2.0` to avoid merge hell. +All future work towards *`v2.2.0`* is done against `master` now.<br> +Any patch work to existing verions will have to create a new branch against the tagged versions. # Describe your PR @@ -23,6 +13,4 @@ If the PR is larger than ~20 lines, please describe it here unless described in # Testing Any new feature or stability improvement should be tested if possible. -Please follow the test instructions at the bottom of the README. - -*These PR guidelines will change after 2021-05-01, which is when `v2.1.4` gets onto the new ISO* +Please follow the test instructions at the bottom of the README or use the ISO built on each PR.
\ No newline at end of file diff --git a/examples/guided.py b/examples/guided.py index dc638d26..40bebabf 100644 --- a/examples/guided.py +++ b/examples/guided.py @@ -1,11 +1,11 @@ -import getpass, time, json, os +import getpass, time, json, os, logging import archinstall from archinstall.lib.hardware import hasUEFI from archinstall.lib.profiles import Profile -if hasUEFI() is False: - archinstall.log("ArchInstall currently only supports machines booted with UEFI.\nMBR & GRUB support is coming in version 2.2.0!", fg="red", level=archinstall.LOG_LEVELS.Error) - exit(1) +if archinstall.arguments.get('help'): + print("See `man archinstall` for help.") + exit(0) # For support reasons, we'll log the disk layout pre installation to match against post-installation layout archinstall.log(f"Disk states before installing: {archinstall.disk_layouts()}", level=archinstall.LOG_LEVELS.Debug) @@ -67,6 +67,7 @@ def ask_user_questions(): partition_mountpoints[partition] = None except archinstall.UnknownFilesystemFormat as err: archinstall.log(f" {partition} (Filesystem not supported)", fg='red') + # We then ask what to do with the partitions. if (option := archinstall.ask_for_disk_layout()) == 'abort': @@ -77,12 +78,18 @@ def ask_user_questions(): archinstall.log(f" ** You will now select which partitions to use by selecting mount points (inside the installation). **") archinstall.log(f" ** The root would be a simple / and the boot partition /boot (as all paths are relative inside the installation). **") + mountpoints_set = [] while True: # Select a partition - partition = archinstall.generic_select(partition_mountpoints.keys(), + # If we provide keys as options, it's better to convert them to list and sort before passing + mountpoints_list = sorted(list(partition_mountpoints.keys())) + partition = archinstall.generic_select(mountpoints_list, "Select a partition by number that you want to set a mount-point for (leave blank when done): ") if not partition: - break + if set(mountpoints_set) & {'/', '/boot'} == {'/', '/boot'}: + break + + continue # Select a mount-point mountpoint = input(f"Enter a mount-point for {partition}: ").strip(' ') @@ -109,7 +116,7 @@ def ask_user_questions(): # we have to check if we support it. We can do this by formatting /dev/null with the partitions filesystem. # There's a nice wrapper for this on the partition object itself that supports a path-override during .format() try: - partition.format(new_filesystem, path='/dev/null', log_formating=False, allow_formatting=True) + partition.format(new_filesystem, path='/dev/null', log_formatting=False, allow_formatting=True) except archinstall.UnknownFilesystemFormat: archinstall.log(f"Selected filesystem is not supported yet. If you want archinstall to support '{new_filesystem}', please create a issue-ticket suggesting it on github at https://github.com/archlinux/archinstall/issues.") archinstall.log(f"Until then, please enter another supported filesystem.") @@ -123,6 +130,7 @@ def ask_user_questions(): # We can safely mark the partition for formatting and where to mount it. # TODO: allow_formatting might be redundant since target_mountpoint should only be # set if we actually want to format it anyway. + mountpoints_set.append(mountpoint) partition.allow_formatting = True partition.target_mountpoint = mountpoint # Only overwrite the filesystem definition if we selected one: @@ -131,12 +139,14 @@ def ask_user_questions(): archinstall.log('Using existing partition table reported above.') elif option == 'format-all': - archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format() + if not archinstall.arguments.get('filesystem', None): + archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format() archinstall.arguments['harddrive'].keep_partitions = False 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() + if not archinstall.arguments.get('filesystem', None): + archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format() archinstall.arguments['harddrive'].keep_partitions = False # Get disk encryption password (or skip if blank) @@ -144,7 +154,7 @@ def ask_user_questions(): 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'] - + archinstall.arguments["bootloader"] = archinstall.ask_for_bootloader() # Get the hostname for the machine if not archinstall.arguments.get('hostname', None): archinstall.arguments['hostname'] = input('Desired hostname for the installation: ').strip(' ') @@ -165,7 +175,7 @@ def ask_user_questions(): # Ask for archinstall-specific profiles (such as desktop environments etc) if not archinstall.arguments.get('profile', None): - archinstall.arguments['profile'] = archinstall.select_profile(filter(lambda profile: (Profile(None, profile).is_top_level_profile()), archinstall.list_profiles())) + archinstall.arguments['profile'] = archinstall.select_profile(archinstall.list_profiles(filter_top_level_profiles=True)) else: archinstall.arguments['profile'] = archinstall.list_profiles()[archinstall.arguments['profile']] @@ -181,7 +191,6 @@ def ask_user_questions(): # Ask about audio server selection if one is not already set if not archinstall.arguments.get('audio', None): - # only ask for audio server selection on a desktop profile if str(archinstall.arguments['profile']) == 'Profile(desktop)': archinstall.arguments['audio'] = archinstall.ask_for_audio_selection() @@ -190,11 +199,16 @@ def ask_user_questions(): # we will not try to remove packages post-installation to not have audio, as that may cause multiple issues archinstall.arguments['audio'] = None + # Ask for preferred kernel: + if not archinstall.arguments.get("kernels", None): + kernels = ["linux", "linux-lts", "linux-zen", "linux-hardened"] + archinstall.arguments['kernels'] = archinstall.select_kernel(kernels) + # Additional packages (with some light weight error handling for invalid package names) + print("Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.") + print("If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.") while True: if not archinstall.arguments.get('packages', None): - print("Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.") - 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)] if len(archinstall.arguments['packages']): @@ -223,8 +237,8 @@ def ask_user_questions(): def perform_installation_steps(): print() print('This is your chosen configuration:') - archinstall.log("-- Guided template chosen (with below config) --", level=archinstall.LOG_LEVELS.Debug) - archinstall.log(json.dumps(archinstall.arguments, indent=4, sort_keys=True, cls=archinstall.JSON), level=archinstall.LOG_LEVELS.Info) + archinstall.log("-- Guided template chosen (with below config) --", level=logging.DEBUG) + archinstall.log(json.dumps(archinstall.arguments, indent=4, sort_keys=True, cls=archinstall.JSON), level=logging.INFO) print() input('Press Enter to continue.') @@ -242,7 +256,11 @@ def perform_installation_steps(): Setup the blockdevice, filesystem (and optionally encryption). Once that's done, we'll hand over to perform_installation() """ - with archinstall.Filesystem(archinstall.arguments['harddrive'], archinstall.GPT) as fs: + mode = archinstall.GPT + 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')) @@ -264,9 +282,9 @@ def perform_installation_steps(): else: partition.format() 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('/boot').format('vfat') + archinstall.log(f"Did not format {partition} because .safe_to_format() returned False or .allow_formatting was False.", level=logging.DEBUG) + if hasUEFI(): + fs.find_partition('/boot').format('vfat')# we don't have a boot partition in bios mode if archinstall.arguments.get('!encryption-password', None): # First encrypt and unlock, then format the desired partition inside the encrypted part. @@ -278,8 +296,8 @@ def perform_installation_steps(): else: fs.find_partition('/').format(fs.find_partition('/').filesystem) fs.find_partition('/').mount('/mnt') - - fs.find_partition('/boot').mount('/mnt/boot') + if hasUEFI(): + fs.find_partition('/boot').mount('/mnt/boot') perform_installation('/mnt') @@ -290,25 +308,25 @@ def perform_installation(mountpoint): Only requirement is that the block devices are formatted and setup prior to entering this function. """ - with archinstall.Installer(mountpoint) as installation: + with archinstall.Installer(mountpoint, kernels=archinstall.arguments.get('kernels', 'linux')) 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 # We need to wait for it before we continue since we opted in to use a custom mirror/region. - installation.log(f'Waiting for automatic mirror selection (reflector) to complete.', level=archinstall.LOG_LEVELS.Info) + installation.log(f'Waiting for automatic mirror selection (reflector) to complete.', level=logging.INFO) while archinstall.service_state('reflector') not in ('dead', 'failed'): time.sleep(1) - # Set mirrors used by pacstrap (outside of installation) if archinstall.arguments.get('mirror-region', None): archinstall.use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium - if installation.minimal_installation(): installation.set_hostname(archinstall.arguments['hostname']) if archinstall.arguments['mirror-region'].get("mirrors",{})!= None: installation.set_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium + if archinstall.arguments["bootloader"]=="grub-install" and hasUEFI()==True: + installation.add_additional_packages("grub") installation.set_keyboard_language(archinstall.arguments['keyboard-language']) - installation.add_bootloader() + installation.add_bootloader(archinstall.arguments["bootloader"]) # If user selected to copy the current ISO network configuration # Perform a copy of the config @@ -324,15 +342,16 @@ def perform_installation(mountpoint): installation.enable_service('systemd-resolved') if archinstall.arguments.get('audio', None) != None: - installation.log(f"This audio server will be used: {archinstall.arguments.get('audio', None)}", level=archinstall.LOG_LEVELS.Info) + installation.log(f"This audio server will be used: {archinstall.arguments.get('audio', None)}", level=logging.INFO) if archinstall.arguments.get('audio', None) == 'pipewire': print('Installing pipewire ...') + 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") else: - installation.log("No audio server will be installed.", level=archinstall.LOG_LEVELS.Info) + installation.log("No audio server will be installed.", level=logging.INFO) if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': installation.add_additional_packages(archinstall.arguments.get('packages', None)) @@ -342,7 +361,7 @@ def perform_installation(mountpoint): for user, user_info in archinstall.arguments.get('users', {}).items(): installation.user_create(user, user_info["!password"], sudo=False) - + for superuser, user_info in archinstall.arguments.get('superusers', {}).items(): installation.user_create(superuser, user_info["!password"], sudo=True) @@ -352,6 +371,15 @@ def perform_installation(mountpoint): if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): installation.user_set_pw('root', root_pw) + if archinstall.arguments['profile'] and archinstall.arguments['profile'].has_post_install(): + with archinstall.arguments['profile'].load_instructions(namespace=f"{archinstall.arguments['profile'].namespace}.py") as imported: + if not imported._post_install(): + archinstall.log( + ' * Profile\'s post configuration requirements was not fulfilled.', + fg='red' + ) + exit(1) + installation.log("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", fg="yellow") choice = input("Would you like to chroot into the newly created installation and perform post-installation configuration? [Y/n] ") if choice.lower() in ("y", ""): diff --git a/profiles/52-54-00-12-34-56.py b/profiles/52-54-00-12-34-56.py index 679c6721..a3347760 100644 --- a/profiles/52-54-00-12-34-56.py +++ b/profiles/52-54-00-12-34-56.py @@ -1,8 +1,14 @@ import archinstall import json import urllib.request -import git +__packages__ = ['nano', 'wget', 'git'] + +if __name__ == '52-54-00-12-34-56': + awesome = archinstall.Application(installation, 'postgresql') + awesome.install() + +""" # Unmount and close previous runs (Mainly only used for re-runs, but won't hurt.) archinstall.sys_command(f'umount -R /mnt', suppress_errors=True) archinstall.sys_command(f'cryptsetup close /dev/mapper/luksloop', suppress_errors=True) @@ -11,7 +17,7 @@ archinstall.sys_command(f'cryptsetup close /dev/mapper/luksloop', suppress_error harddrive = archinstall.all_disks()['/dev/sda'] disk_password = '1234' -with archinstall.Filesystem(harddrive, archinstall.GPT) as fs: +with archinstall.Filesystem(harddrive) as fs: # Use the entire disk instead of setting up partitions on your own fs.use_entire_disk('luks2') @@ -30,22 +36,19 @@ with archinstall.Filesystem(harddrive, archinstall.GPT) as fs: if installation.minimal_installation(): installation.add_bootloader() - installation.add_additional_packages(['nano', 'wget', 'git']) + installation.add_additional_packages(__packages__) installation.install_profile('awesome') - installation.user_create('anton', 'test') + installation.user_create('devel', 'devel') installation.user_set_pw('root', 'toor') - repo = git.Repo('./') - commit = repo.head.commit.hexsha[:7] - - print(f'Submitting {commit}: success') + print(f'Submitting {archinstall.__version__}: success') conditions = { "project": "archinstall", "profile": "52-54-00-12-34-56", "status": "success", - "commit": commit + "version": archinstall.__version__ } req = urllib.request.Request("https://api.archlinux.life/build/success", data=json.dumps(conditions).encode('utf8'), @@ -53,4 +56,5 @@ with archinstall.Filesystem(harddrive, archinstall.GPT) as fs: try: urllib.request.urlopen(req, timeout=5) except: - pass
\ No newline at end of file + pass +"""
\ No newline at end of file diff --git a/profiles/applications/alacritty.py b/profiles/applications/alacritty.py deleted file mode 100644 index aab64bb4..00000000 --- a/profiles/applications/alacritty.py +++ /dev/null @@ -1,3 +0,0 @@ -import archinstall - -installation.add_additional_packages("alacritty")
\ No newline at end of file diff --git a/profiles/applications/awesome.py b/profiles/applications/awesome.py index 793ee52b..a63c707b 100644 --- a/profiles/applications/awesome.py +++ b/profiles/applications/awesome.py @@ -1,10 +1,10 @@ import archinstall +__packages__ = ["awesome", "xorg-xrandr", "xterm", "feh", "slock", "terminus-font", "gnu-free-fonts", "ttf-liberation", "xsel"] + installation.install_profile('xorg') -installation.add_additional_packages( - "awesome xorg-xrandr xterm feh slock terminus-font gnu-free-fonts ttf-liberation xsel" -) +installation.add_additional_packages(__packages__) with open(f'{installation.target}/etc/X11/xinit/xinitrc', 'r') as xinitrc: xinitrc_data = xinitrc.read() diff --git a/profiles/applications/budgie.py b/profiles/applications/budgie.py deleted file mode 100644 index ccec4e14..00000000 --- a/profiles/applications/budgie.py +++ /dev/null @@ -1,4 +0,0 @@ -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")
\ No newline at end of file diff --git a/profiles/applications/cinnamon.py b/profiles/applications/cinnamon.py deleted file mode 100644 index de29aa09..00000000 --- a/profiles/applications/cinnamon.py +++ /dev/null @@ -1,3 +0,0 @@ -import archinstall - -installation.add_additional_packages("cinnamon system-config-printer gnome-keyring gnome-terminal blueberry metacity lightdm lightdm-gtk-greeter")
\ No newline at end of file diff --git a/profiles/applications/cockpit.py b/profiles/applications/cockpit.py new file mode 100644 index 00000000..f1cea1d2 --- /dev/null +++ b/profiles/applications/cockpit.py @@ -0,0 +1,9 @@ +import archinstall + +# Define the package list in order for lib to source +# which packages will be installed by this profile +__packages__ = ["cockpit", "udisks2", "packagekit"] + +installation.add_additional_packages(__packages__) + +installation.enable_service('cockpit.socket') diff --git a/profiles/applications/docker.py b/profiles/applications/docker.py new file mode 100644 index 00000000..afa3f8fb --- /dev/null +++ b/profiles/applications/docker.py @@ -0,0 +1,9 @@ +import archinstall + +# Define the package list in order for lib to source +# which packages will be installed by this profile +__packages__ = ["docker"] + +installation.add_additional_packages(__packages__) + +installation.enable_service('docker') diff --git a/profiles/applications/gnome.py b/profiles/applications/gnome.py deleted file mode 100644 index e26290dc..00000000 --- a/profiles/applications/gnome.py +++ /dev/null @@ -1,4 +0,0 @@ -import archinstall - -installation.add_additional_packages("gnome gnome-tweaks gdm") -# Note: gdm should be part of the gnome group, but adding it here for clarity diff --git a/profiles/applications/httpd.py b/profiles/applications/httpd.py new file mode 100644 index 00000000..00d64b6e --- /dev/null +++ b/profiles/applications/httpd.py @@ -0,0 +1,9 @@ +import archinstall + +# Define the package list in order for lib to source +# which packages will be installed by this profile +__packages__ = ["apache"] + +installation.add_additional_packages(__packages__) + +installation.enable_service('httpd') diff --git a/profiles/applications/i3-gaps.py b/profiles/applications/i3-gaps.py deleted file mode 100644 index 4daed7ad..00000000 --- a/profiles/applications/i3-gaps.py +++ /dev/null @@ -1,2 +0,0 @@ -import archinstall -installation.add_additional_packages("i3-gaps")
\ No newline at end of file diff --git a/profiles/applications/i3-wm.py b/profiles/applications/i3-wm.py deleted file mode 100644 index e7838a64..00000000 --- a/profiles/applications/i3-wm.py +++ /dev/null @@ -1,2 +0,0 @@ -import archinstall -installation.add_additional_packages("i3-wm")
\ No newline at end of file diff --git a/profiles/applications/kde.py b/profiles/applications/kde.py deleted file mode 100644 index af1e6597..00000000 --- a/profiles/applications/kde.py +++ /dev/null @@ -1,5 +0,0 @@ -import archinstall -packages = "plasma-meta konsole kate dolphin sddm plasma-wayland-session" -if "nvidia" in _gfx_driver_packages: - packages = packages + " egl-wayland" -installation.add_additional_packages(packages) diff --git a/profiles/applications/lighttpd.py b/profiles/applications/lighttpd.py new file mode 100644 index 00000000..a1e6a371 --- /dev/null +++ b/profiles/applications/lighttpd.py @@ -0,0 +1,9 @@ +import archinstall + +# Define the package list in order for lib to source +# which packages will be installed by this profile +__packages__ = ["lighttpd"] + +installation.add_additional_packages(__packages__) + +installation.enable_service('lighttpd') diff --git a/profiles/applications/lxqt.py b/profiles/applications/lxqt.py deleted file mode 100644 index 5ce875cc..00000000 --- a/profiles/applications/lxqt.py +++ /dev/null @@ -1,3 +0,0 @@ -import archinstall - -installation.add_additional_packages("lxqt breeze-icons oxygen-icons xdg-utils ttf-freefont leafpad slock archlinux-wallpaper sddm") diff --git a/profiles/applications/mariadb.py b/profiles/applications/mariadb.py new file mode 100644 index 00000000..e458a45a --- /dev/null +++ b/profiles/applications/mariadb.py @@ -0,0 +1,11 @@ +import archinstall + +# Define the package list in order for lib to source +# which packages will be installed by this profile +__packages__ = ["mariadb"] + +installation.add_additional_packages(__packages__) + +installation.arch_chroot("mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql") + +installation.enable_service('mariadb') diff --git a/profiles/applications/mate.py b/profiles/applications/mate.py deleted file mode 100644 index 24d6be47..00000000 --- a/profiles/applications/mate.py +++ /dev/null @@ -1,3 +0,0 @@ -import archinstall - -installation.add_additional_packages("mate mate-extra lightdm lightdm-gtk-greeter")
\ No newline at end of file diff --git a/profiles/applications/nginx.py b/profiles/applications/nginx.py new file mode 100644 index 00000000..50eb0506 --- /dev/null +++ b/profiles/applications/nginx.py @@ -0,0 +1,9 @@ +import archinstall + +# Define the package list in order for lib to source +# which packages will be installed by this profile +__packages__ = ["nginx"] + +installation.add_additional_packages(__packages__) + +installation.enable_service('nginx') diff --git a/profiles/applications/postgresql.py b/profiles/applications/postgresql.py new file mode 100644 index 00000000..fcdce824 --- /dev/null +++ b/profiles/applications/postgresql.py @@ -0,0 +1,11 @@ +import archinstall + +# Define the package list in order for lib to source +# which packages will be installed by this profile +__packages__ = ["postgresql"] + +installation.add_additional_packages(__packages__) + +installation.arch_chroot("initdb -D /var/lib/postgres/data", runas='postgres') + +installation.enable_service('postgresql')
\ No newline at end of file diff --git a/profiles/applications/sshd.py b/profiles/applications/sshd.py new file mode 100644 index 00000000..234638d5 --- /dev/null +++ b/profiles/applications/sshd.py @@ -0,0 +1,9 @@ +import archinstall + +# Define the package list in order for lib to source +# which packages will be installed by this profile +__packages__ = ["openssh"] + +installation.add_additional_packages(__packages__) + +installation.enable_service('sshd') diff --git a/profiles/applications/sway.py b/profiles/applications/sway.py deleted file mode 100644 index 56d7f318..00000000 --- a/profiles/applications/sway.py +++ /dev/null @@ -1,3 +0,0 @@ -import archinstall -packages = "sway swaylock swayidle waybar dmenu light grim slurp pavucontrol alacritty" -installation.add_additional_packages(packages) diff --git a/profiles/applications/tomcat.py b/profiles/applications/tomcat.py new file mode 100644 index 00000000..9c521390 --- /dev/null +++ b/profiles/applications/tomcat.py @@ -0,0 +1,12 @@ +import archinstall + +# This is using Tomcat 10 as that is the latest release at the time of implementation. +# This should probably be updated to use newer releases as they come out. + +# Define the package list in order for lib to source +# which packages will be installed by this profile +__packages__ = ["tomcat10"] + +installation.add_additional_packages(__packages__) + +installation.enable_service('tomcat10') diff --git a/profiles/applications/xfce4.py b/profiles/applications/xfce4.py deleted file mode 100644 index e8f659c2..00000000 --- a/profiles/applications/xfce4.py +++ /dev/null @@ -1,3 +0,0 @@ -import archinstall - -installation.add_additional_packages("xfce4 xfce4-goodies lightdm lightdm-gtk-greeter")
\ No newline at end of file diff --git a/profiles/awesome.py b/profiles/awesome.py index cbd52a3c..a5dedccd 100644 --- a/profiles/awesome.py +++ b/profiles/awesome.py @@ -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__ = ['nemo', 'gpicview-gtk3', 'scrot'] +__packages__ = ['nemo', 'gpicview-gtk3', 'main', 'alacritty'] def _prep_function(*args, **kwargs): """ @@ -35,9 +35,6 @@ if __name__ == 'awesome': installation.add_additional_packages(__packages__) - alacritty = archinstall.Application(installation, 'alacritty') - alacritty.install() - # TODO: Copy a full configuration to ~/.config/awesome/rc.lua instead. with open(f'{installation.target}/etc/xdg/awesome/rc.lua', 'r') as fh: awesome_lua = fh.read() diff --git a/profiles/budgie.py b/profiles/budgie.py index 6c5475ae..fc061cd2 100644 --- a/profiles/budgie.py +++ b/profiles/budgie.py @@ -4,6 +4,9 @@ import archinstall is_top_level_profile = False +# "It is recommended also to install the gnome group, which contains applications required for the standard GNOME experience." - Arch Wiki +__packages__ = ["budgie-desktop", "lightdm", "lightdm-gtk-greeter", "gnome"] + def _prep_function(*args, **kwargs): """ Magic function called by the importing installer @@ -27,8 +30,7 @@ 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() + # Install the Budgie packages + installation.add_additional_packages(__packages__) installation.enable_service('lightdm') # Light Display Manager diff --git a/profiles/cinnamon.py b/profiles/cinnamon.py index 91a59811..4ca9cfed 100644 --- a/profiles/cinnamon.py +++ b/profiles/cinnamon.py @@ -4,6 +4,8 @@ import archinstall is_top_level_profile = False +__packages__ = ["cinnamon", "system-config-printer", "gnome-keyring", "gnome-terminal", "blueberry", "metacity", "lightdm", "lightdm-gtk-greeter"] + def _prep_function(*args, **kwargs): """ Magic function called by the importing installer @@ -27,8 +29,7 @@ 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() + # Install the Cinnamon packages + installation.add_additional_packages(__packages__) installation.enable_service('lightdm') # Light Display Manager diff --git a/profiles/deepin.py b/profiles/deepin.py new file mode 100644 index 00000000..ce59a699 --- /dev/null +++ b/profiles/deepin.py @@ -0,0 +1,37 @@ +# A desktop environment using "Deepin". + +import archinstall, os + +is_top_level_profile = False + +__packages__ = ["deepin", "deepin-terminal", "deepin-editor"] + +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. + """ + + # Deepin 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') + + +# Ensures that this code only gets executed if executed +# through importlib.util.spec_from_file_location("deepin", "/somewhere/deepin.py") +# or through conventional import deepin +if __name__ == 'deepin': + # Install dependency profiles + installation.install_profile('xorg') + + # Install the Deepin packages + installation.add_additional_packages(__packages__) + + # Enable autostart of Deepin for all users + installation.enable_service('lightdm') diff --git a/profiles/desktop.py b/profiles/desktop.py index 846182e2..2aea6d30 100644 --- a/profiles/desktop.py +++ b/profiles/desktop.py @@ -17,7 +17,8 @@ def _prep_function(*args, **kwargs): """ supported_desktops = ['gnome', 'kde', 'awesome', 'sway', 'cinnamon', 'xfce4', 'lxqt', 'i3', 'budgie', 'mate'] - desktop = archinstall.generic_select(supported_desktops, 'Select your desired desktop environment: ') + desktop = archinstall.generic_select(supported_desktops, 'Select your desired desktop environment: ', + allow_empty_input=False, sort=True) # Temporarily store the selected desktop profile # in a session-safe location, since this module will get reloaded diff --git a/profiles/gnome.py b/profiles/gnome.py index c75cafee..77c90859 100644 --- a/profiles/gnome.py +++ b/profiles/gnome.py @@ -4,6 +4,9 @@ import archinstall is_top_level_profile = False +# Note: GDM should be part of the gnome group, but adding it here for clarity +__packages__ = ["gnome", "gnome-tweaks", "gdm"] + def _prep_function(*args, **kwargs): """ Magic function called by the importing installer @@ -28,9 +31,8 @@ if __name__ == 'gnome': # Install dependency profiles installation.install_profile('xorg') - # Install the application gnome from the template under /applications/ - gnome = archinstall.Application(installation, 'gnome') - gnome.install() + # Install the GNOME packages + installation.add_additional_packages(__packages__) installation.enable_service('gdm') # Gnome Display Manager # We could also start it via xinitrc since we do have Xorg, diff --git a/profiles/i3.py b/profiles/i3.py index 67028b2d..e2db5fc7 100644 --- a/profiles/i3.py +++ b/profiles/i3.py @@ -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__ = ['i3lock', 'i3status', 'i3blocks', 'xterm'] +__packages__ = ['i3lock', 'i3status', 'i3blocks', 'xterm', 'lightdm-gtk-greeter', 'lightdm'] def _prep_function(*args, **kwargs): """ @@ -17,7 +17,8 @@ def _prep_function(*args, **kwargs): """ supported_configurations = ['i3-wm', 'i3-gaps'] - desktop = archinstall.generic_select(supported_configurations, 'Select your desired configuration: ') + desktop = archinstall.generic_select(supported_configurations, 'Select your desired configuration: ', + allow_empty_input=False, sort=True) # Temporarily store the selected desktop profile # in a session-safe location, since this module will get reloaded @@ -47,17 +48,16 @@ if __name__ == 'i3': """ # Install common packages for all i3 configurations - installation.add_additional_packages(__packages__) + installation.add_additional_packages(__packages__[:4]) # 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") + # gaps is installed by deafult so we are overriding it here with lightdm + installation.add_additional_packages(__packages__[4:]) # 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() + installation.add_additional_packages(archinstall.storage['_i3_configuration']) diff --git a/profiles/kde.py b/profiles/kde.py index 6654dfa7..c8efdcde 100644 --- a/profiles/kde.py +++ b/profiles/kde.py @@ -4,6 +4,8 @@ import archinstall, os is_top_level_profile = False +__packages__ = ["plasma-meta", "konsole", "kate", "dolphin", "sddm", "plasma-wayland-session", "egl-wayland"] + # TODO: Remove hard dependency of bash (due to .bash_profile) def _prep_function(*args, **kwargs): @@ -37,9 +39,8 @@ if __name__ == 'kde': # Install dependency profiles installation.install_profile('xorg') - # Install the application kde from the template under /applications/ - kde = archinstall.Application(installation, 'kde') - kde.install() + # Install the KDE packages + installation.add_additional_packages(__packages__) # Enable autostart of KDE for all users installation.enable_service('sddm') diff --git a/profiles/lxqt.py b/profiles/lxqt.py index 871488ee..d0727a90 100644 --- a/profiles/lxqt.py +++ b/profiles/lxqt.py @@ -5,6 +5,8 @@ import archinstall is_top_level_profile = False +__packages__ = ["lxqt", "breeze-icons", "oxygen-icons", "xdg-utils", "ttf-freefont", "leafpad", "slock", "sddm"] + def _prep_function(*args, **kwargs): """ Magic function called by the importing installer @@ -28,8 +30,7 @@ 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() + # Install the LXQt packages + installation.add_additional_packages(__packages__) installation.enable_service('sddm') # SDDM Display Manager diff --git a/profiles/mate.py b/profiles/mate.py index b4c697b1..2cfe7305 100644 --- a/profiles/mate.py +++ b/profiles/mate.py @@ -4,6 +4,8 @@ import archinstall is_top_level_profile = False +__packages__ = ["mate", "mate-extra", "lightdm", "lightdm-gtk-greeter"] + def _prep_function(*args, **kwargs): """ Magic function called by the importing installer @@ -27,8 +29,7 @@ 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() + # Install the MATE packages + installation.add_additional_packages(__packages__) installation.enable_service('lightdm') # Light Display Manager diff --git a/profiles/server.py b/profiles/server.py new file mode 100644 index 00000000..9d28054d --- /dev/null +++ b/profiles/server.py @@ -0,0 +1,30 @@ +# Used to select various server application profiles on top of a minimal installation. + +import archinstall, os, logging + +is_top_level_profile = True + +available_servers = ["cockpit", "docker", "httpd", "lighttpd", "mariadb", "nginx", "postgresql", "sshd", "tomcat"] + +def _prep_function(*args, **kwargs): + """ + Magic function called by the importing installer + before continuing any further. + """ + selected_servers = archinstall.generic_multi_select(available_servers, f"Choose which servers to install and enable (leave blank for a minimal installation): ") + archinstall.storage['_selected_servers'] = selected_servers + + return True + +if __name__ == 'server': + """ + This "profile" is a meta-profile. + """ + archinstall.log(f'Now installing the selected servers.', level=logging.INFO) + archinstall.log(archinstall.storage['_selected_servers'], level=logging.DEBUG) + for server in archinstall.storage['_selected_servers']: + archinstall.log(f'Installing {server} ...', level=logging.INFO) + app = archinstall.Application(installation, server) + app.install() + + archinstall.log('If your selections included multiple servers with the same port, you may have to reconfigure them.', fg="yellow", level=logging.INFO) diff --git a/profiles/sway.py b/profiles/sway.py index 5633cce2..db94ae2c 100644 --- a/profiles/sway.py +++ b/profiles/sway.py @@ -4,6 +4,8 @@ import archinstall is_top_level_profile = False +__packages__ = ["sway", "swaylock", "swayidle", "waybar", "dmenu", "light", "grim", "slurp", "pavucontrol", "alacritty"] + def _prep_function(*args, **kwargs): """ Magic function called by the importing installer @@ -11,12 +13,18 @@ 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. """ + if "nvidia" in _gfx_driver_packages: + choice = input("The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues. Continue anyways? [y/N] ") + if choice.lower() in ("n", ""): + raise archinstall.lib.exceptions.HardwareIncompatibilityError("Sway does not support the proprietary nvidia drivers.") + + __builtins__['_gfx_driver_packages'] = archinstall.select_driver() + return True # Ensures that this code only gets executed if executed # through importlib.util.spec_from_file_location("sway", "/somewhere/sway.py") # or through conventional import sway if __name__ == 'sway': - # Install the application sway from the template under /applications/ - sway = archinstall.Application(installation, 'sway') - sway.install() + # Install the Sway packages + installation.add_additional_packages(__packages__) diff --git a/profiles/xfce4.py b/profiles/xfce4.py index fee8c37a..8102919b 100644 --- a/profiles/xfce4.py +++ b/profiles/xfce4.py @@ -5,6 +5,8 @@ import archinstall is_top_level_profile = False +__packages__ = ["xfce4", "xfce4-goodies", "lightdm", "lightdm-gtk-greeter"] + def _prep_function(*args, **kwargs): """ Magic function called by the importing installer @@ -28,8 +30,7 @@ if __name__ == 'xfce4': # Install dependency profiles installation.install_profile('xorg') - # Install the application xfce4 from the template under /applications/ - xfce = archinstall.Application(installation, 'xfce4') - xfce.install() + # Install the XFCE4 packages + installation.add_additional_packages(__packages__) installation.enable_service('lightdm') # Light Display Manager diff --git a/profiles/xorg.py b/profiles/xorg.py index e905d533..7546a01b 100644 --- a/profiles/xorg.py +++ b/profiles/xorg.py @@ -1,91 +1,11 @@ # A system with "xorg" installed -import archinstall, os +import os +import archinstall is_top_level_profile = True -AVAILABLE_DRIVERS = { - # Sub-dicts are layer-2 options to be selected - # and lists are a list of packages to be installed - 'AMD / ATI' : { - 'amd' : ['xf86-video-amdgpu'], - 'ati' : ['xf86-video-ati'] - }, - 'intel' : ['xf86-video-intel'], - 'nvidia' : { - 'open source' : ['xf86-video-nouveau'], - 'proprietary' : ['nvidia'] - }, - 'mesa' : ['mesa'], - 'fbdev' : ['xf86-video-fbdev'], - 'vesa' : ['xf86-video-vesa'], - 'vmware' : ['xf86-video-vmware'] -} - -def select_driver(options): - """ - Some what convoluted function, which's job is simple. - Select a graphics driver from a pre-defined set of popular options. - - (The template xorg is for beginner users, not advanced, and should - there for appeal to the general public first and edge cases later) - """ - drivers = sorted(list(options)) - - if len(drivers) >= 1: - for index, driver in enumerate(drivers): - print(f"{index}: {driver}") - - print(' -- The above list are supported graphic card drivers. --') - print(' -- You need to select (and read about) which one you need. --') - - lspci = archinstall.sys_command(f'/usr/bin/lspci') - for line in lspci.trace_log.split(b'\r\n'): - if b' vga ' in line.lower(): - if b'nvidia' in line.lower(): - print(' ** nvidia card detected, suggested driver: nvidia **') - elif b'amd' in line.lower(): - print(' ** AMD card detected, suggested driver: AMD / ATI **') - - selected_driver = input('Select your graphics card driver: ') - initial_option = selected_driver - - # Disabled search for now, only a few profiles exist anyway - # - #print(' -- You can enter ? or help to search for more drivers --') - #if selected_driver.lower() in ('?', 'help'): - # filter_string = input('Search for layout containing (example: "sv-"): ') - # new_options = search_keyboard_layout(filter_string) - # return select_language(new_options) - if selected_driver.isdigit() and (pos := int(selected_driver)) <= len(drivers)-1: - selected_driver = options[drivers[pos]] - elif selected_driver in options: - selected_driver = options[options.index(selected_driver)] - elif len(selected_driver) == 0: - raise archinstall.RequirementError("At least one graphics driver is needed to support a graphical environment. Please restart the installer and try again.") - else: - raise archinstall.RequirementError("Selected driver does not exist.") - - if type(selected_driver) == dict: - driver_options = sorted(list(selected_driver)) - for index, driver_package_group in enumerate(driver_options): - print(f"{index}: {driver_package_group}") - - selected_driver_package_group = input(f'Which driver-type do you want for {initial_option}: ') - if selected_driver_package_group.isdigit() and (pos := int(selected_driver_package_group)) <= len(driver_options)-1: - selected_driver_package_group = selected_driver[driver_options[pos]] - elif selected_driver_package_group in selected_driver: - selected_driver_package_group = selected_driver[selected_driver.index(selected_driver_package_group)] - elif len(selected_driver_package_group) == 0: - raise archinstall.RequirementError(f"At least one driver package is required for a graphical environment using {selected_driver}. Please restart the installer and try again.") - else: - raise archinstall.RequirementError(f"Selected driver-type does not exist for {initial_option}.") - - return selected_driver_package_group - - return selected_driver - - raise archinstall.RequirementError("Selecting drivers require a least one profile to be given as an option.") +__packages__ = ['dkms', 'xorg-server', 'xorg-xinit', 'nvidia-dkms', 'xorg-server', *archinstall.lib.hardware.__packages__] def _prep_function(*args, **kwargs): """ @@ -94,10 +14,8 @@ 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. """ - print('You need to select which graphics card you\'re using.') - print('This in order to setup the required graphics drivers.') - __builtins__['_gfx_driver_packages'] = select_driver(AVAILABLE_DRIVERS) + __builtins__['_gfx_driver_packages'] = archinstall.select_driver() # TODO: Add language section and/or merge it with the locale selected # earlier in for instance guided.py installer. @@ -109,7 +27,14 @@ def _prep_function(*args, **kwargs): # or through conventional import xorg if __name__ == 'xorg': try: - installation.add_additional_packages(f"xorg-server xorg-xinit {' '.join(_gfx_driver_packages)}") + if "nvidia" in _gfx_driver_packages: + if "linux-zen" in installation.base_packages or "linux-lts" in installation.base_packages: + installation.add_additional_packages("dkms")#I've had kernel regen fail if it wasn't installed before nvidia-dkms + installation.add_additional_packages("xorg-server xorg-xinit nvidia-dkms") + else: + installation.add_additional_packages(f"xorg-server xorg-xinit {' '.join(_gfx_driver_packages)}") + else: + installation.add_additional_packages(f"xorg-server xorg-xinit {' '.join(_gfx_driver_packages)}") except: installation.add_additional_packages(f"xorg-server xorg-xinit") # Prep didn't run, so there's no driver to install diff --git a/pyproject.toml b/pyproject.toml index 9787c3bd..73c7a876 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,30 @@ [build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["flit_core >=2,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.metadata] +module = "archinstall" +author = "Anton Hvornum" +author-email = "anton@hvornum.se" +home-page = "https://archlinux.org" +classifiers = [ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", +"Programming Language :: Python :: 3.8", +"Programming Language :: Python :: 3.9", +"License :: OSI Approved :: GNU General Public License v3 (GPLv3)", +"Operating System :: POSIX :: Linux", +] +description-file = "README.md" +requires-python=">=3.8" +[tool.flit.metadata.urls] +Source = "https://github.com/archlinux/archinstall" +Documentation = "https://archinstall.readthedocs.io/" + +[tool.flit.scripts] +archinstall = "archinstall:run_as_a_module" + +[tool.flit.sdist] +include = ["docs/","profiles"] +exclude = ["docs/*.html", "docs/_static","docs/*.png","docs/*.psd"] + +[tool.flit.metadata.requires-extra] +doc = ["sphinx"]
\ No newline at end of file |