feat(hardware): auto-detect audio, bluetooth, camera with declarative override

This commit is contained in:
2026-05-25 04:36:21 +02:00
parent 44f5adc682
commit d2a19cfd5c
21 changed files with 615 additions and 227 deletions

View File

@@ -1,6 +1,4 @@
---
# Installs vendor-matched microcode/firmware/GPU/peripheral packages into /mnt
# based on hardware_profile_active set by environment/_detect_hardware.yml.
- name: Load hardware package definitions
ansible.builtin.include_vars:
file: hardware.yml
@@ -22,7 +20,7 @@
vars:
_family: "{{ bootstrap_hardware_packages[os_family] }}"
_user_driver: "{{ system_cfg.features.gpu.nvidia_driver | default('auto') }}"
_has_nvidia: "{{ 'nvidia' in (hardware_profile_active.gpus | default([])) }}"
_has_nvidia: "{{ 'nvidia' in (hardware_profile_active.gpus | default([]) | difference(_hardware_profile_disable | default([]))) }}"
_supports_open: "{{ hardware_profile_active.nvidia_supports_open | default(true) | bool }}"
_open_pkgs: "{{ _family.gpu_nvidia.open | default([]) }}"
_prop_pkgs: "{{ _family.gpu_nvidia.proprietary | default([]) }}"
@@ -58,101 +56,7 @@
changed_when: _rpmfusion_result.rc == 0
- name: Resolve hardware package set
vars:
_family: "{{ bootstrap_hardware_packages[os_family] }}"
_cpu: "{{ hardware_profile_active.cpu | default('') | string }}"
_gpus: "{{ hardware_profile_active.gpus | default([]) | list }}"
_wifi: "{{ hardware_profile_active.wireless | default([]) | list }}"
_fp_detected: "{{ hardware_profile_active.fingerprint | default(false) | bool }}"
_firmware_on: "{{ system_cfg.features.firmware.enabled | bool }}"
_microcode_on: "{{ _firmware_on and (system_cfg.features.firmware.microcode | bool) }}"
_gpu_on: "{{ system_cfg.features.gpu.enabled | bool }}"
_peripherals_on: "{{ system_cfg.features.peripherals.enabled | bool }}"
_webcam_pref: "{{ system_cfg.features.peripherals.webcam | default('auto') }}"
_fp_pref: "{{ system_cfg.features.peripherals.fingerprint | default('auto') }}"
_dl_on: "{{ system_cfg.features.peripherals.displaylink | bool }}"
_webcam_on: >-
{{
_peripherals_on
and (_webcam_pref == 'true' or (_webcam_pref == 'auto' and _peripherals_on))
}}
_fp_on: >-
{{
_peripherals_on
and (_fp_pref == 'true' or (_fp_pref == 'auto' and _fp_detected))
}}
# Union of GPU/wireless/CPU vendors; CPU vendor is included so Intel-CPU
# systems pull i915/iwlwifi firmware via the same vendor split.
_cpu_vendor_list: "{{ ([_cpu] if (_cpu | length > 0) else []) | list }}"
_firmware_vendors: >-
{{
(_firmware_on | ternary(
(_gpus + _wifi + _cpu_vendor_list)
| reject('equalto', '') | unique | list,
[]
))
}}
_microcode_pkgs: >-
{{
((_microcode_on and _cpu | length > 0) | ternary(
_family.cpu_microcode[_cpu] | default([]),
[]
)) | list
}}
_firmware_pkgs: >-
{{
(_firmware_on | ternary(
(_family.firmware_base | default([]) | list)
+ (_firmware_vendors
| map('extract', _family.firmware | default({}))
| select('truthy')
| list
| sum(start=[])),
[]
)) | list
}}
_gpu_base_pkgs: "{{ (_gpu_on | ternary(_family.gpu_base | default([]), [])) | list }}"
_gpu_vendor_pkgs: >-
{{
(_gpu_on | ternary(
(_gpus | reject('equalto', 'nvidia') | list)
| map('extract', _family.gpu | default({}))
| select('truthy')
| list
| sum(start=[]),
[]
)) | list
}}
_gpu_nvidia_pkgs: >-
{{
((_gpu_on and ('nvidia' in _gpus)) | ternary(
_family.gpu_nvidia[_nvidia_driver_resolved] | default([]),
[]
)) | list
}}
_peripherals_base_pkgs: >-
{{
(_webcam_on | ternary(_family.peripherals_base | default([]), [])) | list
}}
_peripherals_fingerprint_pkgs: >-
{{
(_fp_on | ternary(_family.peripherals_fingerprint | default([]), [])) | list
}}
_peripherals_displaylink_pkgs: >-
{{
(_dl_on | ternary(_family.peripherals_displaylink | default([]), [])) | list
}}
ansible.builtin.set_fact:
_hardware_packages: >-
{{
(_microcode_pkgs + _firmware_pkgs
+ _gpu_base_pkgs + _gpu_vendor_pkgs + _gpu_nvidia_pkgs
+ _peripherals_base_pkgs + _peripherals_fingerprint_pkgs
+ _peripherals_displaylink_pkgs)
| reject('equalto', '')
| unique
| list
}}
ansible.builtin.include_tasks: _resolve_hardware_packages.yml
- name: Report hardware package selection
ansible.builtin.debug:
@@ -165,6 +69,15 @@
fingerprint={{ hardware_profile_active.fingerprint | default(false) }}
-> {{ _hardware_packages | length }} package(s)
- name: Note Intel IPU6 camera (out-of-tree stack)
when: hardware_profile_active.camera.ipu6 | default(false) | bool
ansible.builtin.debug:
msg: >-
Intel IPU6 MIPI camera detected. Its driver stack (intel-ipu6 firmware,
DKMS module, v4l2-relayd, libcamera) is out-of-tree/AUR and is NOT auto-
installed. Pin the packages in a hardware group via
system.features.hardware.packages[{{ os_family }}].
- name: Install hardware packages
when: _hardware_packages | length > 0
vars:

View File

@@ -0,0 +1,125 @@
---
# Split out of _hardware.yml so fixtures can seed the inputs and assert the
# resolved _hardware_packages list with no chroot/install.
- name: Resolve hardware package set
vars:
_family: "{{ bootstrap_hardware_packages[os_family] }}"
_disable: "{{ _hardware_profile_disable | default([]) | list }}"
_profile_packages: "{{ (_hardware_profile_packages | default({}))[os_family] | default([]) | list }}"
_cpu: "{{ hardware_profile_active.cpu | default('') | string }}"
_gpus: "{{ hardware_profile_active.gpus | default([]) | difference(_disable) | list }}"
_wifi: "{{ hardware_profile_active.wireless | default([]) | difference(_disable) | list }}"
_fp_detected: "{{ hardware_profile_active.fingerprint | default(false) | bool }}"
_audio: "{{ hardware_profile_active.audio | default([]) | difference(_disable) | list }}"
_bt_detected: "{{ hardware_profile_active.bluetooth | default(false) | bool }}"
_firmware_on: "{{ system_cfg.features.firmware.enabled | bool }}"
_microcode_on: "{{ _firmware_on and (system_cfg.features.firmware.microcode | bool) }}"
_gpu_on: "{{ system_cfg.features.gpu.enabled | bool }}"
_peripherals_on: "{{ system_cfg.features.peripherals.enabled | bool }}"
_camera_pref: "{{ system_cfg.features.peripherals.camera | default('auto') }}"
_camera_uvc: "{{ hardware_profile_active.camera.uvc | default(false) | bool }}"
_camera_ipu6: "{{ hardware_profile_active.camera.ipu6 | default(false) | bool }}"
_fp_pref: "{{ system_cfg.features.peripherals.fingerprint | default('auto') }}"
_audio_pref: "{{ system_cfg.features.peripherals.audio | default('auto') }}"
_bt_pref: "{{ system_cfg.features.peripherals.bluetooth | default('auto') }}"
_dl_on: "{{ (system_cfg.features.peripherals.displaylink | bool) and ('displaylink' not in _disable) }}"
_camera_on: >-
{{
_peripherals_on
and ('camera' not in _disable)
and (_camera_pref == 'true' or (_camera_pref == 'auto' and (_camera_uvc or _camera_ipu6)))
}}
_fp_on: >-
{{
_peripherals_on
and ('fingerprint' not in _disable)
and (_fp_pref == 'true' or (_fp_pref == 'auto' and _fp_detected))
}}
_audio_on: >-
{{
_peripherals_on
and ('audio' not in _disable)
and (_audio_pref == 'true' or (_audio_pref == 'auto' and (_audio | length > 0)))
}}
_bt_on: >-
{{
_peripherals_on
and ('bluetooth' not in _disable)
and (_bt_pref == 'true' or (_bt_pref == 'auto' and _bt_detected))
}}
# Union of GPU/wireless/CPU vendors; CPU vendor is included so Intel-CPU
# systems pull i915/iwlwifi firmware via the same vendor split.
_cpu_vendor_list: "{{ ([_cpu] if (_cpu | length > 0 and _cpu not in _disable) else []) | list }}"
_firmware_vendors: >-
{{
(_firmware_on | ternary(
(_gpus + _wifi + _cpu_vendor_list)
| reject('equalto', '') | unique | list,
[]
))
}}
_microcode_pkgs: >-
{{
((_microcode_on and _cpu | length > 0 and _cpu not in _disable) | ternary(
_family.cpu_microcode[_cpu] | default([]),
[]
)) | list
}}
_firmware_pkgs: >-
{{
(_firmware_on | ternary(
(_family.firmware_base | default([]) | list)
+ (_firmware_vendors
| map('extract', _family.firmware | default({}))
| select('truthy')
| list
| sum(start=[])),
[]
)) | list
}}
_gpu_base_pkgs: "{{ (_gpu_on | ternary(_family.gpu_base | default([]), [])) | list }}"
_gpu_vendor_pkgs: >-
{{
(_gpu_on | ternary(
(_gpus | reject('equalto', 'nvidia') | list)
| map('extract', _family.gpu | default({}))
| select('truthy')
| list
| sum(start=[]),
[]
)) | list
}}
_gpu_nvidia_pkgs: >-
{{
((_gpu_on and ('nvidia' in _gpus)) | ternary(
_family.gpu_nvidia[_nvidia_driver_resolved] | default([]),
[]
)) | list
}}
_camera_base_pkgs: >-
{{
(_camera_on | ternary(_family.camera_base | default([]), [])) | list
}}
_peripherals_fingerprint_pkgs: >-
{{
(_fp_on | ternary(_family.peripherals_fingerprint | default([]), [])) | list
}}
_peripherals_displaylink_pkgs: >-
{{
(_dl_on | ternary(_family.peripherals_displaylink | default([]), [])) | list
}}
_audio_base_pkgs: "{{ (_audio_on | ternary(_family.audio_base | default([]), [])) | list }}"
_bluetooth_base_pkgs: "{{ (_bt_on | ternary(_family.bluetooth_base | default([]), [])) | list }}"
ansible.builtin.set_fact:
_hardware_packages: >-
{{
(_microcode_pkgs + _firmware_pkgs
+ _gpu_base_pkgs + _gpu_vendor_pkgs + _gpu_nvidia_pkgs
+ _audio_base_pkgs + _bluetooth_base_pkgs
+ _camera_base_pkgs + _peripherals_fingerprint_pkgs
+ _peripherals_displaylink_pkgs
+ _profile_packages)
| reject('equalto', '')
| unique
| list
}}

View File

@@ -1,18 +1,7 @@
---
# Hardware-aware package definitions keyed by os_family. Schema:
# cpu_microcode[intel|amd] CPU vendor microcode
# firmware_base unconditional firmware packages
# firmware[<vendor>] vendor-split firmware (intel|amd|nvidia|
# atheros|broadcom|mediatek|marvell|realtek|
# qcom|cirrus|other)
# gpu_base mesa + vulkan loader
# gpu[intel|amd] per-GPU userspace
# gpu_nvidia[open|proprietary|nouveau] nvidia driver flavor
# peripherals_base webcam/scanner stack
# peripherals_fingerprint fprintd + libfprint
# peripherals_displaylink evdi kernel module + DisplayLink helpers
# Only packages matching detected hardware are installed; families without
# vendor splits collapse to a single firmware meta package.
# Hardware-aware package definitions keyed by os_family, consumed by
# _resolve_hardware_packages.yml. Only packages matching detected hardware are
# installed; families without vendor splits collapse to one firmware meta package.
bootstrap_hardware_packages:
Archlinux:
cpu_microcode:
@@ -40,9 +29,11 @@ bootstrap_hardware_packages:
proprietary: [nvidia-dkms, nvidia-utils]
# Wayland-only: kernel nouveau module + mesa/gbm drive the display; no Xorg DDX.
nouveau: [vulkan-nouveau]
peripherals_base: [v4l-utils]
camera_base: [v4l-utils]
peripherals_fingerprint: [fprintd, libfprint]
peripherals_displaylink: [] # AUR only; user must wire in AUR helper
audio_base: [sof-firmware, alsa-ucm-conf]
bluetooth_base: [bluez, bluez-utils]
Debian:
cpu_microcode:
@@ -72,9 +63,11 @@ bootstrap_hardware_packages:
proprietary: [nvidia-driver, nvidia-vulkan-icd]
# Wayland-only: kernel module + mesa (gpu_base) cover it; no Xorg DDX, no extra pkg.
nouveau: []
peripherals_base: [v4l-utils]
camera_base: [v4l-utils]
peripherals_fingerprint: [fprintd, libpam-fprintd]
peripherals_displaylink: [evdi-dkms] # userspace driver still needs vendor .run
audio_base: [firmware-sof-signed, alsa-ucm-conf]
bluetooth_base: [bluez]
RedHat:
cpu_microcode:
@@ -103,6 +96,8 @@ bootstrap_hardware_packages:
proprietary: [akmod-nvidia, xorg-x11-drv-nvidia, xorg-x11-drv-nvidia-cuda]
# Wayland-only: kernel module + mesa (gpu_base) cover it; no Xorg DDX, no extra pkg.
nouveau: []
peripherals_base: [v4l-utils]
camera_base: [v4l-utils]
peripherals_fingerprint: [fprintd, fprintd-pam]
peripherals_displaylink: [evdi] # COPR-supplied; repo enablement deferred
audio_base: [alsa-sof-firmware, alsa-ucm]
bluetooth_base: [bluez]

View File

@@ -7,7 +7,7 @@
line: "{{ item.line }}"
loop:
- regexp: ^GRUB_CMDLINE_LINUX_DEFAULT=
line: GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3"
line: 'GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3{{ (" " ~ (_hardware_profile_kernel_params | join(" "))) if (_hardware_profile_kernel_params | default([]) | length > 0) else "" }}"'
- regexp: ^GRUB_TIMEOUT=
line: GRUB_TIMEOUT=1
loop_control:
@@ -43,19 +43,21 @@
}}
grub_root_flags: >-
{{ ['rootflags=subvol=@'] if system_cfg.filesystem == 'btrfs' else [] }}
# String-concat (not list-concat like grub_kernel_cmdline_base below): ansible-lint's
# jinja render trips on list+list when grub_lvm_args leads the expression here.
grub_cmdline_linux_base: >-
{{
(['crashkernel=auto'] + grub_lvm_args)
| join(' ')
((grub_lvm_args | join(' ')) ~ ' ' ~ (_hardware_profile_kernel_params | default([]) | join(' '))) | trim
}}
grub_kernel_cmdline_base: >-
{{
(
(['root=UUID=' + grub_root_uuid]
if grub_root_uuid | length > 0 else [])
+ ['ro', 'crashkernel=auto']
+ ['ro']
+ grub_lvm_args
+ grub_root_flags
+ (_hardware_profile_kernel_params | default([]))
)
| join(' ')
}}

View File

@@ -1,24 +1,21 @@
---
# Connection and timing
environment_wait_timeout: 180
environment_wait_delay: 5
# Pacman installer settings
environment_parallel_downloads: 20
environment_pacman_lock_timeout: 120
environment_pacman_retries: 4
environment_pacman_retry_delay: 15
# Libraries the installer tools pull whose soname may have bumped past the ISO.
# Each library's installed reverse-dependencies are upgraded together with the
# tools so a current install onto an older ISO stays a consistent transaction
# instead of a partial upgrade. Extend if a future transition breaks the install.
# Installer-tool libraries whose soname may have bumped past the ISO. Each one's
# installed reverse-deps are co-upgraded so the install stays a consistent
# transaction. Extend if a future transition breaks the install.
environment_partial_upgrade_libs:
- nettle
- leancrypto
# PCI vendor IDs -> vendor codes used by hardware detection.
# Only vendors that drive distinct firmware/driver packages are mapped.
# PCI vendor ID -> vendor code. Only vendors that drive distinct
# firmware/driver packages are mapped.
environment_pci_vendor_map:
"8086": intel
"1002": amd
@@ -37,8 +34,8 @@ environment_pci_vendor_map:
"1cf3": cirrus
"13d7": cirrus
# USB vendor IDs of fingerprint readers supported by libfprint / fprintd.
# Lowercase, four-digit hex; matched against `lsusb` output.
# USB vendor IDs of fingerprint readers supported by libfprint / fprintd,
# matched against `lsusb` output.
environment_fingerprint_vendor_ids:
- "06cb" # Synaptics (modern ThinkPad/Dell)
- "138a" # Validity Sensors (older ThinkPad)
@@ -49,3 +46,15 @@ environment_fingerprint_vendor_ids:
- "08ff" # AuthenTec (legacy)
- "147e" # Upek (legacy)
- "1491" # Futronic
# USB vendor IDs of common Bluetooth controllers. A fallback: detection also
# matches the literal "Bluetooth" string in `lsusb` for adapters that omit it.
environment_bluetooth_vendor_ids:
- "8087" # Intel (AX2xx combo cards)
- "0a12" # Cambridge Silicon Radio (CSR)
- "0bda" # Realtek
- "0cf3" # Qualcomm Atheros
- "13d3" # IMC / AzureWave
- "0489" # Foxconn / Lite-On
- "04ca" # Lite-On
- "0b05" # ASUS

View File

@@ -1,22 +1,6 @@
---
# Hardware detection on the live installer host.
#
# Resolves system_cfg.features.hardware.profile when not explicitly set, so
# downstream bootstrap can install vendor-matched microcode/firmware/GPU/
# peripheral packages. When the user supplies an override profile, detection
# is skipped (golden-image flow: bake an image with a fixed profile).
#
# Output fact: hardware_profile_active = {
# cpu: 'intel'|'amd'|'',
# gpus: list of 'intel'|'amd'|'nvidia',
# nvidia_supports_open: bool, # true when all detected Nvidia GPUs are
# # Turing or newer (device id >= 0x1e00)
# wireless: list of vendor codes ('intel'|'realtek'|'atheros'|...),
# fingerprint: bool, # USB fingerprint reader detected
# }
#
# Skipped entirely when neither firmware/gpu/peripherals features are enabled.
# A user-supplied override profile skips detection (golden-image flow: bake an
# image with a fixed profile).
- name: Resolve hardware detection requirement
ansible.builtin.set_fact:
_hardware_detection_needed: >-
@@ -37,7 +21,12 @@
gpus: "{{ _hardware_profile_override.gpus | default([]) | map('lower') | list }}"
nvidia_supports_open: "{{ _hardware_profile_override.nvidia_supports_open | default(true) | bool }}"
wireless: "{{ _hardware_profile_override.wireless | default([]) | map('lower') | list }}"
audio: "{{ _hardware_profile_override.audio | default([]) | map('lower') | list }}"
fingerprint: "{{ _hardware_profile_override.fingerprint | default(false) | bool }}"
bluetooth: "{{ _hardware_profile_override.bluetooth | default(false) | bool }}"
camera:
uvc: "{{ _hardware_profile_override.camera.uvc | default(false) | bool }}"
ipu6: "{{ _hardware_profile_override.camera.ipu6 | default(false) | bool }}"
- name: Detect hardware from live host
when:
@@ -61,71 +50,7 @@
failed_when: false
- name: Resolve detected hardware profile
vars:
_vendor_keys: "{{ environment_pci_vendor_map.keys() | list }}"
_cpu_vendor_raw: >-
{{
_hardware_lscpu.stdout
| regex_search('(?im)^Vendor ID:\\s*(\\S+)', '\\1')
| default([''], true)
| first
}}
_cpu_vendor: >-
{{
'intel' if _cpu_vendor_raw == 'GenuineIntel'
else ('amd' if _cpu_vendor_raw == 'AuthenticAMD' else '')
}}
# PCI classes: 0300 = VGA, 0302 = 3D, 0280 = wireless network controller.
_gpu_lines: "{{ _hardware_lspci.stdout_lines | select('search', '\\[(0300|0302)\\]:') | list }}"
_gpu_pairs: >-
{{
_gpu_lines
| map('regex_search', '\\[([0-9a-f]{4}):([0-9a-f]{4})\\]', '\\1', '\\2')
| select('truthy')
| list
}}
_gpu_vendor_ids: "{{ _gpu_pairs | map('first') | select('in', _vendor_keys) | list }}"
_gpu_vendors: "{{ _gpu_vendor_ids | map('extract', environment_pci_vendor_map) | unique | list }}"
_nvidia_device_ids: >-
{{
_gpu_pairs
| selectattr('0', 'equalto', '10de')
| map(attribute=1)
| list
}}
_nvidia_min_id: >-
{{
(_nvidia_device_ids | map('int', base=16) | list | min)
if _nvidia_device_ids | length > 0 else 0
}}
# 0x1e00 = 7680 = first Turing device id; Turing+ supports nvidia-open.
_nvidia_supports_open: "{{ _nvidia_device_ids | length > 0 and (_nvidia_min_id | int) >= 7680 }}"
_wifi_lines: "{{ _hardware_lspci.stdout_lines | select('search', '\\[0280\\]:') | list }}"
_wifi_vendor_ids: >-
{{
_wifi_lines
| map('regex_search', '\\[([0-9a-f]{4}):[0-9a-f]{4}\\]', '\\1')
| select('truthy')
| map('first')
| select('in', _vendor_keys)
| list
}}
_wifi_vendors: "{{ _wifi_vendor_ids | map('extract', environment_pci_vendor_map) | unique | list }}"
_fingerprint_present: >-
{{
(_hardware_lsusb.stdout | default(''))
| regex_search(
'(?i)ID (' ~ (environment_fingerprint_vendor_ids | join('|')) ~ '):'
)
is not none
}}
ansible.builtin.set_fact:
hardware_profile_active:
cpu: "{{ _cpu_vendor }}"
gpus: "{{ _gpu_vendors }}"
nvidia_supports_open: "{{ _nvidia_supports_open | bool }}"
wireless: "{{ _wifi_vendors }}"
fingerprint: "{{ _fingerprint_present | bool }}"
ansible.builtin.include_tasks: _resolve_hardware_profile.yml
- name: Initialize empty hardware profile when detection skipped
when: not (_hardware_detection_needed | bool)
@@ -135,7 +60,14 @@
gpus: []
nvidia_supports_open: true
wireless: []
audio: []
fingerprint: false
bluetooth: false
camera: { uvc: false, ipu6: false }
- name: Merge declarative hardware group over detection
when: _hardware_detection_needed | bool
ansible.builtin.include_tasks: _merge_hardware_profile.yml
- name: Report active hardware profile
when: _hardware_detection_needed | bool
@@ -146,4 +78,7 @@
gpus={{ hardware_profile_active.gpus | default([]) | join(',') | default('-', true) }}
{{ '(open-supported)' if hardware_profile_active.nvidia_supports_open | bool else '(legacy)' }},
wireless={{ hardware_profile_active.wireless | default([]) | join(',') | default('-', true) }},
fingerprint={{ hardware_profile_active.fingerprint | default(false) }}
audio={{ hardware_profile_active.audio | default([]) | join(',') | default('-', true) }},
fingerprint={{ hardware_profile_active.fingerprint | default(false) }},
bluetooth={{ hardware_profile_active.bluetooth | default(false) }},
camera={{ 'uvc' if hardware_profile_active.camera.uvc | default(false) else '' }}{{ '+ipu6' if hardware_profile_active.camera.ipu6 | default(false) else '' }}

View File

@@ -0,0 +1,22 @@
---
# Supplements whatever profile is active (detected or full-override) rather than
# replacing it: vendor lists union, booleans OR, cpu overrides when set.
- name: Merge declarative hardware group over detection
vars:
_hw: "{{ system_cfg.features.hardware }}"
_det: "{{ hardware_profile_active }}"
ansible.builtin.set_fact:
hardware_profile_active:
cpu: "{{ (_hw.cpu | default('') | string | lower) if (_hw.cpu | default('') | length > 0) else _det.cpu }}"
gpus: "{{ ((_det.gpus | default([])) + (_hw.gpus | default([]) | map('lower') | list)) | unique | list }}"
nvidia_supports_open: "{{ _det.nvidia_supports_open | default(true) | bool }}"
wireless: "{{ ((_det.wireless | default([])) + (_hw.wireless | default([]) | map('lower') | list)) | unique | list }}"
audio: "{{ ((_det.audio | default([])) + (_hw.audio | default([]) | map('lower') | list)) | unique | list }}"
fingerprint: "{{ (_det.fingerprint | default(false) | bool) or (_hw.fingerprint | default(false) | bool) }}"
bluetooth: "{{ (_det.bluetooth | default(false) | bool) or (_hw.bluetooth | default(false) | bool) }}"
camera:
uvc: "{{ (_det.camera.uvc | default(false) | bool) or (_hw.camera.uvc | default(false) | bool) }}"
ipu6: "{{ (_det.camera.ipu6 | default(false) | bool) or (_hw.camera.ipu6 | default(false) | bool) }}"
_hardware_profile_packages: "{{ _hw.packages | default({}) }}"
_hardware_profile_disable: "{{ _hw.disable | default([]) | list }}"
_hardware_profile_kernel_params: "{{ _hw.kernel_params | default([]) | list }}"

View File

@@ -0,0 +1,57 @@
---
# Split out of _detect_hardware.yml so fixtures can seed the lscpu/lspci/lsusb
# registers and assert the result with no real hardware. Keep regex exprs
# double-quoted single-line: ansible-core 2.21 set_fact mangles backslash escapes
# inside folded (>-) scalars.
- name: Resolve detected hardware profile
vars:
_vendor_keys: "{{ environment_pci_vendor_map.keys() | list }}"
_cpu_vendor_raw: "{{ _hardware_lscpu.stdout | regex_findall('(?im)^Vendor ID:\\s*(\\S+)') | first | default('') }}"
_cpu_vendor: >-
{{
'intel' if _cpu_vendor_raw == 'GenuineIntel'
else ('amd' if _cpu_vendor_raw == 'AuthenticAMD' else '')
}}
# PCI classes: 0300 = VGA, 0302 = 3D, 0280 = wireless network controller.
_gpu_lines: "{{ _hardware_lspci.stdout_lines | select('search', '\\[(0300|0302)\\]:') | list }}"
_gpu_pairs: "{{ (_gpu_lines | join('\n')) | regex_findall('\\[([0-9a-f]{4}):([0-9a-f]{4})\\]') | list }}"
_gpu_vendor_ids: "{{ _gpu_pairs | map('first') | select('in', _vendor_keys) | list }}"
_gpu_vendors: "{{ _gpu_vendor_ids | map('extract', environment_pci_vendor_map) | unique | list }}"
_nvidia_device_ids: "{{ _gpu_pairs | selectattr('0', 'equalto', '10de') | map(attribute=1) | list }}"
_nvidia_min_id: >-
{{
(_nvidia_device_ids | map('int', base=16) | list | min)
if _nvidia_device_ids | length > 0 else 0
}}
# 0x1e00 = 7680 = first Turing device id; Turing+ supports nvidia-open.
_nvidia_supports_open: "{{ _nvidia_device_ids | length > 0 and (_nvidia_min_id | int) >= 7680 }}"
_wifi_lines: "{{ _hardware_lspci.stdout_lines | select('search', '\\[0280\\]:') | list }}"
_wifi_vendor_ids: "{{ (_wifi_lines | join('\n')) | regex_findall('\\[([0-9a-f]{4}):[0-9a-f]{4}\\]') | select('in', _vendor_keys) | list }}"
_wifi_vendors: "{{ _wifi_vendor_ids | map('extract', environment_pci_vendor_map) | unique | list }}"
# PCI class 0403 = audio device (HD-audio controller). Vendor drives SOF/firmware.
_audio_lines: "{{ _hardware_lspci.stdout_lines | select('search', '\\[0403\\]:') | list }}"
_audio_vendor_ids: "{{ (_audio_lines | join('\n')) | regex_findall('\\[([0-9a-f]{4}):[0-9a-f]{4}\\]') | select('in', _vendor_keys) | list }}"
_audio_vendors: "{{ _audio_vendor_ids | map('extract', environment_pci_vendor_map) | unique | list }}"
_fingerprint_present: "{{ (_hardware_lsusb.stdout | default('')) | regex_search('(?i)ID (' ~ (environment_fingerprint_vendor_ids | join('|')) ~ '):') is not none }}"
_camera_uvc_present: "{{ (_hardware_lsusb.stdout | default('')) is search('(?i)camera|webcam') }}"
# Intel IPU6 MIPI camera: PCI class 0480 (multimedia) under Intel 8086, or an ISP description. Out-of-tree userspace.
_camera_ipu6_desc: "{{ (_hardware_lspci.stdout | default('')) is search('(?i)image signal processor|IPU6') }}"
_camera_ipu6_pci: "{{ (_hardware_lspci.stdout_lines | select('search', '\\[0480\\]:') | select('search', '\\[8086:') | list) | length > 0 }}"
# No backslash escapes here, so a folded scalar is safe (unlike the \[..\] regexes above).
_bluetooth_present: >-
{{
((_hardware_lsusb.stdout | default('')) | regex_search('(?i)ID (' ~ (environment_bluetooth_vendor_ids | join('|')) ~ '):') is not none)
or ((_hardware_lsusb.stdout | default('')) is search('(?i)bluetooth'))
}}
ansible.builtin.set_fact:
hardware_profile_active:
cpu: "{{ _cpu_vendor }}"
gpus: "{{ _gpu_vendors }}"
nvidia_supports_open: "{{ _nvidia_supports_open | bool }}"
wireless: "{{ _wifi_vendors }}"
audio: "{{ _audio_vendors }}"
fingerprint: "{{ _fingerprint_present | bool }}"
bluetooth: "{{ _bluetooth_present | bool }}"
camera:
uvc: "{{ _camera_uvc_present | bool }}"
ipu6: "{{ (_camera_ipu6_desc | bool) or (_camera_ipu6_pci | bool) }}"

View File

@@ -143,11 +143,27 @@ system_defaults:
nvidia_driver: "auto" # auto | open | proprietary | nouveau
peripherals:
enabled: "auto" # auto = follows desktop.enabled
fingerprint: "auto"
webcam: "auto"
fingerprint: "auto" # auto|true|false (auto = install when detected)
camera: "auto" # v4l-utils when a UVC/IPU6 camera is detected
audio: "auto" # SOF firmware + ALSA UCM when an audio device is present
bluetooth: "auto" # bluez when a Bluetooth controller is present
displaylink: false
hardware:
profile: {} # empty = autodetect; set to override (golden image)
profile: {} # full override: non-empty SKIPS detection (golden image)
# Declarative hardware group: a per-device profile that MERGES over
# auto-detect (auto-detect = base; these supplement/override it). Vendor
# lists union with detection, booleans OR with detection, packages append,
# disable[] force-off (applied last), kernel_params append to the cmdline.
cpu: "" # pin a CPU vendor (intel|amd); empty = use detection
gpus: [] # extra GPU vendor codes to force
wireless: [] # extra wireless vendor codes to force
audio: [] # extra audio vendor codes to force
camera: {} # {uvc: true, ipu6: true} to force a camera kind
fingerprint: false # force-on a fingerprint reader detection missed
bluetooth: false # force-on a Bluetooth controller detection missed
packages: {} # per-os_family extra packages, e.g. {Archlinux: [intel-ipu6-dkms]}
disable: [] # feature/vendor names to force-off (audio|bluetooth|camera|fingerprint|displaylink|<vendor>)
kernel_params: [] # extra kernel cmdline params (quirks), e.g. ["i915.enable_psr=0"]
# Per-hypervisor required fields - drives data-driven validation.
# All virtual types additionally require network bridge or interfaces.

View File

@@ -206,23 +206,45 @@
if (system_raw.features.peripherals.enabled | string | lower) == 'auto'
else (system_raw.features.peripherals.enabled | bool)
}}
# fingerprint/webcam stay tri-state ('auto'|'true'|'false') because the
# 'auto' branch is resolved at install time using detection results.
# fingerprint/camera/audio/bluetooth stay tri-state ('auto'|'true'|'false')
# because the 'auto' branch is resolved at install time using detection results.
fingerprint: >-
{{
'auto'
if (system_raw.features.peripherals.fingerprint | string | lower) == 'auto'
else (system_raw.features.peripherals.fingerprint | bool | string | lower)
}}
webcam: >-
camera: >-
{{
'auto'
if (system_raw.features.peripherals.webcam | string | lower) == 'auto'
else (system_raw.features.peripherals.webcam | bool | string | lower)
if (system_raw.features.peripherals.camera | string | lower) == 'auto'
else (system_raw.features.peripherals.camera | bool | string | lower)
}}
audio: >-
{{
'auto'
if (system_raw.features.peripherals.audio | string | lower) == 'auto'
else (system_raw.features.peripherals.audio | bool | string | lower)
}}
bluetooth: >-
{{
'auto'
if (system_raw.features.peripherals.bluetooth | string | lower) == 'auto'
else (system_raw.features.peripherals.bluetooth | bool | string | lower)
}}
displaylink: "{{ system_raw.features.peripherals.displaylink | bool }}"
hardware:
profile: "{{ system_raw.features.hardware.profile | default({}) }}"
cpu: "{{ system_raw.features.hardware.cpu | default('') | string }}"
gpus: "{{ system_raw.features.hardware.gpus | default([]) | list }}"
wireless: "{{ system_raw.features.hardware.wireless | default([]) | list }}"
audio: "{{ system_raw.features.hardware.audio | default([]) | list }}"
camera: "{{ system_raw.features.hardware.camera | default({}) }}"
fingerprint: "{{ system_raw.features.hardware.fingerprint | default(false) | bool }}"
bluetooth: "{{ system_raw.features.hardware.bluetooth | default(false) | bool }}"
packages: "{{ system_raw.features.hardware.packages | default({}) }}"
disable: "{{ system_raw.features.hardware.disable | default([]) | list }}"
kernel_params: "{{ system_raw.features.hardware.kernel_params | default([]) | list }}"
hostname: "{{ system_name }}"
os: "{{ system_os_input if system_os_input | length > 0 else (physical_default_os if system_type == 'physical' else '') }}"
os_version: "{{ system_raw.version | default('') | string }}"

View File

@@ -241,15 +241,21 @@
- system_cfg.features.gpu.nvidia_driver in ["auto", "open", "proprietary", "nouveau"]
- system_cfg.features.peripherals.enabled is defined
- system_cfg.features.peripherals.fingerprint in ["auto", "true", "false"]
- system_cfg.features.peripherals.webcam in ["auto", "true", "false"]
- system_cfg.features.peripherals.camera in ["auto", "true", "false"]
- system_cfg.features.peripherals.audio in ["auto", "true", "false"]
- system_cfg.features.peripherals.bluetooth in ["auto", "true", "false"]
- system_cfg.features.peripherals.displaylink is defined
- system_cfg.features.hardware.profile is mapping
- system_cfg.features.hardware.packages is mapping
- system_cfg.features.hardware.camera is mapping
- system_cfg.features.hardware.disable is sequence
- system_cfg.features.hardware.kernel_params is sequence
fail_msg: >-
Invalid hardware feature flags. firmware.enabled/microcode,
peripherals.enabled and peripherals.displaylink must be bool (or 'auto'
sentinel for firmware); gpu.nvidia_driver in
[auto|open|proprietary|nouveau]; peripherals.fingerprint and
peripherals.webcam in [auto|true|false]; hardware.profile must be a dict.
[auto|open|proprietary|nouveau]; peripherals.fingerprint/camera/audio/
bluetooth in [auto|true|false]; hardware.profile must be a dict.
quiet: true
- name: Validate desktop environment