Skip to content
Closed
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
69 changes: 69 additions & 0 deletions archinstall/applications/aur_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import shlex
from typing import TYPE_CHECKING

from archinstall.lib.exceptions import PackageError, SysCallError
from archinstall.lib.models.aur import AURHelperConfiguration
from archinstall.lib.models.users import User
from archinstall.lib.output import debug, info
from archinstall.lib.translationhandler import tr

if TYPE_CHECKING:
from archinstall.lib.installer import Installer


class AURHelperApp:
"""Bootstraps an AUR helper (paru / yay) inside the chroot.

The helper package name comes from a controlled ``AURHelper`` enum value,
and the build username is validated upstream by archinstall's user-creation
path, so shell interpolation of these values is safe. Dynamic file system
paths are still passed through ``shlex.quote``.
"""

_SUDOERS_FILE = 'etc/sudoers.d/99_aur_build'

def install(
self,
install_session: Installer,
helper_config: AURHelperConfiguration,
build_user: User,
) -> None:
install_session.add_additional_packages(['base-devel', 'git'])

sudoers_path = install_session.target / self._SUDOERS_FILE
sudoers_path.write_text(f'{build_user.username} ALL=(ALL) NOPASSWD: /usr/bin/pacman\n')
sudoers_path.chmod(0o440)

helper_pkg = helper_config.helper.value
# Build inside the user's $HOME rather than /tmp: arch-chroot -S spawns a
# transient systemd-run unit and the chroot's /tmp is the on-disk
# directory (mode 0755, root-owned) since no boot-time tmpfs mount has
# happened, so non-root writes to /tmp fail. Build tools like Go also
# default TMPDIR to /tmp, so we redirect TMPDIR for the same reason.
build_subdir = f'.cache/aur-build/{helper_pkg}'
quoted_build = shlex.quote(build_subdir)
tmp_env = 'TMPDIR="$HOME/.cache/aur-build/tmp"'

try:
info(tr('Installing AUR helper {}').format(helper_config.helper.value))
install_session.arch_chroot(
f'rm -rf -- {quoted_build} && mkdir -p -- "$HOME/.cache/aur-build/tmp" "$(dirname -- {quoted_build})"',
run_as=build_user.username,
)
install_session.arch_chroot(
f'git clone https://aur.archlinux.org/{helper_pkg}.git {quoted_build}',
run_as=build_user.username,
)
install_session.arch_chroot(
f'cd {quoted_build} && {tmp_env} makepkg -si --noconfirm',
run_as=build_user.username,
)
install_session.arch_chroot(
f'rm -rf -- {quoted_build} "$HOME/.cache/aur-build/tmp"',
run_as=build_user.username,
)
except SysCallError as e:
debug(f'AUR helper install failed: {e}')
raise PackageError(tr('Failed to install AUR helper: {}').format(e)) from e
finally:
sudoers_path.unlink(missing_ok=True)
11 changes: 11 additions & 0 deletions archinstall/lib/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from archinstall.lib.crypt import decrypt
from archinstall.lib.menu.util import get_password
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
from archinstall.lib.models.aur import AURConfiguration
from archinstall.lib.models.authentication import AuthenticationConfiguration
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.config import SubConfig
Expand Down Expand Up @@ -76,6 +77,7 @@ class ArchConfigType(StrEnum):
NETWORK_CONFIG = 'network_config'
BOOTLOADER_CONFIG = 'bootloader_config'
APP_CONFIG = 'app_config'
AUR_CONFIG = 'aur_config'
AUTH_CONFIG = 'auth_config'
SWAP = 'swap'
USERS = 'users'
Expand Down Expand Up @@ -112,6 +114,8 @@ def text(self) -> str:
return tr('Bootloader')
case ArchConfigType.APP_CONFIG:
return tr('Application')
case ArchConfigType.AUR_CONFIG:
return tr('Enable AUR')
case ArchConfigType.AUTH_CONFIG:
return tr('Authentication')
case ArchConfigType.SWAP:
Expand Down Expand Up @@ -152,6 +156,7 @@ class ArchConfig:
network_config: NetworkConfiguration | None = None
bootloader_config: BootloaderConfiguration | None = None
app_config: ApplicationConfiguration | None = None
aur_config: AURConfiguration | None = None
auth_config: AuthenticationConfiguration | None = None
swap: ZramConfiguration | None = None
hostname: str = 'archlinux'
Expand Down Expand Up @@ -240,6 +245,9 @@ def sub_cfg(self) -> dict[ArchConfigType, SubConfig]:
if self.app_config:
cfg[ArchConfigType.APP_CONFIG] = self.app_config

if self.aur_config:
cfg[ArchConfigType.AUR_CONFIG] = self.aur_config

return cfg

@classmethod
Expand Down Expand Up @@ -307,6 +315,9 @@ def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self:
if audio_config_args is not None or app_config_args is not None:
arch_config.app_config = ApplicationConfiguration.parse_arg(app_config_args, audio_config_args)

if aur_config_args := args_config.get('aur_config', None):
arch_config.aur_config = AURConfiguration.parse_arg(aur_config_args)

if auth_config_args := args_config.get('auth_config', None):
arch_config.auth_config = AuthenticationConfiguration.parse_arg(auth_config_args)

Expand Down
Empty file.
40 changes: 40 additions & 0 deletions archinstall/lib/aur/aur_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import TYPE_CHECKING

from archinstall.applications.aur_helper import AURHelperApp
from archinstall.lib.exceptions import RequirementError
from archinstall.lib.models.aur import AURConfiguration
from archinstall.lib.models.users import User
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr

if TYPE_CHECKING:
from archinstall.lib.installer import Installer


class AURHandler:
"""Coordinates AUR helper installation as part of a guided install.

The build user is resolved deterministically: first user with ``sudo=True``,
falling back to the first user in ``users``. If no users are configured the
handler raises ``RequirementError`` so ``--silent`` runs surface the missing
dependency immediately.
"""

def install_aur(
self,
install_session: Installer,
aur_config: AURConfiguration | None,
users: list[User],
) -> None:
if aur_config is None or aur_config.helper_config is None:
debug('AUR: no helper configured, skipping')
return

build_user = next((u for u in users if u.sudo), None) or (users[0] if users else None)

if build_user is None:
raise RequirementError(
tr('AUR helper requires a non-root user account. Configure one under Authentication.'),
)

AURHelperApp().install(install_session, aur_config.helper_config, build_user)
77 changes: 77 additions & 0 deletions archinstall/lib/aur/aur_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from typing import override

from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.models.aur import (
AURConfiguration,
AURHelper,
AURHelperConfiguration,
)
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType


class AURMenu(AbstractSubMenu[AURConfiguration]):
def __init__(
self,
preset: AURConfiguration | None = None,
):
if preset:
self._aur_config = preset
else:
self._aur_config = AURConfiguration()

menu_options = self._define_menu_options()
self._item_group = MenuItemGroup(menu_options, checkmarks=True)

super().__init__(
self._item_group,
config=self._aur_config,
allow_reset=True,
)

@override
async def show(self) -> AURConfiguration | None:
_ = await super().show()
return self._aur_config

def _define_menu_options(self) -> list[MenuItem]:
return [
MenuItem(
text=tr('Enable AUR Helper'),
action=select_aur_helper,
value=self._aur_config.helper_config,
preview_action=self._prev_helper,
key='helper_config',
),
]

def _prev_helper(self, item: MenuItem) -> str | None:
if item.value is not None:
config: AURHelperConfiguration = item.value
return f'{tr("AUR helper")}: {config.helper.value}'
return None


async def select_aur_helper(preset: AURHelperConfiguration | None = None) -> AURHelperConfiguration | None:
items = [MenuItem(h.value, value=h) for h in AURHelper]
group = MenuItemGroup(items)

if preset:
group.set_focus_by_value(preset.helper)

result = await Selection[AURHelper](
group,
header=tr('Enable AUR Helper'),
allow_skip=True,
allow_reset=True,
).show()

match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return AURHelperConfiguration(helper=result.get_value())
case ResultType.Reset:
return None
26 changes: 26 additions & 0 deletions archinstall/lib/global_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from archinstall.default_profiles.profile import GreeterType
from archinstall.lib.applications.application_menu import ApplicationMenu
from archinstall.lib.args import ArchConfig
from archinstall.lib.aur.aur_menu import AURMenu
from archinstall.lib.authentication.authentication_menu import AuthenticationMenu
from archinstall.lib.bootloader.bootloader_menu import BootloaderMenu
from archinstall.lib.bootloader.utils import validate_bootloader_layout
Expand All @@ -16,6 +17,7 @@
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
from archinstall.lib.mirror.mirror_menu import MirrorMenu
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
from archinstall.lib.models.aur import AURConfiguration
from archinstall.lib.models.authentication import AuthenticationConfiguration
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, PartitionModification
Expand Down Expand Up @@ -136,6 +138,12 @@ def _get_menu_options(self) -> list[MenuItem]:
preview_action=self._prev_applications,
key='app_config',
),
MenuItem(
text=tr('Enable AUR'),
action=self._select_aur,
preview_action=self._prev_aur,
key='aur_config',
),
MenuItem(
text=tr('Network configuration'),
action=select_network,
Expand Down Expand Up @@ -235,6 +243,13 @@ def check(s: str) -> bool:
if not check(item.key):
missing.add(item.text)

aur_item: MenuItem = self._item_group.find_by_key('aur_config')
aur_config: AURConfiguration | None = aur_item.value
if aur_config and aur_config.helper_config and not (auth_config and auth_config.has_regular_user()):
missing.add(
tr('AUR helper requires a non-root user account. Configure one under Authentication.'),
)

return list(missing)

@override
Expand Down Expand Up @@ -267,6 +282,17 @@ async def _select_applications(self, preset: ApplicationConfiguration | None) ->
app_config = await ApplicationMenu(preset).show()
return app_config

async def _select_aur(self, preset: AURConfiguration | None) -> AURConfiguration | None:
aur_config = await AURMenu(preset).show()
return aur_config

def _prev_aur(self, item: MenuItem) -> str | None:
if item.value is not None:
aur_config: AURConfiguration = item.value
if aur_config.helper_config:
return f'{tr("AUR helper")}: {aur_config.helper_config.helper.value}'
return None

async def _select_authentication(self, preset: AuthenticationConfiguration | None) -> AuthenticationConfiguration | None:
auth_config = await AuthenticationMenu(preset).show()
return auth_config
Expand Down
57 changes: 57 additions & 0 deletions archinstall/lib/models/aur.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from dataclasses import dataclass
from enum import StrEnum
from typing import Any, NotRequired, Self, TypedDict, override

from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr


class AURHelper(StrEnum):
PARU = 'paru'
YAY = 'yay'


class AURHelperConfigSerialization(TypedDict):
helper: str


class AURConfigSerialization(TypedDict, total=False):
helper_config: NotRequired[AURHelperConfigSerialization]


@dataclass
class AURHelperConfiguration:
helper: AURHelper

def json(self) -> AURHelperConfigSerialization:
return {'helper': self.helper.value}

@classmethod
def parse_arg(cls, arg: dict[str, Any]) -> Self:
return cls(helper=AURHelper(arg['helper']))


@dataclass
class AURConfiguration(SubConfig):
helper_config: AURHelperConfiguration | None = None

@classmethod
def parse_arg(cls, args: dict[str, Any] | None = None) -> Self:
cfg = cls()
if args and (helper_config := args.get('helper_config')) is not None:
cfg.helper_config = AURHelperConfiguration.parse_arg(helper_config)
return cfg

@override
def json(self) -> AURConfigSerialization:
config: AURConfigSerialization = {}
if self.helper_config:
config['helper_config'] = self.helper_config.json()
return config

@override
def summary(self) -> list[str]:
out: list[str] = []
if self.helper_config:
out.append(tr('AUR helper "{}"').format(self.helper_config.helper.value))
return out
24 changes: 24 additions & 0 deletions archinstall/locales/base.pot
Original file line number Diff line number Diff line change
Expand Up @@ -2463,3 +2463,27 @@ msgstr ""

msgid "Enter a repository name"
msgstr ""

msgid "Enable AUR"
msgstr ""

msgid "Enable AUR Helper"
msgstr ""

msgid "AUR helper"
msgstr ""

msgid "AUR helper requires a non-root user account. Configure one under Authentication."
msgstr ""

#, python-brace-format
msgid "Installing AUR helper {}"
msgstr ""

#, python-brace-format
msgid "Failed to install AUR helper: {}"
msgstr ""

#, python-brace-format
msgid "AUR helper \"{}\""
msgstr ""
Loading