有用户要在 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,未启用任何服务,未改动网络。
223 lines
9.8 KiB
Python
223 lines
9.8 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""判据:架构无关化(arm64 适配)。
|
||
|
||
2026-10-16 做 arm64 Debian(Armbian 26.8.1 trixie / aarch64 / 6.18.35-ophub)
|
||
适配时,发现项目里有几处把 x86 当默认写死在字符串模板里 —— 在 amd64 上
|
||
看不出来,一到 arm64 就露馅。这个检查器把那几处钉死,防止改回去。
|
||
|
||
分四组:
|
||
A. 更新模块(backend/drouter-update.py)—— 离线包资源名必须随本机架构变
|
||
B. 固件探测(backend/drouter-helper.py)—— 设备树机器不能报「传统 BIOS」
|
||
C. i18n 配套(web/i18n.js)—— 新增的展示串必须登记,否则英文界面漏中文
|
||
D. 打包脚本(packaging/)—— 离线包说明与 Docker platform 支持
|
||
|
||
⚠️ 判据尽量查**语义/运行结果**,不要只查「某个名字出现过」。
|
||
反向验证在 _dev/t-arch-inject.py(改写实现 → 本文件必须变红)。
|
||
"""
|
||
import ast
|
||
import importlib.util
|
||
import os
|
||
import re
|
||
import sys
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
P = lambda *a: os.path.join(ROOT, *a)
|
||
|
||
fails = []
|
||
n = 0
|
||
|
||
|
||
def ck(desc, cond, extra=''):
|
||
global n
|
||
n += 1
|
||
if not cond:
|
||
fails.append('%s %s' % (desc, extra))
|
||
print('FAIL %s %s' % (desc, extra))
|
||
else:
|
||
print('ok %s' % desc)
|
||
|
||
|
||
def read(p):
|
||
with open(p, encoding='utf-8') as f:
|
||
src = f.read()
|
||
return src
|
||
|
||
|
||
UPDATE = read(P('backend', 'drouter-update.py'))
|
||
HELPER = read(P('backend', 'drouter-helper.py'))
|
||
I18N = read(P('web', 'i18n.js'))
|
||
OFFLINE = read(P('packaging', 'build-offline-bundle.sh'))
|
||
DOCKER = read(P('packaging', 'build-docker.sh'))
|
||
|
||
print('=' * 56)
|
||
print('A. 更新模块:附件名必须随架构变')
|
||
print('=' * 56)
|
||
|
||
# A1 —— 最直接的一条:模板里不许再出现具体的 amd64。
|
||
# 只查「资源名模板位置」(带 -offline- 的那串),避免把注释里的 amd64 也算进来。
|
||
ck('离线包资源名模板不含写死的 amd64',
|
||
'offline-amd64' not in UPDATE,
|
||
'offline-amd64 残留=%d 处' % UPDATE.count('offline-amd64'))
|
||
ck('离线包资源名模板不含写死的 arm64(应当用变量拼)',
|
||
'offline-arm64' not in UPDATE)
|
||
|
||
# A2 —— 真的跑一遍:给不同架构必须拼出不同文件名。
|
||
# ⚠️ 这一条是**运行态**判据,比「代码里有没有 _deb_arch 这个字符串」强得多:
|
||
# 把 build_assets 里的 %s 换成写死的 amd64,拼出来立刻露馅。
|
||
spec = importlib.util.spec_from_file_location(
|
||
'drouter_update_arch', P('backend', 'drouter-update.py'))
|
||
U = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(U)
|
||
|
||
ASSETS = getattr(U, 'ASSETS', [])
|
||
|
||
names = {}
|
||
_load_err = ''
|
||
try:
|
||
for arch in ('amd64', 'arm64', 'armhf'):
|
||
d = {a['key']: a['name'] for a in U.asset_list('9.9.9', arch=arch)}
|
||
names[arch] = d
|
||
except Exception as e: # pragma: no cover
|
||
_load_err = str(e)
|
||
|
||
ck('asset_list 支持按架构拼出清单(模块可加载)', not _load_err, _load_err)
|
||
if not _load_err:
|
||
ck('arm64 拼出 -offline-arm64- 的离线包名',
|
||
names['arm64']['offline'] == 'drouter-9.9.9-offline-arm64.tar.gz',
|
||
names['arm64']['offline'])
|
||
ck('amd64 拼出 -offline-amd64- 的离线包名',
|
||
names['amd64']['offline'] == 'drouter-9.9.9-offline-amd64.tar.gz',
|
||
names['amd64']['offline'])
|
||
# 三个架构必须两两不同 —— 防止有人把 _deb_arch() 写成常量
|
||
ck('不同架构拼出的离线包名两两不同',
|
||
names['amd64']['offline'] != names['arm64']['offline']
|
||
!= names['armhf']['offline'] != names['amd64']['offline'])
|
||
# deb 是 Architecture: all,**必须**三架构同名
|
||
ck('deb 本体三架构同名(Architecture: all)',
|
||
names['amd64']['deb'] == names['arm64']['deb'] == names['armhf']['deb'],
|
||
names['arm64']['deb'])
|
||
ck('只有离线包标了 arch_specific',
|
||
[a['arch_specific'] for a in U.asset_list('9.9.9', arch='arm64')]
|
||
== [False, True, False])
|
||
|
||
# A3 —— ASSETS 是四元组,缺一列会让 asset_list 解包炸 ValueError
|
||
# (表现是「更新」页整块渲染不出来)
|
||
ck('ASSETS 非空且每个条目是 4 元组',
|
||
bool(ASSETS) and all(isinstance(a, tuple) and len(a) == 4 for a in ASSETS),
|
||
str([len(a) for a in ASSETS]))
|
||
|
||
# A4 —— 下载前先用官方 SHA256SUMS 清单核对有没有这个架构的包。
|
||
# 没有这条,arm64 用户点「离线包更新」会先传几十 MB 才失败。
|
||
ck('带架构后缀的附件在下载前用 SHA256SUMS 核对存在性',
|
||
re.search(r"target\.get\('arch_specific'\)", UPDATE)
|
||
and re.search(r"_manifest\s*=\s*_fetch_manifest\(ver\)", UPDATE)
|
||
and re.search(r"target\['name'\]\s+not\s+in\s+_manifest", UPDATE))
|
||
ck('清单核对放在起 worker 之前(不能下了才说没有)',
|
||
UPDATE.index("_fetch_manifest(ver)") < UPDATE.index("_apply_worker(ver, target"),
|
||
'核对位置在 worker 之后 = 白做完')
|
||
|
||
print()
|
||
print('=' * 56)
|
||
print('B. 固件探测:设备树机器不能报传统 BIOS')
|
||
print('=' * 56)
|
||
|
||
ck('_read_bios 存在设备树分支(第三种固件类型)',
|
||
'设备树 (Device Tree)' in HELPER)
|
||
ck('设备树分支返回稳定枚举 fw_type=dt',
|
||
re.search(r"firmware,\s*fw_type\s*=\s*'设备树 \(Device Tree\)',\s*'dt'", HELPER)
|
||
is not None)
|
||
# 判据顺序:EFI 优先于设备树 —— 树莓派 4 既能 EFI 也能设备树,应当认 EFI
|
||
_i_dt_branch = HELPER.find("elif has_dt:")
|
||
_i_efi_branch = HELPER.find("if is_efi:")
|
||
ck('判据顺序 EFI 优先于设备树',
|
||
_i_efi_branch >= 0 and _i_dt_branch >= 0 and _i_efi_branch < _i_dt_branch,
|
||
'efi@%s dt@%s' % (_i_efi_branch, _i_dt_branch))
|
||
ck('设备树两个候选路径都查(/proc 与 /sys/firmware)',
|
||
"'/proc/device-tree/model'" in HELPER
|
||
and "'/sys/firmware/devicetree/base/model'" in HELPER)
|
||
# compatible 属性是多条 \0 结尾串拼的,不切开会把整串(含空字符)塞给前端
|
||
ck('_dt_read 按 NUL 切开取第一条(不是整串原样返回)',
|
||
re.search(r"for part in raw\.split\(b'\\0'\):", HELPER) is not None)
|
||
ck('无 DMI 厂商时用设备树板名兜底(面板厂商格不留空)',
|
||
re.search(r"if not vendor:\s*\n\s*vendor = dt_model", HELPER) is not None)
|
||
# 兼容:前端读的是 firmware / is_efi / vendor / version / date。
|
||
# 加字段没问题,**删掉或改名**会让概览页那一行直接空掉。
|
||
_bios_keys = set()
|
||
try:
|
||
_tree = ast.parse(HELPER)
|
||
for _node in ast.walk(_tree):
|
||
if isinstance(_node, ast.FunctionDef) and _node.name == '_read_bios':
|
||
for _sub in ast.walk(_node):
|
||
# 只认**最后的 return 字典**,别把中途别的字典也算进来
|
||
if isinstance(_sub, ast.Return) and isinstance(_sub.value, ast.Dict):
|
||
for _k in _sub.value.keys:
|
||
if isinstance(_k, ast.Constant):
|
||
_bios_keys.add(_k.value)
|
||
except Exception as _e: # pragma: no cover
|
||
print(' (AST 解析异常:%s)' % _e)
|
||
|
||
ck('_read_bios 返回值里有 fw_type 枚举',
|
||
'fw_type' in _bios_keys, str(sorted(_bios_keys)))
|
||
ck('_read_bios 仍返回前端在用的旧字段(firmware/is_efi/vendor/version/date)',
|
||
{'firmware', 'is_efi', 'vendor', 'version', 'date'} <= _bios_keys,
|
||
str(sorted({'firmware', 'is_efi', 'vendor', 'version', 'date'} - _bios_keys)))
|
||
|
||
print()
|
||
print('=' * 56)
|
||
print('C. i18n:新增展示串必须登记')
|
||
print('=' * 56)
|
||
|
||
_i = I18N.find("'SYSINFO_NOTES'")
|
||
_j = I18N.find("'PAGE_NOTES'", _i if _i > 0 else 0)
|
||
_block = I18N[_i:_j] if _i > 0 and _j > _i else ''
|
||
ck('i18n 的 SYSINFO_NOTES 里登记了「设备树 (Device Tree)」',
|
||
"'设备树 (Device Tree)'" in _block)
|
||
# ⚠️ 必须把范围限定在**本词条的花括号内**。早先用 300 字符的松弛窗口找 'd':,
|
||
# 会跨到下一条词条(紧挨着的「未读取到温度传感器…」恰好带 'd':)→
|
||
# 删掉本词条的 d 之后判据照样绿。反向验证 C2 揪出来的假绿。
|
||
_m_dt = re.search(r"'设备树 \(Device Tree\)':\s*(\{.*?\})", _block, re.S)
|
||
_entry_dt = _m_dt.group(1) if _m_dt else ''
|
||
ck('该词条同时给了 n / d 两段英文',
|
||
"'n': '" in _entry_dt and "'d': '" in _entry_dt,
|
||
'词条块=%r' % (_entry_dt[:80],))
|
||
# 同一个 key 重复登记会让 JS 直接语法/语义冲突
|
||
ck('「设备树 (Device Tree)」只登记一次',
|
||
I18N.count("'设备树 (Device Tree)'") == 1)
|
||
|
||
print()
|
||
print('=' * 56)
|
||
print('D. 打包脚本')
|
||
print('=' * 56)
|
||
|
||
ck('离线包名用 dpkg 架构变量拼',
|
||
re.search(r'NAME="drouter-\$\{VERSION\}-offline-\$\{ARCH\}"', OFFLINE) is not None
|
||
or 'drouter-${VERSION}-offline-${ARCH}' in OFFLINE)
|
||
# 包内 README 里的两处 amd64 已换成变量
|
||
ck('包内离线安装说明里的目标机描述用 ${ARCH}(不是写死 amd64)',
|
||
'Debian 13 / ${ARCH}' in OFFLINE)
|
||
ck('包内离线安装说明的系统核对项提到 dpkg --print-architecture',
|
||
'dpkg --print-architecture\\`' in OFFLINE
|
||
or 'dpkg --print-architecture' in OFFLINE)
|
||
ck('包内说明不再出现「Debian 13 (trixie) amd64」的写死表述',
|
||
'Debian 13 (trixie) amd64' not in OFFLINE)
|
||
|
||
ck('Docker 构建接受第 2 个架构参数',
|
||
re.search(r'ARCH="\$\{2:-\$DPKG_NATIVE\}"', DOCKER) is not None)
|
||
ck('交叉构建时传 --platform linux/<arch>',
|
||
re.search(r'PLATFORM_ARG=\(--platform "linux/\$ARCH"\)', DOCKER) is not None)
|
||
ck('交叉构建时镜像标签带 -<arch> 后缀',
|
||
re.search(r'IMGTAG="\$TAG-\$ARCH"', DOCKER) is not None)
|
||
ck('原生构建时 IMGTAG 就是 TAG(不改动既有发版产物)',
|
||
re.search(r'IMGTAG="\$TAG"\s*\n', DOCKER) is not None)
|
||
ck('Docker 构建真的把隐含架构参数传给 docker build',
|
||
'docker build "${PLATFORM_ARG[@]}"' in DOCKER)
|
||
|
||
print()
|
||
print('=' * 56)
|
||
print('判据 %d 条,失败 %d 条' % (n, len(fails)))
|
||
if fails:
|
||
for f in fails:
|
||
print(' FAIL: %s' % f)
|
||
sys.exit(1 if fails else 0)
|