diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4933c40 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,45 @@ +name: Test + +on: + pull_request: + push: + branches: [main, master] + +jobs: + test: + name: Build and test + runs-on: ubuntu-latest + # The app ships as an Arch package built against Arch's Qt, so it's tested there. + container: archlinux:latest + defaults: + run: + shell: bash + steps: + - name: Install git for the checkout + run: pacman -Syu --noconfirm --needed git + + - uses: actions/checkout@v4 + + - name: Install the package's dependencies + # The package lives in omarchy-pkgs, so its PKGBUILD is the one list of what the app needs and CI reads it rather than keeping a copy. + run: | + curl -fsSL "https://raw.githubusercontent.com/omacom/omarchy-pkgs/8be89db82f2cdba1d6538fadf6de81929e7c12f4/pkgbuilds/${{ github.event.repository.name }}/PKGBUILD" -o "$RUNNER_TEMP/PKGBUILD" + source "$RUNNER_TEMP/PKGBUILD" + deps=("${depends[@]}" "${makedepends[@]}" "${checkdepends[@]}") + pacman -S --noconfirm --needed base-devel cmake dbus "${deps[@]%%[<>=]*}" + + - name: Set up a user and a locale like a desktop's + # Root can read any file, so tests about unreadable ones need an ordinary user, with a second group for tests that change a file's group; and the container generates no locale. + run: | + useradd -m tester + groupadd other + usermod -aG other tester + chown -R tester . + sed -i 's/^#en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen + locale-gen + + - name: Build + run: su tester -c bin/build + + - name: Test + run: su tester -c 'LANG=en_US.UTF-8 dbus-run-session -- bin/test' diff --git a/.gitignore b/.gitignore index 193d5b5..8379206 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,4 @@ /build/ /trials/ *.user -/pkgbuild/pkg/ -/pkgbuild/src/ -/pkgbuild/*.pkg.tar.* __pycache__/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e33601f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,76 @@ +cmake_minimum_required(VERSION 3.21) +project(hype LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +# Like Qt's own modules, Hype never throws or catches. Without unwinding tables +# and with link-time optimization, the installed binary is about a quarter smaller. +add_compile_options(-fno-exceptions) +add_compile_definitions(QT_NO_EXCEPTIONS) + +# A Qt from Qt's installer unpacked under build/qt/ wins over an older system +# Qt, newest first, unless CMAKE_PREFIX_PATH picks another. +if(NOT CMAKE_PREFIX_PATH AND NOT DEFINED ENV{CMAKE_PREFIX_PATH}) + file(GLOB bundled_qt LIST_DIRECTORIES true ${CMAKE_SOURCE_DIR}/build/qt/*/gcc_64) + list(SORT bundled_qt COMPARE NATURAL ORDER DESCENDING) + set(CMAKE_PREFIX_PATH ${bundled_qt}) +endif() + +find_package(Qt6 6.9 REQUIRED COMPONENTS + Core Gui Qml Quick QuickControls2 Multimedia Concurrent DBus Svg Widgets Test) +find_package(PkgConfig REQUIRED) +pkg_check_modules(WEBP REQUIRED IMPORTED_TARGET libwebp libwebpdemux) +find_package(ZLIB REQUIRED) + +# Everything but main.cpp, shared by the app and the tests, so each source +# compiles once and a new file in src/ needs no line here. +file(GLOB hype_sources CONFIGURE_DEPENDS src/*.cpp src/*.h) +list(REMOVE_ITEM hype_sources ${CMAKE_SOURCE_DIR}/src/main.cpp) +add_library(hype_core STATIC ${hype_sources}) +target_include_directories(hype_core PUBLIC src) +target_link_libraries(hype_core PUBLIC + Qt6::Core Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2 Qt6::Multimedia Qt6::Concurrent + Qt6::DBus Qt6::Svg PkgConfig::WEBP ZLIB::ZLIB) +target_compile_options(hype_core PUBLIC -Wall -Wextra) +set_target_properties(hype_core PROPERTIES INTERPROCEDURAL_OPTIMIZATION ON) + +add_executable(hype src/main.cpp src/resources.qrc) +target_link_libraries(hype PRIVATE hype_core) +set_target_properties(hype PROPERTIES INTERPROCEDURAL_OPTIMIZATION ON) + +enable_testing() + +# The tests read exports back with Qt Pdf, which ships in qt6-webengine, so +# building the app alone doesn't need it; testing without it fails, not skips. +find_package(Qt6 6.9 COMPONENTS Pdf) +if(TARGET Qt6::Pdf) + # The window-level tests load the same QML as the app, so they embed it too. + add_executable(hype_tests tests/tests.cpp src/resources.qrc) + target_link_libraries(hype_tests PRIVATE hype_core Qt6::Widgets Qt6::Pdf Qt6::Test) + set_target_properties(hype_tests PROPERTIES INTERPROCEDURAL_OPTIMIZATION ON) + add_test(NAME hype_tests COMMAND hype_tests) +else() + message(WARNING "Qt Pdf was not found, so the tests can't be built; install qt6-webengine.") + add_test(NAME hype_tests COMMAND sh -c "echo 'hype_tests needs Qt Pdf, from qt6-webengine' >&2; exit 1") +endif() +# Hype draws its own controls, so the tests keep Qt's platform theme and style out of it. +set_tests_properties(hype_tests PROPERTIES ENVIRONMENT + "QT_QPA_PLATFORM=offscreen;QT_QPA_PLATFORMTHEME=generic;QT_STYLE_OVERRIDE=Fusion") + +# The commands, exports and shutdown, driven through build/hype as an agent would. +# test_trials covers the trial import tools, which need their own Python packages. +find_package(Python3 COMPONENTS Interpreter) +if(Python3_FOUND) + add_test(NAME hype_commands + COMMAND ${Python3_EXECUTABLE} -m unittest test_cli test_export test_shutdown + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests) +else() + add_test(NAME hype_commands COMMAND sh -c "echo 'hype_commands needs Python 3' >&2; exit 1") +endif() +set_tests_properties(hype_commands PROPERTIES ENVIRONMENT QT_QPA_PLATFORM=offscreen) diff --git a/README.md b/README.md index 555ecec..8cc1a93 100644 --- a/README.md +++ b/README.md @@ -244,22 +244,21 @@ The mouse wheel over the sidebar selects the previous or next slide. Home/End ju ## Run from source -To build Hype yourself, install a C++17 compiler, make, Qt 6.9 or newer, FFmpeg, and GNU source-highlight; see [the package definition](pkgbuild/PKGBUILD) for dependencies. Then: +To build Hype yourself, install a C++17 compiler, CMake, Qt 6.9 or newer, FFmpeg, and GNU source-highlight; see [the package definition in omarchy-pkgs](https://github.com/omacom/omarchy-pkgs/blob/master/pkgbuilds/hype/PKGBUILD) for dependencies. Then: ```sh ./bin/build ./build/hype open examples/welcome.md ``` -`bin/build` uses a Qt toolchain under `build/qt/` when present, then falls back to `qmake6` on your PATH. It checks for Qt 6.9 or newer before compiling. Set `HYPE_QMAKE=/path/to/qmake6` to select a different toolchain. +The build uses a Qt toolchain from Qt's installer under `build/qt/` when present, newest first, then the system Qt, and checks for Qt 6.9 or newer before compiling. Set `CMAKE_PREFIX_PATH=/path/to/Qt/6.9.0/gcc_64` to select a different toolchain, and delete `build/CMakeCache.txt` when switching, since CMake remembers the Qt it found. For a launcher entry that rebuilds this checkout when opened, run `./bin/install-dev` and choose **Hype (Development)**. -Run `./bin/test` for the regression suite. After building the tests, the presenter -check can also run with two virtual displays: +Run `./bin/test` for the regression suite: the C++ tests (`hype_tests`, which read exported PDFs back with Qt Pdf from `qt6-webengine`) and the command-line tests (`hype_commands`, in Python). Arguments go to `ctest`, so `./bin/test -R hype_commands` runs only those. Set `HYPE_GUI_TESTS=0` to skip the C++ window-level tests on a machine without multimedia. After building the tests, the presenter check can also run with two virtual displays: ```sh -HYPE_GUI_TESTS=1 QT_QPA_PLATFORMTHEME=generic QT_STYLE_OVERRIDE=Fusion \ +QT_QPA_PLATFORMTHEME=generic QT_STYLE_OVERRIDE=Fusion \ QT_QPA_PLATFORM="offscreen:configfile=$PWD/tests/fixtures/presenter-screens.json" \ - ./build/tests/hype-tests presenterViewUsesSeparateScreens + ./build/hype_tests presenterViewUsesSeparateScreens ``` diff --git a/bin/build b/bin/build index 11f6655..c806951 100755 --- a/bin/build +++ b/bin/build @@ -1,42 +1,6 @@ -#!/bin/sh -set -eu -ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" -mkdir -p "$ROOT/build" -cd "$ROOT/build" +#!/bin/bash +set -euo pipefail -if [ -n "${HYPE_QMAKE:-}" ]; then - build_qmake=$HYPE_QMAKE -else - # Qt's installer names this executable qmake; distro packages also use qmake6. - # Version sorting keeps 6.10 ahead of 6.9 when both toolchains are installed. - build_qmake=$( - for candidate in "$ROOT"/build/qt/*/gcc_64/bin/qmake "$ROOT"/build/qt/*/gcc_64/bin/qmake6; do - if [ -x "$candidate" ]; then - printf '%s\n' "$candidate" - fi - done | sort -V | tail -n 1 - ) - if [ -z "$build_qmake" ]; then - build_qmake=$(command -v qmake6 || true) - fi -fi -if [ -z "$build_qmake" ]; then - printf 'Qt 6.9 or newer is required; qmake6 was not found.\n' >&2 - exit 1 -fi -qt_version=$("$build_qmake" -query QT_VERSION) -qt_major=${qt_version%%.*} -qt_minor=${qt_version#*.} -qt_minor=${qt_minor%%.*} -if [ "$qt_major" -lt 6 ] || { [ "$qt_major" -eq 6 ] && [ "$qt_minor" -lt 9 ]; }; then - printf 'Qt 6.9 or newer is required; %s reports Qt %s.\n' "$build_qmake" "$qt_version" >&2 - exit 1 -fi - -"$build_qmake" "$ROOT/hype.pro" -# Objects built with another Qt version cannot be safely reused. -if [ -f Makefile ] && [ "$(cat .qmake-used 2>/dev/null || true)" != "$build_qmake" ]; then - make clean -fi -make -j"$(nproc)" -printf '%s\n' "$build_qmake" > .qmake-used +root="$(cd "$(dirname "$0")/.." && pwd)" +cmake -S "$root" -B "$root/build" -DCMAKE_BUILD_TYPE=Release >/dev/null +cmake --build "$root/build" -j"$(nproc)" diff --git a/bin/install b/bin/install deleted file mode 100755 index f805111..0000000 --- a/bin/install +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -set -eu -ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" -cd "$ROOT/pkgbuild" -makepkg -fsi "$@" diff --git a/bin/test b/bin/test index d6be55e..080fc52 100755 --- a/bin/test +++ b/bin/test @@ -1,11 +1,7 @@ -#!/bin/sh -set -eu -ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" -mkdir -p "$ROOT/build/tests" -cd "$ROOT/build/tests" -qmake6 "$ROOT/tests/tests.pro" -make -j"$(nproc)" -# The window-level tests catch the regressions unit tests miss, so they run by default. -# Set HYPE_GUI_TESTS=0 to skip them, e.g. on a machine without multimedia. -if [ "${HYPE_GUI_TESTS-1}" = 0 ]; then unset HYPE_GUI_TESTS; else export HYPE_GUI_TESTS=1; fi -QT_QPA_PLATFORM=offscreen QT_QPA_PLATFORMTHEME=generic QT_STYLE_OVERRIDE=Fusion ./hype-tests "$@" +#!/bin/bash +set -euo pipefail + +# Arguments go to ctest, so `bin/test -V` shows every Qt test function as it runs. +root="$(cd "$(dirname "$0")/.." && pwd)" +"$root/bin/build" +QT_QPA_PLATFORM=offscreen ctest --test-dir "$root/build" --output-on-failure "$@" diff --git a/hype.pro b/hype.pro deleted file mode 100644 index 3b9918c..0000000 --- a/hype.pro +++ /dev/null @@ -1,28 +0,0 @@ -QT += core gui qml quick quickcontrols2 multimedia concurrent dbus svg -# Like Qt's own modules, Hype never throws or catches. Without unwinding tables and with -# link-time optimization, the installed binary is about a quarter smaller. -CONFIG += c++17 release ltcg exceptions_off -TARGET = hype -TEMPLATE = app -HEADERS += src/deck.h src/renderer.h src/budget.h -SOURCES += src/main.cpp src/deck.cpp src/renderer.cpp -RESOURCES += src/resources.qrc - -SOURCES += src/syntax.cpp -HEADERS += src/syntax.h -SOURCES += src/pptx.cpp -HEADERS += src/pptx.h -LIBS += -lz -lwebpdemux -lwebp - -SOURCES += src/animationexport.cpp -HEADERS += src/animationexport.h - -SOURCES += src/apptheme.cpp -HEADERS += src/apptheme.h -SOURCES += src/images.cpp -HEADERS += src/images.h -SOURCES += src/filedialog.cpp -HEADERS += src/filedialog.h -SOURCES += src/recovery.cpp -SOURCES += src/cli.cpp -HEADERS += src/cli.h diff --git a/pkgbuild/PKGBUILD b/pkgbuild/PKGBUILD deleted file mode 100644 index 58f6058..0000000 --- a/pkgbuild/PKGBUILD +++ /dev/null @@ -1,21 +0,0 @@ -pkgname=hype -pkgver=0.6.0 -pkgrel=1 -pkgdesc='Simple Markdown presentations with a visual slide editor' -arch=('x86_64' 'aarch64') -license=('MIT') -depends=('qt6-base>=6.9' 'qt6-declarative>=6.9' 'qt6-multimedia>=6.9' 'qt6-imageformats' 'qt6-svg' 'ffmpeg' 'source-highlight' 'zlib' 'libwebp' 'xdg-desktop-portal') -makedepends=('gcc' 'make') -source=() -sha256sums=() -build() { - cd "$startdir/.." - ./bin/build -} -package() { - cd "$startdir/.." - install -Dm755 build/hype "$pkgdir/usr/bin/hype" - install -Dm644 pkgbuild/hype.desktop "$pkgdir/usr/share/applications/hype.desktop" - install -Dm644 pkgbuild/hype.svg "$pkgdir/usr/share/icons/hicolor/scalable/apps/hype.svg" - install -Dm644 LICENSE "$pkgdir/usr/share/licenses/hype/LICENSE" -} diff --git a/tests/tests.cpp b/tests/tests.cpp index 15ece15..320e822 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -646,7 +646,7 @@ static void write(const QString &path, const QString &content) { QVERIFY(latest.recoveryVersions().size() >= 3); // Previous untitled work is still recoverable. } void historyPopupProtectsSlides() { - if (!qEnvironmentVariableIsSet("HYPE_GUI_TESTS")) QSKIP("Set HYPE_GUI_TESTS=1"); + if (qEnvironmentVariable("HYPE_GUI_TESTS") == "0") QSKIP("HYPE_GUI_TESTS=0 skips the window-level tests"); QTemporaryDir files; Deck deck; deck.editSource("# First\n---\n# Last\n"); deck.enableAutosave(files.filePath("recovery")); @@ -677,8 +677,8 @@ static void write(const QString &path, const QString &content) { window->close(); } void presenterScreensAndCloseWhilePresenting() { - if (!qEnvironmentVariableIsSet("HYPE_GUI_TESTS")) - QSKIP("Set HYPE_GUI_TESTS=1"); + if (qEnvironmentVariable("HYPE_GUI_TESTS") == "0") + QSKIP("HYPE_GUI_TESTS=0 skips the window-level tests"); Deck deck; deck.editSource("# First\n---\n# Last\n"); QQuickStyle::setStyle("Basic"); @@ -738,8 +738,8 @@ static void write(const QString &path, const QString &content) { window->close(); } void presenterViewUsesSeparateScreens() { - if (!qEnvironmentVariableIsSet("HYPE_GUI_TESTS")) - QSKIP("Set HYPE_GUI_TESTS=1"); + if (qEnvironmentVariable("HYPE_GUI_TESTS") == "0") + QSKIP("HYPE_GUI_TESTS=0 skips the window-level tests"); const auto screens = QGuiApplication::screens(); if (screens.size() != 2) QSKIP("Run with offscreen:configfile=tests/fixtures/presenter-screens.json"); @@ -827,8 +827,8 @@ static void write(const QString &path, const QString &content) { window->close(); } void placeMenuPlacesImage() { - if (!qEnvironmentVariableIsSet("HYPE_GUI_TESTS")) - QSKIP("Set HYPE_GUI_TESTS=1"); + if (qEnvironmentVariable("HYPE_GUI_TESTS") == "0") + QSKIP("HYPE_GUI_TESTS=0 skips the window-level tests"); Deck deck; deck.editSource("# Headline\n\n![](logo.png)\n"); QQuickStyle::setStyle("Basic"); @@ -1618,8 +1618,8 @@ static void write(const QString &path, const QString &content) { QVERIFY(!unavailable.exporting() && unavailable.exportFailed()); } void pasteCompressionProgress() { - if (!qEnvironmentVariableIsSet("HYPE_GUI_TESTS")) - QSKIP("Set HYPE_GUI_TESTS=1 with local multimedia access"); + if (qEnvironmentVariable("HYPE_GUI_TESTS") == "0") + QSKIP("HYPE_GUI_TESTS=0 skips the window-level tests"); QTemporaryDir files; Deck deck; QVERIFY(deck.savePath(files.path() + "/talk.md")); @@ -1698,8 +1698,8 @@ static void write(const QString &path, const QString &content) { QVERIFY(deck.status().contains("slide changed")); } void videoHoldsLastFrameAndReplays() { - if (!qEnvironmentVariableIsSet("HYPE_GUI_TESTS")) - QSKIP("Set HYPE_GUI_TESTS=1 with local multimedia access"); + if (qEnvironmentVariable("HYPE_GUI_TESTS") == "0") + QSKIP("HYPE_GUI_TESTS=0 skips the window-level tests"); if (qEnvironmentVariable("QT_QUICK_BACKEND") == "software") QSKIP("Video pixel capture requires QT_QUICK_BACKEND=rhi"); QTemporaryDir files; @@ -1769,8 +1769,8 @@ static void write(const QString &path, const QString &content) { QVERIFY(!player->videoSink()->videoFrame().isValid()); } void animatedImages() { - if (!qEnvironmentVariableIsSet("HYPE_GUI_TESTS")) - QSKIP("Set HYPE_GUI_TESTS=1 with local multimedia access"); + if (qEnvironmentVariable("HYPE_GUI_TESTS") == "0") + QSKIP("HYPE_GUI_TESTS=0 skips the window-level tests"); QTemporaryDir files; QVERIFY(QDir().mkpath(files.path() + "/images")); QVERIFY(QFile::copy(QFINDTESTDATA("fixtures/animated.webp"), @@ -1889,8 +1889,8 @@ static void write(const QString &path, const QString &content) { QCOMPARE(empty.property("end").toInt(), 11); } void typingCodeInVisualEditorKeepsTheDeck() { - if (!qEnvironmentVariableIsSet("HYPE_GUI_TESTS")) - QSKIP("Set HYPE_GUI_TESTS=1 with local graphics access"); + if (qEnvironmentVariable("HYPE_GUI_TESTS") == "0") + QSKIP("HYPE_GUI_TESTS=0 skips the window-level tests"); QTemporaryDir files; const QString path = files.path() + "/presentation.md"; const QString tail = "---\n# Following\n---\n# Last\n"; @@ -1985,8 +1985,8 @@ static void write(const QString &path, const QString &content) { QCOMPARE(deck.count(), 5); } void overviewMode() { - if (!qEnvironmentVariableIsSet("HYPE_GUI_TESTS")) - QSKIP("Set HYPE_GUI_TESTS=1 with local graphics access"); + if (qEnvironmentVariable("HYPE_GUI_TESTS") == "0") + QSKIP("HYPE_GUI_TESTS=0 skips the window-level tests"); QQuickStyle::setStyle("Basic"); qmlRegisterType("Hype", 1, 0, "SlideCanvas"); qmlRegisterType("Hype", 1, 0, "AppTheme"); @@ -2180,8 +2180,8 @@ static void write(const QString &path, const QString &content) { QTRY_VERIFY(slideEditor->hasActiveFocus()); } void visualOperations() { - if (!qEnvironmentVariableIsSet("HYPE_GUI_TESTS")) - QSKIP("Set HYPE_GUI_TESTS=1 with local multimedia access"); + if (qEnvironmentVariable("HYPE_GUI_TESTS") == "0") + QSKIP("HYPE_GUI_TESTS=0 skips the window-level tests"); QQuickStyle::setStyle("Basic"); qmlRegisterType("Hype", 1, 0, "SlideCanvas"); qmlRegisterType("Hype", 1, 0, "AppTheme"); @@ -3338,8 +3338,8 @@ static void write(const QString &path, const QString &content) { QCOMPARE(d.count(), 2); } void navigationKeepsDocumentControlsStable() { - if (!qEnvironmentVariableIsSet("HYPE_GUI_TESTS")) - QSKIP("Set HYPE_GUI_TESTS=1 with local multimedia access"); + if (qEnvironmentVariable("HYPE_GUI_TESTS") == "0") + QSKIP("HYPE_GUI_TESTS=0 skips the window-level tests"); QQuickStyle::setStyle("Basic"); qmlRegisterType("Hype", 1, 0, "SlideCanvas"); qmlRegisterType("Hype", 1, 0, "AppTheme"); @@ -3741,8 +3741,8 @@ static void write(const QString &path, const QString &content) { QCOMPARE(provider.requestImage(id, nullptr, QSize(340, 192)).cacheKey(), first.cacheKey()); } void rapidImageNavigation() { - if (!qEnvironmentVariableIsSet("HYPE_GUI_TESTS")) - QSKIP("Set HYPE_GUI_TESTS=1 with local multimedia access"); + if (qEnvironmentVariable("HYPE_GUI_TESTS") == "0") + QSKIP("HYPE_GUI_TESTS=0 skips the window-level tests"); QTemporaryDir tmp; QVERIFY(QDir().mkpath(tmp.path() + "/images")); QString source; diff --git a/tests/tests.pro b/tests/tests.pro deleted file mode 100644 index d8029c8..0000000 --- a/tests/tests.pro +++ /dev/null @@ -1,25 +0,0 @@ -QT += core gui qml quick quickcontrols2 multimedia widgets testlib pdf concurrent dbus svg -CONFIG += c++17 testcase -TEMPLATE = app -TARGET = hype-tests -INCLUDEPATH += ../src -SOURCES += tests.cpp ../src/deck.cpp ../src/renderer.cpp -HEADERS += ../src/deck.h ../src/renderer.h ../src/budget.h -RESOURCES += ../src/resources.qrc - -SOURCES += ../src/syntax.cpp -HEADERS += ../src/syntax.h -SOURCES += ../src/pptx.cpp -HEADERS += ../src/pptx.h -LIBS += -lz -lwebpdemux -lwebp - -SOURCES += ../src/animationexport.cpp -HEADERS += ../src/animationexport.h - -SOURCES += ../src/apptheme.cpp -HEADERS += ../src/apptheme.h -SOURCES += ../src/images.cpp -HEADERS += ../src/images.h -SOURCES += ../src/filedialog.cpp -SOURCES += ../src/recovery.cpp -HEADERS += ../src/filedialog.h