177 lines
7.2 KiB
Bash
177 lines
7.2 KiB
Bash
#!/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_<pkg>_<dir>.py,
|
||
# dir="." 时点号进模块名触发 Nuitka AssertionError;子包副本名带前缀则产物 .so 名与
|
||
# mv 目标不符 FAIL(mv)。实验定型形态:cp 成 <目录名>.py。
|
||
# ② init 的编译输出目录用独立 $WORK/initout/<pkg>(不能直接进最终 dest:Nuitka 按
|
||
# 副本 stem 命名产物,需 mv 成 __init__.<tag>.so)。
|
||
gen_jobs() {
|
||
local pkgdir=$1 outpkg=$2
|
||
find "$pkgdir" -name "*.py" -not -path "*__pycache__*" -not -name "test_*" \
|
||
-not -path "*/tests/*" -not -path "*/.git/*" | while read -r f; do
|
||
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"
|
||
# 清理 .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
|
||
|
||
# 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
|