forked from Mirror/pmbootstrap
pmb: Split devices by category during codename selection
This also reworks list_codenames() somewhat. The option to show archived devices is removed as it never actually was used. It should be easy to restore if someone is interested. Closes https://gitlab.postmarketos.org/postmarketOS/pmbootstrap/-/issues/2558 Part-of: https://gitlab.postmarketos.org/postmarketOS/pmbootstrap/-/merge_requests/2549
This commit is contained in:
parent
a9c3628297
commit
cd672222c4
3 changed files with 173 additions and 16 deletions
|
@ -497,11 +497,24 @@ def ask_for_device(context: Context) -> tuple[str, bool, str]:
|
|||
if not pmb.helpers.cli.confirm(default=True):
|
||||
continue
|
||||
else:
|
||||
# Archived devices can be selected, but are not displayed
|
||||
devices = sorted(pmb.helpers.devices.list_codenames(vendor, archived=False))
|
||||
# Remove "vendor-" prefixes from device list
|
||||
codenames = [x.split("-", 1)[1] for x in devices]
|
||||
logging.info(f"Available codenames ({len(codenames)}): " + ", ".join(codenames))
|
||||
device_list = "Devices are categorised as follows, from best to worst:\n"
|
||||
styles = pmb.config.styles
|
||||
for category in pmb.helpers.devices.DeviceCategory.shown():
|
||||
device_list += f"* {category.color()}{str(category).capitalize()}{styles['END']}: {category.explain()}.\n"
|
||||
device_entries = pmb.helpers.devices.list_codenames(vendor)
|
||||
# Sort devices alphabetically.
|
||||
device_entries = sorted(
|
||||
device_entries, key=pmb.helpers.devices.DeviceEntry.codename_without_vendor
|
||||
)
|
||||
device_count = len(device_entries)
|
||||
device_list += f"\nAvailable devices by codename ({device_count}): "
|
||||
device_strings = []
|
||||
for device_entry in device_entries:
|
||||
codenames.append(device_entry.codename_without_vendor())
|
||||
device_strings.append(str(device_entry))
|
||||
|
||||
device_list += ", ".join(device_strings)
|
||||
logging.info(device_list)
|
||||
|
||||
if current_vendor != vendor:
|
||||
current_codename = ""
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
# Copyright 2023 Oliver Smith
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
import pmb.config
|
||||
from pmb.core.pkgrepo import pkgrepo_glob_one, pkgrepo_iglob
|
||||
from pmb.helpers import logging
|
||||
|
||||
|
||||
def find_path(codename: str, file: str = "") -> Path | None:
|
||||
|
@ -19,20 +25,109 @@ def find_path(codename: str, file: str = "") -> Path | None:
|
|||
return g
|
||||
|
||||
|
||||
def list_codenames(vendor: str | None = None, archived: bool = True) -> list[str]:
|
||||
"""Get all devices, for which aports are available.
|
||||
# TODO: This could be simplified using StrEnum once we stop supporting Python 3.10.
|
||||
class DeviceCategory(Enum):
|
||||
"""Enum for representing a specific device category."""
|
||||
|
||||
:param vendor: vendor name to choose devices from, or None for all vendors
|
||||
:param archived: include archived devices
|
||||
:returns: ["first-device", "second-device", ...]
|
||||
ARCHIVED = "archived"
|
||||
DOWNSTREAM = "downstream"
|
||||
TESTING = "testing"
|
||||
COMMUNITY = "community"
|
||||
MAIN = "main"
|
||||
|
||||
@staticmethod
|
||||
def shown() -> list[DeviceCategory]:
|
||||
"""Get a list of all device categories that typically are visible, in order of "best" to
|
||||
"worst".
|
||||
|
||||
:returns: List of all non-hidden device categories.
|
||||
"""
|
||||
|
||||
return [
|
||||
DeviceCategory.MAIN,
|
||||
DeviceCategory.COMMUNITY,
|
||||
DeviceCategory.TESTING,
|
||||
DeviceCategory.DOWNSTREAM,
|
||||
]
|
||||
|
||||
def explain(self) -> str:
|
||||
"""Provide an explanation of a given category.
|
||||
|
||||
:returns: String explaining the given category.
|
||||
"""
|
||||
|
||||
match self:
|
||||
case DeviceCategory.ARCHIVED:
|
||||
return "ports that have a better alternative available"
|
||||
case DeviceCategory.DOWNSTREAM:
|
||||
return "ports that use a downstream kernel — very limited functionality. Not recommended"
|
||||
case DeviceCategory.TESTING:
|
||||
return 'anything from "just boots in some sense" to almost fully functioning ports'
|
||||
|
||||
case DeviceCategory.COMMUNITY:
|
||||
return "often mostly usable, but may lack important functionality"
|
||||
case DeviceCategory.MAIN:
|
||||
return "ports where mostly everything works"
|
||||
case _:
|
||||
raise AssertionError
|
||||
|
||||
def color(self) -> str:
|
||||
"""Returns the color associated with the given device category.
|
||||
|
||||
:returns: ANSI escape sequence for the color associated with the given device category."""
|
||||
styles = pmb.config.styles
|
||||
|
||||
match self:
|
||||
case DeviceCategory.ARCHIVED:
|
||||
return styles["RED"]
|
||||
case DeviceCategory.DOWNSTREAM:
|
||||
return styles["YELLOW"]
|
||||
case DeviceCategory.TESTING:
|
||||
return styles["GREEN"]
|
||||
case DeviceCategory.COMMUNITY:
|
||||
return styles["BLUE"]
|
||||
case DeviceCategory.MAIN:
|
||||
return styles["MAGENTA"]
|
||||
case _:
|
||||
raise AssertionError
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DeviceEntry:
|
||||
codename: str
|
||||
category: DeviceCategory
|
||||
|
||||
def codename_without_vendor(self) -> str:
|
||||
return self.codename.split("-", 1)[1]
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Remove "vendor-" prefix from device codename and add category."""
|
||||
styles = pmb.config.styles
|
||||
return f"{self.category.color()}{self.codename_without_vendor()}{styles['END']} ({self.category})"
|
||||
|
||||
|
||||
def list_codenames(vendor: str) -> list[DeviceEntry]:
|
||||
"""Get all devices for which aports are available.
|
||||
|
||||
:param vendor: Vendor name to choose devices from.
|
||||
:returns: ["first-device", "second-device", ...]}
|
||||
"""
|
||||
ret = []
|
||||
for path in pkgrepo_iglob("device/*/device-*"):
|
||||
if not archived and "archived" in path.parts:
|
||||
ret: list[DeviceEntry] = []
|
||||
for path in pkgrepo_iglob(f"device/*/device-{vendor}-*"):
|
||||
codename = os.path.basename(path).split("-", 1)[1]
|
||||
# Ensure we don't crash on unknown device categories.
|
||||
try:
|
||||
category = get_device_category_by_apkbuild_path(path / "APKBUILD")
|
||||
except RuntimeError as exception:
|
||||
logging.warning("WARNING: %s: %s", codename, exception)
|
||||
continue
|
||||
device = os.path.basename(path).split("-", 1)[1]
|
||||
if (vendor is None) or device.startswith(vendor + "-"):
|
||||
ret.append(device)
|
||||
# Get rid of ports inside of hidden device categories.
|
||||
if category not in DeviceCategory.shown():
|
||||
continue
|
||||
ret.append(DeviceEntry(codename, category))
|
||||
return ret
|
||||
|
||||
|
||||
|
@ -46,3 +141,26 @@ def list_vendors() -> set[str]:
|
|||
vendor = path.name.split("-", 2)[1]
|
||||
ret.add(vendor)
|
||||
return ret
|
||||
|
||||
|
||||
def get_device_category_by_apkbuild_path(apkbuild_path: Path) -> DeviceCategory:
|
||||
"""Get the category of a device based on the path to its APKBUILD inside of pmaports.
|
||||
|
||||
This will fail to determine the device category from out-of-tree APKBUILDs.
|
||||
|
||||
:apkbuild_path: Path to an APKBUILD within pmaports for a particular device.
|
||||
:returns: The device category of the provided device APKBUILD.
|
||||
"""
|
||||
|
||||
# Path is something like this:
|
||||
# .../device/community/device-samsung-m0/APKBUILD
|
||||
# ↑ ↑ parent 1
|
||||
# | parent 2
|
||||
category_str = apkbuild_path.parent.parent.name
|
||||
|
||||
try:
|
||||
device_category = DeviceCategory(category_str)
|
||||
except ValueError as exception:
|
||||
raise RuntimeError(f'Unknown device category "{category_str}"') from exception
|
||||
|
||||
return device_category
|
||||
|
|
26
pmb/helpers/test_devices.py
Normal file
26
pmb/helpers/test_devices.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Copyright 2025 Stefan Hansson
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .devices import DeviceCategory, get_device_category_by_apkbuild_path
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_get_device_category_by_apkbuild_path() -> None:
|
||||
valid_path_1 = Path("device") / "community" / "device-samsung-m0" / "APKBUILD"
|
||||
valid_path_2 = Path("pmos_work") / "device" / "main" / "device-pine64-pinephone" / "APKBUILD"
|
||||
|
||||
# Missing category segment of path.
|
||||
invalid_path_1 = Path("APKBUILD")
|
||||
# Nonexistent category ("pendeltåg").
|
||||
invalid_path_2 = Path("device") / "pendeltåg" / "device-samsung-m0" / "APKBUILD"
|
||||
|
||||
assert get_device_category_by_apkbuild_path(valid_path_1) == DeviceCategory.COMMUNITY
|
||||
assert get_device_category_by_apkbuild_path(valid_path_2) == DeviceCategory.MAIN
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
get_device_category_by_apkbuild_path(invalid_path_1)
|
||||
with pytest.raises(RuntimeError):
|
||||
get_device_category_by_apkbuild_path(invalid_path_2)
|
Loading…
Add table
Add a link
Reference in a new issue