aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-03-11 07:11:59 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-03-11 07:11:59 -0500
commit03fc1da756d26944d66cff8997d712521b1fde2c (patch)
tree1ae93be8d334a8486425e9b6aa3307a4bbb0f342
parentb3f1ba196c384e3d96f84862a1e6cf21311a9cef (diff)
Remove FUSE passthrough, flake.nix, and stop archiving build output
FUSE passthrough caused Wine DLL loading failures (ERROR_NOACCESS 0x3E6) for all file types, not just executables. Root cause is a kernel-level incompatibility between FUSE passthrough I/O and Wine's file access patterns. Removed all passthrough code, the passthrough helper binary, libcap dependency, and the settings UI toggle. Also removed flake.nix (unused with Docker build system) and stopped tar.gz'ing the build output since GitHub zips release assets automatically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
-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);