fix(pbls): 修复 Bug BWY68rZR_Uh2A41JlGT7D + elS7VxwO7ufbPiPl-ucGA —— build.sh WS_DIR 部署路径推导失效(第7轮 /d/pbl/pbls 未落地代码侧根因)+ requirements 幻影依赖 hashlib-additional + 新增硬门禁 6.6 依赖声明可满足性静态校验(含 40 个模块 pyproject 同类缺陷扫描)+ rsync 缺失 tar 兜底
缺陷1(新发现): WS_DIR=/../.. 只在机构工作空间成立;部署到 env.deploy.path=/d/pbl/pbls 后 WS_DIR=/d,ENV_FILE/WHEELHOUSE/modules 全不存在 → 第0步即 die,build.sh 在部署目录从未跑通 → venv未建/DDL未落库(表数0-37)/进程未起/9380无LISTEN/healthz=000/42条用例blocked。改为 _ws_probe() 三段式(环境变量覆盖 PBL_WS_DIR/WS_DIR_OVERRIDE/WS_DIR → 逐级向上探测含 projects/pbls/env/test.json 的目录 → 兜底绝对路径),WHEELHOUSE 支持 PBL_WHEELHOUSE 覆盖。三场景实测全绿(含 /d/pbl/pbls)。 缺陷2: requirements.txt 幻影依赖 hashlib-additional>=1.0.1 ; python_version<3.0(hashlib 是标准库,marker 恒假;公共源真实名 hashlib_additional 对不上) → 删除。业主口径4项缺陷复扫确认已闭环: 基座7包==版本与 wheelhouse 实物逐字一致、pybricks/mysqlclient/10.20.30.41/相对find-links 生效行残留全0;pip download --no-deps 真实可满足性验证全部 Saved 零 No matching distribution。 缺陷3(机制根因): 门禁6.5 只 grep 声明存在不校验可满足 → 新增硬门禁6.6 scripts/check_requirements_satisfiable.py(349行) 7类校验(幻影包黑名单/标准库误当第三方/基座==版本与wheelhouse一致/wheelhouse覆盖完整/门禁6.5六项/失效内网源/相对find-links),按任务要求同步扫描 40 个 modules/*/pyproject.toml 同类缺陷并输出本地模块依赖图(pbl_common <- pbl_analytics/pbl_appcodes/pbl_blueprint)与 build.sh R8 拓扑序交叉验证。负向测试构造8类缺陷抓出17项FAIL rc=1(证明非空壳);正向 PASS 20项声明/基座7包一致/40模块零幻影。 附带加固: rsync 缺失(pbl 无 sudo 可能未装)时退化 tar 管道复制,避免部署目录建不出来。 自检: bash -n OK / py_compile OK / 门禁6.6 PASS / DDL 37表 / 门禁6.5六项全在 / 幻影包残留0 / 旧口径9300+/opt/pbls 0 / WS_DIR硬编码残留0
This commit is contained in:
parent
b832258689
commit
149dd5c811
91
build.sh
91
build.sh
@ -8,7 +8,41 @@ set -euo pipefail
|
||||
|
||||
APP_NAME="pbls"
|
||||
APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WS_DIR="$(cd "${APP_DIR}/../.." && pwd)"
|
||||
|
||||
# ---- 0a. 机构工作空间根(WS_DIR)解析:可覆盖 + 逐级探测 + 事实源验证 ----
|
||||
# [Bug BWY68rZR_Uh2A41JlGT7D / elS7VxwO7ufbPiPl-ucGA 修复]
|
||||
# 原实现 WS_DIR="${APP_DIR}/../.." 只在机构工作空间 apps/pbls 下成立:
|
||||
# APP_DIR=/d/pipeline/workspaces/0/sdlc_general/apps/pbls → WS_DIR=工作空间根 ✅
|
||||
# 部署到 env.deploy.path=/d/pbl/pbls 后:
|
||||
# APP_DIR=/d/pbl/pbls → WS_DIR=/d ❌(ENV_FILE=/d/projects/... 不存在 → 第 0 步即 die)
|
||||
# 后果链:build.sh 从未跑通 → venv 未建 → DDL 未落库(pbl 库表数 0/37)→ 进程未起
|
||||
# → 9380 无 LISTEN → healthz=000 → 42 条功能用例持续 blocked。
|
||||
# 现改为三段式:① 环境变量显式覆盖(PBL_WS_DIR / WS_DIR)
|
||||
# ② 从 APP_DIR 逐级向上探测含 projects/pbls/env/test.json 的目录
|
||||
# ③ 兜底机构工作空间绝对路径(部署机与工作区同机,实测可读)
|
||||
_ws_probe() {
|
||||
local _c _d
|
||||
for _c in "${PBL_WS_DIR:-}" "${WS_DIR_OVERRIDE:-}" "${WS_DIR:-}"; do
|
||||
[ -n "${_c}" ] || continue
|
||||
if [ -f "${_c}/projects/pbls/env/test.json" ]; then (cd "${_c}" && pwd); return 0; fi
|
||||
done
|
||||
_d="${APP_DIR}"
|
||||
while [ "${_d}" != "/" ] && [ -n "${_d}" ]; do
|
||||
if [ -f "${_d}/projects/pbls/env/test.json" ]; then (cd "${_d}" && pwd); return 0; fi
|
||||
_d="$(dirname "${_d}")"
|
||||
done
|
||||
for _c in /d/pipeline/workspaces/0/sdlc_general "${HOME}/sdlc_general"; do
|
||||
if [ -f "${_c}/projects/pbls/env/test.json" ]; then (cd "${_c}" && pwd); return 0; fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
RESOLVED_WS_DIR="$(_ws_probe || true)"
|
||||
[ -n "${RESOLVED_WS_DIR}" ] || {
|
||||
echo "[build.sh][FATAL] 无法定位机构工作空间根(需含 projects/pbls/env/test.json)。" >&2
|
||||
echo "[build.sh][FATAL] APP_DIR=${APP_DIR};请显式导出 PBL_WS_DIR=<工作空间根> 后重跑。" >&2
|
||||
exit 1
|
||||
}
|
||||
WS_DIR="${RESOLVED_WS_DIR}"
|
||||
ENV_FILE="${WS_DIR}/projects/pbls/env/test.json"
|
||||
DDL_FILE="${APP_DIR}/scripts/ddl/pbls_tables.sql"
|
||||
VENV_DIR="${APP_DIR}/venv"
|
||||
@ -96,6 +130,25 @@ for dep in ahserver sqlor apppublic appbase rbac PyMySQL; do
|
||||
done
|
||||
log "硬门禁 6.5 通过: ahserver/sqlor/apppublic/appbase/rbac/PyMySQL 均已声明"
|
||||
|
||||
# ---- 6.6 依赖声明「可满足性」静态门禁 ----
|
||||
# [Bug BWY68rZR_Uh2A41JlGT7D 修复] 门禁 6.5 只 grep「声明存在」,不校验「声明可满足」,
|
||||
# 于是三类必然让 `pip install -r requirements.txt` 失败(BUILD_RC=1)的缺陷被放行到部署阶段:
|
||||
# 1) 版本下界凭空抬高、公共源无对应发行物(ahserver>=1.4.0 而实装/最高仅 1.2.0)
|
||||
# 2) 幻影包 pybricks(平台前端能力实由 bricks_for_python 提供;公共源同名包是 LEGO 机器人库)
|
||||
# 3) 不可编译包 mysqlclient(目标机无 mysql_config,pbl 无 sudo 装不了 mariadb-devel)
|
||||
# 6.6 把可满足性前移为静态门禁,并按任务要求「检查所有模块的同类问题」一并扫描
|
||||
# 全部 modules/*/pyproject.toml(幻影包 / 标准库误当第三方 / 与 wheelhouse 版本不一致)。
|
||||
REQ_SAT_GATE="${APP_DIR}/scripts/check_requirements_satisfiable.py"
|
||||
[ -f "${REQ_SAT_GATE}" ] || die "缺少依赖可满足性门禁脚本 ${REQ_SAT_GATE}"
|
||||
EXTRA_REQ_ARGS=""
|
||||
[ -f "${APP_DIR}/requirements-test.txt" ] && EXTRA_REQ_ARGS="--extra-requirements ${APP_DIR}/requirements-test.txt"
|
||||
python3 "${REQ_SAT_GATE}" \
|
||||
--requirements "${APP_DIR}/requirements.txt" \
|
||||
${EXTRA_REQ_ARGS} \
|
||||
--wheelhouse "${WS_DIR}/wheelhouse" \
|
||||
--modules-glob "${WS_DIR}/modules/*/pyproject.toml" || die "硬门禁 6.6 未通过:依赖声明不可满足(幻影包/版本与 wheelhouse 不一致/失效内网源/相对 find-links),详见上方逐条回显"
|
||||
log "硬门禁 6.6 通过: 依赖声明可满足(基座版本与 wheelhouse 逐字一致,零幻影包,全模块 pyproject 同类缺陷零命中)"
|
||||
|
||||
# ---- 6. website.processors / indexes 核验(缺一会导致 / 返回 500)----
|
||||
python3 - <<'PYEOF'
|
||||
import json, sys
|
||||
@ -119,8 +172,8 @@ fi
|
||||
# build.sh 在此处 BUILD_RC=1 终止(R4 部署失败根因,见 evidence/r4_build.log)。
|
||||
# 改为:机构 wheelhouse 离线供给(--find-links,绝对路径,不依赖 cwd)+ 公网 pypi.org 取第三方包
|
||||
# (目标机实测 https://pypi.org/simple/pymysql/ HTTP=200)。
|
||||
WHEELHOUSE="${WS_DIR}/wheelhouse"
|
||||
[ -d "${WHEELHOUSE}" ] || die "缺少机构 wheelhouse ${WHEELHOUSE}(平台基础包离线源,内网 pypi 在目标机不可达)"
|
||||
WHEELHOUSE="${PBL_WHEELHOUSE:-${WS_DIR}/wheelhouse}"
|
||||
[ -d "${WHEELHOUSE}" ] || die "缺少机构 wheelhouse ${WHEELHOUSE}(平台基础包离线源,内网 pypi 在目标机不可达);部署机路径不同时请导出 PBL_WHEELHOUSE=<wheelhouse 绝对路径>"
|
||||
for _w in ahserver sqlor apppublic appbase rbac bricks_for_python accounting; do
|
||||
ls "${WHEELHOUSE}"/${_w}-*.whl >/dev/null 2>&1 || die "wheelhouse 缺 ${_w} 的 whl(${WHEELHOUSE})"
|
||||
done
|
||||
@ -299,8 +352,22 @@ log "运行目录就位: ${APP_DIR}/logs ${APP_DIR}/run"
|
||||
|
||||
# ---- 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 数量契约自检未通过"
|
||||
# [deploy_test R11 修复] 原门禁断言字面量 'PASS 74/74' 与 pbl_appcodes.self_check 真实输出格式不符:
|
||||
# 实测输出为 'SELF_CHECK pbl_appcodes: PASS 18/18'(18 = 自检项数),74 项枚举契约由其中
|
||||
# '[PASS] meta: _meta.groups=8 / items=74' 与 '[PASS] groups: 每组项数符合契约(8/5/8/6/6/10/16/15 = 74)'
|
||||
# 两行承载(r11 build 日志 1411-1430 行实测)。故门禁改为「PASS 汇总行 + 74 项契约双证据」联合断言,
|
||||
# 语义不放宽(仍要求自检全绿且枚举数=74),仅修正过期字面量。
|
||||
grep -qE 'SELF_CHECK pbl_appcodes: PASS [0-9]+/[0-9]+' "${APP_DIR}/logs/self_check.log" \
|
||||
|| die "pbl_appcodes 数量契约自检未通过(无 PASS 汇总行)"
|
||||
grep -q 'items=74' "${APP_DIR}/logs/self_check.log" \
|
||||
|| die "pbl_appcodes 数量契约自检未通过(_meta items != 74)"
|
||||
grep -q '= 74)' "${APP_DIR}/logs/self_check.log" \
|
||||
|| die "pbl_appcodes 数量契约自检未通过(每组项数合计 != 74)"
|
||||
# 注:set -euo pipefail 下 `grep && die` 在 grep 无命中(rc=1)时会误触发脚本退出,故用 if 结构
|
||||
if grep -q 'FAIL' "${APP_DIR}/logs/self_check.log"; then
|
||||
die "pbl_appcodes 数量契约自检存在 FAIL 项"
|
||||
fi
|
||||
log "枚举数量契约自检通过: 74 项 / 自检全绿(无 FAIL)"
|
||||
|
||||
# ---- 12. 9 项种子数据幂等注入(重复执行零差异)----
|
||||
log "种子注入(第 1 轮)"
|
||||
@ -355,10 +422,24 @@ else
|
||||
mkdir -p "${DEPLOY_PATH}"
|
||||
# -a 保留权限/时间并**保留软链本身**(8c 建的模块 wwwroot 软链是绝对路径,换机仍可解析);
|
||||
# 排除 venv/logs/run/.git:venv 不可搬移,logs/run 由目标侧自建,.git 不属于运行态。
|
||||
# [Bug elS7VxwO7ufbPiPl-ucGA 修复] rsync 缺失兜底:部署机(pbl 用户,无 sudo)实测
|
||||
# 可能未装 rsync,原实现直接调用 → command not found → set -e 下 die,
|
||||
# 部署目录 /d/pbl/pbls 永远建不出来(复测门禁 ④ 恒红)。
|
||||
# 无 rsync 时退化为 tar 管道复制(POSIX 必备,保留权限与软链本身),并显式清理运行态目录。
|
||||
if command -v rsync >/dev/null 2>&1; then
|
||||
rsync -a --delete \
|
||||
--exclude 'venv/' --exclude '.venv/' --exclude 'logs/' --exclude 'run/' \
|
||||
--exclude '.git/' --exclude '__pycache__/' --exclude '*.pyc' \
|
||||
"${APP_DIR}/" "${DEPLOY_PATH}/"
|
||||
else
|
||||
log "rsync 不可用,退化 tar 管道同步(保留权限/软链)"
|
||||
( cd "${APP_DIR}" && tar cf - \
|
||||
--exclude='./venv' --exclude='./.venv' --exclude='./logs' --exclude='./run' \
|
||||
--exclude='./.git' --exclude='__pycache__' --exclude='*.pyc' . ) \
|
||||
| ( cd "${DEPLOY_PATH}" && tar xf - )
|
||||
# tar 路径无 --delete 语义:手工清理运行态残留,避免旧文件污染
|
||||
rm -rf "${DEPLOY_PATH}/.git" "${DEPLOY_PATH}/logs" "${DEPLOY_PATH}/run" 2>/dev/null || true
|
||||
fi
|
||||
mkdir -p "${DEPLOY_PATH}/logs" "${DEPLOY_PATH}/run"
|
||||
# venv 软链复用(目标侧 start_cmd 要求 ${DEPLOY_PATH}/venv/bin/python 可用)
|
||||
if [ ! -e "${DEPLOY_PATH}/venv" ]; then
|
||||
|
||||
428
build.sh.r11bak
Normal file
428
build.sh.r11bak
Normal file
@ -0,0 +1,428 @@
|
||||
#!/usr/bin/env bash
|
||||
# =====================================================================
|
||||
# PBL Agent OS 验证原型(Phase 0/1)—— 一键部署脚本(test 环境)
|
||||
# 端口唯一事实源:projects/pbls/env/test.json(9380);本脚本不硬编码端口,从 env 读取
|
||||
# 门禁:建表 37 张 + 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']")"
|
||||
# 外网口自检用(复测门禁 ① 同时 curl 127.0.0.1 与 192.168.16.3 两个地址);
|
||||
# env 无 ssh.host 时回退 127.0.0.1,不让 read_env 抛 KeyError 中断部署。
|
||||
SSH_HOST="$(python3 -c "import json;print(json.load(open('${ENV_FILE}')).get('ssh',{}).get('host') or '127.0.0.1')")"
|
||||
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.4:mariadb,BIGINT AUTO_INCREMENT,零 SERIAL/FK/ENUM/TIMESTAMP)----
|
||||
# 【Q4 修复 / deploy_test 第 3 轮实测】旧版直接对 DDL **原文** grep,存在两类假阳性:
|
||||
# (1) DDL 文末「契约自检」注释块为说明"零命中项",逐字写出了
|
||||
# FOREIGN KEY / REFERENCES / ENUM( / SERIAL / BIGSERIAL / nextval / TIMESTAMP
|
||||
# (实测原文命中 3 行:L6 头部说明、L220 COMMENT 串内的 references、L891 契约自检块),
|
||||
# 旧 `grep -Eiq` 一律触发 die —— 注释/COMMENT 串被当成真实 DDL,门禁必然误杀;
|
||||
# (2) `CREATE TABLE` / `AUTO_INCREMENT` 计数同理会把注释行与 COMMENT '...' 串内字样算进去,
|
||||
# 表数不可信(且旧断言 36 与 DDL 实际 37 张不符,新增 pbl_compile_task 后未同步)。
|
||||
# 修复:改为「先剥 SQL 注释 + 字符串字面量(含 COMMENT 串),再在纯 DDL 正文上扫描」,
|
||||
# 由 scripts/ddl_gate_check.py 单遍字符扫描实现(非逐行正则,串内 -- / # 不会误判为注释),
|
||||
# 期望值与 DDL 实际表数对齐为 37(表数 / BIGINT AUTO_INCREMENT 主键数同为 37)。
|
||||
[ -f "${DDL_FILE}" ] || die "缺少 DDL ${DDL_FILE}"
|
||||
DDL_GATE="${APP_DIR}/scripts/ddl_gate_check.py"
|
||||
[ -f "${DDL_GATE}" ] || die "缺少 DDL 门禁扫描器 ${DDL_GATE}(6.4 依赖,禁止回退到原文 grep)"
|
||||
|
||||
# 4a. 扫描器自身可信度:先跑内置最小单测(剥注释/剥串/幂等/真实违规不漏报)
|
||||
SELFTEST_RC=0
|
||||
python3 "${DDL_GATE}" --selftest || SELFTEST_RC=$?
|
||||
[ "${SELFTEST_RC}" = "0" ] || die "DDL 门禁扫描器自检未通过(--selftest rc=${SELFTEST_RC}),扫描结果不可信,终止部署"
|
||||
|
||||
# 4b. 在降噪后的纯 DDL 正文上核验:方言零命中 + 表数 37 + BIGINT AUTO_INCREMENT 37
|
||||
GATE_RC=0
|
||||
python3 "${DDL_GATE}" --ddl "${DDL_FILE}" --expect-tables 37 --expect-autoinc 37 || GATE_RC=$?
|
||||
[ "${GATE_RC}" = "0" ] || die "硬门禁 6.4 未通过(ddl_gate_check.py rc=${GATE_RC}):非 mariadb 方言关键字命中 / 表数≠37 / BIGINT AUTO_INCREMENT≠37,详见上方回显"
|
||||
log "硬门禁 6.4 通过: mariadb 方言(剥注释+COMMENT 串后零命中),37 表 / 37 处 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
|
||||
|
||||
# [deploy_test R5 修复 #1] 平台基础包(ahserver/sqlor/apppublic/appbase/rbac/bricks_for_python/accounting)
|
||||
# 原走内网私有源 http://10.20.30.41:8081 —— 目标机 192.168.16.3 实测 connect timeout(5 次重试全失败),
|
||||
# build.sh 在此处 BUILD_RC=1 终止(R4 部署失败根因,见 evidence/r4_build.log)。
|
||||
# 改为:机构 wheelhouse 离线供给(--find-links,绝对路径,不依赖 cwd)+ 公网 pypi.org 取第三方包
|
||||
# (目标机实测 https://pypi.org/simple/pymysql/ HTTP=200)。
|
||||
WHEELHOUSE="${WS_DIR}/wheelhouse"
|
||||
[ -d "${WHEELHOUSE}" ] || die "缺少机构 wheelhouse ${WHEELHOUSE}(平台基础包离线源,内网 pypi 在目标机不可达)"
|
||||
for _w in ahserver sqlor apppublic appbase rbac bricks_for_python accounting; do
|
||||
ls "${WHEELHOUSE}"/${_w}-*.whl >/dev/null 2>&1 || die "wheelhouse 缺 ${_w} 的 whl(${WHEELHOUSE})"
|
||||
done
|
||||
log "wheelhouse 就位: $(ls "${WHEELHOUSE}"/*.whl | wc -l) 个 whl @ ${WHEELHOUSE}"
|
||||
"${PY}" -m pip install --find-links "${WHEELHOUSE}" -r "${APP_DIR}/requirements.txt"
|
||||
|
||||
# ---- 7b. 测试前置依赖(DISC.2)----
|
||||
# 运行时依赖(requirements.txt)刻意不含 pytest;测试依赖单列 requirements-test.txt,
|
||||
# 由本步装进同一个 venv,使
|
||||
# source venv/bin/activate && python3 -m pytest tests/test_m5a_idempotency.py -v
|
||||
# 在**不手工注入 PYTHONPATH** 的前提下可直接执行(此前靠 PYTHONPATH=<ws>/.pylibs
|
||||
# 临时注入跑通,换机器/换角色即失效,test 角色会误判为环境不可用)。
|
||||
# 只要运行时依赖的场景(如生产镜像)用 PBL_SKIP_TEST_DEPS=1 显式跳过。
|
||||
REQ_TEST="${APP_DIR}/requirements-test.txt"
|
||||
if [ "${PBL_SKIP_TEST_DEPS:-0}" = "1" ]; then
|
||||
log "PBL_SKIP_TEST_DEPS=1:跳过测试前置依赖(venv 内将没有 pytest,测试角色需自行引导)"
|
||||
elif [ ! -f "${REQ_TEST}" ]; then
|
||||
die "缺测试依赖清单 ${REQ_TEST}(DISC.2 要求显式声明;确要跳过请设 PBL_SKIP_TEST_DEPS=1)"
|
||||
else
|
||||
log "安装测试前置依赖: ${REQ_TEST}"
|
||||
"${PY}" -m pip install -r "${REQ_TEST}"
|
||||
# 自证:装完必须真能 import + 跑 --version,否则本步等于没做(不让注释与事实矛盾)
|
||||
"${PY}" -c 'import pytest' >/dev/null 2>&1 \
|
||||
|| die "安装后 venv 内仍无法 import pytest(${PY})"
|
||||
log "7b 自证通过: $("${PY}" -m pytest --version 2>&1 | head -1) @ ${PY}"
|
||||
fi
|
||||
|
||||
# ---- 8. 本地安装 15 个 pbl_* 模块 + 复用模块(可编辑模式)----
|
||||
# [deploy_test R5 修复 #2] 原循环只匹配 modules/pbl_*,但入口 app/pbls.py 还显式 import
|
||||
# world / scene / entity / script_engine / scense_runtime / scense / scense_game
|
||||
# 这 7 个复用域模块(pbls_spec.json referenced_modules)。漏装 → 入口 ImportError →
|
||||
# 进程起不来(端口无 LISTEN、healthz=000)。此处按 spec.json referenced_modules 补装,
|
||||
# 并做「入口 import 全部可解析」自证门禁,装不上就 die,不留到启动阶段才炸。
|
||||
REUSE_MODULES="$(python3 -c "import json;print(' '.join(json.load(open('${WS_DIR}/projects/pbls/pbls_spec.json'))['referenced_modules']))")"
|
||||
# [deploy_test R8 修复] 安装顺序改为拓扑序:pbl_common 必须最先装。
|
||||
# 原实现用 glob `modules/pbl_*` 按字母序安装,pbl_agent_runtime 排在 pbl_common 之前,
|
||||
# 而 pbl_agent_runtime 的 pyproject 依赖 pbl_common(本地模块,任何索引都没有)→
|
||||
# pip 报 "ERROR: No matching distribution found for pbl_common",BUILD_RC=1
|
||||
# (实测 /d/pbl/pbls_ws/build_r8.log L170-176)。故先装 pbl_common,再装其余 pbl_*。
|
||||
INSTALL_DIRS=""
|
||||
if [ -d "${WS_DIR}/modules/pbl_common" ]; then
|
||||
INSTALL_DIRS="${WS_DIR}/modules/pbl_common"
|
||||
fi
|
||||
for mod_dir in "${WS_DIR}"/modules/pbl_*; do
|
||||
[ -d "${mod_dir}" ] || continue
|
||||
[ "$(basename "${mod_dir}")" = "pbl_common" ] && continue
|
||||
INSTALL_DIRS="${INSTALL_DIRS} ${mod_dir}"
|
||||
done
|
||||
for rm in ${REUSE_MODULES}; do
|
||||
# 平台基础包(apppublic/sqlor/ahserver/appbase/rbac/accounting)已由 wheelhouse 装好,跳过
|
||||
case " ${rm} " in *" apppublic "*|*" sqlor "*|*" ahserver "*|*" appbase "*|*" rbac "*|*" accounting "*) continue;; esac
|
||||
[ -d "${WS_DIR}/modules/${rm}" ] || die "spec.json referenced_modules 声明 ${rm},但 ${WS_DIR}/modules/${rm} 不存在"
|
||||
INSTALL_DIRS="${INSTALL_DIRS} ${WS_DIR}/modules/${rm}"
|
||||
done
|
||||
for mod_dir in ${INSTALL_DIRS}; do
|
||||
if [ -f "${mod_dir}/pyproject.toml" ] || [ -f "${mod_dir}/setup.py" ]; then
|
||||
log "安装模块 $(basename "${mod_dir}")"
|
||||
"${PY}" -m pip install --find-links "${WHEELHOUSE}" -e "${mod_dir}"
|
||||
else
|
||||
export PYTHONPATH="${mod_dir}:${PYTHONPATH:-}"
|
||||
log "模块 $(basename "${mod_dir}") 无打包描述,加入 PYTHONPATH"
|
||||
fi
|
||||
done
|
||||
|
||||
# ---- 8a. 入口 import 可解析性自证(部署前拦截 ImportError,不留到启动阶段)----
|
||||
log "入口 import 自证:$(basename "${ENTRY}") 全部 import 必须可解析"
|
||||
IMPORT_RC=0
|
||||
"${PY}" - <<'PYIMP' || IMPORT_RC=$?
|
||||
import ast, importlib.util, sys
|
||||
src = open('app/pbls.py', encoding='utf-8').read()
|
||||
mods = set()
|
||||
for node in ast.walk(ast.parse(src)):
|
||||
if isinstance(node, ast.ImportFrom) and node.module:
|
||||
mods.add(node.module.split('.')[0])
|
||||
elif isinstance(node, ast.Import):
|
||||
for a in node.names:
|
||||
mods.add(a.name.split('.')[0])
|
||||
mods -= set(sys.stdlib_module_names)
|
||||
mods -= {'json', 'os', 'sys', 'ast'}
|
||||
missing = sorted(m for m in mods if importlib.util.find_spec(m) is None)
|
||||
print('[import-check] 需解析顶层模块 %d 个: %s' % (len(mods), ' '.join(sorted(mods))))
|
||||
if missing:
|
||||
print('[import-check][FAIL] 无法解析: %s' % ' '.join(missing))
|
||||
sys.exit(1)
|
||||
print('[import-check] PASS 全部可解析')
|
||||
PYIMP
|
||||
[ "${IMPORT_RC}" = "0" ] || die "入口 import 自证失败(rc=${IMPORT_RC}):上列模块在 venv 内不可解析,应用必然启动失败,终止部署"
|
||||
|
||||
# ---- 8b. M9 覆盖层页面壳生成(构建产物,不入库)+ 一致性门禁 ----
|
||||
# 页面壳由 pbl_scense_ext 的生成器产出;产物目录 wwwroot/pbl_overlay/ 在模块仓
|
||||
# .gitignore 中被排除,因此必须在构建期生成,否则新克隆/新部署环境该目录为空、
|
||||
# /pbl_overlay/<key>.ui 导航全量 404。位置:模块安装之后、模块 wwwroot 软链(紧随其后的
|
||||
# 「8c. 模块 wwwroot 挂载」段)之前——生成物落在模块仓,必须再经 8c 挂载到应用 wwwroot
|
||||
# 才能在应用侧 URL /pbl_scense_ext/pbl_overlay/<key>.ui 上取到。
|
||||
OVERLAY_GEN="${WS_DIR}/modules/pbl_scense_ext/scripts/gen_overlay_pages.py"
|
||||
[ -f "${OVERLAY_GEN}" ] || die "缺少覆盖层页面壳生成器: ${OVERLAY_GEN}(modules/pbl_scense_ext 未就位)"
|
||||
log "生成 PBL 覆盖层页面壳: ${OVERLAY_GEN}"
|
||||
# 注意:不能用 `if ! cmd; then die "... rc=$?"` —— 进入分支时 $? 已被 if 取反为 0,
|
||||
# 打印出的 rc 恒为 0 会误导排障;改为显式捕获真实返回码。
|
||||
GEN_RC=0
|
||||
"${PY}" "${OVERLAY_GEN}" || GEN_RC=$?
|
||||
if [ "${GEN_RC}" -ne 0 ]; then
|
||||
die "覆盖层页面壳生成失败(gen_overlay_pages.py rc=${GEN_RC}):wwwroot/pbl_overlay/ 将为空,/pbl_overlay/<key>.ui 全量 404,终止部署"
|
||||
fi
|
||||
log "覆盖层页面壳门禁校验(gen_overlay_pages.py --check)"
|
||||
CHK_RC=0
|
||||
"${PY}" "${OVERLAY_GEN}" --check || CHK_RC=$?
|
||||
if [ "${CHK_RC}" -ne 0 ]; then
|
||||
die "覆盖层页面壳门禁校验未通过(--check rc=${CHK_RC}):i18n zh/en key 不一致,或页面壳与 PAGES 注册表不一致,终止部署"
|
||||
fi
|
||||
log "覆盖层页面壳生成 + 门禁校验通过(12 个页面壳 / i18n key 一致)"
|
||||
|
||||
# ---- 8c. 模块 wwwroot 挂载(软链)+ 应用侧可达性门禁(QC 退回意见 #1/#2)----
|
||||
# 上一版只做了 8b「生成」,生成物落在模块仓 modules/<m>/wwwroot/ 下,但本脚本从未把
|
||||
# 模块 wwwroot 挂进应用 wwwroot —— 应用 website.root=wwwroot 只指向应用自身目录,
|
||||
# 因此 build.sh 跑完后应用侧依然取不到 /pbl_scense_ext/pbl_overlay/<key>.ui(可达链路
|
||||
# 断在挂载环节,全量 404 并未消除)。此处按 web-application-spec「挂载模块 9 步流程」
|
||||
# 第 5 步(wwwroot 软链,非 cp,保持与模块仓单一副本同步)补齐,并立即以应用侧路径
|
||||
# 实测 12 个页面壳数量作为门禁,rc≠0 终止部署。
|
||||
LINKED_MODULES=""
|
||||
for mod_dir in "${WS_DIR}"/modules/pbl_*; do
|
||||
[ -d "${mod_dir}/wwwroot" ] || continue
|
||||
mod_name="$(basename "${mod_dir}")"
|
||||
www_link="${APP_DIR}/wwwroot/${mod_name}"
|
||||
# 已存在但不是软链(应用仓自带同名真实目录)时不覆盖,直接终止让人工处置,
|
||||
# 避免 ln -sfn 把软链建进既有目录里、或静默删掉应用仓跟踪文件。
|
||||
if [ -e "${www_link}" ] && [ ! -L "${www_link}" ]; then
|
||||
die "wwwroot 挂载冲突:${www_link} 已存在且不是软链(应用仓自带目录?),请人工处置后重试"
|
||||
fi
|
||||
ln -sfn "${mod_dir}/wwwroot" "${www_link}"
|
||||
LINKED_MODULES="${LINKED_MODULES} ${mod_name}"
|
||||
done
|
||||
[ -n "${LINKED_MODULES}" ] || die "未发现任何含 wwwroot/ 的 pbl_* 模块,模块 wwwroot 挂载为空"
|
||||
log "模块 wwwroot 软链挂载完成:${LINKED_MODULES}"
|
||||
|
||||
# 挂载后应用侧可达性门禁:覆盖层页面壳必须能通过应用 wwwroot 路径取到
|
||||
APP_OVERLAY_DIR="${APP_DIR}/wwwroot/pbl_scense_ext/pbl_overlay"
|
||||
APP_UI_COUNT=0
|
||||
if [ -d "${APP_OVERLAY_DIR}" ]; then
|
||||
APP_UI_COUNT="$(find -L "${APP_OVERLAY_DIR}" -maxdepth 1 -name '*.ui' | wc -l | tr -d '[:space:]')"
|
||||
fi
|
||||
if [ "${APP_UI_COUNT}" != "12" ]; then
|
||||
die "应用侧覆盖层页面壳应为 12 个,实际 ${APP_UI_COUNT}(${APP_OVERLAY_DIR}):模块 wwwroot 未挂载成功或生成器未产出,/pbl_scense_ext/pbl_overlay/<key>.ui 将全量 404,终止部署"
|
||||
fi
|
||||
log "应用侧可达性核验通过: ${APP_OVERLAY_DIR}/*.ui = ${APP_UI_COUNT}"
|
||||
|
||||
# ---- 9. 语法验证:全部 .py 跑 py_compile(QC 退回意见 #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}")"
|
||||
# Q4 修复:与 DDL 实际表数对齐(36 → 37,新增 pbl_compile_task)
|
||||
[ "${ACTUAL_TABLES}" = "37" ] || die "建表数应为 37,实际 ${ACTUAL_TABLES}"
|
||||
log "建表完成: pbl_* 共 ${ACTUAL_TABLES} 张"
|
||||
|
||||
# ---- 10b. 运行目录前置创建(Bug WIu0Rkn0KwB0XTjYZz0qk 修复)----
|
||||
# 原脚本到步骤 13 才 mkdir logs/run,而步骤 11 已经 `tee ${APP_DIR}/logs/self_check.log`。
|
||||
# 全新克隆(仓库不含 logs/,.gitignore 也排除 *.log)时 tee 打不开文件 →
|
||||
# set -euo pipefail 下整条管道非零 → die,部署在「建表已成功、自检未跑」处中断,
|
||||
# 表现为门禁 ③ 表数 0/37(DDL 事务已提交但脚本没走到断言)与门禁 ②③ 全红。
|
||||
mkdir -p "${APP_DIR}/logs" "${APP_DIR}/run"
|
||||
log "运行目录就位: ${APP_DIR}/logs ${APP_DIR}/run"
|
||||
|
||||
# ---- 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
|
||||
|
||||
# ---- 12.5 RBAC 路径注册(Bug WIu0Rkn0KwB0XTjYZz0qk 修复:原脚本从不注册)----
|
||||
# 中央聚合器 apps/pbls/scripts/load_path.py 把各模块 + 宿主自有端点(含本次新增的
|
||||
# /healthz、/healthz.dspy、/api/public/health)逐条写入 permission/rolepermission。
|
||||
# 不注册的后果:进程起来了、端口监听了,但 curl 一律 401/403 → 冒烟门禁 ① HTTP!=200。
|
||||
# 先跑 --check 做通配符 + 聚合完整性门禁(rc!=0 即终止,禁止带缺口部署)。
|
||||
LOAD_PATH="${APP_DIR}/scripts/load_path.py"
|
||||
[ -f "${LOAD_PATH}" ] || die "缺少 RBAC 注册入口 ${LOAD_PATH}"
|
||||
log "RBAC 一致性门禁: load_path.py --check"
|
||||
CHK_RBAC=0
|
||||
WS_DIR="${WS_DIR}" "${PY}" "${LOAD_PATH}" --check || CHK_RBAC=$?
|
||||
[ "${CHK_RBAC}" = "0" ] || die "RBAC 聚合门禁未通过(rc=${CHK_RBAC}):存在通配符或整模块聚合失败,终止部署"
|
||||
log "RBAC 注册: load_path.py(逐条显式,无通配符)"
|
||||
REG_RC=0
|
||||
WS_DIR="${WS_DIR}" "${PY}" "${LOAD_PATH}" || REG_RC=$?
|
||||
if [ "${REG_RC}" != "0" ]; then
|
||||
# set_role_perm.py 不在本机时 register() 返回 PENDING 清单且 rc=1;
|
||||
# 这不是代码缺陷而是部署机缺工具,明确回显后终止(不静默放过 → 否则又是 403)。
|
||||
die "RBAC 注册未成功(rc=${REG_RC}):set_role_perm.py 不可用或写库失败,端点将 401/403,终止部署"
|
||||
fi
|
||||
log "RBAC 注册完成"
|
||||
|
||||
# ---- 12.8 落地部署目录 DEPLOY_PATH(Bug WIu0Rkn0KwB0XTjYZz0qk 修复)----
|
||||
# 原脚本把 DEPLOY_PATH 读出来只用于最后一行日志 echo,从未创建/同步任何内容,
|
||||
# 于是复测门禁 ④「ls -la /d/pbl/pbls」恒为 No such file or directory,
|
||||
# 且 env/test.json 的 start_cmd(${DEPLOY_PATH}/venv/bin/python ${DEPLOY_PATH}/app/pbls.py)
|
||||
# 指向的解释器与入口都不存在 → 门禁 ②③(进程数 0 / 端口未监听)不可能转绿。
|
||||
# 处置:把应用树同步到 DEPLOY_PATH,venv 以软链复用(venv 内 shebang/符号链接是
|
||||
# 绝对路径,复制换目录必坏,软链才安全),随后从 DEPLOY_PATH 启动。
|
||||
if [ "${DEPLOY_PATH}" = "${APP_DIR}" ]; then
|
||||
log "DEPLOY_PATH 与仓库目录同址(${DEPLOY_PATH}),跳过同步"
|
||||
RUN_DIR="${APP_DIR}"
|
||||
RUN_PY="${PY}"
|
||||
RUN_ENTRY="${ENTRY}"
|
||||
else
|
||||
log "同步应用树到部署目录: ${DEPLOY_PATH}"
|
||||
mkdir -p "${DEPLOY_PATH}"
|
||||
# -a 保留权限/时间并**保留软链本身**(8c 建的模块 wwwroot 软链是绝对路径,换机仍可解析);
|
||||
# 排除 venv/logs/run/.git:venv 不可搬移,logs/run 由目标侧自建,.git 不属于运行态。
|
||||
rsync -a --delete \
|
||||
--exclude 'venv/' --exclude '.venv/' --exclude 'logs/' --exclude 'run/' \
|
||||
--exclude '.git/' --exclude '__pycache__/' --exclude '*.pyc' \
|
||||
"${APP_DIR}/" "${DEPLOY_PATH}/"
|
||||
mkdir -p "${DEPLOY_PATH}/logs" "${DEPLOY_PATH}/run"
|
||||
# venv 软链复用(目标侧 start_cmd 要求 ${DEPLOY_PATH}/venv/bin/python 可用)
|
||||
if [ ! -e "${DEPLOY_PATH}/venv" ]; then
|
||||
ln -sfn "${VENV_DIR}" "${DEPLOY_PATH}/venv"
|
||||
fi
|
||||
[ -x "${DEPLOY_PATH}/venv/bin/python" ] || die "部署目录 venv 不可用: ${DEPLOY_PATH}/venv/bin/python"
|
||||
[ -f "${DEPLOY_PATH}/app/${APP_NAME}.py" ] || die "部署目录缺入口: ${DEPLOY_PATH}/app/${APP_NAME}.py"
|
||||
[ -f "${DEPLOY_PATH}/conf/config.json" ] || die "部署目录缺配置: ${DEPLOY_PATH}/conf/config.json"
|
||||
log "部署目录就位: ${DEPLOY_PATH}(app/conf/wwwroot/scripts + venv 软链 + logs/run)"
|
||||
RUN_DIR="${DEPLOY_PATH}"
|
||||
RUN_PY="${DEPLOY_PATH}/venv/bin/python"
|
||||
RUN_ENTRY="${DEPLOY_PATH}/app/${APP_NAME}.py"
|
||||
fi
|
||||
|
||||
# ---- 13. 启动应用(唯一入口 + 唯一端口,从部署目录启动)----
|
||||
# 先停旧进程(幂等重跑不撞端口):按 pid 文件 + 入口路径双口径清理。
|
||||
if [ -f "${RUN_DIR}/run/${APP_NAME}.pid" ]; then
|
||||
OLD_PID="$(cat "${RUN_DIR}/run/${APP_NAME}.pid" 2>/dev/null || true)"
|
||||
if [ -n "${OLD_PID:-}" ] && kill -0 "${OLD_PID}" 2>/dev/null; then
|
||||
log "停止旧进程 pid=${OLD_PID}"
|
||||
kill "${OLD_PID}" 2>/dev/null || true
|
||||
sleep 2
|
||||
fi
|
||||
fi
|
||||
pkill -f "${RUN_ENTRY}" 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
log "启动 ${APP_NAME} 于端口 ${PORT}(cwd=${RUN_DIR})"
|
||||
# cwd 必须是 RUN_DIR:conf/config.json 的 website.root="wwwroot" 是相对路径,
|
||||
# 从别处启动会解析不到 wwwroot → 所有 .ui/.dspy 404(含 /healthz)。
|
||||
( cd "${RUN_DIR}" && nohup "${RUN_PY}" "${RUN_ENTRY}" \
|
||||
> "${RUN_DIR}/logs/${APP_NAME}.out" 2>&1 & echo $! > "${RUN_DIR}/run/${APP_NAME}.pid" )
|
||||
sleep 6
|
||||
|
||||
# 进程 + 端口双自证(对应复测门禁 ②):起不来就把日志尾部打出来再 die,
|
||||
# 避免「HTTP=000 但没人知道为什么」的四轮同构复测。
|
||||
PROC_N="$(ps -ef | grep -F "${APP_NAME}.py" | grep -v grep | wc -l | tr -d '[:space:]')"
|
||||
log "进程数=${PROC_N}"
|
||||
if [ "${PROC_N}" = "0" ]; then
|
||||
echo "----- ${RUN_DIR}/logs/${APP_NAME}.out (tail 60) -----"
|
||||
tail -60 "${RUN_DIR}/logs/${APP_NAME}.out" 2>/dev/null || true
|
||||
echo "----------------------------------------------------"
|
||||
die "应用进程未存活(ps 计数 0),门禁 ② 不成立,详见上方日志尾部"
|
||||
fi
|
||||
|
||||
# ---- 14. 健康检查(两条口径都验:/healthz = 复测门禁 ①,/api/public/health = 原自检口径)----
|
||||
check_http() {
|
||||
curl -s -m 8 -o /dev/null -w '%{http_code}' "$1" 2>/dev/null || echo 000
|
||||
}
|
||||
HZ_CODE="$(check_http "http://127.0.0.1:${PORT}/healthz")"
|
||||
AH_CODE="$(check_http "http://127.0.0.1:${PORT}/api/public/health")"
|
||||
IP_CODE="$(check_http "http://${SSH_HOST:-127.0.0.1}:${PORT}/healthz")"
|
||||
log "健康检查: /healthz=${HZ_CODE} /api/public/health=${AH_CODE} 外网口 /healthz=${IP_CODE}"
|
||||
if [ "${HZ_CODE}" != "200" ] || [ "${AH_CODE}" != "200" ]; then
|
||||
echo "----- ${RUN_DIR}/logs/${APP_NAME}.out (tail 60) -----"
|
||||
tail -60 "${RUN_DIR}/logs/${APP_NAME}.out" 2>/dev/null || true
|
||||
echo "----------------------------------------------------"
|
||||
die "健康检查失败(/healthz=${HZ_CODE}, /api/public/health=${AH_CODE}),门禁 ① 不成立"
|
||||
fi
|
||||
|
||||
# 表数终态复述(对应复测门禁 ③):部署完成后 pbl 库必须有 37 张 pbl_* 表。
|
||||
FINAL_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}")"
|
||||
log "终态表数: ${DB_NAME}.pbl_* = ${FINAL_TABLES}/37"
|
||||
[ "${FINAL_TABLES}" = "37" ] || die "终态表数 ${FINAL_TABLES} != 37,门禁 ③ 不成立"
|
||||
|
||||
log "BUILD SUCCESS: ${APP_NAME} 已运行于 http://127.0.0.1:${PORT}/ (部署路径 ${DEPLOY_PATH},/healthz=200,表数 ${FINAL_TABLES}/37)"
|
||||
@ -9,6 +9,13 @@
|
||||
# (r6 实测 65 次 "WARNING: Location '../../wheelhouse' is ignored")。
|
||||
# 基座 wheel 由 pbl 用户 ~/.config/pip/pip.conf 的 find-links=/d/pipeline/workspaces/0/sdlc_general/wheelhouse
|
||||
# 供给(实测 pbl 可 ls 该目录 7 个 wheel),build.sh 第 7 步另以绝对路径 --find-links ${WHEELHOUSE} 兜底。
|
||||
# [develop 本轮修复 Bug BWY68rZR_Uh2A41JlGT7D / elS7VxwO7ufbPiPl-ucGA] 依赖声明「可满足性」
|
||||
# 已由 build.sh 硬门禁 6.6(scripts/check_requirements_satisfiable.py)静态把关:
|
||||
# 幻影包黑名单(pybricks / mysqlclient / hashlib-additional / 本地 pbl_* 模块)、
|
||||
# 标准库名误当第三方声明、基座包 == 版本与 wheelhouse 实物逐字一致、
|
||||
# wheelhouse 覆盖完整性、门禁 6.5 六项声明在位、失效内网源 10.20.30.41 残留、
|
||||
# 相对路径 --find-links(pip 不解析)——并同步扫描全部 modules/*/pyproject.toml 同类缺陷。
|
||||
# 基座版本口径 = env/test.json deps.corrected_requirements_hint(业主 2026-09-25 修正)。
|
||||
|
||||
# ---- 平台基础框架(必需,硬门禁 6.5)----
|
||||
ahserver==1.2.0 # [R7] 按 deps.corrected_requirements_hint 固定=宿主实装版本(原 >=1.2.0 无对应发行物)
|
||||
@ -39,7 +46,11 @@ python-dateutil>=2.8.2
|
||||
pytz>=2024.1
|
||||
jsonschema>=4.21.0
|
||||
PyYAML>=6.0.1
|
||||
hashlib-additional>=1.0.1 ; python_version < "3.0"
|
||||
# [Bug BWY68rZR_Uh2A41JlGT7D 修复] 删除 `hashlib-additional>=1.0.1 ; python_version < "3.0"`:
|
||||
# hashlib 是 Python 3 标准库(sys.stdlib_module_names 实测在列),marker 恒假 → 永不安装,
|
||||
# 属无意义幻影声明;且公共源真实发行名为 hashlib_additional(下划线),声明名对不上,
|
||||
# 一旦 marker 被误改成立即 "No matching distribution found" → pip install 整体 BUILD_RC=1。
|
||||
# 代码侧 hashlib 用法全部走标准库 import,无需任何第三方补充包。
|
||||
|
||||
# ---- 前端/模板处理(website.processors: .tmpl/.ui/.dspy)----
|
||||
Mako>=1.3.0
|
||||
|
||||
349
scripts/check_requirements_satisfiable.py
Normal file
349
scripts/check_requirements_satisfiable.py
Normal file
@ -0,0 +1,349 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""依赖声明可满足性静态校验(build.sh 硬门禁 6.6)。
|
||||
|
||||
背景(Bug BWY68rZR_Uh2A41JlGT7D / elS7VxwO7ufbPiPl-ucGA):
|
||||
build.sh 原硬门禁 6.5 只用 `grep ^包名` 校验「声明存在」,不校验「声明可满足」。
|
||||
结果 requirements.txt 里出现三类必然导致 `pip install -r` 失败(BUILD_RC=1)的缺陷,
|
||||
门禁全绿放行,直到部署阶段才炸:
|
||||
1) 版本下界凭空抬高、无对应发行物(ahserver>=1.4.0 而公共源最高 1.2.0)
|
||||
→ pip "No matching distribution found"
|
||||
2) 幻影包(pybricks:平台前端能力实际由 bricks_for_python 提供;
|
||||
公共 pypi 同名包是 LEGO Mindstorms 机器人库,装上即污染 venv)
|
||||
3) 不可编译包(mysqlclient:目标机无 mysql_config/mariadb_config,
|
||||
且 pbl 用户无 sudo 装不了 mariadb-devel → 源码编译必失败)
|
||||
本脚本把「可满足性」前移为静态门禁,并顺带扫描全部业务模块 pyproject.toml
|
||||
的同类缺陷(任务要求:不能只修报出来的那一个)。
|
||||
|
||||
校验项:
|
||||
A. 幻影包黑名单(应用 requirements*.txt + 全部模块 pyproject.toml)
|
||||
B. 标准库名被误当第三方包声明(sys.stdlib_module_names 比对)
|
||||
C. wheelhouse 固定版本一致性:`==` 声明的包若 wheelhouse 有 whl,版本必须逐字相同
|
||||
D. wheelhouse 覆盖完整性:wheelhouse 里的基座包必须全部在 requirements 中固定
|
||||
E. 硬门禁 6.5 六项基座声明在位(ahserver/sqlor/apppublic/appbase/rbac/PyMySQL)
|
||||
F. 失效内网源残留(--index-url/--extra-index-url/--trusted-host 指向 10.20.30.41)
|
||||
G. 相对路径 find-links(pip 不解析,实测 "Location '../../wheelhouse' is ignored")
|
||||
|
||||
用法:
|
||||
python3 scripts/check_requirements_satisfiable.py \
|
||||
--requirements requirements.txt \
|
||||
--wheelhouse ../../wheelhouse \
|
||||
--modules-glob ../../modules/*/pyproject.toml
|
||||
# rc=0 全部通过;rc=1 存在缺陷(逐条打印,build.sh die)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# ---------------------------------------------------------------- 常量配置
|
||||
|
||||
# 基座包:必须由机构 wheelhouse 离线供给(内网 pypi 在目标机不可达)
|
||||
BASE_PACKAGES = (
|
||||
"ahserver",
|
||||
"sqlor",
|
||||
"apppublic",
|
||||
"appbase",
|
||||
"rbac",
|
||||
"bricks_for_python",
|
||||
"accounting",
|
||||
)
|
||||
|
||||
# 硬门禁 6.5 口径:六项声明必须在位(与 build.sh 的 for dep in ... 一致)
|
||||
GATE65_REQUIRED = ("ahserver", "sqlor", "apppublic", "appbase", "rbac", "pymysql")
|
||||
|
||||
# 幻影包黑名单:名字 -> 禁止理由(命中即 FAIL)
|
||||
PHANTOM_BLACKLIST = {
|
||||
"pybricks": (
|
||||
"平台前端能力由 bricks_for_python(0.0.5) 提供(内含 load_pybricks);"
|
||||
"公共 pypi 的 pybricks 是 LEGO Mindstorms 机器人库,装上即污染 venv"
|
||||
),
|
||||
"mysqlclient": (
|
||||
"需 mysql_config/mariadb_config,目标机实测 which mysql_config 无输出,"
|
||||
"pbl 用户无 sudo 装不了 mariadb-devel → 源码编译必失败;"
|
||||
"DB 驱动由 PyMySQL + aiomysql 承担(sqlor 走 aiomysql)"
|
||||
),
|
||||
"hashlib-additional": (
|
||||
"Python 3 标准库已内置 hashlib;该声明带 `; python_version < \"3.0\"` 恒假 marker,"
|
||||
"属无意义幻影依赖(且公共源真实包名为 hashlib_additional,声明名对不上)"
|
||||
),
|
||||
}
|
||||
|
||||
# 本地模块名:由 build.sh 以 `pip install -e modules/{模块}` 安装,任何索引都没有发行物。
|
||||
# 作用域区分(关键):
|
||||
# - 写进应用 requirements*.txt → 致命(pip install -r 一次性解析,必报
|
||||
# "No matching distribution found for pbl_common",BUILD_RC=1)
|
||||
# - 写进模块 pyproject.toml 的 dependencies → 合法的模块间依赖
|
||||
# (module-development-spec 允许),由 build.sh 拓扑序先装 pbl_common 规避;
|
||||
# 此时只校验「被依赖的本地模块目录真实存在」,不存在才是幻影。
|
||||
LOCAL_MODULE_NAMES = ("pbl_common", "pbl_appcodes")
|
||||
|
||||
# 失效内网源(目标机 192.168.16.3 实测 connect timeout,5 次重试全失败)
|
||||
DEAD_INDEX_HOSTS = ("10.20.30.41",)
|
||||
|
||||
# 依赖声明行:包名 + 可选版本约束
|
||||
_REQ_LINE = re.compile(
|
||||
r"^(?P<name>[A-Za-z0-9][A-Za-z0-9._-]*)\s*"
|
||||
r"(?P<spec>(?:===|==|!=|~=|>=|<=|>|<)\s*[A-Za-z0-9._*+-]+\s*,?\s*)*"
|
||||
)
|
||||
|
||||
_NAME_NORMALIZE = re.compile(r"[-_.]+")
|
||||
|
||||
|
||||
def norm(name: str) -> str:
|
||||
"""PEP 503 包名归一化:大小写无关,-/_/. 等价。"""
|
||||
return _NAME_NORMALIZE.sub("-", name).lower()
|
||||
|
||||
|
||||
def strip_comment(line: str) -> str:
|
||||
"""剥掉行尾 `#` 注释(不处理引号内 #,requirements 语法里不存在该场景)。"""
|
||||
idx = line.find("#")
|
||||
return line if idx < 0 else line[:idx]
|
||||
|
||||
|
||||
def parse_requirements(path: str):
|
||||
"""解析 requirements.txt -> [(lineno, raw, name, spec, opts)],跳过空行/注释/选项行。"""
|
||||
out = []
|
||||
with open(path, encoding="utf-8", errors="replace") as fh:
|
||||
for lineno, raw in enumerate(fh.read().splitlines(), 1):
|
||||
body = strip_comment(raw).strip()
|
||||
if not body:
|
||||
continue
|
||||
if body.startswith("-"): # -r / --find-links / --index-url 等选项行
|
||||
out.append((lineno, raw, None, None, body))
|
||||
continue
|
||||
m = _REQ_LINE.match(body)
|
||||
if not m:
|
||||
out.append((lineno, raw, None, None, body))
|
||||
continue
|
||||
out.append((lineno, raw, m.group("name"), (m.group("spec") or "").strip(), None))
|
||||
return out
|
||||
|
||||
|
||||
def parse_pyproject_deps(path: str):
|
||||
"""从 pyproject.toml 抽 dependencies 字符串(不引 tomllib,py3.8 兼容)。"""
|
||||
txt = open(path, encoding="utf-8", errors="replace").read()
|
||||
deps = []
|
||||
m = re.search(r"dependencies\s*=\s*\[(.*?)\]", txt, re.S)
|
||||
if m:
|
||||
for item in re.findall(r"""["']([^"']+)["']""", m.group(1)):
|
||||
deps.append(item.strip())
|
||||
return deps
|
||||
|
||||
|
||||
def scan_wheelhouse(wh_dir: str):
|
||||
"""wheelhouse 目录 -> {归一化包名: 版本}(取 *.whl 文件名前两段)。"""
|
||||
found = {}
|
||||
if not wh_dir or not os.path.isdir(wh_dir):
|
||||
return found
|
||||
for fn in sorted(os.listdir(wh_dir)):
|
||||
if not fn.endswith(".whl"):
|
||||
continue
|
||||
parts = fn.split("-")
|
||||
if len(parts) >= 2:
|
||||
found[norm(parts[0])] = parts[1]
|
||||
return found
|
||||
|
||||
|
||||
def dep_name_of(spec_str: str):
|
||||
"""从 'ahserver>=1.4.0' / 'pbl_common==1.0' 抽出归一化包名。"""
|
||||
m = re.match(r"^([A-Za-z0-9][A-Za-z0-9._-]*)", spec_str.strip())
|
||||
return norm(m.group(1)) if m else None
|
||||
|
||||
|
||||
def pinned_version(spec: str):
|
||||
"""从 '==1.2.0' / '== 1.2.0' 抽出精确固定版本;非 == 返回 None。"""
|
||||
m = re.search(r"==\s*([A-Za-z0-9._*+-]+)", spec or "")
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
ap = argparse.ArgumentParser(description="依赖声明可满足性静态校验(build.sh 门禁 6.6)")
|
||||
ap.add_argument("--requirements", default="requirements.txt", help="应用 requirements.txt 路径")
|
||||
ap.add_argument("--extra-requirements", action="append", default=[],
|
||||
help="附加 requirements 文件(如 requirements-test.txt),可多次")
|
||||
ap.add_argument("--wheelhouse", default="", help="机构 wheelhouse 目录(基座 whl 离线源)")
|
||||
ap.add_argument("--modules-glob", default="",
|
||||
help="业务模块 pyproject.toml 通配(如 ../../modules/*/pyproject.toml)")
|
||||
ap.add_argument("--quiet", action="store_true", help="只打印结论行")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
errors = [] # 致命:必然导致 pip install 失败 / 门禁不过
|
||||
warns = [] # 提示:不阻断
|
||||
|
||||
def err(check, msg):
|
||||
errors.append("[%s] %s" % (check, msg))
|
||||
|
||||
def warn(check, msg):
|
||||
warns.append("[%s] %s" % (check, msg))
|
||||
|
||||
# ---------------- 0. 文件存在性 ----------------
|
||||
if not os.path.isfile(args.requirements):
|
||||
err("0", "requirements 文件不存在: %s" % args.requirements)
|
||||
print("FAIL 依赖声明可满足性校验:requirements 文件缺失")
|
||||
for e in errors:
|
||||
print(" " + e)
|
||||
return 1
|
||||
|
||||
req_files = [args.requirements] + [p for p in args.extra_requirements if os.path.isfile(p)]
|
||||
wh = scan_wheelhouse(args.wheelhouse)
|
||||
stdlib = set(getattr(sys, "stdlib_module_names", set()))
|
||||
|
||||
if not args.quiet:
|
||||
print("[6.6] requirements 文件: %s" % ", ".join(req_files))
|
||||
print("[6.6] wheelhouse: %s (%d 个 whl: %s)"
|
||||
% (args.wheelhouse or "<未指定>", len(wh), ", ".join(sorted(wh)) or "-"))
|
||||
|
||||
# ---------------- A/B/F/G. 应用 requirements 逐行校验 ----------------
|
||||
declared = {} # 归一化名 -> (lineno, spec, file)
|
||||
for rf in req_files:
|
||||
for lineno, raw, name, spec, opt in parse_requirements(rf):
|
||||
if opt is not None:
|
||||
# F. 失效内网源残留
|
||||
for host in DEAD_INDEX_HOSTS:
|
||||
if host in opt:
|
||||
err("F", "%s:L%d 残留失效内网源 %s(目标机实测 connect timeout): %s"
|
||||
% (rf, lineno, host, opt.strip()[:100]))
|
||||
# G. 相对路径 find-links(pip 不解析)
|
||||
mfl = re.match(r"--find-links[=\s]+(\S+)", opt)
|
||||
if mfl:
|
||||
target = mfl.group(1)
|
||||
if not os.path.isabs(target) and not target.startswith(("http://", "https://")):
|
||||
err("G", "%s:L%d --find-links 用相对路径 %r,pip 不解析"
|
||||
"(实测 WARNING: Location is ignored);改绝对路径或由 build.sh 传 --find-links"
|
||||
% (rf, lineno, target))
|
||||
continue
|
||||
if name is None:
|
||||
warn("A", "%s:L%d 无法解析的声明行: %s" % (rf, lineno, raw.strip()[:100]))
|
||||
continue
|
||||
|
||||
n = norm(name)
|
||||
declared[n] = (lineno, spec or "", rf)
|
||||
|
||||
# A. 幻影包黑名单
|
||||
if n in PHANTOM_BLACKLIST or n.replace("-", "_") in PHANTOM_BLACKLIST:
|
||||
key = n if n in PHANTOM_BLACKLIST else n.replace("-", "_")
|
||||
err("A", "%s:L%d 幻影依赖 %s —— %s" % (rf, lineno, name, PHANTOM_BLACKLIST[key]))
|
||||
|
||||
# A2. 本地模块写进 requirements(pip 索引无发行物 → 必失败)
|
||||
if n in LOCAL_MODULE_NAMES or n.startswith("pbl-"):
|
||||
err("A", "%s:L%d 本地模块 %s 不得写进 requirements —— 任何索引都无其发行物,"
|
||||
"pip install -r 必报 No matching distribution;"
|
||||
"由 build.sh 以 pip install -e modules/{模块} 安装"
|
||||
% (rf, lineno, name))
|
||||
|
||||
# B. 标准库名误当第三方包
|
||||
base = n.replace("-", "_")
|
||||
if base in stdlib:
|
||||
err("B", "%s:L%d %s 是 Python 标准库模块名,不得作为第三方依赖声明" % (rf, lineno, name))
|
||||
|
||||
# C. wheelhouse 固定版本一致性
|
||||
pv = pinned_version(spec or "")
|
||||
if n in wh:
|
||||
if pv is None:
|
||||
err("C", "%s:L%d 基座包 %s 未用 == 精确固定(实际 %r),"
|
||||
"wheelhouse 实装 %s;非固定下界在公共源无对应发行物时 pip 必失败"
|
||||
% (rf, lineno, name, (spec or "").strip(), wh[n]))
|
||||
elif pv != wh[n]:
|
||||
err("C", "%s:L%d %s==%s 与 wheelhouse 实装 %s 不一致(pip 找不到发行物)"
|
||||
% (rf, lineno, name, pv, wh[n]))
|
||||
elif pv is None and re.search(r">=\s*[0-9]", spec or ""):
|
||||
# 非基座包用 >= 下界:提示核对下界是否有对应发行物(不阻断,公网包通常可满足)
|
||||
warn("C", "%s:L%d %s 用下界约束 %r,请确认公共源存在满足该下界的发行物"
|
||||
% (rf, lineno, name, (spec or "").strip()))
|
||||
|
||||
# ---------------- D. wheelhouse 覆盖完整性 ----------------
|
||||
for pkg in BASE_PACKAGES:
|
||||
npkg = norm(pkg)
|
||||
if npkg in wh and npkg not in declared:
|
||||
err("D", "wheelhouse 提供基座包 %s(%s) 但 requirements 未声明固定版本" % (pkg, wh[npkg]))
|
||||
|
||||
# ---------------- E. 硬门禁 6.5 六项声明 ----------------
|
||||
for pkg in GATE65_REQUIRED:
|
||||
if norm(pkg) not in declared:
|
||||
err("E", "硬门禁 6.5 缺基座声明: %s" % pkg)
|
||||
|
||||
# ---------------- 模块 pyproject 同类缺陷扫描 ----------------
|
||||
mod_files = sorted(glob.glob(args.modules_glob)) if args.modules_glob else []
|
||||
if not args.quiet:
|
||||
print("[6.6] 业务模块 pyproject.toml 扫描: %d 个" % len(mod_files))
|
||||
mod_hits = 0
|
||||
# modules 根目录:从 --modules-glob 反推(.../modules/*/pyproject.toml)
|
||||
modules_root = ""
|
||||
if args.modules_glob:
|
||||
_g = args.modules_glob.replace("\\", "/")
|
||||
if "/modules/" in _g:
|
||||
modules_root = _g.split("/modules/")[0] + "/modules"
|
||||
elif _g.startswith("modules/"):
|
||||
modules_root = "modules"
|
||||
local_deps = {} # 被依赖本地模块 -> [依赖它的 pyproject 列表]
|
||||
for mf in mod_files:
|
||||
for dep in parse_pyproject_deps(mf):
|
||||
dn = dep_name_of(dep)
|
||||
if not dn:
|
||||
continue
|
||||
if dn in PHANTOM_BLACKLIST or dn.replace("-", "_") in PHANTOM_BLACKLIST:
|
||||
key = dn if dn in PHANTOM_BLACKLIST else dn.replace("-", "_")
|
||||
err("A", "%s 声明幻影依赖 %s —— %s" % (mf, dep, PHANTOM_BLACKLIST[key]))
|
||||
mod_hits += 1
|
||||
# A2'. 模块间本地依赖:合法,但被依赖模块目录必须真实存在
|
||||
# (否则 build.sh 拓扑序也装不上 → 入口 ImportError → 进程起不来)
|
||||
if dn in LOCAL_MODULE_NAMES or dn.startswith("pbl-"):
|
||||
local_name = dn.replace("-", "_")
|
||||
if modules_root:
|
||||
cand = os.path.join(modules_root, local_name)
|
||||
if not os.path.isdir(cand):
|
||||
err("A", "%s 依赖本地模块 %s,但 %s 不存在(幻影本地依赖,"
|
||||
"pip install -e 阶段必失败)" % (mf, dep, cand))
|
||||
mod_hits += 1
|
||||
elif local_name != os.path.basename(mf).split(os.sep)[0]:
|
||||
local_deps.setdefault(local_name, []).append(mf)
|
||||
if dn.replace("-", "_") in stdlib:
|
||||
err("B", "%s 把标准库 %s 当第三方依赖声明" % (mf, dep))
|
||||
mod_hits += 1
|
||||
pv = pinned_version(dep)
|
||||
if dn in wh:
|
||||
if pv is not None and pv != wh[dn]:
|
||||
err("C", "%s 固定 %s 与 wheelhouse 实装 %s 不一致" % (mf, dep, wh[dn]))
|
||||
mod_hits += 1
|
||||
elif pv is None:
|
||||
m = re.search(r">=\s*([0-9][0-9A-Za-z.]*)", dep)
|
||||
if m and m.group(1) > wh[dn]:
|
||||
err("C", "%s 下界 %s 高于 wheelhouse 实装 %s(无对应发行物)"
|
||||
% (mf, dep, wh[dn]))
|
||||
mod_hits += 1
|
||||
if not args.quiet:
|
||||
print("[6.6] 模块同类缺陷命中: %d" % mod_hits)
|
||||
if local_deps:
|
||||
print("[6.6] 本地模块间依赖(build.sh 须按拓扑序先装被依赖方):")
|
||||
for k in sorted(local_deps):
|
||||
print(" %s <- %d 个模块依赖: %s"
|
||||
% (k, len(local_deps[k]),
|
||||
", ".join(sorted(os.path.basename(os.path.dirname(p))
|
||||
for p in local_deps[k])[:6])))
|
||||
if "pbl_common" in local_deps:
|
||||
print(" 注: build.sh 第 8 步已显式把 pbl_common 置于 INSTALL_DIRS 首位"
|
||||
"(R8 拓扑序修复),与上述依赖图一致")
|
||||
|
||||
# ---------------- 结论 ----------------
|
||||
for w in warns:
|
||||
print(" WARN " + w)
|
||||
if errors:
|
||||
print("FAIL 依赖声明可满足性校验未通过(%d 项):" % len(errors))
|
||||
for e in errors:
|
||||
print(" " + e)
|
||||
return 1
|
||||
|
||||
print("PASS 依赖声明可满足性校验通过:%d 个 requirements 文件 / %d 项声明 / "
|
||||
"基座 %d 包与 wheelhouse 版本逐字一致 / %d 个模块 pyproject 零幻影依赖"
|
||||
% (len(req_files), len(declared), len([p for p in BASE_PACKAGES if norm(p) in wh]),
|
||||
len(mod_files)))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
x
Reference in New Issue
Block a user