We detected you are likely not from a Russian-speaking region. Would you like to switch to the international version of the site?

  Назад к списку статей

Разработка собственного менеджера пакетов (pkg-like) на Python для установки скриптов и бинарников в Termux

Профессиональное руководство по разработке pkg‑like менеджера пакетов на Python для Termux: структура пакетов, метаданные, репозитории, подпись, зависимости, обновления и установка скриптов/бинарников в пределах легального использования в РФ.

Termux — мощная среда для разработки и администрирования на Android, а управление установками через «ручные» скрипты часто превращается в хаос: разные версии, пропущенные зависимости, неочевидные пути установки и отсутствие воспроизводимости. Решение — разработать собственный менеджер пакетов в стиле pkg: он будет читать метаданные пакетов, скачивать архивы, проверять целостность, разворачивать файлы в нужные каталоги и вести локальную базу установленных пакетов.

Ниже — практический, безопасный и легальный подход к созданию собственного pkg‑like менеджера на Python для Termux с поддержкой установки скриптов и бинарников, обновлений, зависимостей и описания репозиториев. Материал ориентирован на разработчиков и администраторов, которым важны контроль, трассируемость и воспроизводимость.

Цели и ограничения

Цели проекта:

  • Унифицированная установка пакетов (скрипты и бинарники) в Termux.
  • Поддержка версий и обновлений.
  • Проверка целостности (хэши) и защита метаданных (подпись/проверка).
  • Зависимости (минимальная модель: реквизиты + проверка наличия).
  • Локальная база установленных пакетов и журнал операций.

Ограничения и правовые принципы:

  • Устанавливайте только легально распространяемые компоненты.
  • Не используйте менеджер для обхода блокировок или для получения доступа к чужим системам.
  • Поддерживайте прозрачность происхождения пакетов: репозиторий, хэши, подписи.

Архитектура менеджера

Рекомендуемая архитектура (минимальная, но расширяемая):

  • CLI слой: команды install, upgrade, remove, list, search, update-repo.
  • Слой репозиториев: загрузка индекса пакетов (manifest) и метаданных конкретного пакета.
  • Пакетный слой: модель Package (название, версия, зависимости, файлы, размещение).
  • Слой загрузки: скачивание архивов/файлов, проверка хэша.
  • Слой установки: безопасное разворачивание в целевые каталоги Termux.
  • Слой БД: хранение состояния в JSON/SQLite (предпочтительно SQLite).

Такой подход позволяет постепенно наращивать функциональность: контроль прав, pre/post-скрипты, зеркала репозиториев, rollback.

Как устроить формат пакета

Существуют два типичных пути:

  • Описательная модель: метаданные содержат список файлов с целевыми путями.
  • Архив-слепок: архив уже содержит дерево каталогов, а метаданные описывают корень и обработку.

Для простоты и контроля предлагаю: один архив .tar.gz (или .zip) + manifest.json, где перечислены файлы и их назначения.

Структура репозитория

Пример (каталог на сервере/в локальной сети):

  • /index.json — список пакетов и актуальных версий
  • /packages/<name>/<version>/manifest.json
  • /packages/<name>/<version>/payload.tar.gz

В Termux можно использовать локальную сеть (например, через Wi‑Fi и общий доступ) для тестирования собственного репозитория. Это безопаснее и удобнее для разработки.

Пример manifest.json

{
  "name": "mytool",
  "version": "1.2.3",
  "arch": "arm64-v8a",
  "description": "Example tool for Termux",
  "depends": [
    {"name": "python", "min_version": "3.10"}
  ],
  "files": [
    {"src": "bin/mytool", "dst": "/data/data/com.termux/files/usr/bin/mytool", "mode": "0755"},
    {"src": "share/mytool/plugin.py", "dst": "/data/data/com.termux/files/usr/share/mytool/plugin.py", "mode": "0644"}
  ],
  "payload": {
    "url": "http://192.168.1.10:8080/packages/mytool/1.2.3/payload.tar.gz",
    "sha256": "b7f3...(hex)...9c"
  },
  "signature": {
    "type": "ed25519",
    "public_key_id": "riblab-2026",
    "signature_b64": "..."
  }
}

Ключевые поля:

  • payload.url + payload.sha256 — контроль целостности.
  • files[] — куда разворачивать.
  • signature — подпись метаданных (по желанию, но рекомендуется).

Подпись метаданных: практичный вариант

Подпись нужна, чтобы не подменяли manifest. На стороне сервера вы подписываете manifest.json (канонизированная форма), на стороне клиента — проверяете подпись публичным ключом, «зашитым» в ваш менеджер или полученным из доверенного источника.

Для простоты в статье концептуально используйте Ed25519. На практике важно:

  • Фиксировать формат сериализации при подписи (канонизация JSON).
  • Подписывать только то, что нужно (например, manifest без поля signature).
  • Хранить список доверенных ключей и идентификаторы.

Локальная база установленных пакетов

Можно сделать на JSON, но для надежности лучше SQLite. Содержимое: имя пакета, версия, дата установки, контрольная сумма, путь и статус.

Пример схемы (идея):

  • installed_packages(name TEXT PRIMARY KEY, version TEXT, installed_at INTEGER, payload_sha256 TEXT)
  • history(id INTEGER PRIMARY KEY, name TEXT, action TEXT, status TEXT, details TEXT, ts INTEGER)

Установка в каталоги Termux

Termux использует фиксированные пользовательские директории в рамках приложения. Обычно:

  • usr/bin: /data/data/com.termux/files/usr/bin
  • usr/share: /data/data/com.termux/files/usr/share
  • var/lib: /data/data/com.termux/files/var/lib

Важно:

  • Проверяйте, что dst лежит в разрешённых каталогах (чтобы избежать «path traversal» в архивах).
  • Ограничивайте символические ссылки: либо запрещайте, либо тщательно проверяйте.
  • Корректно задавайте права (mode) после распаковки.

Ключевые классы и потоки

Поток установки пакета:

  1. Загрузить manifest из репозитория.
  2. Проверить подпись (если включена).
  3. Разрешить зависимости (минимально: проверить наличие или рекурсивно установить).
  4. Скачать payload.
  5. Проверить sha256.
  6. Развернуть в staging-каталог.
  7. Скопировать только перечисленные файлы в dst.
  8. Записать состояние в БД.

Начальный скелет на Python

Пример структуры проекта (упрощённо):

pkgx/
  cli.py
  repo.py
  models.py
  installer.py
  db.py
  crypto.py
  utils.py

CLI: базовые команды

# cli.py
import argparse
from repo import RepoClient
from installer import Installer
from db import DB


def main():
    p = argparse.ArgumentParser(prog="pkgx")
    p.add_argument("--repo", default="http://192.168.1.10:8080", help="Base URL repo")

    sub = p.add_subparsers(dest="cmd", required=True)

    i = sub.add_parser("install")
    i.add_argument("name")

    u = sub.add_parser("upgrade")
    u.add_argument("name")

    l = sub.add_parser("list")

    args = p.parse_args()

    db = DB("/data/data/com.termux/files/var/lib/pkgx/installed.sqlite")
    repo = RepoClient(args.repo)
    inst = Installer(db, repo)

    if args.cmd == "install":
        inst.install(args.name)
    elif args.cmd == "upgrade":
        inst.upgrade(args.name)
    elif args.cmd == "list":
        for row in db.list_installed():
            print(f"{row['name']} {row['version']}")


if name == "main":
    main()

Загрузка manifest из репозитория

# repo.py
import json
import urllib.request


class RepoClient:
    def init(self, base_url: str):
        self.base_url = base_url.rstrip("/")

    def _get_json(self, url: str):
        with urllib.request.urlopen(url, timeout=30) as r:
            return json.loads(r.read().decode("utf-8"))

    def get_index(self):
        url = f"{self.base_url}/index.json"
        return self._get_json(url)

    def get_latest_manifest(self, name: str):
        idx = self.get_index()
        if name not in idx["packages"]:
            raise RuntimeError(f"Package not found: {name}")
        latest_version = idx["packages"][name]["latest_version"]
        manifest_url = f"{self.base_url}/packages/{name}/{latest_version}/manifest.json"
        return self._get_json(manifest_url)

    def get_manifest(self, name: str, version: str):
        manifest_url = f"{self.base_url}/packages/{name}/{version}/manifest.json"
        return self._get_json(manifest_url)

Установка: скачивание, проверка sha256, безопасное разворачивание

# installer.py
import os
import json
import hashlib
import tarfile
import shutil
import urllib.request
from pathlib import Path


ALLOWED_PREFIXES = [
    "/data/data/com.termux/files/usr/",
    "/data/data/com.termux/files/var/lib/",
    "/data/data/com.termux/files/usr/share/",
    "/data/data/com.termux/files/usr/bin/",
]


def sha256_file(path: str) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(1024  1024), b""):
            h.update(chunk)
    return h.hexdigest()


def is_allowed_dst(dst: str) -> bool:
    return any(dst.startswith(p) for p in ALLOWED_PREFIXES)


class Installer:
    def init(self, db, repo):
        self.db = db
        self.repo = repo

    def install(self, name: str):
        manifest = self.repo.get_latest_manifest(name)
        version = manifest["version"]

        installed = self.db.get_installed(name)
        if installed and installed["version"] == version:
            print(f"{name} already installed: {version}")
            return

        payload_url = manifest["payload"]["url"]
        expected_sha = manifest["payload"]["sha256"]

        work_root = Path("/data/data/com.termux/files/var/tmp/pkgx")
        work_root.mkdir(parents=True, exist_ok=True)
        staging = work_root / f"{name}-{version}-staging"
        if staging.exists():
            shutil.rmtree(staging)
        staging.mkdir(parents=True)

        # 1) Скачать payload
        archive_path = work_root / f"{name}-{version}.tar.gz"
        print(f"Downloading: {payload_url}")
        urllib.request.urlretrieve(payload_url, archive_path)

        # 2) Проверить sha256
        got_sha = sha256_file(str(archive_path))
        if got_sha.lower() != expected_sha.lower():
            raise RuntimeError(f"sha256 mismatch for {name}: expected {expected_sha}, got {got_sha}")

        # 3) Развернуть в staging
        # Примечание: избегайте path traversal внутри архива.
        # Здесь предполагаем, что архив содержит файлы с относительными путями.
        with tarfile.open(archive_path, "r:gz") as tf:
            for member in tf.getmembers():
                # Запрет абсолютных путей и попыток выхода наружу
                if member.name.startswith("/") or ".." in Path(member.name).parts:
                    raise RuntimeError(f"Unsafe path in archive: {member.name}")
            tf.extractall(path=str(staging))

        # 4) Разместить только объявленные файлы
        for f in manifest["files"]:
            src_rel = f["src"]
            dst = f["dst"]
            mode = int(f.get("mode", "0644"), 8)

            if not is_allowed_dst(dst):
                raise RuntimeError(f"Destination not allowed: {dst}")

            src_path = staging / src_rel
            if not src_path.exists():
                raise RuntimeError(f"File not found in payload: {src_rel}")

            dst_path = Path(dst)
            dst_path.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(str(src_path), str(dst_path))
            os.chmod(dst, mode)

        # 5) Записать состояние
        self.db.set_installed(name, version, expected_sha)
        print(f"Installed {name} {version}")

    def upgrade(self, name: str):
        # Минимально: переиспользуем install с latest manifest.
        return self.install(name)

SQLite: минимальная БД

# db.py
import os
import sqlite3
import time


class DB:
    def init(self, db_path: str):
        os.makedirs(os.path.dirname(db_path), exist_ok=True)
        self.conn = sqlite3.connect(db_path)
        self.conn.row_factory = sqlite3.Row
        self._init_schema()

    def _init_schema(self):
        cur = self.conn.cursor()
        cur.execute(
            """
            CREATE TABLE IF NOT EXISTS installed_packages (
                name TEXT PRIMARY KEY,
                version TEXT NOT NULL,
                installed_at INTEGER NOT NULL,
                payload_sha256 TEXT
            )
            """
        )
        self.conn.commit()

    def get_installed(self, name: str):
        cur = self.conn.cursor()
        row = cur.execute(
            "SELECT  FROM installed_packages WHERE name = ?", (name,)
        ).fetchone()
        return dict(row) if row else None

    def set_installed(self, name: str, version: str, payload_sha256: str):
        cur = self.conn.cursor()
        cur.execute(
            """
            INSERT INTO installed_packages(name, version, installed_at, payload_sha256)
            VALUES(?, ?, ?, ?)
            ON CONFLICT(name) DO UPDATE SET
              version=excluded.version,
              installed_at=excluded.installed_at,
              payload_sha256=excluded.payload_sha256
            """,
            (name, version, int(time.time()), payload_sha256),
        )
        self.conn.commit()

    def list_installed(self):
        cur = self.conn.cursor()
        rows = cur.execute("SELECT name, version FROM installed_packages ORDER BY name").fetchall()
        return rows

Зависимости: минимальная стратегия

Для стартовой версии можно реализовать простую политику:

  • Если зависимость — это пакет вашего репозитория — рекурсивно вызывайте install(dependency_name).
  • Если зависимость — компонент Termux (например, наличие Python) — проверяйте через which или версию командой.

Важно не выполнять «произвольный код» из manifest. Зависимости должны быть декларативными, а шаги установки — контролируемыми.

Стабильные обновления и rollback (рекомендации)

Чтобы обновления были безопасными:

  • Скачивайте и разворачивайте в staging.
  • После успешной установки коммитьте состояние в БД.
  • Для rollback храните предыдущие версии файлов (или делайте атомарную замену через переименование).

На уровне файловой системы в Termux можно использовать подход «сначала установка версии N+1 в staging, затем замена». Если что-то пошло не так — удаляете staging и оставляете текущую версию как есть.

Практические советы по безопасности

  • Ограничение путей: всегда проверяйте, что dst находится в допустимых префиксах.
  • Проверка архивов: запрещайте абсолютные пути и .. в путях внутри архива.
  • Права: применяйте режимы из manifest и не давайте избыточные права скриптам.
  • Сетевые таймауты: задавайте лимиты на запросы.
  • Логи: сохраняйте ход установки (что скачали, какие проверки прошли).

Как начать разработку в Termux

В Termux установите Python и необходимые библиотеки (при необходимости). Обычно достаточно стандартного окружения Python в Termux. Дальше:

  1. Поднимите локальный сервер репозитория (например, простым HTTP-сервером).
  2. Сформируйте payload-архив и manifest.json.
  3. Проверьте установку на одном тестовом пакете.
  4. Добавьте подпись и валидацию manifest.

Пример запуска CLI (условно):

pkgx --repo http://192.168.1.10:8080 install mytool

Для разработки удобны локальные сети, поскольку вы контролируете содержимое репозитория, хэши и доступ.

Заключение

Разработка собственного pkg‑like менеджера пакетов на Python для Termux — это практичный способ навести порядок в установках: вы получаете единый формат пакетов, воспроизводимые обновления, проверку целостности (sha256), безопасное разворачивание и локальную базу состояния. В основе успеха — строгие правила доверия (manifest и архив), контроль путей и дисциплина установки в staging, а при желании — криптографическая подпись метаданных.

Если вам нужна помощь с проектированием репозитория, форматом manifest, подписью/верификацией, интеграцией в Termux и отладкой установщика — команда РыбинскЛАБ поможет подготовить решение под ваши требования и уровень зрелости (от прототипа до продакшн-архитектуры).

* Текст статьи подготовлен и структурирован с использованием технологий искусственного интеллекта. Проверен и доработан перед публикацией.

Нужна помощь с настройкой Termux, Linux и серверов?

Я оказываю ИТ-услуги: настройка серверов, автоматизация, безопасность, помощь с Linux и инфраструктурой. Материалы сайта — только в ознакомительных и образовательных целях.

Связаться со мной
Поддержать проект