常用命令
nixos-rebuild build --dry-run # 求值层,不碰系统
nixos-rebuild test # 切进运行时,不写引导,重启即回
systemd-analyze verify <unit> # unit 语法
nixos-rebuild build-vm # 关键服务真起一台 VM 验
rebuild list 列出所有世代及其标签
rebuild <序号> 切换到指定世代 (1-3 位数字)
rebuild <描述> 创建新世代(描述字符集: A-Z a-z 0-9 . _ - : ,空格自动转 -)
/etc/nixos/modules/system/boot-menu-names.nix
# 交互式 shell 里启用 rebuild 函数(nix 世代管理);import 本模块即生效
programs.bash.interactiveShellInit = ''
[ -r ${./boot-menu-rebuild.sh} ] && source ${./boot-menu-rebuild.sh}
'';
/etc/nixos/modules/system/boot-menu-rebuild.sh
#!/usr/bin/env bash
# rebuild —— 一个命令管「列世代 / 切世代 / 建世代(带label) / 同步命名池 / 透传 nixos-rebuild」
# 由 ~/.bashrc source;随 /etc/nixos 一起迁移到下台机器(拷 modules/ 即可)
#
# 与 boot-menu-names.nix 的关系:
# - 本函数的 <描述> 经 NIXOS_LABEL 写进 config.system.nixos.label(可在 rebuild list / boot.json 看到)
# - 启动菜单条目标题由 modules/system/boot-menu-names.nix 决定(explicit 表 + auto_label),本函数不参与
_rebuild_label() {
# 构造标准 label:时间戳前缀 + 描述;并做字符集校验(rebuild 与 nixos-rebuild-guard 共用,单一规则)
local desc; desc=$(printf '%s' "$1" | tr ' ' '-')
if [ -z "$desc" ]; then echo "rebuild: 描述不能为空" >&2; return 1; fi
if printf '%s' "$desc" | LC_ALL=C grep -q '[^ -~]'; then
echo "rebuild: 描述含非 ASCII 字符(启动菜单是点阵字体,画不出中文/全角字符)" >&2; return 1
fi
if printf '%s' "$desc" | LC_ALL=C grep -q '[^A-Za-z0-9._:-]'; then
echo "rebuild: 描述含系统不允许的字符(label 允许集: A-Z a-z 0-9 . _ - : ;空格会自动转成 -)" >&2
printf '%s' "$desc" | LC_ALL=C grep -o '[^A-Za-z0-9._:-]' | sort -u | tr '\n' ' ' | sed 's/^/ 不允许的字符: /' >&2; echo >&2
echo " 你的输入: $desc" >&2
return 1
fi
printf '%s-%s' "$(date '+%Y-%m-%d_%H-%M-%S')" "$desc"
}
rebuild() {
# --- 帮助信息 ---
_rebuild_help() {
cat <<'EOF'
rebuild 使用说明
支持:
rebuild list 列出所有世代及其标签
rebuild <序号> 切换到指定世代 (1-3 位数字)
rebuild <描述> 创建新世代(描述字符集: A-Z a-z 0-9 . _ - : ,空格自动转 -)
启动菜单标题 = {序号} {日期时间} {描述},如 42 2026-10-08 15:12:03 amdgpu-mst-bpc
rebuild sync 只保留命名池里「未注释」的世代,其余全部删除
(删世代记录 = nix-env;同步 boot 菜单 = switch-to-configuration boot)
rebuild --<选项> 透传给 nixos-rebuild switch,例如 --upgrade
暂不支持:
无参数调用
多个参数调用
EOF
}
# --- 无参数 → 提示 ---
if [ "$#" -eq 0 ]; then
echo "暂不支持无参数调用。"
_rebuild_help
return 1
fi
# --- 多参数 → 提示 ---
if [ "$#" -gt 1 ]; then
echo "暂不支持多个参数调用。"
_rebuild_help
return 1
fi
# --- 到这里只有一个参数 ---
local arg="$1"
# --- list:列出世代 / 日期 / 标签(启动菜单名) / 内核 ---
if [ "$arg" = "list" ]; then
local -a G=() D=() L=() K=()
local gen date kern label maxlab=0 maxgen=0 gen_link
for gen_link in $(ls -d /nix/var/nix/profiles/system-*-link 2>/dev/null | sort -V); do
gen=$(basename "$gen_link" | sed 's/system-\([0-9]*\)-link/\1/')
date=$(stat -c %y "$gen_link" | cut -d. -f1)
# 标签源①:启动菜单名(boot-menu-names 模块导出的映射,= 开机时你看到的 title)
label=$(sed -n "s/^$gen|//p" /etc/nixos/modules/system/boot-menu-pools/boot-menu-names-titles.tsv 2>/dev/null | head -1)
# 源②:闭包内只读 boot.json 的 bootspec.label(免 root、免 jq)
[ -n "$label" ] || label=$(sed -n 's/.*"label"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$gen_link/boot.json" 2>/dev/null | head -1)
[ -n "$label" ] || label=$("$gen_link/sw/bin/nixos-version" 2>/dev/null || echo "-")
# 内核版本:世代的 kernel 软链指向 ...-linux-<版本>/bzImage
kern=$(readlink -f "$gen_link/kernel" 2>/dev/null | sed -n 's#.*linux-\([0-9][0-9.]*\)/bzImage#\1#p')
G+=("$gen"); D+=("$date"); L+=("$label"); K+=("${kern:--}")
(( ${#L[-1]} > maxlab )) && maxlab=${#L[-1]}
(( ${#gen} > maxgen )) && maxgen=${#gen}
done
printf '%*s %-19s %-*s %s\n' "$maxgen" "ID" "Date" "$maxlab" "Label" "Kernel"
local i
for (( i=0; i<${#G[@]}; i++ )); do
printf '%*s %-19s %-*s %s\n' "$maxgen" "${G[$i]}" "${D[$i]}" "$maxlab" "${L[$i]}" "${K[$i]}"
done
return
fi
# --- sync:只保留命名池里「未注释」的世代,其余全部删除 ---
# 删除用系统命令完成:nix-env 管世代记录(profile 一致性),
# switch-to-configuration boot 重跑引导器安装(builder 的 remove_old_entries 清掉
# 陈旧 boot 菜单条目 + 孤儿内核,随后 boot-menu-names 重放命名)—— 不新增世代。
if [ "$arg" = "sync" ]; then
# 本机命名池:路径按 /boot 的 ESP UUID 派生(与 boot-menu-names.nix 的 machineKey 同源)
local uuid pool
uuid=$(findmnt -no UUID /boot 2>/dev/null)
pool="/etc/nixos/modules/system/boot-menu-pools/${uuid}.nix"
if [ -z "$uuid" ] || [ ! -f "$pool" ]; then
echo "rebuild sync: 找不到本机命名池(ESP UUID='$uuid' → $pool)" >&2
return 1
fi
# 保留集 = 池里「未注释」的世代号(打开注释即纳入,注释掉即排除)
local -a keep=()
local k
while IFS= read -r k; do [ -n "$k" ] && keep+=("$k"); done < <(
grep -oE '^[[:space:]]*"[0-9]+"' "$pool" | tr -dc '0-9\n' | sort -n)
if [ "${#keep[@]}" -eq 0 ]; then
echo "rebuild sync: 池里没有未注释的世代(保留集为空)→ 拒绝执行(否则会删光全部世代)" >&2
return 1
fi
# 待删 = 当前存在的世代 − 保留集
local -a all=() del=()
local g iskeep
while IFS= read -r g; do [ -n "$g" ] && all+=("$g"); done < <(
ls -d /nix/var/nix/profiles/system-*-link 2>/dev/null | sed 's#.*system-##;s#-link##' | sort -n)
for g in "${all[@]}"; do
iskeep=0
for k in "${keep[@]}"; do [ "$g" = "$k" ] && { iskeep=1; break; }; done
[ "$iskeep" -eq 0 ] && del+=("$g")
done
echo "rebuild sync: 保留(池) = ${keep[*]}"
if [ "${#del[@]}" -eq 0 ]; then
echo "rebuild sync: 无待删世代,已同步。"
return 0
fi
echo "rebuild sync: 待删 ${#del[@]} 个世代 = ${del[*]}"
# ① 删世代记录(系统命令;不会删当前世代,profile 一致性由 nix-env 负责)
sudo /run/current-system/sw/bin/nix-env -p /nix/var/nix/profiles/system --delete-generations "${del[@]}" || return
# ② 重跑引导器安装(系统命令)→ 同步 boot 菜单(清陈旧条目/孤儿内核 + 重放命名)
sudo /run/current-system/bin/switch-to-configuration boot
return
fi
# --- 1-3 位数字 → 切换世代 ---
if [[ "$arg" =~ ^[0-9]{1,3}$ ]]; then
sudo nix-env -p /nix/var/nix/profiles/system --switch-generation "$arg" || return
sudo /nix/var/nix/profiles/system/bin/switch-to-configuration switch
return
fi
# --- 以 - 开头 → 透传给 nixos-rebuild switch ---
if [[ "$arg" == -* ]]; then
sudo nixos-rebuild switch "$arg"
return
fi
# --- 其他 → 创建带「时间戳+描述」label 的新世代 ---
# label 字符集被 label.nix 限制为 [a-zA-Z0-9:_.-],不能有空格 → 用下划线/短横线排版;
# 启动菜单标题(含序号)由 boot-menu-names 钩子按这个 label 反解生成
local label
label=$(_rebuild_label "$arg") || return 1
# 执行前记录当前最大世代 id(钩子用它做冗余校验,精确定位本次产生的世代)
local prev_max=$(ls -d /nix/var/nix/profiles/system-*-link 2>/dev/null | sed 's#.*system-##;s#-link##' | sort -n | tail -1)
sudo env NIXOS_LABEL="$label" NIXOS_BOOT_PREV_MAX="${prev_max:-0}" nixos-rebuild switch
}
/etc/nixos/modules/system/boot-menu-names.nix
# 契约(2026-10-11):import 即生效 —— 在 configuration.nix 的清单里打开/注释本行即可,
# 参数写在本文件顶部「参数」区(原 local.*.enable 开关已取消)。
# 世代菜单名保持模块(boot-menu-names)
#
# 作用:nixos-rebuild 时 systemd-boot-builder 会按闭包里的只读 label **重写所有** /boot/loader/entries/*.conf
# (内建行为,无法关闭)→ 本模块用 boot.loader.systemd-boot.extraInstallCommands 在 builder 之后重放命名。
# 名字为什么必须纯 ASCII:systemd-boot 用内置 8x16 点阵字体(只有拉丁字形),二进制里没有任何字体加载路径
# (strings 只有 "Unknown glyph"),NixOS 的 systemd-boot 模块也没有 font 选项 → 中文只会显示成方块。
# 真要在菜单里显示中文需换 GRUB(boot.loader.grub.font = 自制 pf2),见 /etc/nixos/modules/hardware/nvidia-4060-egpu-display.md 第 10 节。
# 改名的正确姿势(2026-10-10 定):编辑 modules/system/boot-menu-pools/<本机ESP-UUID>.nix,
# 然后跑一次 `sudo boot-menu-names` 即生效 —— **不需要 nixos-rebuild**,也不会多出世代。
# (脚本在运行时用 nix-instantiate 读池文件;此前那套“烘进世代的映射”已删除,
# 因为命名是运维动作,不是“用户构建世代”的行为,不该记账。)
#
# 设计:一份机制 + 每机一份命名池
# modules/system/boot-menu-names.nix 机制(共享;不含任何机器数据、不写共享文件)
# modules/system/boot-menu-pools/<ESP-UUID>.nix 命名池(每机一份;只由本机自动改写,绝不交叉)
# 为什么池必须分机:池的键是世代号,而克隆机从新序列开始 ⇒ 同一个号在两台机器上含义不同,
# 共享会导致「盘2 的新 gen5 继承盘1 老 gen5 的名字」,且两台各自追加会在同一文件里撞出重复键。
{ config, lib, pkgs, ... }:
let
# ── 参数(本模块自管;import 即生效,不引入即失效)──────────────────
# 原选项 local.bootMenuNames.* 已取消,值就写在这里
numberFill = "0";
# ===== 机器身份 = 本机 ESP 的 UUID =====
# 取自机器专属的 hardware-configuration.nix(fileSystems."/boot".device),克隆后必然不同
# ⇒ 天然把主机与各僚机的命名池分开。备选为何不行:hostname 克隆后相同(两台都叫 max2-nixos);
# /etc/machine-id 是普通文件,dd 克隆会连它一起复制 ⇒ 不唯一。
machineKey = builtins.baseNameOf config.fileSystems."/boot".device;
# 本机命名池(一份机制 + 每机一份池;池文件只由本机自动改写,另一台机器不碰)
# 池文件的键是世代号 —— 而克隆机从新序列开始,世代号含义与源机完全不同,
# 所以池绝不能跨机共享。新增机器 = 新建 <ESP-UUID>.nix。
poolFile = ./boot-menu-pools + "/${machineKey}.nix";
explicit =
if builtins.pathExists poolFile then import poolFile
else lib.warn ''
boot-menu-names: 本机 ESP UUID = ${machineKey} 还没有命名池
请新建 modules/system/boot-menu-pools/${machineKey}.nix(照抄同目录其它池文件的格式)。
本次 rebuild 不套用任何手工命名(这是缺池的正常语义,不是错误)。
'' { };
# 用 writeShellScriptBin(writeShellScript 产出单文件,无法进 buildEnv;
# writeShellApplication 会引入 shellcheck 构建依赖,故不用)
rename = pkgs.writeShellScriptBin "boot-menu-names" ''
export PATH=${lib.makeBinPath (with pkgs; [ coreutils gnugrep gnused python3 nix ])}
# 本次 rebuild 是否由 rebuild <描述> 发起(空 = 普通 rebuild)
LABEL_IN='${lib.maybeEnv "NIXOS_LABEL" ""}'
PREV_MAX='${lib.maybeEnv "NIXOS_BOOT_PREV_MAX" ""}' # 执行前的最大世代 id(冗余校验)
set -euo pipefail
ENT=/boot/loader/entries
PROF=/nix/var/nix/profiles
if [ ! -d "$ENT" ]; then
echo "boot-menu-names: $ENT 不存在(非 systemd-boot 引导?),跳过" >&2
exit 0
fi
# 表外世代自动起名:时间取世代链接 mtime,说明从包级 diff 关键字映射(词表全 ASCII)
# 世代链接 mtime → "YYYY-MM-DD HH:MM:SS"(表内/表外统一用它)
stamp_of() {
stat -c %y "$PROF/system-$1-link" | cut -c1-19
}
# 显示用短时间戳:2026-10-12 08:37:49 -> 10-12 08:37(纯规则 ✓ 所有标签统一用它 ✓)
short_stamp() {
stamp_of "$1" | cut -c6-16
}
auto_label() {
local n="$1" link="$PROF/system-$n-link" prev="" diff="" tag="" stamp=""
[ -e "$link" ] || { printf '?'; return; }
# 2026-10-07 22:00 -> 20261007-2200(保留分钟)
stamp=$(stamp_of "$n")
prev="$PROF/system-$((n-1))-link"
diff=""
if [ -e "$prev" ]; then
if ! diff=$(nix store diff-closures "$prev" "$link" 2>&1); then
echo "boot-menu-names: gen$n 的 diff-closures 失败,说明列用 config-change" >&2
diff=""
fi
fi
while IFS='|' read -r kw name; do
[ -n "$kw" ] || continue
if printf '%s' "$diff" | grep -qiE "$kw"; then tag="$name"; break; fi
done <<'KW'
linux-[0-9]|kernel-switch
nvidia|nvidia-driver
xf86-video-amdgpu|igpu-ddx
bluez|bluetooth|bluetooth
fprintd|libfprint|fingerprint
plasma|kde|desktop
iwlwifi|firmware|wifi-firmware
docker|docker
fcitx5|ime
gcc|binutils|toolchain
boot-menu|boot-menu-names
git|git
KW
if [ -z "$tag" ] && [ -n "$diff" ]; then
tag=$(printf '%s' "$diff" | head -1 | cut -d: -f1 | tr -d ' ')
fi
[ -n "$tag" ] || tag="config-change"
printf '%s %s' "$(short_stamp "$n")" "$tag"
}
# 自定义标题:rebuild <描述> 会把 label 写成 <YYYY-MM-DD>_<HH-MM-SS>-<描述>
# boot.json 的 label 形如 "NixOS <codeName> <label> (Linux <ver>)"(见 nixpkgs bootspec.nix:29)
# 序号只有这里(build 之后、root 上下文)才知道 → 标题格式: <序号> <日期> <时:分:秒> <描述>
custom_title() {
local n="$1" link="$PROF/system-$n-link" raw="" stamp="" rest="" hh="" mm="" ss="" slug=""
[ -e "$link/boot.json" ] || { printf ""; return; }
raw=$(grep -o '"label"[[:space:]]*:[[:space:]]*"[^"]*"' "$link/boot.json" | head -1 | cut -d'"' -f4)
raw=$(printf '%s' "$raw" | sed 's/^NixOS //; s/ (Linux .*)$//')
raw=$(printf '%s' "$raw" | sed 's/.* //')
case "$raw" in
[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-*)
stamp=$(printf '%s' "$raw" | cut -d_ -f1)
rest=$(printf '%s' "$raw" | cut -d_ -f2-)
hh=$(printf '%s' "$rest" | cut -d- -f1)
mm=$(printf '%s' "$rest" | cut -d- -f2)
ss=$(printf '%s' "$rest" | cut -d- -f3)
slug=$(printf '%s' "$rest" | cut -d- -f4-)
printf '%s %s' "$(short_stamp "$n")" "$slug"
;;
*) printf "" ;;
esac
}
# 改一个世代条目(title + version 两行;断言不含非 ASCII)
apply_one() {
local n="$1" label="$2" f="$ENT/nixos-generation-$1.conf" k nw="''${NW:-1}"
[ -f "$f" ] || return 0
k=$(grep -o 'linux-[0-9][0-9.]*' "$f" | head -1 | cut -d- -f2-)
python3 - "$f" "$n" "$label" "$k" "''${NW:-1}" "${numberFill}" <<'PY'
import re, sys
p, n, label, kern, nw, fill = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], int(sys.argv[5]), sys.argv[6]
stamp = " ".join(label.split()[:2])
s = open(p, encoding='utf-8').read()
s = re.sub(r'^title .*$', f'title {label}', s, count=1, flags=re.M)
s = re.sub(r'^version .*$', f'version NixOS Generation {n} - {stamp} - Linux {kern}', s, count=1, flags=re.M)
bad = sorted({c for c in s if ord(c) > 127})
if bad:
raise SystemExit(f'!! {p} 含非 ASCII 字符 {bad}(systemd-boot 会画成方块)')
open(p, 'w', encoding='utf-8').write(s)
print(f'[boot-menu-names] gen{n} -> {n} {label}')
PY
}
CUSTOM="" # 由 rebuild <描述> 产生的世代(稍后登记回 explicit 表)
# 标题等长化:先收集 (世代, 标签),全部收集完再按最长项动态定宽(宽度不写死)
declare -a GENS=() LABELS=()
# 导出「世代 -> 菜单名」映射(world-readable,供 rebuild list 免 root 读取)
MAP=/etc/nixos/modules/system/boot-menu-pools/boot-menu-names-titles.tsv
mkdir -p /etc/nixos/modules/system/boot-menu-pools
: > "$MAP"
# 池文件(本机专属,路径按 ESP UUID 派生)
POOL=/etc/nixos/modules/system/boot-menu-pools/${machineKey}.nix
echo "boot-menu-names: 池 = $POOL(烘焙时常量条目数 ${toString (builtins.length (builtins.attrNames explicit))};以运行时文件为准)"
# ① 表内世代:**运行时**读池文件,而不是读烘进世代的映射
# 理由(2026-10-10 定):命名是**运维动作**,不是“用户构建世代”的行为,
# 不该为了改一个名字而记账出一个新世代。所以改池后只要跑一次
# sudo boot-menu-names
# 就生效,零新世代。(mapFile 那套烘进世代的映射已删除。)
# 池语法错会让 nix-instantiate 直接报错停下 —— 不静默兜底。
POOLJSON=$(mktemp); POOLTSV=$(mktemp)
trap 'rm -f "$POOLJSON" "$POOLTSV"' EXIT
if [ -f "$POOL" ]; then
nix-instantiate --eval --strict --raw --expr "builtins.toJSON (import $POOL)" > "$POOLJSON"
python3 -c 'import json,sys;d=json.load(open(sys.argv[1]));print("\n".join(f"{k}|{v}" for k,v in sorted(d.items(),key=lambda x:int(x[0]))))' "$POOLJSON" > "$POOLTSV"
else
: > "$POOLTSV"
fi
while IFS='|' read -r n desc; do
[ -n "''${n:-}" ] || continue
[ -e "$PROF/system-$n-link" ] || continue # 世代谢已删除 → 跳过
label="$(short_stamp "$n") $desc"
GENS+=("$n"); LABELS+=("$label")
echo "$n|$label" >> "$MAP"
done < "$POOLTSV"
# ② 表外世代(自动起名)
for n in $(ls -d $PROF/system-*-link 2>/dev/null | sed 's#.*/system-##;s#-link##'); do
grep -q "^$n|" "$POOLTSV" && continue
lbl=$(custom_title "$n")
if [ -n "$lbl" ]; then CUSTOM="$CUSTOM $n"; fi
[ -n "$lbl" ] || lbl=$(auto_label "$n")
GENS+=("$n"); LABELS+=("$lbl")
echo "$n|$lbl" >> "$MAP"
done
chmod 644 "$MAP"
# ③ 只给「世代号」定宽:NW = 最长世代号宽度,用显式填充字符补齐(默认 0 → 01/57)
# 不补标签尾部 —— 2026-10-10 实测:尾部空格虽写进条目文件,它右边没有任何列,
# 视觉上恒等于零(纯死代码,已删)。见本文件头部 numberFill 的说明。
NW=1
for i in "''${!GENS[@]}"; do
[ "''${#GENS[$i]}" -gt "$NW" ] && NW="''${#GENS[$i]}"
done
for i in "''${!GENS[@]}"; do
apply_one "''${GENS[$i]}" "''${LABELS[$i]}"
done
echo "boot-menu-names: 世代号定宽 NW=$NW(填充字符 '${numberFill}')"
if [ -n "''${CUSTOM// /}" ]; then
if [ -f "$POOL" ]; then
python3 - "$POOL" "$MAP" "$CUSTOM" "''${GENS[*]}" <<'PY'
import sys, re
mod, mapf, gens = sys.argv[1], sys.argv[2], sys.argv[3].split()
existing = set(sys.argv[4].split()) if len(sys.argv) > 4 else set()
have = {}
for line in open(mapf, encoding='utf-8'):
if '|' in line:
g, l = line.rstrip().split('|', 1)
have[g] = l
lines = open(mod, encoding='utf-8').read().splitlines(keepends=True)
start = -1
for i, l in enumerate(lines):
if l.strip() == '{' and start < 0:
start = i
break
end = -1
if start >= 0:
for i in range(start + 1, len(lines)):
if lines[i].strip() == '}':
end = i
break
add = []
stale = 0
if start >= 0 and end > start:
# 先把「世代已不存在」的条目就地注释掉(保留原文,供追溯;绝不删除)
body = lines[start + 1:end]
newbody = []
for l in body:
m = re.match(r'^(\s*)"(\d+)"\s*=\s*"([^"]*)"\s*;\s*$', l)
if m and existing and m.group(2) not in existing:
newbody.append(m.group(1) + '# "' + m.group(2) + '" = "' + m.group(3)
+ '"; # 世代已不存在(自动注释,留作追溯)' + chr(10))
stale += 1
else:
newbody.append(l)
lines = lines[:start + 1] + newbody + lines[end:]
end = start + 1 + len(newbody) # end 因注释重排而失效,按新长度重算
known = set()
for l in lines[start + 1:end]:
if '=' in l and '"' in l:
known.add(l.split('"')[1])
for n in gens:
if n in known:
continue
lab = have.get(n, "")
parts = lab.split()
desc = ' '.join(parts[2:]) if len(parts) > 2 else lab
if not desc:
continue
pad = ' ' if len(n) >= 2 else ' '
add.append(' "' + n + '"' + pad + '= "' + desc + '";' + chr(10))
if add or stale:
open(mod, 'w', encoding='utf-8').write("".join(lines[:end] + add + lines[end:]))
print('[boot-menu-names] explicit 表:新登记 ' + str(len(add)) + ' 条,'
+ '失效注释 ' + str(stale) + ' 条(注释保留供追溯)')
else:
print('[boot-menu-names] explicit 表无需变更')
PY
fi
fi
# 精确定位「本次 rebuild 产生的世代」:execution stamp 匹配 + id > 执行前最大 id(冗余校验)
if [ -n "$LABEL_IN" ]; then
STAMP_IN=$(printf '%s' "$LABEL_IN" | cut -c1-19)
NEWGEN=""
for n in $(ls -d $PROF/system-*-link 2>/dev/null | sed 's#.*/system-##;s#-link##'); do
if [ -n "$PREV_MAX" ] && [ "$n" -le "$PREV_MAX" ]; then continue; fi
raw=$(grep -o '"label"[[:space:]]*:[[:space:]]*"[^"]*"' "$PROF/system-$n-link/boot.json" 2>/dev/null | head -1 | cut -d'"' -f4)
case "$raw" in *"$STAMP_IN"*) NEWGEN="$n" ;; esac
done
if [ -z "$NEWGEN" ]; then
echo "boot-menu-names: 本次未产生新世代(配置无变化),描述 [$LABEL_IN] 未登记到任何世代" >&2
else
echo "boot-menu-names: 本次新世代 = gen$NEWGEN(mtime $(stamp_of "$NEWGEN"))"
fi
fi
echo "boot-menu-names: 完成(共 $(ls $ENT/*.conf 2>/dev/null | wc -l) 个条目),映射 -> $MAP"
'';
in
{
config = {
# 也装进系统 PATH,便于手动重放/排查(boot-menu-names)
environment.systemPackages = [ rename ];
# 在 systemd-boot builder 装完条目之后立刻重放命名(root 上下文,/boot 就是真实 ESP)
boot.loader.systemd-boot.extraInstallCommands = lib.mkAfter "${rename}/bin/boot-menu-names";
};
}