Add AUR Helper option in Applications menu
Adds yay and paru as installable AUR helpers via the Applications menu
This commit is contained in:
parent
cd62eff4a7
commit
0218e586e7
|
|
@ -0,0 +1,100 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from archinstall.lib.models.application import AURHelper, AURHelperConfiguration
|
||||
from archinstall.lib.models.users import User
|
||||
from archinstall.lib.output import debug
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from archinstall.lib.installer import Installer
|
||||
|
||||
|
||||
class AURHelperApp:
|
||||
@property
|
||||
def yay_packages(self) -> list[str]:
|
||||
return [
|
||||
'base-devel',
|
||||
'git',
|
||||
'go', # required to build yay
|
||||
]
|
||||
|
||||
@property
|
||||
def paru_packages(self) -> list[str]:
|
||||
return [
|
||||
'base-devel',
|
||||
'git',
|
||||
'rust', # required to build paru
|
||||
]
|
||||
|
||||
def _write_firstboot_service(
|
||||
self,
|
||||
install_session: Installer,
|
||||
helper: str,
|
||||
user: User,
|
||||
) -> None:
|
||||
"""
|
||||
Writes a systemd oneshot service that builds and installs the AUR helper
|
||||
on first boot as the real user, then removes itself.
|
||||
makepkg cannot run as root and needs a real user session to work
|
||||
correctly, so we defer it to first boot instead of running it during
|
||||
the archinstall chroot environment.
|
||||
|
||||
The service runs as root so it can write/remove the temporary sudoers
|
||||
rule, and explicitly su's to the user only for the makepkg step.
|
||||
"""
|
||||
repo_url = f'https://aur.archlinux.org/{helper}.git'
|
||||
build_dir = f'/home/{user.username}/{helper}-build'
|
||||
service_name = f'aur-install-{helper}'
|
||||
sudoers_rule = f'/etc/sudoers.d/99-aur-{helper}-tmp'
|
||||
|
||||
service_content = f"""\
|
||||
[Unit]
|
||||
Description=Install {helper} AUR helper (first boot)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
# Run as root so we can write/remove the sudoers rule
|
||||
ExecStartPre=/bin/bash -c 'echo "{user.username} ALL=(ALL) NOPASSWD: /usr/bin/pacman" > {sudoers_rule} && chmod 440 {sudoers_rule}'
|
||||
# makepkg is invoked via su to the actual user.
|
||||
ExecStart=/bin/su - {user.username} -c 'git clone {repo_url} {build_dir} && cd {build_dir} && makepkg -si --noconfirm && rm -rf {build_dir}'
|
||||
ExecStartPost=/bin/rm -f {sudoers_rule}
|
||||
ExecStartPost=/bin/rm -f /etc/systemd/system/{service_name}.service
|
||||
ExecStartPost=/bin/systemctl daemon-reload
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
"""
|
||||
service_path = install_session.target / 'etc' / 'systemd' / 'system' / f'{service_name}.service'
|
||||
service_path.write_text(service_content)
|
||||
|
||||
install_session.enable_service([f'{service_name}.service'])
|
||||
|
||||
debug(f'First-boot service written for {helper}, will install as {user.username}')
|
||||
|
||||
def install(
|
||||
self,
|
||||
install_session: Installer,
|
||||
AUR_config: AURHelperConfiguration,
|
||||
users: list[User] | None = None,
|
||||
) -> None:
|
||||
debug(f'Installing AUR helper: {AUR_config.AUR_helper.value}')
|
||||
|
||||
if AUR_config.AUR_helper == AURHelper.NO_AUR_HELPER:
|
||||
debug('No AUR helper selected, skipping installation.')
|
||||
return
|
||||
|
||||
if not users:
|
||||
debug('No users provided, skipping AUR helper installation.')
|
||||
return
|
||||
|
||||
user = users[0]
|
||||
|
||||
match AUR_config.AUR_helper:
|
||||
case AURHelper.YAY:
|
||||
install_session.add_additional_packages(self.yay_packages)
|
||||
self._write_firstboot_service(install_session, 'yay', user)
|
||||
case AURHelper.PARU:
|
||||
install_session.add_additional_packages(self.paru_packages)
|
||||
self._write_firstboot_service(install_session, 'paru', user)
|
||||
|
|
@ -6,7 +6,9 @@ from archinstall.applications.firewall import FirewallApp
|
|||
from archinstall.applications.fonts import FontsApp
|
||||
from archinstall.applications.power_management import PowerManagementApp
|
||||
from archinstall.applications.print_service import PrintServiceApp
|
||||
from archinstall.applications.AUR_helper import AURHelperApp
|
||||
from archinstall.lib.models import Audio
|
||||
from archinstall.lib.models.application import AURHelper
|
||||
from archinstall.lib.models.application import ApplicationConfiguration
|
||||
from archinstall.lib.models.users import User
|
||||
|
||||
|
|
@ -38,6 +40,13 @@ class ApplicationHandler:
|
|||
if app_config.print_service_config and app_config.print_service_config.enabled:
|
||||
PrintServiceApp().install(install_session)
|
||||
|
||||
if app_config.AUR_helper_config and app_config.AUR_helper_config.AUR_helper != AURHelper.NO_AUR_HELPER:
|
||||
AURHelperApp().install(
|
||||
install_session,
|
||||
app_config.AUR_helper_config,
|
||||
users,
|
||||
)
|
||||
|
||||
if app_config.firewall_config:
|
||||
FirewallApp().install(
|
||||
install_session,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ from archinstall.lib.models.application import (
|
|||
ApplicationConfiguration,
|
||||
Audio,
|
||||
AudioConfiguration,
|
||||
AURHelper,
|
||||
AURHelperConfiguration,
|
||||
BluetoothConfiguration,
|
||||
Firewall,
|
||||
FirewallConfiguration,
|
||||
|
|
@ -66,6 +68,12 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
|
|||
preview_action=self._prev_print_service,
|
||||
key='print_service_config',
|
||||
),
|
||||
MenuItem(
|
||||
text=tr('AUR helper'),
|
||||
action=select_AUR_helper,
|
||||
preview_action=self._prev_AUR_helper,
|
||||
key='AUR_helper_config',
|
||||
),
|
||||
MenuItem(
|
||||
text=tr('Power management'),
|
||||
action=select_power_management,
|
||||
|
|
@ -118,6 +126,12 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
|
|||
return output
|
||||
return None
|
||||
|
||||
def _prev_AUR_helper(self, item: MenuItem) -> str | None:
|
||||
if item.value is not None:
|
||||
config: AURHelperConfiguration = item.value
|
||||
return f'{tr("AUR helper")}: {config.AUR_helper.value}'
|
||||
return None
|
||||
|
||||
def _prev_firewall(self, item: MenuItem) -> str | None:
|
||||
if item.value is not None:
|
||||
config: FirewallConfiguration = item.value
|
||||
|
|
@ -214,6 +228,28 @@ async def select_audio(preset: AudioConfiguration | None = None) -> AudioConfigu
|
|||
raise ValueError('Unhandled result type')
|
||||
|
||||
|
||||
async def select_AUR_helper(preset: AURHelperConfiguration | None = None) -> AURHelperConfiguration | None:
|
||||
items = [MenuItem(a.value, value=a) for a in AURHelper]
|
||||
group = MenuItemGroup(items)
|
||||
|
||||
if preset:
|
||||
group.set_focus_by_value(preset.AUR_helper)
|
||||
|
||||
result = await Selection[AURHelper](
|
||||
group,
|
||||
header=tr('Select an AUR helper'),
|
||||
allow_skip=True,
|
||||
).show()
|
||||
|
||||
match result.type_:
|
||||
case ResultType.Skip:
|
||||
return preset
|
||||
case ResultType.Selection:
|
||||
return AURHelperConfiguration(AUR_helper=result.get_value())
|
||||
case ResultType.Reset:
|
||||
raise ValueError('Unhandled result type')
|
||||
|
||||
|
||||
async def select_firewall(preset: FirewallConfiguration | None = None) -> FirewallConfiguration | None:
|
||||
group = MenuItemGroup.from_enum(Firewall)
|
||||
|
||||
|
|
|
|||
|
|
@ -352,6 +352,11 @@ class GlobalMenu(AbstractMenu[None]):
|
|||
output += f'{tr("Print service")}: '
|
||||
output += tr('Enabled') if app_config.print_service_config.enabled else tr('Disabled')
|
||||
output += '\n'
|
||||
|
||||
if app_config.AUR_helper_config:
|
||||
AUR_helper_config = app_config.AUR_helper_config
|
||||
output += f'{tr("AUR helper")}: {AUR_helper_config.AUR_helper.value}'
|
||||
output += '\n'
|
||||
|
||||
if app_config.power_management_config:
|
||||
power_management_config = app_config.power_management_config
|
||||
|
|
|
|||
|
|
@ -31,6 +31,13 @@ class AudioConfigSerialization(TypedDict):
|
|||
class PrintServiceConfigSerialization(TypedDict):
|
||||
enabled: bool
|
||||
|
||||
class AURHelper(StrEnum):
|
||||
NO_AUR_HELPER = 'No AUR helper'
|
||||
YAY = auto()
|
||||
PARU = auto()
|
||||
|
||||
class AURHelperConfigSerialization(TypedDict):
|
||||
AUR_helper: str
|
||||
|
||||
class Firewall(StrEnum):
|
||||
UFW = auto()
|
||||
|
|
@ -79,6 +86,7 @@ class ApplicationSerialization(TypedDict):
|
|||
audio_config: NotRequired[AudioConfigSerialization]
|
||||
power_management_config: NotRequired[PowerManagementConfigSerialization]
|
||||
print_service_config: NotRequired[PrintServiceConfigSerialization]
|
||||
AUR_helper_config: NotRequired[AURHelperConfigSerialization]
|
||||
firewall_config: NotRequired[FirewallConfigSerialization]
|
||||
fonts_config: NotRequired[FontsConfigSerialization]
|
||||
|
||||
|
|
@ -139,6 +147,22 @@ class PrintServiceConfiguration:
|
|||
return cls(arg['enabled'])
|
||||
|
||||
|
||||
@dataclass
|
||||
class AURHelperConfiguration:
|
||||
AUR_helper: AURHelper
|
||||
|
||||
def json(self) -> AURHelperConfigSerialization:
|
||||
return {
|
||||
'AUR_helper': self.AUR_helper.value,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def parse_arg(cls, arg: dict[str, Any]) -> Self:
|
||||
return cls(
|
||||
AURHelper(arg['AUR_helper']),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FirewallConfiguration:
|
||||
firewall: Firewall
|
||||
|
|
@ -188,6 +212,7 @@ class ApplicationConfiguration:
|
|||
audio_config: AudioConfiguration | None = None
|
||||
power_management_config: PowerManagementConfiguration | None = None
|
||||
print_service_config: PrintServiceConfiguration | None = None
|
||||
AUR_helper_config: AURHelperConfiguration | None = None
|
||||
firewall_config: FirewallConfiguration | None = None
|
||||
fonts_config: FontsConfiguration | None = None
|
||||
|
||||
|
|
@ -215,6 +240,9 @@ class ApplicationConfiguration:
|
|||
if args and (print_service_config := args.get('print_service_config')) is not None:
|
||||
app_config.print_service_config = PrintServiceConfiguration.parse_arg(print_service_config)
|
||||
|
||||
if args and (AUR_helper_config := args.get('AUR_helper_config')) is not None:
|
||||
app_config.AUR_helper_config = AURHelperConfiguration.parse_arg(AUR_helper_config)
|
||||
|
||||
if args and (firewall_config := args.get('firewall_config')) is not None:
|
||||
app_config.firewall_config = FirewallConfiguration.parse_arg(firewall_config)
|
||||
|
||||
|
|
@ -238,10 +266,13 @@ class ApplicationConfiguration:
|
|||
if self.print_service_config:
|
||||
config['print_service_config'] = self.print_service_config.json()
|
||||
|
||||
if self.AUR_helper_config:
|
||||
config['AUR_helper_config'] = self.AUR_helper_config.json()
|
||||
|
||||
if self.firewall_config:
|
||||
config['firewall_config'] = self.firewall_config.json()
|
||||
|
||||
if self.fonts_config:
|
||||
config['fonts_config'] = self.fonts_config.json()
|
||||
|
||||
return config
|
||||
return config
|
||||
|
|
@ -131,7 +131,7 @@ def perform_installation(
|
|||
auth_handler.setup_auth(installation, config.auth_config, config.hostname)
|
||||
|
||||
if app_config := config.app_config:
|
||||
application_handler.install_applications(installation, app_config)
|
||||
application_handler.install_applications(installation, app_config, users)
|
||||
|
||||
if profile_config := config.profile_config:
|
||||
profile_handler.install_profile_config(installation, profile_config)
|
||||
|
|
|
|||
Loading…
Reference in New Issue