Skip to content

Commit d1c22d7

Browse files
[migrations-gui] Qt 6 GUI for migration management
A new opt-in tool (`LIGHTWEIGHT_BUILD_GUI=ON`) sitting on top of the profile / secrets / migration plumbing introduced in earlier commits. The app gives operators a graphical view of the migration plan for a chosen profile: pending vs. applied counts, per-release grouping (driven by `LIGHTWEIGHT_SQL_RELEASE` markers), per-migration SQL preview, and one-click apply/revert/backup-restore actions running on worker threads with progress reporting. Highlights: - C++/Qt 6 backbone: - `AppController` is the top-level QML-exposed model: owns the current connection, the profile store, and the migration manager handle. - `MigrationRunner`/`BackupRunner` execute on QThread workers and pump progress through `QmlProgressManager` (a QML-friendly adapter around Lightweight's `ProgressManager` interface). - Models for profile lists, ODBC data sources, migration listings, and release groupings — all `QAbstractItemModel` subclasses so the QML side can use Repeater/ListView directly. - `QtKeychainBackend` plugs into the secrets resolver where the user opted into OS keychain storage. - QML UI (Material-inspired theming via `Theme.qml`): - `Main.qml` window with a side connection panel + central migration list, status pills, action bar, and collapsible log panel. - `MigrationView` / `MigrationRow` / `ReleaseGroup` for the list. - `KineticListView` reusable scroller with always-visible scrollbar. - `SqlPreviewDialog` with in-window resizing and copy-to-clipboard. - `BackupRestoreDialog`, `ConnectionPanel`, `ActionsPanel`, `BulkControls`, `FilterTabs`, `PluginsDirField`, `TimestampAutocomplete`, `StatusCard`, `StatusPill`, `Card`, `ToolBar`, `ReleasesSummary`, `LogPanel`. - Ctrl+Q quits. Stale thread-local DataMappers are torn down on reconnect. - Build glue: - `cmake/FindQt.cmake` probes common Qt install locations (C:/Qt, homebrew qt@6, ~/Qt) and falls back gracefully — `LIGHTWEIGHT_BUILD_GUI` is downgraded to OFF (with a warning) instead of failing the whole configure if Qt 6 isn't present. - `src/tools/CMakeLists.txt` only descends into `migrations-gui` when the option survives that gate. - GUI target opts out of clang-tidy because Qt's MOC output trips the project-wide warning ruleset. Includes a README for the GUI subdirectory. Signed-off-by: Christian Parpart <christian@parpart.family>
1 parent bc8df93 commit d1c22d7

43 files changed

Lines changed: 6662 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CMakeLists.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ endif()
4848
option(LIGHTWEIGHT_BUILD_LARGEDB_TOOL "Build large database generator tool" ${LIGHTWEIGHT_BUILD_LARGEDB_TOOL_DEFAULT})
4949
unset(LIGHTWEIGHT_BUILD_LARGEDB_TOOL_DEFAULT)
5050

51+
# GUI tool (Qt 6) — opt-in. If ON we auto-probe common Qt install locations
52+
# (C:/Qt, Homebrew qt@6, ~/Qt) and fall back gracefully if nothing is found.
53+
option(LIGHTWEIGHT_BUILD_GUI "Build the Qt 6 migrations GUI (src/tools/migrations-gui)" OFF)
54+
5155
option(LIGHTWEIGHT_BUILD_DOCUMENTATION "Create and install the HTML based API documentation (requires Doxygen)" OFF)
5256
option(LIGHTWEIGHT_DOCS_WARN_AS_ERROR "Treat Doxygen warnings as errors" OFF)
5357

@@ -177,6 +181,29 @@ set(CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/README.md")
177181

178182
enable_testing()
179183

184+
# Resolve Qt 6 for the optional migrations GUI before descending into tools.
185+
# When LIGHTWEIGHT_BUILD_GUI=ON we auto-probe common install locations; if
186+
# nothing is found we downgrade the option to OFF instead of failing the
187+
# configure, so the rest of the project (library, dbtool, tests) still builds.
188+
if(LIGHTWEIGHT_BUILD_GUI)
189+
include(FindQt)
190+
lightweight_probe_qt6(
191+
MIN_VERSION 6.8.0
192+
REQUIRED_COMPONENTS Core Gui Qml Quick QuickControls2
193+
RESULT_VAR _LIGHTWEIGHT_QT6_PROBED
194+
)
195+
if(NOT _LIGHTWEIGHT_QT6_PROBED)
196+
message(WARNING
197+
"LIGHTWEIGHT_BUILD_GUI=ON but Qt 6 could not be auto-detected.\n"
198+
"Pass -DQt6_DIR=<path-to-qt>/lib/cmake/Qt6 or add the Qt prefix to "
199+
"CMAKE_PREFIX_PATH to enable the GUI. Disabling GUI build for now. "
200+
"See cmake/FindQt.cmake for the list of probed install locations."
201+
)
202+
set(LIGHTWEIGHT_BUILD_GUI OFF CACHE BOOL "" FORCE)
203+
endif()
204+
unset(_LIGHTWEIGHT_QT6_PROBED)
205+
endif()
206+
180207
add_subdirectory(src/Lightweight)
181208

182209
if(LIGHTWEIGHT_BUILD_TOOLS)

cmake/FindQt.cmake

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# FindQt.cmake
2+
#
3+
# Helper that locates a Qt 6 installation when the user has not explicitly
4+
# pointed CMake at one. The goal is to make `cmake -DLIGHTWEIGHT_BUILD_GUI=ON`
5+
# work out-of-the-box when Qt is installed in a standard location, while still
6+
# honouring explicit configuration (Qt6_DIR, CMAKE_PREFIX_PATH, QT_ROOT_DIR).
7+
#
8+
# NOTE: despite the Find*.cmake naming, this is a helper module that exposes
9+
# the `lightweight_probe_qt6()` function — it is not intended to be driven by
10+
# `find_package(Qt)` and does not set the usual `Qt_FOUND` contract. Include
11+
# it explicitly via `include(FindQt)` and then call `lightweight_probe_qt6()`.
12+
#
13+
# Resolution order:
14+
# 1. If Qt6_DIR is already set in the cache -> keep it, do nothing.
15+
# 2. If CMAKE_PREFIX_PATH already resolves Qt6 -> keep it, do nothing.
16+
# 3. Probe well-known install roots for Qt 6.5+ (LTS and newer).
17+
# 4. On failure, report what was searched and let the caller decide whether
18+
# to fall back to LIGHTWEIGHT_BUILD_GUI=OFF.
19+
#
20+
# After a successful probe, CMAKE_PREFIX_PATH is extended and the caller can
21+
# simply do `find_package(Qt6 ... COMPONENTS ...)`.
22+
#
23+
# Public entry point:
24+
# lightweight_probe_qt6([REQUIRED_COMPONENTS <comp> ...]
25+
# [MIN_VERSION <ver>]
26+
# [RESULT_VAR <var>])
27+
#
28+
# RESULT_VAR, if given, is set to TRUE on success and FALSE on failure.
29+
30+
include_guard(GLOBAL)
31+
32+
function(_lw_qt_candidate_roots out_var)
33+
set(roots "")
34+
35+
# Windows: Qt Online Installer default is C:/Qt, occasionally D:/Qt or on
36+
# the system drive. Also respect $USERPROFILE/Qt for per-user installs.
37+
if(WIN32)
38+
list(APPEND roots "C:/Qt" "D:/Qt")
39+
if(DEFINED ENV{SystemDrive})
40+
list(APPEND roots "$ENV{SystemDrive}/Qt")
41+
endif()
42+
if(DEFINED ENV{USERPROFILE})
43+
file(TO_CMAKE_PATH "$ENV{USERPROFILE}/Qt" _up_qt)
44+
list(APPEND roots "${_up_qt}")
45+
endif()
46+
endif()
47+
48+
# macOS: Homebrew keg-only qt@6 (both Apple Silicon and Intel prefixes).
49+
if(APPLE)
50+
list(APPEND roots
51+
"/opt/homebrew/opt/qt@6"
52+
"/opt/homebrew/opt/qt"
53+
"/usr/local/opt/qt@6"
54+
"/usr/local/opt/qt"
55+
)
56+
if(DEFINED ENV{HOME})
57+
list(APPEND roots "$ENV{HOME}/Qt")
58+
endif()
59+
endif()
60+
61+
# Linux: mostly handled by the distro-provided Qt6Config.cmake, but honour
62+
# a user-local Qt Online Installer layout under $HOME/Qt as a fallback.
63+
if(UNIX AND NOT APPLE)
64+
if(DEFINED ENV{HOME})
65+
list(APPEND roots "$ENV{HOME}/Qt")
66+
endif()
67+
list(APPEND roots "/opt/Qt")
68+
endif()
69+
70+
# Also honour QT_ROOT_DIR env var if set (Qt Online Installer sometimes
71+
# exports this in its "Qt Creator"-provided shell).
72+
if(DEFINED ENV{QT_ROOT_DIR})
73+
list(APPEND roots "$ENV{QT_ROOT_DIR}")
74+
endif()
75+
76+
list(REMOVE_DUPLICATES roots)
77+
set(${out_var} "${roots}" PARENT_SCOPE)
78+
endfunction()
79+
80+
# Given a Qt install root (e.g. C:/Qt), return candidate cmake directories
81+
# (.../lib/cmake/Qt6) sorted newest-version-first.
82+
function(_lw_qt_cmake_dirs_under_root root min_version out_var)
83+
set(candidates "")
84+
85+
if(NOT IS_DIRECTORY "${root}")
86+
set(${out_var} "" PARENT_SCOPE)
87+
return()
88+
endif()
89+
90+
# A Qt Online Installer layout looks like:
91+
# C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6
92+
# C:/Qt/6.5.3/mingw_64/lib/cmake/Qt6
93+
# A Homebrew layout looks like:
94+
# /opt/homebrew/opt/qt@6/lib/cmake/Qt6
95+
# A distro layout looks like:
96+
# /usr/lib/x86_64-linux-gnu/cmake/Qt6 (already handled by system pkg)
97+
#
98+
# First, check if root itself is a Qt prefix.
99+
if(EXISTS "${root}/lib/cmake/Qt6/Qt6Config.cmake")
100+
list(APPEND candidates "${root}/lib/cmake/Qt6")
101+
endif()
102+
103+
# Then scan for version subdirectories (Qt Online Installer layout).
104+
file(GLOB _version_dirs RELATIVE "${root}" "${root}/6.*")
105+
# Sort descending so newer versions are tried first.
106+
list(SORT _version_dirs COMPARE NATURAL ORDER DESCENDING)
107+
108+
foreach(vdir IN LISTS _version_dirs)
109+
set(vroot "${root}/${vdir}")
110+
if(NOT IS_DIRECTORY "${vroot}")
111+
continue()
112+
endif()
113+
114+
# Skip versions below the required minimum.
115+
if(min_version AND vdir VERSION_LESS min_version)
116+
continue()
117+
endif()
118+
119+
# Inside a version dir, compiler-specific subdirs hold the actual Qt
120+
# prefix (msvc2022_64, mingw_1200_64, gcc_64, macos, etc.). Prefer
121+
# 64-bit MSVC on Windows, then MinGW; prefer gcc_64 on Linux; prefer
122+
# macos on macOS.
123+
set(_compiler_candidates "")
124+
if(WIN32)
125+
list(APPEND _compiler_candidates
126+
"msvc2022_64" "msvc2019_64" "msvc2022_arm64"
127+
"mingw_1310_64" "mingw_1200_64" "mingw_1120_64" "mingw_64"
128+
"llvm-mingw_64"
129+
)
130+
elseif(APPLE)
131+
list(APPEND _compiler_candidates "macos" "clang_64")
132+
else()
133+
list(APPEND _compiler_candidates "gcc_64" "linux_gcc_64")
134+
endif()
135+
136+
foreach(comp IN LISTS _compiler_candidates)
137+
set(cmake_dir "${vroot}/${comp}/lib/cmake/Qt6")
138+
if(EXISTS "${cmake_dir}/Qt6Config.cmake")
139+
list(APPEND candidates "${cmake_dir}")
140+
endif()
141+
endforeach()
142+
143+
# Fallback: pick whichever compiler subdir exists with a Qt6Config.
144+
file(GLOB _comp_subdirs RELATIVE "${vroot}" "${vroot}/*")
145+
foreach(comp IN LISTS _comp_subdirs)
146+
set(cmake_dir "${vroot}/${comp}/lib/cmake/Qt6")
147+
if(EXISTS "${cmake_dir}/Qt6Config.cmake")
148+
list(APPEND candidates "${cmake_dir}")
149+
endif()
150+
endforeach()
151+
endforeach()
152+
153+
list(REMOVE_DUPLICATES candidates)
154+
set(${out_var} "${candidates}" PARENT_SCOPE)
155+
endfunction()
156+
157+
function(lightweight_probe_qt6)
158+
set(options "")
159+
set(oneValueArgs MIN_VERSION RESULT_VAR)
160+
set(multiValueArgs REQUIRED_COMPONENTS)
161+
cmake_parse_arguments(LW_QT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
162+
163+
if(NOT LW_QT_MIN_VERSION)
164+
set(LW_QT_MIN_VERSION "6.5.0")
165+
endif()
166+
167+
# Fast path: caller already pointed at Qt explicitly.
168+
if(DEFINED CACHE{Qt6_DIR} AND EXISTS "${Qt6_DIR}/Qt6Config.cmake")
169+
message(STATUS "Qt6 already configured via Qt6_DIR=${Qt6_DIR}, skipping probe.")
170+
if(LW_QT_RESULT_VAR)
171+
set(${LW_QT_RESULT_VAR} TRUE PARENT_SCOPE)
172+
endif()
173+
return()
174+
endif()
175+
176+
# Also fast-path if CMAKE_PREFIX_PATH / system package already resolves it.
177+
find_package(Qt6 ${LW_QT_MIN_VERSION} QUIET COMPONENTS Core)
178+
if(Qt6_FOUND)
179+
message(STATUS "Qt6 ${Qt6_VERSION} found via existing CMAKE_PREFIX_PATH / system config.")
180+
if(LW_QT_RESULT_VAR)
181+
set(${LW_QT_RESULT_VAR} TRUE PARENT_SCOPE)
182+
endif()
183+
return()
184+
endif()
185+
186+
_lw_qt_candidate_roots(_roots)
187+
188+
set(_probed "")
189+
set(_picked "")
190+
foreach(root IN LISTS _roots)
191+
_lw_qt_cmake_dirs_under_root("${root}" "${LW_QT_MIN_VERSION}" _dirs)
192+
foreach(d IN LISTS _dirs)
193+
list(APPEND _probed "${d}")
194+
if(NOT _picked)
195+
set(_picked "${d}")
196+
endif()
197+
endforeach()
198+
endforeach()
199+
200+
if(_picked)
201+
get_filename_component(_qt_prefix "${_picked}/../../.." ABSOLUTE)
202+
message(STATUS "Qt6 auto-detected at: ${_qt_prefix}")
203+
list(PREPEND CMAKE_PREFIX_PATH "${_qt_prefix}")
204+
set(CMAKE_PREFIX_PATH "${CMAKE_PREFIX_PATH}" PARENT_SCOPE)
205+
set(Qt6_DIR "${_picked}" CACHE PATH "Qt6 cmake config directory" FORCE)
206+
if(LW_QT_RESULT_VAR)
207+
set(${LW_QT_RESULT_VAR} TRUE PARENT_SCOPE)
208+
endif()
209+
return()
210+
endif()
211+
212+
# Nothing found. Report clearly so the user can fix their install or pass
213+
# -DQt6_DIR=... / -DCMAKE_PREFIX_PATH=... explicitly.
214+
message(STATUS "Qt6 auto-detection failed. Searched roots:")
215+
foreach(r IN LISTS _roots)
216+
message(STATUS " ${r}")
217+
endforeach()
218+
if(LW_QT_RESULT_VAR)
219+
set(${LW_QT_RESULT_VAR} FALSE PARENT_SCOPE)
220+
endif()
221+
endfunction()

src/tools/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,10 @@ add_subdirectory(dbtool)
66
add_subdirectory(large-db-generator)
77
add_subdirectory(lup2dbtool)
88
add_subdirectory(LupMigrationsPlugin)
9+
10+
# Optional: Qt 6 migrations GUI. LIGHTWEIGHT_BUILD_GUI is gated (and possibly
11+
# downgraded to OFF) by the top-level CMakeLists after probing for Qt, so we
12+
# only descend if it survived that gate.
13+
if(LIGHTWEIGHT_BUILD_GUI)
14+
add_subdirectory(migrations-gui)
15+
endif()

0 commit comments

Comments
 (0)