Загрузочное меню ISO было оформлено в стиле Virtuality

- графическая тема GRUB (фон, подсветка выбранного пункта, полоса
  автозапуска) в image/grub, подключается в build-iso.sh; при отсутствии
  модулей GRUB меню остаётся текстовым;
- пункты меню переведены: «Установить Virtuality X.Y.Z», «Загрузить
  с другого диска», «Настройки UEFI», «Проверить память»;
- tests/render_html.mjs рендерит HTML в PNG (для фона темы).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
Этот коммит содержится в:
Claude
2026-09-25 14:22:11 +00:00
родитель 1e29134fb3
Коммит e52967fba1
15 изменённых файлов: 110 добавлений и 6 удалений
+28 -6
Просмотреть файл
@@ -216,20 +216,41 @@ import sys
path, unattended, version = sys.argv[1], sys.argv[2] == "1", sys.argv[3] path, unattended, version = sys.argv[1], sys.argv[2] == "1", sys.argv[3]
text = open(path).read() text = open(path).read()
# /autoinstall.yaml applies to every entry, so all Ubuntu entries become Virtuality entries. # /autoinstall.yaml applies to every entry, so all Ubuntu entries become Virtuality entries.
suffix = " (UNATTENDED: erases disk)" if unattended else "" suffix = " — автоматически, диск будет стёрт" if unattended else ""
text = text.replace('menuentry "Try or Install Ubuntu Server"', 'menuentry "Install Virtuality %s%s"' % (version, suffix), 1) text = text.replace('menuentry "Try or Install Ubuntu Server"', 'menuentry "Установить Virtuality %s%s"' % (version, suffix), 1)
text = text.replace('menuentry "Ubuntu Server with the HWE kernel"', 'menuentry "Install Virtuality %s with the HWE kernel%s"' % (version, suffix), 1) text = text.replace('menuentry "Ubuntu Server with the HWE kernel"', 'menuentry "Установить Virtuality %s (новое ядро HWE)%s"' % (version, suffix), 1)
if "Install Virtuality" not in text: text = text.replace("menuentry 'Boot from next volume'", "menuentry 'Загрузить с другого диска'", 1)
text = text.replace("menuentry 'UEFI Firmware Settings'", "menuentry 'Настройки UEFI'", 1)
text = text.replace("menuentry 'Test memory'", "menuentry 'Проверить память'", 1)
if "Установить Virtuality" not in text:
raise SystemExit("unexpected grub.cfg menu titles") raise SystemExit("unexpected grub.cfg menu titles")
if unattended: if unattended:
text = re.sub(r"^(\s*linux\s+\S+\s+)---", r"\1autoinstall ---", text, flags=re.M) text = re.sub(r"^(\s*linux\s+\S+\s+)---", r"\1autoinstall ---", text, flags=re.M)
text = re.sub(r"^set timeout=\d+", "set default=0\nset timeout=10", text, count=1, flags=re.M) text = re.sub(r"^set timeout=\d+\n", "", text, count=1, flags=re.M)
open(path, "w").write(text) text = text.replace("loadfont unicode\n", "", 1)
# Branded graphical menu; if a module or the font is missing GRUB falls back to the plain text menu.
theme = """insmod all_video
insmod gfxterm
insmod png
insmod gfxmenu
set gfxmode=1024x768,auto
if loadfont /boot/grub/fonts/unicode.pf2; then
terminal_output gfxterm
set theme=/boot/grub/themes/virtuality/theme.txt
export theme
fi
set default=0
set timeout=10
"""
open(path, "w").write(theme + text)
PYEOF PYEOF
new_md5="$(md5sum "${WORK_DIR}/grub.cfg" | cut -d' ' -f1)" new_md5="$(md5sum "${WORK_DIR}/grub.cfg" | cut -d' ' -f1)"
sed -i "s|^[0-9a-f]\{32\} \./boot/grub/grub.cfg$|${new_md5} ./boot/grub/grub.cfg|" "${WORK_DIR}/md5sum.txt" sed -i "s|^[0-9a-f]\{32\} \./boot/grub/grub.cfg$|${new_md5} ./boot/grub/grub.cfg|" "${WORK_DIR}/md5sum.txt"
# ---------------------------------------------------------- repack # ---------------------------------------------------------- repack
THEME_DIR="${WORK_DIR}/theme"
mkdir -p "$THEME_DIR"
cp "${IMAGE_DIR}"/grub/theme.txt "${IMAGE_DIR}"/grub/*.png "$THEME_DIR"/
say "Writing ${OUTPUT_ISO}" say "Writing ${OUTPUT_ISO}"
rm -f "$OUTPUT_ISO" rm -f "$OUTPUT_ISO"
chmod -R a+rX,go-w "$PAYLOAD" chmod -R a+rX,go-w "$PAYLOAD"
@@ -238,6 +259,7 @@ if ! xorriso -indev "$UBUNTU_ISO" -outdev "$OUTPUT_ISO" \
-map "$PAYLOAD" /virtuality \ -map "$PAYLOAD" /virtuality \
-map "${WORK_DIR}/autoinstall.yaml" /autoinstall.yaml \ -map "${WORK_DIR}/autoinstall.yaml" /autoinstall.yaml \
-map "${WORK_DIR}/grub.cfg" /boot/grub/grub.cfg \ -map "${WORK_DIR}/grub.cfg" /boot/grub/grub.cfg \
-map "${THEME_DIR}" /boot/grub/themes/virtuality \
-map "${WORK_DIR}/md5sum.txt" /md5sum.txt \ -map "${WORK_DIR}/md5sum.txt" /md5sum.txt \
-boot_image any replay > "${WORK_DIR}/xorriso.log" 2>&1; then -boot_image any replay > "${WORK_DIR}/xorriso.log" 2>&1; then
tail -n 30 "${WORK_DIR}/xorriso.log" >&2 tail -n 30 "${WORK_DIR}/xorriso.log" >&2
+12
Просмотреть файл
@@ -0,0 +1,12 @@
# Тема загрузочного меню
`theme.txt` — тема GRUB (gfxmenu) для установочного ISO, `background.png` — фон 1024×768,
`select_*.png` — подсветка выбранного пункта.
Фон рендерится из `background.html` (тот же стиль, что у страницы входа панели):
```bash
NODE_PATH=$(npm root -g) node tests/render_html.mjs image/grub/background.html image/grub/background.png 1024 768
```
Пиксмапы подсветки — однотонные RGBA 6×6 (110, 96, 255, 150).
+19
Просмотреть файл
@@ -0,0 +1,19 @@
<!doctype html><html><head><meta charset="utf-8"><style>
html,body{margin:0;width:1024px;height:768px;overflow:hidden;font-family:Inter,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
body{background:radial-gradient(900px 520px at 8% 0%, rgba(123,107,255,.55), transparent 60%), radial-gradient(760px 520px at 100% 100%, rgba(47,115,240,.45), transparent 55%), #0b0a1f;color:#fff;position:relative}
.grid{position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.045) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.045) 1px,transparent 1px);background-size:48px 48px;-webkit-mask-image:radial-gradient(ellipse at 30% 30%,#000 15%,transparent 72%)}
.brand{position:absolute;left:72px;top:64px;display:flex;align-items:center;gap:18px}
.mark{width:64px;height:64px;border-radius:18px;background:linear-gradient(135deg,#7b6bff 0%,#5a48f5 45%,#2f73f0 100%);box-shadow:0 14px 40px -10px rgba(90,72,245,.9), inset 0 1px 0 rgba(255,255,255,.3);display:grid;place-items:center}
.mark svg{width:38px;height:38px}
.name{font-size:38px;font-weight:700;letter-spacing:-.02em;line-height:1}
.name small{display:block;margin-top:8px;font-size:16px;font-weight:500;color:rgba(255,255,255,.62);letter-spacing:0}
.tag{position:absolute;left:72px;top:176px;font-size:30px;font-weight:600;letter-spacing:-.02em;line-height:1.25;max-width:620px}
.tag span{background:linear-gradient(90deg,#c4bcff,#8fb8ff);-webkit-background-clip:text;background-clip:text;color:transparent}
.foot{position:absolute;left:72px;bottom:56px;color:rgba(255,255,255,.5);font-size:15px}
.hint{position:absolute;right:72px;bottom:56px;color:rgba(255,255,255,.5);font-size:15px;text-align:right}
</style></head><body><div class="grid"></div>
<div class="brand"><div class="mark"><svg viewBox="0 0 24 24" fill="none"><path d="M4.5 5.5 12 19l7.5-13.5" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 5.5h6" stroke="#fff" stroke-opacity=".55" stroke-width="2.6" stroke-linecap="round"/></svg></div><div class="name">Virtuality<small>Платформа виртуализации</small></div></div>
<div class="tag">Ваши серверы.<br><span>Под полным контролем.</span></div>
<div class="foot">Установщик · KVM · QEMU · libvirt</div>
<div class="hint">↑ ↓ — выбор пункта, Enter — запуск</div>
</body></html>
Двоичные данные
Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 151 KiB

Двоичные данные
Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 75 B

Двоичные данные
Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 75 B

Двоичные данные
Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 75 B

Двоичные данные
Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 75 B

Двоичные данные
Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 75 B

Двоичные данные
Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 75 B

Двоичные данные
Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 75 B

Двоичные данные
Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 75 B

Двоичные данные
Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 75 B

+41
Просмотреть файл
@@ -0,0 +1,41 @@
desktop-image: "background.png"
desktop-image-scale-method: "stretch"
desktop-color: "#0b0a1f"
title-text: ""
terminal-font: "Unicode Regular"
terminal-left: "7%"
terminal-top: "36%"
terminal-width: "86%"
terminal-height: "50%"
terminal-border: "0"
+ boot_menu {
left = 7%
top = 36%
width = 60%
height = 42%
item_font = "Unicode Regular"
item_color = "#b9b8cf"
selected_item_color = "#ffffff"
item_height = 36
item_padding = 10
item_spacing = 6
icon_width = 0
icon_height = 0
scrollbar = false
selected_item_pixmap_style = "select_*.png"
}
+ progress_bar {
id = "__timeout__"
left = 7%
top = 86%
width = 60%
height = 24
font = "Unicode Regular"
text_color = "#ffffff"
fg_color = "#7b6bff"
bg_color = "#1c1a36"
border_color = "#2a2748"
text = "Автоматический запуск через %d с"
}
+10
Просмотреть файл
@@ -0,0 +1,10 @@
import { createRequire } from 'node:module';
const { chromium } = createRequire(import.meta.url)('playwright');
// Render an HTML file to PNG. Usage: node tests/render_html.mjs in.html out.png [width] [height]
const [,, html, out, width = '1024', height = '768'] = process.argv;
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: +width, height: +height }, deviceScaleFactor: 1 });
await page.goto('file://' + (html.startsWith('/') ? html : process.cwd() + '/' + html));
await page.waitForTimeout(300);
await page.screenshot({ path: out, type: 'png' });
await browser.close();