# Generate a reproducible archive from the content of a directory # # $1 : input directory # $2 : leading component in archive # $3 : ISO8601 date: YYYY-MM-DDThh:mm:ssZZ # $4 : output file # $5... : globs of filenames to exclude from the archive, suitable for # find's -path option, and relative to the input directory $1 # # Notes : # - the timestamp is internally rounded to the highest entire second # less than or equal to the timestamp (i.e. any sub-second fractional # part is ignored) # - must not be called with CWD as, or below, the input directory # - some temporary files are created in CWD, and removed at the end # # Example: # $ find /path/to/temp/dir # /path/to/temp/dir/ # /path/to/temp/dir/some-file # /path/to/temp/dir/some-dir/ # /path/to/temp/dir/some-dir/some-other-file # # $ mk_tar_gz /path/to/some/dir \ # foo_bar-1.2.3 \ # 1970-01-01T00:00:00Z \ # /path/to/foo.tar.gz \ # '.git/*' '.svn/*' # # $ tar tzf /path/to/foo.tar.gz # foo_bar-1.2.3/some-file # foo_bar-1.2.3/some-dir/some-other-file # mk_tar_gz() { local in_dir="${1}" local base_dir="${2}" local date="${3}" local out="${4}" shift 4 local glob tmp pax_options local -a find_opts for glob; do find_opts+=( -or -path "./${glob#./}" ) done # Drop sub-second precision to play nice with GNU tar's valid_timespec check date="$(date -d "${date}" -u +%Y-%m-%dT%H:%M:%S+00:00)" pax_options="delete=atime,delete=ctime,delete=mtime" pax_options+=",exthdr.name=%d/PaxHeaders/%f,exthdr.mtime={${date}}" tmp="$(mktemp --tmpdir="$(pwd)")" pushd "${in_dir}" >/dev/null # Establish list find . -not -type d -and -not \( -false "${find_opts[@]}" \) >"${tmp}.list" # Sort list for reproducibility LC_ALL=C sort <"${tmp}.list" >"${tmp}.sorted" # Create POSIX tarballs, since that's the format the most reproducible tar cf - --transform="s#^\./#${base_dir}/#S" \ --numeric-owner --owner=0 --group=0 --mtime="${date}" \ --format=posix --pax-option="${pax_options}" --mode='go=u,go-w' \ -T "${tmp}.sorted" >"${tmp}.tar" # Compress the archive gzip -6 -n <"${tmp}.tar" >"${out}" rm -f "${tmp}"{.list,.sorted,.tar} popd >/dev/null } post_process_unpack() { local dest="${1}" local tarball="${2}" local one_file mkdir "${dest}" tar -C "${dest}" --strip-components=1 -xzf "${tarball}" one_file="$(find "${dest}" -type f -print0 |LC_ALL=C sort -z |sed 's/\x0.*//')" touch -r "${one_file}" "${dest}.timestamp" } post_process_repack() { local in_dir="${1}" local base_dir="${2}" local out="${3}" local date date="@$(stat -c '%Y' "${in_dir}/${base_dir}.timestamp")" mk_tar_gz "${in_dir}/${base_dir}" "${base_dir}" "${date}" "${out}" } # Keep this line and the following as last lines in this file. # vim: ft=bash