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
-Xsupprime les extra fields (UID/GID, atime, attrs étendus) qui cassent la reproductibilité.-9maximise 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
Cet 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
-xou en amont avecrsync --exclude. - Si votre source est un dépôt Git,
git archive --format=zipest souvent le plus simple pour une archive déterministe sans manipuler les mtimes.