This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
beyondx123 ca1c38417c 本次修改了三个地方:
① set_boot_kernel(去掉确认,新增禁止自动选最新内核)

移除 read -p 确认,直接写入 /etc/default/grub
同时将 GRUB_SAVEDEFAULT 强制写为 false,防止 GRUB 记住上次选择 干扰指定内核
新增 UUID 有效性检查,获取失败时报错退出而非写入空值
② hold_default_boot_kernel(新增锁定元包)

原来只锁定具体版本的 linux-image-6.8.0-52-generic
新增锁定 linux-image-generic 和 linux-headers-generic 两个元包——这才是 apt 升级时切换内核的根本原因
③ 主流程(去掉两个交互确认)

安装完成后自动调用 set_default_boot_kernel + hold_default_boot_kernel,无需用户输入
2026-05-15 17:42:40 +08:00

650 lines
20 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 newlines=""
local uuid_list
uuid_list=$(get_kernel_uuid)
if [[ -z "$uuid_list" || "$uuid_list" == ">" ]]; then
log_err "无法从 grub.cfg 中获取内核 UUID,请确认内核已安装并运行过 update-grub。"
return 1
fi
# 重写 GRUB_DEFAULT,同时确保 GRUB_SAVEDEFAULT 不干扰
while read -r l; do
if [[ $l == GRUB_DEFAULT=* ]]; then
newlines+="GRUB_DEFAULT=\"$uuid_list\"\n"
elif [[ $l == GRUB_SAVEDEFAULT=* ]]; then
newlines+="GRUB_SAVEDEFAULT=false\n"
else
newlines+="$l\n"
fi
done < "$grub_path"
# 如果文件里没有 GRUB_SAVEDEFAULT 这行,追加一条
if ! grep -q "^GRUB_SAVEDEFAULT=" "$grub_path"; then
newlines+="GRUB_SAVEDEFAULT=false\n"
fi
log_info "写入 GRUB_DEFAULT: $uuid_list"
echo -e "$newlines" > "$grub_path"
return 0
}
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"
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
else
log_err "GRUB 配置写入失败,请手动检查 /etc/default/grub。"
exit 1
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"
# 锁定元包,防止 apt 升级时将默认内核切换到新版本
apt-mark hold linux-image-generic
apt-mark hold linux-headers-generic
log_ok "内核版本已锁定。"
}
# --- 主程序 ---
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
# 安装成功后:自动设置默认启动内核 + 锁定版本(无需用户确认)
log_step "设置 $KERNEL_VERSION 为默认启动内核..."
set_default_boot_kernel
log_step "锁定内核版本,防止 apt 自动升级切换..."
hold_default_boot_kernel
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