docs(v0.2): план реализации boot-конвейера (План 6)
P6.1 общий рендер-хелпер shturman-render (рефактор из shell) → P6.2 shturman-splash (Stage 0) → P6.3 фазовые systemd-таргеты + splash/warmup + зонтик → P6.4 justfile/lima/E2E-блок Stage 0/1/2 → P6.5 verify в Lima + acceptance. TDD-шаги с полным кодом, без плейсхолдеров. Self-review: покрытие спеки полное. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
@@ -0,0 +1,524 @@
|
||||
# План 6 — v0.2 Boot-конвейер (Stage 0/1/2 + splash)
|
||||
|
||||
> REQUIRED SUB-SKILL: `executing-plans` (или `subagent-driven-development`) + **TDD**. Спека: `docs/specs/v0.2-boot-pipeline.md`.
|
||||
> Шаги — чекбоксы `- [ ]`. Часть «verify в Lima» (P6.5) — тяжёлая (vm-reset + e2e), но VM уже поднята.
|
||||
|
||||
**Goal:** превратить плоский `shturman.target` (v0.1) в фазовый конвейер: Stage 0 (splash) → Stage 1 (ядро+кадр) →
|
||||
Stage 2 (warmup), с мгновенным splash до первого кадра и деферредом фона после.
|
||||
|
||||
**Architecture:** общий headless-render хелпер (Slint software-renderer → PNG) выделяется из `shturman-shell` в
|
||||
крейт `shturman-render`; новый `shturman-splash` его использует. systemd: зонтик `shturman.target` тянет три
|
||||
под-таргета; splash `Before=shell`, warmup `After=shell`. Новой D-Bus-поверхности нет.
|
||||
|
||||
**Tech Stack:** Rust, Slint 1.16 (software-renderer + `png`), systemd (targets/oneshot), Lima VM, bash E2E.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- **Create** `crates/apps/shturman-render/` — `Cargo.toml`, `src/lib.rs` (хелпер `render_to_png`), `tests/render.rs`.
|
||||
- **Create** `crates/apps/shturman-splash/` — `Cargo.toml`, `src/lib.rs` (Slint splash + `render_splash`), `src/main.rs` (CLI), `tests/screenshot.rs`.
|
||||
- **Modify** `crates/apps/shturman-shell/src/lib.rs` — использовать `shturman_render::render_to_png` (убрать своё плумбинг-дублирование), `Cargo.toml` (+ dep `shturman-render`).
|
||||
- **Modify** `Cargo.toml` (workspace) — добавить два крейта в `members`.
|
||||
- **Create** `systemd/`: `shturman-stage0.target`, `shturman-stage1.target`, `shturman-stage2.target`, `shturman-splash.service`, `shturman-stage2-warmup.service`, `tmpfiles-shturman.conf`.
|
||||
- **Modify** `systemd/shturman.target` (→ зонтик), `shturman-{firstboot,machineid,power,settings,shell}.service` (`WantedBy=` → `shturman-stage1.target`), `shturman-shell.service` (убрать `RuntimeDirectory`, `/run/shturman` даёт tmpfiles).
|
||||
- **Modify** `justfile` (+ `splash-frame`), `lima/shturman.yaml` (разложить новые юниты + tmpfiles), `tests/e2e/run.sh` (+ блок Stage 0/1/2 + install splash-бинаря).
|
||||
|
||||
---
|
||||
|
||||
## P6.1: крейт `shturman-render` — общий headless-рендер (рефактор из shell)
|
||||
|
||||
**Files:** Create `crates/apps/shturman-render/{Cargo.toml,src/lib.rs,tests/render.rs}`; Modify workspace `Cargo.toml`, `crates/apps/shturman-shell/{Cargo.toml,src/lib.rs}`.
|
||||
|
||||
- [ ] **Шаг 1 — падающий тест** `crates/apps/shturman-render/tests/render.rs`:
|
||||
|
||||
```rust
|
||||
use shturman_render::render_to_png;
|
||||
|
||||
slint::slint! {
|
||||
export component Probe inherits Window {
|
||||
width: 64px; height: 48px; background: #101418;
|
||||
Rectangle { background: #ffffff; width: 20px; height: 20px; }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_component_to_nonempty_png() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("probe.png");
|
||||
render_to_png(|| Ok(Probe::new()?), 64, 48, &path).expect("render");
|
||||
let dec = png::Decoder::new(std::fs::File::open(&path).unwrap());
|
||||
let mut r = dec.read_info().unwrap();
|
||||
assert_eq!((r.info().width, r.info().height), (64, 48));
|
||||
let mut buf = vec![0u8; r.output_buffer_size()];
|
||||
let info = r.next_frame(&mut buf).unwrap();
|
||||
let px = &buf[..info.buffer_size()];
|
||||
assert!(px.iter().any(|&b| b != px[0]), "кадр одноцветный");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Шаг 2 — `Cargo.toml` крейта** (`crates/apps/shturman-render/Cargo.toml`):
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "shturman-render"
|
||||
version = "0.0.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
slint.workspace = true
|
||||
png = "0.17"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
```
|
||||
|
||||
Добавить `"crates/apps/shturman-render"` в `members` корневого `Cargo.toml`.
|
||||
|
||||
- [ ] **Шаг 3 — прогнать тест, убедиться что НЕ компилируется/падает.** Run: `cargo test -p shturman-render`. Expected: FAIL (нет `render_to_png`).
|
||||
|
||||
- [ ] **Шаг 4 — реализация** `crates/apps/shturman-render/src/lib.rs` (вынести из текущего `shturman-shell/src/lib.rs`):
|
||||
|
||||
```rust
|
||||
//! Headless software-render Slint-компонента в PNG (без дисплея/композитора).
|
||||
//! Общий для shturman-shell (первый кадр) и shturman-splash (Stage 0). Спека v0.2 §4.1.
|
||||
|
||||
use slint::platform::software_renderer::{MinimalSoftwareWindow, RepaintBufferType};
|
||||
use slint::platform::{Platform, PlatformError, WindowAdapter};
|
||||
use slint::ComponentHandle;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Once;
|
||||
|
||||
thread_local! {
|
||||
static WINDOW: Rc<MinimalSoftwareWindow> =
|
||||
MinimalSoftwareWindow::new(RepaintBufferType::ReusedBuffer);
|
||||
}
|
||||
|
||||
struct SwPlatform;
|
||||
impl Platform for SwPlatform {
|
||||
fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError> {
|
||||
Ok(WINDOW.with(|w| w.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_platform() {
|
||||
static ONCE: Once = Once::new();
|
||||
ONCE.call_once(|| {
|
||||
let _ = slint::platform::set_platform(Box::new(SwPlatform));
|
||||
});
|
||||
}
|
||||
|
||||
/// Построить компонент (ВНУТРИ — после `set_platform`) и отрендерить его кадр в PNG.
|
||||
/// `build` зовётся после установки software-platform (порядок обязателен для Slint).
|
||||
pub fn render_to_png<C: ComponentHandle>(
|
||||
build: impl FnOnce() -> anyhow::Result<C>,
|
||||
w: u32,
|
||||
h: u32,
|
||||
path: &Path,
|
||||
) -> anyhow::Result<()> {
|
||||
ensure_platform();
|
||||
let ui = build()?;
|
||||
let window = WINDOW.with(|x| x.clone());
|
||||
window.set_size(slint::PhysicalSize::new(w, h));
|
||||
ui.show()?;
|
||||
ui.window().request_redraw();
|
||||
let mut buf = vec![slint::Rgb8Pixel { r: 0, g: 0, b: 0 }; (w * h) as usize];
|
||||
let drawn = window.draw_if_needed(|r| {
|
||||
r.render(buf.as_mut_slice(), w as usize);
|
||||
});
|
||||
ui.hide()?;
|
||||
if !drawn {
|
||||
anyhow::bail!("software-renderer не отрисовал кадр");
|
||||
}
|
||||
write_png(path, w, h, &buf)
|
||||
}
|
||||
|
||||
fn write_png(path: &Path, w: u32, h: u32, buf: &[slint::Rgb8Pixel]) -> anyhow::Result<()> {
|
||||
let mut enc = png::Encoder::new(std::io::BufWriter::new(std::fs::File::create(path)?), w, h);
|
||||
enc.set_color(png::ColorType::Rgb);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut writer = enc.write_header()?;
|
||||
let mut data = Vec::with_capacity((w * h * 3) as usize);
|
||||
for px in buf {
|
||||
data.extend_from_slice(&[px.r, px.g, px.b]);
|
||||
}
|
||||
writer.write_image_data(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Шаг 5 — прогнать тест: PASS.** Run: `cargo test -p shturman-render`. Expected: PASS.
|
||||
|
||||
- [ ] **Шаг 6 — рефактор shell на хелпер.** В `crates/apps/shturman-shell/Cargo.toml` добавить `shturman-render = { path = "../shturman-render" }`. В `crates/apps/shturman-shell/src/lib.rs`: удалить локальные `SCREENSHOT_WINDOW`/`ScreenshotPlatform`/`ensure_screenshot_platform`/`write_png`; `render_screenshot` стал:
|
||||
|
||||
```rust
|
||||
pub fn render_screenshot(initial: &Initial, hour: u8, path: &Path) -> anyhow::Result<()> {
|
||||
let (_, clock) = utc_hh_mm();
|
||||
shturman_render::render_to_png(|| build_ui(initial, hour, &clock), FRAME_W, FRAME_H, path)?;
|
||||
tracing::info!(path = %path.display(), "кадр записан (software-render)");
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
(Импорты `slint::platform::*`, `std::rc::Rc`, `std::sync::Once` в shell больше не нужны — убрать.)
|
||||
|
||||
- [ ] **Шаг 7 — прогнать тесты shell + воркспейс.** Run: `cargo test -p shturman-shell && cargo test --workspace`. Expected: PASS (screenshot-тест shell зелёный на новом хелпере).
|
||||
|
||||
- [ ] **Шаг 8 — commit.**
|
||||
|
||||
```bash
|
||||
git add crates/apps/shturman-render crates/apps/shturman-shell Cargo.toml Cargo.lock
|
||||
git commit -s -m "refactor(v0.2): вынести headless render в shturman-render (shell использует)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## P6.2: `shturman-splash` — Stage-0 splash-бинарь
|
||||
|
||||
**Files:** Create `crates/apps/shturman-splash/{Cargo.toml,src/lib.rs,src/main.rs,tests/screenshot.rs}`; Modify workspace `Cargo.toml`.
|
||||
|
||||
- [ ] **Шаг 1 — падающий тест** `crates/apps/shturman-splash/tests/screenshot.rs`:
|
||||
|
||||
```rust
|
||||
use shturman_splash::render_splash;
|
||||
|
||||
#[test]
|
||||
fn renders_dark_branded_splash() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("splash.png");
|
||||
render_splash(&path).expect("render_splash");
|
||||
let dec = png::Decoder::new(std::fs::File::open(&path).unwrap());
|
||||
let mut r = dec.read_info().unwrap();
|
||||
assert_eq!((r.info().width, r.info().height), (1024, 600));
|
||||
let mut buf = vec![0u8; r.output_buffer_size()];
|
||||
let info = r.next_frame(&mut buf).unwrap();
|
||||
let px = &buf[..info.buffer_size()];
|
||||
// фон тёмный (угол) + не одноцветный (wordmark отрисован)
|
||||
assert!(px[0] < 64 && px[1] < 64 && px[2] < 64, "splash фон не тёмный: {},{},{}", px[0], px[1], px[2]);
|
||||
assert!(px.iter().any(|&b| b != px[0]), "splash одноцветный — wordmark не отрисован");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Шаг 2 — `Cargo.toml`** (`crates/apps/shturman-splash/Cargo.toml`) + добавить в workspace `members`:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "shturman-splash"
|
||||
version = "0.0.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
shturman-render = { path = "../shturman-render" }
|
||||
shturman-common = { path = "../../shturman-common" }
|
||||
anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
slint.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
png = "0.17"
|
||||
```
|
||||
|
||||
- [ ] **Шаг 3 — тест падает.** Run: `cargo test -p shturman-splash`. Expected: FAIL (нет `render_splash`).
|
||||
|
||||
- [ ] **Шаг 4 — реализация** `crates/apps/shturman-splash/src/lib.rs`:
|
||||
|
||||
```rust
|
||||
//! `shturman-splash` (lib) — Stage-0 splash-кадр (A05). Статичный брендовый кадр (без шины → «мгновенно»).
|
||||
//! Визуальные токены — каркас (язык design-system — гейт v0.5). Спека v0.2 §6.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
slint::slint! {
|
||||
export component SplashWindow inherits Window {
|
||||
in property <string> brand: "Штурман";
|
||||
width: 1024px;
|
||||
height: 600px;
|
||||
background: #0e1014;
|
||||
Text {
|
||||
text: root.brand;
|
||||
font-size: 64px;
|
||||
color: #e8eaed;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const W: u32 = 1024;
|
||||
const H: u32 = 600;
|
||||
|
||||
/// Headless software-render splash-кадра в PNG (без дисплея/композитора).
|
||||
pub fn render_splash(path: &Path) -> anyhow::Result<()> {
|
||||
shturman_render::render_to_png(|| Ok(SplashWindow::new()?), W, H, path)?;
|
||||
tracing::info!(path = %path.display(), "splash записан (software-render)");
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Шаг 5 — `main.rs`** `crates/apps/shturman-splash/src/main.rs`:
|
||||
|
||||
```rust
|
||||
//! `shturman-splash` (bin) — Stage-0 splash. `--screenshot <path>` → headless PNG (VM/E2E);
|
||||
//! без аргументов — интерактив (dev/HW; в v0.6 VM используется только screenshot-режим).
|
||||
|
||||
use shturman_splash::render_splash;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
shturman_common::init_tracing("shturman-splash");
|
||||
match parse_screenshot_arg() {
|
||||
Some(path) => {
|
||||
render_splash(&path)?;
|
||||
println!("{}", path.display());
|
||||
}
|
||||
None => {
|
||||
// интерактив придёт с живым дисплеем (v0.5); в v0 VM splash — только screenshot.
|
||||
anyhow::bail!("ожидался --screenshot <path> (интерактивный splash — v0.5)");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_screenshot_arg() -> Option<PathBuf> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(a) = args.next() {
|
||||
if a == "--screenshot" {
|
||||
return args.next().map(PathBuf::from);
|
||||
}
|
||||
if let Some(p) = a.strip_prefix("--screenshot=") {
|
||||
return Some(PathBuf::from(p));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Шаг 6 — тест PASS + lint.** Run: `cargo test -p shturman-splash && cargo clippy -p shturman-splash --all-targets -- -D warnings`. Expected: PASS, без warnings.
|
||||
|
||||
- [ ] **Шаг 7 — глазами (опц.).** Run: `cargo run -p shturman-splash -- --screenshot target/splash.png` → открыть PNG (тёмный фон + «Штурман»).
|
||||
|
||||
- [ ] **Шаг 8 — commit.**
|
||||
|
||||
```bash
|
||||
git add crates/apps/shturman-splash Cargo.toml Cargo.lock
|
||||
git commit -s -m "feat(v0.2): shturman-splash — Stage-0 splash (software-render → PNG)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## P6.3: systemd — фазовые таргеты + splash/warmup + рефактор (зонтик)
|
||||
|
||||
**Files:** Create 6 файлов в `systemd/`; Modify `shturman.target` + 5 сервисов.
|
||||
|
||||
- [ ] **Шаг 1 — `systemd/tmpfiles-shturman.conf`** (создаёт `/run/shturman` на boot — общий для splash/shell/warmup; tmpfs/volatile, A11):
|
||||
|
||||
```
|
||||
# /run/shturman — volatile-каталог кадров/маркеров (splash.png, frame.png, stage2.ready). A11.
|
||||
d /run/shturman 0755 root root -
|
||||
```
|
||||
|
||||
- [ ] **Шаг 2 — `systemd/shturman-stage0.target`:**
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Штурман Stage 0 — splash (мгновенно)
|
||||
Wants=shturman-splash.service
|
||||
```
|
||||
|
||||
- [ ] **Шаг 3 — `systemd/shturman-stage1.target`** (нынешний critical set v0.1):
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Штурман Stage 1 — ядро + первый кадр
|
||||
Requires=data.mount
|
||||
After=data.mount
|
||||
Wants=shturman-firstboot.service shturman-machineid.service shturman-power.service shturman-settings.service shturman-shell.service
|
||||
```
|
||||
|
||||
- [ ] **Шаг 4 — `systemd/shturman-stage2.target`:**
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Штурман Stage 2 — фон (после интерактива)
|
||||
After=shturman-stage1.target
|
||||
Wants=shturman-stage2-warmup.service
|
||||
```
|
||||
|
||||
- [ ] **Шаг 5 — `systemd/shturman-splash.service`** (Stage 0; минимум зависимостей; до первого кадра):
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Штурман splash (Stage 0, software-render → PNG)
|
||||
# «Мгновенно»: без Requires=data.mount/dbus — стартует рано, параллельно critical set.
|
||||
# Before=shell гарантирует splash.png раньше frame.png. /run/shturman даёт tmpfiles.
|
||||
After=systemd-tmpfiles-setup.service
|
||||
Before=shturman-shell.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/local/bin/shturman-splash --screenshot /run/shturman/splash.png
|
||||
TimeoutStartSec=15
|
||||
|
||||
[Install]
|
||||
WantedBy=shturman-stage0.target
|
||||
```
|
||||
|
||||
- [ ] **Шаг 6 — `systemd/shturman-stage2-warmup.service`** (плейсхолдер фона; после кадра):
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Штурман Stage 2 warmup (плейсхолдер фона)
|
||||
After=shturman-shell.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/bin/sh -c 'echo "stage2 warmup" | systemd-cat -t shturman-stage2; : > /run/shturman/stage2.ready'
|
||||
|
||||
[Install]
|
||||
WantedBy=shturman-stage2.target
|
||||
```
|
||||
|
||||
- [ ] **Шаг 7 — рефактор `systemd/shturman.target` → зонтик:**
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Штурман — v0 boot-конвейер (зонтик фаз Stage 0/1/2)
|
||||
Requires=data.mount
|
||||
After=data.mount
|
||||
Wants=shturman-stage0.target shturman-stage1.target shturman-stage2.target
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
- [ ] **Шаг 8 — переключить членство сервисов на Stage 1.** В `systemd/shturman-firstboot.service`, `shturman-machineid.service`, `shturman-power.service`, `shturman-settings.service`, `shturman-shell.service` заменить `WantedBy=shturman.target` → `WantedBy=shturman-stage1.target`. (Ordering внутри Stage 1 — без изменений: `After=`/`Requires=` в самих юнитах.)
|
||||
|
||||
- [ ] **Шаг 9 — у `shturman-shell.service` убрать `RuntimeDirectory=shturman`** (каталог теперь от tmpfiles; иначе рестарт shell снёс бы splash.png). Строку `RuntimeDirectory=shturman` удалить.
|
||||
|
||||
- [ ] **Шаг 10 — commit** (конфиги; проверка — в P6.5/Lima).
|
||||
|
||||
```bash
|
||||
git add systemd/
|
||||
git commit -s -m "feat(v0.2): фазовые systemd-таргеты Stage 0/1/2 + splash/warmup + зонтик"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## P6.4: justfile + lima provisioning + E2E-блок Stage 0/1/2
|
||||
|
||||
**Files:** Modify `justfile`, `lima/shturman.yaml`, `tests/e2e/run.sh`.
|
||||
|
||||
- [ ] **Шаг 1 — `justfile`: цель `splash-frame`** (после `shell-frame`):
|
||||
|
||||
```just
|
||||
# инспекция splash-кадра: headless software-render → PNG
|
||||
splash-frame path="target/splash-frame.png":
|
||||
cargo run -q -p shturman-splash -- --screenshot {{path}}
|
||||
@echo "splash записан: {{path}}"
|
||||
```
|
||||
|
||||
- [ ] **Шаг 2 — `lima/shturman.yaml`: разложить новые юниты.** В provision-скрипте, рядом с установкой `systemd/`-юнитов, добавить tmpfiles (новые `.target`/`.service` ставятся тем же `install -m644 /shturman/systemd/shturman-*.service` + явно таргеты):
|
||||
|
||||
```bash
|
||||
install -m644 /shturman/systemd/shturman-stage0.target /etc/systemd/system/
|
||||
install -m644 /shturman/systemd/shturman-stage1.target /etc/systemd/system/
|
||||
install -m644 /shturman/systemd/shturman-stage2.target /etc/systemd/system/
|
||||
install -d /etc/tmpfiles.d
|
||||
install -m644 /shturman/systemd/tmpfiles-shturman.conf /etc/tmpfiles.d/shturman.conf
|
||||
systemd-tmpfiles --create /etc/tmpfiles.d/shturman.conf || true
|
||||
```
|
||||
|
||||
(Существующий `install -m644 /shturman/systemd/shturman-*.service` уже подхватит `shturman-splash.service`/`shturman-stage2-warmup.service`.)
|
||||
|
||||
- [ ] **Шаг 3 — `tests/e2e/run.sh`: установить splash-бинарь.** В цикле install (`for b in firstboot settings power shell`) добавить `splash`:
|
||||
|
||||
```bash
|
||||
for b in firstboot settings power shell splash; do
|
||||
sudo install -m755 "$CARGO_TARGET_DIR/release/shturman-$b" /usr/local/bin/ || fail "install shturman-$b"
|
||||
done
|
||||
```
|
||||
|
||||
- [ ] **Шаг 4 — `tests/e2e/run.sh`: разложить новые юниты + tmpfiles.** ⚠️ `run.sh` ставит сервисы **явным
|
||||
списком** (НЕ glob — в отличие от lima yaml), поэтому splash/warmup добавляем явно. В блоке раскладки юнитов:
|
||||
(а) к строке `install … shturman.target … data.mount …` добавить три таргета; (б) к строке install сервисов
|
||||
добавить splash+warmup; (в) добавить tmpfiles. Итог блока:
|
||||
|
||||
```bash
|
||||
sudo install -m644 systemd/shturman.target systemd/data.mount \
|
||||
systemd/shturman-stage0.target systemd/shturman-stage1.target systemd/shturman-stage2.target \
|
||||
/etc/systemd/system/
|
||||
sudo install -m644 systemd/shturman-firstboot.service systemd/shturman-machineid.service \
|
||||
systemd/shturman-power.service systemd/shturman-settings.service \
|
||||
systemd/shturman-shell.service systemd/shturman-splash.service \
|
||||
systemd/shturman-stage2-warmup.service /etc/systemd/system/
|
||||
sudo install -d /etc/tmpfiles.d
|
||||
sudo install -m644 systemd/tmpfiles-shturman.conf /etc/tmpfiles.d/shturman.conf
|
||||
sudo systemd-tmpfiles --create /etc/tmpfiles.d/shturman.conf || true
|
||||
```
|
||||
|
||||
- [ ] **Шаг 5 — `tests/e2e/run.sh`: блок «Stage 0/1/2»** (вставить после §7 «первый кадр», до §8):
|
||||
|
||||
```bash
|
||||
# ---- Stage 0/1/2 разделены (v0.2) ----
|
||||
info "Stage 0/1/2: фазы разделены + splash до кадра + warmup после"
|
||||
for t in shturman-stage0 shturman-stage1 shturman-stage2; do
|
||||
systemctl is-active --quiet "$t.target" || fail "$t.target не достигнут ($(systemctl is-active "$t.target" 2>&1))"
|
||||
pass "$t.target reached"
|
||||
done
|
||||
sudo test -f /run/shturman/splash.png || fail "нет splash.png (Stage 0)"
|
||||
sudo head -c8 /run/shturman/splash.png | od -An -tx1 | tr -d ' \n' | grep -qi 89504e47 || fail "splash.png не PNG"
|
||||
sp=$(sudo stat -c %Y /run/shturman/splash.png); fr=$(sudo stat -c %Y /run/shturman/frame.png)
|
||||
[ "$sp" -le "$fr" ] || fail "splash.png позже frame.png ($sp > $fr)"
|
||||
sudo test -f /run/shturman/stage2.ready || fail "нет stage2.ready (warmup не отработал)"
|
||||
w2=$(sudo stat -c %Y /run/shturman/stage2.ready)
|
||||
[ "$w2" -ge "$fr" ] || fail "stage2.ready ($w2) раньше кадра ($fr)"
|
||||
pass "порядок фаз: splash($sp) ≤ frame($fr) ≤ stage2($w2)"
|
||||
# boot-тайминг (функц., НЕ гейт; вердикт — RK3588)
|
||||
echo " $(systemd-analyze time 2>/dev/null | head -1 || echo 'systemd-analyze н/д')"
|
||||
```
|
||||
|
||||
- [ ] **Шаг 6 — commit.**
|
||||
|
||||
```bash
|
||||
git add justfile lima/shturman.yaml tests/e2e/run.sh
|
||||
git commit -s -m "feat(v0.2): splash-frame + lima/E2E раскладка Stage 0/1/2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## P6.5: verify в Lima + acceptance + синхронизация доков
|
||||
|
||||
- [ ] **Шаг 1 — host-гейт.** Run: `just ci`. Expected: exit 0 (все unit-тесты, включая `shturman-render`/`shturman-splash`; clippy; deny).
|
||||
|
||||
- [ ] **Шаг 2 — чистый E2E с нуля.** Run: `just vm-reset && just e2e`. Expected: exit 0; в выводе — `shturman-stage0/1/2.target reached`, `splash($sp) ≤ frame($fr) ≤ stage2($w2)`, **вся приёмка v0.1/v0.6 зелёная** (нет регресса), `E2E OK ✅`.
|
||||
|
||||
- [ ] **Шаг 3 — если падёт — итерировать** по реальным ошибкам (ordering таргетов, glob юнитов, tmpfiles, splash before shell) и повторить P6.5 шаг 2. (Систематически: один симптом → одна правка.)
|
||||
|
||||
- [ ] **Шаг 4 — синхронизация доков (швы спеки §10):** в `docs/domains/a-base-system.md` §4 / `docs/architecture.md` §6 — пометка «dev-VM Stage-0-splash = systemd software-render PNG; U-Boot framebuffer — HW»; `docs/roadmap.md` §v0.2 → ✅; `docs/specs/v0.1-v0.6-foundation.md` §13 — шов «shturman.target → зонтик, critical set → stage1.target»; `CLAUDE.md` — статус v0.2 готово → следующее v0.3/v0.5.
|
||||
|
||||
- [ ] **Шаг 5 — commit доков.**
|
||||
|
||||
```bash
|
||||
git add docs/ CLAUDE.md
|
||||
git commit -s -m "docs(v0.2): синхронизация швов boot-конвейера + статус"
|
||||
```
|
||||
|
||||
- [ ] **Шаг 6 — finishing-a-development-branch** (merge/PR — спросить пользователя; в `main` без явного «ок» не мержить).
|
||||
|
||||
---
|
||||
|
||||
## Acceptance (спека v0.2 §9.3)
|
||||
|
||||
- [ ] `shturman.target` = зонтик; `shturman-stage0/1/2.target` достижимы и разделены (per-target active).
|
||||
- [ ] Splash-кадр (`splash.png`) непустой и рендерится **раньше** первого кадра Shell (`frame.png`).
|
||||
- [ ] Stage 2 (warmup) стартует **после** первого кадра (`stage2.ready` mtime ≥ frame).
|
||||
- [ ] Boot-тайминг логируется (`systemd-analyze`); <10 c — пометка «вердикт на RK3588».
|
||||
- [ ] Вся приёмка v0.1/v0.6 (foundation §9.4) — зелёная на фазовой раскладке (нет регресса).
|
||||
- [ ] `just ci` зелёный; красные линии целы (нет CAN/actuator).
|
||||
Reference in New Issue
Block a user