Files
baketnkandClaude Opus 5.5 59f4f9cff8 Add read-only Linux system battery reader
Reads the device's own battery from /sys/class/power_supply, skipping
peripheral (scope=Device) supplies and implausible values. Offline tested
against a fake power_supply tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:47:11 -04:00

252 lines
16 KiB
CMake

cmake_minimum_required(VERSION 3.20)
project(frameyap LANGUAGES C CXX)
if(NOT DEFINED FRAMEYAP_VERSION OR FRAMEYAP_VERSION STREQUAL "")
# CMake honors SOURCE_DATE_EPOCH for reproducible UTC timestamps.
string(TIMESTAMP FRAMEYAP_BUILD_STAMP "%Y%m%d%H%M" UTC)
set(FRAMEYAP_VERSION "0.1.${FRAMEYAP_BUILD_STAMP}")
endif()
# The patch is exactly YYYYMMDDHHMM, not a git suffix or a semver prerelease.
if(NOT FRAMEYAP_VERSION MATCHES "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.[1-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$")
message(FATAL_ERROR "FRAMEYAP_VERSION must be MAJOR.MINOR.YYYYMMDDHHMM (numeric, no leading zeros)")
endif()
set(FRAMEYAP_GIT_INFO "")
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)
execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${CMAKE_CURRENT_SOURCE_DIR}" status --porcelain --untracked-files=normal
OUTPUT_VARIABLE FRAMEYAP_GIT_DIRTY OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${CMAKE_CURRENT_SOURCE_DIR}" tag --points-at HEAD
OUTPUT_VARIABLE FRAMEYAP_HEAD_TAGS OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
string(REPLACE "\n" ";" FRAMEYAP_HEAD_TAGS "${FRAMEYAP_HEAD_TAGS}")
if(FRAMEYAP_GIT_SHA MATCHES "^[0-9a-f]+$" AND
(FRAMEYAP_GIT_DIRTY OR NOT "v${FRAMEYAP_VERSION}" IN_LIST FRAMEYAP_HEAD_TAGS))
set(FRAMEYAP_GIT_INFO "git ${FRAMEYAP_GIT_SHA}")
if(FRAMEYAP_GIT_DIRTY)
string(APPEND FRAMEYAP_GIT_INFO " (uncommitted changes)")
endif()
endif()
endif()
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_library(frameyap_paced src/paced_delivery.cpp)
target_link_libraries(frameyap_paced PUBLIC frameyap_core)
target_include_directories(frameyap_paced PUBLIC src)
add_executable(frameyap src/main.cpp src/cli.cpp src/check.cpp src/model_status.cpp)
target_link_libraries(frameyap PRIVATE frameyap_core)
target_compile_definitions(frameyap PRIVATE FRAMEYAP_VERSION="${FRAMEYAP_VERSION}"
FRAMEYAP_GIT_INFO="${FRAMEYAP_GIT_INFO}"
FRAMEYAP_BUILD_ROOT="${CMAKE_CURRENT_BINARY_DIR}"
FRAMEYAP_SOURCE_ROOT="${CMAKE_CURRENT_SOURCE_DIR}")
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)
add_library(frameyap_battery src/battery.cpp)
target_include_directories(frameyap_battery PUBLIC src)
endif()
if(FRAMEYAP_NATIVE)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(FATAL_ERROR "Native runtime 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 runtime 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/controller.cpp src/backend_manager.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_paced frameyap_worker frameyap_mount frameyap_battery 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)
# BackendManager discovers the self-contained installer at <root>/bin/install.sh.
# Its Python payload is embedded; scripts/install_payload.py is producer source,
# not a runtime dependency of the staged installer.
install(PROGRAMS install.sh DESTINATION bin)
install(DIRECTORY assets/ DESTINATION assets)
install(DIRECTORY python/frameyap DESTINATION python FILES_MATCHING PATTERN "*.py" PATTERN "__pycache__" EXCLUDE)
install(FILES scripts/model-status.py scripts/fetch-model.py scripts/backend-service.py DESTINATION scripts)
include(CTest)
if(BUILD_TESTING AND NOT CMAKE_CROSSCOMPILING)
find_package(Python3 3.10 COMPONENTS Interpreter)
add_test(NAME frameyap.cli COMMAND "${CMAKE_COMMAND}" "-DAPP=$<TARGET_FILE:frameyap>"
"-DEXPECTED_VERSION=${FRAMEYAP_VERSION}" -P "${CMAKE_CURRENT_SOURCE_DIR}/tests/cli.cmake")
if(Python3_FOUND)
add_test(NAME frameyap.model_status COMMAND "${CMAKE_COMMAND}" "-DAPP=$<TARGET_FILE:frameyap>"
"-DROOT=${CMAKE_CURRENT_SOURCE_DIR}" "-DSTAGE=${CMAKE_CURRENT_BINARY_DIR}/model-status-test"
-P "${CMAKE_CURRENT_SOURCE_DIR}/tests/model_status.cmake")
endif()
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_cli_test tests/cli_test.cpp src/cli.cpp)
target_include_directories(frameyap_cli_test PRIVATE src)
target_compile_options(frameyap_cli_test PRIVATE -UNDEBUG)
add_test(NAME frameyap.cli_parser COMMAND frameyap_cli_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)
add_executable(frameyap_paced_delivery_test tests/paced_delivery_test.cpp)
target_link_libraries(frameyap_paced_delivery_test PRIVATE frameyap_paced)
target_compile_options(frameyap_paced_delivery_test PRIVATE -UNDEBUG)
add_test(NAME frameyap.paced_delivery COMMAND frameyap_paced_delivery_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")
if(Python3_FOUND)
add_executable(frameyap_backend_manager_test tests/backend_manager_test.cpp src/backend_manager.cpp)
target_include_directories(frameyap_backend_manager_test PRIVATE src)
target_compile_options(frameyap_backend_manager_test PRIVATE -UNDEBUG)
add_test(NAME frameyap.backend_manager COMMAND frameyap_backend_manager_test)
endif()
add_executable(frameyap_controller_test tests/controller_test.cpp src/controller.cpp)
target_link_libraries(frameyap_controller_test PRIVATE frameyap_paced)
target_include_directories(frameyap_controller_test PRIVATE src)
target_compile_options(frameyap_controller_test PRIVATE -UNDEBUG)
add_test(NAME frameyap.controller COMMAND frameyap_controller_test)
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)
add_executable(frameyap_battery_test tests/battery_test.cpp)
target_link_libraries(frameyap_battery_test PRIVATE frameyap_battery)
target_compile_options(frameyap_battery_test PRIVATE -UNDEBUG)
add_test(NAME frameyap.battery COMMAND frameyap_battery_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)
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 $<TARGET_FILE:frameyap_focus_guard_xcb_test>)
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_paced 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)
foreach(test IN ITEMS test_backends.py test_model_files.py test_backend_dispatch.py cli_git_tag.py)
string(REPLACE ".py" "" test_name "${test}")
add_test(NAME "frameyap.${test_name}" COMMAND "${Python3_EXECUTABLE}" -m unittest discover -s tests -p "${test}")
set_tests_properties("frameyap.${test_name}" PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" TIMEOUT 60)
endforeach()
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()