cmake_minimum_required(VERSION 3.11.4) # for RHEL 8

project(
  linyaps-box
  VERSION 2.3.4
  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." ON)

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

option(linyaps-box_ENABLE_SYSTEMD_INTEGRATION
       "Build with journald (systemd) 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)

option(linyaps-box_ENABLE_SANITIZER
       "Enable address and undefined behavior sanitizers" 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
    warn
    CACHE STRING "Default runtime log level (fatal/error/warn/info/debug).")

set(linyaps-box_ACTIVE_LOG_LEVEL
    info
    CACHE
      STRING
      "Compile-time log level; fatal/error always kept. Default 'info' eliminates debug only."
)

set(linyaps-box_OFFLINE_BUILD
    OFF
    CACHE BOOL "when set, fall back to vendored sources instead of CPM fetch")

set(linyaps-box_HARDENING
    ON
    CACHE BOOL "enable hardening")

set(CMAKE_POSITION_INDEPENDENT_CODE ON)

if(CMAKE_VERSION VERSION_LESS "3.14" AND NOT linyaps-box_OFFLINE_BUILD)
  set(linyaps-box_OFFLINE_BUILD ON)
  message(
    STATUS
      "cmake version ${CMAKE_VERSION} not compatible with CPM.cmake; forcing offline mode."
  )
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")

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

# ==============================================================================
include(DetectArch)

detect_architecture(TARGET_ARCH)

message(STATUS "Detected architecture: ${TARGET_ARCH}")

add_compile_definitions("LINYAPS_BOX_TARGET_ARCH_${TARGET_ARCH}")

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

if(linyaps-box_MAKE_RELEASE AND linyaps-box_HARDENING)
  message(STATUS "applying harden settings")
  set(CMAKE_EXE_LINKER_FLAGS
      "${CMAKE_EXE_LINKER_FLAGS} -pie -Wl,-z,relro -Wl,-z,now")
  add_compile_options(-fstack-protector-strong)
  add_compile_definitions("$<$<NOT:$<CONFIG:Debug>>:_FORTIFY_SOURCE=2>")
endif()

if(linyaps-box_ENABLE_SANITIZER)
  if(linyaps-box_MAKE_RELEASE)
    message(WARNING "enable sanitizer while making a release build")
  endif()

  message(STATUS "enable address and undefined sanitizer")
  add_compile_options(-fsanitize=address,undefined,leak -fno-omit-frame-pointer
                      -g3)
  add_link_options(-fsanitize=address,undefined,leak)
endif()

set(linyaps-box_LIBRARY linyaps-box)
set(linyaps-box_LIBRARY_SOURCE
    # find -regex '\./src/.+\.[c]\(pp\)?' -type f -printf '%P\n'| sort
    src/linyaps_box/app.cpp
    src/linyaps_box/cgroup_manager.cpp
    src/linyaps_box/command/exec.cpp
    src/linyaps_box/command/kill.cpp
    src/linyaps_box/command/list.cpp
    src/linyaps_box/command/options.cpp
    src/linyaps_box/command/run.cpp
    src/linyaps_box/config.cpp
    src/linyaps_box/config/validate.cpp
    src/linyaps_box/container.cpp
    src/linyaps_box/container_monitor.cpp
    src/linyaps_box/container_ref.cpp
    src/linyaps_box/container_status.cpp
    src/linyaps_box/impl/disabled_cgroup_manager.cpp
    src/linyaps_box/infra/process_handle.cpp
    src/linyaps_box/infra/rootfs.cpp
    src/linyaps_box/infra/unix_socket.cpp
    src/linyaps_box/interface.cpp
    src/linyaps_box/io/epoll.cpp
    src/linyaps_box/io/forwarder.cpp
    src/linyaps_box/io/stream.cpp
    src/linyaps_box/log/formatter.cpp
    src/linyaps_box/log/logger.cpp
    src/linyaps_box/log/sink_factory.cpp
    src/linyaps_box/log/sinks/file_sink.cpp
    src/linyaps_box/log/sinks/journald_sink.cpp
    src/linyaps_box/log/sinks/stderr_sink.cpp
    src/linyaps_box/log/sinks/syslog_sink.cpp
    src/linyaps_box/log/utils.cpp
    src/linyaps_box/os/fs.cpp
    src/linyaps_box/os/io.cpp
    src/linyaps_box/os/net.cpp
    src/linyaps_box/os/process.cpp
    src/linyaps_box/os/pty.cpp
    src/linyaps_box/os/system.cpp
    src/linyaps_box/os/termios.cpp
    src/linyaps_box/protocol/message_channel.cpp
    src/linyaps_box/protocol/message.cpp
    src/linyaps_box/protocol/sync_socket_forwarder.cpp
    src/linyaps_box/runtime.cpp
    src/linyaps_box/security/privilege.cpp
    src/linyaps_box/status_directory.cpp
    src/linyaps_box/status_directory_manager.cpp
    src/linyaps_box/terminal.cpp
    src/linyaps_box/utils/cgroups.cpp
    src/linyaps_box/utils/close_range.cpp
    src/linyaps_box/utils/date.cpp
    src/linyaps_box/utils/epoll.cpp
    src/linyaps_box/utils/file_describer.cpp
    src/linyaps_box/utils/mman.cpp
    src/linyaps_box/utils/platform.cpp
    src/linyaps_box/utils/ringbuffer.cpp
    src/linyaps_box/utils/semver.cpp
    src/linyaps_box/utils/session.cpp
    src/linyaps_box/utils/setns.cpp
    src/linyaps_box/utils/signal.cpp
    src/linyaps_box/utils/utils.cpp)

if(NOT linyaps-box_ENABLE_SYSTEMD_INTEGRATION)
  list(REMOVE_ITEM linyaps-box_LIBRARY_SOURCE
       src/linyaps_box/log/sinks/journald_sink.h
       src/linyaps_box/log/sinks/journald_sink.cpp)
endif()

set(LINYAPS_BOX_VERSION ${PROJECT_VERSION})

if(NOT linyaps-box_MAKE_RELEASE)
  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}")
  message(STATUS "make dev build: ${LINYAPS_BOX_VERSION}")
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)
  # TODO: detect seccomp_arch_resolve_name and use if available
endif()

if(linyaps-box_ENABLE_CAP)
  # TODO: maybe remove libcap and implement it by ourself ?
  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_SYSTEMD_INTEGRATION)
  pkg_check_modules(libsystemd REQUIRED IMPORTED_TARGET libsystemd)
  list(APPEND linyaps-box_LIBRARY_LINK_LIBRARIES PRIVATE PkgConfig::libsystemd)
endif()

function(linyaps_box_dependency name min_version repo tag)
  cmake_parse_arguments(ARG "" "" "OPTIONS" ${ARGN})

  find_package(${name} ${min_version} QUIET)
  if(${name}_FOUND)
    message(STATUS "use system ${name} ${${name}_VERSION}")
    return()
  endif()

  if(linyaps-box_OFFLINE_BUILD)
    list(APPEND CMAKE_MODULE_PATH
         "${CMAKE_CURRENT_SOURCE_DIR}/cmake.external/${name}")
    add_subdirectory(external/${name})
    find_package(${name} ${min_version} REQUIRED)
    message(STATUS "use vendor ${name} ${${name}_VERSION}")
  else()
    if(ARG_OPTIONS)
      CPMAddPackage(
        NAME ${name}
        GITHUB_REPOSITORY ${repo}
        GIT_TAG ${tag}
        EXCLUDE_FROM_ALL ON
        OPTIONS ${ARG_OPTIONS})
    else()
      CPMAddPackage(
        NAME ${name}
        GITHUB_REPOSITORY ${repo}
        GIT_TAG ${tag}
        EXCLUDE_FROM_ALL ON)
    endif()
  endif()
endfunction()

if(NOT linyaps-box_OFFLINE_BUILD)
  include(CPM)

  if(linyaps-box_CPM_LOCAL_PACKAGES_ONLY)
    set(CPM_USE_LOCAL_PACKAGES ON)
  endif()
endif()

linyaps_box_dependency(nlohmann_json 3.11.3 nlohmann/json v3.12.0 OPTIONS
                       "JSON_BuildTests OFF")
list(APPEND linyaps-box_LIBRARY_LINK_LIBRARIES PUBLIC
     nlohmann_json::nlohmann_json)

# Need a fix which was introduced after 2.6.0
# https://github.com/CLIUtils/CLI11/commit/7ff65c16f272ea8f0d98bb6458740f626c8924cb
linyaps_box_dependency(CLI11 2.6.0 CLIUtils/CLI11 v2.7.2 OPTIONS
                       "CLI11_BUILD_TESTS OFF")
list(APPEND linyaps-box_LIBRARY_LINK_LIBRARIES CLI11::CLI11)

set(FMT_TARGET
    "$<IF:$<BOOL:${linyaps-box_STATIC}>,fmt::fmt-header-only,fmt::fmt>")

if(linyaps-box_OFFLINE_BUILD)
  # vendored fmt is built as a static library to boost compiling speed
  set(FMT_TARGET fmt::fmt)
  set(OLD_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS})
  set(BUILD_SHARED_LIBS OFF)
endif()

# fmt 10.1.1 announced its version number as 10.1.0
# https://github.com/fmtlib/fmt/blob/f5e54359df4c26b6230fc61d38aa294581393084/include/fmt/core.h#L21
linyaps_box_dependency(fmt 10.1.0 fmtlib/fmt 12.2.0)
if(linyaps-box_OFFLINE_BUILD)
  set(BUILD_SHARED_LIBS ${OLD_BUILD_SHARED_LIBS})
endif()
list(APPEND linyaps-box_LIBRARY_LINK_LIBRARIES ${FMT_TARGET})

linyaps_box_dependency(zeus_expected 1.4.0 zeus-cpp/expected v1.4.0 OPTIONS
                       "BUILD_TESTING OFF")
list(APPEND linyaps-box_LIBRARY_LINK_LIBRARIES zeus::expected)

add_library("${linyaps-box_LIBRARY}" STATIC ${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}"
    "LINYAPS_BOX_STACK_GROWTH_DOWN=${linyaps-box_STACK_GROWTH_DOWN}"
  PUBLIC "LINYAPS_BOX_DEFAULT_LOG_LEVEL_NAME=${linyaps-box_DEFAULT_LOG_LEVEL}"
         "LINYAPS_BOX_ACTIVE_LOG_LEVEL_NAME=${linyaps-box_ACTIVE_LOG_LEVEL}")
target_compile_options("${linyaps-box_LIBRARY}"
                       PRIVATE -fmacro-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=.)
target_compile_definitions(
  "${linyaps-box_LIBRARY}"
  PUBLIC "$<$<BOOL:${linyaps-box_STATIC}>:LINYAPS_BOX_STATIC_LINK>"
         "LINYAPS_BOX_LOG_ENABLE_SOURCE_LOCATION")

if(linyaps-box_ENABLE_SECCOMP)
  target_compile_definitions("${linyaps-box_LIBRARY}"
                             PUBLIC LINYAPS_BOX_ENABLE_SECCOMP)
  if(linyaps-box_HAVE_SECCOMP_ARCH_RESOLVE_NAME)
    target_compile_definitions("${linyaps-box_LIBRARY}"
                               PUBLIC SECCOMP_ARCH_RESOLVE_NAME=1)
  endif()
endif()

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

if(linyaps-box_ENABLE_SYSTEMD_INTEGRATION)
  target_compile_definitions("${linyaps-box_LIBRARY}"
                             PUBLIC LINYAPS_BOX_ENABLE_SYSTEMD_INTEGRATION)
endif()

if(linyaps-box_ENABLE_COVERAGE)
  if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_CXX_COMPILER_ID
                                                   STREQUAL "GNU")
    message(
      FATAL_ERROR "Coverage is not supported for ${CMAKE_CXX_COMPILER_ID}.")
  endif()
  target_compile_options(
    "${linyaps-box_LIBRARY}"
    PRIVATE "$<$<CXX_COMPILER_ID:Clang,AppleClang>:-fprofile-instr-generate>"
            "$<$<CXX_COMPILER_ID:Clang,AppleClang>:-fcoverage-mapping>"
            "$<$<CXX_COMPILER_ID:GNU>:--coverage>")
  target_link_options(
    "${linyaps-box_LIBRARY}"
    PUBLIC
    "$<$<CXX_COMPILER_ID:Clang,AppleClang>:-fprofile-instr-generate>"
    "$<$<CXX_COMPILER_ID:Clang,AppleClang>:-fcoverage-mapping>"
    "$<$<CXX_COMPILER_ID:GNU>:--coverage>")
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})
target_link_options(
  "${linyaps-box_APP}" PRIVATE "$<$<BOOL:${linyaps-box_STATIC}>:-static>"
  "$<$<BOOL:${linyaps-box_STATIC}>:-static-libgcc>"
  "$<$<BOOL:${linyaps-box_STATIC}>:-static-libstdc++>")
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 "heyuming@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()

add_subdirectory(tests)
