#!/bin/bash # Nuitka 二进制化构建(2026-09-17 立项):把自有 Python 包逐模块编译成 .so, # 产出 site-packages 覆盖层 binpkg_overlay/(保留包目录结构 + 包内数据文件)。 # # 实测定型的编译形态(测试机 2026-09-17 实验 out1-out5): # - 逐模块 --module 编译,保留包目录结构(整包单 .so 会断 importlib 动态导入子模块,弃) # - __init__.py 不能直接 --module(Nuitka 拒),用改名 trick:cp 成 <目录名>.py 编译 # (PyInit 符号=文件 stem=目录名,天然正确;objcopy 改动态符号无效,弃) # - 数据文件(.json/.tmpl 等)不进编译,原样拷进覆盖层;运行时 __file__ 是虚拟 .py 路径 # (与 .so 同目录),dirname(__file__) 定位数据目录的模式成立 # - 相对导入/绝对导入/inspect.signature/importlib 动态导入 编译后全验证通过 # # 用法:bash scripts/build_binary.sh [输出目录] (在应用根目录或任意位置跑,自定位) set -uo pipefail ROOT=$(cd "$(dirname "$0")/.." && pwd) PY=$ROOT/py3/bin/python OUT=${1:-$ROOT/binpkg_overlay} TAG="cpython-310-x86_64-linux-gnu" WORK=$(mktemp -d /tmp/nuitka_build.XXXXXX) JOBS=$WORK/jobs.tsv FAILLOG=$WORK/fail.log OKLOG=$WORK/ok.log : > "$JOBS"; : > "$FAILLOG"; : > "$OKLOG" mkdir -p "$OUT" # repo目录:包目录(测试机 pkgs/ 布局;包目录名以仓库实查为准,勿凭记忆) MANIFEST=" pipeline-service:pipeline_service pipeline_core:pipeline_core pipeline_ops:pipeline_ops pipeline-sdlc:pipeline_sdlc pipeline-bidding:pipeline_bidding pipeline-opportunity:pipeline_opportunity pipeline-platform:pipeline_platform pipeline-task:pipeline_task pipeline-llm:pipeline_llm apppublic:appPublic sqlor:sqlor ahserver:ahserver rbac:rbac appbase:appbase app_audit:app_audit xls2ddl:xls2ddl bricks-for-python:bricks_for_python accounting:accounting account_resource:account_resource storage_resource:storage_resource supplychain:supplychain discount:discount pricing:pricing product_management:product_management unipay:unipay smssend:smssend filemgr:filemgr dapi:dapi uapi:uapi rag:rag dingdingflow:dingdingflow showcase:showcase ticket:ticket voucher:voucher tenant:tenant " # 生成编译任务:src dest mode stem cout # init 分支(2026-09-17 首跑实抓两处 bug 的修正): # ① 副本名必须**恰好等于目录名**(顶层=包名/子包=子包名)——曾拼成 init__.py, # dir="." 时点号进模块名触发 Nuitka AssertionError;子包副本名带前缀则产物 .so 名与 # mv 目标不符 FAIL(mv)。实验定型形态:cp 成 <目录名>.py。 # ② init 的编译输出目录用独立 $WORK/initout/(不能直接进最终 dest:Nuitka 按 # 副本 stem 命名产物,需 mv 成 __init__..so)。 gen_jobs() { local pkgdir=$1 outpkg=$2 find "$pkgdir" -name "*.py" -not -path "*__pycache__*" -not -name "test_*" \ -not -path "*/tests/*" -not -path "*/.git/*" \ -not -name "*.bak.py" -not -name "*.old.py" | while read -r f; do # py_compile 预门(2026-09-17 实抓):源码态就语法坏的死文件(p2p_middleware/rsa_key_rw/ # clickhouseor,全仓零引用、从未可 import)直接跳过——Nuitka 对语法零容忍会整批 FAIL "$PY" -m py_compile "$f" 2>/dev/null || { echo "SKIP(broken-syntax,源码态死文件): $f" >> "$FAILLOG.broken"; continue; } local rel=${f#$pkgdir/} dir stem src mode cout dir=$(dirname "$rel"); stem=$(basename "$f" .py) local destdir="$OUT/$outpkg"; [ "$dir" != "." ] && destdir="$OUT/$outpkg/$dir" if [ "$stem" = "__init__" ]; then local comp; comp=$(basename "$dir"); [ "$dir" = "." ] && comp=$outpkg src="$WORK/initcopy/$outpkg/$comp.py" mkdir -p "$(dirname "$src")" "$WORK/initout/$outpkg" cp "$f" "$src" mode=init; stem=$comp; cout="$WORK/initout/$outpkg" else src=$f; mode=mod; cout=$destdir fi # 增量:产物 .so 比源码新则跳过(重跑只补缺/失败项) local sofinal="$destdir/$stem.$TAG.so"; [ "$mode" = init ] && sofinal="$destdir/__init__.$TAG.so" if [ -f "$sofinal" ] && [ "$sofinal" -nt "$f" ]; then continue; fi printf "%s\t%s\t%s\t%s\t%s\n" "$src" "$destdir" "$mode" "$stem" "$cout" >> "$JOBS" done # 包内数据文件原样进覆盖层(不用 rsync:目标机可能没装) mkdir -p "$OUT/$outpkg" (cd "$pkgdir" && find . -type f -not -name "*.py" -not -name "*.pyc" \ -not -path "*__pycache__*" -not -path "./.git/*") | while read -r rel; do mkdir -p "$OUT/$outpkg/$(dirname "$rel")" cp "$pkgdir/$rel" "$OUT/$outpkg/$rel" done } echo "$MANIFEST" | grep -v '^$' | while read -r entry; do repo=${entry%%:*}; pkg=${entry#*:} pkgdir=$ROOT/pkgs/$repo/$pkg [ -d "$pkgdir" ] || { echo "SKIP(目录不存在): $pkgdir"; continue; } gen_jobs "$pkgdir" "$pkg" done # 入口与 app 顶层模块(script 形态,编译成顶层模块进 _appbin;5 字段与 worker 对齐) mkdir -p "$OUT/_appbin" for f in "$ROOT"/app/*.py; do stem=$(basename "$f" .py) printf "%s\t%s\tmod\t%s\t%s\n" "$f" "$OUT/_appbin" "$stem" "$OUT/_appbin" >> "$JOBS" done cat > "$WORK/worker.sh" <<'EOS' src=$1; dest=$2; mode=$3; stem=$4; cout=$5 tag="cpython-310-x86_64-linux-gnu" mkdir -p "$dest" "$cout" out=$("$PYBIN" -m nuitka --module "$src" --output-dir="$cout" --remove-output 2>&1) rc=$? if [ $rc -ne 0 ]; then echo "FAIL $src :: $(echo "$out" | tail -2)" >> "$FAILLOG"; exit 0; fi if [ "$mode" = init ]; then mv "$cout/$stem.$tag.so" "$dest/__init__.$tag.so" 2>/dev/null || { echo "FAIL(mv) $src" >> "$FAILLOG"; exit 0; } else [ -f "$cout/$stem.$tag.so" ] || { echo "FAIL(no-so) $src" >> "$FAILLOG"; exit 0; } fi echo "OK $src" >> "$OKLOG" EOS export PYBIN=$PY export FAILLOG OKLOG total=$(wc -l < "$JOBS") echo "编译任务 $total 个,并行 4 …" xargs -a "$JOBS" -P 4 -n 5 bash "$WORK/worker.sh" # app 命名空间包(2026-09-17 编译态起服务实抓):入口目录 app/ 无 __init__.py,源码态靠 # sys.path 插 root_dir 以命名空间包形式被 `from app.quality_gate import ...` 引用;编译态 # 必须把 app 模块 .so 复制一份进 overlay/app/(无 __init__ = 命名空间包,py3 原生支持 # 目录内 .so 子模块)。_appbin 顶层副本保留给入口自身 `from ktv_adapter import ...` 形态。 mkdir -p "$OUT/app" cp "$OUT"/_appbin/*.so "$OUT/app/" 2>/dev/null # 清理 .build 残留(NFS 上 rm -rf 偶发 Directory not empty;--remove-output 只删自己那轮) find "$OUT" -maxdepth 4 -type d -name "*.build" -exec rm -rf {} + 2>/dev/null find "$OUT" -maxdepth 4 -type d -name "*.build" -exec rm -rf {} + 2>/dev/null find "$OUT" -maxdepth 4 -type d -name "*.build" | while read -r d; do rm -rf "$d" 2>/dev/null; done ok=$(wc -l < "$OKLOG"); fail=$(wc -l < "$FAILLOG") echo "完成: OK=$ok FAIL=$fail" if [ "$fail" -gt 0 ]; then echo "== 失败清单 =="; cat "$FAILLOG"; exit 1; fi # runner(工具脚本,非业务代码,保留源码形态) cat > "$OUT/_appbin/run_compiled.py" <<'EOS' #!/usr/bin/env python3 """编译态入口 runner:sys.argv 原样透传给 ahserver webapp 的参数解析。 sys.path 顺序:_appbin(入口编译模块) > overlay 根(编译业务包,遮蔽 site-packages 源码) > 其余。""" import os, sys _here = os.path.dirname(os.path.abspath(__file__)) _overlay = os.path.dirname(_here) sys.path.insert(0, _overlay) sys.path.insert(0, _here) import pipeline_app pipeline_app.main() EOS cat > "$OUT/_appbin/run_accounting_compiled.py" <<'EOS' #!/usr/bin/env python3 """编译态记账 worker runner:sys.argv 透传(-w workdir)。""" import os, sys _here = os.path.dirname(os.path.abspath(__file__)) _overlay = os.path.dirname(_here) sys.path.insert(0, _overlay) sys.path.insert(0, _here) import pipeline_accounting_worker pipeline_accounting_worker.main() EOS # BUILD_INFO:逐仓哈希(部署对账/回滚点用) HASHF=$WORK/hashes.jsonl : > "$HASHF" echo "$MANIFEST" | grep -v '^$' | while read -r entry; do repo=${entry%%:*} h=$(git -C "$ROOT/pkgs/$repo" rev-parse HEAD 2>/dev/null || echo unknown) printf ' "%s": "%s"\n' "$repo" "$h" >> "$HASHF" done printf ' "pipeline-app": "%s"\n' "$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || echo unknown)" >> "$HASHF" { echo "{" echo " \"built_at\": \"$(date -Is)\"," echo " \"python\": \"$($PY --version 2>&1)\"," awk 'NR>1{printf ",\n"} {printf "%s", $0} END{print ""}' "$HASHF" echo "}" } > "$OUT/BUILD_INFO.json" so=$(find "$OUT" -name "*.$TAG.so" | wc -l) echo "覆盖层: $OUT (.so=$so)" du -sh "$OUT" | cut -f1