aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-03-12 07:08:09 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-03-12 07:08:09 -0500
commit755698f1adceef55f52c8e44c9746537885c9bf7 (patch)
tree23d1a9fea936805708eeccf2ac2684620565d328 /src
parent019870ffd8308d94289d72d45abeb3db39ccde92 (diff)
Replace Python plugins with native C++ .so, remove bundled Python (~95MB)
- Add native plugins: form43_checker, script_extender_checker, preview_dds, basic_games (75 game defs via IPluginProxy pattern with Steam/GOG detection) - Remove portable Python from Dockerfile and build-inner.sh - Add Python settings tab with venv support (system python3 + PyQt6) - Skip loading plugin_python.so when Python disabled (default: off) - Update pythonrunner.cpp to use venv at ~/.local/share/fluorine/python-venv/ - Preserve real file permissions in FUSE VFS (fixes native executable +x bits) - Add FUSE_SET_ATTR_MODE handler so chmod works through VFS - Fix desktop shortcut Exec= to use fluorine-manager launcher, not bare binary Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/src/CMakeLists.txt2
-rw-r--r--src/src/envshortcut.cpp8
-rw-r--r--src/src/plugincontainer.cpp12
-rw-r--r--src/src/settingsdialog.cpp31
-rw-r--r--src/src/settingsdialog.ui161
-rw-r--r--src/src/settingsdialogpython.cpp206
-rw-r--r--src/src/settingsdialogpython.h29
-rw-r--r--src/src/vfs/mo2filesystem.cpp85
-rw-r--r--src/src/vfs/mo2filesystem.h1
9 files changed, 503 insertions, 32 deletions
diff --git a/src/src/CMakeLists.txt b/src/src/CMakeLists.txt
index d7a7642..30dd8b5 100644
--- a/src/src/CMakeLists.txt
+++ b/src/src/CMakeLists.txt
@@ -121,7 +121,7 @@ target_compile_definitions(organizer PRIVATE
if(NOT WIN32)
option(MO2_BUNDLE_7Z_RUNTIME "Copy a Linux 7z module into organizer/dlls" ON)
option(MO2_STAGE_PYTHON_PLUGIN_PAYLOAD
- "Stage shipped Python plugin payload into build plugins/ for Linux runs" ON)
+ "Stage shipped Python plugin payload into build plugins/ for Linux runs" OFF)
option(MO2_STAGE_INSTALLER_WIZARD
"Stage installer_wizard python plugin payload (requires additional wizard package)" OFF)
set(MO2_7Z_SO_PATH "" CACHE FILEPATH
diff --git a/src/src/envshortcut.cpp b/src/src/envshortcut.cpp
index e78e904..5b6ff8e 100644
--- a/src/src/envshortcut.cpp
+++ b/src/src/envshortcut.cpp
@@ -377,6 +377,14 @@ static QString appImageOrBinary()
if (!appImage.isEmpty() && QFile::exists(appImage)) {
return appImage;
}
+ // Prefer the fluorine-manager launcher script over the bare ModOrganizer-core
+ // binary — the launcher sets up bundled library paths, Qt plugin paths, etc.
+ // Without it, shortcuts fail on systems that don't have all deps in PATH.
+ const QString appDir = QCoreApplication::applicationDirPath();
+ const QString launcher = appDir + "/fluorine-manager";
+ if (QFile::exists(launcher)) {
+ return QFileInfo(launcher).absoluteFilePath();
+ }
return QFileInfo(qApp->applicationFilePath()).absoluteFilePath();
}
diff --git a/src/src/plugincontainer.cpp b/src/src/plugincontainer.cpp
index e215c0a..a170c99 100644
--- a/src/src/plugincontainer.cpp
+++ b/src/src/plugincontainer.cpp
@@ -9,6 +9,7 @@
#include <QDirIterator>
#include <QMessageBox>
#include <QToolButton>
+#include <QSettings>
#include <cstdio>
#include <boost/fusion/algorithm/iteration/for_each.hpp>
#include <boost/fusion/include/at_key.hpp>
@@ -1304,6 +1305,17 @@ void PluginContainer::loadPlugins()
}
}
+ // Skip the Python proxy plugin unless the user has enabled Python support
+ // in Settings > Python. Native C++ replacements handle all default plugins.
+ if (iter.fileName() == "libplugin_python.so" ||
+ iter.fileName() == "plugin_python.dll") {
+ if (!QSettings().value("fluorine/python_enabled", false).toBool()) {
+ log::debug("plugin \"{}\" skipped (Python plugins disabled in settings)",
+ iter.fileName());
+ continue;
+ }
+ }
+
if (loadCheck.isOpen()) {
loadCheck.write(iter.fileName().toUtf8());
loadCheck.write("\n");
diff --git a/src/src/settingsdialog.cpp b/src/src/settingsdialog.cpp
index 919bf4e..757c6f8 100644
--- a/src/src/settingsdialog.cpp
+++ b/src/src/settingsdialog.cpp
@@ -21,12 +21,13 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "settingsdialogdiagnostics.h"
#include "settingsdialoggeneral.h"
#include "settingsdialogmodlist.h"
-#include "settingsdialognexus.h"
-#include "settingsdialogpaths.h"
-#include "settingsdialogplugins.h"
-#include "settingsdialogproton.h"
-#include "settingsdialogtheme.h"
-#include "settingsdialogworkarounds.h"
+#include "settingsdialognexus.h"
+#include "settingsdialogpaths.h"
+#include "settingsdialogplugins.h"
+#include "settingsdialogproton.h"
+#include "settingsdialogpython.h"
+#include "settingsdialogtheme.h"
+#include "settingsdialogworkarounds.h"
#include "ui_settingsdialog.h"
using namespace MOBase;
@@ -46,14 +47,16 @@ SettingsDialog::SettingsDialog(PluginContainer* pluginContainer, Settings& setti
m_tabs.push_back(std::unique_ptr<SettingsTab>(new PathsSettingsTab(settings, *this)));
m_tabs.push_back(
std::unique_ptr<SettingsTab>(new DiagnosticsSettingsTab(settings, *this)));
- m_tabs.push_back(std::unique_ptr<SettingsTab>(new NexusSettingsTab(settings, *this)));
- m_tabs.push_back(std::unique_ptr<SettingsTab>(
- new PluginsSettingsTab(settings, m_pluginContainer, *this)));
- m_tabs.push_back(
- std::unique_ptr<SettingsTab>(new ProtonSettingsTab(settings, *this)));
- m_tabs.push_back(
- std::unique_ptr<SettingsTab>(new WorkaroundsSettingsTab(settings, *this)));
-}
+ m_tabs.push_back(std::unique_ptr<SettingsTab>(new NexusSettingsTab(settings, *this)));
+ m_tabs.push_back(std::unique_ptr<SettingsTab>(
+ new PluginsSettingsTab(settings, m_pluginContainer, *this)));
+ m_tabs.push_back(
+ std::unique_ptr<SettingsTab>(new ProtonSettingsTab(settings, *this)));
+ m_tabs.push_back(
+ std::unique_ptr<SettingsTab>(new PythonSettingsTab(settings, *this)));
+ m_tabs.push_back(
+ std::unique_ptr<SettingsTab>(new WorkaroundsSettingsTab(settings, *this)));
+}
PluginContainer* SettingsDialog::pluginContainer()
{
diff --git a/src/src/settingsdialog.ui b/src/src/settingsdialog.ui
index 646b9b6..2089c06 100644
--- a/src/src/settingsdialog.ui
+++ b/src/src/settingsdialog.ui
@@ -1897,6 +1897,167 @@ If you disable this feature, MO will only display official DLCs this way. Please
</item>
</layout>
</widget>
+ <widget class="QWidget" name="pythonTab">
+ <attribute name="title">
+ <string>Python</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_python">
+ <item>
+ <widget class="QGroupBox" name="groupBox_python">
+ <property name="title">
+ <string>Python Plugin Support</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_python">
+ <item row="0" column="0">
+ <widget class="QLabel" name="pythonEnableLabel">
+ <property name="text">
+ <string>Enable Python Plugins:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1" colspan="2">
+ <widget class="QCheckBox" name="pythonEnableCheck">
+ <property name="text">
+ <string>Load .py plugins (requires Python 3 and PyQt6)</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="pythonPathLabel">
+ <property name="text">
+ <string>Python Path:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="pythonPathEdit">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="placeholderText">
+ <string>Auto-detected</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QPushButton" name="pythonRefreshButton">
+ <property name="text">
+ <string>Detect</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="pythonVersionLabel">
+ <property name="text">
+ <string>Version:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1" colspan="2">
+ <widget class="QLabel" name="pythonVersionValue">
+ <property name="text">
+ <string>Not detected</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_venv">
+ <property name="title">
+ <string>Virtual Environment</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_venv">
+ <item row="0" column="0">
+ <widget class="QLabel" name="venvPathLabel">
+ <property name="text">
+ <string>Venv Location:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1" colspan="2">
+ <widget class="QLineEdit" name="venvPathEdit">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="venvStatusLabel">
+ <property name="text">
+ <string>Status:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1" colspan="2">
+ <widget class="QLabel" name="venvStatusValue">
+ <property name="text">
+ <string>Not created</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QPushButton" name="createVenvButton">
+ <property name="text">
+ <string>Create Virtual Environment</string>
+ </property>
+ <property name="toolTip">
+ <string>Creates a Python virtual environment and installs PyQt6. Required for loading .py plugins.</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QPushButton" name="deleteVenvButton">
+ <property name="text">
+ <string>Delete Venv</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QPushButton" name="openVenvFolderButton">
+ <property name="text">
+ <string>Open Folder</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0" colspan="3">
+ <widget class="QTextEdit" name="pythonLogOutput">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="visible">
+ <bool>false</bool>
+ </property>
+ <property name="maximumHeight">
+ <number>150</number>
+ </property>
+ <property name="font">
+ <font>
+ <family>monospace</family>
+ <pointsize>9</pointsize>
+ </font>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_python">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
<widget class="QWidget" name="workaroundTab">
<attribute name="title">
<string>Workarounds</string>
diff --git a/src/src/settingsdialogpython.cpp b/src/src/settingsdialogpython.cpp
new file mode 100644
index 0000000..ebad6eb
--- /dev/null
+++ b/src/src/settingsdialogpython.cpp
@@ -0,0 +1,206 @@
+#include "settingsdialogpython.h"
+
+#include "fluorinepaths.h"
+#include "ui_settingsdialog.h"
+
+#include <log.h>
+#include <uibase/utility.h>
+
+#include <QCheckBox>
+#include <QDir>
+#include <QFileInfo>
+#include <QMessageBox>
+#include <QProcess>
+#include <QPushButton>
+#include <QSettings>
+#include <QStandardPaths>
+
+PythonSettingsTab::PythonSettingsTab(Settings& s, SettingsDialog& d)
+ : QObject(&d), SettingsTab(s, d)
+{
+ m_venvPath = fluorineDataDir() + "/python-venv";
+ ui->venvPathEdit->setText(m_venvPath);
+
+ // Load saved state
+ bool pythonEnabled = QSettings().value("fluorine/python_enabled", false).toBool();
+ ui->pythonEnableCheck->setChecked(pythonEnabled);
+
+ detectPython();
+ refreshVenvState();
+
+ QObject::connect(ui->pythonRefreshButton, &QPushButton::clicked, this,
+ &PythonSettingsTab::detectPython);
+ QObject::connect(ui->createVenvButton, &QPushButton::clicked, this,
+ &PythonSettingsTab::onCreateVenv);
+ QObject::connect(ui->deleteVenvButton, &QPushButton::clicked, this,
+ &PythonSettingsTab::onDeleteVenv);
+ QObject::connect(ui->openVenvFolderButton, &QPushButton::clicked, this,
+ &PythonSettingsTab::onOpenVenvFolder);
+}
+
+void PythonSettingsTab::update()
+{
+ QSettings().setValue("fluorine/python_enabled",
+ ui->pythonEnableCheck->isChecked());
+}
+
+void PythonSettingsTab::detectPython()
+{
+ m_pythonPath = QStandardPaths::findExecutable("python3");
+ if (m_pythonPath.isEmpty()) {
+ m_pythonPath = QStandardPaths::findExecutable("python");
+ }
+
+ if (m_pythonPath.isEmpty()) {
+ ui->pythonPathEdit->setText("");
+ ui->pythonVersionValue->setText("Python 3 not found in PATH");
+ return;
+ }
+
+ ui->pythonPathEdit->setText(m_pythonPath);
+
+ // Get version
+ QProcess proc;
+ proc.start(m_pythonPath, {"--version"});
+ proc.waitForFinished(5000);
+
+ if (proc.exitCode() == 0) {
+ QString version = QString::fromUtf8(proc.readAllStandardOutput()).trimmed();
+ if (version.isEmpty()) {
+ version = QString::fromUtf8(proc.readAllStandardError()).trimmed();
+ }
+ ui->pythonVersionValue->setText(version);
+ } else {
+ ui->pythonVersionValue->setText("Error detecting version");
+ }
+}
+
+void PythonSettingsTab::refreshVenvState()
+{
+ QDir venvDir(m_venvPath);
+ bool exists = venvDir.exists() && venvDir.exists("bin/python3");
+
+ if (exists) {
+ // Check if PyQt6 is installed
+ QProcess proc;
+ proc.start(m_venvPath + "/bin/python3",
+ {"-c", "import PyQt6; print(PyQt6.__file__)"});
+ proc.waitForFinished(5000);
+
+ if (proc.exitCode() == 0) {
+ ui->venvStatusValue->setText("Active (PyQt6 installed)");
+ } else {
+ ui->venvStatusValue->setText("Active (PyQt6 NOT installed)");
+ }
+ } else {
+ ui->venvStatusValue->setText("Not created");
+ }
+
+ ui->deleteVenvButton->setEnabled(exists);
+ ui->openVenvFolderButton->setEnabled(exists);
+}
+
+void PythonSettingsTab::onCreateVenv()
+{
+ if (m_pythonPath.isEmpty()) {
+ QMessageBox::warning(parentWidget(), tr("No Python"),
+ tr("Python 3 was not found in PATH.\n\n"
+ "Install Python 3 using your package manager:\n"
+ " Arch: pacman -S python\n"
+ " Ubuntu: apt install python3 python3-venv\n"
+ " Fedora: dnf install python3"));
+ return;
+ }
+
+ ui->pythonLogOutput->setVisible(true);
+ ui->pythonLogOutput->clear();
+ ui->venvStatusValue->setText("Creating virtual environment...");
+
+ // Create venv
+ {
+ QProcess proc;
+ proc.start(m_pythonPath, {"-m", "venv", m_venvPath});
+ proc.waitForFinished(30000);
+
+ QString output = QString::fromUtf8(proc.readAllStandardOutput());
+ QString errors = QString::fromUtf8(proc.readAllStandardError());
+ if (!output.isEmpty())
+ ui->pythonLogOutput->append(output);
+ if (!errors.isEmpty())
+ ui->pythonLogOutput->append(errors);
+
+ if (proc.exitCode() != 0) {
+ ui->venvStatusValue->setText("Failed to create venv");
+ ui->pythonLogOutput->append("venv creation failed with exit code " +
+ QString::number(proc.exitCode()));
+ return;
+ }
+ }
+
+ ui->pythonLogOutput->append("Virtual environment created. Installing PyQt6...");
+ ui->venvStatusValue->setText("Installing PyQt6...");
+
+ // Install PyQt6
+ {
+ QProcess proc;
+ proc.start(m_venvPath + "/bin/pip",
+ {"install", "--upgrade", "pip", "PyQt6"});
+ proc.waitForFinished(120000); // PyQt6 is large, give it time
+
+ QString output = QString::fromUtf8(proc.readAllStandardOutput());
+ QString errors = QString::fromUtf8(proc.readAllStandardError());
+ if (!output.isEmpty())
+ ui->pythonLogOutput->append(output);
+ if (!errors.isEmpty())
+ ui->pythonLogOutput->append(errors);
+
+ if (proc.exitCode() != 0) {
+ ui->venvStatusValue->setText("PyQt6 installation failed");
+ ui->pythonLogOutput->append("pip install failed with exit code " +
+ QString::number(proc.exitCode()));
+ refreshVenvState();
+ return;
+ }
+ }
+
+ ui->pythonLogOutput->append("PyQt6 installed successfully.");
+ MOBase::log::info("Python venv created at {}", m_venvPath);
+
+ refreshVenvState();
+}
+
+void PythonSettingsTab::onDeleteVenv()
+{
+ QDir venvDir(m_venvPath);
+ if (!venvDir.exists()) {
+ refreshVenvState();
+ return;
+ }
+
+ auto result = QMessageBox::question(
+ parentWidget(), tr("Delete Virtual Environment"),
+ tr("Delete the Python virtual environment at:\n%1\n\n"
+ "This will disable .py plugin loading until recreated.")
+ .arg(m_venvPath));
+
+ if (result != QMessageBox::Yes)
+ return;
+
+ if (venvDir.removeRecursively()) {
+ ui->pythonLogOutput->setVisible(false);
+ MOBase::log::info("Python venv deleted at {}", m_venvPath);
+ } else {
+ QMessageBox::warning(parentWidget(), tr("Error"),
+ tr("Failed to delete the virtual environment."));
+ }
+
+ refreshVenvState();
+}
+
+void PythonSettingsTab::onOpenVenvFolder()
+{
+ QDir venvDir(m_venvPath);
+ if (venvDir.exists()) {
+ MOBase::shell::Explore(venvDir);
+ }
+}
diff --git a/src/src/settingsdialogpython.h b/src/src/settingsdialogpython.h
new file mode 100644
index 0000000..187d04b
--- /dev/null
+++ b/src/src/settingsdialogpython.h
@@ -0,0 +1,29 @@
+#ifndef SETTINGSDIALOGPYTHON_H
+#define SETTINGSDIALOGPYTHON_H
+
+#include <QObject>
+
+#include "settings.h"
+#include "settingsdialog.h"
+
+class PythonSettingsTab : public QObject, public SettingsTab
+{
+ Q_OBJECT
+
+public:
+ PythonSettingsTab(Settings& settings, SettingsDialog& dialog);
+
+ void update() override;
+
+private:
+ void detectPython();
+ void refreshVenvState();
+ void onCreateVenv();
+ void onDeleteVenv();
+ void onOpenVenvFolder();
+
+ QString m_pythonPath;
+ QString m_venvPath;
+};
+
+#endif // SETTINGSDIALOGPYTHON_H
diff --git a/src/src/vfs/mo2filesystem.cpp b/src/src/vfs/mo2filesystem.cpp
index 2b1ef5f..22116f0 100644
--- a/src/src/vfs/mo2filesystem.cpp
+++ b/src/src/vfs/mo2filesystem.cpp
@@ -28,7 +28,8 @@ constexpr double ATTR_CACHE_SECONDS = 86400.0;
void fillStatForDir(struct stat* st, fuse_ino_t ino, uid_t uid, gid_t gid);
void fillStatForFile(struct stat* st, fuse_ino_t ino, uid_t uid, gid_t gid,
uint64_t size,
- const std::chrono::system_clock::time_point& mtime);
+ const std::chrono::system_clock::time_point& mtime,
+ const std::string& real_path = {});
void maybeLogCounters(Mo2FsContext* ctx)
{
@@ -209,6 +210,7 @@ struct ChildSnapshot
bool is_dir = false;
uint64_t size = 0;
std::chrono::system_clock::time_point mtime{};
+ std::string real_path;
};
std::vector<ChildSnapshot> listChildrenSnapshot(
@@ -229,8 +231,9 @@ std::vector<ChildSnapshot> listChildrenSnapshot(
snap.name = name;
snap.is_dir = child->is_directory;
if (!child->is_directory) {
- snap.size = child->file_info.size;
- snap.mtime = child->file_info.mtime;
+ snap.size = child->file_info.size;
+ snap.mtime = child->file_info.mtime;
+ snap.real_path = child->file_info.real_path;
}
out.push_back(std::move(snap));
}
@@ -256,7 +259,8 @@ std::vector<Mo2FsContext::DirEntry> buildDirEntries(
const std::string childPath = joinPath(path, child.name);
entries.push_back(
Mo2FsContext::DirEntry{ctx->inodes->getOrCreate(childPath), child.name,
- child.is_dir, child.size, child.mtime});
+ child.is_dir, child.size, child.mtime,
+ child.real_path});
}
return entries;
@@ -329,8 +333,20 @@ std::vector<char> buildReaddirBlob(
for (const auto& entry : entries) {
struct stat st;
std::memset(&st, 0, sizeof(st));
- st.st_ino = entry.ino;
- st.st_mode = entry.is_dir ? (S_IFDIR | 0755) : (S_IFREG | 0644);
+ st.st_ino = entry.ino;
+ if (entry.is_dir) {
+ st.st_mode = S_IFDIR | 0755;
+ } else {
+ // Preserve executable bits from the real file on disk.
+ mode_t mode = 0644;
+ if (!entry.real_path.empty()) {
+ struct stat real_st;
+ if (::stat(entry.real_path.c_str(), &real_st) == 0) {
+ mode = real_st.st_mode & 0777;
+ }
+ }
+ st.st_mode = S_IFREG | mode;
+ }
const size_t entSize =
fuse_add_direntry(req, nullptr, 0, entry.name.c_str(), &st, 0);
@@ -362,7 +378,7 @@ std::vector<char> buildReaddirPlusBlob(
fillStatForDir(&e.attr, entry.ino, ctx->uid, ctx->gid);
} else {
fillStatForFile(&e.attr, entry.ino, ctx->uid, ctx->gid, entry.size,
- entry.mtime);
+ entry.mtime, entry.real_path);
}
const size_t entSize =
@@ -444,16 +460,27 @@ void fillStatForDir(struct stat* st, fuse_ino_t ino, uid_t uid, gid_t gid)
void fillStatForFile(struct stat* st, fuse_ino_t ino, uid_t uid, gid_t gid,
uint64_t size,
- const std::chrono::system_clock::time_point& mtime)
+ const std::chrono::system_clock::time_point& mtime,
+ const std::string& real_path = {})
{
std::memset(st, 0, sizeof(struct stat));
st->st_ino = ino;
- st->st_mode = S_IFREG | 0644;
st->st_nlink = 1;
st->st_uid = uid;
st->st_gid = gid;
st->st_size = static_cast<off_t>(size);
+ // Preserve executable bits from the real file on disk so native Linux
+ // executables added as mods can actually be launched through the VFS.
+ mode_t mode = 0644;
+ if (!real_path.empty()) {
+ struct stat real_st;
+ if (::stat(real_path.c_str(), &real_st) == 0) {
+ mode = real_st.st_mode & 0777;
+ }
+ }
+ st->st_mode = S_IFREG | mode;
+
const auto secs = std::chrono::duration_cast<std::chrono::seconds>(
mtime.time_since_epoch());
st->st_mtim.tv_sec = secs.count();
@@ -473,7 +500,8 @@ void replyEntryFromSnapshot(fuse_req_t req, const Mo2FsContext* ctx, fuse_ino_t
if (snap.is_directory) {
fillStatForDir(&e.attr, ino, ctx->uid, ctx->gid);
} else {
- fillStatForFile(&e.attr, ino, ctx->uid, ctx->gid, snap.size, snap.mtime);
+ fillStatForFile(&e.attr, ino, ctx->uid, ctx->gid, snap.size, snap.mtime,
+ snap.real_path);
}
fuse_reply_entry(req, &e);
@@ -683,7 +711,8 @@ void mo2_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* /*fi*/)
if (snap.is_directory) {
fillStatForDir(&st, ino, ctx->uid, ctx->gid);
} else {
- fillStatForFile(&st, ino, ctx->uid, ctx->gid, snap.size, snap.mtime);
+ fillStatForFile(&st, ino, ctx->uid, ctx->gid, snap.size, snap.mtime,
+ snap.real_path);
}
{
@@ -800,8 +829,19 @@ void mo2_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
for (size_t i = static_cast<size_t>(off); i < entries->size(); ++i) {
struct stat st;
std::memset(&st, 0, sizeof(st));
- st.st_ino = (*entries)[i].ino;
- st.st_mode = (*entries)[i].is_dir ? (S_IFDIR | 0755) : (S_IFREG | 0644);
+ st.st_ino = (*entries)[i].ino;
+ if ((*entries)[i].is_dir) {
+ st.st_mode = S_IFDIR | 0755;
+ } else {
+ mode_t mode = 0644;
+ if (!(*entries)[i].real_path.empty()) {
+ struct stat real_st;
+ if (::stat((*entries)[i].real_path.c_str(), &real_st) == 0) {
+ mode = real_st.st_mode & 0777;
+ }
+ }
+ st.st_mode = S_IFREG | mode;
+ }
const size_t ent = fuse_add_direntry(req, buf.data() + used, size - used,
(*entries)[i].name.c_str(), &st,
@@ -1164,7 +1204,7 @@ void mo2_write(fuse_req_t req, fuse_ino_t /*ino*/, const char* buf, size_t size,
fuse_reply_write(req, static_cast<size_t>(written));
}
-void mo2_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t /*mode*/,
+void mo2_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode,
struct fuse_file_info* fi)
{
Mo2FsContext* ctx = getContext(req);
@@ -1196,7 +1236,8 @@ void mo2_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t /*mo
std::error_code ec;
fs::create_directories(fs::path(realPath).parent_path(), ec);
// Create the file
- int tmpFd = open(realPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ int tmpFd = open(realPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC,
+ mode != 0 ? (mode & 07777) : 0644);
if (tmpFd < 0) {
// Fall back to staging
trackedMod.clear();
@@ -1259,7 +1300,8 @@ void mo2_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t /*mo
e.ino = newIno;
e.attr_timeout = TTL_SECONDS;
e.entry_timeout = TTL_SECONDS;
- fillStatForFile(&e.attr, newIno, ctx->uid, ctx->gid, snap.size, snap.mtime);
+ fillStatForFile(&e.attr, newIno, ctx->uid, ctx->gid, snap.size, snap.mtime,
+ snap.real_path);
fuse_reply_create(req, &e, fi);
}
@@ -1447,6 +1489,14 @@ void mo2_setattr(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set,
updateFileNode(ctx, path, target, targetIsTracked ? "Mod" : "Staging");
}
+ // Handle chmod — propagate permission changes to the real file on disk.
+ if ((to_set & FUSE_SET_ATTR_MODE) != 0 && attr != nullptr) {
+ const auto snap = snapshotForPath(ctx, path);
+ if (snap.found && !snap.is_directory && !snap.real_path.empty()) {
+ ::chmod(snap.real_path.c_str(), attr->st_mode & 07777);
+ }
+ }
+
// Handle explicit timestamp changes (utimensat / Wine SetFileTime)
if ((to_set & (FUSE_SET_ATTR_MTIME | FUSE_SET_ATTR_MTIME_NOW |
FUSE_SET_ATTR_ATIME | FUSE_SET_ATTR_ATIME_NOW)) != 0 &&
@@ -1523,7 +1573,8 @@ void mo2_setattr(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set,
if (snap.is_directory) {
fillStatForDir(&st, ino, ctx->uid, ctx->gid);
} else {
- fillStatForFile(&st, ino, ctx->uid, ctx->gid, snap.size, snap.mtime);
+ fillStatForFile(&st, ino, ctx->uid, ctx->gid, snap.size, snap.mtime,
+ snap.real_path);
}
fuse_reply_attr(req, &st, TTL_SECONDS);
}
diff --git a/src/src/vfs/mo2filesystem.h b/src/src/vfs/mo2filesystem.h
index 3877bbb..40e0ed7 100644
--- a/src/src/vfs/mo2filesystem.h
+++ b/src/src/vfs/mo2filesystem.h
@@ -51,6 +51,7 @@ struct Mo2FsContext
bool is_dir = false;
uint64_t size = 0;
std::chrono::system_clock::time_point mtime{};
+ std::string real_path;
};
struct OpenDir
{