#! /bin/bash

# Copyright (C) 2021 Charles Atkinson
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA

# Usage: run with root privilege in the root of the bung git tree.

# Purpose:
#   build bung binary .deb

# Usage:
#   * Must be run with root privilege in the root of the bung_packaging git tree
#   * Group logcheck must exist (commonly created by installing package logcheck)
#   * Options: see function usage or use -h option.

# Output: .deb in /root

# Programmers' notes: function call tree
#    +
#    |
#    +-- initialise
#    |   |
#    |   +-- usage
#    |
#    +-- build_deb
#    |
#    +-- finalise
#
# Utility functions called from various places:
#    ck_file msg

# Function definitions in alphabetical order.  Execution begins after the last function definition.

#--------------------------
# Name: build_deb
# Purpose: builds the .deb
#--------------------------
function build_deb {
    local buf cmd msg rc
    local data_dir build_bung_dir control_dir

    # Create directories for build
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    msg I 'Creating directories for build'
    data_dir=$tmp_dir/data
    mkdir -p $data_dir/etc/{cron.weekly,logcheck/ignore.d.server}
    mkdir -p $data_dir/usr/lib/bung
    mkdir -p $data_dir/usr/bin
    mkdir -p $data_dir/usr/share/doc/bung
    mkdir -p $data_dir/usr/share/man/man{5,8}
    control_dir=$tmp_dir/control
    mkdir -p "$control_dir"
    extracted_tarball_dir=$tmp_dir/extracted_tarball
    mkdir -p "$extracted_tarball_dir"
    msg I "$(tree -d --noreport "$tmp_dir")"

    # Populate the temporary directory root
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    msg I "Populating the temporary directory root, $tmp_dir"
    cp -p apt/binary-deb/debian-binary "$tmp_dir" || finalise 1

    # Extract the tarball and move files to Debian compliant locations
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    msg I "Extracting the tarball to $extracted_tarball_dir"
    cmd=(tar --extract --file "$tarball" --directory=$extracted_tarball_dir)
    buf=$("${cmd[@]}" 2>&1)
    rc=$?
    ((rc!=0)) || [[ $buf != '' ]] \
        && msg E "Command: ${cmd[*]}${msg_lf}Return code: $rc${msg_lf}Out: $buf"
    msg I "$(tree -d --noreport $extracted_tarball_dir)"

    msg I "Moving files extracted from the tarball to Debian compliant locations under $data_dir"
    mv $extracted_tarball_dir/lib/* $data_dir/usr/lib/bung/ || finalise 1
    mv $extracted_tarball_dir/share/cron/cron.weekly $data_dir/etc/cron.weekly/bung || finalise 1
    mv $extracted_tarball_dir/share/doc $data_dir/usr/share/doc/bung || finalise 1
    mv $extracted_tarball_dir/share/logcheck/* $data_dir/etc/logcheck/ignore.d.server || finalise 1
    mv $extracted_tarball_dir/share/man $data_dir/usr/share/man || finalise 1
    mv $extracted_tarball_dir/*.sh $data_dir/usr/bin || finalise 1

    # Change non-Debian compliant directory default envars
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    for f in $data_dir/usr/bin/*.sh $data_dir/usr/lib/bung/initialise_1.scrippet
    do
        mtime=$(stat -c %Y $f)
        sed -i 's|BUNG_BIN_DIR:-/opt/bung|BUNG_BIN_DIR:-/usr/bin|' $f
        sed -i 's|BUNG_ETC_DIR:-/etc/opt/bung|BUNG_ETC_DIR:-/etc/bung|' $f
        sed -i 's|BUNG_LIB_DIR:-/opt/bung/lib|BUNG_LIB_DIR:-/usr/lib/bung|' $f
        touch -d @$mtime -m $f 
    done

    # Populate the control directory
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    msg I "Populating the control directory, $control_dir"
    #msg I "DEVEL: apt/binary-deb/control/control: $(cat apt/binary-deb/control/control)"
    cp -p apt/binary-deb/control/{control,postinst,postrm} "$control_dir" || finalise 1
    sed -i "s/%bung_version%/$bung_version/g" "$control_dir"/{control,postinst} || finalise 1
    sed -i "s/%build_version%/$build_version/g" "$control_dir"/control || finalise 1
    #msg I "DEVEL: $control_dir/control: $(cat "$control_dir"/control)"
    #msg I "DEVEL: $control_dir/postinst: $(cat "$control_dir"/postinst)"
    (
        cd $data_dir \
            && find -type f -exec md5sum {} + \
            | sort --key=2 \
            | sed 's|  \./|  |' 
    ) > "$control_dir/md5sums" || finalise 1
    (($?!=0)) && msg E 'Aborting'
    msg I "$(tree --noreport "$control_dir")"

    # Set ownerships and permissions
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    msg I 'Setting ownerships and permissions'
    chown -R root:root "$tmp_dir" \
        && find "$data_dir" -type f -execdir chmod 644 {} \+ \
        && find "$data_dir" -type d -execdir chmod 755 {} \+ \
        && find "$data_dir" -type f -name '*.sh' -execdir chmod 755 {} \+ \
        && chmod 744 $data_dir/etc/cron.weekly/bung \
        && chown -R root:logcheck $data_dir/etc/logcheck/ignore.d.server \
        && chmod 744 "$control_dir"/{postinst,postrm}
    (($?!=0)) && msg E 'Aborting'

    # Create the .deb
    # ~~~~~~~~~~~~~~~
    msg I "Creating $deb"
    tar -czf $tmp_dir/control.tar.gz --directory "$control_dir" control md5sums postinst postrm \
        && tar -czf $tmp_dir/data.tar.gz --directory $data_dir . \
        && (
            cd $tmp_dir \
                && ar rc "$deb" debian-binary control.tar.gz data.tar.gz
           )
    (($?!=0)) && msg E 'Aborting'
    msg I $'Created the .deb: \n'"$(ls -l "$deb")"

}  # end of function build_deb

#--------------------------
# Name: ck_file
# Purpose: for each file listed in the argument list: checks that it is 
#   * reachable and exists
#   * is of the type specified (block special, ordinary file or directory)
#   * has the requested permission(s) for the user
#   * optionally, is absolute (begins with /)
# Usage: ck_file [ path <file_type>:<permissions>[:[a]] ] ...
#   where 
#     file  is a file name (path)
#     file_type  is b (block special file), f (file) or d (directory)
#     permissions  is none or more of r, w and x
#     a  requests an absoluteness test (that the path begins with /)
#   Example:
#     buf=$(ck_file foo f:rw 2>&1)
#     if [[ $buf != '' ]]; then
#          msg W "$buf"
#          return 1
#     fi
# Outputs:
#   * For the first requested property each file does not have, a message to
#     stderr
#   * For the first detected programminng error, a message to
#     stderr
# Returns: 
#   0 when all files have the requested properties
#   1 when at least one of the files have the requested properties
#   2 when a programming error is detected
#--------------------------
function ck_file {

    local absolute_flag buf file_name file_type perm perms retval

    # For each file ...
    # ~~~~~~~~~~~~~~~~~
    retval=0
    while [[ $# -gt 0 ]]
    do
        file_name=$1
        file_type=${2%%:*}
        buf=${2#$file_type:}
        perms=${buf%%:*}
        absolute=${buf#$perms:}
        [[ $absolute = $buf ]] && absolute=
        case $absolute in
            '' | a )
                ;;
            * )
                echo "ck_file: invalid absoluteness flag in '$2' specified for file '$file_name'" >&2
                return 2
        esac
        shift 2

        # Is the file reachable and does it exist?
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        case $file_type in
            b )
                if [[ ! -b $file_name ]]; then
                    echo "file '$file_name' is unreachable, does not exist or is not a block special file" >&2
                    retval=1
                    continue
                fi
                ;;
            f )
                if [[ ! -f $file_name ]]; then
                    echo "file '$file_name' is unreachable, does not exist or is not an ordinary file" >&2
                    retval=1
                    continue
                fi
                ;;
            d )
                if [[ ! -d $file_name ]]; then
                    echo "directory '$file_name' is unreachable, does not exist or is not a directory" >&2
                    retval=1
                    continue
                fi
                ;;
            * )
                echo "Programming error: ck_file: invalid file type '$file_type' specified for file '$file_name'" >&2
                return 2
        esac

        # Does the file have the requested permissions?
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        buf="$perms"
        while [[ $buf ]]
        do
            perm="${buf:0:1}"
            buf="${buf:1}"
            case $perm in
                r )
                    if [[ ! -r $file_name ]]; then
                        echo "$file_name: no read permission" >&2
                        retval=1
                        continue
                    fi
                    ;;
                w )
                    if [[ ! -w $file_name ]]; then
                        echo "$file_name: no write permission" >&2
                        retval=1
                        continue
                    fi
                    ;;
                x )
                    if [[ ! -x $file_name ]]; then
                        echo "$file_name: no execute permission" >&2
                        retval=1
                        continue
                    fi
                    ;;
                * )
                    echo "Programming error: ck_file: invalid permisssion '$perm' requested for file '$file_name'" >&2
                    return 2
            esac
        done

        # Does the file have the requested absoluteness?
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        if [[ $absolute = a && ${file_name:0:1} != / ]]; then
            echo "$file_name: does not begin with /" >&2
            retval=1
        fi

    done

    return $retval

}  #  end of function ck_file

#--------------------------
# Name: finalise
# Purpose: cleans up and exits
# Arguments:
#    $1  exit value
#--------------------------
function finalise {
    local buf msg
    local -r my_retval=${1:-1}

    finalising_flag=$true
    
    # Interrupted?  Message and exit return value
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    buf=
    case $1 in
        130 )
            buf=SIGINT
            ;;
        131 )
            buf=SIGQUIT
    esac
    if [[ $buf != '' ]]; then
        msg="Finalising on $buf"
        msg E "$msg"    # Returns because finalising_flag is set
    fi

    # Temporary directory
    # ~~~~~~~~~~~~~~~~~~~
    # tmp_dir_regex guards against dangerous rm -fr
    local -r tmp_dir_regex="^/tmp/${my_name:-UNSET_GUARD}"
    if [[ $tmp_dir_created_flag ]]; then
        if [[ ! $keep_tmp_dir_flag ]]; then
            if [[ ${tmp_dir:-} =~ $tmp_dir_regex ]]; then
                msg I "Removing temporary directory $tmp_dir"
                rm -fr "$tmp_dir"
            else
                msg="Not removing temporary directory $tmp_dir"
                msg+=" (name does not match $tmp_dir_regex)"
                msg E "$msg"
            fi
        else
            msg I "Kept temporary directory $tmp_dir"
        fi
    fi

    # Exit
    # ~~~~
    exit $my_retval
}  # end of function finalise

#--------------------------
# Name: initialise
# Purpose: sets up environment and parses command line
#--------------------------
function initialise {
    local args buf emsg opt
    local opt_t_flag opt_v_flag opt_V_flag

    # Configure shell environment
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~
    export PATH=/usr/local/bin:/usr/sbin:/sbin:/usr/bin:/bin
    IFS=$' \n\t'
    set -o nounset
    shopt -s extglob            # Enable extended pattern matching operators
    trap 'finalise 130' 'INT'
    trap 'finalise 131' 'QUIT'
    umask 022
    unset CDPATH                # Ensure cd behaves as expected

    # Global logic variables
    # ~~~~~~~~~~~~~~~~~~~~~~
    readonly false=
    readonly true=true
    
    debugging_flag=$false
    finalising_flag=$false
    interrupt_flag=$false
    tmp_dir_created_flag=$false
    keep_tmp_dir_flag=$false

    # Global string variables
    # ~~~~~~~~~~~~~~~~~~~~~~~
    my_name=${0##*/}
    msg_lf=$'\n    '
    tmp_dir_mask=/tmp/$my_name.XXXXXX

    # Ensure effective userid is 0 (root)
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    emsg=
    ((EUID!=0)) && emsg+=$'\nMust be run with effective userid 0 (root)'

    # Ensure current directory is a git tree
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    [[ -d .git ]] && emsg+=$'\nMust be run in root of bung git tree'

    # Parse command line
    # ~~~~~~~~~~~~~~~~~~
    args=("$@")
    opt_t_flag=$false
    opt_v_flag=$false
    opt_V_flag=$false
    emsg=
    while getopts :dhkt:V:v: opt "$@"
    do
        case $opt in
            d )
                debugging_flag=$true
                ;;
            h )
                debugging_flag=$false
                usage verbose
                exit 0
                ;;
            k )
                keep_tmp_dir_flag=$true
                ;;
            t )
                opt_t_flag=$true
                tarball=$OPTARG
                ;;
            V )
                opt_V_flag=$true
                bung_version=$OPTARG
                ;;
            v )
                opt_v_flag=$true
                build_version=$OPTARG
                ;;
            * )
                emsg+=$'\n'"Invalid option '-$OPTARG'"
        esac
    done

    # Test for required options not set
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    [[ ! $opt_t_flag ]] && emsg+=$msg_lf'-t option is required'
    [[ ! $opt_v_flag ]] && emsg+=$msg_lf'-v option is required'
    [[ ! $opt_V_flag ]] && emsg+=$msg_lf'-V option is required'

    # Check option arguments
    # ~~~~~~~~~~~~~~~~~~~~~~
    if [[ $opt_t_flag ]]; then
        buf=$(ck_file "$tarball" f:r: 2>&1)
        [[ $buf != '' ]] && emsg+=$'\n'"$buf"
    fi  

    # Test for mutually exclusive options
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # There are no mutually exclusive options

    # Test for extra arguments
    # ~~~~~~~~~~~~~~~~~~~~~~~~
    shift $(($OPTIND-1))
    if [[ $* != '' ]]; then
        emsg+=$msg_lf"Invalid extra argument(s) '$*'"
    fi

    # Report any errors
    # ~~~~~~~~~~~~~~~~~
    [[ $emsg != '' ]] && msg E "Errors:$emsg"

    # Derive values from options
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~
    deb=/root/bung_$bung_version-$build_version.deb

    # Create temporary directory
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~
    buf=$(mktemp -d "$tmp_dir_mask" 2>&1)
    if (($?==0)); then 
        tmp_dir=$buf
        tmp_dir_created_flag=$true
        chmod 700 "$tmp_dir" || exit 1
        msg D "Created temporary directory $tmp_dir"
    else
        msg E "Unable to create temporary directory:$buf"
    fi

}  # end of function initialise

#--------------------------
# Name: msg
# Purpose: generalised messaging interface
# Arguments:
#    $1 class: D, E, I or W indicating Debug, Error, Information or Warning
#    $2 message text
# Global variables read:
#     conf_name
#     script_name
# Output: information messages to stdout; the rest to stderr
# Returns: 
#   Does not return (calls finalise) when class is E for error
#   Otherwise returns 0
#--------------------------
function msg {
    local buf class message_text prefix

    # Process arguments
    # ~~~~~~~~~~~~~~~~~
    class="${1:-}"
    message_text="${2:-}"

    # Class-dependent set-up
    # ~~~~~~~~~~~~~~~~~~~~~~
    case "$class" in  
        D ) 
            [[ ! $debugging_flag ]] && return
            prefix='DEBUG: '
            ;;  
        E ) 
            error_flag=$true
            prefix='ERROR: '
            ;;  
        I ) 
            prefix=
            ;;  
        W ) 
            warning_flag=$true
            prefix='WARN: '
            ;;  
        * ) 
            msg E "msg: invalid class '$class': '$*'"
    esac

    # Write to stdout or stderr
    # ~~~~~~~~~~~~~~~~~~~~~~~~~
    message_text="$prefix$message_text"
    if [[ $class = I ]]; then
        echo "$message_text"
    else
        echo "$message_text" >&2
        if [[ $class = E ]]; then
            [[ ! $finalising_flag ]] && finalise 1 
        fi
    fi  

    return 0
}  #  end of function msg

#--------------------------
# Name: usage
# Purpose: prints usage message
#--------------------------
function usage {
    local msg usage

    # Build the messages
    # ~~~~~~~~~~~~~~~~~~
    usage="usage: $my_name "
    msg='  where:'
    usage+='[-d] [-h] [-k] -t <tarball> -V <bung_version> -v <build_version>'
    msg+=$'\n    -d debugging on'
    msg+=$'\n    -h prints this help and exits'
    msg+=$'\n    -k keep the temporary directory'
    msg+=$'\n    -t specifies the tarball'
    msg+=$'\n       Example: /root/Downloads/bung-2.0.2.tgz'
    msg+=$'\n    -V specifies the bung version'
    msg+=$'\n       Example: 2.0.2'
    msg+=$'\n    -v specifies the .deb build version'
    msg+=$'\n       Example: 3'

    # Display the message(s)
    # ~~~~~~~~~~~~~~~~~~~~~~
    echo "$usage" >&2
    if [[ ${1:-} != 'verbose' ]]; then
        echo "(use -h for help)" >&2
    else
        echo "$msg" >&2
    fi

}  # end of function usage

#--------------------------
# Name: main
# Purpose: where it all happens
#--------------------------
initialise "${@:-}"
build_deb
finalise 0
