Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions archinstall/applications/AUR_helper.py
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 9 additions & 0 deletions archinstall/lib/applications/application_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
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

Expand Down Expand Up @@ -38,6 +40,13 @@ def install_applications(self, install_session: Installer, app_config: Applicati
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,
Expand Down
36 changes: 36 additions & 0 deletions archinstall/lib/applications/application_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
ApplicationConfiguration,
Audio,
AudioConfiguration,
AURHelper,
AURHelperConfiguration,
BluetoothConfiguration,
Firewall,
FirewallConfiguration,
Expand Down Expand Up @@ -66,6 +68,12 @@ def _define_menu_options(self) -> list[MenuItem]:
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,
Expand Down Expand Up @@ -118,6 +126,12 @@ def _prev_print_service(self, item: MenuItem) -> str | None:
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
Expand Down Expand Up @@ -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)

Expand Down
5 changes: 5 additions & 0 deletions archinstall/lib/global_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,11 @@ def _prev_applications(self, item: MenuItem) -> str | 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
Expand Down
33 changes: 32 additions & 1 deletion archinstall/lib/models/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -139,6 +147,22 @@ def parse_arg(cls, arg: PrintServiceConfigSerialization) -> Self:
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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -215,6 +240,9 @@ def parse_arg(
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)

Expand All @@ -238,10 +266,13 @@ def json(self) -> ApplicationSerialization:
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
2 changes: 1 addition & 1 deletion archinstall/scripts/guided.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down