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) после распаковки.
Ключевые классы и потоки
Поток установки пакета:
- Загрузить manifest из репозитория.
- Проверить подпись (если включена).
- Разрешить зависимости (минимально: проверить наличие или рекурсивно установить).
- Скачать payload.
- Проверить
sha256. - Развернуть в staging-каталог.
- Скопировать только перечисленные файлы в
dst. - Записать состояние в БД.
Начальный скелет на 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. Дальше:
- Поднимите локальный сервер репозитория (например, простым HTTP-сервером).
- Сформируйте payload-архив и manifest.json.
- Проверьте установку на одном тестовом пакете.
- Добавьте подпись и валидацию manifest.
Пример запуска CLI (условно):
pkgx --repo http://192.168.1.10:8080 install mytool
Для разработки удобны локальные сети, поскольку вы контролируете содержимое репозитория, хэши и доступ.
Заключение
Разработка собственного pkg‑like менеджера пакетов на Python для Termux — это практичный способ навести порядок в установках: вы получаете единый формат пакетов, воспроизводимые обновления, проверку целостности (sha256), безопасное разворачивание и локальную базу состояния. В основе успеха — строгие правила доверия (manifest и архив), контроль путей и дисциплина установки в staging, а при желании — криптографическая подпись метаданных.
Если вам нужна помощь с проектированием репозитория, форматом manifest, подписью/верификацией, интеграцией в Termux и отладкой установщика — команда РыбинскЛАБ поможет подготовить решение под ваши требования и уровень зрелости (от прототипа до продакшн-архитектуры).