From 33e8d18f94ab5c8d01105f79743b95133e28fe02 Mon Sep 17 00:00:00 2001 From: "a.gritsenko" Date: Tue, 22 Sep 2026 03:33:52 +0300 Subject: [PATCH] Add altsp-download-repo.sh: build standalone offline apt-rpm repo for ALT SP Server Downloads one or more packages together with their full dependency closure (resolved against an empty rpm db so the result works on a target machine with an arbitrary baseline, not just what's missing on the machine running the script), then builds a local apt-rpm "classic" repository via genbasedir --bloat (required: without --bloat, genbasedir strips version data from some soname Provides, e.g. libssh2 -> mc, which breaks dependency resolution on a truly clean target even though the same packages resolve fine against the real upstream repo). Verified end-to-end against a live ALT SP Server 10.2.2 test machine, including simulating a from-scratch install using only the generated repo (no upstream access). --- .gitattributes | 1 + .gitignore | 2 + README.md | 67 +++++++++++++++++ altsp-download-repo.sh | 159 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 229 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 README.md create mode 100644 altsp-download-repo.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2db88d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +creds.txt +altsp-repo/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc5802d --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# altsp-downloader + +Скрипт для ALT SP Server: скачивает один или несколько пакетов вместе со +всеми зависимостями и собирает из них автономный локальный apt-rpm +репозиторий (схема `classic`), готовый для переноса на машину без +интернета. + +## Требования + +- ALT Linux / ALT SP Server, apt-rpm (`apt-get`, `rpm`). +- Доступ к реальным (интернет) репозиториям на машине, где запускается + скрипт. +- root / sudo. + +Пакет `apt-repo-tools` (для `genbasedir`) устанавливается скриптом +автоматически, если отсутствует. + +## Использование + +```bash +sudo ./altsp-download-repo.sh [-o|--output DIR] PKG1 [PKG2 ...] +``` + +- Пакеты передаются аргументами командной строки — можно указать сразу + несколько для batch-скачивания. +- `-o/--output DIR` — каталог репозитория (по умолчанию `./altsp-repo`). +- `--skip-update` — пропустить `apt-get update` перед скачиванием. + +Пример: + +```bash +sudo ./altsp-download-repo.sh -o /srv/offline-repo bash mc htop +``` + +Повторные запуски с другим набором пакетов дополняют существующий каталог +репозитория, не скачивая заново уже имеющиеся пакеты. + +## Как это работает + +Зависимости резолвятся против **пустой** rpm-базы (`rpm --initdb` во +временном fakeroot + `apt-get -o RPM::RootDir=...`), поэтому скачивается +**полное** дерево зависимостей запрошенных пакетов, а не только то, чего +не хватает на машине, где запущен скрипт. Это важно, так как репозиторий +предназначен для переноса на другую, возможно не идентичную по составу +пакетов машину. + +Скачанные `.rpm` раскладываются по архитектурам +(`//RPMS.classic/`), для каждой архитектуры генерируются +метаданные через `genbasedir` (`//base/`). + +## Установка на офлайн-машине + +Результат работы скрипта (`README.txt` внутри каталога репозитория) +содержит готовые строки для `sources.list`, например: + +``` +rpm file:/srv/offline-repo x86_64 classic +``` + +Затем: + +```bash +apt-get update +apt-get install <имена пакетов> +``` + +Подпись репозитория не требуется — тег вроде `[cert8]` указывать не нужно. diff --git a/altsp-download-repo.sh b/altsp-download-repo.sh new file mode 100644 index 0000000..a7ceba5 --- /dev/null +++ b/altsp-download-repo.sh @@ -0,0 +1,159 @@ +#!/bin/bash +# Скачивает пакеты ALT Linux вместе со всеми зависимостями и собирает +# из них автономный локальный apt-rpm репозиторий (схема "classic"), +# готовый для переноса на машину без интернета. +set -euo pipefail + +SCRIPT_NAME=$(basename "$0") +OUTDIR="./altsp-repo" +SKIP_UPDATE=0 +PACKAGES=() +ORIGINAL_ARGS=("$@") + +usage() { + cat <&2; usage >&2; exit 1 ;; + *) + PACKAGES+=("$1"); shift ;; + esac +done + +if [[ ${#PACKAGES[@]} -eq 0 ]]; then + echo "Ошибка: не указано ни одного пакета." >&2 + usage >&2 + exit 1 +fi + +if [[ $EUID -ne 0 ]]; then + echo "Требуются права root, перезапуск через sudo..." >&2 + exec sudo -- "$0" "${ORIGINAL_ARGS[@]}" +fi + +if ! command -v genbasedir >/dev/null 2>&1; then + echo "genbasedir не найден, устанавливаю пакет apt-repo-tools..." >&2 + apt-get install -y apt-repo-tools +fi + +if [[ $SKIP_UPDATE -eq 0 ]]; then + echo "Обновление индексов apt-get..." >&2 + apt-get update +fi + +mkdir -p "$OUTDIR" +OUTDIR=$(cd "$OUTDIR" && pwd) + +CACHE="$OUTDIR/.cache" +FAKEROOT="$CACHE/fakeroot" +ARCHIVES="$CACHE/archives" +mkdir -p "$FAKEROOT" "$ARCHIVES/partial" + +# Пустая rpm-база => apt-get считает, что на целевой системе не установлено +# вообще ничего, и скачивает ПОЛНОЕ дерево зависимостей, а не только то, +# чего не хватает на этой (текущей) машине. +if [[ ! -d "$FAKEROOT/var/lib/rpm" ]]; then + rpm --root="$FAKEROOT" --initdb +fi + +echo "Скачивание пакетов и зависимостей: ${PACKAGES[*]}" >&2 +apt-get \ + -o RPM::RootDir="$FAKEROOT" \ + -o Dir::Cache::Archives="$ARCHIVES" \ + -y -d install "${PACKAGES[@]}" + +echo "Раскладка пакетов по архитектурам..." >&2 +shopt -s nullglob +for rpmfile in "$ARCHIVES"/*.rpm; do + arch=$(rpm -qp --qf '%{ARCH}' "$rpmfile" 2>/dev/null) || continue + [[ -z "$arch" ]] && continue + destdir="$OUTDIR/$arch/RPMS.classic" + mkdir -p "$destdir" + dest="$destdir/$(basename "$rpmfile")" + [[ -e "$dest" ]] || ln -f "$rpmfile" "$dest" 2>/dev/null || cp -f "$rpmfile" "$dest" +done +shopt -u nullglob + +for archdir in "$OUTDIR"/*/RPMS.classic; do + [[ -d "$archdir" ]] || continue + arch=$(basename "$(dirname "$archdir")") + echo "Генерация метаданных репозитория для $arch..." >&2 + # --bloat: без этого флага genbasedir урезает данные о версии для + # некоторых автоматических soname-зависимостей (Provides с "set:..." + # хешем символьных версий), из-за чего apt-get на офлайн-машине не может + # разрешить некоторые реальные зависимости (например libssh2 -> mc), + # хотя с апстрим-репозиторием те же самые .rpm разрешаются нормально. + genbasedir --progress --bloat --create --topdir="$OUTDIR" "$arch" classic +done + +MANIFEST="$OUTDIR/manifest.txt" +{ + echo "# $(date -u +%Y-%m-%dT%H:%M:%SZ)" + printf '%s\n' "${PACKAGES[@]}" +} >> "$MANIFEST" + +README="$OUTDIR/README.txt" +{ + echo "Автономный apt-rpm репозиторий ALT Linux" + echo "Собран: $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo + echo "Как подключить на машине без интернета:" + echo + echo "1. Скопируйте этот каталог целиком на целевую машину, например в /srv/offline-repo" + echo " (rsync/scp/флешка — как удобно)." + echo + echo "2. Добавьте в /etc/apt/sources.list (или новый файл в /etc/apt/sources.list.d/) строки:" + echo + for archdir in "$OUTDIR"/*/RPMS.classic; do + [[ -d "$archdir" ]] || continue + arch=$(basename "$(dirname "$archdir")") + echo " rpm file:/srv/offline-repo $arch classic" + done + echo + echo " (замените /srv/offline-repo на реальный путь на целевой машине;" + echo " тег подписи вроде [cert8] указывать не нужно, репозиторий не подписан)" + echo + echo "3. Выполните на целевой машине:" + echo " apt-get update" + echo " apt-get install <имена пакетов>" +} > "$README" + +rpm_count=$(find "$OUTDIR" -path "$CACHE" -prune -o -name '*.rpm' -print | wc -l) +total_bytes=0 +for archdir in "$OUTDIR"/*/RPMS.classic; do + [[ -d "$archdir" ]] || continue + sz=$(du -sb "$archdir" | cut -f1) + total_bytes=$((total_bytes + sz)) +done +human_size=$(numfmt --to=iec "$total_bytes" 2>/dev/null || echo "${total_bytes} bytes") + +echo +echo "Готово." +echo "Репозиторий: $OUTDIR" +echo "Пакетов (rpm): $rpm_count" +echo "Суммарный размер: $human_size" +echo "Инструкция: $README"