#!/bin/bash
#
# Generates tarballs and signatures (SHA-512 and PGP) in a directory structure
# that can be rsync'd to a remote host.
#
# - $PWD/dist/
#     - postmill/
#         - releases/
#             - vX.Y.Z/
#                 - postmill-assets-vX.Y.Z.tar.gz
#                 - postmill-assets-vX.Y.Z.tar.gz.asc
#                 - postmill-cacheless-vX.Y.Z.tar.gz
#                 - postmill-cacheless-vX.Y.Z.tar.gz.asc
#                 - SHA512SUMS
#
# Must be run on a system with git, gpg, jq, sha512sum, and GNU coreutils.
# Generated files are assumed to already be in a suitable state for bundling.
#
# Adapted from https://www.gnu.org/software/tar/manual/tar.html#Reproducibility
#
set -eu -o pipefail

function get_commit_time() {
    TZ=UTC0 git log -1 \
        --format=tformat:%cd \
        --date=format-local:%Y-%m-%dT%H:%M:%SZ \
        "$@"
}

function check_executables() {
    for executable in $@; do
        if ! command -v "$executable" > /dev/null; then
            echo "Executable '$executable' must be present" >&2
            exit 1
        fi
    done
}

function usage() {
    {
        echo "Usage: $0"
        echo -e "\t[-d|--working-dir=<...>]"
        echo -e "\t[-g|--gpg-options=<...>]"
        echo -e "\t<version-string>"
    } >&2
    exit 1
}

check_executables git gpg jq sha512sum tar

eval set -- "$(getopt -o d:g:t: -l working-dir:,gpg-options: -- "$@")"

GPG_OPTIONS=""
while [ $# -gt 0 ]; do
    case "$1" in
        -d|--working-dir)
            WORK_DIR="$2"
            shift
        ;;
        -g|--gpg-options)
            GPG_OPTIONS="$2"
            shift
        ;;
        --)
            if [ "$#" -ne 2 ]; then
                usage
            fi
            VERSION_ARG="$2"
            shift
            break
        ;;
        *)
            usage
        ;;
    esac
    shift
done

DIST_DIR="$(pwd)/dist/postmill"
RELEASE_DIR="releases/$VERSION_ARG"
RELEASE_NAME="postmill-$VERSION_ARG"
RELEASE_CACHELESS_NAME="postmill-cacheless-$VERSION_ARG"
RELEASE_ASSETS_NAME="postmill-assets-$VERSION_ARG"

if [ -d "$DIST_DIR/$RELEASE_DIR" ]; then
    echo "Target directory already exists ($DIST_DIR/$RELEASE_DIR)" >&2
    exit 1
fi

if [ ! -f "vendor/autoload.php" ]; then
    echo "Composer autoloader must be generated" >&2
    exit 1
fi

if [ ! -f "public/build/assets-manifest.json" ]; then
    echo "Frontend assets must be built and present" >&2
    exit 1
fi

# Get the branch (e.g. '2.2'), resolved to its branch alias
VERSION_BRANCH="$(jq -r --arg branch "$(git rev-parse --abbrev-ref HEAD)" \
    '.extra["branch-alias"][("dev-" + $branch)] // $branch' \
    < composer.json)"

if [ "${WORK_DIR:-}" == "" ]; then
    WORK_DIR="$(mktemp -d)"
    trap 'rm -rf "$WORK_DIR"' EXIT
fi
mkdir -p "$WORK_DIR/$RELEASE_CACHELESS_NAME"

VCS_FILES="
    bin/console
    config/
    migrations/
    public/.htaccess
    public/index.php
    src/
    templates/
    translations/
    CHANGELOG.md
    CREDITS.md
    LICENSE
"

RUNTIME_FILES="
    README.DIST.md
    public/build/
    vendor/
"

# Copy files from git with their commit time as mtime
echo "Copying repository files"
git ls-files $VCS_FILES | while read -r file; do
    cp --parents "$file" "$WORK_DIR/$RELEASE_CACHELESS_NAME"

    commit_time=$(get_commit_time "$file") &&
        touch -md "$commit_time" "$WORK_DIR/$RELEASE_CACHELESS_NAME/$file"
done

# Copy files needed at runtime
echo "Copying runtime files"
cp --parents -r $RUNTIME_FILES "$WORK_DIR/$RELEASE_CACHELESS_NAME"

echo "Updating README.DIST.md"
sed -i -e "s/vX\.Y\.Z/${VERSION_ARG}/g" \
    "$WORK_DIR/$RELEASE_CACHELESS_NAME/README.DIST.md"

# public/ files
echo "Copying public/ files"
cp example_files/public/* "$WORK_DIR/$RELEASE_CACHELESS_NAME/public"

echo "Generating .env.local.php"
bin/env-to-php.sed < .env > "$WORK_DIR/$RELEASE_CACHELESS_NAME/.env.local.php"

echo "Generating config/dist.yaml"
UNIX_TIME="$(git log -1 --date=unix --format=tformat:%cd)"

echo "# generated
parameters:
    app_branch: '$VERSION_BRANCH'
    app_version: '$VERSION_ARG'
    kernel.container_build_time: $UNIX_TIME" \
> "$WORK_DIR/$RELEASE_CACHELESS_NAME/config/dist.yaml"

# Set timestamp of each directory under $FILES to the latest timestamp of any
# descendant.
echo "Timestamping directories"
find "$WORK_DIR/$RELEASE_CACHELESS_NAME" -depth -type d -exec sh -c \
    'touch -r "$0/$(ls -At "$0" | head -n 1)" "$0"' \
    {} ';'

# Create archives, pretending that the modification time for each newer file is
# that of the most recent commit of any source file.
SOURCE_EPOCH=$(get_commit_time)
TARFLAGS="
    --sort=name --format=posix
    --pax-option=exthdr.name=%d/PaxHeaders/%f
    --pax-option=delete=atime,delete=ctime
    --clamp-mtime --mtime=$SOURCE_EPOCH
    --numeric-owner --owner=0 --group=0
    --mode=go+u,go-w
"
GZIPFLAGS="--no-name --best"

# Prepare releases directory
mkdir -p "$DIST_DIR/$RELEASE_DIR"

cd "$WORK_DIR"
echo "Tarballing $RELEASE_CACHELESS_NAME.tar.gz"
LC_ALL=C tar $TARFLAGS -cf - "$RELEASE_CACHELESS_NAME" |
    gzip $GZIPFLAGS > "$DIST_DIR/$RELEASE_DIR/$RELEASE_CACHELESS_NAME.tar.gz"

cd "$RELEASE_CACHELESS_NAME"
echo "Tarballing $RELEASE_ASSETS_NAME.tar.gz"
LC_ALL=C tar $TARFLAGS -cf - public/build |
    gzip $GZIPFLAGS > "$DIST_DIR/$RELEASE_DIR/$RELEASE_ASSETS_NAME.tar.gz"

# TODO: generate Symfony cache, then bundle as $RELEASE_NAME.tar.gz, if the
# cache ever becomes suitable for distribution. Symfony currently bakes the
# working directory at build-time into the cache, so it is not.

cd "$DIST_DIR/$RELEASE_DIR"
for file in *; do
    echo "Signing $file"
    gpg $GPG_OPTIONS --armor --detach-sign "$file"
done

echo "Generating SHA-512 sums"
sha512sum * > SHA512SUMS
