# PicViewer Packaging Plan ## Goals PicViewer supports two release modes: - Python package: generate `sdist` and `wheel`; after installation, advanced users can run `python -m pic_viewer` or `picviewer`. - Desktop app and installer: use PyInstaller to build distributable apps natively on Windows/macOS, and generate MSI or DMG packages by platform; ordinary users do not need to understand the Python environment. This phase does not include code signing, notarization, auto-updates, or PyPI upload automation. ## Prerequisites All Python commands must be run after entering the project conda environment: ```bash conda activate PicViewer ``` The `PicViewer` environment must use Python 3.10. The project GUI binding is PySide6. Python remains pinned to 3.10 for this release line to avoid changing the interpreter and GUI binding at the same time. To rebuild the environment, use: ```bash conda create -n PicViewer python=3.10 conda activate PicViewer python scripts/packaging/install_release_dependencies.py python scripts/packaging/verify_dependency_sources.py ``` The `packaging` optional dependency group remains in `pyproject.toml` for pip users and package metadata, but release builds do not use it to resolve the desktop packaging environment. PicViewer uses `pyexiv2` as the runtime metadata backend, pyvips/libvips as the runtime color management backend, and Pillow image plugins for AVIF/HEIF decoding. These are installed with the base package. `pyexiv2` includes native Exiv2 runtime files in its wheels, while pyvips requires a matching native libvips runtime with LittleCMS support. Desktop release builds use a conda-forge-first dependency policy: all dependencies available from conda-forge are installed with conda, and PyPI is used only for the explicit `pyexiv2` fallback because `pyexiv2` is not available on conda-forge for the current macOS arm64 and Windows x64 release targets. Some package names differ between PyPI metadata and conda-forge release dependencies: - `PySide6` -> `pyside6` - `opencv-python` -> `opencv` - `Pillow` -> `pillow` - `build` -> `python-build` For desktop release builds, do not let pip resolve PicViewer's runtime dependencies. Install conda-forge dependencies and the PyPI-only fallback with the release dependency script, then verify the resulting sources: ```bash python scripts/packaging/install_release_dependencies.py python scripts/packaging/verify_dependency_sources.py python scripts/packaging/verify_pyvips_runtime.py --environment ``` This prevents pip from replacing conda-forge packages such as pyvips, OpenCV, Pillow plugins, rawpy, and PyInstaller with PyPI wheels during release builds. On macOS, if importing `pyexiv2` reports a missing `libINIReader.0.dylib`, install the native dependency with `brew install inih` before building. Windows MSI builds require WiX Toolset to be installed, and the `wix` command must be available in `PATH`. If the command is not in `PATH`, specify the path to `wix.exe` with `--wix` when running the script. WiX Toolset v7 and later may report `WIX7015` before the first build, indicating that the OSMF EULA must be accepted. First read and confirm the requirements at , then choose one of the following approaches: ```bash wix eula accept wix7 ``` Or explicitly pass the acceptance option in a one-off build command: ```bash python scripts/packaging/build_msi.py --accept-wix-eula ``` `--accept-wix-eula` passes `-acceptEula wix7` to WiX and should only be used after the WiX OSMF EULA requirements have been confirmed. Generating translation source files uses the built-in Python extractor by default: ```bash python scripts/i18n/update_ts.py ``` If a local PySide6 `lupdate` wrapper is available and should be used explicitly, pass it with `--lupdate /path/to/pyside6-lupdate`. Generating translation resources requires Qt's `lrelease` command to be available in `PATH`. If the local `lrelease` command has a different name, use: ```bash python scripts/i18n/build_qm.py --lrelease /path/to/lrelease ``` When neither `pyside6-lrelease` nor `lrelease` is in `PATH`, `build_qm.py` also searches common Qt tool directories under the active conda prefix, including `$CONDA_PREFIX/lib/qt6/bin`, `$CONDA_PREFIX/Library/bin`, and the PySide6 package directory. The legacy `~/.conda/envs/PicViewer/Lib/site-packages/PySide6` location is still checked last for older local setups. Application icon resources are generated from `src/pic_viewer/ui/resources/icons/picviewer.svg` as the primary source: ```bash python scripts/packaging/generate_icons.py ``` Icon PNG generation requires librsvg's `rsvg-convert` command: ```bash conda install -c conda-forge librsvg ``` The script generates the runtime PNG size family with `rsvg-convert`, then generates Windows `.ico` and macOS `.icns` from those PNGs. On macOS, it calls `iconutil` first and falls back to Pillow for generating `.icns` if `iconutil` is unavailable. Pillow is already a runtime dependency because the app keeps it as a fallback decoder and plugin host. The script does not fall back to QtSvg for PNG generation because QtSvg does not support the `` element used by the PicViewer SVG master. ## Python Package Mode Build command: ```bash python scripts/packaging/build_python_package.py ``` The script performs the following steps: - Verify that the current conda environment name is `PicViewer`. - Generate `src/pic_viewer/ui/resources/i18n/*.qm`. - Clean old `dist/`, `build/`, and `src/picviewer.egg-info/`. - Run `python -m build --no-isolation`, with artifacts written to `dist/`. The no-isolation build is intentional. The release dependency script installs `setuptools`, `wheel`, and `python-build` from conda-forge first, so the build step must not create a pip-resolved isolated build environment. The release package includes QSS, TS, and QM resources. After installation, it supports: ```bash python -m pic_viewer picviewer ``` RAW support remains optional for pip users: ```bash pip install "picviewer[raw]" ``` ## PyInstaller App Mode Build command: ```bash python scripts/packaging/build_app.py ``` The script performs the following steps: - Verify that the current conda environment name is `PicViewer`. - Generate `.qm` translation resources. - Invoke PyInstaller with `packaging/pyinstaller/PicViewer.spec`. - Output native platform artifacts to `dist/`. PyInstaller uses `onedir` mode. The Windows artifact is `dist/PicViewer/`, and the macOS artifact is `dist/PicViewer.app`. Windows uses `packaging/icons/picviewer.ico` as the application icon, and macOS uses `packaging/icons/picviewer.icns`. The app version installs `rawpy` through the `packaging` extra by default, providing RAW support for ordinary users. The spec packages the `rawpy` runtime core explicitly by keeping `rawpy`, `rawpy._rawpy`, and `rawpy._version`. The spec also collects `pyexiv2` submodules, `pyexiv2` dynamic libraries, pyvips submodules, pyvips/libvips dynamic libraries, Pillow HEIF/AVIF plugin modules and dynamic libraries, and macOS Homebrew `inih` dynamic libraries so Exiv2 metadata reading, ICC color conversion, and modern image decoding work in packaged apps. Pillow ImageCms hidden imports are intentionally not collected. `rawpy.enhance` and its optional `skimage`/`scipy` image-enhancement chain are also intentionally excluded because PicViewer decodes RAW files through `rawpy.imread(...).postprocess(...)` and does not use rawpy enhancement helpers. The spec generates PicViewer's own package metadata for the About dialog from `pyproject.toml` and sets the macOS bundle version from the same source. The PyInstaller spec prunes unused PySide6 runtime entries after dependency analysis. It keeps QtCore, QtGui, QtWidgets, QtDBus, and QtSvg because the app uses Qt widgets and SVG toolbar icons. It removes QtNetwork, Qt network information plugins, TLS plugins, non-native platform plugins on macOS and Windows, non-SVG image format plugins, and unused Qt translation files. Linux platform plugins are not pruned so X11 and Wayland compatibility remains controlled by the Qt runtime. On macOS, `build_app.py` post-processes the generated app bundle by replacing top-level GLib runtime dylibs used by pyvips/libvips with the active conda environment's `lib/` copies, then re-signs the bundle. This prevents PyInstaller from leaving top-level `libglib`, `libintl`, or `libpcre2` symlinks to OpenCV's private bundled copies, which can be older than the libvips runtime and break ICC conversion. On Windows, `build_app.py` rewrites the bundled `cv2/config.py` and `cv2/config-3.py` loader files after PyInstaller finishes so conda-forge OpenCV resolves `cv2/python-3/cv2.pyd` and the top-level bundled DLL directory from the installed app instead of the GitHub runner's conda environment path. Other pruning only applies to Qt runtime entries and the unused rawpy optional enhancement chain; OpenCV, rawpy's core runtime, pyexiv2, pyvips/libvips, Pillow image plugins, and PicViewer resources are left unchanged except for the Windows OpenCV loader path normalization described above. After building the PyInstaller app, verify that the bundle still contains conda-forge pyvips metadata and native libvips/LittleCMS runtime files: ```bash # Windows: dist\PicViewer\PicViewer.exe --help python scripts/packaging/verify_pyvips_runtime.py --bundle dist/PicViewer # macOS: python scripts/packaging/verify_pyvips_runtime.py --bundle dist/PicViewer.app ``` After building, inspect the packaged Qt runtime with platform-specific file listing tools. On macOS, for example: ```bash find dist/PicViewer.app/Contents/Frameworks -name 'libQt6*.dylib' -print find dist/PicViewer.app/Contents/Frameworks/PySide6/Qt/plugins -type f -print find dist/PicViewer.app/Contents/Resources/PySide6/Qt/translations -type f -print ``` Cross-building is not supported: the Windows app must be built on Windows, and the macOS app must be built on macOS. ## Windows MSI Mode Build command: ```bash python scripts/packaging/build_msi.py ``` The script performs the following steps: - Verify that the current conda environment name is `PicViewer`. - Verify that the current platform is Windows. - Verify that the WiX CLI is available. - Read the version number from `pyproject.toml`; the version must follow Windows Installer's numeric `MAJOR.MINOR.PATCH` format. - Verify that `dist/PicViewer/PicViewer.exe` already exists; if it does not, run `python scripts/packaging/build_app.py` first. - Generate a temporary WiX source file, `build/msi/PicViewer.wxs`, from the actual file tree under `dist/PicViewer/`. - Use WiX to build an x64, per-machine MSI, with the installation directory set to `C:\Program Files\PicViewer`. - Create `PicViewer` shortcuts in the Start menu and on the desktop. - Generate a SHA256 checksum file next to the MSI, with the file name `*.msi.sha256`. The MSI artifact is written to `dist/PicViewer-version.msi`, for example `dist/PicViewer-0.1.0.msi`. The SHA256 checksum file is written to `dist/PicViewer-version.msi.sha256`, with content in the format `SHA256 MSI file name`. If the WiX command is not named `wix`, use: ```bash python scripts/packaging/build_msi.py --wix C:\Path\To\wix.exe ``` If using WiX v7 and no user-level or machine-level EULA acceptance has been saved yet, you can also use: ```bash python scripts/packaging/build_msi.py --accept-wix-eula ``` ## macOS DMG Mode Build command: ```bash python scripts/packaging/build_dmg.py ``` The script performs the following steps: - Verify that the current conda environment name is `PicViewer`. - Verify that the current platform is macOS. - Read the version number from `pyproject.toml`. - Verify that `dist/PicViewer.app` already exists; if it does not, run `python scripts/packaging/build_app.py` first. - Copy `dist/PicViewer.app` to a temporary staging directory. - Create an `Applications -> /Applications` shortcut in the staging directory so users can install by dragging. - Use macOS built-in `hdiutil` to generate a compressed DMG. - Generate a SHA256 checksum file next to the DMG, with the file name `*.dmg.sha256`. The DMG artifact is written to `dist/PicViewer-version.dmg`, for example `dist/PicViewer-0.1.0.dmg`. The SHA256 checksum file is written to `dist/PicViewer-version.dmg.sha256`, with content in the format `SHA256 DMG file name`. ## Release Checklist ```bash conda activate PicViewer python --version # Should output Python 3.10.x python scripts/packaging/install_release_dependencies.py python scripts/packaging/verify_dependency_sources.py python -m unittest discover -s tests/unit python scripts/packaging/build_python_package.py python -m zipfile -l dist/*.whl python scripts/packaging/verify_pyvips_runtime.py --environment python scripts/packaging/build_app.py # Windows: python scripts/packaging/verify_pyvips_runtime.py --bundle dist/PicViewer python scripts/packaging/build_msi.py cd dist certutil -hashfile PicViewer-0.1.0.msi SHA256 cd .. # macOS: python scripts/packaging/verify_pyvips_runtime.py --bundle dist/PicViewer.app python scripts/packaging/build_dmg.py cd dist shasum -a 256 -c *.dmg.sha256 cd .. ``` When checking wheel contents, the following files should be visible: - `pic_viewer/ui/resources/styles/main.qss` - `pic_viewer/ui/resources/icons/picviewer.svg` - `pic_viewer/ui/resources/icons/picviewer-256.png` - `pic_viewer/ui/resources/i18n/picviewer_zh_CN.qm` - `pic_viewer/ui/resources/i18n/picviewer_en.qm` App verification should cover at least: - Starting with the default language. - Starting with `PICVIEWER_LANG=zh_CN`. - Explicitly verifying the English source fallback with `PICVIEWER_LANG=en` when needed. - Opening ordinary JPG/PNG images. - Opening RAW images. - Confirming that Exif/IPTC metadata appears in the Metadata tab for images that contain it. - Confirming that `--developer-mode` can write development logs.