Files
MeowOS-For-R3P2/scripts/build/build_devkit.sh
T
AskaEth 0531603779 fix(devkit): M24 r1 评审 — P1 --with-sources 静默漏源码 + time64 归因更正
P1(阻断):stamp 命中时 main 提前 return,--with-sources 在 return 之后,
于是「先构建再 --with-sources」会打印成功、退出码 0、却不产出 src/。

- 把源码整理抽成 stage_sources(),stamp 命中路径先兑现 WITH_SOURCES 再返回;
  整理后按钉住的 sha256 复核(sha 漂移/缺文件都重铺),任何失败即非零退出
- 新增 test_build_devkit.sh(12 项,秒级):stamp 命中下的 --with-sources、
  src 缺文件/被改坏的自愈、--verify-only 的 MANIFEST 篡改检测
- skip 条件额外要求 MANIFEST 与 bin 逐条 sha 一致;--clean 同时清 out/src;
  全新构建未带 --with-sources 时删除上次残留 src/(避免源码与二进制错配)

P2-a:top 排除理由改写。根因是 zig 0.17 musl 的 time64 ABI 错配(mips o32 是
ILP32 + 内核 4.4 无 time64 syscall,16B timespec 直传 32 位 syscall),不是
procps 的 bug;top 的 Mem 一栏是「百分比/总容量」不是 used GiB。新增 DEVKIT.md
§9 记录证据(vmstat -t 空列、usleep(0.5s)×10 实测 0s、sleep(1) 正常、ps lstart
正常),并说明 M16 的 patch_v2/sdk/meow_ui_time.c 可复用(本轮实测该 .o 独立
编译、仅 5 个 libc 未定义符号)但本轮不接入的取舍——需要单独 mission 做链接期
接管 + 全量回归,不是 -D 能修的。

P2-b:--with-sources 现在同时放 build_devkit.sh + DEVKIT.md(GPLv2 §3 的
"scripts used to control compilation");补 LGPL-2.1 §6 静态链接 libproc2 的
义务说明;MANIFEST/README.txt 写明 src/ 与 licenses/。

P2-c:lsof 内嵌 Termux musl 的 resolv.conf 路径记为工具链副作用(实测
lsof -i 约 3s 超时、-nP 瞬时;端口名/本机名不受影响),不换工具链。

P3:软链路径写实(/bin/ps、/usr/bin/{free,lsof,top});MANIFEST 用 zig version
而非 clang 版本;chmod 措辞改成检查挂载参数;--verify-only 增加 MANIFEST 逐条
sha256/体积比对且不再依赖 date;stamp 纳入 zig 完整版本;许可证复制失败即报错。

验证:test_build_devkit.sh 12/12 通过;--verify-only 通过(含 MANIFEST 对账);
独立工作区离线重建 85s/0 下载,六个二进制 sha256 与上一版逐字节一致;设备侧
二进制未变(sha 与 r1 真机验证过的一致)。
2026-11-06 19:29:27 +08:00

623 lines
31 KiB
Bash
Executable File
Raw 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.
#!/bin/bash
# ============================================================================
# build_devkit.sh — 交叉编译静态 MIPS32r2 调试工具集(TF 卡 MeowOS/opt/devkit)
#
# 为什么:设备(HiBy R3 Pro II / Ingenic X1600 / Linux 4.4.94 / MIPS32r2)上
# busybox 提供的 free/ps/lsof 字段不全(都是 busybox 软链),且没有 vmstat/slabtop。
# 内存分析时最缺 slabtop(拆 Slab 占用)与 vmstat(系统级趋势)。本脚本用 zig 自带
# musl 交叉编出完全静态的二进制,放进 TF 卡供开发调试。
#
# 产物(全部静态链接、strip 过、无 NEEDED):
# fw_apps/devkit/bin/{vmstat,slabtop,free,ps,pmap,lsof}
# fw_apps/devkit/MANIFEST.txt 版本/许可证/体积/校验摘要
# fw_apps/devkit/licenses/ 各上游 COPYING(分发留痕)
# fw_apps/devkit/README.txt 上卡后怎么加 PATH、和 busybox 的同名关系
# (--with-sources 时另有 fw_apps/devkit/src/:三份源码包 + build_devkit.sh +
# DEVKIT.md,GPLv2 §3 的“对应源码”含构建控制脚本)
#
# 源码固定为「版本 + sha256 + 固定 URL」,全部校验后才编译;缓存命中则零联网。
#
# 工具链:zig cc --target=mipsel-linux-musleabi(需 zig 0.17.x)
# 不依赖 ~/.tools 下的私有脚本,自己生成 wrapper($WORK/bin/zigcc)。
# 不需要内核头(musl 头足够;只有 lsof 缺 11 个 TCP 状态宏,本脚本 -D 补上)。
# ⚠ 该工具链在 4.4 内核上是 time64 ABI 错配:`vmstat -t` 时间戳列会是空,
# 原因/证据/后续修法见 DEVKIT.md「已知限制:工具链 time64」。不是 -D 能修的。
#
# 许可证(二进制将来随卡分发时请保留此节):
# procps-ng 4.0.4 GPL-2.0-or-later(COPYING=GPLv2)
# *** 分发二进制必须提供对应源码(GPLv2 §3,含构建脚本)***
# procps library/ LGPL-2.1+:ps/vmstat/slabtop/free/pmap 静态链接 libproc2,
# LGPL-2.1 §6 要求提供可重链接 object code 或书面要约(见 DEVKIT.md §6)
# ncurses 6.5 MIT 风格(X11 许可),保留版权声明即可
# lsof 4.99.4 Purdue Research Foundation 许可(BSD 风格 4 条),
# 要求文档与源码中保留作者与 Purdue 的署名(COPYING 第 2 条)
# 本脚本链接的 ncurses 只作为 slabtop 的静态依赖进入产物,不单独分发。
#
# 用法:
# bash scripts/build/build_devkit.sh # 增量:缓存命中则不联网
# bash scripts/build/build_devkit.sh --force # 强制重编
# bash scripts/build/build_devkit.sh --clean # 清空工作区与 out/src 后重编
# bash scripts/build/build_devkit.sh --offline # 只用已缓存的源码包(CI/离线)
# bash scripts/build/build_devkit.sh --with-sources
# # 整理 src/(三份源码包 + 本脚本 + DEVKIT.md)。**产物已是最新时也会执行**,
# # 且整理后按 sha256 复核;任何一步失败即非零退出(P1 回归点)。
# bash scripts/build/build_devkit.sh --verify-only
# # 只对现有产物自检:静态/架构/NEEDED + MANIFEST 逐条 sha256/体积比对。
# # 只需 file / readelf / sha256sum / awk;不需要 zig、make、date、网络。
#
# 环境变量:
# MEOW_DEVKIT_WORK 工作区(源码包/编译树/prefix),默认 ~/.cache/meow-devkit
# MEOW_DEVKIT_JOBS 并行度,默认 nproc
# ============================================================================
set -eu
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
OUT_DIR="$ROOT/fw_apps/devkit"
BIN_DIR="$OUT_DIR/bin"
WORK="${MEOW_DEVKIT_WORK:-$HOME/.cache/meow-devkit}"
SRC_DIR="$WORK/src"
BUILD_DIR="$WORK/build"
PREFIX="$WORK/prefix"
WRAP_DIR="$WORK/bin"
JOBS="${MEOW_DEVKIT_JOBS:-$(nproc 2>/dev/null || echo 4)}"
ZIG_TARGET="mipsel-linux-musleabi"
ZIG_WANT="0.17" # 已验证;0.18+ 参数可能变,见 check_toolchain
NCURSES_VER="6.5"
PROCPS_VER="4.0.4"
LSOF_VER="4.99.4"
# 设备内核版本:写进 lsof 的 LINUXV,让它按目标内核(4.4)选代码路径
TARGET_KERNEL_VSTR="4.4.94"
# name|url|sha256 (sha256 是“哪份源码”的唯一权威,URL 只是取法之一)
SRC_NCURSES="ncurses-${NCURSES_VER}.tar.gz|https://mirrors.tuna.tsinghua.edu.cn/gnu/ncurses/ncurses-${NCURSES_VER}.tar.gz|136d91bc269a9a5785e5f9e980bc76ab57428f604ce3e5a5a90cebc767971cc6"
SRC_PROCPS="procps-ng-${PROCPS_VER}.tar.xz|https://downloads.sourceforge.net/project/procps-ng/Production/procps-ng-${PROCPS_VER}.tar.xz|22870d6feb2478adb617ce4f09a787addaf2d260c5a8aa7b17d889a962c5e42e"
SRC_LSOF="lsof-${LSOF_VER}.tar.xz|https://deb.debian.org/debian/pool/main/l/lsof/lsof_${LSOF_VER}+dfsg.orig.tar.xz|dc3e7f51cf03bc7fbcdd9e62d4a52825682092031bd1ae08b15fe7f4f582aebf"
# 卡上 src/ 必须包含的文件:三份源码包 + 构建脚本 + 本文档。
# GPLv2 §3 的“对应源码”明确包含 "scripts used to control compilation and
# installation",缺了脚本,收卡人拿不到本轮用的 -D/configure 参数。
SRC_STAGE_LIST="ncurses-${NCURSES_VER}.tar.gz procps-ng-${PROCPS_VER}.tar.xz lsof-${LSOF_VER}.tar.xz build_devkit.sh DEVKIT.md"
# 交付的工具——验证、MANIFEST 比对、stamp 自检都从这里取,避免多处手写漂移
TOOL_NAMES="vmstat slabtop free ps pmap lsof"
FORCE=0
CLEAN=0
OFFLINE=0
WITH_SOURCES=0
VERIFY_ONLY=0
DOWNLOADS=0
log() { printf '\n== %s\n' "$*"; }
info() { printf ' %s\n' "$*"; }
die() { printf '\n!!! %s\n' "$*" >&2; exit 1; }
usage() {
sed -n '3,/^# ============/p' "$SCRIPT_PATH" | sed 's/^# \{0,1\}//'
}
while [ $# -gt 0 ]; do
case "$1" in
-f|--force) FORCE=1 ;;
--clean) CLEAN=1 ;;
--offline) OFFLINE=1 ;;
--with-sources) WITH_SOURCES=1 ;;
--verify-only) VERIFY_ONLY=1 ;;
-h|--help) usage; exit 0 ;;
*) die "未知参数: $1(-h 看用法)" ;;
esac
shift
done
# ---------------------------------------------------------------------------
# 0) 工具链与宿主依赖
# ---------------------------------------------------------------------------
check_toolchain() {
local zig_bin major_minor
zig_bin="$(command -v zig || true)"
[ -n "$zig_bin" ] || die "找不到 zig。装法:pkg install zig(需 0.17.x)"
ZIG_VERSION="$("$zig_bin" version)"
major_minor="${ZIG_VERSION%.*}"
[ "$major_minor" = "$ZIG_WANT" ] || \
die "zig 版本是 $ZIG_VERSION,本脚本只在 $ZIG_WANT.x 上验证过(target/静态链接行为可能变)"
mkdir -p "$WRAP_DIR"
cat > "$WRAP_DIR/zigcc" <<EOF
#!/bin/sh
exec "$zig_bin" cc --target=$ZIG_TARGET "\$@"
EOF
chmod +x "$WRAP_DIR/zigcc"
# 统一用 PATH 上的裸名 "zigcc" 当 CC:这样二进制里不会嵌进绝对工具链路径
# (lsof 会把 ${CC} 写进 version.h),换机器/换工作区也能逐字节复现。
PATH="$WRAP_DIR:$PATH"; export PATH
ZIGCC="zigcc"
info "zig $ZIG_VERSION → $WRAP_DIR/zigcc"
for t in make curl tar sha256sum file readelf; do
command -v "$t" >/dev/null 2>&1 || die "缺宿主工具: $t"
done
# strip 必须能处理 ELF32/MIPS:优先 llvm-strip(zig 自带同源 llvm),
# GNU strip 来自 binutils 一般也支持,但 Android 自带的那份不保证。
if command -v llvm-strip >/dev/null 2>&1; then
STRIP=llvm-strip
elif command -v "$zig_bin" >/dev/null 2>&1 && [ -x "$(dirname "$zig_bin")/llvm-strip" ]; then
STRIP="$(dirname "$zig_bin")/llvm-strip"
else
STRIP=strip
fi
info "strip → $STRIP"
}
# ---------------------------------------------------------------------------
# 1) 源码获取(固定版本 + sha256;缓存命中不联网)
# ---------------------------------------------------------------------------
fetch_src() {
local spec="$1" name url sha got
name="${spec%%|*}"; spec="${spec#*|}"
url="${spec%%|*}"; sha="${spec#*|}"
if [ -f "$SRC_DIR/$name" ]; then
got="$(sha256sum "$SRC_DIR/$name" | cut -d' ' -f1)"
if [ "$got" = "$sha" ]; then
info "源码已缓存: $name"
return 0
fi
info "缓存校验不符,删除重下: $name"
rm -f "$SRC_DIR/$name"
fi
[ "$OFFLINE" = 1 ] && die "离线模式但缺少源码: $SRC_DIR/$name"
info "下载 $name"
curl -fL --retry 3 --connect-timeout 20 -o "$SRC_DIR/$name.part" "$url" \
|| { rm -f "$SRC_DIR/$name.part"; die "下载失败: $url"; }
got="$(sha256sum "$SRC_DIR/$name.part" | cut -d' ' -f1)"
[ "$got" = "$sha" ] || { rm -f "$SRC_DIR/$name.part"; die "sha256 不符: $name(期望 $sha 实得 $got)"; }
mv "$SRC_DIR/$name.part" "$SRC_DIR/$name"
DOWNLOADS=$((DOWNLOADS + 1))
}
unpack() { # unpack <tarball> <destdir>
rm -rf "$2"; mkdir -p "$2"
tar xf "$SRC_DIR/$1" -C "$2" --strip-components=1
}
# ---------------------------------------------------------------------------
# 1b) 交付源码包(--with-sources):卡上 src/ 的完整性与整理
# P1 回归点:stamp 命中时也必须走这里,绝不能因为“跳过编译”就把源码静默漏掉。
# ---------------------------------------------------------------------------
expected_sha_for() { # expected_sha_for <tarball名> → 钉住的 sha256
case "$1" in
"ncurses-${NCURSES_VER}.tar.gz") printf '%s' "$SRC_NCURSES" | awk -F'|' '{print $3}' ;;
"procps-ng-${PROCPS_VER}.tar.xz") printf '%s' "$SRC_PROCPS" | awk -F'|' '{print $3}' ;;
"lsof-${LSOF_VER}.tar.xz") printf '%s' "$SRC_LSOF" | awk -F'|' '{print $3}' ;;
*) return 1 ;;
esac
}
sources_complete() {
local f got
[ -d "$OUT_DIR/src" ] || return 1
for f in $SRC_STAGE_LIST; do
[ -s "$OUT_DIR/src/$f" ] || return 1
done
# 三份源码包按钉住的 sha 逐一核对(版本漂移比缺文件更危险:看起来有源码,其实对不上)
for f in "ncurses-${NCURSES_VER}.tar.gz" "procps-ng-${PROCPS_VER}.tar.xz" "lsof-${LSOF_VER}.tar.xz"; do
got="$(sha256sum "$OUT_DIR/src/$f" | cut -d' ' -f1)"
[ "$got" = "$(expected_sha_for "$f")" ] || return 1
done
cmp -s "$SCRIPT_PATH" "$OUT_DIR/src/build_devkit.sh" || return 1
cmp -s "$ROOT/scripts/build/DEVKIT.md" "$OUT_DIR/src/DEVKIT.md" || return 1
return 0
}
stage_sources() {
local f
log "整理 $OUT_DIR/src(三份源码包 + 构建脚本 + 文档)"
fetch_src "$SRC_NCURSES"
fetch_src "$SRC_PROCPS"
fetch_src "$SRC_LSOF"
rm -rf "$OUT_DIR/src"
mkdir -p "$OUT_DIR/src"
for f in "ncurses-${NCURSES_VER}.tar.gz" "procps-ng-${PROCPS_VER}.tar.xz" "lsof-${LSOF_VER}.tar.xz"; do
cp "$SRC_DIR/$f" "$OUT_DIR/src/" || die "复制源码包失败: $f"
done
cp "$SCRIPT_PATH" "$OUT_DIR/src/build_devkit.sh" \
|| die "复制构建脚本失败: $SCRIPT_PATH(GPLv2 §3 要求带构建脚本)"
cp "$ROOT/scripts/build/DEVKIT.md" "$OUT_DIR/src/DEVKIT.md" \
|| die "复制 DEVKIT.md 失败: $ROOT/scripts/build/DEVKIT.md"
sources_complete || die "src/ 整理后仍不完整(sha256 或文件缺失)"
info "共 $(du -sh "$OUT_DIR/src" | cut -f1):$(ls "$OUT_DIR/src" | tr '\n' ' ')"
}
# ---------------------------------------------------------------------------
# 2) ncurses:只为 slabtop 提供静态 ncursesw(不单独分发)
# procps 的 slabtop/top 硬依赖 ncurses(configure 里 --without-ncurses 会把
# 它们整个跳过),所以这是编 slabtop 的必要成本,不是为 htop 顺手加的。
# terminfo 目录必须指向设备真实路径(/usr/share/terminfo),否则交互式
# slabtop 会 "Error opening terminal":默认值是编译时 prefix,设备上没有。
# slabtop -o(一次性输出)不初始化 curses,本来不受影响。
# ---------------------------------------------------------------------------
build_ncurses() {
log "编译 ncurses $NCURSES_VER(静态 widec,无 progs/man/tests)"
unpack "ncurses-${NCURSES_VER}.tar.gz" "$BUILD_DIR/ncurses-src"
rm -rf "$BUILD_DIR/ncurses"; mkdir -p "$BUILD_DIR/ncurses"
(
cd "$BUILD_DIR/ncurses"
CC="$ZIGCC" BUILD_CC=cc CFLAGS="-O2 -ffile-prefix-map=$BUILD_DIR/ncurses-src=/ncurses-$NCURSES_VER" \
"$BUILD_DIR/ncurses-src/configure" \
--host="$ZIG_TARGET" --prefix="$PREFIX/ncurses" \
--without-shared --with-normal --without-debug --without-profile \
--without-manpages --without-tests --without-ada \
--without-cxx --without-cxx-binding --without-progs --without-tack \
--enable-widec --disable-db-install \
--enable-pc-files --with-pkg-config-libdir="$PREFIX/ncurses/lib/pkgconfig" \
--with-default-terminfo-dir=/usr/share/terminfo \
--with-terminfo-dirs="/usr/share/terminfo:/etc/terminfo:/lib/terminfo" \
> "$BUILD_DIR/ncurses-configure.log" 2>&1 \
|| { tail -20 "$BUILD_DIR/ncurses-configure.log"; die "ncurses configure 失败"; }
make -j"$JOBS" > "$BUILD_DIR/ncurses-make.log" 2>&1 \
|| { tail -20 "$BUILD_DIR/ncurses-make.log"; die "ncurses make 失败"; }
make install > "$BUILD_DIR/ncurses-install.log" 2>&1 \
|| { tail -20 "$BUILD_DIR/ncurses-install.log"; die "ncurses install 失败"; }
)
info "libncursesw.a 就绪: $PREFIX/ncurses/lib"
}
# ---------------------------------------------------------------------------
# 3) procps-ng:vmstat / slabtop / free / ps / top / pmap
# ---------------------------------------------------------------------------
build_procps() {
log "编译 procps-ng $PROCPS_VER"
unpack "procps-ng-${PROCPS_VER}.tar.xz" "$BUILD_DIR/procps-src"
rm -rf "$BUILD_DIR/procps"; mkdir -p "$BUILD_DIR/procps"
(
cd "$BUILD_DIR/procps"
# PKG_CONFIG_LIBDIR 必须收紧:否则会捡到宿主(Termux/aarch64)的 ncursesw.pc,
# 结果是一边用 musl 头一边混进 bionic 头,watch/top 直接编不过。
# 注意:下面这段环境变量赋值必须写成“无注释的连续续行”,
# 注释行会截断续行链,导致变量只作用于一条空命令。
#
# ac_cv_func_malloc_0_nonnull / realloc:交叉编译时 autoconf 的运行期探测
# 无法执行,不缓存这两个值会生成 rpl_malloc/rpl_realloc 引用而链接失败。
PKG_CONFIG_PATH="$PREFIX/ncurses/lib/pkgconfig" \
PKG_CONFIG_LIBDIR="$PREFIX/ncurses/lib/pkgconfig" \
ac_cv_func_malloc_0_nonnull=yes ac_cv_func_realloc_0_nonnull=yes \
CC="$ZIGCC" CFLAGS="-O2 -ffunction-sections -fdata-sections -ffile-prefix-map=$BUILD_DIR/procps-src=/procps-ng-$PROCPS_VER" \
LDFLAGS="-static -Wl,--gc-sections" \
"$BUILD_DIR/procps-src/configure" \
--host="$ZIG_TARGET" --prefix="$PREFIX/procps" \
--disable-shared --enable-static --disable-nls --disable-kill \
> "$BUILD_DIR/procps-configure.log" 2>&1 \
|| { tail -20 "$BUILD_DIR/procps-configure.log"; die "procps configure 失败"; }
# 防呆:确认 ncurses 来自本 devkit 的 prefix,而不是宿主 Termux 的那份
grep -q "$PREFIX/ncurses" "$BUILD_DIR/procps/Makefile" \
|| die "procps 的 NCURSES_CFLAGS/LIBS 没指向 devkit prefix(pkg-config 串到宿主了)"
make -j"$JOBS" > "$BUILD_DIR/procps-make.log" 2>&1 \
|| { tail -20 "$BUILD_DIR/procps-make.log"; die "procps make 失败"; }
)
}
# ---------------------------------------------------------------------------
# 4) lsof:真版 lsof(busybox lsof 只有 pid/fd/设备号,没有 size/offset/协议)
# lsof 4.99.4 自带老式 Configure(不需要 autoconf),但版本探测读的是宿主
# /usr/include/linux/version.h + uname,在 Termux/aarch64 上拿不到目标内核版本,
# 于是 Makefile 里退化成 -DLINUXV=00000 -DLSOF_VSTR="0.0.0"(Configure 会打
# "unsupported dialect or version")。这两个宏在 linux dialect 里只影响
# `lsof -v` 的显示,不影响代码分支 —— 但仍显式覆盖成目标内核 4.4.94,便于事后对账。
# zig 的 musl 头没有 glibc 侧 linux/tcp.h 间接带来的 TCP 状态宏(TCP_ESTABLISHED
# 等 11 个),这里按 linux/tcp.h 的定义补齐;不补则 dsock.c 编译失败。
# ---------------------------------------------------------------------------
build_lsof() {
log "编译 lsof $LSOF_VER"
rm -rf "$BUILD_DIR/lsof"
mkdir -p "$BUILD_DIR/lsof"
tar xf "$SRC_DIR/lsof-${LSOF_VER}.tar.xz" -C "$BUILD_DIR/lsof" --strip-components=1
(
cd "$BUILD_DIR/lsof"
CC="$ZIGCC" ./Configure -n linux > "$BUILD_DIR/lsof-configure.log" 2>&1 \
|| { tail -20 "$BUILD_DIR/lsof-configure.log"; die "lsof Configure 失败"; }
grep -q 'unsupported dialect' "$BUILD_DIR/lsof-configure.log" \
&& info "注意:Configure 报 unsupported dialect(宿主 uname 6.x),版本宏下面用 -D 覆盖"
# lsof 的 Makefile 里 CFLAGS = CDEFS INCL DEP DEBUG;额外宏走 DEBUG 注入。
# -O2 必须显式给:zig cc 无 -O 时按 Debug 编,会插入 UBSan 桩导致链接失败。
make -j"$JOBS" CC="$ZIGCC" \
DEBUG="-O2 -DLINUXV=44094 -DLSOF_VSTR=\"$TARGET_KERNEL_VSTR\" -DTCP_ESTABLISHED=1 -DTCP_SYN_SENT=2 -DTCP_SYN_RECV=3 -DTCP_FIN_WAIT1=4 -DTCP_FIN_WAIT2=5 -DTCP_TIME_WAIT=6 -DTCP_CLOSE=7 -DTCP_CLOSE_WAIT=8 -DTCP_LAST_ACK=9 -DTCP_LISTEN=10 -DTCP_CLOSING=11" \
> "$BUILD_DIR/lsof-make.log" 2>&1 \
|| { tail -20 "$BUILD_DIR/lsof-make.log"; die "lsof make 失败"; }
)
}
# ---------------------------------------------------------------------------
# 5) 收集 + strip + 逐项验证
# ---------------------------------------------------------------------------
# 产出清单:<源路径>|<目标名>|<出处>|<许可证>
# 只放「真机实测能跑且真有价值」的:vmstat/slabtop(busybox 没有)、
# free/ps/pmap(busybox 版字段残缺)、lsof(busybox 版只有 pid/fd)。
# 不交付 top:本机(32 位 MIPS + 4.4.94)上 procps 4.0.4 的 top 表头/内存单位
# 输出明显错误(时长字段溢出、Mem 显示 59.4 GiB 而 MemTotal 只有 55 MB),
# 且卡上已有 htop 3.5.3(673KB),top 属重复且不可信 → 见 DEVKIT.md「未交付清单」。
TOOLS="
$BUILD_DIR/procps/src/vmstat|vmstat|procps-ng $PROCPS_VER|GPL-2.0-or-later
$BUILD_DIR/procps/src/slabtop|slabtop|procps-ng $PROCPS_VER|GPL-2.0-or-later
$BUILD_DIR/procps/src/free|free|procps-ng $PROCPS_VER|GPL-2.0-or-later
$BUILD_DIR/procps/src/ps/pscommand|ps|procps-ng $PROCPS_VER|GPL-2.0-or-later
$BUILD_DIR/procps/src/pmap|pmap|procps-ng $PROCPS_VER|GPL-2.0-or-later
$BUILD_DIR/lsof/lsof|lsof|lsof $LSOF_VER|Purdue/BSD-style
"
FAIL=0
collect_and_verify() {
log "收集产物并验证"
rm -rf "$BIN_DIR"; mkdir -p "$BIN_DIR"
printf '%-8s %10s %s\n' "工具" "体积(B)" "静态/架构"
printf '%-8s %10s %s\n' "--------" "----------" "--------------------"
while IFS='|' read -r src name origin lic; do
[ -n "$src" ] || continue
[ -f "$src" ] || die "缺产物: $src(编译没成功?)"
cp "$src" "$BIN_DIR/$name"
chmod 755 "$BIN_DIR/$name"
"$STRIP" --strip-all "$BIN_DIR/$name"
if verify_binary "$BIN_DIR/$name"; then
printf '%-8s %10s %s\n' "$name" "$(wc -c < "$BIN_DIR/$name")" "OK: static/ELF32/MIPS32r2/0 NEEDED"
else
FAIL=$((FAIL + 1))
fi
done <<EOF
$(printf '%s\n' "$TOOLS" | sed '/^$/d')
EOF
}
verify_binary() {
local f="$1" desc cls mach flags needed
desc="$(file -b "$f")"
cls="$(readelf -h "$f" | sed -n 's/^ *Class: *//p')"
mach="$(readelf -h "$f" | sed -n 's/^ *Machine: *//p')"
flags="$(readelf -h "$f" | sed -n 's/^ *Flags: *//p')"
needed="$(readelf -d "$f" 2>/dev/null | grep -c NEEDED || true)"
case "$desc" in
*"statically linked"*) ;;
*) info "FAIL $(basename "$f"): 非静态 → $desc"; return 1 ;;
esac
[ "$cls" = "ELF32" ] || { info "FAIL $(basename "$f"): Class=$cls"; return 1; }
case "$mach" in *MIPS*) ;; *) info "FAIL $(basename "$f"): Machine=$mach"; return 1 ;; esac
case "$flags" in *mips32r2*) ;; *) info "FAIL $(basename "$f"): Flags=$flags"; return 1 ;; esac
[ "$needed" = "0" ] || { info "FAIL $(basename "$f"): 有 $needed 条 NEEDED"; return 1; }
case "$desc" in *"not stripped"*) info "警告 $(basename "$f"): 未 strip";; esac
return 0
}
# ---------------------------------------------------------------------------
# 6) MANIFEST / README / 许可证 / (可选)源码
# ---------------------------------------------------------------------------
write_meta() {
log "写 MANIFEST / README / 许可证"
mkdir -p "$OUT_DIR/licenses"
# 许可证文件是分发合规的一部分:复制失败必须报错,不能 `|| true` 静默吞掉
cp "$BUILD_DIR/procps-src/COPYING" "$OUT_DIR/licenses/procps-ng-${PROCPS_VER}-COPYING" \
|| die "复制许可证失败: procps COPYING(分发合规必需)"
cp "$BUILD_DIR/procps-src/COPYING.LIB" "$OUT_DIR/licenses/procps-ng-${PROCPS_VER}-COPYING.LIB" \
|| die "复制许可证失败: procps COPYING.LIB(LGPL-2.1 静态链接义务用)"
cp "$BUILD_DIR/ncurses-src/COPYING" "$OUT_DIR/licenses/ncurses-${NCURSES_VER}-COPYING" \
|| die "复制许可证失败: ncurses COPYING"
cp "$BUILD_DIR/lsof/COPYING" "$OUT_DIR/licenses/lsof-${LSOF_VER}-COPYING" \
|| die "复制许可证失败: lsof COPYING"
{
echo "MeowOS devkit — 静态 MIPS32r2 调试工具集"
# zig 版本用 `zig version`(不是 zigcc --version 的 clang 版本号)
echo "生成时间: $(date -u '+%Y-%m-%dT%H:%M:%SZ') zig: $ZIG_VERSION target: $ZIG_TARGET"
echo
printf '%-8s %-18s %-20s %10s %s\n' "tool" "source" "license" "size(B)" "sha256"
while IFS='|' read -r src name origin lic; do
[ -n "$src" ] || continue
printf '%-8s %-18s %-20s %10s %s\n' "$name" "$origin" "$lic" \
"$(wc -c < "$BIN_DIR/$name")" "$(sha256sum "$BIN_DIR/$name" | cut -d' ' -f1)"
done <<EOF
$(printf '%s\n' "$TOOLS" | sed '/^$/d')
EOF
echo
echo "源码(可复现性 + GPL/LGPL 合规):"
echo " ncurses $NCURSES_VER MIT-style sha256 $(printf '%s' "$SRC_NCURSES" | awk -F'|' '{print $3}')"
echo " procps-ng $PROCPS_VER GPL-2.0+ / library=LGPL-2.1+ sha256 $(printf '%s' "$SRC_PROCPS" | awk -F'|' '{print $3}')"
echo " lsof $LSOF_VER Purdue/BSD sha256 $(printf '%s' "$SRC_LSOF" | awk -F'|' '{print $3}')"
echo " (--with-sources 时)src/ 内还有 build_devkit.sh + DEVKIT.md(构建控制脚本,GPLv2 §3 要求)"
echo " 下载地址与重编方法: scripts/build/build_devkit.sh(--with-sources 把上述文件放进 src/)"
echo
echo "注意: procps-ng 是 GPL-2.0-or-later。把本目录随卡交付给第三方时,"
echo " GPLv2 §3 要求提供对应源码(含构建脚本)—— 请用 --with-sources 带上 src/,"
echo " 或随卡附书面要约(有效期 ≥3 年)。仅给出下载 URL 不构成合规。"
echo " 另外 ps/vmstat/slabtop/free/pmap 静态链接了 LGPL-2.1+ 的 libproc2:"
echo " LGPL-2.1 §6 要求提供可重链接的 object code 或书面要约,详见 DEVKIT.md §6。"
} > "$OUT_DIR/MANIFEST.txt"
cat > "$OUT_DIR/README.txt" <<'EOS'
MeowOS devkit — 设备端调试工具(静态 MIPS32r2)
================================================
加到 PATH(SSH 会话,临时):
export PATH=/usr/data/mnt/sd_0/MeowOS/opt/devkit/bin:$PATH
持久化(rootfs 的 /etc/profile.d/meow_card_path.sh 已把 MeowOS/opt/bin 前插,
但 devkit 是独立目录,需要再加一行):
if [ -d /usr/data/mnt/sd_0/MeowOS/opt/devkit/bin ]; then
export PATH="/usr/data/mnt/sd_0/MeowOS/opt/devkit/bin:$PATH"
fi
源码与许可证:
若随卡分发了源码:源码(含构建脚本 build_devkit.sh、DEVKIT.md)在 ../src/;
各上游许可证原文在 ../licenses/;版本/体积/sha256 见 ../MANIFEST.txt。
exFAT 没有 Unix 权限位(挂载 fmask=0022 → 新文件一律 0755),也没有符号链接。
本目录里都是实体可执行文件,拷到卡上即可执行,无需 chmod,也不能用软链。
若拷入后无法执行,请检查挂载参数(不要有 noexec;`mount | grep sd_0`),
而不是反复 chmod —— fmask=0022 下 chmod 本身是 no-op。
与 busybox 同名命令的关系(devkit/bin 前插 PATH 时生效):
free ps lsof → 会被本目录版本覆盖(procps/lsof 字段更全)
vmstat slabtop pmap → busybox 没有,直接新增
要临时用回 busybox 版:busybox free / busybox ps / busybox lsof
注意:/bin/ps -> busybox,/usr/bin/{free,lsof,top} -> ../../bin/busybox,
所以“前插 devkit/bin”是唯一让新版本生效的方式(不要覆盖这些软链:
rootfs 是 ubifs,随固件回刷会被还原,而且 busybox 软链是基线的一部分)。
已知限制:
- pmap 只能读 /proc/<pid>/maps;本机 CONFIG_PROC_PAGE_MONITOR=n,
/proc/<pid>/smaps 不存在,所以 pmap -x 拿不到 PSS/私有脏页。
- slabtop -o(一次性输出)不需要 terminfo,也不要求 TTY;
交互模式走 /usr/share/terminfo(设备已有 xterm 等条目;已编进 ncurses)。
- 本工具链(zig 0.17 musl)在 4.4 内核上是 time64 ABI 错配:
`vmstat -t` 的时间戳列会是**空**(localtime 返回 NULL)。
vmstat 采样间隔、ps 的 lstart、slabtop/free/pmap/lsof 均正常。
原因与后续修法见 ../src/DEVKIT.md「已知限制:工具链 time64」。
- lsof 不带 -n 时会尝试反向解析(编译进 musl 的 resolv.conf 路径是构建机的,
设备上不存在)→ 实测 `lsof -i` 约 3s 超时;请用 `lsof -nP`。
- 不提供 top:见 ../src/DEVKIT.md(同样是 time64 问题,表头时钟溢出)。
EOS
}
# ---------------------------------------------------------------------------
# 主流程
# ---------------------------------------------------------------------------
stamp_value() {
{
sha256sum "$SCRIPT_PATH"
# 带 zig 的完整版本(0.17.0 → 0.17.1 也要触发重建:工具链 patch 会改产物)
echo "zig=$ZIG_VERSION target=$ZIG_TARGET kernel=$TARGET_KERNEL_VSTR"
echo "ncurses=$NCURSES_VER procps=$PROCPS_VER lsof=$LSOF_VER"
} | sha256sum | cut -d' ' -f1
}
main() {
local t0 t1
if [ "$VERIFY_ONLY" = 1 ]; then
# 只校验现有产物:不要求 zig/make/date,评审者用 file/readelf/sha256sum 即可
collect_verify_existing
[ "$FAIL" = 0 ] || die "$FAIL 项未通过自检"
return 0
fi
t0=$(date +%s)
mkdir -p "$SRC_DIR" "$BUILD_DIR" "$PREFIX"
check_toolchain
local cur_stamp old_stamp=""
cur_stamp="$(stamp_value)"
[ -f "$WORK/stamp" ] && old_stamp="$(cat "$WORK/stamp")"
if [ "$CLEAN" = 1 ]; then
log "清理工作区 $WORK 与输出目录的 src/"
rm -rf "$BUILD_DIR" "$PREFIX"
mkdir -p "$BUILD_DIR" "$PREFIX"
if [ -d "$OUT_DIR/src" ]; then
rm -rf "$OUT_DIR/src"
fi
old_stamp=""
fi
local bin_ok=1 mf_ok=0 n
for n in $TOOL_NAMES; do
[ -f "$BIN_DIR/$n" ] && verify_binary "$BIN_DIR/$n" >/dev/null 2>&1 || bin_ok=0
done
# 交付完整性也要一并作为 skip 条件:MANIFEST 缺失或与 bin 不符时宁可重建
if [ -f "$OUT_DIR/MANIFEST.txt" ] && verify_manifest >/dev/null 2>&1; then
mf_ok=1
fi
if [ "$FORCE" = 0 ] && [ "$cur_stamp" = "$old_stamp" ] && [ "$bin_ok" = 1 ] && [ "$mf_ok" = 1 ]; then
# P1 回归点:--with-sources 必须在这里就兑现,不能因为“跳过编译”而漏掉源码。
# (曾经把这段放在 return 之后,结果 stamp 命中时静默不产出 src/,退出码仍是 0。)
if [ "$WITH_SOURCES" = 1 ]; then
if sources_complete; then
info "源码包已就绪: $OUT_DIR/src"
else
stage_sources
fi
elif [ -d "$OUT_DIR/src" ] && ! sources_complete; then
info "注意: $OUT_DIR/src 与当前版本/脚本不一致;若要分发请用 --with-sources 重建"
fi
t1=$(date +%s)
log "产物已是最新(脚本与版本未变),跳过编译"
info "输出目录: $BIN_DIR"
info "用时 $((t1 - t0))s,联网 $DOWNLOADS 次"
return 0
fi
log "取源码(固定版本 + sha256 校验)"
fetch_src "$SRC_NCURSES"
fetch_src "$SRC_PROCPS"
fetch_src "$SRC_LSOF"
build_ncurses
build_procps
build_lsof
collect_and_verify
[ "$FAIL" = 0 ] || die "$FAIL 个产物未通过静态/架构自检(见上)"
write_meta
if [ "$WITH_SOURCES" = 1 ]; then
stage_sources
elif [ -d "$OUT_DIR/src" ]; then
# 上次 --with-sources 的残留可能与本次二进制不对应;留着会被误当“合规源码”
info "删除上次残留的 $OUT_DIR/src(本次未请求源码,避免与二进制错配)"
rm -rf "$OUT_DIR/src"
fi
printf '%s\n' "$cur_stamp" > "$WORK/stamp"
t1=$(date +%s)
log "完成"
info "输出: $OUT_DIR"
info "体积合计: $(du -sh "$BIN_DIR" | cut -f1)"
info "用时 $((t1 - t0))s,本次联网下载 $DOWNLOADS 次"
if [ "$WITH_SOURCES" = 1 ]; then
info "源码包: $OUT_DIR/src($(du -sh "$OUT_DIR/src" | cut -f1))"
fi
}
collect_verify_existing() {
local n bad=0
for n in $TOOL_NAMES; do
if [ ! -f "$BIN_DIR/$n" ]; then
info "缺 $BIN_DIR/$n"; bad=$((bad + 1)); continue
fi
verify_binary "$BIN_DIR/$n" && info "OK $n $(wc -c < "$BIN_DIR/$n") B" || bad=$((bad + 1))
done
verify_manifest || bad=$((bad + 1))
FAIL=$bad
}
# MANIFEST.txt 的逐条 sha256/体积比对:只查“是不是静态 MIPS”看不出内容漂移
# (旧版脚本/旧工具链编出来的同样是 static/MIPS)。这里把 MANIFEST 当权威对账。
verify_manifest() {
local n line got_sha got_size bad=0
local mf="$OUT_DIR/MANIFEST.txt"
if [ ! -f "$mf" ]; then
info "FAIL 缺 $mf(无法核对产物内容)"
return 1
fi
for n in $TOOL_NAMES; do
# 只认表格行:$1=工具名,$(NF-1)=纯数字体积,$NF=64 位 sha。
# (MANIFEST 末尾「源码」小节里也有以 lsof 开头的行,不加这两个约束会把两行都收进来。)
line="$(awk -v k="$n" '$1 == k && $(NF-1) ~ /^[0-9]+$/ && length($NF) == 64' "$mf")"
if [ -z "$line" ]; then
info "FAIL MANIFEST 缺 $n 的表格行"; bad=$((bad + 1)); continue
fi
got_sha="$(printf '%s\n' "$line" | awk '{print $NF}')"
got_size="$(printf '%s\n' "$line" | awk '{print $(NF-1)}')"
if [ "$got_sha" != "$(sha256sum "$BIN_DIR/$n" | cut -d' ' -f1)" ]; then
info "FAIL $n sha256 与 MANIFEST 不一致(产物被替换/旧版残留?)"; bad=$((bad + 1))
elif [ "$got_size" != "$(wc -c < "$BIN_DIR/$n")" ]; then
info "FAIL $n 体积与 MANIFEST 不一致($got_size vs $(wc -c < "$BIN_DIR/$n"))"; bad=$((bad + 1))
fi
done
[ "$bad" = 0 ] && info "OK MANIFEST.txt 与 bin/ 六项 sha256/体积逐条一致"
return $((bad > 0))
}
main "$@"