# ============================================================================
# Copyright (c) 2011-2012 University of Pennsylvania
# Copyright (c) 2013-2014 Andreas Schuh
# All rights reserved.
#
# See COPYING file for license information or visit
# http://opensource.andreasschuh.com/cmake-basis/download.html#license
# ============================================================================

##############################################################################
# @file  ExecuteProcess.cmake
# @brief Execute process using CMake script mode.
#
# This CMake script can be used as argument for the -P option of cmake, when
# another command shall be executed by CMake, for example, as custom build
# command. The advantage of using this script is that all options of the
# CMake command execute_process() can be used, i.e., a timeout can be
# specified.
#
# The arguments of the execute_process() command have to be specified via
# the -D option on the command line of cmake before the -P \<this script\>
# option is given. The name of the CMake variables must be equal the
# name of the arguments to the execute_process() command.
#
# Arguments of execute_process() which are considered:
#
# - @b COMMAND
# - @b WORKING_DIRECTORY
# - @b TIMEOUT
# - @b OUTPUT_FILE
# - @b ERROR_FILE
# - @b OUTPUT_QUIET
# - @b ERROR_QUIET
# - @b OUTPUT_STRIP_TRAILING_WHITESPACE
# - @b ERROR_STRIP_TRAILING_WHITESPACE
#
# Additionally, matching expressions (separated by ';') to identify error messages
# in the output streams stdout and stderr can be specified by the input argument
# ERROR_EXPRESSION. When the output of the executed command matches one of
# the error expressions, a fatal error message is displayed causing CMake to
# return the exit code 1.
#
# Setting VERBOSE to true enables verbose output messages.
#
# When the input argument LOG_ARGS evaluates to true, the values of COMMAND,
# WORKING_DIRECTORY, and TIMEOUT are added to the top of the output files
# specified by OUTPUT_FILE and ERROR_FILE.
#
# The arguments ARGS and ARGS_FILE can be used to specify (additional) command
# arguments. The content of the text file ARGS_FILE is read when it this file
# exists. Separate lines of this file are considered single arguments.
# The arguments specified by ARGS and ARGS_FILE are concatenated where the
# arguments given by ARGS follow after the ones read from the ARGS_FILE.
# All occurences of the string 'ARGS' in the COMMAND are replaced by these
# arguments. If no such string is present, the arguments are simply passed
# to the execute_process() command as its ARGS argument.
# The argument ARGS_SEPARATOR specifies the separator used to separate the
# arguments given by ARGS and ARGS_FILE when the 'ARGS' string in COMMAND
# is replaced. By default, it is set to ';'.
#
# Example:
# @code
# cmake -DCOMMAND='ls;-l' -DWORKING_DIRECTORY='/' -DTIMEOUT=60
#       -P ExecuteProcess.cmake
# @endcode
#
# The output of the executed process can further be searched for error expressions
# specified by the ERROR_EXPRESSION variable. If the process output matches
# this expression, a fatal CMake error is raised.
#
# Certain errors may be temporary such as in particular a license checkout
# error of the MATLAB Compiler. Such errors can be filtered out using the
# RETRY_EXPRESSION variable. If such error is detected, this script sleeps for
# RETRY_DELAY seconds and then executes the process again. This is done
# at maximum RETRY_ATTEMPTS times. If the retry attempts are exceeded, a
# fatal CMake error is raised instead.
#
# @sa http://www.cmake.org/cmake/help/cmake2.6docs.html#command:execute_process
#
# @ingroup CMakeUtilities
##############################################################################

if (POLICY CMP0053)
  cmake_policy (SET CMP0053 NEW)
endif ()

# ----------------------------------------------------------------------------
# unset environment variables that may cause problems otherwise
unset (ENV{PYTHONHOME})
unset (ENV{PYTHONPATH})

# ----------------------------------------------------------------------------
# initialize arguments
set (CONFIGURED_COMMAND "@BUILD_CMD@")

string (REPLACE "#" "@" AT_BUILD_CMD_AT "#BUILD_CMD#")
if (CONFIGURED_COMMAND AND NOT CONFIGURED_COMMAND STREQUAL "${AT_BUILD_CMD_AT}")
  set (COMMAND "${CONFIGURED_COMMAND}")
elseif (NOT COMMAND)
  message (FATAL_ERROR "No command specified for execute_process(): use \"-DCOMMAND=cmd\"")
endif ()

if (NOT ARGS_SEPARATOR)
  set (ARGS_SEPARATOR ";")
endif ()

if (ARGS_FILE)
  include ("${ARGS_FILE}" OPTIONAL)

  if (ARGS AND DEFINED ${ARGS})
    set (ARGS "${${ARGS}}")
  else ()
    set (ARGS "")
  endif ()
endif ()

if ("${COMMAND}" MATCHES "ARGS")
  string (REPLACE ";" "${ARGS_SEPARATOR}" TMP "${ARGS}")
  string (REPLACE "ARGS" "${ARGS_SEPARATOR}${TMP}" COMMAND "${COMMAND}")
  set (ARGS)
endif ()

set (EXECUTE_PROCESS_ARGS "COMMAND" "${COMMAND}")

if (ARGS)
  list (APPEND EXECUTE_PROCESS_ARGS "ARGS" "${ARGS}")
endif ()

list (APPEND EXECUTE_PROCESS_ARGS "RESULT_VARIABLE" "RETVAL")

if (TIMEOUT)
  list (APPEND EXECUTE_PROCESS_ARGS "TIMEOUT" "${TIMEOUT}")
endif ()

if (WORKING_DIRECTORY)
  list (APPEND EXECUTE_PROCESS_ARGS "WORKING_DIRECTORY" "${WORKING_DIRECTORY}")
endif ()

if (OUTPUT_FILE)
  list (APPEND EXECUTE_PROCESS_ARGS "OUTPUT_FILE" "${OUTPUT_FILE}")
endif ()

if (ERROR_FILE)
  list (APPEND EXECUTE_PROCESS_ARGS "ERROR_FILE" "${ERROR_FILE}")
endif ()

if (OUTPUT_QUIET)
  list (APPEND EXECUTE_PROCESS_ARGS "OUTPUT_QUIET")
endif ()

if (ERROR_QUIET)
  list (APPEND EXECUTE_PROCESS_ARGS "ERROR_QUIET")
endif ()

if (OUTPUT_STRIP_TRAILING_WHITESPACE)
  list (APPEND EXECUTE_PROCESS_ARGS "OUTPUT_STRIP_TRAILING_WHITESPACE")
endif ()

if (ERROR_STRIP_TRAILING_WHITESPACE)
  list (APPEND EXECUTE_PROCESS_ARGS "ERROR_STRIP_TRAILING_WHITESPACE")
endif ()

if (NOT OUTPUT_FILE)
  list (APPEND EXECUTE_PROCESS_ARGS "OUTPUT_VARIABLE" "STDOUT")
endif ()

if (NOT ERROR_FILE)
  list (APPEND EXECUTE_PROCESS_ARGS "ERROR_VARIABLE"  "STDERR")
endif ()

if (NOT RETRY_ATTEMPTS)
  set (RETRY_ATTEMPTS 0)
endif ()

if (NOT RETRY_DELAY)
  set (RETRY_DELAY 60)
endif ()

# --------------------------------------------------------------------------
# verbose message of command to execute
set (CMD)
foreach (ARG ${COMMAND})
  if (CMD)
    set (CMD "${CMD} ")
  endif ()
  if (ARG MATCHES " ")
    set (CMD "${CMD}\"${ARG}\"")
  else ()
    set (CMD "${CMD}${ARG}")
  endif ()
endforeach ()

if (VERBOSE)
  message ("${CMD}")
endif ()

# ----------------------------------------------------------------------------
# execute command
set (RETRY TRUE) # execute process at least once
while (RETRY)
  # --------------------------------------------------------------------------
  # whether retry is required is decided upon after process execution
  set (RETRY FALSE)

  # --------------------------------------------------------------------------
  # execute process
  execute_process (${EXECUTE_PROCESS_ARGS})

  # --------------------------------------------------------------------------
  # read in output from log files
  if (OUTPUT_FILE)
    file (READ "${OUTPUT_FILE}" STDOUT)
  endif ()

  if (ERROR_FILE)
    file (READ "${ERROR_FILE}" STDERR)
  endif ()

  # --------------------------------------------------------------------------
  # parse output for recoverable errors
  foreach (EXPR IN LISTS RETRY_EXPRESSION)
    if (STDOUT MATCHES "${EXPR}" OR STDERR MATCHES "${EXPR}")
      if (RETRY_ATTEMPTS GREATER 0)
        message ("Process output matches \"${EXPR}\", retry in ${RETRY_DELAY} seconds")
        # retry
        math (EXPR RETRY_ATTEMPTS "${RETRY_ATTEMPTS} - 1")
        set (RETRY TRUE)
      else ()
        # no retries left
        set (RETVAL 1)
      endif ()
      break ()
    endif ()
  endforeach ()

  # --------------------------------------------------------------------------
  # sleep for given amount of seconds before retrying to execute process
  if (RETRY)
    # use sleep command if available, i.e., on Unix and also on Windows
    # if the Windows 2003 Resource Kit is installed
    find_program (SLEEP sleep)
    if (SLEEP)
      execute_process (
        COMMAND "${SLEEP}" ${RETRY_DELAY}
        TIMEOUT ${RETRY_DELAY}
        ERROR_QUIET OUTPUT_QUIET
      )
    else ()
      # work-around using ping command if sleep command not available, i.e.,
      # on Windows where the Windows 2003 Resource Kit is not installed
      # See http://malektips.com/dos0017.html
      find_program (PING ping)
      if (WIN32)
        execute_process (
          COMMAND ${PING} 127.0.0.1 -n ${RETRY_DELAY} -w 1000
          TIMEOUT ${RETRY_DELAY}
          ERROR_QUIET OUTPUT_QUIET
        )
      else ()
        # usually not required as sleep command should be available
        execute_process (
          COMMAND ${PING} 127.0.0.1 -c ${RETRY_DELAY} -W 1000
          TIMEOUT ${RETRY_DELAY}
          ERROR_QUIET OUTPUT_QUIET
        )
      endif ()
    else ()
      message (WARNING "Cannot delay retries as neither sleep nor ping command is available!")
    endif ()
  endif ()
endwhile ()

# ----------------------------------------------------------------------------
# parse output for errors
foreach (EXPR IN LISTS ERROR_EXPRESSION)
  if (STDOUT MATCHES "${EXPR}" OR STDERR MATCHES "${EXPR}")
    set (RETVAL 1)
    break ()
  endif ()
endforeach ()

# ----------------------------------------------------------------------------
# give some hints for known error messages
if (NOT RETVAL EQUAL 0)
  set (HINTS)
  # MATLAB Compiler
  if (CMD MATCHES "mcc")
    if (STDERR MATCHES "The file.*appears to be a MEX-file.[ ]+It shadows the M-file.*but will not execute properly at runtime, as it does not export a function.*named 'mexFunction.'")
      set (HINTS "${HINTS}

  Note: The error that a MEX-file would shadow a M-file can be caused by a shared library that
        is required by the MEX-file but not found in the search path of the dynamic loader.
")
    endif ()
  endif ()
  # append hints to output
  if ("${STDOUT}" STREQUAL "${STDERR}")
    set (STDERR "${STDERR}${HINTS}")
    set (STDOUT "${STDOUT}${HINTS}")
  else ()
    set (STDERR "${STDERR}${HINTS}")
  endif ()
endif ()

# ----------------------------------------------------------------------------
# prepand command to log file
if (LOG_ARGS)
  if (OUTPUT_FILE)
    set (TMP "Command: ${CMD}\n\nWorking directory: ${WORKING_DIRECTORY}\n\n")
    set (TMP "${TMP}Timeout: ${TIMEOUT}\n\nOutput:\n\n${STDOUT}")
    file (WRITE "${OUTPUT_FILE}" "${TMP}")
    unset (TMP)
  endif ()

  if (ERROR_FILE AND NOT "${ERROR_FILE}" STREQUAL "${OUTPUT_FILE}")
    set (TMP "Command: ${CMD}\n\nWorking directory: ${WORKING_DIRECTORY}\n\n")
    set (TMP "${TMP}Timeout: ${TIMEOUT}\n\nOutput:\n\n${STDERR}")
    file (WRITE "${ERROR_FILE}" "${TMP}")
    unset (TMP)
  endif ()
endif ()

# ----------------------------------------------------------------------------
# print error message (and exit with exit code 1) on error
if (NOT RETVAL EQUAL 0)
  
  # print error
  if ("${STDOUT}" STREQUAL "${STDERR}")
    message (
      FATAL_ERROR "
Command: ${CMD}
Working directory: ${WORKING_DIRECTORY}
Timeout: ${TIMEOUT}
Output:
${STDOUT}")
  else ()
    message (
      FATAL_ERROR "
Command: ${CMD}
Working directory: ${WORKING_DIRECTORY}
Timeout: ${TIMEOUT}
Output (stdout):
${STDOUT}
Output (stderr):
${STDERR}")
  endif ()
endif ()
