start_bambuddy.bat 15 KB

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