aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xdocker/build-inner.sh27
-rw-r--r--flake.nix578
-rw-r--r--src/src/CMakeLists.txt3
-rw-r--r--src/src/fuseconnector.cpp9
-rw-r--r--src/src/fuseconnector.h2
-rw-r--r--src/src/organizercore.cpp60
-rw-r--r--src/src/settingsdialog.ui34
-rw-r--r--src/src/settingsdialogproton.cpp58
-rw-r--r--src/src/vfs/mo2filesystem.cpp93
-rw-r--r--src/src/vfs/mo2filesystem.h12
10 files changed, 11 insertions, 865 deletions
diff --git a/docker/build-inner.sh b/docker/build-inner.sh
index c62285b..ca6bac7 100755
--- a/docker/build-inner.sh
+++ b/docker/build-inner.sh
@@ -290,10 +290,8 @@ for tool in wrestool icotool lootcli; do
done
# ── Fix RPATH so binaries find libs without LD_LIBRARY_PATH ──
-# IMPORTANT: Use --force-rpath to set DT_RPATH (not DT_RUNPATH).
-# DT_RPATH is honored even when the binary has file capabilities
-# (e.g. cap_sys_admin for FUSE passthrough). DT_RUNPATH and
-# LD_LIBRARY_PATH are both ignored by the kernel for capability binaries.
+# Use --force-rpath to set DT_RPATH (not DT_RUNPATH) for reliable
+# library resolution regardless of LD_LIBRARY_PATH.
echo "Patching RPATH..."
patchelf --force-rpath --set-rpath '$ORIGIN/lib:$ORIGIN/python/lib' "${OUT_DIR}/ModOrganizer-core"
[ -f "${OUT_DIR}/lootcli" ] && patchelf --force-rpath --set-rpath '$ORIGIN/lib' "${OUT_DIR}/lootcli"
@@ -350,9 +348,7 @@ PYTHON_DIR="${RUN}/python"
export PATH="${RUN}:${PATH}"
# NOTE: Do NOT set LD_LIBRARY_PATH here. The binary uses DT_RPATH
-# ($ORIGIN/lib:$ORIGIN/python/lib) to find its libraries. Setting
-# LD_LIBRARY_PATH on a capability-bearing binary (cap_sys_admin for FUSE
-# passthrough) triggers AT_SECURE mode, which drops all file capabilities.
+# ($ORIGIN/lib:$ORIGIN/python/lib) to find its libraries.
export MO2_BASE_DIR="${RUN}"
export MO2_PLUGINS_DIR="${RUN}/plugins"
export MO2_DLLS_DIR="${RUN}/dlls"
@@ -372,9 +368,6 @@ LAUNCH
chmod +x "${OUT_DIR}/fluorine-manager"
# ── qt.conf — tells Qt where to find plugins without QT_PLUGIN_PATH env ──
-# This is required when the binary has file capabilities (cap_sys_admin for
-# FUSE passthrough), because glibc strips environment variables in AT_SECURE
-# mode. qt.conf is read by Qt at startup regardless of env.
cat > "${OUT_DIR}/qt.conf" <<'QTCONF'
[Paths]
Plugins = qt6plugins
@@ -389,19 +382,17 @@ cp -f /src/data/com.fluorine.manager.metainfo.xml "${OUT_DIR}/"
# BUILD_MODE is passed from build.sh: tarball (default), installer, appimage, all
BUILD_MODE="${BUILD_MODE:-tarball}"
-# ── Build tarball (portable distribution) ──
+# ── Build portable distribution (directory) ──
+# No archive created — GitHub zips release assets automatically.
build_tarball() {
echo ""
- echo "=== Building tarball ==="
+ echo "=== Building portable distribution ==="
cd /src/build
- # Create a clean directory name for the archive
TARBALL_NAME="fluorine-manager"
rm -rf "${TARBALL_NAME}"
cp -a staging "${TARBALL_NAME}"
- tar czf "${TARBALL_NAME}.tar.gz" "${TARBALL_NAME}"/
- rm -rf "${TARBALL_NAME}"
- echo "Tarball: /src/build/${TARBALL_NAME}.tar.gz"
- ls -lh "/src/build/${TARBALL_NAME}.tar.gz"
+ echo "Output: /src/build/${TARBALL_NAME}/"
+ du -sh "/src/build/${TARBALL_NAME}"
}
# ── Build self-extracting installer (.bin frontloader) ──
@@ -686,4 +677,4 @@ echo "=== Build Summary ==="
du -sh "${OUT_DIR}"/*/ "${OUT_DIR}"/ModOrganizer-core 2>/dev/null | sort -rh
echo ""
echo "Build outputs:"
-ls -lh /src/build/fluorine-manager.tar.gz /src/build/fluorine-manager.bin /src/build/*.AppImage 2>/dev/null || echo " (none found)"
+ls -dh /src/build/fluorine-manager/ /src/build/fluorine-manager.bin /src/build/*.AppImage 2>/dev/null || echo " (none found)"
diff --git a/flake.nix b/flake.nix
deleted file mode 100644
index dc3cc86..0000000
--- a/flake.nix
+++ /dev/null
@@ -1,578 +0,0 @@
-{
- description = "Fluorine Manager — Mod Organizer 2 for Linux";
-
- inputs = {
- nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
- };
-
- outputs = { self, nixpkgs }:
- let
- system = "x86_64-linux";
- pkgs = import nixpkgs { inherit system; };
-
- version = "0.1.0";
- pythonVersion = "3.13";
- portablePythonVersion = "3.13.9";
-
- # ── libloot (not in nixpkgs — build from source) ──
- libloot = pkgs.stdenv.mkDerivation {
- pname = "libloot";
- version = "0.29.0";
-
- src = pkgs.fetchFromGitHub {
- owner = "loot";
- repo = "libloot";
- rev = "master";
- hash = "sha256-ay8ZA5ANXc5ylKOzr66rjQc1/LclW2+0CObtjprn0ms=";
- fetchSubmodules = true;
- };
-
- nativeBuildInputs = with pkgs; [ cmake ninja ];
-
- cmakeDir = "../cpp";
- cmakeFlags = [
- "-DBUILD_SHARED_LIBS=ON"
- "-DLIBLOOT_INSTALL_DOCS=OFF"
- "-DBUILD_TESTING=OFF"
- ];
-
- postInstall = ''
- mkdir -p $out/lib/pkgconfig
- cat > $out/lib/pkgconfig/libloot.pc <<EOF
- prefix=$out
- exec_prefix=''${prefix}
- libdir=''${prefix}/lib
- includedir=''${prefix}/include
-
- Name: libloot
- Description: LOOT C++ API library
- Version: 0.29.0
- Libs: -L''${libdir} -lloot
- Cflags: -I''${includedir}
- EOF
- '';
- };
-
- # ── Rust FFI crates (built separately, then injected into cmake) ──
-
- # bsa_ffi is self-contained (no local path deps).
- bsa-ffi = pkgs.rustPlatform.buildRustPackage {
- pname = "bsa-ffi";
- inherit version;
-
- src = ./libs/bsa_ffi;
- cargoLock.lockFile = ./libs/bsa_ffi/Cargo.lock;
-
- # Output is a cdylib (.so).
- installPhase = ''
- mkdir -p $out/lib
- cp target/release/libbsa_ffi.so $out/lib/
- '';
- };
-
- # nak_ffi depends on nak_rust via `path = "../nak"`. We give
- # rustPlatform the parent directory so both crates are visible,
- # and build only nak_ffi.
- nak-ffi = pkgs.rustPlatform.buildRustPackage {
- pname = "nak-ffi";
- inherit version;
-
- # Source must include both libs/nak and libs/nak_ffi.
- src = pkgs.lib.cleanSourceWith {
- src = ./libs;
- filter = path: type:
- let baseName = builtins.baseNameOf path; in
- # Include nak/ and nak_ffi/ directories (and their contents).
- builtins.match ".*/libs/(nak|nak_ffi)(/.*|$)" path != null
- # Also include the top-level libs/ dir itself.
- || type == "directory" && baseName == "libs";
- };
-
- sourceRoot = "source/nak_ffi";
- cargoLock.lockFile = ./libs/nak_ffi/Cargo.lock;
-
- installPhase = ''
- mkdir -p $out/lib
- cp target/release/libnak_ffi.so $out/lib/
- '';
- };
-
- # ── Portable Python runtime (bundled, not for compilation) ──
- portable-python = pkgs.fetchzip {
- url = "https://github.com/bjia56/portable-python/releases/download/cpython-v${portablePythonVersion}-build.0/python-headless-${portablePythonVersion}-linux-x86_64.zip";
- hash = ""; # nix will tell you the correct hash on first build
- stripRoot = false;
- };
-
- # ── Build Python with packages for compilation (pybind11, sip) ──
- buildPython = pkgs.python313.withPackages (ps: with ps; [
- pybind11
- sip
- psutil
- ]);
-
- # ── Build Fluorine from source ──
- fluorine-unwrapped = pkgs.stdenv.mkDerivation {
- pname = "fluorine-manager-unwrapped";
- inherit version;
-
- src = self;
-
- nativeBuildInputs = with pkgs; [
- cmake
- ninja
- pkg-config
- patchelf
- qt6.wrapQtAppsHook
- ];
-
- buildInputs = with pkgs; [
- # Qt 6
- qt6.qtbase
- qt6.qtwebengine
- qt6.qtwebsockets
- qt6.qtsvg
- qt6.qtwayland
-
- # Boost
- boost
-
- # Python (build-time)
- buildPython
-
- # System libraries
- sqlite
- tinyxml-2
- spdlog
- fuse3
- lz4
- zlib
- zstd
- bzip2
- xz
- openssl
- curl
- tomlplusplus
- fontconfig
- freetype
-
- # libloot (built above)
- libloot
- ];
-
- # Disable Rust FFI builds in cmake — we built them separately.
- cmakeFlags = [
- "-DCMAKE_BUILD_TYPE=RelWithDebInfo"
- "-DPython_EXECUTABLE=${buildPython}/bin/python3"
- "-Dpybind11_DIR=${buildPython}/lib/python${pythonVersion}/site-packages/pybind11/share/cmake/pybind11"
- "-DBUILD_PLUGIN_PYTHON=ON"
- "-DBUILD_NAK_FFI=OFF"
- "-DBUILD_BSA_FFI=OFF"
- ];
-
- # Replicate the staging logic from docker/build-inner.sh.
- installPhase = ''
- runHook preInstall
-
- local RUNDIR=src/src
- local PY_MM="${pythonVersion}"
-
- mkdir -p $out/opt/fluorine-manager/{plugins,dlls,lib,qt6plugins}
-
- # ── Main binary + helpers ──
- cp -f $RUNDIR/ModOrganizer $out/opt/fluorine-manager/ModOrganizer-core
- [ -f libs/lootcli/src/lootcli ] && cp -f libs/lootcli/src/lootcli $out/opt/fluorine-manager/
-
- # ── MO2 plugins (.so) ──
- find 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 \
- -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" \
- \) -exec cp -f {} $out/opt/fluorine-manager/plugins/ \;
-
- # Python plugin payload.
- for f in libplugin_python.so lzokay.py winreg.py pyCfg.py \
- DDSPreview.py Form43Checker.py ScriptExtenderPluginChecker.py; do
- [ -f "src/src/plugins/$f" ] && cp -f "src/src/plugins/$f" $out/opt/fluorine-manager/plugins/
- done
- for d in basic_games data libs dlls; do
- [ -d "src/src/plugins/$d" ] && cp -a "src/src/plugins/$d" $out/opt/fluorine-manager/plugins/
- done
- rm -f $out/opt/fluorine-manager/plugins/FNIS*.py
-
- # Source-tree Python plugins.
- for f in ../src/plugins/*.py; do
- [ -f "$f" ] && cp -f "$f" $out/opt/fluorine-manager/plugins/
- done
-
- # ── Stylesheets ──
- [ -d src/src/stylesheets ] && cp -a src/src/stylesheets $out/opt/fluorine-manager/
-
- # ── 7z runtime ──
- [ -f src/src/dlls/7z.so ] && cp -f src/src/dlls/7z.so $out/opt/fluorine-manager/dlls/
-
- # ── Project libraries ──
- cp -f libs/uibase/src/libuibase.so $out/opt/fluorine-manager/lib/
- cp -f libs/libbsarch/liblibbsarch.so $out/opt/fluorine-manager/lib/
- cp -f libs/archive/src/libarchive.so $out/opt/fluorine-manager/lib/
- cp -f libs/plugin_python/src/runner/librunner.so $out/opt/fluorine-manager/lib/
-
- # ── Pre-built Rust FFI libraries ──
- cp -f ${bsa-ffi}/lib/libbsa_ffi.so $out/opt/fluorine-manager/lib/
- cp -f ${nak-ffi}/lib/libnak_ffi.so $out/opt/fluorine-manager/lib/
-
- # ── libloot ──
- cp -Lf ${libloot}/lib/libloot.so* $out/opt/fluorine-manager/lib/ 2>/dev/null || true
-
- # ── Boost (from nix store) ──
- for boost_lib in ${pkgs.boost}/lib/libboost_program_options.so* \
- ${pkgs.boost}/lib/libboost_thread.so*; do
- [ -f "$boost_lib" ] && cp -Lf "$boost_lib" $out/opt/fluorine-manager/lib/
- done
-
- # ── Qt6 plugins ──
- QT6_PLUGIN_DIR="${pkgs.qt6.qtbase}/lib/qt-6/plugins"
- for plugin_type in platforms tls networkinformation styles \
- imageformats iconengines xcbglintegrations \
- egldeviceintegrations; do
- [ -d "$QT6_PLUGIN_DIR/$plugin_type" ] && \
- cp -a "$QT6_PLUGIN_DIR/$plugin_type" $out/opt/fluorine-manager/qt6plugins/
- done
- # Wayland plugins.
- WAYLAND_PLUGIN_DIR="${pkgs.qt6.qtwayland}/lib/qt-6/plugins"
- for plugin_type in wayland-shell-integration \
- wayland-decoration-client wayland-graphics-integration-client; do
- [ -d "$WAYLAND_PLUGIN_DIR/$plugin_type" ] && \
- cp -a "$WAYLAND_PLUGIN_DIR/$plugin_type" $out/opt/fluorine-manager/qt6plugins/
- done
- # SVG plugin.
- SVG_PLUGIN_DIR="${pkgs.qt6.qtsvg}/lib/qt-6/plugins"
- [ -d "$SVG_PLUGIN_DIR/imageformats" ] && \
- cp -a "$SVG_PLUGIN_DIR/imageformats"/. $out/opt/fluorine-manager/qt6plugins/imageformats/
-
- # ── Bundle shared library dependencies ──
- SKIP_PATTERN="linux-vdso|ld-linux|libc\.so|libm\.so|libdl\.so|librt\.so|libpthread|libresolv|libnss|libgcc_s|libstdc\+\+"
- SKIP_PATTERN="$SKIP_PATTERN|libGL\.so|libEGL|libGLX|libGLdispatch|libdrm|libvulkan|libX11|libxcb|libwayland|libxkbcommon"
- SKIP_PATTERN="$SKIP_PATTERN|libpython"
-
- collect_and_bundle() {
- ldd "$1" 2>/dev/null | grep "=>" | awk '{print $3}' | grep "^/" | while read -r dep; do
- dep_name="$(basename "$dep")"
- echo "$dep_name" | grep -qE "$SKIP_PATTERN" && continue
- [ -f "$out/opt/fluorine-manager/lib/$dep_name" ] && continue
- cp -Lf "$dep" "$out/opt/fluorine-manager/lib/" 2>/dev/null || true
- done
- }
- collect_and_bundle "$out/opt/fluorine-manager/ModOrganizer-core"
- find "$out/opt/fluorine-manager/plugins" -name "*.so" -exec bash -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"'/opt/fluorine-manager/lib/$dep_name" ] && continue
- cp -Lf "$dep" "'"$out"'/opt/fluorine-manager/lib/" 2>/dev/null || true
- done' _ {} \;
- find "$out/opt/fluorine-manager/lib" -name "*.so*" -exec bash -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"'/opt/fluorine-manager/lib/$dep_name" ] && continue
- cp -Lf "$dep" "'"$out"'/opt/fluorine-manager/lib/" 2>/dev/null || true
- done' _ {} \;
- [ -f "$out/opt/fluorine-manager/lootcli" ] && collect_and_bundle "$out/opt/fluorine-manager/lootcli"
- rm -f "$out/opt/fluorine-manager/lib"/libpython*.so* 2>/dev/null || true
-
- # ── Portable Python runtime ──
- PP_SRC=$(echo ${portable-python}/python-headless-*)
- if [ ! -d "$PP_SRC" ]; then
- PP_SRC="${portable-python}"
- fi
- cp -a "$PP_SRC" $out/opt/fluorine-manager/python
- chmod -R u+w $out/opt/fluorine-manager/python
-
- # Trim portable Python.
- PP_STDLIB="$out/opt/fluorine-manager/python/lib/python$PY_MM"
- rm -rf "$PP_STDLIB/test" "$PP_STDLIB/idlelib" "$PP_STDLIB/tkinter" \
- "$PP_STDLIB/turtledemo" "$out/opt/fluorine-manager/python/include" \
- "$out/opt/fluorine-manager/python/share" 2>/dev/null || true
- find "$out/opt/fluorine-manager/python" -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
-
- # Patch portable Python SONAME.
- PP_LIBPY="$out/opt/fluorine-manager/python/lib/libpython$PY_MM.so"
- if [ -f "$PP_LIBPY" ]; then
- CURRENT_SONAME="$(readelf -d "$PP_LIBPY" 2>/dev/null | grep SONAME | sed 's/.*\[//' | sed 's/\]//')"
- if [ "$CURRENT_SONAME" = "libpython$PY_MM.so" ]; then
- patchelf --set-soname "libpython$PY_MM.so.1.0" "$PP_LIBPY"
- mv "$PP_LIBPY" "$PP_LIBPY.1.0"
- ln -sf "libpython$PY_MM.so.1.0" "$PP_LIBPY"
- fi
- [ ! -e "$PP_LIBPY.1.0" ] && ln -sf "libpython$PY_MM.so" "$PP_LIBPY.1.0"
- fi
-
- # Bundle PyQt6 into portable Python site-packages.
- PYSITE="$out/opt/fluorine-manager/python/lib/python$PY_MM/site-packages"
- mkdir -p "$PYSITE"
- for search_dir in ${pkgs.python313Packages.pyqt6}/lib/python${pythonVersion}/site-packages; do
- if [ -d "$search_dir/PyQt6" ]; then
- cp -a "$search_dir/PyQt6" "$PYSITE/"
- [ -d "$search_dir/PyQt6_sip" ] && cp -a "$search_dir/PyQt6_sip" "$PYSITE/"
- [ -d "$search_dir/sip" ] && cp -a "$search_dir/sip" "$PYSITE/"
- break
- fi
- done
-
- # Build-tree Python plugin payload.
- [ -d src/src/python ] && cp -a src/src/python/. $out/opt/fluorine-manager/python/
-
- # ── icoutils (for .exe icon extraction) ──
- cp -f ${pkgs.icoutils}/bin/wrestool $out/opt/fluorine-manager/ 2>/dev/null || true
- cp -f ${pkgs.icoutils}/bin/icotool $out/opt/fluorine-manager/ 2>/dev/null || true
-
- # ── Strip binaries ──
- strip --strip-unneeded $out/opt/fluorine-manager/ModOrganizer-core 2>/dev/null || true
- find $out/opt/fluorine-manager/plugins -name "*.so" -exec strip --strip-unneeded {} \; 2>/dev/null || true
- find $out/opt/fluorine-manager/lib -name "*.so" -exec strip --strip-unneeded {} \; 2>/dev/null || true
- [ -f "$out/opt/fluorine-manager/lootcli" ] && strip --strip-unneeded "$out/opt/fluorine-manager/lootcli" 2>/dev/null || true
-
- # ── Patch RPATH ──
- patchelf --force-rpath --set-rpath '$ORIGIN/lib:$ORIGIN/python/lib' \
- $out/opt/fluorine-manager/ModOrganizer-core
- [ -f "$out/opt/fluorine-manager/lootcli" ] && \
- patchelf --force-rpath --set-rpath '$ORIGIN/lib' $out/opt/fluorine-manager/lootcli
- find $out/opt/fluorine-manager/plugins -name "*.so" \
- -exec patchelf --force-rpath --set-rpath '$ORIGIN/../lib:$ORIGIN/../python/lib' {} \; 2>/dev/null || true
- find $out/opt/fluorine-manager/lib -name "*.so" \
- -exec patchelf --force-rpath --set-rpath '$ORIGIN:$ORIGIN/../python/lib' {} \; 2>/dev/null || true
-
- # ── Launcher script ──
- cat > $out/opt/fluorine-manager/fluorine-manager <<'LAUNCH'
-#!/usr/bin/env bash
-set -euo pipefail
-SELF="$(readlink -f "$0")"
-HERE="$(cd "$(dirname "$SELF")" && pwd)"
-
-export FLUORINE_ORIG_LD_LIBRARY_PATH="''${LD_LIBRARY_PATH:-}"
-export FLUORINE_ORIG_PATH="''${PATH}"
-export FLUORINE_ORIG_XDG_DATA_DIRS="''${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
-export FLUORINE_ORIG_QT_PLUGIN_PATH="''${QT_PLUGIN_PATH:-}"
-
-# Sync entire app to ~/.local/share/fluorine/bin/ so instance plugin
-# symlinks always point to a stable location.
-FLUORINE_DATA="''${HOME}/.local/share/fluorine"
-BIN_DST="''${FLUORINE_DATA}/bin"
-
-CURRENT_VER="$(stat -c '%i:%Y' "''${HERE}/ModOrganizer-core" 2>/dev/null || echo "unknown")"
-MARKER="''${BIN_DST}/.version"
-
-if [ ! -f "''${MARKER}" ] || [ "$(cat "''${MARKER}" 2>/dev/null)" != "''${CURRENT_VER}" ]; then
- echo "Syncing Fluorine to ''${BIN_DST}..." >&2
- rm -rf "''${BIN_DST}"
- mkdir -p "''${BIN_DST}"
- (cd "''${HERE}" && tar --exclude-vcs -cf - .) | (cd "''${BIN_DST}" && tar -xf -)
- echo "''${CURRENT_VER}" > "''${MARKER}"
-fi
-
-RUN="''${BIN_DST}"
-PYTHON_DIR="''${RUN}/python"
-
-export PATH="''${RUN}:''${PATH}"
-# NOTE: Do NOT set LD_LIBRARY_PATH — DT_RPATH handles library resolution.
-# Setting it on a capability-bearing binary drops file capabilities (AT_SECURE).
-export MO2_BASE_DIR="''${RUN}"
-export MO2_PLUGINS_DIR="''${RUN}/plugins"
-export MO2_DLLS_DIR="''${RUN}/dlls"
-export MO2_PYTHON_DIR="''${PYTHON_DIR}"
-MO2_PYTHONHOME="''${PYTHON_DIR}"
-unset PYTHONPATH PYTHONNOUSERSITE PYTHONHOME
-
-export QT_PLUGIN_PATH="''${RUN}/qt6plugins"
-
-cd "''${RUN}"
-exec env PYTHONHOME="''${MO2_PYTHONHOME}" "''${RUN}/ModOrganizer-core" "$@"
-LAUNCH
- chmod +x $out/opt/fluorine-manager/fluorine-manager
-
- # ── Desktop integration files ──
- cp -f ../data/com.fluorine.manager.desktop $out/opt/fluorine-manager/ 2>/dev/null || true
- cp -f ../data/com.fluorine.manager.png $out/opt/fluorine-manager/ 2>/dev/null || true
- cp -f ../data/com.fluorine.manager.metainfo.xml $out/opt/fluorine-manager/ 2>/dev/null || true
-
- mkdir -p $out/share/icons/hicolor/256x256/apps
- mkdir -p $out/share/metainfo
- cp $out/opt/fluorine-manager/com.fluorine.manager.png \
- $out/share/icons/hicolor/256x256/apps/ 2>/dev/null || true
- cp $out/opt/fluorine-manager/com.fluorine.manager.metainfo.xml \
- $out/share/metainfo/ 2>/dev/null || true
-
- runHook postInstall
- '';
- };
-
- desktopItem = pkgs.makeDesktopItem {
- name = "com.fluorine.manager";
- exec = "fluorine-manager";
- icon = "com.fluorine.manager";
- desktopName = "Fluorine Manager";
- genericName = "Mod Manager";
- comment = "Mod Organizer 2 for Linux — manage your game mods";
- categories = [ "Game" "Utility" ];
- mimeTypes = [ "x-scheme-handler/nxm" ];
- startupWMClass = "ModOrganizer";
- keywords = [ "mod" "organizer" "modding" "skyrim" "fallout" ];
- };
-
- # ── FHS wrapper ──
- fluorine-manager = pkgs.buildFHSEnv {
- pname = "fluorine-manager";
- inherit version;
-
- runScript = "${fluorine-unwrapped}/opt/fluorine-manager/fluorine-manager";
-
- targetPkgs = pkgs: with pkgs; [
- glibc
- stdenv.cc.cc.lib
-
- libGL
- libglvnd
- libdrm
- vulkan-loader
- mesa
-
- libx11
- libxcb
- libxext
- libxrandr
- libxcursor
- libxi
- libxfixes
- libxrender
- libxcomposite
- libxdamage
- libxtst
- libxscrnsaver
- libxinerama
- libxkbcommon
-
- wayland
-
- fuse3
-
- fontconfig
- freetype
-
- nss
- nspr
-
- zlib
- dbus
- glib
- udev
- libcap
- polkit
- ];
-
- multiPkgs = pkgs: with pkgs; [
- glibc
- libGL
- libglvnd
- vulkan-loader
- libdrm
- udev
- alsa-lib
- libpulseaudio
- dbus
- freetype
- fontconfig
- zlib
- glib
- libx11
- libxcb
- libxext
- libxrandr
- libxcursor
- libxi
- libxfixes
- libxrender
- libxcomposite
- libxdamage
- libxtst
- libxinerama
- wayland
- libxkbcommon
- gnutls
- SDL2
- ncurses
- cups
- ];
-
- multiArch = true;
-
- unshareIpc = false;
- unsharePid = false;
-
- extraInstallCommands = ''
- mkdir -p $out/share
- ln -s ${desktopItem}/share/applications $out/share/applications
- ln -s ${fluorine-unwrapped}/share/icons $out/share/icons
- ln -s ${fluorine-unwrapped}/share/metainfo $out/share/metainfo
- '';
-
- meta = with pkgs.lib; {
- description = "Mod Organizer 2 for Linux";
- homepage = "https://github.com/SulfurNitride/Fluorine-Manager";
- license = licenses.gpl3;
- platforms = [ "x86_64-linux" ];
- mainProgram = "fluorine-manager";
- };
- };
-
- in {
- packages.${system} = {
- default = fluorine-manager;
- inherit fluorine-manager fluorine-unwrapped libloot bsa-ffi nak-ffi;
- };
-
- apps.${system}.default = {
- type = "app";
- program = "${fluorine-manager}/bin/fluorine-manager";
- };
-
- # NixOS module for system-wide installation.
- nixosModules.default = { config, lib, ... }: {
- options.programs.fluorine-manager = {
- enable = lib.mkEnableOption "Fluorine Manager (Mod Organizer 2 for Linux)";
-
- fusePassthrough = lib.mkOption {
- type = lib.types.bool;
- default = false;
- description = ''
- Enable FUSE passthrough (kernel-direct I/O for mod files).
- Grants CAP_SYS_ADMIN to the Fluorine binary via a
- security wrapper. Requires kernel 6.9+.
- '';
- };
- };
-
- config = lib.mkIf config.programs.fluorine-manager.enable {
- environment.systemPackages = [ self.packages.${system}.default ];
-
- programs.fuse.userAllowOther = true;
-
- security.wrappers = lib.mkIf config.programs.fluorine-manager.fusePassthrough {
- fluorine-manager = {
- source = "${self.packages.${system}.default}/bin/fluorine-manager";
- capabilities = "cap_sys_admin+ep";
- owner = "root";
- group = "root";
- };
- };
- };
- };
- };
-}
diff --git a/src/src/CMakeLists.txt b/src/src/CMakeLists.txt
index fa5d658..d7a7642 100644
--- a/src/src/CMakeLists.txt
+++ b/src/src/CMakeLists.txt
@@ -10,7 +10,6 @@ find_package(Boost CONFIG REQUIRED COMPONENTS
find_package(spdlog CONFIG REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(FUSE3 REQUIRED IMPORTED_TARGET fuse3)
-pkg_check_modules(LIBCAP REQUIRED IMPORTED_TARGET libcap)
find_package(Threads REQUIRED)
qt_standard_project_setup()
@@ -111,8 +110,6 @@ target_link_libraries(organizer PRIVATE
spdlog::spdlog_header_only
# FUSE3 (low-level API)
PkgConfig::FUSE3
- # libcap (runtime capability check for passthrough)
- PkgConfig::LIBCAP
# Linux system libs
dl)
diff --git a/src/src/fuseconnector.cpp b/src/src/fuseconnector.cpp
index b146359..1581423 100644
--- a/src/src/fuseconnector.cpp
+++ b/src/src/fuseconnector.cpp
@@ -300,8 +300,6 @@ bool FuseConnector::mount(
m_context->backing_dir_fd = m_backingFd;
m_context->uid = ::getuid();
m_context->gid = ::getgid();
- m_context->passthrough_requested = m_passthroughEnabled;
-
// NOTE: Do NOT include mount_point here — low-level API passes it
// separately to fuse_session_mount(). Including it here causes
// "fuse: unknown option(s)" error.
@@ -446,13 +444,6 @@ void FuseConnector::setTrackingFilePath(const std::string& path)
std::fprintf(stderr, "[VFS] setTrackingFilePath: '%s'\n", path.c_str());
}
-void FuseConnector::setPassthroughEnabled(bool enabled)
-{
- m_passthroughEnabled = enabled;
- std::fprintf(stderr, "[VFS] passthrough %s by user\n",
- enabled ? "enabled" : "disabled");
-}
-
std::shared_ptr<TrackedWrites> FuseConnector::trackedWrites() const
{
return m_trackedWrites;
diff --git a/src/src/fuseconnector.h b/src/src/fuseconnector.h
index 6d089bb..a13f014 100644
--- a/src/src/fuseconnector.h
+++ b/src/src/fuseconnector.h
@@ -42,7 +42,6 @@ public:
void setPluginLoadOrder(const std::vector<std::string>& load_order);
void setTrackingFilePath(const std::string& path);
- void setPassthroughEnabled(bool enabled);
std::shared_ptr<TrackedWrites> trackedWrites() const;
void unmount();
@@ -97,7 +96,6 @@ private:
std::thread m_fuseThread;
bool m_mounted = false;
bool m_discardStaging = false;
- bool m_passthroughEnabled = false;
};
#endif
diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp
index 0b60fdd..372e9ac 100644
--- a/src/src/organizercore.cpp
+++ b/src/src/organizercore.cpp
@@ -662,66 +662,6 @@ void OrganizerCore::prepareVFS()
m_USVFS.setTrackingFilePath(trackPath.toStdString());
}
- // FUSE passthrough: read the per-instance setting and pass it to the VFS.
- // The binary needs CAP_SYS_ADMIN for passthrough to work — it can be lost
- // when the binary is copied, updated, or extracted to a new location.
- {
- bool passthrough =
- QSettings().value("fluorine/fuse_passthrough", false).toBool();
-
- if (passthrough) {
- // Check if the binary actually has cap_sys_admin right now.
- const QString binary = QCoreApplication::applicationFilePath();
- QProcess getcap;
- getcap.setProgram("getcap");
- getcap.setArguments({binary});
- getcap.start();
- getcap.waitForFinished(5000);
- const QString caps = QString::fromUtf8(getcap.readAllStandardOutput());
- const bool hasCap = caps.contains("cap_sys_admin");
-
- if (!hasCap) {
- std::fprintf(stderr,
- "[VFS] passthrough enabled but binary lacks cap_sys_admin "
- "— requesting via pkexec...\n");
-
- // When cap_sys_admin is set on a binary, glibc's ld.so enters
- // AT_SECURE mode and ignores $ORIGIN in RPATH. Patch RPATH to
- // absolute paths before applying the capability.
- const QFileInfo fi(binary);
- const QString binDir = fi.absolutePath();
- const QString absRpath = binDir + "/lib:" + binDir + "/python/lib";
-
- // The binary may be running (text file busy), so copy → patch → replace.
- QProcess patchelf;
- patchelf.setProgram("pkexec");
- patchelf.setArguments({"bash", "-c",
- QString("cp '%1' '%1.tmp' && "
- "patchelf --force-rpath --set-rpath '%2' '%1.tmp' && "
- "setcap cap_sys_admin+ep '%1.tmp' && "
- "mv -f '%1.tmp' '%1'")
- .arg(binary, absRpath)});
- patchelf.start();
- patchelf.waitForFinished(60000);
-
- if (patchelf.exitCode() != 0) {
- std::fprintf(stderr,
- "[VFS] failed to set cap_sys_admin — disabling passthrough\n");
- passthrough = false;
- } else {
- // The capability is on the file now, but the running process won't
- // have it until next exec. Disable passthrough for this session —
- // it will work automatically on next launch.
- std::fprintf(stderr,
- "[VFS] cap_sys_admin + absolute RPATH applied — passthrough "
- "will activate on next launch\n");
- passthrough = false;
- }
- }
- }
-
- m_USVFS.setPassthroughEnabled(passthrough);
- }
#endif
m_USVFS.updateMapping(fileMapping(m_CurrentProfile->name(), QString()));
}
diff --git a/src/src/settingsdialog.ui b/src/src/settingsdialog.ui
index 7704b56..646b9b6 100644
--- a/src/src/settingsdialog.ui
+++ b/src/src/settingsdialog.ui
@@ -1883,40 +1883,6 @@ If you disable this feature, MO will only display official DLCs this way. Please
</widget>
</item>
<item>
- <widget class="QGroupBox" name="groupBox_vfsPerformance">
- <property name="title">
- <string>VFS Performance</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout_vfsPerf">
- <item>
- <widget class="QCheckBox" name="fusePassthroughCheckBox">
- <property name="text">
- <string>Enable FUSE Passthrough (near-native I/O)</string>
- </property>
- <property name="toolTip">
- <string>Let the kernel serve game file reads directly, bypassing userspace. Dramatically improves I/O performance for loading textures, meshes, and archives. Requires a one-time privilege grant (sudo prompt). Needs kernel 6.9+ and libfuse 3.16+.</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="passthroughStatusLabel">
- <property name="text">
- <string/>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- <property name="font">
- <font>
- <pointsize>9</pointsize>
- </font>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- <item>
<spacer name="verticalSpacer_16">
<property name="orientation">
<enum>Qt::Vertical</enum>
diff --git a/src/src/settingsdialogproton.cpp b/src/src/settingsdialogproton.cpp
index 207ba6d..8aa2481 100644
--- a/src/src/settingsdialogproton.cpp
+++ b/src/src/settingsdialogproton.cpp
@@ -45,64 +45,6 @@ ProtonSettingsTab::ProtonSettingsTab(Settings& s, SettingsDialog& d)
ui->launchWrapperEdit->setPlaceholderText("mangohud --dlsym");
ui->launchWrapperEdit->setText(QSettings().value("fluorine/launch_wrapper").toString());
- // FUSE passthrough toggle — requires CAP_SYS_ADMIN on the binary.
- ui->fusePassthroughCheckBox->setChecked(
- QSettings().value("fluorine/fuse_passthrough", false).toBool());
- ui->passthroughStatusLabel->setText(QString());
-
- QObject::connect(ui->fusePassthroughCheckBox, &QCheckBox::toggled, this,
- [this](bool checked) {
- if (!checked) {
- // Disabling doesn't need sudo — just save the setting.
- QSettings().setValue("fluorine/fuse_passthrough", false);
- ui->passthroughStatusLabel->setText(tr("Passthrough disabled. Takes effect on next game launch."));
- return;
- }
-
- // Enabling requires setting CAP_SYS_ADMIN on the binary.
- // Find the actual binary path (ModOrganizer-core).
- const QString binary = QCoreApplication::applicationFilePath();
- if (binary.isEmpty()) {
- ui->fusePassthroughCheckBox->setChecked(false);
- ui->passthroughStatusLabel->setText(tr("Could not determine binary path."));
- return;
- }
-
- ui->passthroughStatusLabel->setText(
- tr("Granting FUSE passthrough capability... (sudo prompt)"));
-
- // Use pkexec to patch RPATH to absolute paths and set
- // the file capability. $ORIGIN in RPATH is ignored by
- // glibc for capability binaries (AT_SECURE mode).
- const QFileInfo fi(binary);
- const QString binDir = fi.absolutePath();
- const QString absRpath = binDir + "/lib:" + binDir + "/python/lib";
-
- // The binary may be running (text file busy), so copy → patch → replace.
- QProcess proc;
- proc.setProgram("pkexec");
- proc.setArguments({"bash", "-c",
- QString("cp '%1' '%1.tmp' && "
- "patchelf --force-rpath --set-rpath '%2' '%1.tmp' && "
- "setcap cap_sys_admin+ep '%1.tmp' && "
- "mv -f '%1.tmp' '%1'")
- .arg(binary, absRpath)});
- proc.start();
- proc.waitForFinished(60000); // 60s timeout for user to auth
-
- if (proc.exitCode() == 0) {
- QSettings().setValue("fluorine/fuse_passthrough", true);
- ui->passthroughStatusLabel->setText(
- tr("Passthrough enabled. Takes effect on next game launch."));
- } else {
- ui->fusePassthroughCheckBox->setChecked(false);
- const QString err = QString::fromUtf8(proc.readAllStandardError()).trimmed();
- ui->passthroughStatusLabel->setText(
- tr("Failed to set capability: %1").arg(
- err.isEmpty() ? tr("authorization denied or pkexec not found") : err));
- }
- });
-
populateProtons();
QObject::connect(ui->protonVersionCombo, &QComboBox::currentIndexChanged, this,
diff --git a/src/src/vfs/mo2filesystem.cpp b/src/src/vfs/mo2filesystem.cpp
index 908c5bb..2b1ef5f 100644
--- a/src/src/vfs/mo2filesystem.cpp
+++ b/src/src/vfs/mo2filesystem.cpp
@@ -2,7 +2,6 @@
#include <fcntl.h>
#include <linux/fs.h>
-#include <sys/capability.h>
#include <sys/time.h>
#include <unistd.h>
@@ -567,50 +566,6 @@ void mo2_init(void* userdata, struct fuse_conn_info* conn)
conn->max_background = 128;
conn->congestion_threshold = 96;
- // FUSE passthrough: if requested by the user and supported by the kernel,
- // enable kernel-level passthrough for read-only file opens. This lets the
- // kernel serve reads directly from the backing file without round-tripping
- // through userspace — near-native I/O performance for game file reads.
- // Requires: kernel 6.9+, libfuse 3.16+, CAP_SYS_ADMIN on the binary.
- ctx->passthrough_active = false;
- if constexpr (FUSE_CAP_PASSTHROUGH != 0) {
- // Check if the process actually has CAP_SYS_ADMIN in its effective set.
- // Without it, fuse_passthrough_open will fail with EPERM, and negotiating
- // passthrough at the kernel level can break Wine/Proton DLL loading.
- bool hasCap = false;
- {
- cap_t caps = cap_get_proc();
- if (caps) {
- cap_flag_value_t val = CAP_CLEAR;
- cap_get_flag(caps, CAP_SYS_ADMIN, CAP_EFFECTIVE, &val);
- hasCap = (val == CAP_SET);
- cap_free(caps);
- }
- }
-
- if (ctx->passthrough_requested && hasCap &&
- (conn->capable & FUSE_CAP_PASSTHROUGH)) {
- conn->want |= FUSE_CAP_PASSTHROUGH;
- ctx->passthrough_active = true;
- std::fprintf(stderr, "[VFS] FUSE passthrough enabled (kernel supports it)\n");
- } else if (ctx->passthrough_requested && !hasCap) {
- std::fprintf(stderr,
- "[VFS] FUSE passthrough requested but process lacks "
- "CAP_SYS_ADMIN. Falling back to userspace I/O.\n");
- } else if (ctx->passthrough_requested) {
- std::fprintf(stderr,
- "[VFS] FUSE passthrough requested but NOT supported by kernel "
- "(capable=0x%x). Falling back to userspace I/O.\n",
- conn->capable);
- }
- } else {
- if (ctx->passthrough_requested) {
- std::fprintf(stderr,
- "[VFS] FUSE passthrough requested but NOT available at compile time "
- "(libfuse too old). Falling back to userspace I/O.\n");
- }
- }
-
// Increase max read/write buffer to 1MB (kernel 4.20+ supports this).
// Reduces FUSE round-trips for large file reads (textures, meshes, BSAs).
constexpr unsigned int ONE_MB = 1048576;
@@ -623,11 +578,10 @@ void mo2_init(void* userdata, struct fuse_conn_info* conn)
std::fprintf(stderr,
"[VFS] init: auto_inval=%d explicit_inval=%d readdirplus=%d "
- "passthrough=%d max_bg=%u max_readahead=%u\n",
+ "max_bg=%u max_readahead=%u\n",
(conn->want & FUSE_CAP_AUTO_INVAL_DATA) ? 1 : 0,
(conn->want & FUSE_CAP_EXPLICIT_INVAL_DATA) ? 1 : 0,
(conn->want & FUSE_CAP_READDIRPLUS) ? 1 : 0,
- ctx->passthrough_active ? 1 : 0,
conn->max_background, conn->max_readahead);
}
@@ -1061,44 +1015,6 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi)
fi->fh = fh;
fi->keep_cache = 1;
- // FUSE passthrough: for read-only opens, let the kernel serve reads directly
- // from the backing file. This bypasses userspace entirely — the kernel handles
- // read() syscalls by reading the real file, giving near-native I/O performance.
- // Only eligible for non-writable, non-COW files (pure reads).
- if constexpr (FUSE_CAP_PASSTHROUGH != 0) {
- if (ctx->passthrough_active && !writable && !cowPending && fd < 0) {
- // Open the real file so the kernel can passthrough reads from it.
- int ptFd = -1;
- if (isBacking && ctx->backing_dir_fd >= 0) {
- ptFd = openat(ctx->backing_dir_fd, realPath.c_str(), O_RDONLY);
- } else {
- ptFd = open(realPath.c_str(), O_RDONLY);
- }
- if (ptFd >= 0) {
- int backingId = fuse_passthrough_open(req, ptFd);
- if (backingId > 0) {
- fi->backing_id = backingId;
- // Store the fd so we can close it on release
- std::scoped_lock lock(ctx->open_files_mutex);
- auto it = ctx->open_files.find(fh);
- if (it != ctx->open_files.end()) {
- it->second.fd = ptFd;
- it->second.backing_id = backingId;
- }
- } else {
- // Passthrough registration failed (e.g. missing cap_sys_admin at
- // runtime). Disable passthrough for the rest of this session to
- // avoid spamming "Operation not permitted" on every open.
- close(ptFd);
- ctx->passthrough_active = false;
- std::fprintf(stderr,
- "[VFS] fuse_passthrough_open failed — disabling "
- "passthrough for this session\n");
- }
- }
- }
- }
-
fuse_reply_open(req, fi);
}
@@ -1687,7 +1603,7 @@ void mo2_mkdir(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t /*mod
replyEntryFromSnapshot(req, ctx, dirIno, snap);
}
-void mo2_release(fuse_req_t req, fuse_ino_t /*ino*/, struct fuse_file_info* fi)
+void mo2_release(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi)
{
Mo2FsContext* ctx = getContext(req);
if (ctx == nullptr || fi == nullptr) {
@@ -1699,11 +1615,6 @@ void mo2_release(fuse_req_t req, fuse_ino_t /*ino*/, struct fuse_file_info* fi)
std::scoped_lock lock(ctx->open_files_mutex);
auto it = ctx->open_files.find(fi->fh);
if (it != ctx->open_files.end()) {
- if constexpr (FUSE_CAP_PASSTHROUGH != 0) {
- if (it->second.backing_id > 0) {
- fuse_passthrough_close(req, it->second.backing_id);
- }
- }
if (it->second.fd >= 0) {
close(it->second.fd);
}
diff --git a/src/src/vfs/mo2filesystem.h b/src/src/vfs/mo2filesystem.h
index 9b27d76..3877bbb 100644
--- a/src/src/vfs/mo2filesystem.h
+++ b/src/src/vfs/mo2filesystem.h
@@ -16,13 +16,6 @@
#include <unordered_map>
#include <vector>
-// FUSE passthrough support (kernel 6.9+, libfuse 3.16+).
-// When available, the kernel serves reads directly from the backing file
-// without round-tripping through userspace — near-native I/O performance.
-#ifndef FUSE_CAP_PASSTHROUGH
-#define FUSE_CAP_PASSTHROUGH 0
-#endif
-
struct Mo2FsContext
{
std::shared_ptr<VfsTree> tree;
@@ -39,7 +32,6 @@ struct Mo2FsContext
struct OpenFile
{
int fd = -1;
- int backing_id = 0; // FUSE passthrough backing ID (0 = not using passthrough)
std::string real_path;
bool writable = false;
bool is_backing = false;
@@ -99,10 +91,6 @@ struct Mo2FsContext
uid_t uid = 0;
gid_t gid = 0;
- // FUSE passthrough: when true AND kernel supports it, read-only file opens
- // use kernel-level passthrough (zero-copy I/O bypassing userspace).
- bool passthrough_requested = false;
- bool passthrough_active = false; // set in mo2_init after capability check
};
void mo2_init(void* userdata, struct fuse_conn_info* conn);