Source code for duck.cli.commands.sync.platform_utils

"""
Detects the current operating system and the package manager it favors.
"""

from __future__ import annotations

import os
import shutil
import platform

from pathlib import Path

from duck.cli.commands.sync.exceptions import UnsupportedPlatformError


# Backend names in priority order per OS family, used when several
# package managers could be present on the same machine
LINUX_BACKEND_PRIORITY = ["apt", "dnf", "pacman", "zypper", "apk"]


[docs] def is_root() -> bool: """ Checks whether the current process is already running as root. Used to skip prefixing install commands with sudo, since sudo is unavailable or unnecessary inside most containers that already run as root. Returns: True on POSIX systems running as uid 0. Always False on Windows, since sudo does not apply there. """ return hasattr(os, "geteuid") and os.geteuid() == 0
[docs] def is_termux() -> bool: """ Checks whether Duck Sync is running inside Termux on Android. Termux reports itself as a normal Linux uname, so it needs its own check rather than falling through the generic Linux package manager detection. It also has no root and no sudo binary. Returns: True when the Termux environment and pkg command are both present. """ prefix = os.environ.get("PREFIX", "") return "com.termux" in prefix and shutil.which("pkg") is not None
[docs] def read_linux_distro_id() -> str: """ Reads the distro id from /etc/os-release. Returns: The lowercase distro id (e.g. "ubuntu", "fedora"), or an empty string when the file is unavailable. """ os_release = Path("/etc/os-release") if not os_release.is_file(): return "" for line in os_release.read_text(encoding="utf-8").splitlines(): if line.startswith("ID="): return line.split("=", 1)[1].strip().strip('"').lower() return ""
[docs] def detect_os_family(raise_for_unknown: bool = True) -> str: """ Classifies the host machine into a coarse OS family. Returns: One of "linux", "macos", "windows". Raises: UnsupportedPlatformError: If the platform cannot be classified (only is raise_for_uknown=True). """ system = platform.system().lower() if system == "linux": return "linux" if system == "darwin": return "macos" if system == "windows": return "windows" if raise_for_unknown: raise UnsupportedPlatformError(f"Unrecognized platform: '{system}'") return system
[docs] def detect_backend_name() -> str: """ Picks the best matching system package manager for this machine. Returns: A backend identifier such as "apt", "brew", "dnf", "pacman", "choco", or "termux" that `backends.get_backend` understands. Raises: UnsupportedPlatformError: If no known package manager is found. """ # Termux reports as Linux but has no root and its own package tool, # so it must be checked before the generic Linux family below if is_termux(): return "termux" # Detect os family family = detect_os_family() if family == "macos": if shutil.which("brew"): return "brew" # Raise an exception raise UnsupportedPlatformError("Homebrew not found. Install it from https://brew.sh first.") if family == "windows": if shutil.which("choco"): return "choco" # Raise an exception raise UnsupportedPlatformError("Chocolatey not found. Install it from https://chocolatey.org first.") # Linux: prefer whichever package manager is actually on PATH binary_by_backend = { "apt": "apt-get", "dnf": "dnf", "pacman": "pacman", "zypper": "zypper", "apk": "apk", } for backend in LINUX_BACKEND_PRIORITY: if shutil.which(binary_by_backend[backend]): return backend raise UnsupportedPlatformError("No supported Linux package manager found on PATH.")