| # 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}" |
| } |
| |
| trace() { local msg="${1}"; shift; printf "%s: ${msg}" "${my_name}" "${@}"; } |
| warn() { trace "${@}" >&2; } |
| error() { warn "${@}"; exit 1; } |
| |
| # Keep this line and the following as last lines in this file. |
| # vim: ft=bash |