diff options
| -rw-r--r-- | CMakeLists.txt | 38 | ||||
| -rw-r--r-- | README.md | 7 | ||||
| -rw-r--r-- | docker/Dockerfile | 16 | ||||
| -rwxr-xr-x | docker/build-inner.sh | 44 | ||||
| -rw-r--r-- | flatpak/com.fluorine.manager.yml | 38 | ||||
| -rw-r--r-- | libs/plugin_python/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | src/src/CMakeLists.txt | 29 |
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())" @@ -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") |
