feat: add three-profile STEM container workflow
This commit is contained in:
@@ -1,495 +1,139 @@
|
|||||||
# RV Launcher
|
# STEM Launcher
|
||||||
|
|
||||||
Repo `rv-launcher` zawiera narzędzie do pracy ze wspólnym workspace EDU.
|
`stemctl` jest hostowym wejściem do kart pracy, Git i trzech źródłowo
|
||||||
W pierwszej kolejności służy ono do zarządzania tokenami; pozostałe funkcje
|
budowanych kontenerów STEM. Historyczna nazwa zdalnego repo może nadal brzmieć
|
||||||
obejmują pracę z kartami pracy i uruchamianie środowiska kontenerowego.
|
`rv-launcher`; `rvctl` pozostaje ostrzegającym wrapperem zgodności.
|
||||||
|
|
||||||
Właściwym skryptem CLI jest wykonywalny plik `rvctl.py`. To on implementuje
|
Launcher nie pobiera i nie publikuje gotowych obrazów środowiska. Klonuje repo
|
||||||
zarządzanie tokenami, listowanie serii i kart, przygotowanie repo odpowiedzi i
|
narzędzi zawierające `Dockerfile` i `docker-compose.yml`, a brakujący profil
|
||||||
start kontenera.
|
buduje lokalnie rootless Podmanem. Tag `localhost/stem/...:local` jest tylko
|
||||||
|
lokalnym wpisem cache runtime.
|
||||||
|
|
||||||
Publicznym entrypointem dla użytkownika jest krótki wrapper `rvctl`:
|
## Model
|
||||||
|
|
||||||
|
| Profil | Targety | Zastosowanie |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `native-amd64` | `native` | C/C++, golden tests, ASan/UBSan, GDB |
|
||||||
|
| `hazard3-sim` | `hazard3-baremetal`; `hazard3-freertos` po BSP timer/IRQ | RTL Hazard3/Verilator bez płytki |
|
||||||
|
| `rp2350` | `rp2350-rv`, `rp2350-arm` | Pico 2/2 W, FreeRTOS, OpenOCD/picotool |
|
||||||
|
|
||||||
|
W każdym profilu interfejs pozostaje taki sam: tmux z prefixem `Ctrl-s`,
|
||||||
|
Neovim, Termdebug, nvim-dap oraz MCP tmuxa i Neovima. Kod karty, wyniki i
|
||||||
|
artefakty są bind-mountem na hoście. Kontenery nie dostają kluczy SSH, tokenów,
|
||||||
|
socketu Podmana/Dockera ani całego `/dev`.
|
||||||
|
|
||||||
|
## Szybki start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl
|
./stemctl workspace sync
|
||||||
|
./stemctl series list
|
||||||
|
./stemctl series cards fetch inf bss
|
||||||
|
|
||||||
|
./stemctl test native-amd64 inf bss 1
|
||||||
|
./stemctl test hazard3-sim inf bss 1
|
||||||
|
./stemctl debug hazard3-sim inf bss 1
|
||||||
|
|
||||||
|
./stemctl probe list
|
||||||
|
./stemctl deploy rp2350 inf bss 1 \
|
||||||
|
--target rp2350-rv --device /dev/bus/usb/001/006
|
||||||
```
|
```
|
||||||
|
|
||||||
Wrapper uruchamia:
|
Pierwsze wywołanie danego profilu może potrwać, ponieważ buduje go ze
|
||||||
|
źródłowego Dockerfile. Kolejne korzystają z lokalnych warstw cache.
|
||||||
|
|
||||||
|
Wymuszenie samego przygotowania środowiska:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 rvctl.py "$@"
|
./stemctl env sync
|
||||||
|
./stemctl env build native-amd64
|
||||||
|
./stemctl env build hazard3-sim
|
||||||
|
./stemctl env build rp2350
|
||||||
```
|
```
|
||||||
|
|
||||||
`rvctl.py` można też uruchomić bezpośrednio:
|
`env sync` klonuje lub aktualizuje
|
||||||
|
`edu-tools/rv32i-hazard3-student-env`. Repo środowiska zawiera źródłowy
|
||||||
|
Dockerfile i pliki Compose.
|
||||||
|
|
||||||
```bash
|
## Workspace
|
||||||
./rvctl.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Lokalne ścieżki i domyślne ustawienia są trzymane w `workspace.json`.
|
Domyślny układ:
|
||||||
Publiczny opis wspólnego workspace, czyli serie, karty i wersje repozytoriów,
|
|
||||||
jest w osobnym repo `edu-workspace/workspace-info`.
|
|
||||||
|
|
||||||
## Co robi narzędzie
|
|
||||||
|
|
||||||
`rvctl` porządkuje pracę w trzech obszarach.
|
|
||||||
|
|
||||||
1. Zarządzanie tokenami
|
|
||||||
|
|
||||||
Narzędzie obsługuje lokalny store `tokens/tokens.json`, porównuje go z git
|
|
||||||
remotes i potrafi synchronizować token w obie strony. Szczegóły modelu,
|
|
||||||
format pliku i opis komend są w `doc/tokens.md`.
|
|
||||||
|
|
||||||
2. Listowanie i pobieranie kart pracy
|
|
||||||
|
|
||||||
`rvctl` czyta serie i karty z `meta/workspace-info`, zadania z pobranych
|
|
||||||
kart, pomaga wybrać materiał do pracy oraz przygotowuje remotes potrzebne do
|
|
||||||
repo odpowiedzi. Docelowy model namespace `series`, `cards` i `tasks` jest w
|
|
||||||
`doc/series.md`.
|
|
||||||
|
|
||||||
3. Uruchamianie środowiska programistycznego w kontenerach
|
|
||||||
|
|
||||||
Dla wybranej karty i zadania `rvctl` uruchamia osobne profile kontenerów:
|
|
||||||
`rv32i` dla Hazard3/RISC-V oraz `host` dla natywnego debugowania C. Oba
|
|
||||||
profile opierają się na `nvim`, `tmux` i debuggerze. Model komend jest w
|
|
||||||
`doc/containers.md`.
|
|
||||||
|
|
||||||
## Model katalogów
|
|
||||||
|
|
||||||
Podstawowym miejscem pracy użytkownika jest workspace:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
~/dev/workspace/rv
|
|
||||||
```
|
|
||||||
|
|
||||||
Najpierw utwórz tylko katalog bazowy workspace i wejdź do niego:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p ~/dev/workspace/rv
|
|
||||||
cd ~/dev/workspace/rv
|
|
||||||
```
|
|
||||||
|
|
||||||
Po tym kroku workspace istnieje, ale nie ma jeszcze katalogów narzędzi,
|
|
||||||
tokenów ani kart:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
~/dev/workspace/rv
|
~/dev/workspace/stem/
|
||||||
|
├── meta/workspace-info/
|
||||||
|
├── series/<seria>/<karta>/
|
||||||
|
├── tools/
|
||||||
|
│ ├── stem-launcher/
|
||||||
|
│ └── rv32i-hazard3-student-env/
|
||||||
|
└── tokens/tokens.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Potem wybierz jedną z dwóch dróg startu.
|
Jeżeli istnieje tylko starszy `~/dev/workspace/rv`, launcher wykrywa go bez
|
||||||
|
niszczenia danych. Jawna migracja:
|
||||||
### Bez `tokens.json`
|
|
||||||
|
|
||||||
Ten wariant jest dla sytuacji, w której token jest podany w URL-u git remota,
|
|
||||||
a plik `tokens.json` ma powstać dopiero lokalnie.
|
|
||||||
|
|
||||||
Etap 1: utwórz katalog launchera i wejdź do niego:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/dev/workspace/rv/tools/rv-launcher
|
./stemctl workspace migrate
|
||||||
cd ~/dev/workspace/rv/tools/rv-launcher
|
./stemctl workspace doctor
|
||||||
```
|
```
|
||||||
|
|
||||||
Etap 2: utwórz puste repo, dodaj remote `r1` z tokenem, pobierz branch i
|
## Akcje i tożsamość instancji
|
||||||
ustaw lokalny `main`:
|
|
||||||
|
|
||||||
```bash
|
Jedna logiczna instancja zachowuje nazwę między `build`, `test`, `run`,
|
||||||
git init
|
`debug` i `attach`. Bieżący ID kontenera wchodzi natomiast do krótkiej ścieżki
|
||||||
git remote add r1 http://u1:TOKEN@77.90.8.171:3001/edu-tools/rv-launcher.git
|
socketu:
|
||||||
git fetch r1 main
|
|
||||||
git switch --track -c main r1/main
|
|
||||||
```
|
|
||||||
|
|
||||||
`r1` jest nazwą remota, z którego startujemy. `--track` ustawia lokalny branch
|
|
||||||
`main` tak, aby śledził `r1/main`, dzięki czemu późniejsze `git pull` i
|
|
||||||
`git push` wiedzą, z którym branchem zdalnym pracują.
|
|
||||||
|
|
||||||
Po tym kroku w workspace jest już repo launchera:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
~/dev/workspace/rv
|
$XDG_RUNTIME_DIR/stem/<thread12>/<instance12>/<container12>/t.sock
|
||||||
└── tools
|
$XDG_RUNTIME_DIR/stem/<thread12>/<instance12>/<container12>/n.sock
|
||||||
└── rv-launcher
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Repo `workspace-info` nie jest częścią `tools`. `rvctl` pobierze je
|
Resolver wykonuje `podman inspect` przed połączeniem i odrzuca osierocony
|
||||||
automatycznie do `meta/workspace-info` przy pierwszej komendzie zależnej od
|
socket. Pozwala to jednemu Codexowi obsługiwać kilka kontenerów bez kolizji.
|
||||||
manifestu, na przykład `series list`.
|
|
||||||
|
|
||||||
Etap 3: wczytaj token z remota do lokalnego store:
|
Stabilne wejścia MCP wymagają jawnej instancji i uruchamiają serwer znajdujący
|
||||||
|
się w wybranym kontenerze:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/dev/workspace/rv/tools/rv-launcher
|
./stemctl mcp tmux hazard3-sim-inf-bss-t1
|
||||||
./rvctl tokens sync remote r1
|
./stemctl mcp nvim hazard3-sim-inf-bss-t1
|
||||||
./rvctl tokens update r1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`tokens sync remote r1` przepisuje token z URL-a remota `r1` do lokalnego
|
Oddzielne wpisy klienta MCP mogą wskazywać inne nazwy instancji, więc jeden
|
||||||
store. `tokens update r1` odpytuje Gitea API i uzupełnia metadane tokena:
|
Codex obsługuje kilka kontenerów bez globalnego „ostatniego socketu”.
|
||||||
ważność, zakresy oraz uprawnienia w organizacji i repo.
|
|
||||||
|
|
||||||
`rvctl` sam tworzy katalog `~/dev/workspace/rv/tokens`, jeżeli jeszcze go nie
|
## Komputer zdalny
|
||||||
ma. Plik `tokens.json` dostaje prawa `600`, a katalog store prawa `700`.
|
|
||||||
|
|
||||||
Po tym kroku workspace ma już `tokens.json`:
|
Na komputer ucznia wchodzimy wyłącznie SSH z parą kluczy. Launcher, Git,
|
||||||
|
rootless Podman i sockety działają na tym komputerze:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh uczen-lab
|
||||||
|
cd ~/dev/workspace/stem/tools/stem-launcher
|
||||||
|
./stemctl status hazard3-sim --instance lekcja-1
|
||||||
|
./stemctl debug hazard3-sim inf bss 1 --instance lekcja-1
|
||||||
|
```
|
||||||
|
|
||||||
|
Nie kopiujemy prywatnego klucza do kontenera. Jeżeli potrzebny jest zdalny
|
||||||
|
GDB/MCP, używamy jawnego tunelu SSH albo wykonujemy klienta po stronie zdalnej.
|
||||||
|
|
||||||
|
## Zgodność
|
||||||
|
|
||||||
|
Aliasy nadal działają:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
~/dev/workspace/rv
|
host -> native-amd64
|
||||||
├── tools
|
rv32i -> hazard3-sim
|
||||||
│ └── rv-launcher
|
rvctl -> stemctl
|
||||||
└── tokens
|
RV_* -> fallback dla STEM_*
|
||||||
└── tokens.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Etap 4: sprawdź, czy git remote i lokalny store widzą ten sam token:
|
Nowe materiały powinny używać nazw kanonicznych.
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl tokens compare
|
|
||||||
```
|
|
||||||
|
|
||||||
Przykładowy wydruk:
|
|
||||||
|
|
||||||
```text
|
|
||||||
tokens
|
|
||||||
item server proto host org repo user remote token_ref token valid scope org repo
|
|
||||||
---- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ------- --------- ------ ------
|
|
||||||
1 gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 r1 * e59cc...13be forever wwwwwwwww +++++ ++++
|
|
||||||
```
|
|
||||||
|
|
||||||
Najważniejsze pola:
|
|
||||||
|
|
||||||
- `remote` - nazwa git remota, tutaj `r1`
|
|
||||||
- `token_ref` - lokalna nazwa tokena i marker zgodności po prawej stronie
|
|
||||||
- `*` - token w remote i w `tokens.json` jest zgodny
|
|
||||||
- `S` - token jest tylko w `tokens.json`; to jest normalne w trybie store-only
|
|
||||||
- `R` - token jest tylko w git remote
|
|
||||||
- `token` - zamaskowany sekret; `rvctl` nie wypisuje całego tokena
|
|
||||||
- `valid`, `scope`, `org`, `repo` - metadane i uprawnienia pobrane przez
|
|
||||||
`tokens update`
|
|
||||||
|
|
||||||
Pełny opis tabeli tokenów jest w `doc/tokens.md`.
|
|
||||||
|
|
||||||
### Z `tokens.json`
|
|
||||||
|
|
||||||
Ten wariant jest dla sytuacji, w której masz już gotowy plik `tokens.json`.
|
|
||||||
|
|
||||||
Etap 1: skopiuj token store do workspace:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p ~/dev/workspace/rv/tokens
|
|
||||||
cd ~/dev/workspace/rv
|
|
||||||
cp /ścieżka/do/tokens.json tokens/tokens.json
|
|
||||||
chmod 600 tokens/tokens.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Po tym kroku workspace ma store tokenów, ale nie ma jeszcze launchera:
|
|
||||||
|
|
||||||
```text
|
|
||||||
~/dev/workspace/rv
|
|
||||||
└── tokens
|
|
||||||
└── tokens.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Etap 2: wejdź do katalogu narzędzi i sklonuj `rv-launcher`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p ~/dev/workspace/rv/tools
|
|
||||||
cd ~/dev/workspace/rv/tools
|
|
||||||
git clone http://77.90.8.171:3001/edu-tools/rv-launcher.git
|
|
||||||
cd rv-launcher
|
|
||||||
```
|
|
||||||
|
|
||||||
Po `git clone` workspace wygląda tak:
|
|
||||||
|
|
||||||
```text
|
|
||||||
~/dev/workspace/rv
|
|
||||||
├── tools
|
|
||||||
│ └── rv-launcher
|
|
||||||
└── tokens
|
|
||||||
└── tokens.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Etap 3: sprawdź store i wpisz credentials ze store do remota `r1`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl tokens list store
|
|
||||||
./rvctl tokens sync store r1
|
|
||||||
```
|
|
||||||
|
|
||||||
`tokens list store` sprawdza, czy skopiowany `tokens.json` zawiera wpis dla
|
|
||||||
remota `r1`. `tokens sync store r1` bierze token ze store i wpisuje credentials
|
|
||||||
do git remota `r1`, tak aby kolejne operacje Git mogły używać tego tokena.
|
|
||||||
Jeżeli po `git clone` repo ma tylko remote `origin` wskazujący to samo repo,
|
|
||||||
`rvctl` automatycznie przemianuje go na `r1`.
|
|
||||||
|
|
||||||
`list store` powinien pokazać token ze store:
|
|
||||||
|
|
||||||
```text
|
|
||||||
tokens
|
|
||||||
item source kind server proto host org repo user remote token valid
|
|
||||||
---- ------ ---- ------ ----- ---------------- --------- ----------- ---- ------ ------------ -------
|
|
||||||
1 store auth gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 e59cc...13be forever
|
|
||||||
```
|
|
||||||
|
|
||||||
`sync store r1` wpisuje credentials do git remota:
|
|
||||||
|
|
||||||
```text
|
|
||||||
repo_root /home/user/dev/workspace/rv/tools/rv-launcher
|
|
||||||
remote r1
|
|
||||||
renamed_from origin
|
|
||||||
server http://77.90.8.171:3001
|
|
||||||
user u1
|
|
||||||
id r1
|
|
||||||
status updated
|
|
||||||
url http://77.90.8.171:3001/edu-tools/rv-launcher.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Etap 4: odśwież metadane tokena i porównaj store z remote:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl tokens update r1
|
|
||||||
./rvctl tokens compare
|
|
||||||
```
|
|
||||||
|
|
||||||
`tokens update r1` odpytuje Gitea API dla tokena ze store i odświeża jego
|
|
||||||
metadane: ważność, zakresy oraz uprawnienia w organizacji i repo. `tokens
|
|
||||||
compare` sprawdza potem, czy store i git remote wskazują ten sam token.
|
|
||||||
|
|
||||||
`compare` powinien pokazać `*` przy `r1`, jeżeli remote i `tokens.json` są
|
|
||||||
zgodne:
|
|
||||||
|
|
||||||
```text
|
|
||||||
tokens
|
|
||||||
item server proto host org repo user remote token_ref token valid
|
|
||||||
---- ------ ----- ---------------- --------- ----------- ---- ------ --------- ------------ -------
|
|
||||||
1 gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 r1 * e59cc...13be forever
|
|
||||||
```
|
|
||||||
|
|
||||||
Etap 5: po operacjach Git możesz usunąć sekret z `.git/config`, zostawiając
|
|
||||||
token tylko w store:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl tokens rm remote r1
|
|
||||||
./rvctl tokens compare
|
|
||||||
```
|
|
||||||
|
|
||||||
Po usunięciu remota `compare` pokazuje marker `S`, czyli token jest tylko w
|
|
||||||
store:
|
|
||||||
|
|
||||||
```text
|
|
||||||
tokens
|
|
||||||
item server proto host org repo user remote token_ref token valid
|
|
||||||
---- ------ ----- ---------------- --------- ----------- ---- ------ --------- ------------ -------
|
|
||||||
1 gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 r1 S e59cc...13be forever
|
|
||||||
```
|
|
||||||
|
|
||||||
## Kolejny krok: serie i karty pracy
|
|
||||||
|
|
||||||
Po konfiguracji tokenów następnym elementem jest publiczny manifest wspólnego
|
|
||||||
workspace. `rvctl` szuka go lokalnie w:
|
|
||||||
|
|
||||||
```text
|
|
||||||
~/dev/workspace/rv/meta/workspace-info
|
|
||||||
```
|
|
||||||
|
|
||||||
Manifest zawiera listę serii, kart, repozytoriów źródłowych, repozytoriów
|
|
||||||
odpowiedzi i branchy. Nie zawiera tokenów ani lokalnych plików ucznia.
|
|
||||||
|
|
||||||
Nie musisz pobierać go ręcznie. Pierwsza komenda zależna od manifestu, na
|
|
||||||
przykład `series list`, automatycznie sklonuje repo
|
|
||||||
`edu-workspace/workspace-info`, jeżeli lokalnej kopii jeszcze nie ma:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl series list
|
|
||||||
```
|
|
||||||
|
|
||||||
Jeżeli chcesz jawnie odświeżyć istniejący manifest, użyj:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl workspace sync
|
|
||||||
```
|
|
||||||
|
|
||||||
Przed pobieraniem kart warto jeszcze sprawdzić stan tokenów:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl tokens compare
|
|
||||||
```
|
|
||||||
|
|
||||||
Przykładowy wynik:
|
|
||||||
|
|
||||||
```text
|
|
||||||
fiz 3 0
|
|
||||||
inf 3 0
|
|
||||||
```
|
|
||||||
|
|
||||||
Po wybraniu serii `inf` wylistuj karty:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl series use inf
|
|
||||||
./rvctl series cards list inf
|
|
||||||
```
|
|
||||||
|
|
||||||
Przykładowy wynik zawiera krótką nazwę karty, pełną nazwę repo, status i tytuł:
|
|
||||||
|
|
||||||
```text
|
|
||||||
bss lab-rv32i-strlen-bss-data-stack source rv32i-c / bss-data-stack
|
|
||||||
```
|
|
||||||
|
|
||||||
Przykładowa karta `bss` odpowiada repo `lab-rv32i-strlen-bss-data-stack`.
|
|
||||||
Ustaw ją jako domyślną i pobierz do workspace:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl card use bss
|
|
||||||
./rvctl series cards fetch inf bss
|
|
||||||
```
|
|
||||||
|
|
||||||
Jeżeli chcesz jedną komendą ustawić serię i kartę, użyj:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl card use inf bss
|
|
||||||
```
|
|
||||||
|
|
||||||
Karty trafiają do `series` w workspace:
|
|
||||||
|
|
||||||
```text
|
|
||||||
~/dev/workspace/rv
|
|
||||||
├── meta
|
|
||||||
│ └── workspace-info
|
|
||||||
├── tools
|
|
||||||
│ └── rv-launcher
|
|
||||||
├── tokens
|
|
||||||
│ └── tokens.json
|
|
||||||
└── series
|
|
||||||
└── inf
|
|
||||||
└── lab-rv32i-strlen-bss-data-stack
|
|
||||||
```
|
|
||||||
|
|
||||||
`rvctl` pracuje na kartach z `series_root`, czyli na katalogu
|
|
||||||
`~/dev/workspace/rv/series`.
|
|
||||||
|
|
||||||
Po pobraniu karty `rvctl` przygotowuje też remote odpowiedzi `r1a`. Remote
|
|
||||||
powstaje z tokena `r1` i wskazuje na repo pracy:
|
|
||||||
|
|
||||||
```text
|
|
||||||
answer_remote r1a
|
|
||||||
answer_org c2025-1a-inf
|
|
||||||
answer_repo lab-rv32i-strlen-bss-data-stack
|
|
||||||
answer_url http://77.90.8.171:3001/c2025-1a-inf/lab-rv32i-strlen-bss-data-stack.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Następnie wylistuj zadania w karcie. Skrót `tasks` działa na domyślnej serii i
|
|
||||||
karcie ustawionej w `workspace.json`, czyli tutaj na `inf bss`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl tasks list
|
|
||||||
```
|
|
||||||
|
|
||||||
Przykładowy wynik:
|
|
||||||
|
|
||||||
```text
|
|
||||||
inf bss task1_bss ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task1_bss.c
|
|
||||||
inf bss task2_data ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task2_data.c
|
|
||||||
inf bss task3_stack_unused ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task3_stack_unused.c
|
|
||||||
inf bss task4_stack_strlen ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/src/tasks/task4_stack_strlen.c
|
|
||||||
```
|
|
||||||
|
|
||||||
Przełącz się na wybrane zadanie. `rvctl` bierze login ucznia ze store tokenów,
|
|
||||||
tworzy branch pracy w formacie `<login>T<numer_zadania>a` i ustawia mu upstream
|
|
||||||
na `r1a/<branch>`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl tasks switch 4
|
|
||||||
```
|
|
||||||
|
|
||||||
Dla użytkownika `u1` i zadania `task4_stack_strlen` branch będzie miał nazwę:
|
|
||||||
|
|
||||||
```text
|
|
||||||
u1T4a
|
|
||||||
```
|
|
||||||
|
|
||||||
## Kolejny krok: kontenery i debugowanie
|
|
||||||
|
|
||||||
Kontenery są potrzebne dopiero po pobraniu karty pracy, kiedy chcesz uruchomić
|
|
||||||
albo debugować konkretne zadanie. `rvctl` korzysta z narzędzia
|
|
||||||
`rv32i-hazard3-student-env`, które trafia do workspace pod:
|
|
||||||
|
|
||||||
```text
|
|
||||||
~/dev/workspace/rv/tools/rv32i-hazard3-student-env
|
|
||||||
```
|
|
||||||
|
|
||||||
Najpierw sprawdź dostępne profile i pobierz albo odśwież repo środowiska:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl env list
|
|
||||||
./rvctl env sync
|
|
||||||
```
|
|
||||||
|
|
||||||
Po synchronizacji workspace ma dodatkowe narzędzie pod `tools`:
|
|
||||||
|
|
||||||
```text
|
|
||||||
~/dev/workspace/rv
|
|
||||||
├── meta
|
|
||||||
│ └── workspace-info
|
|
||||||
├── tools
|
|
||||||
│ ├── rv-launcher
|
|
||||||
│ └── rv32i-hazard3-student-env
|
|
||||||
├── tokens
|
|
||||||
│ └── tokens.json
|
|
||||||
└── series
|
|
||||||
└── inf
|
|
||||||
└── lab-rv32i-strlen-bss-data-stack
|
|
||||||
```
|
|
||||||
|
|
||||||
Środowisko ma dwa profile:
|
|
||||||
|
|
||||||
- `rv32i` - Hazard3/RISC-V, `gdb-multiarch`, `nvim` i `tmux`,
|
|
||||||
- `host` - natywne uruchamianie i debugowanie C przez `clang` albo `gcc`,
|
|
||||||
`gdb`, `nvim` i `tmux`.
|
|
||||||
|
|
||||||
Zbuduj potrzebne obrazy:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl env build rv32i
|
|
||||||
./rvctl env build host
|
|
||||||
```
|
|
||||||
|
|
||||||
Dla pobranej karty `inf bss` możesz uruchomić debugowanie w obu profilach:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl debug rv32i 4
|
|
||||||
./rvctl debug host 4
|
|
||||||
```
|
|
||||||
|
|
||||||
Jeżeli pracujesz już w tmuksie na hoście, możesz wysłać start kontenera do
|
|
||||||
konkretnego panelu:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl debug rv32i 4 pane 0
|
|
||||||
./rvctl debug rv32i 4 --pane node:0
|
|
||||||
```
|
|
||||||
|
|
||||||
`pane 0` oznacza panel `0` w bieżącym oknie tmuxa. `node:0` oznacza okno
|
|
||||||
`node`, panel `0`.
|
|
||||||
|
|
||||||
Profil `host` ma też szybkie uruchomienie bez sesji debuggera:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl run host 4
|
|
||||||
```
|
|
||||||
|
|
||||||
Szczegóły komend, warianty `--dry-run`, `--instance`, `--editor` i ograniczenia
|
|
||||||
profili są w `doc/containers.md`.
|
|
||||||
|
|
||||||
## Dokumentacja
|
## Dokumentacja
|
||||||
|
|
||||||
- `doc/rvctl.md` - techniczna dokumentacja CLI: wszystkie komendy, argumenty
|
- [plan architektury](doc/architecture-plan.md)
|
||||||
i przełączniki
|
- [kontenery i interfejs](doc/containers.md)
|
||||||
- `doc/tokens.md` - model tokenów, format `tokens/tokens.json` i
|
- [migracja nazw i workspace](doc/migration-stem-launcher.md)
|
||||||
synchronizacja remote <-> store
|
- [serie i karty](doc/series.md)
|
||||||
- `doc/series.md` - docelowy model komend dla serii i kart pracy
|
- [tokeny Gitea](doc/tokens.md)
|
||||||
- `doc/containers.md` - docelowy model komend dla środowisk kontenerowych
|
- [przegląd Claude](doc/review-claude-2026-07-14.md)
|
||||||
- `doc/agents.md` - wnioski i plan późniejszej integracji agentów z kontenerami
|
|
||||||
- `doc/workspace.md` - krótki opis konfiguracji workspace
|
|
||||||
|
|
||||||
README jest tylko mapą projektu. Szczegóły operacyjne trzymamy w `doc/`, żeby
|
|
||||||
nie dublować instrukcji w kilku miejscach.
|
|
||||||
|
|||||||
+50
-41
@@ -1,8 +1,8 @@
|
|||||||
# Agenci w środowisku kontenerowym
|
# Agenci w środowisku kontenerowym
|
||||||
|
|
||||||
Ten dokument zapisuje wnioski do przyszłej integracji agentów, takich jak
|
Ten dokument zapisuje wnioski do integracji agentów, takich jak Codex, Claude,
|
||||||
Codex, Gemini i inne narzędzia asystujące. Integrację robimy po domknięciu
|
Gemini i inne narzędzia asystujące. Docelowo integracja obejmuje trzy profile:
|
||||||
kontenerów `rv32i` i `host`.
|
`native-amd64`, `hazard3-sim` i `rp2350`.
|
||||||
|
|
||||||
## Założenie
|
## Założenie
|
||||||
|
|
||||||
@@ -13,39 +13,40 @@ mieć wgląd w tę samą sesję, w której pracuje uczeń:
|
|||||||
- `nvim` - edycja plików i nawigacja po kodzie,
|
- `nvim` - edycja plików i nawigacja po kodzie,
|
||||||
- `gdb` lub `gdb-multiarch` - stan debuggera,
|
- `gdb` lub `gdb-multiarch` - stan debuggera,
|
||||||
- katalog karty pracy zamontowany w kontenerze,
|
- katalog karty pracy zamontowany w kontenerze,
|
||||||
- stan sesji zapisany pod `.rv/<instance>/`.
|
- stan sesji zapisany pod `.stem/instances/<instance>/`.
|
||||||
|
|
||||||
Dzięki temu agent widzi środowisko debugowania, a nie tylko statyczne pliki.
|
Dzięki temu agent widzi środowisko debugowania, a nie tylko statyczne pliki.
|
||||||
|
|
||||||
## Aktualny fundament
|
## Aktualny fundament
|
||||||
|
|
||||||
`rv32i-hazard3-student-env` ma już elementy potrzebne do takiego modelu:
|
`rv32i-hazard3-student-env` dostarcza źródła wspólnego modelu:
|
||||||
|
|
||||||
- profil `rv32i` jako usługa `env` w `docker compose`,
|
- profile `native-amd64`, `hazard3-sim` i `rp2350` jako trzy usługi Compose;
|
||||||
- profil `host` jako osobna usługa `host`,
|
- jeden wieloetapowy Dockerfile budowany lokalnie, bez dystrybucji obrazów;
|
||||||
- `tmux` jako warstwa sesji terminalowej,
|
- `tmux` jako warstwa sesji terminalowej,
|
||||||
- `nvim` uruchamiany ze stabilnym socketem,
|
- `nvim` uruchamiany ze stabilnym socketem,
|
||||||
- `gdb-multiarch` dla profilu `rv32i`,
|
- `gdb-multiarch` dla profilu `rv32i`,
|
||||||
- `gdb` i opcjonalnie `lldb` dla profilu `host`,
|
- `gdb` i opcjonalnie `lldb` dla profilu `host`,
|
||||||
- katalog stanu `.rv/<instance>/`,
|
- katalog stanu `.stem/instances/<instance>/`, z fallbackiem `.rv`;
|
||||||
- skrypty MCP dla `tmux` i `nvim`:
|
- skrypty MCP dla `tmux` i `nvim`:
|
||||||
- `scripts/mcp-tmux.sh`,
|
- `scripts/mcp-tmux.sh`,
|
||||||
- `scripts/mcp-nvim.sh`,
|
- `scripts/mcp-nvim.sh`,
|
||||||
- `scripts/nvim-in-container.sh`.
|
- `scripts/nvim-in-container.sh`.
|
||||||
|
|
||||||
To oznacza, że kontenery są dobrym miejscem do podłączenia agentów. Brakuje
|
Serwery MCP są instalowane w kontenerze, a hostowe wrappery weryfikują label i
|
||||||
jeszcze spójnej warstwy komend w `rvctl`.
|
bieżący ID Podmana przed `podman exec`. `stemctl` oraz wspólny kontrakt trzech
|
||||||
|
profili są wdrożone; osobne komendy wyższego poziomu `agent start/attach`
|
||||||
|
pozostają rozszerzeniem późniejszym.
|
||||||
|
|
||||||
## Docelowy model komend
|
## Docelowy model komend
|
||||||
|
|
||||||
Docelowo `rvctl` powinien ukrywać szczegóły socketów, kontenerów i providerów.
|
Docelowo `stemctl` powinien ukrywać szczegóły socketów, kontenerów i providerów.
|
||||||
Przykładowy kierunek:
|
Przykładowy kierunek:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl agent start codex rv32i inf bss 4
|
./stemctl agent start codex native-amd64 inf bss 4
|
||||||
./rvctl agent start codex host inf bss 4
|
./stemctl agent start codex hazard3-sim inf bss 4
|
||||||
./rvctl agent start gemini rv32i inf bss 4
|
./stemctl agent start codex rp2350 inf bss 4 --target rp2350-rv
|
||||||
./rvctl agent start gemini host inf bss 4
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Skróty mogą powstać później, ale podstawowy model powinien zostać jawny:
|
Skróty mogą powstać później, ale podstawowy model powinien zostać jawny:
|
||||||
@@ -54,28 +55,27 @@ agent, profil środowiska, seria, karta i zadanie.
|
|||||||
Możliwy wariant dla już uruchomionej sesji:
|
Możliwy wariant dla już uruchomionej sesji:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl agent attach codex --instance rv32i-inf-bss-t4-debug
|
./stemctl agent attach codex --instance hazard3-sim-inf-bss-t4
|
||||||
./rvctl agent attach gemini --instance host-inf-bss-t4-debug
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Co powinien robić `rvctl`
|
## Co powinien robić `stemctl`
|
||||||
|
|
||||||
Przy `agent start` narzędzie powinno:
|
Przy `agent start` narzędzie powinno:
|
||||||
|
|
||||||
1. rozwiązać serię, kartę i zadanie tak samo jak `debug`,
|
1. rozwiązać serię, kartę i zadanie tak samo jak `debug`,
|
||||||
2. wybrać profil `rv32i` albo `host`,
|
2. wybrać jeden z trzech profili i właściwy target,
|
||||||
3. nadać stabilną nazwę instancji, na przykład
|
3. nadać stabilną nazwę instancji, na przykład
|
||||||
`rv32i-inf-bss-t4-debug`,
|
`hazard3-sim-inf-bss-t4`,
|
||||||
4. uruchomić kontener i sesję `tmux`,
|
4. uruchomić kontener i sesję `tmux`,
|
||||||
5. włączyć tryb agentowy przez zmienne środowiskowe, na przykład:
|
5. włączyć tryb agentowy przez zmienne środowiskowe, na przykład:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
RV_AGENT=codex
|
STEM_AGENT=codex
|
||||||
RV_CODEX=1
|
STEM_MCP=1
|
||||||
RV_INSTANCE=rv32i-inf-bss-t4-debug
|
STEM_INSTANCE=hazard3-sim-inf-bss-t4
|
||||||
```
|
```
|
||||||
|
|
||||||
6. zapisać albo odczytać sockety z `.rv/<instance>/`,
|
6. rozwiązać bieżący container ID i sockety z katalogu instancji,
|
||||||
7. uruchomić bridge MCP dla `tmux` i `nvim`,
|
7. uruchomić bridge MCP dla `tmux` i `nvim`,
|
||||||
8. przekazać agentowi minimalny kontekst:
|
8. przekazać agentowi minimalny kontekst:
|
||||||
- ścieżka repo karty,
|
- ścieżka repo karty,
|
||||||
@@ -87,51 +87,60 @@ RV_INSTANCE=rv32i-inf-bss-t4-debug
|
|||||||
|
|
||||||
## Sockety i stan sesji
|
## Sockety i stan sesji
|
||||||
|
|
||||||
Dla każdej instancji powinniśmy konsekwentnie używać katalogu:
|
Dla każdej instancji używamy dwóch poziomów tożsamości:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
<card>/.rv/<instance>/
|
$XDG_RUNTIME_DIR/stem/<thread-key>/<instance-key>/<cid12>/
|
||||||
```
|
```
|
||||||
|
|
||||||
W nim mogą znajdować się:
|
W nim mogą znajdować się:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
tmux.sock
|
t.sock
|
||||||
nvim.sock
|
n.sock
|
||||||
gdb-sync.json
|
gdb-sync.json
|
||||||
vim-mcp.sock
|
container.json
|
||||||
vim-mcp-registry.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`rvctl` powinien traktować te pliki jako szczegóły implementacyjne. Użytkownik
|
Pełna nazwa instancji pozostaje w label i registry. Krótkie klucze oraz
|
||||||
i agent powinni dostawać komendy wyższego poziomu.
|
12-znakowy prefiks container ID utrzymują ścieżkę AF_UNIX poniżej 100 bajtów.
|
||||||
|
Aktualny ID uniemożliwia użycie socketu pozostałego po odtworzeniu kontenera.
|
||||||
|
`stemctl` traktuje te pliki jako szczegóły implementacyjne. Użytkownik i agent
|
||||||
|
dostają komendy wyższego poziomu.
|
||||||
|
|
||||||
## Role profili
|
## Role profili
|
||||||
|
|
||||||
Profil `rv32i`:
|
Profil `hazard3-sim`:
|
||||||
|
|
||||||
- debugowanie kodu dla RISC-V/Hazard3,
|
- debugowanie kodu dla RISC-V/Hazard3,
|
||||||
- `gdb-multiarch`,
|
- `gdb-multiarch`,
|
||||||
- symulator,
|
- symulator,
|
||||||
- przykłady asemblerowe i mieszane C/ASM.
|
- przykłady asemblerowe i mieszane C/ASM.
|
||||||
|
|
||||||
Profil `host`:
|
Profil `native-amd64`:
|
||||||
|
|
||||||
- natywne uruchomienie i debugowanie kodu C,
|
- natywne uruchomienie i debugowanie kodu C,
|
||||||
- szybkie testowanie algorytmów,
|
- szybkie testowanie algorytmów,
|
||||||
- `clang` albo `gcc`,
|
- `clang` albo `gcc`,
|
||||||
- `gdb`, opcjonalnie `lldb` i `valgrind`.
|
- `gdb`, opcjonalnie `lldb` i `valgrind`.
|
||||||
|
|
||||||
Taski czysto asemblerowe pozostają w profilu `rv32i`.
|
Profil `rp2350`:
|
||||||
|
|
||||||
|
- debugowanie fizycznego Pico 2/Pico 2 W;
|
||||||
|
- targety RISC-V Hazard3 i ARM Cortex-M33;
|
||||||
|
- OpenOCD, probe, flash, serial i FreeRTOS;
|
||||||
|
- dostęp tylko do jawnie wybranego urządzenia USB.
|
||||||
|
|
||||||
|
Taski czysto asemblerowe RISC-V pozostają w `hazard3-sim` albo `rp2350-rv`.
|
||||||
|
|
||||||
## Kolejność wdrożenia
|
## Kolejność wdrożenia
|
||||||
|
|
||||||
1. Domknąć komendy kontenerowe `env`, `debug`, `run` i `shell`.
|
1. Domknąć trzy profile i komendy `build`, `test`, `run`, `debug`, `deploy`.
|
||||||
2. Ustabilizować nazwy instancji i katalog `.rv/<instance>/`.
|
2. Ustabilizować labels instancji i katalogi socketów z container ID.
|
||||||
3. Opisać kontrakt socketów dla `tmux` i `nvim`.
|
3. Zaimplementować resolver socketów dla `tmux` i `nvim`.
|
||||||
4. Dodać `rvctl agent list`.
|
4. Dodać `stemctl agent list`.
|
||||||
5. Dodać `rvctl agent start`.
|
5. Dodać `stemctl agent start`.
|
||||||
6. Dodać `rvctl agent attach`.
|
6. Dodać `stemctl agent attach`.
|
||||||
7. Dopiero potem podpinać konkretne providery: Codex, Gemini i kolejne.
|
7. Dopiero potem podpinać konkretne providery: Codex, Gemini i kolejne.
|
||||||
|
|
||||||
## Zasada projektowa
|
## Zasada projektowa
|
||||||
|
|||||||
@@ -0,0 +1,488 @@
|
|||||||
|
# Plan architektury STEM Launcher i środowisk wykonawczych
|
||||||
|
|
||||||
|
Status: wdrożony fundament P0/P1; `fast-dm`, BSP FreeRTOS symulatora i pełne
|
||||||
|
bramki sprzętowe pozostają kolejnymi etapami
|
||||||
|
Data: 2026-07-14
|
||||||
|
|
||||||
|
Role w procesie:
|
||||||
|
|
||||||
|
- Codex — architekt i wykonawca planu;
|
||||||
|
- Claude — senior reviewer;
|
||||||
|
- użytkownik — właściciel produktu i programu nauczania.
|
||||||
|
|
||||||
|
## 1. Decyzja architektoniczna
|
||||||
|
|
||||||
|
System składa się z jednego launchera uruchamianego na hoście, jednego wspólnego
|
||||||
|
stage'a bazowego Dockerfile oraz dokładnie trzech kontenerów roboczych:
|
||||||
|
|
||||||
|
| Profil launchera | Target Dockerfile | Rola |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `native-amd64` | `native-amd64-final` | szybkie testy algorytmów C/C++ na AMD64 |
|
||||||
|
| `hazard3-sim` | `hazard3-sim-final` | RV32 bare metal i FreeRTOS na rzeczywistym RTL Hazard3 w Verilatorze |
|
||||||
|
| `rp2350` | `rp2350-final` | fizyczny Pico 2/Pico 2 W: RISC-V Hazard3 i ARM Cortex-M33 |
|
||||||
|
|
||||||
|
Stage `dev-ui-base` jest wspólną warstwą budowania, a nie czwartym stale
|
||||||
|
uruchomionym kontenerem. Hoverboard nie otrzymuje teraz osobnego kontenera.
|
||||||
|
Dodamy go dopiero po ustaleniu mikrokontrolera, BSP i interfejsu probe.
|
||||||
|
|
||||||
|
Nie tworzymy osobnych kontenerów dla:
|
||||||
|
|
||||||
|
- bare metal i FreeRTOS;
|
||||||
|
- RP2350 RISC-V i RP2350 ARM;
|
||||||
|
- C i C++/STL;
|
||||||
|
- Termdebug, nvim-dap, tmuxa ani MCP.
|
||||||
|
|
||||||
|
Są to targety, tryby albo narzędzia wewnątrz jednego z trzech profili.
|
||||||
|
|
||||||
|
## 2. Granice odpowiedzialności
|
||||||
|
|
||||||
|
### Host i `stem-launcher`
|
||||||
|
|
||||||
|
Launcher działa jako zwykły użytkownik hosta i odpowiada za:
|
||||||
|
|
||||||
|
1. synchronizację publicznego manifestu serii;
|
||||||
|
2. listowanie serii, kart i zadań;
|
||||||
|
3. pobieranie kodu karty z Git do workspace na hoście;
|
||||||
|
4. przygotowanie repo odpowiedzi, brancha ucznia i remotes;
|
||||||
|
5. wybór profilu oraz targetu;
|
||||||
|
6. lokalny build właściwego targetu ze źródłowego Dockerfile albo użycie jego
|
||||||
|
lokalnego cache;
|
||||||
|
7. utworzenie, wznowienie, zatrzymanie i usunięcie kontenera;
|
||||||
|
8. wywołanie `build`, `test`, `run`, `debug` i `deploy`;
|
||||||
|
9. rejestrację artefaktów, logów, wyników i socketów MCP;
|
||||||
|
10. wykrywanie probe i przekazanie wyłącznie potrzebnego urządzenia do profilu
|
||||||
|
`rp2350`.
|
||||||
|
|
||||||
|
Klucze SSH, tokeny Gitea i konfiguracja Git pozostają na hoście. Prywatnego
|
||||||
|
klucza SSH nie montujemy do kontenera. Kontenery nie otrzymują socketu Podmana
|
||||||
|
ani Dockera i nie zarządzają innymi kontenerami.
|
||||||
|
|
||||||
|
### Kontener roboczy
|
||||||
|
|
||||||
|
Kontener odpowiada wyłącznie za powtarzalne środowisko kompilacji,
|
||||||
|
uruchamiania i debugowania. Otrzymuje:
|
||||||
|
|
||||||
|
- repo karty pod `/workspace` jako bind mount;
|
||||||
|
- katalog stanu instancji pod `/workspace/.stem/instances/<instance>`;
|
||||||
|
- deklarację targetu i akcji;
|
||||||
|
- opcjonalnie konkretne urządzenie USB dla RP2350;
|
||||||
|
- katalog wymiany socketów MCP widoczny również na hoście.
|
||||||
|
|
||||||
|
Kod źródłowy i artefakty pozostają na hoście. Usunięcie kontenera nie usuwa
|
||||||
|
pracy ucznia.
|
||||||
|
|
||||||
|
## 3. Przepływ ucznia
|
||||||
|
|
||||||
|
Docelowy przepływ jest jeden dla domu, pracowni i zdalnego komputera:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stemctl workspace sync
|
||||||
|
stemctl series list
|
||||||
|
stemctl series cards fetch inf bss
|
||||||
|
|
||||||
|
stemctl env ensure native-amd64
|
||||||
|
stemctl test native-amd64
|
||||||
|
stemctl run native-amd64
|
||||||
|
|
||||||
|
stemctl env ensure hazard3-sim
|
||||||
|
stemctl test hazard3-sim
|
||||||
|
stemctl debug hazard3-sim
|
||||||
|
|
||||||
|
stemctl env ensure rp2350
|
||||||
|
stemctl probe list
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Pełne selektory pozostają dostępne, na przykład:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stemctl test hazard3-sim inf bss 1
|
||||||
|
stemctl deploy rp2350 inf bss 1 --target rp2350-arm
|
||||||
|
```
|
||||||
|
|
||||||
|
Launcher wykonuje następujący pipeline:
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace-info
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
wybór serii/karty/zadania
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
clone/fetch Git na hoście ──► branch/remotes odpowiedzi
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
wybór profilu i targetu
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
rootless Podman + bind mount repo
|
||||||
|
│
|
||||||
|
├──► build ─► test ─► run/debug
|
||||||
|
│
|
||||||
|
└──► artefakt ─► deploy, tylko gdy target to obsługuje
|
||||||
|
```
|
||||||
|
|
||||||
|
### Znaczenie akcji
|
||||||
|
|
||||||
|
| Akcja | `native-amd64` | `hazard3-sim` | `rp2350` |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `build` | binarka ELF hosta | ELF/BIN RV32 | ELF/UF2 dla RV albo ARM |
|
||||||
|
| `test` | golden tests, ASan/UBSan | te same wektory, RV32/RTL | testy na płytce, gdy karta je deklaruje |
|
||||||
|
| `run` | proces natywny | program w Verilatorze | uruchomienie wcześniej wgranego programu i serial/RTT |
|
||||||
|
| `debug` | GDB/LLDB | obecnie `fast-rsp`, docelowo `fast-dm`, kontrolnie JTAG | GDB przez OpenOCD/probe |
|
||||||
|
| `deploy` | nieobsługiwane | nieobsługiwane; artefakt ładuje `run`/`debug` | flash przez probe |
|
||||||
|
|
||||||
|
Komenda nie może udawać sukcesu dla niewspieranej możliwości. Launcher czyta
|
||||||
|
capabilities profilu i zwraca czytelny komunikat, na przykład
|
||||||
|
`deploy is not supported by native-amd64`.
|
||||||
|
|
||||||
|
`run rp2350` nie wykonuje ukrytego flashowania. Wymaga rekordu, że wybrany
|
||||||
|
artefakt został wcześniej wdrożony na wybraną płytkę; w przeciwnym razie
|
||||||
|
odmawia startu albo wymaga jawnego `--allow-existing-firmware`. Artefakt
|
||||||
|
powinien zawierać build ID możliwy do potwierdzenia przez UART/RTT lub metadata
|
||||||
|
RP2350, aby nie testować przypadkiem starego firmware.
|
||||||
|
|
||||||
|
## 4. Kontrakt karty pracy
|
||||||
|
|
||||||
|
Karta nie może zależeć od zaszytych w launcherze nazw plików. Jej wersjonowany
|
||||||
|
manifest powinien deklarować co najmniej:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
schema: 1
|
||||||
|
card: inf/bss
|
||||||
|
default_task: task1
|
||||||
|
profiles:
|
||||||
|
native-amd64:
|
||||||
|
default_target: native
|
||||||
|
hazard3-sim:
|
||||||
|
default_target: hazard3-baremetal
|
||||||
|
rp2350:
|
||||||
|
default_target: rp2350-rv
|
||||||
|
targets:
|
||||||
|
native:
|
||||||
|
profile: native-amd64
|
||||||
|
actions: [build, test, run, debug]
|
||||||
|
hazard3-baremetal:
|
||||||
|
profile: hazard3-sim
|
||||||
|
runtime: baremetal
|
||||||
|
actions: [build, test, run, debug]
|
||||||
|
hazard3-freertos:
|
||||||
|
profile: hazard3-sim
|
||||||
|
runtime: freertos
|
||||||
|
tick_hz: 1000
|
||||||
|
actions: [build, test, run, debug]
|
||||||
|
rp2350-rv:
|
||||||
|
profile: rp2350
|
||||||
|
arch: riscv
|
||||||
|
actions: [build, test, run, debug, deploy]
|
||||||
|
requires:
|
||||||
|
debug: [probe]
|
||||||
|
deploy: [probe]
|
||||||
|
rp2350-arm:
|
||||||
|
profile: rp2350
|
||||||
|
arch: arm
|
||||||
|
actions: [build, test, run, debug, deploy]
|
||||||
|
requires:
|
||||||
|
debug: [probe]
|
||||||
|
deploy: [probe]
|
||||||
|
actions:
|
||||||
|
build: ./tools/card build
|
||||||
|
test: ./tools/card test
|
||||||
|
run: ./tools/card run
|
||||||
|
debug: ./tools/card debug
|
||||||
|
deploy: ./tools/card deploy
|
||||||
|
artifacts:
|
||||||
|
directory: .stem/artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
Nazwa pliku manifestu i dokładny schemat zostaną zatwierdzone w implementacji.
|
||||||
|
Ważny jest podział: `workspace-info` mówi, skąd pobrać kartę, manifest karty
|
||||||
|
mówi, jak ją wykonać, a profil środowiska dostarcza narzędzia.
|
||||||
|
|
||||||
|
Każda akcja zapisuje maszynowo czytelny wynik do:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.stem/results/<profile>/<target>/<task>/<run-id>/result.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Wynik zawiera co najmniej commit karty, lokalny identyfikator wyniku builda,
|
||||||
|
wersję toolchainu,
|
||||||
|
polecenie, kod wyjścia, czas, testy i skróty artefaktów. Dzięki temu porównanie
|
||||||
|
AMD64 → Hazard3 → RP2350 nie opiera się na ręcznym oglądaniu terminala.
|
||||||
|
|
||||||
|
## 5. Trzy kontenery
|
||||||
|
|
||||||
|
### 5.1 `native-amd64`
|
||||||
|
|
||||||
|
Cel: najszybsza pętla dla algorytmów z podręcznika, matury i ZPE.
|
||||||
|
|
||||||
|
Narzędzia specyficzne:
|
||||||
|
|
||||||
|
- GCC i Clang;
|
||||||
|
- GDB i opcjonalnie LLDB;
|
||||||
|
- ASan, UBSan, coverage i Valgrind;
|
||||||
|
- CMake, Ninja i Make;
|
||||||
|
- testy jednostkowe C/C++.
|
||||||
|
|
||||||
|
Profil nie zawiera SDK RP2350 ani Verilatora. Testuje algorytm i kontrakt API,
|
||||||
|
nie emuluje zachowania 32-bitowego MCU. Testy przenośności muszą dodatkowo
|
||||||
|
przejść w `hazard3-sim`.
|
||||||
|
|
||||||
|
### 5.2 `hazard3-sim`
|
||||||
|
|
||||||
|
Cel: praca domowa bez płytki i dydaktyczny wgląd w procesor.
|
||||||
|
|
||||||
|
Narzędzia specyficzne:
|
||||||
|
|
||||||
|
- przypięty toolchain `riscv64-unknown-elf`;
|
||||||
|
- przypięty Verilator;
|
||||||
|
- Hazard3 RTL oraz testbench;
|
||||||
|
- runtime bare-metal i osobny BSP symulatora dla FreeRTOS;
|
||||||
|
- szybki RSP oraz planowany transport `fast-dm` do wbudowanego DM Hazard3;
|
||||||
|
- OpenOCD/remote-bitbang wyłącznie jako okresowy gate zgodności;
|
||||||
|
- opcjonalne VCD i narzędzia do zajęć z Veriloga.
|
||||||
|
|
||||||
|
Dla FreeRTOS 1 kHz timer symulatora zwiększa `mtime` dokładnie raz na cykl.
|
||||||
|
Przy deklarowanym zegarze 150 MHz tick przypada co 150 000 cykli RTL. Jest to
|
||||||
|
1 kHz czasu gościa, nie obietnica czasu rzeczywistego hosta. Kod aplikacyjny i
|
||||||
|
wektory testowe są wspólne z RP2350, natomiast BSP, startup i linker są osobne.
|
||||||
|
|
||||||
|
Bieżący port FreeRTOS RP2350 ustawia `SIO MTIME` w tryb `FULLSPEED`, dlatego
|
||||||
|
przy zegarze systemowym 150 MHz również używa 150 000 zliczeń na tick. Nie
|
||||||
|
traktujemy jednak tej stałej jako wspólnego API BSP: profil sam wylicza ją z
|
||||||
|
rzeczywistego zegara i trybu timera.
|
||||||
|
|
||||||
|
Testy czasowe mają dwa poziomy. Smoke test PR wykonuje co najmniej 100 ticków z
|
||||||
|
produkcyjnym dzielnikiem 150 000. Długi test schedulera wykonuje 10 000 ticków
|
||||||
|
na jawnie przyspieszonym zegarze testowym i nie jest dowodem częstotliwości.
|
||||||
|
Nightly/release może wykonać dłuższy test produkcyjnego dzielnika w ustalonym
|
||||||
|
budżecie czasu.
|
||||||
|
|
||||||
|
### 5.3 `rp2350`
|
||||||
|
|
||||||
|
Cel: pracownia z fizycznym Pico 2/Pico 2 W.
|
||||||
|
|
||||||
|
Narzędzia specyficzne:
|
||||||
|
|
||||||
|
- przypięty Pico SDK i FreeRTOS Kernel;
|
||||||
|
- toolchain RISC-V dla Hazard3;
|
||||||
|
- toolchain ARM dla Cortex-M33;
|
||||||
|
- CMake/Ninja;
|
||||||
|
- OpenOCD zgodny z RP2350, picotool i obsługa wybranego probe;
|
||||||
|
- serial/RTT oraz reguły diagnostyczne USB.
|
||||||
|
|
||||||
|
Target `rp2350-rv` i `rp2350-arm` wybiera konfigurację CMake, startup, linker,
|
||||||
|
GDB i backend OpenOCD. Nie wymaga kolejnego Dockerfile. Bare metal i FreeRTOS są wariantami
|
||||||
|
manifestu projektu, nie oddzielnymi kontenerami.
|
||||||
|
|
||||||
|
Kontener działa rootless, bez `--privileged`. Launcher przekazuje tylko jawnie
|
||||||
|
wybrane urządzenia. Host otrzymuje regułę udev lub ACL, która pozwala grupie
|
||||||
|
laboratoryjnej korzystać z probe bez `sudo`.
|
||||||
|
|
||||||
|
Debug probe i natywne USB płytki to dwie różne capabilities. Domyślny
|
||||||
|
debug/deploy przez zewnętrzny probe używa stabilnego urządzenia probe; reset
|
||||||
|
targetu nie powinien go odłączać. Opcjonalny BOOTSEL/picotool powoduje
|
||||||
|
re-enumerację targetu i zmianę adresu USB. Taki deploy jest osobną, krótkotrwałą
|
||||||
|
akcją z tego samego profilu `rp2350`: launcher ponownie wykrywa urządzenie po
|
||||||
|
każdej fazie i uruchamia akcję z aktualnym węzłem. Nie przekazujemy całego
|
||||||
|
`/dev`; jeżeli backend wymaga dostępu na poziomie portu/busa USB, zakres jest
|
||||||
|
jawnie raportowany i zatwierdzany dla tej instancji.
|
||||||
|
|
||||||
|
## 6. Wspólny kontrakt UI
|
||||||
|
|
||||||
|
Każdy profil dziedziczy z `dev-ui-base`:
|
||||||
|
|
||||||
|
- tmux z prefiksem `Ctrl-s`;
|
||||||
|
- Neovim i identyczny zestaw skrótów;
|
||||||
|
- Termdebug, nvim-dap i dashboard GDB;
|
||||||
|
- serwery/bridge MCP dla tmuxa i Neovima;
|
||||||
|
- `ripgrep`, `fd`, Git, Python i podstawowe narzędzia diagnostyczne;
|
||||||
|
- użytkownika bez roota i ten sam katalog roboczy `/workspace`.
|
||||||
|
|
||||||
|
Domyślny układ jednego widocznego okna tmuxa:
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Neovim — jeden pane tmuxa │
|
||||||
|
│ ┌─ Termdebug + GDB dashboard ─┬─ edytor/nvim-dap ───────┐ │
|
||||||
|
│ └──────────────────────────────┴──────────────────────────┘ │
|
||||||
|
├────────────────────────────────────────────────────────────┤
|
||||||
|
│ bash, cwd=/workspace — drugi pane tmuxa │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Termdebug i edytor są oknami jednego Neovima. Hazard3 uruchamia symulator w
|
||||||
|
drugim, domyślnie ukrytym oknie tmuxa `tb`.
|
||||||
|
|
||||||
|
Termdebug i nvim-dap są dwiema nakładkami na ten sam opis targetu GDB. Karta
|
||||||
|
nie przechowuje osobnych adresów i poleceń dla każdego frontendu.
|
||||||
|
|
||||||
|
## 7. Tożsamość instancji i MCP
|
||||||
|
|
||||||
|
Każda sesja ma stabilną tożsamość logiczną, na przykład:
|
||||||
|
|
||||||
|
```text
|
||||||
|
hazard3-sim-inf-bss-t1
|
||||||
|
```
|
||||||
|
|
||||||
|
Kontener otrzymuje label `edu.stem.instance`. Aktualny identyfikator Podmana
|
||||||
|
jest tożsamością procesu. Pełne nazwy pozostają w labels/registry, natomiast
|
||||||
|
ścieżka Unix socketu używa krótkich kluczy, aby zmieścić się w limicie
|
||||||
|
`sun_path` (maksymalnie 107 użytecznych bajtów na Linuksie):
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
`thread-key` i `instance-key` są 12-znakowymi skrótami kryptograficznymi, a
|
||||||
|
`cid12` to jednoznaczny na danym hoście prefiks bieżącego container ID. Registry
|
||||||
|
mapuje je na pełne wartości i jest weryfikowane przez `podman inspect`. Ta sama
|
||||||
|
krótka ścieżka jest widoczna na hoście i w kontenerze. Test startowy odrzuca
|
||||||
|
konfigurację przekraczającą budżet 100 bajtów.
|
||||||
|
|
||||||
|
Wrapper MCP wybiera najpierw jawną instancję, przez `podman inspect` sprawdza
|
||||||
|
jej aktualny container ID, a dopiero potem otwiera socket. `current` jest
|
||||||
|
wskaźnikiem pomocniczym ograniczonym do wątku agenta, nie globalnym wyborem dla
|
||||||
|
wszystkich kontenerów. Stary socket nie może zostać użyty po odtworzeniu
|
||||||
|
kontenera.
|
||||||
|
|
||||||
|
Agent widzi dokładnie tę samą sesję tmuxa i Neovima co uczeń. MCP nie jest
|
||||||
|
osobnym kontenerem.
|
||||||
|
|
||||||
|
## 8. Rootless Podman i SSH
|
||||||
|
|
||||||
|
Podman jest domyślnym runtime. Docker może pozostać przejściowym backendem dla
|
||||||
|
istniejących komputerów, ale kontrakt nie może wymagać członkostwa ucznia w
|
||||||
|
grupie `docker`.
|
||||||
|
|
||||||
|
Na zdalnym komputerze ucznia nauczyciel lub agent:
|
||||||
|
|
||||||
|
1. łączy się wyłącznie przez SSH z parą kluczy;
|
||||||
|
2. uruchamia `stemctl` na tym zdalnym hoście;
|
||||||
|
3. tamtejszy launcher zarządza lokalnym rootless Podmanem i lokalnym
|
||||||
|
workspace;
|
||||||
|
4. w razie potrzeby sockety MCP/GDB są udostępniane przez tunel SSH, nigdy
|
||||||
|
przez nasłuch na `0.0.0.0`.
|
||||||
|
|
||||||
|
Nie montujemy zdalnego katalogu przez SSHFS jako podstawowego modelu i nie
|
||||||
|
przekazujemy prywatnego klucza do kontenera. Repo i kontener mają znajdować się
|
||||||
|
na tym samym hoście wykonawczym.
|
||||||
|
|
||||||
|
## 9. Budowanie kontenerów ze źródeł i cache
|
||||||
|
|
||||||
|
Repo przechowuje jeden wieloetapowy `Dockerfile` i pliki Compose. Nie
|
||||||
|
przechowuje ani nie publikuje gotowych obrazów. `dev-ui-base` zawiera
|
||||||
|
przypięte binaria i zależności UI, ale nie zawiera często zmienianych
|
||||||
|
`configs/` ani entrypointów. Graf stage'ów:
|
||||||
|
|
||||||
|
```text
|
||||||
|
os-base
|
||||||
|
└─ dev-ui-base (tmux/nvim/MCP i przypięte zależności, bez configs/)
|
||||||
|
├─ ui-node-deps (przypięte zależności serwerów MCP)
|
||||||
|
├─ native-toolchain
|
||||||
|
│ └─ native-amd64-final + COPY configs/ i entrypointów
|
||||||
|
├─ hazard3-toolchain
|
||||||
|
│ └─ hazard3-sim-final + COPY configs/ i entrypointów
|
||||||
|
└─ rp2350-toolchain
|
||||||
|
└─ rp2350-final + COPY configs/ i entrypointów
|
||||||
|
```
|
||||||
|
|
||||||
|
Duże toolchainy, SDK, Verilator, Hazard3 i OpenOCD są instalowane w stage'ach
|
||||||
|
toolchain przed skopiowaniem konfiguracji UI. Zmiana lockfile zależności UI
|
||||||
|
może celowo przebudować potomków; zwykła zmiana mapowania klawisza, dashboardu
|
||||||
|
albo entrypointu nie może tego zrobić.
|
||||||
|
|
||||||
|
Kod karty nigdy nie jest kopiowany do warstw kontenera. Zmiana konfiguracji końcowej nie
|
||||||
|
może ponownie pobierać toolchainów. W CI sprawdzamy drugi build bez zmian i
|
||||||
|
raportujemy wykorzystanie cache.
|
||||||
|
|
||||||
|
Uczeń wykonuje `stemctl env ensure`, które używa lokalnego cache lub buduje
|
||||||
|
profil ze źródłowego Dockerfile. Nic nie jest pobierane z rejestru obrazów.
|
||||||
|
|
||||||
|
## 10. Migracja `rv-launcher` do `stem-launcher`
|
||||||
|
|
||||||
|
Nazwa RV jest zbyt wąska, ponieważ launcher obsługuje serie informatyczne,
|
||||||
|
fizyczne, AMD64, RISC-V i ARM. Docelowe nazwy to:
|
||||||
|
|
||||||
|
- repo: `edu-tools/stem-launcher`;
|
||||||
|
- publiczne CLI: `stemctl`;
|
||||||
|
- workspace: `~/dev/workspace/stem`;
|
||||||
|
- zmienne: `STEM_*`;
|
||||||
|
- stan karty: `.stem/`.
|
||||||
|
|
||||||
|
Migracja musi zachować działające workspace uczniów:
|
||||||
|
|
||||||
|
1. dodać `stemctl` jako nowy entrypoint do obecnej implementacji;
|
||||||
|
2. akceptować `rvctl` jako wrapper kompatybilności z ostrzeżeniem;
|
||||||
|
3. czytać nowe `STEM_*`, a następnie stare `RV_*` jako fallback;
|
||||||
|
4. domyślnie używać `~/dev/workspace/stem`, ale automatycznie wykrywać
|
||||||
|
istniejące `~/dev/workspace/rv`;
|
||||||
|
5. zmigrować `.rv/` do `.stem/` bez usuwania stanu;
|
||||||
|
6. dopiero wtedy zmienić nazwę repo na Gitea i zaktualizować manifest;
|
||||||
|
7. pozostawić alias lub repo informacyjne pod starą nazwą na co najmniej jeden
|
||||||
|
cykl zajęć;
|
||||||
|
8. usunąć kompatybilność dopiero po telemetrycznym/audytowym potwierdzeniu, że
|
||||||
|
materiały nie wywołują starych nazw.
|
||||||
|
|
||||||
|
Szczegóły i kryteria wycofania opisuje `doc/migration-stem-launcher.md`.
|
||||||
|
|
||||||
|
## 11. Kolejność wdrożenia
|
||||||
|
|
||||||
|
### P0 — kontrakt i bezpieczeństwo
|
||||||
|
|
||||||
|
1. Zatwierdzić nazwy profili, capabilities i schemat manifestu karty.
|
||||||
|
2. Dodać `stemctl` oraz zgodność `rvctl`.
|
||||||
|
3. Przenieść runtime z Docker Compose na abstrakcję z rootless Podmanem jako
|
||||||
|
backendem domyślnym.
|
||||||
|
4. Rozdzielić sekrety hosta od kontenerów i usunąć tokeny z URL-i remotes po
|
||||||
|
operacji Git.
|
||||||
|
5. Ustabilizować tożsamość instancji, labels i sockety MCP.
|
||||||
|
|
||||||
|
### P1 — trzy targety jednego Dockerfile
|
||||||
|
|
||||||
|
1. Wyodrębnić `dev-ui-base`.
|
||||||
|
2. Przenieść istniejący profil `host` do `native-amd64`.
|
||||||
|
3. Przenieść istniejący `rv32i` do `hazard3-sim` i dodać simulatorowy BSP
|
||||||
|
FreeRTOS 1 kHz.
|
||||||
|
4. Zbudować `rp2350` z targetami RV i ARM oraz bezpiecznym przekazywaniem probe.
|
||||||
|
5. Ujednolicić layout tmuxa, Termdebug, DAP i MCP.
|
||||||
|
|
||||||
|
### P2 — pełny pipeline karty
|
||||||
|
|
||||||
|
1. Zaimplementować manifest karty i akcje `build/test/run/debug/deploy`.
|
||||||
|
2. Zapisywać wyniki i metadane artefaktów.
|
||||||
|
3. Porównywać golden tests pomiędzy trzema profilami.
|
||||||
|
4. Dodać `fast-dm` oraz okresowy gate JTAG.
|
||||||
|
5. Dodać zdalny smoke test przez SSH bez otwierania portów debuggera.
|
||||||
|
|
||||||
|
## 12. Bramki akceptacyjne
|
||||||
|
|
||||||
|
| Obszar | Kryterium |
|
||||||
|
| --- | --- |
|
||||||
|
| Fetch | czysty host pobiera manifest i kartę wyłącznie przez `stemctl` |
|
||||||
|
| Persistence | usunięcie kontenera nie zmienia ani nie usuwa repo ucznia |
|
||||||
|
| Native | golden tests, ASan i UBSan przechodzą na AMD64 |
|
||||||
|
| Emulator | te same wektory przechodzą jako RV32 bare metal i FreeRTOS |
|
||||||
|
| FreeRTOS | co najmniej 100 ticków z produkcyjnym dzielnikiem 150 000 oraz 10 000 ticków przyspieszonego testu schedulera bez zgubionego cyklu |
|
||||||
|
| RP2350 RV | build, flash, reset, UART i debug przez probe przechodzą bez `sudo` |
|
||||||
|
| RP2350 ARM | ten sam zestaw operacji przechodzi w tym samym obrazie |
|
||||||
|
| USB | probe pozostaje dostępny po resecie; BOOTSEL/picotool przechodzi kontrolowaną re-enumerację bez `--privileged` |
|
||||||
|
| UI | layout tmux/Nvim, skróty i dashboard są identyczne w trzech profilach |
|
||||||
|
| MCP | agent steruje wskazaną instancją i nie łączy się ze starym socketem |
|
||||||
|
| Isolation | brak socketu runtime, prywatnych kluczy i `--privileged` w kontenerze |
|
||||||
|
| Reproducibility | wynik zapisuje commit karty, lokalny ID builda i wersje toolchainów |
|
||||||
|
| SSH | praca zdalna działa przez klucze i tunele na loopback, bez publicznego GDB |
|
||||||
|
| Cache | zmiana entrypointu lub konfiguracji UI nie powoduje ponownego pobrania toolchainów |
|
||||||
|
| Compatibility | stare `rvctl` i workspace `/rv` działają w okresie migracji |
|
||||||
|
|
||||||
|
## 13. Poza zakresem tej iteracji
|
||||||
|
|
||||||
|
- czwarty kontener dla hoverboardu;
|
||||||
|
- orkiestracja klastra albo Kubernetes;
|
||||||
|
- przechowywanie pracy ucznia wewnątrz warstwy kontenera;
|
||||||
|
- udostępnianie GDB, UART lub MCP na publicznym interfejsie;
|
||||||
|
- automatyczne podawanie agentowi prywatnych kluczy i tokenów;
|
||||||
|
- gwarancja działania symulacji RTL w czasie rzeczywistym.
|
||||||
+250
-129
@@ -1,162 +1,283 @@
|
|||||||
# Kontenery i debug
|
# Kontenery i debug
|
||||||
|
|
||||||
`rvctl` integruje workspace z narzędziem `rv32i-hazard3-student-env`.
|
Ten dokument opisuje wdrożony kontrakt kontenerów i jawnie wskazuje pozostałe
|
||||||
Środowisko ma dwa profile kontenerów:
|
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`,
|
Stan obecny:
|
||||||
`gdb-multiarch`, toolchainem RISC-V i symulatorem Hazard3.
|
|
||||||
- `host` - natywne środowisko na architekturze hosta z `clang`, `gcc`, `gdb`,
|
|
||||||
opcjonalnie `lldb`, `valgrind`, `nvim` i `tmux`.
|
|
||||||
|
|
||||||
Profile są uruchamiane jako oddzielne usługi `docker compose`, więc zależności
|
- `stemctl` uruchamia trzy profile przez rootless Podman;
|
||||||
RISC-V i zależności natywnego debugowania nie mieszają się ze sobą.
|
- 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
|
```bash
|
||||||
./rvctl env list
|
stemctl env list
|
||||||
./rvctl env sync
|
stemctl env ensure native-amd64
|
||||||
./rvctl env build rv32i
|
stemctl env ensure hazard3-sim
|
||||||
./rvctl env build host
|
stemctl env ensure rp2350
|
||||||
|
|
||||||
./rvctl debug rv32i inf bss 1
|
stemctl build native-amd64 inf bss 1
|
||||||
./rvctl debug host inf bss 1
|
stemctl test native-amd64 inf bss 1
|
||||||
./rvctl run host inf bss 1
|
stemctl run native-amd64 inf bss 1
|
||||||
./rvctl shell rv32i inf bss
|
stemctl debug native-amd64 inf bss 1
|
||||||
./rvctl shell host inf bss
|
|
||||||
|
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`,
|
Selektory `series card task` są opcjonalne, jeśli ustawiono wartości domyślne.
|
||||||
`defaults.card` i `defaults.task`, można używać krótszych komend:
|
Każda akcja obsługuje:
|
||||||
|
|
||||||
```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:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
profile service image status purpose
|
--instance NAME
|
||||||
rv32i env edu-inf/rv32i-hazard3-env:latest ready RV32I/Hazard3 nvim + gdb-multiarch
|
--target NAME
|
||||||
host host edu-inf/rv32i-hazard3-host-env:latest ready native host clang/gcc + gdb/lldb
|
--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
|
```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
|
```bash
|
||||||
./rvctl env sync
|
stemctl start hazard3-sim inf bss 1
|
||||||
./rvctl env sync --dry-run
|
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
|
`start` jest idempotentne dla zgodnej instancji. Jeżeli zmienił się lokalny ID
|
||||||
`~/dev/edu/repos/rv/rv32i-hazard3-env`, o ile jest dostępny.
|
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
|
```bash
|
||||||
./rvctl env build rv32i
|
ssh uczen-lab
|
||||||
./rvctl env build host
|
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
|
## Budowanie i cache
|
||||||
./rvctl env build host --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
## `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
|
| Element | Status |
|
||||||
./rvctl debug rv32i inf bss 1
|
| --- | --- |
|
||||||
./rvctl debug host inf bss 1
|
| `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 |
|
||||||
W profilu `rv32i` startuje środowisko Hazard3: symulator, `gdb-multiarch` i
|
| `stemctl` | wdrożony; `rvctl` jest wrapperem zgodności |
|
||||||
`nvim` w sesji `tmux`.
|
| pobieranie kart z Git | istnieje w `rvctl` |
|
||||||
|
| `build/test/run/debug/deploy` | wdrożony dispatcher manifestu i `result.json` |
|
||||||
W profilu `host` wybrane źródło C jest kompilowane natywnie z debug info, a
|
| sockety tmux/nvim | wdrożone z `thread12/instance12/container12` i resolverem |
|
||||||
potem uruchamiane w `gdb` obok `nvim`.
|
| zdalny SSH | kontrakt zaprojektowany, smoke test do dodania |
|
||||||
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`.
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Migracja `rv-launcher` do `stem-launcher`
|
||||||
|
|
||||||
|
Status: M1–M3 wdrożone lokalnie; M4 (zmiana nazwy zdalnego repo) celowo
|
||||||
|
odłożone do zakończenia okresu zgodności
|
||||||
|
Data: 2026-07-14
|
||||||
|
|
||||||
|
## Dlaczego zmieniamy nazwę
|
||||||
|
|
||||||
|
Launcher obsługuje już kod natywny AMD64 i Hazard3, a plan obejmuje RP2350
|
||||||
|
RISC-V, RP2350 ARM, fizykę i kolejne serie STEM. Nazwy `rv-launcher`, `rvctl`,
|
||||||
|
`RV_*` i `~/dev/workspace/rv` błędnie sugerują narzędzie ograniczone do RISC-V.
|
||||||
|
|
||||||
|
Docelowy słownik:
|
||||||
|
|
||||||
|
| Obecnie | Docelowo |
|
||||||
|
| --- | --- |
|
||||||
|
| `rv-launcher` | `stem-launcher` |
|
||||||
|
| `rvctl` | `stemctl` |
|
||||||
|
| `rvctl.py` | wewnętrzny moduł launchera; nazwa nie jest publicznym API |
|
||||||
|
| `RV_*` | `STEM_*` |
|
||||||
|
| `.rv/` | `.stem/` |
|
||||||
|
| `~/dev/workspace/rv` | `~/dev/workspace/stem` |
|
||||||
|
| profil `host` | `native-amd64` |
|
||||||
|
| profil `rv32i` | `hazard3-sim` |
|
||||||
|
|
||||||
|
## Zasada migracji
|
||||||
|
|
||||||
|
Najpierw dostarczamy zgodność w kodzie, a dopiero później zmieniamy nazwę repo
|
||||||
|
na serwerze. Odwrotna kolejność zepsułaby bootstrap, token records, remotes,
|
||||||
|
ścieżki dokumentacji i istniejące workspace uczniów.
|
||||||
|
|
||||||
|
## Etapy
|
||||||
|
|
||||||
|
### M1 — alias CLI (wdrożone)
|
||||||
|
|
||||||
|
- dodać wykonywalny `stemctl` wskazujący tę samą implementację;
|
||||||
|
- pozostawić `rvctl` jako wrapper;
|
||||||
|
- pomoc i nowe materiały pokazują wyłącznie `stemctl`;
|
||||||
|
- `rvctl` drukuje jednorazowe ostrzeżenie o wycofaniu na stderr, ale zachowuje
|
||||||
|
format stdout potrzebny skryptom.
|
||||||
|
|
||||||
|
### M2 — konfiguracja i ścieżki (wdrożone)
|
||||||
|
|
||||||
|
- dodać wersjonowaną migrację `workspace.json`;
|
||||||
|
- nowe zmienne `STEM_*` mają pierwszeństwo, stare `RV_*` są fallbackiem;
|
||||||
|
- jeśli istnieje `/workspace/.stem`, użyć go;
|
||||||
|
- jeśli istnieje tylko `/workspace/.rv`, użyć go i zaproponować migrację;
|
||||||
|
- komenda `stemctl workspace doctor` pokazuje użyte ścieżki i runtime;
|
||||||
|
- komenda `stemctl workspace migrate --dry-run` nie zmienia danych, tylko
|
||||||
|
pokazuje plan.
|
||||||
|
|
||||||
|
Migracja katalogu stanu musi być atomowa na pojedynczym filesystemie i nigdy
|
||||||
|
nie usuwa starego katalogu przed zweryfikowaniem nowego.
|
||||||
|
|
||||||
|
### M3 — profile i kontenery (wdrożone)
|
||||||
|
|
||||||
|
- wprowadzić `native-amd64`, `hazard3-sim`, `rp2350`;
|
||||||
|
- zachować aliasy `host -> native-amd64` oraz `rv32i -> hazard3-sim`;
|
||||||
|
- kontenery otrzymują labels z nazwą kanoniczną, nie aliasem;
|
||||||
|
- wyniki zapisują nazwę kanoniczną oraz opcjonalne `requested_alias`.
|
||||||
|
|
||||||
|
### M4 — repo Gitea (odłożone)
|
||||||
|
|
||||||
|
Po wydaniu kompatybilnego launchera:
|
||||||
|
|
||||||
|
1. utworzyć lub zmienić nazwę na `edu-tools/stem-launcher`;
|
||||||
|
2. zaktualizować `workspace-info`, bootstrap i token records;
|
||||||
|
3. sprawdzić clone/fetch/push przez nowe URL;
|
||||||
|
4. pozostawić pod `edu-tools/rv-launcher` przekierowanie albo małe repo z
|
||||||
|
komunikatem migracyjnym, zależnie od możliwości Gitea;
|
||||||
|
5. nie usuwać starej nazwy podczas trwającego semestru.
|
||||||
|
|
||||||
|
Zmiana repo na Gitea jest operacją administracyjną i nie jest wykonywana przez
|
||||||
|
samą aktualizację dokumentacji.
|
||||||
|
|
||||||
|
### M5 — wycofanie kompatybilności
|
||||||
|
|
||||||
|
Stare nazwy można usunąć dopiero, gdy:
|
||||||
|
|
||||||
|
- wszystkie serie w `workspace-info` wskazują nowe CLI i repo;
|
||||||
|
- CI nie używa `rvctl`, `RV_*`, `.rv` ani profili `host`/`rv32i`;
|
||||||
|
- obraz pracowni został odświeżony;
|
||||||
|
- co najmniej jeden pełny cykl zajęć przeszedł na `stemctl`;
|
||||||
|
- dostępna jest instrukcja ręcznego odzyskania starego workspace.
|
||||||
|
|
||||||
|
## Testy migracji
|
||||||
|
|
||||||
|
1. Nowa instalacja bez istniejącego workspace.
|
||||||
|
2. Istniejący workspace `/rv` z kartami i bez aktywnych kontenerów.
|
||||||
|
3. Workspace z `.rv/<instance>` i zatrzymanym kontenerem.
|
||||||
|
4. Store tokenów wskazujący repo `rv-launcher`.
|
||||||
|
5. Remotes bez sekretu oraz remotes z historycznym tokenem w URL.
|
||||||
|
6. Równoległe instancje profili `host` i `rv32i`.
|
||||||
|
7. Ponowne wykonanie migracji — wynik musi być idempotentny.
|
||||||
|
8. Rollback konfiguracji bez utraty repo, branchy, artefaktów i logów.
|
||||||
|
|
||||||
|
## Kryterium zakończenia
|
||||||
|
|
||||||
|
Uczeń po migracji wykonuje:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stemctl series cards fetch inf bss
|
||||||
|
stemctl test native-amd64 inf bss 1
|
||||||
|
stemctl debug hazard3-sim inf bss 1
|
||||||
|
stemctl deploy rp2350 inf bss 1 --target rp2350-rv --device /dev/bus/usb/001/006
|
||||||
|
```
|
||||||
|
|
||||||
|
Istniejący skrypt używający `rvctl debug rv32i` nadal działa w zadeklarowanym
|
||||||
|
okresie kompatybilności i trafia do tego samego kanonicznego profilu.
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# Senior review Claude — 2026-07-14
|
||||||
|
|
||||||
|
Zakres recenzji:
|
||||||
|
|
||||||
|
- `stem-launcher`: plan architektury, kontenery, agenci i migracja nazw;
|
||||||
|
- `rv32i-hazard3-student-env`: plan ewaluacji, Containerfile/Dockerfile,
|
||||||
|
Compose, integracja launchera i specyfikacja trzech obrazów;
|
||||||
|
- zgodność planu z bieżącym `rvctl.py` i `workspace.json`.
|
||||||
|
|
||||||
|
## Werdykt
|
||||||
|
|
||||||
|
Claude wydał akceptację warunkową. Zaakceptował:
|
||||||
|
|
||||||
|
- dokładnie trzy kontenery robocze i wspólną warstwę UI;
|
||||||
|
- profile `native-amd64`, `hazard3-sim`, `rp2350` oraz targety wewnątrz
|
||||||
|
profilu;
|
||||||
|
- Git, klucze i tokeny wyłącznie na hoście;
|
||||||
|
- rootless Podmana bez socketu runtime i bez `--privileged`;
|
||||||
|
- capabilities i wynik `unsupported` zamiast warunków zaszytych w launcherze;
|
||||||
|
- migrację zgodności przed zmianą nazwy repo Gitea;
|
||||||
|
- `fast-rsp` jako obecny backend, `fast-dm` jako kierunek i JTAG jako gate;
|
||||||
|
- tę samą sesję tmux/Neovim dla ucznia i agenta.
|
||||||
|
|
||||||
|
## Findings i rozstrzygnięcia
|
||||||
|
|
||||||
|
### P1 — graf cache Containerfile
|
||||||
|
|
||||||
|
Problem: częsta konfiguracja UI znajdowała się w stage'u będącym przodkiem
|
||||||
|
ciężkich toolchainów. Jej zmiana przebudowałaby SDK i Verilator.
|
||||||
|
|
||||||
|
Rozstrzygnięcie: `dev-ui-base` zawiera przypięte binaria/dependencies UI, ale
|
||||||
|
nie `configs/` ani entrypointy. Toolchainowe stage'e dziedziczą z niego, a
|
||||||
|
konfiguracja jest kopiowana dopiero do finalnych stage'ów. Test cache obejmuje
|
||||||
|
zmianę entrypointu i konfiguracji UI.
|
||||||
|
|
||||||
|
### P1 — limit Unix socketów
|
||||||
|
|
||||||
|
Problem: pełne `thread/instance/container-id` w ścieżce workspace przekraczały
|
||||||
|
linuksowy limit `sun_path`.
|
||||||
|
|
||||||
|
Rozstrzygnięcie:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$XDG_RUNTIME_DIR/stem/<thread-key>/<instance-key>/<cid12>/t.sock
|
||||||
|
$XDG_RUNTIME_DIR/stem/<thread-key>/<instance-key>/<cid12>/n.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
Pełne wartości są w registry/labels. Budżet ścieżki wynosi 100 bajtów.
|
||||||
|
|
||||||
|
### P1 — `deploy hazard3-sim`
|
||||||
|
|
||||||
|
Problem: jeden dokument utożsamiał deploy z załadowaniem symulatora, pozostałe
|
||||||
|
nie definiowały tej operacji.
|
||||||
|
|
||||||
|
Rozstrzygnięcie: `deploy` dla `hazard3-sim` jest `unsupported`. `run` i `debug`
|
||||||
|
ładują artefakt. Deploy pozostaje operacją targetu sprzętowego.
|
||||||
|
|
||||||
|
### P1 — status `fast-dm`
|
||||||
|
|
||||||
|
Problem: niezaimplementowany backend był w jednym miejscu opisany jako
|
||||||
|
codzienny.
|
||||||
|
|
||||||
|
Rozstrzygnięcie: dokumenty jawnie mówią:
|
||||||
|
|
||||||
|
- teraz: `fast-rsp`;
|
||||||
|
- docelowo: `fast-dm`;
|
||||||
|
- okresowo: `fidelity-jtag`.
|
||||||
|
|
||||||
|
### P1 — USB RP2350
|
||||||
|
|
||||||
|
Problem: statyczne `--device` nie obsługuje hotplug i re-enumeracji BOOTSEL.
|
||||||
|
|
||||||
|
Rozstrzygnięcie: zewnętrzny debug probe jest stabilnym, domyślnym backendem.
|
||||||
|
Natywne USB targetu/picotool jest osobną capability i krótkotrwałą akcją z tego
|
||||||
|
samego obrazu; launcher ponownie rozwiązuje węzeł po każdej fazie. Build/test
|
||||||
|
offline nie wymagają probe.
|
||||||
|
|
||||||
|
### P2 — koszt testu 10 000 ticków
|
||||||
|
|
||||||
|
Problem: 10 000 × 150 000 daje 1,5 miliarda cykli RTL.
|
||||||
|
|
||||||
|
Rozstrzygnięcie: PR wykonuje co najmniej 100 ticków z produkcyjnym dzielnikiem,
|
||||||
|
a długi test schedulera 10 000 ticków działa z jawnie przyspieszonym zegarem.
|
||||||
|
Nightly/release otrzymuje oddzielny budżet testu produkcyjnego.
|
||||||
|
|
||||||
|
### P2 — pozostałe
|
||||||
|
|
||||||
|
- wymaganie `probe` przeniesiono z profilu na akcje `debug/deploy`;
|
||||||
|
- format instancji ujednolicono do `task<N>`;
|
||||||
|
- manifest otrzymał `default_target` per profil;
|
||||||
|
- `run rp2350` wymaga pasującego rekordu deploy albo jawnej zgody na istniejący
|
||||||
|
firmware;
|
||||||
|
- bramka przenośności rozróżnia wektory niezależne od ABI.
|
||||||
|
|
||||||
|
## Korekta jednej sugestii recenzenta
|
||||||
|
|
||||||
|
Claude zasugerował, że realny RP2350 może taktować `MTIME` inną częstotliwością
|
||||||
|
niż symulator. Jest to prawdziwe dla domyślnego trybu SIO, ale nie dla badanego
|
||||||
|
portu FreeRTOS. Lokalny
|
||||||
|
`FreeRTOS-Kernel/portable/ThirdParty/GCC/RP2350_RISC-V/port.c` wywołuje:
|
||||||
|
|
||||||
|
```c
|
||||||
|
riscv_timer_set_fullspeed(true);
|
||||||
|
uxTimerIncrementsForOneTick = clock_get_hz(clk_sys) / configTICK_RATE_HZ;
|
||||||
|
```
|
||||||
|
|
||||||
|
Przy 150 MHz i 1 kHz oba środowiska używają więc 150 000 zliczeń. Plan nadal
|
||||||
|
wymaga osobnych BSP i niezależnego wyliczania dzielnika, ponieważ tryb timera i
|
||||||
|
zegar mogą się zmienić.
|
||||||
|
|
||||||
|
## Wynik etapu projektu
|
||||||
|
|
||||||
|
Wszystkie findings P1 oraz zasadne P2 zostały naniesione do planu. Ten fragment
|
||||||
|
review odbył się przed implementacją i dlatego nie stanowił jeszcze dowodu
|
||||||
|
działania środowiska.
|
||||||
|
|
||||||
|
## Review implementacji
|
||||||
|
|
||||||
|
Po wdrożeniu Claude wykonał dwa pełne oraz zawężone przeglądy diffów. Wykryte
|
||||||
|
problemy i ich rozstrzygnięcia:
|
||||||
|
|
||||||
|
- logi GDB/JTAG/UART sugerowały `0.0.0.0`, mimo planowanego loopbacku — bind i
|
||||||
|
logi są teraz jednoznacznie `127.0.0.1`;
|
||||||
|
- Compose zawierał martwe zmienne MCP — Compose służy wyłącznie do lokalnego
|
||||||
|
buildu i niezarządzanej powłoki, a sesje MCP tworzy kanonicznie `stem`;
|
||||||
|
- zgodnościowy `tmux-container` omijał rootless runtime — deleguje teraz do
|
||||||
|
`stem shell`;
|
||||||
|
- `status/attach/stop/rm --instance` zależały od bieżącej domyślnej karty —
|
||||||
|
operują teraz po stabilnej nazwie instancji bez checkoutu karty;
|
||||||
|
- `attach` zakładał jedną błędną nazwę sesji — rozpoznaje sesje `stem-*`,
|
||||||
|
`rv32i-*`, `host-*` lub jedyną sesję z dedykowanego socketu;
|
||||||
|
- osierocony socket tmux kończył `attach` bez diagnostyki — zwracany jest teraz
|
||||||
|
komunikat nakazujący ponowne uruchomienie debug UI;
|
||||||
|
- rekord deployu pobierał commit z katalogu launchera — używa teraz
|
||||||
|
`git -C <repo-karty> rev-parse HEAD`.
|
||||||
|
|
||||||
|
Końcowe, zawężone review obu repozytoriów zakończyło się wynikiem
|
||||||
|
`NO_P0_P1`. Potwierdzone zostały także:
|
||||||
|
|
||||||
|
- build wszystkich trzech lokalnych profili z warstwami toolchainów w cache;
|
||||||
|
- 12/12 testów kontraktowych środowiska i 12/12 testów launchera;
|
||||||
|
- FreeRTOS Blink 1 kHz dla RP2350 RISC-V oraz ARM;
|
||||||
|
- wykonanie karty bare-metal w Hazard3;
|
||||||
|
- Termdebug i dashboard po lewej oraz źródło po prawej, w górnym pane tmuxa;
|
||||||
|
- działanie MCP dla tmuxa i Neovima w wybranym kontenerze;
|
||||||
|
- nasłuch symulatora GDB wyłącznie na loopbacku.
|
||||||
|
|
||||||
|
Poza bieżącym wdrożeniem pozostają jawnie oznaczone etapy badawcze:
|
||||||
|
`fast-dm` wykorzystujący Debug Module Hazard3 oraz BSP FreeRTOS dla symulatora
|
||||||
|
RTL. Nie są one przedstawiane jako gotowe.
|
||||||
+5
-2
@@ -1164,7 +1164,10 @@ Przykłady:
|
|||||||
|
|
||||||
## `tmux-container [series] [card]`
|
## `tmux-container [series] [card]`
|
||||||
|
|
||||||
Tworzy nową sesję `tmux` i uruchamia kontener w `pane 0`.
|
Komenda zgodności. Tworzy nową sesję hostowego `tmux`, a w `pane 0` uruchamia
|
||||||
|
`stem shell` dla karty. Tworzenie kontenera, bind-mount `/workspace`, labels,
|
||||||
|
registry i sockety pozostają w jednym kanonicznym runtime rootless Podmana;
|
||||||
|
komenda nie wywołuje bezpośrednio Docker Compose.
|
||||||
|
|
||||||
Argumenty pozycyjne:
|
Argumenty pozycyjne:
|
||||||
|
|
||||||
@@ -1180,7 +1183,7 @@ Przełączniki:
|
|||||||
- `--window NAME`
|
- `--window NAME`
|
||||||
Nadpisuje nazwę okna `tmux`.
|
Nadpisuje nazwę okna `tmux`.
|
||||||
- `--instance NAME`
|
- `--instance NAME`
|
||||||
Ustawia `RV_INSTANCE` dla wrappera `rv`.
|
Ustawia stabilne `STEM_INSTANCE` dla runtime `stem`.
|
||||||
- `--attach`
|
- `--attach`
|
||||||
Po utworzeniu sesji robi `tmux attach`.
|
Po utworzeniu sesji robi `tmux attach`.
|
||||||
- `--dry-run`
|
- `--dry-run`
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
if [[ "${STEM_RVCTL_SILENCE_DEPRECATION:-0}" != "1" ]]; then
|
||||||
|
printf '%s\n' 'warning: rvctl is deprecated; use stemctl (compatibility mode remains enabled).' >&2
|
||||||
|
fi
|
||||||
|
export STEM_CLI_NAME=rvctl
|
||||||
exec python3 "$SCRIPT_DIR/rvctl.py" "$@"
|
exec python3 "$SCRIPT_DIR/rvctl.py" "$@"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""rvctl command-line implementation."""
|
"""STEM workspace launcher (``stemctl`` with ``rvctl`` compatibility)."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass, replace
|
from dataclasses import dataclass, replace
|
||||||
@@ -111,30 +112,79 @@ REPO_PERMISSION_FIELDS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
ENV_PROFILES = {
|
ENV_PROFILES = {
|
||||||
"rv32i": {
|
"native-amd64": {
|
||||||
"service": "env",
|
"service": "native-amd64",
|
||||||
"image": "edu-inf/rv32i-hazard3-env:latest",
|
"image": "localhost/stem/dev-native-amd64:local",
|
||||||
"purpose": "RV32I/Hazard3 nvim + gdb-multiarch",
|
"purpose": "native AMD64 C/C++ tests, sanitizers and GDB/LLDB",
|
||||||
},
|
},
|
||||||
"host": {
|
"hazard3-sim": {
|
||||||
"service": "host",
|
"service": "hazard3-sim",
|
||||||
"image": "edu-inf/rv32i-hazard3-host-env:latest",
|
"image": "localhost/stem/dev-hazard3-sim:local",
|
||||||
"purpose": "native host clang/gcc + gdb/lldb",
|
"purpose": "Hazard3 RTL, RV32 bare metal/FreeRTOS and GDB",
|
||||||
|
},
|
||||||
|
"rp2350": {
|
||||||
|
"service": "rp2350",
|
||||||
|
"image": "localhost/stem/dev-rp2350:local",
|
||||||
|
"purpose": "physical RP2350 RISC-V or ARM build, deploy and debug",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ENV_PROFILE_ALIASES = {
|
||||||
|
"host": "native-amd64",
|
||||||
|
"native": "native-amd64",
|
||||||
|
"amd64": "native-amd64",
|
||||||
|
"rv32i": "hazard3-sim",
|
||||||
|
"riscv": "hazard3-sim",
|
||||||
|
"riscv32": "hazard3-sim",
|
||||||
|
"hazard3": "hazard3-sim",
|
||||||
|
}
|
||||||
|
|
||||||
|
ENV_PROFILE_CHOICES = sorted(set(ENV_PROFILES) | set(ENV_PROFILE_ALIASES))
|
||||||
|
PROFILE_DEFAULT_TARGETS = {
|
||||||
|
"native-amd64": "native",
|
||||||
|
"hazard3-sim": "hazard3-baremetal",
|
||||||
|
"rp2350": "rp2350-rv",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def default_runtime_socket_root(workspace_root: Path) -> Path:
|
||||||
|
xdg_runtime = os.environ.get("XDG_RUNTIME_DIR", "").strip()
|
||||||
|
if xdg_runtime:
|
||||||
|
return Path(xdg_runtime).expanduser().resolve() / "stem"
|
||||||
|
run_user = Path(f"/run/user/{os.getuid()}")
|
||||||
|
if run_user.is_dir():
|
||||||
|
return run_user / "stem"
|
||||||
|
return workspace_root / ".runtime" / "stem"
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_path: Path) -> WorkspaceConfig:
|
def load_config(config_path: Path) -> WorkspaceConfig:
|
||||||
raw = json.loads(config_path.read_text(encoding="utf-8"))
|
raw = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
base_dir = config_path.parent
|
base_dir = config_path.parent
|
||||||
|
|
||||||
original_root = expand_path(raw.get("original_root", "~/dev/edu/repos/rv"), base_dir)
|
original_root = expand_path(raw.get("original_root", "~/dev/edu/repos/stem"), base_dir)
|
||||||
workspace_root = expand_path(raw.get("workspace_root", "~/dev/workspace/rv"), base_dir)
|
configured_workspace = os.environ.get("STEM_WORKSPACE_ROOT") or raw.get(
|
||||||
|
"workspace_root", "~/dev/workspace/stem"
|
||||||
|
)
|
||||||
|
workspace_root = expand_path(str(configured_workspace), base_dir)
|
||||||
|
legacy_workspace_root = expand_path(
|
||||||
|
str(raw.get("legacy_workspace_root", "~/dev/workspace/rv")), base_dir
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
"STEM_WORKSPACE_ROOT" not in os.environ
|
||||||
|
and not workspace_root.exists()
|
||||||
|
and legacy_workspace_root.exists()
|
||||||
|
):
|
||||||
|
workspace_root = legacy_workspace_root
|
||||||
git_raw = raw.get("git", {})
|
git_raw = raw.get("git", {})
|
||||||
base_url = git_raw.get("base_url", "http://77.90.8.171:3001").rstrip("/")
|
base_url = git_raw.get("base_url", "http://77.90.8.171:3001").rstrip("/")
|
||||||
original_series_root = expand_path(raw.get("original_series_root", str(original_root / "series")), base_dir)
|
original_series_root = expand_path(raw.get("original_series_root", str(original_root / "series")), base_dir)
|
||||||
series_root = expand_path(raw.get("series_root", str(workspace_root / "series")), base_dir)
|
series_root = expand_path(raw.get("series_root", str(workspace_root / "series")), base_dir)
|
||||||
socket_root = expand_path(raw.get("socket_root", str(workspace_root / "sockets")), base_dir)
|
configured_socket_root = os.environ.get("STEM_SOCKET_ROOT") or raw.get("socket_root")
|
||||||
|
socket_root = (
|
||||||
|
expand_path(str(configured_socket_root), base_dir)
|
||||||
|
if configured_socket_root
|
||||||
|
else default_runtime_socket_root(workspace_root)
|
||||||
|
)
|
||||||
token_path = expand_path(raw.get("token_file", str(workspace_root / "tokens" / "tokens.json")), base_dir)
|
token_path = expand_path(raw.get("token_file", str(workspace_root / "tokens" / "tokens.json")), base_dir)
|
||||||
workspace_info_path = expand_path(
|
workspace_info_path = expand_path(
|
||||||
raw.get("workspace_info_path", str(workspace_root / "meta" / "workspace-info" / "workspace.json")),
|
raw.get("workspace_info_path", str(workspace_root / "meta" / "workspace-info" / "workspace.json")),
|
||||||
@@ -146,13 +196,18 @@ def load_config(config_path: Path) -> WorkspaceConfig:
|
|||||||
candidate_values = [
|
candidate_values = [
|
||||||
raw.get("tools_root", str(workspace_root / "tools" / "rv32i-hazard3-student-env")),
|
raw.get("tools_root", str(workspace_root / "tools" / "rv32i-hazard3-student-env")),
|
||||||
str(workspace_root / "tools" / "rv32i-hazard3-env"),
|
str(workspace_root / "tools" / "rv32i-hazard3-env"),
|
||||||
|
str(Path.home() / "dev" / "workspace" / "stem" / "tools" / "rv32i-hazard3-student-env"),
|
||||||
|
str(Path.home() / "dev" / "workspace" / "rv" / "tools" / "rv32i-hazard3-student-env"),
|
||||||
str(original_root / "rv32i-hazard3-student-env"),
|
str(original_root / "rv32i-hazard3-student-env"),
|
||||||
str(original_root / "rv32i-hazard3-env"),
|
str(original_root / "rv32i-hazard3-env"),
|
||||||
]
|
]
|
||||||
tools_root_candidates = [expand_path(value, base_dir) for value in candidate_values]
|
tools_root_candidates = [expand_path(value, base_dir) for value in candidate_values]
|
||||||
|
adjacent_tools = (base_dir.parent / "rv32i-hazard3-student-env").resolve()
|
||||||
|
if adjacent_tools.is_dir() and adjacent_tools not in tools_root_candidates:
|
||||||
|
tools_root_candidates.insert(0, adjacent_tools)
|
||||||
|
|
||||||
tools_root = next((path for path in tools_root_candidates if path.exists()), tools_root_candidates[0])
|
tools_root = next((path for path in tools_root_candidates if path.exists()), tools_root_candidates[0])
|
||||||
env_tool_url = raw.get("env_tool_url", f"{base_url}/edu-inf/rv32i-hazard3-student-env.git")
|
env_tool_url = raw.get("env_tool_url", f"{base_url}/edu-tools/rv32i-hazard3-student-env.git")
|
||||||
defaults = raw.get("defaults", {})
|
defaults = raw.get("defaults", {})
|
||||||
workspace_info_url = raw.get("workspace_info_url", f"{base_url}/edu-workspace/workspace-info.git")
|
workspace_info_url = raw.get("workspace_info_url", f"{base_url}/edu-workspace/workspace-info.git")
|
||||||
git = GitConfig(
|
git = GitConfig(
|
||||||
@@ -3347,152 +3402,15 @@ def tmux_target_exists(session_name: str) -> bool:
|
|||||||
return result.returncode == 0
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
def current_user_slug() -> str:
|
|
||||||
home_dir = Path(os.environ.get("HOME") or str(Path.home()))
|
|
||||||
return slug_value(os.environ.get("USER") or home_dir.name or f"uid{os.getuid()}", "user name")
|
|
||||||
|
|
||||||
|
|
||||||
def display_user_slug(raw_user: str) -> str:
|
|
||||||
match = re.fullmatch(r"u(\d+)", raw_user)
|
|
||||||
if match:
|
|
||||||
return f"u{int(match.group(1)):02d}"
|
|
||||||
return raw_user
|
|
||||||
|
|
||||||
|
|
||||||
def runtime_name_slug(raw_value: str, field_name: str) -> str:
|
|
||||||
slug = re.sub(r"[^a-z0-9]+", "_", raw_value.strip().lower()).strip("_")
|
|
||||||
if not slug:
|
|
||||||
raise SystemExit(f"Invalid {field_name}: {raw_value!r}")
|
|
||||||
return slug
|
|
||||||
|
|
||||||
|
|
||||||
def display_profile_slug(profile: str) -> str:
|
|
||||||
if profile == "host":
|
|
||||||
return "_host"
|
|
||||||
return profile
|
|
||||||
|
|
||||||
|
|
||||||
def runtime_name(user_name: str, profile: str, selector: str, window_name: str) -> str:
|
|
||||||
selector_parts = selector.split("/")
|
|
||||||
name_parts = [display_user_slug(user_name), display_profile_slug(profile)]
|
|
||||||
name_parts.extend(runtime_name_slug(part, "card selector") for part in selector_parts)
|
|
||||||
name_parts.append(runtime_name_slug(window_name, "tmux window name"))
|
|
||||||
return "_".join(name_parts)
|
|
||||||
|
|
||||||
|
|
||||||
def build_container_command(
|
|
||||||
config: WorkspaceConfig,
|
|
||||||
selector: str,
|
|
||||||
card_path: Path,
|
|
||||||
instance: str,
|
|
||||||
profile: str,
|
|
||||||
session_name: str,
|
|
||||||
window_name: str,
|
|
||||||
) -> str:
|
|
||||||
profile = normalize_env_profile(profile)
|
|
||||||
service = ENV_PROFILES[profile]["service"]
|
|
||||||
tool_root = config.tools_root
|
|
||||||
compose_file = tool_root / "docker-compose.yml"
|
|
||||||
if not (tool_root / "rv").is_file():
|
|
||||||
candidates = "\n".join(f" - {path}" for path in config.tools_root_candidates)
|
|
||||||
raise SystemExit(f"Missing rv launcher. Checked:\n{candidates}")
|
|
||||||
if not compose_file.is_file():
|
|
||||||
raise SystemExit(f"Missing docker compose file: {compose_file}")
|
|
||||||
|
|
||||||
home_dir = Path(os.environ.get("HOME") or str(Path.home()))
|
|
||||||
card_path_text = str(card_path)
|
|
||||||
user_name = current_user_slug()
|
|
||||||
host_uid = str(os.getuid())
|
|
||||||
host_gid = str(os.getgid())
|
|
||||||
container_name = runtime_name(user_name, profile, selector, window_name)
|
|
||||||
shell_rcfile = tool_root / "configs" / "rv-shell.bashrc"
|
|
||||||
env_parts = [
|
|
||||||
f"WORKSPACE_ROOT={shlex.quote(str(config.workspace_root))}",
|
|
||||||
f"RV_CARDS_ROOT={shlex.quote(str(config.series_root))}",
|
|
||||||
f"RV_PROFILE={profile}",
|
|
||||||
f"RV_REPO={shlex.quote(card_path_text)}",
|
|
||||||
f"RV_REPO_PATH={shlex.quote(str(card_path))}",
|
|
||||||
f"RV_REPO_HOST={shlex.quote(str(card_path))}",
|
|
||||||
f"RV_CARD={shlex.quote(selector)}",
|
|
||||||
f"RV_INSTANCE={shlex.quote(instance)}",
|
|
||||||
f"RV_HOST_USER={shlex.quote(user_name)}",
|
|
||||||
f"RV_HOST_UID={host_uid}",
|
|
||||||
f"RV_HOST_GID={host_gid}",
|
|
||||||
"RV_CODEX=0",
|
|
||||||
]
|
|
||||||
compose_args = [
|
|
||||||
"docker",
|
|
||||||
"compose",
|
|
||||||
"-f",
|
|
||||||
str(compose_file),
|
|
||||||
"run",
|
|
||||||
"--rm",
|
|
||||||
"--name",
|
|
||||||
container_name,
|
|
||||||
"--user",
|
|
||||||
f"{os.getuid()}:{os.getgid()}",
|
|
||||||
"--workdir",
|
|
||||||
card_path_text,
|
|
||||||
"-e",
|
|
||||||
f"HOME={home_dir}",
|
|
||||||
"-e",
|
|
||||||
f"USER={user_name}",
|
|
||||||
"-e",
|
|
||||||
f"LOGNAME={user_name}",
|
|
||||||
"-e",
|
|
||||||
f"RV_PROFILE={profile}",
|
|
||||||
"-e",
|
|
||||||
f"RV_HOST_USER={user_name}",
|
|
||||||
"-e",
|
|
||||||
f"RV_HOST_UID={host_uid}",
|
|
||||||
"-e",
|
|
||||||
f"RV_HOST_GID={host_gid}",
|
|
||||||
"-e",
|
|
||||||
f"RV_REPO={card_path_text}",
|
|
||||||
"-e",
|
|
||||||
f"WORKSPACE_ROOT={config.workspace_root}",
|
|
||||||
"-e",
|
|
||||||
f"RV_CARDS_ROOT={config.series_root}",
|
|
||||||
"-e",
|
|
||||||
f"RV_REPO_PATH={card_path_text}",
|
|
||||||
"-e",
|
|
||||||
f"RV_REPO_HOST={card_path_text}",
|
|
||||||
"-e",
|
|
||||||
f"RV_CARD={selector}",
|
|
||||||
"-e",
|
|
||||||
f"RV_INSTANCE={instance}",
|
|
||||||
"-e",
|
|
||||||
"RV_CODEX=0",
|
|
||||||
"-v",
|
|
||||||
f"{card_path_text}:{card_path_text}",
|
|
||||||
]
|
|
||||||
for identity_file in ["/etc/passwd", "/etc/group"]:
|
|
||||||
if Path(identity_file).is_file():
|
|
||||||
compose_args.extend(["-v", f"{identity_file}:{identity_file}:ro"])
|
|
||||||
shell_args = [service, "shell"]
|
|
||||||
if shell_rcfile.is_file():
|
|
||||||
compose_args.extend(["-v", f"{shell_rcfile}:/tmp/rv-shell.bashrc:ro"])
|
|
||||||
shell_args.extend(["--rcfile", "/tmp/rv-shell.bashrc"])
|
|
||||||
compose_args.extend(shell_args)
|
|
||||||
return " && ".join(
|
|
||||||
[
|
|
||||||
f"cd {shlex.quote(str(tool_root))}",
|
|
||||||
" ".join(env_parts + [shlex.join(compose_args)]),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_tmux_container(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
def run_tmux_container(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||||
series_name, card_name = resolve_selector(config, args.series, args.card)
|
series_name, card_name = resolve_selector(config, args.series, args.card)
|
||||||
selector = f"{series_name}/{card_name}"
|
selector = f"{series_name}/{card_name}"
|
||||||
card_path = resolve_card_path(config, series_name, card_name)
|
card_path = resolve_card_path(config, series_name, card_name)
|
||||||
|
|
||||||
profile = normalize_env_profile(args.profile)
|
profile = normalize_env_profile(args.profile)
|
||||||
user_name = current_user_slug()
|
task_name = resolve_submission_task(config, card_path, None)
|
||||||
default_session_name = runtime_name(user_name, profile, selector, args.window or config.defaults.get("tmux_window", "rv"))
|
instance = args.instance or default_env_instance(profile, series_name, card_name, task_name, "shell")
|
||||||
session_name = args.session or default_session_name
|
|
||||||
window_name = args.window or config.defaults.get("tmux_window", "rv")
|
window_name = args.window or config.defaults.get("tmux_window", "rv")
|
||||||
instance = args.instance or config.defaults.get("instance", "shell")
|
session_name = args.session or slug_value(f"stem-{instance}", "tmux session")
|
||||||
|
|
||||||
if tmux_target_exists(session_name):
|
if tmux_target_exists(session_name):
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
@@ -3500,7 +3418,18 @@ def run_tmux_container(config: WorkspaceConfig, args: argparse.Namespace) -> Non
|
|||||||
"Use another --session name or remove the existing session first."
|
"Use another --session name or remove the existing session first."
|
||||||
)
|
)
|
||||||
|
|
||||||
shell_command = build_container_command(config, selector, card_path, instance, profile, session_name, window_name)
|
# Compatibility command: keep the outer host tmux session, but delegate
|
||||||
|
# container creation/mounts/labels entirely to the canonical rootless
|
||||||
|
# ./stem runtime. Do not maintain a second Docker Compose lifecycle here.
|
||||||
|
shell_command, details = env_action_command(
|
||||||
|
config,
|
||||||
|
profile,
|
||||||
|
"shell",
|
||||||
|
series_name=series_name,
|
||||||
|
card_name=card_name,
|
||||||
|
task_name=task_name,
|
||||||
|
instance=instance,
|
||||||
|
)
|
||||||
tmux_command = [
|
tmux_command = [
|
||||||
"tmux",
|
"tmux",
|
||||||
"new-session",
|
"new-session",
|
||||||
@@ -3516,10 +3445,13 @@ def run_tmux_container(config: WorkspaceConfig, args: argparse.Namespace) -> Non
|
|||||||
print("Selector:", selector)
|
print("Selector:", selector)
|
||||||
print("Card path:", card_path)
|
print("Card path:", card_path)
|
||||||
print("Tools root:", config.tools_root)
|
print("Tools root:", config.tools_root)
|
||||||
|
print("Runtime: rootless stem/Podman")
|
||||||
|
print("Instance:", instance)
|
||||||
print("Pane 0 command:")
|
print("Pane 0 command:")
|
||||||
print(shell_command)
|
print(shell_command)
|
||||||
print("tmux command:")
|
print("tmux command:")
|
||||||
print(shlex.join(tmux_command))
|
print(shlex.join(tmux_command))
|
||||||
|
print_env_plan(details)
|
||||||
return
|
return
|
||||||
|
|
||||||
subprocess.run(tmux_command, check=True)
|
subprocess.run(tmux_command, check=True)
|
||||||
@@ -3535,12 +3467,7 @@ def run_tmux_container(config: WorkspaceConfig, args: argparse.Namespace) -> Non
|
|||||||
|
|
||||||
|
|
||||||
def normalize_env_profile(profile: str) -> str:
|
def normalize_env_profile(profile: str) -> str:
|
||||||
aliases = {
|
normalized = ENV_PROFILE_ALIASES.get(profile, profile)
|
||||||
"riscv": "rv32i",
|
|
||||||
"riscv32": "rv32i",
|
|
||||||
"native": "host",
|
|
||||||
}
|
|
||||||
normalized = aliases.get(profile, profile)
|
|
||||||
if normalized not in ENV_PROFILES:
|
if normalized not in ENV_PROFILES:
|
||||||
available = ", ".join(sorted(ENV_PROFILES))
|
available = ", ".join(sorted(ENV_PROFILES))
|
||||||
raise SystemExit(f"Unsupported environment profile '{profile}'. Available: {available}")
|
raise SystemExit(f"Unsupported environment profile '{profile}'. Available: {available}")
|
||||||
@@ -3548,18 +3475,19 @@ def normalize_env_profile(profile: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def env_tool_command_path(config: WorkspaceConfig) -> Path:
|
def env_tool_command_path(config: WorkspaceConfig) -> Path:
|
||||||
return config.tools_root / "rv"
|
stem_script = config.tools_root / "stem"
|
||||||
|
return stem_script if stem_script.is_file() else config.tools_root / "rv"
|
||||||
|
|
||||||
|
|
||||||
def ensure_env_tool(config: WorkspaceConfig) -> Path:
|
def ensure_env_tool(config: WorkspaceConfig) -> Path:
|
||||||
rv_script = env_tool_command_path(config)
|
env_script = env_tool_command_path(config)
|
||||||
if rv_script.is_file():
|
if env_script.is_file():
|
||||||
return rv_script
|
return env_script
|
||||||
candidates = "\n".join(f" - {path}" for path in config.tools_root_candidates)
|
candidates = "\n".join(f" - {path}" for path in config.tools_root_candidates)
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
"Missing rv32i-hazard3-student-env launcher script 'rv'.\n"
|
"Missing environment launcher script 'stem' (or compatible 'rv').\n"
|
||||||
f"Checked:\n{candidates}\n"
|
f"Checked:\n{candidates}\n"
|
||||||
"Run: ./rvctl env sync"
|
"Run: ./stemctl env sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -3701,7 +3629,8 @@ def send_command_to_tmux_pane(shell_command: str, raw_target: str, dry_run: bool
|
|||||||
|
|
||||||
def default_env_instance(profile: str, series_name: str, card_name: str, task_name: str, action: str) -> str:
|
def default_env_instance(profile: str, series_name: str, card_name: str, task_name: str, action: str) -> str:
|
||||||
task_part = task_branch_part(task_name).lower()
|
task_part = task_branch_part(task_name).lower()
|
||||||
return slug_value(f"{profile}-{series_name}-{card_name}-{task_part}-{action}", "environment instance")
|
del action
|
||||||
|
return slug_value(f"{profile}-{series_name}-{card_name}-{task_part}", "environment instance")
|
||||||
|
|
||||||
|
|
||||||
def env_action_command(
|
def env_action_command(
|
||||||
@@ -3713,15 +3642,25 @@ def env_action_command(
|
|||||||
task_name: str | None = None,
|
task_name: str | None = None,
|
||||||
instance: str | None = None,
|
instance: str | None = None,
|
||||||
editor: str | None = None,
|
editor: str | None = None,
|
||||||
|
target: str | None = None,
|
||||||
|
device: str | None = None,
|
||||||
|
deploy_backend: str | None = None,
|
||||||
|
allow_existing_firmware: bool = False,
|
||||||
) -> tuple[str, dict[str, str]]:
|
) -> tuple[str, dict[str, str]]:
|
||||||
profile = normalize_env_profile(profile)
|
profile = normalize_env_profile(profile)
|
||||||
rv_script = ensure_env_tool(config)
|
env_script = ensure_env_tool(config)
|
||||||
|
selected_target = target or PROFILE_DEFAULT_TARGETS[profile]
|
||||||
env_vars = {
|
env_vars = {
|
||||||
|
"STEM_WORKSPACE_ROOT": str(config.workspace_root),
|
||||||
|
"STEM_CARDS_ROOT": str(config.series_root),
|
||||||
|
"STEM_SOCKET_ROOT": str(config.socket_root),
|
||||||
|
"STEM_PROFILE": profile,
|
||||||
|
"STEM_TARGET": selected_target,
|
||||||
"WORKSPACE_ROOT": str(config.workspace_root),
|
"WORKSPACE_ROOT": str(config.workspace_root),
|
||||||
"RV_CARDS_ROOT": str(config.series_root),
|
"RV_CARDS_ROOT": str(config.series_root),
|
||||||
"RV_PROFILE": profile,
|
"RV_PROFILE": profile,
|
||||||
}
|
}
|
||||||
command_args = ["bash", "./rv"]
|
command_args = ["bash", f"./{env_script.name}"]
|
||||||
|
|
||||||
card_path: Path | None = None
|
card_path: Path | None = None
|
||||||
selector = ""
|
selector = ""
|
||||||
@@ -3730,6 +3669,9 @@ def env_action_command(
|
|||||||
selector = f"{series_name}/{card_name}"
|
selector = f"{series_name}/{card_name}"
|
||||||
env_vars.update(
|
env_vars.update(
|
||||||
{
|
{
|
||||||
|
"STEM_REPO_PATH": str(card_path),
|
||||||
|
"STEM_REPO_HOST": str(card_path),
|
||||||
|
"STEM_CARD": card_path.name,
|
||||||
"RV_REPO_PATH": str(card_path),
|
"RV_REPO_PATH": str(card_path),
|
||||||
"RV_REPO_HOST": str(card_path),
|
"RV_REPO_HOST": str(card_path),
|
||||||
"RV_CARD": card_path.name,
|
"RV_CARD": card_path.name,
|
||||||
@@ -3737,48 +3679,55 @@ def env_action_command(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if instance:
|
if instance:
|
||||||
|
env_vars["STEM_INSTANCE"] = instance
|
||||||
env_vars["RV_INSTANCE"] = instance
|
env_vars["RV_INSTANCE"] = instance
|
||||||
elif series_name and card_name and task_name:
|
elif series_name and card_name and task_name:
|
||||||
env_vars["RV_INSTANCE"] = default_env_instance(profile, series_name, card_name, task_name, action)
|
generated_instance = default_env_instance(profile, series_name, card_name, task_name, action)
|
||||||
|
env_vars["STEM_INSTANCE"] = generated_instance
|
||||||
|
env_vars["RV_INSTANCE"] = generated_instance
|
||||||
|
|
||||||
if editor:
|
if editor:
|
||||||
|
env_vars["STEM_EDITOR"] = editor
|
||||||
env_vars["RV_EDITOR"] = editor
|
env_vars["RV_EDITOR"] = editor
|
||||||
|
|
||||||
if action == "build":
|
if device:
|
||||||
command_args.append("build")
|
env_vars["STEM_PROBE_DEVICE"] = device
|
||||||
elif action == "shell":
|
if deploy_backend:
|
||||||
command_args.append("shell")
|
env_vars["STEM_DEPLOY_BACKEND"] = deploy_backend
|
||||||
elif action == "debug":
|
|
||||||
|
if action in {"build", "test", "run", "debug", "deploy"}:
|
||||||
if not task_name:
|
if not task_name:
|
||||||
raise SystemExit("debug requires a task selector.")
|
raise SystemExit(f"{action} requires a task selector.")
|
||||||
if profile == "rv32i":
|
command_args.extend([action, "--target", selected_target, task_name])
|
||||||
command_args.extend(["nvim", task_name])
|
if device:
|
||||||
else:
|
command_args.extend(["--device", device])
|
||||||
command_args.extend(["debug", task_name])
|
if action == "deploy" and deploy_backend:
|
||||||
elif action == "run":
|
command_args.extend(["--backend", deploy_backend])
|
||||||
if not task_name:
|
if allow_existing_firmware:
|
||||||
raise SystemExit("run requires a task selector.")
|
command_args.append("--allow-existing-firmware")
|
||||||
if profile == "rv32i":
|
elif action in {"shell", "start", "status", "attach", "stop", "rm"}:
|
||||||
raise SystemExit("rv32i run is not separate yet. Use: ./rvctl debug rv32i ...")
|
command_args.extend([action, "--target", selected_target])
|
||||||
command_args.extend(["run", task_name])
|
|
||||||
else:
|
else:
|
||||||
raise SystemExit(f"Unsupported environment action: {action}")
|
raise SystemExit(f"Unsupported environment action: {action}")
|
||||||
|
|
||||||
env_prefix = " ".join(f"{key}={shlex.quote(value)}" for key, value in env_vars.items())
|
env_prefix = " ".join(f"{key}={shlex.quote(value)}" for key, value in env_vars.items())
|
||||||
shell_command = " && ".join(
|
shell_command = " && ".join(
|
||||||
[
|
[
|
||||||
f"cd {shlex.quote(str(rv_script.parent))}",
|
f"cd {shlex.quote(str(env_script.parent))}",
|
||||||
" ".join([env_prefix, *[shlex.quote(part) for part in command_args]]),
|
" ".join([env_prefix, *[shlex.quote(part) for part in command_args]]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
details = {
|
details = {
|
||||||
"profile": profile,
|
"profile": profile,
|
||||||
"service": ENV_PROFILES[profile]["service"],
|
"service": ENV_PROFILES[profile]["service"],
|
||||||
|
"target": selected_target,
|
||||||
"selector": selector,
|
"selector": selector,
|
||||||
"task": task_name or "",
|
"task": task_name or "",
|
||||||
"card_path": str(card_path or ""),
|
"card_path": str(card_path or ""),
|
||||||
"tools_root": str(config.tools_root),
|
"tools_root": str(config.tools_root),
|
||||||
"instance": env_vars.get("RV_INSTANCE", ""),
|
"instance": env_vars.get("RV_INSTANCE", ""),
|
||||||
|
"device": device or "",
|
||||||
|
"backend": deploy_backend or "",
|
||||||
"command": shell_command,
|
"command": shell_command,
|
||||||
}
|
}
|
||||||
return shell_command, details
|
return shell_command, details
|
||||||
@@ -3788,11 +3737,14 @@ def print_env_plan(details: dict[str, str]) -> None:
|
|||||||
keys = [
|
keys = [
|
||||||
"profile",
|
"profile",
|
||||||
"service",
|
"service",
|
||||||
|
"target",
|
||||||
"selector",
|
"selector",
|
||||||
"task",
|
"task",
|
||||||
"card_path",
|
"card_path",
|
||||||
"tools_root",
|
"tools_root",
|
||||||
"instance",
|
"instance",
|
||||||
|
"device",
|
||||||
|
"backend",
|
||||||
"pane",
|
"pane",
|
||||||
"tmux_target",
|
"tmux_target",
|
||||||
"tmux_resolved",
|
"tmux_resolved",
|
||||||
@@ -3820,19 +3772,97 @@ def run_env(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
|||||||
return
|
return
|
||||||
if args.env_command == "build":
|
if args.env_command == "build":
|
||||||
profile = normalize_env_profile(args.profile)
|
profile = normalize_env_profile(args.profile)
|
||||||
shell_command, details = env_action_command(config, profile, "build")
|
env_script = ensure_env_tool(config)
|
||||||
|
shell_command = " && ".join(
|
||||||
|
[
|
||||||
|
f"cd {shlex.quote(str(env_script.parent))}",
|
||||||
|
" ".join(
|
||||||
|
[
|
||||||
|
f"STEM_PROFILE={shlex.quote(profile)}",
|
||||||
|
f"STEM_WORKSPACE_ROOT={shlex.quote(str(config.workspace_root))}",
|
||||||
|
f"STEM_SOCKET_ROOT={shlex.quote(str(config.socket_root))}",
|
||||||
|
"bash",
|
||||||
|
shlex.quote(f"./{env_script.name}"),
|
||||||
|
"profile-build",
|
||||||
|
shlex.quote(profile),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
print_env_plan(details)
|
print(f"profile\t{profile}")
|
||||||
|
print(f"command\t{shell_command}")
|
||||||
return
|
return
|
||||||
subprocess.run(["bash", "-lc", shell_command], check=True)
|
subprocess.run(["bash", "-lc", shell_command], check=True)
|
||||||
return
|
return
|
||||||
|
if args.env_command == "ensure":
|
||||||
|
profile = normalize_env_profile(args.profile)
|
||||||
|
env_script = ensure_env_tool(config)
|
||||||
|
env_vars = os.environ.copy()
|
||||||
|
env_vars.update(
|
||||||
|
{
|
||||||
|
"STEM_WORKSPACE_ROOT": str(config.workspace_root),
|
||||||
|
"STEM_SOCKET_ROOT": str(config.socket_root),
|
||||||
|
"STEM_PROFILE": profile,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
command = ["bash", str(env_script), "ensure", profile]
|
||||||
|
if args.dry_run:
|
||||||
|
print(f"profile\t{profile}")
|
||||||
|
print(f"command\t{shlex.join(command)}")
|
||||||
|
return
|
||||||
|
subprocess.run(command, cwd=env_script.parent, env=env_vars, check=True)
|
||||||
|
return
|
||||||
raise SystemExit(f"Unsupported env command: {args.env_command}")
|
raise SystemExit(f"Unsupported env command: {args.env_command}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_probe(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||||
|
env_script = ensure_env_tool(config)
|
||||||
|
command = ["bash", str(env_script), "probe", "list"]
|
||||||
|
if args.json:
|
||||||
|
command.append("--json")
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.update(
|
||||||
|
{
|
||||||
|
"STEM_WORKSPACE_ROOT": str(config.workspace_root),
|
||||||
|
"STEM_SOCKET_ROOT": str(config.socket_root),
|
||||||
|
"STEM_PROFILE": "rp2350",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
subprocess.run(command, cwd=env_script.parent, env=env, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def run_mcp(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||||
|
"""Replace stemctl with the MCP server bound to one verified instance."""
|
||||||
|
env_script = ensure_env_tool(config)
|
||||||
|
wrapper = env_script.parent / "scripts" / f"mcp-{args.mcp_kind}.sh"
|
||||||
|
if not wrapper.is_file():
|
||||||
|
raise SystemExit(f"Missing MCP wrapper in environment sources: {wrapper}")
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.update(
|
||||||
|
{
|
||||||
|
"STEM_WORKSPACE_ROOT": str(config.workspace_root),
|
||||||
|
"STEM_SOCKET_ROOT": str(config.socket_root),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
os.execvpe(str(wrapper), [str(wrapper), args.instance], env)
|
||||||
|
|
||||||
|
|
||||||
def run_profile_action(config: WorkspaceConfig, args: argparse.Namespace, action: str) -> None:
|
def run_profile_action(config: WorkspaceConfig, args: argparse.Namespace, action: str) -> None:
|
||||||
profile = normalize_env_profile(args.profile)
|
profile = normalize_env_profile(args.profile)
|
||||||
selector_values, pane_target = split_pane_selector(args.selector, getattr(args, "pane", None))
|
selector_values, pane_target = split_pane_selector(args.selector, getattr(args, "pane", None))
|
||||||
series_name, card_name, task_name = resolve_env_target(config, selector_values, getattr(args, "task", None))
|
instance = getattr(args, "instance", None)
|
||||||
|
instance_only_actions = {"status", "attach", "stop", "rm"}
|
||||||
|
if instance and action in instance_only_actions and not selector_values:
|
||||||
|
# A stable logical instance is sufficient for lifecycle operations on
|
||||||
|
# an existing container. Resolving workspace defaults here would make
|
||||||
|
# the operation depend on the currently selected (or still checked
|
||||||
|
# out) card, even though the environment launcher does not need it.
|
||||||
|
series_name = card_name = task_name = None
|
||||||
|
else:
|
||||||
|
series_name, card_name, task_name = resolve_env_target(
|
||||||
|
config, selector_values, getattr(args, "task", None)
|
||||||
|
)
|
||||||
shell_command, details = env_action_command(
|
shell_command, details = env_action_command(
|
||||||
config,
|
config,
|
||||||
profile,
|
profile,
|
||||||
@@ -3840,8 +3870,12 @@ def run_profile_action(config: WorkspaceConfig, args: argparse.Namespace, action
|
|||||||
series_name=series_name,
|
series_name=series_name,
|
||||||
card_name=card_name,
|
card_name=card_name,
|
||||||
task_name=task_name,
|
task_name=task_name,
|
||||||
instance=getattr(args, "instance", None),
|
instance=instance,
|
||||||
editor=getattr(args, "editor", None),
|
editor=getattr(args, "editor", None),
|
||||||
|
target=getattr(args, "target", None),
|
||||||
|
device=getattr(args, "device", None),
|
||||||
|
deploy_backend=getattr(args, "backend", None),
|
||||||
|
allow_existing_firmware=getattr(args, "allow_existing_firmware", False),
|
||||||
)
|
)
|
||||||
if pane_target:
|
if pane_target:
|
||||||
pane_details = send_command_to_tmux_pane(shell_command, pane_target, dry_run=args.dry_run)
|
pane_details = send_command_to_tmux_pane(shell_command, pane_target, dry_run=args.dry_run)
|
||||||
@@ -3921,6 +3955,61 @@ def run_workspace(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
|||||||
print(f"status\tready")
|
print(f"status\tready")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if args.workspace_command == "migrate":
|
||||||
|
migrated = 0
|
||||||
|
already_ready = 0
|
||||||
|
candidates = sorted(path for path in config.series_root.glob("*/*") if path.is_dir())
|
||||||
|
for card_path in candidates:
|
||||||
|
legacy_state = card_path / ".rv"
|
||||||
|
stem_state = card_path / ".stem"
|
||||||
|
if stem_state.exists() or stem_state.is_symlink():
|
||||||
|
already_ready += 1
|
||||||
|
continue
|
||||||
|
if not legacy_state.is_dir():
|
||||||
|
continue
|
||||||
|
print(f"state\t{stem_state} -> .rv")
|
||||||
|
migrated += 1
|
||||||
|
if not args.dry_run:
|
||||||
|
stem_state.symlink_to(".rv", target_is_directory=True)
|
||||||
|
print(f"migrated\t{migrated}")
|
||||||
|
print(f"already_ready\t{already_ready}")
|
||||||
|
print(f"mode\t{'dry-run' if args.dry_run else 'applied'}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.workspace_command == "doctor":
|
||||||
|
requested_runtime = os.environ.get("STEM_RUNTIME", "").strip()
|
||||||
|
runtime = requested_runtime or ("podman" if shutil.which("podman") else "")
|
||||||
|
runtime_ok = runtime == "podman" and shutil.which("podman") is not None
|
||||||
|
if requested_runtime and requested_runtime != "podman":
|
||||||
|
runtime_detail = f"{requested_runtime} is not supported for the rootless student workflow"
|
||||||
|
else:
|
||||||
|
runtime_detail = runtime or "missing podman"
|
||||||
|
checks: list[tuple[str, bool, str]] = [
|
||||||
|
("workspace", config.workspace_root.is_dir(), str(config.workspace_root)),
|
||||||
|
("series", config.series_root.is_dir(), str(config.series_root)),
|
||||||
|
("env-tool", env_tool_command_path(config).is_file(), str(env_tool_command_path(config))),
|
||||||
|
("runtime", runtime_ok, runtime_detail),
|
||||||
|
]
|
||||||
|
socket_probe = config.socket_root / ("t" * 12) / ("i" * 12) / ("c" * 12) / "n.sock"
|
||||||
|
checks.append(("socket-path", len(os.fsencode(socket_probe)) <= 100, f"{len(os.fsencode(socket_probe))} bytes"))
|
||||||
|
if runtime_ok:
|
||||||
|
probe = subprocess.run(
|
||||||
|
["podman", "info", "--format", "{{.Host.Security.Rootless}}"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
checks.append(("rootless", probe.returncode == 0 and probe.stdout.strip() == "true", probe.stdout.strip() or probe.stderr.strip()))
|
||||||
|
else:
|
||||||
|
checks.append(("rootless", False, "rootless Podman is required"))
|
||||||
|
print("check\tstatus\tdetail")
|
||||||
|
for name, ok, detail in checks:
|
||||||
|
print(f"{name}\t{'ok' if ok else 'FAIL'}\t{detail}")
|
||||||
|
if not all(ok for _, ok, _ in checks):
|
||||||
|
raise SystemExit(1)
|
||||||
|
return
|
||||||
|
|
||||||
raise SystemExit(f"Unsupported workspace command: {args.workspace_command}")
|
raise SystemExit(f"Unsupported workspace command: {args.workspace_command}")
|
||||||
|
|
||||||
|
|
||||||
@@ -4001,29 +4090,31 @@ def print_config_summary(config_path: Path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def print_main_overview(config_path: Path) -> None:
|
def print_main_overview(config_path: Path) -> None:
|
||||||
print("RVCTL - launcher workspace RV")
|
print("STEMCTL - launcher kart pracy STEM")
|
||||||
print()
|
print()
|
||||||
print("Usage:")
|
print("Usage:")
|
||||||
print(" ./rvctl <command> [options]")
|
print(" ./stemctl <command> [options]")
|
||||||
print(" ./rvctl tokens <command> [options]")
|
print(" ./stemctl tokens <command> [options]")
|
||||||
print(" ./rvctl <command> --help")
|
print(" ./stemctl <command> --help")
|
||||||
print()
|
print()
|
||||||
print("Commands:")
|
print("Commands:")
|
||||||
print_table(
|
print_table(
|
||||||
["command", "arguments", "purpose"],
|
["command", "arguments", "purpose"],
|
||||||
[
|
[
|
||||||
["show-config", "", "show resolved paths and Git defaults"],
|
["show-config", "", "show resolved paths and Git defaults"],
|
||||||
["workspace", "show|sync", "show or update meta/workspace-info"],
|
["workspace", "show|sync|migrate|doctor", "manage and diagnose workspace"],
|
||||||
["series", "list|use|cards|fetch|tasks", "work with series, cards and tasks"],
|
["series", "list|use|cards|fetch|tasks", "work with series, cards and tasks"],
|
||||||
["card", "use [series] card", "set the default card, optionally with the default series"],
|
["card", "use [series] card", "set the default card, optionally with the default series"],
|
||||||
["tasks", "list|show|switch", "shortcut for tasks in the default card"],
|
["tasks", "list|show|switch", "shortcut for tasks in the default card"],
|
||||||
["env", "list|sync|build", "manage rv32i/host container environment"],
|
["env", "list|sync|ensure|build", "build three container profiles from Dockerfile"],
|
||||||
["debug", "rv32i|host [selector]", "open nvim + gdb in the selected container profile"],
|
["build/test/run/debug", "PROFILE [selector]", "execute a card action"],
|
||||||
["run", "host [selector]", "build and run selected task natively in host container"],
|
["deploy", "rp2350 [selector] --target TARGET", "flash a physical RP2350"],
|
||||||
["shell", "rv32i|host [selector]", "open a shell in the selected container profile"],
|
["start/status/attach/stop/rm", "PROFILE [selector]", "manage a persistent rootless container"],
|
||||||
|
["probe", "list", "discover RP2350 probes and BOOTSEL targets"],
|
||||||
|
["mcp", "tmux|nvim INSTANCE", "serve one verified container session over stdio"],
|
||||||
["list-series", "", "compat alias for series list"],
|
["list-series", "", "compat alias for series list"],
|
||||||
["list-cards", "[series]", "compat alias for series cards list"],
|
["list-cards", "[series]", "compat alias for series cards list"],
|
||||||
["tmux-container", "[series] [card]", "start tmux with container in pane 0"],
|
["tmux-container", "[series] [card]", "compat: run canonical rootless stem shell in pane 0"],
|
||||||
["submission", "[series] [card] --class K [--task T]", "prepare answer repo and student branch"],
|
["submission", "[series] [card] --class K [--task T]", "prepare answer repo and student branch"],
|
||||||
["tokens", "list|compare|cmp|sync|update|remove|rm|add|read|stats|write", "manage tokens.json and Git remote credentials"],
|
["tokens", "list|compare|cmp|sync|update|remove|rm|add|read|stats|write", "manage tokens.json and Git remote credentials"],
|
||||||
],
|
],
|
||||||
@@ -4033,17 +4124,13 @@ def print_main_overview(config_path: Path) -> None:
|
|||||||
print_table(
|
print_table(
|
||||||
["step", "command", "result"],
|
["step", "command", "result"],
|
||||||
[
|
[
|
||||||
["1", "./rvctl tokens compare", "compare repo remotes with tokens.json"],
|
["1", "./stemctl workspace sync", "clone or update workspace-info"],
|
||||||
["2", "./rvctl workspace sync", "clone or update workspace-info"],
|
["2", "./stemctl series cards fetch inf bss", "fetch the card on the host"],
|
||||||
["3", "./rvctl series list", "choose a series"],
|
["3", "./stemctl env ensure native-amd64", "build or reuse the local native profile"],
|
||||||
["4", "./rvctl series use inf", "set the default series"],
|
["4", "./stemctl test native-amd64 1", "run native golden/sanitizer tests"],
|
||||||
["5", "./rvctl series cards list inf", "choose a card"],
|
["5", "./stemctl test hazard3-sim 1", "run the same vectors on Hazard3"],
|
||||||
["6", "./rvctl card use bss", "set the default card"],
|
["6", "./stemctl debug hazard3-sim 1", "open common tmux/nvim/GDB UI"],
|
||||||
["7", "./rvctl series cards fetch inf bss", "fetch card and prepare r1a"],
|
["7", "./stemctl deploy rp2350 1 --target rp2350-rv --device /dev/bus/usb/001/006", "flash during a practical class"],
|
||||||
["8", "./rvctl tasks list", "list tasks from the default card"],
|
|
||||||
["9", "./rvctl tasks switch 4", "switch to the answer branch for task 4"],
|
|
||||||
["10", "./rvctl debug rv32i 4", "debug task 4 in RV32I/Hazard3"],
|
|
||||||
["11", "./rvctl debug host 4", "debug task 4 natively in a host container"],
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
print()
|
print()
|
||||||
@@ -4126,8 +4213,8 @@ def extract_config_path(argv: list[str]) -> tuple[Path, list[str]]:
|
|||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="rvctl",
|
prog=os.environ.get("STEM_CLI_NAME", Path(sys.argv[0]).name),
|
||||||
description="rvctl for rv cards and tools.",
|
description="STEM card launcher for native AMD64, Hazard3 simulation and RP2350.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--config",
|
"--config",
|
||||||
@@ -4149,40 +4236,76 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
workspace_subparsers.add_parser("show", help="Show workspace-info configuration.")
|
workspace_subparsers.add_parser("show", help="Show workspace-info configuration.")
|
||||||
workspace_sync_parser = workspace_subparsers.add_parser("sync", help="Clone or update workspace-info into meta/.")
|
workspace_sync_parser = workspace_subparsers.add_parser("sync", help="Clone or update workspace-info into meta/.")
|
||||||
workspace_sync_parser.add_argument("--dry-run", action="store_true", help="Print plan without cloning or pulling.")
|
workspace_sync_parser.add_argument("--dry-run", action="store_true", help="Print plan without cloning or pulling.")
|
||||||
|
workspace_migrate_parser = workspace_subparsers.add_parser(
|
||||||
|
"migrate", help="Expose legacy .rv state as .stem without deleting data."
|
||||||
|
)
|
||||||
|
workspace_migrate_parser.add_argument("--dry-run", action="store_true", help="Only list state directories.")
|
||||||
|
workspace_subparsers.add_parser("doctor", help="Check workspace, rootless runtime and MCP socket budget.")
|
||||||
|
|
||||||
env_parser = subparsers.add_parser("env", help="Manage rv32i/host container environments.")
|
env_parser = subparsers.add_parser("env", help="Manage the three canonical container environments.")
|
||||||
env_subparsers = env_parser.add_subparsers(dest="env_command", required=True)
|
env_subparsers = env_parser.add_subparsers(dest="env_command", required=True)
|
||||||
env_subparsers.add_parser("list", help="List supported environment profiles.")
|
env_subparsers.add_parser("list", help="List supported environment profiles.")
|
||||||
env_sync_parser = env_subparsers.add_parser("sync", help="Clone or update rv32i-hazard3-student-env into tools/.")
|
env_sync_parser = env_subparsers.add_parser("sync", help="Clone or update rv32i-hazard3-student-env into tools/.")
|
||||||
env_sync_parser.add_argument("--dry-run", action="store_true", help="Print plan without cloning or pulling.")
|
env_sync_parser.add_argument("--dry-run", action="store_true", help="Print plan without cloning or pulling.")
|
||||||
env_build_parser = env_subparsers.add_parser("build", help="Build one container profile.")
|
env_build_parser = env_subparsers.add_parser("build", help="Build one container profile.")
|
||||||
env_build_parser.add_argument("profile", choices=sorted(ENV_PROFILES), help="Environment profile.")
|
env_build_parser.add_argument("profile", choices=ENV_PROFILE_CHOICES, help="Environment profile.")
|
||||||
env_build_parser.add_argument("--dry-run", action="store_true", help="Print command without building.")
|
env_build_parser.add_argument("--dry-run", action="store_true", help="Print command without building.")
|
||||||
|
env_ensure_parser = env_subparsers.add_parser("ensure", help="Reuse or locally build a profile from Dockerfile.")
|
||||||
|
env_ensure_parser.add_argument("profile", choices=ENV_PROFILE_CHOICES, help="Environment profile.")
|
||||||
|
env_ensure_parser.add_argument("--dry-run", action="store_true", help="Print command without building.")
|
||||||
|
|
||||||
debug_parser = subparsers.add_parser("debug", help="Open selected task in nvim + debugger inside a container profile.")
|
for action, help_text in (
|
||||||
debug_parser.add_argument("profile", choices=sorted(ENV_PROFILES), help="Environment profile: rv32i or host.")
|
("build", "Build the selected task inside a profile."),
|
||||||
debug_parser.add_argument("selector", nargs="*", help="Optional selector: [task], [card task] or [series card task].")
|
("test", "Run the selected task tests inside a profile."),
|
||||||
debug_parser.add_argument("--task", help="Task override. Defaults to selector or defaults.task.")
|
("run", "Run the selected task without implicit deployment."),
|
||||||
debug_parser.add_argument("--instance", help="Container/tmux instance name.")
|
("debug", "Open the selected task in the common tmux/nvim debugger UI."),
|
||||||
debug_parser.add_argument("--editor", choices=["nvim", "vim"], help="Editor inside the container.")
|
("deploy", "Deploy an RP2350 artifact using the explicitly selected probe."),
|
||||||
debug_parser.add_argument("--pane", help="Send the command to a host tmux pane, for example 0, node:0, :node.0 or %3.")
|
("shell", "Open a shell in the persistent profile container."),
|
||||||
debug_parser.add_argument("--dry-run", action="store_true", help="Print command without starting Docker.")
|
("start", "Create or start the persistent profile container."),
|
||||||
|
("status", "Show status of the selected profile container."),
|
||||||
|
("attach", "Attach to the selected profile container UI."),
|
||||||
|
("stop", "Stop the selected profile container."),
|
||||||
|
("rm", "Remove the selected profile container; sources remain on the host."),
|
||||||
|
):
|
||||||
|
action_parser = subparsers.add_parser(action, help=help_text)
|
||||||
|
action_parser.add_argument("profile", choices=ENV_PROFILE_CHOICES, help="Environment profile.")
|
||||||
|
action_parser.add_argument("selector", nargs="*", help="Optional selector: [task], [card task] or [series card task].")
|
||||||
|
action_parser.add_argument("--task", help="Task override. Defaults to selector or defaults.task.")
|
||||||
|
action_parser.add_argument("--target", help="Target override, for example rp2350-rv or rp2350-arm.")
|
||||||
|
action_parser.add_argument("--instance", help="Stable logical container instance name.")
|
||||||
|
action_parser.add_argument("--pane", help="Send command to a host tmux pane (for example 0 or %%3).")
|
||||||
|
action_parser.add_argument("--dry-run", action="store_true", help="Print command without changing state.")
|
||||||
|
if action in {"debug", "shell", "attach"}:
|
||||||
|
action_parser.add_argument("--editor", choices=["nvim", "vim"], help="Editor inside the container.")
|
||||||
|
if action in {"debug", "deploy"}:
|
||||||
|
action_parser.add_argument(
|
||||||
|
"--device",
|
||||||
|
help="Exact host device path reported by 'stemctl probe list'.",
|
||||||
|
)
|
||||||
|
if action == "deploy":
|
||||||
|
action_parser.add_argument(
|
||||||
|
"--backend",
|
||||||
|
choices=["probe", "bootsel"],
|
||||||
|
default="probe",
|
||||||
|
help="Deployment transport (default: probe).",
|
||||||
|
)
|
||||||
|
if action == "run":
|
||||||
|
action_parser.add_argument(
|
||||||
|
"--allow-existing-firmware",
|
||||||
|
action="store_true",
|
||||||
|
help="For RP2350 only: run firmware whose deployment cannot be verified.",
|
||||||
|
)
|
||||||
|
|
||||||
run_parser = subparsers.add_parser("run", help="Build and run selected task inside a container profile.")
|
probe_parser = subparsers.add_parser("probe", help="Discover RP2350 debug probes and BOOTSEL devices.")
|
||||||
run_parser.add_argument("profile", choices=sorted(ENV_PROFILES), help="Environment profile. Currently host supports run.")
|
probe_subparsers = probe_parser.add_subparsers(dest="probe_command", required=True)
|
||||||
run_parser.add_argument("selector", nargs="*", help="Optional selector: [task], [card task] or [series card task].")
|
probe_list_parser = probe_subparsers.add_parser("list", help="List usable probe/target devices without sudo.")
|
||||||
run_parser.add_argument("--task", help="Task override. Defaults to selector or defaults.task.")
|
probe_list_parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON.")
|
||||||
run_parser.add_argument("--instance", help="Container instance name.")
|
|
||||||
run_parser.add_argument("--pane", help="Send the command to a host tmux pane, for example 0, node:0, :node.0 or %3.")
|
|
||||||
run_parser.add_argument("--dry-run", action="store_true", help="Print command without starting Docker.")
|
|
||||||
|
|
||||||
shell_parser = subparsers.add_parser("shell", help="Open a shell inside a container profile.")
|
mcp_parser = subparsers.add_parser("mcp", help="Run the container-bundled MCP server for one logical instance.")
|
||||||
shell_parser.add_argument("profile", choices=sorted(ENV_PROFILES), help="Environment profile: rv32i or host.")
|
mcp_subparsers = mcp_parser.add_subparsers(dest="mcp_kind", required=True)
|
||||||
shell_parser.add_argument("selector", nargs="*", help="Optional selector: [card] or [series card].")
|
for kind in ("tmux", "nvim"):
|
||||||
shell_parser.add_argument("--task", help="Task used only for instance naming.")
|
kind_parser = mcp_subparsers.add_parser(kind, help=f"Serve {kind} tools over MCP stdio.")
|
||||||
shell_parser.add_argument("--instance", help="Container instance name.")
|
kind_parser.add_argument("instance", help="Stable logical instance name (required to avoid cross-container selection).")
|
||||||
shell_parser.add_argument("--pane", help="Send the command to a host tmux pane, for example 0, node:0, :node.0 or %3.")
|
|
||||||
shell_parser.add_argument("--dry-run", action="store_true", help="Print command without starting Docker.")
|
|
||||||
|
|
||||||
tasks_parser = subparsers.add_parser("tasks", help="Shortcut for tasks in the default card.")
|
tasks_parser = subparsers.add_parser("tasks", help="Shortcut for tasks in the default card.")
|
||||||
tasks_subparsers = tasks_parser.add_subparsers(dest="tasks_command", required=True)
|
tasks_subparsers = tasks_parser.add_subparsers(dest="tasks_command", required=True)
|
||||||
@@ -4205,14 +4328,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
|
|
||||||
tmux_parser = subparsers.add_parser(
|
tmux_parser = subparsers.add_parser(
|
||||||
"tmux-container",
|
"tmux-container",
|
||||||
help="Create a tmux session with pane 0 running the container shell for a selected card.",
|
help="Compatibility wrapper: create host tmux with the canonical rootless stem shell in pane 0.",
|
||||||
)
|
)
|
||||||
tmux_parser.add_argument("series", nargs="?", help="Series id or full selector like 'inf/bss'.")
|
tmux_parser.add_argument("series", nargs="?", help="Series id or full selector like 'inf/bss'.")
|
||||||
tmux_parser.add_argument("card", nargs="?", help="Card id or unique fragment, for example 'bss'.")
|
tmux_parser.add_argument("card", nargs="?", help="Card id or unique fragment, for example 'bss'.")
|
||||||
tmux_parser.add_argument("--profile", choices=sorted(ENV_PROFILES), default="rv32i", help="Container profile: rv32i or host.")
|
tmux_parser.add_argument("--profile", choices=ENV_PROFILE_CHOICES, default="hazard3-sim", help="Container profile.")
|
||||||
tmux_parser.add_argument("--session", help="tmux session name.")
|
tmux_parser.add_argument("--session", help="tmux session name.")
|
||||||
tmux_parser.add_argument("--window", help="tmux window name.")
|
tmux_parser.add_argument("--window", help="tmux window name.")
|
||||||
tmux_parser.add_argument("--instance", help="RV_INSTANCE passed to the tool launcher.")
|
tmux_parser.add_argument("--instance", help="Stable STEM_INSTANCE passed to the canonical runtime.")
|
||||||
tmux_parser.add_argument("--attach", action="store_true", help="Attach to the session after creation.")
|
tmux_parser.add_argument("--attach", action="store_true", help="Attach to the session after creation.")
|
||||||
tmux_parser.add_argument("--dry-run", action="store_true", help="Print commands instead of running tmux.")
|
tmux_parser.add_argument("--dry-run", action="store_true", help="Print commands instead of running tmux.")
|
||||||
|
|
||||||
@@ -4431,7 +4554,8 @@ def main() -> int:
|
|||||||
parser = build_parser()
|
parser = build_parser()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
config = load_config(args.config)
|
config = load_config(args.config)
|
||||||
if args.command in {"list-series", "list-cards", "series", "card", "tasks", "tmux-container", "submission", "debug", "run", "shell"}:
|
profile_actions = {"build", "test", "run", "debug", "deploy", "shell", "start", "status", "attach", "stop", "rm"}
|
||||||
|
if args.command in {"list-series", "list-cards", "series", "card", "tasks", "tmux-container", "submission"} | profile_actions:
|
||||||
ensure_workspace_info(config)
|
ensure_workspace_info(config)
|
||||||
|
|
||||||
if args.command == "show-config":
|
if args.command == "show-config":
|
||||||
@@ -4449,14 +4573,14 @@ def main() -> int:
|
|||||||
if args.command == "env":
|
if args.command == "env":
|
||||||
run_env(config, args)
|
run_env(config, args)
|
||||||
return 0
|
return 0
|
||||||
if args.command == "debug":
|
if args.command == "probe":
|
||||||
run_profile_action(config, args, "debug")
|
run_probe(config, args)
|
||||||
return 0
|
return 0
|
||||||
if args.command == "run":
|
if args.command == "mcp":
|
||||||
run_profile_action(config, args, "run")
|
run_mcp(config, args)
|
||||||
return 0
|
return 0
|
||||||
if args.command == "shell":
|
if args.command in profile_actions:
|
||||||
run_profile_action(config, args, "shell")
|
run_profile_action(config, args, args.command)
|
||||||
return 0
|
return 0
|
||||||
if args.command == "tasks":
|
if args.command == "tasks":
|
||||||
run_tasks(config, args)
|
run_tasks(config, args)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
export STEM_CLI_NAME=stemctl
|
||||||
|
exec python3 "$SCRIPT_DIR/rvctl.py" "$@"
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from contextlib import redirect_stdout
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
import rvctl # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class StemctlContractTests(unittest.TestCase):
|
||||||
|
def test_new_command_help_never_crashes(self) -> None:
|
||||||
|
script = Path(__file__).resolve().parents[1] / "rvctl.py"
|
||||||
|
commands = ["env", "build", "test", "run", "debug", "deploy", "shell", "start", "status", "attach", "stop", "rm", "probe", "mcp"]
|
||||||
|
for command in commands:
|
||||||
|
with self.subTest(command=command):
|
||||||
|
completed = subprocess.run(
|
||||||
|
[sys.executable, str(script), command, "--help"],
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
self.assertEqual(completed.returncode, 0, completed.stderr)
|
||||||
|
|
||||||
|
def test_profile_aliases_are_canonical(self) -> None:
|
||||||
|
self.assertEqual(rvctl.normalize_env_profile("host"), "native-amd64")
|
||||||
|
self.assertEqual(rvctl.normalize_env_profile("rv32i"), "hazard3-sim")
|
||||||
|
self.assertEqual(rvctl.normalize_env_profile("rp2350"), "rp2350")
|
||||||
|
|
||||||
|
def test_instance_is_stable_across_actions(self) -> None:
|
||||||
|
debug = rvctl.default_env_instance("hazard3-sim", "inf", "bss", "task1", "debug")
|
||||||
|
stop = rvctl.default_env_instance("hazard3-sim", "inf", "bss", "task1", "stop")
|
||||||
|
self.assertEqual(debug, stop)
|
||||||
|
self.assertEqual(debug, "hazard3-sim-inf-bss-t1")
|
||||||
|
|
||||||
|
def test_lifecycle_with_explicit_instance_needs_no_card_or_defaults(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
workspace = root / "workspace"
|
||||||
|
workspace.mkdir()
|
||||||
|
tools = root / "tools"
|
||||||
|
tools.mkdir()
|
||||||
|
(tools / "stem").write_text("#!/bin/sh\n", encoding="utf-8")
|
||||||
|
config_path = root / "workspace.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"workspace_root": str(workspace),
|
||||||
|
"original_root": str(root / "original"),
|
||||||
|
"tools_root_candidates": [str(tools)],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
config = rvctl.load_config(config_path)
|
||||||
|
for action in ("status", "attach", "stop", "rm"):
|
||||||
|
with self.subTest(action=action):
|
||||||
|
args = SimpleNamespace(
|
||||||
|
profile="hazard3-sim",
|
||||||
|
selector=[],
|
||||||
|
pane=None,
|
||||||
|
task=None,
|
||||||
|
instance="lesson-1",
|
||||||
|
editor=None,
|
||||||
|
target=None,
|
||||||
|
device=None,
|
||||||
|
backend=None,
|
||||||
|
allow_existing_firmware=False,
|
||||||
|
dry_run=True,
|
||||||
|
)
|
||||||
|
output = io.StringIO()
|
||||||
|
with (
|
||||||
|
mock.patch.object(rvctl, "resolve_env_target") as resolve,
|
||||||
|
redirect_stdout(output),
|
||||||
|
):
|
||||||
|
rvctl.run_profile_action(config, args, action)
|
||||||
|
resolve.assert_not_called()
|
||||||
|
rendered = output.getvalue()
|
||||||
|
self.assertIn("instance\tlesson-1", rendered)
|
||||||
|
self.assertIn(f"./stem {action} --target hazard3-baremetal", rendered)
|
||||||
|
|
||||||
|
def test_tmux_container_delegates_to_canonical_rootless_runtime(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
workspace = root / "workspace"
|
||||||
|
card = workspace / "series" / "inf" / "card"
|
||||||
|
(card / "src/tasks").mkdir(parents=True)
|
||||||
|
(card / "src/tasks/task1.c").write_text("int main(void) { return 0; }\n", encoding="utf-8")
|
||||||
|
tools = root / "tools"
|
||||||
|
tools.mkdir()
|
||||||
|
(tools / "stem").write_text("#!/bin/sh\n", encoding="utf-8")
|
||||||
|
config_path = root / "workspace.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"workspace_root": str(workspace),
|
||||||
|
"original_root": str(root / "original"),
|
||||||
|
"tools_root_candidates": [str(tools)],
|
||||||
|
"defaults": {"series": "inf", "card": "card", "task": "task1"},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
config = rvctl.load_config(config_path)
|
||||||
|
args = SimpleNamespace(
|
||||||
|
series="inf",
|
||||||
|
card="card",
|
||||||
|
profile="hazard3-sim",
|
||||||
|
session="test-shell",
|
||||||
|
window="shell",
|
||||||
|
instance=None,
|
||||||
|
attach=False,
|
||||||
|
dry_run=True,
|
||||||
|
)
|
||||||
|
output = io.StringIO()
|
||||||
|
with (
|
||||||
|
mock.patch.object(rvctl, "tmux_target_exists", return_value=False),
|
||||||
|
mock.patch.object(rvctl, "resolve_workspace_card_path", return_value=card),
|
||||||
|
redirect_stdout(output),
|
||||||
|
):
|
||||||
|
rvctl.run_tmux_container(config, args)
|
||||||
|
rendered = output.getvalue()
|
||||||
|
self.assertIn("./stem shell", rendered)
|
||||||
|
self.assertIn(f"STEM_REPO_PATH={card}", rendered)
|
||||||
|
self.assertIn("STEM_SOCKET_ROOT=", rendered)
|
||||||
|
self.assertNotIn("docker compose", rendered)
|
||||||
|
|
||||||
|
def test_workspace_doctor_rejects_docker_runtime(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
workspace = root / "workspace"
|
||||||
|
(workspace / "series").mkdir(parents=True)
|
||||||
|
tools = root / "tools"
|
||||||
|
tools.mkdir()
|
||||||
|
(tools / "stem").write_text("#!/bin/sh\n", encoding="utf-8")
|
||||||
|
config_path = root / "workspace.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"workspace_root": str(workspace),
|
||||||
|
"original_root": str(root / "original"),
|
||||||
|
"tools_root_candidates": [str(tools)],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
config = rvctl.load_config(config_path)
|
||||||
|
output = io.StringIO()
|
||||||
|
with mock.patch.dict(os.environ, {"STEM_RUNTIME": "docker"}), redirect_stdout(output):
|
||||||
|
with self.assertRaisesRegex(SystemExit, "1"):
|
||||||
|
rvctl.run_workspace(config, SimpleNamespace(workspace_command="doctor"))
|
||||||
|
rendered = output.getvalue()
|
||||||
|
self.assertIn("runtime\tFAIL\tdocker is not supported", rendered)
|
||||||
|
self.assertIn("rootless\tFAIL\trootless Podman is required", rendered)
|
||||||
|
|
||||||
|
def test_existing_legacy_workspace_is_detected(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
legacy = root / "rv"
|
||||||
|
legacy.mkdir()
|
||||||
|
config_path = root / "workspace.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"workspace_root": str(root / "stem"),
|
||||||
|
"legacy_workspace_root": str(legacy),
|
||||||
|
"original_root": str(root / "original"),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with mock.patch.dict(os.environ, {}, clear=False):
|
||||||
|
os.environ.pop("STEM_WORKSPACE_ROOT", None)
|
||||||
|
config = rvctl.load_config(config_path)
|
||||||
|
self.assertEqual(config.workspace_root, legacy.resolve())
|
||||||
|
self.assertEqual(config.series_root, (legacy / "series").resolve())
|
||||||
|
|
||||||
|
def test_xdg_socket_path_stays_inside_budget(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
with mock.patch.dict(os.environ, {"XDG_RUNTIME_DIR": str(root)}):
|
||||||
|
socket_root = rvctl.default_runtime_socket_root(root / "workspace")
|
||||||
|
path = socket_root / ("t" * 12) / ("i" * 12) / ("c" * 12) / "n.sock"
|
||||||
|
self.assertLessEqual(len(os.fsencode(path)), 100)
|
||||||
|
|
||||||
|
def test_action_exports_new_and_legacy_contract(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
workspace = root / "workspace"
|
||||||
|
card = workspace / "series" / "inf" / "card"
|
||||||
|
card.mkdir(parents=True)
|
||||||
|
tools = root / "tools"
|
||||||
|
tools.mkdir()
|
||||||
|
(tools / "stem").write_text("#!/bin/sh\n", encoding="utf-8")
|
||||||
|
config_path = root / "workspace.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"workspace_root": str(workspace),
|
||||||
|
"original_root": str(root / "original"),
|
||||||
|
"tools_root_candidates": [str(tools)],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
config = rvctl.load_config(config_path)
|
||||||
|
with mock.patch.object(rvctl, "resolve_workspace_card_path", return_value=card):
|
||||||
|
command, details = rvctl.env_action_command(
|
||||||
|
config,
|
||||||
|
"rv32i",
|
||||||
|
"test",
|
||||||
|
"inf",
|
||||||
|
"card",
|
||||||
|
"task1",
|
||||||
|
target="hazard3-baremetal",
|
||||||
|
)
|
||||||
|
self.assertEqual(details["profile"], "hazard3-sim")
|
||||||
|
self.assertIn("STEM_PROFILE=hazard3-sim", command)
|
||||||
|
self.assertIn("RV_PROFILE=hazard3-sim", command)
|
||||||
|
self.assertIn("./stem test --target hazard3-baremetal task1", command)
|
||||||
|
|
||||||
|
def test_deploy_passes_explicit_device_and_backend(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
workspace = root / "workspace"
|
||||||
|
card = workspace / "series" / "inf" / "card"
|
||||||
|
card.mkdir(parents=True)
|
||||||
|
tools = root / "tools"
|
||||||
|
tools.mkdir()
|
||||||
|
(tools / "stem").write_text("#!/bin/sh\n", encoding="utf-8")
|
||||||
|
config_path = root / "workspace.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"workspace_root": str(workspace),
|
||||||
|
"original_root": str(root / "original"),
|
||||||
|
"tools_root_candidates": [str(tools)],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
config = rvctl.load_config(config_path)
|
||||||
|
with mock.patch.object(rvctl, "resolve_workspace_card_path", return_value=card):
|
||||||
|
command, details = rvctl.env_action_command(
|
||||||
|
config,
|
||||||
|
"rp2350",
|
||||||
|
"deploy",
|
||||||
|
"inf",
|
||||||
|
"card",
|
||||||
|
"task1",
|
||||||
|
target="rp2350-rv",
|
||||||
|
device="/dev/bus/usb/001/006",
|
||||||
|
deploy_backend="probe",
|
||||||
|
)
|
||||||
|
self.assertEqual(details["device"], "/dev/bus/usb/001/006")
|
||||||
|
self.assertEqual(details["backend"], "probe")
|
||||||
|
self.assertIn("--device /dev/bus/usb/001/006", command)
|
||||||
|
self.assertIn("--backend probe", command)
|
||||||
|
|
||||||
|
def test_env_build_uses_source_profile_command(self) -> None:
|
||||||
|
source = (Path(__file__).resolve().parents[1] / "rvctl.py").read_text(encoding="utf-8")
|
||||||
|
self.assertIn('"profile-build"', source)
|
||||||
|
self.assertNotIn('"image-build",', source)
|
||||||
|
|
||||||
|
def test_mcp_requires_explicit_instance(self) -> None:
|
||||||
|
parser = rvctl.build_parser()
|
||||||
|
args = parser.parse_args(["mcp", "nvim", "hazard3-inf-bss-task1"])
|
||||||
|
self.assertEqual(args.mcp_kind, "nvim")
|
||||||
|
self.assertEqual(args.instance, "hazard3-inf-bss-task1")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+5
-7
@@ -1,15 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 2,
|
||||||
"original_root": "~/dev/edu/repos/rv",
|
"original_root": "~/dev/edu/repos/rv",
|
||||||
"original_series_root": "~/dev/edu/repos/rv/series",
|
"original_series_root": "~/dev/edu/repos/rv/series",
|
||||||
"workspace_root": "~/dev/workspace/rv",
|
"workspace_root": "~/dev/workspace/stem",
|
||||||
"workspace_info_path": "~/dev/workspace/rv/meta/workspace-info/workspace.json",
|
"legacy_workspace_root": "~/dev/workspace/rv",
|
||||||
"workspace_info_url": "http://77.90.8.171:3001/edu-workspace/workspace-info.git",
|
"workspace_info_url": "http://77.90.8.171:3001/edu-workspace/workspace-info.git",
|
||||||
"series_root": "~/dev/workspace/rv/series",
|
"env_tool_url": "http://77.90.8.171:3001/edu-tools/rv32i-hazard3-student-env.git",
|
||||||
"socket_root": "~/dev/workspace/rv/sockets",
|
|
||||||
"token_file": "~/dev/workspace/rv/tokens/tokens.json",
|
|
||||||
"env_tool_url": "http://77.90.8.171:3001/edu-inf/rv32i-hazard3-student-env.git",
|
|
||||||
"tools_root_candidates": [
|
"tools_root_candidates": [
|
||||||
|
"~/dev/workspace/stem/tools/rv32i-hazard3-student-env",
|
||||||
"~/dev/workspace/rv/tools/rv32i-hazard3-student-env",
|
"~/dev/workspace/rv/tools/rv32i-hazard3-student-env",
|
||||||
"~/dev/workspace/rv/tools/rv32i-hazard3-env",
|
"~/dev/workspace/rv/tools/rv32i-hazard3-env",
|
||||||
"~/dev/edu/repos/rv/rv32i-hazard3-student-env",
|
"~/dev/edu/repos/rv/rv32i-hazard3-student-env",
|
||||||
|
|||||||
Reference in New Issue
Block a user