Files
stem-launcher/doc/containers.md
T
2026-07-17 09:22:59 +02:00

13 KiB
Raw Blame History

Kontenery i debug

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.

Stan obecny:

  • 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.

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

stemctl env list
stemctl env ensure native-amd64
stemctl env ensure hazard3-sim
stemctl env ensure rp2350

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

Selektory series card task są opcjonalne, jeśli ustawiono wartości domyślne. Każda akcja obsługuje:

--instance NAME
--target NAME
--editor nvim|vim
--dry-run

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.

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:

{
  "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:

/usr/local/bin/stem-entry
/usr/local/bin/stem-card

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:

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:

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.

Źródła projektu

Źródła kart i projektów pobiera wyłącznie stemctl na hoście, zawsze pod ~/dev/workspace danego konta Unix. Kanoniczny workspace STEM ma postać ~/dev/workspace/stem; nie używamy do pracy katalogów kontenera ani przypadkowych klonów poza ~/dev/workspace. Pierwszy klon launchera trafia do ~/dev/workspace/stem/tools/stem-launcher i od niego zaczyna się cały bootstrap workspace. Typowy przepływ to:

stemctl workspace sync
stemctl series cards fetch inf pointers
stemctl debug rp2350 inf pointers 4

Katalog karty pozostaje na hoście, na przykład ~/dev/workspace/stem/series/inf/pointers/, i jest przekazywany kontenerowi jako bind-mount /workspace. Nie klonujemy repozytoriów źródłowych wewnątrz kontenera: kontenery są odtwarzalne i mogą zostać usunięte, natomiast host zachowuje Git, zmiany ucznia, odpowiedzi i historię pracy. W obrazie lub osobnym cache volume mogą znajdować się wyłącznie narzędzia, zależności i odtwarzalne cache budowania.

Dla Hazard3 po przygotowaniu przez stemctl env sources SERIES CARD ten sam bind-mount zawiera również rzeczywiste (niebędące symlinkami) katalogi vendor/Hazard3 i vendor/lab-runtime. Program, testbench oraz GDB korzystają wyłącznie z nich. Build testbencha i ELF trafia do .stem/instances/<instance>/build, a DWARF zachowuje ścieżki /workspace/vendor/Hazard3/... i /workspace/src/.... /opt pozostaje miejscem toolchainów i programów obrazu; nie jest źródłem kodu wyświetlanego w Neovimie ani GDB.

Instancje i sockety MCP

Stabilna nazwa logiczna:

<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:

$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.

Dla jednego stale działającego klienta hostowego dostępny jest również jawny, atomowy przełącznik obu socketów:

stemctl mcp list
stemctl mcp select INSTANCE_OR_CONTAINER_ID
stemctl mcp status

mcp select sprawdza registry, pełny ID i label kontenera, aktywnie testuje serwery Neovima i tmuxa, a następnie jednym rename(2) przełącza symlink $XDG_RUNTIME_DIR/stem/mcp-selected/current. Jeśli choć jeden serwer nie odpowiada, poprzedni wybór pozostaje bez zmian.

Katalog socketów jest tworzony przez launcher na hoście i bind-mountowany do kontenera pod identyczną ścieżką. Dopiero nvim i tmux działające w kontenerze tworzą odpowiednio n.sock i t.sock; hostowy Codex łączy się z tym samym plikiem przez widok hosta. Sockety nie są przekazywane przez sieć ani nie dają kontenerowi dostępu do socketu Podmana.

Połączenia MCP mają profil observe, assist albo full. full jest przeznaczony dla nauczyciela: może wykonywać dowolne działania dostępne w Neovimie, tmuxie i terminalu wybranego kontenera jako konto ucznia. Profile observe i assist muszą używać oddzielnych, ograniczonych providerów; nie wolno udawać ograniczenia przez samą zmienną środowiskową przy providerze udostępniającym ogólny Vimscript lub polecenia panea.

MCP nie jest czwartym kontenerem. Bridge jest częścią dev-ui-base, a hostowy resolver jest częścią stem-launcher.

Cykl życia

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

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.

Zdalny komputer

Na komputer ucznia wchodzimy przez SSH z kluczem i uruchamiamy stemctl tam:

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

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.

Budowanie i cache

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.

Status implementacji

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