start_bambuddy.bat 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. @echo off
  2. chcp 65001 >nul 2>&1
  3. setlocal enabledelayedexpansion
  4. title Bambuddy
  5. REM ============================================
  6. REM Bambuddy Portable Launcher for Windows
  7. REM
  8. REM Double-click to start. First run downloads
  9. REM Python and Node.js automatically (portable,
  10. REM no system changes). Everything is stored in
  11. REM the .portable\ folder.
  12. REM
  13. REM Usage:
  14. REM start_bambuddy.bat Launch
  15. REM start_bambuddy.bat update Update deps & rebuild frontend
  16. REM start_bambuddy.bat reset Clean all & fresh start
  17. REM set PORT=9000 & start_bambuddy.bat Change port
  18. REM ============================================
  19. REM Resolve ROOT based on the script location (more reliable than %CD%).
  20. set "SCRIPT_DIR=%~dp0"
  21. if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
  22. for %%I in ("%SCRIPT_DIR%") do set "SCRIPT_DIR_NAME=%%~nxI"
  23. if /I "%SCRIPT_DIR_NAME%"=="install" (
  24. set "ROOT=%SCRIPT_DIR%\.."
  25. ) else (
  26. set "ROOT=%SCRIPT_DIR%"
  27. )
  28. if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%"
  29. cd /d "%ROOT%"
  30. set "PORTABLE=%ROOT%\.portable"
  31. set "PYTHON_DIR=%PORTABLE%\python"
  32. set "NODE_DIR=%PORTABLE%\node"
  33. set "FFMPEG_DIR=%PORTABLE%\ffmpeg"
  34. REM NOTE: Python version is intentionally pinned to a specific portable build.
  35. REM If you upgrade the bundled Python runtime, update PYTHON_VER here
  36. REM and make sure it matches the version used in download/installation logic.
  37. if not defined PYTHON_VER set "PYTHON_VER=3.13.1"
  38. REM Default Node.js version for the portable runtime. Override by setting NODE_VER before running this script.
  39. if not defined NODE_VER set "NODE_VER=22.12.0"
  40. REM NOTE: FFmpeg is not downloaded automatically.
  41. REM Install from the official site and add it to PATH:
  42. REM https://ffmpeg.org/download.html
  43. REM Pinned SHA256 hashes for downloads (update when bumping versions)
  44. set "GET_PIP_SHA256=dffc3658baada4ef383f31c3c672d4e5e306a6e376cee8bee5dbdf1385525104"
  45. set "PYTHON_ZIP_HASH_AMD64=7b7923ff0183a8b8fca90f6047184b419b108cb437f75fc1c002f9d2f8bcec16"
  46. set "PYTHON_ZIP_HASH_ARM64=ae8561bf958f77c68cb6c44ced983e5267fe965a7e4168f41ec2291350b81d55"
  47. set "NODE_ZIP_HASH_X64=2b8f2256382f97ad51e29ff71f702961af466c4616393f767455501e6aece9b8"
  48. set "NODE_ZIP_HASH_ARM64=17401720af48976e3f67c41e8968a135fb49ca1f88103a92e0e8c70605763854"
  49. REM Detect system architecture (amd64 or arm64)
  50. set "PYTHON_ARCH=amd64"
  51. set "NODE_ARCH=x64"
  52. if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
  53. set "PYTHON_ARCH=arm64"
  54. set "NODE_ARCH=arm64"
  55. )
  56. if defined PROCESSOR_ARCHITEW6432 (
  57. if /I "%PROCESSOR_ARCHITEW6432%"=="ARM64" (
  58. set "PYTHON_ARCH=arm64"
  59. set "NODE_ARCH=arm64"
  60. )
  61. )
  62. set "PYTHON_ZIP_HASH_EXPECTED=%PYTHON_ZIP_HASH_AMD64%"
  63. if /I "%PYTHON_ARCH%"=="arm64" set "PYTHON_ZIP_HASH_EXPECTED=%PYTHON_ZIP_HASH_ARM64%"
  64. set "NODE_ZIP_HASH_EXPECTED=%NODE_ZIP_HASH_X64%"
  65. if /I "%NODE_ARCH%"=="arm64" set "NODE_ZIP_HASH_EXPECTED=%NODE_ZIP_HASH_ARM64%"
  66. if not defined PORT set "PORT=8000"
  67. REM Validate PORT is a number in the range 1-65535
  68. echo(!PORT!| findstr /R "^[0-9][0-9]*$" >nul
  69. if errorlevel 1 (
  70. echo Invalid PORT value "%PORT%". PORT must be an integer between 1 and 65535.
  71. exit /b 1
  72. )
  73. if %PORT% LSS 1 (
  74. echo Invalid PORT value "%PORT%". PORT must be between 1 and 65535.
  75. exit /b 1
  76. )
  77. if %PORT% GTR 65535 (
  78. echo Invalid PORT value "%PORT%". PORT must be between 1 and 65535.
  79. exit /b 1
  80. )
  81. REM ---- Handle arguments ----
  82. if /i "%~1"=="reset" (
  83. echo Cleaning up portable environment...
  84. call :safe_rmdir "%PORTABLE%" ".portable"
  85. if errorlevel 1 exit /b 1
  86. call :safe_rmdir "%ROOT%\static" "static"
  87. if errorlevel 1 exit /b 1
  88. echo Done. Run again without arguments to set up fresh.
  89. pause
  90. exit /b 0
  91. )
  92. if /i "%~1"=="update" (
  93. echo Forcing dependency update and frontend rebuild...
  94. if exist "%PORTABLE%\.deps-installed" del "%PORTABLE%\.deps-installed"
  95. call :safe_rmdir "%ROOT%\static" "static"
  96. if errorlevel 1 exit /b 1
  97. )
  98. REM ---- Check prerequisites ----
  99. where curl >nul 2>&1
  100. if errorlevel 1 (
  101. echo.
  102. echo [ERROR] curl.exe is not available.
  103. echo Windows 10 version 1803 or later is required.
  104. echo.
  105. pause
  106. exit /b 1
  107. )
  108. where tar >nul 2>&1
  109. if errorlevel 1 (
  110. echo.
  111. echo [ERROR] tar.exe is not available.
  112. echo Windows 10 version 1803 or later is required.
  113. echo.
  114. pause
  115. exit /b 1
  116. )
  117. REM ---- Verify project structure ----
  118. if not exist "%ROOT%\backend\app\main.py" (
  119. echo.
  120. echo [ERROR] backend\app\main.py not found.
  121. echo This script must be in the Bambuddy project root.
  122. echo.
  123. pause
  124. exit /b 1
  125. )
  126. echo.
  127. echo ____ _ _ _
  128. echo ^| __ ) __ _ _ __ ___ ^| ^|__ _ _ __^| ^| __^| ^|_ _
  129. echo ^| _ \ / _` ^| '_ ` _ \^| '_ \^| ^| ^| ^|/ _` ^|/ _` ^| ^| ^| ^|
  130. echo ^| ^|_) ^| (_^| ^| ^| ^| ^| ^| ^| ^|_) ^| ^|_^| ^| (_^| ^| (_^| ^| ^|_^| ^|
  131. echo ^|____/ \__,_^|_^| ^|_^| ^|_^|_.__/ \__,_^|\__,_^|\__,_^|\__, ^|
  132. echo ^|___/
  133. echo.
  134. REM ============================================
  135. REM Step 1: Setup Portable Python
  136. REM ============================================
  137. if exist "%PYTHON_DIR%\python.exe" (
  138. echo [OK] Python %PYTHON_VER% found.
  139. goto :python_ready
  140. )
  141. echo [1/6] Downloading Python %PYTHON_VER% (portable)...
  142. if not exist "%PORTABLE%" mkdir "%PORTABLE%"
  143. if not exist "%PYTHON_DIR%" mkdir "%PYTHON_DIR%"
  144. curl -L --fail --show-error --progress-bar -o "%PORTABLE%\python.zip" ^
  145. "https://www.python.org/ftp/python/%PYTHON_VER%/python-%PYTHON_VER%-embed-%PYTHON_ARCH%.zip"
  146. if errorlevel 1 (
  147. echo [ERROR] Failed to download Python.
  148. pause
  149. exit /b 1
  150. )
  151. call :verify_sha256 "%PORTABLE%\python.zip" "%PYTHON_ZIP_HASH_EXPECTED%" "Python"
  152. if errorlevel 1 (
  153. echo [ERROR] Failed to download Python archive.
  154. del "%PORTABLE%\python.zip" >nul 2>&1
  155. pause
  156. exit /b 1
  157. )
  158. echo Extracting Python...
  159. tar -xf "%PORTABLE%\python.zip" -C "%PYTHON_DIR%"
  160. if errorlevel 1 (
  161. echo [ERROR] Failed to extract Python archive.
  162. del "%PORTABLE%\python.zip" >nul 2>&1
  163. pause
  164. exit /b 1
  165. )
  166. del "%PORTABLE%\python.zip"
  167. if not exist "%PYTHON_DIR%\python.exe" (
  168. echo [ERROR] Python executable not found after extraction.
  169. pause
  170. exit /b 1
  171. )
  172. REM Enable site-packages by rewriting the ._pth file
  173. REM Derive python tag (e.g., 3.13.x -> 313) from %PYTHON_VER%
  174. for /f "tokens=1,2 delims=." %%A in ("%PYTHON_VER%") do (
  175. set "PY_MAJOR=%%A"
  176. set "PY_MINOR=%%B"
  177. )
  178. set "PYTHON_TAG=%PY_MAJOR%%PY_MINOR%"
  179. (
  180. echo python!PYTHON_TAG!.zip
  181. echo .
  182. echo import site
  183. ) > "%PYTHON_DIR%\python!PYTHON_TAG!._pth"
  184. REM ============================================
  185. REM Step 2: Install pip
  186. REM ============================================
  187. echo.
  188. echo [2/6] Installing pip...
  189. curl -L --fail -sS -o "%PORTABLE%\get-pip.py" "https://bootstrap.pypa.io/get-pip.py"
  190. if errorlevel 1 (
  191. echo [ERROR] Failed to download get-pip.py.
  192. pause
  193. exit /b 1
  194. )
  195. call :verify_sha256 "%PORTABLE%\get-pip.py" "%GET_PIP_SHA256%" "get-pip.py"
  196. if errorlevel 1 (
  197. del "%PORTABLE%\get-pip.py" >nul 2>&1
  198. pause
  199. exit /b 1
  200. )
  201. "%PYTHON_DIR%\python.exe" "%PORTABLE%\get-pip.py" --no-warn-script-location -q
  202. if errorlevel 1 (
  203. echo [ERROR] Failed to install pip.
  204. pause
  205. exit /b 1
  206. )
  207. del "%PORTABLE%\get-pip.py"
  208. echo [OK] Python %PYTHON_VER% ready.
  209. :python_ready
  210. REM ============================================
  211. REM Step 2.5: Create Virtual Environment (best effort)
  212. REM ============================================
  213. set "VENV_DIR=%PORTABLE%\venv"
  214. set "PYTHON_EXE=%PYTHON_DIR%\python.exe"
  215. if not exist "%VENV_DIR%\Scripts\python.exe" (
  216. echo.
  217. echo Creating virtual environment [optional]...
  218. "%PYTHON_DIR%\python.exe" -m venv "%VENV_DIR%"
  219. if errorlevel 1 (
  220. echo [WARN] Failed to create virtual environment. Continuing without venv.
  221. )
  222. )
  223. if exist "%VENV_DIR%\Scripts\python.exe" (
  224. set "PYTHON_EXE=%VENV_DIR%\Scripts\python.exe"
  225. )
  226. REM ============================================
  227. REM Step 3: Install Python Dependencies
  228. REM ============================================
  229. if exist "%PORTABLE%\.deps-installed" (
  230. echo [OK] Python packages found.
  231. goto :deps_ready
  232. )
  233. echo.
  234. echo [3/6] Installing Python packages (this may take a few minutes)...
  235. if exist "%ROOT%\requirements.lock" (
  236. "%PYTHON_EXE%" -m pip install -r "%ROOT%\requirements.lock" --require-hashes --no-warn-script-location -q
  237. ) else (
  238. echo [WARN] requirements.lock not found. Falling back to requirements.txt - no hash enforcement.
  239. "%PYTHON_EXE%" -m pip install -r "%ROOT%\requirements.txt" --no-warn-script-location -q
  240. )
  241. if errorlevel 1 (
  242. echo [ERROR] Failed to install Python packages.
  243. pause
  244. exit /b 1
  245. )
  246. REM Create marker file
  247. echo %date% %time% > "%PORTABLE%\.deps-installed"
  248. echo [OK] Packages installed.
  249. :deps_ready
  250. REM ============================================
  251. REM Step 4-6: Build Frontend (if needed)
  252. REM ============================================
  253. if exist "%ROOT%\static\index.html" (
  254. echo [OK] Frontend found.
  255. goto :frontend_ready
  256. )
  257. REM ---- Download Node.js if needed ----
  258. if exist "%NODE_DIR%\node.exe" goto :node_ready
  259. echo.
  260. echo [4/6] Downloading Node.js %NODE_VER% (portable)...
  261. curl -L --fail --show-error --progress-bar -o "%PORTABLE%\node.zip" ^
  262. "https://nodejs.org/dist/v%NODE_VER%/node-v%NODE_VER%-win-%NODE_ARCH%.zip"
  263. if errorlevel 1 (
  264. echo [ERROR] Failed to download Node.js.
  265. pause
  266. exit /b 1
  267. )
  268. call :verify_sha256 "%PORTABLE%\node.zip" "%NODE_ZIP_HASH_EXPECTED%" "Node.js"
  269. if errorlevel 1 (
  270. del "%PORTABLE%\node.zip" >nul 2>&1
  271. pause
  272. exit /b 1
  273. )
  274. echo Extracting Node.js...
  275. tar -xf "%PORTABLE%\node.zip" -C "%PORTABLE%"
  276. if errorlevel 1 (
  277. echo [ERROR] Failed to extract Node.js archive.
  278. del "%PORTABLE%\node.zip" >nul 2>&1
  279. pause
  280. exit /b 1
  281. )
  282. if exist "%PORTABLE%\node-v%NODE_VER%-win-%NODE_ARCH%" (
  283. ren "%PORTABLE%\node-v%NODE_VER%-win-%NODE_ARCH%" node
  284. )
  285. del "%PORTABLE%\node.zip"
  286. echo [OK] Node.js %NODE_VER% ready.
  287. :node_ready
  288. REM ---- Build frontend ----
  289. echo.
  290. echo [5/6] Building frontend (this may take a while)...
  291. set "PATH=%NODE_DIR%;%PATH%"
  292. pushd "%ROOT%\frontend"
  293. if exist "%ROOT%\frontend\package-lock.json" (
  294. call "%NODE_DIR%\npm.cmd" ci
  295. ) else (
  296. call "%NODE_DIR%\npm.cmd" install
  297. )
  298. if errorlevel 1 (
  299. echo [ERROR] npm install failed.
  300. popd
  301. pause
  302. exit /b 1
  303. )
  304. call "%NODE_DIR%\npm.cmd" run build
  305. if errorlevel 1 (
  306. echo [ERROR] Frontend build failed.
  307. popd
  308. pause
  309. exit /b 1
  310. )
  311. popd
  312. if not exist "%ROOT%\frontend\static\index.html" (
  313. echo [ERROR] Frontend build did not produce static\index.html.
  314. echo Expected: "%ROOT%\frontend\static\index.html"
  315. pause
  316. exit /b 1
  317. )
  318. if not exist "%ROOT%\static\index.html" (
  319. echo [ERROR] Frontend build did not produce static\index.html.
  320. echo Expected: "%ROOT%\static\index.html"
  321. pause
  322. exit /b 1
  323. )
  324. echo [OK] Frontend built.
  325. :frontend_ready
  326. REM ============================================
  327. REM Step 6: Setup Portable FFmpeg (if needed)
  328. REM ============================================
  329. where ffmpeg >nul 2>&1
  330. if not errorlevel 1 (
  331. echo [OK] FFmpeg found in system PATH.
  332. goto :ffmpeg_ready
  333. )
  334. if exist "%FFMPEG_DIR%\bin\ffmpeg.exe" (
  335. echo [OK] FFmpeg found.
  336. goto :ffmpeg_ready
  337. )
  338. echo.
  339. echo [6/6] FFmpeg not found.
  340. echo [INFO] Install FFmpeg from the official site and add it to PATH:
  341. echo https://ffmpeg.org/download.html
  342. echo [INFO] Timelapse features will be unavailable until FFmpeg is installed.
  343. :ffmpeg_ready
  344. REM ============================================
  345. REM Launch Bambuddy
  346. REM ============================================
  347. echo.
  348. echo ================================================
  349. echo Bambuddy is starting on port %PORT%
  350. echo Open: http://localhost:%PORT%
  351. echo.
  352. echo Press Ctrl+C to stop
  353. echo ================================================
  354. echo.
  355. REM Set PYTHONPATH so "backend.app.main" module is found
  356. set "PYTHONPATH=%ROOT%"
  357. REM Add portable FFmpeg to PATH if available
  358. if exist "%FFMPEG_DIR%\bin\ffmpeg.exe" set "PATH=%FFMPEG_DIR%\bin;%PATH%"
  359. REM Open browser after server is ready (poll localhost)
  360. 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)"
  361. REM Launch the application
  362. "%PYTHON_EXE%" -m uvicorn backend.app.main:app --host 0.0.0.0 --port %PORT% --loop asyncio
  363. echo.
  364. echo Bambuddy has stopped.
  365. pause
  366. endlocal
  367. goto :eof
  368. REM ============================================
  369. REM Helpers
  370. REM ============================================
  371. :safe_rmdir
  372. set "TARGET=%~1"
  373. set "LABEL=%~2"
  374. if "%TARGET%"=="" (
  375. echo [ERROR] %LABEL% path is empty. Aborting.
  376. exit /b 1
  377. )
  378. if /I "%TARGET%"=="\" (
  379. echo [ERROR] %LABEL% path resolved to root. Aborting.
  380. exit /b 1
  381. )
  382. if not exist "%TARGET%" exit /b 0
  383. echo Deleting "%TARGET%"
  384. rmdir /s /q "%TARGET%"
  385. if errorlevel 1 (
  386. echo [ERROR] Failed to delete "%TARGET%".
  387. exit /b 1
  388. )
  389. exit /b 0
  390. :verify_sha256
  391. set "FILE=%~1"
  392. set "EXPECTED=%~2"
  393. set "LABEL=%~3"
  394. if "%EXPECTED%"=="" (
  395. echo [ERROR] %LABEL% checksum not found.
  396. exit /b 1
  397. )
  398. set "ACTUAL="
  399. for /f "tokens=1" %%H in ('certutil -hashfile "%FILE%" SHA256 ^| findstr /R /I "^[0-9A-F][0-9A-F]"') do (
  400. set "ACTUAL=%%H"
  401. goto :hash_done
  402. )
  403. :hash_done
  404. if not defined ACTUAL (
  405. echo [ERROR] Failed to compute SHA256 for %LABEL%.
  406. exit /b 1
  407. )
  408. if /I not "%ACTUAL%"=="%EXPECTED%" (
  409. echo [ERROR] SHA256 verification failed for %LABEL%.
  410. echo [INFO] Expected: %EXPECTED%
  411. echo [INFO] Actual: %ACTUAL%
  412. exit /b 1
  413. )
  414. exit /b 0