cmake_minimum_required(VERSION 3.10)
project(
  weaseljson
  VERSION 0.0.1
  DESCRIPTION "A constant memory streaming json parser."
  HOMEPAGE_URL "https://git.weaselab.dev/weaselab/weaseljson"
  LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20)

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
  -fno-omit-frame-pointer
  -g)

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

set(relro_flags "LINKER:-z,relro,-z,now,-z,noexecstack")
set(full_relro_flags "-pie;${relro_flags}")
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)
  # -pie only applies to executables; passing it when linking a shared library
  # makes the driver pull in Scrt1.o, which requires main.
  add_link_options("$<$<STREQUAL:$<TARGET_PROPERTY:TYPE>,EXECUTABLE>:-pie>"
                   ${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()

if(CMAKE_SYSTEM_NAME STREQUAL Linux)
  add_link_options(LINKER:--gc-sections)
endif()

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

add_subdirectory(third_party)

set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")

include(CTest)
include(doctest)

add_library(${PROJECT_NAME}-object OBJECT src/lib.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()

set(CMAKE_CXX_IMPLICIT_LINK_LIBRARIES "")

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
  # Depend on the target (for ordering / the Make generator) and on the object
  # files themselves, so the bundle is rebuilt when the objects' contents change
  # (a bare target dependency is only order-only under Ninja).
  DEPENDS ${PROJECT_NAME}-object $<TARGET_OBJECTS:${PROJECT_NAME}-object>
  COMMAND_EXPAND_LISTS)

add_library(${PROJECT_NAME} SHARED ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.o)
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()

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

add_executable(mytest src/test.cpp)
target_include_directories(mytest PRIVATE include)
target_link_libraries(mytest PRIVATE ${PROJECT_NAME} doctest nanobench simdjson)
target_compile_options(mytest PRIVATE ${TEST_FLAGS})
doctest_discover_tests(mytest WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})

find_package(Python3 COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
  add_test(NAME python_bindings
           COMMAND ${Python3_EXECUTABLE}
                   ${CMAKE_CURRENT_SOURCE_DIR}/test_python_bindings.py)
endif()

add_subdirectory(contrib/schemagen)

include(CMakePushCheckState)
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(fuzz src/fuzz.cpp src/lib.cpp)
  target_include_directories(fuzz PRIVATE include)
  target_link_libraries(fuzz PRIVATE simdjson)
  target_compile_options(fuzz PRIVATE -fsanitize=fuzzer ${TEST_FLAGS})
  target_link_options(fuzz PRIVATE -fsanitize=fuzzer)
else()
  add_executable(fuzz src/fuzz.cpp src/lib.cpp src/fuzz_driver.cpp)
  target_include_directories(fuzz PRIVATE include)
  target_link_libraries(fuzz PRIVATE simdjson)
  target_compile_options(fuzz PRIVATE ${TEST_FLAGS})
endif()

# Replay the checked-in corpus through the fuzz target as a regression test.
file(GLOB corpus_files ${CMAKE_CURRENT_SOURCE_DIR}/corpus/*)
if(corpus_files)
  add_test(NAME fuzz_corpus COMMAND fuzz ${corpus_files})
endif()

add_executable(validate src/validate.cpp)
target_link_libraries(validate ${PROJECT_NAME}-static)
target_include_directories(validate PRIVATE include)

# 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)
    # Control flow integrity (CET) is x86-only and branch protection (PAC/BTI)
    # is arm64-only, so ignore whichever doesn't apply.
    if(CMAKE_SYSTEM_PROCESSOR STREQUAL aarch64 OR CMAKE_SYSTEM_PROCESSOR
                                                  STREQUAL arm64)
      set(hardening_check_arch_flags --nocfprotection)
    else()
      # --nobranchprotection was added in a newer version of hardening-check;
      # older versions don't check for it either, so omitting the flag is safe.
      execute_process(
        COMMAND ${HARDENING_CHECK} --help
        OUTPUT_VARIABLE _hardening_check_help
        ERROR_VARIABLE _hardening_check_help)
      if(_hardening_check_help MATCHES "nobranchprotection")
        set(hardening_check_arch_flags --nobranchprotection)
      else()
        set(hardening_check_arch_flags "")
      endif()
    endif()
    add_test(
      NAME hardening_check
      COMMAND ${HARDENING_CHECK} $<TARGET_FILE:${PROJECT_NAME}> --nofortify
              --nostackprotector ${hardening_check_arch_flags})
  endif()
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)
