fix(security): compile the UI sweep only into dev builds

The screenshot sweep and the full UI sweep are dev tools. The full sweep
injects demo data into the live wallet state and the real data and
config dirs, and the tab sweep screenshots real balances and addresses.
Both were compiled into every build: the Settings buttons were
reachable in full-node releases, and the DRAGONX_FULL_SWEEP env hook
worked in every build, Lite releases included.

A default-OFF option alone would fail open: dev and release builds
share build/linux (and build/mac), so a release configured without
--clean would inherit a cached ON from the last dev configure.

- CMake: option(DRAGONX_ENABLE_UI_SWEEP ... OFF), passed to
  ObsidianDragon and ObsidianDragonTests as a compile definition.
- build.sh passes it explicitly: ON in the dev build, OFF in every
  release configure (Linux, Windows, macOS osxcross and native).
- Packaging guard: while the cache says ON, dragonx_release depends on
  dragonx_release_sweep_guard, which fails, so a hand-configured ON
  tree cannot package a release.
- startSweepImpl is a no-op unless the option is ON (an undefined macro
  counts as 0, so it fails closed). The enabled branch logs the
  sentinel "[Sweep] DRAGONX_UI_SWEEP_COMPILED_IN", so a release can be
  checked with `strings <bin> | grep -c DRAGONX_UI_SWEEP_COMPILED_IN`
  (must be 0).
- main.cpp: the DRAGONX_FULL_SWEEP hook is compiled out (autoSweep is
  false). It must be: with the sweep a no-op, the hook would quit the
  app once its warm-up frames ran out.
- Settings > Node > Debug options: both sweep buttons, open-dir, the
  seed-demo-chat button and the current-theme checkbox are compiled only
  into dev builds. The daemon debug categories are unchanged.

No i18n change: the existing strings stay in the tables.

Verified with an isolated release (DRAGONX_BUILD_ROOT/DRAGONX_RELEASE_ROOT
in a scratch dir) whose tree was first configured by hand with ON:
build.sh --linux-release without --clean leaves the cache OFF, and the
sentinel count is 0 in the built binary, the zip and the AppImage.
With the cache set back to ON by hand, --target dragonx_release fails
at the guard before compiling anything. Dev trees configured ON keep
the sweep (sentinel present; a one-surface DRAGONX_FULL_SWEEP run
against the fake daemon captured and exited cleanly).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 13:22:00 -05:00
co-authored by Claude Opus 5.5
parent eac0993512
commit 3ac46abe1a
5 changed files with 38 additions and 0 deletions
+13
View File
@@ -46,6 +46,10 @@ option(DRAGONX_BUILD_LITE "Build ObsidianDragonLite variant without full-node fe
option(DRAGONX_ENABLE_LITE_BACKEND "Enable real lite wallet backend integration" OFF)
option(DRAGONX_ENABLE_CHAT "Enable the HushChat protocol/UI integration" ON)
option(DRAGONX_BUILD_FAKE_DAEMON "Build the fake slow dragonxd test stub (tools/fake_dragonxd); never shipped" OFF)
# The screenshot / full UI sweep injects demo data and touches the wallet's data and config dirs, so it is a
# dev tool only. build.sh passes it EXPLICITLY (ON for dev, OFF for every release configure) because build/linux
# and build/mac are shared between dev and release builds; dragonx_release refuses to package while it is ON.
option(DRAGONX_ENABLE_UI_SWEEP "Compile the UI sweep dev tool (never for shipped builds)" OFF)
# Embedded fallback copy of res/lang/*.json (xxd -i -> lang_*.h). Needed only by the Windows single-file exe (no res/ beside it);
# Linux zip/AppImage and the macOS .app always ship res/lang, which loadLanguage() reads first.
if(WIN32)
@@ -938,6 +942,7 @@ target_compile_definitions(ObsidianDragon PRIVATE
DRAGONX_ENABLE_LITE_BACKEND=$<BOOL:${DRAGONX_LITE_BACKEND_READY}>
DRAGONX_ENABLE_CHAT=$<BOOL:${DRAGONX_ENABLE_CHAT}>
DRAGONX_EMBED_LANG=$<BOOL:${DRAGONX_EMBED_LANG}>
DRAGONX_ENABLE_UI_SWEEP=$<BOOL:${DRAGONX_ENABLE_UI_SWEEP}>
)
if(WIN32)
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11)
@@ -1170,6 +1175,13 @@ foreach(_dragonx_release_dep dragonx-wallet-rebuild copy_langs copy_themes)
add_dependencies(dragonx_release ${_dragonx_release_dep})
endif()
endforeach()
# Backstop for the UI sweep dev tool: a tree whose cache says ON (e.g. hand-configured) cannot package a release.
if(DRAGONX_ENABLE_UI_SWEEP)
add_custom_target(dragonx_release_sweep_guard
COMMAND ${CMAKE_COMMAND} -E echo "DRAGONX_ENABLE_UI_SWEEP=ON: refusing to package a release"
COMMAND ${CMAKE_COMMAND} -E false)
add_dependencies(dragonx_release dragonx_release_sweep_guard)
endif()
# -----------------------------------------------------------------------------
# fake_dragonxd — a deliberately slow, scriptable dragonxd stand-in that reproduces the daemon-stop
@@ -1298,6 +1310,7 @@ if(BUILD_TESTING)
DRAGONX_ENABLE_EMBEDDED_DAEMON=$<BOOL:${DRAGONX_ENABLE_EMBEDDED_DAEMON}>
DRAGONX_ENABLE_LITE_BACKEND=$<BOOL:${DRAGONX_LITE_BACKEND_READY}>
DRAGONX_EMBED_LANG=$<BOOL:${DRAGONX_EMBED_LANG}>
DRAGONX_ENABLE_UI_SWEEP=$<BOOL:${DRAGONX_ENABLE_UI_SWEEP}>
DRAGONX_TEST_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures"
)
+6
View File
@@ -349,6 +349,7 @@ build_dev() {
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=ON \
-DDRAGONX_ENABLE_UI_SWEEP=ON \
"${CMAKE_LITE_ARGS[@]}"
info "Building with $JOBS jobs ..."
@@ -385,10 +386,12 @@ build_release_linux() {
exit 1
fi
info "Configuring ..."
# UI sweep OFF explicitly: build/linux is shared with dev builds, which cache it ON.
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=ON \
-DDRAGONX_ENABLE_UI_SWEEP=OFF \
"${BDB_ARGS[@]}" \
"${CMAKE_LITE_ARGS[@]}"
@@ -864,6 +867,7 @@ HDR
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DDRAGONX_ENABLE_UI_SWEEP=OFF \
"${BDB_ARGS[@]}" \
"${FT_CMAKE_ARG[@]}" \
"${CMAKE_LITE_ARGS[@]}"
@@ -1129,6 +1133,7 @@ TOOLCHAIN
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DDRAGONX_ENABLE_UI_SWEEP=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"} \
"${BDB_ARGS[@]}" \
@@ -1163,6 +1168,7 @@ TOOLCHAIN
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DDRAGONX_ENABLE_UI_SWEEP=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
-DCMAKE_OSX_ARCHITECTURES="$MAC_ARCHS" \
"${BDB_ARGS[@]}" \
+9
View File
@@ -718,7 +718,15 @@ void App::buildSweepCatalog()
// ── State machine ───────────────────────────────────────────────────────────────────────────
void App::startSweepImpl(bool full)
{
#if !DRAGONX_ENABLE_UI_SWEEP
// Dev tool only (CMake DRAGONX_ENABLE_UI_SWEEP, OFF in every release); an undefined macro counts as 0.
(void)full;
DEBUG_LOGF("[Sweep] disabled in this build\n");
return;
#else
if (screenshot_sweep_active_) return;
// Sentinel: a release binary must not contain this literal (strings <bin> | grep -c ... == 0).
DEBUG_LOGF("[Sweep] DRAGONX_UI_SWEEP_COMPILED_IN\n");
sweep_skins_.clear();
for (const auto& sk : ui::schema::SkinManager::instance().available())
@@ -761,6 +769,7 @@ void App::startSweepImpl(bool full)
ui::Notifications::instance().info(full ? "Full UI sweep running…" : "Screenshot sweep running…");
DEBUG_LOGF("[Sweep] %s -> %s (%d skins x %d surfaces)\n", full ? "FULL" : "tabs",
sweep_dir_.c_str(), (int)sweep_skins_.size(), (int)sweep_targets_.size());
#endif
}
void App::startScreenshotSweep() { startSweepImpl(false); }
+6
View File
@@ -1611,7 +1611,13 @@ int main(int argc, char* argv[])
// Headless verification hook (DRAGONX_FULL_SWEEP=1, handled after present), hoisted so the pacing
// decision can keep its warm-up frames coming.
#if DRAGONX_ENABLE_UI_SWEEP
const bool autoSweep = std::getenv("DRAGONX_FULL_SWEEP") != nullptr;
#else
// The env hook is dead in shipped builds. It must be: with startSweepImpl a no-op, an enabled
// autoSweep would quit the app as soon as the warm-up frames ran out.
const bool autoSweep = false;
#endif
int autoSweepFrames = 0;
bool autoSweepStarted = false;
+4
View File
@@ -2895,6 +2895,9 @@ void RenderSettingsPage(App* app) {
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
#if DRAGONX_ENABLE_UI_SWEEP
// Dev builds only (both sweeps, open-dir, demo chat and the theme checkbox): the tab sweep
// screenshots real balances and addresses, and the demo chat fabricates a chat identity.
// Screenshot sweep — capture every theme x every tab (above the daemon debug categories).
ImGui::TextWrapped("%s", TR("screenshot_sweep_desc"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
@@ -2928,6 +2931,7 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Separator();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
#endif
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_debug_select"));
ImGui::TextColored(ImVec4(1,1,1,0.35f), "%s", TR("settings_debug_restart_note"));