refactor: 统一运行/编译脚本为 scripts/ 下纯 Python(自动 venv,傻瓜式)
新增 _env.py 共享工具与 dev/run/tauri_dev/build_server/build_tauri/build_docker/clean 脚本,
删除旧 build.{sh,ps1,bat}、根 run.bat/debug.bat/start.sh/dev.py、supervisord.conf、packaging/Dockerfile,
Docker 改以根 Dockerfile 为准并重写 compose(/data 卷)。
This commit is contained in:
@@ -48,93 +48,62 @@ CapaReport/
|
|||||||
│ └─ config.py # 配置读写
|
│ └─ config.py # 配置读写
|
||||||
├─ frontend/ # Vue 前端
|
├─ frontend/ # Vue 前端
|
||||||
├─ src-tauri/ # Tauri 桌面壳
|
├─ src-tauri/ # Tauri 桌面壳
|
||||||
├─ scripts/ # 一键构建脚本
|
├─ scripts/ # 运行 / 编译 Python 脚本(dev、run、tauri、docker、clean...)
|
||||||
├─ packaging/ # Docker、Compose、PyInstaller 配置
|
├─ packaging/ # Dockerfile、Compose、PyInstaller 配置
|
||||||
├─ dist/ # 一键构建后的最终产物
|
├─ dist/ # 编译后的最终产物
|
||||||
├─ docs/project_context.md # 项目维护记录
|
├─ docs/project_context.md # 项目维护记录
|
||||||
├─ Configure.json # 应用配置
|
├─ Configure.json # 应用配置
|
||||||
├─ ReportScript.sql # SQL 处理脚本
|
├─ ReportScript.sql # SQL 处理脚本
|
||||||
├─ requirements.txt # Python 依赖
|
├─ requirements.txt # Python 依赖
|
||||||
└─ run.bat # Windows 本地启动脚本
|
└─ Dockerfile # 服务端容器镜像
|
||||||
```
|
```
|
||||||
|
|
||||||
## 本地运行
|
## 运行与编译脚本
|
||||||
|
|
||||||
|
所有运行 / 编译都统一为 `scripts/` 下的 Python 脚本,直接用系统 Python 运行即可(`python scripts/xxx.py`)。脚本会**自动创建 `.venv` 并安装依赖**(使用标准库 `venv`,不依赖 uv),自动安装前端依赖;缺少 Node.js / Rust / Docker 时会给出安装引导。
|
||||||
|
|
||||||
前置要求:
|
前置要求:
|
||||||
|
|
||||||
- Windows 10/11
|
- Python 3.10+(加入 PATH,作为创建 `.venv` 的基础解释器)
|
||||||
- Python 与 `uv`
|
- Node.js 18+(前端构建)
|
||||||
- Node.js
|
- MySQL 8.0+(直连仓库模式)
|
||||||
- MySQL 8.0+
|
- Rust 工具链(仅编译 Tauri 桌面版需要)
|
||||||
|
- Docker(仅编译 Docker 镜像需要)
|
||||||
|
|
||||||
安装后端依赖:
|
| 脚本 | 用途 |
|
||||||
|
| --- | --- |
|
||||||
|
| `python scripts/dev.py` | 开发测试:后端自动重载 + 前端 Vite 热更新 |
|
||||||
|
| `python scripts/run.py` | 本地运行:构建前端(如缺失)并启动服务 |
|
||||||
|
| `python scripts/tauri_dev.py` | Tauri 桌面端测试运行 |
|
||||||
|
| `python scripts/build_server.py` | 编译 Server 便携版 |
|
||||||
|
| `python scripts/build_tauri.py` | 编译 Tauri 桌面版 |
|
||||||
|
| `python scripts/build_docker.py` | 编译 / 更新 Docker 镜像 |
|
||||||
|
| `python scripts/clean.py` | 清理缓存、`__pycache__`、编译产物、运行时临时数据 |
|
||||||
|
| `python scripts/gen_license_code.py` | 根据授权 key 生成激活码 |
|
||||||
|
|
||||||
|
### 开发测试
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
uv venv
|
python scripts/dev.py
|
||||||
uv pip install -r requirements.txt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
安装前端依赖:
|
启动后端(`http://127.0.0.1:9081`,自动重载)和前端(`http://127.0.0.1:5174`,Vite 热更新,`/api` 与 `/health` 代理到后端)。按 `Ctrl+C` 停止。
|
||||||
|
|
||||||
|
### 本地运行
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd frontend
|
python scripts/run.py # 默认 0.0.0.0:9081
|
||||||
npm install
|
python scripts/run.py --port 8080
|
||||||
cd ..
|
python scripts/run.py --rebuild # 强制重新构建前端
|
||||||
```
|
```
|
||||||
|
|
||||||
启动服务:
|
访问 `http://localhost:9081`。
|
||||||
|
|
||||||
|
### Server 便携版
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\run.bat
|
python scripts/build_server.py
|
||||||
```
|
python scripts/build_server.py --no-archive
|
||||||
|
|
||||||
访问地址:
|
|
||||||
|
|
||||||
```text
|
|
||||||
http://localhost:9081
|
|
||||||
```
|
|
||||||
|
|
||||||
前端开发模式:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cd frontend
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Vite 会把 `/api` 和 `/health` 代理到 `http://localhost:9081`。如果 `frontend/dist` 不存在,`run.bat` 会自动安装前端依赖并执行构建。
|
|
||||||
|
|
||||||
## 一键构建
|
|
||||||
|
|
||||||
Windows 在项目根目录执行:
|
|
||||||
|
|
||||||
```bat
|
|
||||||
scripts\build.bat -?
|
|
||||||
```
|
|
||||||
|
|
||||||
可用目标:
|
|
||||||
|
|
||||||
```bat
|
|
||||||
scripts\build.bat server
|
|
||||||
scripts\build.bat desktop
|
|
||||||
scripts\build.bat docker
|
|
||||||
scripts\build.bat all
|
|
||||||
```
|
|
||||||
|
|
||||||
常用参数:
|
|
||||||
|
|
||||||
```bat
|
|
||||||
scripts\build.bat server -NoArchive
|
|
||||||
scripts\build.bat all -Clean
|
|
||||||
scripts\build.bat docker -SkipDockerBuild
|
|
||||||
scripts\build.bat all -SkipDesktopBuild
|
|
||||||
```
|
|
||||||
|
|
||||||
构建完成后只保留 `dist/` 下的最终产物;`dist/.tmp`、`frontend/dist`、`src-tauri/target`、`src-tauri/binaries` 等中间产物会自动清理。
|
|
||||||
|
|
||||||
### Server Portable
|
|
||||||
|
|
||||||
```bat
|
|
||||||
scripts\build.bat server
|
|
||||||
```
|
```
|
||||||
|
|
||||||
输出:
|
输出:
|
||||||
@@ -144,80 +113,60 @@ dist\server\CapacityReport-Server-windows-x64\
|
|||||||
dist\server\CapacityReport-Server-windows-x64.zip
|
dist\server\CapacityReport-Server-windows-x64.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
便携版内包含后端可执行文件、前端构建产物、`Configure.json`、`ReportScript.sql`、`cache/`、`logs/` 和启动脚本。默认监听端口为 `9081`。
|
便携版内包含后端可执行文件、前端构建产物、`Configure.json`、`ReportScript.sql`、`CellData.sql`、`cache/`、`logs/` 和启动脚本(Windows 为 `run.bat`,Linux/macOS 为 `start.sh`)。默认监听端口 `9081`。便携版需在目标系统原生构建。
|
||||||
|
|
||||||
### 桌面版
|
### Tauri 桌面版
|
||||||
|
|
||||||
```bat
|
```powershell
|
||||||
scripts\build.bat desktop
|
python scripts/build_tauri.py # 编译当前系统的桌面版
|
||||||
|
python scripts/build_tauri.py --platform windows
|
||||||
```
|
```
|
||||||
|
|
||||||
输出:
|
输出:`dist\desktop\` 下的安装包(Windows: `.msi` / `.exe`;Linux: `.deb` / `.AppImage`;macOS: `.dmg`)。
|
||||||
|
|
||||||
```text
|
- 桌面版使用 Tauri 启动 Python sidecar,sidecar 监听 `127.0.0.1:9081`,运行数据写入系统 app data 目录。
|
||||||
dist\desktop\*-setup.exe
|
- Windows 安装包内置 WebView2 离线安装器,适合无外网、未预装 WebView2 Runtime 的机器。
|
||||||
|
- Windows 默认安装到 `D:\Program Files\CapacityReport`,无 D 盘时回落系统盘。
|
||||||
|
- 首次安装运行 Rust/Tauri 时,脚本会在缺少 Tauri CLI 时自动 `cargo install tauri-cli --locked`。
|
||||||
|
- Tauri 无法可靠跨系统交叉编译:`--platform` 必须与当前系统一致,否则脚本会提示需在目标系统本机构建。
|
||||||
|
|
||||||
|
### Docker 镜像
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python scripts/build_docker.py # 编译镜像并生成 dist/docker/ 离线部署包
|
||||||
|
python scripts/build_docker.py update # 重新编译镜像并就地更新本机容器
|
||||||
|
python scripts/build_docker.py --no-save # 编译但不导出 tar
|
||||||
```
|
```
|
||||||
|
|
||||||
桌面版使用 Tauri 启动 Python sidecar。sidecar 默认监听 `127.0.0.1:9081`,运行数据写入系统 app data 目录,不写入安装目录。
|
离线部署包输出到 `dist\docker\`(`capacity-report-app-latest.tar`、`docker-compose.yml`、`Configure.json`、`ReportScript.sql`、`CellData.sql`、`mysql/`)。运行态数据落在 `/data` 数据卷,首次启动由 `docker/entrypoint.sh` 播种默认配置。
|
||||||
|
|
||||||
Windows 桌面安装包会内置 WebView2 离线安装器,适合没有外网且未预装 WebView2 Runtime 的机器。该模式会让安装包体积增加约 127 MB;构建机需要能在构建阶段下载 WebView2 离线安装器。
|
部署:
|
||||||
首次启动会把安装包内置的 `Configure.json` 和 `ReportScript.sql` 复制到运行数据目录;Windows 下通常是 `%APPDATA%\com.nixevol.capacityreport\`。安装目录中的 `_up_` 只是 Tauri 打包资源目录,程序运行时不会直接编辑它。
|
|
||||||
Windows 桌面版使用 NSIS 安装器,卸载时会询问是否同时删除 `%APPDATA%\com.nixevol.capacityreport\` 中的配置、脚本、授权、缓存和日志。
|
|
||||||
Windows 桌面版默认安装到 `D:\Program Files\CapacityReport`;如果没有 D 盘,则默认安装到系统 `Program Files\CapacityReport`。桌面版已关闭 release DevTools 和右键浏览器菜单。
|
|
||||||
|
|
||||||
构建桌面版需要 Rust 和 Tauri CLI。脚本会在缺少 Tauri CLI 时自动执行:
|
|
||||||
|
|
||||||
```bat
|
|
||||||
cargo install tauri-cli --locked
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker 版
|
|
||||||
|
|
||||||
```bat
|
|
||||||
scripts\build.bat docker
|
|
||||||
```
|
|
||||||
|
|
||||||
输出:
|
|
||||||
|
|
||||||
```text
|
|
||||||
capacity-report-app:latest
|
|
||||||
dist\docker\capacity-report-app-latest.tar
|
|
||||||
dist\docker\docker-compose.yml
|
|
||||||
dist\docker\Configure.json
|
|
||||||
dist\docker\ReportScript.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
启动:
|
|
||||||
|
|
||||||
```bat
|
|
||||||
docker load -i dist\docker\capacity-report-app-latest.tar
|
|
||||||
docker compose -f dist\docker\docker-compose.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
访问:
|
|
||||||
|
|
||||||
```text
|
|
||||||
http://localhost:9081
|
|
||||||
```
|
|
||||||
|
|
||||||
停止:
|
|
||||||
|
|
||||||
```bat
|
|
||||||
docker compose -f dist\docker\docker-compose.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## Linux / macOS 构建
|
|
||||||
|
|
||||||
在对应系统原生环境执行:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sh scripts/build.sh server
|
docker load -i dist/docker/capacity-report-app-latest.tar
|
||||||
sh scripts/build.sh desktop
|
docker compose -f dist/docker/docker-compose.yml up -d
|
||||||
sh scripts/build.sh docker
|
|
||||||
sh scripts/build.sh all
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Server Portable 和桌面版需要在目标系统原生构建:Windows 包在 Windows 构建,Linux 包在 Linux 构建,macOS 包在 macOS 构建。Docker 镜像可以在 Windows 开发机上构建。
|
访问 `http://localhost:9081`;停止:`docker compose -f dist/docker/docker-compose.yml down`。
|
||||||
|
|
||||||
|
### 清理
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python scripts/clean.py # 清理缓存 / 编译产物 / 运行时临时数据
|
||||||
|
python scripts/clean.py --deep # 额外清理 .venv 与 frontend/node_modules
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linux / macOS
|
||||||
|
|
||||||
|
脚本跨平台通用,在对应系统原生环境执行相同命令即可:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/build_server.py
|
||||||
|
python3 scripts/build_tauri.py
|
||||||
|
python3 scripts/build_docker.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Server 便携版与桌面版需在目标系统原生构建(Windows 包在 Windows、Linux 包在 Linux、macOS 包在 macOS);Docker 镜像可在任意装有 Docker 的开发机构建。
|
||||||
|
|
||||||
## 配置说明
|
## 配置说明
|
||||||
|
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
@echo off
|
|
||||||
setlocal
|
|
||||||
title CapacityReport Debug
|
|
||||||
|
|
||||||
cd /d "%~dp0"
|
|
||||||
|
|
||||||
set PYTHON_EXE=.venv\Scripts\python.exe
|
|
||||||
if not exist "%PYTHON_EXE%" (
|
|
||||||
echo [ERROR] Python virtual environment not found: %PYTHON_EXE%
|
|
||||||
echo [HINT] Run this command in the project root first: uv venv
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
"%PYTHON_EXE%" -c "import fastapi, uvicorn, pandas, openpyxl, pymysql, chardet" >nul 2>&1
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo [INFO] Installing or repairing Python dependencies...
|
|
||||||
uv pip install -r requirements.txt
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo [ERROR] Failed to install Python dependencies.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
where npm >nul 2>&1
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo [ERROR] npm not found. Install Node.js first.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
pushd frontend
|
|
||||||
if not exist "node_modules" (
|
|
||||||
echo [INFO] Installing frontend dependencies...
|
|
||||||
call npm ci
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo [ERROR] Failed to install frontend dependencies.
|
|
||||||
popd
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
popd
|
|
||||||
|
|
||||||
echo ========================================
|
|
||||||
echo CapacityReport - debug mode
|
|
||||||
echo ========================================
|
|
||||||
echo [INFO] Backend: http://127.0.0.1:9081
|
|
||||||
echo [INFO] Frontend: http://127.0.0.1:5173
|
|
||||||
echo [INFO] Close both debug windows to stop.
|
|
||||||
echo.
|
|
||||||
|
|
||||||
start "CapacityReport Backend" cmd /k ""%PYTHON_EXE%" -m app.main --host 127.0.0.1 --port 9081"
|
|
||||||
start "CapacityReport Frontend" cmd /k "pushd "%~dp0frontend" && npm run dev -- --host 127.0.0.1 --port 5173"
|
|
||||||
|
|
||||||
echo [OK] Debug services are starting.
|
|
||||||
pause
|
|
||||||
@@ -1,5 +1,27 @@
|
|||||||
# 项目上下文记录
|
# 项目上下文记录
|
||||||
|
|
||||||
|
## 2026-06-26:统一运行/编译脚本为 scripts/ 下纯 Python(傻瓜式,自动 venv)
|
||||||
|
|
||||||
|
把分散的运行/编译入口(根 `run.bat`/`debug.bat`/`start.sh`/`dev.py`、`scripts/build.{sh,ps1,bat}`、`supervisord.conf`、`packaging/Dockerfile`)清理掉,统一改为 `scripts/` 下只依赖标准库的 Python 脚本,可直接 `python scripts/xxx.py` 运行:
|
||||||
|
|
||||||
|
- `scripts/_env.py`:共享工具。自动创建 `.venv`(标准库 `venv`,**不使用 uv**),按 `requirements.txt` 的 sha256(存 `.venv/.requirements.hash`)变化自动重装依赖;自动装前端依赖;检测 Node.js / Rust(cargo,rustc) / Docker,缺失时打印安装引导并退出;提供 `build_frontend`(可传 `VITE_API_BASE`)、PyInstaller 打包(`build_server_binary` onedir/onefile)、`build_tauri_sidecar`(按 rust host triple 命名复制到 `src-tauri/binaries`)、`remove_path`(限工作区内)等助手。额外包用 `pip show` 探测是否已装。
|
||||||
|
- `scripts/dev.py`:开发测试(移植旧 `dev.py`)——后端 uvicorn `--reload`(9081) + 前端 Vite HMR(5174),多进程流式日志、Ctrl+C 统一退出。
|
||||||
|
- `scripts/run.py`:本地运行(替代 `run.bat`)——构建前端(缺失时) + `python -m app.main`,支持 `--host/--port/--rebuild`。
|
||||||
|
- `scripts/tauri_dev.py`:桌面端测试运行——构建前端(desktop api base) + 构建 sidecar(缺失时) + `cargo tauri dev`,`--rebuild` 强制重建。
|
||||||
|
- `scripts/build_tauri.py`:桌面版编译——`--platform current|windows|linux|macos`,与宿主不一致时报错提示需本机构建(Tauri 不可靠交叉编译);Windows 复用旧 ps1 的 NSIS 模板定制(默认装 `D:\Program Files\CapacityReport`、内置 WebView2 离线安装器走 tauri.conf.json),临时改 `tauri.conf.json` 的 `nsis.template` 后构建并还原;产物收集到 `dist/desktop/`。
|
||||||
|
- `scripts/build_server.py`:Server 便携版——PyInstaller onedir + 前端 dist + `Configure.json`/`ReportScript.sql`/`CellData.sql` + `cache/`/`logs/` + 启动脚本(win `run.bat`/unix `start.sh`),zip 打包,`--no-archive` 跳过。
|
||||||
|
- `scripts/build_docker.py`:Docker 编译/更新——以**根 `Dockerfile` + `docker/entrypoint.sh`**(前端预构建、`/data` 卷、uvicorn)为准;`build`(默认)生成 `dist/docker/` 离线包(tar+compose+配置+mysql),`update` 重新构建并 `docker compose ... up -d --force-recreate`,`--no-save` 跳过 tar。
|
||||||
|
- `scripts/clean.py`:清理 `__pycache__`/`*.pyc`、编译产物(`dist`、`frontend/dist`、`.vite`、`src-tauri/target`、`src-tauri/binaries`、各 `.cache`)、运行时临时数据(`cache`/`logs`/`uploads`);`--deep` 再清 `.venv`/`node_modules`。遍历跳过 `.git/.venv/node_modules`,仅删工作区内路径。
|
||||||
|
- `scripts/gen_license_code.py` 保留。
|
||||||
|
|
||||||
|
清理与统一:
|
||||||
|
- 删除 `scripts/build.sh`、`scripts/build.ps1`、`scripts/build.bat`、根 `run.bat`/`debug.bat`/`start.sh`/`dev.py`、`supervisord.conf`、`packaging/Dockerfile`(supervisord 多阶段那套已废弃,确认 `app/` 无 supervisor 引用、根 Dockerfile 用 `docker/entrypoint.sh`)。
|
||||||
|
- 重写 `packaging/docker-compose.yml` 匹配根 Dockerfile:app 服务挂 `capacity-data:/data` 命名卷(不再 `/app` 挂载 + supervisord 环境变量),保留 MySQL 服务与 `mysql/` 初始化/配置。
|
||||||
|
- 运行时 SQL 文件确认为 `ReportScript.sql` 与 `CellData.sql`(`app/config.py`:`CELLDATA_SCRIPT = BASE_DIR / "CellData.sql"`,非 `CellDataScript.sql`)。便携版/Docker 离线包按此打包。
|
||||||
|
- 环境检测:Tauri 桌面端用 Rust(非 Go),脚本检测 Node.js + Rust(+Docker 镜像);用户最初提到的「go」实为 Rust。
|
||||||
|
- `README.md` 改写「运行与编译脚本」表格与各分项说明,目录结构条目改为 `scripts/`(py 脚本) 与根 `Dockerfile`。
|
||||||
|
- 验证:9 个脚本 `python -m py_compile` 全通过;`clean.py`/`build_docker.py`/`build_tauri.py` `--help` 正常。
|
||||||
|
|
||||||
## 2026-06-01:修复前端 npm audit moderate 漏洞
|
## 2026-06-01:修复前端 npm audit moderate 漏洞
|
||||||
|
|
||||||
- `frontend/package.json` 增加 `overrides.dompurify=3.4.7`,将 `monaco-editor@0.55.1` 间接依赖的 vulnerable `dompurify@3.2.7` 覆盖到安全版本。
|
- `frontend/package.json` 增加 `overrides.dompurify=3.4.7`,将 `monaco-editor@0.55.1` 间接依赖的 vulnerable `dompurify@3.2.7` 覆盖到安全版本。
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
# CapacityReport Dockerfile
|
|
||||||
# The frontend is built in a Node stage and served by the FastAPI runtime.
|
|
||||||
|
|
||||||
FROM node:22-slim AS frontend-build
|
|
||||||
|
|
||||||
WORKDIR /frontend
|
|
||||||
|
|
||||||
COPY frontend/package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
COPY frontend/ ./
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
|
|
||||||
FROM python:3.13.11-slim
|
|
||||||
|
|
||||||
LABEL maintainer="CapacityReport"
|
|
||||||
LABEL description="CapacityReport processing service"
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
gcc \
|
|
||||||
libmariadb-dev \
|
|
||||||
curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY requirements.txt ./
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
RUN mkdir -p /var/log/supervisor /var/run /app/cache
|
|
||||||
|
|
||||||
COPY app ./app
|
|
||||||
COPY Configure.json ReportScript.sql start.sh supervisord.conf ./
|
|
||||||
COPY --from=frontend-build /frontend/dist /app/frontend/dist
|
|
||||||
|
|
||||||
RUN chmod +x /app/start.sh 2>/dev/null || true
|
|
||||||
|
|
||||||
EXPOSE 9081
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
|
||||||
CMD curl -f http://localhost:9081/health || exit 1
|
|
||||||
|
|
||||||
CMD ["supervisord", "-c", "/app/supervisord.conf"]
|
|
||||||
@@ -6,14 +6,10 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "9081:9081"
|
- "9081:9081"
|
||||||
volumes:
|
volumes:
|
||||||
- ./cache:/app/cache
|
- capacity-data:/data
|
||||||
- ./Configure.json:/app/Configure.json
|
|
||||||
- ./ReportScript.sql:/app/ReportScript.sql
|
|
||||||
- ./logs:/var/log/supervisor
|
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
- SUPERVISOR_ENABLED=1
|
|
||||||
depends_on:
|
depends_on:
|
||||||
capacity-mysql:
|
capacity-mysql:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -60,3 +56,5 @@ networks:
|
|||||||
volumes:
|
volumes:
|
||||||
capacity-mysql-data:
|
capacity-mysql-data:
|
||||||
driver: local
|
driver: local
|
||||||
|
capacity-data:
|
||||||
|
driver: local
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
@echo off
|
|
||||||
title CapacityReport v3.0.0
|
|
||||||
|
|
||||||
echo ========================================
|
|
||||||
echo CapacityReport - report service
|
|
||||||
echo ========================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
cd /d "%~dp0"
|
|
||||||
|
|
||||||
set PYTHON_EXE=.venv\Scripts\python.exe
|
|
||||||
if not exist "%PYTHON_EXE%" (
|
|
||||||
echo [ERROR] Python virtual environment not found: %PYTHON_EXE%
|
|
||||||
echo [HINT] Run this command in the project root first: uv venv
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
"%PYTHON_EXE%" -c "import fastapi, uvicorn, pandas, openpyxl, pymysql, chardet" >nul 2>&1
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo [INFO] Installing or repairing Python dependencies...
|
|
||||||
uv pip install -r requirements.txt
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo [ERROR] Failed to install Python dependencies.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not exist "frontend\dist\index.html" (
|
|
||||||
echo [INFO] Frontend build output not found. Building frontend...
|
|
||||||
where npm >nul 2>&1
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo [ERROR] npm not found. Install Node.js first.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
pushd frontend
|
|
||||||
if not exist "node_modules" (
|
|
||||||
echo [INFO] Installing frontend dependencies...
|
|
||||||
call npm ci
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo [ERROR] Failed to install frontend dependencies.
|
|
||||||
popd
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
call npm run build
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo [ERROR] Failed to build frontend.
|
|
||||||
popd
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
popd
|
|
||||||
)
|
|
||||||
|
|
||||||
:loop
|
|
||||||
echo.
|
|
||||||
echo [%date% %time%] Starting service...
|
|
||||||
echo [INFO] Frontend: http://localhost:9081
|
|
||||||
echo [INFO] Close this window to stop the service.
|
|
||||||
echo.
|
|
||||||
|
|
||||||
"%PYTHON_EXE%" -m app.main
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo [%date% %time%] Service stopped. Restarting in 3 seconds...
|
|
||||||
timeout /t 3 /nobreak >nul
|
|
||||||
goto loop
|
|
||||||
+342
@@ -0,0 +1,342 @@
|
|||||||
|
"""CapacityReport 脚本共享工具。
|
||||||
|
|
||||||
|
所有 scripts/ 下的运行/编译脚本都只依赖 Python 标准库,可直接用任意系统 Python 运行
|
||||||
|
(python scripts/xxx.py)。本模块负责:
|
||||||
|
|
||||||
|
- 自动创建并维护项目 .venv(使用标准库 venv,不使用 uv),按 requirements.txt 变化自动重装依赖。
|
||||||
|
- 检测 Node.js / Rust(Cargo) / Docker 环境,缺失时给出安装引导。
|
||||||
|
- 统一的日志、命令执行、平台判断等助手。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
IS_WINDOWS = os.name == "nt"
|
||||||
|
|
||||||
|
VENV_DIR = ROOT / ".venv"
|
||||||
|
VENV_BIN = VENV_DIR / ("Scripts" if IS_WINDOWS else "bin")
|
||||||
|
VENV_PYTHON = VENV_BIN / ("python.exe" if IS_WINDOWS else "python")
|
||||||
|
REQUIREMENTS = ROOT / "requirements.txt"
|
||||||
|
REQ_HASH_FILE = VENV_DIR / ".requirements.hash"
|
||||||
|
|
||||||
|
FRONTEND_DIR = ROOT / "frontend"
|
||||||
|
SRC_TAURI_DIR = ROOT / "src-tauri"
|
||||||
|
PACKAGING_DIR = ROOT / "packaging"
|
||||||
|
DIST_DIR = ROOT / "dist"
|
||||||
|
|
||||||
|
DESKTOP_API_BASE = "http://127.0.0.1:9081"
|
||||||
|
SERVER_NAME = "capareport-server"
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- 日志
|
||||||
|
|
||||||
|
|
||||||
|
def _supports_color() -> bool:
|
||||||
|
return sys.stdout.isatty() and not IS_WINDOWS
|
||||||
|
|
||||||
|
|
||||||
|
_C = {
|
||||||
|
"reset": "\033[0m",
|
||||||
|
"green": "\033[32m",
|
||||||
|
"cyan": "\033[36m",
|
||||||
|
"yellow": "\033[33m",
|
||||||
|
"red": "\033[31m",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def setup_console() -> None:
|
||||||
|
"""让 Windows 控制台也能正常输出 UTF-8,避免中文乱码报错。"""
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def step(message: str) -> None:
|
||||||
|
color = _C["green"] if _supports_color() else ""
|
||||||
|
reset = _C["reset"] if _supports_color() else ""
|
||||||
|
print(f"\n{color}==> {message}{reset}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def info(message: str) -> None:
|
||||||
|
print(f" {message}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def warn(message: str) -> None:
|
||||||
|
color = _C["yellow"] if _supports_color() else ""
|
||||||
|
reset = _C["reset"] if _supports_color() else ""
|
||||||
|
print(f"{color}[warn] {message}{reset}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def die(message: str, hint: str | None = None) -> "NoReturn": # type: ignore[name-defined]
|
||||||
|
color = _C["red"] if _supports_color() else ""
|
||||||
|
reset = _C["reset"] if _supports_color() else ""
|
||||||
|
print(f"{color}[error] {message}{reset}", file=sys.stderr, flush=True)
|
||||||
|
if hint:
|
||||||
|
print(f" {hint}", file=sys.stderr, flush=True)
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- 命令执行
|
||||||
|
|
||||||
|
|
||||||
|
def run(args: list[str], cwd: Path | None = None, env: dict[str, str] | None = None) -> None:
|
||||||
|
"""执行命令并实时输出;失败抛 SystemExit。"""
|
||||||
|
printable = " ".join(str(a) for a in args)
|
||||||
|
info(printable)
|
||||||
|
completed = subprocess.run(
|
||||||
|
[str(a) for a in args],
|
||||||
|
cwd=str(cwd) if cwd else None,
|
||||||
|
env=env,
|
||||||
|
shell=False,
|
||||||
|
)
|
||||||
|
if completed.returncode != 0:
|
||||||
|
die(f"命令执行失败(退出码 {completed.returncode}):{printable}")
|
||||||
|
|
||||||
|
|
||||||
|
def which(name: str) -> str | None:
|
||||||
|
return shutil.which(name)
|
||||||
|
|
||||||
|
|
||||||
|
def host_platform() -> str:
|
||||||
|
if IS_WINDOWS:
|
||||||
|
return "windows"
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
return "macos"
|
||||||
|
return "linux"
|
||||||
|
|
||||||
|
|
||||||
|
def server_exe_name(platform: str | None = None) -> str:
|
||||||
|
platform = platform or host_platform()
|
||||||
|
return f"{SERVER_NAME}.exe" if platform == "windows" else SERVER_NAME
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- Python venv
|
||||||
|
|
||||||
|
|
||||||
|
def _base_python() -> str:
|
||||||
|
"""返回用于创建 venv 的基础解释器,优先当前运行脚本的解释器。"""
|
||||||
|
if sys.executable:
|
||||||
|
return sys.executable
|
||||||
|
for name in ("python3", "python"):
|
||||||
|
found = shutil.which(name)
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
die(
|
||||||
|
"未找到可用的 Python 解释器。",
|
||||||
|
"请安装 Python 3.10+ 并加入 PATH:https://www.python.org/downloads/",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _requirements_hash() -> str:
|
||||||
|
try:
|
||||||
|
return hashlib.sha256(REQUIREMENTS.read_bytes()).hexdigest()
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_venv(extra_packages: list[str] | None = None) -> Path:
|
||||||
|
"""确保 .venv 存在且依赖最新,返回 venv 内的 python 路径。"""
|
||||||
|
extra_packages = extra_packages or []
|
||||||
|
created = False
|
||||||
|
|
||||||
|
if not VENV_PYTHON.exists():
|
||||||
|
base = _base_python()
|
||||||
|
step(f"创建虚拟环境 .venv({base})")
|
||||||
|
run([base, "-m", "venv", str(VENV_DIR)])
|
||||||
|
created = True
|
||||||
|
|
||||||
|
current_hash = _requirements_hash()
|
||||||
|
stored_hash = REQ_HASH_FILE.read_text(encoding="utf-8").strip() if REQ_HASH_FILE.exists() else ""
|
||||||
|
|
||||||
|
if created or current_hash != stored_hash:
|
||||||
|
step("安装/更新 Python 依赖(requirements.txt)")
|
||||||
|
run([str(VENV_PYTHON), "-m", "pip", "install", "--upgrade", "pip"])
|
||||||
|
run([str(VENV_PYTHON), "-m", "pip", "install", "-r", str(REQUIREMENTS)], cwd=ROOT)
|
||||||
|
try:
|
||||||
|
REQ_HASH_FILE.write_text(current_hash, encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for package in extra_packages:
|
||||||
|
_ensure_python_package(package)
|
||||||
|
|
||||||
|
return VENV_PYTHON
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_python_package(package: str) -> None:
|
||||||
|
"""确保某个额外包(如 pyinstaller)已安装在 venv 中。"""
|
||||||
|
name = package.split("==")[0].split(">=")[0].strip()
|
||||||
|
probe = subprocess.run(
|
||||||
|
[str(VENV_PYTHON), "-m", "pip", "show", name],
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
if probe.returncode == 0:
|
||||||
|
return
|
||||||
|
step(f"安装构建依赖 {package}")
|
||||||
|
run([str(VENV_PYTHON), "-m", "pip", "install", package], cwd=ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- Node.js
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_node() -> str:
|
||||||
|
"""确保 Node.js / npm 可用,返回 npm 可执行路径。"""
|
||||||
|
npm = shutil.which("npm")
|
||||||
|
node = shutil.which("node")
|
||||||
|
if not npm or not node:
|
||||||
|
die(
|
||||||
|
"未检测到 Node.js / npm(构建前端需要)。",
|
||||||
|
"请安装 Node.js 18+:https://nodejs.org/ ,安装后重新打开终端再运行。",
|
||||||
|
)
|
||||||
|
return npm
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_frontend_deps() -> None:
|
||||||
|
npm = ensure_node()
|
||||||
|
if (FRONTEND_DIR / "node_modules").exists():
|
||||||
|
return
|
||||||
|
step("安装前端依赖(npm install)")
|
||||||
|
run([npm, "install"], cwd=FRONTEND_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
def build_frontend(api_base: str | None = None) -> None:
|
||||||
|
npm = ensure_node()
|
||||||
|
ensure_frontend_deps()
|
||||||
|
step("构建前端(npm run build)")
|
||||||
|
env = os.environ.copy()
|
||||||
|
if api_base:
|
||||||
|
env["VITE_API_BASE"] = api_base
|
||||||
|
else:
|
||||||
|
env.pop("VITE_API_BASE", None)
|
||||||
|
completed = subprocess.run([npm, "run", "build"], cwd=str(FRONTEND_DIR), env=env)
|
||||||
|
if completed.returncode != 0:
|
||||||
|
die("前端构建失败(npm run build)")
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- Rust / Tauri
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_rust() -> None:
|
||||||
|
"""确保 Rust 工具链(cargo + rustc)可用(编译 Tauri 桌面端需要)。"""
|
||||||
|
if shutil.which("cargo") and shutil.which("rustc"):
|
||||||
|
return
|
||||||
|
die(
|
||||||
|
"未检测到 Rust 工具链(cargo / rustc),编译 Tauri 桌面端需要。",
|
||||||
|
"请安装 Rust:https://www.rust-lang.org/tools/install ,安装后重新打开终端再运行。",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rust_host_triple() -> str:
|
||||||
|
output = subprocess.run(["rustc", "-vV"], capture_output=True, text=True)
|
||||||
|
for line in output.stdout.splitlines():
|
||||||
|
if line.startswith("host:"):
|
||||||
|
return line.split(":", 1)[1].strip()
|
||||||
|
die("无法识别 Rust host triple(rustc -vV)")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_tauri_cli() -> None:
|
||||||
|
probe = subprocess.run(["cargo", "tauri", "--version"], capture_output=True)
|
||||||
|
if probe.returncode == 0:
|
||||||
|
return
|
||||||
|
step("安装 Tauri CLI(cargo install tauri-cli)")
|
||||||
|
run(["cargo", "install", "tauri-cli", "--locked"])
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- PyInstaller
|
||||||
|
|
||||||
|
PYINSTALLER_SPEC = PACKAGING_DIR / "capareport-server.spec"
|
||||||
|
|
||||||
|
|
||||||
|
def build_server_binary(python: Path, onefile: bool, work_dir: Path, dist_dir: Path) -> Path:
|
||||||
|
"""用 PyInstaller 打包后端为可执行文件。
|
||||||
|
|
||||||
|
onefile=True 返回单文件可执行文件路径;onefile=False 返回 onedir 目录路径。
|
||||||
|
"""
|
||||||
|
remove_path(work_dir)
|
||||||
|
remove_path(dist_dir)
|
||||||
|
work_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
dist_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
if onefile:
|
||||||
|
env["CAPAREPORT_ONEFILE"] = "1"
|
||||||
|
else:
|
||||||
|
env.pop("CAPAREPORT_ONEFILE", None)
|
||||||
|
|
||||||
|
step(f"PyInstaller 打包后端({'onefile' if onefile else 'onedir'})")
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
str(python), "-m", "PyInstaller", "--clean", "--noconfirm",
|
||||||
|
"--workpath", str(work_dir), "--distpath", str(dist_dir), str(PYINSTALLER_SPEC),
|
||||||
|
],
|
||||||
|
cwd=ROOT,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
if onefile:
|
||||||
|
binary = dist_dir / server_exe_name()
|
||||||
|
if not binary.exists():
|
||||||
|
die(f"未找到 PyInstaller 单文件产物:{binary}")
|
||||||
|
return binary
|
||||||
|
|
||||||
|
out_dir = dist_dir / SERVER_NAME
|
||||||
|
if not out_dir.exists():
|
||||||
|
die(f"未找到 PyInstaller onedir 产物:{out_dir}")
|
||||||
|
return out_dir
|
||||||
|
|
||||||
|
|
||||||
|
def build_tauri_sidecar(python: Path, work_dir: Path, dist_dir: Path) -> Path:
|
||||||
|
"""构建桌面端 sidecar(onefile)并按 Rust host triple 命名复制到 src-tauri/binaries。"""
|
||||||
|
binary = build_server_binary(python, onefile=True, work_dir=work_dir, dist_dir=dist_dir)
|
||||||
|
triple = rust_host_triple()
|
||||||
|
sidecar_dir = SRC_TAURI_DIR / "binaries"
|
||||||
|
sidecar_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
suffix = ".exe" if host_platform() == "windows" else ""
|
||||||
|
target = sidecar_dir / f"{SERVER_NAME}-{triple}{suffix}"
|
||||||
|
shutil.copy2(binary, target)
|
||||||
|
info(f"sidecar -> {target}")
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- Docker
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_docker() -> None:
|
||||||
|
if not shutil.which("docker"):
|
||||||
|
die(
|
||||||
|
"未检测到 Docker,构建镜像需要。",
|
||||||
|
"请安装 Docker Desktop / Docker Engine:https://docs.docker.com/get-docker/",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- 清理助手
|
||||||
|
|
||||||
|
|
||||||
|
def remove_path(path: Path) -> bool:
|
||||||
|
"""安全删除文件或目录,仅允许删除工作区内路径。"""
|
||||||
|
try:
|
||||||
|
resolved = path.resolve()
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
if ROOT not in resolved.parents and resolved != ROOT:
|
||||||
|
warn(f"跳过工作区外路径:{resolved}")
|
||||||
|
return False
|
||||||
|
if not resolved.exists():
|
||||||
|
return False
|
||||||
|
if resolved.is_dir():
|
||||||
|
shutil.rmtree(resolved, ignore_errors=True)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
resolved.unlink()
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
@echo off
|
|
||||||
setlocal
|
|
||||||
|
|
||||||
set SCRIPT_DIR=%~dp0
|
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%build.ps1" %*
|
|
||||||
exit /b %ERRORLEVEL%
|
|
||||||
@@ -1,523 +0,0 @@
|
|||||||
param(
|
|
||||||
[ValidateSet("server", "desktop", "docker", "all")]
|
|
||||||
[string]$Target = "all",
|
|
||||||
|
|
||||||
[ValidateSet("current", "windows", "linux", "macos")]
|
|
||||||
[string]$Platform = "current",
|
|
||||||
|
|
||||||
[switch]$SkipDockerBuild,
|
|
||||||
[switch]$SkipDesktopBuild,
|
|
||||||
[switch]$NoArchive,
|
|
||||||
[switch]$Clean,
|
|
||||||
[Alias("h", "?")]
|
|
||||||
[switch]$Help
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
$ProgressPreference = "SilentlyContinue"
|
|
||||||
|
|
||||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
||||||
$Root = (Resolve-Path (Join-Path $ScriptDir "..")).Path
|
|
||||||
$DistDir = Join-Path $Root "dist"
|
|
||||||
$ServerDistDir = Join-Path $DistDir "server"
|
|
||||||
$DesktopDistDir = Join-Path $DistDir "desktop"
|
|
||||||
$DockerDistDir = Join-Path $DistDir "docker"
|
|
||||||
$TempDir = Join-Path $DistDir ".tmp"
|
|
||||||
$PyInstallerDir = Join-Path $TempDir "pyinstaller"
|
|
||||||
$FrontendDir = Join-Path $Root "frontend"
|
|
||||||
$PackagingDir = Join-Path $Root "packaging"
|
|
||||||
$ServerName = "capareport-server"
|
|
||||||
$DesktopApiBase = "http://127.0.0.1:9081"
|
|
||||||
|
|
||||||
function Write-Step($Message) {
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "==> $Message" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-Info($Message) {
|
|
||||||
Write-Host " $Message"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Show-Usage {
|
|
||||||
Write-Host "Usage: scripts\build.bat [server|desktop|docker|all] [options]"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "Options:"
|
|
||||||
Write-Host " -Platform current|windows|linux|macos"
|
|
||||||
Write-Host " -NoArchive"
|
|
||||||
Write-Host " -Clean"
|
|
||||||
Write-Host " -SkipDockerBuild"
|
|
||||||
Write-Host " -SkipDesktopBuild"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Assert-InWorkspace($Path) {
|
|
||||||
$resolved = (Resolve-Path -LiteralPath $Path).Path
|
|
||||||
if (!$resolved.StartsWith($Root, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
||||||
throw "Refusing to operate outside workspace: $resolved"
|
|
||||||
}
|
|
||||||
return $resolved
|
|
||||||
}
|
|
||||||
|
|
||||||
function Remove-Tree($Path) {
|
|
||||||
if (Test-Path -LiteralPath $Path) {
|
|
||||||
$resolved = Assert-InWorkspace $Path
|
|
||||||
Remove-Item -LiteralPath $resolved -Recurse -Force
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-Checked($Command, $WorkingDirectory = $Root) {
|
|
||||||
Write-Info $Command
|
|
||||||
Push-Location $WorkingDirectory
|
|
||||||
try {
|
|
||||||
cmd /c $Command
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "Command failed with exit code $LASTEXITCODE`: $Command"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-CurrentPlatform {
|
|
||||||
if ($Platform -ne "current") {
|
|
||||||
return $Platform
|
|
||||||
}
|
|
||||||
if ($env:OS -eq "Windows_NT") {
|
|
||||||
return "windows"
|
|
||||||
}
|
|
||||||
$uname = (uname -s 2>$null)
|
|
||||||
if ($uname -eq "Darwin") {
|
|
||||||
return "macos"
|
|
||||||
}
|
|
||||||
return "linux"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-ExecutableName($PlatformName) {
|
|
||||||
if ($PlatformName -eq "windows") {
|
|
||||||
return "$ServerName.exe"
|
|
||||||
}
|
|
||||||
return $ServerName
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-RustHostTriple {
|
|
||||||
$output = rustc -vV
|
|
||||||
foreach ($line in $output) {
|
|
||||||
if ($line -like "host:*") {
|
|
||||||
return $line.Substring(5).Trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw "Unable to detect Rust host triple"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Test-TauriCli {
|
|
||||||
cmd /c "cargo tauri --version >nul 2>nul"
|
|
||||||
return $LASTEXITCODE -eq 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function Ensure-PythonEnvironment {
|
|
||||||
Write-Step "Preparing Python environment"
|
|
||||||
$venvPython = Join-Path $Root ".venv\Scripts\python.exe"
|
|
||||||
if (!(Test-Path $venvPython)) {
|
|
||||||
Invoke-Checked "uv venv" $Root
|
|
||||||
}
|
|
||||||
Invoke-Checked "uv pip install -r requirements.txt" $Root
|
|
||||||
Invoke-Checked "uv pip install pyinstaller" $Root
|
|
||||||
}
|
|
||||||
|
|
||||||
function Build-Frontend($ApiBase) {
|
|
||||||
Write-Step "Building frontend"
|
|
||||||
if (!(Test-Path (Join-Path $FrontendDir "node_modules"))) {
|
|
||||||
Invoke-Checked "npm ci" $FrontendDir
|
|
||||||
}
|
|
||||||
|
|
||||||
$previousApiBase = $env:VITE_API_BASE
|
|
||||||
if ($ApiBase) {
|
|
||||||
$env:VITE_API_BASE = $ApiBase
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Remove-Item Env:\VITE_API_BASE -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Invoke-Checked "npm run build" $FrontendDir
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
if ($null -ne $previousApiBase) {
|
|
||||||
$env:VITE_API_BASE = $previousApiBase
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Remove-Item Env:\VITE_API_BASE -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Build-ServerBinary([switch]$OneFile) {
|
|
||||||
Ensure-PythonEnvironment
|
|
||||||
Write-Step "Building server binary"
|
|
||||||
Remove-Tree $PyInstallerDir
|
|
||||||
New-Item -ItemType Directory -Force -Path $PyInstallerDir | Out-Null
|
|
||||||
|
|
||||||
$python = Join-Path $Root ".venv\Scripts\python.exe"
|
|
||||||
$workPath = Join-Path $PyInstallerDir "work"
|
|
||||||
$distPath = Join-Path $PyInstallerDir "dist"
|
|
||||||
$specPath = Join-Path $PackagingDir "capareport-server.spec"
|
|
||||||
$previousOneFile = $env:CAPAREPORT_ONEFILE
|
|
||||||
|
|
||||||
if ($OneFile) {
|
|
||||||
$env:CAPAREPORT_ONEFILE = "1"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Remove-Item Env:\CAPAREPORT_ONEFILE -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Invoke-Checked "`"$python`" -m PyInstaller --clean --noconfirm --workpath `"$workPath`" --distpath `"$distPath`" `"$specPath`"" $Root
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
if ($null -ne $previousOneFile) {
|
|
||||||
$env:CAPAREPORT_ONEFILE = $previousOneFile
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Remove-Item Env:\CAPAREPORT_ONEFILE -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($OneFile) {
|
|
||||||
$binaryDir = $distPath
|
|
||||||
$binaryPath = Join-Path $binaryDir (Get-ExecutableName (Get-CurrentPlatform))
|
|
||||||
if (!(Test-Path $binaryPath)) {
|
|
||||||
throw "PyInstaller output not found: $binaryPath"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$binaryDir = Join-Path $distPath $ServerName
|
|
||||||
if (!(Test-Path $binaryDir)) {
|
|
||||||
throw "PyInstaller output not found: $binaryDir"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $binaryDir
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-TextFile($Path, $Content, [switch]$Lf) {
|
|
||||||
$parent = Split-Path -Parent $Path
|
|
||||||
New-Item -ItemType Directory -Force -Path $parent | Out-Null
|
|
||||||
if ($Lf) {
|
|
||||||
[System.IO.File]::WriteAllText($Path, $Content.Replace("`r`n", "`n"), [System.Text.UTF8Encoding]::new($false))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[System.IO.File]::WriteAllText($Path, $Content, [System.Text.UTF8Encoding]::new($false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Convert-ToNsisPath($Path) {
|
|
||||||
return (Resolve-Path -LiteralPath $Path).Path
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-TauriNsisTemplatePath {
|
|
||||||
$registryDir = Join-Path $env:USERPROFILE ".cargo\registry\src"
|
|
||||||
if (!(Test-Path $registryDir)) {
|
|
||||||
throw "Cargo registry not found: $registryDir"
|
|
||||||
}
|
|
||||||
|
|
||||||
$templates = Get-ChildItem -Path $registryDir -Recurse -Filter "installer.nsi" |
|
|
||||||
Where-Object { $_.FullName -match "tauri-bundler-[^\\]+\\src\\bundle\\windows\\nsis\\installer\.nsi$" } |
|
|
||||||
Sort-Object FullName -Descending
|
|
||||||
|
|
||||||
if (!$templates) {
|
|
||||||
throw "Tauri NSIS template not found. Run cargo tauri build once to fetch tauri-bundler."
|
|
||||||
}
|
|
||||||
|
|
||||||
return $templates[0].FullName
|
|
||||||
}
|
|
||||||
|
|
||||||
function New-DesktopNsisTemplate {
|
|
||||||
$source = Get-TauriNsisTemplatePath
|
|
||||||
$target = Join-Path $TempDir "tauri-nsis-installer.nsi"
|
|
||||||
$hookPath = Convert-ToNsisPath (Join-Path $Root "src-tauri\windows\nsis-hooks.nsh")
|
|
||||||
$content = [System.IO.File]::ReadAllText($source)
|
|
||||||
|
|
||||||
$content = [regex]::Replace(
|
|
||||||
$content,
|
|
||||||
'\{\{#if installer_hooks\}\}\s*!include "\{\{installer_hooks\}\}"\s*\{\{/if\}\}',
|
|
||||||
"!include `"$hookPath`""
|
|
||||||
)
|
|
||||||
|
|
||||||
$restoreLine = " Call RestorePreviousInstallLocation"
|
|
||||||
$installDirOverride = @'
|
|
||||||
Call RestorePreviousInstallLocation
|
|
||||||
|
|
||||||
!if "${INSTALLMODE}" == "perMachine"
|
|
||||||
${If} ${FileExists} "D:\*.*"
|
|
||||||
StrCpy $INSTDIR "D:\Program Files\${PRODUCTNAME}"
|
|
||||||
${Else}
|
|
||||||
${If} ${RunningX64}
|
|
||||||
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
|
|
||||||
${Else}
|
|
||||||
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
|
|
||||||
${EndIf}
|
|
||||||
${EndIf}
|
|
||||||
!endif
|
|
||||||
'@
|
|
||||||
|
|
||||||
if (!$content.Contains($restoreLine)) {
|
|
||||||
throw "Tauri NSIS template changed: RestorePreviousInstallLocation marker not found"
|
|
||||||
}
|
|
||||||
$content = $content.Replace($restoreLine, $installDirOverride)
|
|
||||||
|
|
||||||
$onInitTail = @'
|
|
||||||
!if "${INSTALLMODE}" == "both"
|
|
||||||
!insertmacro MULTIUSER_INIT
|
|
||||||
!endif
|
|
||||||
FunctionEnd
|
|
||||||
'@
|
|
||||||
$onInitOverride = @'
|
|
||||||
!if "${INSTALLMODE}" == "both"
|
|
||||||
!insertmacro MULTIUSER_INIT
|
|
||||||
!endif
|
|
||||||
|
|
||||||
!if "${INSTALLMODE}" == "perMachine"
|
|
||||||
${If} ${FileExists} "D:\*.*"
|
|
||||||
StrCpy $INSTDIR "D:\Program Files\${PRODUCTNAME}"
|
|
||||||
${Else}
|
|
||||||
${If} ${RunningX64}
|
|
||||||
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
|
|
||||||
${Else}
|
|
||||||
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
|
|
||||||
${EndIf}
|
|
||||||
${EndIf}
|
|
||||||
!endif
|
|
||||||
FunctionEnd
|
|
||||||
'@
|
|
||||||
if (!$content.Contains($onInitTail)) {
|
|
||||||
throw "Tauri NSIS template changed: .onInit tail marker not found"
|
|
||||||
}
|
|
||||||
$content = $content.Replace($onInitTail, $onInitOverride)
|
|
||||||
|
|
||||||
$restoreFunction = @'
|
|
||||||
Function RestorePreviousInstallLocation
|
|
||||||
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
|
|
||||||
StrCmp $4 "" +2 0
|
|
||||||
StrCpy $INSTDIR $4
|
|
||||||
FunctionEnd
|
|
||||||
'@
|
|
||||||
$restoreOverride = @'
|
|
||||||
Function RestorePreviousInstallLocation
|
|
||||||
!if "${INSTALLMODE}" == "perMachine"
|
|
||||||
${If} ${FileExists} "D:\*.*"
|
|
||||||
StrCpy $INSTDIR "D:\Program Files\${PRODUCTNAME}"
|
|
||||||
Return
|
|
||||||
${EndIf}
|
|
||||||
!endif
|
|
||||||
|
|
||||||
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
|
|
||||||
StrCmp $4 "" +2 0
|
|
||||||
StrCpy $INSTDIR $4
|
|
||||||
FunctionEnd
|
|
||||||
'@
|
|
||||||
if (!$content.Contains($restoreFunction)) {
|
|
||||||
throw "Tauri NSIS template changed: RestorePreviousInstallLocation function marker not found"
|
|
||||||
}
|
|
||||||
$content = $content.Replace($restoreFunction, $restoreOverride)
|
|
||||||
|
|
||||||
Write-TextFile $target $content -Lf
|
|
||||||
return $target
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-WithDesktopTauriConfig($NsisTemplatePath, [scriptblock]$Action) {
|
|
||||||
$configPath = Join-Path $Root "src-tauri\tauri.conf.json"
|
|
||||||
$original = [System.IO.File]::ReadAllText($configPath)
|
|
||||||
try {
|
|
||||||
$config = $original | ConvertFrom-Json
|
|
||||||
$config.bundle.windows.nsis | Add-Member -NotePropertyName "template" -NotePropertyValue $NsisTemplatePath -Force
|
|
||||||
Write-TextFile $configPath ($config | ConvertTo-Json -Depth 50)
|
|
||||||
& $Action
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
[System.IO.File]::WriteAllText($configPath, $original, [System.Text.UTF8Encoding]::new($false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Copy-RuntimeFiles($OutputDir) {
|
|
||||||
Copy-Item -Path (Join-Path $Root "Configure.json") -Destination (Join-Path $OutputDir "Configure.json") -Force
|
|
||||||
Copy-Item -Path (Join-Path $Root "ReportScript.sql") -Destination (Join-Path $OutputDir "ReportScript.sql") -Force
|
|
||||||
New-Item -ItemType Directory -Force -Path (Join-Path $OutputDir "cache") | Out-Null
|
|
||||||
New-Item -ItemType Directory -Force -Path (Join-Path $OutputDir "logs") | Out-Null
|
|
||||||
|
|
||||||
$frontendDist = Join-Path $FrontendDir "dist"
|
|
||||||
if (!(Test-Path $frontendDist)) {
|
|
||||||
throw "Frontend dist not found: $frontendDist"
|
|
||||||
}
|
|
||||||
New-Item -ItemType Directory -Force -Path (Join-Path $OutputDir "frontend") | Out-Null
|
|
||||||
Copy-Item -Path $frontendDist -Destination (Join-Path $OutputDir "frontend") -Recurse -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-ServerLaunchers($OutputDir, $PlatformName) {
|
|
||||||
$exe = Get-ExecutableName $PlatformName
|
|
||||||
$bat = @"
|
|
||||||
@echo off
|
|
||||||
setlocal
|
|
||||||
cd /d "%~dp0"
|
|
||||||
if not exist cache mkdir cache
|
|
||||||
if not exist logs mkdir logs
|
|
||||||
echo Starting CapacityReport server...
|
|
||||||
echo URL: http://localhost:9081
|
|
||||||
"%~dp0$exe" --host 0.0.0.0 --port 9081
|
|
||||||
pause
|
|
||||||
"@
|
|
||||||
Write-TextFile (Join-Path $OutputDir "run.bat") $bat
|
|
||||||
|
|
||||||
$sh = @'
|
|
||||||
#!/usr/bin/env sh
|
|
||||||
set -eu
|
|
||||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
mkdir -p cache logs
|
|
||||||
chmod +x "./__SERVER_EXE__" 2>/dev/null || true
|
|
||||||
echo "Starting CapacityReport server..."
|
|
||||||
echo "URL: http://localhost:9081"
|
|
||||||
exec "./__SERVER_EXE__" --host 0.0.0.0 --port 9081
|
|
||||||
'@
|
|
||||||
$sh = $sh.Replace("__SERVER_EXE__", $exe)
|
|
||||||
Write-TextFile (Join-Path $OutputDir "start.sh") $sh -Lf
|
|
||||||
}
|
|
||||||
|
|
||||||
function Build-ServerPortable {
|
|
||||||
$platformName = Get-CurrentPlatform
|
|
||||||
if ($platformName -ne "windows" -and $env:OS -eq "Windows_NT") {
|
|
||||||
Write-Host "Native $platformName portable packages must be built on $platformName." -ForegroundColor Yellow
|
|
||||||
Write-Host "The Windows build will continue for the current host." -ForegroundColor Yellow
|
|
||||||
$platformName = "windows"
|
|
||||||
}
|
|
||||||
|
|
||||||
$outputDir = Join-Path $ServerDistDir "CapacityReport-Server-$platformName-x64"
|
|
||||||
$archive = "$outputDir.zip"
|
|
||||||
Remove-Tree $outputDir
|
|
||||||
Remove-Item -LiteralPath $archive -Force -ErrorAction SilentlyContinue
|
|
||||||
|
|
||||||
Build-Frontend $null
|
|
||||||
$binaryDir = Build-ServerBinary
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
|
|
||||||
|
|
||||||
Copy-Item -Path (Join-Path $binaryDir "*") -Destination $outputDir -Recurse -Force
|
|
||||||
Copy-RuntimeFiles $outputDir
|
|
||||||
Write-ServerLaunchers $outputDir $platformName
|
|
||||||
|
|
||||||
if (!$NoArchive) {
|
|
||||||
Compress-Archive -Path (Join-Path $outputDir "*") -DestinationPath $archive -Force
|
|
||||||
Write-Info "Archive: $archive"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Info "Server package: $outputDir"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Copy-DockerBundle {
|
|
||||||
Remove-Tree $DockerDistDir
|
|
||||||
New-Item -ItemType Directory -Force -Path $DockerDistDir | Out-Null
|
|
||||||
New-Item -ItemType Directory -Force -Path (Join-Path $DockerDistDir "cache") | Out-Null
|
|
||||||
New-Item -ItemType Directory -Force -Path (Join-Path $DockerDistDir "logs") | Out-Null
|
|
||||||
Copy-Item -Path (Join-Path $PackagingDir "docker-compose.yml") -Destination (Join-Path $DockerDistDir "docker-compose.yml") -Force
|
|
||||||
Copy-Item -Path (Join-Path $PackagingDir "mysql") -Destination (Join-Path $DockerDistDir "mysql") -Recurse -Force
|
|
||||||
Copy-Item -Path (Join-Path $Root "Configure.json") -Destination (Join-Path $DockerDistDir "Configure.json") -Force
|
|
||||||
Copy-Item -Path (Join-Path $Root "ReportScript.sql") -Destination (Join-Path $DockerDistDir "ReportScript.sql") -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
function Build-DockerImage {
|
|
||||||
Remove-Tree $DockerDistDir
|
|
||||||
|
|
||||||
if ($SkipDockerBuild) {
|
|
||||||
Write-Host "Docker image build skipped."
|
|
||||||
Copy-DockerBundle
|
|
||||||
Invoke-Checked "docker compose -f `"$DockerDistDir\docker-compose.yml`" config" $Root
|
|
||||||
Write-Info "Docker package: $DockerDistDir"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Step "Building Docker image"
|
|
||||||
Invoke-Checked "docker build --progress=plain -f `"$PackagingDir\Dockerfile`" -t capacity-report-app:latest ." $Root
|
|
||||||
|
|
||||||
Write-Step "Packaging Docker output"
|
|
||||||
Copy-DockerBundle
|
|
||||||
Invoke-Checked "docker save -o `"$DockerDistDir\capacity-report-app-latest.tar`" capacity-report-app:latest" $Root
|
|
||||||
Invoke-Checked "docker compose -f `"$DockerDistDir\docker-compose.yml`" config" $Root
|
|
||||||
Write-Info "Docker package: $DockerDistDir"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Build-DesktopPackage {
|
|
||||||
if ($SkipDesktopBuild) {
|
|
||||||
Write-Host "Desktop build skipped."
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(Get-Command rustc -ErrorAction SilentlyContinue)) {
|
|
||||||
throw "Rust is required for the Tauri desktop build"
|
|
||||||
}
|
|
||||||
|
|
||||||
Remove-Tree $DesktopDistDir
|
|
||||||
|
|
||||||
Build-Frontend $DesktopApiBase
|
|
||||||
$binaryDir = Build-ServerBinary -OneFile
|
|
||||||
$platformName = Get-CurrentPlatform
|
|
||||||
$exe = Get-ExecutableName $platformName
|
|
||||||
$triple = Get-RustHostTriple
|
|
||||||
$sidecarDir = Join-Path $Root "src-tauri\binaries"
|
|
||||||
New-Item -ItemType Directory -Force -Path $sidecarDir | Out-Null
|
|
||||||
$sidecarName = if ($platformName -eq "windows") { "$ServerName-$triple.exe" } else { "$ServerName-$triple" }
|
|
||||||
Copy-Item -Path (Join-Path $binaryDir $exe) -Destination (Join-Path $sidecarDir $sidecarName) -Force
|
|
||||||
|
|
||||||
if (!(Test-TauriCli)) {
|
|
||||||
Write-Step "Installing Tauri CLI"
|
|
||||||
Invoke-Checked "cargo install tauri-cli --locked" $Root
|
|
||||||
}
|
|
||||||
|
|
||||||
$nsisTemplate = New-DesktopNsisTemplate
|
|
||||||
Invoke-WithDesktopTauriConfig $nsisTemplate {
|
|
||||||
Remove-Tree (Join-Path $Root "src-tauri\target\release\bundle")
|
|
||||||
Write-Step "Building Tauri desktop package"
|
|
||||||
Invoke-Checked "cargo tauri build" (Join-Path $Root "src-tauri")
|
|
||||||
}
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Force -Path $DesktopDistDir | Out-Null
|
|
||||||
$bundleDir = Join-Path $Root "src-tauri\target\release\bundle"
|
|
||||||
Get-ChildItem -Path $bundleDir -Recurse -File -Include "*.msi", "*.exe" |
|
|
||||||
Copy-Item -Destination $DesktopDistDir -Force
|
|
||||||
Write-Info "Desktop package: $DesktopDistDir"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Clean-Intermediates {
|
|
||||||
Write-Step "Cleaning intermediate output"
|
|
||||||
Remove-Tree $TempDir
|
|
||||||
Remove-Tree (Join-Path $Root "frontend\dist")
|
|
||||||
Remove-Tree (Join-Path $Root "src-tauri\binaries")
|
|
||||||
Remove-Tree (Join-Path $Root "src-tauri\target")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Help) {
|
|
||||||
Show-Usage
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Clean) {
|
|
||||||
Write-Step "Cleaning dist output"
|
|
||||||
Remove-Tree $DistDir
|
|
||||||
Remove-Tree (Join-Path $Root "frontend\dist")
|
|
||||||
Remove-Tree (Join-Path $Root "src-tauri\binaries")
|
|
||||||
Remove-Tree (Join-Path $Root "src-tauri\target")
|
|
||||||
}
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Force -Path $DistDir | Out-Null
|
|
||||||
|
|
||||||
switch ($Target) {
|
|
||||||
"server" { Build-ServerPortable }
|
|
||||||
"docker" { Build-DockerImage }
|
|
||||||
"desktop" { Build-DesktopPackage }
|
|
||||||
"all" {
|
|
||||||
Build-ServerPortable
|
|
||||||
Build-DockerImage
|
|
||||||
Build-DesktopPackage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Clean-Intermediates
|
|
||||||
|
|
||||||
Write-Step "Build finished"
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
TARGET="${1:-server}"
|
|
||||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
||||||
ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
DIST_DIR="$ROOT/dist"
|
|
||||||
SERVER_DIST_DIR="$DIST_DIR/server"
|
|
||||||
DESKTOP_DIST_DIR="$DIST_DIR/desktop"
|
|
||||||
DOCKER_DIST_DIR="$DIST_DIR/docker"
|
|
||||||
TMP_DIR="$DIST_DIR/.tmp"
|
|
||||||
PYINSTALLER_DIR="$TMP_DIR/pyinstaller"
|
|
||||||
PACKAGING_DIR="$ROOT/packaging"
|
|
||||||
DESKTOP_API_BASE="http://127.0.0.1:9081"
|
|
||||||
SERVER_NAME="capareport-server"
|
|
||||||
cd "$ROOT"
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '\n==> %s\n' "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
run() {
|
|
||||||
printf ' %s\n' "$*"
|
|
||||||
"$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
platform_name() {
|
|
||||||
case "$(uname -s)" in
|
|
||||||
Darwin) printf 'macos' ;;
|
|
||||||
Linux) printf 'linux' ;;
|
|
||||||
*) printf 'windows' ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
server_exe_name() {
|
|
||||||
case "$(platform_name)" in
|
|
||||||
windows) printf '%s.exe' "$SERVER_NAME" ;;
|
|
||||||
*) printf '%s' "$SERVER_NAME" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
clean_intermediates() {
|
|
||||||
log "Cleaning intermediate output"
|
|
||||||
rm -rf "$TMP_DIR" "$ROOT/frontend/dist" "$ROOT/src-tauri/binaries" "$ROOT/src-tauri/target"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_python() {
|
|
||||||
log "Preparing Python environment"
|
|
||||||
if [ ! -x ".venv/bin/python" ]; then
|
|
||||||
run uv venv
|
|
||||||
fi
|
|
||||||
run uv pip install -r requirements.txt
|
|
||||||
run uv pip install pyinstaller
|
|
||||||
}
|
|
||||||
|
|
||||||
build_frontend() {
|
|
||||||
log "Building frontend"
|
|
||||||
if [ ! -d "frontend/node_modules" ]; then
|
|
||||||
(cd frontend && run npm ci)
|
|
||||||
fi
|
|
||||||
if [ "${1:-}" = "desktop" ]; then
|
|
||||||
(cd frontend && VITE_API_BASE="$DESKTOP_API_BASE" run npm run build)
|
|
||||||
else
|
|
||||||
(cd frontend && run npm run build)
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
build_server_binary() {
|
|
||||||
mode="${1:-onedir}"
|
|
||||||
ensure_python
|
|
||||||
log "Building server binary"
|
|
||||||
rm -rf "$PYINSTALLER_DIR"
|
|
||||||
mkdir -p "$PYINSTALLER_DIR"
|
|
||||||
|
|
||||||
if [ "$mode" = "onefile" ]; then
|
|
||||||
CAPAREPORT_ONEFILE=1
|
|
||||||
export CAPAREPORT_ONEFILE
|
|
||||||
run .venv/bin/python -m PyInstaller --clean --noconfirm \
|
|
||||||
--workpath "$PYINSTALLER_DIR/work" \
|
|
||||||
--distpath "$PYINSTALLER_DIR/dist" \
|
|
||||||
"$PACKAGING_DIR/capareport-server.spec"
|
|
||||||
unset CAPAREPORT_ONEFILE
|
|
||||||
else
|
|
||||||
run .venv/bin/python -m PyInstaller --clean --noconfirm \
|
|
||||||
--workpath "$PYINSTALLER_DIR/work" \
|
|
||||||
--distpath "$PYINSTALLER_DIR/dist" \
|
|
||||||
"$PACKAGING_DIR/capareport-server.spec"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
write_launchers() {
|
|
||||||
out_dir="$1"
|
|
||||||
exe_name="$2"
|
|
||||||
cat > "$out_dir/start.sh" <<EOF
|
|
||||||
#!/usr/bin/env sh
|
|
||||||
set -eu
|
|
||||||
SCRIPT_DIR="\$(CDPATH= cd -- "\$(dirname -- "\$0")" && pwd)"
|
|
||||||
cd "\$SCRIPT_DIR"
|
|
||||||
mkdir -p cache logs
|
|
||||||
chmod +x "./$exe_name" 2>/dev/null || true
|
|
||||||
echo "Starting CapacityReport server..."
|
|
||||||
echo "URL: http://localhost:9081"
|
|
||||||
exec "./$exe_name" --host 0.0.0.0 --port 9081
|
|
||||||
EOF
|
|
||||||
chmod +x "$out_dir/start.sh"
|
|
||||||
}
|
|
||||||
|
|
||||||
build_server() {
|
|
||||||
platform="$(platform_name)"
|
|
||||||
exe_name="$(server_exe_name)"
|
|
||||||
out_dir="$SERVER_DIST_DIR/CapacityReport-Server-$platform-x64"
|
|
||||||
rm -rf "$out_dir" "$SERVER_DIST_DIR/$(basename "$out_dir").tar.gz"
|
|
||||||
|
|
||||||
build_frontend server
|
|
||||||
build_server_binary onedir
|
|
||||||
|
|
||||||
mkdir -p "$out_dir/cache" "$out_dir/logs" "$out_dir/frontend"
|
|
||||||
cp -R "$PYINSTALLER_DIR/dist/$SERVER_NAME/." "$out_dir/"
|
|
||||||
cp Configure.json ReportScript.sql "$out_dir/"
|
|
||||||
cp -R frontend/dist "$out_dir/frontend/"
|
|
||||||
write_launchers "$out_dir" "$exe_name"
|
|
||||||
(cd "$SERVER_DIST_DIR" && tar -czf "$(basename "$out_dir").tar.gz" "$(basename "$out_dir")")
|
|
||||||
log "Server package: $out_dir"
|
|
||||||
}
|
|
||||||
|
|
||||||
copy_docker_bundle() {
|
|
||||||
rm -rf "$DOCKER_DIST_DIR"
|
|
||||||
mkdir -p "$DOCKER_DIST_DIR/cache" "$DOCKER_DIST_DIR/logs"
|
|
||||||
cp "$PACKAGING_DIR/docker-compose.yml" "$DOCKER_DIST_DIR/docker-compose.yml"
|
|
||||||
cp Configure.json ReportScript.sql "$DOCKER_DIST_DIR/"
|
|
||||||
cp -R "$PACKAGING_DIR/mysql" "$DOCKER_DIST_DIR/mysql"
|
|
||||||
}
|
|
||||||
|
|
||||||
build_docker() {
|
|
||||||
rm -rf "$DOCKER_DIST_DIR"
|
|
||||||
|
|
||||||
log "Building Docker image"
|
|
||||||
run docker build --progress=plain -f "$PACKAGING_DIR/Dockerfile" -t capacity-report-app:latest .
|
|
||||||
log "Packaging Docker output"
|
|
||||||
copy_docker_bundle
|
|
||||||
run docker save -o "$DOCKER_DIST_DIR/capacity-report-app-latest.tar" capacity-report-app:latest
|
|
||||||
run docker compose -f "$DOCKER_DIST_DIR/docker-compose.yml" config
|
|
||||||
log "Docker package: $DOCKER_DIST_DIR"
|
|
||||||
}
|
|
||||||
|
|
||||||
build_desktop() {
|
|
||||||
rm -rf "$DESKTOP_DIST_DIR"
|
|
||||||
|
|
||||||
build_frontend desktop
|
|
||||||
build_server_binary onefile
|
|
||||||
triple="$(rustc -vV | awk '/^host:/ {print $2}')"
|
|
||||||
mkdir -p src-tauri/binaries
|
|
||||||
cp "$PYINSTALLER_DIR/dist/$(server_exe_name)" "src-tauri/binaries/$SERVER_NAME-$triple"
|
|
||||||
if ! cargo tauri --version >/dev/null 2>&1; then
|
|
||||||
run cargo install tauri-cli --locked
|
|
||||||
fi
|
|
||||||
rm -rf src-tauri/target/release/bundle
|
|
||||||
(cd src-tauri && run cargo tauri build)
|
|
||||||
|
|
||||||
rm -rf "$DESKTOP_DIST_DIR"
|
|
||||||
mkdir -p "$DESKTOP_DIST_DIR"
|
|
||||||
find src-tauri/target/release/bundle -type f \( -name '*.msi' -o -name '*.exe' \) -exec cp {} "$DESKTOP_DIST_DIR/" \;
|
|
||||||
log "Desktop package: $DESKTOP_DIST_DIR"
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdir -p "$DIST_DIR"
|
|
||||||
|
|
||||||
case "$TARGET" in
|
|
||||||
server) build_server ;;
|
|
||||||
docker) build_docker ;;
|
|
||||||
desktop) build_desktop ;;
|
|
||||||
all) build_server; build_docker; build_desktop ;;
|
|
||||||
*) echo "Usage: scripts/build.sh [server|docker|desktop|all]" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
clean_intermediates
|
|
||||||
|
|
||||||
log "Build finished"
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""Docker 镜像编译 / 更新脚本(基于根目录 Dockerfile + docker/entrypoint.sh)。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python scripts/build_docker.py # 编译镜像并生成可部署离线包(dist/docker/)
|
||||||
|
python scripts/build_docker.py update # 重新编译镜像并就地更新本机运行的容器
|
||||||
|
python scripts/build_docker.py --no-save # 编译但不导出 tar(本机调试更快)
|
||||||
|
|
||||||
|
镜像 tag:capacity-report-app:latest。镜像内运行态数据落在 /data 数据卷,
|
||||||
|
首次启动由 docker/entrypoint.sh 播种默认 Configure.json / ReportScript.sql。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
import _env # noqa: E402
|
||||||
|
|
||||||
|
IMAGE_TAG = "capacity-report-app:latest"
|
||||||
|
DOCKERFILE = _env.ROOT / "Dockerfile"
|
||||||
|
COMPOSE_FILE = _env.PACKAGING_DIR / "docker-compose.yml"
|
||||||
|
BUNDLE_FILES = ["Configure.json", "ReportScript.sql", "CellData.sql"]
|
||||||
|
|
||||||
|
|
||||||
|
def build_image() -> None:
|
||||||
|
_env.ensure_docker()
|
||||||
|
_env.build_frontend()
|
||||||
|
_env.step("构建 Docker 镜像")
|
||||||
|
_env.run(
|
||||||
|
["docker", "build", "--progress=plain", "-f", str(DOCKERFILE), "-t", IMAGE_TAG, "."],
|
||||||
|
cwd=_env.ROOT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_bundle(save_tar: bool) -> Path:
|
||||||
|
out_dir = _env.DIST_DIR / "docker"
|
||||||
|
_env.remove_path(out_dir)
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(out_dir / "cache").mkdir(exist_ok=True)
|
||||||
|
(out_dir / "logs").mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
shutil.copy2(COMPOSE_FILE, out_dir / "docker-compose.yml")
|
||||||
|
shutil.copytree(_env.PACKAGING_DIR / "mysql", out_dir / "mysql", dirs_exist_ok=True)
|
||||||
|
for name in BUNDLE_FILES:
|
||||||
|
src = _env.ROOT / name
|
||||||
|
if src.exists():
|
||||||
|
shutil.copy2(src, out_dir / name)
|
||||||
|
|
||||||
|
if save_tar:
|
||||||
|
_env.step("导出镜像 tar")
|
||||||
|
_env.run(["docker", "save", "-o", str(out_dir / "capacity-report-app-latest.tar"), IMAGE_TAG])
|
||||||
|
|
||||||
|
_env.step(f"Docker 离线包:{out_dir}")
|
||||||
|
return out_dir
|
||||||
|
|
||||||
|
|
||||||
|
def update_running() -> None:
|
||||||
|
_env.step("更新本机运行的容器(docker compose up -d)")
|
||||||
|
_env.run(
|
||||||
|
["docker", "compose", "-f", str(COMPOSE_FILE), "up", "-d", "--force-recreate", "capacity-app"],
|
||||||
|
cwd=_env.PACKAGING_DIR,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
_env.setup_console()
|
||||||
|
parser = argparse.ArgumentParser(description="编译 / 更新 Docker 镜像")
|
||||||
|
parser.add_argument("action", nargs="?", choices=["build", "update"], default="build", help="build=编译并打包;update=编译并就地更新容器")
|
||||||
|
parser.add_argument("--no-save", action="store_true", help="编译镜像但不导出 tar")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
build_image()
|
||||||
|
|
||||||
|
if args.action == "update":
|
||||||
|
update_running()
|
||||||
|
else:
|
||||||
|
make_bundle(save_tar=not args.no_save)
|
||||||
|
|
||||||
|
_env.step("清理前端中间产物")
|
||||||
|
_env.remove_path(_env.FRONTEND_DIR / "dist")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
"""Server 便携版编译脚本:打包后端可执行文件 + 前端产物 + 配置 + 启动脚本。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python scripts/build_server.py # 编译当前系统的便携版
|
||||||
|
python scripts/build_server.py --no-archive # 不生成 zip 压缩包
|
||||||
|
|
||||||
|
产物:dist/server/CapacityReport-Server-<平台>-x64/ 及其 .zip。
|
||||||
|
注意:便携版需在目标系统原生构建(Windows 包在 Windows、Linux 包在 Linux、macOS 包在 macOS)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
import _env # noqa: E402
|
||||||
|
|
||||||
|
RUNTIME_FILES = ["Configure.json", "ReportScript.sql", "CellData.sql"]
|
||||||
|
|
||||||
|
RUN_BAT = """@echo off
|
||||||
|
setlocal
|
||||||
|
cd /d "%~dp0"
|
||||||
|
if not exist cache mkdir cache
|
||||||
|
if not exist logs mkdir logs
|
||||||
|
echo Starting CapacityReport server...
|
||||||
|
echo URL: http://localhost:9081
|
||||||
|
"%~dp0{exe}" --host 0.0.0.0 --port 9081
|
||||||
|
pause
|
||||||
|
"""
|
||||||
|
|
||||||
|
START_SH = """#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
mkdir -p cache logs
|
||||||
|
chmod +x "./{exe}" 2>/dev/null || true
|
||||||
|
echo "Starting CapacityReport server..."
|
||||||
|
echo "URL: http://localhost:9081"
|
||||||
|
exec "./{exe}" --host 0.0.0.0 --port 9081
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def write_launchers(out_dir: Path, platform: str) -> None:
|
||||||
|
exe = _env.server_exe_name(platform)
|
||||||
|
if platform == "windows":
|
||||||
|
(out_dir / "run.bat").write_text(RUN_BAT.format(exe=exe), encoding="utf-8")
|
||||||
|
else:
|
||||||
|
start = out_dir / "start.sh"
|
||||||
|
start.write_text(START_SH.format(exe=exe).replace("\r\n", "\n"), encoding="utf-8", newline="\n")
|
||||||
|
start.chmod(0o755)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
_env.setup_console()
|
||||||
|
parser = argparse.ArgumentParser(description="编译 Server 便携版")
|
||||||
|
parser.add_argument("--no-archive", action="store_true", help="不生成 zip 压缩包")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
platform = _env.host_platform()
|
||||||
|
python = _env.ensure_venv(extra_packages=["pyinstaller"])
|
||||||
|
_env.build_frontend()
|
||||||
|
|
||||||
|
tmp = _env.DIST_DIR / ".tmp" / "pyinstaller"
|
||||||
|
binary_dir = _env.build_server_binary(python, onefile=False, work_dir=tmp / "work", dist_dir=tmp / "dist")
|
||||||
|
|
||||||
|
out_name = f"CapacityReport-Server-{platform}-x64"
|
||||||
|
out_dir = _env.DIST_DIR / "server" / out_name
|
||||||
|
_env.remove_path(out_dir)
|
||||||
|
_env.remove_path(_env.DIST_DIR / "server" / f"{out_name}.zip")
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
_env.step("组装便携版目录")
|
||||||
|
shutil.copytree(binary_dir, out_dir, dirs_exist_ok=True)
|
||||||
|
for name in RUNTIME_FILES:
|
||||||
|
src = _env.ROOT / name
|
||||||
|
if src.exists():
|
||||||
|
shutil.copy2(src, out_dir / name)
|
||||||
|
(out_dir / "cache").mkdir(exist_ok=True)
|
||||||
|
(out_dir / "logs").mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
frontend_dist = _env.FRONTEND_DIR / "dist"
|
||||||
|
if not frontend_dist.exists():
|
||||||
|
_env.die(f"前端产物不存在:{frontend_dist}")
|
||||||
|
shutil.copytree(frontend_dist, out_dir / "frontend" / "dist", dirs_exist_ok=True)
|
||||||
|
|
||||||
|
write_launchers(out_dir, platform)
|
||||||
|
|
||||||
|
if not args.no_archive:
|
||||||
|
_env.step("生成压缩包")
|
||||||
|
archive = shutil.make_archive(str(out_dir), "zip", root_dir=out_dir)
|
||||||
|
_env.info(f"压缩包:{archive}")
|
||||||
|
|
||||||
|
_env.step("清理中间产物")
|
||||||
|
_env.remove_path(_env.DIST_DIR / ".tmp")
|
||||||
|
_env.remove_path(frontend_dist)
|
||||||
|
|
||||||
|
_env.step(f"便携版完成:{out_dir}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
"""Tauri 桌面版编译脚本。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python scripts/build_tauri.py # 编译当前系统的桌面版
|
||||||
|
python scripts/build_tauri.py --platform windows # 指定平台(windows/linux/macos)
|
||||||
|
|
||||||
|
特性:
|
||||||
|
- Windows 安装包内置 WebView2 离线安装器(tauri.conf.json 已配置 offlineInstaller),
|
||||||
|
适合无外网、未预装 WebView2 Runtime 的机器。
|
||||||
|
- Windows NSIS 安装器默认安装到 D:\\Program Files\\CapacityReport(无 D 盘时回落系统盘)。
|
||||||
|
- 跨平台限制:Tauri 无法可靠跨系统交叉编译,--platform 必须与当前系统一致,否则会提示需在目标系统本机构建。
|
||||||
|
|
||||||
|
产物:dist/desktop/ 下的安装包(Windows: .msi/.exe;Linux: .deb/.AppImage;macOS: .dmg)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
import _env # noqa: E402
|
||||||
|
|
||||||
|
TAURI_CONF = _env.SRC_TAURI_DIR / "tauri.conf.json"
|
||||||
|
NSIS_HOOK = _env.SRC_TAURI_DIR / "windows" / "nsis-hooks.nsh"
|
||||||
|
|
||||||
|
INSTALLER_EXTS = {
|
||||||
|
"windows": (".msi", ".exe"),
|
||||||
|
"linux": (".deb", ".AppImage", ".rpm"),
|
||||||
|
"macos": (".dmg", ".app.tar.gz"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- NSIS 定制(Windows 默认装 D 盘)
|
||||||
|
|
||||||
|
_RESTORE_LINE = " Call RestorePreviousInstallLocation"
|
||||||
|
|
||||||
|
_INSTALL_DIR_OVERRIDE = """ Call RestorePreviousInstallLocation
|
||||||
|
|
||||||
|
!if "${INSTALLMODE}" == "perMachine"
|
||||||
|
${If} ${FileExists} "D:\\*.*"
|
||||||
|
StrCpy $INSTDIR "D:\\Program Files\\${PRODUCTNAME}"
|
||||||
|
${Else}
|
||||||
|
${If} ${RunningX64}
|
||||||
|
StrCpy $INSTDIR "$PROGRAMFILES64\\${PRODUCTNAME}"
|
||||||
|
${Else}
|
||||||
|
StrCpy $INSTDIR "$PROGRAMFILES\\${PRODUCTNAME}"
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ON_INIT_TAIL = """ !if "${INSTALLMODE}" == "both"
|
||||||
|
!insertmacro MULTIUSER_INIT
|
||||||
|
!endif
|
||||||
|
FunctionEnd"""
|
||||||
|
|
||||||
|
_ON_INIT_OVERRIDE = """ !if "${INSTALLMODE}" == "both"
|
||||||
|
!insertmacro MULTIUSER_INIT
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!if "${INSTALLMODE}" == "perMachine"
|
||||||
|
${If} ${FileExists} "D:\\*.*"
|
||||||
|
StrCpy $INSTDIR "D:\\Program Files\\${PRODUCTNAME}"
|
||||||
|
${Else}
|
||||||
|
${If} ${RunningX64}
|
||||||
|
StrCpy $INSTDIR "$PROGRAMFILES64\\${PRODUCTNAME}"
|
||||||
|
${Else}
|
||||||
|
StrCpy $INSTDIR "$PROGRAMFILES\\${PRODUCTNAME}"
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
FunctionEnd"""
|
||||||
|
|
||||||
|
_RESTORE_FUNCTION = """Function RestorePreviousInstallLocation
|
||||||
|
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
|
||||||
|
StrCmp $4 "" +2 0
|
||||||
|
StrCpy $INSTDIR $4
|
||||||
|
FunctionEnd"""
|
||||||
|
|
||||||
|
_RESTORE_OVERRIDE = """Function RestorePreviousInstallLocation
|
||||||
|
!if "${INSTALLMODE}" == "perMachine"
|
||||||
|
${If} ${FileExists} "D:\\*.*"
|
||||||
|
StrCpy $INSTDIR "D:\\Program Files\\${PRODUCTNAME}"
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
|
||||||
|
StrCmp $4 "" +2 0
|
||||||
|
StrCpy $INSTDIR $4
|
||||||
|
FunctionEnd"""
|
||||||
|
|
||||||
|
|
||||||
|
def find_nsis_template() -> Path:
|
||||||
|
registry = Path.home() / ".cargo" / "registry" / "src"
|
||||||
|
if not registry.exists():
|
||||||
|
_env.die(f"未找到 Cargo registry:{registry}", "先运行一次 cargo tauri build 拉取 tauri-bundler。")
|
||||||
|
candidates = [
|
||||||
|
path
|
||||||
|
for path in registry.rglob("installer.nsi")
|
||||||
|
if "tauri-bundler-" in str(path) and path.parts[-4:] == ("bundle", "windows", "nsis", "installer.nsi")
|
||||||
|
]
|
||||||
|
if not candidates:
|
||||||
|
_env.die("未找到 Tauri NSIS 模板(installer.nsi)", "先运行一次 cargo tauri build 拉取 tauri-bundler。")
|
||||||
|
return sorted(candidates, reverse=True)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def make_nsis_template(tmp_dir: Path) -> Path:
|
||||||
|
source = find_nsis_template()
|
||||||
|
content = source.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
hook_path = str(NSIS_HOOK.resolve())
|
||||||
|
content, n = re.subn(
|
||||||
|
r'\{\{#if installer_hooks\}\}\s*!include "\{\{installer_hooks\}\}"\s*\{\{/if\}\}',
|
||||||
|
f'!include "{hook_path}"',
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
if n == 0:
|
||||||
|
_env.die("Tauri NSIS 模板已变化:installer_hooks 标记未找到")
|
||||||
|
|
||||||
|
for marker, replacement, label in (
|
||||||
|
(_RESTORE_LINE, _INSTALL_DIR_OVERRIDE, "RestorePreviousInstallLocation 调用点"),
|
||||||
|
(_ON_INIT_TAIL, _ON_INIT_OVERRIDE, ".onInit 结尾"),
|
||||||
|
(_RESTORE_FUNCTION, _RESTORE_OVERRIDE, "RestorePreviousInstallLocation 函数"),
|
||||||
|
):
|
||||||
|
if marker not in content:
|
||||||
|
_env.die(f"Tauri NSIS 模板已变化:{label}标记未找到")
|
||||||
|
content = content.replace(marker, replacement)
|
||||||
|
|
||||||
|
target = tmp_dir / "tauri-nsis-installer.nsi"
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target.write_text(content.replace("\r\n", "\n"), encoding="utf-8", newline="\n")
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
def build_with_windows_nsis(tmp_dir: Path) -> None:
|
||||||
|
template = make_nsis_template(tmp_dir)
|
||||||
|
original = TAURI_CONF.read_text(encoding="utf-8")
|
||||||
|
try:
|
||||||
|
config = json.loads(original)
|
||||||
|
config["bundle"]["windows"]["nsis"]["template"] = str(template)
|
||||||
|
TAURI_CONF.write_text(json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
_env.remove_path(_env.SRC_TAURI_DIR / "target" / "release" / "bundle")
|
||||||
|
_env.step("编译 Tauri 桌面版(cargo tauri build)")
|
||||||
|
_env.run(["cargo", "tauri", "build"], cwd=_env.SRC_TAURI_DIR)
|
||||||
|
finally:
|
||||||
|
TAURI_CONF.write_text(original, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def collect_installers(platform: str) -> None:
|
||||||
|
bundle_dir = _env.SRC_TAURI_DIR / "target" / "release" / "bundle"
|
||||||
|
out_dir = _env.DIST_DIR / "desktop"
|
||||||
|
_env.remove_path(out_dir)
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
exts = INSTALLER_EXTS[platform]
|
||||||
|
found = 0
|
||||||
|
for path in bundle_dir.rglob("*"):
|
||||||
|
if path.is_file() and path.name.endswith(exts):
|
||||||
|
shutil.copy2(path, out_dir / path.name)
|
||||||
|
found += 1
|
||||||
|
_env.info(f"安装包:{path.name}")
|
||||||
|
if found == 0:
|
||||||
|
_env.warn(f"未在 {bundle_dir} 找到安装包产物")
|
||||||
|
_env.step(f"桌面版完成:{out_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
_env.setup_console()
|
||||||
|
parser = argparse.ArgumentParser(description="编译 Tauri 桌面版")
|
||||||
|
parser.add_argument(
|
||||||
|
"--platform",
|
||||||
|
choices=["current", "windows", "linux", "macos"],
|
||||||
|
default="current",
|
||||||
|
help="目标平台(默认当前系统)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
host = _env.host_platform()
|
||||||
|
platform = host if args.platform == "current" else args.platform
|
||||||
|
if platform != host:
|
||||||
|
_env.die(
|
||||||
|
f"无法在 {host} 上交叉编译 {platform} 桌面版。",
|
||||||
|
f"Tauri 需要在目标系统本机构建,请在 {platform} 机器上运行本脚本。",
|
||||||
|
)
|
||||||
|
|
||||||
|
_env.ensure_rust()
|
||||||
|
python = _env.ensure_venv(extra_packages=["pyinstaller"])
|
||||||
|
_env.build_frontend(api_base=_env.DESKTOP_API_BASE)
|
||||||
|
|
||||||
|
tmp = _env.DIST_DIR / ".tmp"
|
||||||
|
_env.build_tauri_sidecar(python, tmp / "pyinstaller" / "work", tmp / "pyinstaller" / "dist")
|
||||||
|
_env.ensure_tauri_cli()
|
||||||
|
|
||||||
|
if platform == "windows":
|
||||||
|
build_with_windows_nsis(tmp)
|
||||||
|
else:
|
||||||
|
_env.remove_path(_env.SRC_TAURI_DIR / "target" / "release" / "bundle")
|
||||||
|
_env.step("编译 Tauri 桌面版(cargo tauri build)")
|
||||||
|
_env.run(["cargo", "tauri", "build"], cwd=_env.SRC_TAURI_DIR)
|
||||||
|
|
||||||
|
collect_installers(platform)
|
||||||
|
|
||||||
|
_env.step("清理中间产物")
|
||||||
|
_env.remove_path(tmp)
|
||||||
|
_env.remove_path(_env.FRONTEND_DIR / "dist")
|
||||||
|
_env.remove_path(_env.SRC_TAURI_DIR / "binaries")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"""清理脚本:清除缓存、__pycache__、编译产物和运行时临时数据,保持项目整洁。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python scripts/clean.py # 清理缓存 / 编译产物 / 运行时临时数据(保留 .venv 与前端依赖)
|
||||||
|
python scripts/clean.py --deep # 额外清理 .venv 与 frontend/node_modules(下次运行会重装)
|
||||||
|
|
||||||
|
默认不会删除源码、配置(Configure.json/*.sql)、授权文件或 .git。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
import _env # noqa: E402
|
||||||
|
|
||||||
|
# 遍历时跳过这些目录(既慢又无意义)
|
||||||
|
PRUNE_DIRS = {".git", ".venv", "node_modules"}
|
||||||
|
|
||||||
|
# 编译产物 / 缓存目录(相对项目根)
|
||||||
|
ARTIFACT_DIRS = [
|
||||||
|
"dist",
|
||||||
|
"frontend/dist",
|
||||||
|
"frontend/.vite",
|
||||||
|
"src-tauri/target",
|
||||||
|
"src-tauri/binaries",
|
||||||
|
".ruff_cache",
|
||||||
|
".pytest_cache",
|
||||||
|
".mypy_cache",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 运行时临时数据目录
|
||||||
|
RUNTIME_DIRS = ["cache", "logs", "uploads"]
|
||||||
|
|
||||||
|
DEEP_DIRS = [".venv", "frontend/node_modules"]
|
||||||
|
|
||||||
|
|
||||||
|
def clean_pycache() -> int:
|
||||||
|
removed = 0
|
||||||
|
for current, dirs, files in os.walk(_env.ROOT):
|
||||||
|
dirs[:] = [d for d in dirs if d not in PRUNE_DIRS]
|
||||||
|
for d in list(dirs):
|
||||||
|
if d == "__pycache__":
|
||||||
|
if _env.remove_path(Path(current) / d):
|
||||||
|
removed += 1
|
||||||
|
dirs.remove(d)
|
||||||
|
for f in files:
|
||||||
|
if f.endswith((".pyc", ".pyo")):
|
||||||
|
if _env.remove_path(Path(current) / f):
|
||||||
|
removed += 1
|
||||||
|
return removed
|
||||||
|
|
||||||
|
|
||||||
|
def clean_dirs(rel_dirs: list[str]) -> int:
|
||||||
|
removed = 0
|
||||||
|
for rel in rel_dirs:
|
||||||
|
if _env.remove_path(_env.ROOT / rel):
|
||||||
|
_env.info(f"已删除 {rel}")
|
||||||
|
removed += 1
|
||||||
|
return removed
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
_env.setup_console()
|
||||||
|
parser = argparse.ArgumentParser(description="清理缓存与编译产物")
|
||||||
|
parser.add_argument("--deep", action="store_true", help="额外清理 .venv 与 frontend/node_modules")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
_env.step("清理 __pycache__ / *.pyc")
|
||||||
|
n = clean_pycache()
|
||||||
|
_env.info(f"清理 {n} 项")
|
||||||
|
|
||||||
|
_env.step("清理编译产物")
|
||||||
|
clean_dirs(ARTIFACT_DIRS)
|
||||||
|
|
||||||
|
_env.step("清理运行时临时数据(cache / logs / uploads)")
|
||||||
|
clean_dirs(RUNTIME_DIRS)
|
||||||
|
|
||||||
|
if args.deep:
|
||||||
|
_env.step("深度清理依赖(.venv / node_modules)")
|
||||||
|
clean_dirs(DEEP_DIRS)
|
||||||
|
|
||||||
|
_env.step("清理完成")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
+26
-80
@@ -1,18 +1,18 @@
|
|||||||
"""CapacityReport standalone dev launcher.
|
"""开发测试脚本:同时启动后端(自动重载)和前端(Vite 热更新)。
|
||||||
|
|
||||||
Usage:
|
用法:
|
||||||
python dev.py
|
python scripts/dev.py
|
||||||
|
|
||||||
Starts two processes:
|
启动两个进程:
|
||||||
[api] CapacityReport API http://127.0.0.1:9081 (auto-reload)
|
[api] CapacityReport API http://127.0.0.1:9081 (自动重载)
|
||||||
[web] CapacityReport web http://127.0.0.1:5174 (vite HMR, proxy /api -> 9081)
|
[web] CapacityReport web http://127.0.0.1:5174 (Vite 热更新,/api 代理到 9081)
|
||||||
Press Ctrl+C to stop.
|
|
||||||
|
按 Ctrl+C 停止全部。首次运行会自动创建 .venv 并安装依赖、自动安装前端依赖。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -22,39 +22,24 @@ import urllib.error
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
IS_WINDOWS = os.name == "nt"
|
import _env # noqa: E402
|
||||||
VENV_DIR = ROOT / ".venv"
|
|
||||||
VENV_PYTHON = VENV_DIR / ("Scripts" if IS_WINDOWS else "bin") / ("python.exe" if IS_WINDOWS else "python")
|
|
||||||
REQUIREMENTS = ROOT / "requirements.txt"
|
|
||||||
API_PORT = 9081
|
API_PORT = 9081
|
||||||
WEB_PORT = 5174
|
WEB_PORT = 5174
|
||||||
|
|
||||||
SYSTEM_PYTHON_CANDIDATES = [
|
|
||||||
Path(r"D:\Program Files\uv\python\cpython-3.14.3-windows-x86_64-none\python.exe"),
|
|
||||||
Path(shutil.which("python3") or ""),
|
|
||||||
Path(shutil.which("python") or ""),
|
|
||||||
]
|
|
||||||
|
|
||||||
_processes: list[tuple[str, subprocess.Popen]] = []
|
_processes: list[tuple[str, subprocess.Popen]] = []
|
||||||
_stopping = threading.Event()
|
_stopping = threading.Event()
|
||||||
|
|
||||||
|
|
||||||
def log(name: str, message: str) -> None:
|
def log(name: str, message: str) -> None:
|
||||||
line = f"[{name}] {message}".rstrip("\r\n") + "\n"
|
print(f"[{name}] {message}".rstrip(), flush=True)
|
||||||
try:
|
|
||||||
sys.stdout.write(line)
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
encoding = sys.stdout.encoding or "utf-8"
|
|
||||||
sys.stdout.write(line.encode(encoding, "replace").decode(encoding, "replace"))
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def _stream(name: str, process: subprocess.Popen) -> None:
|
def _stream(name: str, process: subprocess.Popen) -> None:
|
||||||
assert process.stdout is not None
|
assert process.stdout is not None
|
||||||
for raw in iter(process.stdout.readline, b""):
|
for raw in iter(process.stdout.readline, b""):
|
||||||
line = raw.decode("utf-8", "replace").rstrip("\r\n")
|
log(name, raw.decode("utf-8", "replace").rstrip("\r\n"))
|
||||||
log(name, line)
|
|
||||||
|
|
||||||
|
|
||||||
def _spawn(name: str, args: list[str], cwd: Path, env: dict[str, str] | None = None) -> subprocess.Popen:
|
def _spawn(name: str, args: list[str], cwd: Path, env: dict[str, str] | None = None) -> subprocess.Popen:
|
||||||
@@ -64,7 +49,7 @@ def _spawn(name: str, args: list[str], cwd: Path, env: dict[str, str] | None = N
|
|||||||
"stderr": subprocess.STDOUT,
|
"stderr": subprocess.STDOUT,
|
||||||
"env": env,
|
"env": env,
|
||||||
}
|
}
|
||||||
if IS_WINDOWS:
|
if _env.IS_WINDOWS:
|
||||||
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP | getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP | getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||||
else:
|
else:
|
||||||
kwargs["start_new_session"] = True
|
kwargs["start_new_session"] = True
|
||||||
@@ -83,7 +68,7 @@ def _stop_all() -> None:
|
|||||||
continue
|
continue
|
||||||
log(name, "stopping...")
|
log(name, "stopping...")
|
||||||
try:
|
try:
|
||||||
if IS_WINDOWS:
|
if _env.IS_WINDOWS:
|
||||||
subprocess.run(["taskkill", "/F", "/T", "/PID", str(process.pid)], capture_output=True, check=False)
|
subprocess.run(["taskkill", "/F", "/T", "/PID", str(process.pid)], capture_output=True, check=False)
|
||||||
else:
|
else:
|
||||||
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
|
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
|
||||||
@@ -108,62 +93,22 @@ def _wait_for_http(proc_name: str, url: str, timeout: float = 90.0) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _find_system_python() -> Path | None:
|
|
||||||
for candidate in SYSTEM_PYTHON_CANDIDATES:
|
|
||||||
if candidate and candidate.exists():
|
|
||||||
return candidate
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_venv() -> Path:
|
|
||||||
if VENV_PYTHON.exists():
|
|
||||||
return VENV_PYTHON
|
|
||||||
|
|
||||||
base_python = _find_system_python()
|
|
||||||
if base_python is None:
|
|
||||||
log("dev", "no Python found; install Python or uv first")
|
|
||||||
raise SystemExit(1)
|
|
||||||
|
|
||||||
log("dev", f"creating venv with {base_python} ...")
|
|
||||||
subprocess.run([str(base_python), "-m", "venv", str(VENV_DIR)], check=True)
|
|
||||||
|
|
||||||
log("dev", "installing dependencies ...")
|
|
||||||
subprocess.run([str(VENV_PYTHON), "-m", "pip", "install", "-r", str(REQUIREMENTS)], cwd=str(ROOT), check=True)
|
|
||||||
|
|
||||||
return VENV_PYTHON
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_node_modules() -> None:
|
|
||||||
web_dir = ROOT / "frontend"
|
|
||||||
if (web_dir / "node_modules").exists():
|
|
||||||
return
|
|
||||||
npm = shutil.which("npm")
|
|
||||||
if not npm:
|
|
||||||
log("dev", "npm not found; install Node.js first")
|
|
||||||
raise SystemExit(1)
|
|
||||||
log("web", "installing frontend dependencies ...")
|
|
||||||
subprocess.run([npm, "install"], cwd=str(web_dir), shell=IS_WINDOWS, check=True)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
try:
|
_env.setup_console()
|
||||||
sys.stdout.reconfigure(errors="replace")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
python = _ensure_venv()
|
python = _env.ensure_venv()
|
||||||
_ensure_node_modules()
|
_env.ensure_frontend_deps()
|
||||||
|
|
||||||
node = shutil.which("node")
|
node = _env.which("node")
|
||||||
vite_bin = ROOT / "frontend" / "node_modules" / "vite" / "bin" / "vite.js"
|
vite_bin = _env.FRONTEND_DIR / "node_modules" / "vite" / "bin" / "vite.js"
|
||||||
if not node or not vite_bin.exists():
|
if not node or not vite_bin.exists():
|
||||||
log("dev", "node or vite not found after npm install")
|
_env.die("未找到 node 或 vite(请确认前端依赖已安装)")
|
||||||
return 1
|
|
||||||
|
|
||||||
api_proc = _spawn(
|
_env.step("启动开发服务")
|
||||||
|
_spawn(
|
||||||
"api",
|
"api",
|
||||||
[str(python), "-m", "uvicorn", "app.main:app", "--reload", "--host", "127.0.0.1", "--port", str(API_PORT)],
|
[str(python), "-m", "uvicorn", "app.main:app", "--reload", "--host", "127.0.0.1", "--port", str(API_PORT)],
|
||||||
ROOT,
|
_env.ROOT,
|
||||||
)
|
)
|
||||||
log("api", f"starting -> http://127.0.0.1:{API_PORT} (auto-reload)")
|
log("api", f"starting -> http://127.0.0.1:{API_PORT} (auto-reload)")
|
||||||
|
|
||||||
@@ -173,10 +118,11 @@ def main() -> int:
|
|||||||
elif not _stopping.is_set():
|
elif not _stopping.is_set():
|
||||||
log("web", "backend not ready in time; starting frontend anyway")
|
log("web", "backend not ready in time; starting frontend anyway")
|
||||||
|
|
||||||
|
api_proc = _processes[0][1]
|
||||||
if _stopping.is_set() or api_proc.poll() is not None:
|
if _stopping.is_set() or api_proc.poll() is not None:
|
||||||
return api_proc.poll() or 1
|
return api_proc.poll() or 1
|
||||||
|
|
||||||
_spawn("web", [node, str(vite_bin), "--host", "127.0.0.1", "--port", str(WEB_PORT)], ROOT / "frontend")
|
_spawn("web", [node, str(vite_bin), "--host", "127.0.0.1", "--port", str(WEB_PORT)], _env.FRONTEND_DIR)
|
||||||
log("web", f"starting -> http://127.0.0.1:{WEB_PORT} (hot reload)")
|
log("web", f"starting -> http://127.0.0.1:{WEB_PORT} (hot reload)")
|
||||||
log("dev", "press Ctrl+C to stop")
|
log("dev", "press Ctrl+C to stop")
|
||||||
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""本地运行脚本:构建前端(如缺失)并启动后端服务。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python scripts/run.py # 默认 0.0.0.0:9081
|
||||||
|
python scripts/run.py --port 8080 # 指定端口
|
||||||
|
python scripts/run.py --rebuild # 强制重新构建前端
|
||||||
|
|
||||||
|
首次运行会自动创建 .venv、安装依赖;前端构建产物缺失时自动构建。按 Ctrl+C 停止。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
import _env # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
_env.setup_console()
|
||||||
|
parser = argparse.ArgumentParser(description="本地运行 CapacityReport 服务")
|
||||||
|
parser.add_argument("--host", default="0.0.0.0", help="监听地址(默认 0.0.0.0)")
|
||||||
|
parser.add_argument("--port", default="9081", help="监听端口(默认 9081)")
|
||||||
|
parser.add_argument("--rebuild", action="store_true", help="强制重新构建前端")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
python = _env.ensure_venv()
|
||||||
|
|
||||||
|
dist_index = _env.FRONTEND_DIR / "dist" / "index.html"
|
||||||
|
if args.rebuild or not dist_index.exists():
|
||||||
|
_env.build_frontend()
|
||||||
|
|
||||||
|
_env.step(f"启动服务 -> http://localhost:{args.port}")
|
||||||
|
_env.info("按 Ctrl+C 停止")
|
||||||
|
try:
|
||||||
|
completed = subprocess.run(
|
||||||
|
[str(python), "-m", "app.main", "--host", args.host, "--port", str(args.port)],
|
||||||
|
cwd=str(_env.ROOT),
|
||||||
|
)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return 0
|
||||||
|
return completed.returncode
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Tauri 桌面端测试运行脚本:构建前端 + 后端 sidecar,然后 cargo tauri dev。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python scripts/tauri_dev.py # 复用已有 sidecar/前端产物(缺失才构建)
|
||||||
|
python scripts/tauri_dev.py --rebuild # 强制重建前端和 sidecar
|
||||||
|
|
||||||
|
桌面端会启动本机 sidecar(http://127.0.0.1:9081)。首次运行会自动准备 .venv、
|
||||||
|
前端依赖、PyInstaller,并在缺少 Rust / Tauri CLI 时给出安装引导。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
import _env # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
_env.setup_console()
|
||||||
|
parser = argparse.ArgumentParser(description="Tauri 桌面端测试运行")
|
||||||
|
parser.add_argument("--rebuild", action="store_true", help="强制重建前端与 sidecar")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
_env.ensure_rust()
|
||||||
|
python = _env.ensure_venv(extra_packages=["pyinstaller"])
|
||||||
|
|
||||||
|
dist_index = _env.FRONTEND_DIR / "dist" / "index.html"
|
||||||
|
if args.rebuild or not dist_index.exists():
|
||||||
|
_env.build_frontend(api_base=_env.DESKTOP_API_BASE)
|
||||||
|
|
||||||
|
triple = _env.rust_host_triple()
|
||||||
|
suffix = ".exe" if _env.host_platform() == "windows" else ""
|
||||||
|
sidecar = _env.SRC_TAURI_DIR / "binaries" / f"{_env.SERVER_NAME}-{triple}{suffix}"
|
||||||
|
if args.rebuild or not sidecar.exists():
|
||||||
|
tmp = _env.DIST_DIR / ".tmp" / "pyinstaller"
|
||||||
|
_env.build_tauri_sidecar(python, tmp / "work", tmp / "dist")
|
||||||
|
|
||||||
|
_env.ensure_tauri_cli()
|
||||||
|
|
||||||
|
_env.step("启动 Tauri 开发模式(cargo tauri dev)")
|
||||||
|
_env.info("按 Ctrl+C 停止")
|
||||||
|
try:
|
||||||
|
completed = subprocess.run(["cargo", "tauri", "dev"], cwd=str(_env.SRC_TAURI_DIR))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return 0
|
||||||
|
return completed.returncode
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# CapacityReport container startup script
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
mkdir -p /var/log/supervisor /var/run /app/cache
|
|
||||||
|
|
||||||
exec supervisord -c /app/supervisord.conf
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
[supervisord]
|
|
||||||
nodaemon=true
|
|
||||||
logfile=/var/log/supervisor/supervisord.log
|
|
||||||
pidfile=/var/run/supervisord.pid
|
|
||||||
childlogdir=/var/log/supervisor
|
|
||||||
user=root
|
|
||||||
|
|
||||||
[unix_http_server]
|
|
||||||
file=/var/run/supervisor.sock
|
|
||||||
chmod=0700
|
|
||||||
|
|
||||||
[rpcinterface:supervisor]
|
|
||||||
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
|
||||||
|
|
||||||
[supervisorctl]
|
|
||||||
serverurl=unix:///var/run/supervisor.sock
|
|
||||||
|
|
||||||
[program:fastapi]
|
|
||||||
command=python -m app.main
|
|
||||||
directory=/app
|
|
||||||
autostart=true
|
|
||||||
autorestart=true
|
|
||||||
startsecs=5
|
|
||||||
startretries=3
|
|
||||||
redirect_stderr=true
|
|
||||||
stdout_logfile=/var/log/supervisor/fastapi.log
|
|
||||||
stdout_logfile_maxbytes=50MB
|
|
||||||
stdout_logfile_backups=5
|
|
||||||
environment=PYTHONUNBUFFERED=1
|
|
||||||
Reference in New Issue
Block a user