cmake_minimum_required(VERSION 3.18)
project(
  conflict-set
  VERSION 0.0.14
  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)

file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/version.txt ${PROJECT_VERSION})
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.txt.in
               ${CMAKE_CURRENT_SOURCE_DIR}/paper/version.txt)

include(CMakePushCheckState)
include(CheckCXXCompilerFlag)
include(CheckIncludeFileCXX)
include(CheckCXXSourceCompiles)

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(
  -Werror=switch-enum
  -Wswitch-enum
  -Wunused-variable
  -fPIC
  -fdata-sections
  -ffunction-sections
  -fno-jump-tables # https://github.com/llvm/llvm-project/issues/54247
)

if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
  add_link_options("-Wno-unused-command-line-argument")
  find_program(LLVM_OBJCOPY llvm-objcopy)
  if(LLVM_OBJCOPY)
    set(CMAKE_OBJCOPY
        ${LLVM_OBJCOPY}
        CACHE FILEPATH "path to objcopy binary" FORCE)
  endif()
endif()

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
  add_compile_options("-Wno-maybe-uninitialized")
  add_compile_options("-Wno-maybe-musttail-local-addr")
endif()

if(NOT APPLE)
  # This causes some versions of clang to crash on macos
  add_compile_options(-g -fno-omit-frame-pointer)
endif()

set(full_relro_flags "-pie;LINKER:-z,relro,-z,now,-z,noexecstack")
cmake_push_check_state()
list(APPEND CMAKE_REQUIRED_LINK_OPTIONS ${full_relro_flags})
check_cxx_source_compiles("int main(){}" HAS_FULL_RELRO FAIL_REGEX "warning:")
if(HAS_FULL_RELRO)
  add_link_options(${full_relro_flags})
endif()
cmake_pop_check_state()

if(CMAKE_SYSTEM_PROCESSOR STREQUAL aarch64 OR CMAKE_SYSTEM_PROCESSOR STREQUAL
                                              arm64)
  add_compile_options(-mbranch-protection=standard)
else()
  add_compile_options(-fcf-protection)
  set(rewrite_endbr_flags "-fuse-ld=mold;LINKER:-z,rewrite-endbr")
  cmake_push_check_state()
  list(APPEND CMAKE_REQUIRED_LINK_OPTIONS ${rewrite_endbr_flags})
  check_cxx_source_compiles("int main(){}" HAS_REWRITE_ENDBR FAIL_REGEX
                            "warning:")
  if(HAS_REWRITE_ENDBR)
    add_link_options(${rewrite_endbr_flags})
  endif()
  cmake_pop_check_state()
endif()

set(version_script_flags
    LINKER:--version-script=${CMAKE_CURRENT_SOURCE_DIR}/linker.map)
cmake_push_check_state()
list(APPEND CMAKE_REQUIRED_LINK_OPTIONS ${version_script_flags})
check_cxx_source_compiles("int main(){}" HAS_VERSION_SCRIPT FAIL_REGEX
                          "warning:")
cmake_pop_check_state()

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

option(DISABLE_TSAN "Disable TSAN" OFF)

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

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

if(USE_SIMD_FALLBACK)
  add_compile_definitions(USE_SIMD_FALLBACK)
else()
  if(CMAKE_SYSTEM_PROCESSOR STREQUAL x86_64)
    add_compile_options(-mavx)
  endif()
endif()

set(CMAKE_CXX_IMPLICIT_LINK_LIBRARIES "")

add_library(${PROJECT_NAME}-object OBJECT ConflictSet.cpp)
target_compile_options(${PROJECT_NAME}-object PRIVATE -fno-exceptions
                                                      -fvisibility=hidden)
target_include_directories(${PROJECT_NAME}-object
                           PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
if(NOT LD_EXE)
  set(LD_EXE ld)
endif()
add_custom_command(
  OUTPUT ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.o
  COMMAND ${LD_EXE} -r $<TARGET_OBJECTS:${PROJECT_NAME}-object> -o
          ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.o
  DEPENDS $<TARGET_OBJECTS:${PROJECT_NAME}-object>
  COMMAND_EXPAND_LISTS)

add_library(${PROJECT_NAME} SHARED ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.o)
set_target_properties(
  ${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY
                             "${CMAKE_CURRENT_BINARY_DIR}/radix_tree")
if(CMAKE_BUILD_TYPE STREQUAL Debug)
  set_target_properties(${PROJECT_NAME} PROPERTIES LINKER_LANGUAGE CXX)
else()
  set_target_properties(${PROJECT_NAME} PROPERTIES LINKER_LANGUAGE C)
endif()

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

add_library(${PROJECT_NAME}-static STATIC ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.o)
if(CMAKE_BUILD_TYPE STREQUAL Debug)
  set_target_properties(${PROJECT_NAME}-static PROPERTIES LINKER_LANGUAGE CXX)
else()
  set_target_properties(${PROJECT_NAME}-static PROPERTIES LINKER_LANGUAGE C)
endif()
if(NOT APPLE)
  add_custom_command(
    TARGET ${PROJECT_NAME}-static
    POST_BUILD
    COMMAND
      ${CMAKE_OBJCOPY}
      --keep-global-symbols=${CMAKE_CURRENT_SOURCE_DIR}/symbol-exports.txt
      $<TARGET_FILE:${PROJECT_NAME}-static> || echo
      "Proceeding with all symbols global in static library")
endif()

include(CTest)

# disable tests if this is being used through e.g. FetchContent
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)

  add_library(nanobench ${CMAKE_CURRENT_SOURCE_DIR}/nanobench.cpp)

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

  # corpus tests, which are tests curated by libfuzzer. The goal is to get broad
  # coverage with a small number of tests.

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

  # Shared library version of FoundationDB's skip list implementation
  add_library(skip_list SHARED SkipList.cpp)
  target_compile_options(skip_list PRIVATE -fno-exceptions -fvisibility=hidden)
  target_include_directories(skip_list
                             PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
  set_target_properties(
    skip_list PROPERTIES LIBRARY_OUTPUT_DIRECTORY
                         "${CMAKE_CURRENT_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 -fno-exceptions -fvisibility=hidden)
  target_include_directories(hash_table
                             PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
  set_target_properties(
    hash_table PROPERTIES LIBRARY_OUTPUT_DIRECTORY
                          "${CMAKE_CURRENT_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(driver_skip_list TestDriver.cpp)
  target_compile_options(driver_skip_list PRIVATE ${TEST_FLAGS})
  target_link_libraries(driver_skip_list PRIVATE skip_list)

  # enable to test skip list
  if(0)
    foreach(TEST ${CORPUS_TESTS})
      get_filename_component(hash ${TEST} NAME)
      add_test(NAME skip_list_${hash} COMMAND driver_skip_list ${TEST})
    endforeach()
  endif()

  # ad hoc testing
  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)
  target_link_libraries(conflict_set_main PRIVATE nanobench)

  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()

  # whitebox tests
  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 tests
  if(NOT CMAKE_CROSSCOMPILING AND NOT DISABLE_TSAN)
    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()

  # blackbox tests
  add_executable(driver TestDriver.cpp)
  target_compile_options(driver PRIVATE ${TEST_FLAGS})
  target_link_libraries(driver PRIVATE ${PROJECT_NAME})
  foreach(TEST ${CORPUS_TESTS})
    get_filename_component(hash ${TEST} NAME)
    add_test(NAME conflict_set_blackbox_${hash} COMMAND driver ${TEST})
  endforeach()

  find_program(VALGRIND_EXE valgrind)
  if(VALGRIND_EXE AND NOT CMAKE_CROSSCOMPILING)
    list(LENGTH CORPUS_TESTS len)
    math(EXPR last "${len} - 1")
    set(partition_size 100)
    foreach(i RANGE 0 ${last} ${partition_size})
      list(SUBLIST CORPUS_TESTS ${i} ${partition_size} partition)
      add_test(NAME conflict_set_blackbox_valgrind_${i}
               COMMAND ${VALGRIND_EXE} --error-exitcode=99 --
                       $<TARGET_FILE:driver> ${partition})
    endforeach()
  endif()

  # scripted tests. Written manually to fill in anything libfuzzer couldn't
  # find.
  if(NOT CMAKE_CROSSCOMPILING)
    find_package(Python3 REQUIRED COMPONENTS Interpreter)
    set_property(
      DIRECTORY
      APPEND
      PROPERTY CMAKE_CONFIGURE_DEPENDS
               ${CMAKE_CURRENT_SOURCE_DIR}/test_conflict_set.py)
    execute_process(
      COMMAND ${Python3_EXECUTABLE}
              ${CMAKE_CURRENT_SOURCE_DIR}/test_conflict_set.py list
      OUTPUT_VARIABLE SCRIPT_TESTS)
    foreach(TEST ${SCRIPT_TESTS})
      add_test(
        NAME script_test_${TEST}
        COMMAND
          ${Python3_EXECUTABLE}
          ${CMAKE_CURRENT_SOURCE_DIR}/test_conflict_set.py test ${TEST}
          --build-dir ${CMAKE_CURRENT_BINARY_DIR})
      if(VALGRIND_EXE AND NOT CMAKE_CROSSCOMPILING)
        add_test(
          NAME script_test_${TEST}_valgrind
          COMMAND
            ${VALGRIND_EXE} ${Python3_EXECUTABLE}
            ${CMAKE_CURRENT_SOURCE_DIR}/test_conflict_set.py test ${TEST}
            --build-dir ${CMAKE_CURRENT_BINARY_DIR})
      endif()
    endforeach()
  endif()

  # 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)

  # symbol visibility tests
  if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
    if(APPLE)
      set(symbol_exports ${CMAKE_CURRENT_SOURCE_DIR}/apple-symbol-exports.txt)
      set(symbol_imports ${CMAKE_CURRENT_SOURCE_DIR}/apple-symbol-imports.txt)
    else()
      set(symbol_exports ${CMAKE_CURRENT_SOURCE_DIR}/symbol-exports.txt)
      if(CMAKE_SYSTEM_PROCESSOR STREQUAL aarch64)
        set(symbol_imports
            ${CMAKE_CURRENT_SOURCE_DIR}/aarch64-symbol-imports.txt)
      else()
        set(symbol_imports ${CMAKE_CURRENT_SOURCE_DIR}/symbol-imports.txt)
      endif()
    endif()
    add_test(
      NAME conflict_set_shared_symbols
      COMMAND
        ${CMAKE_CURRENT_SOURCE_DIR}/test_symbols.sh
        $<TARGET_FILE:${PROJECT_NAME}> ${symbol_exports} ${symbol_imports})
    add_test(
      NAME conflict_set_static_symbols
      COMMAND
        ${CMAKE_CURRENT_SOURCE_DIR}/test_symbols.sh
        $<TARGET_FILE:${PROJECT_NAME}-static> ${symbol_exports}
        ${symbol_imports})
  endif()

  if(NOT CMAKE_CROSSCOMPILING)
    find_program(HARDENING_CHECK hardening-check)
    if(HARDENING_CHECK)
      add_test(NAME hardening_check
               COMMAND ${HARDENING_CHECK} $<TARGET_FILE:${PROJECT_NAME}>
                       --nofortify --nostackprotector)
    endif()
  endif()

  # bench
  add_executable(conflict_set_bench Bench.cpp)
  target_link_libraries(conflict_set_bench PRIVATE ${PROJECT_NAME} nanobench)
  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)

  # fuzzer-based perf
  add_executable(driver_perf TestDriver.cpp)
  target_compile_definitions(driver_perf PRIVATE PERF_TEST=1)
  target_link_libraries(driver_perf PRIVATE ${PROJECT_NAME})

  # server bench
  add_executable(server_bench ServerBench.cpp)
  target_link_libraries(server_bench PRIVATE ${PROJECT_NAME})
  set_target_properties(server_bench PROPERTIES SKIP_BUILD_RPATH ON)

  add_executable(interleaving_test InterleavingTest.cpp)
  # work around lack of musttail for gcc
  if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_BUILD_TYPE STREQUAL "Debug")
    target_compile_options(interleaving_test PRIVATE -Og
                                                     -foptimize-sibling-calls)
  endif()
  target_link_libraries(interleaving_test PRIVATE nanobench)
endif()

# packaging

set(CPACK_PACKAGE_CONTACT andrew@weaselab.dev)
set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME all)

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)
# see *-imports.txt - dependency versions need to be synced with symbol versions
if(CMAKE_SYSTEM_PROCESSOR STREQUAL aarch64)
  set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.17)")
else()
  set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.14)")
endif()

# macos
set(CMAKE_OSX_DEPLOYMENT_TARGET 11.0)
if(APPLE)
  find_program(PANDOC_EXE pandoc)
  if(PANDOC_EXE)
    execute_process(COMMAND ${PANDOC_EXE} ${CMAKE_CURRENT_SOURCE_DIR}/README.md
                            -o ${CMAKE_CURRENT_BINARY_DIR}/README.txt)
    set(CPACK_RESOURCE_FILE_README ${CMAKE_CURRENT_BINARY_DIR}/README.txt)
  endif()
  configure_file(${CMAKE_CURRENT_SOURCE_DIR}/LICENSE
                 ${CMAKE_CURRENT_BINARY_DIR}/LICENSE.txt COPYONLY)
  set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_CURRENT_BINARY_DIR}/LICENSE.txt)
endif()

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 ${PROJECT_NAME}Config
  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 ${PROJECT_NAME}Config
        DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}/cmake)

cpack_add_component(all)
