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):
|
if not pmb.helpers.cli.confirm(default=True):
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
# Archived devices can be selected, but are not displayed
|
device_list = "Devices are categorised as follows, from best to worst:\n"
|
||||||
devices = sorted(pmb.helpers.devices.list_codenames(vendor, archived=False))
|
styles = pmb.config.styles
|
||||||
# Remove "vendor-" prefixes from device list
|
for category in pmb.helpers.devices.DeviceCategory.shown():
|
||||||
codenames = [x.split("-", 1)[1] for x in devices]
|
device_list += f"* {category.color()}{str(category).capitalize()}{styles['END']}: {category.explain()}.\n"
|
||||||
logging.info(f"Available codenames ({len(codenames)}): " + ", ".join(codenames))
|
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:
|
if current_vendor != vendor:
|
||||||
current_codename = ""
|
current_codename = ""
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
# Copyright 2023 Oliver Smith
|
# Copyright 2023 Oliver Smith
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import pmb.config
|
||||||
from pmb.core.pkgrepo import pkgrepo_glob_one, pkgrepo_iglob
|
from pmb.core.pkgrepo import pkgrepo_glob_one, pkgrepo_iglob
|
||||||
|
from pmb.helpers import logging
|
||||||
|
|
||||||
|
|
||||||
def find_path(codename: str, file: str = "") -> Path | None:
|
def find_path(codename: str, file: str = "") -> Path | None:
|
||||||
|
@ -19,20 +25,109 @@ def find_path(codename: str, file: str = "") -> Path | None:
|
||||||
return g
|
return g
|
||||||
|
|
||||||
|
|
||||||
def list_codenames(vendor: str | None = None, archived: bool = True) -> list[str]:
|
# TODO: This could be simplified using StrEnum once we stop supporting Python 3.10.
|
||||||
"""Get all devices, for which aports are available.
|
class DeviceCategory(Enum):
|
||||||
|
"""Enum for representing a specific device category."""
|
||||||
|
|
||||||
:param vendor: vendor name to choose devices from, or None for all vendors
|
ARCHIVED = "archived"
|
||||||
:param archived: include archived devices
|
DOWNSTREAM = "downstream"
|
||||||
:returns: ["first-device", "second-device", ...]
|
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 = []
|
ret: list[DeviceEntry] = []
|
||||||
for path in pkgrepo_iglob("device/*/device-*"):
|
for path in pkgrepo_iglob(f"device/*/device-{vendor}-*"):
|
||||||
if not archived and "archived" in path.parts:
|
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
|
continue
|
||||||
device = os.path.basename(path).split("-", 1)[1]
|
# Get rid of ports inside of hidden device categories.
|
||||||
if (vendor is None) or device.startswith(vendor + "-"):
|
if category not in DeviceCategory.shown():
|
||||||
ret.append(device)
|
continue
|
||||||
|
ret.append(DeviceEntry(codename, category))
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,3 +141,26 @@ def list_vendors() -> set[str]:
|
||||||
vendor = path.name.split("-", 2)[1]
|
vendor = path.name.split("-", 2)[1]
|
||||||
ret.add(vendor)
|
ret.add(vendor)
|
||||||
return ret
|
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