版本号 1.0.7 → 1.0.8(19 个文件 55 处,逐条过;历史陈述不动)。
顺带补上1.0.7 漏掉的更新日志小节(那一版只改了版本号)。
## 本轮修的缺陷
**先说结论:Debian 13 虚拟机上跑 drouter,KVM 不是容器,
内核里一直有 WireGuard —— 是面板自己判错了。**
原 _vpn_installed() 只看两件事:/sys/module/wireguard 在不在(模块当前
已加载)、/usr/bin/wg 在不在(工具已安装)。**从没查过 /lib/modules 下
有没有模块文件** → 「模块文件在但没加载」被压进「内核真没这个模块」。
而 Debian 走 udev 按需加载,/etc/modules 与 /etc/modules-load.d 默认都
不写 wireguard,模块在磁盘上但没进内存。本机实测:内核 6.12.107+deb13-amd64,
wireguard.ko.xz 在,未加载;wireguard-tools 也没装。
前端还配了一句写死的文案把它坐实:「通常说明跑在精简容器里」——
而这台是 PVE 上的 KVM 虚拟机。排查方向被这句话整个带偏。
修法:
- 判据扩到四态(ready / need_module / need_tool / unsupported),
每态给可执行的下一步。unsupported 明确说装不了且不给一键修复。
- 模块文件名覆盖 .ko / .ko.xz / .ko.zst(Debian 默认压成 .ko.xz,
只认 .ko 会把正常机器也判成 unsupported)
- 新增「一键修复」:modprobe → 写 /etc/modules-load.d/ 开机自启 →
缺工具则 apt-get install,确认弹窗写清三步并告知不动网络
- apply 改为先自愈再报错,不再直接弹「内核不支持」把人挡在门外
- 老后端兜底 unknown 而非把旧的 no 猜成 unsupported(猜错=复活本 bug)
- 页面新增「运行环境」行;改掉「不需要装任何第三方软件」这句误导
## 测试基建:这轮抓出 3 条恒绿断言 + 1 处死分支
反向验证(把 bug 注入回去看它变红)逮到三条永远不会红的断言:
把探测路径改成 /nonexistent 仍全绿(docstring 里就写着那句话);
把 'env': _vpn_env() 注释掉仍全绿(我写的是 #注释,正则照样匹配);
把 if (!confirm( 改成 if (false && confirm( 仍全绿(提示文案里有那句话)。
根因:习惯在注释里引用被删掉的旧代码来解释 bug 来历,这些字面量让
「这段代码还在吗」类的判据永远成立。修法:t-107 统一走 py_code_only() /
js_code_only()(先按ast 判语义剥 docstring,再剥行注释),16 个注入全部被抓住。
死分支:用小锚点 Edit 只替换 if (d.installed === 'no') { 这一行时,
旧 body 与它的 } else if (!d.has_systemd) { 原样留在原地,拼出一个
「没有 systemd → 报内核不支持」的假分支——语法合法、静态检查也过。
防线:断言 } else if ( 的数量恰好等于分支数。
另有 4 处判据写错导致假红,共同形态是「脚本假设了一个修好之后就不成立
的前提」——比如「模块必须未加载」这条,fix 一跑完就假红。
## 验证
- t-107:126 → 167 项
- 反向验证 16/16 被抓住
- 新增 devtools/check-vpn-render.js 36 项(挂进 sync.sh 预检链)
- 新增 _dev/live-108-wg.py 真机验收 43 项;真机 _vpn_env 从 need_module
修到 ready,wg 装上(v1.0.20210914),modules-load.d 已写入
- t-*.py 51 个全绿 / 静态检查器 39 个全绿 / 前端四查全绿 / md 0 死链
- 容器冒烟新增第 14 节:镜像里真跑 _vpn_env() 验四态判定
(不 grep 字面量——注释里也会写这些词)
- deb 内 helper 与 app.js sha256 与本地逐字节一致
325 lines
17 KiB
Bash
325 lines
17 KiB
Bash
#!/bin/bash
|
||
# =============================================================================
|
||
# 离线依赖打包:把 drouter 的全部依赖(含传递依赖)预下载成一个自洽目录,
|
||
# 供**没有外网**的目标机离线安装。
|
||
#
|
||
# bash scripts/make-offline-deps.sh [输出目录]
|
||
#
|
||
# 产出目录结构:
|
||
# debs/ 所有依赖的 .deb(含 libc6 这类最底层传递依赖)
|
||
# Packages.gz 由 dpkg-scanpackages 生成的本地索引(没有就退化为无索引模式)
|
||
# install.sh 离线安装脚本(先装依赖,再装 drouter 本体)
|
||
#
|
||
# 为什么不能只 apt-get download 一串包名:
|
||
# `apt-get download a b c` 只下载**指定**的那些包,不拉传递依赖。
|
||
# 目标机上 dpkg -i *.deb 会因为 libxxx 没装而大面积失败。必须先把
|
||
# apt-cache depends --recurse 展开成闭包,再整体下载。
|
||
#
|
||
# 为什么用 apt-cache depends 而不是 apt-rdepends:
|
||
# apt-rdepends 不是 Debian 基础系统自带的(要额外装),而 apt-cache 一定有。
|
||
# `apt-cache depends --recurse --no-recommends --no-suggests ...` 的输出
|
||
# 已经能拿到完整闭包,不必引入外部依赖。
|
||
#
|
||
# 用法(目标机):
|
||
# 把整个目录拷过去,执行
|
||
# sudo bash install.sh drouter_1.0.8_all.deb
|
||
# =============================================================================
|
||
set -euo pipefail
|
||
|
||
HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
||
OUT="${1:-$HERE/dist/offline-deps}"
|
||
|
||
# 与 backend/drouter-helper.py 的 DEPS「必需项 + 常用可选项」保持一致。
|
||
# 手动同步点:改了 DEPS 就要同步这里,_dev/t-deps.py 会核对关键包是否都在。
|
||
# 注意:samba / nfs-kernel-server / docker.io / cups / snmpd / smartmontools / xfce4
|
||
# 会拉起常驻服务或体积巨大,不进离线包,用户在界面上按需安装即可。
|
||
DEPS="python3 nftables dnsmasq radvd dhcpcd-base ppp pppoe pppoeconf miniupnpd chrony
|
||
ethtool iperf3 mtr-tiny traceroute dnsutils conntrack bridge-utils iproute2
|
||
sqlite3 logrotate rsyslog curl wget procps sudo openssl ipset kmod systemd
|
||
adduser ca-certificates tcpdump jq htop lm-sensors dmidecode
|
||
e2fsprogs exfatprogs ntfs-3g dosfstools xfsprogs btrfs-progs f2fs-tools
|
||
util-linux usbutils tar coreutils hostname udev
|
||
etherwake zip unzip p7zip-full iputils-ping vim-tiny wireguard-tools"
|
||
|
||
command -v apt-get >/dev/null 2>&1 || { echo "需要 apt-get(Debian/Ubuntu)"; exit 1; }
|
||
|
||
echo "== 1/4 准备目录 =="
|
||
rm -rf "$OUT"
|
||
mkdir -p "$OUT/debs"
|
||
cd "$OUT/debs"
|
||
|
||
echo "== 2/4 展开传递依赖闭包 =="
|
||
# --no-recommends / --no-suggests:把可选依赖排除掉,否则 4GB 机器光依赖就装爆。
|
||
# 过滤规则:只保留顶格的包名行(依赖关系行前面有空格和「依赖:」等前缀)。
|
||
#
|
||
# ⚠️ 这里有一个必须显式补的坑:`Essential: yes` 的包**不会**出现在
|
||
# apt-cache depends --recurse 的输出里 —— apt 认定「任何 Debian 系统都必有它们」,
|
||
# 所以从不把它们列为别人 的依赖。实测缺失的有:dash / diffutils / libc-bin /
|
||
# grep / findutils / base-files / base-passwd / dpkg / coreutils / sed / tar …
|
||
# 在正常的 Debian 上装没问题(本机已有了),但如果目标机是**刚 debootstrap
|
||
# 出来的极简根**或换了个发行版,缺了它们的表现是:
|
||
# dpkg: error: 3 expected programs not found in PATH or not executable
|
||
# (缺 sh / diff / ldconfig —— 分别来自 dash / diffutils / libc-bin)
|
||
# 而且这个错在装 drouter 本体时才暴露,排查起来很费劲。所以显式补一份。
|
||
ESSENTIAL="${DROUTER_ESSENTIAL:-base-files base-passwd bash coreutils dash diffutils
|
||
dpkg grep gzip findutils hostname libc-bin libc6 libgcc-s1 libselinux1 libpcre2-8-0
|
||
libacl1 libattr1 libcap2 libaudit1 libaudit-common sed tar zlib1g liblzma5 libbz2-1.0
|
||
xz-utils libtinfo6 libreadline8 libmount1 libblkid1 libsmartcols1 libuuid1
|
||
libsystemd0 libudev1 libzstd1 libcap-ng0 libcrypt1 libgmp10 libnettle8 libhogweed6
|
||
libidn2-0 libunistring5 libtasn1-6 libp11-kit0 libffi8 libgnutls30 libgpg-error0
|
||
libgcrypt20 libseccomp2 libstdc++6 libmd0 libedit2 libncursesw6 libfdisk1
|
||
libapparmor1 libxxhash0 libcom-err2 libext2fs2 libjansson4 libargon2-1
|
||
debianutils debconf init-system-helpers mount util-linux passwd login.defs
|
||
sysvinit-utils mawk perl-base readline-common sensible-utils}"
|
||
|
||
# 再补一层「基线库」。踩过的坑:apt 自己的依赖链(libapt-pkg / liblz4-1 /
|
||
# libgnutls30 / libnettle8 / libstdc++6 …)**不会**被 drouter 的依赖闭包带进来 ——
|
||
# 因为 apt 在任何 Debian 上都已装好,它的依赖也被视为已满足。
|
||
# 后果是:在**没有 apt 的干净根 / 换发行版**里跑 install.sh 时,
|
||
# 「建立本地仓库索引」这一步会直接报
|
||
# apt-get: error while loading shared libraries: liblz4.so.1
|
||
# 然后整条安装链路退化成 dpkg 乱装。补上这几个库成本极小(几百 KB),
|
||
# 换来的是「任何 Debian 用户态环境都能装」。
|
||
BASELIBS="${DROUTER_BASELIBS:-liblz4-1 libapt-pkg7.0 libgnutls30 libnettle8
|
||
libhogweed6 libstdc++6 libgmp10 libgnutls30 libunistring5 libidn2-0 libp11-kit0
|
||
libffi8 libtasn1-6 libssl3t64 zlib1g liblzma5 libzstd1 libbz2-1.0 libpcre2-8-0
|
||
libselinux1 libmount1 libblkid1 libuuid1 libcap2 libcap-ng0 libaudit1
|
||
libsystemd0 libudev1 libseccomp2 libgpg-error0 libgcrypt20}"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 闭包求解:用 apt 自己算,不要用 `apt-cache depends --recurse` 做文本展开。
|
||
#
|
||
# 踩过的坑(很隐蔽,装到最后一步才炸):
|
||
# `apt-cache depends --recurse` 会把**虚拟包的每一个提供者**都展开成一行。
|
||
# systemd / systemd-standalone-sysusers / opensysusers 三者都 Provides
|
||
# `systemd-sysusers`,同时又互相 Conflicts。文本展开会把三家**全**收进来,
|
||
# 下载没问题;但装上时 apt 解冲突发现无解:
|
||
# E: Unmet dependencies. Try 'apt --fix-broken install'
|
||
# systemd : 预依赖 libsystemd-shared 但是它将不会被安装
|
||
# 看起来像「离线包缺包」,实际是一堆互相冲突的替代品被硬凑在一起。
|
||
#
|
||
# `apt-get install --print-uris -y <目标>` 走的是**真正的依赖求解器**:
|
||
# 它会按 Conflicts/Provides 选一套自洽的方案,只给出确实要装的包。
|
||
# 这才是「离线包」该用的闭包来源。
|
||
# ---------------------------------------------------------------------------
|
||
CLOSURE_FILE="$OUT/.closure"
|
||
: > "$CLOSURE_FILE"
|
||
|
||
# ⚠️ 关键:求解必须在一个**空的 dpkg status** 上做,不能直接用宿主机的状态。
|
||
# 踩过的坑:在装了一堆东西的开发机上求解,apt 会认为「这些依赖已满足」而不列出来
|
||
# (libicu76 / liburcu8t64 / python3-dbus 就是这么漏的 —— 宿主机上有,
|
||
# 于是不进闭包;到了干净目标机上就变成
|
||
# `Depends: libicu76 but it is not installable`,整包装不上)。
|
||
# 所以先造一个只有「空 status」的临时 apt 根,让 apt 以为什么都没装。
|
||
EMPTYROOT="$OUT/.solveroot"
|
||
rm -rf "$EMPTYROOT"
|
||
mkdir -p "$EMPTYROOT/var/lib/dpkg" "$EMPTYROOT/var/lib/apt/lists/partial" \
|
||
"$EMPTYROOT/var/cache/apt/archives/partial" "$EMPTYROOT/etc/apt/apt.conf.d" \
|
||
"$EMPTYROOT/etc/apt/preferences.d" "$EMPTYROOT/etc/apt/sources.list.d" \
|
||
"$EMPTYROOT/etc/apt/trusted.gpg.d" "$EMPTYROOT/usr/share/keyrings" \
|
||
"$EMPTYROOT/var/lib/dpkg/info" "$EMPTYROOT/var/lib/dpkg/updates"
|
||
|
||
# 签名校验要用到 keyring,否则 apt 会拒绝所有源:
|
||
# E: 仓库 “…” 没有数字签名。 (trusted.gpg.d 不是目录)
|
||
# 只借「公钥」目录即可,不复制 status —— status 必须是空的,那是本步的目的。
|
||
cp -f /etc/apt/trusted.gpg.d/* "$EMPTYROOT/etc/apt/trusted.gpg.d/" 2>/dev/null || true
|
||
cp -f /usr/share/keyrings/* "$EMPTYROOT/usr/share/keyrings/" 2>/dev/null || true
|
||
# apt 自身仍从宿主机运行,签名校验走宿主 keyring 也通常可行,这里只是兜底。
|
||
: > "$EMPTYROOT/var/lib/dpkg/status"
|
||
touch "$EMPTYROOT/etc/apt/apt.conf.d/.keep"
|
||
|
||
# 复用宿主机的源列表(只是用来读 Packages 索引,不会真的下载)
|
||
[ -f /etc/apt/sources.list ] && cp -f /etc/apt/sources.list "$EMPTYROOT/etc/apt/" 2>/dev/null || true
|
||
mkdir -p "$EMPTYROOT/etc/apt/sources.list.d"
|
||
cp -f /etc/apt/sources.list.d/*.list "$EMPTYROOT/etc/apt/sources.list.d/" 2>/dev/null || true
|
||
cp -f /etc/apt/sources.list.d/*.sources "$EMPTYROOT/etc/apt/sources.list.d/" 2>/dev/null || true
|
||
|
||
SOLVER_OPTS=(
|
||
-o "Dir=$EMPTYROOT/"
|
||
-o "Dir::State=$EMPTYROOT/var/lib/apt"
|
||
-o "Dir::State::status=$EMPTYROOT/var/lib/dpkg/status"
|
||
-o "Dir::Cache=$EMPTYROOT/var/cache/apt"
|
||
-o "Dir::Etc::sourcelist=$EMPTYROOT/etc/apt/sources.list"
|
||
)
|
||
echo " 在空 status 上求解依赖(模拟全新目标机)…"
|
||
# update 失败不致命:说明宿主机源列表不可用,但本地 Packages 索引可能还在,
|
||
# 后面的 -s 求解会自己报出真正的问题,这里不吞错也不中止。
|
||
apt-get "${SOLVER_OPTS[@]}" update -qq >/dev/null 2>&1 || \
|
||
echo " (索引刷新失败,继续用已有索引求解)"
|
||
|
||
# 求解:只列 Inst 行的包名。--no-install-recommends 与发布口径一致。
|
||
if apt-get "${SOLVER_OPTS[@]}" -s --no-install-recommends -y install $DEPS \
|
||
>"$OUT/.sim.txt" 2>"$OUT/.sim.err"; then
|
||
sed -n 's/^Inst \([^ ]*\) .*/\1/p' "$OUT/.sim.txt" >> "$CLOSURE_FILE"
|
||
echo " 依赖求解成功(apt 求解器 / 空 status)"
|
||
else
|
||
echo " ⚠ 空 status 求解失败,回退到宿主状态求解"
|
||
if apt-get -s --no-install-recommends -y install $DEPS >"$OUT/.sim.txt" 2>/dev/null; then
|
||
sed -n 's/^Inst \([^ ]*\) .*/\1/p' "$OUT/.sim.txt" >> "$CLOSURE_FILE"
|
||
echo " 已用宿主状态求解"
|
||
else
|
||
echo " ⚠ 仍失败,回退到文本展开(可能含冲突替代品)"
|
||
apt-cache depends --recurse --no-recommends --no-suggests \
|
||
--no-conflicts --no-breaks --no-replaces --no-enhances \
|
||
$DEPS 2>/dev/null \
|
||
| grep -E '^[a-zA-Z0-9][a-zA-Z0-9.+_-]*$' >> "$CLOSURE_FILE"
|
||
fi
|
||
fi
|
||
rm -f "$OUT/.sim.txt" "$OUT/.sim.err"
|
||
|
||
ALL=$(cat "$CLOSURE_FILE" | sort -u)
|
||
ALL="$ALL $ESSENTIAL $BASELIBS"
|
||
ALL=$(echo $ALL | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ')
|
||
|
||
# 显式排掉已知的「冲突替代品」:它们是 systemd-sysusers 的多余提供者,
|
||
# 只要 systemd 在,就不能同时出现。留着必然导致上面那个 Unmet dependencies。
|
||
for drop in systemd-standalone-sysusers opensysusers systemd-standalone-tmpfiles; do
|
||
if [ -n "$(ls "$OUT"/debs/${drop}_*.deb 2>/dev/null)" ]; then
|
||
echo " (移除冲突替代品:$drop)"
|
||
rm -f "$OUT"/debs/${drop}_*.deb
|
||
fi
|
||
ALL=$(echo "$ALL" | tr ' ' '\n' | grep -vx "$drop" | tr '\n' ' ')
|
||
done
|
||
|
||
# 过滤掉纯粹是虚拟/元包而下载不到实体的名字
|
||
REAL=""
|
||
for p in $ALL; do
|
||
if apt-get download --print-uris "$p" >/dev/null 2>&1; then
|
||
REAL="$REAL $p"
|
||
fi
|
||
done
|
||
ALL="$REAL"
|
||
echo " 闭包内可下载的包:$(echo $ALL | wc -w) 个"
|
||
|
||
echo "== 3/4 下载(只下不装)=="
|
||
# 分批下载:命令行过长会在老 shell 上踩 ARG_MAX,且失败时更容易定位是哪个包。
|
||
# 陷阱:apt-get download 的落地目录取自**进程的当前目录**,不是 -o 参数;
|
||
# 早期写法用 `xargs -a partfile apt-get download`——xargs 在部分实现下会以
|
||
# 自己的 CWD 起子进程,结果 apt 把 .deb 往 / 根目录写,全部「权限不够」。
|
||
# 改成显式 for 循环 + 显式 cd,且每批都重新确认 CWD,绝不让 xargs 起进程。
|
||
DEBDIR="$OUT/debs"
|
||
echo "$ALL" | tr ' ' '\n' | grep -v '^$' > "$OUT/.allpkgs"
|
||
BATCH=40
|
||
total=$(wc -l < "$OUT/.allpkgs")
|
||
i=0
|
||
while [ "$i" -lt "$total" ]; do
|
||
batch=$(sed -n "$((i+1)),$((i+BATCH))p" "$OUT/.allpkgs")
|
||
# shellcheck disable=SC2086
|
||
( cd "$DEBDIR" && apt-get download $batch 2>&1 | grep -E '^E:' | head -3 ) || true
|
||
i=$((i + BATCH))
|
||
done
|
||
rm -f "$OUT/.allpkgs"
|
||
N=$(ls -1 "$DEBDIR"/*.deb 2>/dev/null | wc -l)
|
||
echo " 已下载 $N 个 .deb(约 $(du -sh "$DEBDIR" | cut -f1))"
|
||
[ "$N" -gt 0 ] || { echo "✘ 一个都没下到,检查网络/源"; exit 1; }
|
||
|
||
echo "== 4/4 生成本地索引与安装脚本 =="
|
||
# 有 dpkg-scanpackages(dpkg-dev 提供)就生成真正的本地仓库索引,
|
||
# 这样目标机可以用 `apt-get install -o Dir::Etc::sourcelist=... drouter` 一次装齐。
|
||
cd "$OUT"
|
||
if command -v dpkg-scanpackages >/dev/null 2>&1; then
|
||
# ⚠️ 索引必须落在 **debs/** 里,和 install.sh 里那条
|
||
# `deb [trusted=yes] file:$HERE/debs ./` 对上。
|
||
# 踩过的坑:原来生成到 $OUT/Packages.gz,而 apt 去 $OUT/debs/ 找 Packages,
|
||
# 结果 `E: Failed to fetch file:.../debs/./Packages File not found`,
|
||
# 整条「本地仓库」链路静默退化成 dpkg -i 乱装。
|
||
( cd debs && dpkg-scanpackages -m . /dev/null 2>/dev/null > Packages )
|
||
# 同时给一份 gzip 版:apt 优先用 Packages.gz,没有才用 Packages。
|
||
# 两个都放,避免不同 apt 版本偏好不一致又踩一次。
|
||
[ -s debs/Packages ] && gzip -9c debs/Packages > debs/Packages.gz
|
||
echo " 已生成 debs/Packages(+.gz)(本地仓库索引)"
|
||
HAS_IDX=1
|
||
else
|
||
echo " 未装 dpkg-dev,跳过 Packages,改用 dpkg -i 逐个安装"
|
||
HAS_IDX=0
|
||
fi
|
||
|
||
cat > "$OUT/install.sh" <<'EOS'
|
||
#!/bin/bash
|
||
# =============================================================================
|
||
# drouter 离线安装脚本
|
||
#
|
||
# sudo bash install.sh [drouter_x.y.z_all.deb]
|
||
#
|
||
# 不传参数则在当前目录里自动找 drouter_*.deb。
|
||
# 做三件事:① 装齐全部依赖;② 装 drouter 本体;③ 打印后续步骤。
|
||
# =============================================================================
|
||
set -e
|
||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||
PKG="${1:-}"
|
||
|
||
yesno() { printf ' → %s' "$1"; }
|
||
|
||
if [ "$(id -u)" -ne 0 ]; then
|
||
# 先切到 sudo 再回来执行自己,免得用户忘了加 sudo 跑到一半才失败
|
||
exec sudo -E bash "$0" "$@"
|
||
fi
|
||
|
||
# 1) 找本体
|
||
if [ -z "$PKG" ]; then
|
||
PKG=$(ls -1 "$HERE"/drouter_*_all.deb "$HERE"/*/drouter_*_all.deb 2>/dev/null | head -1 || true)
|
||
fi
|
||
[ -n "$PKG" ] && [ -f "$PKG" ] || PKG="$HERE/$PKG"
|
||
[ -f "$PKG" ] || { echo "✘ 找不到 drouter 安装包(用法:bash install.sh drouter_1.0.8_all.deb)"; exit 1; }
|
||
echo "安装包:$PKG"
|
||
|
||
echo ""
|
||
echo "== 1/3 安装离线依赖 =="
|
||
# 优先走本地仓库模式:让 apt 自己算依赖顺序,比 dpkg -i 一通乱装可靠得多。
|
||
if [ -f "$HERE/Packages.gz" ] && command -v apt-get >/dev/null 2>&1; then
|
||
echo " 使用本地仓库索引安装(推荐)"
|
||
tmp_list=$(mktemp)
|
||
trap 'rm -f "$tmp_list"' EXIT
|
||
cat > "$tmp_list" <<EOF
|
||
deb [trusted=yes] file:$HERE/debs ./
|
||
EOF
|
||
apt-get -o Dir::Etc::sourcelist="$tmp_list" \
|
||
-o Dir::Etc::sourceparts=/dev/null \
|
||
-o APT::Get::List-Cleanup=0 \
|
||
-o Acquire::Languages=none \
|
||
update -qq 2>&1 | tail -2 || true
|
||
# 先把闭包里的包整体装掉(--no-install-recommends 避免又去网上拉推荐包)
|
||
DEBLIST=$(ls -1 "$HERE"/debs/*.deb 2>/dev/null | sed 's|.*/||; s|_.*||' | sort -u)
|
||
# shellcheck disable=SC2086
|
||
apt-get -o Dir::Etc::sourcelist="$tmp_list" \
|
||
-o Dir::Etc::sourceparts=/dev/null \
|
||
-o APT::Get::List-Cleanup=0 \
|
||
-o Acquire::Languages=none \
|
||
--no-install-recommends -y install $DEBLIST 2>&1 | tail -6 || true
|
||
else
|
||
echo " 无本地索引,逐个 dpkg -i(可能报依赖未满足,最后会用 -f 补齐)"
|
||
dpkg -i "$HERE"/debs/*.deb 2>&1 | tail -4 || true
|
||
apt-get --no-download -y -f install 2>&1 | tail -4 || true
|
||
fi
|
||
|
||
echo ""
|
||
echo "== 2/3 安装 drouter 本体 =="
|
||
dpkg -i "$PKG" 2>&1 | tail -6 || apt-get --no-download -y -f install 2>&1 | tail -6
|
||
|
||
echo ""
|
||
echo "== 3/3 完成 =="
|
||
echo " · 查看状态:sudo drouter-ctl status"
|
||
echo " · 打开面板:https://<本机IP>:8443/ (默认账号 admin / admin123,请尽快改掉)"
|
||
echo " · 首次部署建议在界面里先走「新手向导」"
|
||
EOS
|
||
chmod +x "$OUT/install.sh"
|
||
|
||
# 把本体也放进离线目录,方便整包分发
|
||
if ls "$HERE"/dist/drouter_*.deb >/dev/null 2>&1; then
|
||
cp -f "$HERE"/dist/drouter_*.deb "$OUT/" 2>/dev/null || true
|
||
fi
|
||
|
||
# 清掉构建期的中间产物。踩过的坑:`.closure`(包名列表)和 `.solveroot/`
|
||
# (空 status 求解用的临时 apt 根)会被原样打进 tar 包,用户解包后会看到两个
|
||
# 莫名其妙的隐藏目录 —— 既占地方又让人困惑「这是不是漏了什么」。
|
||
rm -rf "$OUT/.solveroot" "$OUT/.closure" "$OUT/.allpkgs" \
|
||
"$OUT/.sim.txt" "$OUT/.sim.err" 2>/dev/null || true
|
||
|
||
echo ""
|
||
echo "======================================================================"
|
||
echo "离线包已生成:$OUT"
|
||
echo " 体积:$(du -sh "$OUT" | cut -f1) (依赖 $(ls -1 "$OUT"/debs/*.deb 2>/dev/null | wc -l) 个 deb)"
|
||
echo " 分发:把整个目录拷到目标机,执行"
|
||
echo " sudo bash install.sh drouter_1.0.8_all.deb"
|
||
echo "======================================================================"
|