From 020d59a033119bcf91f2dc1e6708a9a8664929e5 Mon Sep 17 00:00:00 2001 From: beyondx123 <109916089+beyondx123@users.noreply.github.com> Date: Thu, 14 May 2026 12:14:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=86=85=E6=A0=B8=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=20V1.3=EF=BC=8C=E6=94=AF=E6=8C=81=E7=A6=BB?= =?UTF-8?q?=E7=BA=BF=E5=AE=89=E8=A3=85/DKMS=E5=A4=84=E7=90=86/=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 340 ++++++++++++++++++++++++- switch_kernel.sh | 641 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 979 insertions(+), 2 deletions(-) create mode 100644 switch_kernel.sh diff --git a/README.md b/README.md index fe58a7a..be2ec0c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,339 @@ -# dacs-switch-kernel +# 内核切换脚本使用指南 -dacs 统一内核 \ No newline at end of file +## 版本:V1.3(支持离线安装 + DKMS 故障处理 + 自动日志记录) + +--- + +## 功能特性 + +✅ **在线/离线双模式**:自动检测网络,无网时切换离线模式 +✅ **DKMS 智能处理**:自动检测问题驱动,提供交互式修复 +✅ **预下载支持**:在有网机器上打包,传到离线机器安装 +✅ **驱动自动编译**:网卡、显卡驱动随内核自动重编译 +✅ **版本锁定**:防止系统自动更新覆盖指定内核 +✅ **自动日志记录**:所有操作追加写入 `log-日期-dacs.log` + +--- + +## 日志功能 + +脚本会自动记录所有操作到当前目录下的日志文件: + +**日志文件命名:** `log-YYYYMMDD-dacs.log`(例如:`log-20240514-dacs.log`) + +**日志内容包括:** +- 脚本启动/完成时间 +- 执行的命令和参数 +- 所有关键操作的输出(apt-get、dpkg、dkms 等) +- 错误信息和警告 +- 系统信息(主机名、当前内核等) + +**查看日志:** +```bash +# 实时查看日志 +tail -f log-$(date +%Y%m%d)-dacs.log + +# 查看完整日志 +less log-$(date +%Y%m%d)-dacs.log + +# 搜索错误 +grep -i error log-*.log +``` + +--- + +## 使用场景 + +### 场景 1:在线安装(推荐) + +**适用:** 目标机器有网络连接 + +```bash +sudo ./switch_kernel_V1.2.sh +# 或指定版本 +sudo ./switch_kernel_V1.2.sh --version 6.8.0-52-generic +``` + +**流程:** +1. 自动检测网络 ✓ +2. 检查 DKMS 驱动兼容性 +3. 在线下载并安装内核 +4. 自动编译驱动(DKMS) +5. 配置 GRUB 引导 + +--- + +### 场景 2:离线安装(两步法) + +**适用:** 目标机器无网络,需要提前准备 + +#### 步骤 1:在有网机器上下载包 + +```bash +sudo ./switch_kernel_V1.2.sh --download /tmp/kernel-debs --version 6.8.0-52-generic +``` + +**输出:** +``` +[OK] 下载完成,包列表: +-rw-r--r-- 1 root root 14M linux-image-6.8.0-52-generic_6.8.0-52.53_amd64.deb +-rw-r--r-- 1 root root 3.9M linux-headers-6.8.0-52-generic_6.8.0-52.53_amd64.deb +-rw-r--r-- 1 root root 38M linux-modules-6.8.0-52-generic_6.8.0-52.53_amd64.deb +-rw-r--r-- 1 root root 108M linux-modules-extra-6.8.0-52-generic_6.8.0-52.53_amd64.deb + +将 /tmp/kernel-debs 目录复制到目标机器后,运行: + sudo ./switch_kernel_V1.2.sh --offline /tmp/kernel-debs --version 6.8.0-52-generic +``` + +#### 步骤 2:在目标机器上安装 + +```bash +# 复制包目录到目标机器(U盘、scp 等方式) +scp -r /tmp/kernel-debs user@target-machine:/tmp/ + +# 在目标机器上运行 +sudo ./switch_kernel_V1.2.sh --offline /tmp/kernel-debs --version 6.8.0-52-generic +``` + +--- + +### 场景 3:跳过 DKMS 编译(解决驱动兼容问题) + +**适用:** 已知某些驱动与新内核不兼容,想先安装内核 + +```bash +sudo ./switch_kernel_V1.2.sh --skip-dkms +``` + +**效果:** +- 临时禁用 DKMS 自动编译钩子 +- 内核安装完成后自动恢复钩子 +- 可重启后手动编译驱动:`sudo dkms autoinstall -k 6.8.0-52-generic` + +--- + +## 参数说明 + +| 参数 | 说明 | 示例 | +|------|------|------| +| `--version <版本>` | 指定内核版本 | `--version 6.8.0-52-generic` | +| `--download <目录>` | 仅下载包到目录(需要网络) | `--download /tmp/debs` | +| `--offline <目录>` | 强制离线模式,从目录安装 | `--offline /tmp/debs` | +| `--skip-dkms` | 跳过 DKMS 自动编译 | `--skip-dkms` | +| `-h, --help` | 显示帮助信息 | `-h` | + +--- + +## DKMS 故障处理 + +### 自动检测与提示 + +脚本会在安装前检测已知问题驱动(如 `elkeid-driver`、`bbr` 等),并提示: + +``` +警告:以下驱动可能与新内核不兼容: + - elkeid-driver/1.7.0.22 + +是否跳过 DKMS 自动编译?(Y/N): +``` + +### 安装时遇到 DKMS 错误 + +如果安装过程中 DKMS 编译失败,脚本会自动捕获并提供选项: + +``` +[ERROR] DKMS 驱动编译失败! + +可能的原因: + 1. 某些第三方驱动与新内核不兼容 + 2. 缺少编译依赖 + +解决方案: + A. 临时移除问题驱动后重试 + B. 强制完成安装(忽略 DKMS 错误) + C. 退出并手动排查 + +请选择 [A/B/C]: +``` + +**选项说明:** + +- **A(推荐)**:列出所有 DKMS 驱动,选择移除问题驱动,然后自动重试安装 +- **B**:强制完成内核安装,忽略驱动编译错误(驱动可能不可用) +- **C**:退出脚本,查看 `TROUBLESHOOTING.md` 手动排查 + +--- + +## 常见问题 + +### Q1: 如何查看当前已安装的内核? + +```bash +dpkg -l | grep linux-image +``` + +### Q2: 如何查看 DKMS 驱动状态? + +```bash +dkms status +``` + +### Q3: 安装后如何验证新内核? + +```bash +# 检查 GRUB 菜单 +grep menuentry /boot/grub/grub.cfg | grep 6.8.0-52 + +# 重启后查看当前内核 +uname -r +``` + +### Q4: 如何回退到旧内核? + +1. 重启时按 `Shift` 或 `Esc` 进入 GRUB 菜单 +2. 选择 "Advanced options" +3. 选择旧内核版本启动 +4. 启动后移除新内核: + ```bash + sudo apt-get remove linux-image-6.8.0-52-generic + ``` + +### Q5: elkeid-driver 编译失败怎么办? + +**临时移除(推荐):** +```bash +sudo dkms remove elkeid-driver/1.7.0.22 --all +sudo ./switch_kernel_V1.2.sh +``` + +**安装完成后恢复:** +```bash +sudo apt-get install --reinstall elkeid-driver +# 或手动编译 +sudo dkms install elkeid-driver/1.7.0.22 -k 6.8.0-52-generic +``` + +### Q6: 显卡驱动失效怎么办? + +**NVIDIA 显卡:** +```bash +# 检查驱动状态 +nvidia-smi + +# 如果失效,重新安装驱动 +sudo apt-get install --reinstall nvidia-driver-xxx +# 或使用 DKMS 重新编译 +sudo dkms install nvidia/xxx -k 6.8.0-52-generic +``` + +**注意:** 如果启用了 Secure Boot,需要关闭或签名驱动模块。 + +--- + +## 完整示例 + +### 示例 1:在线安装并锁定版本 + +```bash +sudo ./switch_kernel_V1.2.sh --version 6.8.0-52-generic +# 按提示操作: +# - 是否设置为默认启动内核?Y +# - 是否锁定该内核版本?Y +# - 是否立刻重启系统?Y +``` + +### 示例 2:离线安装(完整流程) + +```bash +# 在有网机器上 +sudo ./switch_kernel_V1.2.sh --download /tmp/kernel-6.8.0-52 --version 6.8.0-52-generic +tar czf kernel-6.8.0-52.tar.gz -C /tmp kernel-6.8.0-52 + +# 传输到目标机器 +scp kernel-6.8.0-52.tar.gz user@target:/tmp/ + +# 在目标机器上 +cd /tmp +tar xzf kernel-6.8.0-52.tar.gz +sudo ./switch_kernel_V1.2.sh --offline /tmp/kernel-6.8.0-52 --version 6.8.0-52-generic +``` + +### 示例 3:跳过 DKMS 快速安装 + +```bash +sudo ./switch_kernel_V1.2.sh --skip-dkms --version 6.8.0-52-generic +# 重启后手动编译驱动 +sudo dkms autoinstall -k 6.8.0-52-generic +``` + +--- + +## 日志与调试 + +### 自动日志记录 + +脚本会自动将所有操作记录到 `log-YYYYMMDD-dacs.log`: + +```bash +# 查看今天的日志 +cat log-$(date +%Y%m%d)-dacs.log + +# 实时监控日志(另一个终端) +tail -f log-$(date +%Y%m%d)-dacs.log + +# 搜索错误 +grep -E "ERROR|failed|Failed" log-*.log +``` + +### 查看系统日志 + +```bash +# APT 安装日志 +cat /var/log/apt/term.log + +# DKMS 编译日志 +cat /var/lib/dkms///build/make.log + +# 内核消息 +dmesg | tail -100 +``` + +### 详细故障排查 + +参见 `TROUBLESHOOTING.md` 文档。 + +--- + +## 安全提示 + +⚠️ **Secure Boot**:如果启用了 Secure Boot,第三方驱动(NVIDIA、VirtualBox 等)可能无法加载。建议: +- 关闭 Secure Boot(BIOS 设置) +- 或为驱动模块签名(高级) + +⚠️ **备份重要数据**:内核切换有风险,建议提前备份关键数据。 + +⚠️ **生产环境**:建议先在测试环境验证,确认无问题后再部署到生产。 + +--- + +## 更新日志 + +### V1.3 (2024-05-14) +- ✨ 新增 DKMS 兼容性预检查 +- ✨ 新增 DKMS 编译失败自动修复 +- ✨ 新增 `--skip-dkms` 参数 +- ✨ 新增自动日志记录功能(log-日期-dacs.log) +- 🐛 修复离线模式下 DKMS 钩子未恢复的问题 +- 📝 添加详细的故障排查文档 + +### V1.2 (2024-05-14) +- ✨ 新增离线安装支持 +- ✨ 新增 `--download` 模式 +- ✨ 新增网络自动检测 +- 🐛 修复依赖安装顺序问题 + +### V1.0 +- 🎉 初始版本 +- ✅ 在线安装内核 +- ✅ DKMS 驱动自动编译 +- ✅ GRUB 引导配置 diff --git a/switch_kernel.sh b/switch_kernel.sh new file mode 100644 index 0000000..8ec1e7a --- /dev/null +++ b/switch_kernel.sh @@ -0,0 +1,641 @@ +#!/bin/bash + +# ================================================================= +# 脚本名称: switch_kernel.sh +# 描述: 自动更换指定内核版本,并确保驱动程序(网卡、显卡)自动同步编译。 +# 支持在线安装和离线安装(从本地 .deb 包目录安装)。 +# +# 用法: +# sudo ./switch_kernel.sh # 交互式(自动检测网络) +# sudo ./switch_kernel.sh --offline /path/to/debs # 强制离线,指定包目录 +# sudo ./switch_kernel.sh --download /path/to/debs # 仅下载包(需要网络) +# ================================================================= + +DEFAULT_KERNEL_VERSION="6.8.0-52-generic" +KERNEL_VERSION="" +OFFLINE_MODE=false +DOWNLOAD_ONLY=false +DEB_DIR="" +SKIP_DKMS=false +DKMS_BACKUP_DIR="/tmp/dkms_backup_$$" + +# 日志文件路径(当前目录) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LOG_FILE="${SCRIPT_DIR}/log-$(date +%Y%m%d)-dacs.log" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# 日志函数:同时输出到终端和文件 +log_info() { + local msg="[INFO] $*" + echo -e "${YELLOW}${msg}${NC}" + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ${msg}" >> "$LOG_FILE" +} + +log_ok() { + local msg="[OK] $*" + echo -e "${GREEN}${msg}${NC}" + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ${msg}" >> "$LOG_FILE" +} + +log_err() { + local msg="[ERROR] $*" + echo -e "${RED}${msg}${NC}" + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ${msg}" >> "$LOG_FILE" +} + +log_step() { + local msg="[STEP] $*" + echo -e "${BLUE}${msg}${NC}" + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ${msg}" >> "$LOG_FILE" +} + +# 清理函数:确保退出时恢复 DKMS 钩子 +cleanup() { + restore_dkms_hook +} + +trap cleanup EXIT INT TERM + +# --- 参数解析 --- + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --offline) + OFFLINE_MODE=true + DEB_DIR="${2:?'--offline 需要指定包目录路径'}" + shift 2 + ;; + --download) + DOWNLOAD_ONLY=true + DEB_DIR="${2:?'--download 需要指定保存目录路径'}" + shift 2 + ;; + --version) + KERNEL_VERSION="$2" + shift 2 + ;; + --skip-dkms) + SKIP_DKMS=true + shift + ;; + -h|--help) + echo "用法:" + echo " sudo $0 # 交互式(自动检测网络)" + echo " sudo $0 --offline /path/to/debs # 强制离线,指定包目录" + echo " sudo $0 --download /path/to/debs # 仅下载包(需要网络)" + echo " sudo $0 --version 6.8.0-52-generic # 指定内核版本(可与上述组合)" + echo " sudo $0 --skip-dkms # 跳过 DKMS 自动编译(解决驱动兼容问题)" + exit 0 + ;; + *) + log_err "未知参数: $1,使用 --help 查看用法。" + exit 1 + ;; + esac + done +} + +# --- 网络检测 --- + +check_network() { + curl -s --max-time 3 http://archive.ubuntu.com/ubuntu/ -o /dev/null 2>&1 +} + +# --- 内核检测 --- + +get_kernel_list() { + grep menuentry_id_option /boot/grub/grub.cfg +} + +check_kernel_installed() { + [[ $(get_kernel_list) == *"$KERNEL_VERSION"* ]] +} + +is_kernel_package_installed() { + dpkg -l | grep -q "linux-image-$KERNEL_VERSION" +} + +check_kernel_version_format() { + [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+-[0-9]+-generic$ ]] +} + +# --- DKMS 管理 --- + +list_dkms_modules() { + dkms status 2>/dev/null | awk -F'[,:]' '{print $1"/"$2}' | sort -u +} + +check_dkms_compatibility() { + log_info "检查 DKMS 驱动兼容性..." + local modules + modules=$(list_dkms_modules) + + if [[ -z "$modules" ]]; then + log_info "未检测到 DKMS 驱动。" + return 0 + fi + + echo -e "${YELLOW}当前系统已安装的 DKMS 驱动:${NC}" + echo "$modules" + echo "" + + local problematic_modules=() + while IFS= read -r mod; do + # 已知问题驱动列表 + if [[ "$mod" =~ elkeid-driver|bbr|sch_cake ]]; then + problematic_modules+=("$mod") + fi + done <<< "$modules" + + if [[ ${#problematic_modules[@]} -gt 0 ]]; then + echo -e "${YELLOW}警告:以下驱动可能与新内核不兼容:${NC}" + for m in "${problematic_modules[@]}"; do + echo " - $m" + done + echo "" + return 1 + fi + return 0 +} + +disable_dkms_hook() { + if [[ -x /etc/kernel/postinst.d/dkms ]]; then + log_info "临时禁用 DKMS 自动编译钩子..." + mkdir -p "$DKMS_BACKUP_DIR" + mv /etc/kernel/postinst.d/dkms "$DKMS_BACKUP_DIR/" + log_ok "DKMS 钩子已禁用(备份到 $DKMS_BACKUP_DIR)" + fi +} + +restore_dkms_hook() { + if [[ -f "$DKMS_BACKUP_DIR/dkms" ]]; then + log_info "恢复 DKMS 钩子..." + mv "$DKMS_BACKUP_DIR/dkms" /etc/kernel/postinst.d/ + chmod +x /etc/kernel/postinst.d/dkms + rm -rf "$DKMS_BACKUP_DIR" + log_ok "DKMS 钩子已恢复" + fi +} + +handle_dkms_failure() { + log_err "DKMS 驱动编译失败!" + echo "" + echo "可能的原因:" + echo " 1. 某些第三方驱动与新内核不兼容(如 elkeid-driver、旧版 nvidia 等)" + echo " 2. 缺少编译依赖" + echo "" + echo "解决方案:" + echo " A. 临时移除问题驱动后重试" + echo " B. 强制完成安装(忽略 DKMS 错误)" + echo " C. 退出并手动排查" + echo "" + + read -p "请选择 [A/B/C]: " choice + case "$choice" in + A|a) + echo "" + list_dkms_modules + echo "" + read -p "请输入要移除的驱动名称(格式: driver/version,如 elkeid-driver/1.7.0.22): " module_to_remove + if [[ -n "$module_to_remove" ]]; then + log_info "移除驱动: $module_to_remove" + dkms remove "$module_to_remove" --all + log_ok "驱动已移除,重新尝试安装..." + return 0 + else + log_err "未输入驱动名称,退出。" + exit 1 + fi + ;; + B|b) + log_info "强制完成安装(忽略 DKMS 错误)..." + dpkg --configure -a + apt-get install -f -y + if is_kernel_package_installed; then + log_ok "内核已安装(部分驱动可能未编译)" + return 0 + else + log_err "强制安装失败。" + exit 1 + fi + ;; + C|c|*) + log_info "退出。请查看 TROUBLESHOOTING.md 获取详细排查步骤。" + exit 1 + ;; + esac +} + +# --- 在线安装 --- + +apt_update() { + log_info "正在更新软件包列表..." + if apt-get update 2>&1 | tee -a "$LOG_FILE"; then + return 0 + else + log_err "apt-get update 失败,请检查网络连接。" + exit 1 + fi +} + +install_kernel_online() { + log_step "在线安装内核: ${KERNEL_VERSION}" + + # 如果启用了 --skip-dkms,临时禁用 DKMS 钩子 + if [[ "$SKIP_DKMS" == true ]]; then + disable_dkms_hook + fi + + if apt-get install -y \ + linux-image-"$KERNEL_VERSION" \ + linux-headers-"$KERNEL_VERSION" \ + linux-modules-extra-"$KERNEL_VERSION" \ + dkms \ + build-essential 2>&1 | tee -a "$LOG_FILE"; then + log_ok "内核相关基础包安装成功!" + + # 恢复 DKMS 钩子 + if [[ "$SKIP_DKMS" == true ]]; then + restore_dkms_hook + fi + else + # 检查是否是 DKMS 导致的失败 + if grep -q "dkms.*failed" /var/log/apt/term.log 2>/dev/null || \ + grep -q "post-installation script subprocess returned error" /var/log/apt/term.log 2>/dev/null; then + + # 恢复钩子(如果之前禁用了) + restore_dkms_hook + + # 提供交互式修复 + handle_dkms_failure + + # 用户选择修复后,重新尝试 + install_kernel_online + else + log_err "内核安装失败,请检查网络或版本号是否存在。" + restore_dkms_hook + exit 1 + fi + fi +} + +# --- 离线安装 --- + +check_offline_packages() { + local dir="$1" + local missing=() + local required_patterns=( + "linux-image-${KERNEL_VERSION}" + "linux-headers-${KERNEL_VERSION}" + "linux-modules-extra-${KERNEL_VERSION}" + ) + + for pattern in "${required_patterns[@]}"; do + if ! ls "$dir"/${pattern}*.deb &>/dev/null; then + missing+=("$pattern") + fi + done + + if [[ ${#missing[@]} -gt 0 ]]; then + log_err "离线包目录 '$dir' 中缺少以下包:" + for m in "${missing[@]}"; do + echo " - ${m}*.deb" + done + echo "" + echo "请先在有网络的机器上运行:" + echo " sudo $0 --download $dir --version $KERNEL_VERSION" + return 1 + fi + return 0 +} + +install_kernel_offline() { + local dir="$1" + log_step "离线安装内核: ${KERNEL_VERSION},包目录: $dir" + + if ! check_offline_packages "$dir"; then + exit 1 + fi + + # 如果启用了 --skip-dkms,临时禁用 DKMS 钩子 + if [[ "$SKIP_DKMS" == true ]]; then + disable_dkms_hook + fi + + # 安装 dkms / build-essential(如果包目录中有的话) + for pkg in dkms build-essential; do + if ! dpkg -l | grep -q "^ii $pkg"; then + if ls "$dir"/${pkg}*.deb &>/dev/null; then + log_info "从包目录安装 $pkg ..." + dpkg -i "$dir"/${pkg}*.deb || true + else + log_info "包目录中无 $pkg,跳过(DKMS 驱动编译可能受影响)。" + fi + fi + done + + log_info "安装内核包(按依赖顺序)..." + # headers → modules → modules-extra → image + local debs=() + for pattern in \ + "linux-headers-${KERNEL_VERSION}" \ + "linux-modules-${KERNEL_VERSION}" \ + "linux-modules-extra-${KERNEL_VERSION}" \ + "linux-image-${KERNEL_VERSION}"; do + while IFS= read -r -d '' f; do + debs+=("$f") + done < <(find "$dir" -name "${pattern}*.deb" -print0) + done + + if [[ ${#debs[@]} -eq 0 ]]; then + log_err "未找到任何匹配的 .deb 文件。" + restore_dkms_hook + exit 1 + fi + + if dpkg -i "${debs[@]}" 2>&1 | tee -a "$LOG_FILE"; then + log_ok "内核离线安装成功!" + restore_dkms_hook + else + # 检查是否是 DKMS 错误 + if dmesg | tail -50 | grep -q "dkms.*failed" || \ + dpkg -l | grep -q "^iU.*linux-image-${KERNEL_VERSION}"; then + + restore_dkms_hook + handle_dkms_failure + + # 重新尝试 + install_kernel_offline "$dir" + else + log_info "尝试修复依赖关系(apt-get install -f)..." + apt-get install -f -y 2>&1 | tee -a "$LOG_FILE" + + if is_kernel_package_installed; then + log_ok "内核离线安装成功!" + restore_dkms_hook + else + log_err "内核离线安装失败,请检查 .deb 包是否完整或版本是否匹配。" + restore_dkms_hook + exit 1 + fi + fi + fi +} + +# --- 下载模式 --- + +download_packages() { + local dir="$1" + mkdir -p "$dir" + log_step "下载内核包到目录: $dir(内核版本: $KERNEL_VERSION)" + + apt_update + + log_info "下载内核相关包(不安装)..." + apt-get install --download-only -y \ + linux-image-"$KERNEL_VERSION" \ + linux-headers-"$KERNEL_VERSION" \ + linux-modules-extra-"$KERNEL_VERSION" \ + dkms \ + build-essential 2>&1 | tee -a "$LOG_FILE" + + log_info "复制下载的包到 $dir ..." + find /var/cache/apt/archives/ \( \ + -name "linux-image-${KERNEL_VERSION}*.deb" \ + -o -name "linux-headers-${KERNEL_VERSION}*.deb" \ + -o -name "linux-modules-${KERNEL_VERSION}*.deb" \ + -o -name "linux-modules-extra-${KERNEL_VERSION}*.deb" \ + -o -name "dkms*.deb" \ + -o -name "build-essential*.deb" \ + \) -exec cp {} "$dir/" \; 2>&1 | tee -a "$LOG_FILE" + + log_ok "下载完成,包列表:" + ls -lh "$dir"/*.deb 2>&1 | tee -a "$LOG_FILE" || log_err "目录中没有找到 .deb 文件,下载可能失败。" + echo "" + echo "将 $dir 目录复制到目标机器后,运行:" + echo " sudo $0 --offline $dir --version $KERNEL_VERSION" +} + +# --- GRUB 配置 --- + +get_kernel_uuid() { + local kernel_list + kernel_list=$(get_kernel_list) + local gnulinux="gnulinux-advanced" + local kernel_advanced="gnulinux-$KERNEL_VERSION-advanced" + local gnulinux_uuid="" + local kernel_advanced_uuid="" + + while read -r l; do + if [[ $l == *"$gnulinux"* ]]; then + gnulinux_uuid=$(echo "$l" | sed -n "s/.*\($gnulinux[^']*\).*/\1/p") + elif [[ $l == *"$kernel_advanced"* ]]; then + kernel_advanced_uuid=$(echo "$l" | sed -n "s/.*\($kernel_advanced[^']*\).*/\1/p") + fi + done <<< "$kernel_list" + echo "$gnulinux_uuid>$kernel_advanced_uuid" +} + +set_boot_kernel() { + local grub_path="/etc/default/grub" + local default="GRUB_DEFAULT=" + local newlines="" + local uuid_list + uuid_list=$(get_kernel_uuid) + + while read -r l; do + if [[ $l == $default* ]]; then + newlines+="$default\"$uuid_list\"\n" + else + newlines+="$l\n" + fi + done < "$grub_path" + + echo -e "\n${YELLOW}新的 /etc/default/grub 配置预览:${NC}" + echo -e "$newlines" + + read -p "请确认配置是否正确,输入Y确认回写文件,按任意键跳过: " answer + if [[ "$answer" == "Y" || "$answer" == "y" ]]; then + echo -e "$newlines" > "$grub_path" + return 0 + fi + return 1 +} + +set_default_boot_kernel() { + if ! check_kernel_installed; then + if [[ "$OFFLINE_MODE" == true ]]; then + install_kernel_offline "$DEB_DIR" + else + apt_update + install_kernel_online + fi + fi + + if set_boot_kernel; then + log_info "正在更新 GRUB 引导程序..." + update-grub 2>&1 | tee -a "$LOG_FILE" + + # 如果没有跳过 DKMS,尝试为新内核编译驱动 + if [[ "$SKIP_DKMS" == false ]]; then + log_info "正在为新内核 ($KERNEL_VERSION) 构建驱动程序 (DKMS)..." + if dkms autoinstall -k "$KERNEL_VERSION" 2>&1 | tee -a "$LOG_FILE"; then + log_ok "DKMS 驱动编译成功。" + else + log_err "DKMS 驱动编译失败(部分驱动可能不可用)。" + echo "可以重启后手动运行: sudo dkms autoinstall -k $KERNEL_VERSION" + fi + else + log_info "已跳过 DKMS 自动编译(可重启后手动运行: sudo dkms autoinstall -k $KERNEL_VERSION)。" + fi + fi +} + +hold_default_boot_kernel() { + log_info "正在锁定内核版本,防止被系统更新替换..." + apt-mark hold linux-image-"$KERNEL_VERSION" + apt-mark hold linux-headers-"$KERNEL_VERSION" + apt-mark hold linux-modules-extra-"$KERNEL_VERSION" +} + +# --- 主程序 --- + +parse_args "$@" + +if [[ $EUID -ne 0 ]]; then + log_err "请使用 root 权限运行此脚本!" + exit 1 +fi + +# 记录脚本启动信息 +{ + echo "" + echo "==========================================" + echo "脚本启动时间: $(date '+%Y-%m-%d %H:%M:%S')" + echo "执行命令: $0 $*" + echo "当前用户: $(whoami)" + echo "主机名: $(hostname)" + echo "当前内核: $(uname -r)" + echo "==========================================" + echo "" +} >> "$LOG_FILE" + +log_info "日志文件: $LOG_FILE" + +# 下载模式:只下载,不安装 +if [[ "$DOWNLOAD_ONLY" == true ]]; then + if [[ -z "$KERNEL_VERSION" ]]; then + read -p "请输入要下载的内核版本号 (直接回车使用 $DEFAULT_KERNEL_VERSION): " user_input + KERNEL_VERSION="${user_input:-$DEFAULT_KERNEL_VERSION}" + fi + if ! check_kernel_version_format "$KERNEL_VERSION"; then + log_err "版本格式不正确(示例: 6.8.0-52-generic)" + exit 1 + fi + download_packages "$DEB_DIR" + exit 0 +fi + +# 交互式选择内核版本(命令行未指定时) +if [[ -z "$KERNEL_VERSION" ]]; then + read -p "请输入要安装的内核版本号 (直接回车使用 $DEFAULT_KERNEL_VERSION): " user_input + if [[ -z "$user_input" ]]; then + KERNEL_VERSION="$DEFAULT_KERNEL_VERSION" + else + KERNEL_VERSION="$user_input" + if ! check_kernel_version_format "$KERNEL_VERSION"; then + log_err "格式不正确,将使用默认版本 $DEFAULT_KERNEL_VERSION。" + KERNEL_VERSION="$DEFAULT_KERNEL_VERSION" + fi + fi +fi + +# 自动检测网络(未强制指定离线模式时) +if [[ "$OFFLINE_MODE" == false ]]; then + if check_network; then + log_info "网络可用,使用在线模式。" + else + log_info "网络不可用,自动切换到离线模式。" + OFFLINE_MODE=true + if [[ -z "$DEB_DIR" ]]; then + read -p "请输入本地 .deb 包目录路径: " DEB_DIR + if [[ -z "$DEB_DIR" || ! -d "$DEB_DIR" ]]; then + log_err "目录不存在或未指定,退出。" + exit 1 + fi + fi + fi +fi + +# DKMS 兼容性检查(除非用户已指定 --skip-dkms) +if [[ "$SKIP_DKMS" == false ]]; then + if ! check_dkms_compatibility; then + echo "" + read -p "检测到可能不兼容的 DKMS 驱动,是否跳过 DKMS 自动编译?(Y/N): " answer + if [[ "$answer" == "Y" || "$answer" == "y" ]]; then + SKIP_DKMS=true + log_info "将跳过 DKMS 自动编译。" + else + log_info "继续安装(如遇 DKMS 错误会提供修复选项)。" + fi + fi +fi + +# 安装阶段 +if is_kernel_package_installed; then + log_info "内核包 linux-image-$KERNEL_VERSION 已经安装。" + # 确保 headers 存在以便编译驱动 + if [[ "$OFFLINE_MODE" == true ]]; then + if ls "$DEB_DIR"/linux-headers-"${KERNEL_VERSION}"*.deb &>/dev/null; then + dpkg -i "$DEB_DIR"/linux-headers-"${KERNEL_VERSION}"*.deb || true + dpkg -i "$DEB_DIR"/dkms*.deb "$DEB_DIR"/build-essential*.deb 2>/dev/null || true + fi + else + apt-get install -y linux-headers-"$KERNEL_VERSION" dkms build-essential + fi +else + if [[ "$OFFLINE_MODE" == true ]]; then + install_kernel_offline "$DEB_DIR" + else + install_kernel_online + fi +fi + +# 设置默认启动内核 +read -p "是否设置 $KERNEL_VERSION 为默认启动内核?(Y/N): " answer +if [[ "$answer" == "Y" || "$answer" == "y" ]]; then + set_default_boot_kernel +fi + +# 锁定内核版本 +read -p "是否锁定该内核版本(防止系统自动更新内核)?(Y/N): " answer +if [[ "$answer" == "Y" || "$answer" == "y" ]]; then + hold_default_boot_kernel +fi + +log_ok "操作完成!" +echo -e "${YELLOW}注意:如果重启后显卡驱动不正常,请检查 Secure Boot 是否已关闭。${NC}" + +# 记录脚本完成信息 +{ + echo "" + echo "==========================================" + echo "脚本完成时间: $(date '+%Y-%m-%d %H:%M:%S')" + echo "目标内核版本: $KERNEL_VERSION" + echo "安装模式: $([ "$OFFLINE_MODE" == true ] && echo "离线" || echo "在线")" + echo "DKMS 状态: $([ "$SKIP_DKMS" == true ] && echo "已跳过" || echo "已执行")" + echo "==========================================" + echo "" +} >> "$LOG_FILE" + +read -p "是否立刻重启系统?(Y/N): " answer +if [[ "$answer" == "Y" || "$answer" == "y" ]]; then + log_info "系统即将重启..." + reboot +fi