1
0
Fork 1
mirror of https://gitlab.postmarketos.org/postmarketOS/pmbootstrap.git synced 2025-07-12 19:09:56 +03:00
pmbootstrap/pmb/helpers/devices.py
2025-07-06 19:15:21 +02:00

190 lines
6.3 KiB
Python

# 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
import pmb.helpers.pmaports
def find_path(codename: str, file: str = "") -> Path | None:
"""Find path to device APKBUILD under `device/*/device-`.
:param codename: device codename
:param file: file to look for (e.g. APKBUILD or deviceinfo), may be empty
:returns: path to APKBUILD
"""
g = pkgrepo_glob_one(f"device/*/device-{codename}/{file}")
if not g:
return None
return g
# TODO: This could be simplified using StrEnum once we stop supporting Python 3.10.
class DeviceCategory(Enum):
"""Enum for representing a specific device category."""
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: 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
# Get rid of ports inside of hidden device categories.
if category not in DeviceCategory.shown():
continue
ret.append(DeviceEntry(codename, category))
return ret
def list_vendors() -> set[str]:
"""Get all device vendors, for which aports are available.
:returns: {"vendor1", "vendor2", ...}
"""
ret = set()
for path in pkgrepo_iglob("device/*/device-*"):
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
def get_device_category_by_directory_path(device_directory: Path) -> DeviceCategory:
"""Get the category of a device based on the path to its directory inside of pmaports.
:device_directory: Path to the device package directory for a particular device.
:returns: The device category of the provided device directory.
"""
device_apkbuild_path = device_directory / "APKBUILD"
return get_device_category_by_apkbuild_path(device_apkbuild_path)
def get_device_category_by_name(device_name: str) -> DeviceCategory:
"""Get the category of a device based on its name.
:device_name: Name of a particular device to determine the category of.
Format should be "vendor-codename".
:returns: The device category of the provided device name.
"""
device_directory = pmb.helpers.pmaports.find(f"device-{device_name}")
return get_device_category_by_directory_path(device_directory)