From c3c0ff92bcbde40473022adcb1b6ea3171635092 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 11 Nov 2026 23:00:28 +0100 Subject: [PATCH 1/9] Build with CMake Omarchy's apps now build the same way: one CMakeLists.txt with the sources as a static library shared by the app and its tests, and identical bin/build, bin/test, bin/install and CI across every app. Everything the two .pro files set carries over: the modules, the resources, -fno-exceptions with link-time optimization, -Wall -Wextra, zlib and libwebp (now found through pkg-config), and the Qt 6.9 floor, with a toolchain under build/qt/ still preferred. bin/test now runs ctest, which takes the C++ suite and the Python command tests, which nothing ran before. With the environment no longer set by the script, the window-level tests run unless HYPE_GUI_TESTS=0, rather than only when it is set. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 42 ++++++++++++++++++++++++ CMakeLists.txt | 67 ++++++++++++++++++++++++++++++++++++++ README.md | 11 +++---- bin/build | 46 +++----------------------- bin/install | 9 ++--- bin/test | 18 ++++------ hype.pro | 28 ---------------- pkgbuild/PKGBUILD | 3 +- tests/tests.cpp | 46 +++++++++++++------------- tests/tests.pro | 25 -------------- 10 files changed, 156 insertions(+), 139 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 CMakeLists.txt delete mode 100644 hype.pro delete mode 100644 tests/tests.pro diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e56f4a0 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,42 @@ +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 PKGBUILD is the one list of what the app needs, so CI reads it rather than keeping a copy. + run: | + source pkgbuild/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, and the container generates no locale. + run: | + useradd -m 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/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..bb30cec --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,67 @@ +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; CMAKE_PREFIX_PATH picks any other. +file(GLOB bundled_qt LIST_DIRECTORIES true ${CMAKE_SOURCE_DIR}/build/qt/*/gcc_64) +list(SORT bundled_qt COMPARE NATURAL ORDER DESCENDING) +list(PREPEND CMAKE_PREFIX_PATH ${bundled_qt}) + +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. +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) + add_test(NAME hype_tests COMMAND hype_tests) + # 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_PLATFORMTHEME=generic;QT_STYLE_OVERRIDE=Fusion") +else() + message(WARNING "Qt Pdf was not found, so the tests can't be built; install qt6-webengine.") +endif() + +# The commands, exports and shutdown, driven through build/hype as an agent would. +find_package(Python3 COMPONENTS Interpreter) +if(Python3_FOUND) + add_test(NAME hype_commands + COMMAND ${Python3_EXECUTABLE} -m unittest discover -s tests -p "test_*.py" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) +endif() diff --git a/README.md b/README.md index 555ecec..5ba95a7 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](pkgbuild/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 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 index f805111..b982bd9 100755 --- a/bin/install +++ b/bin/install @@ -1,5 +1,6 @@ -#!/bin/sh -set -eu -ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" -cd "$ROOT/pkgbuild" +#!/bin/bash +set -euo pipefail + +root="$(cd "$(dirname "$0")/.." && pwd)" +cd "$root/pkgbuild" makepkg -fsi "$@" diff --git a/bin/test b/bin/test index d6be55e..0fc642c 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 -R theme` runs only the matching tests. +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 index 58f6058..7f04568 100644 --- a/pkgbuild/PKGBUILD +++ b/pkgbuild/PKGBUILD @@ -5,7 +5,8 @@ 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') +makedepends=('cmake' 'gcc' 'make' 'pkgconf') +checkdepends=('qt6-webengine' 'python') source=() sha256sums=() build() { 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 From 85499b7f291b9db27b7e12e1cfa1d748b3bf2d49 Mon Sep 17 00:00:00 2001 From: Omarchybot <317366263+omarchybot@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:06:48 +0100 Subject: [PATCH 2/9] Give the tests fonts, and leave the trial tools out of ctest A bare Arch container has no fonts, so slides painted nothing and the font picker had one family to choose from. test_trials needs the trial importer's own Python packages, which only that tooling uses. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 5 +++-- pkgbuild/PKGBUILD | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bb30cec..c382b9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,9 +59,10 @@ else() endif() # 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 discover -s tests -p "test_*.py" - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + COMMAND ${Python3_EXECUTABLE} -m unittest test_cli test_export test_shutdown + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests) endif() diff --git a/pkgbuild/PKGBUILD b/pkgbuild/PKGBUILD index 7f04568..1cefdf2 100644 --- a/pkgbuild/PKGBUILD +++ b/pkgbuild/PKGBUILD @@ -6,7 +6,7 @@ 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=('cmake' 'gcc' 'make' 'pkgconf') -checkdepends=('qt6-webengine' 'python') +checkdepends=('qt6-webengine' 'python' 'ttf-jetbrains-mono' 'ttf-dejavu') source=() sha256sums=() build() { From 063d2c618c94a0fa9dfb30298dfa9d8642ba583b Mon Sep 17 00:00:00 2001 From: Omarchybot <317366263+omarchybot@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:07:27 +0100 Subject: [PATCH 3/9] Let CMAKE_PREFIX_PATH choose Qt over the one under build/qt The bundled toolchain was prepended to the search path, which CMake searches before the environment variable, so the documented override couldn't select another Qt. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c382b9e..1a8c692 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,10 +15,12 @@ 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; CMAKE_PREFIX_PATH picks any other. -file(GLOB bundled_qt LIST_DIRECTORIES true ${CMAKE_SOURCE_DIR}/build/qt/*/gcc_64) -list(SORT bundled_qt COMPARE NATURAL ORDER DESCENDING) -list(PREPEND CMAKE_PREFIX_PATH ${bundled_qt}) +# 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) From 2b409a53be36f46edb8b08978693c6a546d13b91 Mon Sep 17 00:00:00 2001 From: Omarchybot <317366263+omarchybot@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:11:08 +0100 Subject: [PATCH 4/9] Test with Noto fonts, as Omarchy installs them The font picker folds Noto's many families into a few, and the test that it rebuilds on a font change relies on that fold being there. Co-Authored-By: Claude Opus 5.5 --- pkgbuild/PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgbuild/PKGBUILD b/pkgbuild/PKGBUILD index 1cefdf2..a5c4c64 100644 --- a/pkgbuild/PKGBUILD +++ b/pkgbuild/PKGBUILD @@ -6,7 +6,7 @@ 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=('cmake' 'gcc' 'make' 'pkgconf') -checkdepends=('qt6-webengine' 'python' 'ttf-jetbrains-mono' 'ttf-dejavu') +checkdepends=('qt6-webengine' 'python' 'ttf-jetbrains-mono' 'noto-fonts') source=() sha256sums=() build() { From 56e20238f1635f149b0d44f804986364c8dc4d72 Mon Sep 17 00:00:00 2001 From: Omarchybot <317366263+omarchybot@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:18:31 +0100 Subject: [PATCH 5/9] Link the tests with LTO too, and depend on qt6-wayland The core library's objects are LTO-only, so the test link says so rather than relying on the linker plugin. qt6-wayland supplies Qt's native Wayland platform plugin; without it Hype runs under XWayland. The HYPE_GUI_TESTS note now says it covers the C++ window-level tests, since test_shutdown always opens the editor. Co-Authored-By: Claude Opus 5.5 Co-Authored-By: Codex --- CMakeLists.txt | 1 + README.md | 2 +- pkgbuild/PKGBUILD | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a8c692..c4c1c5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,7 @@ 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) # 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_PLATFORMTHEME=generic;QT_STYLE_OVERRIDE=Fusion") diff --git a/README.md b/README.md index 5ba95a7..cb5fb77 100644 --- a/README.md +++ b/README.md @@ -255,7 +255,7 @@ The build uses a Qt toolchain from Qt's installer under `build/qt/` when present 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: 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 window-level tests on a machine without multimedia. 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 QT_QPA_PLATFORMTHEME=generic QT_STYLE_OVERRIDE=Fusion \ diff --git a/pkgbuild/PKGBUILD b/pkgbuild/PKGBUILD index a5c4c64..da100a5 100644 --- a/pkgbuild/PKGBUILD +++ b/pkgbuild/PKGBUILD @@ -4,7 +4,7 @@ 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') +depends=('qt6-base>=6.9' 'qt6-declarative>=6.9' 'qt6-multimedia>=6.9' 'qt6-imageformats' 'qt6-svg' 'qt6-wayland' 'ffmpeg' 'source-highlight' 'zlib' 'libwebp' 'xdg-desktop-portal') makedepends=('cmake' 'gcc' 'make' 'pkgconf') checkdepends=('qt6-webengine' 'python' 'ttf-jetbrains-mono' 'noto-fonts') source=() From d888f2e8ad5015055b421f48a0dcbf8fd70deca3 Mon Sep 17 00:00:00 2001 From: Omarchybot <317366263+omarchybot@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:23:06 +0100 Subject: [PATCH 6/9] Take the corrected comment in the shared bin/test ctest -R matches test names, not Qt test functions; -V is what shows those. Co-Authored-By: Claude Opus 5.5 --- bin/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/test b/bin/test index 0fc642c..080fc52 100755 --- a/bin/test +++ b/bin/test @@ -1,7 +1,7 @@ #!/bin/bash set -euo pipefail -# Arguments go to ctest, so `bin/test -R theme` runs only the matching tests. +# 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 "$@" From 772af76486241eea72c275147f13cd84dcde1c09 Mon Sep 17 00:00:00 2001 From: Omarchybot <317366263+omarchybot@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:31:19 +0100 Subject: [PATCH 7/9] Leave packaging to omarchy-pkgs and take its dependencies in CI The package lives in omarchy-pkgs, so CI now reads the dependency list from there, and this change keeps the in-repo PKGBUILD and bin/install as they were apart from what building with CMake needs. Each test sets its own offscreen platform so a bare ctest works too, and a test whose dependency is missing fails with what to install rather than vanishing from the run. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 9 ++++++--- CMakeLists.txt | 11 ++++++++--- bin/install | 9 ++++----- pkgbuild/PKGBUILD | 3 +-- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e56f4a0..d09dcca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,16 +21,19 @@ jobs: - uses: actions/checkout@v4 - name: Install the package's dependencies - # The PKGBUILD is the one list of what the app needs, so CI reads it rather than keeping a copy. + # 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: | - source pkgbuild/PKGBUILD + curl -fsSL "https://raw.githubusercontent.com/omacom/omarchy-pkgs/master/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, and the container generates no locale. + # 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 diff --git a/CMakeLists.txt b/CMakeLists.txt index c4c1c5d..e33601f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,7 +47,7 @@ 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. +# 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. @@ -55,11 +55,13 @@ if(TARGET Qt6::Pdf) 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) - # 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_PLATFORMTHEME=generic;QT_STYLE_OVERRIDE=Fusion") 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. @@ -68,4 +70,7 @@ 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/bin/install b/bin/install index b982bd9..f805111 100755 --- a/bin/install +++ b/bin/install @@ -1,6 +1,5 @@ -#!/bin/bash -set -euo pipefail - -root="$(cd "$(dirname "$0")/.." && pwd)" -cd "$root/pkgbuild" +#!/bin/sh +set -eu +ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +cd "$ROOT/pkgbuild" makepkg -fsi "$@" diff --git a/pkgbuild/PKGBUILD b/pkgbuild/PKGBUILD index da100a5..6635a60 100644 --- a/pkgbuild/PKGBUILD +++ b/pkgbuild/PKGBUILD @@ -4,9 +4,8 @@ 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' 'qt6-wayland' 'ffmpeg' 'source-highlight' 'zlib' 'libwebp' 'xdg-desktop-portal') +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=('cmake' 'gcc' 'make' 'pkgconf') -checkdepends=('qt6-webengine' 'python' 'ttf-jetbrains-mono' 'noto-fonts') source=() sha256sums=() build() { From bdc417e3db24c4834457c314046c73c497e3c10d Mon Sep 17 00:00:00 2001 From: Omarchybot <317366263+omarchybot@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:52:43 +0100 Subject: [PATCH 8/9] Leave packaging to omarchy-pkgs The Arch package is built in omarchy-pkgs from the release tarball, with bin/build and the desktop entry and icon in pkgbuild/. The local PKGBUILD and the bin/install that ran makepkg on it went unused, so they go, with the ignores for their output, and the README points at omarchy-pkgs for dependencies. bin/install-dev stays: it registers a launcher for this checkout and needs only the icon. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 3 --- README.md | 2 +- bin/install | 5 ----- pkgbuild/PKGBUILD | 21 --------------------- 4 files changed, 1 insertion(+), 30 deletions(-) delete mode 100755 bin/install delete mode 100644 pkgbuild/PKGBUILD 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/README.md b/README.md index cb5fb77..8cc1a93 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,7 @@ 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, CMake, 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 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/pkgbuild/PKGBUILD b/pkgbuild/PKGBUILD deleted file mode 100644 index 6635a60..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=('cmake' 'gcc' 'make' 'pkgconf') -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" -} From 5c064bfb9d9159e6346a7d764bbe12709398da18 Mon Sep 17 00:00:00 2001 From: Omarchybot <317366263+omarchybot@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:54:17 +0100 Subject: [PATCH 9/9] Proof only: read dependencies from omarchy-pkgs#2573 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d09dcca..4933c40 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: - 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/master/pkgbuilds/${{ github.event.repository.name }}/PKGBUILD" -o "$RUNNER_TEMP/PKGBUILD" + 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[@]%%[<>=]*}"