3 Commits

Author SHA1 Message Date
7f799a914d feat: add NSIS Windows installer to release workflow
Some checks failed
Build Release / create-release (push) Successful in 1s
Lint & Test / test (push) Failing after 15s
Build Release / build-linux (push) Successful in 1m21s
Build Release / build-docker (push) Failing after 9s
Build Release / build-windows (push) Failing after 1m37s
- installer.nsi: per-user install to AppData, Start Menu shortcuts,
  optional desktop shortcut and autostart, clean uninstall (preserves
  data/), Add/Remove Programs registration
- build-dist-windows.sh: runs makensis after ZIP if available
- release.yml: install nsis in CI, upload both ZIP and setup.exe
- Fix Docker registry login (sed -E for https:// stripping)
2026-03-22 03:35:34 +03:00
d5b5c255e8 feat: bundle tkinter into Windows portable build
Some checks failed
Build Release / create-release (push) Successful in 5s
Lint & Test / test (push) Failing after 21s
Build Release / build-linux (push) Successful in 1m7s
Build Release / build-docker (push) Failing after 4s
Build Release / build-windows (push) Successful in 1m50s
Download _tkinter.pyd, tkinter package, and Tcl/Tk DLLs from the
official Python nuget package and copy them into the embedded Python
directory. This enables the screen overlay visualization during
calibration in the portable build.
2026-03-22 03:32:02 +03:00
564e4c9c9c fix: accurate port banner and tkinter graceful fallback
Some checks failed
Build Release / build-linux (push) Successful in 1m24s
Build Release / create-release (push) Successful in 1s
Lint & Test / test (push) Failing after 13s
Build Release / build-windows (push) Successful in 1m35s
Build Release / build-docker (push) Failing after 9s
- Move startup banner into main.py so it shows the actual configured
  port instead of a hardcoded 8080 in the launcher scripts
- Wrap tkinter import in try/except so embedded Python (which lacks
  tkinter) logs a warning instead of crashing the overlay thread
2026-03-22 03:30:19 +03:00
6 changed files with 282 additions and 42 deletions

View File

@@ -63,37 +63,50 @@ jobs:
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends zip libportaudio2
sudo apt-get install -y --no-install-recommends zip libportaudio2 nsis
- name: Cross-build Windows distribution
run: |
chmod +x build-dist-windows.sh
./build-dist-windows.sh "${{ gitea.ref_name }}"
- name: Upload build artifact
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: LedGrab-${{ gitea.ref_name }}-win-x64
path: build/LedGrab-*.zip
path: |
build/LedGrab-*.zip
build/LedGrab-*-setup.exe
retention-days: 90
- name: Attach ZIP to release
- name: Attach assets to release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
TAG="${{ gitea.ref_name }}"
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
ZIP_FILE=$(ls build/LedGrab-*.zip | head -1)
ZIP_NAME=$(basename "$ZIP_FILE")
# Upload ZIP
ZIP_FILE=$(ls build/LedGrab-*.zip | head -1)
if [ -f "$ZIP_FILE" ]; then
curl -s -X POST \
"$BASE_URL/releases/$RELEASE_ID/assets?name=$ZIP_NAME" \
"$BASE_URL/releases/$RELEASE_ID/assets?name=$(basename "$ZIP_FILE")" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$ZIP_FILE"
echo "Uploaded: $(basename "$ZIP_FILE")"
fi
echo "Uploaded: $ZIP_NAME"
# Upload installer
SETUP_FILE=$(ls build/LedGrab-*-setup.exe 2>/dev/null | head -1)
if [ -f "$SETUP_FILE" ]; then
curl -s -X POST \
"$BASE_URL/releases/$RELEASE_ID/assets?name=$(basename "$SETUP_FILE")" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$SETUP_FILE"
echo "Uploaded: $(basename "$SETUP_FILE")"
fi
# ── Linux tarball ──────────────────────────────────────────
build-linux:
@@ -165,17 +178,19 @@ jobs:
run: |
TAG="${{ gitea.ref_name }}"
VERSION="${TAG#v}"
REGISTRY="${{ gitea.server_url }}/${{ gitea.repository }}"
# Lowercase the registry path (Docker requires it)
REGISTRY=$(echo "$REGISTRY" | tr '[:upper:]' '[:lower:]' | sed 's|https\?://||')
# Strip protocol and lowercase for Docker registry path
SERVER_HOST=$(echo "${{ gitea.server_url }}" | sed -E 's|https?://||')
REPO=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
REGISTRY="${SERVER_HOST}/${REPO}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT"
echo "server_host=$SERVER_HOST" >> "$GITHUB_OUTPUT"
- name: Login to Gitea Container Registry
run: |
echo "${{ secrets.GITEA_TOKEN }}" | docker login \
"$(echo '${{ gitea.server_url }}' | sed 's|https\?://||')" \
"${{ steps.meta.outputs.server_host }}" \
-u "${{ gitea.actor }}" --password-stdin
- name: Build Docker image

View File

@@ -85,6 +85,74 @@ if ! grep -q '\.\./app/src' "$PTH_FILE"; then
fi
echo " Patched $(basename "$PTH_FILE")"
# ── Bundle tkinter into embedded Python ───────────────────────
# Embedded Python doesn't include tkinter. We download it from the
# official Windows Python nuget package (same version) which contains
# the _tkinter.pyd, tkinter/ package, and Tcl/Tk DLLs.
echo "[3b/8] Bundling tkinter for screen overlay support..."
# Python minor version for nuget package (e.g., 3.11.9 -> 3.11)
PYTHON_MINOR="${PYTHON_VERSION%.*}"
# Download the full Python nuget package (contains all stdlib + DLLs)
NUGET_URL="https://www.nuget.org/api/v2/package/python/${PYTHON_VERSION}"
NUGET_PKG="$BUILD_DIR/python-nuget.zip"
if [ ! -f "$NUGET_PKG" ]; then
curl -sL "$NUGET_URL" -o "$NUGET_PKG"
fi
NUGET_DIR="$BUILD_DIR/python-nuget"
rm -rf "$NUGET_DIR"
mkdir -p "$NUGET_DIR"
unzip -qo "$NUGET_PKG" -d "$NUGET_DIR"
# Copy _tkinter.pyd (the C extension)
TKINTER_PYD=$(find "$NUGET_DIR" -name "_tkinter.pyd" | head -1)
if [ -n "$TKINTER_PYD" ]; then
cp "$TKINTER_PYD" "$PYTHON_DIR/"
echo " Copied _tkinter.pyd"
else
echo " WARNING: _tkinter.pyd not found in nuget package"
fi
# Copy tkinter Python package from the stdlib zip or Lib/
# The nuget package has Lib/tkinter/
TKINTER_PKG=$(find "$NUGET_DIR" -type d -name "tkinter" | head -1)
if [ -n "$TKINTER_PKG" ]; then
mkdir -p "$PYTHON_DIR/Lib"
cp -r "$TKINTER_PKG" "$PYTHON_DIR/Lib/tkinter"
echo " Copied tkinter/ package"
else
echo " WARNING: tkinter package not found in nuget package"
fi
# Copy Tcl/Tk DLLs (tcl86t.dll, tk86t.dll, etc.)
for dll in tcl86t.dll tk86t.dll; do
DLL_PATH=$(find "$NUGET_DIR" -name "$dll" | head -1)
if [ -n "$DLL_PATH" ]; then
cp "$DLL_PATH" "$PYTHON_DIR/"
echo " Copied $dll"
fi
done
# Copy Tcl/Tk data directories (tcl8.6, tk8.6)
for tcldir in tcl8.6 tk8.6; do
TCL_PATH=$(find "$NUGET_DIR" -type d -name "$tcldir" | head -1)
if [ -n "$TCL_PATH" ]; then
cp -r "$TCL_PATH" "$PYTHON_DIR/$tcldir"
echo " Copied $tcldir/"
fi
done
# Add Lib to ._pth so tkinter package is importable
if ! grep -q '^Lib$' "$PTH_FILE"; then
echo 'Lib' >> "$PTH_FILE"
fi
rm -rf "$NUGET_DIR"
echo " tkinter bundled successfully"
# ── Download pip and install into embedded Python ────────────
echo "[4/8] Installing pip into embedded Python..."
@@ -243,22 +311,11 @@ cd /d "%~dp0"
set PYTHONPATH=%~dp0app\src
set WLED_CONFIG_PATH=%~dp0app\config\default_config.yaml
:: Read port from env var or use default
if "%WLED_SERVER__PORT%"=="" set WLED_SERVER__PORT=8080
:: Create data directory if missing
if not exist "%~dp0data" mkdir "%~dp0data"
if not exist "%~dp0logs" mkdir "%~dp0logs"
echo.
echo =============================================
echo LedGrab v${VERSION_CLEAN}
echo Open http://localhost:%WLED_SERVER__PORT% in your browser
echo =============================================
echo.
:: Start the server — uses config from WLED_CONFIG_PATH, port from config or env
start "" /b cmd /c "timeout /t 2 /nobreak >nul && start http://localhost:%WLED_SERVER__PORT%"
:: Start the server — reads port from config, prints its own banner
"%~dp0python\python.exe" -m wled_controller.main
pause
@@ -333,8 +390,29 @@ rm -f "$ZIP_PATH"
(cd "$BUILD_DIR" && zip -rq "$ZIP_NAME" "$DIST_NAME")
ZIP_SIZE=$(du -h "$ZIP_PATH" | cut -f1)
# ── Build NSIS installer (if makensis is available) ──────────
SETUP_NAME="LedGrab-v${VERSION_CLEAN}-win-x64-setup.exe"
SETUP_PATH="$BUILD_DIR/$SETUP_NAME"
if command -v makensis &>/dev/null; then
echo "[9/8] Building NSIS installer..."
makensis -DVERSION="${VERSION_CLEAN}" "$SCRIPT_DIR/installer.nsi" >/dev/null 2>&1
if [ -f "$SETUP_PATH" ]; then
SETUP_SIZE=$(du -h "$SETUP_PATH" | cut -f1)
echo " Installer: $SETUP_PATH ($SETUP_SIZE)"
else
echo " WARNING: makensis ran but installer not found at $SETUP_PATH"
fi
else
echo "[9/8] Skipping installer (makensis not found — install nsis to enable)"
fi
echo ""
echo "=== Build complete ==="
echo " Archive: $ZIP_PATH"
echo " Size: $ZIP_SIZE"
echo " ZIP: $ZIP_PATH ($ZIP_SIZE)"
if [ -f "$SETUP_PATH" ]; then
echo " Installer: $SETUP_PATH ($SETUP_SIZE)"
fi
echo ""

View File

@@ -101,18 +101,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
export PYTHONPATH="$SCRIPT_DIR/app/src"
export WLED_CONFIG_PATH="$SCRIPT_DIR/app/config/default_config.yaml"
# Read port from env var or use default
PORT="${WLED_SERVER__PORT:-8080}"
mkdir -p "$SCRIPT_DIR/data" "$SCRIPT_DIR/logs"
echo ""
echo " ============================================="
echo " LedGrab vVERSION_PLACEHOLDER"
echo " Open http://localhost:$PORT in your browser"
echo " ============================================="
echo ""
source "$SCRIPT_DIR/venv/bin/activate"
exec python -m wled_controller.main
LAUNCHER

148
installer.nsi Normal file
View File

@@ -0,0 +1,148 @@
; LedGrab NSIS Installer Script
; Cross-compilable on Linux: apt install nsis && makensis installer.nsi
;
; Expects the portable build to already exist at build/LedGrab/
; (run build-dist-windows.sh first)
!include "MUI2.nsh"
!include "FileFunc.nsh"
; ── Metadata ────────────────────────────────────────────────
!define APPNAME "LedGrab"
!define DESCRIPTION "Ambient lighting system — captures screen content and drives LED strips in real time"
!define VERSIONMAJOR 0
!define VERSIONMINOR 1
!define VERSIONBUILD 0
; Set from command line: makensis -DVERSION=0.1.0 installer.nsi
!ifndef VERSION
!define VERSION "${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}"
!endif
Name "${APPNAME} v${VERSION}"
OutFile "build\${APPNAME}-v${VERSION}-win-x64-setup.exe"
InstallDir "$LOCALAPPDATA\${APPNAME}"
InstallDirRegKey HKCU "Software\${APPNAME}" "InstallDir"
RequestExecutionLevel user
SetCompressor /SOLID lzma
; ── Modern UI Configuration ─────────────────────────────────
!define MUI_ABORTWARNING
!define MUI_ICON "server\src\wled_controller\static\icon-192.png"
!define MUI_UNICON "server\src\wled_controller\static\icon-192.png"
; ── Pages ───────────────────────────────────────────────────
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
; ── Installer Sections ──────────────────────────────────────
Section "!${APPNAME} (required)" SecCore
SectionIn RO
SetOutPath "$INSTDIR"
; Copy the entire portable build
File /r "build\LedGrab\python"
File /r "build\LedGrab\app"
File "build\LedGrab\LedGrab.bat"
; Create data and logs directories
CreateDirectory "$INSTDIR\data"
CreateDirectory "$INSTDIR\logs"
; Create uninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
; Start Menu shortcuts
CreateDirectory "$SMPROGRAMS\${APPNAME}"
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" "$INSTDIR\LedGrab.bat" \
"" "" "" SW_SHOWMINIMIZED "" "${DESCRIPTION}"
CreateShortcut "$SMPROGRAMS\${APPNAME}\Uninstall.lnk" "$INSTDIR\uninstall.exe"
; Registry: install location + Add/Remove Programs entry
WriteRegStr HKCU "Software\${APPNAME}" "InstallDir" "$INSTDIR"
WriteRegStr HKCU "Software\${APPNAME}" "Version" "${VERSION}"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"DisplayName" "${APPNAME}"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"DisplayVersion" "${VERSION}"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"UninstallString" '"$INSTDIR\uninstall.exe"'
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"InstallLocation" "$INSTDIR"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"Publisher" "Alexei Dolgolyov"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"URLInfoAbout" "https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed"
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"NoModify" 1
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"NoRepair" 1
; Calculate installed size for Add/Remove Programs
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"EstimatedSize" "$0"
SectionEnd
Section "Desktop shortcut" SecDesktop
CreateShortcut "$DESKTOP\${APPNAME}.lnk" "$INSTDIR\LedGrab.bat" \
"" "" "" SW_SHOWMINIMIZED "" "${DESCRIPTION}"
SectionEnd
Section "Start with Windows" SecAutostart
CreateShortcut "$SMSTARTUP\${APPNAME}.lnk" "$INSTDIR\LedGrab.bat" \
"" "" "" SW_SHOWMINIMIZED "" "${DESCRIPTION}"
SectionEnd
; ── Section Descriptions ────────────────────────────────────
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${SecCore} \
"Install ${APPNAME} server and all required files."
!insertmacro MUI_DESCRIPTION_TEXT ${SecDesktop} \
"Create a shortcut on your desktop."
!insertmacro MUI_DESCRIPTION_TEXT ${SecAutostart} \
"Start ${APPNAME} automatically when you log in."
!insertmacro MUI_FUNCTION_DESCRIPTION_END
; ── Uninstaller ─────────────────────────────────────────────
Section "Uninstall"
; Remove shortcuts
Delete "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk"
Delete "$SMPROGRAMS\${APPNAME}\Uninstall.lnk"
RMDir "$SMPROGRAMS\${APPNAME}"
Delete "$DESKTOP\${APPNAME}.lnk"
Delete "$SMSTARTUP\${APPNAME}.lnk"
; Remove application files (but NOT data/ — preserve user config)
RMDir /r "$INSTDIR\python"
RMDir /r "$INSTDIR\app"
Delete "$INSTDIR\LedGrab.bat"
Delete "$INSTDIR\uninstall.exe"
; Remove logs (but keep data/)
RMDir /r "$INSTDIR\logs"
; Try to remove install dir (only succeeds if empty — data/ may remain)
RMDir "$INSTDIR"
; Remove registry keys
DeleteRegKey HKCU "Software\${APPNAME}"
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}"
SectionEnd

View File

@@ -278,7 +278,12 @@ class OverlayManager:
def _start_tk_thread(self) -> None:
def _run():
import tkinter as tk # lazy import — tkinter unavailable in headless CI
try:
import tkinter as tk # lazy import — tkinter unavailable in embedded Python / headless CI
except ImportError:
logger.warning("tkinter not available — screen overlay disabled")
self._tk_ready.set()
return
try:
self._tk_root = tk.Tk()

View File

@@ -98,6 +98,10 @@ async def lifespan(app: FastAPI):
logger.info(f"Starting LED Grab v{__version__}")
logger.info(f"Python version: {sys.version}")
logger.info(f"Server listening on {config.server.host}:{config.server.port}")
print(f"\n =============================================")
print(f" LED Grab v{__version__}")
print(f" Open http://localhost:{config.server.port} in your browser")
print(f" =============================================\n")
# Validate authentication configuration
if not config.auth.api_keys: