cmake_minimum_required(VERSION 3.18)
project(
  conflict-set
  VERSION 0.0.1
  DESCRIPTION
    "A data structure for optimistic concurrency control on ranges of bitwise-lexicographically-ordered keys."
  HOMEPAGE_URL "https://git.weaselab.dev/weaselab/conflict-set"
  LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20)

set(DEFAULT_BUILD_TYPE "Release")

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  message(
    STATUS
      "Setting build type to '${DEFAULT_BUILD_TYPE}' as none was specified.")
  set(CMAKE_BUILD_TYPE
      "${DEFAULT_BUILD_TYPE}"
      CACHE STRING "Choose the type of build." FORCE)
  # Set the possible values of build type for cmake-gui
  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release"
                                               "MinSizeRel" "RelWithDebInfo")
endif()

add_compile_options(-fdata-sections -ffunction-sections -Wswitch-enum
                    -Werror=switch-enum)

option(USE_SIMD_FALLBACK
       "Use fallback implementations of functions that use SIMD" OFF)

# This is encouraged according to
# https://valgrind.org/docs/manual/manual-core-adv.html#manual-core-adv.clientreq
include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/third_party/valgrind)

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
  add_compile_options(-Wno-maybe-uninitialized
                      $<$<COMPILE_LANGUAGE:CXX>:-Wno-invalid-offsetof>)
endif()

if(APPLE)
  add_link_options(-Wl,-dead_strip)
else()
  add_link_options(-Wl,--gc-sections)
endif()

include(CheckIncludeFileCXX)
include(CMakePushCheckState)

if(NOT USE_SIMD_FALLBACK)
  cmake_push_check_state()
  list(APPEND CMAKE_REQUIRED_FLAGS -mavx)
  check_include_file_cxx("immintrin.h" HAS_AVX)
  if(HAS_AVX)
    add_compile_options(-mavx)
    add_compile_definitions(HAS_AVX)
  endif()
  cmake_pop_check_state()

  check_include_file_cxx("arm_neon.h" HAS_ARM_NEON)
  if(HAS_ARM_NEON)
    add_compile_definitions(HAS_ARM_NEON)
  endif()
endif()

set(CMAKE_CXX_IMPLICIT_LINK_LIBRARIES "")

add_library(${PROJECT_NAME}_object OBJECT ConflictSet.cpp)
target_compile_options(${PROJECT_NAME}_object PRIVATE -fPIC -fno-exceptions
                                                      -fvisibility=hidden)
target_include_directories(${PROJECT_NAME}_object
                           PRIVATE ${CMAKE_SOURCE_DIR}/include)

add_library(${PROJECT_NAME} SHARED $<TARGET_OBJECTS:${PROJECT_NAME}_object>)
set_target_properties(
  ${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY
                             "${CMAKE_BINARY_DIR}/radix_tree")
if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
  set_target_properties(${PROJECT_NAME} PROPERTIES LINKER_LANGUAGE C)
endif()

if(NOT APPLE)
  target_link_options(${PROJECT_NAME} PRIVATE
                      LINKER:--version-script=${CMAKE_SOURCE_DIR}/linker.map)
endif()

add_library(${PROJECT_NAME}-static STATIC
            $<TARGET_OBJECTS:${PROJECT_NAME}_object>)
if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
  set_target_properties(${PROJECT_NAME}-static PROPERTIES LINKER_LANGUAGE C)
endif()

if(NOT APPLE AND CMAKE_OBJCOPY)
  add_custom_command(
    TARGET conflict-set-static
    POST_BUILD
    COMMAND
      ${CMAKE_OBJCOPY} --keep-global-symbols=${CMAKE_SOURCE_DIR}/symbols.txt
      $<TARGET_FILE:${PROJECT_NAME}-static>)
endif()

set(TEST_FLAGS -Wall -Wextra -Wunreachable-code -Wpedantic -UNDEBUG)

include(CTest)

if(BUILD_TESTING)

  # Shared library version of FoundationDB's skip list implementation
  add_library(skip_list SHARED SkipList.cpp)
  target_compile_options(skip_list PRIVATE -fPIC -fno-exceptions
                                           -fvisibility=hidden)
  target_include_directories(skip_list PUBLIC ${CMAKE_SOURCE_DIR}/include)
  set_target_properties(skip_list PROPERTIES LIBRARY_OUTPUT_DIRECTORY
                                             "${CMAKE_BINARY_DIR}/skip_list")
  set_target_properties(skip_list PROPERTIES OUTPUT_NAME ${PROJECT_NAME})
  set_target_properties(skip_list PROPERTIES VERSION ${PROJECT_VERSION}
                                             SOVERSION ${PROJECT_VERSION_MAJOR})

  # Shared library version of a std::unordered_map-based conflict set (point
  # queries only)
  add_library(hash_table SHARED HashTable.cpp)
  target_compile_options(hash_table PRIVATE -fPIC -fno-exceptions
                                            -fvisibility=hidden)
  target_include_directories(hash_table PUBLIC ${CMAKE_SOURCE_DIR}/include)
  set_target_properties(hash_table PROPERTIES LIBRARY_OUTPUT_DIRECTORY
                                              "${CMAKE_BINARY_DIR}/hash_table")
  set_target_properties(hash_table PROPERTIES OUTPUT_NAME ${PROJECT_NAME})
  set_target_properties(
    hash_table PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION
                                                     ${PROJECT_VERSION_MAJOR})

  add_executable(conflict_set_main ConflictSet.cpp)
  target_include_directories(conflict_set_main
                             PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
  target_compile_definitions(conflict_set_main PRIVATE ENABLE_MAIN)

  if(NOT APPLE)
    # libfuzzer target, to generate/manage corpus
    set(FUZZ_FLAGS "-fsanitize=fuzzer-no-link,address,undefined")
    include(CheckCXXCompilerFlag)
    cmake_push_check_state()
    set(CMAKE_REQUIRED_LINK_OPTIONS -fsanitize=fuzzer-no-link)
    check_cxx_compiler_flag(-fsanitize=fuzzer-no-link HAS_LIB_FUZZER)
    cmake_pop_check_state()

    if(HAS_LIB_FUZZER)
      add_executable(conflict_set_fuzz_test ConflictSet.cpp)
      target_include_directories(conflict_set_fuzz_test
                                 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
      target_compile_definitions(conflict_set_fuzz_test PRIVATE ENABLE_FUZZ)
      target_compile_options(conflict_set_fuzz_test PRIVATE ${TEST_FLAGS})
      target_compile_options(conflict_set_fuzz_test PRIVATE ${FUZZ_FLAGS})
      target_link_options(conflict_set_fuzz_test PRIVATE ${FUZZ_FLAGS}
                          -fsanitize=fuzzer)
    endif()
  endif()

  # corpus tests

  file(GLOB CORPUS_TESTS ${CMAKE_SOURCE_DIR}/corpus/*)

  add_executable(fuzz_driver ConflictSet.cpp FuzzTestDriver.cpp)
  target_compile_options(fuzz_driver PRIVATE ${TEST_FLAGS})
  if(NOT CMAKE_CROSSCOMPILING)
    target_compile_options(fuzz_driver PRIVATE -fsanitize=address,undefined)
    target_link_options(fuzz_driver PRIVATE -fsanitize=address,undefined)
  endif()
  target_compile_definitions(fuzz_driver PRIVATE ENABLE_FUZZ)
  target_include_directories(fuzz_driver
                             PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
  foreach(TEST ${CORPUS_TESTS})
    get_filename_component(hash ${TEST} NAME)
    add_test(NAME conflict_set_fuzz_${hash} COMMAND fuzz_driver ${TEST})
  endforeach()

  # tsan

  if(NOT CMAKE_CROSSCOMPILING AND NOT CMAKE_BUILD_TYPE STREQUAL Debug)
    add_executable(tsan_driver ConflictSet.cpp FuzzTestDriver.cpp)
    target_compile_options(tsan_driver PRIVATE ${TEST_FLAGS} -fsanitize=thread)
    target_link_options(tsan_driver PRIVATE -fsanitize=thread)
    target_compile_definitions(tsan_driver PRIVATE ENABLE_FUZZ THREAD_TEST)
    target_include_directories(tsan_driver
                               PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
    foreach(TEST ${CORPUS_TESTS})
      get_filename_component(hash ${TEST} NAME)
      add_test(NAME conflict_set_tsan_${hash} COMMAND tsan_driver ${TEST})
    endforeach()
  endif()

  add_executable(driver TestDriver.cpp)
  target_compile_options(driver PRIVATE ${TEST_FLAGS})
  target_link_libraries(driver PRIVATE ${PROJECT_NAME})

  add_executable(script_test ScriptTest.cpp)
  target_compile_options(script_test PRIVATE ${TEST_FLAGS})
  target_link_libraries(script_test PRIVATE ${PROJECT_NAME})

  file(GLOB SCRIPT_TESTS ${CMAKE_SOURCE_DIR}/script_tests/*)
  foreach(TEST ${SCRIPT_TESTS})
    get_filename_component(name ${TEST} NAME)
    add_test(NAME conflict_set_script_${name} COMMAND script_test ${TEST})
  endforeach()

  add_executable(driver_skip_list TestDriver.cpp)
  target_compile_options(driver_skip_list PRIVATE ${TEST_FLAGS})
  target_link_libraries(driver_skip_list PRIVATE skip_list)

  find_program(VALGRIND_EXE valgrind)
  if(VALGRIND_EXE AND NOT CMAKE_CROSSCOMPILING)
    add_test(NAME conflict_set_blackbox_valgrind
             COMMAND ${VALGRIND_EXE} --error-exitcode=99 --
                     $<TARGET_FILE:driver> ${CORPUS_TESTS})
  endif()

  foreach(TEST ${CORPUS_TESTS})
    get_filename_component(hash ${TEST} NAME)
    add_test(NAME conflict_set_blackbox_${hash} COMMAND driver ${TEST})
    add_test(NAME skip_list_${hash} COMMAND driver_skip_list ${TEST})
  endforeach()

  # api smoke tests

  # c90
  add_executable(conflict_set_c_api_test conflict_set_c_api_test.c)
  target_compile_options(conflict_set_c_api_test PRIVATE ${TEST_FLAGS})
  target_link_libraries(conflict_set_c_api_test PRIVATE ${PROJECT_NAME})
  set_target_properties(conflict_set_c_api_test PROPERTIES C_STANDARD 90)
  set_target_properties(conflict_set_c_api_test PROPERTIES C_STANDARD_REQUIRED
                                                           ON)
  add_test(NAME conflict_set_c_api_test COMMAND conflict_set_c_api_test)

  # c++98
  add_executable(conflict_set_cxx_api_test conflict_set_cxx_api_test.cpp)
  target_compile_options(conflict_set_cxx_api_test PRIVATE ${TEST_FLAGS})
  target_link_libraries(conflict_set_cxx_api_test PRIVATE ${PROJECT_NAME})
  set_target_properties(conflict_set_cxx_api_test PROPERTIES CXX_STANDARD 98)
  set_target_properties(conflict_set_cxx_api_test
                        PROPERTIES CXX_STANDARD_REQUIRED ON)
  add_test(NAME conflict_set_cxx_api_test COMMAND conflict_set_cxx_api_test)

  if(NOT APPLE AND NOT CMAKE_BUILD_TYPE STREQUAL Debug)
    add_test(
      NAME conflict_set_shared_symbols
      COMMAND ${CMAKE_SOURCE_DIR}/test_symbols.sh
              $<TARGET_FILE:${PROJECT_NAME}> ${CMAKE_SOURCE_DIR}/symbols.txt)
    add_test(
      NAME conflict_set_static_symbols
      COMMAND
        ${CMAKE_SOURCE_DIR}/test_symbols.sh
        $<TARGET_FILE:${PROJECT_NAME}-static> ${CMAKE_SOURCE_DIR}/symbols.txt)
  endif()

  # bench

  add_executable(conflict_set_bench Bench.cpp)
  target_link_libraries(conflict_set_bench PRIVATE ${PROJECT_NAME})
  set_target_properties(conflict_set_bench PROPERTIES SKIP_BUILD_RPATH ON)

  add_executable(real_data_bench RealDataBench.cpp)
  target_link_libraries(real_data_bench PRIVATE ${PROJECT_NAME})
  set_target_properties(real_data_bench PROPERTIES SKIP_BUILD_RPATH ON)
endif()
# packaging

set(CPACK_PACKAGE_CONTACT andrew@weaselab.dev)

set(CPACK_PACKAGE_VENDOR "Weaselab")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE")
set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md")

# rpm
set(CPACK_RPM_PACKAGE_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR})
set(CPACK_RPM_SPEC_INSTALL_POST "/bin/true") # avoid stripping
set(CPACK_RPM_PACKAGE_LICENSE "Apache 2.0")
set(CPACK_RPM_FILE_NAME RPM-DEFAULT)

# deb
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)

include(CPack)

include(GNUInstallDirs)

target_include_directories(
  ${PROJECT_NAME}
  PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
         $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}>)

target_include_directories(
  ${PROJECT_NAME}-static
  PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
         $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}>)

set_target_properties(
  ${PROJECT_NAME} PROPERTIES VERSION ${PROJECT_VERSION}
                             SOVERSION ${PROJECT_VERSION_MAJOR})

install(
  TARGETS ${PROJECT_NAME} ${PROJECT_NAME}-static
  EXPORT conflict-setConfig
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(DIRECTORY include/
        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME})

install(EXPORT conflict-setConfig
        DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/conflict-set/cmake)
