1
0
Fork 1
mirror of https://gitlab.postmarketos.org/postmarketOS/pmbootstrap.git synced 2025-07-12 19:09:56 +03:00

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:
Newbyte 2025-02-11 18:51:40 +01:00 committed by Oliver Smith
parent a9c3628297
commit cd672222c4
No known key found for this signature in database
GPG key ID: 5AE7F5513E0885CB
3 changed files with 173 additions and 16 deletions

View file

@ -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 = ""

View file

@ -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

View 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)