diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index eabfd9b1c..903861a43 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -110,21 +110,21 @@ if (LINUX) macro(makeLink src dest target) if (NOT EXISTS ${dest}) add_custom_command(TARGET ${target} PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E create_symlink ${src} ${dest} DEPENDS ${dest} COMMENT "mklink ${src} -> ${dest}") + COMMAND ${CMAKE_COMMAND} -E create_symlink ${src} ${dest} COMMENT "mklink ${src} -> ${dest}") endif () endmacro() # macro that copy folder macro(makeCopy src dest target) if (NOT EXISTS ${dest}) add_custom_command(TARGET ${target} PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory ${src} ${dest} DEPENDS ${dest} COMMENT "copy ${src} -> ${dest}") + COMMAND ${CMAKE_COMMAND} -E copy_directory ${src} ${dest} COMMENT "copy ${src} -> ${dest}") endif () endmacro() # macro that copy file macro(makeCopyFile src dest target) if (NOT EXISTS ${dest}) add_custom_command(TARGET ${target} PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${src} ${dest} DEPENDS ${dest} COMMENT "copy ${src} -> ${dest}") + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${src} ${dest} COMMENT "copy ${src} -> ${dest}") endif () endmacro() else () @@ -132,7 +132,7 @@ else () macro(makeLink src dest target) if (NOT EXISTS ${dest}) add_custom_command(TARGET ${target} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E create_symlink ${src} ${dest} DEPENDS ${dest} COMMENT "mklink ${src} -> ${dest}") + COMMAND ${CMAKE_COMMAND} -E create_symlink ${src} ${dest} COMMENT "mklink ${src} -> ${dest}") endif () endmacro() @@ -140,7 +140,7 @@ else () macro(makeCopyDir src dest target) if (NOT EXISTS ${dest}) add_custom_command(TARGET ${target} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory ${src} ${dest} DEPENDS ${dest} COMMENT "copy ${src} -> ${dest}") + COMMAND ${CMAKE_COMMAND} -E copy_directory ${src} ${dest} COMMENT "copy ${src} -> ${dest}") endif () endmacro() @@ -148,7 +148,7 @@ else () macro(makeCopy src dest target) if (NOT EXISTS ${dest}) add_custom_command(TARGET ${target} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${src} ${dest} DEPENDS ${dest} COMMENT "copy ${src} -> ${dest}") + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${src} ${dest} COMMENT "copy ${src} -> ${dest}") endif () endmacro() endif (LINUX) @@ -370,6 +370,7 @@ endif (NOT FFTW3F_FOUND AND NOT USE_PRECOMPILED_LIBS) include(FetchContent) FetchContent_Declare(boost URL "https://sourceforge.net/projects/boost/files/boost/${BOOST_TAG}/boost_${BOOST_TAG2}.tar.gz/download" + DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) find_package(Boost QUIET) @@ -895,14 +896,24 @@ if(ENABLE_FEATURE_STARTRACKER) if (WIN32) set(CSPICE_LIBRARIES "${SDRANGEL_BINARY_LIB_DIR}/cspice.lib" CACHE INTERNAL "") elseif (LINUX) - # We need a shared library on Linux, as the static library has non PIC code, and we need to link it into a shared library - # ExternalProject cspice currently outputs libcspice.so under - # ${EXTERNAL_BUILD_LIBRARIES}/cspice/src/cspice-build/lib/libcspice.so - # so point CSPICE_LIBRARIES there. - set(CSPICE_LIBRARIES - "${EXTERNAL_BUILD_LIBRARIES}/lib${LIB_SUFFIX}/libcspice.so" - CACHE INTERNAL "" - ) + # We need a shared library on Linux, as the static library has non PIC code, + # and we need to link it into a shared library + if (CMAKE_BUILD_TYPE STREQUAL "None") + # ExternalProject cspice currently outputs libcspice.so under + # ${EXTERNAL_BUILD_LIBRARIES}/cspice/src/cspice-build/lib/libcspice.so + # so point CSPICE_LIBRARIES there. + set(CSPICE_LIBRARIES + "${EXTERNAL_BUILD_LIBRARIES}/cspice/src/cspice-build/lib/libcspice.so" + CACHE INTERNAL "" + ) + else() + # Release, Debug, and RelWithDebInfo have configuration-specific output + # directories supplied by SDRangel. + set(CSPICE_LIBRARIES + "${EXTERNAL_BUILD_LIBRARIES}/lib${LIB_SUFFIX}/libcspice.so" + CACHE INTERNAL "" + ) + endif() # Disable -Werror=format-security for cspice only: distro hardening flags (e.g. Ubuntu Noble) # inject -Werror=format-security which fails cspice sources that lack -Wformat. set(CSPICE_ARGS "-DCSPICE_BUILD_STATIC_LIBRARY=OFF" @@ -1226,8 +1237,27 @@ if (LINUX) if (ENABLE_BLADERF AND (NOT LIBBLADERF_FOUND OR LIBBLADERF_EXTERNAL)) set_global_cache(LIBBLADERF_FOUND ON) set(LIBBLADERF_EXTERNAL ON CACHE INTERNAL "") - set(LIBBLADERF_INCLUDE_DIRS "${EXTERNAL_BUILD_LIBRARIES}/bladerf/src/bladerf/host/libraries/libbladeRF/include" CACHE INTERNAL "") - set(LIBBLADERF_LIBRARIES "${EXTERNAL_BUILD_LIBRARIES}/lib${LIB_SUFFIX}/libbladeRF.so.2" CACHE INTERNAL "") + set(LIBBLADERF_INCLUDE_DIRS + "${EXTERNAL_BUILD_LIBRARIES}/bladerf/src/bladerf/host/libraries/libbladeRF/include" + CACHE INTERNAL "" + ) + if (CMAKE_BUILD_TYPE STREQUAL "None") + # ExternalProject bladerf currently outputs libbladeRF.so.2 under + # ${EXTERNAL_BUILD_LIBRARIES}/bladerf/src/bladerf-build/output/libbladeRF.so.2 + # so point LIBBLADERF_LIBRARIES there. + set(LIBBLADERF_LIBRARIES + "${EXTERNAL_BUILD_LIBRARIES}/bladerf/src/bladerf-build/output/libbladeRF.so.2" + CACHE INTERNAL "" + ) + else() + # Release, Debug, and RelWithDebInfo have configuration-specific output + # directories supplied by SDRangel. + set(LIBBLADERF_LIBRARIES + "${EXTERNAL_BUILD_LIBRARIES}/lib${LIB_SUFFIX}/libbladeRF.so.2" + CACHE INTERNAL "" + ) + endif() + ExternalProject_Add(bladerf GIT_REPOSITORY https://github.com/Nuand/bladeRF.git GIT_TAG ${BLADERF_TAG} diff --git a/plugins/channelrx/filesink/filesinkgui.cpp b/plugins/channelrx/filesink/filesinkgui.cpp index adbcb4c4a..5e754ff42 100644 --- a/plugins/channelrx/filesink/filesinkgui.cpp +++ b/plugins/channelrx/filesink/filesinkgui.cpp @@ -86,8 +86,12 @@ bool FileSinkGUI::handleMessage(const Message& message) if (m_fixedPosition) { + const qint32 inputFrequencyOffset = m_settings.m_inputFrequencyOffset; setFrequencyFromPos(); - applySettings(QStringList(), true); + + if (m_settings.m_inputFrequencyOffset != inputFrequencyOffset) { + applySettings(QStringList("inputFrequencyOffset")); + } } else { diff --git a/plugins/feature/mcpserver/bridge/CMakeLists.txt b/plugins/feature/mcpserver/bridge/CMakeLists.txt index 23f9d996c..10b89c41a 100644 --- a/plugins/feature/mcpserver/bridge/CMakeLists.txt +++ b/plugins/feature/mcpserver/bridge/CMakeLists.txt @@ -93,6 +93,11 @@ if(BUILD_MCP_BUNDLE) set_target_properties(sdrangel-mcp-tools-dump PROPERTIES BUILD_RPATH "${MACOS_EXTERNAL_LIBS_FIXUP}") endif() + set(MCPTOOLSDUMP_TIMEOUT 60 CACHE STRING "Timeout in seconds for generating the MCP tool list") + if(NOT MCPTOOLSDUMP_TIMEOUT MATCHES "^[1-9][0-9]*$") + message(FATAL_ERROR "MCPTOOLSDUMP_TIMEOUT must be a positive integer") + endif() + set(MCPBRIDGE_BUNDLE_DIR "${CMAKE_CURRENT_BINARY_DIR}/bundle") # Alongside the built binaries, which is bin/ on Windows and the build directory itself # elsewhere. That is also where the running application looks for it in a build tree @@ -109,14 +114,23 @@ if(BUILD_MCP_BUNDLE) COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/cmake/cpack/sdrangel_icon.png" "${MCPBRIDGE_BUNDLE_DIR}/icon.png" # The tool list, straight from the server's own registry, beside the bridge that serves # it when SDRangel is not up - COMMAND $ "${MCPBRIDGE_BUNDLE_DIR}/server/tools.json" + COMMAND ${CMAKE_COMMAND} + "-DTOOLSDUMP=$" + "-DTOOLS_JSON=${MCPBRIDGE_BUNDLE_DIR}/server/tools.json" + "-DTOOLSDUMP_TIMEOUT=${MCPTOOLSDUMP_TIMEOUT}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/run-tools-dump.cmake" COMMAND ${CMAKE_COMMAND} -E rm -f "${MCPBRIDGE_BUNDLE_FILE}" # Relative paths and a working directory, so that manifest.json lands at the root of # the archive, which is where the installer looks for it + COMMAND ${CMAKE_COMMAND} -E echo "mcpbundle: archiving ${MCPBRIDGE_BUNDLE_FILE}" COMMAND ${CMAKE_COMMAND} -E tar cf "${MCPBRIDGE_BUNDLE_FILE}" --format=zip -- manifest.json icon.png server + COMMAND ${CMAKE_COMMAND} -E echo "mcpbundle: archive complete" WORKING_DIRECTORY "${MCPBRIDGE_BUNDLE_DIR}" DEPENDS sdrangel-mcp-bridge sdrangel-mcp-tools-dump "${MCPBRIDGE_BUNDLE_DIR}/manifest.json" COMMENT "Packaging ${MCPBRIDGE_BUNDLE_FILE}" + # Stream diagnostics to GitHub Actions even while the helper is still running. + # Otherwise Ninja buffers them until the entire bundle command finishes. + USES_TERMINAL VERBATIM ) diff --git a/plugins/feature/mcpserver/bridge/readme.md b/plugins/feature/mcpserver/bridge/readme.md index bed9d79ae..b81bb172f 100644 --- a/plugins/feature/mcpserver/bridge/readme.md +++ b/plugins/feature/mcpserver/bridge/readme.md @@ -36,7 +36,7 @@ CMake builds one on its own; the `mcpb` CLI only adds validation and signing, an npm to build SDRangel would be a poor trade for that. The bundle is not signed, so Claude warns that the publisher is unverified when it is installed. -Both options default to off, so a normal build is untouched. With `BUILD_MCP_BUNDLE=ON` the +With `BUILD_MCP_BUNDLE=ON` the bundle is part of `all`, so building the Windows installer picks it up with nothing else to remember: diff --git a/plugins/feature/mcpserver/bridge/run-tools-dump.cmake b/plugins/feature/mcpserver/bridge/run-tools-dump.cmake new file mode 100644 index 000000000..d828fb678 --- /dev/null +++ b/plugins/feature/mcpserver/bridge/run-tools-dump.cmake @@ -0,0 +1,33 @@ +# Run at build time, with the parent custom target's terminal attached. Keep stdout/stderr +# inherited so the last checkpoint is visible even if the process never exits. +foreach(required TOOLSDUMP TOOLS_JSON TOOLSDUMP_TIMEOUT) + if(NOT DEFINED ${required} OR "${${required}}" STREQUAL "") + message(FATAL_ERROR "Missing ${required} for MCP tool list generation") + endif() +endforeach() + +if(NOT TOOLSDUMP_TIMEOUT MATCHES "^[1-9][0-9]*$") + message(FATAL_ERROR "TOOLSDUMP_TIMEOUT must be a positive integer") +endif() + +message(STATUS "mcpbundle: starting ${TOOLSDUMP}") +message(STATUS "mcpbundle: output ${TOOLS_JSON}; timeout ${TOOLSDUMP_TIMEOUT}s") +get_filename_component(toolsdump_working_directory "${TOOLS_JSON}" DIRECTORY) +execute_process( + COMMAND "${TOOLSDUMP}" "${TOOLS_JSON}" + WORKING_DIRECTORY "${toolsdump_working_directory}" + TIMEOUT "${TOOLSDUMP_TIMEOUT}" + RESULT_VARIABLE toolsdump_result +) + +if(NOT "${toolsdump_result}" STREQUAL "0") + message(FATAL_ERROR + "MCP tool list generation failed: ${toolsdump_result}. " + "See the last toolsdump checkpoint above; no 'entered main' checkpoint points to " + "executable/DLL startup, and 'initializing MainCore' points to runtime service initialization.") +endif() + +if(NOT EXISTS "${TOOLS_JSON}") + message(FATAL_ERROR "MCP tool list generator exited successfully without creating ${TOOLS_JSON}") +endif() +message(STATUS "mcpbundle: tool list generation complete") diff --git a/plugins/feature/mcpserver/toolsdump.cpp b/plugins/feature/mcpserver/toolsdump.cpp index 65dbc0e43..5e5c886e8 100644 --- a/plugins/feature/mcpserver/toolsdump.cpp +++ b/plugins/feature/mcpserver/toolsdump.cpp @@ -21,8 +21,9 @@ // the tool list once and does not ask again, so the bridge has to be able to answer with the // tools this very build of the server provides, before it has ever spoken to it. // -// The registry is metadata and handler functions: building it touches neither the Web API nor -// any device, so a null adapter is all it needs. +// A null adapter prevents Web API calls, but MCPTools members still initialize MainCore, +// including its pipe workers and platform location/permission services. Log that separately +// from registry construction, since it can block on a headless CI runner. #include #include @@ -33,13 +34,23 @@ #include #include +#include #include #include +#include "maincore.h" #include "mcptools.h" int main(int argc, char *argv[]) { + // Ninja and CI capture stderr through pipes; publish each checkpoint immediately. + std::setvbuf(stderr, nullptr, _IONBF, 0); +#ifdef _WIN32 + std::fprintf(stderr, "toolsdump: entered main (pid %lu)\n", GetCurrentProcessId()); +#else + std::fprintf(stderr, "toolsdump: entered main\n"); +#endif + std::fprintf(stderr, "toolsdump: creating QCoreApplication\n"); QCoreApplication application(argc, argv); if (argc < 2) @@ -49,35 +60,55 @@ int main(int argc, char *argv[]) } std::fprintf(stderr, "toolsdump: application up\n"); + + // MainCore starts position updates as it is constructed, and the Windows position plugin + // asks the system for location access and spins until it has an answer. On a machine where + // nobody has ever answered, such as a CI runner, that is for ever. Nothing here needs a + // plugin, so leave Qt nowhere to find one and MainCore carries on without a position source + QCoreApplication::setLibraryPaths(QStringList()); + + std::fprintf(stderr, "toolsdump: initializing MainCore (pipe workers, permissions, positioning)\n"); + MainCore::instance(); + std::fprintf(stderr, "toolsdump: MainCore ready; constructing registry\n"); MCPTools tools(nullptr); std::fprintf(stderr, "toolsdump: registry built\n"); QJsonObject result; + std::fprintf(stderr, "toolsdump: listing tools\n"); result["tools"] = tools.listTools(); std::fprintf(stderr, "toolsdump: listed\n"); QFile file(QString::fromLocal8Bit(argv[1])); + std::fprintf(stderr, "toolsdump: opening %s\n", argv[1]); + if (!file.open(QIODevice::WriteOnly)) { std::fprintf(stderr, "cannot write %s: %s\n", argv[1], qPrintable(file.errorString())); return 1; } + std::fprintf(stderr, "toolsdump: writing JSON\n"); file.write(QJsonDocument(result).toJson(QJsonDocument::Compact)); + std::fprintf(stderr, "toolsdump: closing output\n"); file.close(); std::fprintf(stderr, "toolsdump: written\n"); - std::fprintf(stderr, "%s: %d tools\n", argv[1], result["tools"].toArray().size()); + std::fprintf(stderr, "%s: %lld tools\n", argv[1], + static_cast(result["tools"].toArray().size())); - // The registry brings background threads up with it and they do not all stop when main - // returns, which leaves the process, and so the build, hanging. Even _Exit does not get - // out, since the runtime's own teardown waits on them, so end the process from the kernel - // side. The file is written and flushed by this point + // Preserve the existing teardown workaround: MainCore has background workers, and + // this one-shot helper never runs the Qt event loop. This does not protect against + // a hang during initialization; the build target applies a separate process timeout. + std::fprintf(stderr, "toolsdump: flushing streams before process exit\n"); std::fflush(nullptr); #ifdef _WIN32 - TerminateProcess(GetCurrentProcess(), 0); + std::fprintf(stderr, "toolsdump: calling TerminateProcess\n"); + if (!TerminateProcess(GetCurrentProcess(), 0)) { + std::fprintf(stderr, "toolsdump: TerminateProcess failed: %lu\n", GetLastError()); + } #endif + std::fprintf(stderr, "toolsdump: calling _Exit\n"); std::_Exit(0); }