Files
MeowOS-For-R3P2/scripts/build/build_devkit.sh
T
AskaEth adaed09f94 fix(devkit): M24 r2 评审 — 交付完整性闸门 + 补 sysctl + time64 文档更正
P2-1(与 P1 同类):licenses/ 与 README 缺失仍会假成功。
- 新增 meta_complete()/verify_meta():MANIFEST + README + 五份许可证副本
  (procps COPYING/COPYING.LIB、ncurses、lsof、musl-COPYRIGHT)缺一即不完整
- 纳入 skip 条件与 --verify-only;产物未变但 meta 缺失时就地 write_meta 补全(不重编),
  构建树不在则退回全量重建
- 回归测试新增 T6(删 licenses 后 --with-sources 必须恢复)、T7(--verify-only 必须拒绝)、
  T8(删 README 后恢复);共 20 项全过

P2-2:README.txt/MANIFEST.txt 的卡上路径全错(写成 ../src、scripts/build/…)。
- 全部改为卡内相对路径:src/、licenses/、MANIFEST.txt、src/DEVKIT.md、src/build_devkit.sh
- README 增加「目录内容」段并注明 src/ 是生成目录、勿手放文件

P2-3:valgrind 的排除理由(“无 MIPS32 后端”)是错的 —— 官方支持 MIPS32/Linux。
- 改为「未尝试」:交叉构建需 autotools + 目标 vDSO 细节、产物 10MB+,56MB 单核不实用

P2-4:busybox 1.31 实测有 pgrep/kill/killall,没有 pkill、没有 sysctl。
- 表格改准确;pgrep 保留(增量有限不交付);pkill 写明「busybox 无此 applet,
  killall 只能部分替代,本轮未做」
- 新增交付 sysctl(procps 4.0.4,148184 B,静态):MANIFEST/verify-only/真机
  (sysctl -a 428 键、kernel.hostname/panic 读取、-n)均已验证

P2-5:free -s <小数> 也受 time64 影响(usleep 亚秒被抹平),文档说反了。
- DEVKIT §9 增加一行(free -s 0.5 实测 0s、free -s 1 实测 1s);README 同步;
  “收益只有 vmstat -t 一列”改为包含 free -s

P3:
1. §9 机制改准:内核只写 8 字节 → tv_sec 高 32 位是内核写入的 nsec、offset 8 的
   tv_nsec 才是未初始化;nanosleep 是把 tv_sec 高半字当 nsec 读(0)
2. 删掉脚本里 top 的旧错误归因(时长溢出/59.4 GiB),与 DEVKIT 对齐
3. 测试耗时改为「热缓存约 15s,冷启动 1–2 分钟」(实测 28s 含多次构建/修复路径)
4. --verify-only 依赖清单写全(file/readelf/sha256sum/awk/sed/grep/wc/cut/basename)
5. licenses/ 增加 musl-COPYRIGHT(zig env → libc/musl/COPYRIGHT,取不到即失败),§6 补一行
6. htop 说明改准:亚秒刷新走 ncurses halfdelay()(select + 32 位 timeval),大概率不受
   time64 影响;受影响的是时钟显示与部分采样(仍注明未做运行时验证)
7. README/脚本注明 src/ 是生成目录
8. LGPL 摘要改为「源码(§6a) 或 共享库(§6b) 或 书面要约(§6c)」
9. ltrace/sysstat 从「不可行」改为「未做(成本高)」,与 perf 的硬阻塞区分

验证:test_build_devkit.sh 20/20;--verify-only 通过(7 个二进制 + MANIFEST + 5 份许可证);
独立工作区离线重建 64s/0 下载,七个 sha256 与前一次逐字节一致;设备实测 sysctl 与其余六项正常。
2026-11-06 19:53:45 +08:00

683 lines
35 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,sysctl,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/sysctl 静态链接 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 sysctl lsof"
# licenses/ 下必须存在的许可证副本(缺一个都算交付不完整):
# procps 是 GPL-2.0+(COPYING)且 library/ 是 LGPL-2.1+(COPYING.LIB),
# ncurses/lsof 是 MIT 风格 / Purdue,musl 是 MIT 且七个产物都静态链入了它。
LICENSE_FILES="procps-ng-${PROCPS_VER}-COPYING procps-ng-${PROCPS_VER}-COPYING.LIB ncurses-${NCURSES_VER}-COPYING lsof-${LSOF_VER}-COPYING musl-COPYRIGHT"
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"
# 七个产物都静态链入了 zig 自带 musl(MIT),其 COPYRIGHT 要求随副本保留,
# 所以构建时必须能拿到它,拿不到就明确失败(不能交付“缺许可证”的包)。
ZIG_LIB_DIR="$("$zig_bin" env 2>/dev/null | sed -n 's/.*\.lib_dir *= *"\(.*\)".*/\1/p')"
MUSL_COPYRIGHT="${ZIG_LIB_DIR}/libc/musl/COPYRIGHT"
[ -s "$MUSL_COPYRIGHT" ] || die "找不到 musl 的 COPYRIGHT(zig lib_dir=$ZIG_LIB_DIR),无法留痕许可证"
info "musl COPYRIGHT → $MUSL_COPYRIGHT"
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 版字段残缺)、sysctl(busybox 1.31 没有此 applet)、
# lsof(busybox 版只有 pid/fd)。
# 不交付 top:表头时钟被工具链 time64 打坏(不是 procps 的 bug,见 DEVKIT.md §9),
# 且卡上已有 htop 3.5.3(673KB)→ 属重复且不可信。
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/procps/src/sysctl|sysctl|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"
# musl:七个 procps/lsof 产物都静态链入了 zig 自带 musl(MIT),必须随副本留声明
cp "$MUSL_COPYRIGHT" "$OUT_DIR/licenses/musl-COPYRIGHT" \
|| die "复制许可证失败: musl COPYRIGHT($MUSL_COPYRIGHT)"
{
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 " musl zig $ZIG_VERSION 自带(MIT) 许可证原文见 licenses/musl-COPYRIGHT"
echo " (--with-sources 时)src/ 内还有 build_devkit.sh + DEVKIT.md(构建控制脚本,GPLv2 §3 要求)"
echo " 重编方法: src/build_devkit.sh(卡上路径;仓库里是 scripts/build/build_devkit.sh)"
echo " 许可证原文: licenses/*-COPYING / licenses/*-COPYRIGHT;说明见 src/DEVKIT.md"
echo
echo "注意: procps-ng 是 GPL-2.0-or-later。把本目录随卡交付给第三方时,"
echo " GPLv2 §3 要求提供对应源码(含构建脚本)—— 请用 --with-sources 带上 src/,"
echo " 或随卡附书面要约(有效期 ≥3 年)。仅给出下载 URL 不构成合规。"
echo " 另外 ps/vmstat/slabtop/free/pmap/sysctl 静态链接了 LGPL-2.1+ 的 libproc2:"
echo " LGPL-2.1 §6 要求「源码(§6a) 或 共享库(§6b) 或 书面要约(§6c)」三选一,详见 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
目录内容(本文件所在目录就是 devkit 根):
bin/ 可执行文件(实体文件,非软链)
MANIFEST.txt 版本 / 许可证 / 体积 / sha256
licenses/ 上游许可证原文(procps GPLv2 + LGPL-2.1、ncurses、lsof、musl)
src/ 仅当随卡附带了源码(--with-sources)时存在:三份源码包 +
build_devkit.sh + DEVKIT.md。**src/ 是生成目录,请勿手放文件。**
说明文档:src/DEVKIT.md(若本源码未随附,见仓库 scripts/build/DEVKIT.md)
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 sysctl → busybox 1.31 没有这些 applet,直接新增
要临时用回 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)
* `free -s 0.5`(小数间隔)会**立即返回**(亚秒等待被抹平);
`free -s 1` 与 `vmstat 1 N` 的整秒间隔正常
* ps 的 lstart、slabtop/pmap/sysctl 不受影响
原因与后续修法见 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 meta_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/许可证/README 缺失或与 bin 不符时
# 宁可重建(与 P1 同类——不能因为“跳过编译”就让合规材料静默消失)。
if [ -f "$OUT_DIR/MANIFEST.txt" ] && verify_manifest >/dev/null 2>&1; then
mf_ok=1
fi
meta_complete && meta_ok=1
# 产物本身没问题、只是 MANIFEST/README/licenses 被删:就地补,不必重编
if [ "$FORCE" = 0 ] && [ "$cur_stamp" = "$old_stamp" ] && [ "$bin_ok" = 1 ] \
&& { [ "$mf_ok" = 0 ] || [ "$meta_ok" = 0 ]; } \
&& [ -d "$BUILD_DIR/procps-src" ] && [ -d "$BUILD_DIR/ncurses-src" ] && [ -d "$BUILD_DIR/lsof" ]; then
log "产物未变,但 MANIFEST/README/licenses 不全 → 就地补全"
write_meta
if verify_manifest >/dev/null 2>&1; then mf_ok=1; else mf_ok=0; fi
if meta_complete; then meta_ok=1; else meta_ok=0; fi
fi
if [ "$FORCE" = 0 ] && [ "$cur_stamp" = "$old_stamp" ] && [ "$bin_ok" = 1 ] \
&& [ "$mf_ok" = 1 ] && [ "$meta_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))
verify_meta || 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))
}
# 交付完整性:MANIFEST + README + 五份许可证副本,缺一个都算不完整
# (和 P1 同类:这些是合规/可用性的一部分,不能因为“跳过编译”就静默缺失)
meta_complete() {
local f
[ -s "$OUT_DIR/MANIFEST.txt" ] || return 1
[ -s "$OUT_DIR/README.txt" ] || return 1
for f in $LICENSE_FILES; do
[ -s "$OUT_DIR/licenses/$f" ] || return 1
done
return 0
}
verify_meta() {
local f bad=0
[ -s "$OUT_DIR/MANIFEST.txt" ] || { info "FAIL 缺 MANIFEST.txt(版本/许可证/sha 无从核对)"; bad=$((bad + 1)); }
[ -s "$OUT_DIR/README.txt" ] || { info "FAIL 缺 README.txt(上卡说明)"; bad=$((bad + 1)); }
for f in $LICENSE_FILES; do
[ -s "$OUT_DIR/licenses/$f" ] || { info "FAIL 缺 licenses/$f(许可证留痕)"; bad=$((bad + 1)); }
done
[ "$bad" = 0 ] && info "OK MANIFEST/README/licenses($(printf '%s' "$LICENSE_FILES" | wc -w) 份)齐全"
return $((bad > 0))
}
main "$@"