cmake_minimum_required(VERSION 3.20) project(frameyap LANGUAGES C CXX) if(NOT DEFINED FRAMEYAP_VERSION OR FRAMEYAP_VERSION STREQUAL "") # An ISO-like UTC build stamp is both readable and safe in archive filenames. # SOURCE_DATE_EPOCH is honored by CMake's TIMESTAMP for reproducible builds. string(TIMESTAMP FRAMEYAP_VERSION "%Y-%m-%dT%H%M%SZ" UTC) find_package(Git QUIET) if(GIT_FOUND) execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${CMAKE_CURRENT_SOURCE_DIR}" rev-parse --show-toplevel OUTPUT_VARIABLE FRAMEYAP_GIT_ROOT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) if(FRAMEYAP_GIT_ROOT STREQUAL CMAKE_CURRENT_SOURCE_DIR) execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${CMAKE_CURRENT_SOURCE_DIR}" rev-parse --short=8 HEAD OUTPUT_VARIABLE FRAMEYAP_GIT_SHA OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) if(FRAMEYAP_GIT_SHA MATCHES "^[0-9a-f]+$") string(APPEND FRAMEYAP_VERSION "-g${FRAMEYAP_GIT_SHA}") endif() execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${CMAKE_CURRENT_SOURCE_DIR}" status --porcelain --untracked-files=no OUTPUT_VARIABLE FRAMEYAP_GIT_DIRTY OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) if(FRAMEYAP_GIT_DIRTY) string(APPEND FRAMEYAP_VERSION "-dirty") endif() endif() endif() endif() if(NOT FRAMEYAP_VERSION MATCHES "^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]Z(-g[0-9a-f]+)?(-dirty)?$") message(FATAL_ERROR "FRAMEYAP_VERSION must be an ISO-like UTC timestamp: YYYY-MM-DDTHHMMSSZ[-gHASH][-dirty]") endif() message(STATUS "FrameYap build version: ${FRAMEYAP_VERSION}") set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) option(FRAMEYAP_NATIVE "Build opt-in OpenVR/SDL3/Gamescope runtime (no automatic downloads)" OFF) option(FRAMEYAP_UI_TESTS "Build hardware-free panel renderer tests (requires FreeType)" OFF) add_library(frameyap_config src/config.cpp) target_include_directories(frameyap_config PUBLIC src) if(FRAMEYAP_NATIVE OR FRAMEYAP_UI_TESTS) find_package(Freetype REQUIRED) add_library(frameyap_panel src/panel_surface.cpp) target_include_directories(frameyap_panel PUBLIC src) target_link_libraries(frameyap_panel PUBLIC frameyap_config PRIVATE Freetype::Freetype) endif() add_library(frameyap_core src/core.cpp) target_include_directories(frameyap_core PUBLIC src) add_executable(frameyap src/main.cpp) target_link_libraries(frameyap PRIVATE frameyap_core) target_compile_definitions(frameyap PRIVATE FRAMEYAP_VERSION="${FRAMEYAP_VERSION}") if(CMAKE_SYSTEM_NAME STREQUAL "Linux") add_library(frameyap_mount src/mount.cpp src/laser_setting.cpp) target_include_directories(frameyap_mount PUBLIC src) add_library(frameyap_worker src/worker.cpp) target_include_directories(frameyap_worker PUBLIC src) endif() if(FRAMEYAP_NATIVE) if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") message(FATAL_ERROR "Native POC supports Linux only") endif() find_package(PkgConfig REQUIRED) find_package(Vulkan REQUIRED) pkg_check_modules(SDL3 REQUIRED IMPORTED_TARGET sdl3>=3.2) # SDL's .pc can inject its producer-prefix RPATH. Let CMake manage build # RPATH and install only our relative path, never a developer environment. get_target_property(SDL3_LINK_OPTIONS PkgConfig::SDL3 INTERFACE_LINK_OPTIONS) if(SDL3_LINK_OPTIONS) list(FILTER SDL3_LINK_OPTIONS EXCLUDE REGEX "^-Wl,-rpath,") set_property(TARGET PkgConfig::SDL3 PROPERTY INTERFACE_LINK_OPTIONS "${SDL3_LINK_OPTIONS}") endif() pkg_check_modules(WAYLAND REQUIRED IMPORTED_TARGET wayland-client) pkg_check_modules(XCB REQUIRED IMPORTED_TARGET xcb) find_program(WAYLAND_SCANNER wayland-scanner REQUIRED) set(OPENVR_ROOT "" CACHE PATH "Explicit standalone OpenVR v2.15.6 SDK root") find_path(OPENVR_INCLUDE_DIR openvr.h HINTS "${OPENVR_ROOT}/headers" REQUIRED) if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") set(OPENVR_PLATFORM linuxarm64) elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$") set(OPENVR_PLATFORM linux64) else() message(FATAL_ERROR "Native POC requires Linux ARM64 or x86-64") endif() find_library(OPENVR_LIBRARY openvr_api HINTS "${OPENVR_ROOT}/lib/${OPENVR_PLATFORM}" "${OPENVR_ROOT}/bin/${OPENVR_PLATFORM}" REQUIRED) set(PROTOCOL "${CMAKE_CURRENT_SOURCE_DIR}/protocol/gamescope-input-method.xml") add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method-client.h" COMMAND "${WAYLAND_SCANNER}" client-header "${PROTOCOL}" "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method-client.h" DEPENDS "${PROTOCOL}" VERBATIM) add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method.c" COMMAND "${WAYLAND_SCANNER}" private-code "${PROTOCOL}" "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method.c" DEPENDS "${PROTOCOL}" VERBATIM) target_sources(frameyap PRIVATE src/runtime.cpp src/overlay.cpp src/overlay_texture.cpp src/audio.cpp src/text_input.cpp src/focus_guard.cpp "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method.c" "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method-client.h") target_include_directories(frameyap PRIVATE "${OPENVR_INCLUDE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}") target_link_libraries(frameyap PRIVATE frameyap_worker frameyap_mount frameyap_panel PkgConfig::SDL3 PkgConfig::WAYLAND PkgConfig::XCB Vulkan::Vulkan "${OPENVR_LIBRARY}") target_compile_definitions(frameyap PRIVATE FRAMEYAP_NATIVE=1) set_target_properties(frameyap PROPERTIES INSTALL_RPATH "$ORIGIN/../lib") endif() install(TARGETS frameyap RUNTIME DESTINATION bin) install(DIRECTORY assets/ DESTINATION assets) install(DIRECTORY python/frameyap DESTINATION python FILES_MATCHING PATTERN "*.py" PATTERN "__pycache__" EXCLUDE) include(CTest) if(BUILD_TESTING AND NOT CMAKE_CROSSCOMPILING) add_test(NAME frameyap.cli COMMAND "${CMAKE_COMMAND}" "-DAPP=$" "-DEXPECTED_VERSION=${FRAMEYAP_VERSION}" -P "${CMAKE_CURRENT_SOURCE_DIR}/tests/cli.cmake") add_test(NAME frameyap.install_preflight COMMAND sh "${CMAKE_CURRENT_SOURCE_DIR}/tests/install-preflight.sh" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/install-preflight.sh") add_executable(frameyap_audio_test tests/audio_test.cpp src/audio.cpp) target_include_directories(frameyap_audio_test PRIVATE src tests/fake_sdl) target_compile_options(frameyap_audio_test PRIVATE -UNDEBUG) add_test(NAME frameyap.audio_lifecycle COMMAND frameyap_audio_test) add_executable(frameyap_core_test tests/core_test.cpp) target_link_libraries(frameyap_core_test PRIVATE frameyap_core) add_test(NAME frameyap.core COMMAND frameyap_core_test) if(TARGET frameyap_panel) add_executable(frameyap_panel_test tests/panel_test.cpp) target_link_libraries(frameyap_panel_test PRIVATE frameyap_panel) target_compile_options(frameyap_panel_test PRIVATE -UNDEBUG) add_test(NAME frameyap.panel COMMAND frameyap_panel_test "${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/Inconsolata-Regular.ttf") endif() if(CMAKE_SYSTEM_NAME STREQUAL "Linux") add_executable(frameyap_config_test tests/config_test.cpp) target_link_libraries(frameyap_config_test PRIVATE frameyap_config) target_compile_options(frameyap_config_test PRIVATE -UNDEBUG) add_test(NAME frameyap.config COMMAND frameyap_config_test "${CMAKE_CURRENT_SOURCE_DIR}/assets") add_executable(frameyap_mount_test tests/mount_test.cpp) target_link_libraries(frameyap_mount_test PRIVATE frameyap_mount) add_test(NAME frameyap.mount COMMAND frameyap_mount_test) add_executable(frameyap_angle_fade_test tests/angle_fade_test.cpp) target_include_directories(frameyap_angle_fade_test PRIVATE src) target_compile_options(frameyap_angle_fade_test PRIVATE -UNDEBUG) add_test(NAME frameyap.angle_fade COMMAND frameyap_angle_fade_test) add_executable(frameyap_laser_setting_test tests/laser_setting_test.cpp) target_link_libraries(frameyap_laser_setting_test PRIVATE frameyap_mount) target_compile_options(frameyap_laser_setting_test PRIVATE -UNDEBUG) add_test(NAME frameyap.laser_setting COMMAND frameyap_laser_setting_test) add_executable(frameyap_lock_test tests/lock_test.cpp) target_include_directories(frameyap_lock_test PRIVATE src) add_test(NAME frameyap.instance_lock COMMAND frameyap_lock_test) endif() add_executable(frameyap_panel_drag_test tests/panel_drag_test.cpp) target_include_directories(frameyap_panel_drag_test PRIVATE src) target_compile_options(frameyap_panel_drag_test PRIVATE -UNDEBUG) add_test(NAME frameyap.panel_drag COMMAND frameyap_panel_drag_test) add_executable(frameyap_gestures_test tests/gestures_test.cpp) target_include_directories(frameyap_gestures_test PRIVATE src) target_compile_options(frameyap_gestures_test PRIVATE -UNDEBUG) add_test(NAME frameyap.gestures COMMAND frameyap_gestures_test) find_package(Python3 3.10 COMPONENTS Interpreter) if(FRAMEYAP_NATIVE) add_executable(frameyap_focus_guard_test tests/focus_guard_test.cpp src/focus_guard.cpp) target_include_directories(frameyap_focus_guard_test PRIVATE src) target_link_libraries(frameyap_focus_guard_test PRIVATE PkgConfig::XCB) add_test(NAME frameyap.focus_guard COMMAND frameyap_focus_guard_test) find_program(XVFB_RUN xvfb-run) if(XVFB_RUN) add_executable(frameyap_focus_guard_xcb_test tests/focus_guard_xcb_test.cpp src/focus_guard.cpp) target_include_directories(frameyap_focus_guard_xcb_test PRIVATE src) target_link_libraries(frameyap_focus_guard_xcb_test PRIVATE PkgConfig::XCB) target_compile_options(frameyap_focus_guard_xcb_test PRIVATE -UNDEBUG) add_test(NAME frameyap.focus_guard_xcb COMMAND "${XVFB_RUN}" -a $) endif() add_executable(frameyap_overlay_texture_test tests/overlay_texture_test.cpp src/overlay_texture.cpp) target_include_directories(frameyap_overlay_texture_test PRIVATE src "${OPENVR_INCLUDE_DIR}" "${Vulkan_INCLUDE_DIRS}") target_compile_options(frameyap_overlay_texture_test PRIVATE -UNDEBUG) # Test-provided Vulkan symbols; no GPU, loader or OpenVR initialization. add_test(NAME frameyap.overlay_texture COMMAND frameyap_overlay_texture_test) add_executable(frameyap_texture_check EXCLUDE_FROM_ALL tests/overlay_texture_gpu_check.cpp src/overlay_texture.cpp) target_include_directories(frameyap_texture_check PRIVATE src "${OPENVR_INCLUDE_DIR}") target_link_libraries(frameyap_texture_check PRIVATE Vulkan::Vulkan) # Explicit build and --run only; never register a hardware check with CTest. pkg_check_modules(WAYLAND_SERVER IMPORTED_TARGET wayland-server) if(WAYLAND_SERVER_FOUND) find_package(Threads REQUIRED) add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method-server.h" COMMAND "${WAYLAND_SCANNER}" server-header "${PROTOCOL}" "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method-server.h" DEPENDS "${PROTOCOL}" VERBATIM) add_executable(frameyap_text_input_test tests/text_input_test.cpp src/text_input.cpp "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method.c" "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method-client.h" "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method-server.h") target_include_directories(frameyap_text_input_test PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") target_link_libraries(frameyap_text_input_test PRIVATE frameyap_core PkgConfig::WAYLAND PkgConfig::WAYLAND_SERVER Threads::Threads) add_test(NAME frameyap.text_input_protocol COMMAND frameyap_text_input_test) set_tests_properties(frameyap.text_input_protocol PROPERTIES TIMEOUT 15) endif() endif() if(Python3_FOUND) add_test(NAME frameyap.action_bindings COMMAND "${Python3_EXECUTABLE}" -m unittest discover -s tests -p test_action_bindings.py) add_test(NAME frameyap.python_worker COMMAND "${Python3_EXECUTABLE}" -m unittest discover -s tests -p test_worker.py) add_test(NAME frameyap.installer COMMAND "${Python3_EXECUTABLE}" -m unittest discover -s tests -p test_installer.py) set_tests_properties(frameyap.action_bindings frameyap.python_worker frameyap.installer PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" TIMEOUT 60) if(TARGET frameyap_worker) add_executable(frameyap_worker_test tests/worker_test.cpp) target_link_libraries(frameyap_worker_test PRIVATE frameyap_worker) target_compile_options(frameyap_worker_test PRIVATE -UNDEBUG) add_test(NAME frameyap.worker_ipc COMMAND frameyap_worker_test "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_worker.py") set_tests_properties(frameyap.worker_ipc PROPERTIES TIMEOUT 30) endif() endif() endif()