Source code for rmote.tools.pacman_repository

import re
from dataclasses import dataclass
from pathlib import Path

from rmote.protocol import Tool

_PACMAN_CONF = Path("/etc/pacman.conf")
_KEYRINGS_DIR = Path("/etc/pacman.d")


@dataclass
class Result:
    name: str
    changed: bool


def _build_section(name: str, servers: list[str], sig_level: str) -> str:
    lines = [f"[{name}]"]
    if sig_level:
        lines.append(f"SigLevel = {sig_level}")
    for server in servers:
        lines.append(f"Server = {server}")
    return "\n".join(lines) + "\n"


def _section_bounds(content: str, name: str) -> tuple[int, int] | None:
    """Return (start, end) byte offsets of the [name] section, or None."""
    m = re.search(r"^\[" + re.escape(name) + r"\][ \t]*$", content, re.MULTILINE)
    if not m:
        return None
    next_m = re.search(r"^\[", content[m.end() :], re.MULTILINE)
    end = m.end() + next_m.start() if next_m else len(content)
    return m.start(), end


[docs] class PacmanRepository(Tool): """Manage Arch Linux repository sections and GPG keys. Repository sections are read from and written to ``/etc/pacman.conf``. GPG key files are stored in ``/etc/pacman.d/``. All operations are idempotent and require root. """
[docs] @staticmethod def key(name: str, data: bytes) -> Result: """Write a GPG key to ``/etc/pacman.d/{name}``. Idempotent — the file is left untouched if it already contains identical bytes. Args: name: Filename to write the key under (e.g. ``"blackarch.gpg"``). data: Raw key bytes. Returns: :class:`Result` indicating whether the key was written. """ _KEYRINGS_DIR.mkdir(parents=True, exist_ok=True) path = _KEYRINGS_DIR / name if path.exists() and path.read_bytes() == data: return Result(name=name, changed=False) path.write_bytes(data) return Result(name=name, changed=True)
[docs] @staticmethod def present( name: str, *, servers: list[str], sig_level: str = "Optional TrustAll", ) -> Result: """Ensure a repository section is present in ``/etc/pacman.conf``. If the section already exists with the same content it is left unchanged. If it exists with different content it is updated in-place. Otherwise it is appended at the end of the file. Args: name: Repository section name (e.g. ``"blackarch"``). servers: One or more server URLs for the repository. sig_level: pacman ``SigLevel`` value. Defaults to ``"Optional TrustAll"``. Returns: :class:`Result` indicating whether ``pacman.conf`` was modified. """ section = _build_section(name, servers, sig_level) content = _PACMAN_CONF.read_text() bounds = _section_bounds(content, name) if bounds is not None: start, end = bounds if content[start:end] == section: return Result(name=name, changed=False) new_content = content[:start] + section + content[end:] else: new_content = content.rstrip("\n") + "\n\n" + section _PACMAN_CONF.write_text(new_content) return Result(name=name, changed=True)
[docs] @staticmethod def absent(name: str) -> Result: """Remove the repository section *name* from ``/etc/pacman.conf``. Args: name: Repository section name to remove. Returns: :class:`Result` indicating whether ``pacman.conf`` was modified. """ content = _PACMAN_CONF.read_text() bounds = _section_bounds(content, name) if bounds is None: return Result(name=name, changed=False) start, end = bounds new_content = re.sub(r"\n{3,}", "\n\n", content[:start] + content[end:]) _PACMAN_CONF.write_text(new_content) return Result(name=name, changed=True)