Skip to content

Commit 93fd789

Browse files
committed
feat: add AppImage build via GitHub Actions + vcpkg buildenv
Add AppImage build support using the project's vcpkg buildenv on GitHub Actions (Ubuntu 24.04). Key changes: - New CI workflow: .github/workflows/appimage.yml (workflow_call for publishing) - AppImage matrix entry in build.yml (reuses configure/build/cache infrastructure) - Build script: packaging/appimage/build-appimage.sh (bundles system libs via ldd loop, copies Qt plugins/QML with system fallback) - AppRun entry point: packaging/appimage/AppRun.sh - CMake: MIXXX_APPIMAGE option for codec bundling, LINK_GROUP:RESCAN for static FFmpeg cross-dependencies - FindGLIB.cmake: prefer system shared GLib over vcpkg static GLib - FindSndFile.cmake: add Opus dependency - linux_appimage_buildenv.sh: buildenv setup + build dependencies - release.yml: calls appimage.yml for publishing (like Flatpak) - Buildenv: tools/linux_appimage_buildenv.sh
1 parent ab848eb commit 93fd789

11 files changed

Lines changed: 646 additions & 5 deletions

File tree

.github/workflows/appimage.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow
2+
3+
name: AppImage
4+
5+
on:
6+
workflow_call:
7+
inputs:
8+
run_id:
9+
type: string
10+
required: true
11+
ref:
12+
type: string
13+
required: true
14+
secrets:
15+
DOWNLOADS_HOSTGATOR_DOT_MIXXX_DOT_ORG_KEY:
16+
required: false
17+
DOWNLOADS_HOSTGATOR_DOT_MIXXX_DOT_ORG_KEY_PASSWORD:
18+
required: false
19+
20+
permissions:
21+
contents: read
22+
23+
jobs:
24+
publish-appimage:
25+
name: Publish AppImage
26+
runs-on: ubuntu-24.04
27+
steps:
28+
- name: "Download AppImage artifact"
29+
uses: actions/download-artifact@v4
30+
with:
31+
name: Mixxx-AppImage-x86_64
32+
path: build/
33+
run-id: ${{ inputs.run_id }}
34+
github-token: ${{ secrets.GITHUB_TOKEN }}
35+
36+
- name: "Publish AppImage"
37+
env:
38+
SSH_PRIVATE_KEY: ${{ secrets.DOWNLOADS_HOSTGATOR_DOT_MIXXX_DOT_ORG_KEY }}
39+
if: env.SSH_PRIVATE_KEY != null
40+
run: |
41+
# TODO: Publish to downloads.mixxx.org
42+
#
43+
# Implementation steps (when secrets are available):
44+
#
45+
# 1. Set up SSH agent with DOWNLOADS_HOSTGATOR_DOT_MIXXX_DOT_ORG_KEY
46+
# (see flatpak.yml for the SSH agent setup pattern)
47+
#
48+
# 2. Determine deploy path (same logic as build.yml's prepare_deploy):
49+
# - Tags: releases/{git_describe}/mixxx-{git_describe}-linux-appimage{ext}
50+
# - Main: snapshots/{git_branch}/mixxx-{git_describe}-linux-appimage{ext}
51+
#
52+
# 3. Upload to downloads.mixxx.org using:
53+
# - rsync over SSH
54+
# - or tools/deploy.py prepare-deployment + rsync
55+
#
56+
# 4. After upload, update the download manifest (tools/deploy.py or similar).
57+
echo "Publishing AppImage for ref: ${{ inputs.ref }}"
58+
ls -la build/

.github/workflows/build.yml

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,29 @@ jobs:
194194
compiler_cache_path: /home/runner/.cache/ccache
195195
crosscompile: true
196196
arch: arm64
197+
- name: AppImage Linux x86_64
198+
os: ubuntu-24.04
199+
cmake_args: >-
200+
-DQT6=ON
201+
-DQML=ON
202+
-DMIXXX_APPIMAGE=ON
203+
-DINSTALL_USER_UDEV_RULES=OFF
204+
-DPKG_CONFIG_EXECUTABLE=/usr/bin/pkg-config
205+
-DCMAKE_MODULE_PATH="${GITHUB_WORKSPACE}/packaging/appimage"
206+
-DCMAKE_CXX_FLAGS_RELWITHDEBINFO="-O2 -DNDEBUG"
207+
-DCMAKE_INSTALL_PREFIX=/
208+
ctest_args: []
209+
cpack_generator: AppImage
210+
crosscompile: true
211+
compiler_cache: ccache
212+
compiler_cache_path: /home/runner/.cache/ccache
213+
compiler_cache_id: appimage-qt6
214+
buildenv_basepath: /home/runner/buildenv
215+
buildenv_script: tools/linux_appimage_buildenv.sh
216+
artifacts_name: AppImage Linux x86_64
217+
artifacts_path: build/*.AppImage
218+
artifacts_slug: linux-appimage
219+
arch: x86_64
197220

198221
env:
199222
# macOS codesigning
@@ -481,9 +504,13 @@ jobs:
481504
dotnet.exe tool install --global wix --version 6.0.2
482505
wix extension add --global WixToolset.UI.wixext/6.0.2
483506
507+
- name: "[AppImage] Install appimagetool"
508+
if: matrix.cpack_generator == 'AppImage'
509+
run: packaging/appimage/install-appimagetool.sh ${{ matrix.arch }}
510+
484511
- name: "Package"
485512
id: package
486-
if: matrix.cpack_generator != null
513+
if: matrix.cpack_generator != null && matrix.cpack_generator != 'AppImage'
487514
# Use retry loop to work around a race condition on macOS causing
488515
# 'Resource busy' errors with 'hdiutil'. See
489516
# https://github.com/actions/runner-images/issues/7522
@@ -496,6 +523,21 @@ jobs:
496523
cd build
497524
cpack -G ${{ matrix.cpack_generator }} -V
498525
526+
- name: "[AppImage] Package"
527+
if: matrix.cpack_generator == 'AppImage'
528+
run: |
529+
GIT_DESC=$(git describe --always --first-parent --dirty=-modified)
530+
packaging/appimage/build-appimage.sh build ${{ matrix.arch }}
531+
mv build/Mixxx-${{ matrix.arch }}.AppImage "build/Mixxx-${GIT_DESC}-${{ matrix.arch }}.AppImage"
532+
533+
- name: "[AppImage] Upload AppImage"
534+
if: matrix.cpack_generator == 'AppImage'
535+
uses: actions/upload-artifact@v4
536+
with:
537+
name: Mixxx-AppImage-${{ matrix.arch }}
538+
path: ${{ matrix.artifacts_path }}
539+
if-no-files-found: error
540+
499541
- name: Upload failed packaging logs
500542
if: always() && steps.package.outcome == 'failure' && runner.os == 'windows'
501543
uses: actions/upload-artifact@v7

.github/workflows/release.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ jobs:
6565
DOWNLOADS_HOSTGATOR_DOT_MIXXX_DOT_ORG_KEY: ${{ secrets.DOWNLOADS_HOSTGATOR_DOT_MIXXX_DOT_ORG_KEY }}
6666
RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY: ${{ secrets.RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY }}
6767

68+
publish-appimage:
69+
needs:
70+
- build
71+
uses: ./.github/workflows/appimage.yml
72+
if: ${{ (github.ref_type == 'tag' && !contains('-', github.ref_name)) || github.ref_name == 'main' }}
73+
with:
74+
ref: ${{ github.ref }}
75+
run_id: ${{ github.run_id }}
76+
secrets:
77+
DOWNLOADS_HOSTGATOR_DOT_MIXXX_DOT_ORG_KEY: ${{ secrets.DOWNLOADS_HOSTGATOR_DOT_MIXXX_DOT_ORG_KEY }}
78+
DOWNLOADS_HOSTGATOR_DOT_MIXXX_DOT_ORG_KEY_PASSWORD: ${{ secrets.DOWNLOADS_HOSTGATOR_DOT_MIXXX_DOT_ORG_KEY_PASSWORD }}
79+
6880
sync:
6981
if: ${{ github.ref_type != 'tag' && github.ref != 'refs/heads/main' && github.repository == 'mixxxdj/mixxx' }}
7082
uses: ./.github/workflows/sync_branches.yml

CMakeLists.txt

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL Android)
9999
endif()
100100

101101
if(
102-
((APPLE AND NOT IOS) OR WIN32 OR ANDROID)
102+
(DEFINED ENV{BUILDENV_URL} OR (APPLE AND NOT IOS) OR WIN32 OR ANDROID)
103103
AND NOT IS_DIRECTORY "${MIXXX_VCPKG_ROOT}"
104104
)
105105
if(NOT DEFINED BUILDENV_BASEPATH)
@@ -403,6 +403,7 @@ endif()
403403
include(CMakeDependentOption)
404404

405405
option(QT6 "Build with Qt6" ON)
406+
option(MIXXX_APPIMAGE "Build for AppImage distribution (bundle dlopen dependencies)" OFF)
406407
cmake_dependent_option(
407408
QML
408409
"Build with QML"
@@ -540,6 +541,19 @@ include(GNUInstallDirs)
540541
include(DefaultOption)
541542
include(IsStaticLibrary)
542543

544+
if(DEFINED MIXXX_VCPKG_ROOT AND UNIX AND NOT APPLE)
545+
# Prepend system paths to find the system's shared GLib instead of the
546+
# vcpkg static GLib (which causes constructor-ordering issues when linked
547+
# alongside static FFmpeg via LINK_GROUP:RESCAN).
548+
# CMAKE_LIBRARY_ARCHITECTURE is set automatically on Debian-based systems.
549+
if(CMAKE_LIBRARY_ARCHITECTURE)
550+
list(PREPEND CMAKE_PREFIX_PATH
551+
"/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}"
552+
"/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}/cmake"
553+
)
554+
endif()
555+
endif()
556+
543557
# Verify VCPKG settings
544558
if(DEFINED _VCPKG_INSTALLED_DIR)
545559
if(NOT EXISTS "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}")
@@ -2632,6 +2646,19 @@ if(UNIX AND NOT APPLE)
26322646
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications"
26332647
)
26342648

2649+
# AppImage desktop file (simplified Exec=mixxx)
2650+
if(MIXXX_APPIMAGE)
2651+
set(MIXXX_APPIMAGE_DESKTOP_FILE "${CMAKE_CURRENT_BINARY_DIR}/mixxx-appimage.desktop")
2652+
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/res/linux/org.mixxx.Mixxx.desktop" MIXXX_APPIMAGE_DESKTOP_CONTENT)
2653+
string(REGEX REPLACE "\nExec=[^\n]*" "\nExec=mixxx" MIXXX_APPIMAGE_DESKTOP_CONTENT "${MIXXX_APPIMAGE_DESKTOP_CONTENT}")
2654+
file(WRITE "${MIXXX_APPIMAGE_DESKTOP_FILE}" "${MIXXX_APPIMAGE_DESKTOP_CONTENT}")
2655+
install(
2656+
FILES "${MIXXX_APPIMAGE_DESKTOP_FILE}"
2657+
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications"
2658+
RENAME "mixxx-appimage.desktop"
2659+
)
2660+
endif()
2661+
26352662
# Icon files for menu entry
26362663
install(
26372664
DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/images/icons/"
@@ -2843,7 +2870,19 @@ if(FFMPEG)
28432870
__STDC_FORMAT_MACROS
28442871
)
28452872

2846-
target_link_libraries(mixxx-lib PRIVATE FFmpeg::FFmpeg)
2873+
# Static FFmpeg libraries have cross-references (e.g. libavformat references
2874+
# av_adts_header_parse from libavcodec). LINK_GROUP:RESCAN wraps them in
2875+
# --start-group/--end-group so the GNU linker resolves these. Harmless for
2876+
# shared builds; Apple's ld64 does not support the group flags.
2877+
if(UNIX AND NOT APPLE)
2878+
target_link_libraries(
2879+
mixxx-lib
2880+
PRIVATE
2881+
"$<LINK_GROUP:RESCAN,FFmpeg::avformat,FFmpeg::avcodec,FFmpeg::avutil,FFmpeg::swresample>"
2882+
)
2883+
else()
2884+
target_link_libraries(mixxx-lib PRIVATE FFmpeg::FFmpeg)
2885+
endif()
28472886
target_include_directories(mixxx-lib PUBLIC "${FFmpeg_INCLUDE_DIRS}")
28482887
endif()
28492888

@@ -5009,6 +5048,37 @@ else()
50095048
message(STATUS "Could NOT find fdk-aac (FdkAac_FOUND is FALSE)")
50105049
endif()
50115050

5051+
# Bundle codec libraries that Mixxx loads at runtime via QLibrary (dlopen).
5052+
# The vcpkg buildenv provides static (.a) versions, but QLibrary needs the
5053+
# shared (.so.*) files with versioned sonames (e.g. libfaad.so.2).
5054+
if(MIXXX_APPIMAGE)
5055+
set(_suffixes "${CMAKE_FIND_LIBRARY_SUFFIXES}")
5056+
5057+
# libfaad: loader always expects libfaad.so.2
5058+
set(CMAKE_FIND_LIBRARY_SUFFIXES ".so.2")
5059+
find_library(FAAD_LIBRARY NAMES faad)
5060+
if(FAAD_LIBRARY)
5061+
install(FILES "${FAAD_LIBRARY}" DESTINATION "${CMAKE_INSTALL_LIBDIR}")
5062+
endif()
5063+
5064+
# libfdk-aac: loader probes .so.1 first, then .so.2 — try both
5065+
unset(FDKAAC_LIBRARY CACHE)
5066+
foreach(_soname IN ITEMS 1 2)
5067+
set(CMAKE_FIND_LIBRARY_SUFFIXES ".so.${_soname}")
5068+
find_library(FDKAAC_LIBRARY NAMES fdk-aac)
5069+
if(FDKAAC_LIBRARY)
5070+
break()
5071+
endif()
5072+
endforeach()
5073+
if(FDKAAC_LIBRARY)
5074+
install(FILES "${FDKAAC_LIBRARY}" DESTINATION "${CMAKE_INSTALL_LIBDIR}")
5075+
else()
5076+
message(WARNING "libfdk-aac not found — fdk-aac encoding will be unavailable in the AppImage")
5077+
endif()
5078+
5079+
set(CMAKE_FIND_LIBRARY_SUFFIXES "${_suffixes}")
5080+
endif()
5081+
50125082
# Google PerfTools
50135083
option(GPERFTOOLS "Google PerfTools libtcmalloc linkage" OFF)
50145084
option(GPERFTOOLSPROFILER "Google PerfTools libprofiler linkage" OFF)

cmake/modules/FindGLIB.cmake

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,24 @@ if(PkgConfig_FOUND)
4646
pkg_check_modules(PC_GLIB QUIET glib-2.0)
4747
endif()
4848

49+
# Prefer the system's shared GLib over the vcpkg buildenv's static GLib.
50+
# The vcpkg static GLib, when linked alongside static FFmpeg via
51+
# LINK_GROUP:RESCAN, pulls in extra symbols whose constructor ordering
52+
# crashes on startup. Search the system library dir first.
53+
# CMAKE_LIBRARY_ARCHITECTURE is set automatically on Debian-based systems.
54+
if(CMAKE_LIBRARY_ARCHITECTURE)
55+
set(_glib_system_libdir "/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}")
56+
else()
57+
set(_glib_system_libdir "")
58+
endif()
4959
find_library(
5060
GLIB_LIBRARIES
5161
NAMES glib-2.0
52-
HINTS ${PC_GLIB_LIBDIR} ${PC_GLIB_LIBRARY_DIRS}
62+
HINTS
63+
${_glib_system_libdir}
64+
${PC_GLIB_LIBDIR}
65+
${PC_GLIB_LIBRARY_DIRS}
66+
NO_CMAKE_FIND_ROOT_PATH
5367
)
5468

5569
# Files in glib's main include path may include glibconfig.h, which,
@@ -111,13 +125,14 @@ endif()
111125
# glib ones.
112126
foreach(component ${GLIB_FIND_COMPONENTS})
113127
if(${component} STREQUAL "gio")
114-
find_library(GLIB_GIO_LIBRARIES NAMES gio-2.0 HINTS ${_GLIB_LIBRARY_DIR})
128+
find_library(GLIB_GIO_LIBRARIES NAMES gio-2.0 HINTS ${_GLIB_LIBRARY_DIR} NO_CMAKE_FIND_ROOT_PATH)
115129
set(ADDITIONAL_REQUIRED_VARS ${ADDITIONAL_REQUIRED_VARS} GLIB_GIO_LIBRARIES)
116130
elseif(${component} STREQUAL "gobject")
117131
find_library(
118132
GLIB_GOBJECT_LIBRARIES
119133
NAMES gobject-2.0
120134
HINTS ${_GLIB_LIBRARY_DIR}
135+
NO_CMAKE_FIND_ROOT_PATH
121136
)
122137
set(
123138
ADDITIONAL_REQUIRED_VARS
@@ -129,6 +144,7 @@ foreach(component ${GLIB_FIND_COMPONENTS})
129144
GLIB_GMODULE_LIBRARIES
130145
NAMES gmodule-2.0
131146
HINTS ${_GLIB_LIBRARY_DIR}
147+
NO_CMAKE_FIND_ROOT_PATH
132148
)
133149
set(
134150
ADDITIONAL_REQUIRED_VARS
@@ -140,6 +156,7 @@ foreach(component ${GLIB_FIND_COMPONENTS})
140156
GLIB_GTHREAD_LIBRARIES
141157
NAMES gthread-2.0
142158
HINTS ${_GLIB_LIBRARY_DIR}
159+
NO_CMAKE_FIND_ROOT_PATH
143160
)
144161
set(
145162
ADDITIONAL_REQUIRED_VARS

cmake/modules/FindSndFile.cmake

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,15 @@ if(SndFile_FOUND)
111111
)
112112
endif()
113113

114+
find_package(Opus)
115+
if(Opus_FOUND)
116+
set_property(
117+
TARGET SndFile::sndfile
118+
APPEND
119+
PROPERTY INTERFACE_LINK_LIBRARIES Opus::Opus
120+
)
121+
endif()
122+
114123
# The mpg123 dependency was introduced in libsndfile 1.1.0
115124
if(SndFile_VERSION VERSION_GREATER_EQUAL "1.1.0")
116125
find_package(mpg123 CONFIG)

packaging/appimage/AppRun.sh

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/bin/bash
2+
# AppImage entry point (AppRun).
3+
# Sets up the environment so the bundled libraries, Qt plugins, QML modules,
4+
# and data files are found at runtime.
5+
set -euo pipefail
6+
7+
APPDIR="$(dirname "$(readlink -f "$0")")"
8+
9+
# Library and Qt paths
10+
export LD_LIBRARY_PATH="${APPDIR}/usr/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
11+
export QT_PLUGIN_PATH="${APPDIR}/usr/plugins"
12+
export QML2_IMPORT_PATH="${APPDIR}/usr/qml"
13+
export QT_QPA_PLATFORM_PLUGIN_PATH="${APPDIR}/usr/plugins/platforms"
14+
15+
# XDG data dirs
16+
export XDG_DATA_DIRS="${APPDIR}/usr/share:${XDG_DATA_DIRS:-}"
17+
18+
# Set HOME if not set (minimal environments)
19+
if [ -z "${HOME:-}" ]; then
20+
HOME="${APPDIR}/../.mixxx-home"
21+
mkdir -p "$HOME" 2>/dev/null || true
22+
fi
23+
24+
exec "${APPDIR}/usr/bin/mixxx" "$@"

packaging/appimage/FindXKB.cmake

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# FindXKB module for the vcpkg buildenv, which does not bundle Qt6's XKB config.
2+
if(NOT TARGET XKB::XKB)
3+
find_package(PkgConfig QUIET)
4+
if(PkgConfig_FOUND)
5+
pkg_check_modules(xkbcommon IMPORTED_TARGET xkbcommon)
6+
if(xkbcommon_FOUND)
7+
add_library(XKB::XKB ALIAS PkgConfig::xkbcommon)
8+
set(XKB_FOUND TRUE)
9+
set(XKB_LIBRARY ${xkbcommon_LIBRARIES})
10+
set(XKB_INCLUDE_DIR ${xkbcommon_INCLUDE_DIRS})
11+
set(XKB_VERSION ${xkbcommon_VERSION})
12+
endif()
13+
endif()
14+
endif()
15+
if(NOT XKB_FOUND)
16+
find_path(XKB_INCLUDE_DIR xkbcommon/xkbcommon.h)
17+
find_library(XKB_LIBRARY xkbcommon)
18+
if(XKB_INCLUDE_DIR AND XKB_LIBRARY)
19+
set(XKB_FOUND TRUE)
20+
endif()
21+
endif()
22+
if(XKB_FOUND)
23+
if(NOT TARGET XKB::XKB)
24+
add_library(XKB::XKB UNKNOWN IMPORTED)
25+
set_target_properties(XKB::XKB PROPERTIES
26+
IMPORTED_LOCATION "${XKB_LIBRARY}"
27+
INTERFACE_INCLUDE_DIRECTORIES "${XKB_INCLUDE_DIR}"
28+
)
29+
endif()
30+
endif()
31+
mark_as_advanced(XKB_LIBRARY XKB_INCLUDE_DIR)

0 commit comments

Comments
 (0)