diff options
| -rwxr-xr-x | docker/build-inner.sh | 27 | ||||
| -rw-r--r-- | flake.nix | 578 | ||||
| -rw-r--r-- | src/src/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | src/src/fuseconnector.cpp | 9 | ||||
| -rw-r--r-- | src/src/fuseconnector.h | 2 | ||||
| -rw-r--r-- | src/src/organizercore.cpp | 60 | ||||
| -rw-r--r-- | src/src/settingsdialog.ui | 34 | ||||
| -rw-r--r-- | src/src/settingsdialogproton.cpp | 58 | ||||
| -rw-r--r-- | src/src/vfs/mo2filesystem.cpp | 93 | ||||
| -rw-r--r-- | src/src/vfs/mo2filesystem.h | 12 |
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); |
