Files
udpscope/CMakeLists.txt
T
Martino FerrariandClaude Sonnet 4.6 a0fc0762d5 ui: compact cursor and measurement readouts at the top of the dock
Both views are written from the same variable at the same call site, so the dock cannot disagree with the full panels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-11-03 05:22:41 +01:00

325 lines
17 KiB
CMake

cmake_minimum_required(VERSION 3.24)
project(udpscope VERSION 0.1.0 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
endif()
find_package(Threads REQUIRED)
# `CMAKE_INSTALL_DATADIR` (the `share` under the prefix) for the help set's install path.
include(GNUInstallDirs)
# --- libadwaita: the shortcut overview ---------------------------------------
# GTK4's own `GtkShortcutsWindow` is deprecated as of 4.18 and libadwaita's
# `AdwShortcutsDialog` (1.8) is its replacement, so the overview is built from the
# platform widget rather than from a hand-rolled grid. This is the only thing libadwaita
# is used for; `adw_init()` is called for it, which also means the app picks up
# libadwaita's stylesheet.
find_package(PkgConfig REQUIRED)
pkg_check_modules(ADWAITA REQUIRED IMPORTED_TARGET libadwaita-1>=1.8)
# --- deps/plotter: built from the submodule, GTK4 adapter only ----------------
set(PLOT_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(PLOT_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(PLOT_BUILD_GLFW OFF CACHE BOOL "" FORCE)
set(PLOT_BUILD_IMGUI OFF CACHE BOOL "" FORCE)
set(PLOT_BUILD_QT6 OFF CACHE BOOL "" FORCE)
set(PLOT_BUILD_GTK4 ON CACHE BOOL "" FORCE)
set(PLOT_INSTALL OFF CACHE BOOL "" FORCE)
add_subdirectory(deps/plotter EXCLUDE_FROM_ALL)
# --- deps/udpsclient: vendored C99 receiver ----------------------------------
add_library(udpsclient STATIC deps/udpsclient/udps_client.c)
target_include_directories(udpsclient PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/deps/udpsclient)
set_target_properties(udpsclient PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON)
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(udpsclient PRIVATE -Wall -Wextra)
endif()
# --- icons: six SVGs compiled into the binary --------------------------------
# There is no stock icon for an oscilloscope trigger, a pair of cursors or a measurement
# ruler, so this app draws its own. They go in a GResource rather than beside the binary
# because udpscope is run straight out of the build tree and has no install step: a
# search path would have to be located at runtime, and the failure mode of not locating
# it is a silent missing-image placeholder.
find_program(GLIB_COMPILE_RESOURCES NAMES glib-compile-resources REQUIRED)
set(UDPSCOPE_ICON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/icons)
file(GLOB UDPSCOPE_ICON_FILES CONFIGURE_DEPENDS
${UDPSCOPE_ICON_DIR}/scalable/actions/*.svg
${UDPSCOPE_ICON_DIR}/scalable/apps/*.svg)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
COMMAND ${GLIB_COMPILE_RESOURCES}
--target=${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
--sourcedir=${UDPSCOPE_ICON_DIR}
--generate-source
${UDPSCOPE_ICON_DIR}/udpscope.gresource.xml
DEPENDS ${UDPSCOPE_ICON_DIR}/udpscope.gresource.xml ${UDPSCOPE_ICON_FILES}
COMMENT "Compiling udpscope icon resources"
VERBATIM)
# --- the manual: a Mallard help set, installed for the `help:` URI ------------
# The manual is no longer a GResource compiled into the binary. It is a Mallard set that the
# desktop help viewer (Yelp on GNOME) reads off disk, because that is the only way `F1` can
# open a real, searchable, translatable document and the only way the freedesktop Help System
# Specification's `help:` URI resolves it. The user documentation is then *files*, checked and
# installed rather than compiled in.
#
# The document identifier -- the `<name>` in `help:<name>` -- is derived from the desktop
# file's own basename (its desktop-file id), not written out twice: the same string is the
# `.desktop` file, the GApplication id and the installed directory. The specification installs
# a document at `<datadir>/help/<lang>/<document>/` and resolves `help:<document>` to the first
# match under $XDG_DATA_HOME/$XDG_DATA_DIRS, so the directory name *is* the identifier.
# NAME_WE is not used: it strips the *longest* extension, so it would cut `org.udpscope.app`
# down to `org`. Strip the two suffixes this one file actually carries, in order.
get_filename_component(_udpscope_desktop
${CMAKE_CURRENT_SOURCE_DIR}/data/org.udpscope.app.desktop.in NAME)
string(REGEX REPLACE "\\.desktop\\.in$" "" UDPSCOPE_HELP_NAME "${_udpscope_desktop}")
unset(_udpscope_desktop)
set(UDPSCOPE_HELP_SRC ${CMAKE_CURRENT_SOURCE_DIR}/help/C/${UDPSCOPE_HELP_NAME})
file(GLOB UDPSCOPE_HELP_FILES CONFIGURE_DEPENDS
${UDPSCOPE_HELP_SRC}/*.page ${UDPSCOPE_HELP_SRC}/figures/*.png)
# The install rule the specification asks for. `CMAKE_INSTALL_DATADIR` is `share` by default,
# so the set lands at `<prefix>/share/help/C/<document>/`, which is exactly where a viewer
# looks. There is no install step for the binary itself (see the desktop-entry target below),
# but the manual is *read from a datadir* by another process, so it has to be installable.
#
# `COMPONENT Help` is what lets the opt-in targets below install *only* the manual: it names this
# rule, and `cmake --install --component Help` runs it and nothing else. The ordinary
# `cmake --install` installs every component, so its behaviour is unchanged -- the component is a
# filter a caller may apply, not a change to the default.
install(DIRECTORY ${UDPSCOPE_HELP_SRC}/
DESTINATION ${CMAKE_INSTALL_DATADIR}/help/C/${UDPSCOPE_HELP_NAME}
COMPONENT Help
FILES_MATCHING PATTERN "*.page" PATTERN "*.png" PATTERN "*.jpg")
# --- installing the manual alone: prefix-wide and user-local, both opt-in -----
# A user who runs the binary out of a build tree wants *just* the manual where `help:` resolves,
# and not the `cmake --install` of a whole project. These two targets are that, one per
# destination; neither is built by default and neither is touched by `cmake --build build`.
#
# `install-help` honours `CMAKE_INSTALL_PREFIX`, so a packager or a system-wide user points it at
# `/usr` with -DCMAKE_INSTALL_PREFIX at configure time. It goes through the install machinery
# (`cmake --install`) rather than a hand-rolled copy, which is what makes DESTDIR and `--prefix`
# behave exactly as they do for the ordinary install. It cannot install system-wide on its own:
# `/usr` is root-owned, so the target writes to the configured prefix and a system-wide caller
# wraps it in `sudo` -- a build target has no more privilege than the shell that runs it.
add_custom_target(install-help
COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --component Help
COMMENT "Installing the manual into ${CMAKE_INSTALL_PREFIX}"
VERBATIM)
# `install-help-user` is the no-root case the launch-failure alert names. It resolves the user
# data directory the way `help_installed_path()` does -- `$XDG_DATA_HOME` when it is set,
# `$HOME/.local/share` otherwise, the XDG Base Directory Specification's own default -- and copies
# the set there. The copy is done by a script rather than by `cmake --install --prefix` because
# the destination is the *data directory*, not a prefix: `cmake --install`'s datadir is always
# `<prefix>/share`, so an arbitrary `$XDG_DATA_HOME` cannot be expressed as a prefix. The script
# prints every path it wrote; the copy uses the install rule's file filter, so both destinations
# carry the same tree.
add_custom_target(install-help-user
COMMAND ${CMAKE_COMMAND}
-DHELP_SRC=${UDPSCOPE_HELP_SRC}
-DHELP_NAME=${UDPSCOPE_HELP_NAME}
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/install_help_user.cmake
COMMENT "Installing the manual into the user's XDG data directory"
VERBATIM)
# --- UI files: GtkBuilder XML in a GResource ----------------------------------
# The third bundle, for the same reason as the other two: `.ui` files are data, so they are compiled
# in rather than looked for at runtime -- and `preprocess="xml-stripblanks"` makes a malformed one a
# build failure with a file and line number, instead of a window that silently refuses to open.
set(UDPSCOPE_UI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/ui)
file(GLOB UDPSCOPE_UI_FILES CONFIGURE_DEPENDS ${UDPSCOPE_UI_DIR}/*.ui)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
COMMAND ${GLIB_COMPILE_RESOURCES}
--target=${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
--sourcedir=${UDPSCOPE_UI_DIR}
--c-name=udpscope_ui
--generate-source
${UDPSCOPE_UI_DIR}/udpscope-ui.gresource.xml
DEPENDS ${UDPSCOPE_UI_DIR}/udpscope-ui.gresource.xml ${UDPSCOPE_UI_FILES}
COMMENT "Compiling udpscope UI resources"
VERBATIM)
# The generated source carries each file as one C string literal, and a `.ui` is big enough to pass the
# 4095-character minimum the standard requires a compiler to support: `-Woverlength-strings` fires on
# generated *data*, not on anything anyone wrote. Silenced for this one generated file (the icon bundle
# is small enough per file never to need it), so the build stays at its one warning. The manual is
# no longer compiled in, so the help bundle's own warning is gone with it.
set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
PROPERTIES COMPILE_OPTIONS "-Wno-overlength-strings")
# --- the UI files: an opt-in check, never part of `cmake --build` ---------------
# The interface is hand-written GtkBuilder XML, and nothing in the build sees a mistake in it: a
# property a class does not have is a `Gtk-ERROR` when the window is built, and `Gtk-ERROR` *aborts
# the process* -- so a typo takes the application down rather than failing to open a dialog.
# `xml-stripblanks` catches malformed XML and nothing else, and the probes catch the rest only for the
# windows they open.
#
# blueprint-compiler type-checks every property and child type against the GIR, and it reads XML as
# well as blueprint (`decompile` turns one into the other), so `tools/check_ui.py` round-trips each
# file through it. The XML stays the source of truth and stays hand-written; nothing here is needed to
# build the application, which is the whole point of not converting the files.
#
# No ALL: this runs only when it is named, so `cmake --build build` is unaffected and
# blueprint-compiler is not a build dependency.
find_program(UDPSCOPE_PYTHON NAMES python3)
if(UDPSCOPE_PYTHON)
add_custom_target(test-ui
COMMAND ${UDPSCOPE_PYTHON} ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_ui.py ${UDPSCOPE_UI_DIR}
DEPENDS ${UDPSCOPE_UI_FILES} ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_ui.py
COMMENT "Checking the UI files with blueprint-compiler"
VERBATIM)
else()
message(WARNING "python3 was not found: the test-ui target is not available")
endif()
# --- the manual: an opt-in check, never part of `cmake --build` -----------------
# The manual is hand-written Mallard, and `cmake --build` never reads it -- a broken xref or a
# missing figure is a page in the viewer with no content or no image, not a build failure.
# `tools/check_help.sh` runs yelp-check's four offline subcommands over the set (links, media,
# orphans, ids) and renders the whole set to HTML with yelp-build, so the manual is checked the
# way the `.ui` files are. `yelp-check validate` is *not* used: it fetches the Mallard RelaxNG
# schema over the network, which is not available here and would make the check depend on it.
#
# No ALL: this runs only when it is named, so `cmake --build build` is unaffected and
# yelp-tools is not a build dependency.
find_program(UDPSCOPE_YELP_CHECK NAMES yelp-check)
find_program(UDPSCOPE_YELP_BUILD NAMES yelp-build)
if(UDPSCOPE_YELP_CHECK AND UDPSCOPE_YELP_BUILD)
add_custom_target(test-help
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_help.sh ${UDPSCOPE_HELP_SRC}
DEPENDS ${UDPSCOPE_HELP_FILES} ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_help.sh
COMMENT "Checking the Mallard help set with yelp-check and yelp-build"
VERBATIM)
else()
message(WARNING "yelp-check/yelp-build were not found: the test-help target is not available")
endif()
# --- build info: version, commit and timestamp compiled into the binary -------
# Written at configure time so add_executable can name the file, and again on every
# build so the commit and the timestamp belong to the binary that is running rather
# than to whenever the tree was last configured.
set(UDPSCOPE_BUILD_INFO ${CMAKE_CURRENT_BINARY_DIR}/build_info.cpp)
set(UDPSCOPE_BUILD_INFO_ARGS
-DSRC=${CMAKE_CURRENT_SOURCE_DIR}
-DOUT=${UDPSCOPE_BUILD_INFO}
-DVERSION=${PROJECT_VERSION}
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/write_build_info.cmake)
execute_process(COMMAND ${CMAKE_COMMAND} ${UDPSCOPE_BUILD_INFO_ARGS})
add_custom_target(udpscope_build_info
BYPRODUCTS ${UDPSCOPE_BUILD_INFO}
COMMAND ${CMAKE_COMMAND} ${UDPSCOPE_BUILD_INFO_ARGS}
COMMENT "Writing udpscope build info"
VERBATIM)
set_source_files_properties(${UDPSCOPE_BUILD_INFO} PROPERTIES GENERATED TRUE)
# --- the application ----------------------------------------------------------
add_executable(udpscope
${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
${UDPSCOPE_BUILD_INFO}
src/main.cpp
src/config.cpp
src/channel.cpp
src/model.cpp
src/condition.cpp
src/trigger.cpp
src/capture.cpp
src/waveform.cpp
src/event_store.cpp
src/acquisition.cpp
src/source/source_config.cpp
src/source/data_source.cpp
src/source/registry.cpp
src/source/udps_source.cpp
src/source/udps_kind.cpp
src/source/probe_source.cpp
src/source/e1608/e1608_link.cpp
src/source/e1608/e1608_device.cpp
src/source/e1608/e1608_source.cpp
src/source/e1608/e1608_kind.cpp
src/source/e1608/e1608_panels.cpp
src/source/flatudp/flatudp_signals.cpp
src/source/flatudp/flatudp_source.cpp
src/source/flatudp/flatudp_kind.cpp
src/plot_host.cpp
src/export.cpp
src/search.cpp
src/layout_store.cpp
src/source_dialog.cpp
src/measure.cpp
src/math_channel.cpp
src/status_text.cpp
src/shortcuts.cpp
src/help_window.cpp
src/settings.cpp
src/ui_prefs.cpp
src/ui.cpp
src/ui_probe.cpp
src/ui_widgets.cpp
src/ui_events.cpp
src/ui_status.cpp
src/ui_measure.cpp
src/ui_cursors.cpp
src/ui_dock.cpp
src/ui_math.cpp
src/ui_layout.cpp
src/ui_source.cpp
src/ui_slideover.cpp
src/ui_badges.cpp
src/ui_trigger.cpp
src/ui_signals.cpp
src/ui_signal_page.cpp
src/ui_context_help.cpp
src/ui_tour.cpp
)
target_include_directories(udpscope PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
# The manual's identifier (`show_help()` builds `help:<id>` from it) and its directory in the
# source tree (what `help_self_check()` scans). Both come from the block near the top, so the
# identifier is the desktop file's basename and nothing is written out twice.
target_compile_definitions(udpscope PRIVATE
UDPSCOPE_HELP_NAME="${UDPSCOPE_HELP_NAME}"
UDPSCOPE_HELP_DIR="${UDPSCOPE_HELP_SRC}")
target_link_libraries(udpscope PRIVATE plot::gtk4 udpsclient Threads::Threads
PkgConfig::ADWAITA)
add_dependencies(udpscope udpscope_build_info)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(udpscope PRIVATE -Wall -Wextra -Wpedantic)
endif()
# --- the desktop entry: opt-in, never part of `cmake --build` ------------------
# `Exec` is the built binary's absolute path in the build tree, so the entry always launches
# exactly the binary that was last built -- and moving or deleting the build directory breaks
# it. That is the price of having no install step, and it is the right one here: an install()
# rule would create a second copy of the app that can diverge from the build tree, and a
# documented `cp` would need the binary on PATH, which it is not.
#
# configure_file() cannot evaluate a generator expression and file(GENERATE) cannot substitute
# an @VAR@, so the two run in sequence: the first writes the genex out literally, the second
# evaluates it. This is the documented idiom.
set(UDPSCOPE_EXEC "$<TARGET_FILE:udpscope>")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/data/org.udpscope.app.desktop.in
${CMAKE_CURRENT_BINARY_DIR}/desktop-entry.in @ONLY)
file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/org.udpscope.app.desktop
INPUT ${CMAKE_CURRENT_BINARY_DIR}/desktop-entry.in)
# No ALL: this runs only when it is named, so `cmake --build build` is unaffected.
add_custom_target(desktop-entry
COMMAND ${CMAKE_COMMAND}
-DENTRY=${CMAKE_CURRENT_BINARY_DIR}/org.udpscope.app.desktop
-DICON=${UDPSCOPE_ICON_DIR}/scalable/apps/org.udpscope.app.svg
-DSYMBOLIC=${UDPSCOPE_ICON_DIR}/scalable/apps/org.udpscope.app-symbolic.svg
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/install_desktop_entry.cmake
COMMENT "Installing the udpscope desktop entry into XDG_DATA_HOME"
VERBATIM)
add_dependencies(desktop-entry udpscope)