cmake_minimum_required(VERSION 2.8.10)

# Default install location. Must be set here, before setting the project.
if (NOT DEFINED CMAKE_INSTALL_PREFIX)
    set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/install CACHE PATH "" FORCE)
endif()

set(PROJECT unity-scopes-api)
project(${PROJECT} C CXX)

if(${PROJECT_BINARY_DIR} STREQUAL ${PROJECT_SOURCE_DIR})
   message(FATAL_ERROR "In-tree build attempt detected, aborting. Set your build dir outside your source dir, delete CMakeCache.txt from source root and try again.")
endif()

include(CheckCXXSourceCompiles)
CHECK_CXX_SOURCE_COMPILES("#ifdef __clang__\n#else\n#error \"Not clang.\"\n#endif\nint main(int argc, char **argv) { return 0; }" IS_CLANG)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules)

if(IS_CLANG)
  message(STATUS "Compiling with Clang, disabling lttng.")
endif()

string(TOLOWER "${CMAKE_BUILD_TYPE}" cmake_build_type_lower) # Build types should always be lowercase but sometimes they are not.

# Static C++ checks

find_program(CPPCHECK_COMMAND NAMES cppcheck)
if (CPPCHECK_COMMAND)
    set(CPPCHECK_COMMAND_OPTIONS --check-config --inline-suppr --enable=all -q --error-exitcode=2)
    set(CPPCHECK_COMMAND_OPTIONS ${CPPCHECK_COMMAND_OPTIONS} --template "'{file}({line}): {severity} ({id}): {message}'")
    add_custom_target(cppcheck COMMAND ${CPPCHECK_COMMAND} ${CPPCHECK_COMMAND_OPTIONS}
        ${CMAKE_SOURCE_DIR}/src
        ${CMAKE_SOURCE_DIR}/test
        ${CMAKE_BINARY_DIR}/test
    )
else()
    message(WARNING "Cannot find cppcheck: cppcheck target will not be available")
endif()

#
# Definitions for testing with valgrind.
#

configure_file(CTestCustom.cmake.in CTestCustom.cmake) # Tests in CTestCustom.cmake are skipped for valgrind

find_program(MEMORYCHECK_COMMAND NAMES valgrind)
if (MEMORYCHECK_COMMAND)
    set(MEMORYCHECK_COMMAND_OPTIONS
        "--suppressions=${CMAKE_SOURCE_DIR}/valgrind-suppress --leak-check=full --num-callers=40 --error-exitcode=3"
    )
    add_custom_target(valgrind DEPENDS NightlyMemCheck)
else()
    message(WARNING "Cannot find valgrind: valgrind target will not be available")
endif()

include(FindPkgConfig)
find_package(Boost COMPONENTS regex serialization REQUIRED)
pkg_check_modules(UNITY_API libunity-api>=0.1.1 REQUIRED)
pkg_check_modules(PROCESS_CPP process-cpp>=1.0.0 REQUIRED)
pkg_check_modules(LTTNG_UST lttng-ust REQUIRED)
pkg_check_modules(LIBURCU_BP liburcu-bp REQUIRED)
find_program(LTTNG_EXECUTABLE lttng)
if (NOT LTTNG_EXECUTABLE)
    message(SEND_ERROR "Cannot find LTTng executable: ensure that lttng-tools is installed")
endif()

#
# Code style fixer. We put the code through astyle first because it makes some fixes that
# clang-format won't do (such as "char *p" -> "char* p"). But astyle messes up other things
# (particularly lambdas and assembly-style comments), which clang-format does right.
# So, we run clang-format after running astyle, which undoes the damage done by astyle
# without reverting desirable astyle fixes.
#

find_program(ASTYLE_COMMAND NAMES astyle)
if (NOT ASTYLE_COMMAND)
    message(WARNING "Cannot find astyle: formatcode target will not be available")
else()
    # astyle 2.03 creates DOS line endings, so we need to fix its output
    find_program(DOS2UNIX_COMMAND NAMES dos2unix)
    if (NOT DOS2UNIX_COMMAND)
        message(WARNING "Cannot find dos2unix: formatcode target will not be available")
    else()
        find_program(CLANG_FORMAT_COMMAND NAMES clang-format-3.5)
        if (NOT CLANG_FORMAT_COMMAND)
            message(WARNING "Cannot find clang-format >= clang-format-3.5: formatcode target will not be available")
        endif()
    endif()
endif()

if (ASTYLE_COMMAND AND DOS2UNIX_COMMAND AND CLANG_FORMAT_COMMAND)
set(UNITY_SCOPES_LIB_HDRS ${UNITY_SCOPES_LIB_HDRS} ${fmt_h})
    add_custom_target(formatcode
                      ${PROJECT_SOURCE_DIR}/tools/format-files.sh ${PROJECT_SOURCE_DIR} ${ASTYLE_COMMAND} ${CLANG_FORMAT_COMMAND})
endif()

find_program(CAPNPC_BIN capnpc)
if(NOT CAPNPC_BIN)
  message(FATAL_ERROR "Capnp compiler not found.")
endif()
set(CAPNPC_FLAGS "" CACHE STRING "Extra flags for capnpc.")

set(OTHER_INCLUDE_DIRS ${OTHER_INCLUDE_DIRS} ${UNITY_API_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS} ${JSONCPP_INCLUDE_DIRS} ${PROCESS_CPP_INCLUDE_DIRS})
set(OTHER_LIBS ${UNITY_API_LDFLAGS} ${OTHER_LIBS})

# Standard install paths
include(GNUInstallDirs)

include_directories(
    include/
    ${CMAKE_BINARY_DIR}/include
    ${CMAKE_BINARY_DIR}/src
    ${OTHER_INCLUDE_DIRS}
)

# When building the library, we set the default symbol visibility
# to "hidden", so we don't export things by default.
# Exported functions and classes are prefixed by a UNITY_API macro,
# which explicitly exports a symbol if UNITY_DLL_EXPORTS is defined.
add_definitions(-DUNITY_DLL_EXPORTS)

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -pedantic -Wno-variadic-macros -Wextra -fPIC")

# By default, all symbols are visible in the library. We strip out things we don't
# want at link time, by running a version script (see unity-scopes.map and the
# setting of LINK_FLAGS below).
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=default")

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -pedantic -Wall -Wextra")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pedantic -Wall -Wextra") # Can't use -std=c++11 with C compiler

# -fno-permissive causes warnings with clang, so we only enable it for gcc
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-permissive")
endif()

if ("${cmake_build_type_lower}" STREQUAL "release" OR "${cmake_build_type_lower}" STREQUAL "relwithdebinfo")
    option(Werror "Treat warnings as errors" ON)
else()
    option(Werror "Treat warnings as errors" OFF)
endif()

# Some tests are slow, so make it possible not to run them
# during day-to-day development.
option(slowtests "Run slow tests" ON)

if (${Werror})
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror")
    if ("${cmake_build_type_lower}" STREQUAL "release" OR "${cmake_build_type_lower}" STREQUAL "relwithdebinfo")
        set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-error=deprecated-declarations")
        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=deprecated-declarations")
    endif()
endif()

set(SANITIZER "" CACHE STRING "Build with -fsanitize=<value> (legal values: thread, address)")

if ("${SANITIZER}" STREQUAL "")
    # Do nothing
elseif (${SANITIZER} STREQUAL "thread")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread -fno-omit-frame-pointer")
elseif (${SANITIZER} STREQUAL "address")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
else()
    message(FATAL_ERROR "Invalid SANITIZER setting: ${SANITIZER}")
endif()

option(IPC_MONITORING "Enable IPC traffic monitoring")

if(IPC_MONITORING)
    add_definitions(-DENABLE_IPC_MONITOR)
endif()

set(SCOPES_DEFAULT_CONFIGDIR ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT})

# API version
set(UNITY_SCOPES_MAJOR 0)
set(UNITY_SCOPES_MINOR 4)
set(UNITY_SCOPES_MICRO 2)

# Scopes library
set(UNITY_SCOPES_LIB unity-scopes)

# Version for testing, with all symbols visible
set(UNITY_SCOPES_TEST_LIB unity-scopes-test)

pkg_check_modules(JSONCPP jsoncpp REQUIRED)

find_library(ZMQLIB zmq)
if(NOT ZMQLIB)
  message(FATAL_ERROR "Zmq lib not found.")
endif()

find_library(ZMQPPLIB zmqpp)
if(NOT ZMQPPLIB)
  message(FATAL_ERROR "Zmqpp lib not found.")
endif()

find_library(CAPNPLIB capnp)
if(NOT CAPNPLIB)
  message(FATAL_ERROR "Cap'n Proto lib not found.")
endif()

find_library(KJLIB kj)
if(NOT KJLIB)
  message(FATAL_ERROR "Kj lib not found.")
endif()

find_library(DLLIB dl)
if(NOT DLLIB)
  message(FATAL_ERROR "Dl lib not found.")
endif()


# Other libraries we depend on
set(OTHER_LIBS
    ${OTHER_LIBS}
    ${Boost_LIBRARIES}
    ${JSONCPP_LDFLAGS}
    ${PROCESS_CPP_LDFLAGS}
    ${ZMQPPLIB}
    ${ZMQLIB}
    ${CAPNPLIB}
    ${KJLIB}
    ${DLLIB}
    pthread
    smartscopes
)

# All the libraries we need to link a normal executable that uses Unity scopes
set(LIBS ${UNITY_SCOPES_LIB})

# All the libraries we need to link a gtest executable. (We link the tests against a static version
# so we can do whitebox testing on internal classes.

# TODO: HACK: static linking of the tests doesn't work because the tests won't pull in virtual methods in a
# base class from a static library without an unresolved dependency. But, without static linking, we can't
# do whitebox tests because of the visibility=hidden compile flags. For now, we link dynamically and
# with visbility=default.

set(TESTLIBS ${UNITY_SCOPES_TEST_LIB})

# Library install prefix
set(LIB_INSTALL_PREFIX lib/${CMAKE_LIBRARY_ARCHITECTURE})

set(LIBDIR ${CMAKE_INSTALL_LIBDIR})
set(HDR_INSTALL_DIR include/unity-scopes-${UNITY_SCOPES_MAJOR})

# Tests
include(CTest)
enable_testing()

# Add subdirectories.
add_subdirectory(include)
add_subdirectory(src)
add_subdirectory(scoperegistry)
add_subdirectory(scoperunner)
add_subdirectory(smartscopesproxy)
add_subdirectory(data)
add_subdirectory(test)
add_subdirectory(demo)
add_subdirectory(tools)

# Custom rules to compile .capnp files
foreach(file ${CAPNPROTO_FILES})

    string(REPLACE "${CMAKE_SOURCE_DIR}" "${CMAKE_BINARY_DIR}" build_dir ${file})
    get_filename_component(build_dir ${build_dir} PATH)

    get_filename_component(proto_file ${file} NAME)
    string(REPLACE ".capnp" ".capnp.c++" src_file ${proto_file})
    string(REPLACE ".capnp" ".capnp.h" hdr_file ${proto_file})

    set(src_file ${build_dir}/${src_file})
    set(hdr_file ${build_dir}/${hdr_file})

    set(CAPNPROTO_HDRS ${CAPNPROTO_HDRS} ${hdr_file})
    set(CAPNPROTO_SRC ${CAPNPROTO_SRC} ${src_file})
endforeach()

# cmake cannot analyze include file dependencies of .capnproto files, so the easiest option is to recompile
# all files if any one of them changes
add_custom_command(OUTPUT ${CAPNPROTO_SRC} ${CAPNPROTO_HDRS}
                   DEPENDS ${CAPNPROTO_FILES}
                   COMMAND ${CAPNPC_BIN} ${CAPNPC_FLAGS} -oc++:${CMAKE_BINARY_DIR} --src-prefix=${CMAKE_SOURCE_DIR} ${CAPNPROTO_FILES})

# Set up coverage testing
include(EnableCoverageReport)
#####################################################################
# Enable code coverage calculation with gcov/gcovr/lcov
# Usage:
#  * Switch build type to coverage (use ccmake or cmake-gui)
#  * Invoke make, make test, make coverage (or ninja if you use that backend)
#  * Find html report in subdir coveragereport
#  * Find xml report suitable for jenkins in coverage.xml
#####################################################################
if(cmake_build_type_lower MATCHES coverage)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --coverage")
    set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} --coverage")
    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --coverage")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")

    # We add -g when building with coverage so valgrind reports line numbers.
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g" )
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g" )
endif()

# Pseudo-library of object files. We need a version of the library for normal clients
# that does not expose symbols in the unity::scopes::internal namespace, and another
# version for whitebox tests, so we can write unit tests for classes in the internal namespaces.
# Here, we create an object library that then is used to link the normal and the test libraries.

set(UNITY_SCOPES_LIB_OBJ ${UNITY_SCOPES_LIB}-obj)
add_library(${UNITY_SCOPES_LIB_OBJ} OBJECT ${UNITY_SCOPES_LIB_SRC} ${CAPNPROTO_SRC})
set_target_properties(${UNITY_SCOPES_LIB_OBJ} PROPERTIES COMPILE_FLAGS "-fPIC")

# Use the object files to make the normal library.
set(UNITY_SCOPES_SOVERSION 1)
add_library(${UNITY_SCOPES_LIB} SHARED $<TARGET_OBJECTS:${UNITY_SCOPES_LIB_OBJ}>)
set_target_properties(${UNITY_SCOPES_LIB} PROPERTIES
    VERSION "${UNITY_SCOPES_MAJOR}.${UNITY_SCOPES_MINOR}.${UNITY_SCOPES_MICRO}"
    SOVERSION ${UNITY_SCOPES_SOVERSION}
)

# Use the object files to make the test library.
add_library(${UNITY_SCOPES_TEST_LIB} SHARED $<TARGET_OBJECTS:${UNITY_SCOPES_LIB_OBJ}>)
set_target_properties(${UNITY_SCOPES_TEST_LIB} PROPERTIES
    VERSION "${UNITY_SCOPES_MAJOR}.${UNITY_SCOPES_MINOR}.${UNITY_SCOPES_MICRO}"
    SOVERSION ${UNITY_SCOPES_SOVERSION}
)

set(ldflags "")

# Clang sanitizers don't work if --no-undefined is given as a linker argument.
if(NOT IS_CLANG)
    set(ldflags "-Wl,--no-undefined")
endif()

set_target_properties(${UNITY_SCOPES_TEST_LIB} PROPERTIES LINK_FLAGS "${ldflags}")

# We compile with all symbols visible by default. For the shipping library, we strip
# out all symbols that (recursively) are in the unity::scopes::internal namespace,
# except for a few exceptions that are needed by the scoperegistry, scoperunner,
# smartscopesproxy, and any other binaries that reach into the internal namespace.
set(symbol_map "${CMAKE_SOURCE_DIR}/unity-scopes.map")
set_target_properties(${UNITY_SCOPES_LIB} PROPERTIES
                      LINK_FLAGS "${ldflags} -Wl,--version-script,${symbol_map}")
set_target_properties(${UNITY_SCOPES_LIB} PROPERTIES LINK_DEPENDS ${symbol_map})

target_link_libraries(${UNITY_SCOPES_LIB} ${OTHER_LIBS})
target_link_libraries(${UNITY_SCOPES_TEST_LIB} ${OTHER_LIBS})

# Only the normal library gets installed.
install(TARGETS ${UNITY_SCOPES_LIB} LIBRARY DESTINATION ${LIB_INSTALL_PREFIX})

# Enable coverage testing

if (cmake_build_type_lower MATCHES coverage)
  ENABLE_COVERAGE_REPORT(TARGETS ${UNITY_SCOPES_LIB} smartscopes FILTER /usr/include ${CMAKE_SOURCE_DIR}/test/* ${CMAKE_BINARY_DIR}/*)
endif()

#
# Documentation
#
# Pass -DDEVEL_DOCS=ON to cmake for more detailed documentation

option(DEVEL_DOCS "Enable detailed Doxygen documentation")

set(CMAKE_INSTALL_DOCDIR doc)

find_package(Doxygen)
find_program(DOT_EXECUTABLE dot /usr/bin)
if (NOT DOXYGEN_FOUND OR NOT DOT_EXECUTABLE)
    message(WARNING "Cannot generate documentation: doxygen and/or graphviz not found")
else()
    if (DEVEL_DOCS)
      message(STATUS "Creating devel documentation")
      configure_file(${PROJECT_SOURCE_DIR}/doc/Doxyfile-devel.in ${PROJECT_BINARY_DIR}/doc/Doxyfile @ONLY IMMEDIATE)
    else()
      configure_file(${PROJECT_SOURCE_DIR}/doc/Doxyfile.in ${PROJECT_BINARY_DIR}/doc/Doxyfile @ONLY IMMEDIATE)
    endif()
    configure_file(${PROJECT_SOURCE_DIR}/doc/index.html ${PROJECT_BINARY_DIR}/doc/index.html COPYONLY)
    add_custom_command(OUTPUT ${PROJECT_BINARY_DIR}/doc/${PROJECT}/index.html
                       COMMAND ${DOXYGEN_EXECUTABLE} ${PROJECT_BINARY_DIR}/doc/Doxyfile
                       DEPENDS ${PROJECT_BINARY_DIR}/doc/Doxyfile
                               ${PROJECT_SOURCE_DIR}/doc/tutorial.dox
                               ${UNITY_SCOPES_LIB_SRC}
                               ${UNITY_SCOPES_LIB_HDRS})
    add_custom_target(doc ALL
                       DEPENDS ${PROJECT_BINARY_DIR}/doc/${PROJECT}/index.html)
    install(DIRECTORY ${PROJECT_BINARY_DIR}/doc/${PROJECT}
            DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${CMAKE_INSTALL_DOCDIR})
endif()
