feat: add three-profile STEM container workflow

This commit is contained in:
mpabi
2026-07-14 17:49:36 +02:00
parent 9c0b833371
commit 61d00a6992
12 changed files with 1819 additions and 882 deletions
+250 -129
View File
@@ -1,162 +1,283 @@
# Kontenery i debug
`rvctl` integruje workspace z narzędziem `rv32i-hazard3-student-env`.
Środowisko ma dwa profile kontenerów:
Ten dokument opisuje wdrożony kontrakt kontenerów i jawnie wskazuje pozostałe
etapy. Pełne
uzasadnienie, przepływ ucznia i kolejność wdrożenia są w
`doc/architecture-plan.md`.
- `rv32i` - docelowe środowisko RISC-V/Hazard3 z `nvim`, `tmux`,
`gdb-multiarch`, toolchainem RISC-V i symulatorem Hazard3.
- `host` - natywne środowisko na architekturze hosta z `clang`, `gcc`, `gdb`,
opcjonalnie `lldb`, `valgrind`, `nvim` i `tmux`.
Stan obecny:
Profile są uruchamiane jako oddzielne usługi `docker compose`, więc zależności
RISC-V i zależności natywnego debugowania nie mieszają się ze sobą.
- `stemctl` uruchamia trzy profile przez rootless Podman;
- repo środowiska zawiera wieloetapowy `Dockerfile` i Compose z dokładnie
trzema usługami; gotowych obrazów nie publikujemy;
- profil RP2350 zawiera oba toolchainy, Pico SDK, FreeRTOS, OpenOCD i picotool;
- `rvctl`, `host` i `rv32i` pozostają aliasami okresu zgodności.
## Szybki przepływ
## Profile docelowe
| Profil | Alias przejściowy | Service | Target Dockerfile | Targety |
| --- | --- | --- | --- | --- |
| `native-amd64` | `host` | `native-amd64` | `native-amd64-final` | `native` |
| `hazard3-sim` | `rv32i` | `hazard3-sim` | `hazard3-sim-final` | `hazard3-baremetal`; `hazard3-freertos` po BSP timer/IRQ |
| `rp2350` | brak | `rp2350` | `rp2350-final` | `rp2350-rv`, `rp2350-arm` |
Wszystkie targety dziedziczą stage `dev-ui-base`. Nie jest on osobnym
kontenerem roboczym.
## Jednolity kontrakt CLI
```bash
./rvctl env list
./rvctl env sync
./rvctl env build rv32i
./rvctl env build host
stemctl env list
stemctl env ensure native-amd64
stemctl env ensure hazard3-sim
stemctl env ensure rp2350
./rvctl debug rv32i inf bss 1
./rvctl debug host inf bss 1
./rvctl run host inf bss 1
./rvctl shell rv32i inf bss
./rvctl shell host inf bss
stemctl build native-amd64 inf bss 1
stemctl test native-amd64 inf bss 1
stemctl run native-amd64 inf bss 1
stemctl debug native-amd64 inf bss 1
stemctl build hazard3-sim inf bss 1 --target hazard3-baremetal
stemctl test hazard3-sim inf bss 1 --target hazard3-baremetal
stemctl run hazard3-sim inf bss 1
stemctl debug hazard3-sim inf bss 1
stemctl build rp2350 inf bss 1 --target rp2350-rv
stemctl deploy rp2350 inf bss 1 --target rp2350-rv --device /dev/bus/usb/001/006
stemctl debug rp2350 inf bss 1 --target rp2350-rv --device /dev/bus/usb/001/006
```
Jeżeli `workspace.json` ma ustawione domyślne wartości `defaults.series`,
`defaults.card` i `defaults.task`, można używać krótszych komend:
```bash
./rvctl debug rv32i
./rvctl debug host
./rvctl debug rv32i 4
./rvctl debug host 4
```
## `env list`
Pokazuje dostępne profile i bieżący katalog narzędzia:
```bash
./rvctl env list
```
Typowy wynik:
Selektory `series card task` są opcjonalne, jeśli ustawiono wartości domyślne.
Każda akcja obsługuje:
```text
profile service image status purpose
rv32i env edu-inf/rv32i-hazard3-env:latest ready RV32I/Hazard3 nvim + gdb-multiarch
host host edu-inf/rv32i-hazard3-host-env:latest ready native host clang/gcc + gdb/lldb
--instance NAME
--target NAME
--editor nvim|vim
--dry-run
```
## `env sync`
`debug` dodatkowo może przyjąć `--pane TARGET` i `--device PATH`. `deploy`
przyjmuje `--device PATH` oraz `--backend probe|bootsel`. `probe list --json`
ma stabilny format maszynowy, a akcje zapisują maszynowy `result.json`.
Klonuje albo aktualizuje repo środowiska do:
`deploy` jest capability wyłącznie targetów sprzętowych RP2350. W
`hazard3-sim` artefakt ładuje `run` albo `debug`; osobne `deploy` zwraca
`unsupported`.
## Capabilities zamiast wyjątków w kodzie
Launcher nie powinien zawierać warunków typu „jeśli profil rv32i, uruchom ten
konkretny skrypt”. Profil publikuje capabilities i adaptery:
```json
{
"profile": "rp2350",
"targets": ["rp2350-rv", "rp2350-arm"],
"actions": {
"build": {"requires": []},
"test": {"requires": []},
"run": {"requires": ["matching-deploy-record"]},
"debug": {"requires": ["probe"]},
"deploy": {"requires": ["probe"]},
"shell": {"requires": []}
},
"debug_backends": ["openocd"],
"devices": ["debug-probe", "target-usb-optional"]
}
```
Karta deklaruje target, a profil dostarcza implementację. Brak capability jest
normalnym, maszynowo czytelnym wynikiem `unsupported`, nie awarią launchera.
## Wspólny interfejs w kontenerze
Każdy profil zapewnia kanoniczny dispatcher:
```text
~/dev/workspace/rv/tools/rv32i-hazard3-student-env
/usr/local/bin/stem-entry
/usr/local/bin/stem-card
```
Komenda:
Dispatcher przyjmuje `build/test/run/debug/deploy`; manifest może zwrócić
`unsupported`, jeżeli akcja nie ma sensu dla targetu.
Skrypty korzystają wyłącznie z manifestu karty i zmiennych kontraktowych:
```text
STEM_WORKSPACE=/workspace
STEM_PROFILE=hazard3-sim
STEM_TARGET=hazard3-freertos
STEM_CARD=inf/bss
STEM_TASK=task1
STEM_INSTANCE=hazard3-sim-inf-bss-t1
STEM_STATE_DIR=/workspace/.stem/instances/<instance>
STEM_ARTIFACT_DIR=/workspace/.stem/artifacts/<profile>/<target>/<task>
```
Stare `RV_*` są czytane wyłącznie jako fallback w okresie migracji.
## Montowania
Minimalny zestaw:
| Źródło hosta | Cel | Tryb |
| --- | --- | --- |
| repo karty | `/workspace` | `rw` |
| cache profilu | `/cache` | `rw`, osobny named volume |
| `$XDG_RUNTIME_DIR/stem` | ta sama krótka ścieżka | `rw` |
| konkretne urządzenie probe | urządzenie | tylko profil `rp2350` |
Nie montujemy:
- `$HOME/.ssh`;
- pliku tokenów;
- całego `/dev`;
- socketu Docker/Podman;
- katalogu domowego hosta;
- repo innych uczniów.
Repo jest własnością użytkownika hosta. Rootless Podman uruchamia kontener z
mapowaniem UID/GID (`keep-id` albo równoważnym), żeby artefakty nie powstawały
jako root.
## Layout tmuxa i Neovima
W każdym profilu po `debug` powstaje ten sam układ:
```text
work
├─ pane 0 (góra): jeden Neovim
│ ├─ okno lewe: Termdebug/GDB dashboard
│ └─ okno prawe: edytor źródeł i nvim-dap
└─ pane 1 (dół): bash w /workspace
```
Lewa i prawa część są oknami Neovima, nie osobnymi pane'ami tmuxa. Hazard3
trzyma symulator w ukrytym oknie tmuxa `tb`. Prefix tmuxa to `Ctrl-s`. Neovim
ma identyczne mapowania Termdebug i nvim-dap.
Opis backendu GDB pochodzi z jednego target descriptor, dzięki czemu oba
frontendy nie rozjeżdżają się.
## Debugger według profilu
### `native-amd64`
- GDB jest backendem domyślnym;
- LLDB pozostaje dostępny ręcznie w shellu; wspólny interfejs Termdebug używa GDB;
- przed debugowaniem powstaje build `-g3 -O0`;
- `test` uruchamia co najmniej ASan i UBSan.
### `hazard3-sim`
- obecny codzienny backend: `fast-rsp`, jawnie opisany jako uproszczony;
- docelowy, planowany backend: `fast-dm`, czyli GDB RSP -> APB/DMI ->
prawdziwy DM Hazard3;
- gate zgodności: OpenOCD + JTAG remote-bitbang;
- planowany simulatorowy BSP FreeRTOS ma używać timera 1 kHz, czyli ticka co
150 000 cykli przy zegarze gościa 150 MHz; nie jest jeszcze wdrożony.
### `rp2350`
- OpenOCD/probe dla targetów RISC-V i ARM;
- picotool dla informacji, resetu i operacji wspieranych przez RP2350;
- `deploy` zawsze raportuje probe, target, artefakt i jego SHA-256;
- launcher nie wybiera pierwszego przypadkowego probe, gdy widocznych jest
kilka urządzeń.
Zewnętrzny debug probe i natywne USB płytki są rozdzielone. Standardowy
debug/flash używa stabilnego probe, który nie re-enumeruje się przy resecie
targetu. BOOTSEL/picotool jest opcjonalną, krótkotrwałą akcją z tego samego
profilu: launcher wykrywa bieżący węzeł USB przed każdą fazą i ponownie po
re-enumeracji. Nie przekazuje całego `/dev`. Build i test offline nie wymagają
żadnego urządzenia.
`run rp2350` wymaga zgodnego rekordu ostatniego deploy dla wybranej płytki i
artefaktu. Bez niego odmawia wykonania, chyba że użytkownik jawnie zaakceptuje
istniejący firmware. Zapobiega to testowaniu starego programu.
## Instancje i sockety MCP
Stabilna nazwa logiczna:
```text
<profile>-<series>-<card>-<task>
```
Aktualny container ID jest pobierany z `podman inspect`. Pełne nazwy logiczne
są w labels i registry, a sockety używają krótkich kluczy:
```text
$XDG_RUNTIME_DIR/stem/<thread-key>/<instance-key>/<cid12>/t.sock
$XDG_RUNTIME_DIR/stem/<thread-key>/<instance-key>/<cid12>/n.sock
$XDG_RUNTIME_DIR/stem/<thread-key>/<instance-key>/current
```
Klucze wątku i instancji mają 12 znaków, `cid12` jest prefiksem bieżącego ID.
Registry przechowuje pełne wartości. Host i kontener używają tej samej ścieżki,
a całkowita długość musi być mniejsza lub równa 100 bajtów, pozostawiając
zapas względem linuksowego limitu `sun_path`.
Wrapper MCP otrzymuje `instance`, sprawdza label kontenera, aktualny ID i typ
socketu. Dopiero wtedy łączy się z tmuxem albo Neovimem. Pozwala to Codexowi
obsługiwać kilka kontenerów bez przypadkowego wejścia do starej sesji.
Publiczne wejścia launchera to `stemctl mcp tmux INSTANCE` oraz
`stemctl mcp nvim INSTANCE`. Hostowy wrapper wykonuje wyłącznie resolver i
`podman exec`; sam serwer wraz z przypiętymi zależnościami działa wewnątrz
zweryfikowanego kontenera. Neovim MCP publikuje również stan i komendy
Termdebug/nvim-dap.
MCP nie jest czwartym kontenerem. Bridge jest częścią `dev-ui-base`, a hostowy
resolver jest częścią `stem-launcher`.
## Cykl życia
```bash
./rvctl env sync
./rvctl env sync --dry-run
stemctl start hazard3-sim inf bss 1
stemctl status hazard3-sim --instance hazard3-sim-inf-bss-t1
stemctl attach hazard3-sim --instance hazard3-sim-inf-bss-t1
stemctl stop hazard3-sim --instance hazard3-sim-inf-bss-t1
stemctl rm hazard3-sim --instance hazard3-sim-inf-bss-t1
```
Jeżeli tego repo jeszcze nie ma w workspace, `rvctl` używa fallbacku z
`~/dev/edu/repos/rv/rv32i-hazard3-env`, o ile jest dostępny.
`start` jest idempotentne dla zgodnej instancji. Jeżeli zmienił się lokalny ID
wyniku builda albo konfiguracja targetu, launcher odtwarza kontener.
`stop/start` zachowuje container ID; `rm/start` tworzy nowy ID i nowy katalog
socketów. Stan karty pozostaje na hoście.
## `env build`
## Zdalny komputer
Buduje wybrany profil kontenera:
Na komputer ucznia wchodzimy przez SSH z kluczem i uruchamiamy `stemctl` tam:
```bash
./rvctl env build rv32i
./rvctl env build host
ssh uczen-lab
cd ~/dev/workspace/stem/tools/stem-launcher
./stemctl status hazard3-sim --instance hazard3-sim-inf-bss-t1
./stemctl debug hazard3-sim inf bss 1
```
Wariant `--dry-run` pokazuje dokładną komendę bez uruchamiania Dockera:
GDB, UART i MCP nasłuchują na loopback albo Unix socketach. Jeżeli potrzebny
jest dostęp z komputera nauczyciela, launcher generuje jawne polecenie tunelu
SSH. Nie otwiera portów na `0.0.0.0`.
```bash
./rvctl env build host --dry-run
```
## Budowanie i cache
## `debug`
`dev-ui-base` zawiera przypięte binaria/dependencies UI, lecz nie zawiera
często zmienianych `configs/` i entrypointów. Trzy toolchainowe stage'e
dziedziczą z niego, instalują ciężkie SDK/kompilatory, a dopiero finalne stage'e
kopiują konfigurację UI i skrypty. Kod karty jest montowany, nie kopiowany.
Zmiana entrypointu, dashboardu albo mapowania klawisza nie może invalidować
warstwy pobierającej toolchain.
Uruchamia debugowanie zadania w wybranym profilu.
## Status implementacji
```bash
./rvctl debug rv32i inf bss 1
./rvctl debug host inf bss 1
```
W profilu `rv32i` startuje środowisko Hazard3: symulator, `gdb-multiarch` i
`nvim` w sesji `tmux`.
W profilu `host` wybrane źródło C jest kompilowane natywnie z debug info, a
potem uruchamiane w `gdb` obok `nvim`.
Ten profil jest przeznaczony dla tasków C z funkcją `main`; taski czysto
asemblerskie nadal debugujemy w profilu `rv32i`.
Przydatne warianty:
```bash
./rvctl debug rv32i bss 4
./rvctl debug host bss 4
./rvctl debug host 4 --editor vim
./rvctl debug host 4 --instance host-bss-t4
./rvctl debug rv32i 4 --pane 0
./rvctl debug rv32i 4 pane node:0
./rvctl debug host 4 --dry-run
```
`--pane TARGET` oraz forma `pane TARGET` wysyłają wygenerowaną komendę do
istniejącego panelu tmuxa na hoście przez `tmux send-keys`. Skróty targetów:
- `0` - panel `0` w bieżącym oknie,
- `node:0` - okno `node`, panel `0`,
- `%3` albo `:node.0` - natywny target tmuxa.
## `run`
Na razie `run` jest wdrożone dla profilu `host`. Buduje wybrany task natywnie i
uruchamia wynikowy program:
```bash
./rvctl run host inf bss 1
./rvctl run host 4
```
Dla profilu `rv32i` używamy obecnie `debug rv32i`, bo przepływ RISC-V zakłada
symulator Hazard3 i GDB.
## `shell`
Otwiera shell w wybranym profilu kontenera z podmontowaną kartą:
```bash
./rvctl shell rv32i inf bss
./rvctl shell host inf bss
```
## Selektory
Komendy `debug`, `run` i `shell` używają tych samych skrótów co `tasks`:
```bash
./rvctl debug host 1
./rvctl debug host bss 1
./rvctl debug host inf bss 1
```
`rvctl` rozwiązuje `1` do właściwego zadania, na przykład
`task1_bss`, na podstawie plików w `src/tasks`.
## Starsza komenda
`tmux-container` zostaje jako alias kompatybilności dla starszego trybu
shellowego. Nowe przykłady powinny używać `debug`, `run`, `shell` i `env`.
| Element | Status |
| --- | --- |
| `native-amd64` | wdrożony i sprawdzony z ASan/UBSan |
| `hazard3-sim` | wdrożony dla bare metal; `fast-dm` i BSP FreeRTOS są następne |
| `rp2350` | wdrożony; FreeRTOS 1 kHz buduje ELF/UF2 dla RV i ARM, hardware gate wymaga ACL probe |
| `stemctl` | wdrożony; `rvctl` jest wrapperem zgodności |
| pobieranie kart z Git | istnieje w `rvctl` |
| `build/test/run/debug/deploy` | wdrożony dispatcher manifestu i `result.json` |
| sockety tmux/nvim | wdrożone z `thread12/instance12/container12` i resolverem |
| zdalny SSH | kontrakt zaprojektowany, smoke test do dodania |