#! /bin/bash
#  This program is part of the Qi package manager
#
#  Copyright (C) 2015, 2016  Matias A. Fonzo
#
#  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, see <http://www.gnu.org/licenses/>.

# EXIT STATUS
# 0   = Successful completion
# 1   = Minor common errors (e.g: help usage, support not available)
# 2   = Command execution error
# 3   = Integrity check error for compressed files
# 4   = File empty, not regular, or expected
# 5   = Empty or not defined variable
# 10  = Network manager error

# Override locale settings
export LC_ALL=C

# Functions
show_help() {
  printf '%s\n' \
   "Usage: qi build [options] recipe(s)"                                 \
   "Build packages using recipe files."                                  \
   ""                                                                    \
   "When recipe is -, read standard input."                              \
   ""                                                                    \
   "Build command options:"                                              \
   "  -h          display this help and exit"                            \
   "  -A          avoid automatic error detection"                       \
   "  -C          create named pipe for communicate package name"        \
   "  -N          don't read runtime configuration file"                 \
   "  -S          don't strip unneeded or debugging symbols from files"  \
   "  -k          keep (don't delete) srcdir and destdir"                \
   "  -n          don't create a .tlz package"                           \
   "  -o          where the produced packages are written"               \
   "  -t          temporary directory for compilation"                   \
   "  -w          working directory tree for archive, patches, recipes"  \
   "  -z          where to find the compressed sources"                  \
   "  -a          architecture to use"                                   \
   "  -i          increment release number when a package is produced"   \
   "  -j          parallel jobs for the compiler"                        \
   ""
}

# Directory of functions
readonly fndir="${LIBEXECDIR}/functions"

# Load (externally) internal functions
source "${fndir}"/helpers	|| exit 1
source "${fndir}"/readconfig	|| exit 1

unpack() {
  local file tool_verify tool_extract

  for file in "${tardir}/${@##*/}" ; do
    if [[ ! -f $file ]] ; then
      quit 2 "qi: build: Cannot unpack '${file}': File not found or not regular."
    fi

    # Assign an appropriate tool for options according to the file extension
    case "$file" in
      *.tar.*|*.tar)
        tool_verify="tar -tf"
        tool_extract="tar -xf"
        ;;
      *.zip|*.ZIP)
        tool_verify="unzip -t"
        tool_extract=unzip
        ;;
      *)
        quit 1 "qi: build: ${file}: Unsupported file format, extension '${file#*.}'.";
    esac

    echo "==> Checking SHA1 sums ..."
    if [[ -f ${file}.sha1 ]] ; then
      eval_cmd "( cd "$tardir" && sha1sum -c "${file}.sha1" )"
    fi

    echo "==> Checking integrity of '${file}' ..."
    eval_cmd 3 "$tool_verify "$file" > /dev/null"

    # Generate a check sum or update it according to the timestamp of the source
    if [[ ! -e ${file}.sha1 || $file -nt ${file}.sha1 ]] ; then
      echo "==> SHA1: Creating or updating '${file}.sha1' ..."
      eval_cmd "( cd "$tardir" && sha1sum "${file##*/}" > "${file}.sha1" )"
    fi

    echo "==> Unpacking source '${file}' ..."
    eval_cmd 3 "$tool_extract "$file""
  done
}

# Default variable values
avoid_errors=noavoid_errors
create_pipe=nocreate_pipe
rcfile=rcfile
strip=strip
keepdir=nokeepdir
nopkg=no_nopkg
outdir=/var/cache/qi/packages
tmpdir=/tmp/sources
worktree=/usr/src/qi
tardir=${worktree}/sources
arch="$(uname -m)"
increment_release=noincrement_release
jobs=1
netget="wget --continue --wait=1 --tries=3 --no-check-certificate --passive-ftp"
rsync="rsync --verbose --archive --copy-links --compress --progress --itemize-changes"
compress_pages=compress_pages

# Handle command-line options.
#
# An indexed array is created when an option is set.  It helps to the
# command line in order to take precedence over the config file

while getopts ":hACNSkni o:t:w:z:a:j:" option ; do
  case "$option" in
    (k)
      keepdir=keepdir
      ;;
    (A)
      avoid_errors=avoid_errors
      ;;
    (C)
      create_pipe=create_pipe
      ;;
    (N)
      rcfile=no_rcfile
      ;;
    (S)
      strip=nostrip
      cmdLineSets+=( strip )
      ;;
    (n)
      nopkg=nopkg
      ;;
    (o)
      outdir="$OPTARG"
      cmdLineSets+=( outdir )
      ;;
    (t)
      tmpdir="$OPTARG"
      cmdLineSets+=( tmpdir )
      ;;
    (w)
      worktree="$OPTARG"
      cmdLineSets+=( worktree )
      ;;
    (z)
      tardir="$OPTARG"
      cmdLineSets+=( tardir )
      ;;
    (a)
      arch="$OPTARG"
      cmdLineSets+=( arch )
      ;;
    (i)
      increment_release=increment_release
      ;;
    (j)
      jobs="$OPTARG"
      cmdLineSets+=( jobs )
      ;;
    (h)
      show_help
      exit 0
      ;;
     :)
      quit 1 \
       "The option '-${OPTARG}' requires an argument" \
       "Usage: qi build [-hACNSkni] [-otwz <dir>] [-a <arch>] [-j <jobs>] [recipe ...]"
      ;;
    \?)
      quit 1 \
       "qi: build: illegal option -- '-${OPTARG}'" \
       "Usage: qi build [-hACNSkni] [-otwz <dir>] [-a <arch>] [-j <jobs>] [recipe ...]"
      ;;
  esac
done
shift "$((OPTIND - 1))"

# If there are no arguments, one recipe is required
if (( $# == 0 )) ; then
  quit 4 \
   "\nqi: build: You must specify at least one recipe file." \
   "Try 'qi build -h' for more information."
fi

# To mark some variables.  Redefinitions of this variables
# are not allowed from the config, or from the recipes
readonly create_pipe nopkg keepdir increment_release

# Read configuration file
if [[ $rcfile = rcfile ]] ; then
  # Check existence, from where to read
  if [[ -e ~/.qirc ]] ; then
    configFrom="~/.qirc"		# $HOME.
  elif [[ -e @SYSCONFDIR@/qirc ]] ; then
    configFrom="@SYSCONFDIR@/qirc"	# System-wide.
  fi

  # Check file integrity, content
  if [[ -f $configFrom && -s $configFrom ]] ; then
    echo "Processing '${configFrom}' ..."
    readConfig < "$configFrom"
  else
    warn "WARNING: ${configFrom}: File empty or not regular."
  fi
fi

# Unset from memory unnecessary variables, arrays, functions
unset rcfile configFrom cmdLineSets readConfig

# Set default mask
umask 022

# Create required directories (if needed)
eval_cmd "mkdir -p "${worktree}"/{archive,patches,recipes} "$tardir""

# Read from the standard input if '-' was given
if [[ $1 = - ]] ; then
  set --	# Unset positional parameters, setting '#' to zero.
  while IFS="" read -r filename ; do
    set -- "$@" "$filename"
  done
fi

# We prepare to export specific variables that will serve to restore
# their values when one stops processing a recipe and place is given
# to the following one
export avoid_errors arch jobs strip

# Create a Private Randomized Directory
PRD="qi-build-${USER}-$RANDOM$$"

eval_cmd "mkdir -p -m 700 "${tmpdir}"/$PRD"

# To remove directory and pipe when it leaves
trap 'eval_cmd "rm -rf "${tmpdir}"/$PRD" /var/tmp/qi-pipe' EXIT

# Save all exported variables
export -p > "${tmpdir}"/${PRD}/default_env$$

# Mark some variables as read only.
#
# This allows the use without modification on the recipes
readonly outdir tmpdir worktree tardir netget rsync PRD

# Main loop
for recipe ; do
  if [[ ! -f $recipe ]] ; then
    quit 4 "qi: build: File '${recipe}' not found or not regular."
  fi

  # Save the current working directory.  We will go back here later.
  CWD="$PWD"

  echo "==> Loading \"${recipe}\" from \`${CWD}' ..."

  # Mark all the variables for export
  set -o allexport

  # Include recipe
  source "$recipe"

  # Sanity check: checking required variables
  if [[ -z $program ]] ; then
    quit 5 "qi: build: 'program' is not defined."
  fi
  if [[ -z $version ]] ; then
    quit 5 "qi: build: 'version' is not defined."
  fi
  if [[ -z $release ]] ; then
    quit 5 "qi: build: 'release' is not defined."
  fi
  if [[ $increment_release = increment_release ]] ; then
    release="$((release + 1))"
  fi
  if [[ -z $tardir ]] ; then
    quit 5 "qi: build: 'tardir' is not defined."
  fi
  # Allow the dot as the current working directory
  if [[ $tardir = . ]] ; then
    tardir="$CWD"
  fi
  if [[ -z $tarname ]] ; then
    quit 5 "qi: build: 'tarname' is not defined."
  fi

  shopt -s extglob
  if [[ $jobs != +([0-9]) ]] ; then
    quit 5 "qi: build: '$jobs' needs to be a valid number."
  fi
  shopt -u extglob	# Deactivate shell option.

  # Check if build() is present
  if [[ $(command -v build) != build ]] ; then
    quit 1 "qi: build: The main function build() is not present."
  fi

  # Use or assign default values for variables
  srcdir="${srcdir:-$program-$version}"
  destdir="${destdir:-${tmpdir}/package-$program}"
  arch="${arch:=$(uname -m)}"
  case "$arch" in
    x86_64)
      export libSuffix=64
      ;;
  esac
  infodir="${infodir:-/usr/share/info}"
  mandir="${mandir:-/usr/share/man}"
  docdir="${docdir:-/usr/share/doc/${program}-${version}}"
  pkgname="${pkgname:-$program}"
  full_pkgname="${pkgname}-${version}-${arch}+${release}"
  full_tarname="${tardir}/$tarname"

  echo "==> Preparing temporary directories ..."
  rmIfDir "${tmpdir}/${srcdir}" "$destdir"

  echo "==> Creating required directories for the package ..."
  eval_cmd "mkdir -p "$outdir" "${destdir}"/var/lib/qi/recipes"

  # Fetch remote sources
  echo "==> Fetching remote source(s) if they do not exist locally ..."
  if (( ${#fetch[@]} > 0 )) ; then
    for address in "${fetch[@]}" ; do
      file_name="${address##*/}"

      if [[ ! -r ${tardir}/$file_name ]] ; then
        warn "Source file '${file_name}' not found in \`${tardir}'."
        echo "==> Obtaining source from ${address%/*} ..."
        case "$address" in
          rsync://*)
            eval_cmd "cd "$tardir""
            eval_cmd 10 ""${rsync//[\"\']/}" "$address""
            ;;
          *://*)
            eval_cmd "cd "$tardir""
            eval_cmd 10 ""${netget//[\"\']/}" "$address""
            ;;
          *)
            quit 1 "qi: build: Source file '${file_name}' cannot be obtained, protocol not supported.";
        esac

        # Update timestamp and SHA1 sum
        eval_cmd \
         "touch "$file_name" && sha1sum "$file_name" > "${file_name}.sha1sum""
      fi
    done
    unset address file_name fetch
  else
    warn "qi: build: WARNING: Array 'fetch' empty.  No source(s) will be downloaded."
  fi

  # Decompress main source in tmpdir
  eval_cmd "cd "$tmpdir" && unpack "$full_tarname""

  # Set sane ownerships
  if (( $UID == 0 )) ; then
    eval_cmd "chown -R 0:0 "${tmpdir}/${srcdir}""
  fi
  # Set sane permissions
  eval_cmd "chmod -R u+w,go-w,a+rX-s "${tmpdir}/${srcdir}""

  # Exit immediately on any error before build()
  if [[ $avoid_errors != avoid_errors ]] ; then
    set -o errexit	# Exit immediately on any error.
  else
    warn "WARNING: Automatic detection of errors will be avoided."
  fi

  # Run the build function
  echo "==> Running build() ..."
  build

  set +o errexit	# Deactivate shell option.

  # If for some reason destdir is empty, the package won't be created
  if rmdir "$destdir" 2> /dev/null ; then
    warn "qi: build: WARNING: ${full_pkgname}.tlz: Won't be created, 'destdir' is empty."
    nopkg=nopkg
    strip=nostrip
  fi

  # Strip binaries and libraries
  if [[ $strip != nostrip && $arch != noarch ]] ; then
    echo "==> Stripping binaries and libraries ..."

    shopt -s globstar	# To find all the files recursively
    for element in "${destdir}"/** ; do
      if [[ ! -e $element ]] ; then
        continue;
      fi
      if [[ -f $element && -s $element ]] ; then
        fileList+=( $element )
      fi
    done

    # Iterate over the fileList
    if (( ${#fileList[@]} > 0 )) ; then
      for element in "${fileList[@]}" ; do
        string="$(file -- "$element")"
        if [[ $string =~ ELF && $string =~ executable|'shared object'|relocatable ]] ; then
          strip --strip-unneeded "$element"
        fi
        if [[ $string =~ 'current ar archive' ]] ; then
          strip --strip-debug "$element"
        fi
      done
    else
      warn "qi: build: WARNING: No elements or symbols found to strip."
    fi
    shopt -u globstar	# Deactivate shell option.
    unset element string fileList
  fi

  # Copy the documentation
  if [[ $nopkg != nopkg ]] ; then
    echo "==> Copying documentation ..."

    if [[ $compress_pages != nocompress_pages ]] ; then
      # Compress .info documents (if needed)
      if [[ -d ${destdir}/$infodir ]] ; then
        echo "--- Info documents:"
        rm -f "${destdir}/${infodir}"/dir

        while IFS="" read -r ; do
          echo "${REPLY##*/}"		# Show processed file
          eval_cmd "lzip -9 "$REPLY""	# Compress it using the max level
        done < <(eval_cmd "find "${destdir}/${infodir}" -type f")
        unset REPLY
      fi

      # Compress and link manual pages (if needed)
      if [[ -d ${destdir}/$mandir ]] ; then
        echo "--- Manual pages:"
        eval_cmd \
         "( cd "${destdir}/$mandir" && find . -type f -exec lzip -9 '{}' \; -print)"

        # Make soft links
        while IFS="" read -r ; do
          eval_cmd "ln -sf -v "$(readlink -- "$REPLY").lz" "${REPLY}.lz""
          eval_cmd "rm -v -- "$REPLY""
        done < <(eval_cmd "find "${destdir}/$mandir" -type l")
        unset REPLY
      fi
    fi

    # Source documentation
    if (( ${#docs[@]} > 0 )) ; then
      echo "--- Source documentation:"
      eval "mkdir -p "${destdir}/$docdir""

      shopt -s globstar
      for element in "${docs[@]}" ; do
        eval_cmd "cp -a "$element" "${destdir}/$docdir""
      done
      shopt -u globstar
      unset element docdir

      echo "${#docs[@]} elements have been copied from the 'docs' array."
      unset docs
    fi
  fi

  # Package creation
  if [[ $nopkg != nopkg ]] ; then
    echo "==> Creating package \"${full_pkgname}.tlz\" in \`${outdir}' ..."

    # Recipe edition when release is incremented
    if [[ $increment_release = increment_release ]] ; then
      eval_cmd "ed -s "$recipe"" <<< ",s/^\(release\)=.*/\1=${release}/"$'\nw'
    fi

    # Add recipe to the (package) database
    eval_cmd "( cd "$CWD" && cp -p "$recipe" "${destdir}/var/lib/qi/recipes/" )"

    # Make the package
    eval_cmd \
     "cd "$destdir" && tar -c ./* | lzip -9v > "${outdir}/${full_pkgname}.tlz""
  fi

  # Save variables of the current recipe and possible exported functions
  eval_cmd "cd "${tmpdir}"/$PRD && export -p > recipe_env$$"

  # Prints the lines only in recipe_env but not in default_env, extracting
  # the name of the variable and function for easy deactivation
  eval_cmd \
   "grep -F -xvf default_env$$ recipe_env$$ | sed '/^declare -x/!d ; s/^declare -x \([^=]*\).*/unset \1/' > substraction$$"

  # Deactivate exportation
  set +o allexport

  # Back to the current working directory
  eval_cmd "cd "$CWD""

  # Clean temporary directories
  if [[ $keepdir != keepdir ]] ; then
    echo "==> Cleaning up temporary directories ..."
    rmIfDir "${tmpdir}/${srcdir}" "$destdir"
  fi

  # Save pkgname before restoring the variables,
  # this will be used to send the pkgname via pipe
  s_full_pkgname="$full_pkgname"

  # Deactivate variables used in the recipe
  source "${tmpdir}"/${PRD}/substraction$$

  # Restore default environment for the next recipe
  source "${tmpdir}/${PRD}/default_env$$" 2> /dev/null

  echo "<== Done [ ${CWD}/$recipe ]"

  # Create named pipe if -C is given (server/writer)
  if [[ $create_pipe = create_pipe ]] ; then
    if [[ ! -p /var/tmp/qi-pipe ]] ; then
      eval_cmd "mkfifo -m 077 /var/tmp/qi-pipe"
    fi

    printf '%s\n' \
     "qi: build: PIPE: Sending full package name to /var/tmp/qi-pipe ..." \
     "qi: build: PIPE: Waiting for the reader ..."
    eval_cmd "echo \"${outdir}/${s_full_pkgname}.tlz\" > /var/tmp/qi-pipe"
  fi

  unset s_full_pkgname
done

