# face-unlock: the compiled half
#
# The Makefile is the entry point and calls this. Everything that has to be
# compiled lives here: the daemon, the agent, the client the shell code uses,
# the PAM module and the tests. The shell code, the units and the models are
# installed by the Makefile, which is where the paths come from.

cmake_minimum_required(VERSION 3.22)
project(face-unlock VERSION 2.1.0 LANGUAGES C CXX)

set(FU_VERSION "${PROJECT_VERSION}" CACHE STRING "Version reported by every binary")

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_C_STANDARD 11)
set(CMAKE_AUTOMOC ON)

if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE RelWithDebInfo)
endif()

include(GNUInstallDirs)

option(FU_BUILD_AGENT "Build the agent (bubble, lock screen, set up window)" ON)
option(FU_BUILD_PAM "Build the PAM module" ON)
option(FU_BUILD_TESTS "Build the tests" ON)

# Where things go at run time. The defaults suit a normal install into /usr;
# the Makefile passes its own values so the two never disagree.
set(FU_LIBEXECDIR "${CMAKE_INSTALL_PREFIX}/lib/face-unlock" CACHE PATH "Helper programs")
set(FU_MODELDIR "${CMAKE_INSTALL_FULL_DATADIR}/face-unlock/models" CACHE PATH "Neural network models")
set(FU_LOCALEDIR "${CMAKE_INSTALL_FULL_LOCALEDIR}" CACHE PATH "Translations")
set(FU_SOCKET "/run/face-unlock/socket" CACHE STRING "The daemon's socket")
set(FU_STATEDIR "/var/lib/face-unlock" CACHE PATH "Face data")
set(FU_CONFIG "/etc/face-unlock/config" CACHE FILEPATH "System settings")
set(FU_PAMDIR "${CMAKE_INSTALL_PREFIX}/lib/security" CACHE PATH "Where PAM modules live")

configure_file(src/shared/buildconfig.h.in ${CMAKE_CURRENT_BINARY_DIR}/buildconfig.h @ONLY)
include_directories(${CMAKE_CURRENT_BINARY_DIR} src/shared)

add_compile_options(-Wall -Wextra -Wno-unused-parameter)

# ---------------------------------------------------------------------------
# The core: camera, vision, liveness, store
# ---------------------------------------------------------------------------

find_package(Qt6 6.5 REQUIRED COMPONENTS Core DBus Network)
# OpenCV 5 split the contour and transform helpers into "geometry".
find_package(OpenCV REQUIRED COMPONENTS core)
set(FU_OPENCV_MODULES core imgproc imgcodecs videoio objdetect dnn)
if(OpenCV_VERSION_MAJOR GREATER_EQUAL 5)
    list(APPEND FU_OPENCV_MODULES geometry)
endif()
find_package(OpenCV REQUIRED COMPONENTS ${FU_OPENCV_MODULES})

# The settings file reader, shared with the agent (which has no use for
# OpenCV).
add_library(fu_common STATIC src/core/keyvalue.cpp)
target_include_directories(fu_common PUBLIC src/core)
target_link_libraries(fu_common PUBLIC Qt6::Core)
set_target_properties(fu_common PROPERTIES POSITION_INDEPENDENT_CODE ON)

add_library(fu_core STATIC
    src/core/settings.cpp
    src/core/camera.cpp
    src/core/vision.cpp
    src/core/liveness.cpp
    src/core/store.cpp
)
target_include_directories(fu_core PUBLIC src/core ${OpenCV_INCLUDE_DIRS})
target_link_libraries(fu_core PUBLIC fu_common Qt6::Core ${OpenCV_LIBS})
set_target_properties(fu_core PROPERTIES POSITION_INDEPENDENT_CODE ON)

# ---------------------------------------------------------------------------
# The daemon
# ---------------------------------------------------------------------------

add_executable(face-unlockd
    src/daemon/job.h
    src/daemon/agentlink.cpp
    src/daemon/main.cpp
    src/daemon/server.cpp
    src/daemon/client.cpp
    src/daemon/scanjob.cpp
    src/daemon/enrolljob.cpp
    src/daemon/polkit.cpp
    src/daemon/userstate.cpp
    src/daemon/system.cpp
)
target_link_libraries(face-unlockd PRIVATE fu_core Qt6::DBus Qt6::Network)
install(TARGETS face-unlockd DESTINATION ${FU_LIBEXECDIR})

# ---------------------------------------------------------------------------
# The client the shell code uses
# ---------------------------------------------------------------------------

add_executable(face-unlock-ctl src/ctl/main.cpp)
target_link_libraries(face-unlock-ctl PRIVATE Qt6::Core Qt6::Network)
install(TARGETS face-unlock-ctl DESTINATION ${FU_LIBEXECDIR})

# ---------------------------------------------------------------------------
# The PAM module
# ---------------------------------------------------------------------------

# The PAM module, and the agent's own lock screen, which checks the password.
find_path(PAM_INCLUDE_DIR security/pam_modules.h REQUIRED)
find_library(PAM_LIBRARY pam REQUIRED)

if(FU_BUILD_PAM)
    find_package(PkgConfig REQUIRED)
    pkg_check_modules(SYSTEMD IMPORTED_TARGET libsystemd)

    add_library(pam_face_unlock MODULE src/pam/pam_face_unlock.c)
    set_target_properties(pam_face_unlock PROPERTIES PREFIX "" C_VISIBILITY_PRESET hidden)
    target_include_directories(pam_face_unlock PRIVATE ${PAM_INCLUDE_DIR})
    target_link_libraries(pam_face_unlock PRIVATE ${PAM_LIBRARY})
    if(SYSTEMD_FOUND)
        target_compile_definitions(pam_face_unlock PRIVATE HAVE_SYSTEMD=1)
        target_link_libraries(pam_face_unlock PRIVATE PkgConfig::SYSTEMD)
    endif()
    install(TARGETS pam_face_unlock DESTINATION ${FU_PAMDIR})

    if(FU_BUILD_TESTS)
        add_executable(pam_harness tests/pam_harness.c)
        target_include_directories(pam_harness PRIVATE ${PAM_INCLUDE_DIR})
        target_link_libraries(pam_harness PRIVATE ${PAM_LIBRARY})
    endif()
endif()

# ---------------------------------------------------------------------------
# The agent
# ---------------------------------------------------------------------------

if(FU_BUILD_AGENT)
    find_package(Qt6 6.5 REQUIRED COMPONENTS Gui Quick WaylandClient)
    # The bubble needs the Wayland surface of its window, which only the
    # private API hands out. It is the same one Plasma itself uses for this.
    # Newer Qt ships the private modules as packages of their own; older Qt
    # (6.8 in Debian 13) brings them along with the public ones.
    if(EXISTS "${Qt6_DIR}/../Qt6GuiPrivate/Qt6GuiPrivateConfig.cmake")
        set(QT_NO_PRIVATE_MODULE_WARNING ON)
        find_package(Qt6 REQUIRED COMPONENTS GuiPrivate WaylandClientPrivate)
    endif()
    find_package(LayerShellQt REQUIRED)
    find_package(KF6I18n REQUIRED)

    qt_add_executable(face-unlock-agent
        src/agent/main.cpp
        src/agent/daemonclient.cpp
        src/agent/userconfig.cpp
        src/agent/agentsocket.cpp
        src/agent/bubblewindow.cpp
        src/agent/bubblecontroller.cpp
        src/agent/bubbleservice.cpp
        src/agent/lockcontroller.cpp
        src/agent/lockwatcher.cpp
        src/agent/lockers.cpp
        src/agent/inputwatcher.cpp
        src/agent/wayland.cpp
        src/agent/sessionlock.cpp
        src/agent/lockscreencontroller.cpp
        src/agent/wallpaper.cpp
        src/agent/locktext.cpp
        src/agent/lockpreview.cpp
        src/agent/enrollcontroller.cpp
    )
    target_include_directories(face-unlock-agent PRIVATE src/agent)
    if(LayerShellQt_VERSION VERSION_GREATER_EQUAL 6.6)
        target_compile_definitions(face-unlock-agent PRIVATE FU_LAYERSHELL_HAS_SCREEN)
    endif()

    qt6_generate_wayland_protocol_client_sources(face-unlock-agent
        FILES ${CMAKE_CURRENT_SOURCE_DIR}/protocols/kde-lockscreen-overlay-v1.xml
              ${CMAKE_CURRENT_SOURCE_DIR}/protocols/ext-idle-notify-v1.xml
              ${CMAKE_CURRENT_SOURCE_DIR}/protocols/ext-session-lock-v1.xml)

    # The QML files sit next to the module's qmldir in the resources, so they
    # see each other (and the Theme singleton) without importing anything.
    # Under /qt/qml, which the engine searches, or an installed agent finds no
    # types (the build directory only works by its FaceUnlock/qmldir).
    set(FU_QML_FILES Theme FaceGlyph LockGlyph CameraGlyph Checkmark Bubble TickRing Enroll PillButton LockScreen LockChoice LockPreview)
    foreach(f ${FU_QML_FILES})
        set_source_files_properties(src/agent/qml/${f}.qml PROPERTIES QT_RESOURCE_ALIAS ${f}.qml)
    endforeach()
    set_source_files_properties(src/agent/qml/Theme.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)

    qt_add_qml_module(face-unlock-agent
        URI FaceUnlock
        VERSION 1.0
        RESOURCE_PREFIX /qt/qml
        SOURCES
            src/agent/previewitem.cpp
            src/agent/previewitem.h
        QML_FILES
            src/agent/qml/Theme.qml
            src/agent/qml/FaceGlyph.qml
            src/agent/qml/LockGlyph.qml
            src/agent/qml/CameraGlyph.qml
            src/agent/qml/Checkmark.qml
            src/agent/qml/Bubble.qml
            src/agent/qml/TickRing.qml
            src/agent/qml/Enroll.qml
            src/agent/qml/PillButton.qml
            src/agent/qml/LockScreen.qml
            src/agent/qml/LockChoice.qml
            src/agent/qml/LockPreview.qml
    )

    target_link_libraries(face-unlock-agent PRIVATE
        Qt6::Gui Qt6::GuiPrivate Qt6::Quick Qt6::DBus Qt6::Network
        Qt6::WaylandClient Qt6::WaylandClientPrivate
        LayerShellQt::Interface
        KF6::I18n KF6::I18nQml
        fu_common
        ${PAM_LIBRARY}
    )
    target_include_directories(face-unlock-agent PRIVATE ${PAM_INCLUDE_DIR})
    install(TARGETS face-unlock-agent DESTINATION ${FU_LIBEXECDIR})
endif()

# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

if(FU_BUILD_TESTS)
    enable_testing()

    add_executable(test_liveness tests/test_liveness.cpp)
    target_link_libraries(test_liveness PRIVATE fu_core)
    add_test(NAME liveness COMMAND test_liveness)

    add_executable(test_store tests/test_store.cpp)
    target_link_libraries(test_store PRIVATE fu_core)
    add_test(NAME store COMMAND test_store)

    add_executable(test_images tests/test_images.cpp)
    target_link_libraries(test_images PRIVATE fu_core)

    if(FU_BUILD_AGENT)
        add_executable(test_lockscreen tests/test_lockscreen.cpp src/agent/lockscreencontroller.cpp)
        target_include_directories(test_lockscreen PRIVATE src/agent ${PAM_INCLUDE_DIR})
        target_link_libraries(test_lockscreen PRIVATE Qt6::Core KF6::I18n ${PAM_LIBRARY})
        add_test(NAME lockscreen COMMAND test_lockscreen)

        add_executable(test_wallpaper tests/test_wallpaper.cpp src/agent/wallpaper.cpp)
        target_include_directories(test_wallpaper PRIVATE src/agent)
        target_link_libraries(test_wallpaper PRIVATE Qt6::Core)
        add_test(NAME wallpaper COMMAND test_wallpaper)

        add_executable(test_lockpreview tests/test_lockpreview.cpp src/agent/lockpreview.cpp)
        target_include_directories(test_lockpreview PRIVATE src/agent)
        target_link_libraries(test_lockpreview PRIVATE Qt6::Gui KF6::I18n)
        add_test(NAME lockpreview COMMAND test_lockpreview)

        add_executable(test_lockers tests/test_lockers.cpp src/agent/lockers.cpp)
        target_include_directories(test_lockers PRIVATE src/agent)
        target_link_libraries(test_lockers PRIVATE Qt6::Core)
        add_test(NAME lockers COMMAND test_lockers)
    endif()
endif()
