start_bambuddy.bat 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  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. pause
  155. exit /b 1
  156. )
  157. echo Extracting Python...
  158. tar -xf "%PORTABLE%\python.zip" -C "%PYTHON_DIR%"
  159. if errorlevel 1 (
  160. echo [ERROR] Failed to extract Python archive.
  161. del "%PORTABLE%\python.zip" >nul 2>&1
  162. pause
  163. exit /b 1
  164. )
  165. del "%PORTABLE%\python.zip"
  166. if not exist "%PYTHON_DIR%\python.exe" (
  167. echo [ERROR] Python executable not found after extraction.
  168. pause
  169. exit /b 1
  170. )
  171. REM Enable site-packages by rewriting the ._pth file
  172. REM Derive python tag (e.g., 3.13.x -> 313) from %PYTHON_VER%
  173. for /f "tokens=1,2 delims=." %%A in ("%PYTHON_VER%") do (
  174. set "PY_MAJOR=%%A"
  175. set "PY_MINOR=%%B"
  176. )
  177. set "PYTHON_TAG=%PY_MAJOR%%PY_MINOR%"
  178. (
  179. echo python!PYTHON_TAG!.zip
  180. echo .
  181. echo import site
  182. ) > "%PYTHON_DIR%\python!PYTHON_TAG!._pth"
  183. REM ============================================
  184. REM Step 2: Install pip
  185. REM ============================================
  186. echo.
  187. echo [2/6] Installing pip...
  188. curl -L --fail -sS -o "%PORTABLE%\get-pip.py" "https://bootstrap.pypa.io/get-pip.py"
  189. if errorlevel 1 (
  190. echo [ERROR] Failed to download get-pip.py.
  191. pause
  192. exit /b 1
  193. )
  194. "%PYTHON_DIR%\python.exe" "%PORTABLE%\get-pip.py" --no-warn-script-location -q
  195. if errorlevel 1 (
  196. echo [ERROR] Failed to install pip.
  197. pause
  198. exit /b 1
  199. )
  200. del "%PORTABLE%\get-pip.py"
  201. "%PYTHON_DIR%\python.exe" -m pip install setuptools wheel --no-warn-script-location -q
  202. if errorlevel 1 (
  203. echo [ERROR] Failed to install setuptools/wheel.
  204. pause
  205. exit /b 1
  206. )
  207. echo [OK] Python %PYTHON_VER% ready.
  208. :python_ready
  209. set "PYTHON_EXE=%PYTHON_DIR%\python.exe"
  210. REM ============================================
  211. REM Step 3: Install Python Dependencies
  212. REM ============================================
  213. if exist "%PORTABLE%\.deps-installed" (
  214. echo [OK] Python packages found.
  215. goto :deps_ready
  216. )
  217. echo.
  218. echo [3/6] Installing Python packages (this may take a few minutes)...
  219. if exist "%ROOT%\requirements.lock" (
  220. "%PYTHON_EXE%" -m pip install -r "%ROOT%\requirements.lock" --require-hashes --no-build-isolation --no-warn-script-location -q
  221. ) else (
  222. echo [WARN] requirements.lock not found. Falling back to requirements.txt - no hash enforcement.
  223. "%PYTHON_EXE%" -m pip install -r "%ROOT%\requirements.txt" --no-build-isolation --no-warn-script-location -q
  224. )
  225. if errorlevel 1 (
  226. echo [ERROR] Failed to install Python packages.
  227. pause
  228. exit /b 1
  229. )
  230. REM Create marker file
  231. echo %date% %time% > "%PORTABLE%\.deps-installed"
  232. echo [OK] Packages installed.
  233. :deps_ready
  234. REM ============================================
  235. REM Step 4-6: Build Frontend (if needed)
  236. REM ============================================
  237. if exist "%ROOT%\static\index.html" (
  238. echo [OK] Frontend found.
  239. goto :frontend_ready
  240. )
  241. REM ---- Download Node.js if needed ----
  242. if exist "%NODE_DIR%\node.exe" goto :node_ready
  243. echo.
  244. echo [4/6] Downloading Node.js %NODE_VER% (portable)...
  245. curl -L --fail --show-error --progress-bar -o "%PORTABLE%\node.zip" ^
  246. "https://nodejs.org/dist/v%NODE_VER%/node-v%NODE_VER%-win-%NODE_ARCH%.zip"
  247. if errorlevel 1 (
  248. echo [ERROR] Failed to download Node.js.
  249. pause
  250. exit /b 1
  251. )
  252. call :verify_sha256 "%PORTABLE%\node.zip" "%NODE_ZIP_HASH_EXPECTED%" "Node.js"
  253. if errorlevel 1 (
  254. del "%PORTABLE%\node.zip" >nul 2>&1
  255. pause
  256. exit /b 1
  257. )
  258. echo Extracting Node.js...
  259. tar -xf "%PORTABLE%\node.zip" -C "%PORTABLE%"
  260. if errorlevel 1 (
  261. echo [ERROR] Failed to extract Node.js archive.
  262. del "%PORTABLE%\node.zip" >nul 2>&1
  263. pause
  264. exit /b 1
  265. )
  266. if exist "%PORTABLE%\node-v%NODE_VER%-win-%NODE_ARCH%" (
  267. ren "%PORTABLE%\node-v%NODE_VER%-win-%NODE_ARCH%" node
  268. )
  269. del "%PORTABLE%\node.zip"
  270. echo [OK] Node.js %NODE_VER% ready.
  271. :node_ready
  272. REM ---- Build frontend ----
  273. echo.
  274. echo [5/6] Building frontend (this may take a while)...
  275. set "PATH=%NODE_DIR%;%PATH%"
  276. pushd "%ROOT%\frontend"
  277. if exist "%ROOT%\frontend\package-lock.json" (
  278. call "%NODE_DIR%\npm.cmd" ci
  279. ) else (
  280. call "%NODE_DIR%\npm.cmd" install
  281. )
  282. if errorlevel 1 (
  283. echo [ERROR] npm install failed.
  284. popd
  285. pause
  286. exit /b 1
  287. )
  288. call "%NODE_DIR%\npm.cmd" run build
  289. if errorlevel 1 (
  290. echo [ERROR] Frontend build failed.
  291. popd
  292. pause
  293. exit /b 1
  294. )
  295. popd
  296. if not exist "%ROOT%\frontend\static\index.html" (
  297. echo [ERROR] Frontend build did not produce static\index.html.
  298. echo Expected: "%ROOT%\frontend\static\index.html"
  299. pause
  300. exit /b 1
  301. )
  302. if not exist "%ROOT%\static\index.html" (
  303. echo [ERROR] Frontend build did not produce static\index.html.
  304. echo Expected: "%ROOT%\static\index.html"
  305. pause
  306. exit /b 1
  307. )
  308. echo [OK] Frontend built.
  309. :frontend_ready
  310. REM ============================================
  311. REM Step 6: Setup Portable FFmpeg (if needed)
  312. REM ============================================
  313. where ffmpeg >nul 2>&1
  314. if not errorlevel 1 (
  315. echo [OK] FFmpeg found in system PATH.
  316. goto :ffmpeg_ready
  317. )
  318. if exist "%FFMPEG_DIR%\bin\ffmpeg.exe" (
  319. echo [OK] FFmpeg found.
  320. goto :ffmpeg_ready
  321. )
  322. echo.
  323. echo [6/6] FFmpeg not found.
  324. echo [INFO] Install FFmpeg from the official site and add it to PATH:
  325. echo https://ffmpeg.org/download.html
  326. echo [INFO] Timelapse features will be unavailable until FFmpeg is installed.
  327. :ffmpeg_ready
  328. REM ============================================
  329. REM Launch Bambuddy
  330. REM ============================================
  331. echo.
  332. echo ================================================
  333. echo Bambuddy is starting on port %PORT%
  334. echo Open: http://localhost:%PORT%
  335. echo.
  336. echo Press Ctrl+C to stop
  337. echo ================================================
  338. echo.
  339. REM Set PYTHONPATH so "backend.app.main" module is found
  340. set "PYTHONPATH=%ROOT%"
  341. REM Add portable FFmpeg to PATH if available
  342. if exist "%FFMPEG_DIR%\bin\ffmpeg.exe" set "PATH=%FFMPEG_DIR%\bin;%PATH%"
  343. REM Open browser after server is ready (poll localhost)
  344. 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)"
  345. REM Launch the application
  346. "%PYTHON_EXE%" -m uvicorn backend.app.main:app --host 0.0.0.0 --port %PORT% --loop asyncio
  347. echo.
  348. echo Bambuddy has stopped.
  349. pause
  350. endlocal
  351. goto :eof
  352. REM ============================================
  353. REM Helpers
  354. REM ============================================
  355. :safe_rmdir
  356. set "TARGET=%~1"
  357. set "LABEL=%~2"
  358. if "%TARGET%"=="" (
  359. echo [ERROR] %LABEL% path is empty. Aborting.
  360. exit /b 1
  361. )
  362. if /I "%TARGET%"=="\" (
  363. echo [ERROR] %LABEL% path resolved to root. Aborting.
  364. exit /b 1
  365. )
  366. if not exist "%TARGET%" exit /b 0
  367. echo Deleting "%TARGET%"
  368. rmdir /s /q "%TARGET%"
  369. if errorlevel 1 (
  370. echo [ERROR] Failed to delete "%TARGET%".
  371. exit /b 1
  372. )
  373. exit /b 0
  374. :verify_sha256
  375. set "FILE=%~1"
  376. set "EXPECTED=%~2"
  377. set "LABEL=%~3"
  378. if "%EXPECTED%"=="" (
  379. echo [ERROR] %LABEL% checksum not found.
  380. exit /b 1
  381. )
  382. set "ACTUAL="
  383. for /f "tokens=1" %%H in ('certutil -hashfile "%FILE%" SHA256 ^| findstr /R /I "^[0-9A-F][0-9A-F]"') do (
  384. set "ACTUAL=%%H"
  385. goto :hash_done
  386. )
  387. :hash_done
  388. if not defined ACTUAL (
  389. echo [ERROR] Failed to compute SHA256 for %LABEL%.
  390. exit /b 1
  391. )
  392. if /I not "%ACTUAL%"=="%EXPECTED%" (
  393. echo [ERROR] SHA256 verification failed for %LABEL%.
  394. echo [INFO] Expected: %EXPECTED%
  395. echo [INFO] Actual: %ACTUAL%
  396. exit /b 1
  397. )
  398. exit /b 0