aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-15 13:54:21 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-15 13:54:21 -0600
commit2d5cf8d6d6f137bb10b2a63823fc5382d7c78602 (patch)
tree4da6093e46d26a8aa199edc2d920c6b323a47993
parent49918c699153ad35290f961ae150bf5d4513e440 (diff)
Replace Python build deps with uv across all build paths
Use uv as the single Python package manager for Docker, source, and Flatpak builds. Python 3.13 and pybind11==2.13.6 are now consistent across all three paths. - Docker: install uv + build venv instead of python3-dev/pip/pybind11-dev - CMake: bootstrap pyvenv with uv (REQUIRED/FATAL_ERROR) when no explicit Python provided; pin pybind11==2.13.6, add sip - Flatpak: remove pybind11 cmake module, use uv pip install --target for build deps, uv for portable Python packages - build-inner.sh: use BUILD_PY throughout, uv pip install for portable runtime packages, simplify embed check to direct Python invocation - VFS helper: fall back to shared libfuse3 when static .a unavailable Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
-rw-r--r--CMakeLists.txt38
-rw-r--r--README.md7
-rw-r--r--docker/Dockerfile16
-rwxr-xr-xdocker/build-inner.sh44
-rw-r--r--flatpak/com.fluorine.manager.yml38
-rw-r--r--libs/plugin_python/CMakeLists.txt3
-rw-r--r--src/src/CMakeLists.txt29
7 files changed, 85 insertions, 90 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3a78fd1..a7d3451 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -165,35 +165,35 @@ if(BUILD_BSA_FFI)
endif()
if(BUILD_PLUGIN_PYTHON)
- set(_mo2_pyvenv "${CMAKE_SOURCE_DIR}/build/pyvenv")
- set(_mo2_pyvenv_python "${_mo2_pyvenv}/bin/python")
+ # If an explicit Python executable was provided (Docker/CI), use it directly.
+ # Otherwise, bootstrap a local venv with uv for source builds.
+ if(NOT DEFINED Python_EXECUTABLE OR NOT Python_EXECUTABLE)
+ set(_mo2_pyvenv "${CMAKE_SOURCE_DIR}/build/pyvenv")
+ set(_mo2_pyvenv_python "${_mo2_pyvenv}/bin/python")
- # Bootstrap a local Python venv with uv if the pyvenv doesn't exist yet.
- if(NOT EXISTS "${_mo2_pyvenv_python}")
- find_program(_uv_exe uv)
- if(_uv_exe)
+ if(NOT EXISTS "${_mo2_pyvenv_python}")
+ find_program(_uv_exe uv REQUIRED)
message(STATUS "Bootstrapping Python venv with uv...")
execute_process(
COMMAND ${_uv_exe} venv "${_mo2_pyvenv}" --python 3.13 --seed
RESULT_VARIABLE _uv_venv_rc)
- if(_uv_venv_rc EQUAL 0)
- execute_process(
- COMMAND ${_uv_exe} pip install --python "${_mo2_pyvenv_python}"
- pybind11 psutil vdf
- RESULT_VARIABLE _uv_pip_rc)
- if(NOT _uv_pip_rc EQUAL 0)
- message(WARNING "uv pip install failed (rc=${_uv_pip_rc})")
- endif()
- else()
- message(WARNING "uv venv creation failed (rc=${_uv_venv_rc})")
+ if(NOT _uv_venv_rc EQUAL 0)
+ message(FATAL_ERROR
+ "uv venv creation failed (rc=${_uv_venv_rc}). "
+ "Install uv: https://docs.astral.sh/uv/")
+ endif()
+ execute_process(
+ COMMAND ${_uv_exe} pip install --python "${_mo2_pyvenv_python}"
+ pybind11==2.13.6 sip psutil vdf
+ RESULT_VARIABLE _uv_pip_rc)
+ if(NOT _uv_pip_rc EQUAL 0)
+ message(FATAL_ERROR "uv pip install failed (rc=${_uv_pip_rc})")
endif()
endif()
- endif()
- if(EXISTS "${_mo2_pyvenv_python}")
set(Python_EXECUTABLE "${_mo2_pyvenv_python}" CACHE FILEPATH
"Python executable for plugin_python build" FORCE)
- # Detect pybind11 cmake dir dynamically (version-independent path).
+ # Detect pybind11 cmake dir dynamically.
execute_process(
COMMAND "${_mo2_pyvenv_python}" -c
"import pybind11; print(pybind11.get_cmake_dir())"
diff --git a/README.md b/README.md
index 73f9ace..428dd95 100644
--- a/README.md
+++ b/README.md
@@ -62,17 +62,16 @@ Requires podman (or docker). The container handles all dependencies automaticall
If you want to build directly on your system without a container, you need:
-**Build tools:** GCC/Clang, CMake, Ninja, Rust toolchain, pkg-config, patchelf
+**Build tools:** GCC/Clang, CMake, Ninja, Rust toolchain, pkg-config, patchelf, [uv](https://docs.astral.sh/uv/)
**Libraries:**
- Qt 6 (base, webengine, websockets, wayland)
- Boost (program_options, thread)
-- Python 3 dev headers, pybind11, PyQt6
- spdlog, toml++, tinyxml2, sqlite3, fontconfig
- libfuse3, lz4, zlib, zstd, bzip2, lzma
- OpenSSL, libcurl
-**Python packages:** `sip` (build tools), `psutil`, `vdf`
+Python 3.13, pybind11, and all Python packages (sip, psutil, vdf) are managed automatically by `uv` during the CMake configure step — no system Python dev packages needed.
Then build:
```bash
@@ -80,8 +79,6 @@ cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_PLUGIN_PYTHON=
cmake --build build --parallel
```
-Note: `python-sip` (the build tools package providing `sipbuild`) is required in addition to `python-pyqt6-sip` (the runtime module). If you see `ModuleNotFoundError: No module named 'sipbuild'`, install `python-sip`.
-
## Known Limitations
- Some third-party MO2 plugins are Windows-only and will fail on Linux (for example DLL/ctypes `windll` assumptions).
diff --git a/docker/Dockerfile b/docker/Dockerfile
index d6b38ce..aee1635 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -27,15 +27,21 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libqt6websockets6-dev \
qt6-wayland \
# Python
- python3 python3-dev python3-pip python3-venv \
- pybind11-dev \
- python3-pyqt6 \
+ python3 python3-pyqt6 \
# Misc
icoutils zip unzip \
&& rm -rf /var/lib/apt/lists/*
-# ── Python SIP tooling + runtime deps ──
-RUN pip3 install --break-system-packages sip psutil vdf || true
+# ── uv (Python package manager) ──
+RUN curl -LsSf https://astral.sh/uv/install.sh | sh
+ENV PATH="/root/.local/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}"
# ── 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 7c83796..0483aa2 100755
--- a/docker/build-inner.sh
+++ b/docker/build-inner.sh
@@ -1,10 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
+BUILD_PY="${BUILD_PYTHON:-$(command -v python3)}"
+
# ── Build ──
+PYBIND11_DIR="$("${BUILD_PY}" -c 'import pybind11; print(pybind11.get_cmake_dir())' 2>/dev/null || true)"
+
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
- -DPython3_EXECUTABLE="$(command -v python3)" \
+ -DPython_EXECUTABLE="${BUILD_PY}" \
+ ${PYBIND11_DIR:+-Dpybind11_DIR="${PYBIND11_DIR}"} \
-DBUILD_PLUGIN_PYTHON=ON
cmake --build build --parallel
@@ -16,7 +21,7 @@ if [ ! -f "${MODORG_BIN}" ]; then
fi
RUNDIR="build/src/src"
-PY_MM="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
+PY_MM="$("${BUILD_PY}" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
# ── Output layout (staging area — installed to ~/.local/share/fluorine by build-native.sh) ──
OUT_DIR="/src/build/staging"
@@ -38,7 +43,7 @@ if [ -f "${RUNDIR}/umu-run" ]; then
# Steam games (SteamAppId != "0") we delete UMU_ID so Proton uses the
# correct steam.exe launch path.
UMU_PATCH_DIR="$(mktemp -d)"
- (cd "${UMU_PATCH_DIR}" && python3 << PATCHEOF
+ (cd "${UMU_PATCH_DIR}" && "${BUILD_PY}" << PATCHEOF
import zipfile, pathlib
zf = zipfile.ZipFile('/src/${RUNDIR}/umu-run')
zf.extractall('src')
@@ -67,7 +72,7 @@ if old2 in src and 'del os.environ["UMU_ID"]' not in src:
run_py.write_text(src)
PATCHEOF
)
- python3 -m zipapp "${UMU_PATCH_DIR}/src" -o "${OUT_DIR}/umu-run" -p '/usr/bin/env python3'
+ "${BUILD_PY}" -m zipapp "${UMU_PATCH_DIR}/src" -o "${OUT_DIR}/umu-run" -p '/usr/bin/env python3'
chmod +x "${OUT_DIR}/umu-run"
rm -rf "${UMU_PATCH_DIR}"
fi
@@ -187,15 +192,8 @@ for search_dir in /usr/lib/python3/dist-packages \
fi
done
-# Bundle pip-installed Python packages (psutil etc.).
-for search_dir in "/usr/local/lib/python${PY_MM}/dist-packages" \
- /usr/lib/python3/dist-packages \
- "/usr/lib/python${PY_MM}/dist-packages"; do
- for pkg in psutil vdf; do
- [ -d "${search_dir}/${pkg}" ] && [ ! -d "${PYSITE}/${pkg}" ] && \
- cp -a "${search_dir}/${pkg}" "${PYSITE}/"
- done
-done
+# Install Python packages into portable runtime via uv.
+uv pip install --python "${OUT_DIR}/python/bin/python3" psutil vdf
# Build-tree Python plugin payload.
[ -d build/src/src/python ] && cp -a build/src/src/python/. "${OUT_DIR}/python/"
@@ -217,27 +215,11 @@ patchelf --set-rpath '$ORIGIN/lib' "${OUT_DIR}/ModOrganizer-core"
find "${OUT_DIR}/plugins" -name "*.so" -exec patchelf --set-rpath '$ORIGIN/../lib' {} \; 2>/dev/null || true
# ── Validate embedded Python runtime ──
-cat > /tmp/mo2_embed_py_check.c <<'C'
-#include <Python.h>
-int main(void) {
- Py_Initialize();
- int rc = PyRun_SimpleString(
- "import zlib\n"
- "import runpy\n"
- "import zipimport\n"
- "print('python embed check ok')\n");
- if (PyErr_Occurred()) {
- PyErr_Print();
- }
- Py_Finalize();
- return rc;
-}
-C
-gcc /tmp/mo2_embed_py_check.c -o /tmp/mo2_embed_py_check $(python3-config --embed --cflags --ldflags)
if ! PYTHONHOME="${OUT_DIR}/python" \
PYTHONPATH="${OUT_DIR}/python/lib/python${PY_MM}:${PYSITE}" \
LD_LIBRARY_PATH="${OUT_DIR}/lib:${OUT_DIR}/python/lib:${LD_LIBRARY_PATH:-}" \
- /tmp/mo2_embed_py_check; then
+ "${OUT_DIR}/python/bin/python3" -c \
+ "import zlib; import runpy; import zipimport; print('python embed check ok')"; then
echo "ERROR: Embedded Python runtime check failed."
exit 1
fi
diff --git a/flatpak/com.fluorine.manager.yml b/flatpak/com.fluorine.manager.yml
index a8ad61c..ec88dab 100644
--- a/flatpak/com.fluorine.manager.yml
+++ b/flatpak/com.fluorine.manager.yml
@@ -19,7 +19,6 @@ finish-args:
- --filesystem=~/.steam:ro
- --filesystem=~/.local/share/Steam
- --filesystem=~/.var/app/com.valvesoftware.Steam:ro
- - --filesystem=/usr/share/steam:ro
- --talk-name=org.freedesktop.Flatpak
cleanup:
@@ -91,18 +90,7 @@ modules:
url: https://github.com/libfuse/libfuse.git
tag: fuse-3.16.2
- # ── 6. pybind11 ──
- - name: pybind11
- buildsystem: cmake-ninja
- config-opts:
- - -DCMAKE_BUILD_TYPE=Release
- - -DPYBIND11_TEST=OFF
- sources:
- - type: git
- url: https://github.com/pybind/pybind11.git
- tag: v2.13.6
-
- # ── 7. libloot ──
+ # ── 6. libloot ──
# libloot has a Rust crate at the root and a C++ cmake project in cpp/.
# The Rust static library must be built first, then cmake links against it.
- name: libloot
@@ -146,7 +134,7 @@ modules:
url: https://github.com/loot/libloot.git
branch: master
- # ── 8. icoutils ──
+ # ── 7. icoutils ──
- name: icoutils
buildsystem: autotools
build-options:
@@ -157,7 +145,7 @@ modules:
url: https://download.savannah.gnu.org/releases/icoutils/icoutils-0.32.3.tar.bz2
sha256: 17abe02d043a253b68b47e3af69c9fc755b895db68fdc8811786125df564c6e0
- # ── 9. cabextract ──
+ # ── 8. cabextract ──
# Required by winetricks for extracting Windows cab archives.
- name: cabextract
buildsystem: autotools
@@ -166,7 +154,7 @@ modules:
url: https://www.cabextract.org.uk/cabextract-1.11.tar.gz
sha256: b5546db1155e4c718ff3d4b278573604f30dd64c3c5bfd4657cd089b823a3ac6
- # ── 10. Portable Python runtime ──
+ # ── 9. Portable Python runtime ──
# Pre-built Python used as the embedded interpreter for MO2's Python plugins.
# pip packages are installed via network during build (--share=network).
- name: portable-python
@@ -200,9 +188,10 @@ modules:
2>/dev/null || true
find /app/lib/fluorine/python -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
find /app/lib/fluorine/python -name "*.pyc" -delete 2>/dev/null || true
- # Install pip packages (PyQt6 for plugin UI, psutil, vdf).
- /app/lib/fluorine/python/bin/python3 -m ensurepip --default-pip 2>/dev/null || true
- /app/lib/fluorine/python/bin/python3 -m pip install --no-cache-dir PyQt6 psutil vdf
+ # Install uv for Python package management.
+ curl -LsSf https://astral.sh/uv/install.sh | INSTALLER_NO_MODIFY_PATH=1 UV_INSTALL_DIR=/app/bin sh
+ # Install Python packages via uv (PyQt6 for plugin UI, psutil, vdf).
+ /app/bin/uv pip install --python /app/lib/fluorine/python/bin/python3 PyQt6 psutil vdf
sources:
- type: file
url: https://github.com/bjia56/portable-python/releases/download/cpython-v3.13.9-build.0/python-headless-3.13.9-linux-x86_64.zip
@@ -210,6 +199,7 @@ modules:
dest-filename: portable-python.zip
# ── 10. Fluorine Manager (main project) ──
+ # uv (installed in portable-python step) handles pybind11 + sip for the build.
- name: fluorine
buildsystem: simple
build-options:
@@ -221,14 +211,18 @@ modules:
build-args:
- --share=network
build-commands:
- # Install sip build tools into /app (SDK /usr is read-only).
- - /usr/bin/python3 -m pip install --target=/app/lib/python3/dist-packages sip
-
# ── Configure and build ──
+ # Install pybind11 + sip into /app (SDK /usr is read-only).
+ # Uses SDK's python3 directly (not a venv) so find_package(Python)
+ # finds Development headers on cmake reconfiguration.
- |
+ /app/bin/uv pip install --target /app/lib/python3/dist-packages pybind11==2.13.6 sip
+ PYBIND11_DIR="$(python3 -c 'import pybind11; print(pybind11.get_cmake_dir())')"
cmake -S . -B _build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/app \
+ -DPython_EXECUTABLE=/usr/bin/python3 \
+ -Dpybind11_DIR="${PYBIND11_DIR}" \
-DBUILD_PLUGIN_PYTHON=ON
- cmake --build _build --parallel
diff --git a/libs/plugin_python/CMakeLists.txt b/libs/plugin_python/CMakeLists.txt
index 6d33f5b..411a82e 100644
--- a/libs/plugin_python/CMakeLists.txt
+++ b/libs/plugin_python/CMakeLists.txt
@@ -7,9 +7,6 @@ set(MO2_QT_VERSION_MAJOR 6)
find_package(Python COMPONENTS Interpreter Development REQUIRED)
find_package(pybind11 CONFIG REQUIRED)
-if(pybind11_VERSION VERSION_GREATER_EQUAL "2.14")
- message(WARNING "pybind11 ${pybind11_VERSION} detected. Versions >= 2.14 may cause build errors with MO2's Python plugin. Use 2.13.x or build with the container (./build-native.sh).")
-endif()
get_filename_component(Python_HOME ${Python_EXECUTABLE} PATH)
set(Python_DLL_DIR "${Python_HOME}")
diff --git a/src/src/CMakeLists.txt b/src/src/CMakeLists.txt
index 664b236..699dfae 100644
--- a/src/src/CMakeLists.txt
+++ b/src/src/CMakeLists.txt
@@ -130,12 +130,25 @@ if(NOT WIN32)
vfs/mo2filesystem.cpp
vfs/inodetable.cpp
vfs/overwritemanager.cpp)
- # Statically link libfuse3 so the helper is fully self-contained (runs on
- # the host via flatpak-spawn where the Flatpak SDK's .so files don't exist).
+ # Prefer static libfuse3 so the helper is fully self-contained (Flatpak
+ # runs it on the host via flatpak-spawn where SDK .so files don't exist).
+ # Fall back to shared linking for source builds where libfuse3.a isn't
+ # available (the helper still works since libfuse3.so is on the host).
+ find_library(_fuse3_static libfuse3.a PATHS ${FUSE3_LIBRARY_DIRS} NO_DEFAULT_PATH)
+ if(NOT _fuse3_static)
+ find_library(_fuse3_static libfuse3.a)
+ endif()
target_link_directories(mo2-vfs-helper PRIVATE ${FUSE3_LIBRARY_DIRS})
- target_link_libraries(mo2-vfs-helper PRIVATE
- -Wl,-Bstatic -lfuse3 -Wl,-Bdynamic
- Threads::Threads)
+ if(_fuse3_static)
+ target_link_libraries(mo2-vfs-helper PRIVATE
+ -Wl,-Bstatic -lfuse3 -Wl,-Bdynamic
+ Threads::Threads)
+ else()
+ message(STATUS "libfuse3.a not found, linking mo2-vfs-helper against shared libfuse3")
+ target_link_libraries(mo2-vfs-helper PRIVATE
+ PkgConfig::FUSE3
+ Threads::Threads)
+ endif()
target_include_directories(mo2-vfs-helper PRIVATE ${FUSE3_INCLUDE_DIRS})
target_compile_definitions(mo2-vfs-helper PRIVATE FUSE_USE_VERSION=35)
target_compile_features(mo2-vfs-helper PRIVATE cxx_std_23)
@@ -262,6 +275,12 @@ if(NOT WIN32)
endif()
endif()
+ # ── Deploy bundled stylesheets (themes) ──
+ add_custom_command(TARGET organizer POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_directory
+ "${CMAKE_CURRENT_SOURCE_DIR}/stylesheets"
+ "$<TARGET_FILE_DIR:organizer>/stylesheets")
+
# Keep only the canonical Fallout NV plugin filename on Linux (case-sensitive FS).
add_custom_command(TARGET organizer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rm -f "$<TARGET_FILE_DIR:organizer>/plugins/libgame_falloutnv.so")