diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 7daefd36af..05b9821826 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -22,3 +22,5 @@ jobs: run: pip install --break-system-packages .[dev] - name: Test with pytest run: pytest + - name: Check generated package inventory + run: python -m archinstall.lib.packages.meta --check diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 794d042056..3b31f70eef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,6 +50,12 @@ This will install the pre-commit hook and run it every time a `git commit` is ex If you'd like to contribute to the documentation, refer to [this guide](docs/README.md) on how to build the documentation locally. +## Package inventory + +`archinstall-meta/PKGBUILD` is generated from the installer package choices, applications, and profiles. Add installer-only package choices to `InstallationPackage`; profiles and application package properties are discovered automatically. New profile settings must also be covered in `PROFILE_SETTINGS` in `archinstall/lib/packages/meta.py`. + +After changing package choices, run `python -m archinstall.lib.packages.meta` on Arch Linux with an up-to-date package database, and commit the generated PKGBUILD with the source change. The pytest suite rejects untracked literal package choices and checks that source targets match the committed inventory. The CI pytest job also runs `python -m archinstall.lib.packages.meta --check` against its refreshed package database, so changes in repository groups are detected. Regenerate the file when that check reports drift; do not edit its dependency list by hand. + ## Submitting Changes Archinstall uses GitHub's pull-request workflow and all contributions in terms of code should be done through pull requests.
diff --git a/archinstall-meta/PKGBUILD b/archinstall-meta/PKGBUILD new file mode 100644 index 0000000000..9da6c9d2e3 --- /dev/null +++ b/archinstall-meta/PKGBUILD @@ -0,0 +1,545 @@ +# Generated by python -m archinstall.lib.packages.meta + +pkgname=archinstall-meta +pkgver=1 +pkgrel=1 +pkgdesc="Package inventory for archinstall" +arch=(any) +url="https://github.com/archlinux/archinstall" +license=(GPL-3.0-only) +depends=(archinstall) +optdepends=( + alacritty + alsa-firmware + alsa-utils + amd-ucode + apache + awesome + base + bluez + bluez-utils + breeze-icons + brightnessctl + brltty + bspwm + btrfs-progs + cava + cinnamon + cockpit + cockpit-packagekit + cockpit-storaged + cosmic-greeter + cronie + cups + cups-pk-helper + deepin-editor + deepin-terminal + dkms + dmenu + dms-shell + docker + dolphin + dunst + efibootmgr + engrampa + enlightenment + espeakup + f2fs-tools + feh + firewalld + foot + fuzzel + gdm + ghostscript + git + gnome-keyring + gnome-screenshot + gnome-terminal + gnome-tweaks + gnu-free-fonts + greetd + grim + grub + grub-btrfs + gst-plugin-pipewire + gvfs + gvfs-smb + htop + hyprland + i3-wm + i3blocks + i3lock + i3status + inotify-tools + intel-media-driver + intel-ucode + iwd + kimageformats + kitty + l3afpad + labwc + libfido2 + libpulse + libva-intel-driver + libva-nvidia-driver + libvpl + lightdm + lightdm-gtk-greeter + lightdm-slick-greeter + lighttpd + limine + linux + linux-firmware + linux-firmware-liquidio + linux-firmware-marvell + linux-firmware-mellanox + linux-firmware-nfp + linux-firmware-qcom + linux-firmware-qlogic + linux-hardened + linux-hardened-headers + linux-headers + linux-lts + linux-lts-headers + linux-rt + linux-rt-headers + linux-rt-lts + linux-rt-lts-headers + linux-zen + linux-zen-headers + lvm2 + ly + mako + mariadb + mate-terminal + materia-gtk-theme + matugen + mesa + mkinitcpio + nano + nemo + nemo-fileroller + network-manager-applet + networkmanager + nginx + niri + noto-fonts + noto-fonts-cjk + noto-fonts-emoji + nvidia-open + nvidia-open-dkms + openssh + oxygen-icons + pam-u2f + papirus-icon-theme + pavucontrol + pipewire + pipewire-alsa + pipewire-jack + pipewire-pulse + plasma-desktop + plasma-login-manager + plasma-meta + plymouth + polkit + polkit-kde-agent + postgresql + power-profiles-daemon + pulseaudio + qt5-wayland + qt6-wayland + qtile + refind + river + rxvt-unicode + sddm + seatd + slock + slurp + smartmontools + snapper + sof-firmware + sudo + sway + swaybg + swayidle + swaylock + sxhkd + system-config-printer + terminology + terminus-font + timeshift + tomcat10 + ttf-dejavu + ttf-freefont + ttf-liberation + tuned + tuned-ppd + ufw + uwsm + vim + vpl-gpu-rt + vulkan-intel + vulkan-nouveau + vulkan-radeon + waybar + wget + wireplumber + wmenu + wofi + wpa_supplicant + xarchiver + xdg-desktop-portal-gnome + xdg-desktop-portal-hyprland + xdg-desktop-portal-wlr + xdg-user-dirs + xdg-user-dirs-gtk + xdg-utils + xdo + xed + xf86-video-amdgpu + xf86-video-ati + xf86-video-nouveau + xfsprogs + xmonad + xmonad-contrib + xmonad-extras + xorg-server + xorg-xinit + xorg-xrandr + xorg-xwayland + xsel + xss-lock + xterm + zram-generator + # group: budgie + budgie-backgrounds + budgie-control-center + budgie-desktop + budgie-desktop-services + budgie-desktop-view + budgie-session + # group: cosmic + cosmic-app-library + cosmic-applets + cosmic-bg + cosmic-comp + cosmic-files + cosmic-idle + cosmic-initial-setup + cosmic-launcher + cosmic-monitor + cosmic-notifications + cosmic-osd + cosmic-osk + cosmic-panel + cosmic-player + cosmic-randr + cosmic-screenshot + cosmic-session + cosmic-settings + cosmic-settings-daemon + cosmic-sound-theme + cosmic-store + cosmic-terminal + cosmic-text-editor + cosmic-viewer + cosmic-wallpapers + cosmic-workspaces + xdg-desktop-portal-cosmic + # group: deepin + ddm + deepin-account-faces + deepin-anything + deepin-api + deepin-app-services + deepin-appearance + deepin-application-manager + deepin-calendar + deepin-control-center + deepin-daemon + deepin-desktop-base + deepin-desktop-schemas + deepin-desktop-theme + deepin-fcitx5configtool-plugin + deepin-file-manager + deepin-gtk-theme + deepin-icon-theme + deepin-image-viewer + deepin-launchpad + deepin-menu + deepin-network-core + deepin-polkit-agent + deepin-polkit-agent-ext-gnomekeyring + deepin-pw-check + deepin-qt5integration + deepin-qt5platform-plugins + deepin-qt6integration + deepin-qt6platform-plugins + deepin-screensaver + deepin-seatd + deepin-services + deepin-session + deepin-session-shell + deepin-session-ui + deepin-shell + deepin-sound-theme + deepin-system-monitor + deepin-turbo + deepin-wallpapers + deepin-widgets + docparser + dtk6core + dtk6declarative + dtk6gui + dtk6log + dtk6widget + dtkcore + dtkdeclarative + dtkgui + dtklog + dtkwidget + treeland + xdg-desktop-portal-dde + # group: gnome + baobab + decibels + epiphany + gnome-backgrounds + gnome-calculator + gnome-calendar + gnome-characters + gnome-clocks + gnome-color-manager + gnome-connections + gnome-console + gnome-contacts + gnome-control-center + gnome-disk-utility + gnome-font-viewer + gnome-logs + gnome-maps + gnome-menus + gnome-music + gnome-remote-desktop + gnome-session + gnome-settings-daemon + gnome-shell + gnome-software + gnome-system-monitor + gnome-text-editor + gnome-tour + gnome-user-docs + gnome-user-share + gnome-weather + grilo-plugins + gst-thumbnailers + gvfs-afc + gvfs-dnssd + gvfs-goa + gvfs-gphoto2 + gvfs-mtp + gvfs-nfs + gvfs-onedrive + gvfs-wsdd + loupe + malcontent + nautilus + orca + papers + rygel + showtime + simple-scan + snapshot + sushi + tecla + yelp + # group: lxqt + lximage-qt + lxqt-about + lxqt-admin + lxqt-archiver + lxqt-config + lxqt-globalkeys + lxqt-menu-data + lxqt-notificationd + lxqt-openssh-askpass + lxqt-panel + lxqt-policykit + lxqt-powermanagement + lxqt-qtplugin + lxqt-runner + lxqt-session + lxqt-sudo + lxqt-themes + obconf-qt + openbox + pavucontrol-qt + pcmanfm-qt + qps + qterminal + screengrab + xdg-desktop-portal-lxqt + # group: mate + caja + marco + mate-backgrounds + mate-control-center + mate-desktop + mate-icon-theme + mate-menus + mate-notification-daemon + mate-panel + mate-polkit + mate-session-manager + mate-settings-daemon + mate-themes + mate-user-guide + # group: mate-extra + atril + caja-actions + caja-audio-video-properties + caja-image-converter + caja-open-terminal + caja-sendto + caja-share + caja-wallpaper + caja-xattr-tags + eom + mate-applets + mate-calc + mate-media + mate-netbook + mate-power-manager + mate-screensaver + mate-sensors-applet + mate-system-monitor + mate-user-share + mate-utils + mozo + pluma + # group: plasma + aurorae + bluedevil + breeze + breeze-cursors + breeze-gtk + breeze-plymouth + discover + drkonqi + flatpak-kcm + kactivitymanagerd + kde-cli-tools + kde-gtk-config + kdecoration + kdeplasma-addons + kgamma + kglobalacceld + kinfocenter + kmenuedit + knighttime + kpipewire + krdp + kscreen + kscreenlocker + ksshaskpass + ksystemstats + kwallet-pam + kwayland + kwin + kwin-x11 + kwrited + layer-shell-qt + libkscreen + libksysguard + libplasma + milou + ocean-sound-theme + oxygen + oxygen-cursors + oxygen-sounds + plasma-activities + plasma-activities-stats + plasma-bigscreen + plasma-browser-integration + plasma-disks + plasma-firewall + plasma-integration + plasma-keyboard + plasma-nm + plasma-pa + plasma-sdk + plasma-systemmonitor + plasma-thunderbolt + plasma-vault + plasma-welcome + plasma-workspace + plasma-workspace-wallpapers + plasma5support + plymouth-kcm + powerdevil + print-manager + qqc2-breeze-style + sddm-kcm + spectacle + systemsettings + union + wacomtablet + xdg-desktop-portal-kde + # group: xfce4 + exo + garcon + thunar + thunar-volman + tumbler + xfce4-appfinder + xfce4-panel + xfce4-power-manager + xfce4-session + xfce4-settings + xfce4-terminal + xfconf + xfdesktop + xfwm4 + # group: xfce4-goodies + mousepad + parole + ristretto + thunar-archive-plugin + thunar-media-tags-plugin + xfburn + xfce4-battery-plugin + xfce4-clipman-plugin + xfce4-cpufreq-plugin + xfce4-cpugraph-plugin + xfce4-dict + xfce4-diskperf-plugin + xfce4-eyes-plugin + xfce4-fsguard-plugin + xfce4-genmon-plugin + xfce4-mailwatch-plugin + xfce4-mount-plugin + xfce4-mpc-plugin + xfce4-netload-plugin + xfce4-notes-plugin + xfce4-notifyd + xfce4-places-plugin + xfce4-pulseaudio-plugin + xfce4-screensaver + xfce4-screenshooter + xfce4-sensors-plugin + xfce4-smartbookmark-plugin + xfce4-systemload-plugin + xfce4-taskmanager + xfce4-time-out-plugin + xfce4-timer-plugin + xfce4-verve-plugin + xfce4-wavelan-plugin + xfce4-weather-plugin + xfce4-whiskermenu-plugin + xfce4-xkb-plugin +) + +package() { + : +} diff --git a/archinstall/applications/audio.py b/archinstall/applications/audio.py index e5d7971825..720444868f 100644 --- a/archinstall/applications/audio.py +++ b/archinstall/applications/audio.py @@ -3,6 +3,7 @@ from archinstall.lib.hardware import SysInfo from archinstall.lib.log import debug from archinstall.lib.models.application import Audio, AudioConfiguration +from archinstall.lib.models.package_types import InstallationPackage from archinstall.lib.models.users import User if TYPE_CHECKING: @@ -67,10 +68,10 @@ def install( return if SysInfo.requires_sof_fw(): - install_session.add_additional_packages('sof-firmware') + install_session.add_additional_packages(InstallationPackage.SOF_FIRMWARE.value) if SysInfo.requires_alsa_fw(): - install_session.add_additional_packages('alsa-firmware') + install_session.add_additional_packages(InstallationPackage.ALSA_FIRMWARE.value) match audio_config.audio: case Audio.PIPEWIRE: diff --git a/archinstall/default_profiles/desktop.py b/archinstall/default_profiles/desktop.py index 1196d47742..ef4f03f478 100644 --- a/archinstall/default_profiles/desktop.py +++ b/archinstall/default_profiles/desktop.py @@ -4,6 +4,7 @@ from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType, SelectResult from archinstall.lib.log import info from archinstall.lib.menu.helpers import Selection +from archinstall.lib.models.package_types import InstallationPackage from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.result import ResultType @@ -112,7 +113,7 @@ def install(self, install_session: Installer) -> None: install_session.enable_service(profile.services) if not xorg_installed and profile.display_server == DisplayServerType.Xorg: - install_session.add_additional_packages(['xorg-server', 'xorg-xinit']) + install_session.add_additional_packages([InstallationPackage.XORG_SERVER.value, InstallationPackage.XORG_XINIT.value]) xorg_installed = True profile.install(install_session) diff --git a/archinstall/default_profiles/desktops/niri_dms.py b/archinstall/default_profiles/desktops/niri_dms.py index bcb8387afe..da3ae6fcd5 100644 --- a/archinstall/default_profiles/desktops/niri_dms.py +++ b/archinstall/default_profiles/desktops/niri_dms.py @@ -27,7 +27,7 @@ def __init__(self) -> None: def packages(self) -> list[str]: return [ 'niri', - 'dms-shell-niri', + 'dms-shell', 'polkit', 'xdg-desktop-portal-gnome', 'xorg-xwayland', diff --git a/archinstall/lib/authentication/authentication_handler.py b/archinstall/lib/authentication/authentication_handler.py index bae192fd25..b315148655 100644 --- a/archinstall/lib/authentication/authentication_handler.py +++ b/archinstall/lib/authentication/authentication_handler.py @@ -5,6 +5,7 @@ from archinstall.lib.command import SysCommandWorker from archinstall.lib.log import debug, info from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod +from archinstall.lib.models.package_types import InstallationPackage from archinstall.lib.models.users import User from archinstall.lib.translationhandler import tr @@ -79,7 +80,7 @@ def _configure_u2f_mapping( ) -> None: debug(f'Setting up U2F login: {u2f_config.u2f_login_method.value}') - install_session.pacman.strap('pam-u2f') + install_session.pacman.strap(InstallationPackage.PAM_U2F.value) print(tr('Setting up U2F login: {}').format(u2f_config.u2f_login_method.value)) diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 11b89a9cad..c7a848e669 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -55,7 +55,7 @@ from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.mirrors import MirrorConfiguration from archinstall.lib.models.network import Nic -from archinstall.lib.models.package_types import DEFAULT_KERNEL, Kernel +from archinstall.lib.models.package_types import DEFAULT_KERNEL, InstallationPackage, Kernel from archinstall.lib.models.packages import Repository from archinstall.lib.models.pacman import PacmanConfiguration from archinstall.lib.models.users import User @@ -793,7 +793,7 @@ def copy_iso_network_config(self, enable_services: bool = False) -> bool: psk.copy(iwd_target / psk.name, preserve_metadata=True) if enable_services: - iwd = 'iwd' + iwd = InstallationPackage.IWD.value # 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) @@ -877,7 +877,7 @@ def _prepare_fs_type(self, fs_type: FilesystemType) -> None: def _prepare_encrypt(self, before: str = 'filesystems') -> None: if self._disk_encryption.hsm_device: # Required by mkinitcpio to add support for fido2-device options - self.pacman.strap('libfido2') + self.pacman.strap(InstallationPackage.LIBFIDO2.value) if 'sd-encrypt' not in self._hooks: self._hooks.insert(self._hooks.index(before), 'sd-encrypt') @@ -894,7 +894,7 @@ def minimal_installation( pacman_config: PacmanConfiguration | None = None, ) -> None: if self._disk_config.lvm_config: - lvm = 'lvm2' + lvm = InstallationPackage.LVM2.value self.add_additional_packages(lvm) self._hooks.insert(self._hooks.index('filesystems') - 1, lvm) @@ -988,7 +988,7 @@ def setup_btrfs_snapshot( ) -> None: if snapshot_type == SnapshotType.Snapper: debug('Setting up Btrfs snapper') - self.pacman.strap('snapper') + self.pacman.strap(InstallationPackage.SNAPPER.value) snapper: dict[str, str] = { 'root': '/', @@ -1009,20 +1009,20 @@ def setup_btrfs_snapshot( elif snapshot_type == SnapshotType.Timeshift: debug('Setting up Btrfs timeshift') - self.pacman.strap('cronie') - self.pacman.strap('timeshift') + self.pacman.strap(InstallationPackage.CRONIE.value) + self.pacman.strap(InstallationPackage.TIMESHIFT.value) self.enable_service('cronie.service') if bootloader and bootloader == Bootloader.Grub: debug('Setting up grub integration for either') - self.pacman.strap('grub-btrfs') - self.pacman.strap('inotify-tools') + self.pacman.strap(InstallationPackage.GRUB_BTRFS.value) + self.pacman.strap(InstallationPackage.INOTIFY_TOOLS.value) self._configure_grub_btrfsd(snapshot_type) self.enable_service('grub-btrfsd.service') def setup_swap(self, algo: ZramAlgorithm = ZramAlgorithm.ZSTD) -> None: info('Setting up swap on zram') - self.pacman.strap('zram-generator') + self.pacman.strap(InstallationPackage.ZRAM_GENERATOR.value) info(f'Zram compression algorithm: {algo.value}') @@ -1245,7 +1245,7 @@ def _add_systemd_bootloader( ) -> None: debug('Installing systemd bootloader') - self.pacman.strap('efibootmgr') + self.pacman.strap(InstallationPackage.EFIBOOTMGR.value) if not SysInfo.has_uefi(): raise HardwareIncompatibilityError @@ -1336,7 +1336,7 @@ def _add_grub_bootloader( ) -> None: debug('Installing grub bootloader') - self.pacman.strap('grub') + self.pacman.strap(InstallationPackage.GRUB.value) info(f'GRUB boot partition: {boot_partition.dev_path}') @@ -1350,7 +1350,7 @@ def _add_grub_bootloader( info(f'GRUB EFI partition: {efi_partition.dev_path}') - self.pacman.strap('efibootmgr') # TODO: Do we need? Yes, but remove from minimal_installation() instead? + self.pacman.strap(InstallationPackage.EFIBOOTMGR.value) boot_dir_arg = [] if boot_partition.mountpoint and boot_partition.mountpoint != boot_dir: @@ -1451,7 +1451,7 @@ def _add_limine_bootloader( ) -> None: debug('Installing Limine bootloader') - self.pacman.strap('limine') + self.pacman.strap(InstallationPackage.LIMINE.value) info(f'Limine boot partition: {boot_partition.dev_path}') @@ -1460,7 +1460,7 @@ def _add_limine_bootloader( hook_command = None if SysInfo.has_uefi(): - self.pacman.strap('efibootmgr') + self.pacman.strap(InstallationPackage.EFIBOOTMGR.value) if not efi_partition: raise ValueError('Could not detect efi partition') @@ -1616,7 +1616,7 @@ def _add_efistub_bootloader( ) -> None: debug('Installing efistub bootloader') - self.pacman.strap('efibootmgr') + self.pacman.strap(InstallationPackage.EFIBOOTMGR.value) if not SysInfo.has_uefi(): raise HardwareIncompatibilityError @@ -1672,7 +1672,7 @@ def _add_refind_bootloader( ) -> None: debug('Installing rEFInd bootloader') - self.pacman.strap('refind') + self.pacman.strap(InstallationPackage.REFIND.value) if not SysInfo.has_uefi(): raise HardwareIncompatibilityError @@ -1765,7 +1765,7 @@ def _add_refind_bootloader( def _install_plymouth(self, plymouth: PlymouthTheme) -> None: debug(f'Installing plymouth with theme: {plymouth.value}') - self.add_additional_packages(['plymouth']) + self.add_additional_packages([InstallationPackage.PLYMOUTH.value]) for param in ('quiet', 'splash'): if param not in self._kernel_params: @@ -2038,7 +2038,7 @@ def set_vconsole(self, locale_config: LocaleConfiguration) -> None: font_vconsole = locale_config.console_font if font_vconsole.startswith('ter-'): - self.pacman.strap(['terminus-font']) + self.pacman.strap([InstallationPackage.TERMINUS_FONT.value]) # Ensure /etc exists vconsole_dir: Path = self.target / 'etc' diff --git a/archinstall/lib/models/package_types.py b/archinstall/lib/models/package_types.py index 73dd00eea7..e1b7988d0a 100644 --- a/archinstall/lib/models/package_types.py +++ b/archinstall/lib/models/package_types.py @@ -16,6 +16,44 @@ class Kernel(StrEnum): DEFAULT_KERNEL: Final = Kernel.LINUX +class InstallationPackage(StrEnum): + ALSA_FIRMWARE = 'alsa-firmware' + COSMIC_GREETER = 'cosmic-greeter' + CRONIE = 'cronie' + EFIBOOTMGR = 'efibootmgr' + GDM = 'gdm' + GIT = 'git' + GREETD = 'greetd' + GRUB = 'grub' + GRUB_BTRFS = 'grub-btrfs' + INOTIFY_TOOLS = 'inotify-tools' + IWD = 'iwd' + LIBFIDO2 = 'libfido2' + LIGHTDM = 'lightdm' + LIGHTDM_GTK_GREETER = 'lightdm-gtk-greeter' + LIGHTDM_SLICK_GREETER = 'lightdm-slick-greeter' + LIMINE = 'limine' + LVM2 = 'lvm2' + LY = 'ly' + NANO = 'nano' + NETWORK_MANAGER_APPLET = 'network-manager-applet' + NETWORKMANAGER = 'networkmanager' + PAM_U2F = 'pam-u2f' + PLASMA_LOGIN_MANAGER = 'plasma-login-manager' + PLYMOUTH = 'plymouth' + REFIND = 'refind' + SDDM = 'sddm' + SNAPPER = 'snapper' + SOF_FIRMWARE = 'sof-firmware' + TERMINUS_FONT = 'terminus-font' + TIMESHIFT = 'timeshift' + WGET = 'wget' + WPA_SUPPLICANT = 'wpa_supplicant' + XORG_SERVER = 'xorg-server' + XORG_XINIT = 'xorg-xinit' + ZRAM_GENERATOR = 'zram-generator' + + class FirmwareOptdep(StrEnum): """ The optional dependencies of linux-firmware. diff --git a/archinstall/lib/network/network_handler.py b/archinstall/lib/network/network_handler.py index 0df7c7870e..c0a5ad2919 100644 --- a/archinstall/lib/network/network_handler.py +++ b/archinstall/lib/network/network_handler.py @@ -2,6 +2,7 @@ from archinstall.lib.installer import Installer from archinstall.lib.models.network import NetworkConfiguration, NicType +from archinstall.lib.models.package_types import InstallationPackage from archinstall.lib.models.profile import ProfileConfiguration @@ -15,16 +16,16 @@ def install_network_config( # Sources the ISO network configuration to the install medium. installation.copy_iso_network_config(enable_services=True) case NicType.NM | NicType.NM_IWD: - packages = ['networkmanager'] + packages = [InstallationPackage.NETWORKMANAGER.value] if network_config.type == NicType.NM: - packages.append('wpa_supplicant') + packages.append(InstallationPackage.WPA_SUPPLICANT.value) else: - packages.append('iwd') + packages.append(InstallationPackage.IWD.value) if profile_config and profile_config.profile: if profile_config.profile.is_desktop_profile(): - packages.append('network-manager-applet') + packages.append(InstallationPackage.NETWORK_MANAGER_APPLET.value) installation.add_additional_packages(packages) installation.enable_service('NetworkManager.service') @@ -34,7 +35,7 @@ def install_network_config( installation.disable_service('iwd.service') case NicType.IWD: - installation.add_additional_packages(['iwd']) + installation.add_additional_packages([InstallationPackage.IWD.value]) _configure_iwd_standalone(installation) installation.enable_service('iwd.service') installation.enable_service('systemd-networkd.service') diff --git a/archinstall/lib/packages/meta.py b/archinstall/lib/packages/meta.py new file mode 100644 index 0000000000..e945de3148 --- /dev/null +++ b/archinstall/lib/packages/meta.py @@ -0,0 +1,120 @@ +import argparse +import inspect +import shlex +import subprocess +from enum import Enum +from itertools import product +from pathlib import Path + +from archinstall.default_profiles.desktops.plasma import PlasmaFlavor +from archinstall.default_profiles.desktops.utils import SeatAccess +from archinstall.default_profiles.profile import CustomSetting +from archinstall.lib.applications import application_handler +from archinstall.lib.hardware import CPUVendor, GfxPackage +from archinstall.lib.installer import __accessibility_packages__, __packages__ +from archinstall.lib.models.application import FontPackage +from archinstall.lib.models.device import FilesystemType +from archinstall.lib.models.package_types import FirmwareOptdep, InstallationPackage, Kernel +from archinstall.lib.profile.profiles_handler import ProfileHandler + +PROFILE_SETTINGS: dict[CustomSetting, type[Enum]] = { + CustomSetting.SeatAccess: SeatAccess, + CustomSetting.PlasmaFlavor: PlasmaFlavor, +} + + +def package_targets() -> set[str]: + if set(PROFILE_SETTINGS) != set(CustomSetting): + raise ValueError('Add missing custom settings to PROFILE_SETTINGS') + + packages = set(__packages__ + __accessibility_packages__) + for package_enum in (FontPackage, GfxPackage, FirmwareOptdep, InstallationPackage): + packages.update(choice.value for choice in package_enum) + packages.update(f'{kernel.value}-headers' for kernel in Kernel) + packages.update(package for fs in FilesystemType if (package := fs.installation_pkg)) + packages.update(ucode.stem for vendor in CPUVendor if (ucode := vendor.get_ucode())) + + for profile in ProfileHandler().profiles: + original = profile.custom_settings + try: + packages.update(profile.packages) + for choices in product(*PROFILE_SETTINGS.values()): + profile.custom_settings = original | dict(zip(PROFILE_SETTINGS, (choice.value for choice in choices), strict=True)) + packages.update(profile.packages) + finally: + profile.custom_settings = original + + for cls in vars(application_handler).values(): + if inspect.isclass(cls) and cls.__module__.startswith('archinstall.applications.'): + app = cls() + for name, member in inspect.getmembers(cls): + if isinstance(member, property) and (name == 'packages' or name.endswith('_packages')): + packages.update(getattr(app, name)) + return packages + + +def pacman(*args: str) -> list[str]: + return subprocess.run(['pacman', *args], check=True, capture_output=True, text=True).stdout.splitlines() + + +def repository_groups() -> tuple[set[str], dict[str, set[str]]]: + names = set(pacman('-Slq')) + if not names: + raise ValueError('No package database available; refresh it before generating the inventory') + groups: dict[str, set[str]] = {} + for line in pacman('-Sgg'): + group, package = line.split() + groups.setdefault(group, set()).add(package) + return names, groups + + +def render(targets: set[str], names: set[str], groups: dict[str, set[str]]) -> str: + selected_groups = (targets & groups.keys()) - names + direct = targets - selected_groups + entries = [f'\t{shlex.quote(package)}' for package in sorted(direct)] + written = set(direct) + for group in sorted(selected_groups): + entries.append(f'\t# group: {group}') + entries.extend(f'\t{shlex.quote(package)}' for package in sorted(groups[group] - written)) + written.update(groups[group]) + + dependencies = '\n'.join(entries) + return f"""# Generated by python -m archinstall.lib.packages.meta + +pkgname=archinstall-meta +pkgver=1 +pkgrel=1 +pkgdesc="Package inventory for archinstall" +arch=(any) +url="https://github.com/archlinux/archinstall" +license=(GPL-3.0-only) +depends=(archinstall) +optdepends=( +{dependencies} +) + +package() {{ + : +}} +""" + + +def main() -> None: + parser = argparse.ArgumentParser(description='Generate archinstall-meta from the installation code and local package database.') + parser.add_argument('--check', action='store_true', help='fail if the generated PKGBUILD differs') + parser.add_argument('--output', type=Path, default=Path(__file__).resolve().parents[3] / 'archinstall-meta/PKGBUILD') + args = parser.parse_args() + targets = package_targets() + names, groups = repository_groups() + if aliases := sorted(targets - names - groups.keys()): + pacman('-Spdd', '--noconfirm', '--print-format', '%n', '--', *aliases) + content = render(targets, names, groups) + if args.check: + if args.output.read_text() != content: + parser.exit(1, 'Run python -m archinstall.lib.packages.meta to update archinstall-meta/PKGBUILD\n') + else: + args.output.write_text(content) + + +if __name__ == '__main__': + main() diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index d09364fb33..19b4038ae8 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -10,6 +10,7 @@ from archinstall.default_profiles.profile import CustomSetting, GreeterType, Profile from archinstall.lib.hardware import GfxDriver, GfxPackage from archinstall.lib.log import debug, error, info +from archinstall.lib.models.package_types import InstallationPackage from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.networking import fetch_data_from_url from archinstall.lib.translationhandler import tr @@ -153,29 +154,29 @@ def install_greeter(self, install_session: Installer, greeter: GreeterType) -> N match greeter: case GreeterType.LightdmSlick: - packages = ['lightdm', 'lightdm-slick-greeter'] + packages = [InstallationPackage.LIGHTDM.value, InstallationPackage.LIGHTDM_SLICK_GREETER.value] service = ['lightdm'] case GreeterType.Lightdm: - packages = ['lightdm', 'lightdm-gtk-greeter'] + packages = [InstallationPackage.LIGHTDM.value, InstallationPackage.LIGHTDM_GTK_GREETER.value] service = ['lightdm'] case GreeterType.Sddm: - packages = ['sddm'] + packages = [InstallationPackage.SDDM.value] service = ['sddm'] case GreeterType.Gdm: - packages = ['gdm'] + packages = [InstallationPackage.GDM.value] service = ['gdm'] case GreeterType.Ly: - packages = ['ly'] + packages = [InstallationPackage.LY.value] service = ['ly@tty1'] service_disable = ['getty@tty1'] case GreeterType.CosmicSession: - packages = ['cosmic-greeter'] + packages = [InstallationPackage.COSMIC_GREETER.value] service = ['cosmic-greeter'] case GreeterType.PlasmaLoginManager: - packages = ['plasma-login-manager'] + packages = [InstallationPackage.PLASMA_LOGIN_MANAGER.value] service = ['plasmalogin'] case GreeterType.GreetdDms: - packages = ['greetd'] + packages = [InstallationPackage.GREETD.value] service = ['greetd'] if packages: diff --git a/archinstall/locales/ur/LC_MESSAGES/base.mo b/archinstall/locales/ur/LC_MESSAGES/base.mo index e526215491..3a07585a2b 100644 Binary files a/archinstall/locales/ur/LC_MESSAGES/base.mo and b/archinstall/locales/ur/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/ur/LC_MESSAGES/base.po b/archinstall/locales/ur/LC_MESSAGES/base.po index a6689a517c..06b0fc0fe8 100644 --- a/archinstall/locales/ur/LC_MESSAGES/base.po +++ b/archinstall/locales/ur/LC_MESSAGES/base.po @@ -996,7 +996,7 @@ msgstr "{} graphics driver" #, python-brace-format msgid "{} greeter" -msgstr "Greeter" +msgstr "{} Greeter" msgid "very weak" msgstr "Buhat kamzor" diff --git a/archinstall/scripts/minimal.py b/archinstall/scripts/minimal.py index f257d4d4a5..05d14f25c5 100644 --- a/archinstall/scripts/minimal.py +++ b/archinstall/scripts/minimal.py @@ -7,6 +7,7 @@ from archinstall.lib.log import debug, error, info from archinstall.lib.menu.util import delayed_warning from archinstall.lib.models import Bootloader +from archinstall.lib.models.package_types import InstallationPackage from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.users import Password, User from archinstall.lib.network.network_handler import install_network_config @@ -46,7 +47,7 @@ def perform_installation(arch_config_handler: ArchConfigHandler) -> None: config.profile_config, ) - installation.add_additional_packages(['nano', 'wget', 'git']) + installation.add_additional_packages([InstallationPackage.NANO.value, InstallationPackage.WGET.value, InstallationPackage.GIT.value]) profile_config = ProfileConfiguration(MinimalProfile()) profile_handler.install_profile_config(installation, profile_config) diff --git a/tests/test_package_inventory.py b/tests/test_package_inventory.py new file mode 100644 index 0000000000..3086f73b9a --- /dev/null +++ b/tests/test_package_inventory.py @@ -0,0 +1,158 @@ +import ast +import shlex +import sys +from enum import StrEnum +from pathlib import Path + +import pytest + +from archinstall.default_profiles.profile import CustomSetting, Profile, ProfileType +from archinstall.lib.applications import application_handler +from archinstall.lib.models.package_types import InstallationPackage +from archinstall.lib.packages import meta +from archinstall.lib.profile.profiles_handler import ProfileHandler + + +def test_committed_source_targets() -> None: + path = Path(__file__).resolve().parents[1] / 'archinstall-meta/PKGBUILD' + entries = path.read_text().split('optdepends=(\n', 1)[1].split('\n)', 1)[0] + targets = set() + in_group = False + for raw_line in entries.splitlines(): + line = raw_line.strip() + if line.startswith('# group: '): + targets.add(line.removeprefix('# group: ')) + in_group = True + elif not in_group: + targets.update(shlex.split(line)) + assert targets == meta.package_targets(), 'Run python -m archinstall.lib.packages.meta' + + +def test_conditional_package_choices() -> None: + targets = meta.package_targets() + assert {'seatd', 'polkit', 'plasma', 'plasma-meta', 'plasma-desktop'} <= targets + assert {'amd-ucode', 'intel-ucode', 'linux-lts-headers', 'btrfs-progs', 'brltty', 'network-manager-applet', 'dms-shell'} <= targets + assert {'linux-firmware-qcom', 'linux-firmware-mellanox'} <= targets + + +def test_profile_settings_are_preserved(monkeypatch: pytest.MonkeyPatch) -> None: + profile = Profile('New', ProfileType.Custom, packages=['new-profile']) + profile.custom_settings = {CustomSetting.SeatAccess: 'seatd'} + monkeypatch.setattr(ProfileHandler, 'profiles', property(lambda _self: [profile])) + before = profile.custom_settings.copy() + assert 'new-profile' in meta.package_targets() + assert profile.custom_settings == before + + +def test_new_sources_are_discovered(monkeypatch: pytest.MonkeyPatch) -> None: + before = meta.package_targets() + profiles = [*ProfileHandler().profiles, Profile('New', ProfileType.Custom, packages=['new-profile'])] + monkeypatch.setattr(ProfileHandler, 'profiles', property(lambda _self: profiles)) + + class NewApp: + @property + def packages(self) -> list[str]: + return ['new-app'] + + NewApp.__module__ = 'archinstall.applications.new' + monkeypatch.setattr(application_handler, 'NewApp', NewApp, raising=False) + choices = {package.name: package.value for package in InstallationPackage} | {'NEW': 'new-choice'} + monkeypatch.setattr(meta, 'InstallationPackage', StrEnum('NewPackages', choices)) + assert meta.package_targets() == before | {'new-profile', 'new-app', 'new-choice'} + + +def test_new_setting_requires_coverage(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delitem(meta.PROFILE_SETTINGS, CustomSetting.SeatAccess) + with pytest.raises(ValueError, match='missing custom settings'): + meta.package_targets() + + +def test_group_expansion_preserves_packages_and_aliases() -> None: + content = meta.render({'desktop', 'both', 'virtual'}, {'both'}, {'desktop': {'member'}, 'both': {'wrong'}}) + assert '\n\tboth\n' in content + assert '\n\tvirtual\n' in content + assert '\n\tmember\n' in content + assert '# group: desktop' in content + assert 'wrong' not in content + assert '# group: both' not in content + + +def test_check_reports_drift(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + output = tmp_path / 'PKGBUILD' + targets = {'first', 'desktop', 'virtual'} + monkeypatch.setattr(meta, 'package_targets', lambda: targets) + monkeypatch.setattr(meta, 'repository_groups', lambda: ({'first', 'second'}, {'desktop': {'member'}})) + calls: list[tuple[str, ...]] = [] + + def pacman(*args: str) -> list[str]: + calls.append(args) + return ['provider'] + + monkeypatch.setattr(meta, 'pacman', pacman) + monkeypatch.setattr(sys, 'argv', ['meta', '--output', str(output)]) + meta.main() + assert calls[-1] == ('-Spdd', '--noconfirm', '--print-format', '%n', '--', 'virtual') + before = output.read_text() + monkeypatch.setattr(sys, 'argv', ['meta', '--output', str(output), '--check']) + meta.main() + targets.remove('first') + targets.add('second') + with pytest.raises(SystemExit) as result: + meta.main() + assert result.value.code == 1 + assert output.read_text() == before + + +def is_package_list(node: ast.AST) -> bool: + return (isinstance(node, ast.Name) and node.id == 'packages') or (isinstance(node, ast.Attribute) and node.attr == '_base_packages') + + +def literal_installations(tree: ast.AST) -> list[int]: + roots: list[ast.AST] = [] + for node in ast.walk(tree): + if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute): + if node.func.attr in {'strap', 'add_additional_packages'} or (is_package_list(node.func.value) and node.func.attr in {'append', 'extend'}): + roots.extend(node.args) + roots.extend(keyword.value for keyword in node.keywords if keyword.arg == 'packages') + elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + getter = node.name == 'packages' or node.name.endswith('_packages') + if getter and any(isinstance(decorator, ast.Name) and decorator.id == 'property' for decorator in node.decorator_list): + continue + for child in ast.walk(node): + if isinstance(child, (ast.Assign, ast.AnnAssign, ast.AugAssign)): + targets = child.targets if isinstance(child, ast.Assign) else [child.target] + if any(is_package_list(target) for target in targets) and isinstance(child.value, (ast.List, ast.Tuple, ast.Set, ast.Constant)): + roots.append(child.value) + return [node.lineno for root in roots for node in ast.walk(root) if isinstance(node, ast.Constant) and isinstance(node.value, str)] + + +def test_installation_literals_use_metadata() -> None: + root = Path(__file__).resolve().parents[1] / 'archinstall' + trees = {str(path.relative_to(root)): ast.parse(path.read_text()) for path in root.rglob('*.py')} + violations = {path: lines for path, tree in trees.items() if (lines := literal_installations(tree))} + assert not violations, f'Use InstallationPackage for package selections: {violations}' + used = { + node.attr + for tree in trees.values() + for node in ast.walk(tree) + if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) and node.value.id == 'InstallationPackage' + } + assert used == set(InstallationPackage.__members__), 'Remove unused InstallationPackage entries' + + +@pytest.mark.parametrize( + 'source', + [ + "installer.add_additional_packages(['new'])", + "pacman.strap('new')", + "def install():\n packages = ['new']\n installer.add_additional_packages(packages)", + "packages.append('new')", + "installer.add_additional_packages(packages=['new'])", + "def install():\n packages: list[str] = ['new']", + "def install():\n packages += ['new']", + "def install(self):\n self._base_packages += ['new']", + "def install_packages():\n packages = ['new']", + ], +) +def test_new_literal_is_rejected(source: str) -> None: + assert literal_installations(ast.parse(source))