diff --git a/archinstall-meta/PKGBUILD b/archinstall-meta/PKGBUILD new file mode 100644 index 0000000000..5ff7931b41 --- /dev/null +++ b/archinstall-meta/PKGBUILD @@ -0,0 +1,538 @@ +# 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-niri + 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-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-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-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-icon-theme-faenza + 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/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 8c6bee22e5..06a15c3c49 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -49,7 +49,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 @@ -799,7 +799,7 @@ def copy_iso_network_config(self, enable_services: bool = False) -> bool: if enable_services: # 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') + self._base_packages.append(InstallationPackage.IWD.value) # This function will be called after minimal_installation() # as a hook for post-installs. This hook is only needed if @@ -811,7 +811,7 @@ def post_install_enable_iwd_service(*args: str, **kwargs: str) -> None: # Otherwise, we can go ahead and add the required package # and enable it's service: else: - self.pacman.strap('iwd') + self.pacman.strap(InstallationPackage.IWD.value) self.enable_service('iwd') self.systemd_resolved_stub_mode() @@ -890,7 +890,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') @@ -907,7 +907,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) @@ -999,7 +999,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': '/', @@ -1020,20 +1020,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}') @@ -1250,7 +1250,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 @@ -1340,7 +1340,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}') @@ -1354,7 +1354,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: @@ -1455,7 +1455,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}') @@ -1464,7 +1464,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') @@ -1619,7 +1619,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 @@ -1675,7 +1675,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 @@ -1767,7 +1767,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: @@ -2041,7 +2041,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 fa44668b06..792b301fe0 100644 --- a/archinstall/lib/models/package_types.py +++ b/archinstall/lib/models/package_types.py @@ -12,3 +12,41 @@ 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' 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..57cd858444 --- /dev/null +++ b/archinstall/lib/packages/meta.py @@ -0,0 +1,117 @@ +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 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, 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 + 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) + + 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 eb54671642..d12823ac51 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 @@ -154,29 +155,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/scripts/minimal.py b/archinstall/scripts/minimal.py index 0ab6da83e5..51a7655c2d 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..cfe06fa0f0 --- /dev/null +++ b/tests/test_package_inventory.py @@ -0,0 +1,148 @@ +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 line in entries.splitlines(): + line = 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-niri'} <= targets + + +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))