O espelho (repo.gnucego.com.br)

Como o repositório Debian filtrado e re-assinado é construído.

Visão geral

O repositório repo.gnucego.com.br fica em /media/wellton/backup/debian e é servido pelo nginx. Ele contém:

O processo de atualização tem quatro scripts que rodam em cadeia (agendados por um timer às 02:00 diário):

  1. sync-debian-mirror.sh — o orquestrador: baixa os pacotes do Debian com debmirror e rsync.
  2. fix-releases.py — reescreve os Release como GNUcego (remove contrib/non-free, re-assina GPG).
  3. regen-installer-index.py — corrige os hashes dos .udeb patcheados do debian-installer.
  4. update-gnu-repo.py — atualiza o repo gnu (IceCat do Trisquel, Linux-libre do Freesh).

Por que filtrar só "main"?

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).

Detalhe crítico (bug de caminho absoluto): uma versão antiga regenerava o 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.

1. sync-debian-mirror.sh (o orquestrador)

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.

Código completo

#!/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."

2. fix-releases.py (rebranding)

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.

O 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.

Código completo

#!/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")

Bug corrigido: o rsync travado (que impedia baixar os .deb)

Um bug importante foi encontrado e corrigido no sync-debian-mirror.sh:

Sintoma: o 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.

2.1 Os codinomes exu e ogum

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.

3. regen-installer-index.py (udebs patcheados)

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.

Código completo

#!/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()

4. update-gnu-repo.py (repo "gnu")

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.

Código completo (resumo)

#!/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)
O 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.

Agendamento

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