diff --git a/mirror/Dockerfile b/mirror/Dockerfile new file mode 100644 index 0000000..0901fff --- /dev/null +++ b/mirror/Dockerfile @@ -0,0 +1,65 @@ +FROM almalinux:10 + +# dnf-plugins-core -> dnf download, repoquery, config-manager +# createrepo_c -> Repo-Metadaten bauen +# rpm-sign, gnupg2 -> Pakete umsignieren +# git, openssh-clients -> packages.yaml vor jedem Sync aus einem Git-Repo ziehen +# python3-pyyaml, jq -> packages.yaml (YAML) nach JSON wandeln und auswerten +# cpio -> Pakete fuer die AppStream-Erzeugung teilweise entpacken +# rsync -> privaten Paketstand pflegen + alle Fremdrepos zu +# einem gemeinsamen Repo zusammenfuehren +# util-linux-core -> stellt setpriv (root -> mirror-User droppen) sicher +RUN dnf -y install epel-release \ + && dnf -y install \ + dnf-plugins-core \ + createrepo_c \ + rpm-sign \ + gnupg2 \ + git \ + openssh-clients \ + python3-pyyaml \ + util-linux-core \ + findutils \ + cpio \ + rsync \ + procps-ng \ + jq \ + && dnf clean all + +# AppStream-Metadaten fuer Discover/GNOME Software: das noetige Werkzeug +# unterscheidet sich je nach Alma-Version. +# - AlmaLinux 9: appstream-glib (aus EPEL) liefert `appstream-builder`, +# das direkt ein Verzeichnis voller .rpm-Dateien einliest. +# - AlmaLinux 10: appstream-glib ist (Stand jetzt) weder in AlmaLinux 10 +# selbst noch in EPEL 10 paketiert. Stattdessen wird `appstreamcli` +# genutzt - dessen "compose"-Kommando ist aber NICHT im Basispaket +# `appstream` enthalten, sondern im separaten Subpaket +# `appstream-compose` (liegt im selben AlmaLinux-10-AppStream-Repo). +# Ohne dieses Subpaket bricht `appstreamcli compose` zur Laufzeit mit +# "AppStream Compose binary ... was not found" ab. +# sync.sh erkennt zur Laufzeit, welches der beiden Werkzeuge vorhanden ist. +# Ist keins von beiden installierbar, bricht der Build NICHT ab - die +# Discover-Katalogerzeugung wird dann zur Laufzeit einfach uebersprungen +# (Pakete bleiben trotzdem ganz normal per dnf/pkcon installierbar). +RUN (dnf -y install appstream-glib || dnf -y install appstream appstream-compose || dnf -y install appstream || \ + echo "WARNUNG (Build): weder appstream-glib noch appstream verfuegbar - AppStream/Discover-Metadaten werden zur Laufzeit uebersprungen.") \ + && dnf clean all + +RUN useradd -r -m -d /var/lib/mirror-gpg mirror + +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +COPY run-as-mirror.sh /usr/local/bin/run-as-mirror.sh +COPY sync.sh /usr/local/bin/sync.sh +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/run-as-mirror.sh /usr/local/bin/sync.sh + +RUN mkdir -p /srv/dnf /var/cache/mirror /var/lib/mirror-gpg \ + && chown -R mirror:mirror /srv/dnf /var/cache/mirror /var/lib/mirror-gpg + +ENV HOME=/var/lib/mirror-gpg +ENV GNUPGHOME=/var/lib/mirror-gpg/.gnupg + +# Bleibt root: entrypoint.sh chownt den bind-gemounteten ./gpg-Ordner (der dem +# Host gehoert, nicht dem Image) und wechselt danach per setpriv fest zu +# "mirror". Alles Weitere (Sync-Loop, git, dnf download, gpg) laeuft +# unprivilegiert. +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/mirror/entrypoint.sh b/mirror/entrypoint.sh new file mode 100644 index 0000000..5444f7f --- /dev/null +++ b/mirror/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Startet als root, damit der bind-gemountete ./gpg-Ordner (der auf dem Host +# meist root oder dem einladenden User gehoert) dem "mirror"-User im Container +# gehoert - danach wird per setpriv dauerhaft zu diesem unprivilegierten User +# gewechselt (ersetzt den Prozess direkt, kein Wrapper-Kindprozess, damit +# Signale/PID 1 sauber bleiben). +set -euo pipefail + +mkdir -p /var/lib/mirror-gpg /srv/dnf /var/cache/mirror +chown -R mirror:mirror /var/lib/mirror-gpg /srv/dnf /var/cache/mirror + +exec setpriv --reuid=mirror --regid=mirror --init-groups /usr/local/bin/run-as-mirror.sh diff --git a/mirror/run-as-mirror.sh b/mirror/run-as-mirror.sh new file mode 100644 index 0000000..fdc9469 --- /dev/null +++ b/mirror/run-as-mirror.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Laeuft als unprivilegierter User "mirror" (siehe entrypoint.sh, der als +# root die Verzeichnisrechte setzt und dann per setpriv hierher wechselt). +set -euo pipefail + +: "${GPG_NAME:?GPG_NAME muss gesetzt sein}" +: "${GPG_EMAIL:?GPG_EMAIL muss gesetzt sein}" +: "${SYNC_INTERVAL_SECONDS:=21600}" + +mkdir -p "$GNUPGHOME" +chmod 700 "$GNUPGHOME" +echo "allow-loopback-pinentry" > "$GNUPGHOME/gpg-agent.conf" +echo "pinentry-mode loopback" > "$GNUPGHOME/gpg.conf" + +PASSPHRASE_FILE=/run/secrets/gpg_passphrase +if [ -z "${GPG_PASSPHRASE:-}" ] && [ ! -f "$PASSPHRASE_FILE" ]; then + PASSPHRASE_FILE="" +fi + +# GPG-Key nur beim allerersten Start erzeugen. Das gnupg-Home liegt im +# bind-gemounteten ./gpg-Ordner neben der docker-compose.yml - der Key (und +# damit der Public Key, den alle Clients einmalig vertrauen) bleibt ueber +# Container-Neustarts/-Updates hinweg gleich UND ist direkt auf dem Host +# sichtbar/sicherbar, statt in einem anonymen Docker-Volume zu stecken. +if ! gpg --batch --list-secret-keys "$GPG_NAME" >/dev/null 2>&1; then + echo "[entrypoint] Erzeuge neuen Signing-Key fuer '$GPG_NAME <$GPG_EMAIL>' ..." + KEYPARAMS=$(mktemp) + { + echo "%echo Generating mirror signing key" + echo "Key-Type: RSA" + echo "Key-Length: 4096" + echo "Name-Real: $GPG_NAME" + echo "Name-Email: $GPG_EMAIL" + echo "Expire-Date: 0" + if [ -n "$PASSPHRASE_FILE" ]; then + echo "Passphrase: $(cat "$PASSPHRASE_FILE")" + else + echo "%no-protection" + fi + echo "%commit" + } > "$KEYPARAMS" + gpg --batch --gen-key "$KEYPARAMS" + rm -f "$KEYPARAMS" +else + echo "[entrypoint] Signing-Key existiert bereits, wird wiederverwendet." +fi + +mkdir -p /srv/dnf +gpg --batch --yes --armor --export "$GPG_NAME" > /srv/dnf/RPM-GPG-KEY-mirror +echo "[entrypoint] Public Key nach /srv/dnf/RPM-GPG-KEY-mirror exportiert." + +term_handler() { + echo "[entrypoint] Beende nach aktuellem Sync-Lauf ..." + exit 0 +} +trap term_handler SIGTERM SIGINT + +while true; do + /usr/local/bin/sync.sh || echo "[entrypoint] sync.sh ist mit Fehler beendet, naechster Versuch in ${SYNC_INTERVAL_SECONDS}s" + sleep "$SYNC_INTERVAL_SECONDS" & + wait $! +done diff --git a/mirror/sync.sh b/mirror/sync.sh new file mode 100644 index 0000000..27ae619 --- /dev/null +++ b/mirror/sync.sh @@ -0,0 +1,526 @@ +#!/bin/bash +# Ein Sync-Durchlauf: +# 0. packages.yaml frisch aus Git ziehen (Fremdrepos + Paketnamen je Repo) +# 1. je Fremdrepo genau die dort gelisteten Pakete per `dnf download` holen, +# filtern (Alma-Baseline) und in einen privaten, pro Fremdrepo getrennten +# Paket-Cache signieren +# 2. ALLE Fremdrepos zu EINEM gemeinsamen, oeffentlichen Ausgabe-Repo +# zusammenfuehren (ein repodata, ein GPG-Key, eine Client-.repo-Datei) - +# Clients binden nur dieses eine Repo ein, unabhaengig davon, aus wie +# vielen Fremdrepos die Pakete urspruenglich stammen +# 3. Repo-Metadaten (createrepo_c) + AppStream-Metadaten (fuer Discover) bauen +# 4. repomd.xml signieren, Client-.repo-Datei schreiben +set -uo pipefail + +CACHE=/var/cache/mirror +OUT=/srv/dnf +SOURCES_GIT_CHECKOUT="$CACHE/sources-git" +PACKAGES_JSON="$CACHE/packages.json" +SIGNED_ROOT="$CACHE/signed" + +: "${GPG_NAME:?}" +: "${MIRROR_DOMAIN:?}" +: "${SOURCES_GIT_URL:?SOURCES_GIT_URL muss auf das Git-Repo mit packages.yaml zeigen}" +: "${SOURCES_GIT_REF:=main}" +: "${MIRROR_REPO_ID:=company-mirror}" +: "${ALMA_RELEASEVER:=10}" +: "${ALMA_BASEARCH:=x86_64}" +: "${ALMA_BASELINE_REPOIDS:=baseos,appstream,extras,crb,epel}" + +mkdir -p "$CACHE/stage" "$CACHE/appstream-cache" "$CACHE/tmp-repos" "$SIGNED_ROOT" "$OUT" + +log() { echo "[sync $(date '+%F %T')] $*"; } + +# Zieht packages.yaml vor JEDEM Sync-Lauf frisch aus einem Git-Repo (statt aus +# lokal gemounteten Dateien), damit Aenderungen an der Paketliste ohne +# Container-Neubau/-Neustart wirksam werden. +fetch_packages_yaml() { + if [ -n "${SOURCES_GIT_SSH_KEY:-}" ] && [ -f "$SOURCES_GIT_SSH_KEY" ]; then + export GIT_SSH_COMMAND="ssh -i ${SOURCES_GIT_SSH_KEY} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=${CACHE}/known_hosts" + fi + + if [ -d "$SOURCES_GIT_CHECKOUT/.git" ]; then + log "Aktualisiere packages.yaml aus Git ($SOURCES_GIT_URL @ $SOURCES_GIT_REF) ..." + if git -C "$SOURCES_GIT_CHECKOUT" fetch --depth 1 origin "$SOURCES_GIT_REF" >>"$CACHE/git-sources.log" 2>&1 \ + && git -C "$SOURCES_GIT_CHECKOUT" reset --hard FETCH_HEAD >>"$CACHE/git-sources.log" 2>&1 \ + && git -C "$SOURCES_GIT_CHECKOUT" clean -fdx >>"$CACHE/git-sources.log" 2>&1; then + : + else + log "WARNUNG: git fetch fehlgeschlagen, siehe $CACHE/git-sources.log - verwende letzten bekannten Stand von packages.yaml weiter." + fi + else + log "Klone packages.yaml aus Git ($SOURCES_GIT_URL @ $SOURCES_GIT_REF) ..." + rm -rf "$SOURCES_GIT_CHECKOUT" + if ! git clone --branch "$SOURCES_GIT_REF" --single-branch --depth 1 \ + "$SOURCES_GIT_URL" "$SOURCES_GIT_CHECKOUT" >>"$CACHE/git-sources.log" 2>&1; then + log "FEHLER: Erstklon von packages.yaml aus Git fehlgeschlagen, siehe $CACHE/git-sources.log" + return 1 + fi + fi + + local yaml_file="$SOURCES_GIT_CHECKOUT/packages.yaml" + if [ ! -f "$yaml_file" ]; then + log "FEHLER: $yaml_file existiert nicht im Git-Repo." + return 1 + fi + + if ! python3 -c ' +import sys, json, yaml +with open(sys.argv[1]) as f: + data = yaml.safe_load(f) or {} +data.setdefault("repos", []) +data.setdefault("force_include", []) +json.dump(data, sys.stdout) +' "$yaml_file" > "$PACKAGES_JSON.tmp" 2>"$CACHE/packages-yaml-parse.log"; then + log "FEHLER: packages.yaml ist kein gueltiges YAML, siehe $CACHE/packages-yaml-parse.log" + rm -f "$PACKAGES_JSON.tmp" + return 1 + fi + mv "$PACKAGES_JSON.tmp" "$PACKAGES_JSON" +} + +refresh_alma_names() { + log "Ermittle Paketnamen, die AlmaLinux selbst bereitstellt ..." + local -a enable_args=() + local id + IFS=',' read -ra ids <<< "$ALMA_BASELINE_REPOIDS" + for id in "${ids[@]}"; do + enable_args+=(--enablerepo="$id") + done + + local -a reposdir_args=() + if [ -n "${ALMA_BASELINE_REPOSDIR:-}" ] && ls "${ALMA_BASELINE_REPOSDIR}"/*.repo >/dev/null 2>&1; then + reposdir_args=(--setopt="reposdir=${ALMA_BASELINE_REPOSDIR}") + fi + + if ! dnf repoquery -y --quiet -a --qf '%{name}' \ + --disablerepo='*' "${enable_args[@]}" \ + --releasever="$ALMA_RELEASEVER" --forcearch="$ALMA_BASEARCH" \ + "${reposdir_args[@]}" 2>>"$CACHE/repoquery.log" | sort -u > "$CACHE/alma-names.txt.tmp"; then + log "WARNUNG: repoquery gegen AlmaLinux-Baseline fehlgeschlagen, siehe $CACHE/repoquery.log" + log "Verwende ggf. vorherigen Stand von alma-names.txt weiter." + rm -f "$CACHE/alma-names.txt.tmp" + return + fi + mv "$CACHE/alma-names.txt.tmp" "$CACHE/alma-names.txt" + log "$(wc -l < "$CACHE/alma-names.txt") Paketnamen als 'ueber Alma aufloesbar' erfasst." +} + +is_force_included() { + local name="$1" f + for f in "${FORCE_INCLUDE_NAMES[@]}"; do + [ "$f" = "$name" ] && return 0 + done + return 1 +} + +# Liest einen einzelnen Schluessel aus dem [Desktop Entry]-Abschnitt einer +# .desktop-Datei (ignoriert lokalisierte Varianten wie Name[de]= und andere +# Abschnitte wie [Desktop Action ...]). +desktop_get() { + local file="$1" key="$2" + awk -v key="$key" ' + /^\[Desktop Entry\]/ { insec=1; next } + /^\[/ { insec=0 } + insec && index($0, key "=") == 1 { sub("^" key "=", ""); print; exit } + ' "$file" +} + +xml_escape() { + local s="$1" + s="${s//&/&}" + s="${s///>}" + s="${s//\"/"}" + s="${s//\'/'}" + printf '%s' "$s" +} + +# appstreamcli compose (AppStream 1.0+) ignoriert .desktop-Dateien komplett, +# wenn keine begleitende .metainfo.xml existiert - anders als das aeltere +# appstream-builder, das genau das frueher automatisch synthetisiert hat. +# Die meisten unabhaengigen Drittanbieter-RPMs (z.B. jpackage-basierte Apps +# wie XPipe) liefern aber nur eine .desktop-Datei mit. Deshalb hier selbst +# eine minimale Ersatz-Metainfo aus den Desktop-Datei-Feldern erzeugen, damit +# die Anwendung ueberhaupt einen Katalogeintrag in Discover bekommt. Bringt +# ein Paket schon eine echte .metainfo.xml mit (die auf dasselbe Desktop-File +# verweist), hat diese immer Vorrang - hier wird dann nichts generiert. +generate_fallback_metainfo() { + local composeroot="$1" pkgmap="$2" + local appdir="$composeroot/usr/share/applications" + local metadir="$composeroot/usr/share/metainfo" + local desktop_file base name summary categories cat pkgname + + [ -d "$appdir" ] || return + mkdir -p "$metadir" + + for desktop_file in "$appdir"/*.desktop; do + [ -e "$desktop_file" ] || continue + base=$(basename "$desktop_file") + + if grep -rlq "type=\"desktop-id\">${base}<" "$metadir" 2>/dev/null; then + continue + fi + + pkgname="" + [ -f "$pkgmap" ] && pkgname=$(awk -F'\t' -v b="$base" '$1 == b {print $2; exit}' "$pkgmap") + [ -z "$pkgname" ] && pkgname="${base%.desktop}" + + name=$(desktop_get "$desktop_file" "Name") + summary=$(desktop_get "$desktop_file" "Comment") + categories=$(desktop_get "$desktop_file" "Categories") + [ -z "$name" ] && name="${base%.desktop}" + [ -z "$summary" ] && summary="$name" + + { + echo '' + echo '' + echo " ${base}" + echo " $(xml_escape "$pkgname")" + echo ' CC0-1.0' + echo ' LicenseRef-proprietary' + echo " $(xml_escape "$name")" + echo " $(xml_escape "$summary")" + echo " " + echo "

$(xml_escape "$summary")

" + echo "
" + echo " ${base}" + if [ -n "$categories" ]; then + echo ' ' + local IFS=';' + for cat in $categories; do + [ -z "$cat" ] && continue + echo " $(xml_escape "$cat")" + done + echo ' ' + fi + echo '
' + } > "$metadir/${base%.desktop}.metainfo.xml" + done +} + +# AlmaLinux 9: appstream-glib (aus EPEL) liest ein Verzeichnis voller +# .rpm-Dateien direkt ein. +generate_appstream_via_builder() { + local id="$1" out="$2" + local asout="$CACHE/appstream/$id" + mkdir -p "$asout" + if appstream-builder \ + --origin="$id" --basename="$id" \ + --cache-dir="$CACHE/appstream-cache/$id" \ + --packages-dir="$out" \ + --output-dir="$asout" \ + --min-icon-size=48 --enable-hidpi \ + >>"$CACHE/appstream-$id.log" 2>&1; then + [ -f "$asout/$id.xml.gz" ] && modifyrepo_c --mdtype=appstream "$asout/$id.xml.gz" "$out/repodata" + [ -f "$asout/$id-icons.tar.gz" ] && modifyrepo_c --mdtype=appstream-icons "$asout/$id-icons.tar.gz" "$out/repodata" + else + log "WARNUNG: appstream-builder fuer $id fehlgeschlagen (nicht fatal), siehe $CACHE/appstream-$id.log" + fi +} + +# AlmaLinux 10: appstream-glib gibt es (Stand jetzt) weder dort noch in +# EPEL 10, stattdessen `appstream`/appstreamcli. Dessen "compose"-Kommando +# arbeitet nicht auf .rpm-Dateien, sondern auf einem Verzeichnisbaum wie +# einem Dateisystem-Root (/usr/share/applications, /usr/share/metainfo, ...). +# Wir entpacken deshalb erst die dafuer relevanten Pfade aus allen Paketen in +# so einen Baum und rufen dann `appstreamcli compose` darauf auf. +generate_appstream_via_compose() { + local id="$1" out="$2" + local composeroot="$CACHE/appstream-root/$id" + local asout="$CACHE/appstream/$id" + local pkgmap="$CACHE/appstream-pkgmap-$id.txt" + local rpm + + rm -rf "$composeroot" + mkdir -p "$composeroot/usr/share/applications" "$composeroot/usr/share/metainfo" + : > "$pkgmap" + + for rpm in "$out"/*.rpm; do + [ -e "$rpm" ] || continue + local pkgname + pkgname=$(rpm -qp --qf '%{NAME}' "$rpm" 2>/dev/null) + + # Icons/Pixmaps liegen so gut wie immer am Standard-FHS-Pfad - dafuer + # reicht ein blinder Glob-Extract, eine Paketzuordnung brauchen wir + # dafuer nicht. + (cd "$composeroot" && rpm2cpio "$rpm" | cpio -idm --quiet \ + './usr/share/icons/*' \ + './usr/share/pixmaps/*') >>"$CACHE/appstream-$id.log" 2>&1 + + # .desktop-/AppStream-Dateien koennen dagegen auch ausserhalb der + # Standardpfade liegen (u.a. alle jpackage-basierten Java/JavaFX-Apps + # wie z.B. XPipe legen sie direkt im eigenen Installationsverzeichnis + # ab, z.B. /opt//.desktop, und verschieben sie erst per + # %post-Skript - das sehen wir beim reinen Lesen des Pakets nicht). + # Deshalb IMMER das ganze Paket nur AUFLISTEN (kein teurer Voll- + # Extract, bei z.T. >100 MB grossen JRE-Bundles) und alle + # .desktop/.metainfo.xml/.appdata.xml-Dateien gezielt herausgreifen - + # dabei merken wir uns, aus welchem RPM-Paket jede .desktop-Datei + # stammt, fuer in der (Fallback-)Metainfo weiter unten. + local matches + mapfile -t matches < <(rpm2cpio "$rpm" 2>/dev/null | cpio -t --quiet 2>/dev/null \ + | grep -E '\.desktop$|\.metainfo\.xml$|\.appdata\.xml$') + + if [ "${#matches[@]}" -gt 0 ]; then + local extractroot f base destdir + extractroot=$(mktemp -d) + (cd "$extractroot" && rpm2cpio "$rpm" | cpio -idm --quiet "${matches[@]}") >>"$CACHE/appstream-$id.log" 2>&1 + for f in "${matches[@]}"; do + [ -f "$extractroot/$f" ] || continue + base=$(basename "$f") + case "$base" in + *.desktop) + destdir="$composeroot/usr/share/applications" + printf '%s\t%s\n' "$base" "$pkgname" >> "$pkgmap" + ;; + *) + destdir="$composeroot/usr/share/metainfo" + ;; + esac + # Nicht ueberschreiben, falls bereits ein anderes Paket schon + # eine gleichnamige Datei mitgebracht hat. + if [ ! -e "$destdir/$base" ]; then + cp "$extractroot/$f" "$destdir/$base" + # Vom Hersteller mitgelieferte .metainfo.xml/.appdata.xml + # haben so gut wie nie ein (das kennt nur die + # Distro/das Repo, nicht der Upstream-Entwickler) - ohne + # das bleibt die Komponente in Discover trotz gueltiger + # Metainfo nicht installierbar. Deshalb hier direkt + # ergaenzen, falls es fehlt. + if [ "$base" != "${base%.desktop}" ]; then + : # ist eine .desktop-Datei, braucht selbst kein + elif ! grep -q '' "$destdir/$base" 2>/dev/null; then + sed -i "s## $(xml_escape "$pkgname")\n#" "$destdir/$base" + fi + fi + done + rm -rf "$extractroot" + fi + done + + if [ -z "$(find "$composeroot" -type f 2>/dev/null)" ]; then + log "Keine Desktop-/AppStream-Dateien in den Paketen gefunden, ueberspringe Katalog-Generierung." + return + fi + + generate_fallback_metainfo "$composeroot" "$pkgmap" + + rm -rf "$asout" + mkdir -p "$asout" + # Exit-Code NICHT sofort als fatal behandeln: appstreamcli compose liefert + # bei einzelnen fehlerhaften Komponenten ("Run failed, some data was + # ignored") einen Fehler-Exitcode, schreibt aber trotzdem einen Katalog + # mit allen UEBRIGEN, gueltigen Komponenten. Nur wenn am Ende wirklich + # gar kein Katalog entstanden ist, brechen wir ab. + if ! appstreamcli compose --prefix=/usr --origin="$id" --result-root="$asout" "$composeroot" \ + >>"$CACHE/appstream-$id.log" 2>&1; then + log "WARNUNG: appstreamcli compose meldet Fehler bei einzelnen Komponenten, siehe $CACHE/appstream-$id.log - verwende ggf. trotzdem erzeugten Teil-Katalog." + fi + + local catalog + catalog=$(find "$asout" -type f \( -iname '*.xml.gz' -o -iname '*.yml.gz' \) | head -n1) + if [ -z "$catalog" ]; then + log "WARNUNG: appstreamcli compose hat keinen Katalog erzeugt, siehe $CACHE/appstream-$id.log" + return + fi + modifyrepo_c --mdtype=appstream "$catalog" "$out/repodata" + + local icons_dir + icons_dir=$(find "$asout" -type d -iname 'icons' | head -n1) + if [ -n "$icons_dir" ] && [ -n "$(find "$icons_dir" -type f 2>/dev/null)" ]; then + tar -C "$icons_dir" -czf "$asout/$id-icons.tar.gz" . + modifyrepo_c --mdtype=appstream-icons "$asout/$id-icons.tar.gz" "$out/repodata" + fi +} + +# Holt + filtert + signiert die Pakete EINES Fremdrepos in einen privaten, +# nur intern genutzten Cache ($SIGNED_ROOT/$id) - noch NICHT im oeffentlichen +# Ausgabeverzeichnis. Der eigentliche Merge zu einem gemeinsamen Repo passiert +# erst danach, einmalig fuer alle Fremdrepos zusammen (siehe unten). +# Args: id baseurl gpgkey gpgcheck(0/1) paket1 [paket2 ...] +sync_one_repo() { + local id="$1" baseurl="$2" gpgkey="$3" gpgcheck="$4" + shift 4 + local -a packages=("$@") + local stage="$CACHE/stage/$id" + local signed="$SIGNED_ROOT/$id" + local tmp_repofile="$CACHE/tmp-repos/$id.repo" + + log "== $id: hole ${#packages[@]} Paket(e) aus $baseurl ==" + rm -f "$CACHE/tmp-repos"/*.repo + { + echo "[$id]" + echo "name=$id" + echo "baseurl=$baseurl" + echo "enabled=1" + echo "gpgcheck=$gpgcheck" + [ -n "$gpgkey" ] && echo "gpgkey=$gpgkey" + } > "$tmp_repofile" + + rm -rf "$stage" + mkdir -p "$stage" + if ! dnf --setopt="reposdir=$CACHE/tmp-repos" \ + --disablerepo='*' --enablerepo="$id" \ + --forcearch="$ALMA_BASEARCH" \ + download --destdir="$stage" "${packages[@]}" \ + >>"$CACHE/download-$id.log" 2>&1; then + log "FEHLER: dnf download fuer $id fehlgeschlagen, siehe $CACHE/download-$id.log - ueberspringe (bisheriger Stand von $id bleibt im gemeinsamen Repo erhalten)." + return 1 + fi + + log "== $id: filtere Pakete, die Alma bereits bereitstellt ==" + local removed=0 kept=0 + while IFS= read -r -d '' rpm; do + local name + name=$(rpm -qp --qf '%{NAME}' "$rpm" 2>/dev/null) + if [ -z "$name" ]; then + continue + fi + if grep -qxF "$name" "$CACHE/alma-names.txt" 2>/dev/null && ! is_force_included "$name"; then + rm -f "$rpm" + removed=$((removed + 1)) + else + kept=$((kept + 1)) + fi + done < <(find "$stage" -maxdepth 1 -name '*.rpm' -print0) + log "$id: $kept Pakete werden gespiegelt, $removed uebersprungen (bereits ueber Alma verfuegbar)." + + log "== $id: aktualisiere internen Paketstand und signiere neue/geaenderte Pakete ==" + mkdir -p "$signed" + local changed_list rsync_out + changed_list=$(mktemp) + rsync_out=$(mktemp) + if ! rsync -ai --delete --include='*.rpm' --exclude='*' "$stage"/ "$signed"/ > "$rsync_out" 2>&1; then + log "FEHLER: rsync (interner Paketstand) fuer $id fehlgeschlagen: $(cat "$rsync_out") - ueberspringe $id fuer diesen Lauf." + rm -f "$changed_list" "$rsync_out" + return 1 + fi + awk '/^[><]f/{print $2}' "$rsync_out" > "$changed_list" + rm -f "$rsync_out" + + local resign_failed=0 + while IFS= read -r f; do + [ -z "$f" ] && continue + if ! rpmsign --resign \ + --define "_gpg_name $GPG_NAME" \ + --define "_gpg_path $GNUPGHOME" \ + "$signed/$f" >>"$CACHE/rpmsign-$id.log" 2>&1; then + log "FEHLER: Signieren von $signed/$f fehlgeschlagen, siehe $CACHE/rpmsign-$id.log" + resign_failed=1 + fi + done < "$changed_list" + local changed_count + changed_count=$(wc -l < "$changed_list") + rm -f "$changed_list" + log "$id: $changed_count Pakete neu signiert." + if [ "$resign_failed" -ne 0 ]; then + log "FEHLER: mind. ein Paket in $id konnte nicht signiert werden - $id wird bei der Zusammenfuehrung uebersprungen." + return 1 + fi + + log "== $id: fertig ==" +} + +if ! fetch_packages_yaml; then + log "FEHLER: keine packages.yaml verfuegbar, breche Sync-Lauf komplett ab." + exit 1 +fi + +mapfile -t FORCE_INCLUDE_NAMES < <(jq -r '.force_include[]? // empty' "$PACKAGES_JSON") + +refresh_alma_names +if [ ! -s "$CACHE/alma-names.txt" ]; then + log "FEHLER: keine Alma-Baseline-Namen verfuegbar, breche Sync-Lauf ab (Sicherheitsnetz gegen versehentliches Doppel-Spiegeln)." + exit 1 +fi + +overall_status=0 +all_ids=() +repo_count=$(jq '.repos | length' "$PACKAGES_JSON") +for ((i = 0; i < repo_count; i++)); do + repo_json=$(jq -c ".repos[$i]" "$PACKAGES_JSON") + id=$(jq -r '.id // empty' <<<"$repo_json") + baseurl=$(jq -r '.baseurl // empty' <<<"$repo_json") + gpgkey=$(jq -r '.gpgkey // empty' <<<"$repo_json") + gpgcheck=$(jq -r 'if .gpgcheck == false then "0" else "1" end' <<<"$repo_json") + mapfile -t packages < <(jq -r '.packages[]? // empty' <<<"$repo_json") + + if [ -z "$id" ] || [ -z "$baseurl" ] || [ "${#packages[@]}" -eq 0 ]; then + log "WARNUNG: Eintrag Nr. $((i + 1)) in packages.yaml unvollstaendig (id/baseurl/packages fehlt), ueberspringe." + overall_status=1 + continue + fi + + all_ids+=("$id") + sync_one_repo "$id" "$baseurl" "$gpgkey" "$gpgcheck" "${packages[@]}" || overall_status=1 +done + +# Internen Paketstand von Fremdrepos aufraeumen, die nicht mehr in +# packages.yaml stehen - sonst wuerden deren alte Pakete beim Merge unten +# unnoetig im Cache liegen bleiben (sie werden ja schon jetzt nicht mehr in +# die Merge-Quellenliste aufgenommen, siehe unten - das hier ist nur Hygiene). +if [ -d "$SIGNED_ROOT" ]; then + for d in "$SIGNED_ROOT"/*/; do + [ -d "$d" ] || continue + existing_id=$(basename "$d") + keep=0 + for id in "${all_ids[@]}"; do + [ "$id" = "$existing_id" ] && keep=1 && break + done + if [ "$keep" -eq 0 ]; then + log "Entferne internen Paketstand des nicht mehr in packages.yaml gelisteten Fremdrepos '$existing_id' ..." + rm -rf "$d" + fi + done +fi + +log "== Fuehre alle Fremdrepos zu EINEM gemeinsamen Ausgabe-Repo zusammen ==" +merge_sources=() +for id in "${all_ids[@]}"; do + [ -d "$SIGNED_ROOT/$id" ] && merge_sources+=("$SIGNED_ROOT/$id/") +done + +if [ "${#merge_sources[@]}" -eq 0 ]; then + log "FEHLER: kein Fremdrepo hat je erfolgreich Pakete geliefert - gemeinsames Repo bleibt unveraendert/leer." + exit 1 +fi + +if ! rsync -ai --delete --include='*.rpm' --exclude='*' "${merge_sources[@]}" "$OUT"/ >>"$CACHE/merge.log" 2>&1; then + log "FEHLER: Zusammenfuehren der Fremdrepos zum gemeinsamen Repo fehlgeschlagen, siehe $CACHE/merge.log" + exit 1 +fi + +log "== baue Repo-Metadaten fuer das gemeinsame Repo ==" +if ! createrepo_c --update "$OUT" >>"$CACHE/createrepo.log" 2>&1; then + log "FEHLER: createrepo_c fehlgeschlagen, siehe $CACHE/createrepo.log" + exit 1 +fi + +log "== baue AppStream-Metadaten (fuer Discover/GNOME Software) ==" +if command -v appstream-builder >/dev/null 2>&1; then + generate_appstream_via_builder "$MIRROR_REPO_ID" "$OUT" +elif command -v appstreamcli >/dev/null 2>&1; then + generate_appstream_via_compose "$MIRROR_REPO_ID" "$OUT" +else + log "WARNUNG: weder appstream-builder noch appstreamcli installiert - Pakete erscheinen ohne App-Katalogeintrag in Discover." +fi + +log "== signiere repomd.xml ==" +gpg --batch --yes --detach-sign --armor --local-user "$GPG_NAME" \ + --output "$OUT/repodata/repomd.xml.asc" "$OUT/repodata/repomd.xml" + +cat > "$OUT/$MIRROR_REPO_ID.repo" <