aboutsummaryrefslogtreecommitdiff
path: root/.github
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /.github
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to '.github')
-rw-r--r--.github/workflows/ci.yml193
-rw-r--r--.github/workflows/docker.yml45
2 files changed, 238 insertions, 0 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9f0e11b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,193 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ tags: ['v*']
+ pull_request:
+ workflow_dispatch:
+
+env:
+ CCACHE_DIR: /__w/.ccache
+ CCACHE_BASEDIR: /__w/Fluorine-Manager/Fluorine-Manager
+ CCACHE_COMPILERCHECK: content
+ CCACHE_NOHASHDIR: "1"
+ CCACHE_SLOPPINESS: time_macros
+ CCACHE_MAXSIZE: 2G
+ CMAKE_BUILD_TYPE: RelWithDebInfo
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ container:
+ image: ghcr.io/sulfurnitride/fluorine-builder:latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Restore ccache
+ uses: actions/cache@v4
+ with:
+ path: /__w/.ccache
+ key: ccache-v1-${{ github.ref_name }}-${{ hashFiles('CMakeLists.txt', '**/CMakeLists.txt') }}
+ restore-keys: |
+ ccache-v1-${{ github.ref_name }}-
+ ccache-v1-
+
+ - name: Rust cache
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: |
+ libs/nak_ffi
+ libs/bsa_ffi
+
+ - name: Configure
+ run: |
+ cmake -S . -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \
+ -DCMAKE_C_COMPILER_LAUNCHER=ccache \
+ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
+ -DPython3_EXECUTABLE="$(command -v python3)" \
+ -DBUILD_PLUGIN_PYTHON=ON
+
+ - name: Build
+ run: cmake --build build --parallel
+
+ - name: Build AppImage
+ run: |
+ set -euo pipefail
+
+ MODORG_BIN="$(find build -type f -name ModOrganizer | head -n1)"
+ if [ -z "${MODORG_BIN}" ]; then
+ echo "::error::ModOrganizer binary not found under build/"
+ exit 1
+ fi
+ RUNDIR="$(dirname "${MODORG_BIN}")"
+
+ # ── Populate AppDir ──
+ APPDIR="${GITHUB_WORKSPACE}/AppDir"
+ rm -rf "${APPDIR}"
+ mkdir -p "${APPDIR}/usr/bin" "${APPDIR}/usr/plugins" "${APPDIR}/usr/dlls" "${APPDIR}/usr/python"
+
+ cp -f "${RUNDIR}/ModOrganizer" "${APPDIR}/usr/bin/ModOrganizer"
+ [ -f "${RUNDIR}/umu-run" ] && cp -f "${RUNDIR}/umu-run" "${APPDIR}/usr/bin/"
+ [ -f "${RUNDIR}/README-PORTABLE.txt" ] && cp -f "${RUNDIR}/README-PORTABLE.txt" "${APPDIR}/usr/bin/"
+
+ # Gather built plugin .so files
+ find build/libs -type f \( \
+ -name 'libgame_*.so' -o \
+ -name 'libinstaller_*.so' -o \
+ -name 'libpreview_*.so' -o \
+ -name 'libdiagnose_*.so' -o \
+ -name 'libcheck_*.so' -o \
+ -name 'libtool_*.so' -o \
+ -name 'libinieditor.so' -o \
+ -name 'libinibakery.so' -o \
+ -name 'libbsa_extractor.so' -o \
+ -name 'libbsa_packer.so' -o \
+ -name 'libproxy.so' -o \
+ -name 'librunner.so' \
+ \) -exec cp -f {} "${APPDIR}/usr/plugins/" \;
+
+ # Python plugin payload
+ for f in libplugin_python.so lzokay.py winreg.py pyCfg.py \
+ DDSPreview.py Form43Checker.py ScriptExtenderPluginChecker.py; do
+ [ -f "build/src/src/plugins/${f}" ] && cp -f "build/src/src/plugins/${f}" "${APPDIR}/usr/plugins/"
+ done
+ for d in basic_games data libs dlls; do
+ [ -d "build/src/src/plugins/${d}" ] && cp -a "build/src/src/plugins/${d}" "${APPDIR}/usr/plugins/"
+ done
+ rm -f "${APPDIR}/usr/plugins/FNIS"*.py
+
+ # 7z runtime
+ SO7="$(find build -type f -name 7z.so | head -n1 || true)"
+ if [ -n "${SO7}" ]; then
+ cp -f "${SO7}" "${APPDIR}/usr/dlls/7z.so"
+ cp -f "${SO7}" "${APPDIR}/usr/dlls/7zip.dll"
+ fi
+
+ # Python runtime payload
+ [ -d build/src/src/python ] && cp -a build/src/src/python/. "${APPDIR}/usr/python/"
+
+ # icoutils
+ for tool in wrestool icotool; do
+ command -v "${tool}" >/dev/null 2>&1 && cp -f "$(command -v "${tool}")" "${APPDIR}/usr/bin/"
+ done
+
+ # Desktop metadata
+ printf '%s\n' \
+ '[Desktop Entry]' \
+ 'Type=Application' \
+ 'Name=ModOrganizer' \
+ 'Exec=ModOrganizer' \
+ 'Icon=modorganizer' \
+ 'Categories=Game;' \
+ 'Terminal=false' \
+ > "${APPDIR}/ModOrganizer.desktop"
+ cp -f src/src/resources/mo_icon.png "${APPDIR}/modorganizer.png"
+
+ # ── Run linuxdeploy ──
+ export PATH="/opt/linuxdeploy:${PATH}"
+ export LINUXDEPLOY_PLUGIN_QT="/opt/linuxdeploy/linuxdeploy-plugin-qt.AppDir/AppRun"
+ export QMAKE=/usr/bin/qmake6
+ export NO_STRIP=1
+ export EXTRA_QT_PLUGINS="waylandcompositor;wayland-decoration-client;wayland-graphics-integration-client;wayland-shell-integration"
+
+ /opt/linuxdeploy/linuxdeploy-x86_64.AppImage --appimage-extract-and-run \
+ --appdir "${APPDIR}" \
+ --desktop-file "${APPDIR}/ModOrganizer.desktop" \
+ --icon-file "${APPDIR}/modorganizer.png" \
+ --executable "${APPDIR}/usr/bin/ModOrganizer" \
+ --plugin qt \
+ --output appimage
+
+ # ── Verify + collect artifact ──
+ APPIMAGE_FILE="$(find "${GITHUB_WORKSPACE}" -maxdepth 1 -type f -name '*.AppImage' | head -n1)"
+ if [ -z "${APPIMAGE_FILE}" ]; then
+ echo "::error::AppImage was not produced"
+ exit 1
+ fi
+
+ # Sanity checks
+ copy_relocs="$(readelf -r "${APPDIR}/usr/bin/ModOrganizer" | grep "R_X86_64_COPY" || true)"
+ if [ -n "${copy_relocs}" ]; then
+ echo "::error::ModOrganizer contains COPY relocations"
+ echo "${copy_relocs}"
+ exit 1
+ fi
+
+ mkdir -p artifact
+ mv -f "${APPIMAGE_FILE}" "artifact/ModOrganizer-x86_64.AppImage"
+ echo "AppImage size: $(du -sh artifact/)"
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ModOrganizer-AppImage
+ path: artifact/
+ if-no-files-found: error
+
+ - name: ccache stats
+ if: always()
+ run: ccache -sv || true
+
+ release:
+ if: startsWith(github.ref, 'refs/tags/v')
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+
+ steps:
+ - name: Download artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: ModOrganizer-AppImage
+ path: artifact/
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: artifact/ModOrganizer-x86_64.AppImage
+ generate_release_notes: true
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 0000000..ec101c1
--- /dev/null
+++ b/.github/workflows/docker.yml
@@ -0,0 +1,45 @@
+name: Build Docker Image
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'docker/**'
+ workflow_dispatch:
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository_owner }}/fluorine-builder
+
+jobs:
+ build-image:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Log in to GHCR
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ context: docker
+ file: docker/Dockerfile
+ push: true
+ tags: |
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max