Como o repositório Debian filtrado e re-assinado é construído.
O repositório repo.gnucego.com.br fica em /media/wellton/backup/debian e é servido pelo nginx. Ele contém:
main..deb e .udeb).O processo de atualização tem quatro scripts que rodam em cadeia (agendados por um timer às 02:00 diário):
sync-debian-mirror.sh — o orquestrador: baixa os pacotes do Debian com debmirror e rsync.fix-releases.py — reescreve os Release como GNUcego (remove contrib/non-free, re-assina GPG).regen-installer-index.py — corrige os hashes dos .udeb patcheados do debian-installer.update-gnu-repo.py — atualiza o repo gnu (IceCat do Trisquel, Linux-libre do Freesh).O Debian distribui software em três seções: main (100% livre), contrib (livre mas depende de não-livre) e non-free/non-free-firmware (não-livre). Para ser FSDG, o GNUcego mantém apenas main. O debmirror já é executado com --section=main, e o fix-releases.py reescreve o Release para anunciar apenas main (e remover as referências a contrib/non-free dos checksums).
Release com apt-ftparchive, que produz caminhos absolutos nos checksums e quebrava o download dos componentes do instalador (erro "falhou ao baixar um arquivo do espelho"). O fix-releases.py corrige isso baixando o Release upstream (que já tem caminhos relativos corretos) e apenas filtrando as linhas indesejadas.Roda debmirror para cada suíte (com --section=main, --arch=amd64, --nosource), sincroniza o índice do debian-installer via rsync, e depois chama o rebranding + reindex. Tem um trap EXIT que garante que o Release nunca fique quebrado, mesmo se um debmirror travar.
#!/bin/bash
#
# sync-debian-mirror.sh - Sincroniza espelho Debian
# Repositórios: stable (trixie), testing (forky) + security/updates + installer indices
#
MIRROR_DIR="/media/wellton/backup/debian"
LOG="/media/wellton/backup/debian/sync.log"
LOCK="/tmp/debian-mirror-sync.lock"
exec 200>"$LOCK"
flock -n 200 || { echo "$(date): já em execução" >> "$LOG"; exit 0; }
# ── Garante que o rebranding SEMPRE rode, mesmo se o debmirror travar ──
apply_gnucego_finalize() {
LC_ALL=C python3 /usr/local/bin/fix-releases.py >> "$LOG" 2>&1 || true
if [ -x /usr/local/bin/regen-installer-index.py ]; then
python3 /usr/local/bin/regen-installer-index.py >> "$LOG" 2>&1 || true
fi
fix_aliases
echo "$(date): finalize GNUcego aplicado" >> "$LOG"
}
trap 'apply_gnucego_finalize' EXIT
# ── Mantém os aliases de suíte (debmirror não toca neles) ──
fix_aliases() {
ln -sfn trixie "$MIRROR_DIR/dists/stable"
ln -sfn trixie-security "$MIRROR_DIR/dists/stable-security"
ln -sfn trixie-updates "$MIRROR_DIR/dists/stable-updates"
ln -sfn forky "$MIRROR_DIR/dists/testing"
}
fix_aliases
# ── 1. Debian stable main (trixie) ──
timeout 900 debmirror --arch=amd64 --dist=trixie --section=main \
--host=ftp.br.debian.org --root=/debian --method=http \
--nosource --no-check-gpg --i18n --progress --nocleanup \
--omit-suite-symlinks --rsync-extra=doc,tools,trace,installer \
"$MIRROR_DIR"
# ── 2. stable security ──
timeout 900 debmirror --arch=amd64 --dist=trixie-security --section=main \
--host=security.debian.org --root=/debian-security --method=http \
--nosource --no-check-gpg --i18n --progress --nocleanup \
--omit-suite-symlinks "$MIRROR_DIR"
# ── 3. stable updates ──
timeout 900 debmirror --arch=amd64 --dist=trixie-updates --section=main \
--host=ftp.br.debian.org --root=/debian --method=http \
--nosource --no-check-gpg --i18n --progress --nocleanup \
--omit-suite-symlinks "$MIRROR_DIR"
# ── 4. testing main (forky) ──
timeout 900 debmirror --arch=amd64 --dist=testing --section=main \
--host=ftp.br.debian.org --root=/debian --method=http \
--nosource --no-check-gpg --i18n --progress --nocleanup \
--omit-suite-symlinks --rsync-extra=installer "$MIRROR_DIR"
# ── 5. debian-installer indices (rsync, não coberto pelo debmirror) ──
rsync -a --no-motd --timeout=60 \
rsync://ftp.br.debian.org/debian/dists/trixie/main/debian-installer/ \
"$MIRROR_DIR/dists/trixie/main/debian-installer/"
rsync -a --no-motd --timeout=60 \
rsync://ftp.br.debian.org/debian/dists/testing/main/debian-installer/ \
"$MIRROR_DIR/dists/testing/main/debian-installer/"
# ── 5b. Sincroniza os .udeb referenciados pelo índice do instalador ──
if command -v zcat >/dev/null 2>&1; then
TMPLIST=$(mktemp)
for D in trixie testing; do
IDX="$MIRROR_DIR/dists/$D/main/debian-installer/binary-amd64/Packages.gz"
[ -f "$IDX" ] || continue
zcat "$IDX" 2>/dev/null | awk '/^Filename:/{print $2}' >> "$TMPLIST"
done
if [ -s "$TMPLIST" ]; then
sort -u "$TMPLIST" > "$TMPLIST.sorted"
rsync -a --no-motd --timeout=120 --relative --files-from="$TMPLIST.sorted" \
rsync://ftp.br.debian.org/debian/ "$MIRROR_DIR/"
fi
rm -f "$TMPLIST" "$TMPLIST.sorted"
fi
# ── Regenera o Release GNUcego (rebranding) ──
GPG_KEY=29BA79EBD86228A8
regen_gnucego_release() {
local dist="$1" url="$2" suite="$3" codename="$4"
local dir="$MIRROR_DIR/dists/$dist"
[ -d "$dir/main" ] || return 0
local up
up=$(curl -s --max-time 60 "$url") || return 0
[ -n "$up" ] || return 0
local tmp; tmp=$(mktemp)
{
echo "$up" | while IFS= read -r ln; do
case "$ln" in
" "*) case "$ln" in
*"contrib/"*|*"non-free"*) : ;;
*) printf '%s\n' "$ln" ;;
esac ;;
*) printf '%s\n' "$ln" ;;
esac
done | grep -vE "^(Origin|Label|Suite|Codename|Architectures|Components|Description|Date|Valid-Until|Acquire-By-Hash|Version|Changelogs):"
printf 'Origin: GNUcego\n'
printf 'Label: GNUcego\n'
printf 'Suite: %s\n' "$suite"
printf 'Codename: %s\n' "$codename"
printf 'Architectures: amd64\n'
printf 'Components: main\n'
printf 'Description: GNUcego (GNU/Linux-libre) - main\n'
printf 'Date: %s\n' "$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S UTC')"
} > "$tmp"
gpg --batch --yes --pinentry-mode loopback --passphrase "" \
--default-key "$GPG_KEY" --armor --detach-sign --output "$tmp.gpg" "$tmp"
gpg --batch --yes --pinentry-mode loopback --passphrase "" \
--default-key "$GPG_KEY" --armor --clearsign --output "$tmp.inrelease" "$tmp"
mv -f "$tmp" "$dir/Release"
mv -f "$tmp.gpg" "$dir/Release.gpg"
mv -f "$tmp.inrelease" "$dir/InRelease"
chmod 644 "$dir/Release" "$dir/Release.gpg" "$dir/InRelease"
}
regen_gnucego_release trixie "https://deb.debian.org/debian/dists/trixie/Release" stable trixie
regen_gnucego_release trixie-security "https://deb.debian.org/debian-security/dists/trixie-security/Release" stable-security trixie-security
regen_gnucego_release trixie-updates "https://deb.debian.org/debian/dists/trixie-updates/Release" stable-updates trixie-updates
regen_gnucego_release testing "https://deb.debian.org/debian/dists/testing/Release" testing forky
# ── 7b. Re-aplica o índice dos udebs PATCHEADOS do GNUcego ──
if [ -x /usr/local/bin/regen-installer-index.py ]; then
/usr/local/bin/regen-installer-index.py
fi
fix_aliases
echo "Sincronização concluída."
Baixa o Release upstream de cada suíte, remove as linhas de contrib/non-free/non-free-firmware (mantendo os caminhos de checksum relativos), reescreve os headers como Origin: GNUcego / Label: GNUcego e re-assina com a chave GPG 29BA79EBD86228A8.
LC_ALL=C (via locale.setlocale(LC_TIME, "C")) é essencial: garante que o campo Date seja gerado em inglês (RFC 2822), evitando o erro "Data inválida" no apt.
#!/usr/bin/env python3
import subprocess, os, sys, datetime, locale
locale.setlocale(locale.LC_TIME, "C") # garante Date em ingles (RFC 2822)
MIRROR = "/media/wellton/backup/debian"
GPG_KEY = "29BA79EBD86228A8"
DISTS = {
"trixie": ("https://deb.debian.org/debian/dists/trixie/Release", "stable", "trixie"),
"trixie-security": ("https://deb.debian.org/debian-security/dists/trixie-security/Release", "stable-security", "trixie-security"),
"trixie-updates": ("https://deb.debian.org/debian/dists/trixie-updates/Release", "stable-updates", "trixie-updates"),
"testing": ("https://deb.debian.org/debian/dists/testing/Release", "testing", "forky"),
}
def is_main(line):
for bad in ("contrib/", "non-free/", "non-free-firmware/"):
if bad in line:
return False
return True
def regen(dist, url, suite, codename):
local = f"{MIRROR}/dists/{dist}"
if not os.path.isdir(f"{local}/main"):
return
up = subprocess.run(["curl", "-s", "--max-time", "40", url], capture_output=True, text=True)
if up.returncode != 0 or not up.stdout.strip():
return
lines = up.stdout.splitlines()
out = []
for ln in lines:
if ln.startswith(" "):
if is_main(ln):
out.append(ln)
continue
out.append(ln)
for h in ("Origin", "Label", "Suite", "Codename", "Architectures", "Components",
"Description", "Date", "Valid-Until", "Acquire-By-Hash"):
out = [ln for ln in out if not ln.startswith(h + ":")]
out.append("Origin: GNUcego")
out.append("Label: GNUcego")
out.append(f"Suite: {suite}")
out.append(f"Codename: {codename}")
out.append("Architectures: all amd64")
out.append("Components: main")
out.append("Description: GNUcego (GNU/Linux-libre) - main")
out.append("Date: " + datetime.datetime.now(datetime.timezone.utc).strftime("%a, %d %b %Y %H:%M:%S UTC"))
new = "\n".join(out) + "\n"
with open(f"{local}/Release", "w") as f:
f.write(new)
subprocess.run(["gpg", "--batch", "--yes", "--pinentry-mode", "loopback",
"--passphrase", "", "--default-key", GPG_KEY, "--armor",
"--detach-sign", "--output", f"{local}/Release.gpg", f"{local}/Release"], check=True)
subprocess.run(["gpg", "--batch", "--yes", "--pinentry-mode", "loopback",
"--passphrase", "", "--default-key", GPG_KEY, "--armor",
"--clearsign", "--output", f"{local}/InRelease", f"{local}/Release"], check=True)
for d, (url, suite, codename) in DISTS.items():
regen(d, url, suite, codename)
print("CONCLUÍDO")
Um bug importante foi encontrado e corrigido no sync-debian-mirror.sh:
trixie-security (e o updates) ficou com o índice atualizado mas o pool com dezenas de .deb faltando, causando erro 404 Not Found no apt dist-upgrade dos usuários.
Causa raiz: o security.debian.org não serve rsync (a porta 873 não responde). Como o debmirror sempre usa rsync para baixar o trace (project/trace/*), ele travava na etapa final e nunca chegava a baixar os .deb.
Correção: adicionar --rsync-extra=none ao debmirror do trixie-security (e remover trace do --rsync-extra do trixie). Isso força o download 100% via HTTP, contornando o rsync quebrado:
# No debmirror do security:
--omit-suite-symlinks --rsync-extra=none
# No debmirror do stable (remover 'trace'):
--omit-suite-symlinks --rsync-extra=doc,tools,installer
Após isso, o debmirror termina em segundos ("All done") e baixa todos os pacotes corretamente.
O fix-releases.py reescreve o Codename de cada Release como exu (estável), exu-security, exu-updates e ogum (teste) — os codinomes brasileiros do GNUcego. Os symlinks exu/ogum (e exu-security/exu-updates) apontam para os diretórios físicos trixie/forky que o debmirror sincroniza do Debian upstream.
O GNUcego patcheia três .udeb do debian-installer (base-installer, bootstrap-base, cdebconf-newt-udeb) para instalar o kernel Linux-libre diretamente e aplicar o tema. Como o sync re-baixa o índice upstream (com os hashes dos udebs originais), este script recalcula Size/MD5sum/SHA256 dos udebs patcheados (a partir do pool real) e atualiza o Packages.gz/.xz + Release + assinatura.
#!/usr/bin/env python3
import subprocess, os, hashlib, gzip, re
MIRROR = "/media/wellton/backup/debian"
GPG_KEY = "29BA79EBD86228A8"
PATCHED = {
"base-installer": "pool/main/b/base-installer/base-installer_1.226_all.udeb",
"bootstrap-base": "pool/main/b/base-installer/bootstrap-base_1.226_amd64.udeb",
"cdebconf-newt-udeb": "pool/main/c/cdebconf/cdebconf-newt-udeb_0.280_amd64.udeb",
}
def file_hashes(path):
full = os.path.join(MIRROR, path)
with open(full, "rb") as f:
data = f.read()
return len(data), hashlib.md5(data).hexdigest(), hashlib.sha256(data).hexdigest()
def update_packages(text):
groups = text.split("\n\n")
for i, g in enumerate(groups):
lines = g.split("\n")
pkg = None
for ln in lines:
if ln.startswith("Package: "):
pkg = ln.split(": ", 1)[1].strip()
break
if pkg in PATCHED:
size, md5, sha = file_hashes(PATCHED[pkg])
newlines = []
for ln in lines:
if ln.startswith("Size: "):
newlines.append(f"Size: {size}")
elif ln.startswith("MD5sum: "):
newlines.append(f"MD5sum: {md5}")
elif ln.startswith("SHA256: "):
newlines.append(f"SHA256: {sha}")
else:
newlines.append(ln)
groups[i] = "\n".join(newlines)
return "\n\n".join(groups)
def sign(dirstr):
d = os.path.join(MIRROR, "dists", dirstr)
subprocess.run(["gpg", "--batch", "--yes", "--pinentry-mode", "loopback",
"--passphrase", "", "--default-key", GPG_KEY, "--armor",
"--detach-sign", "--output", "Release.gpg", "Release"], cwd=d, check=True)
subprocess.run(["gpg", "--batch", "--yes", "--pinentry-mode", "loopback",
"--passphrase", "", "--default-key", GPG_KEY, "--armor",
"--clearsign", "--output", "InRelease", "Release"], cwd=d, check=True)
def main():
for dist in ("trixie", "testing"):
bin_dir = os.path.join(MIRROR, "dists", dist, "main", "debian-installer", "binary-amd64")
if not os.path.isdir(bin_dir):
continue
real = os.path.realpath(os.path.join(bin_dir, "Packages.gz"))
with open(real, "rb") as f:
raw = f.read()
text = gzip.decompress(raw).decode("utf-8", "replace")
new_text = update_packages(text)
# Packages (sem gz) + recompactar
with open(os.path.join(bin_dir, "Packages"), "w") as f:
f.write(new_text)
gz_data = gzip.compress(new_text.encode("utf-8"))
gz_sha = hashlib.sha256(gz_data).hexdigest()
gz_md5 = hashlib.md5(gz_data).hexdigest()
# grava em by-hash/SHA256 e atualiza o symlink Packages.gz
# (... xz idem ...)
# atualiza o Release com os novos hashes e re-assina
sign(dist)
if __name__ == "__main__":
main()
Atualiza o repositório gnu, que contém o GNU IceCat (espelho do Trisquel via rsync) e o GNU Linux-libre (baixado do Freesh, mantendo as versões latest e LTS), e regenera o Packages + Release + assinatura.
#!/usr/bin/env python3
# Atualiza o repositório gnu: icecat (Trisquel) e linux-libre LTS+latest (Freesh).
POOL = "/media/wellton/backup/debian/pool/gnu"
DISTS = "/media/wellton/backup/debian/dists/gnu"
GPG_KEY = "29BA79EBD86228A8"
FREESH_BASE = "https://linux-libre.fsfla.org/pub/linux-libre/freesh"
# pacotes cuja "última versão" deve ser mantida
MAINTAINED_LATEST = ["icecat", "libjpeg8", "linux-libre", "linux-libre-headers",
"linux-libre-lts", "linux-libre-lts-headers"]
# nunca apagar
STATIC_PREFIXES = ("freesh-archive-keyring", "gnucego-keyring", "gnucego-tasks", "task-gnucego-i3wm")
# 1) icecat via rsync do mirror FSF (Trisquel)
# 2) linux-libre: parseia o Packages do Freesh, baixa linux-image/headers de latest + LTS
# 3) cleanup: remove séries antigas, mantém apenas latest + lts
# 4) regen: dpkg-scanpackages + apt-ftparchive + gpg (Release/InRelease)
update-gnu-repo.py mantém apenas as versões latest e LTS do Linux-libre. No momento da escrita, são 7.3.1-gnu (latest) e 6.18.60-gnu (LTS). O netinstall instala a versão mais recente via linux-image-<versão>-gnu.
O sync roda automaticamente por um systemd timer às 02:00 diariamente (não a cada hora, para não sobrecarregar). O comando abaixo mostra a ideia geral:
# timer diário às 02:00
OnCalendar=daily
# (o comando real chama /usr/local/bin/sync-debian-mirror.sh)
→ Próximo: como o netinstall é construído