适配 arm64 Debian(Armbian 26.8.1 trixie):去掉架构假设
有用户要在 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,未启用任何服务,未改动网络。
This commit is contained in:
1 parent
8303fa3359
commit
6df3a94b90
11 files changed
+1119
-31
No files matched your search
@@ -427,7 +427,7 @@ python3 devtools/check-shots.py
|
||||
|
||||
| | 要求 |
|
||||
|---|---|
|
||||
| CPU | 1 核 x86-64 |
|
||||
| CPU | 1 核(x86-64 或 arm64,见下方「支持矩阵」) |
|
||||
| 内存 | 1 GB |
|
||||
| 硬盘 | 8 GB |
|
||||
| 网卡 | 1 个(先做 LAN + 管理口) |
|
||||
@@ -644,7 +644,8 @@ bash packaging/build-docker.sh 1.0.8
|
||||
构建顺序上,`build-offline-bundle.sh` 会**自己**先调 `build-deb.sh`
|
||||
和 `make-offline-deps.sh`,所以想一步到位只跑它就行。
|
||||
|
||||
> **构建环境要求**:Debian 13 (trixie) / amd64。
|
||||
> **构建环境要求**:Debian 13 (trixie);离线包**必须在与目标机同架构的机器上**
|
||||
> 构建(`dpkg --print-architecture` 决定包名后缀),交叉打包出来的包装不上。
|
||||
> 依赖闭包是在**空 dpkg status** 的临时 apt 根里求解的(而不是用当前机器的
|
||||
> 已装状态),否则「开发机上恰好装过」的包会被 apt 判为"已满足"而漏收 ——
|
||||
> 到了干净目标机上就变成 `Depends: xxx but it is not installable`。
|
||||
@@ -722,7 +723,7 @@ bash packaging/build-docker.sh 1.0.8
|
||||
| **端口冲突** | 装完**不会**自动启动 `dnsmasq` / `radvd` / `kea`。但如果这些服务**本来就在跑**,请先停掉再让 Drouter 接管 |
|
||||
| **PVE 虚拟机** | 需要给 VM 网卡开**混杂模式**,否则桥接 / VLAN 的包会被宿主丢掉 |
|
||||
| **系统时间** | 面板和证书校验都依赖正确时间。裸装 Debian 若时间不对,先 `sudo timedatectl set-ntp true` |
|
||||
| **离线安装前** | 确认目标机是 **amd64**、剩余磁盘 **≥ 1.5 GB**。离线包里的依赖是按 **Debian 13 (trixie)** 下载的,装到别的版本上可能出现版本不匹配 |
|
||||
| **离线安装前** | 确认目标机与离线包**同一架构**(`dpkg --print-architecture`)、剩余磁盘 **≥ 1.5 GB**。离线包里的依赖是按 **Debian 13 (trixie)** 下载的,装到别的版本上可能出现版本不匹配 |
|
||||
|
||||
### 📴 关于离线安装(目标机无外网)
|
||||
|
||||
@@ -743,7 +744,7 @@ bash packaging/build-docker.sh 1.0.8
|
||||
|
||||
| 约束 | 原因 |
|
||||
|---|---|
|
||||
| **架构必须匹配** | 包名带 `-amd64`,里面是 x86_64 的二进制。arm64 目标机请按文档自行重建 |
|
||||
| **架构必须匹配** | 包名带 `-<架构>` 后缀,里面的 deb 是对应 CPU 的二进制。**amd64 的包装不到 arm64 上** —— arm64 机器请在 arm64 机器上重跑 `packaging/build-offline-bundle.sh`,或改用 deb 本体(本机架构见 `dpkg --print-architecture`) |
|
||||
| **发行版必须匹配** | 依赖来自 `trixie` 仓库。跨版本(如 bookworm)会出现 `libc6` 版本冲突 |
|
||||
| **磁盘 ≥ 1.5 GB** | 236 个 deb 解包 + 安装要占几百 MB;`install.sh` 会在低于 1.5 GB 时告警 |
|
||||
| **装完要手动改密码** | 默认 `admin` / `admin123`,离线环境下尤其要第一时间改 |
|
||||
@@ -786,7 +787,7 @@ bash packaging/build-docker.sh 1.0.8
|
||||
| Debian 12 (bookworm) | ⚠️ 可用,但 `docker-compose` 包名不同,nftables 版本较老 |
|
||||
| Ubuntu 24.04 | ⚠️ 大体可用,`docker-compose-v2` 是 Ubuntu 的叫法 |
|
||||
| Debian 11 及更早 | ❌ 不支持(Python 版本与 nftables 语法不兼容) |
|
||||
| 架构 | amd64 已充分验证;arm64 理论可用(`Architecture: all`,依赖需自行确认) |
|
||||
| 架构 | amd64 全功能已验证。**arm64 已在一个 Debian 系发行版上实测通过**:Armbian 26.8.1 trixie(Debian 13.6)/ aarch64 / 内核 6.18.35-ophub / Amlogic S912 开发板 —— 构建、依赖自检、配置渲染、系统探测全部通过,详见 [docs/07-arm64-适配说明.md](docs/07-arm64-适配说明.md)。<br>其它 arm64 发行版(树莓派系、Debian 官方 aarch64 镜像等)**未验证**。<br>⚠️ arm64 上请优先用 **deb 本体**(`Architecture: all`,各 CPU 通用);Release 的离线包与 Docker 镜像仍只有 amd64 版本 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ EXCL = {
|
||||
# 更糟的是中途被打断会留下污染(已经发生过一次)。
|
||||
# 只能手动跑:python _dev/t-107-inject-wg.py --batch 0
|
||||
't-107-inject-wg',
|
||||
# arm64 适配(2026-10-16)的反向验证,同上:覆盖 backend/drouter-update.py、
|
||||
# backend/drouter-helper.py、web/i18n.js、packaging/*.sh 四处源码。
|
||||
't-arch-inject', 't-update-inject', 'upd-inject',
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""反向验证:把 arm64 适配的每处改动改坏,确认 t-arch.py 真的会红。
|
||||
|
||||
跟着 t-update-inject.py 的惯例写:每个用例「注入 → 跑判据 → 必须因为这一条
|
||||
而红 → 无条件还原」。落点是**语义层**,不是被检查的那个名字。
|
||||
|
||||
用法(**不要**加进 scripts/test-static.sh,那里会改写源文件):
|
||||
python _dev/t-arch-inject.py
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PY = sys.executable
|
||||
JUDGE = os.path.join(ROOT, '_dev', 't-arch.py')
|
||||
|
||||
HELPER = 'backend/drouter-helper.py'
|
||||
UPDATE = 'backend/drouter-update.py'
|
||||
I18N = 'web/i18n.js'
|
||||
OFFLINE = 'packaging/build-offline-bundle.sh'
|
||||
DOCKER = 'packaging/build-docker.sh'
|
||||
|
||||
CASES = [
|
||||
# ---------------- A. 更新模块 ----------------
|
||||
# 把 %s 拼架构换回写死的 amd64 —— 就是 arm64 适配前的原状。
|
||||
# 判据第一条查 'offline-amd64' 是否**不在** 源码里,注入后立刻出现。
|
||||
('A1 离线包资源名写死 amd64(arm64 会下到 x86 包)',
|
||||
UPDATE,
|
||||
" ('offline', 'drouter-{ver}-offline-%s.tar.gz' % a,\n"
|
||||
" '离线安装包(%s,87 MB,断网可装)' % a, True),",
|
||||
" ('offline', 'drouter-{ver}-offline-amd64.tar.gz',\n"
|
||||
" '离线安装包(amd64,87 MB,断网可装)', True),",
|
||||
'离线包资源名模板不含写死的 amd64'),
|
||||
|
||||
# build_assets 忽略入参、一律按本机算 —— 三架构拼出同一个名字。
|
||||
# ⚠️ 不删 _deb_arch(),因为判据查的是「拼出来的结果」而非函数是否存在。
|
||||
('A2 build_assets 忽略 arch 入参(三架构同名)',
|
||||
UPDATE,
|
||||
" a = arch or _deb_arch()",
|
||||
" a = _deb_arch()",
|
||||
'不同架构拼出的离线包名两两不同'),
|
||||
|
||||
# deb 是 Architecture: all,跟进架构走就错了。
|
||||
('A3 deb 包名也带上架构后缀(违背 Architecture: all)',
|
||||
UPDATE,
|
||||
" ('deb', 'drouter_{ver}_all.deb', 'Deb 安装包(推荐,523 KB)', False),",
|
||||
" ('deb', 'drouter_{ver}_%s.deb' % a, 'Deb 安装包(推荐,523 KB)', False),",
|
||||
'deb 本体三架构同名'),
|
||||
|
||||
# arch_specific 全 False → apply 不再先用 SHA256SUMS 核对,arm64 白下 87MB。
|
||||
('A4 离线包漏标 arch_specific(失去下载前核对)',
|
||||
UPDATE,
|
||||
" '离线安装包(%s,87 MB,断网可装)' % a, True),",
|
||||
" '离线安装包(%s,87 MB,断网可装)' % a, False),",
|
||||
'只有离线包标了 arch_specific'),
|
||||
|
||||
# 元组少一列 → asset_list 解包 ValueError,「更新」页整块渲染不出来。
|
||||
# 这条注入会让前面几条也一起红(模块在 judge 里加载失败),属实完好:
|
||||
# 断言关键词挑的是**本条对应**的那条判据。
|
||||
('A5 ASSETS 条目退回三元组(asset_list 解包炸)',
|
||||
UPDATE,
|
||||
" ('docker', 'drouter-{ver}-docker.tar', 'Docker 镜像(169 MB)', False),",
|
||||
" ('docker', 'drouter-{ver}-docker.tar', 'Docker 镜像(169 MB)'),",
|
||||
'ASSETS 非空且每个条目是 4 元组'),
|
||||
|
||||
# 把整个核对块删掉 —— 等于回到「下载完才发现没有这个架构」。
|
||||
('A6 下载前的 SHA256SUMS 名单核对被删',
|
||||
UPDATE,
|
||||
" if target.get('arch_specific'):\n"
|
||||
" _manifest = _fetch_manifest(ver)\n"
|
||||
" if _manifest and target['name'] not in _manifest:\n"
|
||||
" return {'ok': False,\n"
|
||||
" 'msg_cn': '该版本没有发布本机架构(%s)的离线包:%s。'\n"
|
||||
" 'deb 包是 Architecture: all,各 CPU 通用,'\n"
|
||||
" '请改用「Deb 安装包」这一项更新。'\n"
|
||||
" % (_deb_arch(), target['name'])}\n",
|
||||
"",
|
||||
'带架构后缀的附件在下载前用 SHA256SUMS 核对存在性'),
|
||||
|
||||
# ---------------- B. 固件探测 ----------------
|
||||
# 删掉设备树分支 —— ARM 板重新落到 else,显示「传统 BIOS」。
|
||||
('B1 删掉设备树分支(ARM 板重新误报传统 BIOS)',
|
||||
HELPER,
|
||||
" elif has_dt:\n"
|
||||
" firmware, fw_type = '设备树 (Device Tree)', 'dt'\n",
|
||||
"",
|
||||
'设备树分支返回稳定枚举 fw_type=dt'),
|
||||
|
||||
# 枚举值不是小写 dt —— 前端拿它做分支会失配。
|
||||
('B2 设备树枚举值写成大写 DT(前端分支失配)',
|
||||
HELPER,
|
||||
" firmware, fw_type = '设备树 (Device Tree)', 'dt'",
|
||||
" firmware, fw_type = '设备树 (Device Tree)', 'DT'",
|
||||
'设备树分支返回稳定枚举 fw_type=dt'),
|
||||
|
||||
# 顺序反了:设备树优先于 EFI —— 树莓派 4 以 EFI 启动会被认成设备树。
|
||||
('B3 判据顺序改成设备树优先于 EFI',
|
||||
HELPER,
|
||||
" if is_efi:\n"
|
||||
" firmware, fw_type = 'UEFI', 'efi'\n"
|
||||
" elif has_dt:\n"
|
||||
" firmware, fw_type = '设备树 (Device Tree)', 'dt'\n",
|
||||
" if has_dt:\n"
|
||||
" firmware, fw_type = '设备树 (Device Tree)', 'dt'\n"
|
||||
" elif is_efi:\n"
|
||||
" firmware, fw_type = 'UEFI', 'efi'\n",
|
||||
'判据顺序 EFI 优先于设备树'),
|
||||
|
||||
# compatible 是一串 \0 分隔的标识,不切开会把整串塞给前端。
|
||||
('B4 _dt_read 不再按 NUL 切开(拿整串当板名)',
|
||||
HELPER,
|
||||
" for part in raw.split(b'\\0'):",
|
||||
" for part in [raw]:",
|
||||
'_dt_read 按 NUL 切开取第一条'),
|
||||
|
||||
# 兜底删掉 → ARM 板「厂商」一格空白,用户以为探测坏了。
|
||||
('B5 去掉设备树板名兜底(厂商格留空)',
|
||||
HELPER,
|
||||
" if not vendor:\n vendor = dt_model\n",
|
||||
"",
|
||||
'无 DMI 厂商时用设备树板名兜底'),
|
||||
|
||||
# 改字段名会让概览页那一行直接空掉(前端读 fw_type)。
|
||||
('B6 fw_type 字段改名(前端读不到)',
|
||||
HELPER,
|
||||
" 'fw_type': fw_type,",
|
||||
" 'fwtype': fw_type,",
|
||||
'_read_bios 返回值里有 fw_type 枚举'),
|
||||
|
||||
# ---------------- C. i18n ----------------
|
||||
# 词条没登记 → 英文界面这一格直接漏中文。
|
||||
('C1 i18n 漏登记「设备树 (Device Tree)」',
|
||||
I18N,
|
||||
" '设备树 (Device Tree)':\n"
|
||||
" { 'n': 'Device Tree',\n"
|
||||
" 'd': 'Device Tree / U-Boot boot. ARM and RISC-V boards have "
|
||||
"no BIOS: the bootloader hands the kernel a device tree describing the "
|
||||
"hardware.' },\n",
|
||||
"",
|
||||
'i18n 的 SYSINFO_NOTES 里登记了'),
|
||||
|
||||
# 只有 n 没有 d → 面板里那段英文说明是空的。
|
||||
('C2 词条只给了 n 没给 d(说明落空)',
|
||||
I18N,
|
||||
" 'd': 'Device Tree / U-Boot boot. ARM and RISC-V boards have "
|
||||
"no BIOS: the bootloader hands the kernel a device tree describing the "
|
||||
"hardware.' },",
|
||||
" 'x': 'unused' },",
|
||||
'该词条同时给了 n / d 两段英文'),
|
||||
|
||||
# ---------------- D. 打包脚本 ----------------
|
||||
('D1 离线包名写死 amd64',
|
||||
OFFLINE,
|
||||
'NAME="drouter-${VERSION}-offline-${ARCH}"',
|
||||
'NAME="drouter-${VERSION}-offline-amd64"',
|
||||
'离线包名用 dpkg 架构变量拼'),
|
||||
|
||||
('D2 包内 README 目标机描述写死 amd64',
|
||||
OFFLINE,
|
||||
'**适用场景**:目标机(Debian 13 / ${ARCH})**没有外网**,想一次装完。',
|
||||
'**适用场景**:目标机(Debian 13 (trixie) amd64)**没有外网**,想一次装完。',
|
||||
'包内离线安装说明里的目标机描述用 ${ARCH}'),
|
||||
|
||||
('D3 Docker 构建不接受第 2 个架构参数',
|
||||
DOCKER,
|
||||
'ARCH="${2:-$DPKG_NATIVE}"',
|
||||
'ARCH="$DPKG_NATIVE"',
|
||||
'Docker 构建接受第 2 个架构参数'),
|
||||
|
||||
('D4 交叉构建不传 --platform',
|
||||
DOCKER,
|
||||
' PLATFORM_ARG=(--platform "linux/$ARCH")',
|
||||
' PLATFORM_ARG=()',
|
||||
'交叉构建时传 --platform linux/<arch>'),
|
||||
|
||||
('D5 交叉构建镜像标签不带架构后缀',
|
||||
DOCKER,
|
||||
' IMGTAG="$TAG-$ARCH"',
|
||||
' IMGTAG="$TAG"',
|
||||
'交叉构建时镜像标签带 -<arch> 后缀'),
|
||||
|
||||
('D6 docker build 没接 PLATFORM_ARG',
|
||||
DOCKER,
|
||||
'docker build "${PLATFORM_ARG[@]}" -f',
|
||||
'docker build -f',
|
||||
'Docker 构建真的把隐含架构参数传给 docker build'),
|
||||
]
|
||||
|
||||
|
||||
def run():
|
||||
r = subprocess.run([PY, JUDGE], capture_output=True, cwd=ROOT)
|
||||
return r.returncode, (r.stdout or b'').decode('utf-8', 'replace')
|
||||
|
||||
|
||||
def main():
|
||||
print('基线(应全绿):')
|
||||
rc, out = run()
|
||||
tail = out.strip().splitlines()[-1] if out.strip() else ''
|
||||
print(' rc=%d %s' % (rc, tail))
|
||||
if rc != 0:
|
||||
print('✘ 基线就是红的,先修好再谈反向验证')
|
||||
print(out[-2000:])
|
||||
return 1
|
||||
|
||||
bad = []
|
||||
for desc, rel, old, new, kw in CASES:
|
||||
p = os.path.join(ROOT, rel)
|
||||
with open(p, 'rb') as f:
|
||||
orig = f.read().decode('utf-8')
|
||||
if old not in orig:
|
||||
print(' 跳过 %-52s (锚点未命中)' % desc)
|
||||
bad.append(desc + ' 锚点未命中')
|
||||
continue
|
||||
try:
|
||||
with open(p, 'wb') as f:
|
||||
f.write(orig.replace(old, new, 1).encode('utf-8'))
|
||||
rc, out = run()
|
||||
caused = (rc != 0) and (kw in out)
|
||||
if caused:
|
||||
print(' ok %-52s → 如约变红' % desc)
|
||||
else:
|
||||
print(' FAIL %-52s → rc=%d 关键词命中=%s'
|
||||
% (desc, rc, kw in out))
|
||||
bad.append(desc)
|
||||
finally:
|
||||
with open(p, 'wb') as f:
|
||||
f.write(orig.encode('utf-8'))
|
||||
|
||||
rc, out = run()
|
||||
tail = out.strip().splitlines()[-1] if out.strip() else ''
|
||||
print()
|
||||
print('还原后基线:rc=%d %s' % (rc, tail))
|
||||
if rc != 0:
|
||||
bad.append('还原后未恢复全绿')
|
||||
print(out[-1500:])
|
||||
|
||||
print()
|
||||
print('反向验证 %d 例,异常 %d 例' % (len(CASES), len(bad)))
|
||||
for b in bad:
|
||||
print(' ! %s' % b)
|
||||
return 1 if bad else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
#!/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)
|
||||
@@ -1857,8 +1857,55 @@ def _read_temp():
|
||||
return temp_c, temp_src
|
||||
|
||||
|
||||
# ---- 设备树(Device Tree)属性:ARM / RISC-V 板子的「硬件名片」-------------
|
||||
# 这类机器用 U-Boot + 设备树启动,没有 SMBIOS/DMI,也常常没有 EFI。
|
||||
# 属性值是若干条以 \0 结尾的字符串拼在一起,取第一个非空的那条。
|
||||
# 两个路径都写:/proc/device-tree 是内核 6.x 的常规挂载点,
|
||||
# /sys/firmware/devicetree/base 是 sysfs 侧的路径,个别发行版只有后者。
|
||||
DT_MODEL_PATHS = ('/proc/device-tree/model',
|
||||
'/sys/firmware/devicetree/base/model')
|
||||
DT_COMPAT_PATHS = ('/proc/device-tree/compatible',
|
||||
'/sys/firmware/devicetree/base/compatible')
|
||||
|
||||
|
||||
def _dt_read(paths):
|
||||
"""读设备树里的一条字符串属性。读不到返回空串,不抛异常。
|
||||
|
||||
⚠️ 必须按 \0 切开再取第一条:compatible 属性里塞的是一串匹配优先级
|
||||
从高到低的标识(例如 amlogic,s912 后跟 amlogic,meson-gxm),
|
||||
原样返回会把一整串连同结尾的空字符一起塞给前端。
|
||||
"""
|
||||
for p in paths:
|
||||
try:
|
||||
with open(p, 'rb') as f:
|
||||
raw = f.read()
|
||||
except Exception:
|
||||
continue
|
||||
for part in raw.split(b'\0'):
|
||||
s = part.decode('utf-8', 'replace').strip()
|
||||
if s:
|
||||
return s
|
||||
return ''
|
||||
|
||||
|
||||
def _read_bios():
|
||||
"""真实探测固件类型:UEFI / 传统 BIOS,并尽量读出厂商与版本。"""
|
||||
"""真实探测固件:UEFI / 传统 BIOS / 设备树,并尽量读出厂商与版本。
|
||||
|
||||
⚠️ 第三个分支「设备树」是 2026-10-16 做 arm64 适配时才加的。
|
||||
早先只看两件事 —— /sys/firmware/efi 在不在、/sys/class/dmi 下的字段能不能读到。
|
||||
在 x86 上这样够用(UEFI 与传统 BIOS 二分),但 **ARM 机器两个都没有**:
|
||||
它不走 EFI,也从来没有 SMBIOS/DMI。于是这类机器一律落到 else 分支,
|
||||
系统概览显示「传统 BIOS」,厂商 / 版本 / 日期还是空白 —— 面板上这一行是**错的**,
|
||||
ARM 开发板根本没有 BIOS 这个东西。
|
||||
|
||||
实测形态(本次 arm64 适配的验证机,见 docs/arm64 部署说明):
|
||||
Armbian 26.8.1 trixie(Debian 13.6)/ aarch64 / 内核 6.18.35-ophub
|
||||
Amlogic S912 板 —— 无 /sys/firmware/efi、无 /sys/class/dmi,
|
||||
/proc/device-tree/model 读到板名。
|
||||
|
||||
判据顺序不能换:先 EFI,再设备树,最后才是 BIOS。
|
||||
树莓派 4 这类既能以 EFI 也能以设备树启动的机器,应当优先认 EFI。
|
||||
"""
|
||||
is_efi = os.path.isdir('/sys/firmware/efi')
|
||||
vendor = version = date = ''
|
||||
# DMI 信息(部分虚拟机/hypervisor 不暴露,容错处理)
|
||||
@@ -1883,15 +1930,35 @@ def _read_bios():
|
||||
vendor = f.read().strip()
|
||||
except Exception:
|
||||
pass
|
||||
# 设备树:ARM / RISC-V 板子的板名与 SoC compatible
|
||||
dt_model = _dt_read(DT_MODEL_PATHS)
|
||||
compat = _dt_read(DT_COMPAT_PATHS)
|
||||
has_dt = bool(dt_model or compat)
|
||||
# 没有 DMI 厂商时,拿设备树的板名顶上 —— 面板上「厂商」这一格为空
|
||||
# 会让用户以为探测坏了,而板名恰恰是这类机器上唯一有意义的标识。
|
||||
if not vendor:
|
||||
vendor = dt_model
|
||||
if is_efi:
|
||||
firmware, fw_type = 'UEFI', 'efi'
|
||||
elif has_dt:
|
||||
firmware, fw_type = '设备树 (Device Tree)', 'dt'
|
||||
else:
|
||||
firmware, fw_type = '传统 BIOS', 'bios'
|
||||
return {
|
||||
'firmware': 'UEFI' if is_efi else '传统 BIOS',
|
||||
'firmware': firmware,
|
||||
# ⚠️ 面板展示用 firmware(中文,走前端 bt4 查英文),这里是给程序判断的
|
||||
# 稳定枚举 —— 别拿中文串做分支,加语言就要跟着改代码。
|
||||
'fw_type': fw_type,
|
||||
'is_efi': is_efi,
|
||||
'is_dt': has_dt,
|
||||
'vendor': vendor,
|
||||
'version': version,
|
||||
'date': date,
|
||||
'compat': compat,
|
||||
}
|
||||
|
||||
|
||||
|
||||
def _read_file(path, limit=200):
|
||||
"""安全读取一个小文本文件,失败返回空串。"""
|
||||
try:
|
||||
|
||||
@@ -87,12 +87,60 @@ DL_SOURCES = [
|
||||
OFFICIAL_ASSET_TMPL = DL_SOURCES[0][1]
|
||||
ASSET_TMPL = OFFICIAL_ASSET_TMPL # 向后兼容旧引用
|
||||
|
||||
# ---- 本机架构 ------------------------------------------------------------
|
||||
# ⚠️ 必须知道本机架构,因为**离线包是分架构发布的**:包里那几百个 .deb 全是
|
||||
# 某一个 CPU 的二进制,引用名里带着架构后缀。
|
||||
_UNAME2ARCH = {'x86_64': 'amd64', 'amd64': 'amd64', 'i686': 'i386',
|
||||
'i386': 'i386', 'aarch64': 'arm64', 'arm64': 'arm64',
|
||||
'armv7l': 'armhf', 'armv6l': 'armhf', 'riscv64': 'riscv64'}
|
||||
|
||||
|
||||
def _deb_arch():
|
||||
"""本机 dpkg 架构名(amd64 / arm64 / armhf …)。
|
||||
|
||||
arm64 适配(2026-10-16)加的:离线包的模板原先写死 amd64,
|
||||
于是 arm64 机器上「查看更新内容并升级」会列出并下载一份 x86 离线包,
|
||||
装的时候才炸依赖校验 —— 白白传了几十 MB。现在按本机架构拼名字。
|
||||
|
||||
取不到 dpkg 时(非 Debian 系)退到 uname -m 的映射;映射也对不上就原样返回,
|
||||
这是「猜」,但总比一头栽向明确不匹配的架构强。
|
||||
"""
|
||||
try:
|
||||
p = subprocess.run(['dpkg', '--print-architecture'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
if p.returncode == 0 and p.stdout.strip():
|
||||
return p.stdout.strip()
|
||||
except Exception:
|
||||
pass
|
||||
machine = ''
|
||||
try:
|
||||
machine = os.uname().machine
|
||||
except Exception:
|
||||
pass
|
||||
return _UNAME2ARCH.get(machine, machine or 'amd64')
|
||||
|
||||
|
||||
# 我们关心的附件(顺序即前端展示顺序)
|
||||
ASSETS = [
|
||||
('deb', 'drouter_{ver}_all.deb', 'Deb 安装包(推荐,523 KB)'),
|
||||
('offline', 'drouter-{ver}-offline-amd64.tar.gz', '离线安装包(87 MB,断网可装)'),
|
||||
('docker', 'drouter-{ver}-docker.tar', 'Docker 镜像(169 MB)'),
|
||||
]
|
||||
# ⚠️ 每个条目是**四元组** (key, 文件名模板, 说明, 是否随架构变化)。
|
||||
# 第四个字段是 arm64 适配时加的:apply_update 靠它判断要不要先拿
|
||||
# SHA256SUMS 清单核对文件名。改这里的元组长度,asset_list() 的解包
|
||||
# 必须同步改 —— 不同步的表现是面板上「更新」页整块渲染不出来。
|
||||
# 说明文字里的体积是 1.0.10 的实测值,仅作参考。
|
||||
def build_assets(arch=None):
|
||||
"""按架构拼出附件清单。传 arch 便于单测覆盖各种 CPU。"""
|
||||
a = arch or _deb_arch()
|
||||
return [
|
||||
('deb', 'drouter_{ver}_all.deb', 'Deb 安装包(推荐,523 KB)', False),
|
||||
('offline', 'drouter-{ver}-offline-%s.tar.gz' % a,
|
||||
'离线安装包(%s,87 MB,断网可装)' % a, True),
|
||||
('docker', 'drouter-{ver}-docker.tar', 'Docker 镜像(169 MB)', False),
|
||||
]
|
||||
|
||||
|
||||
# ⚠️ 保留模块级常量给旧调用方(内容 == 本机架构下的清单)。
|
||||
# 它是**导入时**算一次的:同一进程里 dpkg 的架构不会变,
|
||||
# 没必要每次 asset_list() 都跑一遍子进程。
|
||||
ASSETS = build_assets()
|
||||
|
||||
_stop_flag = threading.Event()
|
||||
_lock = threading.Lock()
|
||||
@@ -299,12 +347,16 @@ def _write_state(**kw):
|
||||
pass
|
||||
|
||||
|
||||
def asset_list(ver):
|
||||
"""给定版本,列出官方附件的真实文件名 + 说明。"""
|
||||
def asset_list(ver, arch=None):
|
||||
"""给定版本,列出官方附件的真实文件名 + 说明。
|
||||
|
||||
arch 缺省时按本机架构拼;传值可用于单测(不看机器)。
|
||||
'arch_specific' 为 True 的条目,文件名里带架构后缀。
|
||||
"""
|
||||
out = []
|
||||
for key, tmpl, desc in ASSETS:
|
||||
for key, tmpl, desc, arch_specific in (build_assets(arch) if arch else ASSETS):
|
||||
out.append({'key': key, 'name': tmpl.format(ver=ver.lstrip('v')),
|
||||
'desc': desc})
|
||||
'desc': desc, 'arch_specific': arch_specific})
|
||||
return out
|
||||
|
||||
|
||||
@@ -438,6 +490,18 @@ def apply_update(ver, key, backup=True):
|
||||
if not target:
|
||||
return {'ok': False, 'msg_cn': '未知的附件类型:%s' % key}
|
||||
|
||||
# arm64 适配(2026-10-16):带架构后缀的附件(离线包)不是每个版本每个
|
||||
# 架构都发。别等下完 87 MB 才炸 —— 先拿 SHA256SUMS 清单对一下有没有这个名字。
|
||||
# 清单取不到(离线 / 三个源都不通)时不拦:那种情况本来也走不到下载成功。
|
||||
if target.get('arch_specific'):
|
||||
_manifest = _fetch_manifest(ver)
|
||||
if _manifest and target['name'] not in _manifest:
|
||||
return {'ok': False,
|
||||
'msg_cn': '该版本没有发布本机架构(%s)的离线包:%s。'
|
||||
'deb 包是 Architecture: all,各 CPU 通用,'
|
||||
'请改用「Deb 安装包」这一项更新。'
|
||||
% (_deb_arch(), target['name'])}
|
||||
|
||||
# ⚠️ 「检查 running」和「写 running=True」必须在**同一把锁内**。
|
||||
# 早先写在锁外,两个快速请求都能通过检查 → 各自起一个 worker →
|
||||
# 同时向同一个 dest + '.part' 写,字节交错 → 产出损坏的安装包。
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/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)
|
||||
@@ -0,0 +1,247 @@
|
||||
# Drouter arm64 Debian 适配说明
|
||||
|
||||
> 适用版本:**v1.0.10**
|
||||
> 适配日期:2026-10-16
|
||||
> 本轮适配**只针对一个发行版**(下面第 1 节那台机器),别的 arm64 发行版未经实测。
|
||||
|
||||
## 0. 先说清楚这次做了什么、没做什么
|
||||
|
||||
有用户提需要在 arm64 的 Debian 上跑 drouter,于是拿了一台 arm64 的 Debian 系
|
||||
发行版做**纯适配**:把项目里所有写死 x86 假设的地方找出来修掉,
|
||||
在这个发行版上实测构建与运行,然后把「发行版名 / 内核名 / CPU / 固件 /
|
||||
板卡」这些机器特征提取出来写进本说明。
|
||||
|
||||
**做了**:源码层面的架构无关化修复(见第 3 节)+ 在目标发行版上实测
|
||||
(见第 2 节)+ 本说明文档。
|
||||
|
||||
**没做**(不要被后面的绿勾误导):
|
||||
|
||||
- **没有在目标机上启用 drouter 的 Web 面板与常驻服务**,也没装依赖、没执行任何
|
||||
「应用到系统」。目标机的 8080 端口已有别的业务在跑,出于不影响现场业务的前提,
|
||||
只做了**只读**验证:构建、渲染、依赖自检、各种探测函数。
|
||||
- **没有做 NAT / DHCP / PPPoE 的实际转发验证**。那需要在面板上点「应用」,
|
||||
会真的改网卡与起服务 —— 本次刻意避开。
|
||||
- **没有验证除下面这台以外的任何 arm64 发行版**。树莓派版 Debian、Debian 官方
|
||||
aarch64 安装镜像、Ubuntu arm64 都不在本次范围内,它们是否同样可用未知。
|
||||
|
||||
---
|
||||
|
||||
## 1. 验证机环境(提取自目标机真实输出)
|
||||
|
||||
事实来源全部是目标机上的命令输出,不是推断。
|
||||
|
||||
| 项目 | 实际值 | 取值命令 |
|
||||
|---|---|---|
|
||||
| **发行版名** | `Armbian 26.8.1 trixie` | `/etc/os-release` → `PRETTY_NAME` |
|
||||
| 底层发行版 | `Debian GNU/Linux 13 (trixie)` | `NAME` / `VERSION` |
|
||||
| 版本标识 | `VERSION_ID=13`、`VERSION_CODENAME=trixie`、`DEBIAN_VERSION_FULL=13.6` | `/etc/os-release` |
|
||||
| 发行版 ID | `ID=debian` | `/etc/os-release` |
|
||||
| 主机标识 | `ARMBIAN_PRETTY_NAME="Armbian 26.8.1 trixie"`(Armbian 附加字段) | `/etc/os-release` |
|
||||
| **内核名** | `6.18.35-ophub` | `uname -r` |
|
||||
| 内核全串 | `Linux armbian 6.18.35-ophub #1 SMP PREEMPT_DYNAMIC Wed Jun 17 14:15:20 UTC 2026 aarch64 GNU/Linux` | `uname -a` |
|
||||
| 内核编译器 | `aarch64-none-linux-gnu-gcc (Arm GNU Toolchain 15.2.Rel1 (Build arm-15.86)) 15.2.1 20251203` | `/proc/version` |
|
||||
| **CPU 架构** | `aarch64`(dpkg 架构名 `arm64`) | `uname -m` / `dpkg --print-architecture` |
|
||||
| CPU | `ARM Cortex-A53` × **8 核**(`ARMv8 Processor rev 4 (v8l)`),100–1512 MHz | `lscpu` / `/proc/cpuinfo` |
|
||||
| **板卡(设备树)** | `Octopus Planet` | `/proc/device-tree/model` |
|
||||
| SoC compatible | `amlogic,q200` | `/proc/device-tree/compatible` |
|
||||
| Armbian 板型记录 | `BOARD="S912"`、`BOARD_NAME="Aml.S912"`、`BOARDFAMILY="meson-gxm"`、`LINUXFAMILY="amlogic"`、`ARCH=arm64` | `/etc/armbian-release` |
|
||||
| **启动方式** | **无 EFI、无 DMI**,U-Boot + 设备树 | `/sys/firmware/efi`、`/sys/class/dmi` 均不存在 |
|
||||
| 虚拟化 | 无(物理板,非容器非虚机) | `systemd-detect-virt` → `none` |
|
||||
| 温度源 | `thermal_zone0` = `cpu-thermal`(实测 44.0 ℃)、`hwmon0` = `scpi_sensors` | `/sys/class/thermal/*`、`/sys/class/hwmon/*` |
|
||||
| 有线网卡 | `eth0`(altname `end0`) | `ip -o link` |
|
||||
| APT 架构 | `APT::Architecture "arm64"`,无外来架构 | `apt-config dump` |
|
||||
| 资源 | 8 核 / 1787 MB 内存 / 根分区 6.5 GB | `nproc` / `free -m` / `df -h /` |
|
||||
|
||||
> 关于 SoC 的一点说明,照实记录差异:设备树的 `compatible` 写的是
|
||||
> `amlogic,q200`,而 Armbian 的板型记录是 `S912 / meson-gxm`(Amlogic S912
|
||||
> 正是 meson-gxm 家族)。两者指向同一个 SoC 家族,只是来源不同 —— 前者是内核
|
||||
> 设备树由 bootloader 提供,后者是 Armbian 构建配置里的板型声明。
|
||||
> drouter 面板显示的是设备树那一套。
|
||||
|
||||
---
|
||||
|
||||
## 2. 在目标机上的实测结果
|
||||
|
||||
### 2.1 构建:`.deb` 一次通过
|
||||
|
||||
```bash
|
||||
bash packaging/build-deb.sh
|
||||
```
|
||||
|
||||
```
|
||||
== 4/6 校验 ==
|
||||
后端模块 14 个、语法全部通过
|
||||
systemd 单元 6 个齐全
|
||||
== 5/6 构建 ==
|
||||
dpkg-deb: building package 'drouter' in '.../drouter_1.0.10_all.deb'.
|
||||
```
|
||||
|
||||
产物 `drouter_1.0.10_all.deb`,**753 396 字节**,`Architecture: all`。
|
||||
包里 `docs/07-arm64-适配说明.md` 也在(随 deb 进 `/opt/drouter/docs/`)。
|
||||
|
||||
> `Architecture: all` 是关键:drouter 是纯 Python + 原生 JS,没有编译产物,
|
||||
> 因此**同一个 deb 在 amd64 与 arm64 上通用**,不需要分架构重建本体。
|
||||
> 真正分架构的只有「离线依赖包」(见 2.5)。
|
||||
|
||||
### 2.2 依赖自检:`depcheck op=check` 正常
|
||||
|
||||
```
|
||||
自检完成:42 项已就绪,5 项缺失,26 项可选未装 # 合计 73 项
|
||||
extra: {"os": "Armbian 26.8.1 trixie", "os_id": "13", "desktops": [], "has_desktop": false}
|
||||
```
|
||||
|
||||
发行版被正确识别成 `Armbian 26.8.1 trixie`,版本号读出 `13` 且按 trixie 核对通过
|
||||
(`offline-install.sh` 就是看 `VERSION_CODENAME` 这一项做版本匹配的)。
|
||||
|
||||
**5 项必需的缺失项**(这台机器本身没装路由相关软件包,属预期,不是适配问题):
|
||||
|
||||
| 缺失项 | 对应包名 |
|
||||
|---|---|
|
||||
| radvd(IPv6 RA) | `radvd` |
|
||||
| dhcpcd(DHCPv6 客户端) | `dhcpcd-base` |
|
||||
| pppd(PPPoE 拨号) | `ppp` |
|
||||
| miniupnpd(UPnP) | `miniupnpd` |
|
||||
| conntrack(连接跟踪) | `conntrack` |
|
||||
|
||||
### 2.3 配置渲染:`render_text`(只渲染,不落盘)
|
||||
|
||||
按 `backend/render.py` 的 `RENDERERS` 真源逐个渲染,全部成功:
|
||||
|
||||
渲染方式是 `render(模块, {})` —— **空配置**。结果分两类:
|
||||
|
||||
| 模块 | 输出 |
|
||||
|---|---|
|
||||
| `chrony` | 8 行 / 170 字符 |
|
||||
| `dhcpv6` | 12 行 / 168 字符 |
|
||||
| `miniupnpd` | 16 行 / 425 字符 |
|
||||
| `nfs` | 5 行 / 99 字符 |
|
||||
| `nft_v4` | 44 行 / 1406 字符 |
|
||||
| `nft_v6` | 35 行 / 1257 字符 |
|
||||
| `radvd` | 22 行 / 517 字符 |
|
||||
| `samba` | 22 行 / 637 字符 |
|
||||
|
||||
| 模块 | 空配置下的响应 |
|
||||
|---|---|
|
||||
| `dnsmasq` | `LAN 接口不能为空` |
|
||||
| `network` | `WAN 网卡不能为空` |
|
||||
| `ppp` | `PPPoE 网卡不能为空` |
|
||||
|
||||
这三条是**必填项校验**在起作用(`render.py` 的 `ValidateError`),不是渲染失败 ——
|
||||
**与架构无关**,在 amd64 上喂同样的空配置会得到一模一样的三句话。
|
||||
判据里把这三句写死成期望值,为的是反过来抓「校验本身挂了」的情况:
|
||||
真出了 `ImportError` / `KeyError` 那类才算问题。
|
||||
|
||||
### 2.4 修复前后对比(本轮真正改掉的东西)
|
||||
|
||||
| 探测项 | 修复前在本机的输出 | 修复后 |
|
||||
|---|---|---|
|
||||
| 固件类型 | `传统 BIOS` ❌ | `设备树 (Device Tree)` ✅ |
|
||||
| 厂商 | 空 ❌ | `Octopus Planet` ✅ |
|
||||
| 固件枚举 | 无 | `fw_type: "dt"`、`is_dt: true` ✅ |
|
||||
|
||||
这是本轮**唯一一个在 arm64 上必然报错的功能点**,详见第 3 节。
|
||||
|
||||
### 2.5 WireGuard 四态:正好踩中「裸 `.ko`」那一档
|
||||
|
||||
```json
|
||||
{
|
||||
"state": "need_module",
|
||||
"mod_loaded": false,
|
||||
"mod_file": "/lib/modules/6.18.35-ophub/kernel/drivers/net/wireguard/wireguard.ko",
|
||||
"tool": true, "kernel": "6.18.35-ophub", "fixable": true
|
||||
}
|
||||
```
|
||||
|
||||
值得一记:`helper` 里查模块文件的三种扩展名(`.ko` / `.ko.xz` / `.ko.zst`),
|
||||
在这台 **ophub 内核**上命中的是**裸 `.ko`** —— Debian 官方内核通常是压缩过的
|
||||
`.ko.xz`。如果当初只按 Debian 默认形态写一种,这台机器就会被误判成
|
||||
「内核不支持 WireGuard」。这条不是本次改出来的,是被这台机器**佐证**了。
|
||||
|
||||
---
|
||||
|
||||
### 2.6 想在自己的板子上复验?
|
||||
|
||||
仓库里留了一份只读验收脚本 `devtools/verify-arm64.py`,上面 2.1–2.5 这几项
|
||||
都写在里面了,在自己的 arm64 机器上一跑就有结论:
|
||||
|
||||
```bash
|
||||
python3 devtools/verify-arm64.py # 源码树 = 仓库根目录
|
||||
python3 devtools/verify-arm64.py /opt/drouter # 也可以指向已安装的位置
|
||||
```
|
||||
|
||||
它只干「能不能跑」这件事:**不装包、不写配置、不起服务、不动网络**。
|
||||
退 0 就是全通过。没DMI 也没 EFI 的机器它会坚持要判成设备树;
|
||||
有 EFI 的机器(比如以 EFI 启动的树莓派)它按 EFI 那一档去核。
|
||||
|
||||
> ⚠️ 它**不**验「当路由器实际转发起来行不行」。那要点面板上的「应用」,
|
||||
> 会真的改网卡、起服务 —— 别在跑着业务的机器上试。
|
||||
|
||||
---
|
||||
|
||||
## 3. 代码改动清单
|
||||
|
||||
| 文件 | 改动 | 为什么必须改 |
|
||||
|---|---|---|
|
||||
| `backend/drouter-helper.py` | `_read_bios()` 增加**设备树分支**;新增 `_dt_read()`、`DT_MODEL_PATHS`、`DT_COMPAT_PATHS`;返回结构加 `fw_type` / `is_dt` / `compat`,无 DMI 时拿设备树板名顶替厂商 | ARM 板没有 EFI 也没有 DMI,原代码把它们一律落到 else 分支写成「传统 BIOS」,面板上是**错的**信息。判据顺序刻意排成 EFI → 设备树 → BIOS:树莓派这类既能 EFI 又能设备树启动的机器要优先认 EFI |
|
||||
| `web/i18n.js` | `bt.SYSINFO_NOTES` 增加 `设备树 (Device Tree)` 词条(`n` / `d` 两段英文) | 后端新增的展示串必须登记,否则英文界面直接漏出中文(项目 i18n 约定) |
|
||||
| `backend/drouter-update.py` | 新增 `_deb_arch()`、`build_assets()`;`ASSETS` 由 `build_assets()` 生成;离线包文件名模板改为 `drouter-{ver}-offline-<本机架构>.tar.gz`;`asset_list()` 增加 `arch_specific` 标志;`apply_update()` 对带架构后缀的附件**先拿 SHA256SUMS 清单核对**,没有就明确报错 | 原先模板写死 `-offline-amd64`,arm64 机器点「一键更新」会下载一份 x86 离线包,白传几十 MB 才在装包阶段失败。清单核对让它在**下载之前**就报清楚 |
|
||||
| `packaging/build-offline-bundle.sh` | 头部产物说明、塞进包里的 `README-离线安装.md` 两处的 `amd64` 改为 `$ARCH`;结尾打印架构提示 | 包名早就是 `$ARCH` 拼的,但说明文档写死 amd64 —— arm64 上打出来的包里带着一份「本机是 amd64」的说明,用户照着排查会走偏 |
|
||||
| `packaging/build-docker.sh` | 新增可选第 2 个参数 `[arch]`;与本机架构不同时加 `docker build --platform linux/<arch>`,镜像标签带 `-<arch>` 后缀;dist 附件渲染用 `$IMGTAG` | Docker 镜像是分架构的。不传第 2 参数时行为**与改动前逐字节一致**(原生构建、标签不带后缀),不影响现有发版流程 |
|
||||
|
||||
### 关于 Docker 的一条重要提醒
|
||||
|
||||
Release 里发布的 `drouter-<版本>-docker.tar` 仍然是 **amd64** 镜像。
|
||||
arm64 机器请走 **`deb` / 离线包** 这两条路;要用容器形态,需要在自己的 arm64
|
||||
机器上 `bash packaging/build-docker.sh <版本> arm64` 自行重建
|
||||
(Release 里没有 arm64 镜像附件)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 给自己手上的 arm64 发行版打包
|
||||
|
||||
### 4.1 本体 deb(架构无关,推荐)
|
||||
|
||||
直接在目标机上构建即可,无需交叉:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/liuzhuohua/drouter.git
|
||||
cd drouter
|
||||
bash packaging/build-deb.sh # → dist/drouter_<版本>_all.deb
|
||||
sudo apt install ./dist/drouter_<版本>_all.deb
|
||||
```
|
||||
|
||||
### 4.2 离线包(**必须在同架构的机器上构建**)
|
||||
|
||||
```bash
|
||||
bash packaging/build-offline-bundle.sh
|
||||
# → dist/drouter-<版本>-offline-<架构>.tar.gz
|
||||
```
|
||||
|
||||
架构取自 `dpkg --print-architecture`。⚠️ 包里几百个 `.deb` 全是某一 CPU 的二进制,
|
||||
**不能**拿 amd64 机器上打出来的包给 arm64 用。要在 arm64 上出包,
|
||||
就在 arm64 机器上跑这条命令(或用 qemu-user-static chroot)。
|
||||
|
||||
### 4.3 Docker 镜像
|
||||
|
||||
```bash
|
||||
bash packaging/build-docker.sh <版本> arm64 # → drouter:<版本>-arm64
|
||||
docker save drouter:<版本>-arm64 -o drouter-<版本>-docker-arm64.tar
|
||||
```
|
||||
|
||||
交叉构建依赖宿主机的 `binfmt_misc` / buildx qemu 支持;没有的话请直接到 ARM 主机上构建。
|
||||
|
||||
---
|
||||
|
||||
## 5. 已知限制
|
||||
|
||||
1. **端到端未验证**:本轮没有真的启用 Web 面板与常驻服务,也没有下发 nft / dnsmasq
|
||||
配置,因此「当路由器实际转发」这一层在 arm64 上尚无实测结论。
|
||||
2. **样本只有一台**:只验证了这个发行版这一个组合;别的 arm64 Debian 系
|
||||
(含树莓派内核、Debian 官方 aarch64 内核)没有验证。
|
||||
3. **依赖需自行补齐**:目标机上 radvd / ppp / miniupnpd / conntrack / dhcpcd-base
|
||||
未装(系统本身没带),面板「依赖自检」页可以逐项安装。
|
||||
4. **Release 的离线包与 Docker 镜像仍是 amd64**:arm64 用户要么用 `deb` 本体
|
||||
(`Architecture: all`,通用),要么按第 4 节自行重建。
|
||||
5. **x86 专属能力在 ARM 上天然不可用**:`dmidecode`(BIOS/主板/内存详情)、
|
||||
`lm-sensors` 的经典 x86 温度通道等。这些项在依赖自检里本来就是「可选」,
|
||||
缺失不影响主流程;本次把这类 x86 专属依赖**没有**改成必需。
|
||||
+35
-10
@@ -2,9 +2,17 @@
|
||||
# =============================================================================
|
||||
# 构建 drouter 的 Docker 镜像
|
||||
#
|
||||
# bash packaging/build-docker.sh [tag]
|
||||
# bash packaging/build-docker.sh [tag] [arch]
|
||||
#
|
||||
# 产物:drouter:<tag> (arch 省略 / 与本机架构相同时)
|
||||
# drouter:<tag>-<arch> (交叉构建时,标签带架构后缀)
|
||||
#
|
||||
# arm64 适配(2026-10-16):Debian 13 的 arm64 机器也能跑 drouter
|
||||
# (deb 是 Architecture: all,纯 Python),镜像自然也该能出 arm64 版本:
|
||||
# bash packaging/build-docker.sh 1.0.10 arm64
|
||||
# ⚠️ 交叉构建要靠 buildx/qemu 提供目标架构的执行能力,
|
||||
# 宿主机没有装 binfmt_misc 会卡在 RUN 那一步;这种机器上请直接跑到 ARM 主机上构建。
|
||||
#
|
||||
# 产物:drouter:<tag>
|
||||
# 注意:Dockerfile 里 COPY 的是相对路径,所以构建上下文必须是项目根目录。
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
@@ -14,9 +22,25 @@ HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
VF="$HERE/packaging/VERSION"
|
||||
[ -f "$VF" ] || { echo "缺少 $VF(版本号唯一真源)"; exit 1; }
|
||||
TAG="${1:-$(tr -d ' \t\r\n' < "$VF")}"
|
||||
# 目标架构。省略时按本机 dpkg 架构处理 —— 那等于「原生构建」:
|
||||
# 不加 --platform,标签也不带后缀,产物与改动前**逐字节一致**。
|
||||
DPKG_NATIVE="$(dpkg --print-architecture 2>/dev/null || echo amd64)"
|
||||
ARCH="${2:-$DPKG_NATIVE}"
|
||||
|
||||
command -v docker >/dev/null 2>&1 || { echo "需要 docker"; exit 1; }
|
||||
|
||||
# 交叉构建(目标架构 ≠ 本机架构)才加 --platform 与标签后缀。
|
||||
# 原生构建硬塞 --platform 反而会拖慢(docker 可能跳过本地缓存层)。
|
||||
if [ "$ARCH" = "$DPKG_NATIVE" ]; then
|
||||
PLATFORM_ARG=()
|
||||
IMGTAG="$TAG"
|
||||
echo "== 目标架构:$ARCH(与本机一致,原生构建)=="
|
||||
else
|
||||
PLATFORM_ARG=(--platform "linux/$ARCH")
|
||||
IMGTAG="$TAG-$ARCH"
|
||||
echo "== 目标架构:$ARCH(交叉构建,标签后缀 -$ARCH)=="
|
||||
fi
|
||||
|
||||
echo "== 1/4 构建前自测(避免把坏代码打进镜像)=="
|
||||
python3 - <<'PY'
|
||||
import ast, glob
|
||||
@@ -30,12 +54,13 @@ raise SystemExit(1 if bad else 0)
|
||||
PY
|
||||
command -v node >/dev/null 2>&1 && node --check web/app.js && echo " OK: web/app.js"
|
||||
|
||||
echo "== 2/4 构建镜像 drouter:$TAG =="
|
||||
docker build -f "$HERE/packaging/docker/Dockerfile" -t "drouter:$TAG" "$HERE"
|
||||
echo "== 2/4 构建镜像 drouter:$IMGTAG =="
|
||||
docker build "${PLATFORM_ARG[@]}" -f "$HERE/packaging/docker/Dockerfile" \
|
||||
-t "drouter:$IMGTAG" "$HERE"
|
||||
|
||||
# 同时打一个带用户名的别名,方便日后直推 Docker Hub / 私有仓库。
|
||||
# 两个标签指向同一个 image id,不额外占空间。
|
||||
docker tag "drouter:$TAG" "liuzhuohua/drouter:$TAG"
|
||||
docker tag "drouter:$IMGTAG" "liuzhuohua/drouter:$IMGTAG"
|
||||
|
||||
echo "== 3/4 渲染 Release 附件(docker-compose.yml / DOCKER-GUIDE.md)=="
|
||||
# 这两个文件是**模板**,里面写的是 __VERSION__ 占位符,不是具体版本号。
|
||||
@@ -52,7 +77,7 @@ for f in docker-compose.yml DOCKER-GUIDE.md; do
|
||||
# 顺序不能反 —— 先替换的话,标记块里的 __VERSION__ 也会被换掉,
|
||||
# 删的时候连同「这是模板」的语义一起留在了附件里。
|
||||
sed '/TEMPLATE-NOTE:BEGIN/,/TEMPLATE-NOTE:END/d' "$src" \
|
||||
| sed "s/__VERSION__/$TAG/g" > "$HERE/dist/$f"
|
||||
| sed "s/__VERSION__/$IMGTAG/g" > "$HERE/dist/$f"
|
||||
# 渲染完必须一个占位符都不剩 —— 剩下就说明模板里有漏网的写法
|
||||
if grep -q '__VERSION__' "$HERE/dist/$f"; then
|
||||
echo "✘ $f 渲染后仍含 __VERSION__,模板有问题"; exit 1
|
||||
@@ -61,7 +86,7 @@ for f in docker-compose.yml DOCKER-GUIDE.md; do
|
||||
if grep -q 'TEMPLATE-NOTE' "$HERE/dist/$f"; then
|
||||
echo "✘ $f 渲染后仍含 TEMPLATE-NOTE 标记"; exit 1
|
||||
fi
|
||||
echo " dist/$f ($TAG)"
|
||||
echo " dist/$f ($IMGTAG)"
|
||||
done
|
||||
|
||||
echo "== 4/4 产物 =="
|
||||
@@ -82,11 +107,11 @@ cat <<RUN
|
||||
-v drouter-data:/opt/drouter/data \\
|
||||
-v drouter-snapshots:/opt/drouter/snapshots \\
|
||||
-v drouter-log:/var/log/drouter \\
|
||||
drouter:$TAG
|
||||
drouter:$IMGTAG
|
||||
|
||||
导出成 tar(用于分发):
|
||||
docker save drouter:$TAG liuzhuohua/drouter:$TAG \\
|
||||
-o dist/drouter-$TAG-docker.tar
|
||||
docker save drouter:$IMGTAG liuzhuohua/drouter:$IMGTAG \\
|
||||
-o dist/drouter-$IMGTAG-docker.tar
|
||||
|
||||
说明:
|
||||
--network host 让容器直接看到宿主机网卡(路由类功能的前提)
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
#
|
||||
# bash packaging/build-offline-bundle.sh [版本号] [输出目录]
|
||||
#
|
||||
# 产物:dist/drouter-<版本>-offline-amd64.tar.gz
|
||||
# 产物:dist/drouter-<版本>-offline-<架构>.tar.gz
|
||||
# 架构取自 `dpkg --print-architecture`(amd64 / arm64 / armhf …)。
|
||||
# ⚠️ 离线包是**分架构**的:里头几百个 .deb 全是某一 CPU 的二进制,
|
||||
# 所以必须在与目标机同架构的机器上构建(或用 chroot/qemu 交叉构建),
|
||||
# 不能拿 amd64 打出来的包往 arm64 上装。
|
||||
#
|
||||
# 为什么要有这个(而不是让用户自己拼 deb + 依赖):
|
||||
# .deb 的 Depends 只声明依赖,真正把依赖装上去是 apt 的活。目标机没外网时
|
||||
@@ -14,7 +18,7 @@
|
||||
# 拷过去解包、跑一条命令就完事。
|
||||
#
|
||||
# 包内结构:
|
||||
# drouter-<版本>-offline-amd64/
|
||||
# drouter-<版本>-offline-<架构>/
|
||||
# ├── install.sh 一条命令装完
|
||||
# ├── README-离线安装.md 给不熟悉的人看的步骤说明
|
||||
# ├── SHA256SUMS 每个 deb 的校验和
|
||||
@@ -85,7 +89,11 @@ chmod +x "$STAGE/install.sh"
|
||||
cat > "$STAGE/README-离线安装.md" <<EOF
|
||||
# Drouter ${VERSION} 离线安装包
|
||||
|
||||
**适用场景**:目标机(Debian 13 amd64)**没有外网**,想一次装完。
|
||||
**适用场景**:目标机(Debian 13 / ${ARCH})**没有外网**,想一次装完。
|
||||
|
||||
> ⚠️ **架构必须匹配**:这个包里的依赖是按 **${ARCH}** 下载的,
|
||||
> 只能装在同架构的机器上(如 arm64 主机要打 arm64 的包)。
|
||||
> 想知道本机架构:\`dpkg --print-architecture\`。
|
||||
|
||||
## 三步装完
|
||||
|
||||
@@ -126,8 +134,8 @@ ls debs/*.deb | wc -l # 应该有两百多个
|
||||
## 如果装不上
|
||||
|
||||
1. **先看硬件够不够**:\`free -m\` / \`df -h /\`。内存 < 1G 或根分区 < 3G 会失败。
|
||||
2. **确认系统版本**:\`cat /etc/os-release\`,必须是 Debian 13 (trixie) amd64。
|
||||
别的版本依赖版本对不上。
|
||||
2. **确认系统版本与架构**:\`cat /etc/os-release\` 要是 Debian 13 (trixie),
|
||||
\`dpkg --print-architecture\` 要是 **${ARCH}**。别的版本/架构依赖对不上。
|
||||
3. **看具体报错**:\`bash install.sh 2>&1 | tee /tmp/install.log\`,
|
||||
然后按 log 里的第一条 \`E:\` 定位。
|
||||
|
||||
@@ -174,4 +182,7 @@ echo " 大小:$(du -h "$BUNDLE" | cut -f1)"
|
||||
echo ""
|
||||
echo " 上传到 GitHub Release 时,只传这一个文件即可 ——"
|
||||
echo " 目标机解包后跑 install.sh 就能在完全无外网的环境装好。"
|
||||
echo ""
|
||||
echo " ⚠️ 这是 $ARCH 架构的包。其他架构请在对应架构的机器上重跑本脚本,"
|
||||
echo " 包名里的架构后缀会自动变(别的 CPU 装了这一份会依赖不满足)。"
|
||||
echo "======================================================================"
|
||||
@@ -4304,6 +4304,10 @@
|
||||
'QEMU / KVM 虚拟机': { 'n': 'QEMU / KVM virtual machine' },
|
||||
'传统 BIOS': { 'n': 'Legacy BIOS', 'd': 'Legacy BIOS' },
|
||||
'UEFI': { 'n': 'UEFI', 'd': 'UEFI' },
|
||||
/* arm64 适配:ARM 板没有 BIOS,走 U-Boot + 设备树启动 */
|
||||
'设备树 (Device Tree)':
|
||||
{ 'n': 'Device Tree',
|
||||
'd': 'Device Tree / U-Boot boot. ARM and RISC-V boards have no BIOS: the bootloader hands the kernel a device tree describing the hardware.' },
|
||||
'未读取到温度传感器,仅显示进程数':
|
||||
{ 'd': 'No temperature sensor found; only the process count is shown' },
|
||||
'未读取到温度': { 'd': 'No temperature reading' },
|
||||
|
||||
Reference in new issue
Block a user