#!/usr/bin/env bash

# The most important line in every shell script. Basically this says: This
# script should fail once a command invocation itself fails.
set -e

#/ usage: php-build [options] <definition> <prefix>
#/
#/   <definition>       What release should be used, as well as release-specific
#/                      configuration. This is either the name of a builtin
#/                      definition file (as listed with --definitions) or a path
#/                      to the definition file.
#/   <prefix>           All built executables, configurations and libraries are
#/                      placed in this directory. It's created if it doesn't
#/                      exist.
#/
#/   --definitions      Lists all available definitions and exit
#/   -h|--help          Display this help and exit
#/   -i|--ini <env>     php.ini to use. If <env> is a file then this file is
#/                      used, otherwise php.ini-<env> from the source
#/                      distribution is used. Defaults to "production".
#/   -v|--version       Display version information and exit
#/
#
# Usage message done like [shocco](https://github.com/rtomayko/shocco)

# Set the `PHP_BUILD_DEBUG` environment variable to `yes` to trigger the
# `set -x` call, which in turn outputs every issued shell command to `STDOUT`.
if [ -n "$PHP_BUILD_DEBUG" ]; then
    set -x
fi

# Preserve STDERR on FD3, so we can easily log build errors on FD2 to a file and
# use FD3 for php-build's visible error messages.
exec 3<&2

# Simple function for resolving a relative path to an absolute one
function realpath() {
    local path="$1"
    local cwd="$(pwd)"

    if [ -z "$path" ]; then
        echo "realpath: Path is empty" >&3
        return 1
    fi

    while [ -n "$path" ]; do
        cd "${path%/*}"
        local name="${path##*/}"
        path="$(readlink "$name" || true)"
    done

    echo "$(pwd)"
    cd "$cwd"
}

# Common Variables
# ----------------

# This is the path where php-build is installed. This is treated as the base for
# the `share/` and `tmp/` folders of php-build.
PHP_BUILD_ROOT="$(realpath "$0")/.."
TMP="$(dirname $(mktemp --dry-run 2>/dev/null || echo "/var/tmp/tmp_dir"))/php-build"

# This file gets copied to `$PREFIX/etc/php.ini` once the build is complete.
# This is by default the PHP tarball's `php.ini-production`.
PHP_DEFAULT_INI="php.ini-production"

# Initialize the builtin definition path.
[ -z "$PHP_BUILD_DEFINITION_PATH" ] && PHP_BUILD_DEFINITION_PATH="$PHP_BUILD_ROOT/share/php-build/definitions"

# Read the list of arguments for the call to `./configure` in PHP's source.
# These arguments are read from `share/php-build/default_configure_options`.
CONFIGURE_OPTIONS=$(cat "$PHP_BUILD_ROOT/share/php-build/default_configure_options")

# Patches to be applied at the source
PATCH_FILES=""

# Whether do "make clean" after installation or not
[ -z "$PHP_BUILD_KEEP_OBJECT_FILES" ] && PHP_BUILD_KEEP_OBJECT_FILES=off

[ -z "$PHP_BUILD_EXTRA_MAKE_ARGUMENTS" ] && PHP_BUILD_EXTRA_MAKE_ARGUMENTS=""

[ -z "$PHP_BUILD_INSTALL_EXTENSION" ] && PHP_BUILD_INSTALL_EXTENSION=""

[ -z "$PHP_BUILD_CONFIGURE_OPTS" ] && PHP_BUILD_CONFIGURE_OPTS=""

[ -z "$CONFIGURE_OPTS" ] && CONFIGURE_OPTS=""

# Enable Zend Thread Safety by setting this value to "yes"
[ -z "$PHP_BUILD_ZTS_ENABLE" ] && PHP_BUILD_ZTS_ENABLE=off

PHP_BUILD_VERSION="0.11.0dev"

# Error Code to return if a defintion was not found.
E_DEFINITION_NOT_FOUND=127

# Processes the Help front matter of the script and displays it on STDERR
function display_usage() {
    grep '^#/' <"$0" | cut -c4- >&3
}

function display_version() {
    echo "php-build v$PHP_BUILD_VERSION"
}

# Init the directories on first run
function init() {
    if [ ! -d "$TMP" ]; then
        mkdir -p "$TMP"
    fi

    if [ ! -d "$TMP/packages" ]; then
        mkdir "$TMP/packages"
    fi

    if [ ! -d "$TMP/source" ]; then
        mkdir "$TMP/source"
    fi

    if [ ! -d "$PHP_BUILD_ROOT/share/php-build/before-install.d" ]; then
        mkdir -p "$PHP_BUILD_ROOT/share/php-build/before-install.d"
    fi

    if [ ! -d "$PHP_BUILD_ROOT/share/php-build/after-install.d" ]; then
        mkdir -p "$PHP_BUILD_ROOT/share/php-build/after-install.d"
    fi
}

# Credits to Sam Stephenson
function http() {
    local method="$1"
    local url="$2"
    [ -n "$url" ] || return 1

    if type curl &>/dev/null; then
        "http_${method}_curl" "$url"
    elif type wget &>/dev/null; then
        "http_${method}_wget" "$url"
    else
        echo "error: please install \`curl\` or \`wget\` and try again" >&2
        exit 1
    fi
}

function http_head_curl() {
    curl -qsILf "$1"
}

function http_get_curl() {
    curl -qsSLf "$1"
}

function http_head_wget() {
    wget -q --server-response --spider "$1" 2>&1
}

function http_get_wget() {
    wget -nv -O- "$1"
}

# Logs a given log text with a [marker] to STDERR
function log() {
    local marker="$1"
    local text="$2"

    echo "[$marker]: $text" >&3
}

# Uses uname to check if php-build is run on OSX. This is used later on to
# enable specifc fixes for OSX oddnesses in library file placement.
function is_osx {
    local uname=$(uname)

    if [ "$uname" = "Darwin" ]; then
        return 0
    else
        return 1
    fi
}

function osx_major {
    if is_osx ; then
        local osxVersionString=$(sw_vers -productVersion)
        local osx_major=${osxVersionString%%.*}
        echo $osx_major
    fi
    return 0
}

function osx_minor {
    if is_osx ; then
        local osxVersionString=$(sw_vers -productVersion)
        local tmp=${osxVersionString#*.}
        local osx_minor=${tmp%%.*}
        echo $osx_minor
    fi
    return 0
}

# Downloads a PHP Source Tarball and extracts it to `$TMP/source/$DEFINITION`
function download() {
    local url=$1
    local basename=$(download_filename $url)
    local package_file="$TMP/packages/$basename"
    local archive_type=$2
    local temp_package="$TMP/$basename"

    if [ -z $archive_type ]; then
        archive_type=${package_file##*.}
    fi

    if [ -d "$TMP/source/$DEFINITION" ]; then
        log "Skipping" "Already downloaded and extracted $url"
        return
    fi

    log "Downloading" "$url"

    # Remove the temp file if one exists.
    if [ -f "$temp_package" ]; then
        rm "$temp_package"
    fi

    # Do not download a package when it's already downloaded.
    if [ ! -f "$package_file" ]; then
        http get "$url" > "$temp_package"
        cp "$temp_package" "$TMP/packages"
        rm "$temp_package"
    fi

    mkdir "$TMP/source/$DEFINITION"

    "extract_$archive_type" "$package_file" "$TMP/source/$DEFINITION"
}

function download_filename() {
    local url=$1

    # Try to get "filename" in Content-Disposition header; strip quotes and newlines
    local filename=$(http head $url | grep -o -E 'filename=.*$' | sed -e 's/filename=//' -e 's/\"//g' | tr -d '\r\n')

    # If header was empty use basename
    if [ -z $filename ]; then
        filename=$(basename $url)
    fi

    echo $filename
}

function extract_gz() {
    tar -x -z --strip-components 1 -f "$1" -C "$2"
}

function extract_bz2() {
    tar -x -j --strip-components 1 -f "$1" -C "$2"
}

# List all defintions found in $PHP_BUILD_DEFINITION_PATH
function list_definitions() {
    local sed_regex_switch='E'
   
    # sed switch -E is BSD specific and only added to UNIX sed after v4.2
    # fallback to -r if -E does not work
    if ! sed -${sed_regex_switch} '' /dev/null > /dev/null 2>&1; then
        sed_regex_switch='r'
    fi

    ls -1 "${PHP_BUILD_DEFINITION_PATH}/"* |
        xargs -n1 basename |
        sed -${sed_regex_switch} 's,([0-9])([a-z]),\1.\2,g; s,\b([0-9])\b,0\1,g;s,$,~,g' |
        sort |
        sed -${sed_regex_switch} 's,~$,,g; s,\b0([0-9])\b,\1,g; s,\.([a-z]),\1,g'
}

function trigger_before_install() {
    export PHP_BUILD_ROOT
    export PREFIX
    export SOURCE_PATH="$1"

    local triggers_dir="$PHP_BUILD_ROOT/share/php-build/before-install.d/"
    local triggers=$(ls "$triggers_dir")

    if [ -n "$triggers" ]; then
        for trigger in "$triggers_dir"*; do
            log "Before Install Trigger" "$(basename $trigger)"
            /usr/bin/env PATH="$PREFIX/bin:$PATH" "$trigger" 2>&4
        done
    fi
}

function trigger_after_install() {
    export PHP_BUILD_ROOT
    export PREFIX

    after_install "$PREFIX" 2>&4

    local triggers_dir="$PHP_BUILD_ROOT/share/php-build/after-install.d/"
    local triggers=$(ls "$triggers_dir")

    if [ -n "$triggers" ]; then
        for trigger in "$triggers_dir"*; do
            log "After Install Trigger" "$(basename $trigger)"
            /usr/bin/env PATH="$PREFIX/bin:$PATH" "$trigger" 2>&4
        done
    fi
}

# after_install(BUILD_DIR)
#
# Gets called after install has finished.
# Implement this function in your definitions.
function after_install() {
    local stub=1
}

function load_plugins() {
    if [ ! -d "$1" ]; then
        return 1
    fi

    for plugin in "$1/"*.sh
    do
        source $plugin
        log "Info" "Loaded $(basename $plugin .sh) Plugin."
    done
}

function build_package() {
    local source_path=$1
    local cwd="$(pwd)"
    if [ -z $1 ]; then
      local source_path="$TMP/source/$DEFINITION"
    fi

    if [ ! -d "$PREFIX" ]; then
        mkdir -p "$PREFIX"
    fi

    configure_package "$source_path"

    trigger_before_install "$source_path" 2>&4

    apply_patches "$source_path" 2>&4

    log "Compiling" "$source_path"

    cd "$source_path"
    {
        make $PHP_BUILD_EXTRA_MAKE_ARGUMENTS
        make install
        if [ "$PHP_BUILD_KEEP_OBJECT_FILES" == "off" ]; then
            make clean
        fi
    } > /dev/null
    cd "$cwd"

    # Remove .dSYM extension from executables (OSX issue).
    if test -n "$(find $PREFIX/bin -maxdepth 1 -name '*.dSYM' -print -quit)"
    then
        for bin in "$PREFIX/bin/"*.dSYM; do
            mv "$bin" "${bin%*.dSYM}"
        done
    fi

    if [ -n "$PHP_DEFAULT_INI" ]; then
        if [ -f "$source_path/$PHP_DEFAULT_INI" ]; then
            cp "$source_path/$PHP_DEFAULT_INI" "$PREFIX/etc/php.ini"
        else
            if [ -f "$PHP_DEFAULT_INI" ]; then
                cp "$PHP_DEFAULT_INI" "$PREFIX/etc/php.ini"
            fi
        fi
    fi

    # Comment out 'extension_dir' in old default php.ini files (PHP 5.2). In
    # newer ones (>= 5.3) this is already the default.
    if [ -f "$PREFIX/etc/php.ini" ]; then
        sed -i.bak -e 's/^\(extension_dir\)/; \1/g' "$PREFIX/etc/php.ini"
        rm "$PREFIX/etc/php.ini.bak"
    fi
}

# Apply patch files
function apply_patches() {
    local source_path=$1

    if [ -n "$PATCH_FILES" ]; then
        log "Info" "Applying patches: $PATCH_FILES"
        for patch in $PATCH_FILES; do
            patch -d "$source_path" -N -p1 -s < $patch || true
        done
    fi
}

# Install extensions
function install_extensions() {
    # handle extensions that should be installed by defined environment variable
    # variable must be in the format: extension_name=version extension_name=version
    for extension_def in $PHP_BUILD_INSTALL_EXTENSION; do
        local extension=$(echo $extension_def | cut -d"=" -f1)
        local version=$(echo $extension_def | cut -d"=" -f2)
        local first_char=$(echo $version | cut -c1 )

        # if first character of version is an "@" it's meant to be a revision
        if [ $first_char = "@" ]; then
            local version=$(echo $version | cut -c"2-")
            install_extension_source $extension "$version"
        else
            install_extension $extension $version
        fi
    done
}

# Definition commands
# -------------------

# ### install_package
#
# Downloads and builds the PHP tarball from the given URL.
function install_package() {
    local url=$1
    local archive_type=$2

    {
        download $url $archive_type
        cd "$TMP/source/$DEFINITION"
        build_package
        cd - > /dev/null
    } >&4 2>&1
}

# ### configure_option
#
# This function sets and unsets arguments for `configure`. Pass it
# the `-D` option to unset the argument given in `$2`. Otherwise
# the first argument is the name of the option and the second
# argument contains the optional value.
function configure_option() {
    if [ "$1" = "-D" ]; then
        # This variable will contain the filtered arguments.
        local filtered=

        for option in $CONFIGURE_OPTIONS; do
            # If the argument starts with the given string in `$1`
            # then skip it from concatenation. Otherwise add it to
            # the filtered options.
            case "$option" in
                "$2"*) ;;
                *) filtered="$filtered $option";;
            esac
        done

        # Trim the leading whitespace added in the concatenation.
        filtered=$(echo "$filtered" | sed -e 's/[ ]*//')

        # Then replace the old argument list with the new one.
        CONFIGURE_OPTIONS="$filtered"
        return 0
    else
        if [ "$1" = "-R" ]; then
            configure_option -D "$2"
            configure_option "$2" "$3"
            return 0
        fi
    fi

    CONFIGURE_OPTIONS="$CONFIGURE_OPTIONS $1"

    if [ -n "$2" ]; then
        CONFIGURE_OPTIONS="$CONFIGURE_OPTIONS=$2"
    fi
}

# ### patch_file
#
# Add a patch to internal patch list
function patch_file() {
    local patches_dir="$PHP_BUILD_ROOT/share/php-build/patches"
    local patch="$patches_dir/$1"

    if [ -f "$1" ]; then
        local patch="$1"
    fi

    if [ -n "$PATCH_FILES" ]; then
        PATCH_FILES="$PATCH_FILES $patch"
    else
        PATCH_FILES="$patch"
    fi
}

# ### with_openssl
#
# Configures PHP with OpenSSL support.
#
# This is left in for backwards compatibility with
# definitions which were written before `--with-openssl`
# was in the list of default configure arguments.
function with_openssl() {
    stub=1
}

function with_apxs2() {
    local apxs="$1"
    shift

    if [ -z "$apxs" ]; then
        apxs="$PHP_BUILD_APXS"
    fi
    PHP_BUILD_APXS="$apxs"

    configure_option "--with-apxs2" "$apxs"
}

# Configure Stage
# ---------------

# This is invoked by `build_package` and is used to
# build PHP with the arguments in `$CONFIGURE_OPTIONS`.
#
function configure_package() {
    local source_path=$1
    local backup_pwd=$(pwd)

    cd "$source_path"

    if [ ! -f ./configure ]; then
        ./buildconf
    fi

    # Mac OSX stores some libraries (for example `libpng`)
    # in `/usr/X11/lib` instead of `/usr/lib`.
    #
    # This currently builds PHP without the `gettext` and `readline`
    # extensions, as I've currently not got them to work on my machine.
    if is_osx; then
        configure_option -D "--with-gettext"
        configure_option -D "--with-readline"
        configure_option "--with-libedit"

        configure_option -R "--with-png-dir" "/usr/X11"
    fi

    if [ "$PHP_BUILD_ZTS_ENABLE" == "on" ]; then
        configure_option "--enable-maintainer-zts"
        log "Warning" "Enabling Zend Thread Safety is meant only for maintainers!"
    fi

    # Override `--with-openssl` and `--with-libxml-dir`
    # if Homebrew's openssl and libxml2 exist on Mac OS X 10.11+.
    if is_osx && [ "$(osx_major)" -eq 10 ] && [ "$(osx_minor)" -ge 11 ] && [ -n "$(which brew)" ] && [ -e "$(brew --prefix openssl)" ] && [ -e "$(brew --prefix libxml2)" ]; then
        configure_option -R "--with-openssl" "$(brew --prefix openssl)"
        configure_option -R "--with-libxml-dir" "$(brew --prefix libxml2)"
    fi

    CONFIGURE_OPTIONS="$CONFIGURE_OPTIONS $PHP_BUILD_CONFIGURE_OPTS $CONFIGURE_OPTS"

    # Add the config-file-path, config-file-scan-dir aswell as the
    # prefix to the build options, these cannot be changed by definitions.
    local argv="--with-config-file-path="$PREFIX/etc" \
--with-config-file-scan-dir="$PREFIX/etc/conf.d" \
--prefix=$PREFIX \
--libexecdir=$PREFIX/libexec \
$CONFIGURE_OPTIONS"

    log "Preparing" "$source_path"

    if [ ! -d "$PREFIX/etc/conf.d" ]; then
        mkdir -p "$PREFIX/etc/conf.d"
    fi

    if [ ! -d "$PREFIX/libexec" ]; then
        mkdir -p "$PREFIX/libexec"
    fi

    # Set the lib dir to `lib64` on **x86_64**
    # systems.
    local append_default_libdir='yes'
    for option in $CONFIGURE_OPTIONS; do
      case "$option" in
        "--with-libdir"*) append_default_libdir='no' ;;
      esac
    done
    if [ "$(uname -p)" = "x86_64" ] && [ "${append_default_libdir}" = 'yes' ]; then
        argv="$argv --with-libdir=lib64"
    fi

    # Avoid installing PHP binary as "php.dSYM" on MacOSX 10.7 and 10.8.
    # PHP 5.2 and 5.3 has the problem.
    # See https://github.com/php/php-src/pull/135
    if is_osx; then
        export ac_cv_exeext=''
    fi

    ./configure $argv > /dev/null

    # Use php-build prefix for the Apache libexec folder
    if [ -n "$PHP_BUILD_APXS" ]; then
        apxs_libexecdir=$($PHP_BUILD_APXS -q LIBEXECDIR)
        sed -i"" -e "s|'\$(INSTALL_ROOT)${apxs_libexecdir}'|${PREFIX}${apxs_libexecdir}|g" ${source_path}/Makefile
    fi

    cd "$backup_pwd"
}

# Handles build errors, and displays the last 10 lines of the build log
function build_error() {
    {
        echo
        echo "-----------------"
        echo "|  BUILD ERROR  |"
        echo "-----------------"
        echo
        echo "Here are the last 10 lines from the log:"
        echo
        echo "-----------------------------------------"
        echo "$(tail -n 10 "$LOG_PATH")"
        echo "-----------------------------------------"
        echo
        echo "The full Log is available at '${LOG_PATH}'."
    } >&3

    # Removes the prefix when the build fails.
    if [ -z "$PHP_BUILD_DEBUG" ]; then
        cleanup_abort > /dev/null
    fi
}

function cleanup_abort() {
    log "Warn" "Aborting build."

    make -C "$TMP/source/$DEFINITION" clean &> /dev/null
}

function find_definition() {
    local definition="$1"

    # Check if the supplied argument is an existing file itself,
    # to use definitions outside of the builtin definition path.
    if [ -f "$definition" ]; then
        echo "$definition"
        return 0
    fi

    if [ -f "$PHP_BUILD_DEFINITION_PATH/$definition" ]; then
        echo "$PHP_BUILD_DEFINITION_PATH/$definition"
        return 0
    else
        return 1
    fi
}

function enable_builtin_opcache {
    log Info "Enabling Opcache..."

    local ext_dir=$("$PREFIX/bin/php-config" --extension-dir)

    if [ ! -f "$ext_dir/opcache.so" ]; then
        log Info "Aborting: opcache.so not found"
        return 0
    fi

    echo "zend_extension=$ext_dir/opcache.so" >> "$PREFIX/etc/php.ini"
    log Info "Done"
}

# Here the magic begins
# ---------------------

# Display the Usage message if no arguments are given.
if [ -z $1 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
    display_usage
    exit
fi

if [ "$1" = "-v" ] || [ "$1" = "--version" ]; then
    display_version
    exit
fi

if [ "$1" = "--definitions" ]; then
    list_definitions
    exit
fi

# Set up the directories needed for the source and the downloaded packages
init

# If `-i` or `--ini` is given as first argument, then treat the second argument
# as `php.ini` file.
if [ "$1" = "-i" ] || [ "$1" = "--ini" ]; then
    # If an existing path is passed (and the path is a file) then use this file,
    # otherwise use `php.ini-<value>` from the tarball.
    if [ -f "$2" ]; then
        PHP_DEFAULT_INI="$2"
    else
        PHP_DEFAULT_INI="php.ini-$2"
    fi
    shift
    shift
fi

# This is the name of the definition we want to build.
DEFINITION=$1

# The built PHP version is placed in this directory.
PREFIX=$2

if [ -z "$PREFIX" ]; then
    display_usage
    exit 1
fi

if ! find_definition "$DEFINITION" > /dev/null; then
    log Error "Definition $DEFINITION not found."
    exit $E_DEFINITION_NOT_FOUND
fi

DEFINITION_PATH="$(find_definition "$DEFINITION")"
DEFINITION="$(basename "$DEFINITION_PATH")"
LOG_NAME="$(basename "$DEFINITION_PATH")"
# Generate the Path for the build log.
TIME="$(date "+%Y%m%d%H%M%S")"
LOG_PATH="/tmp/php-build.$LOG_NAME.$TIME.log"

# Redirect everything logged to STDERR (except messages by php-build itself)
# to the Log file.
exec 4<> "$LOG_PATH"

# Load extension plugin
source "$PHP_BUILD_ROOT/share/php-build/extension/extension.sh"
log Info "Loaded extension plugin"

# Load all definition plugins. Plugins register functions
# for use whithin definitions. See the xdebug plugin examples.
load_plugins "$PHP_BUILD_ROOT/share/php-build/plugins.d"

log Info "$PHP_DEFAULT_INI gets used as php.ini"
log Info "Building $DEFINITION into $PREFIX"

# Handle script termination with.
trap cleanup_abort SIGINT SIGTERM

# Handle Script Errors.
trap build_error ERR EXIT

# Source the definition file
source "$DEFINITION_PATH"

# Run executables placed in `share/php-build/after-install.d`
trigger_after_install 2>&4

# Installed extensions defined by environment variable
install_extensions 2>&4

# Unbind the error handler.
trap - ERR
trap - EXIT

# Display a notice if build warnings got logged.
if [ -n $LOG_PATH ]; then
    log "Info" "The Log File is not empty, but the Build did not fail.\
 Maybe just warnings got logged.\
 You can review the log in $LOG_PATH"
fi

log "Success" "Built $DEFINITION successfully."

trap - SIGINT
trap - SIGTERM

