Move audio configuration to application config (#3612)

This commit is contained in:
Daniel Girtler 2025-06-20 07:27:23 +10:00 committed by GitHub
parent 63848f645f
commit 37b3985625
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 241 additions and 190 deletions

View File

@ -0,0 +1,80 @@
from typing import TYPE_CHECKING
from archinstall.lib.hardware import SysInfo
from archinstall.lib.models.application import Audio, AudioConfiguration
from archinstall.lib.models.users import User
from archinstall.lib.output import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class AudioApp:
@property
def pulseaudio_packages(self) -> list[str]:
return [
'pulseaudio',
]
@property
def pipewire_packages(self) -> list[str]:
return [
'pipewire',
'pipewire-alsa',
'pipewire-jack',
'pipewire-pulse',
'gst-plugin-pipewire',
'libpulse',
'wireplumber',
]
def _enable_pipewire(
self,
install_session: 'Installer',
users: list['User'] | None = None,
) -> None:
if users is None:
return
for user in users:
# Create the full path for enabling the pipewire systemd items
service_dir = install_session.target / 'home' / user.username / '.config' / 'systemd' / 'user' / 'default.target.wants'
service_dir.mkdir(parents=True, exist_ok=True)
# Set ownership of the entire user catalogue
install_session.arch_chroot(f'chown -R {user.username}:{user.username} /home/{user.username}')
# symlink in the correct pipewire systemd items
install_session.arch_chroot(
f'ln -sf /usr/lib/systemd/user/pipewire-pulse.service /home/{user.username}/.config/systemd/user/default.target.wants/pipewire-pulse.service',
run_as=user.username,
)
install_session.arch_chroot(
f'ln -sf /usr/lib/systemd/user/pipewire-pulse.socket /home/{user.username}/.config/systemd/user/default.target.wants/pipewire-pulse.socket',
run_as=user.username,
)
def install(
self,
install_session: 'Installer',
audio_config: AudioConfiguration,
users: list[User] | None = None,
) -> None:
debug(f'Installing audio server: {audio_config.audio.value}')
if audio_config.audio == Audio.NO_AUDIO:
debug('No audio server selected, skipping installation.')
return
if SysInfo.requires_sof_fw():
install_session.add_additional_packages('sof-firmware')
if SysInfo.requires_alsa_fw():
install_session.add_additional_packages('alsa-firmware')
match audio_config.audio:
case Audio.PIPEWIRE:
install_session.add_additional_packages(self.pipewire_packages)
self._enable_pipewire(install_session, users)
case Audio.PULSEAUDIO:
install_session.add_additional_packages(self.pulseaudio_packages)

View File

@ -6,7 +6,7 @@ if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class Bluetooth:
class BluetoothApp:
@property
def packages(self) -> list[str]:
return [

View File

@ -1,57 +0,0 @@
from typing import TYPE_CHECKING, override
from archinstall.default_profiles.profile import Profile, ProfileType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
from archinstall.lib.models.users import User
class PipewireProfile(Profile):
def __init__(self) -> None:
super().__init__('Pipewire', ProfileType.Application)
@property
@override
def packages(self) -> list[str]:
return [
'pipewire',
'pipewire-alsa',
'pipewire-jack',
'pipewire-pulse',
'gst-plugin-pipewire',
'libpulse',
'wireplumber',
]
def _enable_pipewire_for_all(self, install_session: 'Installer') -> None:
from archinstall.lib.args import arch_config_handler
users: list[User] | None = arch_config_handler.config.users
if users is None:
return
for user in users:
# Create the full path for enabling the pipewire systemd items
service_dir = install_session.target / 'home' / user.username / '.config' / 'systemd' / 'user' / 'default.target.wants'
service_dir.mkdir(parents=True, exist_ok=True)
# Set ownership of the entire user catalogue
install_session.arch_chroot(f'chown -R {user.username}:{user.username} /home/{user.username}')
# symlink in the correct pipewire systemd items
install_session.arch_chroot(
f'ln -sf /usr/lib/systemd/user/pipewire-pulse.service /home/{user.username}/.config/systemd/user/default.target.wants/pipewire-pulse.service',
run_as=user.username,
)
install_session.arch_chroot(
f'ln -sf /usr/lib/systemd/user/pipewire-pulse.socket /home/{user.username}/.config/systemd/user/default.target.wants/pipewire-pulse.socket',
run_as=user.username,
)
@override
def install(self, install_session: 'Installer') -> None:
super().install(install_session)
install_session.add_additional_packages(self.packages)
self._enable_pipewire_for_all(install_session)

View File

@ -1,7 +1,9 @@
from typing import TYPE_CHECKING
from archinstall.applications.bluetooth import Bluetooth
from archinstall.applications.audio import AudioApp
from archinstall.applications.bluetooth import BluetoothApp
from archinstall.lib.models.application import ApplicationConfiguration
from archinstall.lib.models.users import User
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
@ -11,13 +13,16 @@ class ApplicationHandler:
def __init__(self) -> None:
pass
def install_applications(
self,
install_session: 'Installer',
app_config: ApplicationConfiguration,
) -> None:
def install_applications(self, install_session: 'Installer', app_config: ApplicationConfiguration, users: list['User'] | None = None) -> None:
if app_config.bluetooth_config:
Bluetooth().install(install_session)
BluetoothApp().install(install_session)
if app_config.audio_config:
AudioApp().install(
install_session,
app_config.audio_config,
users,
)
application_handler = ApplicationHandler()

View File

@ -1,11 +1,12 @@
from typing import override
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.models.application import ApplicationConfiguration, BluetoothConfiguration
from archinstall.lib.models.application import ApplicationConfiguration, Audio, AudioConfiguration, BluetoothConfiguration
from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.types import Alignment, Orientation
from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties, Orientation
class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
@ -41,6 +42,12 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
preview_action=self._prev_bluetooth,
key='bluetooth_config',
),
MenuItem(
text=tr('Audio'),
action=select_audio,
preview_action=self._prev_audio,
key='audio_config',
),
]
def _prev_bluetooth(self, item: MenuItem) -> str | None:
@ -52,6 +59,12 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
return output
return None
def _prev_audio(self, item: MenuItem) -> str | None:
if item.value is not None:
config: AudioConfiguration = item.value
return f'{tr("Audio")}: {config.audio.value}'
return None
def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfiguration | None:
group = MenuItemGroup.yes_no()
@ -74,3 +87,26 @@ def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfigur
enabled = result.item() == MenuItem.yes()
return BluetoothConfiguration(enabled)
def select_audio(preset: AudioConfiguration | None = None) -> AudioConfiguration | None:
items = [MenuItem(a.value, value=a) for a in Audio]
group = MenuItemGroup(items)
if preset:
group.set_focus_by_value(preset.audio)
result = SelectMenu[Audio](
group,
allow_skip=True,
alignment=Alignment.CENTER,
frame=FrameProperties.min(tr('Audio')),
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return AudioConfiguration(audio=result.get_value())
case ResultType.Reset:
raise ValueError('Unhandled result type')

View File

@ -14,7 +14,6 @@ from pydantic.dataclasses import dataclass as p_dataclass
from archinstall.lib.crypt import decrypt
from archinstall.lib.models.application import ApplicationConfiguration
from archinstall.lib.models.audio_configuration import AudioConfiguration
from archinstall.lib.models.bootloader import Bootloader
from archinstall.lib.models.device_model import DiskEncryption, DiskLayoutConfiguration
from archinstall.lib.models.locale import LocaleConfiguration
@ -63,8 +62,7 @@ class ArchConfig:
network_config: NetworkConfiguration | None = None
bootloader: Bootloader = field(default=Bootloader.get_default())
uki: bool = False
audio_config: AudioConfiguration | None = None
application_config: ApplicationConfiguration | None = None
app_config: ApplicationConfiguration | None = None
hostname: str = 'archlinux'
kernels: list[str] = field(default_factory=lambda: ['linux'])
ntp: bool = True
@ -106,8 +104,7 @@ class ArchConfig:
'services': self.services,
'custom_commands': self.custom_commands,
'bootloader': self.bootloader.json(),
'audio_config': self.audio_config.json() if self.audio_config else None,
'app_config': self.application_config.json() if self.application_config else None,
'app_config': self.app_config.json() if self.app_config else None,
}
if self.locale_config:
@ -184,11 +181,12 @@ class ArchConfig:
if args_config.get('uki') and not arch_config.bootloader.has_uki_support():
arch_config.uki = False
if audio_config := args_config.get('audio_config', None):
arch_config.audio_config = AudioConfiguration.parse_arg(audio_config)
# deprecated: backwards compatibility
audio_config_args = args_config.get('audio_config', None)
app_config_args = args_config.get('app_config', None)
if app_config := args_config.get('app_config', None):
arch_config.application_config = ApplicationConfiguration.parse_arg(app_config)
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 hostname := args_config.get('hostname', ''):
arch_config.hostname = hostname

View File

@ -16,7 +16,6 @@ from .interactions.general_conf import (
add_number_of_parallel_downloads,
ask_additional_packages_to_install,
ask_for_a_timezone,
ask_for_audio_selection,
ask_hostname,
ask_ntp,
)
@ -26,7 +25,6 @@ from .interactions.system_conf import ask_for_bootloader, ask_for_swap, ask_for_
from .locale.locale_menu import LocaleMenu
from .menu.abstract_menu import CONFIG_KEY, AbstractMenu
from .mirrors import MirrorMenu
from .models.audio_configuration import AudioConfiguration
from .models.bootloader import Bootloader
from .models.locale import LocaleConfiguration
from .models.mirrors import MirrorConfiguration
@ -128,12 +126,6 @@ class GlobalMenu(AbstractMenu[None]):
preview_action=self._prev_profile,
key='profile_config',
),
MenuItem(
text=tr('Audio'),
action=ask_for_audio_selection,
preview_action=self._prev_audio,
key='audio_config',
),
MenuItem(
text=tr('Applications'),
action=self._select_applications,
@ -311,7 +303,13 @@ class GlobalMenu(AbstractMenu[None]):
if app_config.bluetooth_config:
output += f'{tr("Bluetooth")}: '
output += tr('Enabled') if app_config.bluetooth_config.enabled else tr('Disabled') + '\n'
output += tr('Enabled') if app_config.bluetooth_config.enabled else tr('Disabled')
output += '\n'
if app_config.audio_config:
audio_config = app_config.audio_config
output += f'{tr("Audio")}: {audio_config.audio.value}'
output += '\n'
return output
@ -378,12 +376,6 @@ class GlobalMenu(AbstractMenu[None]):
return f'{tr("Root password")}: {password.hidden()}'
return None
def _prev_audio(self, item: MenuItem) -> str | None:
if item.value is not None:
config: AudioConfiguration = item.value
return f'{tr("Audio")}: {config.audio.value}'
return None
def _prev_parallel_dw(self, item: MenuItem) -> str | None:
if item.value is not None:
return f'{tr("Parallel Downloads")}: {item.value}'

View File

@ -10,7 +10,6 @@ from .general_conf import (
add_number_of_parallel_downloads,
ask_additional_packages_to_install,
ask_for_a_timezone,
ask_for_audio_selection,
ask_hostname,
ask_ntp,
select_archinstall_language,
@ -26,7 +25,6 @@ __all__ = [
'ask_additional_packages_to_install',
'ask_for_a_timezone',
'ask_for_additional_users',
'ask_for_audio_selection',
'ask_for_bootloader',
'ask_for_swap',
'ask_for_uki',

View File

@ -13,7 +13,6 @@ from archinstall.tui.result import ResultType
from archinstall.tui.types import Alignment, FrameProperties, Orientation, PreviewStyle
from ..locale.utils import list_timezones
from ..models.audio_configuration import Audio, AudioConfiguration
from ..models.packages import AvailablePackage, PackageGroup
from ..output import warn
from ..translationhandler import Language
@ -102,29 +101,6 @@ def ask_for_a_timezone(preset: str | None = None) -> str | None:
return result.get_value()
def ask_for_audio_selection(preset: AudioConfiguration | None = None) -> AudioConfiguration | None:
items = [MenuItem(a.value, value=a) for a in Audio]
group = MenuItemGroup(items)
if preset:
group.set_focus_by_value(preset.audio)
result = SelectMenu[Audio](
group,
allow_skip=True,
alignment=Alignment.CENTER,
frame=FrameProperties.min(tr('Audio')),
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return AudioConfiguration(audio=result.get_value())
case ResultType.Reset:
raise ValueError('Unhandled result type')
def select_language(preset: str | None = None) -> str | None:
from ..locale.locale_menu import select_kb_layout

View File

@ -1,4 +1,9 @@
from .audio_configuration import Audio, AudioConfiguration
from .application import (
ApplicationConfiguration,
Audio,
AudioConfiguration,
BluetoothConfiguration,
)
from .bootloader import Bootloader
from .device_model import (
BDevice,
@ -35,9 +40,11 @@ from .profile_model import ProfileConfiguration
from .users import PasswordStrength, User
__all__ = [
'ApplicationConfiguration',
'Audio',
'AudioConfiguration',
'BDevice',
'BluetoothConfiguration',
'Bootloader',
'CustomRepository',
'DeviceGeometry',

View File

@ -1,13 +1,41 @@
from dataclasses import dataclass
from typing import NotRequired, TypedDict
from enum import StrEnum, auto
from typing import Any, NotRequired, TypedDict
class BluetoothConfigSerialization(TypedDict):
enabled: bool
class Audio(StrEnum):
NO_AUDIO = 'No audio server'
PIPEWIRE = auto()
PULSEAUDIO = auto()
class AudioConfigSerialization(TypedDict):
audio: str
class ApplicationSerialization(TypedDict):
bluetooth_config: NotRequired[BluetoothConfigSerialization]
audio_config: NotRequired[AudioConfigSerialization]
@dataclass
class AudioConfiguration:
audio: Audio
def json(self) -> AudioConfigSerialization:
return {
'audio': self.audio.value,
}
@staticmethod
def parse_arg(arg: dict[str, Any]) -> 'AudioConfiguration':
return AudioConfiguration(
Audio(arg['audio']),
)
@dataclass
@ -18,23 +46,33 @@ class BluetoothConfiguration:
return {'enabled': self.enabled}
@staticmethod
def parse_arg(arg: BluetoothConfigSerialization) -> 'BluetoothConfiguration':
def parse_arg(arg: dict[str, Any]) -> 'BluetoothConfiguration':
return BluetoothConfiguration(arg['enabled'])
@dataclass
class ApplicationConfiguration:
bluetooth_config: BluetoothConfiguration | None = None
audio_config: AudioConfiguration | None = None
@staticmethod
def parse_arg(args: ApplicationSerialization) -> 'ApplicationConfiguration':
bluetooth_config: BluetoothConfiguration | None = None
if 'bluetooth_config' in args:
bluetooth_config = BluetoothConfiguration.parse_arg(args['bluetooth_config'])
def parse_arg(
args: dict[str, Any] | None = None,
old_audio_config: dict[str, Any] | None = None,
) -> 'ApplicationConfiguration':
app_config = ApplicationConfiguration()
return ApplicationConfiguration(
bluetooth_config=bluetooth_config,
)
if args and (bluetooth_config := args.get('bluetooth_config')) is not None:
app_config.bluetooth_config = BluetoothConfiguration.parse_arg(bluetooth_config)
# deprecated: backwards compatibility
if old_audio_config is not None:
app_config.audio_config = AudioConfiguration.parse_arg(old_audio_config)
if args and (audio_config := args.get('audio_config')) is not None:
app_config.audio_config = AudioConfiguration.parse_arg(audio_config)
return app_config
def json(self) -> ApplicationSerialization:
config: ApplicationSerialization = {}
@ -42,4 +80,7 @@ class ApplicationConfiguration:
if self.bluetooth_config:
config['bluetooth_config'] = self.bluetooth_config.json()
if self.audio_config:
config['audio_config'] = self.audio_config.json()
return config

View File

@ -1,52 +0,0 @@
from dataclasses import dataclass
from enum import StrEnum, auto
from typing import TYPE_CHECKING
from ..hardware import SysInfo
from ..output import info
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class Audio(StrEnum):
NO_AUDIO = 'No audio server'
PIPEWIRE = auto()
PULSEAUDIO = auto()
@dataclass
class AudioConfiguration:
audio: Audio
def json(self) -> dict[str, str]:
return {
'audio': self.audio.value,
}
@staticmethod
def parse_arg(arg: dict[str, str]) -> 'AudioConfiguration':
return AudioConfiguration(
Audio(arg['audio']),
)
def install_audio_config(
self,
installation: 'Installer',
) -> None:
info(f'Installing audio server: {self.audio.name}')
from ...default_profiles.applications.pipewire import PipewireProfile
match self.audio:
case Audio.PIPEWIRE:
PipewireProfile().install(installation)
case Audio.PULSEAUDIO:
installation.add_additional_packages('pulseaudio')
if self.audio != Audio.NO_AUDIO:
if SysInfo.requires_sof_fw():
installation.add_additional_packages('sof-firmware')
if SysInfo.requires_alsa_fw():
installation.add_additional_packages('alsa-firmware')

View File

@ -116,19 +116,13 @@ def perform_installation(mountpoint: Path) -> None:
if users := config.users:
installation.create_users(users)
audio_config = config.audio_config
if audio_config:
audio_config.install_audio_config(installation)
else:
info('No audio server will be installed')
if config.packages and config.packages[0] != '':
installation.add_additional_packages(config.packages)
if profile_config := config.profile_config:
profile_handler.install_profile_config(installation, profile_config)
if app_config := config.application_config:
if app_config := config.app_config:
application_handler.install_applications(installation, app_config)
if timezone := config.timezone:

View File

@ -33,6 +33,11 @@ def deprecated_mirror_config() -> Path:
return Path(__file__).parent / 'data' / 'test_deprecated_mirror_config.json'
@pytest.fixture(scope='session')
def deprecated_audio_config() -> Path:
return Path(__file__).parent / 'data' / 'test_deprecated_audio_config.json'
@pytest.fixture(scope='session')
def mirrorlist_no_country_fixture() -> Path:
return Path(__file__).parent / 'data' / 'mirrorlists' / 'test_no_country'

View File

@ -3,6 +3,9 @@
"app_config": {
"bluetooth_config": {
"enabled": true
},
"audio_config": {
"audio": "pipewire"
}
},
"audio_config": {

View File

@ -0,0 +1,5 @@
{
"audio_config": {
"audio": "pipewire"
}
}

View File

@ -6,8 +6,7 @@ from pytest import MonkeyPatch
from archinstall.default_profiles.profile import GreeterType
from archinstall.lib.args import ArchConfig, ArchConfigHandler, Arguments
from archinstall.lib.hardware import GfxDriver
from archinstall.lib.models.application import ApplicationConfiguration, BluetoothConfiguration
from archinstall.lib.models.audio_configuration import Audio, AudioConfiguration
from archinstall.lib.models.application import ApplicationConfiguration, Audio, AudioConfiguration, BluetoothConfiguration
from archinstall.lib.models.bootloader import Bootloader
from archinstall.lib.models.device_model import DiskLayoutConfiguration, DiskLayoutType
from archinstall.lib.models.locale import LocaleConfiguration
@ -128,8 +127,9 @@ def test_config_file_parsing(
assert arch_config == ArchConfig(
version='3.0.2',
application_config=ApplicationConfiguration(
app_config=ApplicationConfiguration(
bluetooth_config=BluetoothConfiguration(enabled=True),
audio_config=AudioConfiguration(audio=Audio.PIPEWIRE),
),
locale_config=LocaleConfiguration(
kb_layout='us',
@ -199,7 +199,6 @@ def test_config_file_parsing(
),
bootloader=Bootloader.Systemd,
uki=False,
audio_config=AudioConfiguration(Audio.PIPEWIRE),
hostname='archy',
kernels=['linux-zen'],
ntp=True,
@ -285,6 +284,27 @@ def test_deprecated_creds_config_parsing(
]
def test_deprecated_audio_config_parsing(
monkeypatch: MonkeyPatch,
deprecated_audio_config: Path,
) -> None:
monkeypatch.setattr(
'sys.argv',
[
'archinstall',
'--config',
str(deprecated_audio_config),
],
)
handler = ArchConfigHandler()
arch_config = handler.config
assert arch_config.app_config == ApplicationConfiguration(
audio_config=AudioConfiguration(audio=Audio.PIPEWIRE),
)
def test_encrypted_creds_with_arg(
monkeypatch: MonkeyPatch,
encrypted_creds_fixture: Path,