From de4db79e5158017c002cc91c14fa9e58420e7774 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Fri, 13 Mar 2026 06:26:18 -0500 Subject: Bundle PBS Python 3.12 + PyQt6, restore Python plugins as .py files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace portable Python approach with python-build-standalone 3.12.13 (20260310 release) bundled directly in the distribution. Python is now always available without user setup — removes the Python settings tab. - PyQt6 staged to plugins/libs/PyQt6/ with bundled Qt stripped out; patchelf'd to use our existing bundled Qt in lib/ instead of duplicating it. - Restored native-converted plugins back to .py files: Form43Checker, ScriptExtenderPluginChecker, DDSPreview, basic_games, rootbuilder, installer_omod. Removed all *_native CMake targets. - pythonrunner.cpp: removed venv/MO2_PYTHON_DIR lookup; always uses bundled Python at /python, system Python as last-resort fallback. - plugincontainer.cpp: removed fluorine/python_enabled gate (Python always loads), demoted proxy loading log messages from warn to debug, silenced [plugin-diag] stderr spam. - Dockerfile: switched from uv venv to PBS install_only tarball; PBS Python used for both build-time pybind11 compilation and runtime distribution. - build-inner.sh: aggressive Python staging (strip test/tkinter/ensurepip/ distutils/lib2to3/idlelib; wipe build-time site-packages, restore psutil+vdf). Co-Authored-By: Claude Sonnet 4.6 --- src/src/plugincontainer.cpp | 23 +---- src/src/settingsdialog.cpp | 3 - src/src/settingsdialog.ui | 161 ------------------------------ src/src/settingsdialogpython.cpp | 206 --------------------------------------- src/src/settingsdialogpython.h | 29 ------ 5 files changed, 5 insertions(+), 417 deletions(-) delete mode 100644 src/src/settingsdialogpython.cpp delete mode 100644 src/src/settingsdialogpython.h (limited to 'src') diff --git a/src/src/plugincontainer.cpp b/src/src/plugincontainer.cpp index a170c99..aa11f6c 100644 --- a/src/src/plugincontainer.cpp +++ b/src/src/plugincontainer.cpp @@ -63,10 +63,8 @@ static void ensureBundledPluginsLinked(const QString& bundledDir, } #endif -static void printPluginDiagToStderr(const QString& message) +static void printPluginDiagToStderr(const QString&) { - std::fprintf(stderr, "[plugin-diag] %s\n", qUtf8Printable(message)); - std::fflush(stderr); } // Welcome to the wonderful world of MO2 plugin management! @@ -654,7 +652,7 @@ IPlugin* PluginContainer::registerPlugin(QObject* plugin, const QString& filepat const QString pluginRoot = m_PluginPath.isEmpty() ? AppConfig::pluginsPath() : m_PluginPath; QStringList filepaths = proxy->pluginList(pluginRoot); - log::warn("proxy '{}' discovered {} proxied plugin candidate(s) in '{}'", + log::debug("proxy '{}' discovered {} proxied plugin candidate(s) in '{}'", proxy->name(), filepaths.size(), QDir::toNativeSeparators(pluginRoot)); printPluginDiagToStderr( @@ -859,7 +857,7 @@ std::vector PluginContainer::loadProxied(const QString& filepath, std::vector proxiedPlugins; try { - log::warn("loading proxied plugin candidate '{}' via proxy '{}'", + log::debug("loading proxied plugin candidate '{}' via proxy '{}'", QDir::toNativeSeparators(filepath), (proxy ? proxy->name() : QStringLiteral(""))); printPluginDiagToStderr( @@ -871,7 +869,7 @@ std::vector PluginContainer::loadProxied(const QString& filepath, // per file and do not have a good way of supporting multiple inheritance. QList matchingPlugins = proxy->load(filepath); if (matchingPlugins.isEmpty()) { - log::warn("no plugins were returned for proxied candidate '{}' via proxy '{}'", + log::debug("no plugins were returned for proxied candidate '{}' via proxy '{}'", QDir::toNativeSeparators(filepath), proxy->name()); printPluginDiagToStderr( QString("no plugins were returned for proxied candidate '%1' via proxy '%2'") @@ -930,7 +928,7 @@ std::vector PluginContainer::loadProxied(const QString& filepath, } } } - log::warn("finished proxied candidate '{}' via proxy '{}': {} plugin(s) loaded", + log::debug("finished proxied candidate '{}' via proxy '{}': {} plugin(s) loaded", QDir::toNativeSeparators(filepath), proxy->name(), proxiedPlugins.size()); printPluginDiagToStderr( @@ -1305,17 +1303,6 @@ 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 757c6f8..7a4c2b9 100644 --- a/src/src/settingsdialog.cpp +++ b/src/src/settingsdialog.cpp @@ -25,7 +25,6 @@ along with Mod Organizer. If not, see . #include "settingsdialogpaths.h" #include "settingsdialogplugins.h" #include "settingsdialogproton.h" -#include "settingsdialogpython.h" #include "settingsdialogtheme.h" #include "settingsdialogworkarounds.h" #include "ui_settingsdialog.h" @@ -52,8 +51,6 @@ SettingsDialog::SettingsDialog(PluginContainer* pluginContainer, Settings& setti new PluginsSettingsTab(settings, m_pluginContainer, *this))); m_tabs.push_back( std::unique_ptr(new ProtonSettingsTab(settings, *this))); - m_tabs.push_back( - std::unique_ptr(new PythonSettingsTab(settings, *this))); m_tabs.push_back( std::unique_ptr(new WorkaroundsSettingsTab(settings, *this))); } diff --git a/src/src/settingsdialog.ui b/src/src/settingsdialog.ui index 2089c06..646b9b6 100644 --- a/src/src/settingsdialog.ui +++ b/src/src/settingsdialog.ui @@ -1897,167 +1897,6 @@ If you disable this feature, MO will only display official DLCs this way. Please - - - Python - - - - - - Python Plugin Support - - - - - - Enable Python Plugins: - - - - - - - Load .py plugins (requires Python 3 and PyQt6) - - - - - - - Python Path: - - - - - - - true - - - Auto-detected - - - - - - - Detect - - - - - - - Version: - - - - - - - Not detected - - - - - - - - - - Virtual Environment - - - - - - Venv Location: - - - - - - - true - - - - - - - Status: - - - - - - - Not created - - - - - - - Create Virtual Environment - - - Creates a Python virtual environment and installs PyQt6. Required for loading .py plugins. - - - - - - - Delete Venv - - - - - - - Open Folder - - - - - - - true - - - false - - - 150 - - - - monospace - 9 - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - Workarounds diff --git a/src/src/settingsdialogpython.cpp b/src/src/settingsdialogpython.cpp deleted file mode 100644 index ebad6eb..0000000 --- a/src/src/settingsdialogpython.cpp +++ /dev/null @@ -1,206 +0,0 @@ -#include "settingsdialogpython.h" - -#include "fluorinepaths.h" -#include "ui_settingsdialog.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -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 deleted file mode 100644 index 187d04b..0000000 --- a/src/src/settingsdialogpython.h +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef SETTINGSDIALOGPYTHON_H -#define SETTINGSDIALOGPYTHON_H - -#include - -#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 -- cgit v1.3.1