← retour aux snippets

zip: archives reproductibles et déterministes

Produire un .zip stable bit-à-bit (ordre, métadonnées, mtime) avec zip -X et mtimes figés.

objectif

Générer une archive .zip identique bit-à-bit d’un run à l’autre: ordre des fichiers déterministe, métadonnées épurées (-X), dates de modification figées (ex: commit Git), exclusions sûres (.DS_Store, node_modules).

code minimal

# créer un zip reproductible à partir d'un dossier (ordre/mtime stables)
SRC="./build/data.pm"
OUT="site.data.pm.zip"
EPOCH="$(git -C "$SRC" log -1 --format=%ct 2>/dev/null || date -u +%s)"

# stage dans un temp puis figer les mtimes à EPOCH
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
rsync -a --delete --exclude '.DS_Store' --exclude 'node_modules' "$SRC"/ "$TMP/src"/

# fixer les mtimes (fichiers et dossiers) pour reproductibilité
find "$TMP/src" -exec touch -h -d "@$EPOCH" {} +

# construire le zip avec métadonnées minimales (-X), ordre stable (LC_ALL=C)
( cd "$TMP/src" && LC_ALL=C zip -r -q -9 -X "../$OUT" . )
mv -f "$TMP/$OUT" "./$OUT"

# vérifier l'empreinte (identique entre builds si contenu inchangé)
sha256sum "./$OUT" 2>/dev/null || shasum -a 256 "./$OUT"

utilisation

# 1) produire un zip à partir de la release courante sur le serveur
SRC="/srv/data.pm/current"
OUT="data.pm_$(date -u +%Y%m%dT%H%M%SZ).zip"
EPOCH="$(cat "$SRC/.build_epoch" 2>/dev/null || git -C "$SRC" log -1 --format=%ct 2>/dev/null || date -u +%s)"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
rsync -a --delete --exclude '.DS_Store' --exclude 'node_modules' "$SRC"/ "$TMP/src"/
find "$TMP/src" -exec touch -h -d "@$EPOCH" {} +
( cd "$TMP/src" && LC_ALL=C zip -r -q -9 -X "../$OUT" . )
mv -f "$TMP/$OUT" "./$OUT"

# 2) comparer deux builds (doivent matcher si sources identiques)
sha256sum build1.zip build2.zip 2>/dev/null || shasum -a 256 build1.zip build2.zip

# 3) exclure des artefacts inutiles (cartes sources, tests, caches)
EXCL=( -x '*.map' -x 'coverage/*' -x '*.tmp' )
( cd "$TMP/src" && LC_ALL=C zip -r -q -9 -X "${EXCL[@]}" "../$OUT" . )

# 4) générer un zip à partir de Git directement (reproductible par design)
# (noms/ordre contrôlés par Git; timestamps = commit time)
git -C /srv/data.pm archive --format=zip -o ./site.data.pm.zip --prefix=site/ HEAD

variante(s) utile(s)

# A) build CI: SOURCE_DATE_EPOCH (standard) pour figer l'horloge
: "${SOURCE_DATE_EPOCH:=$(git -C "$SRC" log -1 --format=%ct 2>/dev/null || date -u +%s)}"
find "$TMP/src" -exec touch -h -d "@$SOURCE_DATE_EPOCH" {} +
( cd "$TMP/src" && LC_ALL=C zip -r -q -9 -X "../$OUT" . )

# B) ordre strict défini par un manifest (si vous avez un listing)
sort -u manifest.txt | ( cd "$SRC" && LC_ALL=C zip -q -9 -X -@ "../$OUT" )

# C) macOS: éviter les forks/attrs Apple (réduire la variabilité)
# -X retire les extra fields; ajoutez des exclusions dédiées
rsync -a --delete --exclude '.DS_Store' --exclude '._*' --exclude '.Spotlight-V100' "$SRC"/ "$TMP/src"/

# D) vérifier l'absence de timestamps divergents dans l'archive
unzip -Z -t "./$OUT" >/dev/null
unzip -Z -v "./$OUT" | awk 'NR>3 && NF>=9 {print $(NF-3),$(NF-2),$(NF-1),$NF}' | head

# E) produire aussi une tarball zstd reproductible (utile côté Linux)
( cd "$TMP/src" && LC_ALL=C tar --sort=name --owner=0 --group=0 --numeric-owner \
  --mtime="@$EPOCH" -cf - . | zstd -T0 -q -19 > "../site.data.pm.tar.zst" )

notes

  • -X supprime les extra fields (UID/GID, atime, attrs étendus) qui cassent la reproductibilité. -9 maximise la compression.
  • Fixez tous les mtimes (fichiers et dossiers) à une valeur stable (SOURCE_DATE_EPOCH, date du commit Git) avant de zipper.
  • Forcez un ordre stable (locale C et tri alphabétique) en zippant depuis un répertoire et non depuis des chemins aléatoires.
  • Évitez de zipper des fichiers volatils (.DS_Store, caches, cartes sources si non nécessaires). Listez-les via -x ou en amont avec rsync --exclude.
  • Si votre source est un dépôt Git, git archive --format=zip est souvent le plus simple pour une archive déterministe sans manipuler les mtimes.