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).
This commit is contained in:
2026-09-22 03:33:52 +03:00
commit 33e8d18f94
4 changed files with 229 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
*.sh text eol=lf
+2
View File
@@ -0,0 +1,2 @@
creds.txt
altsp-repo/
+67
View File
@@ -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` раскладываются по архитектурам
(`<repo>/<arch>/RPMS.classic/`), для каждой архитектуры генерируются
метаданные через `genbasedir` (`<repo>/<arch>/base/`).
## Установка на офлайн-машине
Результат работы скрипта (`README.txt` внутри каталога репозитория)
содержит готовые строки для `sources.list`, например:
```
rpm file:/srv/offline-repo x86_64 classic
```
Затем:
```bash
apt-get update
apt-get install <имена пакетов>
```
Подпись репозитория не требуется — тег вроде `[cert8]` указывать не нужно.
+159
View File
@@ -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 <<EOF
Использование: $SCRIPT_NAME [-o|--output DIR] [--skip-update] PKG1 [PKG2 ...]
Опции:
-o, --output DIR Каталог репозитория (по умолчанию: $OUTDIR)
--skip-update Не выполнять apt-get update перед скачиванием
-h, --help Показать эту справку
Повторные запуски с другим набором пакетов дополняют существующий
каталог репозитория, не скачивая заново уже имеющиеся пакеты.
Пример:
sudo $SCRIPT_NAME -o /srv/offline-repo bash mc htop
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output)
OUTDIR="$2"; shift 2 ;;
--skip-update)
SKIP_UPDATE=1; shift ;;
-h|--help)
usage; exit 0 ;;
--)
shift; PACKAGES+=("$@"); break ;;
-*)
echo "Неизвестная опция: $1" >&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"