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 `` in `help:` -- 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 `/help///` and resolves `help:` 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 `/share/help/C//`, 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 # `/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:` 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 "$") 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)