#!/bin/bash

# render dependencies into separate subdirectories
# ================================================
#
# requires
#  - a linux host with wine, wine with python and mingw installed
#  - the sourcecode mounted to /var/src/
#  - a rw directory mounted to /var/build
#  returns nonzero exit code when pyinstaller failed
#
# prepares a read-write copy of the sourcecode
# executes qt-uic and qt-rcc for gui dialogs
# installs dependencies from pkg/dependencies-windows.pip
# runs pyinstaller
# cleans up (remove wine-dlls, remove read-write copy)
# creates nsis install/uninstall scripts for the files for each package
# if $1 is set it is expected to be a branch/git-tag

product=bitmask
# the location where the pyinstaller results are placed
absolute_executable_path=/var/build/executables
# the location of the nsis installer nis files dictates the path of the files
relative_executable_path=../../build/executables
source_ro_path=/var/src/${product}
temporary_build_path=/var/build/pyinstaller
git_tag=HEAD
version_prefix=leap.bitmask
git_version=unknown
# option that is changed when a dependency-cache is found
install_dependencies=true
# default options for components
with_eip=false
with_mail=true

setups=($(ls -1 ${source_ro_path}/pkg/windows | grep '.nis$' | sed 's|.nis$||'))
# add mingw dlls that are build in other steps
function addMingwDlls() {
  root=$1
  cp /usr/lib/gcc/i686-w64-mingw32/4.9-win32/libgcc_s_sjlj-1.dll ${root}
  cp /root/.wine/drive_c/Python27/Lib/site-packages/zmq/libzmq.pyd ${root}
  cp /root/.wine/drive_c/Python27/Lib/site-packages/zmq/libzmq.pyd ${root}
  mkdir -p ${root}/pysqlcipher
  cp /var/build/pyinstaller/pkg/pyinst/build/bitmask/pysqlcipher-2.6.4-py2.7-win32.egg/pysqlcipher/_sqlite.pyd ${root}/pysqlcipher
  cp ~/.wine/drive_c/openssl/bin/*.dll ${root}
}
# cleanup the temporary build path for subsequent executes
function cleanup() {
  rm -rf ${temporary_build_path} 2>/dev/null
}
# create files that are not part of the repository but are needed
# in the windows environment:
# - license with \r\n
# - ico from png (multiple sizes for best results on high-res displays)
function createInstallablesDependencies() {
  pushd ${temporary_build_path} > /dev/null
  cat LICENSE | sed 's|\n|\r\n|g' > LICENSE.txt
  convert data/images/mask-icon.png  -filter Cubic -scale 256x256! data/images/mask-icon-256.png
  convert data/images/mask-icon-256.png -define icon:auto-resize data/images/mask-icon.ico
  # execute qt-uic / qt-rcc
  wine mingw32-make all || die 'qt-uic / qt-rcc failed'
  # get version using git (only available in host)
  git_version=$(python setup.py version| grep 'Version is currently' | awk -F': ' '{print $2}')
  # run setup.py in a path with the version contained so versioneer can
  # find the information and put it into the egg
  versioned_build_path=/var/tmp/${version_prefix}-${git_version}
  mkdir -p ${versioned_build_path}
  cp -r ${temporary_build_path}/* ${versioned_build_path}
  # apply patches to the source that are required for working code
  # should not be required in the future as it introduces possible
  # hacks that are hard to debug
  applyPatches ${versioned_build_path}
  pushd ${versioned_build_path} > /dev/null
  # XXX what's this update_files command?
  #wine python setup.py update_files || die 'setup.py update_files failed'
  wine python setup.py build || die 'setup.py build failed'
  wine python setup.py install || die 'setup.py install failed'
  popd
  rm -rf ${versioned_build_path}
  popd
}
# create installer version that may be used by installer-build.sh / makensis
# greps the version-parts from the previously extracted git_version and stores
# the result in a setup_version.nsh
# when the git_version does provide a suffix it is prefixed with a dash so the
# installer output needs no conditional for this
function createInstallerVersion() {
  setup=$1
  # [0-9]*.[0-9]*.[0-9]*-[0-9]*_g[0-9a-f]*_dirty
  VERSIONMAJOR=$(echo ${git_version} | sed 's|^\([0-9]*\)\..*$|\1|')
  VERSIONMINOR=$(echo ${git_version} | sed 's|^[0-9]*\.\([0-9]*\).*$|\1|')
  VERSIONBUILD=$(echo ${git_version} | sed 's|^[0-9]*\.[0-9]*\.\([0-9]*\).*$|\1|')
  VERSIONSUFFIX=$(echo ${git_version} | sed 's|^[0-9]*\.[0-9]*\.[0-9]*-\(.*\)$|\1|')
  echo "!define VERSIONMAJOR ${VERSIONMAJOR}" > ${absolute_executable_path}/${setup}_version.nsh
  echo "!define VERSIONMINOR ${VERSIONMINOR}" >> ${absolute_executable_path}/${setup}_version.nsh
  echo "!define VERSIONBUILD ${VERSIONBUILD}" >> ${absolute_executable_path}/${setup}_version.nsh
  if [ ${VERSIONSUFFIX} != "" ]; then
    VERSIONSUFFIX="-${VERSIONSUFFIX}"
  fi
  echo "!define VERSIONSUFFIX ${VERSIONSUFFIX}" >> ${absolute_executable_path}/${setup}_version.nsh
}
# create installable binaries with dlls
function createInstallables() {
  mkdir -p ${absolute_executable_path}
  pushd ${temporary_build_path}/pkg/pyinst
  # build install directories (contains multiple files with pyd,dll, some of
  # them look like windows WS_32.dll but are from wine)
  for setup in ${setups[@]}
  do
    # --clean do not cache anything and overwrite everything --noconfirm
    # --distpath to place on correct location
    # --debug to see what may be wrong with the result
    # --paths=c:\python\lib\site-packages;c:\python27\lib\site-packages
    wine pyinstaller \
      --clean \
      --noconfirm \
      --distpath=.\\installables \
      --paths=Z:\\var\\build\\pyinstaller\\src\\ \
      --paths=C:\\Python27\\Lib\\site-packages\\ \
      --debug \
      ${setup}.spec \
    || die 'pyinstaller for "'${setup}'" failed'
    removeWineDlls installables/${setup}
    addMingwDlls installables/${setup}
    rm -r ${absolute_executable_path}/${setup}
    cp -r installables/${setup} ${absolute_executable_path}
    cp ${absolute_executable_path}/cacert.pem ${absolute_executable_path}/${setup}
    rm -r installables
    createInstallerVersion ${setup}
  done
  popd
  pushd ${temporary_build_path}
  cp data/images/mask-icon.ico ${absolute_executable_path}/
  popd
}
# install (windows)dependencies of project
function installProjectDependencies() {
  pushd ${temporary_build_path} > /dev/null
  unsupported_packages="dirspec"
  pip_flags="--find-links=Z:${temporary_build_path}/wheels"
  for unsupported_package in ${unsupported_packages}
  do
    pip_flags="${pip_flags} --allow-external ${unsupported_package} --allow-unverified ${unsupported_package}"
  done
  pip_flags="${pip_flags} -r"

  # install dependencies
  mkdir -p ${temporary_build_path}/wheels
  wine pip install ${pip_flags} pkg/requirements-leap.pip || die 'requirements-leap.pip could not be installed'
  # fix requirements
  # python-daemon breaks windows build
  sed -i 's|^python-daemon|#python-daemon|' pkg/requirements.pip
  wine pip install ${pip_flags} pkg/requirements.pip || die 'requirements.pip could not be installed'
  git checkout pkg/requirements.pip
  popd
  cp -r /root/.wine/drive_c/Python27/Lib/site-packages ${absolute_executable_path}
  curl https://curl.haxx.se/ca/cacert.pem > ${absolute_executable_path}/cacert.pem || die 'cacert.pem could not be fetched - would result in bad ssl in installer'
}
# workaround for broken dependencies
# runs before pip install requirements
# fixes failure for pysqlcipher as this requests a https file that the
# windows-python fails to request
function installProjectDependenciesBroken() {
  pushd ${temporary_build_path} > /dev/null
  curl https://pypi.python.org/packages/source/p/pysqlcipher/pysqlcipher-2.6.4.tar.gz \
    > pysqlcipher-2.6.4.tar.gz \
    || die 'fetch pysqlcipher failed'
  tar xzf pysqlcipher-2.6.4.tar.gz
  pushd pysqlcipher-2.6.4
  curl https://downloads.leap.se/libs/pysqlcipher/amalgamation-sqlcipher-2.1.0.zip \
    > amalgamation-sqlcipher-2.1.0.zip \
    || die 'fetch amalgamation for pysqlcipher failed'
  unzip -o amalgamation-sqlcipher-2.1.0.zip || die 'unzip amalgamation failed'
  mv sqlcipher amalgamation
  patch -p0 < ${source_ro_path}/pkg/windows/pyinstaller/pysqlcipher_setup.py.patch \
    || die 'patch pysqlcipher setup.py failed'
  wine python setup.py build install || die 'setup.py for pysqlcipher failed'
  popd
  popd # temporary_build_path
}
# prepare read-write copy
function prepareBuildPath() {
  cleanup
  # ensure shared openssl for all pip builds
  test -d ${absolute_executable_path}/openvpn || die 'openvpn not available run docker-compose run --rm openvpn'
  cp -r ${absolute_executable_path}/openvpn /root/.wine/drive_c/openssl
  if [ -d ${absolute_executable_path}/site-packages ]; then
    # use pip install cache for slow connections
    rm -r /root/.wine/drive_c/Python27/Lib/site-packages
    cp -r ${absolute_executable_path}/site-packages /root/.wine/drive_c/Python27/Lib/
    install_dependencies=false
  fi
  if [ ! -z $1 ]; then
    git_tag=$1
  fi
  if [ ${git_tag} != "HEAD" ]; then
    echo "using ${git_tag} as source for the project"
    git clone ${source_ro_path} ${temporary_build_path}
    pushd ${temporary_build_path}
    git checkout ${git_tag} || die 'checkout "'${git_tag}'" failed'
    popd
  else
    echo "using current source tree for build"
    mkdir -p ${temporary_build_path}/data
    mkdir -p ${temporary_build_path}/docs
    mkdir -p ${temporary_build_path}/pkg
    mkdir -p ${temporary_build_path}/src
    mkdir -p ${temporary_build_path}/.git
    cp -r ${source_ro_path}/data/* ${temporary_build_path}/data
    cp -r ${source_ro_path}/data/* ${temporary_build_path}/docs
    cp -r ${source_ro_path}/pkg/* ${temporary_build_path}/pkg
    cp -r ${source_ro_path}/src/* ${temporary_build_path}/src
    cp -r ${source_ro_path}/.git/* ${temporary_build_path}/.git
    cp ${source_ro_path}/* ${temporary_build_path}/
  fi
}
# add patches to the sourcetree
# this function should do nothing some day and should be run after
# the version has been evaluated
function applyPatches() {
  root_path=$1
  # disable eip
  if [ !${with_eip} ]; then
    sed -i "s|HAS_EIP = True|HAS_EIP = False|" ${root_path}/src/leap/bitmask/_components.py
  fi
  # disable mail
  if [ !${with_mail} ]; then
    sed -i "s|HAS_MAIL = True|HAS_MAIL = False|" ${root_path}/src/leap/bitmask/_components.py
  fi
  # hack the logger
  sed -i "s|'bitmask.log'|str(random.random()) + '_bitmask.log'|;s|import sys|import sys\nimport random|" ${root_path}/src/leap/bitmask/logs/utils.py
  sed -i "s|perform_rollover=True|perform_rollover=False|" ${root_path}/src/leap/bitmask/app.py
  # fix requirements
  # python-daemon breaks windows build
  sed -i 's|^python-daemon|#python-daemon|' ${root_path}/pkg/requirements.pip
}
# remove wine dlls that should not be in the installer
# root: path that should be cleaned from dlls
function removeWineDlls() {
  root=$1
  declare -a wine_dlls=(\
    advapi32.dll \
    comctl32.dll \
    comdlg32.dll \
    gdi32.dll \
    imm32.dll \
    iphlpapi.dll \
    ktmw32.dll \
    msvcp90.dll \
    msvcrt.dll \
    mswsock.dll \
    mpr.dll \
    netapi32.dll \
    ole32.dll \
    oleaut32.dll \
    opengl32.dll \
    psapi.dll \
    rpcrt4.dll \
    shell32.dll \
    user32.dll \
    version.dll \
    winmm.dll \
    winspool.drv \
    ws2_32.dll \
    wtsapi32.dll \
    )
  for wine_dll in "${wine_dlls[@]}"
  do
    # not all of the listed dlls are in all directories
    rm ${root}/${wine_dll} 2>/dev/null
  done
}
# display failure message and emit non-zero exit code
function die() {
  echo "die:" $@
  exit 1
}
function main() {
  prepareBuildPath $@
  if [ ${install_dependencies} == true ]; then
    installProjectDependenciesBroken
    installProjectDependencies
  fi
  createInstallablesDependencies
  createInstallables
  cleanup
}
main $@