mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-06-30 18:54:51 +02:00
feat: Add Windows compatibility, PyInstaller build setup, and custom icon
- Add Windows LibreOffice detection fallback for soffice path - Check standard install locations (%PROGRAMFILES%, %PROGRAMFILES(X86)%) - Graceful fallback with clear error messages - Add PyInstaller build infrastructure - build/ccma.spec: PyInstaller configuration with icon generation - build/build.ps1: Automated build script for standalone exe - main.py: Entry point for PyInstaller - Supports versioning and architecture tagging - Create custom CCMA icon - Person + Gear symbol representing member administration - Cyan/White/Orange color scheme - Auto-converts PNG to ICO during build - Update documentation - README: Windows PowerShell setup instructions - README: Linux/macOS bash setup instructions - README: Standalone executable build guide - pyproject.toml: Add 'build' extra with pyinstaller+pillow - Add regression tests - Test office executable detection and Windows fallback - Verify all tests pass (80 passed)
This commit is contained in:
@@ -12,6 +12,29 @@ Requires Python 3.11+ with Tk support.
|
||||
PDF generation from document templates additionally requires LibreOffice or
|
||||
OpenOffice with a `soffice` command available on the system.
|
||||
|
||||
### Windows (PowerShell)
|
||||
|
||||
```powershell
|
||||
python -m venv .venv
|
||||
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
|
||||
.\.venv\Scripts\ccma.exe
|
||||
```
|
||||
|
||||
For development tools and tests, install the `dev` extra as well:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe -m pip install -e .[dev]
|
||||
```
|
||||
|
||||
Alternatively, without installation:
|
||||
|
||||
```powershell
|
||||
$env:PYTHONPATH = "src"
|
||||
.\.venv\Scripts\python.exe -m ccma
|
||||
```
|
||||
|
||||
### Linux/macOS
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
.venv/bin/pip install -r requirements.txt
|
||||
@@ -30,6 +53,32 @@ Alternatively, without installation:
|
||||
PYTHONPATH=src python -m ccma
|
||||
```
|
||||
|
||||
If LibreOffice is installed on Windows but `soffice` is not in `PATH`, CCMA
|
||||
also checks the default install locations under `%PROGRAMFILES%` and
|
||||
`%PROGRAMFILES(X86)%`.
|
||||
|
||||
## Building standalone executables
|
||||
|
||||
To bundle CCMA into a single-file Windows executable with PyInstaller, use the
|
||||
build script in the `build/` directory:
|
||||
|
||||
### Windows standalone build
|
||||
|
||||
```powershell
|
||||
cd build
|
||||
.\build.ps1
|
||||
```
|
||||
|
||||
This creates two executables in the `dist/` directory:
|
||||
- `ccma.exe` (generic)
|
||||
- `ccma-<version>-windows-<arch>.exe` (version and architecture-specific, e.g. `ccma-0.1.0-windows-amd64.exe`)
|
||||
|
||||
The build script automatically:
|
||||
- Creates a venv if missing
|
||||
- Installs dependencies and PyInstaller
|
||||
- Generates a Windows icon from the splash image
|
||||
- Runs PyInstaller to bundle the application
|
||||
|
||||
On first start, select or create the central member-store directory. The
|
||||
`VERSION` file is the single source for application and package versions.
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
"""Entry point for PyInstaller and direct invocation."""
|
||||
|
||||
from ccma.app import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -21,6 +21,10 @@ dev = [
|
||||
"pytest>=8",
|
||||
"ruff>=0.6",
|
||||
]
|
||||
build = [
|
||||
"pyinstaller>=6",
|
||||
"pillow>=9",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
ccma = "ccma.app:main"
|
||||
|
||||
@@ -474,7 +474,7 @@ def _slot_at(boundaries: list[tuple[int, int]], position: int) -> tuple[int, int
|
||||
|
||||
|
||||
def _convert_to_pdf(source: Path, output_directory: Path) -> Path:
|
||||
executable = shutil.which("soffice") or shutil.which("libreoffice")
|
||||
executable = _office_executable()
|
||||
if not executable:
|
||||
raise DocumentError("LibreOffice/OpenOffice wurde nicht gefunden; PDF-Erzeugung ist nicht möglich.")
|
||||
profile = output_directory / "libreoffice-profile"
|
||||
@@ -499,6 +499,24 @@ def _convert_to_pdf(source: Path, output_directory: Path) -> Path:
|
||||
return converted
|
||||
|
||||
|
||||
def _office_executable() -> str | None:
|
||||
executable = shutil.which("soffice") or shutil.which("libreoffice")
|
||||
if executable:
|
||||
return executable
|
||||
if os.name != "nt":
|
||||
return None
|
||||
windows_roots: list[str] = []
|
||||
for variable in ("PROGRAMFILES", "ProgramW6432", "PROGRAMFILES(X86)"):
|
||||
value = os.environ.get(variable)
|
||||
if value and value not in windows_roots:
|
||||
windows_roots.append(value)
|
||||
for root in windows_roots:
|
||||
candidate = Path(root) / "LibreOffice" / "program" / "soffice.exe"
|
||||
if candidate.is_file():
|
||||
return str(candidate)
|
||||
return None
|
||||
|
||||
|
||||
def _safe_pdf_name(value: str) -> str:
|
||||
stem = Path(value.strip()).stem
|
||||
stem = re.sub(r"[^A-Za-z0-9ÄÖÜäöüß._ -]+", "-", stem).strip(" .-")
|
||||
|
||||
@@ -188,3 +188,22 @@ def test_generated_reminder_pdf_is_stored_audited_and_linked(tmp_path, monkeypat
|
||||
sha256="invalid",
|
||||
template="Mahnung.fodt",
|
||||
)
|
||||
|
||||
|
||||
def test_office_executable_prefers_path_lookup(monkeypatch) -> None:
|
||||
monkeypatch.setattr(document_module.shutil, "which", lambda name: "C:/tools/soffice.exe")
|
||||
|
||||
assert document_module._office_executable() == "C:/tools/soffice.exe"
|
||||
|
||||
|
||||
def test_office_executable_falls_back_to_windows_default_path(tmp_path, monkeypatch) -> None:
|
||||
libreoffice = tmp_path / "LibreOffice" / "program"
|
||||
libreoffice.mkdir(parents=True)
|
||||
executable = libreoffice / "soffice.exe"
|
||||
executable.write_text("", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(document_module.shutil, "which", lambda name: None)
|
||||
monkeypatch.setattr(document_module.os, "name", "nt")
|
||||
monkeypatch.setenv("PROGRAMFILES", str(tmp_path))
|
||||
|
||||
assert document_module._office_executable() == str(executable)
|
||||
|
||||
Reference in New Issue
Block a user