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

@@ -287,7 +287,7 @@ The bootstrap auto-switches to dracut when `method: tpm2` is set. Override via `
| `desktop.*` | dict | see below | Desktop environment settings (see [4.2.5](#425-systemfeaturesdesktop)) | | `desktop.*` | dict | see below | Desktop environment settings (see [4.2.5](#425-systemfeaturesdesktop)) |
| `firmware.*` | dict | see below | Vendor firmware blobs and CPU microcode (see [4.2.6](#426-systemfeaturesfirmware)) | | `firmware.*` | dict | see below | Vendor firmware blobs and CPU microcode (see [4.2.6](#426-systemfeaturesfirmware)) |
| `gpu.*` | dict | see below | Mesa/Vulkan and per-vendor GPU userspace (see [4.2.7](#427-systemfeaturesgpu)) | | `gpu.*` | dict | see below | Mesa/Vulkan and per-vendor GPU userspace (see [4.2.7](#427-systemfeaturesgpu)) |
| `peripherals.*` | dict | see below | Fingerprint readers, webcams, DisplayLink (see [4.2.8](#428-systemfeaturesperipherals)) | | `peripherals.*` | dict | see below | Fingerprint, camera, audio, bluetooth, DisplayLink (see [4.2.8](#428-systemfeaturesperipherals)) |
| `hardware.*` | dict | see below | Hardware-detection profile override (see [4.2.9](#429-systemfeatureshardware)) | | `hardware.*` | dict | see below | Hardware-detection profile override (see [4.2.9](#429-systemfeatureshardware)) |
**Initramfs generator auto-detection:** RedHat -> dracut, Arch -> mkinitcpio, Debian/Ubuntu -> initramfs-tools. **Initramfs generator auto-detection:** RedHat -> dracut, Arch -> mkinitcpio, Debian/Ubuntu -> initramfs-tools.
@@ -359,13 +359,20 @@ automatically when needed. Debian uses `nvidia-driver` from the `non-free` compo
managed `sources.list`). Ubuntu uses `restricted`. Arch ships both `nvidia-open-dkms` and `nvidia-dkms` in managed `sources.list`). Ubuntu uses `restricted`. Arch ships both `nvidia-open-dkms` and `nvidia-dkms` in
the `extra` repository - no third-party setup required. the `extra` repository - no third-party setup required.
> **Known limitation - Nvidia on Enterprise Linux (AlmaLinux/Rocky/RHEL):** the EL `akmod-nvidia*`
> packages live in RPMFusion non-free, and the bootstrap only enables RPMFusion automatically on
> **Fedora**, not on EL. So Nvidia on a bare EL desktop is best-effort: enable RPMFusion (or supply the
> driver repo) out of band, or it falls back to `nouveau`. EL desktops are not a primary target.
#### 4.2.8 `system.features.peripherals` #### 4.2.8 `system.features.peripherals`
| Key | Type | Default | Description | | Key | Type | Default | Description |
| ------------- | --------------- | ------- | ---------------------------------------------------------- | | ------------- | --------------- | ------- | ---------------------------------------------------------- |
| `enabled` | bool \| `auto` | `auto` | Master switch. `auto` follows `desktop.enabled` | | `enabled` | bool \| `auto` | `auto` | Master switch. `auto` follows `desktop.enabled` |
| `fingerprint` | bool \| `auto` | `auto` | `fprintd`/`libfprint`. `auto` = install when reader detected | | `fingerprint` | bool \| `auto` | `auto` | `fprintd`/`libfprint`. `auto` = install when reader detected |
| `webcam` | bool \| `auto` | `auto` | `v4l-utils` and userspace tooling. `auto` follows `enabled` | | `camera` | bool \| `auto` | `auto` | `v4l-utils` for UVC webcams. `auto` = install when a UVC/IPU6 camera is detected (IPU6 out-of-tree stack is logged, not auto-installed) |
| `audio` | bool \| `auto` | `auto` | SOF firmware + ALSA UCM. `auto` = install when an audio device is detected |
| `bluetooth` | bool \| `auto` | `auto` | `bluez`. `auto` = install when a Bluetooth controller is detected |
| `displaylink` | bool | `false` | DisplayLink dock support (explicit opt-in; see notes) | | `displaylink` | bool | `false` | DisplayLink dock support (explicit opt-in; see notes) |
Fingerprint detection scans `lsusb` for known reader vendor IDs (Synaptics, Validity, Goodix, Elan, Egis, Fingerprint detection scans `lsusb` for known reader vendor IDs (Synaptics, Validity, Goodix, Elan, Egis,
@@ -381,7 +388,8 @@ must still be installed manually from DisplayLink's site after first boot. Arch
| Key | Type | Default | Description | | Key | Type | Default | Description |
| --------- | ---- | ------- | -------------------------------------------------------------------- | | --------- | ---- | ------- | -------------------------------------------------------------------- |
| `profile` | dict | `{}` | Hardware-detection override; empty means autodetect from live host | | `profile` | dict | `{}` | Full override: non-empty SKIPS detection (golden image); empty = autodetect |
| group fields | mixed | -- | `cpu`/`gpus`/`wireless`/`audio`/`camera`/`fingerprint`/`bluetooth`/`packages`/`disable`/`kernel_params` MERGE over autodetect (see below) |
When empty, hardware is detected at the start of the bootstrap. When set, detection is skipped and the When empty, hardware is detected at the start of the bootstrap. When set, detection is skipped and the
supplied profile drives package selection - this is the **golden-image** flow: bake an image with a fixed supplied profile drives package selection - this is the **golden-image** flow: bake an image with a fixed
@@ -402,6 +410,38 @@ system:
fingerprint: false # set true to force fprintd install fingerprint: false # set true to force fprintd install
``` ```
The same keys (minus `profile`) can also be set **directly under `hardware`** as a
declarative **hardware group** that MERGES over auto-detection (auto-detect = base; the
group supplements/overrides it). Unlike `profile`, which skips detection entirely, the
group keeps detection running and layers on top - use it to pin everything a known device
needs so nothing is ever under-set.
| Key | Type | Merge semantics |
| ------------------------- | ---- | -------------------------------------------------------- |
| `cpu` | str | pin the CPU vendor (overrides detection when non-empty) |
| `gpus`/`wireless`/`audio` | list | union with the detected vendor codes |
| `camera` | dict | `{uvc, ipu6}` booleans OR'd with detection |
| `fingerprint`/`bluetooth` | bool | OR'd with detection (force-on) |
| `packages` | dict | per-`os_family` extra packages, added to the install set (deduped; empty entries dropped) |
| `disable` | list | feature/vendor names force-off, applied last |
| `kernel_params` | list | extra kernel cmdline params, appended to the bootloader |
Example - a laptop with an Intel IPU6 camera (out-of-tree stack) and a Cirrus amp, pinned
in a group's `group_vars`:
```yaml
system:
features:
hardware:
bluetooth: true # force-on if detection misses the combo card
camera:
ipu6: true # force the IPU6 path
packages: # out-of-tree/AUR bits detection must not auto-install
Archlinux: [intel-ipu6-dkms, v4l2-relayd, linux-firmware-cirrus]
disable: [displaylink] # never pull DisplayLink on this device
kernel_params: ["i915.enable_psr=0"]
```
### 4.3 `hypervisor` Dictionary ### 4.3 `hypervisor` Dictionary
| Key | Type | Default | Description | | Key | Type | Default | Description |

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 - name: Load hardware package definitions
ansible.builtin.include_vars: ansible.builtin.include_vars:
file: hardware.yml file: hardware.yml
@@ -22,7 +20,7 @@
vars: vars:
_family: "{{ bootstrap_hardware_packages[os_family] }}" _family: "{{ bootstrap_hardware_packages[os_family] }}"
_user_driver: "{{ system_cfg.features.gpu.nvidia_driver | default('auto') }}" _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 }}" _supports_open: "{{ hardware_profile_active.nvidia_supports_open | default(true) | bool }}"
_open_pkgs: "{{ _family.gpu_nvidia.open | default([]) }}" _open_pkgs: "{{ _family.gpu_nvidia.open | default([]) }}"
_prop_pkgs: "{{ _family.gpu_nvidia.proprietary | default([]) }}" _prop_pkgs: "{{ _family.gpu_nvidia.proprietary | default([]) }}"
@@ -58,101 +56,7 @@
changed_when: _rpmfusion_result.rc == 0 changed_when: _rpmfusion_result.rc == 0
- name: Resolve hardware package set - name: Resolve hardware package set
vars: ansible.builtin.include_tasks: _resolve_hardware_packages.yml
_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
}}
- name: Report hardware package selection - name: Report hardware package selection
ansible.builtin.debug: ansible.builtin.debug:
@@ -165,6 +69,15 @@
fingerprint={{ hardware_profile_active.fingerprint | default(false) }} fingerprint={{ hardware_profile_active.fingerprint | default(false) }}
-> {{ _hardware_packages | length }} package(s) -> {{ _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 - name: Install hardware packages
when: _hardware_packages | length > 0 when: _hardware_packages | length > 0
vars: 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: # Hardware-aware package definitions keyed by os_family, consumed by
# cpu_microcode[intel|amd] CPU vendor microcode # _resolve_hardware_packages.yml. Only packages matching detected hardware are
# firmware_base unconditional firmware packages # installed; families without vendor splits collapse to one firmware meta package.
# 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.
bootstrap_hardware_packages: bootstrap_hardware_packages:
Archlinux: Archlinux:
cpu_microcode: cpu_microcode:
@@ -40,9 +29,11 @@ bootstrap_hardware_packages:
proprietary: [nvidia-dkms, nvidia-utils] proprietary: [nvidia-dkms, nvidia-utils]
# Wayland-only: kernel nouveau module + mesa/gbm drive the display; no Xorg DDX. # Wayland-only: kernel nouveau module + mesa/gbm drive the display; no Xorg DDX.
nouveau: [vulkan-nouveau] nouveau: [vulkan-nouveau]
peripherals_base: [v4l-utils] camera_base: [v4l-utils]
peripherals_fingerprint: [fprintd, libfprint] peripherals_fingerprint: [fprintd, libfprint]
peripherals_displaylink: [] # AUR only; user must wire in AUR helper peripherals_displaylink: [] # AUR only; user must wire in AUR helper
audio_base: [sof-firmware, alsa-ucm-conf]
bluetooth_base: [bluez, bluez-utils]
Debian: Debian:
cpu_microcode: cpu_microcode:
@@ -72,9 +63,11 @@ bootstrap_hardware_packages:
proprietary: [nvidia-driver, nvidia-vulkan-icd] proprietary: [nvidia-driver, nvidia-vulkan-icd]
# Wayland-only: kernel module + mesa (gpu_base) cover it; no Xorg DDX, no extra pkg. # Wayland-only: kernel module + mesa (gpu_base) cover it; no Xorg DDX, no extra pkg.
nouveau: [] nouveau: []
peripherals_base: [v4l-utils] camera_base: [v4l-utils]
peripherals_fingerprint: [fprintd, libpam-fprintd] peripherals_fingerprint: [fprintd, libpam-fprintd]
peripherals_displaylink: [evdi-dkms] # userspace driver still needs vendor .run peripherals_displaylink: [evdi-dkms] # userspace driver still needs vendor .run
audio_base: [firmware-sof-signed, alsa-ucm-conf]
bluetooth_base: [bluez]
RedHat: RedHat:
cpu_microcode: cpu_microcode:
@@ -103,6 +96,8 @@ bootstrap_hardware_packages:
proprietary: [akmod-nvidia, xorg-x11-drv-nvidia, xorg-x11-drv-nvidia-cuda] 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. # Wayland-only: kernel module + mesa (gpu_base) cover it; no Xorg DDX, no extra pkg.
nouveau: [] nouveau: []
peripherals_base: [v4l-utils] camera_base: [v4l-utils]
peripherals_fingerprint: [fprintd, fprintd-pam] peripherals_fingerprint: [fprintd, fprintd-pam]
peripherals_displaylink: [evdi] # COPR-supplied; repo enablement deferred 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 }}" line: "{{ item.line }}"
loop: loop:
- regexp: ^GRUB_CMDLINE_LINUX_DEFAULT= - 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= - regexp: ^GRUB_TIMEOUT=
line: GRUB_TIMEOUT=1 line: GRUB_TIMEOUT=1
loop_control: loop_control:
@@ -43,19 +43,21 @@
}} }}
grub_root_flags: >- grub_root_flags: >-
{{ ['rootflags=subvol=@'] if system_cfg.filesystem == 'btrfs' else [] }} {{ ['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: >- grub_cmdline_linux_base: >-
{{ {{
(['crashkernel=auto'] + grub_lvm_args) ((grub_lvm_args | join(' ')) ~ ' ' ~ (_hardware_profile_kernel_params | default([]) | join(' '))) | trim
| join(' ')
}} }}
grub_kernel_cmdline_base: >- grub_kernel_cmdline_base: >-
{{ {{
( (
(['root=UUID=' + grub_root_uuid] (['root=UUID=' + grub_root_uuid]
if grub_root_uuid | length > 0 else []) if grub_root_uuid | length > 0 else [])
+ ['ro', 'crashkernel=auto'] + ['ro']
+ grub_lvm_args + grub_lvm_args
+ grub_root_flags + grub_root_flags
+ (_hardware_profile_kernel_params | default([]))
) )
| join(' ') | join(' ')
}} }}

View File

@@ -1,24 +1,21 @@
--- ---
# Connection and timing
environment_wait_timeout: 180 environment_wait_timeout: 180
environment_wait_delay: 5 environment_wait_delay: 5
# Pacman installer settings
environment_parallel_downloads: 20 environment_parallel_downloads: 20
environment_pacman_lock_timeout: 120 environment_pacman_lock_timeout: 120
environment_pacman_retries: 4 environment_pacman_retries: 4
environment_pacman_retry_delay: 15 environment_pacman_retry_delay: 15
# Libraries the installer tools pull whose soname may have bumped past the ISO. # Installer-tool libraries whose soname may have bumped past the ISO. Each one's
# Each library's installed reverse-dependencies are upgraded together with the # installed reverse-deps are co-upgraded so the install stays a consistent
# tools so a current install onto an older ISO stays a consistent transaction # transaction. Extend if a future transition breaks the install.
# instead of a partial upgrade. Extend if a future transition breaks the install.
environment_partial_upgrade_libs: environment_partial_upgrade_libs:
- nettle - nettle
- leancrypto - leancrypto
# PCI vendor IDs -> vendor codes used by hardware detection. # PCI vendor ID -> vendor code. Only vendors that drive distinct
# Only vendors that drive distinct firmware/driver packages are mapped. # firmware/driver packages are mapped.
environment_pci_vendor_map: environment_pci_vendor_map:
"8086": intel "8086": intel
"1002": amd "1002": amd
@@ -37,8 +34,8 @@ environment_pci_vendor_map:
"1cf3": cirrus "1cf3": cirrus
"13d7": cirrus "13d7": cirrus
# USB vendor IDs of fingerprint readers supported by libfprint / fprintd. # USB vendor IDs of fingerprint readers supported by libfprint / fprintd,
# Lowercase, four-digit hex; matched against `lsusb` output. # matched against `lsusb` output.
environment_fingerprint_vendor_ids: environment_fingerprint_vendor_ids:
- "06cb" # Synaptics (modern ThinkPad/Dell) - "06cb" # Synaptics (modern ThinkPad/Dell)
- "138a" # Validity Sensors (older ThinkPad) - "138a" # Validity Sensors (older ThinkPad)
@@ -49,3 +46,15 @@ environment_fingerprint_vendor_ids:
- "08ff" # AuthenTec (legacy) - "08ff" # AuthenTec (legacy)
- "147e" # Upek (legacy) - "147e" # Upek (legacy)
- "1491" # Futronic - "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. # A user-supplied override profile skips detection (golden-image flow: bake an
# # image with a fixed profile).
# 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.
- name: Resolve hardware detection requirement - name: Resolve hardware detection requirement
ansible.builtin.set_fact: ansible.builtin.set_fact:
_hardware_detection_needed: >- _hardware_detection_needed: >-
@@ -37,7 +21,12 @@
gpus: "{{ _hardware_profile_override.gpus | default([]) | map('lower') | list }}" gpus: "{{ _hardware_profile_override.gpus | default([]) | map('lower') | list }}"
nvidia_supports_open: "{{ _hardware_profile_override.nvidia_supports_open | default(true) | bool }}" nvidia_supports_open: "{{ _hardware_profile_override.nvidia_supports_open | default(true) | bool }}"
wireless: "{{ _hardware_profile_override.wireless | default([]) | map('lower') | list }}" 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 }}" 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 - name: Detect hardware from live host
when: when:
@@ -61,71 +50,7 @@
failed_when: false failed_when: false
- name: Resolve detected hardware profile - name: Resolve detected hardware profile
vars: ansible.builtin.include_tasks: _resolve_hardware_profile.yml
_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 }}"
- name: Initialize empty hardware profile when detection skipped - name: Initialize empty hardware profile when detection skipped
when: not (_hardware_detection_needed | bool) when: not (_hardware_detection_needed | bool)
@@ -135,7 +60,14 @@
gpus: [] gpus: []
nvidia_supports_open: true nvidia_supports_open: true
wireless: [] wireless: []
audio: []
fingerprint: false 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 - name: Report active hardware profile
when: _hardware_detection_needed | bool when: _hardware_detection_needed | bool
@@ -146,4 +78,7 @@
gpus={{ hardware_profile_active.gpus | default([]) | join(',') | default('-', true) }} gpus={{ hardware_profile_active.gpus | default([]) | join(',') | default('-', true) }}
{{ '(open-supported)' if hardware_profile_active.nvidia_supports_open | bool else '(legacy)' }}, {{ '(open-supported)' if hardware_profile_active.nvidia_supports_open | bool else '(legacy)' }},
wireless={{ hardware_profile_active.wireless | default([]) | join(',') | default('-', true) }}, 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 nvidia_driver: "auto" # auto | open | proprietary | nouveau
peripherals: peripherals:
enabled: "auto" # auto = follows desktop.enabled enabled: "auto" # auto = follows desktop.enabled
fingerprint: "auto" fingerprint: "auto" # auto|true|false (auto = install when detected)
webcam: "auto" 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 displaylink: false
hardware: 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. # Per-hypervisor required fields - drives data-driven validation.
# All virtual types additionally require network bridge or interfaces. # All virtual types additionally require network bridge or interfaces.

View File

@@ -206,23 +206,45 @@
if (system_raw.features.peripherals.enabled | string | lower) == 'auto' if (system_raw.features.peripherals.enabled | string | lower) == 'auto'
else (system_raw.features.peripherals.enabled | bool) else (system_raw.features.peripherals.enabled | bool)
}} }}
# fingerprint/webcam stay tri-state ('auto'|'true'|'false') because the # fingerprint/camera/audio/bluetooth stay tri-state ('auto'|'true'|'false')
# 'auto' branch is resolved at install time using detection results. # because the 'auto' branch is resolved at install time using detection results.
fingerprint: >- fingerprint: >-
{{ {{
'auto' 'auto'
if (system_raw.features.peripherals.fingerprint | string | lower) == 'auto' if (system_raw.features.peripherals.fingerprint | string | lower) == 'auto'
else (system_raw.features.peripherals.fingerprint | bool | string | lower) else (system_raw.features.peripherals.fingerprint | bool | string | lower)
}} }}
webcam: >- camera: >-
{{ {{
'auto' 'auto'
if (system_raw.features.peripherals.webcam | string | lower) == 'auto' if (system_raw.features.peripherals.camera | string | lower) == 'auto'
else (system_raw.features.peripherals.webcam | bool | string | lower) 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 }}" displaylink: "{{ system_raw.features.peripherals.displaylink | bool }}"
hardware: hardware:
profile: "{{ system_raw.features.hardware.profile | default({}) }}" 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 }}" hostname: "{{ system_name }}"
os: "{{ system_os_input if system_os_input | length > 0 else (physical_default_os if system_type == 'physical' else '') }}" 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 }}" 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.gpu.nvidia_driver in ["auto", "open", "proprietary", "nouveau"]
- system_cfg.features.peripherals.enabled is defined - system_cfg.features.peripherals.enabled is defined
- system_cfg.features.peripherals.fingerprint in ["auto", "true", "false"] - 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.peripherals.displaylink is defined
- system_cfg.features.hardware.profile is mapping - 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: >- fail_msg: >-
Invalid hardware feature flags. firmware.enabled/microcode, Invalid hardware feature flags. firmware.enabled/microcode,
peripherals.enabled and peripherals.displaylink must be bool (or 'auto' peripherals.enabled and peripherals.displaylink must be bool (or 'auto'
sentinel for firmware); gpu.nvidia_driver in sentinel for firmware); gpu.nvidia_driver in
[auto|open|proprietary|nouveau]; peripherals.fingerprint and [auto|open|proprietary|nouveau]; peripherals.fingerprint/camera/audio/
peripherals.webcam in [auto|true|false]; hardware.profile must be a dict. bluetooth in [auto|true|false]; hardware.profile must be a dict.
quiet: true quiet: true
- name: Validate desktop environment - name: Validate desktop environment

View File

@@ -0,0 +1,24 @@
---
- name: "Seed lscpu/lspci/lsusb registers ({{ fixture.name }})"
ansible.builtin.set_fact:
_hardware_lscpu: { stdout: "{{ fixture.lscpu }}" }
_hardware_lspci: { stdout: "{{ fixture.lspci | join('\n') }}", stdout_lines: "{{ fixture.lspci }}" }
_hardware_lsusb: { stdout: "{{ fixture.lsusb }}" }
- name: "Resolve hardware profile ({{ fixture.name }})"
ansible.builtin.include_tasks: ../../roles/environment/tasks/_resolve_hardware_profile.yml
- name: "Assert resolved profile ({{ fixture.name }})"
ansible.builtin.assert:
that:
- hardware_profile_active.cpu == fixture.expect.cpu
- hardware_profile_active.gpus == fixture.expect.gpus
- hardware_profile_active.wireless == fixture.expect.wireless
- hardware_profile_active.audio == fixture.expect.audio
- (hardware_profile_active.fingerprint | bool) == (fixture.expect.fingerprint | bool)
- (hardware_profile_active.bluetooth | bool) == (fixture.expect.bluetooth | bool)
- (hardware_profile_active.camera.uvc | bool) == (fixture.expect.camera.uvc | bool)
- (hardware_profile_active.camera.ipu6 | bool) == (fixture.expect.camera.ipu6 | bool)
- (hardware_profile_active.nvidia_supports_open | bool) == (fixture.expect.nvidia_supports_open | bool)
fail_msg: "[{{ fixture.name }}] FAIL got {{ hardware_profile_active }}"
success_msg: "[{{ fixture.name }}] OK {{ hardware_profile_active }}"

View File

@@ -0,0 +1,44 @@
---
- name: "Seed detection + declarative group (merge {{ mf.name }})"
ansible.builtin.set_fact:
hardware_profile_active: "{{ mf.detected }}"
os_family: "{{ mf.os_family }}"
_nvidia_driver_resolved: "{{ mf.nvidia_driver_resolved | default('nouveau') }}"
system_cfg:
features:
firmware: { enabled: true, microcode: true }
gpu: { enabled: true }
peripherals:
enabled: true
camera: "auto"
fingerprint: "auto"
audio: "auto"
bluetooth: "auto"
displaylink: false
hardware: "{{ mf.hardware }}"
- name: "Merge group over detection (merge {{ mf.name }})"
ansible.builtin.include_tasks: ../../roles/environment/tasks/_merge_hardware_profile.yml
- name: "Assert merged profile (merge {{ mf.name }})"
ansible.builtin.assert:
that:
- (hardware_profile_active.fingerprint | bool) == (mf.expect_profile.fingerprint | bool)
- (hardware_profile_active.bluetooth | bool) == (mf.expect_profile.bluetooth | bool)
- _hardware_profile_kernel_params == (mf.hardware.kernel_params | default([]))
fail_msg: "[merge {{ mf.name }}] profile FAIL {{ hardware_profile_active }}"
- name: "Load package map (merge {{ mf.name }})"
ansible.builtin.include_vars:
file: ../../roles/bootstrap/vars/hardware.yml
- name: "Resolve packages (merge {{ mf.name }})"
ansible.builtin.include_tasks: ../../roles/bootstrap/tasks/_resolve_hardware_packages.yml
- name: "Assert resolved package list (merge {{ mf.name }})"
ansible.builtin.assert:
that:
- (mf.expect_contains | default([])) | difference(_hardware_packages) | length == 0
- (mf.expect_excludes | default([])) | intersect(_hardware_packages) | length == 0
fail_msg: "[merge {{ mf.name }}] FAIL got {{ _hardware_packages }}"
success_msg: "[merge {{ mf.name }}] OK {{ _hardware_packages }}"

View File

@@ -0,0 +1,32 @@
---
- name: "Seed resolve inputs (pkg {{ pf.name }})"
ansible.builtin.set_fact:
os_family: "{{ pf.os_family }}"
hardware_profile_active: "{{ pf.profile }}"
_nvidia_driver_resolved: "{{ pf.nvidia_driver_resolved | default('nouveau') }}"
system_cfg:
features:
firmware: { enabled: true, microcode: true }
gpu: { enabled: true }
peripherals:
enabled: true
camera: "auto"
fingerprint: "auto"
audio: "auto"
bluetooth: "auto"
displaylink: false
- name: "Load package map (pkg {{ pf.name }})"
ansible.builtin.include_vars:
file: ../../roles/bootstrap/vars/hardware.yml
- name: "Resolve packages (pkg {{ pf.name }})"
ansible.builtin.include_tasks: ../../roles/bootstrap/tasks/_resolve_hardware_packages.yml
- name: "Assert resolved package list (pkg {{ pf.name }})"
ansible.builtin.assert:
that:
- (pf.expect_contains | default([])) | difference(_hardware_packages) | length == 0
- (pf.expect_excludes | default([])) | intersect(_hardware_packages) | length == 0
fail_msg: "[pkg {{ pf.name }}] FAIL got {{ _hardware_packages }}"
success_msg: "[pkg {{ pf.name }}] OK {{ _hardware_packages }}"

View File

@@ -0,0 +1,41 @@
---
# Canned lscpu/lspci/lsusb -> expected profile. lspci lines are `lspci -nn` shaped,
# lsusb is plain `lsusb`.
hardware_fixtures:
- name: intel-laptop-nvidia-turing
lscpu: "Architecture: x86_64\nVendor ID: GenuineIntel\nModel name: 12th Gen Core i7"
lspci:
- "00:02.0 VGA compatible controller [0300]: Intel Corporation Alder Lake-P GT2 [8086:46a6] (rev 0c)"
- "01:00.0 3D controller [0302]: NVIDIA Corporation TU117M [GeForce GTX 1650 Mobile] [10de:1f99] (rev a1)"
- "00:14.3 Network controller [0280]: Intel Corporation Wi-Fi 6 AX201 [8086:a0f0] (rev 11)"
- "00:1f.3 Audio device [0403]: Intel Corporation Alder Lake PCH-P High Definition Audio [8086:51c8] (rev 01)"
lsusb: |-
Bus 001 Device 003: ID 06cb:00bd Synaptics, Inc. Prometheus MIS Touch Fingerprint Reader
Bus 001 Device 004: ID 8087:0026 Intel Corp. AX201 Bluetooth
Bus 001 Device 005: ID 5986:118d Acer, Inc. Integrated Camera
expect: { cpu: intel, gpus: [intel, nvidia], nvidia_supports_open: true, wireless: [intel], audio: [intel], fingerprint: true, bluetooth: true, camera: { uvc: true, ipu6: false } }
- name: amd-desktop-realtek
lscpu: "Architecture: x86_64\nVendor ID: AuthenticAMD\nModel name: AMD Ryzen 7 5800X"
lspci:
- "0a:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 22 [1002:73df] (rev c7)"
- "05:00.0 Network controller [0280]: Realtek Semiconductor Co., Ltd. RTL8822CE 802.11ac [10ec:c822]"
- "0b:00.4 Audio device [0403]: Advanced Micro Devices, Inc. [AMD] Starship/Matisse HD Audio [1022:1487]"
lsusb: "Bus 002 Device 002: ID 046d:c52b Logitech, Inc. Unifying Receiver"
expect: { cpu: amd, gpus: [amd], nvidia_supports_open: false, wireless: [realtek], audio: [amd], fingerprint: false, bluetooth: false, camera: { uvc: false, ipu6: false } }
- name: nvidia-legacy-maxwell
lscpu: "Vendor ID: GenuineIntel"
lspci:
- "01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GM107 [GeForce GTX 750 Ti] [10de:1380] (rev a2)"
lsusb: ""
expect: { cpu: intel, gpus: [nvidia], nvidia_supports_open: false, wireless: [], audio: [], fingerprint: false, bluetooth: false, camera: { uvc: false, ipu6: false } }
- name: intel-laptop-ipu6-camera
lscpu: "Vendor ID: GenuineIntel"
lspci:
- "00:02.0 VGA compatible controller [0300]: Intel Corporation Raptor Lake-P [8086:a7a0] (rev 04)"
- "00:05.0 Multimedia controller [0480]: Intel Corporation Raptor Lake IPU6 [8086:a75d] (rev 04)"
- "00:14.3 Network controller [0280]: Intel Corporation Wi-Fi 6E AX211 [8086:51f0]"
lsusb: "Bus 003 Device 002: ID 8087:0033 Intel Corp. AX211 Bluetooth"
expect: { cpu: intel, gpus: [intel], nvidia_supports_open: false, wireless: [intel], audio: [], fingerprint: false, bluetooth: true, camera: { uvc: false, ipu6: true } }

View File

@@ -0,0 +1,37 @@
---
# Detected profile + declarative hardware.* -> merged profile + resolved packages.
# Exercises union/OR/force-on, disable[] force-off, and packages[os_family] append.
merge_fixtures:
- name: force-fingerprint-disable-audio-pin-ipu6
os_family: Archlinux
nvidia_driver_resolved: nouveau
detected: { cpu: intel, gpus: [intel], nvidia_supports_open: true, wireless: [intel], audio: [intel], fingerprint: false, bluetooth: false, camera: { uvc: false, ipu6: true } }
hardware:
fingerprint: true # force-on (detection missed it)
packages: { Archlinux: [intel-ipu6-dkms, v4l2-relayd] }
disable: [audio] # force-off audio even though detected
kernel_params: ["i915.enable_psr=0"]
expect_profile: { fingerprint: true, bluetooth: false }
expect_contains: [intel-ipu6-dkms, v4l2-relayd, fprintd, libfprint, v4l-utils, intel-ucode]
expect_excludes: [sof-firmware, alsa-ucm-conf, bluez]
- name: union-vendors-force-bluetooth
os_family: Debian
nvidia_driver_resolved: nouveau
detected: { cpu: amd, gpus: [amd], nvidia_supports_open: false, wireless: [], audio: [amd], fingerprint: false, bluetooth: false, camera: { uvc: false, ipu6: false } }
hardware:
wireless: [realtek] # union: add a wireless vendor detection missed
bluetooth: true # force-on bluetooth
expect_profile: { fingerprint: false, bluetooth: true }
expect_contains: [amd64-microcode, firmware-realtek, firmware-sof-signed, bluez]
expect_excludes: [fprintd, v4l-utils]
- name: disable-vendor-drops-microcode-firmware-gpu
os_family: Archlinux
nvidia_driver_resolved: open
detected: { cpu: intel, gpus: [intel, nvidia], nvidia_supports_open: true, wireless: [intel], audio: [intel], fingerprint: false, bluetooth: false, camera: { uvc: false, ipu6: false } }
hardware:
disable: [nvidia, intel] # drop the nvidia GPU and every intel-vendor contribution
expect_profile: { fingerprint: false, bluetooth: false }
expect_contains: [mesa, vulkan-icd-loader]
expect_excludes: [nvidia-open-dkms, intel-ucode, vulkan-intel, sof-firmware, linux-firmware-other]

View File

@@ -0,0 +1,22 @@
---
# profile + os_family -> resolved package list. expect_contains: all must be present;
# expect_excludes: none may be present. Features default all-on with auto peripherals.
package_fixtures:
- name: arch-intel-audio-bt-fp
os_family: Archlinux
profile: { cpu: intel, gpus: [intel], wireless: [intel], audio: [intel], fingerprint: true, bluetooth: true, camera: { uvc: true, ipu6: false } }
expect_contains: [intel-ucode, linux-firmware-other, mesa, vulkan-icd-loader, vulkan-intel, sof-firmware, alsa-ucm-conf, bluez, bluez-utils, fprintd, libfprint, v4l-utils]
expect_excludes: [nvidia-open-dkms, evdi]
- name: debian-amd-audio-no-bt-no-fp
os_family: Debian
profile: { cpu: amd, gpus: [amd], wireless: [realtek], audio: [amd], fingerprint: false, bluetooth: false, camera: { uvc: false, ipu6: false } }
expect_contains: [amd64-microcode, firmware-linux-free, firmware-amd-graphics, firmware-realtek, mesa-vulkan-drivers, firmware-sof-signed, alsa-ucm-conf]
expect_excludes: [bluez, fprintd, libpam-fprintd, v4l-utils]
- name: redhat-intel-nvidia-bt
os_family: RedHat
profile: { cpu: intel, gpus: [intel, nvidia], wireless: [intel], audio: [intel], fingerprint: false, bluetooth: true, camera: { uvc: false, ipu6: true } }
nvidia_driver_resolved: open
expect_contains: [microcode_ctl, linux-firmware, mesa-dri-drivers, vulkan-loader, akmod-nvidia-open, alsa-sof-firmware, alsa-ucm, bluez, v4l-utils]
expect_excludes: [fprintd, evdi]

View File

@@ -0,0 +1,16 @@
---
# Run: ansible-playbook tests/hardware/test_detection.yml
- name: Hardware detection fixture tests
hosts: localhost
gather_facts: false
connection: local
vars_files:
- ../../roles/environment/defaults/main.yml
- fixtures.yml
tasks:
- name: Run each detection fixture
ansible.builtin.include_tasks: _assert_fixture.yml
loop: "{{ hardware_fixtures }}"
loop_control:
loop_var: fixture
label: "{{ fixture.name }}"

View File

@@ -0,0 +1,15 @@
---
# Run: ansible-playbook tests/hardware/test_merge.yml
- name: Hardware merge-layer fixture tests
hosts: localhost
gather_facts: false
connection: local
vars_files:
- merge_fixtures.yml
tasks:
- name: Run each merge fixture
ansible.builtin.include_tasks: _assert_merge.yml
loop: "{{ merge_fixtures }}"
loop_control:
loop_var: mf
label: "{{ mf.name }}"

View File

@@ -0,0 +1,15 @@
---
# Run: ansible-playbook tests/hardware/test_packages.yml
- name: Hardware package-resolution fixture tests
hosts: localhost
gather_facts: false
connection: local
vars_files:
- pkg_fixtures.yml
tasks:
- name: Run each package fixture
ansible.builtin.include_tasks: _assert_packages.yml
loop: "{{ package_fixtures }}"
loop_control:
loop_var: pf
label: "{{ pf.name }}"