index : archinstall32 | |
Archlinux32 installer | gitolite user |
summaryrefslogtreecommitdiff |
-rw-r--r-- | .github/workflows/iso-build.yaml | 4 | ||||
-rw-r--r-- | .github/workflows/python-publish.yml | 9 | ||||
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | .pypirc | 6 | ||||
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | archinstall/__init__.py | 5 | ||||
-rw-r--r-- | archinstall/lib/disk.py | 59 | ||||
-rw-r--r-- | archinstall/lib/general.py | 54 | ||||
-rw-r--r-- | archinstall/lib/hardware.py | 57 | ||||
-rw-r--r-- | archinstall/lib/installer.py | 64 | ||||
-rw-r--r-- | archinstall/lib/profiles.py | 46 | ||||
-rw-r--r-- | archinstall/lib/user_interaction.py | 88 | ||||
-rw-r--r-- | docs/archinstall/general.rst | 2 | ||||
-rw-r--r-- | docs/pull_request_template.md | 18 | ||||
-rw-r--r-- | examples/guided.py | 55 | ||||
-rw-r--r-- | profiles/52-54-00-12-34-56.py | 2 | ||||
-rw-r--r-- | profiles/applications/awesome.py | 6 | ||||
-rw-r--r-- | profiles/applications/cinnamon.py | 2 | ||||
-rw-r--r-- | profiles/applications/deepin.py | 5 | ||||
-rw-r--r-- | profiles/applications/lxqt.py | 2 | ||||
-rw-r--r-- | profiles/applications/sway.py | 4 | ||||
-rw-r--r-- | profiles/applications/xfce4.py | 4 | ||||
-rw-r--r-- | profiles/deepin.py | 37 | ||||
-rw-r--r-- | profiles/sway.py | 3 | ||||
-rw-r--r-- | profiles/xorg.py | 76 | ||||
-rw-r--r-- | pyproject.toml | 31 |
diff --git a/.github/workflows/iso-build.yaml b/.github/workflows/iso-build.yaml index d08e68ff..acef87fb 100644 --- a/.github/workflows/iso-build.yaml +++ b/.github/workflows/iso-build.yaml @@ -3,6 +3,10 @@ name: Build Arch ISO with ArchInstall Commit on: + push: + branches: + - master + - main # In case we adopt this convention in the future pull_request: paths-ignore: - '.github/**' diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 09bcba55..c24fc0d7 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -21,11 +21,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine + pip install setuptools wheel flit - name: Build and publish env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + FLIT_USERNAME: ${{ secrets.PYPI_USERNAME }} + FLIT_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | - python setup.py sdist bdist_wheel - twine upload dist/* + flit publish @@ -21,3 +21,5 @@ SAFETY_LOCK **/**.target **/**.qcow2 **/test.py +**/archiso +/guided.py diff --git a/.pypirc b/.pypirc new file mode 100644 index 00000000..7b926de7 --- /dev/null +++ b/.pypirc @@ -0,0 +1,6 @@ +[distutils] +index-servers = + pypi + +[pypi] +repository = https://upload.pypi.org/legacy/
\ No newline at end of file @@ -95,10 +95,10 @@ with archinstall.Installer('/mnt') as installation: This installer will perform the following: * Prompt the user to select a disk and disk-password - * Proceed to wipe the selected disk with a `GPT` partition table. + * Proceed to wipe the selected disk with a `GPT` partition table on a UEFI system and MBR on a bios system. * Sets up a default 100% used disk with encryption. * Installs a basic instance of Arch Linux *(base base-devel linux linux-firmware btrfs-progs efibootmgr)* - * Installs and configures a bootloader to partition 0. + * Installs and configures a bootloader to partition 0 on uefi. on bios it sets the root to partition 0. * Install additional packages *(nano, wget, git)* * Installs a profile with a window manager called [awesome](https://github.com/archlinux/archinstall/blob/master/profiles/awesome.py) *(more on profile installations in the [documentation](https://python-archinstall.readthedocs.io/en/latest/archinstall/Profile.html))*. diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 84ba0ec0..bc58af54 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -1,8 +1,9 @@ +"""Arch Linux installer - guided, templates etc.""" from .lib.general import * from .lib.disk import * from .lib.user_interaction import * from .lib.exceptions import * -from .lib.installer import * +from .lib.installer import __packages__, __base_packages__, Installer from .lib.profiles import * from .lib.luks import * from .lib.mirrors import * @@ -14,7 +15,7 @@ from .lib.output import * from .lib.storage import * from .lib.hardware import * -__version__ = "2.1.4" +__version__ = "2.2.0" ## Basic version of arg.parse() supporting: ## --key=value diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index bada4076..67c2bdcd 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -5,6 +5,7 @@ from .exceptions import DiskError from .general import * from .output import log, LOG_LEVELS from .storage import storage +from .hardware import hasUEFI ROOT_DIR_PATTERN = re.compile('^.*?/devices') GPT = 0b00000001 @@ -73,7 +74,7 @@ class BlockDevice(): raise DiskError(f'Could not locate backplane info for "{self.path}"') if self.info['type'] == 'loop': - for drive in json.loads(b''.join(sys_command(f'losetup --json', hide_from_log=True)).decode('UTF_8'))['loopdevices']: + for drive in json.loads(b''.join(sys_command(['losetup', '--json'], hide_from_log=True)).decode('UTF_8'))['loopdevices']: if not drive['name'] == self.path: continue return drive['back-file'] @@ -94,10 +95,10 @@ class BlockDevice(): @property def partitions(self): - o = b''.join(sys_command(f'partprobe {self.path}')) + o = b''.join(sys_command(['partprobe', self.path])) #o = b''.join(sys_command('/usr/bin/lsblk -o name -J -b {dev}'.format(dev=dev))) - o = b''.join(sys_command(f'/usr/bin/lsblk -J {self.path}')) + o = b''.join(sys_command(['/usr/bin/lsblk', '-J', self.path])) if b'not a block device' in o: raise DiskError(f'Can not read partitions off something that isn\'t a block device: {self.path}') @@ -427,7 +428,7 @@ class Filesystem(): # TODO: # When instance of a HDD is selected, check all usages and gracefully unmount them # as well as close any crypto handles. - def __init__(self, blockdevice, mode=GPT): + def __init__(self, blockdevice,mode): self.blockdevice = blockdevice self.mode = mode @@ -440,6 +441,11 @@ class Filesystem(): return self else: raise DiskError(f'Problem setting the partition format to GPT:', f'/usr/bin/parted -s {self.blockdevice.device} mklabel gpt') + elif self.mode == MBR: + if sys_command(f'/usr/bin/parted -s {self.blockdevice.device} mklabel msdos').exit_code == 0: + return self + else: + raise DiskError(f'Problem setting the partition format to GPT:', f'/usr/bin/parted -s {self.blockdevice.device} mklabel msdos') else: raise DiskError(f'Unknown mode selected to format in: {self.mode}') @@ -482,28 +488,39 @@ class Filesystem(): def use_entire_disk(self, root_filesystem_type='ext4'): log(f"Using and formatting the entire {self.blockdevice}.", level=LOG_LEVELS.Debug) - self.add_partition('primary', start='1MiB', end='513MiB', format='fat32') - self.set_name(0, 'EFI') - self.set(0, 'boot on') - # TODO: Probably redundant because in GPT mode 'esp on' is an alias for "boot on"? - # https://www.gnu.org/software/parted/manual/html_node/set.html - self.set(0, 'esp on') - self.add_partition('primary', start='513MiB', end='100%') - - self.blockdevice.partition[0].filesystem = 'vfat' - self.blockdevice.partition[1].filesystem = root_filesystem_type - log(f"Set the root partition {self.blockdevice.partition[1]} to use filesystem {root_filesystem_type}.", level=LOG_LEVELS.Debug) - - self.blockdevice.partition[0].target_mountpoint = '/boot' - self.blockdevice.partition[1].target_mountpoint = '/' - - self.blockdevice.partition[0].allow_formatting = True - self.blockdevice.partition[1].allow_formatting = True + if hasUEFI(): + self.add_partition('primary', start='1MiB', end='513MiB', format='fat32') + self.set_name(0, 'EFI') + self.set(0, 'boot on') + # TODO: Probably redundant because in GPT mode 'esp on' is an alias for "boot on"? + # https://www.gnu.org/software/parted/manual/html_node/set.html + self.set(0, 'esp on') + self.add_partition('primary', start='513MiB', end='100%') + + self.blockdevice.partition[0].filesystem = 'vfat' + self.blockdevice.partition[1].filesystem = root_filesystem_type + log(f"Set the root partition {self.blockdevice.partition[1]} to use filesystem {root_filesystem_type}.", level=LOG_LEVELS.Debug) + + self.blockdevice.partition[0].target_mountpoint = '/boot' + self.blockdevice.partition[1].target_mountpoint = '/' + + self.blockdevice.partition[0].allow_formatting = True + self.blockdevice.partition[1].allow_formatting = True + else: + #we don't need a seprate boot partition it would be a waste of space + self.add_partition('primary', start='1MB', end='100%') + self.blockdevice.partition[0].filesystem=root_filesystem_type + log(f"Set the root partition {self.blockdevice.partition[0]} to use filesystem {root_filesystem_type}.", level=LOG_LEVELS.Debug) + self.blockdevice.partition[0].target_mountpoint = '/' + self.blockdevice.partition[0].allow_formatting = True def add_partition(self, type, start, end, format=None): log(f'Adding partition to {self.blockdevice}', level=LOG_LEVELS.Info) previous_partitions = self.blockdevice.partitions + if self.mode == MBR: + if len(self.blockdevice.partitions)>3: + DiskError("Too many partitions on disk, MBR disks can only have 3 parimary partitions") if format: partitioning = self.parted(f'{self.blockdevice.device} mkpart {type} {format} {start} {end}') == 0 else: diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 6e3b66f1..dc0f018a 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -76,7 +76,7 @@ class sys_command():#Thread): """ Stolen from archinstall_gui """ - def __init__(self, cmd, callback=None, start_callback=None, environment_vars={}, *args, **kwargs): + def __init__(self, cmd, callback=None, start_callback=None, peak_output=False, environment_vars={}, *args, **kwargs): kwargs.setdefault("worker_id", gen_uid()) kwargs.setdefault("emulate", False) kwargs.setdefault("suppress_errors", False) @@ -86,13 +86,22 @@ class sys_command():#Thread): if kwargs['emulate']: self.log(f"Starting command '{cmd}' in emulation mode.", level=LOG_LEVELS.Debug) - self.raw_cmd = cmd - try: - self.cmd = shlex.split(cmd) - except Exception as e: - raise ValueError(f'Incorrect string to split: {cmd}\n{e}') + if type(cmd) is list: + # if we get a list of arguments + self.raw_cmd = shlex.join(cmd) + self.cmd = cmd + else: + # else consider it a single shell string + # this should only be used if really necessary + self.raw_cmd = cmd + try: + self.cmd = shlex.split(cmd) + except Exception as e: + raise ValueError(f'Incorrect string to split: {cmd}\n{e}') + self.args = args self.kwargs = kwargs + self.peak_output = peak_output self.environment_vars = environment_vars self.kwargs.setdefault("worker", None) @@ -151,6 +160,38 @@ class sys_command():#Thread): 'exit_code': self.exit_code } + def peak(self, output :str): + if type(output) == bytes: + try: + output = output.decode('UTF-8') + except UnicodeDecodeError: + return None + + output = output.strip('\r\n ') + if len(output) <= 0: + return None + + if self.peak_output: + from .user_interaction import get_terminal_width + + # Move back to the beginning of the terminal + sys.stdout.flush() + sys.stdout.write("\033[%dG" % 0) + sys.stdout.flush() + + # Clear the line + sys.stdout.write(" " * get_terminal_width()) + sys.stdout.flush() + + # Move back to the beginning again + sys.stdout.flush() + sys.stdout.write("\033[%dG" % 0) + sys.stdout.flush() + + # And print the new output we're peaking on: + sys.stdout.write(output) + sys.stdout.flush() + def run(self): self.status = 'running' old_dir = os.getcwd() @@ -182,6 +223,7 @@ class sys_command():#Thread): for fileno, event in poller.poll(0.1): try: output = os.read(child_fd, 8192) + self.peak(output) self.trace_log += output except OSError: alive = False diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index 5828fd09..d6cf982c 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -1,14 +1,42 @@ -import os +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 hasUEFI(): +def hasAMDCPU()->bool: + if subprocess.check_output("lscpu | grep AMD", shell=True).strip().decode(): + return True + return False +def hasIntelCPU()->bool: + if subprocess.check_output("lscpu | grep Intel", shell=True).strip().decode(): + return True + return False + +def hasUEFI()->bool: return os.path.isdir('/sys/firmware/efi') -def graphicsDevices(): +def graphicsDevices()->dict: cards = {} for line in sys_command(f"lspci"): if b' VGA ' in line: @@ -16,13 +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 diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 758033a7..e2762603 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -9,6 +9,11 @@ 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"] +__base_packages__ = __packages__[:6] class Installer(): """ @@ -18,7 +23,7 @@ class Installer(): :param partition: Requires a partition as the first argument, this is so that the installer can mount to `mountpoint` and strap packages there. :type partition: class:`archinstall.Partition` - + :param boot_partition: There's two reasons for needing a boot partition argument, The first being so that `mkinitcpio` can place the `vmlinuz` kernel at the right place during the `pacstrap` or `linux` and the base packages for a minimal installation. @@ -29,12 +34,13 @@ class Installer(): :param profile: A profile to install, this is optional and can be called later manually. This just simplifies the process by not having to call :py:func:`~archinstall.Installer.install_profile` later on. :type profile: str, optional - + :param hostname: The given /etc/hostname for the machine. :type hostname: str, optional """ - def __init__(self, target, *, base_packages='base base-devel linux linux-firmware efibootmgr'): + def __init__(self, target, *, base_packages='base base-devel linux linux-firmware', kernels='linux'): + base_packages = base_packages + kernels.replace(',', ' ') self.target = target self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S') self.milliseconds = int(str(time.time()).split('.')[1]) @@ -43,8 +49,12 @@ class Installer(): 'base' : False, 'bootloader' : False } - + self.base_packages = base_packages.split(' ') if type(base_packages) is str else base_packages + if hasUEFI(): + self.base_packages.append("efibootmgr") + else: + self.base_packages.append("grub") self.post_base_install = [] storage['session'] = self @@ -141,7 +151,7 @@ class Installer(): if not os.path.isfile(f'{self.target}/etc/fstab'): raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n{fstab}') - + return True def set_hostname(self, hostname :str, *args, **kwargs): @@ -223,7 +233,7 @@ class Installer(): # If we haven't installed the base yet (function called pre-maturely) if self.helper_flags.get('base', False) is False: self.base_packages.append('iwd') - # This function will be called after minimal_installation() + # This function will be called after minimal_installation() # as a hook for post-installs. This hook is only needed if # base is not installed yet. def post_install_enable_iwd_service(*args, **kwargs): @@ -273,6 +283,7 @@ class Installer(): ## (encrypted partitions default to btrfs for now, so we need btrfs-progs) ## TODO: Perhaps this should be living in the function which dictates ## the partitioning. Leaving here for now. + MODULES = [] BINARIES = [] FILES = [] @@ -298,10 +309,20 @@ class Installer(): if 'encrypt' not in HOOKS: HOOKS.insert(HOOKS.index('filesystems'), 'encrypt') + if not(hasUEFI()): # TODO: Allow for grub even on EFI + self.base_packages.append('grub') + self.pacstrap(self.base_packages) self.helper_flags['base-strapped'] = True #self.genfstab() - + if not isVM(): + vendor = cpuVendor() + if vendor == "AuthenticAMD": + self.base_packages.append("amd-ucode") + elif vendor == "GenuineIntel": + self.base_packages.append("intel-ucode") + else: + self.log("Unknown cpu vendor not installing ucode") with open(f"{self.target}/etc/fstab", "a") as fstab: fstab.write( "\ntmpfs /tmp tmpfs defaults,noatime,mode=1777 0 0\n" @@ -322,7 +343,7 @@ class Installer(): mkinit.write(f"BINARIES=({' '.join(BINARIES)})\n") mkinit.write(f"FILES=({' '.join(FILES)})\n") mkinit.write(f"HOOKS=({' '.join(HOOKS)})\n") - sys_command(f'/usr/bin/arch-chroot {self.target} mkinitcpio -p linux') + sys_command(f'/usr/bin/arch-chroot {self.target} mkinitcpio -P') self.helper_flags['base'] = True @@ -345,6 +366,8 @@ class Installer(): self.log(f'Adding bootloader {bootloader} to {boot_partition}', level=LOG_LEVELS.Info) if bootloader == 'systemd-bootctl': + if not hasUEFI(): + raise HardwareIncompatibilityError # TODO: Ideally we would want to check if another config # points towards the same disk and/or partition. # And in which case we should do some clean up. @@ -372,13 +395,20 @@ class Installer(): ## For some reason, blkid and /dev/disk/by-uuid are not getting along well. ## And blkid is wrong in terms of LUKS. #UUID = sys_command('blkid -s PARTUUID -o value {drive}{partition_2}'.format(**args)).decode('UTF-8').strip() - # Setup the loader entry with open(f'{self.target}/boot/loader/entries/{self.init_time}.conf', 'w') as entry: entry.write(f'# Created by: archinstall\n') entry.write(f'# Created on: {self.init_time}\n') entry.write(f'title Arch Linux\n') entry.write(f'linux /vmlinuz-linux\n') + if not isVM(): + vendor = cpuVendor() + if vendor == "AuthenticAMD": + entry.write("initrd /amd-ucode.img\n") + elif vendor == "GenuineIntel": + entry.write("initrd /intel-ucode.img\n") + else: + self.log("unknow cpu vendor, not adding ucode to systemd-boot config") entry.write(f'initrd /initramfs-linux.img\n') ## blkid doesn't trigger on loopback devices really well, ## so we'll use the old manual method until we get that sorted out. @@ -396,9 +426,21 @@ class Installer(): self.helper_flags['bootloader'] = bootloader return True - raise RequirementError(f"Could not identify the UUID of {root_partition}, there for {self.target}/boot/loader/entries/arch.conf will be broken until fixed.") + raise RequirementError(f"Could not identify the UUID of {self.partition}, there for {self.target}/boot/loader/entries/arch.conf will be broken until fixed.") + elif bootloader == "grub-install": + if hasUEFI(): + o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.target} grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB')) + sys_command('/usr/bin/arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg') + return True + else: + root_device = subprocess.check_output(f'basename "$(readlink -f /sys/class/block/{root_partition.path.replace("/dev/","")}/..)"', shell=True).decode().strip() + if root_device == "block": + root_device = f"{root_partition.path}" + o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.target} grub-install --target=i386-pc /dev/{root_device}')) + sys_command('/usr/bin/arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg') + return True else: - raise RequirementError(f"Unknown (or not yet implemented) bootloader added to add_bootloader(): {bootloader}") + raise RequirementError(f"Unknown (or not yet implemented) bootloader requested: {bootloader}") def add_additional_packages(self, *packages): return self.pacstrap(*packages) diff --git a/archinstall/lib/profiles.py b/archinstall/lib/profiles.py index 95962fd6..265ca26a 100644 --- a/archinstall/lib/profiles.py +++ b/archinstall/lib/profiles.py @@ -182,7 +182,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() @@ -198,7 +198,6 @@ class Profile(Script): with self.load_instructions(namespace=f"{self.namespace}.py") as imported: if hasattr(imported, '_post_install'): return True - """ def is_top_level_profile(self): with open(self.path, 'r') as source: @@ -229,6 +228,49 @@ class Profile(Script): 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: + """ + 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 + + class Application(Profile): def __repr__(self, *args, **kwargs): return f'Application({os.path.basename(self.profile)})' diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py index 572acf4f..5eb25b19 100644 --- a/archinstall/lib/user_interaction.py +++ b/archinstall/lib/user_interaction.py @@ -6,6 +6,8 @@ from .locale_helpers import list_keyboard_languages, verify_keyboard_layout, sea 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, hasUEFI ## TODO: Some inconsistencies between the selection processes. ## Some return the keys from the options, some the values? @@ -131,7 +133,7 @@ def ask_for_additional_users(prompt='Any additional users to install (leave blan def ask_for_a_timezone(): while True: - timezone = input('Enter a valid timezone (examples: Europe/Stockholm, US/Eastern) or press enter to use UTC: ').strip() + timezone = input('Enter a valid timezone (examples: Europe/Stockholm, US/Eastern) or press enter to use UTC: ').strip().strip('*.') if timezone == '': timezone = 'UTC' if (pathlib.Path("/usr")/"share"/"zoneinfo"/timezone).exists(): @@ -142,7 +144,17 @@ def ask_for_a_timezone(): level=LOG_LEVELS.Warning, fg='red' ) - + +def ask_for_bootloader() -> str: + bootloader = "systemd-bootctl" + if hasUEFI()==False: + bootloader="grub-install" + else: + bootloader_choice = input("Would you like to use GRUB as a bootloader instead of systemd-boot? [y/N] ").lower() + if bootloader_choice == "y": + bootloader="grub-install" + return bootloader + def ask_for_audio_selection(): audio = "pulseaudio" # Default for most desktop environments pipewire_choice = input("Would you like to install pipewire instead of pulseaudio as the default audio server? [Y/n] ").lower() @@ -319,10 +331,13 @@ 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 = generic_select(drives, 'Select one of the above disks (by number or full path) or leave blank to skip partitioning: ', + + log(f"You can skip selecting a drive and partitioning and use whatever drive-setup is mounted at /mnt (experimental)", fg="yellow") + drive = generic_select(drives, 'Select one of the above disks (by name or number) or leave blank to use /mnt: ', options_output=False) if not drive: return drive + drive = dict_o_disks[drive] return drive @@ -453,3 +468,70 @@ def select_mirror_regions(mirrors, show_top_mirrors=True): selected_mirrors[selected_mirror] = mirrors[selected_mirror] return selected_mirrors + + raise RequirementError("Selecting mirror region require a least one region to be given as an option.") + +def select_driver(options=AVAILABLE_GFX_DRIVERS): + """ + Some what convoluted function, which's job is simple. + Select a graphics driver from a pre-defined set of popular options. + + (The template xorg is for beginner users, not advanced, and should + there for appeal to the general public first and edge cases later) + """ + drivers = sorted(list(options)) + + if 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 = 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 RequirementError("At least one graphics driver is needed to support a graphical environment. Please restart the installer and try again.") + else: + raise 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 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 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.") diff --git a/docs/archinstall/general.rst b/docs/archinstall/general.rst index c3913ea9..7319d244 100644 --- a/docs/archinstall/general.rst +++ b/docs/archinstall/general.rst @@ -12,7 +12,7 @@ Packages ======== .. autofunction:: archinstall.find_package - +Be .. autofunction:: archinstall.find_packages Locale related diff --git a/docs/pull_request_template.md b/docs/pull_request_template.md index c2f694ce..729c1aae 100644 --- a/docs/pull_request_template.md +++ b/docs/pull_request_template.md @@ -2,18 +2,8 @@ # New features *(v2.2.0)* -Merge new features in to `torxed-v2.2.0`.<br> -This branch is designated for potential breaking changes, added complexity and new functionality. - -# Bug fixes *(v2.1.4)* - -Merge against `master` for bug fixes and anything that improves stability and quality of life.<br> -This excludes: - * New functionality - * Added complexity - * Breaking changes - -Any changes to `master` automatically gets pulled in to `torxed-v2.2.0` to avoid merge hell. +All future work towards *`v2.2.0`* is done against `master` now.<br> +Any patch work to existing verions will have to create a new branch against the tagged versions. # Describe your PR @@ -23,6 +13,4 @@ If the PR is larger than ~20 lines, please describe it here unless described in # Testing Any new feature or stability improvement should be tested if possible. -Please follow the test instructions at the bottom of the README. - -*These PR guidelines will change after 2021-05-01, which is when `v2.1.4` gets onto the new ISO* +Please follow the test instructions at the bottom of the README or use the ISO built on each PR.
\ No newline at end of file diff --git a/examples/guided.py b/examples/guided.py index cc9cf5fc..6b9cfd24 100644 --- a/examples/guided.py +++ b/examples/guided.py @@ -2,10 +2,11 @@ import getpass, time, json, os import archinstall from archinstall.lib.hardware import hasUEFI from archinstall.lib.profiles import Profile +from archinstall.lib.user_interaction import generic_select -if hasUEFI() is False: - archinstall.log("ArchInstall currently only supports machines booted with UEFI.\nMBR & GRUB support is coming in version 2.2.0!", fg="red", level=archinstall.LOG_LEVELS.Error) - exit(1) +if archinstall.arguments.get('help'): + print("See `man archinstall` for help.") + exit(0) def ask_user_questions(): """ @@ -64,6 +65,7 @@ def ask_user_questions(): partition_mountpoints[partition] = None except archinstall.UnknownFilesystemFormat as err: archinstall.log(f" {partition} (Filesystem not supported)", fg='red') + # We then ask what to do with the partitions. if (option := archinstall.ask_for_disk_layout()) == 'abort': @@ -148,7 +150,7 @@ def ask_user_questions(): if (passwd := archinstall.get_password(prompt='Enter disk encryption password (leave blank for no encryption): ')): archinstall.arguments['!encryption-password'] = passwd archinstall.arguments['harddrive'].encryption_password = archinstall.arguments['!encryption-password'] - + archinstall.arguments["bootloader"] = archinstall.ask_for_bootloader() # Get the hostname for the machine if not archinstall.arguments.get('hostname', None): archinstall.arguments['hostname'] = input('Desired hostname for the installation: ').strip(' ') @@ -185,7 +187,6 @@ def ask_user_questions(): # Ask about audio server selection if one is not already set if not archinstall.arguments.get('audio', None): - # only ask for audio server selection on a desktop profile if str(archinstall.arguments['profile']) == 'Profile(desktop)': archinstall.arguments['audio'] = archinstall.ask_for_audio_selection() @@ -194,6 +195,15 @@ def ask_user_questions(): # we will not try to remove packages post-installation to not have audio, as that may cause multiple issues archinstall.arguments['audio'] = None + # Ask what kernel user wants: + if not archinstall.arguments.get("kernels", None): + archinstall.log(f"Here you can choose which kernel to use, leave blank for default which is 'linux'.") + + if (kernel := generic_select(["linux", "linux-lts", "linux-zen", "continue"], "choose a kernel:")): + archinstall.arguments['kernels'] = kernel + else: + archinstall.arguments['kernels'] = 'linux' + # Additional packages (with some light weight error handling for invalid package names) print("Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.") print("If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.") @@ -246,7 +256,11 @@ def perform_installation_steps(): Setup the blockdevice, filesystem (and optionally encryption). Once that's done, we'll hand over to perform_installation() """ - with archinstall.Filesystem(archinstall.arguments['harddrive'], archinstall.GPT) as fs: + mode = archinstall.GPT + if hasUEFI() is False: + mode = archinstall.MBR + + with archinstall.Filesystem(archinstall.arguments['harddrive'], mode) as fs: # Wipe the entire drive if the disk flag `keep_partitions`is False. if archinstall.arguments['harddrive'].keep_partitions is False: fs.use_entire_disk(root_filesystem_type=archinstall.arguments.get('filesystem', 'btrfs')) @@ -269,8 +283,8 @@ def perform_installation_steps(): partition.format() else: archinstall.log(f"Did not format {partition} because .safe_to_format() returned False or .allow_formatting was False.", level=archinstall.LOG_LEVELS.Debug) - - fs.find_partition('/boot').format('vfat') + if hasUEFI(): + fs.find_partition('/boot').format('vfat')# we don't have a boot partition in bios mode if archinstall.arguments.get('!encryption-password', None): # First encrypt and unlock, then format the desired partition inside the encrypted part. @@ -282,8 +296,8 @@ def perform_installation_steps(): else: fs.find_partition('/').format(fs.find_partition('/').filesystem) fs.find_partition('/').mount('/mnt') - - fs.find_partition('/boot').mount('/mnt/boot') + if hasUEFI(): + fs.find_partition('/boot').mount('/mnt/boot') perform_installation('/mnt') @@ -294,7 +308,7 @@ def perform_installation(mountpoint): Only requirement is that the block devices are formatted and setup prior to entering this function. """ - with archinstall.Installer(mountpoint) as installation: + with archinstall.Installer(mountpoint, kernels=archinstall.arguments.get('kernels', 'linux')) as installation: ## if len(mirrors): # Certain services might be running that affects the system during installation. # Currently, only one such service is "reflector.service" which updates /etc/pacman.d/mirrorlist @@ -302,17 +316,17 @@ def perform_installation(mountpoint): 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) - # Set mirrors used by pacstrap (outside of installation) if archinstall.arguments.get('mirror-region', None): archinstall.use_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors for the live medium - if installation.minimal_installation(): installation.set_hostname(archinstall.arguments['hostname']) if archinstall.arguments['mirror-region'].get("mirrors",{})!= None: installation.set_mirrors(archinstall.arguments['mirror-region']) # Set the mirrors in the installation medium + if archinstall.arguments["bootloader"]=="grub-install" and hasUEFI()==True: + installation.add_additional_packages("grub") installation.set_keyboard_language(archinstall.arguments['keyboard-language']) - installation.add_bootloader() + installation.add_bootloader(archinstall.arguments["bootloader"]) # If user selected to copy the current ISO network configuration # Perform a copy of the config @@ -331,6 +345,7 @@ def perform_installation(mountpoint): 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 ...') @@ -346,7 +361,7 @@ def perform_installation(mountpoint): for user, user_info in archinstall.arguments.get('users', {}).items(): installation.user_create(user, user_info["!password"], sudo=False) - + for superuser, user_info in archinstall.arguments.get('superusers', {}).items(): installation.user_create(superuser, user_info["!password"], sudo=True) @@ -356,6 +371,15 @@ def perform_installation(mountpoint): if (root_pw := archinstall.arguments.get('!root-password', None)) and len(root_pw): installation.user_set_pw('root', root_pw) + if archinstall.arguments['profile'] and archinstall.arguments['profile'].has_post_install(): + with archinstall.arguments['profile'].load_instructions(namespace=f"{archinstall.arguments['profile'].namespace}.py") as imported: + if not imported._post_install(): + archinstall.log( + ' * Profile\'s post configuration requirements was not fulfilled.', + fg='red' + ) + exit(1) + installation.log("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", fg="yellow") choice = input("Would you like to chroot into the newly created installation and perform post-installation configuration? [Y/n] ") if choice.lower() in ("y", ""): @@ -366,4 +390,3 @@ def perform_installation(mountpoint): ask_user_questions() perform_installation_steps() - diff --git a/profiles/52-54-00-12-34-56.py b/profiles/52-54-00-12-34-56.py index 679c6721..ed2c9d78 100644 --- a/profiles/52-54-00-12-34-56.py +++ b/profiles/52-54-00-12-34-56.py @@ -11,7 +11,7 @@ archinstall.sys_command(f'cryptsetup close /dev/mapper/luksloop', suppress_error harddrive = archinstall.all_disks()['/dev/sda'] disk_password = '1234' -with archinstall.Filesystem(harddrive, archinstall.GPT) as fs: +with archinstall.Filesystem(harddrive) as fs: # Use the entire disk instead of setting up partitions on your own fs.use_entire_disk('luks2') diff --git a/profiles/applications/awesome.py b/profiles/applications/awesome.py index 793ee52b..a63c707b 100644 --- a/profiles/applications/awesome.py +++ b/profiles/applications/awesome.py @@ -1,10 +1,10 @@ import archinstall +__packages__ = ["awesome", "xorg-xrandr", "xterm", "feh", "slock", "terminus-font", "gnu-free-fonts", "ttf-liberation", "xsel"] + installation.install_profile('xorg') -installation.add_additional_packages( - "awesome xorg-xrandr xterm feh slock terminus-font gnu-free-fonts ttf-liberation xsel" -) +installation.add_additional_packages(__packages__) with open(f'{installation.target}/etc/X11/xinit/xinitrc', 'r') as xinitrc: xinitrc_data = xinitrc.read() diff --git a/profiles/applications/cinnamon.py b/profiles/applications/cinnamon.py index de29aa09..0a1d9cc2 100644 --- a/profiles/applications/cinnamon.py +++ b/profiles/applications/cinnamon.py @@ -1,3 +1,3 @@ import archinstall -installation.add_additional_packages("cinnamon system-config-printer gnome-keyring gnome-terminal blueberry metacity lightdm lightdm-gtk-greeter")
\ No newline at end of file +installation.add_additional_packages("cinnamon system-config-printer gnome-keyring gnome-terminal blueberry metacity lightdm lightdm-gtk-greeter") diff --git a/profiles/applications/deepin.py b/profiles/applications/deepin.py new file mode 100644 index 00000000..0db1572d --- /dev/null +++ b/profiles/applications/deepin.py @@ -0,0 +1,5 @@ +import archinstall + +packages = "deepin deepin-terminal deepin-editor" + +installation.add_additional_packages(packages) diff --git a/profiles/applications/lxqt.py b/profiles/applications/lxqt.py index 5ce875cc..2099f3fa 100644 --- a/profiles/applications/lxqt.py +++ b/profiles/applications/lxqt.py @@ -1,3 +1,3 @@ import archinstall -installation.add_additional_packages("lxqt breeze-icons oxygen-icons xdg-utils ttf-freefont leafpad slock archlinux-wallpaper sddm") +installation.add_additional_packages("lxqt breeze-icons oxygen-icons xdg-utils ttf-freefont leafpad slock sddm") diff --git a/profiles/applications/sway.py b/profiles/applications/sway.py index 56d7f318..59921aa0 100644 --- a/profiles/applications/sway.py +++ b/profiles/applications/sway.py @@ -1,3 +1,3 @@ import archinstall -packages = "sway swaylock swayidle waybar dmenu light grim slurp pavucontrol alacritty" -installation.add_additional_packages(packages) +__packages__ = "sway swaylock swayidle waybar dmenu light grim slurp pavucontrol alacritty" +installation.add_additional_packages(__packages__) diff --git a/profiles/applications/xfce4.py b/profiles/applications/xfce4.py index e8f659c2..9f4260da 100644 --- a/profiles/applications/xfce4.py +++ b/profiles/applications/xfce4.py @@ -1,3 +1,3 @@ import archinstall - -installation.add_additional_packages("xfce4 xfce4-goodies lightdm lightdm-gtk-greeter")
\ No newline at end of file +__packages__ = "xfce4 xfce4-goodies lightdm lightdm-gtk-greeter" +installation.add_additional_packages(__packages__)
\ No newline at end of file diff --git a/profiles/deepin.py b/profiles/deepin.py new file mode 100644 index 00000000..52bcdde5 --- /dev/null +++ b/profiles/deepin.py @@ -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') diff --git a/profiles/sway.py b/profiles/sway.py index 5633cce2..53eb8c5a 100644 --- a/profiles/sway.py +++ b/profiles/sway.py @@ -11,6 +11,9 @@ def _prep_function(*args, **kwargs): other code in this stage. So it's a safe way to ask the user for more input before any other installer steps start. """ + + __builtins__['_gfx_driver_packages'] = archinstall.select_driver() + return True # Ensures that this code only gets executed if executed diff --git a/profiles/xorg.py b/profiles/xorg.py index 130f3ed0..413a6308 100644 --- a/profiles/xorg.py +++ b/profiles/xorg.py @@ -2,79 +2,9 @@ import os from archinstall import generic_select, sys_command, RequirementError - +import archinstall is_top_level_profile = True -AVAILABLE_DRIVERS = { - # Sub-dicts are layer-2 options to be selected - # and lists are a list of packages to be installed - 'AMD / ATI' : { - 'amd' : ['xf86-video-amdgpu'], - 'ati' : ['xf86-video-ati'] - }, - 'intel' : ['xf86-video-intel'], - 'nvidia' : { - 'open source' : ['xf86-video-nouveau'], - 'proprietary' : ['nvidia'] - }, - 'mesa' : ['mesa'], - 'fbdev' : ['xf86-video-fbdev'], - 'vesa' : ['xf86-video-vesa'], - 'vmware' : ['xf86-video-vmware'] -} - -def select_driver(options): - """ - Some what convoluted function, which's job is simple. - Select a graphics driver from a pre-defined set of popular options. - - (The template xorg is for beginner users, not advanced, and should - there for appeal to the general public first and edge cases later) - """ - drivers = sorted(list(options)) - - if len(drivers) >= 1: - for index, driver in enumerate(drivers): - print(f"{index}: {driver}") - - print(' -- The above list are supported graphic card drivers. --') - print(' -- You need to select (and read about) which one you need. --') - - lspci = 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(drivers, 'Select your graphics card driver: ', - allow_empty_input=False, options_output=False) - 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) - - selected_driver = options[selected_driver] - - if type(selected_driver) == dict: - driver_options = sorted(list(selected_driver)) - - driver_package_group = generic_select(driver_options, f'Which driver-type do you want for {initial_option}: ', - allow_empty_input=False) - driver_package_group = selected_driver[driver_package_group] - - return driver_package_group - - return selected_driver - - raise RequirementError("Selecting drivers require a least one profile to be given as an option.") - def _prep_function(*args, **kwargs): """ Magic function called by the importing installer @@ -82,10 +12,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. diff --git a/pyproject.toml b/pyproject.toml index 9787c3bd..73c7a876 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,30 @@ [build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["flit_core >=2,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.metadata] +module = "archinstall" +author = "Anton Hvornum" +author-email = "anton@hvornum.se" +home-page = "https://archlinux.org" +classifiers = [ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", +"Programming Language :: Python :: 3.8", +"Programming Language :: Python :: 3.9", +"License :: OSI Approved :: GNU General Public License v3 (GPLv3)", +"Operating System :: POSIX :: Linux", +] +description-file = "README.md" +requires-python=">=3.8" +[tool.flit.metadata.urls] +Source = "https://github.com/archlinux/archinstall" +Documentation = "https://archinstall.readthedocs.io/" + +[tool.flit.scripts] +archinstall = "archinstall:run_as_a_module" + +[tool.flit.sdist] +include = ["docs/","profiles"] +exclude = ["docs/*.html", "docs/_static","docs/*.png","docs/*.psd"] + +[tool.flit.metadata.requires-extra] +doc = ["sphinx"]
\ No newline at end of file |