cmake_minimum_required(VERSION 3.24) project(Alvin VERSION 0.1.0 DESCRIPTION "Agent Leveraging Vision and Input: cross-platform input synthesis and screen capture" LANGUAGES C CXX) # C only for wayland-scanner output set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) include(GNUInstallDirs) # --------------------------------------------------------------------------- # Options. "AUTO" enables a backend when its dependencies are found. # --------------------------------------------------------------------------- set(ALVIN_WITH_WAYLAND AUTO CACHE STRING "Wayland virtual pointer/keyboard input + monitor listing (ON/OFF/AUTO)") set(ALVIN_WITH_LIBEI AUTO CACHE STRING "libei input via the RemoteDesktop portal (ON/OFF/AUTO)") set(ALVIN_WITH_PORTAL AUTO CACHE STRING "ScreenCast portal + PipeWire capture (ON/OFF/AUTO)") set(ALVIN_WITH_UINPUT ON CACHE BOOL "/dev/uinput input backend") set(ALVIN_WITH_VISION AUTO CACHE STRING "OpenCV-based alvin::vision library (ON/OFF/AUTO)") option(ALVIN_BUILD_EXAMPLES "Build the alvin command-line tool" ON) option(ALVIN_BUILD_TESTS "Build unit tests" ${PROJECT_IS_TOP_LEVEL}) # Resolves an AUTO/ON/OFF option against whether its dependencies were found. function(alvin_resolve option found) if(${option} STREQUAL "AUTO") set(${option}_ENABLED ${found} PARENT_SCOPE) elseif(${option}) if(NOT ${found}) message(FATAL_ERROR "${option}=ON but its dependencies were not found") endif() set(${option}_ENABLED TRUE PARENT_SCOPE) else() set(${option}_ENABLED FALSE PARENT_SCOPE) endif() endfunction() add_library(alvin src/common/core.cpp src/common/frame.cpp src/common/json.cpp src/common/keys.cpp ) add_library(alvin::alvin ALIAS alvin) target_include_directories(alvin PUBLIC $ $ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) target_compile_features(alvin PUBLIC cxx_std_23) if(MSVC) target_compile_options(alvin PRIVATE /W4 /utf-8) else() target_compile_options(alvin PRIVATE -Wall -Wextra -Wpedantic) endif() if(WIN32) # No export macros in the headers; let CMake export every symbol from the DLL. set_target_properties(alvin PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) endif() set(ALVIN_ENABLED_BACKENDS "") # Links a pkg-config dependency privately and records how AlvinConfig.cmake can # find it again: a static libalvin carries no record of its own dependencies. set(ALVIN_CONFIG_DEPENDENCIES "") macro(alvin_private_pkg var) target_link_libraries(alvin PRIVATE PkgConfig::${var}) string(APPEND ALVIN_CONFIG_DEPENDENCIES " pkg_check_modules(${var} REQUIRED QUIET IMPORTED_TARGET ${ARGN})\n") endmacro() # --------------------------------------------------------------------------- # Linux # --------------------------------------------------------------------------- if(CMAKE_SYSTEM_NAME STREQUAL "Linux") find_package(PkgConfig REQUIRED) pkg_check_modules(XKBCOMMON REQUIRED IMPORTED_TARGET xkbcommon) pkg_check_modules(WAYLAND IMPORTED_TARGET wayland-client) pkg_check_modules(WAYLAND_PROTOCOLS wayland-protocols) find_program(WAYLAND_SCANNER wayland-scanner) pkg_check_modules(GIO IMPORTED_TARGET gio-2.0 gio-unix-2.0) pkg_check_modules(PIPEWIRE IMPORTED_TARGET libpipewire-0.3) pkg_check_modules(LIBEI IMPORTED_TARGET libei-1.0) set(_wayland_found FALSE) if(WAYLAND_FOUND AND WAYLAND_PROTOCOLS_FOUND AND WAYLAND_SCANNER) set(_wayland_found TRUE) endif() set(_portal_found FALSE) if(GIO_FOUND AND PIPEWIRE_FOUND) set(_portal_found TRUE) endif() set(_libei_found FALSE) if(GIO_FOUND AND LIBEI_FOUND) set(_libei_found TRUE) endif() alvin_resolve(ALVIN_WITH_WAYLAND _wayland_found) alvin_resolve(ALVIN_WITH_PORTAL _portal_found) alvin_resolve(ALVIN_WITH_LIBEI _libei_found) target_sources(alvin PRIVATE src/linux/platform_linux.cpp src/linux/hyprland_windows.cpp src/linux/keycodes.cpp src/linux/xkb.cpp) alvin_private_pkg(XKBCOMMON xkbcommon) if(ALVIN_WITH_WAYLAND_ENABLED) pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) set(_proto_out ${CMAKE_CURRENT_BINARY_DIR}/protocols) file(MAKE_DIRECTORY ${_proto_out}) set(_protocols ${CMAKE_CURRENT_SOURCE_DIR}/protocols/wlr-virtual-pointer-unstable-v1.xml ${CMAKE_CURRENT_SOURCE_DIR}/protocols/virtual-keyboard-unstable-v1.xml ${WAYLAND_PROTOCOLS_DIR}/unstable/xdg-output/xdg-output-unstable-v1.xml) foreach(xml ${_protocols}) get_filename_component(name ${xml} NAME_WE) add_custom_command( OUTPUT ${_proto_out}/${name}-client-protocol.h ${_proto_out}/${name}-protocol.c COMMAND ${WAYLAND_SCANNER} client-header ${xml} ${_proto_out}/${name}-client-protocol.h COMMAND ${WAYLAND_SCANNER} private-code ${xml} ${_proto_out}/${name}-protocol.c DEPENDS ${xml} VERBATIM) list(APPEND _proto_sources ${_proto_out}/${name}-protocol.c) list(APPEND _proto_headers ${_proto_out}/${name}-client-protocol.h) endforeach() # wayland-scanner emits C; build it as a small C object library. add_library(alvin_wayland_protocols OBJECT ${_proto_sources} ${_proto_headers}) target_include_directories(alvin_wayland_protocols PUBLIC ${_proto_out}) target_link_libraries(alvin_wayland_protocols PUBLIC PkgConfig::WAYLAND) set_target_properties(alvin_wayland_protocols PROPERTIES POSITION_INDEPENDENT_CODE ON) target_sources(alvin PRIVATE src/linux/wayland.cpp src/linux/wlr_input.cpp $) target_include_directories(alvin PRIVATE ${_proto_out}) alvin_private_pkg(WAYLAND wayland-client) add_dependencies(alvin alvin_wayland_protocols) target_compile_definitions(alvin PRIVATE ALVIN_HAVE_WAYLAND) list(APPEND ALVIN_ENABLED_BACKENDS "wlr-input" "wayland-monitors") endif() if(ALVIN_WITH_PORTAL_ENABLED OR ALVIN_WITH_LIBEI_ENABLED) target_sources(alvin PRIVATE src/linux/portal.cpp) alvin_private_pkg(GIO gio-2.0 gio-unix-2.0) endif() if(ALVIN_WITH_PORTAL_ENABLED) target_sources(alvin PRIVATE src/linux/pipewire_capture.cpp) alvin_private_pkg(PIPEWIRE libpipewire-0.3) target_compile_definitions(alvin PRIVATE ALVIN_HAVE_PORTAL) list(APPEND ALVIN_ENABLED_BACKENDS "portal-capture") endif() if(ALVIN_WITH_LIBEI_ENABLED) target_sources(alvin PRIVATE src/linux/libei_input.cpp) alvin_private_pkg(LIBEI libei-1.0) target_compile_definitions(alvin PRIVATE ALVIN_HAVE_LIBEI) if(LIBEI_VERSION VERSION_GREATER_EQUAL 1.6) target_compile_definitions(alvin PRIVATE ALVIN_LIBEI_HAS_TEXT) endif() list(APPEND ALVIN_ENABLED_BACKENDS "libei-input") endif() if(ALVIN_WITH_UINPUT) target_sources(alvin PRIVATE src/linux/uinput_input.cpp) target_compile_definitions(alvin PRIVATE ALVIN_HAVE_UINPUT) list(APPEND ALVIN_ENABLED_BACKENDS "uinput-input") endif() list(APPEND ALVIN_ENABLED_BACKENDS "hyprland-windows") # --------------------------------------------------------------------------- # Windows # --------------------------------------------------------------------------- elseif(WIN32) target_sources(alvin PRIVATE src/windows/platform_windows.cpp src/windows/sendinput_input.cpp src/windows/gdi_capture.cpp src/windows/win32_windows.cpp) target_compile_definitions(alvin PRIVATE UNICODE _UNICODE) target_link_libraries(alvin PRIVATE user32 gdi32 dwmapi) list(APPEND ALVIN_ENABLED_BACKENDS "sendinput-input" "gdi-capture" "win32-windows") else() message(FATAL_ERROR "Alvin does not support ${CMAKE_SYSTEM_NAME} yet") endif() # --------------------------------------------------------------------------- # Optional OpenCV integration # --------------------------------------------------------------------------- find_package(OpenCV QUIET COMPONENTS core imgproc imgcodecs) alvin_resolve(ALVIN_WITH_VISION OpenCV_FOUND) if(ALVIN_WITH_VISION_ENABLED) add_library(alvin_vision src/vision/vision.cpp) add_library(alvin::vision ALIAS alvin_vision) target_link_libraries(alvin_vision PUBLIC alvin ${OpenCV_LIBS}) set_target_properties(alvin_vision PROPERTIES EXPORT_NAME vision) if(WIN32) set_target_properties(alvin_vision PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) endif() list(APPEND ALVIN_ENABLED_BACKENDS "vision(OpenCV ${OpenCV_VERSION})") endif() message(STATUS "Alvin backends: ${ALVIN_ENABLED_BACKENDS}") if(ALVIN_BUILD_EXAMPLES) add_subdirectory(examples) endif() if(ALVIN_BUILD_TESTS) enable_testing() add_subdirectory(tests) endif() # --------------------------------------------------------------------------- # Install + find_package(Alvin) support # --------------------------------------------------------------------------- include(CMakePackageConfigHelpers) set(ALVIN_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/Alvin) set(_alvin_install_targets alvin) if(ALVIN_WITH_VISION_ENABLED) list(APPEND _alvin_install_targets alvin_vision) endif() install(TARGETS ${_alvin_install_targets} EXPORT AlvinTargets RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) install(EXPORT AlvinTargets NAMESPACE alvin:: DESTINATION ${ALVIN_CMAKE_DIR}) # Values baked into AlvinConfig.cmake. get_target_property(_alvin_type alvin TYPE) if(_alvin_type STREQUAL "STATIC_LIBRARY" AND NOT ALVIN_CONFIG_DEPENDENCIES STREQUAL "") set(ALVIN_NEEDS_PKGCONFIG TRUE) else() set(ALVIN_NEEDS_PKGCONFIG FALSE) endif() if(ALVIN_WITH_VISION_ENABLED) set(ALVIN_HAS_VISION TRUE) else() set(ALVIN_HAS_VISION FALSE) endif() configure_package_config_file(cmake/AlvinConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/AlvinConfig.cmake INSTALL_DESTINATION ${ALVIN_CMAKE_DIR}) write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/AlvinConfigVersion.cmake COMPATIBILITY SameMinorVersion) # pre-1.0: minor versions may break the API install(FILES ${CMAKE_CURRENT_BINARY_DIR}/AlvinConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/AlvinConfigVersion.cmake DESTINATION ${ALVIN_CMAKE_DIR})