Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

A desktop application for annotating Deep Brain Stimulation (DBS) clinical programming sessions. Built for clinicians and researchers working with DBS systems (Medtronic Percept and others).

**Version:** 0.3.0-beta
**Version:** derived from `clinical_dbs_annotator.__version__`
**Author:** Lucia Poma (lucia.poma@wysscenter.ch)

## For End Users

**No installation required.** Download the pre-built executable and run it directly:

1. Go to the `dist/` folder
2. Run `ClinicalDBSAnnot_v0_3_testing.exe`
2. Run the `ClinicalDBSAnnotator_*.exe` artifact for your platform/version
3. That's it — the application opens immediately

The executable is self-contained and includes all dependencies.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ classifiers = [
"Operating System :: OS Independent",
]
dependencies = [
"pytz>=2025.2",
"tzdata>=2025.3",
"pandas>=2.1",
"python-docx>=1.1",
"docx2pdf>=0.1.8",
Expand Down
15 changes: 13 additions & 2 deletions scripts/build_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""

import argparse
import re
import shutil
import subprocess
import sys
Expand All @@ -18,9 +19,19 @@
SRC_DIR = PROJECT_ROOT / "src"

APP_NAME = "ClinicalDBSAnnot"
VERSION = "v0.3_testing"
PLATFORM = "macOS"

def _read_version() -> str:
init_path = SRC_DIR / "clinical_dbs_annotator" / "__init__.py"
text = init_path.read_text(encoding="utf-8")
m = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']\s*$', text, flags=re.MULTILINE)
if not m:
raise RuntimeError(f"Could not determine version from {init_path}")
return m.group(1)


VERSION = _read_version()


def update_macos_logo(png_path: Path) -> bool:
"""Update logoneutral.png from the provided file and regenerate logoneutral.icns."""
Expand Down Expand Up @@ -80,7 +91,7 @@ def build_macos_app(*, console: bool, onefile: bool):
f"--workpath={BUILD_DIR / 'pyinstaller'}",
f"--specpath={BUILD_DIR / 'pyinstaller'}",
f"--icon={icon_path}",
"--hidden-import=pytz",
"--hidden-import=tzdata",
"--hidden-import=pandas",
"--hidden-import=openpyxl",
"--hidden-import=xlrd",
Expand Down
23 changes: 14 additions & 9 deletions scripts/build_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,37 @@
"""

import argparse
import re
import subprocess
import sys
from pathlib import Path

# Get project root directory
PROJECT_ROOT = Path(__file__).parent.parent
DIST_DIR = PROJECT_ROOT / "dist"
BUILD_DIR = PROJECT_ROOT / "build"
ICONS_DIR = PROJECT_ROOT / "icons"
SRC_DIR = PROJECT_ROOT / "src"

APP_NAME = "ClinicalDBSAnnot"
VERSION = "v0.3_testing"
APP_NAME = "ClinicalDBSAnnotator"
PLATFORM = "Windows"

def _read_version() -> str:
init_path = SRC_DIR / "clinical_dbs_annotator" / "__init__.py"
text = init_path.read_text(encoding="utf-8")
m = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']\s*$', text, flags=re.MULTILINE)
if not m:
raise RuntimeError(f"Could not determine version from {init_path}")
return m.group(1)


VERSION = _read_version()


def build_windows_exe(*, console: bool, onefile: bool) -> bool:
"""Build Windows executable using PyInstaller."""
print(f"Building {APP_NAME} {VERSION} for Windows...")

name = f"{APP_NAME}_{PLATFORM}_{VERSION.replace('.', '_')}"
# Use run.py as entrypoint
entrypoint = PROJECT_ROOT / "run.py"
styles_dir = PROJECT_ROOT / "styles"
config_dir = SRC_DIR / "clinical_dbs_annotator" / "config"
Expand All @@ -40,10 +49,7 @@ def build_windows_exe(*, console: bool, onefile: bool) -> bool:
f"--distpath={DIST_DIR}",
f"--workpath={BUILD_DIR / 'pyinstaller'}",
f"--specpath={BUILD_DIR / 'pyinstaller'}",
# "--exclude-module=pythoncom",
# "--exclude-module=pywintypes",
# "--exclude-module=win32com",
"--hidden-import=pytz",
"--hidden-import=tzdata",
"--hidden-import=pandas",
"--hidden-import=openpyxl",
"--hidden-import=xlrd",
Expand Down Expand Up @@ -73,7 +79,6 @@ def build_windows_exe(*, console: bool, onefile: bool) -> bool:
]
)

# Run PyInstaller
try:
subprocess.run(cmd, check=True, cwd=PROJECT_ROOT)
print("\n✓ Build successful!")
Expand Down
49 changes: 24 additions & 25 deletions scripts/build_windows_nuitka.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""

import argparse
import re
import sys
from pathlib import Path

Expand All @@ -26,11 +27,28 @@
STYLES_DIR = PROJECT_ROOT / "styles"
CONFIG_DIR = SRC_DIR / "clinical_dbs_annotator" / "config"

# --- Build configuration ----------------------------------------------------
NAME = "ClinicalDBSAnnot"
NAME = "ClinicalDBSAnnotator"
ENTRYPOINT = PROJECT_ROOT / "run.py"

# Data files to include (src_path:dest_path)
def _read_version() -> str:
init_path = SRC_DIR / "clinical_dbs_annotator" / "__init__.py"
text = init_path.read_text(encoding="utf-8")
m = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']\s*$', text, flags=re.MULTILINE)
if not m:
raise RuntimeError(f"Could not determine version from {init_path}")
return m.group(1)


def _base_version(version: str) -> str:
m = re.search(r"(\d+\.\d+\.\d+)", version)
if not m:
raise RuntimeError(f"Could not extract base version from {version!r}")
return m.group(1)


VERSION = _read_version()
BASE_VERSION = _base_version(VERSION)

DATA_FILES = [
(ICONS_DIR / "logoneutral.ico", "icons"),
(ICONS_DIR / "logoneutral.png", "icons"),
Expand All @@ -40,9 +58,8 @@
(CONFIG_DIR / "session_scales_presets.json", "config"),
]

# Hidden imports (modules that are imported dynamically)
HIDDEN_IMPORTS = [
"pytz",
"tzdata",
"pandas",
"openpyxl",
"xlrd",
Expand All @@ -58,20 +75,16 @@
"PIL.ImageFont",
]

# Qt plugins to include
QT_PLUGINS = [
"platforms",
"imageformats",
]

# --- Main build function --------------------------------------------------
def build_nuitka(console: bool = False, onefile: bool = False) -> None:
"""Build the executable with Nuitka."""
# Ensure directories exist
DIST_DIR.mkdir(exist_ok=True)
BUILD_DIR.mkdir(exist_ok=True)

# Base command
cmd = [
sys.executable,
"-m",
Expand All @@ -82,68 +95,55 @@ def build_nuitka(console: bool = False, onefile: bool = False) -> None:
f"--output-filename={NAME}.exe",
]

# Console vs windowed
if console:
cmd.append("--console")
else:
cmd.append("--windows-disable-console")

# Onefile vs onedir
if onefile:
cmd.append("--onefile")
# Note: --remove-output is not needed, Nuitka handles cleanup

# Include data files
for src_path, dest_path in DATA_FILES:
if src_path.exists():
cmd.append(f"--include-data-file={src_path}={dest_path}")
else:
print(f"Warning: Data file not found: {src_path}")

# Hidden imports
for module in HIDDEN_IMPORTS:
cmd.append(f"--include-module={module}")

# Include Qt plugins
for plugin in QT_PLUGINS:
cmd.append(f"--include-qt-plugin={plugin}")

# Include PySide6 completely
cmd.append("--follow-imports")

# Optimization flags
cmd.extend([
"--enable-plugin=pyside6",
])

# Icon
icon_path = ICONS_DIR / "logoneutral.ico"
if icon_path.exists():
cmd.append(f"--windows-icon-from-ico={icon_path}")

# Windows metadata
cmd.extend([
"--windows-company-name=Brain Modulation Lab",
"--windows-product-name=Clinical DBS Annotator",
"--windows-file-version=0.3.0",
"--windows-product-version=0.3.0",
f"--windows-file-version={BASE_VERSION}",
f"--windows-product-version={BASE_VERSION}",
])

# Entry point
cmd.append(str(ENTRYPOINT))

# Print command for debugging
print("Nuitka command:")
print(" ".join(f'"{arg}"' if " " in str(arg) else str(arg) for arg in cmd))
print("\nBuilding... (this may take several minutes)")

# Execute
try:
import subprocess
subprocess.run(cmd, check=True, cwd=PROJECT_ROOT)
print("\n✅ Build completed successfully!")

# Show output location
if onefile:
exe_path = DIST_DIR / f"{NAME}.exe"
else:
Expand All @@ -162,7 +162,6 @@ def build_nuitka(console: bool = False, onefile: bool = False) -> None:
print("\n❌ Nuitka not found. Install it with: pip install nuitka")
sys.exit(1)

# -------------------------------------------------------------------------
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Build Windows executable with Nuitka")
parser.add_argument("--console", action="store_true",
Expand Down
7 changes: 4 additions & 3 deletions scripts/create_installer.nsi
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@
; Download from: https://nsis.sourceforge.io/

!define APP_NAME "Clinical DBS Annotator"
!define APP_VERSION "0.1.0"
; Derive version from the Python package (single source of truth).
!searchparse /file "..\src\clinical_dbs_annotator\__init__.py" '__version__ = "' APP_VERSION '"'
!define APP_PUBLISHER "BML"
!define APP_EXE "ClinicalDBSAnnot_v0_1.exe"
!define APP_EXE "ClinicalDBSAnnot_${APP_VERSION}.exe"
!define INSTALL_DIR "$PROGRAMFILES\${APP_PUBLISHER}\${APP_NAME}"

; Includes
!include "MUI2.nsh"

; General settings
Name "${APP_NAME}"
OutFile "..\dist\ClinicalDBSAnnot_Installer_v0.1.exe"
OutFile "..\dist\ClinicalDBSAnnot_Installer_${APP_VERSION}.exe"
InstallDir "${INSTALL_DIR}"
InstallDirRegKey HKCU "Software\${APP_PUBLISHER}\${APP_NAME}" "InstallDir"
RequestExecutionLevel admin
Expand Down
Loading