cmake_minimum_required(VERSION 3.11.4) # for RHEL 8

project(
  linyaps-box
  VERSION 2.0.3
  DESCRIPTION "A simple OCI runtime for desktop applications"
  HOMEPAGE_URL "https://github.com/OpenAtom-Linyaps/linyaps-box"
  LANGUAGES CXX)

# ==============================================================================
# Utilities
# ==============================================================================

# NOTE: Modified from https://www.scivision.dev/cmake-project-is-top-level/
if(CMAKE_VERSION VERSION_LESS 3.21)
  get_property(
    not_top
    DIRECTORY
    PROPERTY PARENT_DIRECTORY)
  if(NOT not_top)
    set(linyaps-box_IS_TOP_LEVEL true)
  else()
    set(linyaps-box_IS_TOP_LEVEL false)
  endif()
endif()

# ==============================================================================
# Build options
# ==============================================================================

option(linyaps-box_STATIC "Build linyaps-box as statically-linked binary." OFF)

option(linyaps-box_ENABLE_SECCOMP "Build linyaps-box with seccomp support." OFF)

option(linyaps-box_ENABLE_CAP "Build linyaps-box with capability support" ON)

option(linyaps-box_ENABLE_UNIT_TESTS "Enable unit tests."
       ${linyaps-box_IS_TOP_LEVEL})

option(linyaps-box_ENABLE_SMOKE_TESTS "Enable smoke tests." OFF)

option(linyaps-box_MAKE_RELEASE "Make release build." OFF)

if(linyaps-box_ENABLE_SMOKE_TESTS OR linyaps-box_ENABLE_UNIT_TESTS)
  set(linyaps-box_ENABLE_TESTING ON)
endif()

option(linyaps-box_ENABLE_COVERAGE "Enable coverage." OFF)

if(linyaps-box_ENABLE_COVERAGE AND NOT linyaps-box_ENABLE_TESTING)
  message(
    FATAL_ERROR
      "linyaps-box_ENABLE_COVERAGE requires linyaps-box_ENABLE_UNIT_TESTS or linyaps-box_ENABLE_SMOKE_TESTS."
  )
endif()

option(linyaps-box_ENABLE_CPACK "Enable CPack." OFF)

if(linyaps-box_ENABLE_CPACK)
  set(linyaps-box_CPACK_PACKAGING_INSTALL_PREFIX
      "/opt/org.openatom.linyaps-box"
      CACHE STRING "Install prefix for package generated by CPack.")
endif()

set(linyaps-box_CLONE_CHILD_STACK_SIZE
    "(1U << 20)"
    CACHE
      STRING
      "DO NOT MODIFY THIS SETTINGS UNLESS YOU KNOW WHAT YOU ARE DOING. Size of child stack in bytes when using clone(2) to create container."
)

set(linyaps-box_STACK_GROWTH_DOWN
    true
    CACHE
      BOOL
      "DO NOT MODIFY THIS SETTINGS UNLESS YOU KNOW WHAT YOU ARE DOING. If stacks grow upward on your processor, set this to false."
)

set(linyaps-box_DEFAULT_LOG_LEVEL
    LOG_WARNING
    CACHE
      STRING
      "The default syslog priority. This is used to filter log messages at runtime time."
)

set(linyaps-box_ACTIVE_LOG_LEVEL
    LOG_WARNING
    CACHE
      STRING
      "The active syslog priority. This is used to filter log messages at compile time."
)

set(linyaps-box_ENABLE_CPM
    ON
    CACHE BOOL "enable CPM")

if(CMAKE_VERSION VERSION_LESS "3.14")
  set(linyaps-box_ENABLE_CPM OFF)
  message(
    STATUS "cmake version ${CMAKE_VERSION} not compatible with CPM.cmake.")
endif()

# if just want to try local packages at first set CPM_USE_LOCAL_PACKAGES ON
set(linyaps-box_CPM_LOCAL_PACKAGES_ONLY
    OFF
    CACHE BOOL "use local packages only")

if(linyaps-box_CPM_LOCAL_PACKAGES_ONLY)
  message(STATUS "CPM is disabled")
  set(linyaps-box_ENABLE_CPM OFF)
endif()

# ==============================================================================

set(linyaps-box_LIBRARY linyaps-box)
set(linyaps-box_LIBRARY_SOURCE
    # find -regex '\./src/.+\.[ch]\(pp\)?' -type f -printf '%P\n'| sort
    src/linyaps_box/app.cpp
    src/linyaps_box/app.h
    src/linyaps_box/cgroup.h
    src/linyaps_box/cgroup_manager.c
    src/linyaps_box/cgroup_manager.h
    src/linyaps_box/command/exec.cpp
    src/linyaps_box/command/exec.h
    src/linyaps_box/command/kill.cpp
    src/linyaps_box/command/kill.h
    src/linyaps_box/command/list.cpp
    src/linyaps_box/command/list.h
    src/linyaps_box/command/options.cpp
    src/linyaps_box/command/options.h
    src/linyaps_box/command/run.cpp
    src/linyaps_box/command/run.h
    src/linyaps_box/config.cpp
    src/linyaps_box/config.h
    src/linyaps_box/container.cpp
    src/linyaps_box/container.h
    src/linyaps_box/container_ref.cpp
    src/linyaps_box/container_ref.h
    src/linyaps_box/container_status.cpp
    src/linyaps_box/container_status.h
    src/linyaps_box/impl/disabled_cgroup_manager.cpp
    src/linyaps_box/impl/disabled_cgroup_manager.h
    src/linyaps_box/impl/json_printer.cpp
    src/linyaps_box/impl/json_printer.h
    src/linyaps_box/impl/status_directory.cpp
    src/linyaps_box/impl/status_directory.h
    src/linyaps_box/impl/table_printer.cpp
    src/linyaps_box/impl/table_printer.h
    src/linyaps_box/interface.cpp
    src/linyaps_box/interface.h
    src/linyaps_box/printer.cpp
    src/linyaps_box/printer.h
    src/linyaps_box/runtime.cpp
    src/linyaps_box/runtime.h
    src/linyaps_box/status_directory.cpp
    src/linyaps_box/status_directory.h
    src/linyaps_box/utils/atomic_write.cpp
    src/linyaps_box/utils/atomic_write.h
    src/linyaps_box/utils/cgroups.cpp
    src/linyaps_box/utils/cgroups.h
    src/linyaps_box/utils/close_range.cpp
    src/linyaps_box/utils/close_range.h
    src/linyaps_box/utils/defer.h
    src/linyaps_box/utils/file_describer.cpp
    src/linyaps_box/utils/file_describer.h
    src/linyaps_box/utils/fstat.cpp
    src/linyaps_box/utils/fstat.h
    src/linyaps_box/utils/inspect.cpp
    src/linyaps_box/utils/inspect.h
    src/linyaps_box/utils/log.cpp
    src/linyaps_box/utils/log.h
    src/linyaps_box/utils/mkdir.cpp
    src/linyaps_box/utils/mkdir.h
    src/linyaps_box/utils/mknod.cpp
    src/linyaps_box/utils/mknod.h
    src/linyaps_box/utils/open_file.cpp
    src/linyaps_box/utils/open_file.h
    src/linyaps_box/utils/platform.cpp
    src/linyaps_box/utils/platform.h
    src/linyaps_box/utils/semver.cpp
    src/linyaps_box/utils/semver.h
    src/linyaps_box/utils/socketpair.cpp
    src/linyaps_box/utils/socketpair.h
    src/linyaps_box/utils/symlink.cpp
    src/linyaps_box/utils/symlink.h
    src/linyaps_box/utils/touch.cpp
    src/linyaps_box/utils/touch.h)

set(LINYAPS_BOX_VERSION ${PROJECT_VERSION})

if(NOT linyaps-box_MAKE_RELEASE)
  message(STATUS "make dev build")

  execute_process(
    COMMAND git rev-parse --short=7 HEAD
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    OUTPUT_VARIABLE GIT_COMMIT_HASH
    OUTPUT_STRIP_TRAILING_WHITESPACE
  )

  set(LINYAPS_BOX_VERSION "${LINYAPS_BOX_VERSION}-dev-${GIT_COMMIT_HASH}")
endif()

configure_file(${CMAKE_CURRENT_SOURCE_DIR}/src/linyaps_box/version.h.in
               ${CMAKE_CURRENT_BINARY_DIR}/src/linyaps_box/version.h @ONLY)

# for gcc 8
set(linyaps-box_LIBRARY_LINK_LIBRARIES "stdc++fs")
set(linyaps-box_LIBRARY_INCLUDE_DIRS PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src"
                                     "${CMAKE_CURRENT_BINARY_DIR}/src")

if(linyaps-box_STATIC)
  set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
endif()

find_package(PkgConfig REQUIRED)

if(linyaps-box_ENABLE_SECCOMP)
  pkg_check_modules(libseccomp REQUIRED IMPORTED_TARGET libseccomp>=2.3.3)
  list(APPEND linyaps-box_LIBRARY_LINK_LIBRARIES PUBLIC PkgConfig::libseccomp)
endif()

if(linyaps-box_ENABLE_CAP)
  pkg_check_modules(libcap REQUIRED IMPORTED_TARGET libcap>=2.25)
  list(APPEND linyaps-box_LIBRARY_LINK_LIBRARIES PUBLIC PkgConfig::libcap)
endif()

if(linyaps-box_ENABLE_CPM)
  list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
  include(CPM)

  CPMFindPackage(
    NAME nlohmann_json
    VERSION 3.11.3
    GITHUB_REPOSITORY nlohmann/json
    GIT_TAG v3.12.0
    EXCLUDE_FROM_ALL ON
    OPTIONS "JSON_BuildTests OFF")
  CPMFindPackage(
    NAME CLI11
    VERSION 2.4.1
    GITHUB_REPOSITORY CLIUtils/CLI11
    GIT_TAG v2.5.0
    EXCLUDE_FROM_ALL ON
    OPTIONS "CLI11_BUILD_TESTS OFF")
endif()

find_package(nlohmann_json 3.11.3 QUIET)
if(NOT nlohmann_json_FOUND)
  add_subdirectory(external/nlohmann_json)
  list(APPEND CMAKE_MODULE_PATH
       "${CMAKE_CURRENT_SOURCE_DIR}/cmake.external/nlohmann_json")
  find_package(nlohmann_json 3.11.3 REQUIRED)
  message(STATUS "use vendor nlohmann_json ${nlohmann_json_VERSION}")
endif()

list(APPEND linyaps-box_LIBRARY_LINK_LIBRARIES PUBLIC
     nlohmann_json::nlohmann_json)

find_package(CLI11 2.4.1 QUIET)
if(NOT CLI11_FOUND)
  add_subdirectory(external/CLI11)
  list(APPEND CMAKE_MODULE_PATH
       "${CMAKE_CURRENT_SOURCE_DIR}/cmake.external/CLI11")
  find_package(CLI11 2.4.1 REQUIRED)
  message(STATUS "use vendor CLI11 ${CLI11_VERSION}")
endif()

list(APPEND linyaps-box_LIBRARY_LINK_LIBRARIES PUBLIC CLI11::CLI11)

add_library("${linyaps-box_LIBRARY}" ${linyaps-box_LIBRARY_SOURCE})
target_include_directories("${linyaps-box_LIBRARY}"
                           ${linyaps-box_LIBRARY_INCLUDE_DIRS})
target_link_libraries("${linyaps-box_LIBRARY}"
                      PRIVATE ${linyaps-box_LIBRARY_LINK_LIBRARIES})
target_compile_features("${linyaps-box_LIBRARY}" PUBLIC cxx_std_17)
set_property(TARGET "${linyaps-box_LIBRARY}" PROPERTY CXX_STANDARD 17)
set_property(TARGET "${linyaps-box_LIBRARY}" PROPERTY CXX_EXTENSIONS OFF)
set_property(TARGET "${linyaps-box_LIBRARY}" PROPERTY CXX_STANDARD_REQUIRED ON)

include(CheckIncludeFileCXX)
check_include_file_cxx("linux/openat2.h" LINYAPS_BOX_HAVE_OPENAT2_H)

if(${LINYAPS_BOX_HAVE_OPENAT2_H})
  target_compile_definitions("${linyaps-box_LIBRARY}"
                             PRIVATE "LINYAPS_BOX_HAVE_OPENAT2_H")
endif()

target_compile_definitions(
  "${linyaps-box_LIBRARY}"
  PRIVATE
    "LINYAPS_BOX_CLONE_CHILD_STACK_SIZE=${linyaps-box_CLONE_CHILD_STACK_SIZE}"
  PRIVATE "LINYAPS_BOX_STACK_GROWTH_DOWN=${linyaps-box_STACK_GROWTH_DOWN}"
  PRIVATE "LINYAPS_BOX_DEFAULT_LOG_LEVEL=${linyaps-box_DEFAULT_LOG_LEVEL}"
  PRIVATE "LINYAPS_BOX_ACTIVE_LOG_LEVEL=${linyaps-box_ACTIVE_LOG_LEVEL}")
target_compile_options("${linyaps-box_LIBRARY}"
                       PRIVATE -fmacro-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=.)
if(linyaps-box_STATIC)
  target_compile_definitions("${linyaps-box_LIBRARY}"
                             PUBLIC "LINYAPS_BOX_STATIC_LINK")
endif()

if(linyaps-box_ENABLE_SECCOMP)
  target_compile_definitions("${linyaps-box_LIBRARY}"
                             PUBLIC LINYAPS_BOX_ENABLE_SECCOMP)
endif()

if(linyaps-box_ENABLE_CAP)
  target_compile_definitions("${linyaps-box_LIBRARY}"
                             PUBLIC LINYAPS_BOX_ENABLE_CAP)
endif()

if(linyaps-box_ENABLE_COVERAGE)
  if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    target_compile_options("${linyaps-box_LIBRARY}"
                           PRIVATE -fprofile-instr-generate -fcoverage-mapping)
    target_link_options("${linyaps-box_LIBRARY}" PUBLIC
                        -fprofile-instr-generate -fcoverage-mapping)
  elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    target_compile_options("${linyaps-box_LIBRARY}" PRIVATE --coverage)
    target_link_options("${linyaps-box_LIBRARY}" PUBLIC --coverage)
  else()
    message(
      FATAL_ERROR "Coverage is not supported for ${CMAKE_CXX_COMPILER_ID}.")
  endif()
endif()

# ==============================================================================

set(linyaps-box_APP ll-box)
set(linyaps-box_APP_SOURCE "./app/${linyaps-box_APP}/src/main.cpp")
set(linyaps-box_APP_LINK_LIBRARIES PRIVATE "${linyaps-box_LIBRARY}")
set(linyaps-box_APP_SOURCE_INCLUDE_DIRS
    PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/app/${linyaps-box_APP}/src")

add_executable("${linyaps-box_APP}" ${linyaps-box_APP_SOURCE})
target_include_directories("${linyaps-box_APP}"
                           ${linyaps-box_APP_SOURCE_INCLUDE_DIRS})
target_link_libraries("${linyaps-box_APP}" ${linyaps-box_APP_LINK_LIBRARIES})
if(linyaps-box_STATIC)
  target_link_options("${linyaps-box_APP}" PRIVATE -static -static-libgcc
                      -static-libstdc++)
endif()
target_compile_features("${linyaps-box_APP}" PRIVATE cxx_std_17)
set_property(TARGET "${linyaps-box_APP}" PROPERTY CXX_STANDARD 17)
set_property(TARGET "${linyaps-box_APP}" PROPERTY CXX_EXTENSIONS OFF)
set_property(TARGET "${linyaps-box_APP}" PROPERTY CXX_STANDARD_REQUIRED ON)
target_compile_definitions(
  "${linyaps-box_APP}"
  PRIVATE "LINYAPS_BOX_DEFAULT_LOG_LEVEL=${linyaps-box_DEFAULT_LOG_LEVEL}"
  PRIVATE "LINYAPS_BOX_ACTIVE_LOG_LEVEL=${linyaps-box_AVTIVE_LOG_LEVEL}")
target_compile_options("${linyaps-box_APP}"
                       PRIVATE -fmacro-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=.)

# ==============================================================================

include(GNUInstallDirs)

# parameter TYPE was added in CMake 3.14
if(CMAKE_VERSION VERSION_LESS "3.14")
  install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/${linyaps-box_APP}"
          DESTINATION "${CMAKE_INSTALL_BINDIR}")
else()
  install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/${linyaps-box_APP}" TYPE BIN)
endif()

if(linyaps-box_ENABLE_CPACK)
  set(CPACK_PACKAGING_INSTALL_PREFIX
      "${linyaps-box_CPACK_PACKAGING_INSTALL_PREFIX}")
  set(CPACK_PACKAGE_CONTACT "chenlinxuan@uniontech.com")
  set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
  set(CPACK_RPM_PACKAGE_AUTOREQ ON)
  include(CPack)
endif()

# ==============================================================================

if(NOT linyaps-box_ENABLE_TESTING)
  return()
endif()

enable_testing()

include(GoogleTest)

set(linyaps-box_UNIT_TESTS ll-box-ut)
set(linyaps-box_UNIT_TESTS_SOURCE ./tests/ll-box-ut/src/test.cpp)
set(linyaps-box_UNIT_TESTS_LINK_LIBRARIES PRIVATE "${linyaps-box_LIBRARY}")
set(linyaps-box_UNIT_TESTS_SOURCE_INCLUDE_DIRS
    PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests/ll-box-ut/src")

find_package(GTest REQUIRED)

if(CMAKE_VERSION VERSION_LESS 3.20)
  add_library(GTest::gtest_main INTERFACE IMPORTED)
  target_link_libraries(GTest::gtest_main INTERFACE GTest::Main)
endif()

list(APPEND linyaps-box_UNIT_TESTS_LINK_LIBRARIES PRIVATE GTest::gtest_main)

add_executable("${linyaps-box_UNIT_TESTS}" ${linyaps-box_UNIT_TESTS_SOURCE})
target_include_directories("${linyaps-box_UNIT_TESTS}"
                           ${linyaps-box_UNIT_TESTS_SOURCE_INCLUDE_DIRS})
target_link_libraries("${linyaps-box_UNIT_TESTS}"
                      ${linyaps-box_UNIT_TESTS_LINK_LIBRARIES})
target_compile_features("${linyaps-box_UNIT_TESTS}" PRIVATE cxx_std_17)
set_property(TARGET "${linyaps-box_UNIT_TESTS}" PROPERTY CXX_STANDARD 17)
set_property(TARGET "${linyaps-box_UNIT_TESTS}" PROPERTY CXX_EXTENSIONS OFF)
set_property(TARGET "${linyaps-box_UNIT_TESTS}" PROPERTY CXX_STANDARD_REQUIRED
                                                         ON)
target_compile_options("${linyaps-box_UNIT_TESTS}"
                       PRIVATE -fmacro-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=.)

set(GTEST_DISCOVER_TESTS_ARGS "${linyaps-box_UNIT_TESTS}" WORKING_DIRECTORY
                              "${CMAKE_CURRENT_SOURCE_DIR}/tests/ll-box-ut")

if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  list(APPEND GTEST_DISCOVER_TESTS_ARGS PROPERTIES ENVIRONMENT
       "LLVM_PROFILE_FILE=/dev/null")
endif()

gtest_discover_tests(${GTEST_DISCOVER_TESTS_ARGS})

function(setup_linyaps_box_smoke_tests)
  if(NOT linyaps-box_ENABLE_SMOKE_TESTS)
    return()
  endif()

  set(linyaps-box_SMOKE_TESTS
      ./tests/ll-box-st/01-run-whoami.json
      ./tests/ll-box-st/02-check-procfs.json
      ./tests/ll-box-st/03-check-mounts.json
      ./tests/ll-box-st/04-check-noNewPrivs.json
      ./tests/ll-box-st/05-check-env.json
      ./tests/ll-box-st/06-check-cwd.json
      ./tests/ll-box-st/07-check-capability.json
      ./tests/ll-box-st/08-check-umask.json
      ./tests/ll-box-st/09-check-rlimit.json
      ./tests/ll-box-st/10-check-oom.json
      ./tests/ll-box-st/11-output-to-null.json
      ./tests/ll-box-st/12-bind-host-dev.json)

  foreach(test ${linyaps-box_SMOKE_TESTS})
    add_test(
      NAME "${test}"
      COMMAND
        "${CMAKE_CURRENT_SOURCE_DIR}/tests/ll-box-st/ll-box-st"
        "${CMAKE_CURRENT_BINARY_DIR}/${linyaps-box_APP}"
        "${CMAKE_CURRENT_BINARY_DIR}/st-data"
        "${CMAKE_CURRENT_SOURCE_DIR}/${test}")
    list(APPEND linyaps-box_TESTS "${test}")

    set_tests_properties(
      "${test}"
      PROPERTIES
        ENVIRONMENT
        "LSAN_OPTIONS=suppressions=${CMAKE_CURRENT_LIST_DIR}/tests/ll-box-st/glibc_leaks.txt"
    )
  endforeach()
endfunction()

setup_linyaps_box_smoke_tests()

function(setup_linyaps_box_coverage)
  if(NOT linyaps-box_ENABLE_COVERAGE)
    return()
  endif()

  set(COVERAGE_INFO "coverage.info")

  if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    set_tests_properties(
      ${linyaps-box_TESTS}
      PROPERTIES ENVIRONMENT
                 "LLVM_PROFILE_FILE=${CMAKE_CURRENT_BINARY_DIR}/default.profraw"
    )

    find_program(LLVM_PROFDATA llvm-profdata REQUIRED)

    add_custom_command(
      OUTPUT default.profdata
      DEPENDS "${linyaps-box_APP}" test
      COMMAND "${LLVM_PROFDATA}" merge -sparse default.profraw -o
              default.profdata)

    find_program(LLVM_COV llvm-cov REQUIRED)

    add_custom_command(
      OUTPUT coverage.info
      DEPENDS default.profdata
      COMMAND "${LLVM_COV}" show -instr-profile=default.profdata
              -object="$<TARGET_FILE_NAME:${linyaps-box_APP}>" > coverage.info)

    add_custom_command(
      OUTPUT coverage.tar.gz
      DEPENDS default.profdata
      COMMAND rm -rf coverage
      COMMAND
        "${LLVM_COV}" show -instr-profile=default.profdata -format=html
        -object="$<TARGET_FILE_NAME:${linyaps-box_APP}>" -output-dir=coverage
      COMMAND tar -czf coverage-html.tar.gz coverage)

    add_custom_target(coverage DEPENDS coverage.info coverage.tar.gz)
  elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    find_program(LCOV lcov REQUIRED)
    find_program(SED sed REQUIRED)
    add_custom_target(
      coverage
      COMMAND "${LCOV}" --capture --directory . --output-file coverage.info
              --branch-coverage --rc geninfo_unexecuted_blocks=1
      COMMAND "${LCOV}" --remove coverage.info '/usr/*' --output-file
              coverage.info
      WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
      DEPENDS "${linyaps-box_APP}" test)
  else()
    message(
      FATAL_ERROR "Coverage is not supported for ${CMAKE_CXX_COMPILER_ID}.")
  endif()
endfunction()

setup_linyaps_box_coverage()
