aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt6
-rw-r--r--docker/Dockerfile25
-rwxr-xr-xdocker/build-inner.sh103
-rw-r--r--libs/plugin_python/src/runner/pythonrunner.cpp76
-rw-r--r--src/src/plugincontainer.cpp23
-rw-r--r--src/src/settingsdialog.cpp3
-rw-r--r--src/src/settingsdialog.ui161
-rw-r--r--src/src/settingsdialogpython.cpp206
-rw-r--r--src/src/settingsdialogpython.h29
9 files changed, 122 insertions, 510 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 21e13f9..3b569ac 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -217,7 +217,6 @@ add_subdirectory(libs/game_features)
add_subdirectory(libs/game_bethesda)
add_subdirectory(libs/installer_fomod)
add_subdirectory(libs/installer_fomod_plus)
-add_subdirectory(libs/installer_omod_native)
add_subdirectory(libs/installer_bain)
add_subdirectory(libs/installer_bundle)
add_subdirectory(libs/installer_manual)
@@ -227,14 +226,9 @@ add_subdirectory(libs/preview_bsa)
add_subdirectory(libs/bsa_extractor)
add_subdirectory(libs/bsapacker)
add_subdirectory(libs/diagnose_basic)
-add_subdirectory(libs/form43_checker_native)
-add_subdirectory(libs/script_extender_checker_native)
-add_subdirectory(libs/preview_dds_native)
-add_subdirectory(libs/basic_games_native)
add_subdirectory(libs/check_fnis)
add_subdirectory(libs/tool_inibakery)
add_subdirectory(libs/tool_inieditor)
-add_subdirectory(libs/rootbuilder_native)
if(BUILD_DOTNET_PLUGINS)
if(WIN32 AND MSVC)
add_subdirectory(libs/installer_fomod_csharp)
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 5ab99dc..ec5653b 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -50,16 +50,23 @@ ENV CMAKE_PREFIX_PATH="/opt/qt6/6.10.2/gcc_64:${CMAKE_PREFIX_PATH}"
ENV PATH="/opt/qt6/6.10.2/gcc_64/bin:${PATH}"
ENV LD_LIBRARY_PATH="/opt/qt6/6.10.2/gcc_64/lib:${LD_LIBRARY_PATH}"
-# ── uv (Python package manager) ──
-RUN curl -LsSf https://astral.sh/uv/install.sh | sh
-ENV PATH="/root/.local/bin:${PATH}"
+# ── python-build-standalone 3.12 (build-time + bundled runtime) ──
+# Single Python for both compiling pybind11 modules and shipping in the tarball.
+# The install_only_stripped tarball is ~28MB; we strip it further in build-inner.sh.
+ARG PBS_VERSION=3.12.13
+ARG PBS_DATE=20260310
+RUN curl -fL --retry 3 -o /tmp/pbs-python.tar.gz \
+ "https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_DATE}/cpython-${PBS_VERSION}%2B${PBS_DATE}-x86_64-unknown-linux-gnu-install_only.tar.gz" && \
+ tar xzf /tmp/pbs-python.tar.gz -C /opt/ && \
+ mv /opt/python /opt/python-bundled && \
+ rm /tmp/pbs-python.tar.gz
+ENV BUILD_PYTHON=/opt/python-bundled/bin/python3
+ENV PATH="/opt/python-bundled/bin:${PATH}"
-# ── Build-time Python venv (pybind11, sip, etc.) ──
-RUN uv venv /opt/build-python --python 3.13 --seed && \
- uv pip install --python /opt/build-python/bin/python \
- pybind11==2.13.6 sip psutil vdf
-ENV BUILD_PYTHON=/opt/build-python/bin/python
-ENV PATH="/opt/build-python/bin:${PATH}"
+# ── Build-time Python packages ──
+# pybind11/sip/etc. for compiling mobase.so; PyQt6 is staged into the distribution.
+RUN /opt/python-bundled/bin/pip3 install --no-cache-dir \
+ pybind11==2.13.6 sip psutil vdf PyQt6
# ── Rust toolchain ──
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
diff --git a/docker/build-inner.sh b/docker/build-inner.sh
index 0c0fbc7..3290069 100755
--- a/docker/build-inner.sh
+++ b/docker/build-inner.sh
@@ -20,9 +20,12 @@ if [ -n "${CMAKE_CXX_COMPILER_LAUNCHER:-}" ]; then
CMAKE_EXTRA_ARGS+=("-DCMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER}")
fi
+PYTHON_ROOT="$(dirname "$(dirname "${BUILD_PY}")")"
+
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DPython_EXECUTABLE="${BUILD_PY}" \
+ -DPython_ROOT_DIR="${PYTHON_ROOT}" \
${PYBIND11_DIR:+-Dpybind11_DIR="${PYBIND11_DIR}"} \
-DBUILD_PLUGIN_PYTHON=ON \
"${CMAKE_EXTRA_ARGS[@]}"
@@ -65,13 +68,7 @@ find build/libs -type f \( \
-name "libinibakery.so" -o \
-name "libbsa_extractor.so" -o \
-name "libbsa_packer.so" -o \
- -name "libproxy.so" -o \
- -name "libform43_checker_native.so" -o \
- -name "libscript_extender_checker_native.so" -o \
- -name "libpreview_dds_native.so" -o \
- -name "libbasic_games_native.so" -o \
- -name "librootbuilder_native.so" -o \
- -name "libinstaller_omod_native.so" \
+ -name "libproxy.so" \
\) -exec cp -f {} "${OUT_DIR}/plugins/" \;
# Python plugin loader (small — kept for optional Python support).
@@ -81,10 +78,31 @@ if [ -d "build/src/src/plugins/libs" ]; then
mkdir -p "${OUT_DIR}/plugins/libs"
cp -f build/src/src/plugins/libs/mobase*.so "${OUT_DIR}/plugins/libs/" 2>/dev/null || true
fi
-# Python helper shims (needed by Python plugins when Python is enabled).
+# Python helper shims
for f in lzokay.py winreg.py pyCfg.py; do
[ -f "build/src/src/plugins/${f}" ] && cp -f "build/src/src/plugins/${f}" "${OUT_DIR}/plugins/"
done
+
+# Python plugins (simple single-file)
+for pyfile in \
+ "libs/form43_checker/src/Form43Checker.py" \
+ "libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker.py" \
+ "libs/preview_dds/src/DDSPreview.py" \
+ "src/plugins/rootbuilder.py" \
+ "src/plugins/installer_omod.py"; do
+ [ -f "${pyfile}" ] && cp -f "${pyfile}" "${OUT_DIR}/plugins/"
+done
+
+# basic_games Python module (directory package) — copy the whole tree, .py files only
+if [ -d "libs/basic_games" ]; then
+ cp -a "libs/basic_games" "${OUT_DIR}/plugins/basic_games"
+ # Remove non-Python clutter (metadata, lock files, vcpkg, etc.)
+ find "${OUT_DIR}/plugins/basic_games" \
+ \( -name "*.toml" -o -name "*.lock" -o -name "*.json" \
+ -o -name "*.txt" -o -name "*.md" -o -name "LICENSE" \
+ -o -name "CMakeLists.txt" -o -name "CMakePresets.json" \) \
+ -delete 2>/dev/null || true
+fi
# data/ dir (DDS headers etc., used by native plugins too).
[ -d "build/src/src/plugins/data" ] && cp -a "build/src/src/plugins/data" "${OUT_DIR}/plugins/"
@@ -204,9 +222,72 @@ if [ -f /usr/local/lib/libloot.so.0 ]; then
ln -sf libloot.so.0 "${OUT_DIR}/lib/libloot.so"
fi
-# ── No portable Python runtime ──
-# Python plugins are optional and use the system Python + venv when enabled.
-# Native C++ plugins replace DDSPreview, Form43Checker, ScriptExtenderChecker, basic_games.
+# ── Bundle PBS Python 3.12 runtime ──
+# PYTHONHOME only needs lib/python3.12/ (the stdlib). We do NOT copy the
+# binary, headers, static lib, or .py sources — only stripped .pyc + .so.
+PBS_SRC="/opt/python-bundled"
+PYTHON_OUT="${OUT_DIR}/python"
+mkdir -p "${PYTHON_OUT}/lib"
+
+# Copy only the stdlib directory
+cp -a "${PBS_SRC}/lib/python3.12" "${PYTHON_OUT}/lib/"
+
+# Remove only what is safe — test suites, GUI toolkits, dev tools.
+# Do NOT strip network/stdlib modules; basic_games uses email, http, xml, urllib, etc.
+find "${PYTHON_OUT}" -type d \( -name "test" -o -name "tests" \) \
+ -exec rm -rf {} + 2>/dev/null || true
+rm -rf "${PYTHON_OUT}/lib/python3.12/tkinter"
+rm -rf "${PYTHON_OUT}/lib/python3.12/ensurepip"
+rm -rf "${PYTHON_OUT}/lib/python3.12/distutils"
+rm -rf "${PYTHON_OUT}/lib/python3.12/lib2to3"
+rm -rf "${PYTHON_OUT}/lib/python3.12/idlelib"
+rm -rf "${PYTHON_OUT}/lib/python3.12/turtledemo"
+rm -f "${PYTHON_OUT}/lib/python3.12/turtle.py"
+# Wipe site-packages entirely — build-time packages (pybind11, PyQt6, sip, etc.)
+# are not needed at runtime. PyQt6 is staged separately to plugins/libs/PyQt6/.
+rm -rf "${PYTHON_OUT}/lib/python3.12/site-packages"
+mkdir -p "${PYTHON_OUT}/lib/python3.12/site-packages"
+# Copy runtime-required packages back in
+for pkg in psutil vdf; do
+ pkg_dir="$("${PBS_SRC}/bin/python3" -c "import importlib.util; s=importlib.util.find_spec('${pkg}'); print(s.submodule_search_locations[0] if s and s.submodule_search_locations else (s.origin if s else ''))" 2>/dev/null || true)"
+ if [ -d "${pkg_dir}" ]; then
+ cp -a "${pkg_dir}" "${PYTHON_OUT}/lib/python3.12/site-packages/"
+ elif [ -f "${pkg_dir}" ]; then
+ cp -f "${pkg_dir}" "${PYTHON_OUT}/lib/python3.12/site-packages/"
+ fi
+done
+
+# Pre-compile .py → .pyc (PBS ships .py + .pyc; this ensures cache is fresh).
+# We keep the .py source files — Python's SourceFileLoader requires them to
+# find the corresponding __pycache__/*.pyc files. Deleting them breaks imports.
+"${PBS_SRC}/bin/python3" -m compileall -q "${PYTHON_OUT}/lib/python3.12/" 2>/dev/null || true
+
+# Strip debug info from extension modules
+find "${PYTHON_OUT}/lib/python3.12" -name "*.so" \
+ -exec strip --strip-unneeded {} \; 2>/dev/null || true
+
+# libpython shared library goes in our lib/ (dlopen'd by librunner.so via $ORIGIN RPATH)
+# Not placed inside python/ — PYTHONHOME doesn't need the shared lib alongside the stdlib.
+cp -Lf "${PBS_SRC}/lib/libpython3.12.so.1.0" "${OUT_DIR}/lib/"
+strip --strip-unneeded "${OUT_DIR}/lib/libpython3.12.so.1.0" 2>/dev/null || true
+ln -sf libpython3.12.so.1.0 "${OUT_DIR}/lib/libpython3.12.so"
+echo "Bundled PBS Python 3.12: $(du -sh "${PYTHON_OUT}" | cut -f1)"
+
+# ── Bundle PyQt6 (bindings only — reuse our bundled Qt, no duplicate Qt .so) ──
+# PyQt6 pip wheel bundles Qt under PyQt6/Qt6/lib/ which we strip out.
+# The binding .so files are patchelf'd to find our Qt in lib/.
+PYQT6_SRC="$("${PBS_SRC}/bin/python3" -c 'import PyQt6, os; print(os.path.dirname(PyQt6.__file__))')"
+PYQT6_OUT="${OUT_DIR}/plugins/libs/PyQt6"
+mkdir -p "${PYQT6_OUT}"
+cp -a "${PYQT6_SRC}/." "${PYQT6_OUT}/"
+# Remove PyQt6's bundled Qt — we already have Qt in lib/
+rm -rf "${PYQT6_OUT}/Qt6"
+# Patchelf all PyQt6 binding .so files to reach our lib/ via RPATH
+# Path: plugins/libs/PyQt6/*.so → ../../.. = staging root → lib/
+find "${PYQT6_OUT}" -name "*.so" -exec \
+ patchelf --force-rpath --set-rpath '$ORIGIN/../../../lib' {} \; 2>/dev/null || true
+strip --strip-unneeded "${PYQT6_OUT}"/*.so 2>/dev/null || true
+echo "Bundled PyQt6 (no Qt dupe): $(du -sh "${PYQT6_OUT}" | cut -f1)"
# ── Strip all MO2 binaries ──
echo "Stripping MO2 binaries..."
diff --git a/libs/plugin_python/src/runner/pythonrunner.cpp b/libs/plugin_python/src/runner/pythonrunner.cpp
index 0e6d596..c630530 100644
--- a/libs/plugin_python/src/runner/pythonrunner.cpp
+++ b/libs/plugin_python/src/runner/pythonrunner.cpp
@@ -148,64 +148,21 @@ namespace mo2::python {
#endif
// Determine Python home directory.
- // Priority: 1) venv at ~/.local/share/fluorine/python-venv/
- // 2) MO2_PYTHON_DIR env var (legacy bundled Python)
- // 3) <exe_dir>/python (legacy bundled Python)
- // 4) system Python (no PYTHONHOME needed)
+ // Priority: 1) <exe_dir>/python (bundled PBS Python)
+ // 2) system Python (no PYTHONHOME — last resort fallback)
QString pythonHome;
- QString venvPath;
- bool usingVenv = false;
- // Check for user-created venv
{
- QString dataDir = QDir::homePath() + "/.local/share/fluorine";
- QString venvDir = dataDir + "/python-venv";
- if (QDir(venvDir).exists() && QFile::exists(venvDir + "/bin/python3")) {
- venvPath = venvDir;
- // Read pyvenv.cfg to find the base Python prefix
- QFile cfg(venvDir + "/pyvenv.cfg");
- if (cfg.open(QIODevice::ReadOnly | QIODevice::Text)) {
- while (!cfg.atEnd()) {
- QString line = QString::fromUtf8(cfg.readLine()).trimmed();
- if (line.startsWith("home")) {
- // "home = /usr/bin" → base prefix is parent of bin dir
- int eq = line.indexOf('=');
- if (eq >= 0) {
- QString binDir = line.mid(eq + 1).trimmed();
- QDir parent(binDir);
- parent.cdUp();
- pythonHome = parent.absolutePath();
- usingVenv = true;
- MOBase::log::info("python: using venv at '{}', base prefix '{}'",
- venvPath, pythonHome);
- }
- break;
- }
- }
- }
- }
- }
-
- // Fallback: legacy bundled Python
- if (pythonHome.isEmpty()) {
- const char* envPy = std::getenv("MO2_PYTHON_DIR");
- if (envPy && envPy[0] != '\0') {
- pythonHome = QString::fromUtf8(envPy);
+ QString bundled = QCoreApplication::applicationDirPath() + "/python";
+ if (QDir(bundled).exists()) {
+ pythonHome = bundled;
+ MOBase::log::info("python: using bundled Python at '{}'", pythonHome);
} else {
- QString bundled = QCoreApplication::applicationDirPath() + "/python";
- if (QDir(bundled).exists()) {
- pythonHome = bundled;
- }
+ MOBase::log::warn("python: bundled Python not found at '{}', "
+ "falling back to system Python", bundled);
}
}
- // If no bundled Python, use system Python (don't set PYTHONHOME)
- if (!pythonHome.isEmpty() && !QDir(pythonHome).exists()) {
- MOBase::log::warn("python: PYTHONHOME dir '{}' does not exist, "
- "falling back to system Python", pythonHome);
- pythonHome.clear();
- }
-
std::optional<QByteArray> oldPythonHome;
std::optional<QByteArray> oldPythonPath;
auto restorePythonEnv = [&]() {
@@ -259,29 +216,14 @@ namespace mo2::python {
setenv("PYTHONHOME", pythonHome.toUtf8().constData(), 1);
}
- // If using a venv, prepend its site-packages so venv packages
- // (PyQt6, etc.) take priority over system site-packages.
- if (usingVenv && !venvPath.isEmpty()) {
- const QDir venvLibDir(venvPath + "/lib");
- const auto venvPyDirs =
- venvLibDir.entryList({"python3.*"}, QDir::Dirs | QDir::NoDotAndDotDot);
- if (!venvPyDirs.isEmpty()) {
- QString venvSite = venvPath + "/lib/" + venvPyDirs.first() + "/site-packages";
- if (QDir(venvSite).exists()) {
- corePaths.prepend(venvSite);
- }
- }
- }
-
if (!corePaths.isEmpty()) {
setenv("PYTHONPATH", corePaths.join(":").toUtf8().constData(), 1);
}
MOBase::log::debug(
"python: calling Py_InitializeFromConfig, PYTHONHOME='{}', "
- "venv='{}', Py_IsInitialized before={}",
+ "Py_IsInitialized before={}",
pythonHome.isEmpty() ? "(system)" : pythonHome,
- venvPath.isEmpty() ? "(none)" : venvPath,
Py_IsInitialized());
// Use Py_InitializeFromConfig (Python 3.8+) for explicit error reporting.
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<QObject*> PluginContainer::loadProxied(const QString& filepath,
std::vector<QObject*> 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("<null>")));
printPluginDiagToStderr(
@@ -871,7 +869,7 @@ std::vector<QObject*> PluginContainer::loadProxied(const QString& filepath,
// per file and do not have a good way of supporting multiple inheritance.
QList<QObject*> 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<QObject*> 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 <http://www.gnu.org/licenses/>.
#include "settingsdialogpaths.h"
#include "settingsdialogplugins.h"
#include "settingsdialogproton.h"
-#include "settingsdialogpython.h"
#include "settingsdialogtheme.h"
#include "settingsdialogworkarounds.h"
#include "ui_settingsdialog.h"
@@ -53,8 +52,6 @@ SettingsDialog::SettingsDialog(PluginContainer* pluginContainer, Settings& setti
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)));
}
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
</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
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 <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
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 <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