Files
udpscope/CMakeLists.txt
T
Martino Ferrari c1460a6977 feat: the manual is a Mallard set opened in Yelp, not a GResource in the binary
The help was 14 Pango-markup `.txt` bodies in a second GResource, drawn into a paged `GtkWindow`
built in `help_window.cpp`. That made F1 a viewer of ours with no search index, no cross-references
and no translation, and it kept the manual in the *binary* when every other desktop application's is
a document on disk. It is now a real Mallard set:

  help/C/org.udpscope.app/  index.page + 14 topic pages + figures/*.png

and `show_help()` hands `help:org.udpscope.app` to whatever the desktop registered for the `help:`
scheme -- Yelp on GNOME -- through a `GtkUriLauncher`, reporting a launch failure with a
`GtkAlertDialog` rather than a click that silently does nothing. The paged window's builder, the
page table and `help_self_check()`'s markup validation are gone with the resource.

The document identifier is the desktop file's own basename (its desktop-file id), derived in
CMakeLists from `data/org.udpscope.app.desktop.in`, so the same string is the `.desktop` name, the
GApplication id and the install directory at once. The set is installed by a new `install()` rule to
`<datadir>/help/C/org.udpscope.app/` -- where the freedesktop Help System Specification's `help:`
URI resolves `help:<id>` -- so the manual is read from a datadir by another process even though the
binary has no install step. `UDPSCOPE_HELP_NAME` and `UDPSCOPE_HELP_DIR` are compiled in from that
one place.

The 14 pages were migrated fact for fact, but restructured the Mallard way: a `<desc>` per page
(what a search result shows), `<steps>` for procedures, `<terms>` for option lists,
`<gui>`/`<key>`/`<keyseq>` for interface words and keys, `<note>` for the asides, and a topic graph
via `<link type="guide" xref="index#main"/>`. The figures are annotated screenshots produced by
`tools/make-help-shots.sh`, which drives the app headlessly against `tools/mock_udpstreamer.py` and
draws the boxes with ImageMagick; the side panel's box is measured from the app's own `side=NNNx`
dump reading, the header and status bands come from a documented table, and a crop whose standard
deviation is 0 fails the script.

`cmake --build build --target test-help` checks the set the way `test-ui` checks the `.ui` files:
`yelp-check`'s offline subcommands (links, media, orphans, ids) and a `yelp-build html` render.
`yelp-check validate` is deliberately absent -- it fetches the Mallard RelaxNG schema over the
network and fails offline for that reason, not for a fault in the pages.

The `[help]` probe reading changes with the resource it was written for. `parsed == pages` over
in-binary markup cannot exist now, so `help_self_check()` scans `UDPSCOPE_HELP_DIR` and the line
reads `topics=14 index=1 figures=3`; `topics=0 index=0` is the manual missing from the tree, which
is the finding the old count named.

Verified: clean build with the documented three warnings (the help bundle's `-Woverlength-strings`
is gone, so the baseline moves from four to three); test-ui 14/14; test-help clean (the four
subcommands and 15 pages rendered); a temp-prefix install lands 15 `.page` files and 3 figures at
`share/help/C/org.udpscope.app/`; the headless run reads `[help] topics=14 index=1 figures=3 dir=...`
with 0 Gtk-CRITICAL. Yelp's own window could not be exercised on this box -- it starts under Xvfb,
activates its portals and never maps, the same WebKit/portal limitation that stops a grab -- so its
rendering and its search index are unverified here; the content pipeline was (`yelp-build` renders
the whole set).

Docs: the manual's build/install/open story in `build-and-run.md` and README; the warning baseline
in `verification.md`/CLAUDE.md; the help bullets in `invariants-gtk-and-chrome.md`; the `[help]`
line in `probe-readings.md`.
2026-10-14 16:49:30 +02:00

266 lines
14 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.
install(DIRECTORY ${UDPSCOPE_HELP_SRC}/
DESTINATION ${CMAKE_INSTALL_DATADIR}/help/C/${UDPSCOPE_HELP_NAME}
FILES_MATCHING PATTERN "*.page" PATTERN "*.png" PATTERN "*.jpg")
# --- 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 three warnings. 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/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_math.cpp
src/ui_layout.cpp
src/ui_trigger.cpp
src/ui_signals.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)