#  Copyright (C) 2026 ChiPass Team <contact@chipass.org>
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 2 or (at your option)
#  version 3 of the License.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Project configuration
#
cmake_minimum_required(VERSION 3.26)
project(ChiPass LANGUAGES NONE)
set(PROJECT_APP_ID org.chipass.ChiPass)

enable_language(C CXX)
if (APPLE)
  enable_language(OBJCXX)
endif()


# CMake modules
#
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
include(CMakeDependentOption)
include(CheckPIESupported)
include(CheckCXXSourceCompiles)
include(CheckOBJCXXSourceCompiles)
include(Condition)
include(FeatureSummary)
include(PkgConfigImport)
include(ChiPassVersion)
include(ClangFormat)


# Feature and configuration options
#
if (UNIX AND NOT APPLE)
  set(FREEDESKTOP TRUE)
endif()

chipass_extract_version()

set(CHIPASS_COMPILER_LAUNCHER "" CACHE STRING "Compiler launcher (ccache, sccache)")

option(CHIPASS_WITH_AUTOTYPE "Enable Auto-Type feature" ON)
option(CHIPASS_WITH_BROWSER "Enable browser integration" ON)
cmake_dependent_option(CHIPASS_WITH_BROWSER_PASSKEYS "Enable passkey support for browser integration" ON
  CHIPASS_WITH_BROWSER OFF)
option(CHIPASS_WITH_YUBIKEY "Enable YubiKey HSM support" ON)
option(CHIPASS_WITH_SSHAGENT "Enable OpenSSH agent support" ON)
option(CHIPASS_WITH_KEESHARE "Enable KeeShare support" ON)

if (FREEDESKTOP)
  option(CHIPASS_WITH_FDOSECRETS "Enable freedesktop.org Secret Storage server" ON)
  set(CHIPASS_DESKTOP_TYPES "X11;Wayland" CACHE STRING "Build for desktop type (one or more of: X11, Wayland)")
  set(chipass_valid_desktop_types "X11;Wayland")
  foreach (desktop_type ${CHIPASS_DESKTOP_TYPES})
    if (NOT desktop_type IN_LIST chipass_valid_desktop_types)
      message(FATAL_ERROR "Invalid -DCHIPASS_DESKTOP_TYPES=${CHIPASS_DESKTOP_TYPES}: unknown value ${desktop_type}")
    endif()
  endforeach()
endif()


option(CHIPASS_WITH_NETWORKING "Build with networking support enabled" ON)
cmake_dependent_option(CHIPASS_WITH_UPDATE_CHECK "Build with version update checks enabled" ON
  CHIPASS_WITH_NETWORKING OFF)

option(CHIPASS_WITH_MANUAL "Build the documentation manual" ON)

set(CHIPASS_DIST_TYPE "" CACHE STRING "Build for platform distribution (one of: <empty>, Flatpak, AppBundle)")
set(chipass_valid_dist_types "Flatpak;AppBundle")
if (CHIPASS_DIST_TYPE AND NOT CHIPASS_DIST_TYPE IN_LIST chipass_valid_dist_types)
  message(FATAL_ERROR "Invalid -DCHIPASS_DIST_TYPE=${CHIPASS_DIST_TYPE}: unknown value")
elseif (CHIPASS_DIST_TYPE STREQUAL "Flatpak" AND NOT LINUX)
  message(FATAL_ERROR "Invalid -DCHIPASS_DIST_TYPE=${CHIPASS_DIST_TYPE}: must build for Linux")
elseif (CHIPASS_DIST_TYPE STREQUAL "AppBundle" AND NOT APPLE)
  message(FATAL_ERROR "Invalid -DCHIPASS_DIST_TYPE=${CHIPASS_DIST_TYPE}: must build for macOS")
endif()

option(CHIPASS_WITH_TESTS "Build automatic tests" ON)
option(CHIPASS_WITH_GUI_TESTS "Build automatic GUI tests" ON)

# These options are primarily intended for builds with Nix or Guix, to propagate dependencies.
set(CHIPASS_WL_COPY_EXECUTABLE "wl-copy" CACHE STRING "Name or path to the wl-copy executable")
set(CHIPASS_XCLIP_EXECUTABLE "xclip" CACHE STRING "Name or path to the xclip executable")
mark_as_advanced(CHIPASS_WL_COPY_EXECUTABLE CHIPASS_XCLIP_EXECUTABLE)

set(CHIPASS_SANITIZE "address;leak" CACHE STRING "Sanitizers to enable for this build")
mark_as_advanced(CHIPASS_SANITIZE)


# Dependencies
#
find_package(PkgConfig)
set_package_properties(PkgConfig PROPERTIES
  URL "https://www.freedesktop.org/wiki/Software/pkg-config/"
  DESCRIPTION "Library metadata manager"
  PURPOSE "Discovering dependencies"
  TYPE REQUIRED
)

pkg_config_import(botan MODULES botan-3)
if (TARGET PkgConfig::botan)
  set(CHIPASS_USE_BOTAN3 ON)
else()
  pkg_config_import(botan MODULES botan-2)
endif()
set_package_properties(botan PROPERTIES
  URL "https://botan.randombit.net"
  DESCRIPTION "C++ cryptography library"
  PURPOSE "Encrypting the database and communications"
  TYPE REQUIRED
)

pkg_config_import(libargon2)
set_package_properties(libargon2 PROPERTIES
  URL "https://github.com/P-H-C/phc-winner-argon2"
  DESCRIPTION "Password hashing function"
  PURPOSE "Key derivation for KDBX4 databases"
  TYPE REQUIRED
)

pkg_config_import(zlib)
set_package_properties(zlib PROPERTIES
  URL "https://github.com/madler/zlib"
  DESCRIPTION "A massively spiffy yet delicately unobtrusive compression library"
  PURPOSE "Handling Gzip-compressed files"
  TYPE REQUIRED
)

pkg_config_import(minizip)
set_package_properties(minizip PROPERTIES
  URL "https://www.winimage.com/zLibDll/minizip.html"
  DESCRIPTION "Minizip: Zip and UnZip additionnal library"
  PURPOSE "Exchanging KeeShare data files"
  TYPE REQUIRED
)

pkg_config_import(libqrencode)
set_package_properties(libqrencode PROPERTIES
  URL "https://fukuchi.org/en/works/qrencode/index.html"
  DESCRIPTION "A fast and compact library for encoding data in a QR Code symbol"
  PURPOSE "Displaying QR codes during TOTP configuration"
  TYPE REQUIRED
)

pkg_config_import(readline)
if (TARGET PkgConfig::readline)
  set(CHIPASS_USE_READLINE ON)
endif()
set_package_properties(readline PROPERTIES
  URL "https://tiswww.case.edu/php/chet/readline/rltop.html"
  DESCRIPTION "Line editing and history library (GPL)"
  PURPOSE "Enhancing the CLI command prompt"
  TYPE RECOMMENDED
)

if (FREEDESKTOP)
  pkg_config_import(pcsclite MODULES libpcsclite)
  set_package_properties(pcsclite PROPERTIES
    URL "https://pcsclite.apdu.fr"
    DESCRIPTION "Middleware to access a smart card using SCard API (PC/SC)"
    PURPOSE "Communicating with YubiKey HSM"
    TYPE RECOMMENDED
  )

  find_package(X11 COMPONENTS XTest)
  if (NOT X11_FOUND)
    # Unfortunately, if `FindX11.cmake` (as of CMake 3.28) finds only a subset of the components,
    # it will not define the top-level X11::X11 target, but will define the component target(s) for
    # the components it did find. This then causes an error since the latter are dependent on
    # a non-existent target, and configuration doesn't finish.
    add_library(X11::X11 INTERFACE IMPORTED)
  endif()
  set_package_properties(X11 PROPERTIES
    URL "https://xorg.freedesktop.org/archive/current/doc/libX11/libX11/libX11.html"
    DESCRIPTION "X Window System protocol client library written in the C programming language"
    PURPOSE "Emulating keyboard events on the X11 platform"
    TYPE RECOMMENDED
  )

  pkg_config_import(libusb MODULES libusb-1.0)
  set_package_properties(libusb PROPERTIES
    URL "https://libusb.info"
    DESCRIPTION "C library that provides generic access to USB devices"
    PURPOSE "Detecting USB hotplug events for HSM integration"
    # Should be made optional, but is required at the moment.
    TYPE REQUIRED
  )
endif()

if (APPLE)
  # This is the same library as pcsclite, but provided as a system framework on macOS.
  # It's not optional since if this package fails to find, the build system is faulty.
  find_package(PCSCFramework REQUIRED)
endif()

set(qt_required_components
  Core
  Core5Compat
  Gui
  Widgets
  Svg
  SvgWidgets
  Concurrent
  Network
  LinguistTools
)
set(qt_optional_components
  Test
)
if (FREEDESKTOP)
  list(APPEND qt_required_components DBus)
  list(APPEND qt_optional_components GuiPrivate)
elseif (APPLE)
  # No additional components.
elseif (WIN32)
  list(APPEND qt_required_components GuiPrivate)
endif()

set(Qt6_FIND_QUIETLY ON)
set(QT_NO_PRIVATE_MODULE_WARNING ON)
find_package(Qt6 "6.4.2...<7.0.0"
  COMPONENTS "${qt_required_components}"
  OPTIONAL_COMPONENTS "${qt_optional_components}"
)
if (Qt6_VERSION VERSION_LESS "6.10")
  # Qt6GuiPrivate is only a separate components since Qt 6.10; until then it's a part of Qt6Gui.
  set(Qt6GuiPrivate_FOUND YES)
endif()

set_package_properties(Qt6 PROPERTIES
  URL "https://www.qt.io"
  DESCRIPTION "Cross-platform software library for building GUI applications"
  TYPE REQUIRED
)
foreach (qt_component ${qt_required_components} ${qt_optional_components})
  if (qt_component IN_LIST qt_required_components)
    set(type REQUIRED)
  else()
    set(type RECOMMENDED)
  endif()
  set_package_properties(Qt6${qt_component} PROPERTIES
    URL "https://www.qt.io"
    DESCRIPTION "Qt framework component"
    TYPE ${type}
  )
endforeach()

set_package_properties(OpenGL PROPERTIES
  URL "https://www.opengl.org"
  DESCRIPTION "Cross-platform accelerated graphics API"
  PURPOSE "GUI rendering backend for Qt 6"
)

find_program(ASCIIDOCTOR asciidoctor)
if (ASCIIDOCTOR)
  message(STATUS "Using asciidoctor: ${ASCIIDOCTOR}")
  set_property(GLOBAL APPEND PROPERTY PACKAGES_FOUND Asciidoctor)
else()
  set_property(GLOBAL APPEND PROPERTY PACKAGES_NOT_FOUND Asciidoctor)
endif()
set_package_properties(Asciidoctor PROPERTIES
  URL "https://asciidoc.org/"
  DESCRIPTION "Reference implementation of an AsciiDoc language processor"
  PURPOSE "Rendering documentation"
)

# Feature information
#
message(VERBOSE "Conditional features:")
condition(CHIPASS_ENABLE_AUTOTYPE CHIPASS_WITH_AUTOTYPE AND (NOT FREEDESKTOP OR (X11_FOUND AND Qt6GuiPrivate_FOUND)))
condition(CHIPASS_ENABLE_BROWSER CHIPASS_WITH_BROWSER)
condition(CHIPASS_ENABLE_BROWSER_PASSKEYS CHIPASS_WITH_BROWSER_PASSKEYS)
condition(CHIPASS_ENABLE_YUBIKEY CHIPASS_WITH_YUBIKEY AND (NOT FREEDESKTOP OR pcsclite_FOUND))
condition(CHIPASS_ENABLE_SSHAGENT CHIPASS_WITH_SSHAGENT)
condition(CHIPASS_ENABLE_KEESHARE CHIPASS_WITH_KEESHARE)
condition(CHIPASS_ENABLE_FDOSECRETS CHIPASS_WITH_FDOSECRETS)
condition(CHIPASS_ENABLE_DESKTOP_X11 FREEDESKTOP AND "X11" IN_LIST CHIPASS_DESKTOP_TYPES AND X11_FOUND)
condition(CHIPASS_ENABLE_DESKTOP_WAYLAND FREEDESKTOP AND "Wayland" IN_LIST CHIPASS_DESKTOP_TYPES)
condition(CHIPASS_ENABLE_NETWORKING CHIPASS_WITH_NETWORKING)
condition(CHIPASS_ENABLE_UPDATE_CHECK CHIPASS_WITH_UPDATE_CHECK)
condition(CHIPASS_ENABLE_MANUAL CHIPASS_WITH_MANUAL AND ASCIIDOCTOR)
condition(CHIPASS_ENABLE_TESTS CHIPASS_WITH_TESTS AND Qt6Test_FOUND)
condition(CHIPASS_ENABLE_GUI_TESTS CHIPASS_ENABLE_TESTS AND CHIPASS_WITH_GUI_TESTS)

add_feature_info(Auto-Type CHIPASS_ENABLE_AUTOTYPE "Emulation of keyboard input")
add_feature_info(Browser CHIPASS_ENABLE_BROWSER "Browser integration with KeePassXC-Browser")
add_feature_info(Browser-Passkeys CHIPASS_ENABLE_BROWSER_PASSKEYS "Passkey support in browser integration")
add_feature_info(YubiKey CHIPASS_ENABLE_YUBIKEY "YubiKey HMAC-SHA1 challenge-response database protection")
add_feature_info(SSHAgent CHIPASS_ENABLE_SSHAGENT "OpenSSH Agent integration, compatible with KeeAgent")
add_feature_info(KeeShare CHIPASS_ENABLE_KEESHARE "Sharing integration with KeeShare")
if (FREEDESKTOP)
  add_feature_info(FDOSecrets CHIPASS_ENABLE_FDOSECRETS "freedesktop.org Secret Storage API server")
  add_feature_info(Desktop-X11 CHIPASS_ENABLE_DESKTOP_X11 "Integration with X11 desktop")
  add_feature_info(Desktop-Wayland CHIPASS_ENABLE_DESKTOP_WAYLAND "Integration with Wayland desktop")
endif()
add_feature_info(Networking CHIPASS_ENABLE_NETWORKING "Features that use networking")
add_feature_info(UpdateCheck CHIPASS_ENABLE_UPDATE_CHECK "Automatic update checking")
add_feature_info(Manual CHIPASS_ENABLE_MANUAL "Documentation manual")
add_feature_info(Tests CHIPASS_ENABLE_TESTS "Automatic tests")
add_feature_info(GUITests CHIPASS_ENABLE_GUI_TESTS "Automatic tests that use a GUI environment")

message(STATUS "")
feature_summary(WHAT PACKAGES_FOUND
  DEFAULT_DESCRIPTION)
feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND
  DEFAULT_DESCRIPTION QUIET_ON_EMPTY FATAL_ON_MISSING_REQUIRED_PACKAGES
)
feature_summary(WHAT PACKAGES_NOT_FOUND
  DEFAULT_DESCRIPTION QUIET_ON_EMPTY
)
feature_summary(WHAT ENABLED_FEATURES
  DEFAULT_DESCRIPTION QUIET_ON_EMPTY
)
feature_summary(WHAT DISABLED_FEATURES
  DEFAULT_DESCRIPTION QUIET_ON_EMPTY
)


# Compiler configuration
#
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

if (CHIPASS_COMPILER_LAUNCHER)
  set(CMAKE_C_COMPILER_LAUNCHER "${CHIPASS_COMPILER_LAUNCHER}")
  set(CMAKE_CXX_COMPILER_LAUNCHER "${CHIPASS_COMPILER_LAUNCHER}")
  set(CMAKE_OBJCXX_COMPILER_LAUNCHER "${CHIPASS_COMPILER_LAUNCHER}")
endif()

set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)

set(CMAKE_CXX_STANDARD 20) # Botan3 requires C++20
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(CMAKE_POSITION_INDEPENDENT_CODE ON)
check_pie_supported() # opportunistically enable PIE

if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
  set(common_options -Wall -Wextra)
  if (CMAKE_BUILD_TYPE STREQUAL Debug)
    list(APPEND common_options -Werror -Wno-error=deprecated -Wno-error=deprecated-declarations)
  else()
    list(APPEND common_options -Wno-deprecated -Wno-deprecated-declarations)
  endif()
  add_compile_options(${common_options})
	add_link_options(${common_options})

	set(CMAKE_CXX_FLAGS_DEBUG "-Og -ggdb")
	set(CMAKE_CXX_FLAGS_RELEASE "-O3")
	set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O3 -ggdb")
	set(CMAKE_CXX_FLAGS_MINSIZEREL "-Os")

  string(REPLACE ";" "," sanitizers "${CHIPASS_SANITIZE}")
	set(CMAKE_CXX_FLAGS_SANITIZE "-O1 -fsanitize=${sanitizers}")
  set(CMAKE_CXX_FLAGS_SANITIZE "${CMAKE_CXX_FLAGS_SANITIZE} -fno-omit-frame-pointer -fno-optimize-sibling-calls")
	if (memory IN_LIST CHIPASS_SANITIZE)
		set(CMAKE_CXX_FLAGS_SANITIZE "${CMAKE_CXX_FLAGS_SANITIZE} -fsanitize-memory-track-origins")
	endif()
else()
  message(FATAL_ERROR "${CMAKE_CXX_COMPILER_ID} compiler is not supported")
endif()

foreach (lang C OBJCXX)
  if (lang STREQUAL OBJCXX AND NOT APPLE)
    continue()
  endif()
  if (NOT CMAKE_${lang}_COMPILER_ID STREQUAL CMAKE_CXX_COMPILER_ID)
    message(FATAL_ERROR "CXX and ${lang} compilers must be provided by the same vendor")
  endif()
  set(CMAKE_${lang}_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}")
  set(CMAKE_${lang}_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}")
  set(CMAKE_${lang}_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
  set(CMAKE_${lang}_FLAGS_MINSIZEREL "${CMAKE_CXX_FLAGS_MINSIZEREL}")
  set(CMAKE_${lang}_FLAGS_SANITIZE "${CMAKE_CXX_FLAGS_SANITIZE}")
endforeach()

if (WIN32 AND MINGW)
  # Enable DEP and ASLR on all Windows platforms
  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase")
  if (CMAKE_SIZEOF_VOID_P EQUAL 8)
    # Enable high entropy ASLR on 64-bit Windows platforms
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--high-entropy-va")
  endif()
endif()

if (FREEDESKTOP)
  # Enable ABI-preserving hardening features
  add_compile_options(-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3)
  # Enable stack protector
  add_compile_options(-fstack-protector-strong)
  # Enable early binding and read-only relocations
  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,relro,-z,now")
endif()


# Qt configuration
#
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)

set(qt_compile_definitions
  QT_NO_EXCEPTIONS
  QT_STRICT_ITERATORS
  QT_NO_CAST_TO_ASCII
)
if (NOT CMAKE_BUILD_TYPE STREQUAL Debug)
  list(APPEND qt_compile_definitions QT_NO_DEBUG_OUTPUT)
endif()

set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS ${qt_compile_definitions})


# Installation directories
#
if (WIN32)
  # Improves compatibility with the Nix build, which is used for official Windows builds and releases.
  set(CHIPASS_INSTALL_BINDIR "${CMAKE_INSTALL_BINDIR}")
  set(CHIPASS_INSTALL_DATADIR "${CMAKE_INSTALL_BINDIR}/data")
elseif (APPLE AND CHIPASS_DIST_TYPE STREQUAL "AppBundle")
  set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR})
  set(CMAKE_INSTALL_BINDIR "") # (only actually used for MACOSX_BUNDLE targets)
  set(CHIPASS_BUNDLE_CONTENTS "${PROJECT_NAME}.app/Contents")
  set(CHIPASS_INSTALL_BINDIR "${CHIPASS_BUNDLE_CONTENTS}/MacOS")
  set(CHIPASS_INSTALL_DATADIR "${CHIPASS_BUNDLE_CONTENTS}/Resources")
else()
  include(GNUInstallDirs)
  set(CHIPASS_INSTALL_BINDIR "${CMAKE_INSTALL_BINDIR}")
  set(CHIPASS_INSTALL_DATADIR "${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}")
endif()


# Application components
#
add_subdirectory(src)
add_subdirectory(share)
if (CHIPASS_ENABLE_TESTS)
  enable_testing()
  add_subdirectory(tests)
endif()
if (CHIPASS_ENABLE_MANUAL)
  add_subdirectory(docs)
endif()


# At the moment we always use the vendored dependencies.
#  - ykpass is not available in major distributions like Debian
#  - zxcvbn doesn't play well with cross-compilation under Nix
add_subdirectory(vendor)


# Success message
#
if (CHIPASS_DIST_TYPE)
  message(STATUS "Ready to build ChiPass ${CHIPASS_VERSION} for ${CHIPASS_DIST_TYPE}")
else()
  message(STATUS "Ready to build ChiPass ${CHIPASS_VERSION}")
endif()
