Browse Source

Windows Portable Launcher

maziggy 3 months ago
parent
commit
91a4e2a0f5
3 changed files with 550 additions and 1 deletions
  1. 10 0
      CHANGELOG.md
  2. 20 1
      README.md
  3. 520 0
      start_bambuddy.bat

+ 10 - 0
CHANGELOG.md

@@ -5,6 +5,16 @@ All notable changes to Bambuddy will be documented in this file.
 
 
 ## [0.1.8b] - Not released
 ## [0.1.8b] - Not released
 
 
+### Added
+- **Windows Portable Launcher** (contributed by nmori):
+  - New `start_bambuddy.bat` for Windows users - double-click to run, no installation required
+  - Automatically downloads Python 3.13 and Node.js 22 on first run (portable, no system changes)
+  - Everything stored in `.portable\` folder for easy cleanup
+  - Commands: `start_bambuddy.bat` (launch), `start_bambuddy.bat update` (update deps), `start_bambuddy.bat reset` (clean start)
+  - Custom port via `set PORT=9000 & start_bambuddy.bat`
+  - Verifies all downloads with SHA256 checksums for security
+  - Supports both x64 and ARM64 Windows systems
+
 ## [0.1.7] - 2026-02-03
 ## [0.1.7] - 2026-02-03
 
 
 ### Security
 ### Security

+ 20 - 1
README.md

@@ -442,7 +442,26 @@ services:
 
 
 </details>
 </details>
 
 
-#### Manual Installation
+#### Windows (Portable Launcher)
+
+The easiest way to run Bambuddy on Windows - no installation required:
+
+```batch
+git clone https://github.com/maziggy/bambuddy.git
+cd bambuddy
+start_bambuddy.bat
+```
+
+Double-click `start_bambuddy.bat` and it will:
+- Download Python and Node.js automatically (portable, no system changes)
+- Install dependencies and build the frontend
+- Open your browser to http://localhost:8000
+
+Everything is stored in the `.portable\` folder. Use `start_bambuddy.bat reset` to clean up.
+
+> **Custom port:** `set PORT=9000 & start_bambuddy.bat`
+
+#### Manual Installation (Linux/macOS)
 
 
 ```bash
 ```bash
 # Clone and setup
 # Clone and setup

+ 520 - 0
start_bambuddy.bat

@@ -0,0 +1,520 @@
+@echo off
+
+chcp 65001 >nul 2>&1
+setlocal enabledelayedexpansion
+
+title Bambuddy
+
+REM ============================================
+REM  Bambuddy Portable Launcher for Windows
+REM
+REM  Double-click to start. First run downloads
+REM  Python and Node.js automatically (portable,
+REM  no system changes). Everything is stored in
+REM  the .portable\ folder.
+REM
+REM  Usage:
+REM    start_bambuddy.bat            Launch
+REM    start_bambuddy.bat update     Update deps & rebuild frontend
+REM    start_bambuddy.bat reset      Clean all & fresh start
+REM    set PORT=9000 & start_bambuddy.bat   Change port
+REM ============================================
+
+set "ROOT=%~dp0"
+if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%"
+
+set "PORTABLE=%ROOT%\.portable"
+set "PYTHON_DIR=%PORTABLE%\python"
+set "NODE_DIR=%PORTABLE%\node"
+set "FFMPEG_DIR=%PORTABLE%\ffmpeg"
+REM NOTE: Python version is intentionally pinned to a specific portable build.
+REM       If you upgrade the bundled Python runtime, update PYTHON_VER here
+REM       and make sure it matches the version used in download/installation logic.
+if not defined PYTHON_VER set "PYTHON_VER=3.13.1"
+REM Default Node.js version for the portable runtime. Override by setting NODE_VER before running this script.
+if not defined NODE_VER set "NODE_VER=22.12.0"
+REM NOTE: FFmpeg is not downloaded automatically.
+REM       Install from the official site and add it to PATH:
+REM       https://ffmpeg.org/download.html
+
+REM Pinned SHA256 hashes for downloads (update when bumping versions)
+set "GET_PIP_SHA256=dffc3658baada4ef383f31c3c672d4e5e306a6e376cee8bee5dbdf1385525104"
+set "PYTHON_ZIP_HASH_AMD64=7b7923ff0183a8b8fca90f6047184b419b108cb437f75fc1c002f9d2f8bcec16"
+set "PYTHON_ZIP_HASH_ARM64=ae8561bf958f77c68cb6c44ced983e5267fe965a7e4168f41ec2291350b81d55"
+set "NODE_ZIP_HASH_X64=2b8f2256382f97ad51e29ff71f702961af466c4616393f767455501e6aece9b8"
+set "NODE_ZIP_HASH_ARM64=17401720af48976e3f67c41e8968a135fb49ca1f88103a92e0e8c70605763854"
+
+REM Detect system architecture (amd64 or arm64)
+set "PYTHON_ARCH=amd64"
+set "NODE_ARCH=x64"
+if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
+    set "PYTHON_ARCH=arm64"
+    set "NODE_ARCH=arm64"
+)
+if defined PROCESSOR_ARCHITEW6432 (
+    if /I "%PROCESSOR_ARCHITEW6432%"=="ARM64" (
+        set "PYTHON_ARCH=arm64"
+        set "NODE_ARCH=arm64"
+    )
+)
+
+set "PYTHON_ZIP_HASH_EXPECTED=%PYTHON_ZIP_HASH_AMD64%"
+if /I "%PYTHON_ARCH%"=="arm64" set "PYTHON_ZIP_HASH_EXPECTED=%PYTHON_ZIP_HASH_ARM64%"
+
+set "NODE_ZIP_HASH_EXPECTED=%NODE_ZIP_HASH_X64%"
+if /I "%NODE_ARCH%"=="arm64" set "NODE_ZIP_HASH_EXPECTED=%NODE_ZIP_HASH_ARM64%"
+
+if not defined PORT set "PORT=8000"
+
+REM Validate PORT is a number in the range 1-65535
+echo(!PORT!| findstr /R "^[0-9][0-9]*$" >nul
+if errorlevel 1 (
+    echo Invalid PORT value "%PORT%". PORT must be an integer between 1 and 65535.
+    exit /b 1
+)
+
+if %PORT% LSS 1 (
+    echo Invalid PORT value "%PORT%". PORT must be between 1 and 65535.
+    exit /b 1
+)
+if %PORT% GTR 65535 (
+    echo Invalid PORT value "%PORT%". PORT must be between 1 and 65535.
+    exit /b 1
+)
+
+REM ---- Handle arguments ----
+if /i "%~1"=="reset" (
+    echo Cleaning up portable environment...
+    call :safe_rmdir "%PORTABLE%" ".portable"
+    if errorlevel 1 exit /b 1
+    call :safe_rmdir "%ROOT%\static" "static"
+    if errorlevel 1 exit /b 1
+    echo Done. Run again without arguments to set up fresh.
+    pause
+    exit /b 0
+)
+
+if /i "%~1"=="update" (
+    echo Forcing dependency update and frontend rebuild...
+    if exist "%PORTABLE%\.deps-installed" del "%PORTABLE%\.deps-installed"
+    call :safe_rmdir "%ROOT%\static" "static"
+    if errorlevel 1 exit /b 1
+)
+
+REM ---- Check prerequisites ----
+where curl >nul 2>&1
+if errorlevel 1 (
+    echo.
+    echo [ERROR] curl.exe is not available.
+    echo         Windows 10 version 1803 or later is required.
+    echo.
+    pause
+    exit /b 1
+)
+where tar >nul 2>&1
+if errorlevel 1 (
+    echo.
+    echo [ERROR] tar.exe is not available.
+    echo         Windows 10 version 1803 or later is required.
+    echo.
+    pause
+    exit /b 1
+)
+
+REM ---- Verify project structure ----
+if not exist "%ROOT%\backend\app\main.py" (
+    echo.
+    echo [ERROR] backend\app\main.py not found.
+    echo         This script must be in the Bambuddy project root.
+    echo.
+    pause
+    exit /b 1
+)
+
+echo.
+echo  ____                  _               _     _
+echo ^| __ )  __ _ _ __ ___ ^| ^|__  _   _  __^| ^| __^| ^|_   _
+echo ^|  _ \ / _` ^| '_ ` _ \^| '_ \^| ^| ^| ^|/ _` ^|/ _` ^| ^| ^| ^|
+echo ^| ^|_) ^| (_^| ^| ^| ^| ^| ^| ^| ^|_) ^| ^|_^| ^| (_^| ^| (_^| ^| ^|_^| ^|
+echo ^|____/ \__,_^|_^| ^|_^| ^|_^|_.__/ \__,_^|\__,_^|\__,_^|\__, ^|
+echo                                                ^|___/
+echo.
+
+REM ============================================
+REM  Step 1: Setup Portable Python
+REM ============================================
+if exist "%PYTHON_DIR%\python.exe" (
+    echo [OK] Python %PYTHON_VER% found.
+    goto :python_ready
+)
+
+echo [1/6] Downloading Python %PYTHON_VER% (portable)...
+
+if not exist "%PORTABLE%" mkdir "%PORTABLE%"
+if not exist "%PYTHON_DIR%" mkdir "%PYTHON_DIR%"
+
+curl -L --fail --show-error --progress-bar -o "%PORTABLE%\python.zip" ^
+    "https://www.python.org/ftp/python/%PYTHON_VER%/python-%PYTHON_VER%-embed-%PYTHON_ARCH%.zip"
+if errorlevel 1 (
+    echo [ERROR] Failed to download Python.
+    pause
+    exit /b 1
+)
+call :verify_sha256 "%PORTABLE%\python.zip" "%PYTHON_ZIP_HASH_EXPECTED%" "Python"
+if errorlevel 1 (
+    echo [ERROR] Failed to download Python archive.
+    pause
+    exit /b 1
+)
+
+REM Download official SHA256 checksum for the Python archive
+curl -L --progress-bar -o "%PORTABLE%\python.zip.sha256" ^
+    "https://www.python.org/ftp/python/%PYTHON_VER%/python-%PYTHON_VER%-embed-amd64.zip.sha256"
+if errorlevel 1 (
+    echo [ERROR] Failed to download Python checksum file.
+    del "%PORTABLE%\python.zip" >nul 2>&1
+    pause
+    exit /b 1
+)
+
+REM Compute SHA256 hash of the downloaded archive
+set "PYTHON_ZIP_HASH="
+for /f "tokens=1 usebackq" %%H in (`
+    certutil -hashfile "%PORTABLE%\python.zip" SHA256 ^| findstr /R /I "^[0-9A-F][0-9A-F]"
+`) do (
+    set "PYTHON_ZIP_HASH=%%H"
+    goto :python_hash_done
+)
+
+:python_hash_done
+if not defined PYTHON_ZIP_HASH (
+    echo [ERROR] Failed to compute SHA256 hash for Python archive.
+    del "%PORTABLE%\python.zip" >nul 2>&1
+    del "%PORTABLE%\python.zip.sha256" >nul 2>&1
+    pause
+    exit /b 1
+)
+
+REM Read expected SHA256 hash from the checksum file
+set "PYTHON_ZIP_HASH_EXPECTED="
+for /f "tokens=1" %%H in ('type "%PORTABLE%\python.zip.sha256"') do (
+    set "PYTHON_ZIP_HASH_EXPECTED=%%H"
+    goto :python_expected_hash_done
+)
+
+:python_expected_hash_done
+if not defined PYTHON_ZIP_HASH_EXPECTED (
+    echo [ERROR] Failed to read expected SHA256 hash for Python archive.
+    del "%PORTABLE%\python.zip" >nul 2>&1
+    del "%PORTABLE%\python.zip.sha256" >nul 2>&1
+    pause
+    exit /b 1
+)
+
+REM Compare actual and expected hashes (case-insensitive)
+if /I not "%PYTHON_ZIP_HASH%"=="%PYTHON_ZIP_HASH_EXPECTED%" (
+    echo [ERROR] SHA256 checksum verification for Python archive failed.
+    echo [INFO] Expected: %PYTHON_ZIP_HASH_EXPECTED%
+    echo [INFO] Actual:   %PYTHON_ZIP_HASH%
+    del "%PORTABLE%\python.zip" >nul 2>&1
+    del "%PORTABLE%\python.zip.sha256" >nul 2>&1
+    pause
+    exit /b 1
+)
+
+del "%PORTABLE%\python.zip.sha256" >nul 2>&1
+echo Extracting Python...
+tar -xf "%PORTABLE%\python.zip" -C "%PYTHON_DIR%"
+if errorlevel 1 (
+    echo [ERROR] Failed to extract Python archive.
+    del "%PORTABLE%\python.zip" >nul 2>&1
+    pause
+    exit /b 1
+)
+del "%PORTABLE%\python.zip"
+if not exist "%PYTHON_DIR%\python.exe" (
+    echo [ERROR] Python executable not found after extraction.
+    pause
+    exit /b 1
+)
+
+REM Enable site-packages by rewriting the ._pth file
+REM Derive python tag (e.g., 3.13.x -> 313) from %PYTHON_VER%
+for /f "tokens=1,2 delims=." %%A in ("%PYTHON_VER%") do (
+    set "PY_MAJOR=%%A"
+    set "PY_MINOR=%%B"
+)
+set "PYTHON_TAG=%PY_MAJOR%%PY_MINOR%"
+(
+    echo python!PYTHON_TAG!.zip
+    echo .
+    echo import site
+) > "%PYTHON_DIR%\python!PYTHON_TAG!._pth"
+
+REM ============================================
+REM  Step 2: Install pip
+REM ============================================
+echo.
+echo [2/6] Installing pip...
+
+curl -L --fail -sS -o "%PORTABLE%\get-pip.py" "https://bootstrap.pypa.io/get-pip.py"
+if errorlevel 1 (
+    echo [ERROR] Failed to download get-pip.py.
+    pause
+    exit /b 1
+)
+call :verify_sha256 "%PORTABLE%\get-pip.py" "%GET_PIP_SHA256%" "get-pip.py"
+if errorlevel 1 (
+    del "%PORTABLE%\get-pip.py" >nul 2>&1
+    pause
+    exit /b 1
+)
+
+"%PYTHON_DIR%\python.exe" "%PORTABLE%\get-pip.py" --no-warn-script-location -q
+if errorlevel 1 (
+    echo [ERROR] Failed to install pip.
+    pause
+    exit /b 1
+)
+del "%PORTABLE%\get-pip.py"
+
+echo [OK] Python %PYTHON_VER% ready.
+
+:python_ready
+
+REM ============================================
+REM  Step 2.5: Create Virtual Environment (best effort)
+REM ============================================
+set "VENV_DIR=%PORTABLE%\venv"
+set "PYTHON_EXE=%PYTHON_DIR%\python.exe"
+if not exist "%VENV_DIR%\Scripts\python.exe" (
+    echo.
+    echo Creating virtual environment [optional]...
+    "%PYTHON_DIR%\python.exe" -m venv "%VENV_DIR%"
+    if errorlevel 1 (
+        echo [WARN] Failed to create virtual environment. Continuing without venv.
+    )
+)
+if exist "%VENV_DIR%\Scripts\python.exe" (
+    set "PYTHON_EXE=%VENV_DIR%\Scripts\python.exe"
+)
+
+REM ============================================
+REM  Step 3: Install Python Dependencies
+REM ============================================
+if exist "%PORTABLE%\.deps-installed" (
+    echo [OK] Python packages found.
+    goto :deps_ready
+)
+
+echo.
+echo [3/6] Installing Python packages (this may take a few minutes)...
+if exist "%ROOT%\requirements.lock" (
+    "%PYTHON_EXE%" -m pip install -r "%ROOT%\requirements.lock" --require-hashes --no-warn-script-location -q
+) else (
+    echo [WARN] requirements.lock not found. Falling back to requirements.txt - no hash enforcement.
+    "%PYTHON_EXE%" -m pip install -r "%ROOT%\requirements.txt" --no-warn-script-location -q
+)
+if errorlevel 1 (
+    echo [ERROR] Failed to install Python packages.
+    pause
+    exit /b 1
+)
+
+REM Create marker file
+echo %date% %time% > "%PORTABLE%\.deps-installed"
+echo [OK] Packages installed.
+
+:deps_ready
+
+REM ============================================
+REM  Step 4-6: Build Frontend (if needed)
+REM ============================================
+if exist "%ROOT%\static\index.html" (
+    echo [OK] Frontend found.
+    goto :frontend_ready
+)
+
+REM ---- Download Node.js if needed ----
+if exist "%NODE_DIR%\node.exe" goto :node_ready
+
+echo.
+echo [4/6] Downloading Node.js %NODE_VER% (portable)...
+
+curl -L --fail --show-error --progress-bar -o "%PORTABLE%\node.zip" ^
+    "https://nodejs.org/dist/v%NODE_VER%/node-v%NODE_VER%-win-%NODE_ARCH%.zip"
+if errorlevel 1 (
+    echo [ERROR] Failed to download Node.js.
+    pause
+    exit /b 1
+)
+call :verify_sha256 "%PORTABLE%\node.zip" "%NODE_ZIP_HASH_EXPECTED%" "Node.js"
+if errorlevel 1 (
+    del "%PORTABLE%\node.zip" >nul 2>&1
+    pause
+    exit /b 1
+)
+
+echo Extracting Node.js...
+tar -xf "%PORTABLE%\node.zip" -C "%PORTABLE%"
+if errorlevel 1 (
+    echo [ERROR] Failed to extract Node.js archive.
+    del "%PORTABLE%\node.zip" >nul 2>&1
+    pause
+    exit /b 1
+)
+if exist "%PORTABLE%\node-v%NODE_VER%-win-%NODE_ARCH%" (
+    ren "%PORTABLE%\node-v%NODE_VER%-win-%NODE_ARCH%" node
+)
+del "%PORTABLE%\node.zip"
+echo [OK] Node.js %NODE_VER% ready.
+
+:node_ready
+
+REM ---- Build frontend ----
+echo.
+echo [5/6] Building frontend (this may take a while)...
+
+set "PATH=%NODE_DIR%;%PATH%"
+
+pushd "%ROOT%\frontend"
+
+if exist "%ROOT%\frontend\package-lock.json" (
+    call "%NODE_DIR%\npm.cmd" ci
+) else (
+    call "%NODE_DIR%\npm.cmd" install
+)
+if errorlevel 1 (
+    echo [ERROR] npm install failed.
+    popd
+    pause
+    exit /b 1
+)
+
+call "%NODE_DIR%\npm.cmd" run build
+if errorlevel 1 (
+    echo [ERROR] Frontend build failed.
+    popd
+    pause
+    exit /b 1
+)
+
+popd
+if not exist "%ROOT%\frontend\static\index.html" (
+    echo [ERROR] Frontend build did not produce static\index.html.
+    echo        Expected: "%ROOT%\frontend\static\index.html"
+    pause
+    exit /b 1
+)
+if not exist "%ROOT%\static\index.html" (
+    echo [ERROR] Frontend build did not produce static\index.html.
+    echo        Expected: "%ROOT%\static\index.html"
+    pause
+    exit /b 1
+)
+echo [OK] Frontend built.
+
+:frontend_ready
+
+REM ============================================
+REM  Step 6: Setup Portable FFmpeg (if needed)
+REM ============================================
+where ffmpeg >nul 2>&1
+if not errorlevel 1 (
+    echo [OK] FFmpeg found in system PATH.
+    goto :ffmpeg_ready
+)
+
+if exist "%FFMPEG_DIR%\bin\ffmpeg.exe" (
+    echo [OK] FFmpeg found.
+    goto :ffmpeg_ready
+)
+
+echo.
+echo [6/6] FFmpeg not found.
+echo [INFO] Install FFmpeg from the official site and add it to PATH:
+echo        https://ffmpeg.org/download.html
+echo [INFO] Timelapse features will be unavailable until FFmpeg is installed.
+
+:ffmpeg_ready
+
+REM ============================================
+REM  Launch Bambuddy
+REM ============================================
+echo.
+echo ================================================
+echo   Bambuddy is starting on port %PORT%
+echo   Open: http://localhost:%PORT%
+echo.
+echo   Press Ctrl+C to stop
+echo ================================================
+echo.
+
+REM Set PYTHONPATH so "backend.app.main" module is found
+set "PYTHONPATH=%ROOT%"
+
+REM Add portable FFmpeg to PATH if available
+if exist "%FFMPEG_DIR%\bin\ffmpeg.exe" set "PATH=%FFMPEG_DIR%\bin;%PATH%"
+
+REM Open browser after server is ready (poll localhost)
+start /b cmd /c "for /l %%i in (1,1,30) do (curl -s -f -o nul http://localhost:%PORT% && (start http://localhost:%PORT% & exit /b 0) & timeout /t 1 /nobreak >nul)"
+
+REM Launch the application
+"%PYTHON_EXE%" -m uvicorn backend.app.main:app --host 0.0.0.0 --port %PORT% --loop asyncio
+
+echo.
+echo Bambuddy has stopped.
+pause
+
+endlocal
+goto :eof
+
+
+REM ============================================
+REM  Helpers
+REM ============================================
+:safe_rmdir
+set "TARGET=%~1"
+set "LABEL=%~2"
+if "%TARGET%"=="" (
+    echo [ERROR] %LABEL% path is empty. Aborting.
+    exit /b 1
+)
+if /I "%TARGET%"=="\" (
+    echo [ERROR] %LABEL% path resolved to root. Aborting.
+    exit /b 1
+)
+if not exist "%TARGET%" exit /b 0
+echo Deleting "%TARGET%"
+rmdir /s /q "%TARGET%"
+if errorlevel 1 (
+    echo [ERROR] Failed to delete "%TARGET%".
+    exit /b 1
+)
+exit /b 0
+
+:verify_sha256
+set "FILE=%~1"
+set "EXPECTED=%~2"
+set "LABEL=%~3"
+if "%EXPECTED%"=="" (
+    echo [ERROR] %LABEL% checksum not found.
+    exit /b 1
+)
+set "ACTUAL="
+for /f "tokens=1" %%H in ('certutil -hashfile "%FILE%" SHA256 ^| findstr /R /I "^[0-9A-F][0-9A-F]"') do (
+    set "ACTUAL=%%H"
+    goto :hash_done
+)
+:hash_done
+if not defined ACTUAL (
+    echo [ERROR] Failed to compute SHA256 for %LABEL%.
+    exit /b 1
+)
+if /I not "%ACTUAL%"=="%EXPECTED%" (
+    echo [ERROR] SHA256 verification failed for %LABEL%.
+    echo [INFO] Expected: %EXPECTED%
+    echo [INFO] Actual:   %ACTUAL%
+    exit /b 1
+)
+exit /b 0