有用户要在 arm64 的 Debian 上跑 drouter。这一轮只对一个发行版做了适配 —— Armbian 26.8.1 trixie(Debian 13.6)/ aarch64 / 内核 6.18.35-ophub (Amlogic S912 板),别的 arm64 发行版未验证,验证范围与实测数据全部 写在 docs/07-arm64-适配说明.md。 改掉的四处 x86 假设: 1. helper._read_bios():ARM 板既无 /sys/firmware/efi 也无 /sys/class/dmi, 原代码把它们一律落到 else 分支,系统概览显示「传统 BIOS」(错的, 这类机器根本没有 BIOS)。新增设备树分支,判据顺序 EFI → 设备树 → BIOS (树莓派 4 这类两可的机器要优先认 EFI);读 /proc/device-tree 与 /sys/firmware/devicetree 两个候选路径,compatible 按 NUL 切开取第一条; 没有 DMI 厂商时拿设备树板名顶上,厂商格不再空白。返回结构加 fw_type / is_dt / compat,旧字段一个没动。 2. update.py:离线包资源名原先写死 -offline-amd64,arm64 机器点一键更新 会下载 x86 离线包,传几十 MB 才在装包阶段炸依赖。改为按 dpkg --print-architecture 拼名(_deb_arch / build_assets),并为带架构 后缀的附件在下载前先拿官方 SHA256SUMS 核对存在性,没有就明确报错。 3. packaging:离线包说明里两处写死的 amd64 换成 $ARCH(包名早就是变量拼的, 但包里那份 README 还在教用户核对 amd64);build-docker.sh 新增可选的 第 2 个参数 [arch],交叉构建时加 --platform 并给标签加 -<arch> 后缀, 不传第二参数时行为与改动前逐字节一致。 4. web/i18n.js:给新增的「设备树 (Device Tree)」展示串登记英文, 否则英文界面漏中文。 配套: - _dev/t-arch.py(31 条判据)+ _dev/t-arch-inject.py(20 例反向验证)。 反向验证抓出一条假绿:i18n 词条的 n/d 判据用了 300 字符松弛窗口, 会跨到下一条词条,删掉本词条的 d 之后照样绿 —— 已收紧为本词条花括号内。 - _dev/run-static.py 的 EXCL 补上 t-update-inject —— 它名字也是 t- 前缀, 之前一直被全量回归自动执行,会在回归过程中反复改写 backend/ 与 web/。 - devtools/verify-arm64.py:只读验收脚本,方便他人在自己的板子上复验。 - README 支持矩阵里 arm64 从「理论可用」改为「已实测 1 个发行版」。 本机全量静态回归 71 个检查器 / 0 失败;目标机上只读验证 12 条全通 (构建 753 396 字节的 deb / depcheck 73 项 / 8 个渲染模块 / 架构感知清单)。 目标机未安装 drouter,未启用任何服务,未改动网络。
198 lines
7.3 KiB
Python
198 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""arm64 / 非 x86 板子的本地只读验收。
|
||
|
||
在目标机上跑一遍,确认 drouter 的代码在**这台**机器上站得住:
|
||
|
||
python3 devtools/verify-arm64.py # 默认以仓库根目录为源码树
|
||
python3 devtools/verify-arm64.py /opt/drouter
|
||
|
||
查什么:
|
||
1. 后端每个 .py 能 compile
|
||
2. 固件探测:设备树机器必须报「设备树」,不能落到「传统 BIOS」
|
||
3. 依赖自检 depcheck 能跑完并给出发行版名
|
||
4. 配置渲染:能出配置的出配置,缺必填字段的报**指定的**校验,不许报别的
|
||
5. 更新模块:离线包文件名必须跟本机 dpkg 架构一致
|
||
|
||
⚠️ 全程只读:不装包、不写配置、不起任何服务、不动网络。
|
||
8080 端口上有别的业务在跑的机器也能安全跑这一份。
|
||
退出码 0 = 全通过。
|
||
"""
|
||
import glob
|
||
import importlib.util
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
|
||
ROOT = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else os.path.dirname(
|
||
os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
# 这三个依赖 Linux 专属的 /sys 接口,别在 Windows 开发机上跑,会误报。
|
||
if not sys.platform.startswith('linux'):
|
||
print('这份验收脚本要在目标 Linux 机器上跑(依赖 /sys/firmware、'
|
||
'/sys/class/dmi 等接口)。')
|
||
print('ROOT=%s' % ROOT)
|
||
sys.exit(2)
|
||
|
||
fails = []
|
||
n = 0
|
||
|
||
|
||
def ck(desc, cond, extra=''):
|
||
global n
|
||
n += 1
|
||
if cond:
|
||
print('ok %s' % desc)
|
||
else:
|
||
print('FAIL %s %s' % (desc, extra))
|
||
fails.append(desc)
|
||
|
||
|
||
def load(name):
|
||
spec = importlib.util.spec_from_file_location(
|
||
name, os.path.join(ROOT, 'backend', name + '.py'))
|
||
m = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(m)
|
||
return m
|
||
|
||
|
||
print('源码树:%s' % ROOT)
|
||
print('=' * 60)
|
||
print('1. 后端模块语法')
|
||
print('=' * 60)
|
||
names = sorted(os.path.basename(p)[:-3]
|
||
for p in glob.glob(os.path.join(ROOT, 'backend', '*.py')))
|
||
if not names:
|
||
print('✘ %s/backend 下没找到 .py —— 源码树给错了?' % ROOT)
|
||
sys.exit(1)
|
||
bad = []
|
||
for nm in names:
|
||
with open(os.path.join(ROOT, 'backend', nm + '.py'),
|
||
encoding='utf-8') as f:
|
||
src = f.read()
|
||
try:
|
||
compile(src, nm, 'exec')
|
||
except SyntaxError as e:
|
||
bad.append('%s: %s' % (nm, e))
|
||
ck('后端 %d 个模块 compile 通过' % len(names), not bad, '; '.join(bad))
|
||
|
||
H = load('drouter-helper')
|
||
print()
|
||
print('=' * 60)
|
||
print('2. 固件探测(ARM 板最容易误报的一项)')
|
||
print('=' * 60)
|
||
bios = H._read_bios()
|
||
print(' _read_bios() =', json.dumps(bios, ensure_ascii=False))
|
||
machine = os.uname().machine
|
||
# 这一条只替 placeholders 做提示,不参与判定 —— 有没有 DMI 是**实探测**出来的。
|
||
has_dmi = os.path.isdir('/sys/class/dmi')
|
||
has_efi = os.path.isdir('/sys/firmware/efi')
|
||
print(' 本机:machine=%s /sys/class/dmi=%s /sys/firmware/efi=%s'
|
||
% (machine, has_dmi, has_efi))
|
||
ck('fw_type 是三者之一(efi / dt / bios)',
|
||
bios.get('fw_type') in ('efi', 'dt', 'bios'), str(bios.get('fw_type')))
|
||
if not (has_dmi or has_efi):
|
||
ck('无 EFI 且无 DMI ⇒ 必须判成设备树,不能写成传统 BIOS',
|
||
bios.get('fw_type') == 'dt', str(bios.get('fw_type')))
|
||
ck('厂商格有值(拿设备树板名兜底,不能空着)',
|
||
bool(bios.get('vendor')), repr(bios.get('vendor')))
|
||
if bios.get('fw_type') == 'dt':
|
||
ck('compat 按 NUL 切开,不含空字符',
|
||
'\x00' not in (bios.get('compat') or ''),
|
||
repr(bios.get('compat')))
|
||
ck('前端在用的旧字段一个没少',
|
||
all(k in bios for k in ('firmware', 'is_efi', 'vendor', 'version', 'date')))
|
||
|
||
print()
|
||
print('=' * 60)
|
||
print('3. 依赖自检 depcheck')
|
||
print('=' * 60)
|
||
p = subprocess.run([sys.executable, 'drouter-helper.py', 'depcheck',
|
||
'{"op":"check"}'], cwd=os.path.join(ROOT, 'backend'),
|
||
capture_output=True, text=True, timeout=180)
|
||
try:
|
||
d = json.loads(p.stdout or '{}')
|
||
except ValueError:
|
||
d = {'ok': False, 'msg_cn': (p.stdout or p.stderr or '')[:200]}
|
||
ck('depcheck 返回 ok', bool(d.get('ok')), str(d.get('msg_cn'))[:160])
|
||
data = d.get('data') or {}
|
||
print(' summary =', json.dumps(data.get('summary'), ensure_ascii=False))
|
||
print(' extra =', json.dumps(data.get('extra'), ensure_ascii=False))
|
||
ck('发行版名识别出来了', bool((data.get('extra') or {}).get('os')))
|
||
need = [i['name'] for i in (data.get('items') or [])
|
||
if i.get('required') and not i.get('ok')]
|
||
print(' 必需但未装 %d 项:%s' % (len(need), '、'.join(need)))
|
||
print(' (缺的都是路由功能包,面板「依赖自检」页可以逐项安装;')
|
||
print(' Python 运行时与本项目自身的可导入性已经在第 1 步验过)')
|
||
|
||
print()
|
||
print('=' * 60)
|
||
print('4. 配置渲染(渲染但绝不落盘)')
|
||
print('=' * 60)
|
||
# 空 cfg 下这三个模块第一步就是必填字段校验。写死期望串是为了抓「校验本身
|
||
# 挂了」这种情况 —— 报别的(比如 ImportError、KeyError)才是真问题。
|
||
EXPECT_VALIDATE = {
|
||
'dnsmasq': 'LAN 接口不能为空',
|
||
'network': 'WAN 网卡不能为空',
|
||
'ppp': 'PPPoE 网卡不能为空',
|
||
}
|
||
spec = importlib.util.spec_from_file_location(
|
||
'render', os.path.join(ROOT, 'backend', 'render.py'))
|
||
R = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(R)
|
||
rendered = {}
|
||
wrong = []
|
||
for mod in sorted(R.RENDERERS):
|
||
try:
|
||
r = R.render(mod, {})
|
||
if isinstance(r, (list, tuple)) and r and len(r[0]) == 2:
|
||
rendered[mod] = r[0][1]
|
||
else:
|
||
wrong.append('%s 返回了非二元组' % mod)
|
||
except Exception as e:
|
||
want = EXPECT_VALIDATE.get(mod)
|
||
got = str(e)
|
||
if want is None or want not in got:
|
||
wrong.append('%s 抛了非预期异常:%s' % (mod, got[:60]))
|
||
rendered[mod] = None
|
||
NL = chr(10)
|
||
for mod in sorted(rendered):
|
||
body = rendered[mod]
|
||
if body is None:
|
||
print(' %-12s 必填校验:%s' % (mod, EXPECT_VALIDATE.get(mod, '?')))
|
||
else:
|
||
print(' %-12s %3d 行 / %5d 字符'
|
||
% (mod, body.count(NL) + 1, len(body)))
|
||
ck('每个模块要么出配置、要么报指定的必填校验', not wrong, '; '.join(wrong))
|
||
|
||
print()
|
||
print('=' * 60)
|
||
print('5. 更新模块的架构感知')
|
||
print('=' * 60)
|
||
U = load('drouter-update')
|
||
arch = U._deb_arch()
|
||
print(' 本机 _deb_arch() =', arch)
|
||
lst = U.asset_list('1.0.10')
|
||
for a in lst:
|
||
print(' %-8s %-42s arch_specific=%s'
|
||
% (a['key'], a['name'], a['arch_specific']))
|
||
ck('离线包名带本机架构后缀',
|
||
any(a['key'] == 'offline' and ('offline-%s' % arch) in a['name']
|
||
for a in lst))
|
||
ck('deb 不带架构后缀(Architecture: all)',
|
||
any(a['key'] == 'deb' and '_all.deb' in a['name'] for a in lst))
|
||
ck('别的架构拼出别的名字(不是写死一个)',
|
||
[x['name'] for x in U.asset_list('1.0.10', arch='amd64')]
|
||
!= [x['name'] for x in lst] or arch == 'amd64')
|
||
|
||
print()
|
||
print('=' * 60)
|
||
print('判据 %d 条,失败 %d 条' % (n, len(fails)))
|
||
for f in fails:
|
||
print(' ! %s' % f)
|
||
print()
|
||
print('⚠️ 这份脚本只验「代码在这台机器上能不能跑」,')
|
||
print(' 不验「当路由器转发起来行不行」—— 那要点面板上的应用,会真改网络。')
|
||
sys.exit(1 if fails else 0)
|