feat: create autonomous Nivora package repository

Maintain a validated cross-distribution package catalog.
Automate upstream updates, isolated builds, diagnostics, and direct publication.
Build the official GitHub Desktop sources for Linux with working OAuth.
This commit is contained in:
Александр
2026-07-25 00:49:32 +10:00
commit f405ae2bcf
168 changed files with 8483 additions and 0 deletions
+448
View File
@@ -0,0 +1,448 @@
#!/bin/bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "${script_dir}/.." && pwd)"
# shellcheck source=tools/lib/source_cache.sh
source "${script_dir}/lib/source_cache.sh"
cd "$repo_root"
readonly stplr_version='0.1.1'
readonly stplr_archive_url="https://altlinux.space/stapler/stplr/releases/download/v${stplr_version}/stplr-${stplr_version}-linux-x86_64.tar.gz"
readonly stplr_archive_sha256='b1ec1e98c04ab928377d0cef1706e3dc62171b9e256f4bf36a219addc53117b8'
readonly deb_image="nivora-lifecycle-deb:$$"
readonly rpm_image="nivora-lifecycle-rpm:$$"
readonly deb_cache_volume="nivora-lifecycle-deb-cache-$$"
readonly deb_build_mode="${NIVORA_DEB_BUILD_MODE:-container}"
case "$deb_build_mode" in
container | host) ;;
*)
echo "Неизвестный режим DEB-сборки: ${deb_build_mode}" >&2
exit 2
;;
esac
# package | command | persistent state marker | desktop/unit | icon
readonly -a lifecycle_packages=(
'clash-verge-rev|/usr/bin/clash-verge|/home/nivora-test/.local/share/io.github.clash-verge-rev.clash-verge-rev/nivora-lifecycle-state|/usr/share/applications/Clash Verge.desktop|/usr/share/icons/hicolor/128x128/apps/clash-verge.png'
'claude-desktop|/usr/bin/claude-desktop|/home/nivora-test/.config/Claude/nivora-lifecycle-state|/usr/share/applications/com.anthropic.Claude.desktop|/usr/share/icons/hicolor/128x128/apps/claude-desktop.png'
'codex|/usr/bin/codex-app|/home/nivora-test/.codex/nivora-lifecycle-state|/usr/share/applications/codex-app.desktop|/usr/share/icons/hicolor/512x512/apps/codex-app.png'
'nivora-stplr|/usr/bin/sl|/home/nivora-test/.config/nivora-stplr/nivora-lifecycle-state|-|-'
'opencode|/usr/bin/opencode-desktop|/home/nivora-test/.config/opencode/nivora-lifecycle-state|/usr/share/applications/opencode-desktop.desktop|/usr/share/icons/hicolor/128x128/apps/ai.opencode.desktop.png'
'tailscale|/usr/bin/tailscale|/var/lib/tailscale/nivora-lifecycle-state|/usr/lib/systemd/system/tailscaled.service|-'
'netbird|/usr/bin/netbird|/var/lib/netbird/nivora-lifecycle-state|/usr/lib/systemd/system/netbird.service|-'
'chatbox|/usr/bin/chatbox|/home/nivora-test/.config/Chatbox/nivora-lifecycle-state|/usr/share/applications/xyz.chatboxapp.app.desktop|/usr/share/icons/hicolor/128x128/apps/xyz.chatboxapp.app.png'
)
for command in curl docker dpkg-deb find rpm rpmbuild sha256sum sqlite3 stplr-spec tar; do
command -v "$command" >/dev/null 2>&1 || {
echo "Для lifecycle-теста требуется команда ${command}" >&2
exit 2
}
done
[[ "$(uname -m)" == 'x86_64' ]] || {
echo 'Lifecycle-тест сейчас поддерживает только x86_64 runner' >&2
exit 2
}
work_dir="$(mktemp -d)"
source_cache="${NIVORA_SOURCE_CACHE:-${XDG_CACHE_HOME:-${HOME}/.cache}/stplr/dl}"
apparmor_userns_original=''
cleanup() {
docker volume rm "$deb_cache_volume" >/dev/null 2>&1 || true
docker image rm "$deb_image" >/dev/null 2>&1 || true
docker image rm "$rpm_image" >/dev/null 2>&1 || true
if [[ "$deb_build_mode" == 'host' && -d "$work_dir" ]]; then
sudo chown -R "$(id -u):$(id -g)" "$work_dir" >/dev/null 2>&1 || true
fi
if [[ -n "$apparmor_userns_original" ]]; then
sudo sysctl -q -w \
"kernel.apparmor_restrict_unprivileged_userns=${apparmor_userns_original}" \
>/dev/null 2>&1 || true
fi
find "$work_dir" -mindepth 1 -delete
rmdir "$work_dir"
}
trap cleanup EXIT
install -d \
"${work_dir}/builder" \
"${work_dir}/fixtures/previous-deb" \
"${work_dir}/fixtures/previous-rpm"
prepare_stplr() {
local target="${work_dir}/builder/stplr"
local archive actual
if command -v stplr >/dev/null 2>&1 &&
[[ "$(stplr version 2>/dev/null)" == "v${stplr_version}" ]]; then
cp "$(command -v stplr)" "$target"
else
archive="${work_dir}/stplr.tar.gz"
curl -fL \
--retry 3 \
--retry-all-errors \
--connect-timeout 20 \
--max-time 300 \
-o "$archive" \
"$stplr_archive_url"
actual="$(sha256sum "$archive")"
actual="${actual%% *}"
[[ "$actual" == "$stplr_archive_sha256" ]] || {
echo "Неверная SHA-256 stplr: ${actual}" >&2
exit 1
}
tar -xzf "$archive" -C "${work_dir}/builder" stplr
fi
chmod 0755 "$work_dir" "${work_dir}/builder" "$target"
}
build_images() {
cat >"${work_dir}/builder/Containerfile.deb" <<'EOF'
FROM ubuntu:24.04
ARG DEBIAN_FRONTEND=noninteractive
COPY stplr /usr/local/bin/stplr
RUN set -eux; \
success=0; \
for attempt in 1 2 3; do \
if apt-get -o Acquire::Retries=2 -o Acquire::http::Timeout=20 update \
&& apt-get install -y ca-certificates binutils passwd python3 xz-utils zstd; then \
success=1; \
break; \
fi; \
sleep "$((attempt * 5))"; \
done; \
test "$success" -eq 1; \
useradd --system --create-home stapler-builder; \
mkdir -p /var/cache/stplr; \
chown -R stapler-builder:stapler-builder /var/cache/stplr
EOF
cat >"${work_dir}/builder/Containerfile.rpm" <<'EOF'
FROM registry.altlinux.org/sisyphus/base:latest
RUN for attempt in 1 2 3; do \
apt-get -o Acquire::Retries=2 -o Acquire::http::Timeout=20 update \
&& apt-get dist-upgrade -y \
&& apt-get install -y ca-certificates stplr binutils python3 \
&& exit 0; \
sleep "$((attempt * 5))"; \
done; \
exit 1
EOF
docker build -t "$deb_image" -f "${work_dir}/builder/Containerfile.deb" \
"${work_dir}/builder"
docker build -t "$rpm_image" -f "${work_dir}/builder/Containerfile.rpm" \
"${work_dir}/builder"
}
prepare_host_deb_builder() {
if ! getent group wheel >/dev/null; then
sudo groupadd --system wheel
fi
if ! getent passwd stapler-builder >/dev/null; then
sudo useradd --system --create-home stapler-builder
fi
sudo usermod -a -G wheel stapler-builder
sudo install -d -o stapler-builder -g stapler-builder /var/cache/stplr
sudo -u stapler-builder test -x "${work_dir}/builder/stplr"
if [[ -r /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]]; then
apparmor_userns_original="$(
sysctl -n kernel.apparmor_restrict_unprivileged_userns
)"
if [[ "$apparmor_userns_original" != '0' ]]; then
sudo sysctl -q -w kernel.apparmor_restrict_unprivileged_userns=0
else
apparmor_userns_original=''
fi
fi
sudo -u stapler-builder unshare \
--user --map-root-user --mount --pid --fork --uts --ipc --cgroup \
true
}
build_deb_on_host() {
local package="$1"
local stage="${work_dir}/host-packages/${package}"
local builder_home
local -a artifacts
install -d "$stage"
cp -a "${package}/." "$stage/"
find "$stage" -maxdepth 1 -type f \( -name '*.deb' -o -name '*.rpm' \) -delete
sudo chown -R stapler-builder:stapler-builder "$stage"
builder_home="$(getent passwd stapler-builder | cut -d: -f6)"
(
cd "$stage"
sudo -u stapler-builder env \
HOME="$builder_home" \
TERM=xterm-256color \
"${work_dir}/builder/stplr" \
--interactive=false build --clean -s Staplerfile
)
mapfile -t artifacts < <(find "$stage" -maxdepth 1 -type f -name '*.deb' -print)
[[ "${#artifacts[@]}" -eq 1 ]]
install -m0644 "${artifacts[0]}" "${package}/$(basename "${artifacts[0]}")"
}
mapfile -t packages < <(
printf '%s\n' "${lifecycle_packages[@]}" | cut -d '|' -f 1 | sort -u
)
missing_rpm=()
for package in "${packages[@]}"; do
mapfile -t artifacts < <(find "$package" -maxdepth 1 -type f -name '*.rpm' -print)
if [[ "${#artifacts[@]}" -ne 1 ]]; then
missing_rpm+=("$package")
fi
done
if [[ "${#missing_rpm[@]}" -gt 0 ]]; then
"${script_dir}/clean_build.sh" "${missing_rpm[@]}"
fi
"${script_dir}/verify_artifacts.sh" "${packages[@]}"
prepare_stplr
build_images
if [[ "$deb_build_mode" == 'container' ]]; then
docker volume create "$deb_cache_volume" >/dev/null
import_stplr_source_cache \
docker "$deb_image" "$deb_cache_volume" "$work_dir" "$source_cache" \
"${packages[@]}"
else
echo '==> DEB packages build directly on the host runner'
prepare_host_deb_builder
fi
deb_is_current() {
local package="$1"
local expected_name expected_version expected_release artifact
local -a existing
mapfile -t existing < <(find "$package" -maxdepth 1 -type f -name '*.deb' -print)
[[ "${#existing[@]}" -eq 1 ]] || return 1
artifact="${existing[0]}"
expected_name="$(stplr-spec get-field --path "${package}/Staplerfile" name)"
expected_version="$(stplr-spec get-field --path "${package}/Staplerfile" version)"
expected_release="$(stplr-spec get-field --path "${package}/Staplerfile" release)"
[[ "$(dpkg-deb -f "$artifact" Package 2>/dev/null)" == "${expected_name}+stplr-default" ]] &&
[[ "$(dpkg-deb -f "$artifact" Version 2>/dev/null)" == "${expected_version}-${expected_release}" ]]
}
for package in "${packages[@]}"; do
if [[ "${NIVORA_REBUILD_DEB:-0}" != '1' ]] && deb_is_current "$package"; then
echo "==> DEB reuse ${package}"
continue
fi
echo "==> DEB build ${package}"
find "$package" -maxdepth 1 -type f -name '*.deb' -delete
if [[ "$deb_build_mode" == 'host' ]]; then
build_deb_on_host "$package"
else
docker run --rm --privileged \
-e TERM=xterm-256color \
-v "${repo_root}/${package}:/app" \
-v "${deb_cache_volume}:/var/cache/stplr" \
-w /app \
"$deb_image" \
stplr --interactive=false build --clean -s Staplerfile
fi
done
build_previous_deb() {
local package="$1"
local system_name="$2"
local root="${work_dir}/previous-deb/${package}"
install -d \
"${root}/DEBIAN" \
"${root}/usr/share/nivora-lifecycle-previous"
printf '%s\n' \
"Package: ${system_name}" \
'Version: 0:0.0.2-1' \
'Architecture: all' \
'Maintainer: Nivora tests <noreply@example.invalid>' \
"Provides: ${package}" \
"Replaces: ${package}" \
"Conflicts: ${package}" \
'Description: Previous Nivora package fixture for lifecycle tests' \
>"${root}/DEBIAN/control"
printf 'previous fixture\n' \
>"${root}/usr/share/nivora-lifecycle-previous/${package}"
dpkg-deb --root-owner-group --build "$root" \
"${work_dir}/fixtures/previous-deb/${package}.deb" >/dev/null
}
build_previous_rpm() {
local package="$1"
local system_name="$2"
local topdir="${work_dir}/previous-rpmbuild/${package}"
local spec="${topdir}/SPECS/previous.spec"
local built
install -d \
"${topdir}/BUILD" \
"${topdir}/BUILDROOT" \
"${topdir}/RPMS" \
"${topdir}/SOURCES" \
"${topdir}/SPECS" \
"${topdir}/SRPMS" \
"${topdir}/TMP"
printf '%s\n' \
"Name: ${system_name}" \
'Version: 0.0.2' \
'Release: 1' \
'Summary: Previous Nivora package fixture for lifecycle tests' \
'Group: System/Configuration/Packaging' \
'License: MIT' \
'BuildArch: noarch' \
"Provides: ${package}" \
"Obsoletes: ${package}" \
"Conflicts: ${package}" \
'%description' \
'Previous Nivora package fixture for lifecycle tests.' \
'%install' \
'mkdir -p %{buildroot}/usr/share/nivora-lifecycle-previous' \
"printf 'previous fixture\\n' >%{buildroot}/usr/share/nivora-lifecycle-previous/${package}" \
'%files' \
"/usr/share/nivora-lifecycle-previous/${package}" \
>"$spec"
if ! rpmbuild \
--define "_topdir ${topdir}" \
--define "_tmppath ${topdir}/TMP" \
-bb "$spec" >"${topdir}/build.log" 2>&1; then
cat "${topdir}/build.log" >&2
return 1
fi
built="$(find "${topdir}/RPMS" -type f -name '*.rpm' -print -quit)"
[[ -n "$built" ]]
cp "$built" "${work_dir}/fixtures/previous-rpm/${package}.rpm"
}
metadata_contains() {
local value="$1"
local expected="$2"
value="${value//|/,}"
value="${value// /}"
[[ ",${value}," == *",${expected},"* ]]
}
printf '%s\n' "${lifecycle_packages[@]}" >"${work_dir}/lifecycle-packages.txt"
while IFS='|' read -r package _; do
mapfile -t debs < <(find "$package" -maxdepth 1 -type f -name '*.deb' -print)
mapfile -t rpms < <(find "$package" -maxdepth 1 -type f -name '*.rpm' -print)
[[ "${#debs[@]}" -eq 1 && "${#rpms[@]}" -eq 1 ]] || {
echo "${package}: ожидалось по одному DEB и RPM" >&2
exit 1
}
build_previous_deb "$package" "$(dpkg-deb -f "${debs[0]}" Package)"
build_previous_rpm "$package" "$(rpm -qp --queryformat '%{NAME}' "${rpms[0]}")"
for field in Provides Replaces Conflicts; do
value="$(dpkg-deb -f "${debs[0]}" "$field")"
metadata_contains "$value" "$package" || {
echo "${package}: DEB ${field} не содержит ${package}" >&2
exit 1
}
done
rpm -qp --provides "${rpms[0]}" | grep -Fxq "$package"
rpm -qp --obsoletes "${rpms[0]}" | grep -Fxq "$package"
rpm -qp --conflicts "${rpms[0]}" | grep -Fxq "$package"
done <"${work_dir}/lifecycle-packages.txt"
cat >"${work_dir}/run-deb.sh" <<'EOF'
#!/bin/bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
useradd --create-home nivora-test
is_installed() {
[[ "$(dpkg-query -W -f='${db:Status-Abbrev}' "$1" 2>/dev/null || true)" == 'ii '* ]]
}
while IFS='|' read -r package command_path state_path expected_a expected_b; do
mapfile -t artifacts < <(find "/repo/${package}" -maxdepth 1 -type f -name '*.deb' -print)
[[ "${#artifacts[@]}" -eq 1 ]]
artifact="${artifacts[0]}"
system_name="$(dpkg-deb -f "$artifact" Package)"
echo "==> DEB lifecycle ${package}"
dpkg -i "/previous/${package}.deb"
install -d "${state_path%/*}"
printf 'keep\n' >"$state_path"
if [[ "$state_path" == /home/nivora-test/* ]]; then
chown -R nivora-test:nivora-test /home/nivora-test
fi
apt-get -qq install -y "$artifact"
is_installed "$system_name"
test -x "$command_path"
[[ "$expected_a" == '-' ]] || test -e "$expected_a"
[[ "$expected_b" == '-' ]] || test -e "$expected_b"
apt-get -qq remove -y "$system_name"
test -f "$state_path"
! is_installed "$system_name"
done </lifecycle-packages.txt
EOF
cat >"${work_dir}/run-rpm.sh" <<'EOF'
#!/bin/bash
set -euo pipefail
useradd --create-home nivora-test
while IFS='|' read -r package command_path state_path expected_a expected_b; do
mapfile -t artifacts < <(find "/repo/${package}" -maxdepth 1 -type f -name '*.rpm' -print)
[[ "${#artifacts[@]}" -eq 1 ]]
artifact="${artifacts[0]}"
system_name="$(rpm -qp --queryformat '%{NAME}' "$artifact")"
echo "==> RPM lifecycle ${package}"
rpm -ivh --quiet "/previous/${package}.rpm"
install -d "${state_path%/*}"
printf 'keep\n' >"$state_path"
if [[ "$state_path" == /home/nivora-test/* ]]; then
chown -R nivora-test:nivora-test /home/nivora-test
fi
apt-get -qq install -y "$artifact"
rpm -q "$system_name"
test -x "$command_path"
[[ "$expected_a" == '-' ]] || test -e "$expected_a"
[[ "$expected_b" == '-' ]] || test -e "$expected_b"
apt-get -qq remove -y "$system_name"
test -f "$state_path"
! rpm -q "$system_name"
done </lifecycle-packages.txt
EOF
chmod 0755 "${work_dir}/run-deb.sh" "${work_dir}/run-rpm.sh"
docker run --rm --privileged \
-v "${repo_root}:/repo:ro" \
-v "${work_dir}/fixtures/previous-deb:/previous:ro" \
-v "${work_dir}/lifecycle-packages.txt:/lifecycle-packages.txt:ro" \
-v "${work_dir}/run-deb.sh:/run-lifecycle.sh:ro" \
"$deb_image" \
/run-lifecycle.sh
docker run --rm --privileged \
-v "${repo_root}:/repo:ro" \
-v "${work_dir}/fixtures/previous-rpm:/previous:ro" \
-v "${work_dir}/lifecycle-packages.txt:/lifecycle-packages.txt:ro" \
-v "${work_dir}/run-rpm.sh:/run-lifecycle.sh:ro" \
"$rpm_image" \
/run-lifecycle.sh
echo "OK: DEB/RPM lifecycle проверен для ${#lifecycle_packages[@]} пакетов"