cmake_minimum_required(VERSION 3.25)

project(xwmshim VERSION 0.0.0 LANGUAGES C)

include(GNUInstallDirs)
include(CTest)

option(XWMSHIM_BUILD_SHARED "Build the LD_PRELOAD shared object" ON)
option(XWMSHIM_BUILD_BINARY "Build the xws launcher" ON)
option(XWMSHIM_BUILD_TESTS "Build tests" ON)
option(XWMSHIM_BUILD_FUZZERS "Build libFuzzer targets" OFF)
option(XWMSHIM_INSTALL "Enable install rules" ON)
option(XWMSHIM_ENABLE_ASAN "Enable AddressSanitizer and UndefinedBehaviorSanitizer" OFF)
option(XWMSHIM_ENABLE_MSAN "Enable MemorySanitizer" OFF)
option(XWMSHIM_ENABLE_FUZZER "Enable libFuzzer instrumentation" OFF)
set(XWMSHIM_DIST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dist" CACHE PATH "Distribution output directory")
set(XWMSHIM_TARGET_ID "" CACHE STRING "Release target identifier")
set(XWMSHIM_TARGET_ARCH "" CACHE STRING "Release target architecture")
set(XWMSHIM_TARGET_OS "linux" CACHE STRING "Release target OS")
set(XWMSHIM_TARGET_LIBC "" CACHE STRING "Release target libc")
set(XWMSHIM_ALT_SHIM_CC "" CACHE STRING "Compiler for the alternate-libc embedded shim")
set(XWMSHIM_VERSION_OVERRIDE "" CACHE STRING "Release candidate version override")

set(XWMSHIM_EFFECTIVE_VERSION "0.0.0")
if(XWMSHIM_VERSION_OVERRIDE)
  set(XWMSHIM_EFFECTIVE_VERSION "${XWMSHIM_VERSION_OVERRIDE}")
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git")
  execute_process(
    COMMAND git describe --exact-match --tags --match "v[0-9]*.[0-9]*.[0-9]*"
    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
    RESULT_VARIABLE XWMSHIM_GIT_TAG_RESULT
    OUTPUT_VARIABLE XWMSHIM_GIT_TAG
    ERROR_QUIET
    OUTPUT_STRIP_TRAILING_WHITESPACE)
  if(XWMSHIM_GIT_TAG_RESULT EQUAL 0)
    string(REGEX REPLACE "^v" "" XWMSHIM_EFFECTIVE_VERSION "${XWMSHIM_GIT_TAG}")
  endif()
endif()

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS ON)

function(xwmshim_project_warnings target)
  target_compile_options("${target}" PRIVATE
    -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Wstrict-prototypes
    -Wmissing-prototypes -Werror)
endfunction()

function(xwmshim_sanitizers target)
  if(XWMSHIM_ENABLE_ASAN)
    target_compile_options("${target}" PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
    target_link_options("${target}" PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
  endif()
  if(XWMSHIM_ENABLE_MSAN)
    target_compile_options("${target}" PRIVATE -fsanitize=memory -fno-omit-frame-pointer)
    target_link_options("${target}" PRIVATE -fsanitize=memory -fno-omit-frame-pointer)
  endif()
  if(XWMSHIM_ENABLE_FUZZER)
    target_compile_options("${target}" PRIVATE -fsanitize=fuzzer-no-link,address,undefined -fno-omit-frame-pointer)
  endif()
endfunction()

configure_file(src/xwmshim_version.h.in generated/xwmshim_version.h @ONLY)

add_library(xws_core STATIC src/xws_core.c)
target_include_directories(xws_core PUBLIC
  "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>"
  "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/generated>")
xwmshim_project_warnings(xws_core)
xwmshim_sanitizers(xws_core)

if(XWMSHIM_BUILD_SHARED)
  add_library(xwmshim SHARED src/xwmshim.c)
  set_target_properties(xwmshim PROPERTIES
    OUTPUT_NAME xwmshim
    PREFIX ""
    SUFFIX ".so"
    BUILD_RPATH ""
    INSTALL_RPATH "")
  target_include_directories(xwmshim PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
  target_link_libraries(xwmshim PRIVATE dl)
  xwmshim_project_warnings(xwmshim)
  xwmshim_sanitizers(xwmshim)
endif()

if(XWMSHIM_BUILD_BINARY)
  if(NOT TARGET xwmshim)
    message(FATAL_ERROR "XWMSHIM_BUILD_BINARY requires XWMSHIM_BUILD_SHARED so xws can embed the shim")
  endif()

  set(XWMSHIM_ALT_SHIM_SO "${CMAKE_CURRENT_BINARY_DIR}/generated/xwmshim-alt.so")
  set(XWMSHIM_ALT_SHIM_AVAILABLE OFF)
  if(XWMSHIM_ALT_SHIM_CC)
    set(XWMSHIM_ALT_SHIM_AVAILABLE ON)
    add_custom_command(
      OUTPUT "${XWMSHIM_ALT_SHIM_SO}"
      COMMAND "${XWMSHIM_ALT_SHIM_CC}" -shared -fPIC -O2
              "${CMAKE_CURRENT_SOURCE_DIR}/src/xwmshim.c"
              -o "${XWMSHIM_ALT_SHIM_SO}" -ldl
      DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/xwmshim.c"
      VERBATIM)
  endif()

  if(XWMSHIM_TARGET_LIBC STREQUAL "musl")
    set(XWMSHIM_MUSL_SHIM_SO "$<TARGET_FILE:xwmshim>")
    set(XWMSHIM_GNU_SHIM_SO "${XWMSHIM_ALT_SHIM_SO}")
    set(XWMSHIM_GNU_SHIM_IS_ALT ON)
    set(XWMSHIM_MUSL_SHIM_IS_ALT OFF)
  else()
    set(XWMSHIM_GNU_SHIM_SO "$<TARGET_FILE:xwmshim>")
    set(XWMSHIM_MUSL_SHIM_SO "${XWMSHIM_ALT_SHIM_SO}")
    set(XWMSHIM_GNU_SHIM_IS_ALT OFF)
    set(XWMSHIM_MUSL_SHIM_IS_ALT ON)
  endif()

  set(XWMSHIM_EMBEDDED_GNU_SHIM_C "${CMAKE_CURRENT_BINARY_DIR}/generated/xws_embedded_shim_gnu.c")
  set(XWMSHIM_EMBEDDED_MUSL_SHIM_C "${CMAKE_CURRENT_BINARY_DIR}/generated/xws_embedded_shim_musl.c")
  if(XWMSHIM_GNU_SHIM_IS_ALT AND NOT XWMSHIM_ALT_SHIM_AVAILABLE)
    add_custom_command(
      OUTPUT "${XWMSHIM_EMBEDDED_GNU_SHIM_C}"
      COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/scripts/embed_unavailable_shim.sh"
              "${XWMSHIM_EMBEDDED_GNU_SHIM_C}"
              "xws_embedded_shim_gnu"
      DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/scripts/embed_unavailable_shim.sh"
      VERBATIM)
  else()
    if(XWMSHIM_GNU_SHIM_IS_ALT)
      set(XWMSHIM_GNU_SHIM_DEPS xwmshim "${XWMSHIM_ALT_SHIM_SO}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/embed_file.sh")
    else()
      set(XWMSHIM_GNU_SHIM_DEPS xwmshim "${CMAKE_CURRENT_SOURCE_DIR}/scripts/embed_file.sh")
    endif()
    add_custom_command(
      OUTPUT "${XWMSHIM_EMBEDDED_GNU_SHIM_C}"
      COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/scripts/embed_file.sh"
              "${XWMSHIM_GNU_SHIM_SO}"
              "${XWMSHIM_EMBEDDED_GNU_SHIM_C}"
              "xws_embedded_shim_gnu"
      DEPENDS ${XWMSHIM_GNU_SHIM_DEPS}
      VERBATIM)
  endif()

  if(XWMSHIM_MUSL_SHIM_IS_ALT AND NOT XWMSHIM_ALT_SHIM_AVAILABLE)
    add_custom_command(
      OUTPUT "${XWMSHIM_EMBEDDED_MUSL_SHIM_C}"
      COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/scripts/embed_unavailable_shim.sh"
              "${XWMSHIM_EMBEDDED_MUSL_SHIM_C}"
              "xws_embedded_shim_musl"
      DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/scripts/embed_unavailable_shim.sh"
      VERBATIM)
  else()
    if(XWMSHIM_MUSL_SHIM_IS_ALT)
      set(XWMSHIM_MUSL_SHIM_DEPS xwmshim "${XWMSHIM_ALT_SHIM_SO}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/embed_file.sh")
    else()
      set(XWMSHIM_MUSL_SHIM_DEPS xwmshim "${CMAKE_CURRENT_SOURCE_DIR}/scripts/embed_file.sh")
    endif()
    add_custom_command(
      OUTPUT "${XWMSHIM_EMBEDDED_MUSL_SHIM_C}"
      COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/scripts/embed_file.sh"
              "${XWMSHIM_MUSL_SHIM_SO}"
              "${XWMSHIM_EMBEDDED_MUSL_SHIM_C}"
              "xws_embedded_shim_musl"
      DEPENDS ${XWMSHIM_MUSL_SHIM_DEPS}
      VERBATIM)
  endif()

  add_executable(xws src/xws.c "${XWMSHIM_EMBEDDED_GNU_SHIM_C}" "${XWMSHIM_EMBEDDED_MUSL_SHIM_C}")
  target_link_libraries(xws PRIVATE xws_core)
  if(NOT XWMSHIM_ENABLE_ASAN AND NOT XWMSHIM_ENABLE_MSAN)
    target_link_options(xws PRIVATE -static)
  endif()
  xwmshim_project_warnings(xws)
  xwmshim_sanitizers(xws)
endif()

if(BUILD_TESTING AND XWMSHIM_BUILD_TESTS)
  add_executable(test_xws_core tests/test_xws_core.c)
  target_link_libraries(test_xws_core PRIVATE xws_core)
  xwmshim_project_warnings(test_xws_core)
  xwmshim_sanitizers(test_xws_core)
  add_test(NAME xws_core COMMAND test_xws_core)
  set_tests_properties(xws_core PROPERTIES LABELS "unit;local" TIMEOUT 5)

  add_executable(test_xwmshim_raw tests/test_xwmshim_raw.c)
  target_link_libraries(test_xwmshim_raw PRIVATE dl)
  xwmshim_project_warnings(test_xwmshim_raw)
  xwmshim_sanitizers(test_xwmshim_raw)
  add_test(NAME xwmshim_raw COMMAND test_xwmshim_raw)
  set_tests_properties(xwmshim_raw PROPERTIES LABELS "unit;local" TIMEOUT 5)

  if(TARGET xwmshim AND NOT XWMSHIM_ENABLE_ASAN AND NOT XWMSHIM_ENABLE_MSAN)
    add_test(NAME preload_bind_now
      COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_preload_bind_now.sh" "$<TARGET_FILE:xwmshim>")
    set_tests_properties(preload_bind_now PROPERTIES LABELS "smoke;local" TIMEOUT 5)
  endif()

  if(TARGET xws)
    add_test(NAME xws_help COMMAND xws --help)
    set_tests_properties(xws_help PROPERTIES LABELS "smoke;local" TIMEOUT 5 PASS_REGULAR_EXPRESSION "Usage: xws")

    add_test(NAME xws_version COMMAND xws --version)
    set_tests_properties(xws_version PROPERTIES LABELS "smoke;local" TIMEOUT 5 PASS_REGULAR_EXPRESSION "${XWMSHIM_EFFECTIVE_VERSION}")

    if(NOT XWMSHIM_ENABLE_ASAN AND NOT XWMSHIM_ENABLE_MSAN)
      add_test(NAME xws_exec_env COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_xws_exec.sh" "$<TARGET_FILE:xws>")
      set_tests_properties(xws_exec_env PROPERTIES LABELS "smoke;local" TIMEOUT 5)
    endif()
  endif()

  add_test(NAME cmake_option_surface
    COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_cmake_options.sh"
            "${CMAKE_CURRENT_SOURCE_DIR}"
            "${CMAKE_CURRENT_BINARY_DIR}/option-tests"
            "${CMAKE_GENERATOR}")
  set_tests_properties(cmake_option_surface PROPERTIES LABELS "configure;local" TIMEOUT 30)

  add_test(NAME release_matrix_tool_surface
    COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_release_matrix_tools.sh")
  set_tests_properties(release_matrix_tool_surface PROPERTIES LABELS "configure;local" TIMEOUT 5)

  add_test(NAME source_manifest_tracked_only
    COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_source_manifest.sh")
  set_tests_properties(source_manifest_tracked_only PROPERTIES LABELS "packaging;local" TIMEOUT 5)
endif()

if(XWMSHIM_BUILD_FUZZERS)
  add_executable(fuzz_xws_core fuzz/fuzz_xws_core.c)
  target_link_libraries(fuzz_xws_core PRIVATE xws_core)
  target_compile_options(fuzz_xws_core PRIVATE -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer)
  target_link_options(fuzz_xws_core PRIVATE -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer)
  xwmshim_project_warnings(fuzz_xws_core)
endif()

if(XWMSHIM_INSTALL)
  if(TARGET xws)
    install(TARGETS xws RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
  endif()
  if(TARGET xwmshim)
    install(TARGETS xwmshim LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}")
  endif()
  install(FILES
    README.md
    LICENSE
    DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/doc/xwmshim")
endif()
