# Conflicts: # moonraker/components/file_manager/metadata.py resolved by ada08d4574e13099a392bba4b3ebe34485807c0a version
172 lines
6.7 KiB
Python
172 lines
6.7 KiB
Python
# Helpers for parsing system dependencies
|
|
#
|
|
# Copyright (C) 2025 Eric Callahan <arksine.code@gmail.com>
|
|
#
|
|
# This file may be distributed under the terms of the GNU GPLv3 license
|
|
from __future__ import annotations
|
|
import shlex
|
|
import re
|
|
import pathlib
|
|
import logging
|
|
|
|
from typing import Tuple, Dict, List, Any
|
|
|
|
def _get_distro_info() -> Dict[str, Any]:
|
|
try:
|
|
# try importing the distro module first. It can detect
|
|
# old/obscure releases that do not have the standard
|
|
# os-release fle.
|
|
import distro
|
|
except ModuleNotFoundError:
|
|
pass
|
|
else:
|
|
return dict(
|
|
distro_id=distro.id(),
|
|
distro_version=distro.version(),
|
|
aliases=distro.like().split()
|
|
)
|
|
# fall back to manual parsing of /etc/os-release
|
|
release_file = pathlib.Path("/etc/os-release")
|
|
release_info: Dict[str, str] = {}
|
|
with release_file.open("r") as f:
|
|
lexer = shlex.shlex(f, posix=True)
|
|
lexer.whitespace_split = True
|
|
for item in list(lexer):
|
|
if "=" in item:
|
|
key, val = item.split("=", maxsplit=1)
|
|
release_info[key] = val
|
|
return dict(
|
|
distro_id=release_info.get("ID", ""),
|
|
distro_version=release_info.get("VERSION_ID", ""),
|
|
aliases=release_info.get("ID_LIKE", "").split()
|
|
)
|
|
|
|
def _convert_version(version: str) -> Tuple[str | int, ...]:
|
|
version = version.strip()
|
|
ver_match = re.match(r"\d+(\.\d+)*((?:-|\.).+)?", version)
|
|
if ver_match is not None:
|
|
return tuple([
|
|
int(part) if part.isdigit() else part
|
|
for part in re.split(r"\.|-", version)
|
|
])
|
|
return (version,)
|
|
|
|
class SysDepsParser:
|
|
def __init__(self, distro_info: Dict[str, Any] | None = None) -> None:
|
|
if distro_info is None:
|
|
distro_info = _get_distro_info()
|
|
self.distro_id: str = distro_info.get("distro_id", "")
|
|
self.aliases: List[str] = distro_info.get("aliases", [])
|
|
self.distro_version: Tuple[int | str, ...] = tuple()
|
|
version = distro_info.get("distro_version")
|
|
if version:
|
|
self.distro_version = _convert_version(version)
|
|
|
|
def _parse_spec(self, full_spec: str) -> str | None:
|
|
parts = full_spec.split(";", maxsplit=1)
|
|
if len(parts) == 1:
|
|
return full_spec
|
|
pkg_name = parts[0].strip()
|
|
expressions = re.split(r"( and | or )", parts[1].strip())
|
|
if not len(expressions) & 1:
|
|
# There should always be an odd number of expressions. Each
|
|
# expression is separated by an "and" or "or" operator
|
|
logging.info(
|
|
f"Requirement specifier is missing an expression "
|
|
f"between logical operators : {full_spec}"
|
|
)
|
|
return None
|
|
last_result: bool = True
|
|
last_logical_op: str | None = "and"
|
|
for idx, exp in enumerate(expressions):
|
|
if idx & 1:
|
|
if last_logical_op is not None:
|
|
logging.info(
|
|
"Requirement specifier contains sequential logical "
|
|
f"operators: {full_spec}"
|
|
)
|
|
return None
|
|
logical_op = exp.strip()
|
|
if logical_op not in ("and", "or"):
|
|
logging.info(
|
|
f"Invalid logical operator {logical_op} in requirement "
|
|
f"specifier: {full_spec}")
|
|
return None
|
|
last_logical_op = logical_op
|
|
continue
|
|
elif last_logical_op is None:
|
|
logging.info(
|
|
f"Requirement specifier contains two seqential expressions "
|
|
f"without a logical operator: {full_spec}")
|
|
return None
|
|
dep_parts = re.split(r"(==|!=|<=|>=|<|>)", exp.strip())
|
|
req_var = dep_parts[0].strip().lower()
|
|
if len(dep_parts) != 3:
|
|
logging.info(f"Invalid comparison, must be 3 parts: {full_spec}")
|
|
return None
|
|
elif req_var == "distro_id":
|
|
left_op: str | Tuple[int | str, ...] = self.distro_id
|
|
right_op = dep_parts[2].strip().strip("\"'")
|
|
elif req_var == "distro_version":
|
|
if not self.distro_version:
|
|
logging.info(
|
|
"Distro Version not detected, cannot satisfy requirement: "
|
|
f"{full_spec}"
|
|
)
|
|
return None
|
|
left_op = self.distro_version
|
|
right_op = _convert_version(dep_parts[2].strip().strip("\"'"))
|
|
else:
|
|
logging.info(f"Invalid requirement specifier: {full_spec}")
|
|
return None
|
|
operator = dep_parts[1].strip()
|
|
try:
|
|
compfunc = {
|
|
"<": lambda x, y: x < y,
|
|
">": lambda x, y: x > y,
|
|
"==": lambda x, y: x == y,
|
|
"!=": lambda x, y: x != y,
|
|
">=": lambda x, y: x >= y,
|
|
"<=": lambda x, y: x <= y
|
|
}.get(operator, lambda x, y: False)
|
|
result = compfunc(left_op, right_op)
|
|
if last_logical_op == "and":
|
|
last_result &= result
|
|
else:
|
|
last_result |= result
|
|
last_logical_op = None
|
|
except Exception:
|
|
logging.exception(f"Error comparing requirements: {full_spec}")
|
|
return None
|
|
if last_result:
|
|
return pkg_name
|
|
return None
|
|
|
|
def parse_dependencies(self, sys_deps: Dict[str, List[str]]) -> List[str]:
|
|
if not self.distro_id:
|
|
logging.info(
|
|
"Failed to detect current distro ID, cannot parse dependencies"
|
|
)
|
|
return []
|
|
all_ids = [self.distro_id] + self.aliases
|
|
for distro_id in all_ids:
|
|
if distro_id in sys_deps:
|
|
if not sys_deps[distro_id]:
|
|
logging.info(
|
|
f"Dependency data contains an empty package definition "
|
|
f"for linux distro '{distro_id}'"
|
|
)
|
|
continue
|
|
processed_deps: List[str] = []
|
|
for dep in sys_deps[distro_id]:
|
|
parsed_dep = self._parse_spec(dep)
|
|
if parsed_dep is not None:
|
|
processed_deps.append(parsed_dep)
|
|
return processed_deps
|
|
else:
|
|
logging.info(
|
|
f"Dependency data has no package definition for linux "
|
|
f"distro '{self.distro_id}'"
|
|
)
|
|
return []
|