diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-03-12 07:08:09 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-03-12 07:08:09 -0500 |
| commit | 755698f1adceef55f52c8e44c9746537885c9bf7 (patch) | |
| tree | 23d1a9fea936805708eeccf2ac2684620565d328 /src | |
| parent | 019870ffd8308d94289d72d45abeb3db39ccde92 (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.txt | 2 | ||||
| -rw-r--r-- | src/src/envshortcut.cpp | 8 | ||||
| -rw-r--r-- | src/src/plugincontainer.cpp | 12 | ||||
| -rw-r--r-- | src/src/settingsdialog.cpp | 31 | ||||
| -rw-r--r-- | src/src/settingsdialog.ui | 161 | ||||
| -rw-r--r-- | src/src/settingsdialogpython.cpp | 206 | ||||
| -rw-r--r-- | src/src/settingsdialogpython.h | 29 | ||||
| -rw-r--r-- | src/src/vfs/mo2filesystem.cpp | 85 | ||||
| -rw-r--r-- | src/src/vfs/mo2filesystem.h | 1 |
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 { |
