aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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")