P2-1:preflight reuse/CPU/内存校验块小提取为 _preflight_config_checks(纯移动,深耦合处如实停)。 P2-2:build_vm_command 降级为私有 _build_vm_command; Docker goal 内联解析顶部加正典分叉说明 + 同步检查清单。 P2-3:gaps grep 文本断言改成真实 Worker._run_attempt 行为断言。P2-4:README/build.sh 口径对齐(章节顺序、 BUILDINFO 名字列表、Node.js 不装写实)。P2-5:stop_vm 删除失败记审计。P2-6:CPU/内存校验统一进 preflight。 测试:原 84 + 新增 33 = 117 项,mock,无真 VM。
139 lines
6.2 KiB
Bash
Executable File
139 lines
6.2 KiB
Bash
Executable File
#!/bin/bash
|
||
# chv-guest 镜像构建脚本(补缺口轨:从 TODO 骨架改成真实步骤)。
|
||
#
|
||
# 用法(必须在带 /dev/kvm 的宿主上跑全量模式):
|
||
# CODEX_BIN_DIR=bin UBUNTU_VERSION=24.04 CODEX_VERSION=0.164.0-alpha.9 \
|
||
# REPO_COMMIT=$(git rev-parse --short HEAD) ./build.sh out/
|
||
# ./build.sh --dry-run out/ # 只打印命令,不执行、不要求 KVM
|
||
#
|
||
# 产物(out/ 内):
|
||
# vmlinux # virt-get-kernel 从云镜像取出的内核(CHV --kernel 指向它)
|
||
# disk.raw # qemu-img 转成的 raw 根文件系统镜像(CHV_ROOTFS 指向它)
|
||
# BUILDINFO.json # {tag, ubuntu, codex_version, repo_commit, built_at, contents}
|
||
# 绝不进镜像的东西:任何模型 key、系统 key、用户账号数据、
|
||
# 生产凭证;模型 key 由客机 cloud-init 预置的运行环境变量名
|
||
# 注入,不写进镜像层。
|
||
#
|
||
# 客机 host key 硬性要求(见 README §5,补缺口轨新增):
|
||
# 客机必须装 cloud-init 且支持 NoCloud seed,首启先应用
|
||
# seed 再启动 sshd,且不得无条件重新生成 host key,否则
|
||
# 执行器的 per-VM 预置钉死即失效。本脚本在镜像里写
|
||
# /etc/cloud/cloud.cfg.d/99-chv-hostkey.cfg(ssh_deletekeys:
|
||
# false、ssh_genkeytypes: []),把“不重生成”钉进镜像。
|
||
set -eu
|
||
|
||
DRY_RUN=0
|
||
if [ "${1:-}" = "--dry-run" ]; then
|
||
DRY_RUN=1
|
||
shift
|
||
fi
|
||
OUT="${1:-out}"
|
||
: "${CODEX_BIN_DIR:?CODEX_BIN_DIR 未设:需 npm 安装 @openai/codex 后的配对二进制目录}"
|
||
: "${UBUNTU_VERSION:?UBUNTU_VERSION 未设}"
|
||
: "${CODEX_VERSION:?CODEX_VERSION 未设}"
|
||
: "${REPO_COMMIT:?REPO_COMMIT 未设:构建时仓库 commit 短 hash}"
|
||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
REPO_DIR="${REPO_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
||
TAG="chv-guest-${UBUNTU_VERSION}-${CODEX_VERSION}-${REPO_COMMIT}"
|
||
UBUNTU_IMAGE_URL="${UBUNTU_IMAGE_URL:-https://cloud-images.ubuntu.com/releases/${UBUNTU_VERSION}/release/ubuntu-${UBUNTU_VERSION}-server-cloudimg-amd64.img}"
|
||
|
||
run() {
|
||
if [ "$DRY_RUN" = "1" ]; then
|
||
echo "DRY-RUN: $*"
|
||
else
|
||
echo "+ $*"
|
||
"$@"
|
||
fi
|
||
}
|
||
|
||
# 参数校验(干跑与全量都做,不依赖 KVM)。
|
||
for bin in codex codex-code-mode-host; do
|
||
if [ ! -x "$CODEX_BIN_DIR/$bin" ]; then
|
||
echo "缺少配对二进制:$CODEX_BIN_DIR/$bin,构建失败" >&2
|
||
exit 1
|
||
fi
|
||
done
|
||
if [ ! -d "$CODEX_BIN_DIR/codex-resources" ]; then
|
||
echo "缺少 codex-resources(bwrap 载体),构建失败" >&2
|
||
exit 1
|
||
fi
|
||
if [ ! -f "$REPO_DIR/host.py" ] || [ ! -d "$REPO_DIR/cloud" ]; then
|
||
echo "REPO_DIR 不是 codex-app-server 仓库根(缺 host.py/cloud/):$REPO_DIR" >&2
|
||
exit 1
|
||
fi
|
||
if [ "$DRY_RUN" = "0" ]; then
|
||
if [ ! -e /dev/kvm ]; then
|
||
echo "CHV_GUEST_BUILD needs a KVM host: /dev/kvm 不存在" >&2
|
||
exit 1
|
||
fi
|
||
for tool in curl qemu-img virt-customize virt-get-kernel python3; do
|
||
if ! command -v "$tool" >/dev/null 2>&1; then
|
||
echo "缺少构建工具:$tool,构建失败" >&2
|
||
exit 1
|
||
fi
|
||
done
|
||
fi
|
||
|
||
run mkdir -p "$OUT"
|
||
IMG="$OUT/ubuntu-${UBUNTU_VERSION}-server-cloudimg-amd64.img"
|
||
if [ "$DRY_RUN" = "1" ]; then
|
||
echo "DRY-RUN: curl -fL $UBUNTU_IMAGE_URL -o $IMG"
|
||
echo "DRY-RUN: [ -s $IMG ] || exit 1"
|
||
else
|
||
if [ ! -s "$IMG" ]; then
|
||
curl -fL "$UBUNTU_IMAGE_URL" -o "$IMG"
|
||
fi
|
||
[ -s "$IMG" ] || { echo "下载的云镜像为空:$IMG" >&2; exit 1; }
|
||
if [ -n "${UBUNTU_IMAGE_SHA256:-}" ]; then
|
||
echo "$UBUNTU_IMAGE_SHA256 $IMG" | sha256sum -c -
|
||
fi
|
||
fi
|
||
run qemu-img convert -f qcow2 -O raw "$IMG" "$OUT/disk.raw"
|
||
# 清单软件(照抄 cloud/docker/Dockerfile 的 apt/pip 清单,
|
||
# 中文 LaTeX xelatex+CJK 故意不装,见 README;Playwright
|
||
# 默认不装,PLAYWRIGHT=1 时才加装,属可选步骤)。
|
||
run virt-customize -a "$OUT/disk.raw" \
|
||
--install git,pandoc,poppler-utils,latexmk,texlive-latex-base,openssh-server,cloud-init,qemu-guest-agent,python3,python3-pip \
|
||
--run-command 'pip3 install --no-cache-dir --break-system-packages python-docx openpyxl python-pptx PyMuPDF Pillow || pip3 install --no-cache-dir python-docx openpyxl python-pptx PyMuPDF Pillow' \
|
||
--copy-in "$CODEX_BIN_DIR/codex:/usr/local/bin/" \
|
||
--copy-in "$CODEX_BIN_DIR/codex-code-mode-host:/usr/local/bin/" \
|
||
--copy-in "$CODEX_BIN_DIR/codex-resources:/usr/local/bin/" \
|
||
--run-command 'chmod +x /usr/local/bin/codex /usr/local/bin/codex-code-mode-host; ln -sf /usr/local/bin/codex-resources/bwrap /usr/local/bin/bwrap' \
|
||
--mkdir /app \
|
||
--copy-in "$REPO_DIR/host.py:/app/" \
|
||
--copy-in "$REPO_DIR/cloud:/app/" \
|
||
--run-command 'printf "ssh_deletekeys: false\nssh_genkeytypes: []\n" > /etc/cloud/cloud.cfg.d/99-chv-hostkey.cfg; systemctl enable ssh || true'
|
||
if [ "${PLAYWRIGHT:-0}" = "1" ]; then
|
||
run virt-customize -a "$OUT/disk.raw" \
|
||
--run-command 'pip3 install --no-cache-dir playwright; python3 -m playwright install --with-deps chromium'
|
||
fi
|
||
run virt-get-kernel -a "$OUT/disk.raw" -o "$OUT/vmlinux"
|
||
# 产物校验(干跑只打印,不执行校验命令的副作用部分)。
|
||
if [ "$DRY_RUN" = "1" ]; then
|
||
echo "DRY-RUN: test -s $OUT/vmlinux && test -s $OUT/disk.raw"
|
||
echo "DRY-RUN: python3 生成 $OUT/BUILDINFO.json 并校验 tag=$TAG"
|
||
else
|
||
test -s "$OUT/vmlinux" || { echo "内核产物为空:$OUT/vmlinux" >&2; exit 1; }
|
||
test -s "$OUT/disk.raw" || { echo "磁盘产物为空:$OUT/disk.raw" >&2; exit 1; }
|
||
python3 - "$OUT/BUILDINFO.json" "$TAG" "$UBUNTU_VERSION" "$CODEX_VERSION" "$REPO_COMMIT" <<'PYEOF'
|
||
import json, sys, time
|
||
path, tag, ubuntu, codex, commit = sys.argv[1:6]
|
||
info = {"tag": tag, "ubuntu": ubuntu, "codex_version": codex,
|
||
"repo_commit": commit, "built_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||
"contents": ["git", "pandoc", "poppler-utils", "latexmk", "texlive-latex-base",
|
||
"openssh-server", "cloud-init", "qemu-guest-agent",
|
||
"python-docx", "openpyxl", "python-pptx", "PyMuPDF", "Pillow",
|
||
"codex", "codex-code-mode-host", "codex-resources/bwrap", "/app/host.py"]}
|
||
with open(path, "w") as fh:
|
||
json.dump(info, fh, indent=2)
|
||
PYEOF
|
||
python3 -c "import json; d=json.load(open('$OUT/BUILDINFO.json')); assert d['tag'] == '$TAG', d"
|
||
fi
|
||
echo "tag=$TAG"
|
||
echo "out=$OUT"
|
||
if [ "$DRY_RUN" = "1" ]; then
|
||
echo "干跑结束:未执行任何构建步骤,镜像未建成"
|
||
else
|
||
echo "构建完成:$OUT/vmlinux $OUT/disk.raw $OUT/BUILDINFO.json"
|
||
fi
|