diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml new file mode 100644 index 0000000000..54811d830c --- /dev/null +++ b/.github/workflows/linux-build.yml @@ -0,0 +1,585 @@ +name: Linux Build + +# Produces self-contained AppImages for QLC+ (qlcplus-qml / v5) on: +# • x86_64 — native GitHub runner, Qt installed via jurplel/install-qt-action +# • aarch64 — GitHub-hosted ARM64 runner (ubuntu-22.04-arm), system Qt 6 from apt +# +# armv7 is NOT built automatically: GitHub has no armv7 runners, Qt 6 for armv7 +# is only available as Qt 6.2.x via apt on Ubuntu 22.04, and full QEMU emulation +# of a Qt 6 build takes 45–90 minutes which is impractical for CI. If you need +# an armv7 artifact, set the `build_armv7` workflow_dispatch input to true; the +# job will run but may time out on free-tier runners. + +on: + workflow_dispatch: + inputs: + build_armv7: + description: 'Also attempt armv7 (slow QEMU cross-build, may time-out)' + type: boolean + default: false + + push: + tags: + - 'v*' + + pull_request: + paths: + - '**.cpp' + - '**.h' + - '**.qml' + - '**/CMakeLists.txt' + - 'variables.cmake' + - 'coverage.cmake' + - '.github/workflows/linux-build.yml' + - 'platforms/linux/**' + +# Cancel in-progress runs for the same ref on PRs so we don't waste runners. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +# Default to read-only token; individual jobs that publish releases override. +permissions: + contents: read + +# --------------------------------------------------------------------------- +# Shared setup steps are inlined per job because GitHub Actions does not +# allow the `matrix` context in job-level `if` conditions, so we cannot use +# a single matrix entry with a per-row skip flag. Two separate jobs keep +# the conditional logic simple and explicit. +# --------------------------------------------------------------------------- + +jobs: + # ── x86_64 ────────────────────────────────────────────────────────────────── + # Runs on every trigger (PR, tag, dispatch, branch push). + # Qt installed via jurplel/install-qt-action (prebuilt binaries, known paths). + # Unit tests executed here. + build-x86_64: + name: Build (x86_64) + runs-on: ubuntu-22.04 + permissions: + contents: write # needed by softprops/action-gh-release + + defaults: + run: + shell: bash + + env: + APPIMAGE_EXTRACT_AND_RUN: "1" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: false + fetch-depth: 0 + + - name: Install system dependencies + run: | + set -euxo pipefail + sudo apt-get update -q + sudo apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + ninja-build \ + pkg-config \ + libusb-1.0-0-dev \ + libftdi1-dev \ + libasound2-dev \ + libudev-dev \ + libfftw3-dev \ + libmad0-dev \ + libsndfile1-dev \ + liblo-dev \ + libpulse-dev \ + libfuse2 \ + patchelf \ + shared-mime-info \ + wget \ + file + + - name: Install Qt via install-qt-action (x86_64) + uses: jurplel/install-qt-action@v4 + with: + version: '6.9.3' + cache: true + modules: 'qt3d qtimageformats qtmultimedia qtserialport qtwebsockets' + + - name: Cache CMake build directory + uses: actions/cache@v4 + with: + path: build + key: Linux-x86_64-cmake-${{ hashFiles('**/CMakeLists.txt', 'variables.cmake') }} + restore-keys: Linux-x86_64-cmake- + + - name: Set build environment variables + run: | + set -euxo pipefail + APPVERSION=$(awk -F'"' '/set\(APPVERSION "5\./{print $2; exit}' variables.cmake \ + | tr ' ' '-') + GIT_REV=$(git rev-parse --short HEAD) + { + echo "APPVERSION=${APPVERSION}-${GIT_REV}" + echo "GIT_REV=${GIT_REV}" + echo "INSTALL_ROOT=$(pwd)/AppDir" + # install-qt-action exports QT_ROOT_DIR (not QTDIR) + echo "QMAKE=${QT_ROOT_DIR}/bin/qmake" + } >> "$GITHUB_ENV" + + - name: Configure CMake + run: | + set -euxo pipefail + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -Dqmlui=ON \ + -Dmcp_server=ON \ + -Dappimage=ON \ + "-DINSTALL_ROOT=${INSTALL_ROOT}" \ + "-DCMAKE_PREFIX_PATH=${QT_ROOT_DIR}/lib/cmake" + + - name: Build + run: | + set -euxo pipefail + cmake --build build -j"$(nproc)" + + - name: Run unit tests + run: | + set -euxo pipefail + QT_QPA_PLATFORM=offscreen build/mcp/test/mcp_vc_query_filter_test + QT_QPA_PLATFORM=offscreen build/mcp/test/mcp_vc_validation_test + + - name: Install to AppDir + run: | + set -euxo pipefail + cmake --install build + + - name: Download linuxdeploy and plugin-qt + run: | + set -euxo pipefail + wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage \ + -O linuxdeploy.AppImage + wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage \ + -O linuxdeploy-plugin-qt.AppImage + wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage \ + -O appimagetool.AppImage + chmod +x linuxdeploy.AppImage linuxdeploy-plugin-qt.AppImage appimagetool.AppImage + + # linuxdeploy uses ldd to find all shared-library dependencies and copies + # them to AppDir/usr/lib. linuxdeploy-plugin-qt additionally copies Qt + # platform plugins, image-format plugins, QML imports, and creates a + # qt.conf so Qt finds its plugins at runtime inside the AppImage. + # LD_LIBRARY_PATH must include AppDir/usr/lib so linuxdeploy can resolve + # libqlcplusengine.so and other QLC+ libraries installed by cmake --install. + - name: Bundle dependencies with linuxdeploy + env: + QMAKE: ${{ env.QMAKE }} + QML_SOURCES_PATHS: ${{ github.workspace }}/qmlui + LD_LIBRARY_PATH: ${{ github.workspace }}/AppDir/usr/lib + run: | + set -euxo pipefail + ./linuxdeploy.AppImage \ + --appdir AppDir \ + --executable AppDir/usr/bin/qlcplus-qml \ + --desktop-file platforms/linux/qlcplus5.desktop \ + --icon-file resources/icons/png/qlcplus.png \ + --plugin qt \ + --verbosity 1 + + # QLC+ compiles data paths as relative strings (e.g. "../share/qlcplus") + # resolved against the process CWD. -Dappimage=ON installs data under + # AppDir/share/ and the binary under AppDir/usr/bin/. Setting CWD to + # AppDir/usr before exec makes those "../…" paths resolve correctly. + - name: Write custom AppRun + run: | + set -euxo pipefail + cat > AppDir/AppRun << 'APPRUN_EOF' + #!/bin/bash + set -e + HERE="$(dirname "$(readlink -f "${0}")")" + export APPDIR="$HERE" + export QT_PLUGIN_PATH="${HERE}/usr/plugins${QT_PLUGIN_PATH:+:${QT_PLUGIN_PATH}}" + export QML2_IMPORT_PATH="${HERE}/usr/qml${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" + export LD_LIBRARY_PATH="${HERE}/usr/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" + cd "${HERE}/usr" + exec "${HERE}/usr/bin/qlcplus-qml" "$@" + APPRUN_EOF + sed -i 's/^[[:space:]]*//' AppDir/AppRun + chmod +x AppDir/AppRun + + - name: Build AppImage + run: | + set -euxo pipefail + OUTPUT="QLC+-${APPVERSION}-x86_64.AppImage" + ./appimagetool.AppImage -v AppDir "$OUTPUT" + echo "APPIMAGE_PATH=${OUTPUT}" >> "$GITHUB_ENV" + + - name: Generate SHA256 checksum + run: | + set -euxo pipefail + sha256sum "${APPIMAGE_PATH}" > "${APPIMAGE_PATH}.sha256" + + - name: Upload AppImage artifact + uses: actions/upload-artifact@v4 + with: + name: QLC+-${{ env.APPVERSION }}-x86_64 + path: | + ${{ env.APPIMAGE_PATH }} + ${{ env.APPIMAGE_PATH }}.sha256 + if-no-files-found: error + + - name: Attach to GitHub Release + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + uses: softprops/action-gh-release@v2 + with: + files: | + ${{ env.APPIMAGE_PATH }} + ${{ env.APPIMAGE_PATH }}.sha256 + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # ── aarch64 ───────────────────────────────────────────────────────────────── + # Skipped on pull_request events to keep PR CI fast (ARM builds are ~2x slower). + # Uses system Qt 6 from Ubuntu 22.04 apt because jurplel/install-qt-action does + # not ship Linux/ARM desktop Qt prebuilts. + # Runner: ubuntu-22.04-arm (GitHub-hosted ARM64). If that label is unavailable, + # replace with "ubuntu-22.04" and add docker/setup-qemu-action for emulation. + build-aarch64: + name: Build (aarch64) + runs-on: ubuntu-22.04-arm + permissions: + contents: write + if: ${{ github.event_name != 'pull_request' }} + + defaults: + run: + shell: bash + + env: + APPIMAGE_EXTRACT_AND_RUN: "1" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: false + fetch-depth: 0 + + - name: Install system dependencies + run: | + set -euxo pipefail + sudo apt-get update -q + sudo apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + ninja-build \ + pkg-config \ + libusb-1.0-0-dev \ + libftdi1-dev \ + libasound2-dev \ + libudev-dev \ + libfftw3-dev \ + libmad0-dev \ + libsndfile1-dev \ + liblo-dev \ + libpulse-dev \ + libfuse2 \ + patchelf \ + shared-mime-info \ + wget \ + file + + - name: Install Qt via apt (aarch64) + run: | + set -euxo pipefail + # Ensure the 'universe' repository is enabled (ARM runners may ship + # with a minimal sources.list that omits it). + sudo apt-add-repository universe -y + sudo apt-get update -q + sudo apt-get install -y --no-install-recommends \ + qt6-base-dev \ + qt6-base-private-dev \ + qt6-declarative-dev \ + qt6-declarative-private-dev \ + qt6-multimedia-dev \ + libqt6websockets6-dev \ + libqt6svg6-dev \ + qt6-3d-dev \ + libqt6serialport6-dev \ + qt6-tools-dev \ + qt6-tools-dev-tools \ + qt6-l10n-tools \ + qt6-image-formats-plugins \ + libqt6opengl6-dev \ + qml6-module-qtquick \ + qml6-module-qtquick-controls \ + qml6-module-qtquick-layouts \ + qml6-module-qtquick-window \ + qml6-module-qtmultimedia \ + libgl1-mesa-dev + + - name: Add Qt tools to PATH (aarch64) + run: | + set -euxo pipefail + echo "/usr/lib/qt6/bin" >> "$GITHUB_PATH" + + - name: Cache CMake build directory + uses: actions/cache@v4 + with: + path: build + key: Linux-aarch64-cmake-${{ hashFiles('**/CMakeLists.txt', 'variables.cmake') }} + restore-keys: Linux-aarch64-cmake- + + - name: Set build environment variables + run: | + set -euxo pipefail + APPVERSION=$(awk -F'"' '/set\(APPVERSION "5\./{print $2; exit}' variables.cmake \ + | tr ' ' '-') + GIT_REV=$(git rev-parse --short HEAD) + QMAKE=$(command -v qmake6 || command -v qmake) + { + echo "APPVERSION=${APPVERSION}-${GIT_REV}" + echo "GIT_REV=${GIT_REV}" + echo "INSTALL_ROOT=$(pwd)/AppDir" + echo "QMAKE=${QMAKE}" + } >> "$GITHUB_ENV" + + - name: Configure CMake + run: | + set -euxo pipefail + # Ubuntu arm64 packaging: Qt6LinguistToolsTargets.cmake expects + # lprodump in /usr/lib/qt6/libexec/ but it may be in /usr/lib/qt6/bin/. + # Check for the binary, not the directory. + sudo mkdir -p /usr/lib/qt6/libexec + if [ ! -x /usr/lib/qt6/libexec/lprodump ] && [ -d /usr/lib/qt6/bin ]; then + for f in /usr/lib/qt6/bin/*; do + [ -f "$f" ] && sudo ln -sf "$f" "/usr/lib/qt6/libexec/$(basename "$f")" + done + fi + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -Dqmlui=ON \ + -Dmcp_server=ON \ + -Dappimage=ON \ + "-DINSTALL_ROOT=${INSTALL_ROOT}" + + - name: Build + run: | + set -euxo pipefail + cmake --build build -j"$(nproc)" + + - name: Install to AppDir + run: | + set -euxo pipefail + cmake --install build + + - name: Download linuxdeploy and plugin-qt + run: | + set -euxo pipefail + wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage \ + -O linuxdeploy.AppImage + wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-aarch64.AppImage \ + -O linuxdeploy-plugin-qt.AppImage + wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-aarch64.AppImage \ + -O appimagetool.AppImage + chmod +x linuxdeploy.AppImage linuxdeploy-plugin-qt.AppImage appimagetool.AppImage + + - name: Bundle dependencies with linuxdeploy + env: + QMAKE: ${{ env.QMAKE }} + QML_SOURCES_PATHS: ${{ github.workspace }}/qmlui + LD_LIBRARY_PATH: ${{ github.workspace }}/AppDir/usr/lib + run: | + set -euxo pipefail + ./linuxdeploy.AppImage \ + --appdir AppDir \ + --executable AppDir/usr/bin/qlcplus-qml \ + --desktop-file platforms/linux/qlcplus5.desktop \ + --icon-file resources/icons/png/qlcplus.png \ + --plugin qt \ + --verbosity 1 + + - name: Write custom AppRun + run: | + set -euxo pipefail + cat > AppDir/AppRun << 'APPRUN_EOF' + #!/bin/bash + set -e + HERE="$(dirname "$(readlink -f "${0}")")" + export APPDIR="$HERE" + export QT_PLUGIN_PATH="${HERE}/usr/plugins${QT_PLUGIN_PATH:+:${QT_PLUGIN_PATH}}" + export QML2_IMPORT_PATH="${HERE}/usr/qml${QML2_IMPORT_PATH:+:${QML2_IMPORT_PATH}}" + export LD_LIBRARY_PATH="${HERE}/usr/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" + cd "${HERE}/usr" + exec "${HERE}/usr/bin/qlcplus-qml" "$@" + APPRUN_EOF + sed -i 's/^[[:space:]]*//' AppDir/AppRun + chmod +x AppDir/AppRun + + - name: Build AppImage + run: | + set -euxo pipefail + OUTPUT="QLC+-${APPVERSION}-aarch64.AppImage" + ./appimagetool.AppImage -v AppDir "$OUTPUT" + echo "APPIMAGE_PATH=${OUTPUT}" >> "$GITHUB_ENV" + + - name: Generate SHA256 checksum + run: | + set -euxo pipefail + sha256sum "${APPIMAGE_PATH}" > "${APPIMAGE_PATH}.sha256" + + - name: Upload AppImage artifact + uses: actions/upload-artifact@v4 + with: + name: QLC+-${{ env.APPVERSION }}-aarch64 + path: | + ${{ env.APPIMAGE_PATH }} + ${{ env.APPIMAGE_PATH }}.sha256 + if-no-files-found: error + + - name: Attach to GitHub Release + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + uses: softprops/action-gh-release@v2 + with: + files: | + ${{ env.APPIMAGE_PATH }} + ${{ env.APPIMAGE_PATH }}.sha256 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # --------------------------------------------------------------------------- + # armv7 — optional, workflow_dispatch only + # --------------------------------------------------------------------------- + # armv7 is gated behind a manual input because: + # 1. GitHub does not offer native armv7 (32-bit ARM) hosted runners. + # 2. QEMU emulation of a full Qt 6 CMake build takes 45–90 minutes, + # which exceeds typical free-tier job time limits. + # 3. Qt 6 for Linux/armhf is only available as Qt 6.2.x via Ubuntu 22.04 + # apt (no official Qt prebuilts for Linux armhf desktop). + # If you need armv7 artifacts, consider a dedicated self-hosted armhf runner + # or a Raspberry Pi build farm instead. + build-armv7: + name: Build (armv7) [manual only] + runs-on: ubuntu-22.04 + if: ${{ github.event_name == 'workflow_dispatch' && inputs.build_armv7 == true }} + permissions: + contents: write + + defaults: + run: + shell: bash + + env: + APPIMAGE_EXTRACT_AND_RUN: "1" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: false + fetch-depth: 0 + + - name: Set up QEMU for armv7 + uses: docker/setup-qemu-action@v3 + with: + platforms: arm + + - name: Build inside armv7 Docker container + uses: addnab/docker-run-action@v3 + with: + image: arm32v7/ubuntu:22.04 + options: >- + --platform linux/arm/v7 + -v ${{ github.workspace }}:/workspace + -e DEBIAN_FRONTEND=noninteractive + run: | + set -euxo pipefail + apt-get update -q + apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build pkg-config \ + qt6-base-dev qt6-base-private-dev \ + qt6-declarative-dev qt6-declarative-private-dev \ + qt6-multimedia-dev qt6-websockets-dev qt6-svg-dev \ + qt6-3d-dev qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools \ + libusb-1.0-0-dev libftdi1-dev libasound2-dev libudev-dev \ + libfftw3-dev libmad0-dev libsndfile1-dev liblo-dev libpulse-dev \ + patchelf wget file git + # Fix Qt6 libexec layout if lprodump is missing + mkdir -p /usr/lib/qt6/libexec + if [ ! -x /usr/lib/qt6/libexec/lprodump ] && [ -d /usr/lib/qt6/bin ]; then + for f in /usr/lib/qt6/bin/*; do + [ -f "$f" ] && ln -sf "$f" "/usr/lib/qt6/libexec/$(basename "$f")" + done + fi + cd /workspace + APPVERSION=$(awk -F'"' '/set\(APPVERSION "5\./{print $2; exit}' variables.cmake \ + | tr ' ' '-') + GIT_REV=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") + APPVERSION="${APPVERSION}-${GIT_REV}" + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -Dqmlui=ON \ + -Dmcp_server=ON \ + -Dappimage=ON \ + "-DINSTALL_ROOT=/workspace/AppDir-armv7" + cmake --build build -j"$(nproc)" + cmake --install build + tar -czf "QLC+-${APPVERSION}-armv7.tar.gz" -C /workspace AppDir-armv7 + sha256sum "QLC+-${APPVERSION}-armv7.tar.gz" \ + > "QLC+-${APPVERSION}-armv7.tar.gz.sha256" + echo "APPVERSION=${APPVERSION}" >> /workspace/.armv7_env + + - name: Read armv7 env + run: | + set -euxo pipefail + source .armv7_env + { + echo "APPVERSION=${APPVERSION}" + echo "TARBALL=QLC+-${APPVERSION}-armv7.tar.gz" + } >> "$GITHUB_ENV" + + - name: Upload armv7 tarball artifact + uses: actions/upload-artifact@v4 + with: + name: QLC+-${{ env.APPVERSION }}-armv7 + path: | + ${{ env.TARBALL }} + ${{ env.TARBALL }}.sha256 + if-no-files-found: error + + - name: Attach armv7 tarball to GitHub Release + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + uses: softprops/action-gh-release@v2 + with: + files: | + ${{ env.TARBALL }} + ${{ env.TARBALL }}.sha256 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # --------------------------------------------------------------------------- + # Aggregation job — branch protection can require this single check + # --------------------------------------------------------------------------- + ci-success: + name: CI Success + runs-on: ubuntu-22.04 + needs: [build-x86_64, build-aarch64] + if: always() + permissions: + contents: read + steps: + - name: Check required build jobs + run: | + x86_result="${{ needs.build-x86_64.result }}" + arm_result="${{ needs.build-aarch64.result }}" + if [[ "${x86_result}" != "success" ]]; then + echo "❌ x86_64 build ${x86_result}." + exit 1 + fi + # aarch64 is intentionally skipped on pull_request events; only fail + # if it actually ran and failed. + if [[ "${arm_result}" == "failure" ]]; then + echo "❌ aarch64 build failed." + exit 1 + fi + echo "✅ x86_64=${x86_result}, aarch64=${arm_result}." diff --git a/README.md b/README.md index 4bf77b43a7..486a768e2f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,13 @@

Q Light Controller+

+## Linux Builds (AppImage) + +Pre-built, self-contained AppImages for **x86\_64** and **aarch64** are +attached to every [tagged release](../../releases). See +[docs/LINUX_BUILDS.md](docs/LINUX_BUILDS.md) for download, verification, and +run instructions. + > ## ⚠️ EXPERIMENTAL FORK — USE AT YOUR OWN RISK ⚠️ > > **This fork was largely vibe-coded with AI pair-programming.** diff --git a/docs/LINUX_BUILDS.md b/docs/LINUX_BUILDS.md new file mode 100644 index 0000000000..33be4ae864 --- /dev/null +++ b/docs/LINUX_BUILDS.md @@ -0,0 +1,112 @@ +# Linux Builds — AppImage Downloads + +QLC+ (the QML/v5 variant, `qlcplus-qml`) is built automatically as a +self-contained **AppImage** for multiple Linux architectures on every tagged +release. + +## Supported Architectures + +| Architecture | Description | +|---|---| +| `x86_64` | Intel / AMD 64-bit (most desktop and server Linux) | +| `aarch64` | ARMv8 64-bit (Raspberry Pi 4/5 in 64-bit mode, Ampere, Apple Silicon under Rosetta/Asahi, etc.) | +| `armv7` | ARMv7 hard-float *(manual dispatch only — see notes below)* | + +## Downloading an AppImage + +1. Go to the [**Releases** page](../../releases) of this repository. +2. Expand the **Assets** section of the latest release. +3. Download the AppImage that matches your architecture, for example: + - `QLC+-5.2.2-GIT-abcdef1-x86_64.AppImage` + - `QLC+-5.2.2-GIT-abcdef1-aarch64.AppImage` +4. Optionally download the matching `.sha256` file to verify integrity. + +## Verifying the Download + +```bash +sha256sum --check QLC+-5.2.2-GIT-abcdef1-x86_64.AppImage.sha256 +``` + +## Running the AppImage + +```bash +# Make it executable (only needed once) +chmod +x QLC+-5.2.2-GIT-abcdef1-x86_64.AppImage + +# Run it +./QLC+-5.2.2-GIT-abcdef1-x86_64.AppImage +``` + +### Without FUSE (fallback) + +AppImages normally require FUSE to mount themselves. If FUSE is unavailable +(e.g. inside a container or on a stripped-down server image), you can still run +the AppImage by extracting it first: + +```bash +# Extract the AppImage into a folder called "squashfs-root" +./QLC+-5.2.2-GIT-abcdef1-x86_64.AppImage --appimage-extract + +# Run the extracted binary +./squashfs-root/AppRun +``` + +Or set the environment variable before running: + +```bash +APPIMAGE_EXTRACT_AND_RUN=1 ./QLC+-5.2.2-GIT-abcdef1-x86_64.AppImage +``` + +## Runtime Requirements + +| Requirement | Minimum version | +|---|---| +| Linux kernel | 3.10+ (5.x+ recommended) | +| glibc | 2.35 (Ubuntu 22.04 baseline) | +| FUSE 2 | Required for normal AppImage execution (`libfuse2` on Debian/Ubuntu) — or use `--appimage-extract-and-run` | +| Display | X11 or Wayland (via XWayland) | + +No Qt installation is required — all Qt libraries and QML modules are bundled +inside the AppImage. + +## Installing FUSE 2 (if needed) + +```bash +# Debian / Ubuntu +sudo apt-get install libfuse2 + +# Fedora / RHEL +sudo dnf install fuse-libs + +# Arch Linux +sudo pacman -S fuse2 +``` + +## Notes on armv7 + +armv7 (32-bit ARM) builds are gated behind a manual `workflow_dispatch` trigger +because: + +- GitHub does not offer hosted armv7 runners. +- Qt 6 for Linux armv7 is only available as Qt 6.2.x (Ubuntu 22.04 apt). +- Full QEMU emulation of the build takes 45–90 minutes, exceeding typical + free-tier CI time limits. + +If you need armv7 packages, consider building on a native Raspberry Pi or +self-hosted ARM runner. armv7 artifacts are distributed as a `.tar.gz` +tarball rather than an AppImage (no `linuxdeploy` armhf AppImage tool is +publicly available). + +## Build System Details + +The CI workflow lives at +[`.github/workflows/linux-build.yml`](../.github/workflows/linux-build.yml). + +Key choices: + +| Aspect | x86_64 | aarch64 | +|---|---|---| +| Runner | `ubuntu-22.04` | `ubuntu-22.04-arm` | +| Qt installation | `jurplel/install-qt-action` (Qt 6.9.3) | System apt Qt 6 | +| Library bundling | `linuxdeploy` + `linuxdeploy-plugin-qt` | `linuxdeploy` + `linuxdeploy-plugin-qt` | +| Output | AppImage | AppImage | diff --git a/engine/src/inputpatch.h b/engine/src/inputpatch.h index a4dc4885b6..4891d6aecd 100644 --- a/engine/src/inputpatch.h +++ b/engine/src/inputpatch.h @@ -166,7 +166,9 @@ private slots: struct InputValue { - InputValue() {} + InputValue() + : value(0) + {} InputValue(uchar v, QString const& k) : value(v) , key(k) diff --git a/platforms/linux/CMakeLists.txt b/platforms/linux/CMakeLists.txt index 8d4a1fd6c1..6a257aff19 100644 --- a/platforms/linux/CMakeLists.txt +++ b/platforms/linux/CMakeLists.txt @@ -155,22 +155,28 @@ if(appimage) set(PLUGINS_DESTINATION ${INSTALLROOT}/${LIBSDIR}/qt5/plugins) endif() + # OPTIONAL on all Qt plugin installs so that builds using system Qt on non-x86_64 + # architectures (where paths may differ) do not fail — linuxdeploy handles + # bundling the missing Qt plugins in those cases. install( FILES ${QT_PLUGINS_PATH}/platforms/libqlinuxfb.so ${QT_PLUGINS_PATH}/platforms/libqxcb.so ${QT_PLUGINS_PATH}/platforms/libqminimal.so + OPTIONAL DESTINATION ${PLUGINS_DESTINATION}/platforms ) install( FILES ${QT_PLUGINS_PATH}/xcbglintegrations/libqxcb-glx-integration.so + OPTIONAL DESTINATION ${PLUGINS_DESTINATION}/xcbglintegrations ) if (QT_VERSION_MAJOR GREATER 5) install( FILES ${QT_PLUGINS_PATH}/multimedia/libffmpegmediaplugin.so + OPTIONAL DESTINATION ${PLUGINS_DESTINATION}/multimedia ) get_filename_component(LIBAVFORMAT_REAL "${QT_LIBS_PATH}/libavformat.so" REALPATH) @@ -179,6 +185,7 @@ if(appimage) ${QT_LIBS_PATH}/libavformat.so ${QT_LIBS_PATH}/libavformat.so.61 ${LIBAVFORMAT_REAL} + OPTIONAL DESTINATION ${INSTALLROOT}/${LIBSDIR} ) else() @@ -186,6 +193,7 @@ if(appimage) FILES ${QT_PLUGINS_PATH}/audio/libqtaudio_alsa.so ${QT_PLUGINS_PATH}/audio/libqtmedia_pulse.so + OPTIONAL DESTINATION ${PLUGINS_DESTINATION}/audio ) @@ -193,6 +201,7 @@ if(appimage) FILES ${QT_PLUGINS_PATH}/mediaservice/libgstaudiodecoder.so ${QT_PLUGINS_PATH}/mediaservice/libgstmediaplayer.so + OPTIONAL DESTINATION ${PLUGINS_DESTINATION}/mediaservice ) endif() @@ -200,6 +209,7 @@ if(appimage) install( FILES ${QT_PLUGINS_PATH}/imageformats/libqsvg.so + OPTIONAL DESTINATION ${PLUGINS_DESTINATION}/imageformats ) @@ -207,6 +217,7 @@ if(appimage) install( FILES ${QT_PLUGINS_PATH}/printsupport/libcupsprintersupport.so + OPTIONAL DESTINATION ${PLUGINS_DESTINATION}/printsupport ) @@ -219,11 +230,12 @@ if(appimage) install( FILES ${QT_PLUGINS_PATH}/geometryloaders/libdefaultgeometryloader.so + OPTIONAL DESTINATION ${PLUGINS_DESTINATION}/geometryloaders ) if(QT_VERSION VERSION_GREATER_EQUAL 5.15.0) - install(FILES ${QT_PLUGINS_PATH}/renderers/libopenglrenderer.so DESTINATION ${PLUGINS_DESTINATION}/renderers) + install(FILES ${QT_PLUGINS_PATH}/renderers/libopenglrenderer.so OPTIONAL DESTINATION ${PLUGINS_DESTINATION}/renderers) endif() set(qmldeps_files @@ -233,7 +245,13 @@ if(appimage) ${QT_QML_PATH}/Qt3D ${QT_QML_PATH}/QtMultimedia ) - install(DIRECTORY ${qmldeps_files} DESTINATION ${INSTALLROOT}/bin) + # Use EXISTS check instead of OPTIONAL (OPTIONAL on DIRECTORY requires CMake 3.22, + # but cmake_minimum_required is 3.16; CI runners have 3.22+ but protect older builds) + foreach(_qml_dir ${qmldeps_files}) + if(EXISTS "${_qml_dir}") + install(DIRECTORY "${_qml_dir}" DESTINATION ${INSTALLROOT}/bin) + endif() + endforeach() endif() diff --git a/qmlui/main.cpp b/qmlui/main.cpp index 6b22fc65b0..a0afc32853 100644 --- a/qmlui/main.cpp +++ b/qmlui/main.cpp @@ -58,7 +58,9 @@ int main(int argc, char *argv[]) QApplication app(argc, argv); /* Force dark mode so the macOS title bar and window chrome match the dark UI */ +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) QGuiApplication::styleHints()->setColorScheme(Qt::ColorScheme::Dark); +#endif // Since Qt6, the default rendering backend is Rhi. // QLC+ doesn't support it yet so OpenGL have to be forced.