pbls/build.sh
2026-09-16 15:32:49 +08:00

167 lines
8.5 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# =====================================================================
# PBL Agent OS 验证原型Phase 0/1—— 一键部署脚本test 环境)
# 端口唯一事实源projects/pbls/env/test.json9300本脚本不硬编码端口从 env 读取
# 门禁:建表 36 张 + 9 项种子全绿 + self_check PASS 74/74 + py_compile 全通过
# =====================================================================
set -euo pipefail
APP_NAME="pbls"
APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WS_DIR="$(cd "${APP_DIR}/../.." && pwd)"
ENV_FILE="${WS_DIR}/projects/pbls/env/test.json"
DDL_FILE="${APP_DIR}/scripts/ddl/pbls_tables.sql"
VENV_DIR="${APP_DIR}/venv"
PY="${VENV_DIR}/bin/python"
log() { echo "[build.sh][$(date '+%F %T')] $*"; }
die() { echo "[build.sh][FATAL] $*" >&2; exit 1; }
# ---- 0. 前置检查env 唯一事实源必须存在且无占位 ----
[ -f "${ENV_FILE}" ] || die "缺少环境事实源 ${ENV_FILE}"
if grep -Eq '待明确|TBD|占位|CHANGE_ME|xxx' "${ENV_FILE}"; then
die "${ENV_FILE} 含占位/待明确字段,禁止部署"
fi
read_env() { python3 -c "import json,sys;print(json.load(open('${ENV_FILE}'))$1)"; }
PORT="$(read_env "['port']")"
DB_HOST="$(read_env "['db']['host']")"
DB_PORT="$(read_env "['db']['port']")"
DB_USER="$(read_env "['db']['user']")"
DB_PASS="$(read_env "['db']['password']")"
DB_NAME="$(read_env "['db']['dbname']")"
DEPLOY_PATH="$(read_env "['deploy']['path']")"
log "env 事实源: port=${PORT} db=${DB_NAME}@${DB_HOST}:${DB_PORT} deploy=${DEPLOY_PATH}"
# ---- 1. 端口一致性核验conf/config.json 必须与 env 同值)----
CONF_PORT="$(python3 -c "import json;print(json.load(open('${APP_DIR}/conf/config.json'))['port'])")"
[ "${CONF_PORT}" = "${PORT}" ] || die "端口不一致: conf=${CONF_PORT} env=${PORT}"
log "端口一致性核验通过: ${PORT}"
# ---- 2. 应用仓库不得另存 env/(端口唯一事实源在项目侧)----
[ ! -d "${APP_DIR}/env" ] || die "应用仓库存在 env/ 目录,违反端口唯一事实源规则"
log "应用仓库无 env/ 目录,核验通过"
# ---- 3. 入口硬门禁核验6.1 / 6.2----
ENTRY="${APP_DIR}/app/${APP_NAME}.py"
[ -f "${ENTRY}" ] || die "缺少入口 ${ENTRY}"
grep -q 'from ahserver.webapp import webapp' "${ENTRY}" || die "入口未使用 ahserver.webapp.webapp"
grep -q 'webapp(init)' "${ENTRY}" || die "入口未调用 webapp(init)"
if grep -Eq 'Flask|FastAPI|Django|http\.server|jsonify|@app\.route' "${ENTRY}"; then
die "入口含禁用框架关键字Flask/FastAPI/Django/http.server/jsonify/@app.route"
fi
log "硬门禁 6.1 通过: ahserver.webapp + webapp(init),无禁用框架"
# ServerEnv() 行号必须早于 load_rbac() 行号
ENV_LINE="$(grep -n 'ServerEnv()' "${ENTRY}" | head -1 | cut -d: -f1)"
RBAC_LINE="$(grep -n 'load_rbac()' "${ENTRY}" | head -1 | cut -d: -f1)"
[ -n "${ENV_LINE}" ] && [ -n "${RBAC_LINE}" ] || die "入口缺 ServerEnv() 或 load_rbac() 显式调用"
[ "${ENV_LINE}" -lt "${RBAC_LINE}" ] || die "ServerEnv() 必须早于 load_rbac()env 行 ${ENV_LINE} >= rbac 行 ${RBAC_LINE}"
grep -q 'load_appbase()' "${ENTRY}" || die "入口缺 load_appbase() 显式调用"
grep -q 'load_pybricks()' "${ENTRY}" || die "入口缺 load_pybricks() 显式调用"
log "硬门禁 6.2 通过: ServerEnv()(L${ENV_LINE}) < load_rbac()(L${RBAC_LINE})load_appbase/load_rbac/load_pybricks 均显式调用"
# ---- 4. DDL 方言核验6.4mariadbBIGINT AUTO_INCREMENT零 SERIAL/FK/ENUM/TIMESTAMP----
[ -f "${DDL_FILE}" ] || die "缺少 DDL ${DDL_FILE}"
if grep -Eiq 'BIGSERIAL|SERIAL|nextval|FOREIGN KEY|REFERENCES|ENUM\(|TIMESTAMP' "${DDL_FILE}"; then
grep -Ein 'BIGSERIAL|SERIAL|nextval|FOREIGN KEY|REFERENCES|ENUM\(|TIMESTAMP' "${DDL_FILE}" | head -20
die "DDL 含非 mariadb 方言关键字(应为零命中)"
fi
TABLE_COUNT="$(grep -c 'CREATE TABLE IF NOT EXISTS' "${DDL_FILE}")"
[ "${TABLE_COUNT}" = "36" ] || die "DDL 表数应为 36实际 ${TABLE_COUNT}"
AUTOINC_COUNT="$(grep -c 'BIGINT NOT NULL AUTO_INCREMENT' "${DDL_FILE}")"
[ "${AUTOINC_COUNT}" = "36" ] || die "BIGINT AUTO_INCREMENT 主键应为 36 处,实际 ${AUTOINC_COUNT}"
log "硬门禁 6.4 通过: mariadb 方言36 表 / 36 处 BIGINT AUTO_INCREMENT禁用关键字零命中"
# ---- 5. 依赖声明核验6.5----
for dep in ahserver sqlor apppublic appbase rbac PyMySQL; do
grep -qi "^${dep}" "${APP_DIR}/requirements.txt" || die "requirements.txt 缺依赖 ${dep}"
done
log "硬门禁 6.5 通过: ahserver/sqlor/apppublic/appbase/rbac/PyMySQL 均已声明"
# ---- 6. website.processors / indexes 核验(缺一会导致 / 返回 500----
python3 - <<'PYEOF'
import json, sys
conf = json.load(open('conf/config.json'))
procs = dict((p[0], p[1]) for p in conf['website']['processors'])
for ext, name in (('.tmpl', 'tmpl'), ('.ui', 'bui'), ('.dspy', 'dspy')):
assert procs.get(ext) == name, 'processors 缺 %s->%s实际 %r' % (ext, name, procs.get(ext))
assert 'index.ui' in conf['website']['indexes'], 'indexes 缺 index.ui'
print('[build.sh] website 核验通过: processors=%s indexes=%s' % (procs, conf['website']['indexes']))
PYEOF
# ---- 7. venv + 依赖安装 ----
if [ ! -x "${PY}" ]; then
log "创建 venv: ${VENV_DIR}"
python3 -m venv "${VENV_DIR}"
fi
"${PY}" -m pip install --upgrade pip wheel setuptools
"${PY}" -m pip install -r "${APP_DIR}/requirements.txt"
# ---- 8. 本地安装 15 个 pbl_* 模块 + 复用模块(可编辑模式)----
for mod_dir in "${WS_DIR}"/modules/pbl_*; do
[ -d "${mod_dir}" ] || continue
if [ -f "${mod_dir}/pyproject.toml" ] || [ -f "${mod_dir}/setup.py" ]; then
log "安装模块 $(basename "${mod_dir}")"
"${PY}" -m pip install -e "${mod_dir}"
else
export PYTHONPATH="${mod_dir}:${PYTHONPATH:-}"
log "模块 $(basename "${mod_dir}") 无打包描述,加入 PYTHONPATH"
fi
done
# ---- 9. 语法验证:全部 .py 跑 py_compileQC 退回意见 #16----
log "py_compile 全量语法验证"
FAIL_SYNTAX=0
while IFS= read -r pyfile; do
if ! "${PY}" -m py_compile "${pyfile}"; then
echo "[build.sh][SYNTAX FAIL] ${pyfile}"
FAIL_SYNTAX=1
fi
done < <(find "${APP_DIR}/app" "${WS_DIR}/modules/pbl_common" "${WS_DIR}/modules/pbl_appcodes" -name '*.py' -not -path '*/venv/*' 2>/dev/null)
[ "${FAIL_SYNTAX}" = "0" ] || die "存在语法错误文件,终止部署"
log "py_compile 全部通过"
# ---- 10. 建表mariadb显式 TCP禁 socket----
log "执行 DDL: ${DDL_FILE}"
mysql --protocol=TCP -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \
--default-character-set=utf8mb4 "${DB_NAME}" < "${DDL_FILE}"
ACTUAL_TABLES="$(mysql --protocol=TCP -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \
-N -B -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${DB_NAME}' AND table_name LIKE 'pbl\\_%'" "${DB_NAME}")"
[ "${ACTUAL_TABLES}" = "36" ] || die "建表数应为 36实际 ${ACTUAL_TABLES}"
log "建表完成: pbl_* 共 ${ACTUAL_TABLES}"
# ---- 11. 枚举数量契约自检QC 退回意见 #15必须真实可运行输出----
"${PY}" -m pbl_appcodes.self_check | tee "${APP_DIR}/logs/self_check.log"
grep -q 'SELF_CHECK pbl_appcodes: PASS 74/74' "${APP_DIR}/logs/self_check.log" \
|| die "pbl_appcodes 数量契约自检未通过"
# ---- 12. 9 项种子数据幂等注入(重复执行零差异)----
log "种子注入(第 1 轮)"
"${PY}" - <<'PYEOF'
import json
from pbl_appcodes.seed import seed_all
s1 = seed_all()
print('[seed round1]', json.dumps(s1, ensure_ascii=False, sort_keys=True))
s2 = seed_all()
print('[seed round2]', json.dumps(s2, ensure_ascii=False, sort_keys=True))
assert s2['groups_inserted'] == 0 and s2['groups_updated'] == 0, '重复执行组有差异'
assert s2['items_inserted'] == 0 and s2['items_updated'] == 0, '重复执行项有差异'
assert s2['idempotent'] is True, '幂等校验失败'
print('[seed] 幂等零差异校验通过: 8 组 74 项')
PYEOF
# ---- 13. 启动应用(唯一入口 + 唯一端口)----
log "启动 ${APP_NAME} 于端口 ${PORT}"
mkdir -p "${APP_DIR}/logs" "${APP_DIR}/run"
nohup "${PY}" "${ENTRY}" > "${APP_DIR}/logs/${APP_NAME}.out" 2>&1 &
echo $! > "${APP_DIR}/run/${APP_NAME}.pid"
sleep 5
# ---- 14. 健康检查 ----
HTTP_CODE="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/api/public/health" || echo 000)"
log "健康检查 HTTP ${HTTP_CODE}"
[ "${HTTP_CODE}" = "200" ] || die "健康检查失败HTTP ${HTTP_CODE}),查看 ${APP_DIR}/logs/${APP_NAME}.out"
log "BUILD SUCCESS: ${APP_NAME} 已运行于 http://127.0.0.1:${PORT}/ (部署路径 ${DEPLOY_PATH}"