Merge branch 'torxed-v2.2.0' into torxed-v2.2.0

This commit is contained in:
Anton Hvornum 2021-04-20 12:57:14 +00:00 committed by GitHub
commit f5ef751d53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 1311 additions and 740 deletions

37
.github/workflows/iso-build.yaml vendored Normal file
View File

@ -0,0 +1,37 @@
# This workflow will build an Arch Linux ISO file with the commit on it
name: Build Arch ISO with ArchInstall Commit
on:
pull_request:
paths-ignore:
- '.github/**'
- 'docs/**'
- '**.editorconfig'
- '**.gitignore'
- '**.md'
- 'LICENSE'
- 'PKGBUILD'
jobs:
build:
runs-on: ubuntu-latest
container:
image: archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@v2
- run: pwd
- 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: 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
- run: find /tmp/archlive
- run: cd /tmp/archlive; mkarchiso -v -w work/ -o out/ ./
- uses: actions/upload-artifact@v2
with:
name: Arch Live ISO
path: /tmp/archlive/out/*.iso

View File

@ -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

6
.pypirc Normal file
View File

@ -0,0 +1,6 @@
[distutils]
index-servers =
pypi
[pypi]
repository = https://upload.pypi.org/legacy/

View File

@ -6,7 +6,7 @@ Therefore guidelines and style changes to the code might come into affect as wel
## Discussions
Currently, questions, bugs and suggestions should be reported through [GitHub issue tracker](https://github.com/Torxed/archinstall/issues).<br>
Currently, questions, bugs and suggestions should be reported through [GitHub issue tracker](https://github.com/archlinux/archinstall/issues).<br>
For less formal discussions there are also a [archinstall Discord server](https://discord.gg/cqXU88y).
## Coding convention

View File

@ -2,43 +2,24 @@
# Contributor: Giancarlo Razzolini <grazzolini@archlinux.org>
# Contributor: demostanis worlds <demostanis@protonmail.com>
pkgbase=archinstall-git
pkgname=('archinstall-git' 'python-archinstall-git')
pkgname=archinstall-git
pkgver=$(git describe --long | sed 's/\([^-]*-g\)/r\1/;s/-/./g')
pkgrel=1
pkgdesc="Just another guided/automated Arch Linux installer with a twist"
arch=('any')
url="https://github.com/Torxed/archinstall"
url="https://github.com/archlinux/archinstall"
license=('GPL')
depends=('python')
makedepends=('python-setuptools')
provides=('python-archinstall')
conflicts=('archinstall' 'python-archinstall' 'python-archinstall-git')
build() {
cd "$startdir"
cd "$startdir"
python setup.py build
}
package_archinstall-git() {
depends=('python-archinstall-git')
conflicts=('archinstall')
cd "$startdir"
mkdir -p "${pkgdir}/usr/bin"
# Install a guided profile
cat - > "${pkgdir}/usr/bin/archinstall" <<EOF
#!/bin/sh
python -m archinstall $@
EOF
chmod +x "${pkgdir}/usr/bin/archinstall"
}
package_python-archinstall-git() {
conflicts=('python-archinstall')
cd "$startdir"
python setup.py install --prefix=/usr --root="${pkgdir}" --optimize=1 --skip-build
package() {
cd "$startdir"
python setup.py install --root="${pkgdir}" --optimize=1 --skip-build
}

View File

@ -1,39 +0,0 @@
# Maintainer: Anton Hvornum anton@hvornum.se
# Contributor: Anton Hvornum anton@hvornum.se
pkgname="archinstall-bin"
pkgver="2.1.3"
pkgdesc="Installs a pre-built binary of ${pkgname}"
pkgrel=1
url="https://github.com/Torxed/archinstall"
license=('GPLv3')
provides=("${pkgname}")
arch=('x86_64')
source=("${pkgname}-v${pkgver}-x86_64.tar.gz::https://github.com/Torxed/archinstall/archive/v$pkgver.tar.gz")
#depends=('python>=3.8')
makedepends=('python>=3.8' 'nuitka')
optdepends=('pyttsx3: Adds text-to-speach support for log/screen output.')
sha256sums=('53c00f7e7ad245cd2cbbf041b5a735df2fc29454c24b1d369f678cc0610b7cea')
build() {
cd "${pkgname}-${pkgver}"
nuitka3 --standalone --show-progress archinstall
cp -r examples/ archinstall.dist/
}
package() {
echo "${srcdir}"
cd "${pkgname}-${pkgver}"
mkdir -p "${pkgdir}/var/lib/archinstall/"
mkdir -p "${pkgdir}/usr/bin"
mv archinstall.dist/* "${pkgdir}/var/lib/archinstall/"
echo '#!/bin/bash' > "${pkgdir}/usr/bin/archinstall-bin"
echo '(cd /var/lib/archinstall && exec ./archinstall)' >> "${pkgdir}/usr/bin/archinstall-bin"
chmod +x "${pkgdir}/var/lib/archinstall/archinstall"
chmod +x "${pkgdir}/usr/bin/archinstall-bin"
}

View File

@ -1,30 +0,0 @@
# Maintainer: Anton Hvornum <anton@hvornum.se>
# Contributor: demostanis worlds <demostanis@protonmail.com>
pkgname="archinstall"
pkgver="2.1.3"
pkgdesc="Installs launcher scripts for archinstall"
pkgrel=1
url="https://github.com/Torxed/archinstall"
license=('GPLv3')
provides=("${pkgname}")
arch=('x86_64')
source=("${pkgname}-v${pkgver}-x86_64.tar.gz::https://github.com/Torxed/archinstall/archive/v$pkgver.tar.gz")
depends=('python-archinstall')
sha256sums=('53c00f7e7ad245cd2cbbf041b5a735df2fc29454c24b1d369f678cc0610b7cea')
package() {
mkdir -p "${pkgdir}/usr/bin"
# Install a guided profile
cat - > "${pkgdir}/usr/bin/archinstall" <<EOF
#!/bin/sh
python -m archinstall $@
EOF
chmod +x "${pkgdir}/usr/bin/archinstall"
}
# vim:ft=sh

View File

@ -1,40 +0,0 @@
# Maintainer: Anton Hvornum <anton@hvornum.se>
# Contributor: demostanis worlds <demostanis@protonmail.com>
pkgname="python-archinstall"
pkgver="2.1.3"
pkgdesc="Installs ${pkgname} as a python library."
pkgrel=1
url="https://github.com/Torxed/archinstall"
source=("${pkgname}-v${pkgver}-x86_64.tar.gz::https://github.com/Torxed/archinstall/archive/v$pkgver.tar.gz")
license=('GPLv3')
provides=("${pkgname}")
arch=('x86_64')
depends=('python>=3.8')
makedepends=('python-setuptools')
optdepends=('pyttsx3: Adds text-to-speech support for log/screen output.')
sha256sums=('53c00f7e7ad245cd2cbbf041b5a735df2fc29454c24b1d369f678cc0610b7cea')
build() {
cd "archinstall-${pkgver}"
python setup.py build
# Build man pages
cd docs
make man
}
package() {
cd "archinstall-${pkgver}"
python setup.py install \
--prefix=/usr \
--root="${pkgdir}" \
--optimize=1
install -Dm644 docs/_build/man/archinstall.1 "${pkgdir}"/usr/share/man/man1/archinstall.1
}
# vim:ft=sh

View File

@ -1,4 +1,9 @@
# <img src="https://github.com/Torxed/archinstall/raw/master/docs/logo.png" alt="drawing" width="200"/>
<!-- <div align="center"> -->
<img src="https://github.com/archlinux/archinstall/raw/master/docs/logo.png" alt="drawing" width="200"/>
# Arch Installer
<!-- </div> -->
Just another guided/automated [Arch Linux](https://wiki.archlinux.org/index.php/Arch_Linux) installer with a twist.
The installer also doubles as a python library to install Arch Linux and manage services, packages and other things inside the installed system *(Usually from a live medium)*.
@ -21,6 +26,18 @@ Assuming you are on a Arch Linux live-ISO and booted into EFI mode.
# python -m archinstall guided
# Mission Statement
Archinstall promises to ship a [guided installer](https://github.com/archlinux/archinstall/blob/master/examples/guided.py) that follows the [Arch Principles](https://wiki.archlinux.org/index.php/Arch_Linux#Principles) as well as a library to manage services, packages and other Arch Linux aspects.
The guided installer will provide user friendly options along the way, but the keyword here is options, they are optional and will never be forced upon anyone. The guided installer itself is also optional to use if so desired and not forced upon anyone.
---
Archinstall has one fundamental function which is to be a flexible library to manage services, packages and other aspects inside the installed system. This library is in turn used by the provided guided installer but is also for anyone who wants to script their own installations.
Therefore, Archinstall will try its best to not introduce any breaking changes except for major releases which may break backwards compability after notifying about such changes.
# Scripting your own installation
You could just copy [guided.py](examples/guided.py) as a starting point.
@ -35,23 +52,44 @@ import archinstall, getpass
harddrive = archinstall.select_disk(archinstall.all_disks())
disk_password = getpass.getpass(prompt='Disk password (won\'t echo): ')
# We disable safety precautions in the library that protects the partitions
harddrive.keep_partitions = False
# First, we configure the basic filesystem layout
with archinstall.Filesystem(harddrive, archinstall.GPT) as fs:
# use_entire_disk() is a helper to not have to format manually
fs.use_entire_disk('luks2')
# We create a filesystem layout that will use the entire drive
# (this is a helper function, you can partition manually as well)
fs.use_entire_disk(root_filesystem_type='btrfs')
harddrive.partition[0].format('fat32')
with archinstall.luks2(harddrive.partition[1], 'luksloop', disk_password) as unlocked_device:
unlocked_device.format('btrfs')
boot = fs.find_partition('/boot')
root = fs.find_partition('/')
boot.format('vfat')
# Set the flag for encrypted to allow for encryption and then encrypt
root.encrypted = True
root.encrypt(password=disk_password)
with archinstall.luks2(root, 'luksloop', disk_password) as unlocked_root:
unlocked_root.format(root.filesystem)
unlocked_root.mount('/mnt')
boot.mount('/mnt/boot')
with archinstall.Installer('/mnt') as installation:
if installation.minimal_installation():
installation.set_hostname('minimal-arch')
installation.add_bootloader()
installation.add_additional_packages(['nano', 'wget', 'git'])
# Optionally, install a profile of choice.
# In this case, we install a minimal profile that is empty
installation.install_profile('minimal')
installation.user_create('devel', 'devel')
installation.user_set_pw('root', 'airoot')
with archinstall.Installer(unlocked_device, hostname='testmachine') as installation:
if installation.minimal_installation():
installation.add_bootloader(harddrive.partition[0])
installation.add_additional_packages(['nano', 'wget', 'git'])
installation.install_profile('awesome')
installation.user_create('anton', 'test')
installation.user_set_pw('root', 'toor')
```
This installer will perform the following:
@ -62,7 +100,7 @@ This installer will perform the following:
* Installs a basic instance of Arch Linux *(base base-devel linux linux-firmware btrfs-progs efibootmgr)*
* 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 network-profile called [awesome](https://github.com/Torxed/archinstall/blob/master/profiles/awesome.py) *(more on network profiles in the documentation)*
* 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.
@ -73,6 +111,23 @@ When doing so, attach any `install-session_*.log` to the issue ticket which can
# Testing
## Using a Live ISO Image
If you want to test a commit, branch or bleeding edge release from the repository using the vanilla Arch Live ISO image, you can replace the version of archinstall with a new version and run that with the steps described below.
1. You need a working network connection
2. Install the build requirements with `pacman -Sy; pacman -S git python-pip`
*(note that this may or may not work depending on your RAM and current state of the squashfs maximum filesystem free space)*
3. Uninstall the previous version of archinstall with `pip uninstall archinstall`
4. Now clone the latest repository with `git clone https://github.com/archlinux/archinstall`
5. Enter the repository with `cd archinstall`
*At this stage, you can choose to check out a feature branch for instance with `git checkout torxed-v2.2.0`*
6. Build the project and install it using `python setup.py install`
After this, running archinstall with `python -m archinstall` will run against whatever branch you chose in step 5.
## Without a Live ISO Image
To test this without a live ISO, the simplest approach is to use a local image and create a loop device.<br>
This can be done by installing `pacman -S arch-install-scripts util-linux` locally and doing the following:
@ -87,5 +142,5 @@ This will create a *5GB* `testimage.img` and create a loop device which we can u
`archinstall` is installed and executed in [guided mode](#docs-todo). Once the installation is complete,<br>
~~you can use qemu/kvm to boot the test media.~~ *(You'd actually need to do some EFI magic in order to point the EFI vars to the partition 0 in the test medium so this won't work entirely out of the box, but gives you a general idea of what we're going for here)*
There's also a [Building and Testing](https://github.com/Torxed/archinstall/wiki/Building-and-Testing) guide.<br>
There's also a [Building and Testing](https://github.com/archlinux/archinstall/wiki/Building-and-Testing) guide.<br>
It will go through everything from packaging, building and running *(with qemu)* the installer against a dev branch.

View File

@ -1 +0,0 @@
2.1.3

View File

@ -1,3 +0,0 @@
# This __init__ file is just here to support the
# use of archinstall as a git submodule.
from .archinstall import *

View File

@ -1,3 +1,4 @@
"""Arch Linux installer - guided, templates etc."""
from .lib.general import *
from .lib.disk import *
from .lib.user_interaction import *
@ -14,6 +15,8 @@ from .lib.output import *
from .lib.storage import *
from .lib.hardware import *
__version__ = "2.2.0"
## Basic version of arg.parse() supporting:
## --key=value
## --boolean
@ -27,4 +30,33 @@ for arg in sys.argv[1:]:
key, val = arg[2:], True
arguments[key] = val
else:
positionals.append(arg)
positionals.append(arg)
# TODO: Learn the dark arts of argparse...
# (I summon thee dark spawn of cPython)
def run_as_a_module():
"""
Since we're running this as a 'python -m archinstall' module OR
a nuitka3 compiled version of the project.
This function and the file __main__ acts as a entry point.
"""
# Add another path for finding profiles, so that list_profiles() in Script() can find guided.py, unattended.py etc.
storage['PROFILE_PATH'].append(os.path.abspath(f'{os.path.dirname(__file__)}/examples'))
if len(sys.argv) == 1:
sys.argv.append('guided')
try:
script = Script(sys.argv[1])
except ProfileNotFound as err:
print(f"Couldn't find file: {err}")
sys.exit(1)
os.chdir(os.path.abspath(os.path.dirname(__file__)))
# Remove the example directory from the PROFILE_PATH, to avoid guided.py etc shows up in user input questions.
storage['PROFILE_PATH'].pop()
script.execute()

View File

@ -2,33 +2,5 @@ import archinstall
import sys
import os
# TODO: Learn the dark arts of argparse...
# (I summon thee dark spawn of cPython)
def run_as_a_module():
"""
Since we're running this as a 'python -m archinstall' module OR
a nuitka3 compiled version of the project.
This function and the file __main__ acts as a entry point.
"""
# Add another path for finding profiles, so that list_profiles() in Script() can find guided.py, unattended.py etc.
archinstall.storage['PROFILE_PATH'].append(os.path.abspath(f'{os.path.dirname(__file__)}/examples'))
if len(sys.argv) == 1:
sys.argv.append('guided')
try:
script = archinstall.Script(sys.argv[1])
except archinstall.ProfileNotFound as err:
print(f"Couldn't find file: {err}")
sys.exit(1)
os.chdir(os.path.abspath(os.path.dirname(__file__)))
# Remove the example directory from the PROFILE_PATH, to avoid guided.py etc shows up in user input questions.
archinstall.storage['PROFILE_PATH'].pop()
script.execute()
if __name__ == '__main__':
run_as_a_module()
archinstall.run_as_a_module()

View File

@ -25,11 +25,12 @@ class BlockDevice():
self.path = path
self.info = info
self.keep_partitions = True
self.part_cache = OrderedDict()
# TODO: Currently disk encryption is a BIT missleading.
# TODO: Currently disk encryption is a BIT misleading.
# It's actually partition-encryption, but for future-proofing this
# I'm placing the encryption password on a BlockDevice level.
self.encryption_passwoed = None
self.encryption_password = None
def __repr__(self, *args, **kwargs):
return f"BlockDevice({self.device})"
@ -126,6 +127,18 @@ class BlockDevice():
def partition_table_type(self):
return GPT
@property
def uuid(self):
log(f'BlockDevice().uuid is untested!', level=LOG_LEVELS.Warning, fg='yellow')
"""
Returns the disk UUID as returned by lsblk.
This is more reliable than relying on /dev/disk/by-partuuid as
it doesn't seam to be able to detect md raid partitions.
"""
lsblk = b''.join(sys_command(f'lsblk -J -o+UUID {self.path}'))
for partition in json.loads(lsblk.decode('UTF-8'))['blockdevices']:
return partition.get('uuid', None)
def has_partitions(self):
return len(self.partitions)
@ -166,7 +179,7 @@ class Partition():
self.mountpoint = target
if not self.filesystem and autodetect_filesystem:
if (fstype := mount_information.get('fstype', get_filesystem_type(self.real_device))):
if (fstype := mount_information.get('fstype', get_filesystem_type(path))):
self.filesystem = fstype
if self.filesystem == 'crypto_LUKS':
@ -187,9 +200,9 @@ class Partition():
mount_repr = f", rel_mountpoint={self.target_mountpoint}"
if self._encrypted:
return f'Partition(path={self.path}, real_device={self.real_device}, fs={self.filesystem}{mount_repr})'
return f'Partition(path={self.path}, size={self.size}, real_device={self.real_device}, fs={self.filesystem}{mount_repr})'
else:
return f'Partition(path={self.path}, fs={self.filesystem}{mount_repr})'
return f'Partition(path={self.path}, size={self.size}, fs={self.filesystem}{mount_repr})'
@property
def uuid(self) -> str:
@ -214,14 +227,15 @@ class Partition():
self._encrypted = value
@property
def parent(self):
return self.real_device
@property
def real_device(self):
if not self._encrypted:
return self.path
else:
for blockdevice in json.loads(b''.join(sys_command(['lsblk', '-J'])).decode('UTF-8'))['blockdevices']:
if (parent := self.find_parent_of(blockdevice, os.path.basename(self.path))):
return f"/dev/{parent}"
for blockdevice in json.loads(b''.join(sys_command('lsblk -J')).decode('UTF-8'))['blockdevices']:
if (parent := self.find_parent_of(blockdevice, os.path.basename(self.path))):
return f"/dev/{parent}"
# raise DiskError(f'Could not find appropriate parent for encrypted partition {self}')
return self.path
@ -285,10 +299,10 @@ class Partition():
handle = luks2(self, None, None)
return handle.encrypt(self, *args, **kwargs)
def format(self, filesystem=None, path=None, allow_formatting=None, log_formating=True):
def format(self, filesystem=None, path=None, allow_formatting=None, log_formatting=True):
"""
Format can be given an overriding path, for instance /dev/null to test
the formating functionality and in essence the support for the given filesystem.
the formatting functionality and in essence the support for the given filesystem.
"""
if filesystem is None:
filesystem = self.filesystem
@ -306,7 +320,7 @@ class Partition():
if not allow_formatting:
raise PermissionError(f"{self} is not formatable either because instance is locked ({self.allow_formatting}) or a blocking flag was given ({allow_formatting})")
if log_formating:
if log_formatting:
log(f'Formatting {path} -> {filesystem}', level=LOG_LEVELS.Info)
if filesystem == 'btrfs':
@ -366,14 +380,16 @@ class Partition():
if not fs:
if not self.filesystem: raise DiskError(f'Need to format (or define) the filesystem on {self} before mounting.')
fs = self.filesystem
## libc has some issues with loop devices, defaulting back to sys calls
# ret = libc.mount(self.path.encode(), target.encode(), fs.encode(), 0, options.encode())
# if ret < 0:
# errno = ctypes.get_errno()
# raise OSError(errno, f"Error mounting {self.path} ({fs}) on {target} with options '{options}': {os.strerror(errno)}")
if sys_command(f'/usr/bin/mount {self.path} {target}').exit_code == 0:
self.mountpoint = target
return True
pathlib.Path(target).mkdir(parents=True, exist_ok=True)
try:
sys_command(f'/usr/bin/mount {self.path} {target}')
except SysCallError as err:
raise err
self.mountpoint = target
return True
def unmount(self):
try:
@ -401,7 +417,7 @@ class Partition():
2. UnknownFilesystemFormat that indicates that we don't support the given filesystem type
"""
try:
self.format(self.filesystem, '/dev/null', log_formating=False, allow_formatting=True)
self.format(self.filesystem, '/dev/null', log_formatting=False, allow_formatting=True)
except SysCallError:
pass # We supported it, but /dev/null is not formatable as expected so the mkfs call exited with an error code
except UnknownFilesystemFormat as err:
@ -588,6 +604,24 @@ def get_mount_info(path):
return output['filesystems'][0]
def get_partitions_in_use(mountpoint):
try:
output = b''.join(sys_command(f'/usr/bin/findmnt --json -R {mountpoint}'))
except SysCallError:
return {}
mounts = []
output = output.decode('UTF-8')
output = json.loads(output)
for target in output.get('filesystems', []):
mounts.append(Partition(target['source'], None, filesystem=target.get('fstype', None), mountpoint=target['target']))
for child in target.get('children', []):
mounts.append(Partition(child['source'], None, filesystem=child.get('fstype', None), mountpoint=child['target']))
return mounts
def get_filesystem_type(path):
try:
handle = sys_command(f"blkid -o value -s TYPE {path}")

View File

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

View File

@ -76,7 +76,7 @@ class sys_command():#Thread):
"""
Stolen from archinstall_gui
"""
def __init__(self, cmd, callback=None, start_callback=None, peak_output=False, *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)
@ -102,6 +102,7 @@ class sys_command():#Thread):
self.args = args
self.kwargs = kwargs
self.peak_output = peak_output
self.environment_vars = environment_vars
self.kwargs.setdefault("worker", None)
self.callback = callback
@ -200,7 +201,7 @@ class sys_command():#Thread):
# Replace child process with our main process
if not self.kwargs['emulate']:
try:
os.execv(self.cmd[0], self.cmd)
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)
@ -304,6 +305,11 @@ class sys_command():#Thread):
with open(f'{self.cwd}/trace.log', 'wb') as fh:
fh.write(self.trace_log)
try:
os.close(child_fd)
except:
pass
def prerequisite_check():
if not os.path.isdir("/sys/firmware/efi"):

View File

@ -1,19 +1,42 @@
import os, subprocess
import os, subprocess, json
from .general import sys_command
from .networking import list_interfaces, enrichIfaceTypes
from typing import Optional
def hasWifi():
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' : ['xf86-video-vmware']
}
def hasWifi()->bool:
return 'WIRELESS' in enrichIfaceTypes(list_interfaces().values()).values()
def hasAMDCPU():
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():
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:
@ -21,13 +44,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 none 0 exit code if it is not on a virtual machine
return True
except:
return False
# TODO: Add more identifiers

View File

@ -9,6 +9,7 @@ from .mirrors import *
from .systemd import Networkd
from .output import log, LOG_LEVELS
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", "linux-firmware", "efibootmgr", "nano", "ntp", "iwd"]
@ -38,30 +39,21 @@ class Installer():
:type hostname: str, optional
"""
def __init__(self, partition, boot_partition, *, base_packages=__base_packages__, profile=None, mountpoint='/mnt', hostname='ArchInstalled', logdir=None, logfile=None):
self.profile = profile
self.hostname = hostname
self.mountpoint = mountpoint
def __init__(self, target, *, base_packages='base base-devel linux linux-firmware efibootmgr'):
self.target = target
self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S')
self.milliseconds = int(str(time.time()).split('.')[1])
if logdir:
storage['LOG_PATH'] = logdir
if logfile:
storage['LOG_FILE'] = logfile
self.helper_flags = {
'bootloader' : False,
'base' : False,
'user' : False # Root counts as a user, if additional users are skipped.
'bootloader' : False
}
self.base_packages = base_packages.split(' ') if type(base_packages) is str else base_packages
self.post_base_install = []
storage['session'] = self
self.partition = partition
self.boot_partition = boot_partition
storage['session'] = self
self.partitions = get_partitions_in_use(self.target)
def log(self, *args, level=LOG_LEVELS.Debug, **kwargs):
"""
@ -71,15 +63,10 @@ class Installer():
log(*args, level=level, **kwargs)
def __enter__(self, *args, **kwargs):
if hasUEFI():
# on bios we don't have a boot partition
self.partition.mount(self.mountpoint)
os.makedirs(f'{self.mountpoint}/boot', exist_ok=True)
self.boot_partition.mount(f'{self.mountpoint}/boot')
return self
def __exit__(self, *args, **kwargs):
# b''.join(sys_command(f'sync')) # No need to, since the underlaying fs() object will call sync.
# b''.join(sys_command(f'sync')) # No need to, since the underlying fs() object will call sync.
# TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
if len(args) >= 2 and args[1]:
@ -91,7 +78,7 @@ class Installer():
# We avoid printing /mnt/<log path> because that might confuse people if they note it down
# and then reboot, and a identical log file will be found in the ISO medium anyway.
print(f"[!] A log file has been created here: {os.path.join(storage['LOG_PATH'], storage['LOG_FILE'])}")
print(f" Please submit this issue (and file) to https://github.com/Torxed/archinstall/issues")
print(f" Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues")
raise args[1]
self.genfstab()
@ -99,13 +86,16 @@ class Installer():
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.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)
for step in missing_steps:
self.log(f' - {step}', bg='black', fg='red', level=LOG_LEVELS.Warning)
self.log(f"Detailed error logs can be found at: {log_path}", level=LOG_LEVELS.Warning)
self.log(f"Submit this zip file as an issue to https://github.com/Torxed/archinstall/issues", level=LOG_LEVELS.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.sync_log_to_install_medium()
return False
@ -116,18 +106,18 @@ class Installer():
if (filename := storage.get('LOG_FILE', None)):
absolute_logfile = os.path.join(storage.get('LOG_PATH', './'), filename)
if not os.path.isdir(f"{self.mountpoint}/{os.path.dirname(absolute_logfile)}"):
os.makedirs(f"{self.mountpoint}/{os.path.dirname(absolute_logfile)}")
shutil.copy2(absolute_logfile, f"{self.mountpoint}/{absolute_logfile}")
if not os.path.isdir(f"{self.target}/{os.path.dirname(absolute_logfile)}"):
os.makedirs(f"{self.target}/{os.path.dirname(absolute_logfile)}")
shutil.copy2(absolute_logfile, f"{self.target}/{absolute_logfile}")
return True
def mount(self, partition, mountpoint, create_mountpoint=True):
if create_mountpoint and not os.path.isdir(f'{self.mountpoint}{mountpoint}'):
os.makedirs(f'{self.mountpoint}{mountpoint}')
partition.mount(f'{self.mountpoint}{mountpoint}')
if create_mountpoint and not os.path.isdir(f'{self.target}{mountpoint}'):
os.makedirs(f'{self.target}{mountpoint}')
partition.mount(f'{self.target}{mountpoint}')
def post_install_check(self, *args, **kwargs):
return [step for step, flag in self.helper_flags.items() if flag is False]
@ -137,7 +127,7 @@ class Installer():
self.log(f'Installing packages: {packages}', level=LOG_LEVELS.Info)
if (sync_mirrors := sys_command('/usr/bin/pacman -Syy')).exit_code == 0:
if (pacstrap := sys_command(f'/usr/bin/pacstrap {self.mountpoint} {" ".join(packages)}', peak_output=True, **kwargs)).exit_code == 0:
if (pacstrap := sys_command(f'/usr/bin/pacstrap {self.target} {" ".join(packages)}', **kwargs)).exit_code == 0:
return True
else:
self.log(f'Could not strap in packages: {pacstrap.exit_code}', level=LOG_LEVELS.Info)
@ -145,42 +135,41 @@ class Installer():
self.log(f'Could not sync mirrors: {sync_mirrors.exit_code}', level=LOG_LEVELS.Info)
def set_mirrors(self, mirrors):
return use_mirrors(mirrors, destination=f'{self.mountpoint}/etc/pacman.d/mirrorlist')
return use_mirrors(mirrors, destination=f'{self.target}/etc/pacman.d/mirrorlist')
def genfstab(self, flags='-pU'):
self.log(f"Updating {self.mountpoint}/etc/fstab", level=LOG_LEVELS.Info)
fstab = sys_command(f'/usr/bin/genfstab {flags} {self.mountpoint}').trace_log
with open(f"{self.mountpoint}/etc/fstab", 'ab') as fstab_fh:
self.log(f"Updating {self.target}/etc/fstab", level=LOG_LEVELS.Info)
fstab = sys_command(f'/usr/bin/genfstab {flags} {self.target}').trace_log
with open(f"{self.target}/etc/fstab", 'ab') as fstab_fh:
fstab_fh.write(fstab)
if not os.path.isfile(f'{self.mountpoint}/etc/fstab'):
raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n{o}')
if not os.path.isfile(f'{self.target}/etc/fstab'):
raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n{fstab}')
return True
def set_hostname(self, hostname=None, *args, **kwargs):
if not hostname: hostname = self.hostname
with open(f'{self.mountpoint}/etc/hostname', 'w') as fh:
fh.write(self.hostname + '\n')
def set_hostname(self, hostname :str, *args, **kwargs):
with open(f'{self.target}/etc/hostname', 'w') as fh:
fh.write(hostname + '\n')
def set_locale(self, locale, encoding='UTF-8', *args, **kwargs):
if not len(locale): return True
with open(f'{self.mountpoint}/etc/locale.gen', 'a') as fh:
with open(f'{self.target}/etc/locale.gen', 'a') as fh:
fh.write(f'{locale}.{encoding} {encoding}\n')
with open(f'{self.mountpoint}/etc/locale.conf', 'w') as fh:
with open(f'{self.target}/etc/locale.conf', 'w') as fh:
fh.write(f'LANG={locale}.{encoding}\n')
return True if sys_command(f'/usr/bin/arch-chroot {self.mountpoint} locale-gen').exit_code == 0 else False
return True if sys_command(f'/usr/bin/arch-chroot {self.target} locale-gen').exit_code == 0 else False
def set_timezone(self, zone, *args, **kwargs):
if not zone: return True
if not len(zone): return True # Redundant
if (pathlib.Path("/usr")/"share"/"zoneinfo"/zone).exists():
(pathlib.Path(self.mountpoint)/"etc"/"localtime").unlink(missing_ok=True)
sys_command(f'/usr/bin/arch-chroot {self.mountpoint} ln -s /usr/share/zoneinfo/{zone} /etc/localtime')
(pathlib.Path(self.target)/"etc"/"localtime").unlink(missing_ok=True)
sys_command(f'/usr/bin/arch-chroot {self.target} ln -s /usr/share/zoneinfo/{zone} /etc/localtime')
return True
else:
self.log(
@ -195,16 +184,21 @@ class Installer():
if self.enable_service('ntpd'):
return True
def enable_service(self, service):
self.log(f'Enabling service {service}', level=LOG_LEVELS.Info)
return self.arch_chroot(f'systemctl enable {service}').exit_code == 0
def enable_service(self, *services):
for service in services:
self.log(f'Enabling service {service}', level=LOG_LEVELS.Info)
if (output := self.arch_chroot(f'systemctl enable {service}')).exit_code != 0:
raise ServiceException(f"Unable to start service {service}: {output}")
def run_command(self, cmd, *args, **kwargs):
return sys_command(f'/usr/bin/arch-chroot {self.mountpoint} {cmd}')
return sys_command(f'/usr/bin/arch-chroot {self.target} {cmd}')
def arch_chroot(self, cmd, *args, **kwargs):
return self.run_command(cmd)
def drop_to_shell(self):
subprocess.check_call(f"/usr/bin/arch-chroot {self.target}", shell=True)
def configure_nic(self, nic, dhcp=True, ip=None, gateway=None, dns=None, *args, **kwargs):
if dhcp:
conf = Networkd(Match={"Name": nic}, Network={"DHCP": "yes"})
@ -219,16 +213,16 @@ class Installer():
network["DNS"] = dns
conf = Networkd(Match={"Name": nic}, Network=network)
with open(f"{self.mountpoint}/etc/systemd/network/10-{nic}.network", "a") as netconf:
with open(f"{self.target}/etc/systemd/network/10-{nic}.network", "a") as netconf:
netconf.write(str(conf))
def copy_ISO_network_config(self, enable_services=False):
# Copy (if any) iwd password and config files
if os.path.isdir('/var/lib/iwd/'):
if (psk_files := glob.glob('/var/lib/iwd/*.psk')):
if not os.path.isdir(f"{self.mountpoint}/var/lib/iwd"):
os.makedirs(f"{self.mountpoint}/var/lib/iwd")
if not os.path.isdir(f"{self.target}/var/lib/iwd"):
os.makedirs(f"{self.target}/var/lib/iwd")
if enable_services:
# If we haven't installed the base yet (function called pre-maturely)
@ -248,81 +242,103 @@ class Installer():
self.enable_service('iwd')
for psk in psk_files:
shutil.copy2(psk, f"{self.mountpoint}/var/lib/iwd/{os.path.basename(psk)}")
shutil.copy2(psk, f"{self.target}/var/lib/iwd/{os.path.basename(psk)}")
# Copy (if any) systemd-networkd config files
if (netconfigurations := glob.glob('/etc/systemd/network/*')):
if not os.path.isdir(f"{self.mountpoint}/etc/systemd/network/"):
os.makedirs(f"{self.mountpoint}/etc/systemd/network/")
if not os.path.isdir(f"{self.target}/etc/systemd/network/"):
os.makedirs(f"{self.target}/etc/systemd/network/")
for netconf_file in netconfigurations:
shutil.copy2(netconf_file, f"{self.mountpoint}/etc/systemd/network/{os.path.basename(netconf_file)}")
shutil.copy2(netconf_file, f"{self.target}/etc/systemd/network/{os.path.basename(netconf_file)}")
if enable_services:
# If we haven't installed the base yet (function called pre-maturely)
if self.helper_flags.get('base', False) is False:
def post_install_enable_networkd_resolved(*args, **kwargs):
self.enable_service('systemd-networkd')
self.enable_service('systemd-resolved')
self.enable_service('systemd-networkd', 'systemd-resolved')
self.post_base_install.append(post_install_enable_networkd_resolved)
# Otherwise, we can go ahead and enable the services
else:
self.enable_service('systemd-networkd')
self.enable_service('systemd-resolved')
self.enable_service('systemd-networkd', 'systemd-resolved')
return True
def detect_encryption(self, partition):
if partition.encrypted:
return partition
elif partition.parent not in partition.path and Partition(partition.parent, None, autodetect_filesystem=True).filesystem == 'crypto_LUKS':
return Partition(partition.parent, None, autodetect_filesystem=True)
return False
def minimal_installation(self):
## Add nessecary packages if encrypting the drive
## Add necessary packages if encrypting the drive
## (encrypted partitions default to btrfs for now, so we need btrfs-progs)
## TODO: Perhaps this should be living in the function which dictates
## the partitioning. Leaving here for now.
if self.partition.filesystem == 'btrfs':
#if self.partition.encrypted:
self.base_packages.append('btrfs-progs')
if self.partition.filesystem == 'xfs':
self.base_packages.append('xfsprogs')
if self.partition.filesystem == 'f2fs':
self.base_packages.append('f2fs-tools')
if not(hasUEFI()):
MODULES = []
BINARIES = []
FILES = []
HOOKS = ["base", "udev", "autodetect", "keyboard", "keymap", "modconf", "block", "filesystems", "fsck"]
for partition in self.partitions:
if partition.filesystem == 'btrfs':
#if partition.encrypted:
self.base_packages.append('btrfs-progs')
if partition.filesystem == 'xfs':
self.base_packages.append('xfsprogs')
if partition.filesystem == 'f2fs':
self.base_packages.append('f2fs-tools')
# Configure mkinitcpio to handle some specific use cases.
if partition.filesystem == 'btrfs':
if 'btrfs' not in MODULES:
MODULES.append('btrfs')
if '/usr/bin/btrfs-progs' not in BINARIES:
BINARIES.append('/usr/bin/btrfs')
if self.detect_encryption(partition):
if 'encrypt' not in HOOKS:
HOOKS.insert(HOOKS.index('filesystems'), 'encrypt')
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()
with open(f"{self.mountpoint}/etc/fstab", "a") as fstab:
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"
) # Redundant \n at the start? who knows?
## TODO: Support locale and timezone
#os.remove(f'{self.mountpoint}/etc/localtime')
#sys_command(f'/usr/bin/arch-chroot {self.mountpoint} ln -s /usr/share/zoneinfo/{localtime} /etc/localtime')
#os.remove(f'{self.target}/etc/localtime')
#sys_command(f'/usr/bin/arch-chroot {self.target} ln -s /usr/share/zoneinfo/{localtime} /etc/localtime')
#sys_command('/usr/bin/arch-chroot /mnt hwclock --hctosys --localtime')
self.set_hostname()
self.set_hostname('archinstall')
self.set_locale('en_US')
# TODO: Use python functions for this
sys_command(f'/usr/bin/arch-chroot {self.mountpoint} chmod 700 /root')
sys_command(f'/usr/bin/arch-chroot {self.target} chmod 700 /root')
# Configure mkinitcpio to handle some specific use cases.
# TODO: Yes, we should not overwrite the entire thing, but for now this should be fine
# since we just installed the base system.
if self.partition.filesystem == 'btrfs':
with open(f'{self.mountpoint}/etc/mkinitcpio.conf', 'w') as mkinit:
mkinit.write('MODULES=(btrfs)\n')
mkinit.write('BINARIES=(/usr/bin/btrfs)\n')
mkinit.write('FILES=()\n')
mkinit.write('HOOKS=(base udev autodetect modconf block encrypt filesystems keymap keyboard fsck)\n')
sys_command(f'/usr/bin/arch-chroot {self.mountpoint} mkinitcpio -p linux')
elif self.partition.encrypted:
with open(f'{self.mountpoint}/etc/mkinitcpio.conf', 'w') as mkinit:
mkinit.write('MODULES=()\n')
mkinit.write('BINARIES=()\n')
mkinit.write('FILES=()\n')
mkinit.write('HOOKS=(base udev autodetect modconf block encrypt filesystems keymap keyboard fsck)\n')
sys_command(f'/usr/bin/arch-chroot {self.mountpoint} mkinitcpio -p linux')
with open(f'{self.target}/etc/mkinitcpio.conf', 'w') as mkinit:
mkinit.write(f"MODULES=({' '.join(MODULES)})\n")
mkinit.write(f"BINARIES=({' '.join(BINARIES)})\n")
mkinit.write(f"FILES=({' '.join(FILES)})\n")
mkinit.write(f"HOOKS=({' '.join(HOOKS)})\n")
sys_command(f'/usr/bin/arch-chroot {self.target} mkinitcpio -p linux')
self.helper_flags['base'] = True
@ -334,7 +350,15 @@ class Installer():
return True
def add_bootloader(self, bootloader='systemd-bootctl'):
self.log(f'Adding bootloader {bootloader} to {self.boot_partition}', level=LOG_LEVELS.Info)
boot_partition = None
root_partition = None
for partition in self.partitions:
if partition.mountpoint == self.target+'/boot':
boot_partition = partition
elif partition.mountpoint == self.target:
root_partition = partition
self.log(f'Adding bootloader {bootloader} to {boot_partition}', level=LOG_LEVELS.Info)
if bootloader == 'systemd-bootctl':
if not hasUEFI():
@ -344,19 +368,19 @@ class Installer():
# And in which case we should do some clean up.
# Install the boot loader
sys_command(f'/usr/bin/arch-chroot {self.mountpoint} bootctl --no-variables --path=/boot install')
sys_command(f'/usr/bin/arch-chroot {self.target} bootctl --no-variables --path=/boot install')
# Modify or create a loader.conf
if os.path.isfile(f'{self.mountpoint}/boot/loader/loader.conf'):
with open(f'{self.mountpoint}/boot/loader/loader.conf', 'r') as loader:
if os.path.isfile(f'{self.target}/boot/loader/loader.conf'):
with open(f'{self.target}/boot/loader/loader.conf', 'r') as loader:
loader_data = loader.read().split('\n')
else:
loader_data = [
f"default {self.init_time}",
f"timeout 5"
]
with open(f'{self.mountpoint}/boot/loader/loader.conf', 'w') as loader:
with open(f'{self.target}/boot/loader/loader.conf', 'w') as loader:
for line in loader_data:
if line[:8] == 'default ':
loader.write(f'default {self.init_time}\n')
@ -366,49 +390,47 @@ 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.mountpoint}/boot/loader/entries/{self.init_time}.conf', 'w') as entry:
with open(f'{self.target}/boot/loader/entries/{self.init_time}.conf', 'w') as entry:
entry.write(f'# Created by: archinstall\n')
entry.write(f'# Created on: {self.init_time}\n')
entry.write(f'title Arch Linux\n')
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.
if self.partition.encrypted:
log(f"Identifying root partition by DISK-UUID on {self.partition}, looking for '{os.path.basename(self.partition.real_device)}'.", level=LOG_LEVELS.Debug)
for root, folders, uids in os.walk('/dev/disk/by-uuid'):
for uid in uids:
real_path = os.path.realpath(os.path.join(root, uid))
log(f"Checking root partition match {os.path.basename(real_path)} against {os.path.basename(self.partition.real_device)}: {os.path.basename(real_path) == os.path.basename(self.partition.real_device)}", level=LOG_LEVELS.Debug)
if not os.path.basename(real_path) == os.path.basename(self.partition.real_device): continue
entry.write(f'options cryptdevice=UUID={uid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp\n')
self.helper_flags['bootloader'] = bootloader
return True
break
if (real_device := self.detect_encryption(root_partition)):
# TODO: We need to detect if the encrypted device is a whole disk encryption,
# or simply a partition encryption. Right now we assume it's a partition (and we always have)
log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.uuid}'.", level=LOG_LEVELS.Debug)
entry.write(f'options cryptdevice=PARTUUID={real_device.uuid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp\n')
else:
log(f"Identifying root partition by PART-UUID on {self.partition}, looking for '{os.path.basename(self.partition.path)}'.", level=LOG_LEVELS.Debug)
entry.write(f'options root=PARTUUID={self.partition.uuid} rw intel_pstate=no_hwp\n')
log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=LOG_LEVELS.Debug)
entry.write(f'options root=PARTUUID={root_partition.uuid} rw intel_pstate=no_hwp\n')
self.helper_flags['bootloader'] = bootloader
return True
self.helper_flags['bootloader'] = bootloader
return True
raise RequirementError(f"Could not identify the UUID of {self.partition}, there for {self.mountpoint}/boot/loader/entries/arch.conf will be broken until fixed.")
raise RequirementError(f"Could not identify the UUID of {self.partition}, there for {self.target}/boot/loader/entries/arch.conf will be broken until fixed.")
elif bootloader == "grub-install":
if hasUEFI():
o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB'))
o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.target} grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB'))
sys_command('/usr/bin/arch-chroot grub-mkconfig -o /boot/grub/grub.cfg')
else:
root_device = subprocess.check_output(f'basename "$(readlink -f "/sys/class/block/{self.partition.path.strip("/dev/")}/..")',shell=True).decode().strip()
root_device = subprocess.check_output(f'basename "$(readlink -f "/sys/class/block/{root_partition.path.strip("/dev/")}/..")', shell=True).decode().strip()
if root_device == "block":
root_device = f"{self.partition.path}"
o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} grub-install --target=--target=i386-pc /dev/{root_device}'))
root_device = f"{root_partition.path}"
o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.target} grub-install --target=--target=i386-pc /dev/{root_device}'))
sys_command('/usr/bin/arch-chroot grub-mkconfig -o /boot/grub/grub.cfg')
else:
raise RequirementError(f"Unknown (or not yet implemented) bootloader added to add_bootloader(): {bootloader}")
@ -421,7 +443,7 @@ class Installer():
# The tricky thing with doing the import archinstall.session instead is that
# profiles might be run from a different chroot, and there's no way we can
# guarantee file-path safety when accessing the installer object that way.
# Doing the __builtins__ replacement, ensures that the global vriable "installation"
# Doing the __builtins__ replacement, ensures that the global variable "installation"
# is always kept up to date. It's considered a nasty hack - but it's a safe way
# of ensuring 100% accuracy of archinstall session variables.
__builtins__['installation'] = self
@ -434,19 +456,19 @@ class Installer():
def enable_sudo(self, entity :str, group=False):
self.log(f'Enabling sudo permissions for {entity}.', level=LOG_LEVELS.Info)
with open(f'{self.mountpoint}/etc/sudoers', 'a') as sudoers:
with open(f'{self.target}/etc/sudoers', 'a') as sudoers:
sudoers.write(f'{"%" if group else ""}{entity} ALL=(ALL) ALL\n')
return True
def user_create(self, user :str, password=None, groups=[], sudo=False):
self.log(f'Creating user {user}', level=LOG_LEVELS.Info)
o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} useradd -m -G wheel {user}'))
o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.target} useradd -m -G wheel {user}'))
if password:
self.user_set_pw(user, password)
if groups:
for group in groups:
o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} gpasswd -a {user} {group}'))
o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.target} gpasswd -a {user} {group}'))
if sudo and self.enable_sudo(user):
self.helper_flags['user'] = True
@ -458,12 +480,20 @@ class Installer():
# This means the root account isn't locked/disabled with * in /etc/passwd
self.helper_flags['user'] = True
o = b''.join(sys_command(f"/usr/bin/arch-chroot {self.mountpoint} sh -c \"echo '{user}:{password}' | chpasswd\""))
o = b''.join(sys_command(f"/usr/bin/arch-chroot {self.target} sh -c \"echo '{user}:{password}' | chpasswd\""))
pass
def user_set_shell(self, user, shell):
self.log(f'Setting shell for {user} to {shell}', level=LOG_LEVELS.Info)
o = b''.join(sys_command(f"/usr/bin/arch-chroot {self.target} sh -c \"chsh -s {shell} {user}\""))
pass
def set_keyboard_language(self, language):
if len(language.strip()):
with open(f'{self.mountpoint}/etc/vconsole.conf', 'w') as vconsole:
with open(f'{self.target}/etc/vconsole.conf', 'w') as vconsole:
vconsole.write(f'KEYMAP={language}\n')
vconsole.write(f'FONT=lat9w-16\n')
else:
self.log(f'Keyboard language was not changed from default (no language specified).', fg="yellow", level=LOG_LEVELS.Info)
return True

View File

@ -1,29 +1,25 @@
import subprocess
import os
from .exceptions import *
# from .general import sys_command
def list_keyboard_languages(layout='qwerty'):
def list_keyboard_languages():
locale_dir = '/usr/share/kbd/keymaps/'
if not os.path.isdir(locale_dir):
raise RequirementError(f'Directory containing locales does not exist: {locale_dir}')
for root, folders, files in os.walk(locale_dir):
# Since qwerty is under /i386/ but other layouts are
# in different spots, we'll need to filter the last foldername
# of the path to verify against the desired layout.
if os.path.basename(root) != layout:
continue
for file in files:
if os.path.splitext(file)[1] == '.gz':
yield file.strip('.gz').strip('.map')
def search_keyboard_layout(filter, layout='qwerty'):
for language in list_keyboard_languages(layout):
def search_keyboard_layout(filter):
for language in list_keyboard_languages():
if filter.lower() in language.lower():
yield language
def set_keyboard_language(locale):
return os.system(f'loadkeys {locale}') == 0
return subprocess.call(['loadkeys', locale]) == 0

View File

@ -1,5 +1,7 @@
import os
import shlex
import time
import pathlib
from .exceptions import *
from .general import *
from .disk import Partition
@ -43,8 +45,6 @@ class luks2():
return True
def encrypt(self, partition, password=None, key_size=512, hash_type='sha512', iter_time=10000, key_file=None):
# TODO: We should be able to integrate this into the main log some how.
# Perhaps post-mortem?
if not self.partition.allow_formatting:
raise DiskError(f'Could not encrypt volume {self.partition} due to it having a formatting lock.')
@ -116,7 +116,7 @@ class luks2():
def unlock(self, partition, mountpoint, key_file):
"""
Mounts a lukts2 compatible partition to a certain mountpoint.
Mounts a luks2 compatible partition to a certain mountpoint.
Keyfile must be specified as there's no way to interact with the pw-prompt atm.
:param mountpoint: The name without absolute path, for instance "luksdev" will point to /dev/mapper/luksdev
@ -125,6 +125,11 @@ class luks2():
from .disk import get_filesystem_type
if '/' in mountpoint:
os.path.basename(mountpoint) # TODO: Raise exception instead?
wait_timer = time.time()
while pathlib.Path(partition.path).exists() is False and time.time() - wait_timer < 10:
time.sleep(0.025)
sys_command(f'/usr/bin/cryptsetup open {partition.path} {mountpoint} --key-file {os.path.abspath(key_file)} --type luks2')
if os.path.islink(f'/dev/mapper/{mountpoint}'):
self.mapdev = f'/dev/mapper/{mountpoint}'

View File

@ -74,10 +74,15 @@ def re_rank_mirrors(top=10, *positionals, **kwargs):
def list_mirrors():
url = f"https://archlinux.org/mirrorlist/?protocol=https&ip_version=4&ip_version=6&use_mirror_status=on"
response = urllib.request.urlopen(url)
regions = {}
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")
return regions
region = 'Unknown region'
for line in response.readlines():
if len(line.strip()) == 0:

View File

@ -56,7 +56,7 @@ def wirelessScan(interface):
storage['_WIFI'][interface]['scanning'] = True
# TOOD: Full WiFi experience might get evolved in the future, pausing for now 2021-01-25
# TODO: Full WiFi experience might get evolved in the future, pausing for now 2021-01-25
def getWirelessNetworks(interface):
# TODO: Make this oneliner pritter to check if the interface is scanning or not.
if not '_WIFI' in storage or interface not in storage['_WIFI'] or storage['_WIFI'][interface].get('scanning', False) is False:
@ -65,4 +65,4 @@ def getWirelessNetworks(interface):
time.sleep(5)
for line in sys_command(f"iwctl station {interface} get-networks"):
print(line)
print(line)

View File

@ -6,7 +6,7 @@ from pathlib import Path
from .storage import storage
# TODO: use logging's built in levels instead.
# Altough logging is threaded and I wish to avoid that.
# Although logging is threaded and I wish to avoid that.
# It's more Pythonistic or w/e you want to call it.
class LOG_LEVELS:
Critical = 0b001
@ -88,7 +88,7 @@ def log(*args, **kwargs):
# Attempt to colorize the output if supported
# Insert default colors and override with **kwargs
if supports_color():
kwargs = {'bg' : 'black', 'fg': 'white', **kwargs}
kwargs = {'fg': 'white', **kwargs}
string = stylize_output(string, **kwargs)
# If a logfile is defined in storage,
@ -130,4 +130,4 @@ def log(*args, **kwargs):
# We use sys.stdout.write()+flush() instead of print() to try and
# fix issue #94
sys.stdout.write(f"{string}\n")
sys.stdout.flush()
sys.stdout.flush()

View File

@ -112,11 +112,11 @@ class Script():
if f"{self.profile}" in self.examples:
return self.localize_path(self.examples[self.profile]['path'])
# TODO: Redundant, the below block shouldnt be needed as profiles are stripped of their .py, but just in case for now:
# TODO: Redundant, the below block shouldn't be needed as profiles are stripped of their .py, but just in case for now:
elif f"{self.profile}.py" in self.examples:
return self.localize_path(self.examples[f"{self.profile}.py"]['path'])
# Path was not found in any known examples, check if it's an abolute path
# Path was not found in any known examples, check if it's an absolute path
if os.path.isfile(self.profile):
return self.profile
@ -156,7 +156,7 @@ class Profile(Script):
def install(self):
# Before installing, revert any temporary changes to the namespace.
# This ensures that the namespace during installation is the original initation namespace.
# This ensures that the namespace during installation is the original initiation namespace.
# (For instance awesome instead of aweosme.py or app-awesome.py)
self.namespace = self.original_namespace
return self.execute()
@ -177,6 +177,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,6 +194,56 @@ class Profile(Script):
if hasattr(imported, '_post_install'):
return True
def is_top_level_profile(self):
with open(self.path, 'r') as source:
source_data = source.read()
# TODO: I imagine that there is probably a better way to write this.
return 'top_level_profile = True' in source_data
@property
def packages(self) -> list:
"""
Returns a list of packages baked into the profile definition.
If no package definition has been done, .packages() will return None.
"""
with open(self.path, 'r') as source:
source_data = source.read()
# Some crude safety checks, make sure the imported profile has
# a __name__ check before importing.
#
# If the requirements are met, import with .py in the namespace to not
# trigger a traditional:
# if __name__ == 'moduleName'
if '__name__' in source_data and '__packages__' in source_data:
with self.load_instructions(namespace=f"{self.namespace}.py") as imported:
if hasattr(imported, '__packages__'):
return imported.__packages__
return None
def has_post_install(self):
with open(self.path, 'r') as source:
source_data = source.read()
# Some crude safety checks, make sure the imported profile has
# a __name__ check and if so, check if it's got a _prep_function()
# we can call to ask for more user input.
#
# If the requirements are met, import with .py in the namespace to not
# trigger a traditional:
# if __name__ == 'moduleName'
if '__name__' in source_data and '_post_install' in source_data:
with self.load_instructions(namespace=f"{self.namespace}.py") as imported:
if hasattr(imported, '_post_install'):
return True
def is_top_level_profile(self):
with open(self.path, 'r') as source:
source_data = source.read()
return 'top_level_profile = True' in source_data
@property
def packages(self) -> list:
"""
@ -231,11 +282,11 @@ class Application(Profile):
if f"{self.profile}" in self.examples:
return self.localize_path(self.examples[self.profile]['path'])
# TODO: Redundant, the below block shouldnt be needed as profiles are stripped of their .py, but just in case for now:
# TODO: Redundant, the below block shouldn't be needed as profiles are stripped of their .py, but just in case for now:
elif f"{self.profile}.py" in self.examples:
return self.localize_path(self.examples[f"{self.profile}.py"]['path'])
# Path was not found in any known examples, check if it's an abolute path
# Path was not found in any known examples, check if it's an absolute path
if os.path.isfile(self.profile):
return os.path.basename(self.profile)
@ -247,7 +298,7 @@ class Application(Profile):
def install(self):
# Before installing, revert any temporary changes to the namespace.
# This ensures that the namespace during installation is the original initation namespace.
# This ensures that the namespace during installation is the original initiation namespace.
# (For instance awesome instead of aweosme.py or app-awesome.py)
self.namespace = self.original_namespace
return self.execute()
return self.execute()

View File

@ -7,6 +7,6 @@ def service_state(service_name: str):
if os.path.splitext(service_name)[1] != '.service':
service_name += '.service' # Just to be safe
state = b''.join(sys_command(f'systemctl show -p SubState --value {service_name}'))
state = b''.join(sys_command(f'systemctl show --no-pager -p SubState --value {service_name}', environment_vars={'SYSTEMD_COLORS' : '0'}))
return state.strip().decode('UTF-8')

View File

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

View File

@ -1,10 +1,13 @@
import getpass, pathlib, os, shutil
import getpass, pathlib, os, shutil, re
import sys, time, signal
from .exceptions import *
from .profiles import Profile
from .locale_helpers import search_keyboard_layout
from .output import log, LOG_LEVELS
from .storage import storage
from .networking import list_interfaces
from .general import sys_command
from .hardware import AVAILABLE_GFX_DRIVERS
## TODO: Some inconsistencies between the selection processes.
## Some return the keys from the options, some the values?
@ -18,11 +21,56 @@ def get_terminal_width():
def get_longest_option(options):
return max([len(x) for x in options])
def check_for_correct_username(username):
if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32:
return True
log(
"The username you entered is invalid. Try again",
level=LOG_LEVELS.Warning,
fg='red'
)
return False
def do_countdown():
SIG_TRIGGER = False
def kill_handler(sig, frame):
print()
exit(0)
def sig_handler(sig, frame):
global SIG_TRIGGER
SIG_TRIGGER = True
signal.signal(signal.SIGINT, kill_handler)
original_sigint_handler = signal.getsignal(signal.SIGINT)
signal.signal(signal.SIGINT, sig_handler)
for i in range(5, 0, -1):
print(f"{i}", end='')
for x in range(4):
sys.stdout.flush()
time.sleep(0.25)
print(".", end='')
if SIG_TRIGGER:
abort = input('\nDo you really want to abort (y/n)? ')
if abort.strip() != 'n':
exit(0)
if SIG_TRIGGER is False:
sys.stdin.read()
SIG_TRIGGER = False
signal.signal(signal.SIGINT, sig_handler)
print()
signal.signal(signal.SIGINT, original_sigint_handler)
return True
def get_password(prompt="Enter a password: "):
while (passwd := getpass.getpass(prompt)):
passwd_verification = getpass.getpass(prompt='And one more time for verification: ')
if passwd != passwd_verification:
log(' * Passwords did not match * ', bg='black', fg='red')
log(' * Passwords did not match * ', fg='red')
continue
if len(passwd.strip()) <= 0:
@ -50,14 +98,16 @@ def print_large_list(options, padding=5, margin_bottom=0, separator=': '):
def ask_for_superuser_account(prompt='Create a required super-user 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!', bg='black', fg='red')
log(' * Since root is disabled, you need to create a least one (super) user!', fg='red')
continue
elif not new_user and not forced:
raise UserError("No superuser was created.")
elif not check_for_correct_username(new_user):
continue
password = get_password(prompt=f'Password for user {new_user}: ')
return {new_user: {"!password" : password}}
@ -70,6 +120,8 @@ def ask_for_additional_users(prompt='Any additional users to install (leave blan
new_user = input(prompt).strip(' ')
if not new_user:
break
if not check_for_correct_username(new_user):
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'):
@ -80,7 +132,9 @@ def ask_for_additional_users(prompt='Any additional users to install (leave blan
return users, super_users
def ask_for_a_timezone():
timezone = input('Enter a valid timezone (Example: Europe/Stockholm): ').strip()
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
else:
@ -89,6 +143,14 @@ def ask_for_a_timezone():
level=LOG_LEVELS.Warning,
fg='red'
)
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()
if pipewire_choice in ("y", ""):
audio = "pipewire"
return audio
def ask_to_configure_network():
# Optionally configure one network interface.
@ -110,7 +172,6 @@ def ask_to_configure_network():
log(
"You need to enter a valid IP in IP-config mode.",
level=LOG_LEVELS.Warning,
bg='black',
fg='red'
)
@ -127,7 +188,7 @@ def ask_to_configure_network():
elif nic:
return nic
return None
return {}
def ask_for_disk_layout():
options = {
@ -153,7 +214,7 @@ def ask_for_main_filesystem_format():
def generic_select(options, input_text="Select one of the above by index or absolute value: ", sort=True):
"""
A generic select function that does not output anything
other than the options and their indexs. As an example:
other than the options and their indexes. As an example:
generic_select(["first", "second", "third option"])
1: first
@ -173,7 +234,7 @@ def generic_select(options, input_text="Select one of the above by index or abso
return None
elif selected_option.isdigit():
selected_option = int(selected_option)
if selected_option >= len(options):
if selected_option > len(options):
raise RequirementError(f'Selected option "{selected_option}" is out of range')
selected_option = options[selected_option]
elif selected_option in options:
@ -198,8 +259,10 @@ def select_disk(dict_o_disks):
if len(drives) >= 1:
for index, drive in enumerate(drives):
print(f"{index}: {drive} ({dict_o_disks[drive]['size'], dict_o_disks[drive].device, dict_o_disks[drive]['label']})")
drive = input('Select one of the above disks (by number or full path): ')
if drive.isdigit():
drive = input('Select one of the above disks (by number or full path) or write /mnt to skip partitioning: ')
if drive.strip() == '/mnt':
return None
elif drive.isdigit():
drive = int(drive)
if drive >= len(drives):
raise DiskError(f'Selected option "{drive}" is out of range')
@ -262,6 +325,8 @@ def select_language(options, show_only_country_codes=True):
:return: The language/dictionary key of the selected language
:rtype: str
"""
DEFAULT_KEYBOARD_LANGUAGE = 'us'
if show_only_country_codes:
languages = sorted([language for language in list(options) if len(language) == 2])
else:
@ -271,9 +336,12 @@ 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 --')
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 selected_language.lower() in ('?', 'help'):
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))
@ -286,6 +354,7 @@ def select_language(options, show_only_country_codes=True):
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
@ -293,9 +362,9 @@ def select_language(options, show_only_country_codes=True):
# go through the search step.
elif selected_language in options:
selected_language = options[options.index(selected_language)]
return selected_language
else:
RequirementError("Selected language does not exist.")
return selected_language
raise RequirementError("Selected language does not exist.")
raise RequirementError("Selecting languages require a least one language to be given as an option.")
@ -319,26 +388,64 @@ def select_mirror_regions(mirrors, show_top_mirrors=True):
selected_mirrors = {}
if len(regions) >= 1:
print_large_list(regions, margin_bottom=2)
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:
# 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 (pos := int(selected_mirror)) <= len(regions)-1:
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]
# 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.
elif selected_mirror in mirrors:
selected_mirrors[selected_mirror] = mirrors[selected_mirror]
else:
RequirementError("Selected region does not exist.")
raise RequirementError("Selected region does not exist.")
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)
"""
if len(options) >= 1:
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 **')
selected_driver = generic_select(options, input_text="Select your graphics card driver: ", sort=True)
initial_option = selected_driver
if type(options[initial_option]) == dict:
driver_options = sorted(options[initial_option].keys())
selected_driver_package_group = generic_select(driver_options, input_text=f"Which driver-type do you want for {initial_option}: ")
if selected_driver_package_group in options[initial_option].keys():
print(options[initial_option][selected_driver_package_group])
selected_driver = options[initial_option][selected_driver_package_group]
else:
raise RequirementError(f"Selected driver-type does not exist for {initial_option}.")
return selected_driver_package_group
return selected_driver
raise RequirementError("Selecting drivers require a least one profile to be given as an option.")

View File

@ -12,7 +12,7 @@ Packages
========
.. autofunction:: archinstall.find_package
Be
.. autofunction:: archinstall.find_packages
Locale related
@ -90,4 +90,14 @@ Exceptions
.. autofunction:: archinstall.ProfileError
.. autofunction:: archinstall.SysCallError
.. autofunction:: archinstall.SysCallError
.. autofunction:: archinstall.ProfileNotFound
.. autofunction:: archinstall.HardwareIncompatibilityError
.. autofunction:: archinstall.PermissionError
.. autofunction:: archinstall.UserError
.. autofunction:: archinstall.ServiceException

View File

@ -1,4 +1,4 @@
.. _examples.python:
.. _examples.binary:
Binary executable
=================
@ -11,7 +11,7 @@ It's compiled using `nuitka <https://nuitka.net/>`_ with the flag `--standalone`
Executing the binary
--------------------
As an example we'll use the `guided <https://github.com/Torxed/archinstall/blob/master/examples/guided.py>`_ installer.
As an example we'll use the `guided <https://github.com/archlinux/archinstall/blob/master/examples/guided.py>`_ installer.
To run the `guided` installed, all you have to do *(after installing or compiling the binary)*, is run:
@ -20,4 +20,4 @@ To run the `guided` installed, all you have to do *(after installing or compilin
./archinstall guided
As mentioned, the binary is a bit rudimentary and only supports executing whatever is found directly under `./archinstall/examples`.
Anything else won't be found. This is subject to change in the future to make it a bit more flexible.
Anything else won't be found. This is subject to change in the future to make it a bit more flexible.

View File

@ -3,7 +3,7 @@
Discord
=======
There's a discord channel which is frequent by some `contributors <https://github.com/Torxed/archinstall/graphs/contributors>`_.
There's a discord channel which is frequent by some `contributors <https://github.com/archlinux/archinstall/graphs/contributors>`_.
To join the server, head over to `https://discord.gg/cqXU88y <https://discord.gg/cqXU88y>`_'s server and join in.
There's not many rules other than common sense and treat others with respect.

View File

@ -3,7 +3,7 @@
Issue tracker & bugs
====================
Issues and bugs should be reported over at `https://github.com/Torxed/archinstall/issues <https://github.com/Torxed/archinstall/issues>`_.
Issues and bugs should be reported over at `https://github.com/archlinux/archinstall/issues <https://github.com/Torxed/archinstall/issues>`_.
General questions, enhancements and security issues can be reported over there too.
For quick issues or if you need help, head over the to the Discord server which has a help channel.

View File

@ -2,7 +2,7 @@ python-archinstall Documentation
================================
| **python-archinstall** *(or, archinstall for short)* is a helper library to install Arch Linux and manage services, packages and other things.
| It comes packaged with different pre-configured installers, such as the :ref:`guided <installing.guided>` installer.
| It comes packaged with different pre-configured installers, such as the `Guided installation`_ installer.
|
| A demo can be viewed here: `https://www.youtube.com/watch?v=9Xt7X_Iqg6E <https://www.youtube.com/watch?v=9Xt7X_Iqg6E>`_ which uses the default guided installer.
@ -46,7 +46,8 @@ Some of the features of Archinstall are:
..
examples/scripting
.. toctree::
..
.. toctree::
:maxdepth: 3
:caption: Programming Guide
@ -66,4 +67,4 @@ Some of the features of Archinstall are:
:maxdepth: 3
:caption: API Helper functions
archinstall/general
archinstall/general

View File

@ -1,10 +1,10 @@
.. _installing.binary
.. _installing.binary:
Binary executable
=================
Archinstall can be compiled into a standalone executable.
For Arch Linux based systems, there's a package for this called `archinstall <https://archlinux.life/>`_.
For Arch Linux based systems, there's a package for this called `archinstall <https://archlinux.org/packages/extra/any/archinstall/>`_.
.. warning::
This is not required if you're running archinstall on a pre-built ISO. The installation is only required if you're creating your own scripted installations.
@ -21,7 +21,7 @@ Archinstall is on the `official repositories <https://wiki.archlinux.org/index.p
Using PKGBUILD
--------------
The `source <https://github.com/Torxed/archinstall>`_ contains a binary `PKGBUILD <https://github.com/Torxed/archinstall/tree/master/PKGBUILD/archinstall>`_ which can be either copied straight off the website. Or cloned using `git clone https://github.com/Torxed/archinstall`.
The `source <https://github.com/archlinux/archinstall>`_ contains a binary `PKGBUILD <https://github.com/Torxed/archinstall/tree/master/PKGBUILD/archinstall>`_ which can be either copied straight off the website. Or cloned using `git clone https://github.com/Torxed/archinstall`.
Once you've obtained the `PKGBUILD`, building it is pretty straight forward.
@ -37,7 +37,7 @@ Which should produce a `archinstall-X.x.z-1.pkg.tar.zst` that can be installed u
.. note::
For a complete guide on the build process, please consult the wiki on `PKGBUILD <https://wiki.archlinux.org/index.php/PKGBUILD>`_.
For a complete guide on the build process, please consult the `PKGBUILD on ArchWiki <https://wiki.archlinux.org/index.php/PKGBUILD>`_.
Manual compilation
------------------
@ -49,4 +49,4 @@ Simply clone or download the source, and while standing in the cloned folder `./
nuitka3 --standalone --show-progress archinstall
This requires the `nuitka <https://archlinux.org/packages/community/any/nuitka/>`_ package as well as `python3` to be installed locally.
This requires the `nuitka <https://archlinux.org/packages/community/any/nuitka/>`_ package as well as `python3` to be installed locally.

View File

@ -64,7 +64,7 @@ Default is :code:`auto detect best mirror`
As an example:
* :code:`Sweden` *(wich a capital :code:`S`)* will only use mirrors from Sweden.
* :code:`Sweden` *(with a capital :code:`S`)* will only use mirrors from Sweden.
Selection of drive
------------------
@ -173,4 +173,4 @@ After which you can press :code:`Enter` can be pressed in order to start the for
Post installation
-----------------
Once the installation is complete, green text should appear saying that it's safe to `reboot`, which is also the command you use to reboot.
Once the installation is complete, green text should appear saying that it's safe to `reboot`, which is also the command you use to reboot.

View File

@ -54,7 +54,7 @@ Or you can clone it, we'll clone it here but both methods work the same.
.. code-block:: console
git clone https://github.com/Torxed/archinstall
git clone https://github.com/archlinux/archinstall
Either you can move the folder into your project and simply do

View File

@ -1,31 +1,28 @@
# Pull Request Template
🚨 PR Guidelines:
Make sure you've checked out the [contribution guideline](https://github.com/Torxed/archinstall/blob/master/CONTRIBUTING.md).<br>
Most of the guidelines are not enforced, but is heavily encouraged.
# New features *(v2.2.0)*
## Description
Merge new features in to `torxed-v2.2.0`.<br>
This branch is designated for potential breaking changes, added complexity and new functionality.
Please include a summary of the change and which issue is fixed.<br>
It is also helpful to add links to online documentation or to the implementation of the code you are changing.
# Bug fixes *(v2.1.4)*
## Bugs and Issues
Merge against `master` for bug fixes and anything that improves stability and quality of life.<br>
This excludes:
* New functionality
* Added complexity
* Breaking changes
If this pull-request fixes an issue or a bug, please mention the issues with the approriate issue referece *(Example: &#35;8)*.
Any changes to `master` automatically gets pulled in to `torxed-v2.2.0` to avoid merge hell.
## How Has This Been Tested?
# Describe your PR
If possible, mention any tests you have made with the current code base included in the pull-requests.<br>
Any core-developer will also run tests, but this helps speed things up. Below is a template that can be used:
If the changes has been discussed in an Issue, please tag it so we can backtrace from the Issue later on.<br>
If the PR is larger than ~20 lines, please describe it here unless described in an issue.
As an example:
# Testing
**Test Configuration**:
* Hardware: VirtualBox 6.1
* Specific steps: Ran installer with additional packages `nano` and `wget`
Any new feature or stability improvement should be tested if possible.
Please follow the test instructions at the bottom of the README.
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code to the best of my abilities
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation where possible/if applicable
*These PR guidelines will change after 2021-05-01, which is when `v2.1.4` gets onto the new ISO*

View File

@ -1,24 +1,7 @@
import getpass, time, json, sys, signal, os
import getpass, time, json, os
import archinstall
from archinstall.lib.hardware import hasUEFI
"""
This signal-handler chain (and global variable)
is used to trigger the "Are you sure you want to abort?" question further down.
It might look a bit odd, but have a look at the line: "if SIG_TRIGGER:"
"""
SIG_TRIGGER = False
def kill_handler(sig, frame):
print()
exit(0)
def sig_handler(sig, frame):
global SIG_TRIGGER
SIG_TRIGGER = True
signal.signal(signal.SIGINT, kill_handler)
original_sigint_handler = signal.getsignal(signal.SIGINT)
signal.signal(signal.SIGINT, sig_handler)
from archinstall.lib.profiles import Profile
if archinstall.arguments.get('help'):
print("See `man archinstall` for help.")
@ -31,7 +14,12 @@ def ask_user_questions():
will we continue with the actual installation steps.
"""
if not archinstall.arguments.get('keyboard-language', None):
archinstall.arguments['keyboard-language'] = archinstall.select_language(archinstall.list_keyboard_languages()).strip()
while True:
try:
archinstall.arguments['keyboard-language'] = archinstall.select_language(archinstall.list_keyboard_languages()).strip()
break
except archinstall.RequirementError as err:
archinstall.log(err, fg="red")
# Before continuing, set the preferred keyboard layout/language in the current terminal.
# This will just help the user with the next following questions.
@ -40,7 +28,12 @@ def ask_user_questions():
# Set which region to download packages from during the installation
if not archinstall.arguments.get('mirror-region', None):
archinstall.arguments['mirror-region'] = archinstall.select_mirror_regions(archinstall.list_mirrors())
while True:
try:
archinstall.arguments['mirror-region'] = archinstall.select_mirror_regions(archinstall.list_mirrors())
break
except archinstall.RequirementError as e:
archinstall.log(e, fg="red")
else:
selected_region = archinstall.arguments['mirror-region']
archinstall.arguments['mirror-region'] = {selected_region : archinstall.list_mirrors()[selected_region]}
@ -51,15 +44,17 @@ def ask_user_questions():
archinstall.arguments['harddrive'] = archinstall.BlockDevice(archinstall.arguments['harddrive'])
else:
archinstall.arguments['harddrive'] = archinstall.select_disk(archinstall.all_disks())
if archinstall.arguments['harddrive'] is None:
archinstall.arguments['target-mount'] = '/mnt'
# Perform a quick sanity check on the selected harddrive.
# 1. Check if it has partitions
# 3. Check that we support the current partitions
# 2. If so, ask if we should keep them or wipe everything
if archinstall.arguments['harddrive'].has_partitions():
if archinstall.arguments['harddrive'] and archinstall.arguments['harddrive'].has_partitions():
archinstall.log(f"{archinstall.arguments['harddrive']} contains the following partitions:", fg='yellow')
# We curate a list pf supported paritions
# We curate a list pf supported partitions
# and print those that we don't support.
partition_mountpoints = {}
for partition in archinstall.arguments['harddrive']:
@ -70,7 +65,7 @@ def ask_user_questions():
except archinstall.UnknownFilesystemFormat as err:
archinstall.log(f" {partition} (Filesystem not supported)", fg='red')
# We then ask what to do with the paritions.
# We then ask what to do with the partitions.
if (option := archinstall.ask_for_disk_layout()) == 'abort':
archinstall.log(f"Safely aborting the installation. No changes to the disk or system has been made.")
exit(1)
@ -90,7 +85,7 @@ def ask_user_questions():
mountpoint = input(f"Enter a mount-point for {partition}: ").strip(' ')
if len(mountpoint):
# Get a valid & supported filesystem for the parition:
# Get a valid & supported filesystem for the partition:
while 1:
new_filesystem = input(f"Enter a valid filesystem for {partition} (leave blank for {partition.filesystem}): ").strip(' ')
if len(new_filesystem) <= 0:
@ -113,7 +108,7 @@ def ask_user_questions():
try:
partition.format(new_filesystem, path='/dev/null', log_formating=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/Torxed/archinstall/issues.")
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.")
continue
except archinstall.SysCallError:
@ -121,7 +116,7 @@ def ask_user_questions():
# But that means our .format() function supported it.
break
# When we've selected all three criterias,
# When we've selected all three criteria,
# 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.
@ -135,14 +130,14 @@ def ask_user_questions():
elif option == 'format-all':
archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format()
archinstall.arguments['harddrive'].keep_partitions = False
else:
elif archinstall.arguments['harddrive']:
# If the drive doesn't have any partitions, safely mark the disk with keep_partitions = False
# and ask the user for a root filesystem.
archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format()
archinstall.arguments['harddrive'].keep_partitions = False
# Get disk encryption password (or skip if blank)
if not archinstall.arguments.get('!encryption-password', None):
if archinstall.arguments['harddrive'] and archinstall.arguments.get('!encryption-password', None) is None:
if (passwd := archinstall.get_password(prompt='Enter disk encryption password (leave blank for no encryption): ')):
archinstall.arguments['!encryption-password'] = passwd
archinstall.arguments['harddrive'].encryption_password = archinstall.arguments['!encryption-password']
@ -167,31 +162,50 @@ 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(archinstall.list_profiles())
archinstall.arguments['profile'] = archinstall.select_profile(filter(lambda profile: (Profile(None, profile).is_top_level_profile()), archinstall.list_profiles()))
else:
archinstall.arguments['profile'] = archinstall.list_profiles()[archinstall.arguments['profile']]
# Check the potentially selected profiles preperations to get early checks if some additional questions are needed.
# Check the potentially selected profiles preparations to get early checks if some additional questions are needed.
if archinstall.arguments['profile'] and archinstall.arguments['profile'].has_prep_function():
with archinstall.arguments['profile'].load_instructions(namespace=f"{archinstall.arguments['profile'].namespace}.py") as imported:
if not imported._prep_function():
archinstall.log(
' * Profile\'s preparation requirements was not fulfilled.',
bg='black',
fg='red'
)
exit(1)
# Additional packages (with some light weight error handling for invalid package names)
if not archinstall.arguments.get('packages', None):
archinstall.arguments['packages'] = [package for package in input('Write additional packages to install (space separated, leave blank to skip): ').split(' ') if len(package)]
# 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()
else:
# packages installed by a profile may depend on audio and something may get installed anyways, not much we can do about that.
# we will not try to remove packages post-installation to not have audio, as that may cause multiple issues
archinstall.arguments['audio'] = None
# Verify packages that were given
try:
archinstall.validate_package_list(archinstall.arguments['packages'])
except archinstall.RequirementError as e:
archinstall.log(e, fg='red')
exit(1)
# Additional packages (with some light weight error handling for invalid package names)
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']):
# Verify packages that were given
try:
archinstall.log(f"Verifying that additional packages exist (this might take a few seconds)")
archinstall.validate_package_list(archinstall.arguments['packages'])
break
except archinstall.RequirementError as e:
archinstall.log(e, fg='red')
archinstall.arguments['packages'] = None # Clear the packages to trigger a new input question
else:
# no additional packages were selected, which we'll allow
break
# Ask or Call the helper function that asks the user to optionally configure a network.
if not archinstall.arguments.get('nic', None):
@ -204,8 +218,6 @@ def ask_user_questions():
def perform_installation_steps():
global SIG_TRIGGER
print()
print('This is your chosen configuration:')
archinstall.log("-- Guided template chosen (with below config) --", level=archinstall.LOG_LEVELS.Debug)
@ -219,121 +231,111 @@ def perform_installation_steps():
We mention the drive one last time, and count from 5 to 0.
"""
print(f" ! Formatting {archinstall.arguments['harddrive']} in ", end='')
if archinstall.arguments.get('harddrive', None):
print(f" ! Formatting {archinstall.arguments['harddrive']} in ", end='')
archinstall.do_countdown()
for i in range(5, 0, -1):
print(f"{i}", end='')
for x in range(4):
sys.stdout.flush()
time.sleep(0.25)
print(".", end='')
if SIG_TRIGGER:
abort = input('\nDo you really want to abort (y/n)? ')
if abort.strip() != 'n':
exit(0)
if SIG_TRIGGER is False:
sys.stdin.read()
SIG_TRIGGER = False
signal.signal(signal.SIGINT, sig_handler)
# Put back the default/original signal handler now that we're done catching
# and interrupting SIGINT with "Do you really want to abort".
print()
signal.signal(signal.SIGINT, original_sigint_handler)
"""
Setup the blockdevice, filesystem (and optionally encryption).
Once that's done, we'll hand over to perform_installation()
"""
# maybe we can ask the user what they would prefer on uefi systems?
if hasUEFI():
"""
Setup the blockdevice, filesystem (and optionally encryption).
Once that's done, we'll hand over to perform_installation()
"""
mode = archinstall.GPT
else:
mode = archinstall.MBR
with archinstall.Filesystem(archinstall.arguments['harddrive'],mode) as fs:
# Wipe the entire drive if the disk flag `keep_partitions`is False.
if archinstall.arguments['harddrive'].keep_partitions is False:
fs.use_entire_disk(root_filesystem_type=archinstall.arguments.get('filesystem', 'btrfs'))
if hasUEFI() is False:
mode = archinstall.MBR
# Check if encryption is desired and mark the root partition as encrypted.
if archinstall.arguments.get('!encryption-password', None):
root_partition = fs.find_partition('/')
root_partition.encrypted = True
# After the disk is ready, iterate the partitions and check
# which ones are safe to format, and format those.
for partition in archinstall.arguments['harddrive']:
if partition.safe_to_format():
# Partition might be marked as encrypted due to the filesystem type crypt_LUKS
# But we might have omitted the encryption password question to skip encryption.
# In which case partition.encrypted will be true, but passwd will be false.
if partition.encrypted and (passwd := archinstall.arguments.get('!encryption-password', None)):
partition.encrypt(password=passwd)
with archinstall.Filesystem(archinstall.arguments['harddrive'], mode) as fs:
# Wipe the entire drive if the disk flag `keep_partitions`is False.
if archinstall.arguments['harddrive'].keep_partitions is False:
fs.use_entire_disk(root_filesystem_type=archinstall.arguments.get('filesystem', 'btrfs'))
# Check if encryption is desired and mark the root partition as encrypted.
if archinstall.arguments.get('!encryption-password', None):
root_partition = fs.find_partition('/')
root_partition.encrypted = True
# After the disk is ready, iterate the partitions and check
# which ones are safe to format, and format those.
for partition in archinstall.arguments['harddrive']:
if partition.safe_to_format():
# Partition might be marked as encrypted due to the filesystem type crypt_LUKS
# But we might have omitted the encryption password question to skip encryption.
# In which case partition.encrypted will be true, but passwd will be false.
if partition.encrypted and (passwd := archinstall.arguments.get('!encryption-password', None)):
partition.encrypt(password=passwd)
else:
partition.format()
else:
partition.format()
archinstall.log(f"Did not format {partition} because .safe_to_format() returned False or .allow_formatting was False.", level=archinstall.LOG_LEVELS.Debug)
fs.find_partition('/boot').format('vfat')
if archinstall.arguments.get('!encryption-password', None):
# First encrypt and unlock, then format the desired partition inside the encrypted part.
# archinstall.luks2() encrypts the partition when entering the with context manager, and
# unlocks the drive so that it can be used as a normal block-device within archinstall.
with archinstall.luks2(fs.find_partition('/'), 'luksloop', archinstall.arguments.get('!encryption-password', None)) as unlocked_device:
unlocked_device.format(fs.find_partition('/').filesystem)
unlocked_device.mount('/mnt')
else:
archinstall.log(f"Did not format {partition} because .safe_to_format() returned False or .allow_formatting was False.", level=archinstall.LOG_LEVELS.Debug)
fs.find_partition('/').format(fs.find_partition('/').filesystem)
fs.find_partition('/').mount('/mnt')
if archinstall.arguments.get('!encryption-password', None):
# First encrypt and unlock, then format the desired partition inside the encrypted part.
# archinstall.luks2() encrypts the partition when entering the with context manager, and
# unlocks the drive so that it can be used as a normal block-device within archinstall.
with archinstall.luks2(fs.find_partition('/'), 'luksloop', archinstall.arguments.get('!encryption-password', None)) as unlocked_device:
unlocked_device.format(fs.find_partition('/').filesystem)
perform_installation(device=unlocked_device,
boot_partition=fs.find_partition('/boot'),
language=archinstall.arguments['keyboard-language'],
mirrors=archinstall.arguments['mirror-region'])
else:
perform_installation(device=fs.find_partition('/'),
boot_partition=fs.find_partition('/boot'),
language=archinstall.arguments['keyboard-language'],
mirrors=archinstall.arguments['mirror-region'])
fs.find_partition('/boot').mount('/mnt/boot')
perform_installation('/mnt')
def perform_installation(device, boot_partition, language, mirrors):
def perform_installation(mountpoint):
"""
Performs the installation steps on a block device.
Only requirement is that the block devices are
formatted and setup prior to entering this function.
"""
with archinstall.Installer(device, boot_partition=boot_partition, hostname=archinstall.arguments.get('hostname', 'Archinstall')) as installation:
with archinstall.Installer(mountpoint) as installation:
## if len(mirrors):
# Certain services might be running that affects the system during installation.
# Currently, only one such service is "reflector.service" which updates /etc/pacman.d/mirrorlist
# 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 has completed before using custom mirrors.')
while 'dead' not in (status := archinstall.service_state('reflector')):
installation.log(f'Waiting for automatic mirror selection (reflector) to complete.', level=archinstall.LOG_LEVELS.Info)
while archinstall.service_state('reflector') not in ('dead', 'failed'):
time.sleep(1)
archinstall.use_mirrors(mirrors) # Set the mirrors for the live medium
if installation.minimal_installation():
installation.set_mirrors(mirrors) # Set the mirrors in the installation medium
installation.set_keyboard_language(language)
if hasUEFI():
installation.add_bootloader()
else:
installation.add_bootloader(bootloder='grub-install')
# 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
installation.set_keyboard_language(archinstall.arguments['keyboard-language'])
installation.add_bootloader()
# If user selected to copy the current ISO network configuration
# Perform a copy of the config
if archinstall.arguments.get('nic', None) == 'Copy ISO network configuration to installation':
if archinstall.arguments.get('nic', {}) == 'Copy ISO network configuration to installation':
installation.copy_ISO_network_config(enable_services=True) # Sources the ISO network configuration to the install medium.
elif archinstall.arguments.get('nic',{}).get('NetworkManager',False):
elif archinstall.arguments.get('nic', {}).get('NetworkManager',False):
installation.add_additional_packages("networkmanager")
installation.enable_service('NetworkManager.service')
# Otherwise, if a interface was selected, configure that interface
elif archinstall.arguments.get('nic', None):
elif archinstall.arguments.get('nic', {}):
installation.configure_nic(**archinstall.arguments.get('nic', {}))
installation.enable_service('systemd-networkd')
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)
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)
if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '':
installation.add_additional_packages(archinstall.arguments.get('packages', None))
@ -351,6 +353,7 @@ def perform_installation(device, boot_partition, language, mirrors):
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():
@ -360,7 +363,13 @@ def perform_installation(device, boot_partition, language, mirrors):
)
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", ""):
try:
installation.drop_to_shell()
except:
pass
ask_user_questions()
perform_installation_steps()
perform_installation_steps()

View File

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

View File

@ -6,7 +6,7 @@ installation.install_profile('xorg')
installation.add_additional_packages(__packages__)
with open(f'{installation.mountpoint}/etc/X11/xinit/xinitrc', 'r') as xinitrc:
with open(f'{installation.target}/etc/X11/xinit/xinitrc', 'r') as xinitrc:
xinitrc_data = xinitrc.read()
for line in xinitrc_data.split('\n'):
@ -20,5 +20,5 @@ for line in xinitrc_data.split('\n'):
xinitrc_data += '\n'
xinitrc_data += 'exec awesome\n'
with open(f'{installation.mountpoint}/etc/X11/xinit/xinitrc', 'w') as xinitrc:
xinitrc.write(xinitrc_data)
with open(f'{installation.target}/etc/X11/xinit/xinitrc', 'w') as xinitrc:
xinitrc.write(xinitrc_data)

View File

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

View File

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

View File

@ -0,0 +1,5 @@
import archinstall
packages = "deepin deepin-terminal deepin-editor"
installation.add_additional_packages(packages)

View File

@ -1,4 +1,4 @@
import archinstall
installation.add_additional_packages("gnome gnome-extra gdm") # We'll create a gnome-minimal later, but for now, we'll avoid issues by giving more than we need.
# Note: gdm should be part of the gnome group, but adding it here for clarity
installation.add_additional_packages("gnome gnome-tweaks gdm")
# Note: gdm should be part of the gnome group, but adding it here for clarity

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,11 @@
import archinstall
__packages__ = ['nano', 'nemo', 'gpicview-gtk3', 'chromium', 'openssh', 'sshfs', 'htop', 'scrot', 'wget']
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']
def _prep_function(*args, **kwargs):
"""
@ -35,13 +39,13 @@ if __name__ == 'awesome':
alacritty.install()
# TODO: Copy a full configuration to ~/.config/awesome/rc.lua instead.
with open(f'{installation.mountpoint}/etc/xdg/awesome/rc.lua', 'r') as fh:
with open(f'{installation.target}/etc/xdg/awesome/rc.lua', 'r') as fh:
awesome_lua = fh.read()
## Replace xterm with alacritty for a smoother experience.
awesome_lua = awesome_lua.replace('"xterm"', '"alacritty"')
with open(f'{installation.mountpoint}/etc/xdg/awesome/rc.lua', 'w') as fh:
with open(f'{installation.target}/etc/xdg/awesome/rc.lua', 'w') as fh:
fh.write(awesome_lua)
## TODO: Configure the right-click-menu to contain the above packages that were installed. (as a user config)

56
profiles/budgie.py Normal file
View File

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

View File

@ -2,6 +2,8 @@
import archinstall
is_top_level_profile = False
def _prep_function(*args, **kwargs):
"""
Magic function called by the importing installer

37
profiles/deepin.py Normal file
View File

@ -0,0 +1,37 @@
# A desktop environment using "Deepin".
import archinstall, os
is_top_level_profile = False
def _prep_function(*args, **kwargs):
"""
Magic function called by the importing installer
before continuing any further. It also avoids executing any
other code in this stage. So it's a safe way to ask the user
for more input before any other installer steps start.
"""
# 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 application deepin from the template under /applications/
deepin = archinstall.Application(installation, 'deepin')
deepin.install()
# Enable autostart of Deepin for all users
installation.enable_service('lightdm')

View File

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

View File

@ -2,6 +2,8 @@
import archinstall
is_top_level_profile = False
def _prep_function(*args, **kwargs):
"""
Magic function called by the importing installer

View File

@ -1,5 +1,7 @@
import archinstall, subprocess
is_top_level_profile = False
def _prep_function(*args, **kwargs):
"""
Magic function called by the importing installer

View File

@ -1,5 +1,7 @@
import archinstall, subprocess
is_top_level_profile = False
def _prep_function(*args, **kwargs):
"""
Magic function called by the importing installer

63
profiles/i3.py Normal file
View File

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

View File

@ -1,7 +1,9 @@
# A desktop environement using "KDE".
# A desktop environment using "KDE".
import archinstall, os
is_top_level_profile = False
# TODO: Remove hard dependency of bash (due to .bash_profile)
def _prep_function(*args, **kwargs):
@ -20,14 +22,14 @@ def _prep_function(*args, **kwargs):
else:
print('Deprecated (??): xorg profile has no _prep_function() anymore')
"""
def _post_install(*args, **kwargs):
if "nvidia" in _gfx_driver_packages:
print("Plasma Wayland has known compatibility issues with the proprietary Nvidia driver")
choice = input("Would you like plasma-wayland to be the default session [Y/n] ").lower()
if choice == "y":
installation.arch_chroot("mv /usr/share/xsessions/plasma.desktop /usr/share/xsessions/plasmax11.desktop")
installation.arch_chroot("mv /usr/share/wayland-sessions/plasmawayland.desktop /usr/share/wayland-sessions/plasma.desktop")
print("After booting, you can choose between Wayland and Xorg using the drop-down menu")
return True
"""
# Ensures that this code only gets executed if executed
# through importlib.util.spec_from_file_location("kde", "/somewhere/kde.py")
# or through conventional import kde

35
profiles/lxqt.py Normal file
View File

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

34
profiles/mate.py Normal file
View File

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

20
profiles/minimal.py Normal file
View File

@ -0,0 +1,20 @@
# Used to do a minimal install
import archinstall, os
is_top_level_profile = True
def _prep_function(*args, **kwargs):
"""
Magic function called by the importing installer
before continuing any further. For minimal install,
we don't need to do anything special here, but it
needs to exist and return True.
"""
return True # Do nothing and just return True
if __name__ == 'minimal':
"""
This "profile" is a meta-profile.
It is used for a custom minimal installation, without any desktop-specific packages.
"""

22
profiles/sway.py Normal file
View File

@ -0,0 +1,22 @@
# A desktop environment using "Sway"
import archinstall
is_top_level_profile = False
def _prep_function(*args, **kwargs):
"""
Magic function called by the importing installer
before continuing any further. It also avoids executing any
other code in this stage. So it's a safe way to ask the user
for more input before any other installer steps start.
"""
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()

View File

@ -3,6 +3,8 @@
import archinstall
is_top_level_profile = False
def _prep_function(*args, **kwargs):
"""
Magic function called by the importing installer

View File

@ -2,88 +2,7 @@
import archinstall, os
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.")
is_top_level_profile = True
def _prep_function(*args, **kwargs):
"""
@ -92,10 +11,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.

30
pyproject.toml Normal file
View File

@ -0,0 +1,30 @@
[build-system]
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/"]
exclude = ["docs/*.html", "docs/_static","docs/*.png","docs/*.psd"]
[tool.flit.metadata.requires-extra]
doc = ["sphinx"]

39
setup.cfg Normal file
View File

@ -0,0 +1,39 @@
[metadata]
name = archinstall
version = attr: archinstall.__version__
description = Arch Linux installer - guided, templates etc.
author = Anton Hvornum
author_email = anton@hvornum.se
long_description = file: README.md
long_description_content_type = text/markdown
license = GPL
license_files =
LICENSE
project_urls =
Source = https://github.com/archlinux/archinstall
Documentation = https://archinstall.readthedocs.io/
classifers =
Programming Language :: Python :: 3
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Operating System :: POSIX :: Linux
[options]
packages = find:
python_requires = >= 3.8
[options.packages.find]
include =
archinstall
archinstall.*
[options.package_data]
archinstall =
examples/*.py
profiles/*.py
profiles/applications/*.py
[options.entry_points]
console_scripts =
archinstall = archinstall:run_as_a_module

View File

@ -1,27 +1,2 @@
import setuptools, glob, shutil
with open("README.md", "r") as fh:
long_description = fh.read()
with open('VERSION', 'r') as fh:
VERSION = fh.read()
setuptools.setup(
name="archinstall",
version=VERSION,
author="Anton Hvornum",
author_email="anton@hvornum.se",
description="Arch Linux installer - guided, templates etc.",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/Torxed/archinstall",
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3.8",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: POSIX :: Linux",
],
python_requires='>=3.8',
setup_requires=['wheel'],
package_data={'archinstall': glob.glob('examples/*.py') + glob.glob('profiles/*.py') + glob.glob('profiles/applications/*.py')},
)
import setuptools
setuptools.setup()