diff --git a/CMakeLists.txt b/CMakeLists.txt index ed3aae7..61d5c9a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,3 +126,30 @@ 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) diff --git a/cmake/install_desktop_entry.cmake b/cmake/install_desktop_entry.cmake new file mode 100644 index 0000000..ae71f33 --- /dev/null +++ b/cmake/install_desktop_entry.cmake @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: MIT +# +# The `desktop-entry` target's body. Copies the generated entry and the two app icons into +# $XDG_DATA_HOME, validates the entry, refreshes the caches that make the shell notice, and +# prints every path it wrote. +# +# This runs only when the target is named. The project deliberately has no install step -- that +# is the stated reason its icons are a GResource -- and a target that does nothing until asked +# preserves that while still producing a working entry. +# +# Expects -DENTRY=, -DICON= and -DSYMBOLIC= on the command line. + +if(DEFINED ENV{XDG_DATA_HOME} AND NOT "$ENV{XDG_DATA_HOME}" STREQUAL "") + set(data_home "$ENV{XDG_DATA_HOME}") +else() + set(data_home "$ENV{HOME}/.local/share") +endif() + +set(apps_dir "${data_home}/applications") +set(icon_dir "${data_home}/icons/hicolor/scalable/apps") +set(sym_dir "${data_home}/icons/hicolor/symbolic/apps") + +# The icons are copied from the source tree rather than extracted from the GResource: the same +# two files serve both the in-binary resource and the installed theme, so there is one copy to +# edit. `file(COPY)` creates the directories. +file(COPY "${ENTRY}" DESTINATION "${apps_dir}") +file(COPY "${ICON}" DESTINATION "${icon_dir}") +file(COPY "${SYMBOLIC}" DESTINATION "${sym_dir}") + +get_filename_component(entry_name "${ENTRY}" NAME) +get_filename_component(icon_name "${ICON}" NAME) +get_filename_component(sym_name "${SYMBOLIC}" NAME) + +# Validated here rather than left to the shell, which ignores a malformed entry in silence: the +# failure belongs at the moment the entry is asked for. +find_program(DESKTOP_FILE_VALIDATE desktop-file-validate) +if(DESKTOP_FILE_VALIDATE) + execute_process(COMMAND "${DESKTOP_FILE_VALIDATE}" "${apps_dir}/${entry_name}" + RESULT_VARIABLE validate_result) + if(NOT validate_result EQUAL 0) + message(FATAL_ERROR "desktop-file-validate rejected ${apps_dir}/${entry_name}") + endif() + message(STATUS "desktop-file-validate: ok") +else() + message(STATUS "desktop-file-validate: not on PATH, entry not validated") +endif() + +find_program(UPDATE_DESKTOP_DATABASE update-desktop-database) +if(UPDATE_DESKTOP_DATABASE) + execute_process(COMMAND "${UPDATE_DESKTOP_DATABASE}" "${apps_dir}") +endif() + +find_program(GTK4_UPDATE_ICON_CACHE gtk4-update-icon-cache) +if(GTK4_UPDATE_ICON_CACHE) + execute_process(COMMAND "${GTK4_UPDATE_ICON_CACHE}" -f -t "${data_home}/icons/hicolor") +endif() + +# Printed because there is no uninstall target: undoing this is these three paths. +message(STATUS "wrote ${apps_dir}/${entry_name}") +message(STATUS "wrote ${icon_dir}/${icon_name}") +message(STATUS "wrote ${sym_dir}/${sym_name}") diff --git a/data/org.udpscope.app.desktop.in b/data/org.udpscope.app.desktop.in new file mode 100644 index 0000000..4d85751 --- /dev/null +++ b/data/org.udpscope.app.desktop.in @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Name=udpscope +GenericName=Oscilloscope +Comment=An oscilloscope for UDPStreamer sources +Exec=@UDPSCOPE_EXEC@ +Icon=org.udpscope.app +Terminal=false +Categories=Development;Science;Electronics; +Keywords=oscilloscope;UDP;signal;waveform;trigger; +StartupNotify=true