aboutsummaryrefslogtreecommitdiff
path: root/docker
diff options
context:
space:
mode:
Diffstat (limited to 'docker')
-rw-r--r--docker/Dockerfile1
-rwxr-xr-xdocker/build-inner.sh279
2 files changed, 230 insertions, 50 deletions
diff --git a/docker/Dockerfile b/docker/Dockerfile
index aee1635..f4098af 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -26,6 +26,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
qt6-webengine-dev \
libqt6websockets6-dev \
qt6-wayland \
+ kde-style-breeze \
# Python
python3 python3-pyqt6 \
# Misc
diff --git a/docker/build-inner.sh b/docker/build-inner.sh
index 0483aa2..dda9b14 100755
--- a/docker/build-inner.sh
+++ b/docker/build-inner.sh
@@ -31,17 +31,10 @@ mkdir -p "${OUT_DIR}/plugins" "${OUT_DIR}/dlls" "${OUT_DIR}/lib"
# ── Main binary + helpers ──
cp -f "${RUNDIR}/ModOrganizer" "${OUT_DIR}/ModOrganizer-core"
if [ -f "${RUNDIR}/umu-run" ]; then
- # Patch umu-run to fix two issues with Steamworks DRM:
- #
- # 1. Preserve STEAM_COMPAT_CLIENT_INSTALL_PATH from the parent environment.
- # Upstream umu-run initialises this to "" and never picks up the caller's
- # value, which prevents the Steam client libraries from being found.
- #
- # 2. Remove UMU_ID from the environment before Proton runs. GE-Proton
- # treats games with UMU_ID as non-Steam titles and skips the steam.exe
- # DRM bridge, causing "Application load error 5:0000065434". For actual
- # Steam games (SteamAppId != "0") we delete UMU_ID so Proton uses the
- # correct steam.exe launch path.
+ # Patch umu-run to preserve STEAM_COMPAT_CLIENT_INSTALL_PATH from the
+ # parent environment. Upstream umu-run initialises this to "" and never
+ # picks up the caller's value, which prevents the Steam client libraries
+ # from being found.
UMU_PATCH_DIR="$(mktemp -d)"
(cd "${UMU_PATCH_DIR}" && "${BUILD_PY}" << PATCHEOF
import zipfile, pathlib
@@ -59,16 +52,6 @@ new1 = (old1
if old1 in src and 'STEAM_COMPAT_CLIENT_INSTALL_PATH"] = os.environ' not in src:
src = src.replace(old1, new1)
-# Patch 2: delete UMU_ID before Proton launch for Steam games
-old2 = ' os.environ[key] = val'
-new2 = (old2
- + '\n\n # GE-Proton treats games with UMU_ID as non-Steam titles and skips'
- + '\n # the steam.exe DRM bridge. Remove it for Steam games.'
- + '\n if "UMU_ID" in os.environ and env.get("SteamAppId", "0") != "0":'
- + '\n del os.environ["UMU_ID"]')
-if old2 in src and 'del os.environ["UMU_ID"]' not in src:
- src = src.replace(old2, new2, 1)
-
run_py.write_text(src)
PATCHEOF
)
@@ -91,6 +74,7 @@ done
find build/libs -type f \( \
-name "libgame_*.so" -o \
-name "libinstaller_*.so" -o \
+ -name "libfomod_plus_*.so" -o \
-name "libpreview_*.so" -o \
-name "libdiagnose_*.so" -o \
-name "libcheck_*.so" -o \
@@ -117,6 +101,12 @@ for f in /src/src/plugins/*.py; do
[ -f "${f}" ] && cp -f "${f}" "${OUT_DIR}/plugins/"
done
+# ── Stylesheets (themes) ──
+if [ -d "build/src/src/stylesheets" ]; then
+ cp -a "build/src/src/stylesheets" "${OUT_DIR}/"
+ echo "Bundled stylesheets"
+fi
+
# ── 7z runtime ──
SO7="build/src/src/dlls/7z.so"
if [ -f "${SO7}" ]; then
@@ -140,6 +130,80 @@ for boost_lib in /lib/x86_64-linux-gnu/libboost_program_options.so* \
[ -f "${boost_lib}" ] && cp -Lf "${boost_lib}" "${OUT_DIR}/lib/"
done
+# ── Bundle ALL shared library dependencies ──
+# Collect every .so the binary and plugins link against, then bundle
+# everything except core glibc/system libs (which must come from the host).
+echo "Bundling shared library dependencies..."
+
+# Libraries that MUST come from the host (glibc, GPU drivers, etc.)
+SKIP_PATTERN="linux-vdso|ld-linux|libc\.so|libm\.so|libdl\.so|librt\.so|libpthread|libresolv|libnss|libgcc_s|libstdc\+\+"
+# GPU/graphics drivers must be host-provided
+SKIP_PATTERN="${SKIP_PATTERN}|libGL\.so|libEGL|libGLX|libGLdispatch|libdrm|libvulkan|libX11|libxcb|libwayland-client|libwayland-server|libwayland-cursor|libwayland-egl|libxkbcommon"
+# libpython is shipped by the portable Python runtime in python/lib/ — do NOT
+# copy the container's version into lib/ or it will shadow the portable one.
+SKIP_PATTERN="${SKIP_PATTERN}|libpython"
+
+collect_deps() {
+ ldd "$1" 2>/dev/null | grep "=>" | awk '{print $3}' | grep "^/" | sort -u
+}
+
+ALL_DEPS=$(mktemp)
+# Main binary
+collect_deps "${OUT_DIR}/ModOrganizer-core" >> "${ALL_DEPS}"
+# All plugin .so files
+find "${OUT_DIR}/plugins" -name "*.so" -exec sh -c 'ldd "$1" 2>/dev/null | grep "=>" | awk "{print \$3}" | grep "^/"' _ {} \; >> "${ALL_DEPS}"
+# Our own libs
+find "${OUT_DIR}/lib" -name "*.so*" -exec sh -c 'ldd "$1" 2>/dev/null | grep "=>" | awk "{print \$3}" | grep "^/"' _ {} \; >> "${ALL_DEPS}"
+# lootcli
+[ -f "${OUT_DIR}/lootcli" ] && collect_deps "${OUT_DIR}/lootcli" >> "${ALL_DEPS}"
+
+sort -u "${ALL_DEPS}" | while read -r dep; do
+ dep_name="$(basename "${dep}")"
+ # Skip system libs
+ if echo "${dep_name}" | grep -qE "${SKIP_PATTERN}"; then
+ continue
+ fi
+ # Skip if already bundled
+ if [ -f "${OUT_DIR}/lib/${dep_name}" ]; then
+ continue
+ fi
+ cp -Lf "${dep}" "${OUT_DIR}/lib/" 2>/dev/null || true
+done
+rm -f "${ALL_DEPS}"
+# Remove any libpython that leaked into lib/ — must come from python/lib/ only.
+rm -f "${OUT_DIR}/lib"/libpython*.so* 2>/dev/null || true
+echo "Dependencies bundled."
+
+# ── Qt6 platform plugins ──
+QT6_PLUGIN_DIR="/usr/lib/x86_64-linux-gnu/qt6/plugins"
+if [ ! -d "${QT6_PLUGIN_DIR}" ]; then
+ QT6_PLUGIN_DIR="$(qtpaths6 --plugin-dir 2>/dev/null || echo "")"
+fi
+if [ -d "${QT6_PLUGIN_DIR}" ]; then
+ mkdir -p "${OUT_DIR}/qt6plugins"
+ for plugin_type in platforms tls networkinformation styles \
+ wayland-shell-integration \
+ wayland-decoration-client wayland-graphics-integration-client \
+ platformthemes imageformats iconengines xcbglintegrations \
+ egldeviceintegrations; do
+ if [ -d "${QT6_PLUGIN_DIR}/${plugin_type}" ]; then
+ cp -a "${QT6_PLUGIN_DIR}/${plugin_type}" "${OUT_DIR}/qt6plugins/"
+ fi
+ done
+ # Bundle deps of Qt plugins too
+ find "${OUT_DIR}/qt6plugins" -name "*.so" -exec sh -c '
+ ldd "$1" 2>/dev/null | grep "=>" | awk "{print \$3}" | grep "^/" | while read dep; do
+ dep_name="$(basename "${dep}")"
+ echo "${dep_name}" | grep -qE "'"${SKIP_PATTERN}"'" && continue
+ [ -f "'"${OUT_DIR}"'/lib/${dep_name}" ] && continue
+ cp -Lf "${dep}" "'"${OUT_DIR}"'/lib/" 2>/dev/null || true
+ done
+ ' _ {} \;
+ echo "Bundled Qt6 plugins from ${QT6_PLUGIN_DIR}"
+else
+ echo "WARNING: Could not find Qt6 plugin directory"
+fi
+
# libloot (custom-built, never on user systems).
if [ -f /usr/local/lib/libloot.so.0 ]; then
cp -Lf /usr/local/lib/libloot.so.0 "${OUT_DIR}/lib/"
@@ -242,41 +306,156 @@ export MO2_PYTHON_DIR="${HERE}/python"
MO2_PYTHONHOME="${HERE}/python"
unset PYTHONPATH PYTHONNOUSERSITE PYTHONHOME
-# Find system Qt6 plugins (compiled-in path won't match across distros).
-if [ -z "${QT_PLUGIN_PATH:-}" ]; then
- for qt_dir in /usr/lib/qt6/plugins \
- /usr/lib/x86_64-linux-gnu/qt6/plugins \
- /usr/lib64/qt6/plugins; do
- if [ -d "${qt_dir}/platforms" ]; then
- export QT_PLUGIN_PATH="${qt_dir}"
- break
- fi
- done
- if [ -z "${QT_PLUGIN_PATH:-}" ] && command -v qtpaths6 >/dev/null 2>&1; then
- export QT_PLUGIN_PATH="$(qtpaths6 --plugin-dir)"
- fi
-fi
-
-# Quick dependency check.
-missing="$(ldd "${HERE}/ModOrganizer-core" 2>/dev/null | grep "not found" || true)"
-if [ -n "${missing}" ]; then
- echo "ERROR: Missing system libraries:"
- echo "${missing}"
- echo ""
- echo "On Arch/CachyOS: sudo pacman -S qt6-base qt6-websockets qt6-wayland"
- echo "On Fedora: sudo dnf install qt6-qtbase qt6-qtwebsockets qt6-qtwayland"
- echo "On Ubuntu/Debian: sudo apt install qt6-base-dev libqt6websockets6-dev qt6-wayland"
- exit 1
-fi
+# Use bundled Qt6 plugins.
+export QT_PLUGIN_PATH="${HERE}/qt6plugins"
cd "${HERE}"
exec env PYTHONHOME="${MO2_PYTHONHOME}" "${HERE}/ModOrganizer-core" "$@"
LAUNCH
chmod +x "${OUT_DIR}/fluorine-manager"
-# ── Summary ──
+# ── Desktop integration files (for AppImage) ──
+cp -f /src/data/com.fluorine.manager.desktop "${OUT_DIR}/"
+cp -f /src/data/com.fluorine.manager.png "${OUT_DIR}/"
+cp -f /src/data/com.fluorine.manager.metainfo.xml "${OUT_DIR}/"
+
+# ── Build AppImage ──
+echo ""
+echo "=== Building AppImage ==="
+
+APPDIR="/src/build/AppDir"
+rm -rf "${APPDIR}"
+mkdir -p "${APPDIR}/usr/bin" "${APPDIR}/usr/lib" "${APPDIR}/usr/share/applications" \
+ "${APPDIR}/usr/plugins" \
+ "${APPDIR}/usr/share/icons/hicolor/256x256/apps" \
+ "${APPDIR}/usr/share/metainfo"
+
+# Copy staging into AppDir layout
+cp -a "${OUT_DIR}"/. "${APPDIR}/usr/bin/"
+# Move libs to standard location
+mv "${APPDIR}/usr/bin/lib"/* "${APPDIR}/usr/lib/" 2>/dev/null || true
+rmdir "${APPDIR}/usr/bin/lib" 2>/dev/null || true
+# Flatpak runtime exposed Qt plugins from a stable system location. Mirror that
+# layout in AppImage by placing bundled Qt plugins under /usr/plugins.
+if [ -d "${APPDIR}/usr/bin/qt6plugins" ]; then
+ cp -a "${APPDIR}/usr/bin/qt6plugins"/. "${APPDIR}/usr/plugins/"
+fi
+
+# Desktop integration
+cp -f "${OUT_DIR}/com.fluorine.manager.desktop" "${APPDIR}/usr/share/applications/"
+cp -f "${OUT_DIR}/com.fluorine.manager.png" "${APPDIR}/usr/share/icons/hicolor/256x256/apps/"
+cp -f "${OUT_DIR}/com.fluorine.manager.metainfo.xml" "${APPDIR}/usr/share/metainfo/"
+
+# Bundle icon themes so QIcon::fromTheme() calls resolve inside the AppImage.
+# Flatpak runtime provided this globally; AppImage must carry it explicitly.
+mkdir -p "${APPDIR}/usr/share/icons"
+for theme_dir in /usr/share/icons/*; do
+ [ -d "${theme_dir}" ] || continue
+ cp -a "${theme_dir}" "${APPDIR}/usr/share/icons/"
+done
+if [ -f "/usr/share/icons/default/index.theme" ]; then
+ mkdir -p "${APPDIR}/usr/share/icons/default"
+ cp -f "/usr/share/icons/default/index.theme" "${APPDIR}/usr/share/icons/default/"
+fi
+
+# Update RPATH for new lib location
+patchelf --set-rpath '$ORIGIN/../lib' "${APPDIR}/usr/bin/ModOrganizer-core"
+[ -f "${APPDIR}/usr/bin/lootcli" ] && patchelf --set-rpath '$ORIGIN/../lib' "${APPDIR}/usr/bin/lootcli"
+find "${APPDIR}/usr/bin/plugins" -name "*.so" -exec patchelf --set-rpath '$ORIGIN/../../lib' {} \; 2>/dev/null || true
+
+# Create AppRun wrapper
+cat > "${APPDIR}/AppRun" <<'APPRUN'
+#!/usr/bin/env bash
+set -euo pipefail
+SELF="$(readlink -f "$0")"
+HERE="$(cd "$(dirname "$SELF")" && pwd)"
+BIN="${HERE}/usr/bin"
+APPIMAGE_DIR="$(dirname "$(readlink -f "${APPIMAGE:-$0}")")"
+
+sync_dir() {
+ local name="$1"
+ local src="${BIN}/${name}"
+ local dst="${APPIMAGE_DIR}/${name}"
+ [ -d "${src}" ] || return 0
+ mkdir -p "${dst}"
+ (cd "${src}" && tar --exclude-vcs -cf - .) | (cd "${dst}" && tar -xf -)
+}
+
+# Keep runtime payload outside the transient AppImage mount.
+sync_dir "plugins"
+sync_dir "dlls"
+sync_dir "python"
+
+export PATH="${BIN}:${PATH}"
+if [ -d "${APPIMAGE_DIR}/python/lib" ]; then
+ export LD_LIBRARY_PATH="${HERE}/usr/lib:${APPIMAGE_DIR}/python/lib:${LD_LIBRARY_PATH:-}"
+else
+ export LD_LIBRARY_PATH="${HERE}/usr/lib:${BIN}/python/lib:${LD_LIBRARY_PATH:-}"
+fi
+
+export MO2_BASE_DIR="${APPIMAGE_DIR}"
+export MO2_PLUGINS_DIR="${APPIMAGE_DIR}/plugins"
+export MO2_DLLS_DIR="${APPIMAGE_DIR}/dlls"
+if [ -d "${APPIMAGE_DIR}/python/lib" ]; then
+ export MO2_PYTHON_DIR="${APPIMAGE_DIR}/python"
+else
+ export MO2_PYTHON_DIR="${BIN}/python"
+fi
+
+unset PYTHONPATH PYTHONNOUSERSITE PYTHONHOME
+
+# Use bundled Qt6 plugins.
+export QT_PLUGIN_PATH="${HERE}/usr/plugins"
+export QT_QPA_PLATFORM_PLUGIN_PATH="${HERE}/usr/plugins/platforms"
+export XDG_DATA_DIRS="${HERE}/usr/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
+export QT_ICON_THEME_NAME="${QT_ICON_THEME_NAME:-breeze}"
+export QT_STYLE_OVERRIDE="${QT_STYLE_OVERRIDE:-Breeze}"
+export XDG_CURRENT_DESKTOP="${XDG_CURRENT_DESKTOP:-KDE}"
+
+cd "${APPIMAGE_DIR}"
+exec "${BIN}/ModOrganizer-core" "$@"
+APPRUN
+chmod +x "${APPDIR}/AppRun"
+
+# Symlinks required by AppImage spec
+ln -sf usr/share/applications/com.fluorine.manager.desktop "${APPDIR}/com.fluorine.manager.desktop"
+ln -sf usr/share/icons/hicolor/256x256/apps/com.fluorine.manager.png "${APPDIR}/com.fluorine.manager.png"
+ln -sf usr/share/icons/hicolor/256x256/apps/com.fluorine.manager.png "${APPDIR}/.DirIcon"
+
+# Extract linuxdeploy (can't use FUSE inside Docker)
+DEPLOY_DIR="/tmp/linuxdeploy-extract"
+if [ ! -d "${DEPLOY_DIR}" ]; then
+ cd /opt/linuxdeploy
+ ./linuxdeploy-x86_64.AppImage --appimage-extract >/dev/null
+ mv squashfs-root "${DEPLOY_DIR}"
+ chmod +x "${DEPLOY_DIR}/AppRun"
+fi
+
+# Use linuxdeploy to generate the AppImage.
+# We skip the Qt plugin since we already handle Qt plugin paths at runtime
+# and our deps are already staged.
+export ARCH=x86_64
+"${DEPLOY_DIR}/AppRun" \
+ --appdir "${APPDIR}" \
+ --output appimage \
+ --desktop-file "${APPDIR}/usr/share/applications/com.fluorine.manager.desktop" \
+ --icon-file "${APPDIR}/usr/share/icons/hicolor/256x256/apps/com.fluorine.manager.png" \
+ 2>&1 || {
+ echo "WARNING: linuxdeploy AppImage generation failed, falling back to staging dir output"
+}
+
+# Move AppImage to output
+APPIMAGE_FILE=$(ls -1 Fluorine*.AppImage 2>/dev/null || ls -1 *.AppImage 2>/dev/null || true)
+if [ -n "${APPIMAGE_FILE}" ]; then
+ mv "${APPIMAGE_FILE}" "/src/build/"
+ echo ""
+ echo "=== AppImage built ==="
+ ls -lh "/src/build/"*.AppImage
+else
+ echo ""
+ echo "=== Staging complete (AppImage generation skipped) ==="
+fi
+
echo ""
echo "=== Build Summary ==="
du -sh "${OUT_DIR}"/*/ "${OUT_DIR}"/ModOrganizer-core 2>/dev/null | sort -rh
-echo ""
-echo "Staging complete."