mirror of
https://github.com/Cheviiot/Nivora.git
synced 2026-08-03 15:51:12 +00:00
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.
449 lines
16 KiB
Bash
Executable File
449 lines
16 KiB
Bash
Executable File
#!/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[@]} пакетов"
|