From 2204f8226838288be38e0510cfca968f6443be13 Mon Sep 17 00:00:00 2001 From: mpabi Date: Fri, 1 May 2026 22:23:08 +0200 Subject: [PATCH] Integrate rvctl with rv32i and host debug profiles --- README.md | 5 +- doc/containers.md | 197 ++++++++++++++-------------- doc/rvctl.md | 84 ++++++++++++ rvctl.py | 322 +++++++++++++++++++++++++++++++++++++++++++++- workspace.json | 3 + 5 files changed, 505 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index acc0d1d..6fec7b8 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,9 @@ jest w osobnym repo `edu-workspace/workspace-info`. 3. Uruchamianie środowiska programistycznego w kontenerach - Dla wybranej karty `rvctl` uruchamia sesję tmux i kontener z przygotowanym - środowiskiem developerskim. Docelowy model namespace `containers` jest w + 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 diff --git a/doc/containers.md b/doc/containers.md index e3d5ce1..a405efd 100644 --- a/doc/containers.md +++ b/doc/containers.md @@ -1,156 +1,153 @@ -# Containers +# Kontenery i debug -Ten dokument opisuje docelowy model komend `rvctl` do uruchamiania -środowiska programistycznego w kontenerach. +`rvctl` integruje workspace z narzędziem `rv32i-hazard3-student-env`. +Środowisko ma dwa profile kontenerów: -## Zasada +- `rv32i` - docelowe środowisko RISC-V/Hazard3 z `nvim`, `tmux`, + `gdb-multiarch`, toolchainem RISC-V i symulatorem Hazard3. +- `host` - natywne środowisko na architekturze hosta z `clang`, `gcc`, `gdb`, + opcjonalnie `lldb`, `valgrind`, `nvim` i `tmux`. -Kontener jest środowiskiem pracy dla wybranej karty, opcjonalnie zawężonym do -konkretnego zadania. `rvctl` uruchamia je przez sesję `tmux`, tak aby terminal -z kontenerem był łatwy do ponownego podłączenia. - -Używamy namespace `containers`, a nie `container`, bo jest spójny z pluralnymi -namespace'ami `tokens`, `cards` i `tasks`. +Profile są uruchamiane jako oddzielne usługi `docker compose`, więc zależności +RISC-V i zależności natywnego debugowania nie mieszają się ze sobą. ## Szybki przepływ -Typowy przepływ pracy: - ```bash -./rvctl tokens compare -./rvctl series cards show inf bss -./rvctl series cards tasks list inf bss -./rvctl containers show inf bss -./rvctl containers start inf bss --session rv-inf-bss --attach -./rvctl containers list -./rvctl containers stop rv-inf-bss +./rvctl env list +./rvctl env sync +./rvctl env build rv32i +./rvctl env build host + +./rvctl debug rv32i inf bss 1 +./rvctl debug host inf bss 1 +./rvctl run host inf bss 1 +./rvctl shell rv32i inf bss +./rvctl shell host inf bss ``` -## Model +Jeżeli `workspace.json` ma ustawione domyślne wartości `defaults.series`, +`defaults.card` i `defaults.task`, można używać krótszych komend: -Docelowo komendy `containers` pracują na trzech warstwach: +```bash +./rvctl debug rv32i +./rvctl debug host +./rvctl debug rv32i 4 +./rvctl debug host 4 +``` -- karta pracy, np. `inf bss` -- opcjonalne zadanie, np. `--task task1` -- instancja środowiska, np. `--instance shell` +## `env list` -Domyślne wartości pochodzą z `workspace.json`: +Pokazuje dostępne profile i bieżący katalog narzędzia: + +```bash +./rvctl env list +``` + +Typowy wynik: ```text -defaults.series -defaults.card -defaults.task -defaults.instance -defaults.tmux_session -defaults.tmux_window +profile service image status purpose +rv32i env edu-inf/rv32i-hazard3-env:latest ready RV32I/Hazard3 nvim + gdb-multiarch +host host edu-inf/rv32i-hazard3-host-env:latest ready native host clang/gcc + gdb/lldb ``` -## Komendy +## `env sync` -### `containers show [SERIES] [CARD]` - -Pokazuje plan uruchomienia środowiska bez tworzenia sesji `tmux`. - -```bash -./rvctl containers show inf bss -./rvctl containers show inf/bss --task task1 -``` - -Typowe pola: +Klonuje albo aktualizuje repo środowiska do: ```text -selector inf/bss -task task1 -card_path ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack -task_path ~/dev/workspace/rv/series/inf/lab-rv32i-strlen-bss-data-stack/task1 -tools_root ~/dev/workspace/rv/tools/rv32i-hazard3-env -session rv-inf-bss -window rv -instance shell +~/dev/workspace/rv/tools/rv32i-hazard3-student-env ``` -Ta komenda jest odpowiednikiem obecnego trybu: +Komenda: ```bash -./rvctl tmux-container inf bss --dry-run +./rvctl env sync +./rvctl env sync --dry-run ``` -### `containers start [SERIES] [CARD]` +Jeżeli tego repo jeszcze nie ma w workspace, `rvctl` używa fallbacku z +`~/dev/edu/repos/rv/rv32i-hazard3-env`, o ile jest dostępny. -Uruchamia sesję `tmux` i kontener dla wybranej karty. +## `env build` + +Buduje wybrany profil kontenera: ```bash -./rvctl containers start inf bss -./rvctl containers start inf/bss --session rv-inf-bss -./rvctl containers start inf bss --task task1 --instance shell --attach +./rvctl env build rv32i +./rvctl env build host ``` -Przełączniki: - -- `--task TASK` - Wybiera zadanie w karcie. Bez tego `rvctl` używa domyślnego zadania z - `workspace.json` albo pracuje na katalogu karty. -- `--session NAME` - Nadpisuje nazwę sesji `tmux`. -- `--window NAME` - Nadpisuje nazwę okna `tmux`. -- `--instance NAME` - Ustawia wariant środowiska, np. `shell`. -- `--attach` - Po uruchomieniu od razu podłącza terminal do sesji. - -### `containers list` - -Listuje aktywne środowiska uruchomione przez `rvctl`. +Wariant `--dry-run` pokazuje dokładną komendę bez uruchamiania Dockera: ```bash -./rvctl containers list +./rvctl env build host --dry-run ``` -Przykładowy wynik: +## `debug` -```text -session selector task instance window --------- -------- ----- -------- ------ -rv-inf-bss inf/bss task1 shell rv -``` - -### `containers attach SESSION` - -Podłącza terminal do istniejącej sesji. +Uruchamia debugowanie zadania w wybranym profilu. ```bash -./rvctl containers attach rv-inf-bss +./rvctl debug rv32i inf bss 1 +./rvctl debug host inf bss 1 ``` -Technicznie odpowiada to: +W profilu `rv32i` startuje środowisko Hazard3: symulator, `gdb-multiarch` i +`nvim` w sesji `tmux`. + +W profilu `host` wybrane źródło C jest kompilowane natywnie z debug info, a +potem uruchamiane w `gdb` obok `nvim`. +Ten profil jest przeznaczony dla tasków C z funkcją `main`; taski czysto +asemblerskie nadal debugujemy w profilu `rv32i`. + +Przydatne warianty: ```bash -tmux attach -t rv-inf-bss +./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 host 4 --dry-run ``` -### `containers stop SESSION` +## `run` -Zamyka sesję `tmux`, a razem z nią uruchomiony w niej proces kontenera. +Na razie `run` jest wdrożone dla profilu `host`. Buduje wybrany task natywnie i +uruchamia wynikowy program: ```bash -./rvctl containers stop rv-inf-bss +./rvctl run host inf bss 1 +./rvctl run host 4 ``` -Technicznie odpowiada to: +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 -tmux kill-session -t rv-inf-bss +./rvctl shell rv32i inf bss +./rvctl shell host inf bss ``` -## Aliasowanie Starej Komendy +## Selektory -Stara komenda może zostać jako alias kompatybilności: +Komendy `debug`, `run` i `shell` używają tych samych skrótów co `tasks`: -```text -tmux-container inf bss --dry-run -> containers show inf bss -tmux-container inf bss --session NAME -> containers start inf bss --session NAME -tmux-container inf/bss --attach -> containers start inf/bss --attach +```bash +./rvctl debug host 1 +./rvctl debug host bss 1 +./rvctl debug host inf bss 1 ``` -Dokumentacja i nowe przykłady powinny promować namespace `containers`. +`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`. diff --git a/doc/rvctl.md b/doc/rvctl.md index 011af9d..04f1fbd 100644 --- a/doc/rvctl.md +++ b/doc/rvctl.md @@ -220,6 +220,12 @@ Komendy: - `series cards tasks list SERIES CARD` - `series cards tasks show SERIES CARD TASK` - `series cards tasks switch SERIES CARD TASK` +- `env list` +- `env sync` +- `env build rv32i|host` +- `debug rv32i|host [TASK|CARD TASK|SERIES CARD TASK]` +- `run host [TASK|CARD TASK|SERIES CARD TASK]` +- `shell rv32i|host [CARD|SERIES CARD]` - `tasks list [CARD|SERIES CARD]` - `tasks show TASK|CARD TASK|SERIES CARD TASK` - `tasks switch TASK|CARD TASK|SERIES CARD TASK` @@ -560,6 +566,84 @@ Ta komenda działa tak samo jak pełna forma: ./rvctl series cards tasks switch inf bss 4 ``` +## `env list` + +Pokazuje profile środowiska kontenerowego obsługiwane przez `rvctl`. + +```bash +./rvctl env list +``` + +Profile: + +- `rv32i` - RISC-V/Hazard3, `nvim`, `tmux`, `gdb-multiarch` +- `host` - natywny kontener z `clang`, `gcc`, `gdb`, opcjonalnie `lldb` + +## `env sync` + +Klonuje albo aktualizuje repo `rv32i-hazard3-student-env` do katalogu +`~/dev/workspace/rv/tools/rv32i-hazard3-student-env`. + +```bash +./rvctl env sync +./rvctl env sync --dry-run +``` + +## `env build rv32i|host` + +Buduje wybrany profil `docker compose`. + +```bash +./rvctl env build rv32i +./rvctl env build host +./rvctl env build host --dry-run +``` + +## `debug rv32i|host` + +Uruchamia debugowanie zadania w kontenerze. Selektor może być pełny albo krótki. + +```bash +./rvctl debug rv32i inf bss 1 +./rvctl debug host inf bss 1 +./rvctl debug rv32i 4 +./rvctl debug host 4 +``` + +`rv32i` uruchamia symulator Hazard3, `gdb-multiarch` i `nvim`. `host` kompiluje +zadanie natywnie i otwiera `gdb` obok `nvim`. Profil `host` jest przeznaczony +dla tasków C z funkcją `main`; taski czysto asemblerowe debugujemy w profilu +`rv32i`. + +Przełączniki: + +- `--task TASK` - nadpisuje zadanie z selektora albo z `workspace.json` +- `--instance NAME` - ustawia nazwę instancji kontenera/tmuxa +- `--editor nvim|vim` - wybiera edytor w kontenerze +- `--dry-run` - pokazuje komendę bez uruchamiania Dockera + +## `run host` + +Buduje i uruchamia wybrane zadanie natywnie w kontenerze `host`. + +```bash +./rvctl run host inf bss 1 +./rvctl run host 4 +``` + +Profil `rv32i` pracuje obecnie przez `debug rv32i`, bo wymaga symulatora +Hazard3 i połączenia GDB. + +## `shell rv32i|host` + +Otwiera shell w wybranym profilu kontenera z podmontowaną kartą. + +```bash +./rvctl shell rv32i inf bss +./rvctl shell host inf bss +./rvctl shell host --dry-run +``` + ## `list-series` Alias kompatybilności dla `series list`. diff --git a/rvctl.py b/rvctl.py index 9938468..2c271a9 100755 --- a/rvctl.py +++ b/rvctl.py @@ -52,6 +52,7 @@ class WorkspaceConfig: token_path: Path tools_root_candidates: list[Path] tools_root: Path + env_tool_url: str defaults: dict[str, str] git: GitConfig @@ -109,6 +110,19 @@ REPO_PERMISSION_FIELDS = [ ("r", "read"), ] +ENV_PROFILES = { + "rv32i": { + "service": "env", + "image": "edu-inf/rv32i-hazard3-env:latest", + "purpose": "RV32I/Hazard3 nvim + gdb-multiarch", + }, + "host": { + "service": "host", + "image": "edu-inf/rv32i-hazard3-host-env:latest", + "purpose": "native host clang/gcc + gdb/lldb", + }, +} + def load_config(config_path: Path) -> WorkspaceConfig: raw = json.loads(config_path.read_text(encoding="utf-8")) @@ -116,6 +130,8 @@ def load_config(config_path: Path) -> WorkspaceConfig: original_root = expand_path(raw.get("original_root", "~/dev/edu/repos/rv"), base_dir) workspace_root = expand_path(raw.get("workspace_root", "~/dev/workspace/rv"), base_dir) + git_raw = raw.get("git", {}) + 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) 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) @@ -128,15 +144,16 @@ def load_config(config_path: Path) -> WorkspaceConfig: candidate_values = raw.get("tools_root_candidates") if not candidate_values: candidate_values = [ - raw.get("tools_root", str(workspace_root / "tools" / "rv32i-hazard3-env")), + raw.get("tools_root", str(workspace_root / "tools" / "rv32i-hazard3-student-env")), + str(workspace_root / "tools" / "rv32i-hazard3-env"), + str(original_root / "rv32i-hazard3-student-env"), str(original_root / "rv32i-hazard3-env"), ] tools_root_candidates = [expand_path(value, base_dir) for value in candidate_values] 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") defaults = raw.get("defaults", {}) - git_raw = raw.get("git", {}) - base_url = git_raw.get("base_url", "http://77.90.8.171:3001").rstrip("/") workspace_info_url = raw.get("workspace_info_url", f"{base_url}/edu-workspace/workspace-info.git") git = GitConfig( base_url=base_url, @@ -162,6 +179,7 @@ def load_config(config_path: Path) -> WorkspaceConfig: token_path=token_path, tools_root_candidates=tools_root_candidates, tools_root=tools_root, + env_tool_url=env_tool_url, defaults=defaults, git=git, ) @@ -3316,6 +3334,251 @@ def run_tmux_container(config: WorkspaceConfig, args: argparse.Namespace) -> Non print(f"Attach with: tmux attach -t {session_name}") +def normalize_env_profile(profile: str) -> str: + aliases = { + "riscv": "rv32i", + "riscv32": "rv32i", + "native": "host", + } + normalized = aliases.get(profile, profile) + if normalized not in ENV_PROFILES: + available = ", ".join(sorted(ENV_PROFILES)) + raise SystemExit(f"Unsupported environment profile '{profile}'. Available: {available}") + return normalized + + +def env_tool_command_path(config: WorkspaceConfig) -> Path: + return config.tools_root / "rv" + + +def ensure_env_tool(config: WorkspaceConfig) -> Path: + rv_script = env_tool_command_path(config) + if rv_script.is_file(): + return rv_script + candidates = "\n".join(f" - {path}" for path in config.tools_root_candidates) + raise SystemExit( + "Missing rv32i-hazard3-student-env launcher script 'rv'.\n" + f"Checked:\n{candidates}\n" + "Run: ./rvctl env sync" + ) + + +def env_sync_target(config: WorkspaceConfig) -> Path: + return config.workspace_root / "tools" / "rv32i-hazard3-student-env" + + +def sync_env_tool(config: WorkspaceConfig, dry_run: bool = False) -> dict[str, str]: + target = env_sync_target(config) + if dry_run: + command = "pull" if (target / ".git").is_dir() else "clone" + return { + "env_tool_path": str(target), + "env_tool_url": config.env_tool_url, + "command": command, + "status": "dry-run", + } + target.parent.mkdir(parents=True, exist_ok=True) + if (target / ".git").is_dir(): + subprocess.run(["git", "-C", str(target), "pull", "--ff-only"], check=True) + config.tools_root = target + return { + "env_tool_path": str(target), + "env_tool_url": config.env_tool_url, + "command": "pull", + "status": "ready", + } + if target.exists(): + raise SystemExit(f"Environment tool path exists but is not a git repo: {target}") + subprocess.run(["git", "clone", config.env_tool_url, str(target)], check=True) + config.tools_root = target + return { + "env_tool_path": str(target), + "env_tool_url": config.env_tool_url, + "command": "clone", + "status": "ready", + } + + +def task_selector_like(value: str) -> bool: + return bool(re.fullmatch(r"(?i)(task[-_]?)?\d+.*|t\d+", value.strip())) + + +def resolve_env_target( + config: WorkspaceConfig, + values: list[str], + task_override: str | None = None, +) -> tuple[str, str, str]: + if len(values) > 3: + raise SystemExit("Pass '', ' [task]' or ' [task]'.") + + if not values: + series_name, card_name = resolve_selector(config, None, None) + card_path = resolve_workspace_card_path(config, series_name, card_name) + task_name = resolve_submission_task(config, card_path, task_override) + return series_name, card_name, task_name + + if len(values) == 1: + if task_selector_like(values[0]): + series_name, card_name = resolve_selector(config, None, None) + task_selector = task_override or values[0] + else: + series_name, card_name = resolve_selector(config, values[0], None) + task_selector = task_override + card_path = resolve_workspace_card_path(config, series_name, card_name) + task_name = resolve_submission_task(config, card_path, task_selector) + return series_name, card_name, task_name + + if len(values) == 2: + if task_selector_like(values[1]): + series_name, card_name = resolve_selector(config, values[0], None) + task_selector = task_override or values[1] + else: + series_name, card_name = resolve_selector(config, values[0], values[1]) + task_selector = task_override + card_path = resolve_workspace_card_path(config, series_name, card_name) + task_name = resolve_submission_task(config, card_path, task_selector) + return series_name, card_name, task_name + + series_name, card_name = resolve_selector(config, values[0], values[1]) + card_path = resolve_workspace_card_path(config, series_name, card_name) + task_name = resolve_submission_task(config, card_path, task_override or values[2]) + return series_name, card_name, task_name + + +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() + return slug_value(f"{profile}-{series_name}-{card_name}-{task_part}-{action}", "environment instance") + + +def env_action_command( + config: WorkspaceConfig, + profile: str, + action: str, + series_name: str | None = None, + card_name: str | None = None, + task_name: str | None = None, + instance: str | None = None, + editor: str | None = None, +) -> tuple[str, dict[str, str]]: + profile = normalize_env_profile(profile) + rv_script = ensure_env_tool(config) + env_vars = { + "WORKSPACE_ROOT": str(config.workspace_root), + "RV_CARDS_ROOT": str(config.series_root), + "RV_PROFILE": profile, + } + command_args = ["bash", "./rv"] + + card_path: Path | None = None + selector = "" + if series_name and card_name: + card_path = resolve_workspace_card_path(config, series_name, card_name) + selector = f"{series_name}/{card_name}" + env_vars.update( + { + "RV_REPO_PATH": str(card_path), + "RV_REPO_HOST": str(card_path), + "RV_CARD": card_path.name, + } + ) + + if instance: + env_vars["RV_INSTANCE"] = instance + elif series_name and card_name and task_name: + env_vars["RV_INSTANCE"] = default_env_instance(profile, series_name, card_name, task_name, action) + + if editor: + env_vars["RV_EDITOR"] = editor + + if action == "build": + command_args.append("build") + elif action == "shell": + command_args.append("shell") + elif action == "debug": + if not task_name: + raise SystemExit("debug requires a task selector.") + if profile == "rv32i": + command_args.extend(["nvim", task_name]) + else: + command_args.extend(["debug", task_name]) + elif action == "run": + if not task_name: + raise SystemExit("run requires a task selector.") + if profile == "rv32i": + raise SystemExit("rv32i run is not separate yet. Use: ./rvctl debug rv32i ...") + command_args.extend(["run", task_name]) + else: + raise SystemExit(f"Unsupported environment action: {action}") + + env_prefix = " ".join(f"{key}={shlex.quote(value)}" for key, value in env_vars.items()) + shell_command = " && ".join( + [ + f"cd {shlex.quote(str(rv_script.parent))}", + " ".join([env_prefix, *[shlex.quote(part) for part in command_args]]), + ] + ) + details = { + "profile": profile, + "service": ENV_PROFILES[profile]["service"], + "selector": selector, + "task": task_name or "", + "card_path": str(card_path or ""), + "tools_root": str(config.tools_root), + "instance": env_vars.get("RV_INSTANCE", ""), + "command": shell_command, + } + return shell_command, details + + +def print_env_plan(details: dict[str, str]) -> None: + for key in ["profile", "service", "selector", "task", "card_path", "tools_root", "instance", "command"]: + print(f"{key}\t{details.get(key, '')}") + + +def run_env(config: WorkspaceConfig, args: argparse.Namespace) -> None: + if args.env_command == "list": + print("profile\tservice\timage\tstatus\tpurpose") + for profile, info in ENV_PROFILES.items(): + print( + f"{profile}\t{info['service']}\t{info['image']}\t" + f"{'ready' if env_tool_command_path(config).is_file() else 'missing'}\t{info['purpose']}" + ) + print(f"tools_root\t{config.tools_root}") + print(f"env_tool_url\t{config.env_tool_url}") + return + if args.env_command == "sync": + print_key_values(sync_env_tool(config, dry_run=args.dry_run)) + return + if args.env_command == "build": + profile = normalize_env_profile(args.profile) + shell_command, details = env_action_command(config, profile, "build") + if args.dry_run: + print_env_plan(details) + return + subprocess.run(["bash", "-lc", shell_command], check=True) + return + raise SystemExit(f"Unsupported env command: {args.env_command}") + + +def run_profile_action(config: WorkspaceConfig, args: argparse.Namespace, action: str) -> None: + profile = normalize_env_profile(args.profile) + series_name, card_name, task_name = resolve_env_target(config, args.selector, getattr(args, "task", None)) + shell_command, details = env_action_command( + config, + profile, + action, + series_name=series_name, + card_name=card_name, + task_name=task_name, + instance=getattr(args, "instance", None), + editor=getattr(args, "editor", None), + ) + if args.dry_run: + print_env_plan(details) + return + subprocess.run(["bash", "-lc", shell_command], check=True) + + def workspace_info_status(config: WorkspaceConfig) -> str: info_root = workspace_info_root(config) if (info_root / ".git").is_dir(): @@ -3395,6 +3658,7 @@ def print_config(config: WorkspaceConfig) -> None: print(f"socket_root\t{config.socket_root}") print(f"token_path\t{config.token_path}") print(f"tools_root\t{config.tools_root}") + print(f"env_tool_url\t{config.env_tool_url}") print(f"git_base_url\t{config.git.base_url}") print(f"git_source_org\t{config.git.source_org}") print(f"git_answer_org\t{config.git.answer_org}") @@ -3474,6 +3738,10 @@ def print_main_overview(config_path: Path) -> None: ["workspace", "show|sync", "show or update meta/workspace-info"], ["series", "list|cards|fetch|tasks", "work with series, cards and tasks"], ["tasks", "list|show|switch", "shortcut for tasks in the default card"], + ["env", "list|sync|build", "manage rv32i/host container environment"], + ["debug", "rv32i|host [selector]", "open nvim + gdb in the selected container profile"], + ["run", "host [selector]", "build and run selected task natively in host container"], + ["shell", "rv32i|host [selector]", "open a shell in the selected container profile"], ["list-series", "", "compat alias for series list"], ["list-cards", "[series]", "compat alias for series cards list"], ["tmux-container", "[series] [card]", "start tmux with container in pane 0"], @@ -3493,6 +3761,8 @@ def print_main_overview(config_path: Path) -> None: ["5", "./rvctl series cards fetch inf bss", "fetch card and prepare r1a"], ["6", "./rvctl tasks list", "list tasks from the default card"], ["7", "./rvctl tasks switch 4", "switch to the answer branch for task 4"], + ["8", "./rvctl debug rv32i 4", "debug task 4 in RV32I/Hazard3"], + ["9", "./rvctl debug host 4", "debug task 4 natively in a host container"], ], ) print() @@ -3502,6 +3772,7 @@ def print_main_overview(config_path: Path) -> None: print(" doc/rvctl.md") print(" doc/tokens.md") print(" doc/tokens.schema.json") + print(" doc/containers.md") print(" doc/workspace.md") @@ -3598,6 +3869,37 @@ def build_parser() -> argparse.ArgumentParser: 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.") + env_parser = subparsers.add_parser("env", help="Manage rv32i/host container environments.") + env_subparsers = env_parser.add_subparsers(dest="env_command", required=True) + 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.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.add_argument("profile", choices=sorted(ENV_PROFILES), help="Environment profile.") + env_build_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.") + debug_parser.add_argument("profile", choices=sorted(ENV_PROFILES), help="Environment profile: rv32i or host.") + debug_parser.add_argument("selector", nargs="*", help="Optional selector: [task], [card task] or [series card task].") + debug_parser.add_argument("--task", help="Task override. Defaults to selector or defaults.task.") + debug_parser.add_argument("--instance", help="Container/tmux instance name.") + debug_parser.add_argument("--editor", choices=["nvim", "vim"], help="Editor inside the container.") + debug_parser.add_argument("--dry-run", action="store_true", help="Print command without starting Docker.") + + run_parser = subparsers.add_parser("run", help="Build and run selected task inside a container profile.") + run_parser.add_argument("profile", choices=sorted(ENV_PROFILES), help="Environment profile. Currently host supports run.") + run_parser.add_argument("selector", nargs="*", help="Optional selector: [task], [card task] or [series card task].") + run_parser.add_argument("--task", help="Task override. Defaults to selector or defaults.task.") + run_parser.add_argument("--instance", help="Container instance name.") + 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.") + shell_parser.add_argument("profile", choices=sorted(ENV_PROFILES), help="Environment profile: rv32i or host.") + shell_parser.add_argument("selector", nargs="*", help="Optional selector: [card] or [series card].") + shell_parser.add_argument("--task", help="Task used only for instance naming.") + shell_parser.add_argument("--instance", help="Container instance name.") + 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_subparsers = tasks_parser.add_subparsers(dest="tasks_command", required=True) @@ -3836,7 +4138,7 @@ def main() -> int: parser = build_parser() args = parser.parse_args() config = load_config(args.config) - if args.command in {"list-series", "list-cards", "series", "tasks", "tmux-container", "submission"}: + if args.command in {"list-series", "list-cards", "series", "tasks", "tmux-container", "submission", "debug", "run", "shell"}: ensure_workspace_info(config) if args.command == "show-config": @@ -3851,6 +4153,18 @@ def main() -> int: if args.command == "workspace": run_workspace(config, args) return 0 + if args.command == "env": + run_env(config, args) + return 0 + if args.command == "debug": + run_profile_action(config, args, "debug") + return 0 + if args.command == "run": + run_profile_action(config, args, "run") + return 0 + if args.command == "shell": + run_profile_action(config, args, "shell") + return 0 if args.command == "tasks": run_tasks(config, args) return 0 diff --git a/workspace.json b/workspace.json index 3ccf811..168a33b 100644 --- a/workspace.json +++ b/workspace.json @@ -8,8 +8,11 @@ "series_root": "~/dev/workspace/rv/series", "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": [ + "~/dev/workspace/rv/tools/rv32i-hazard3-student-env", "~/dev/workspace/rv/tools/rv32i-hazard3-env", + "~/dev/edu/repos/rv/rv32i-hazard3-student-env", "~/dev/edu/repos/rv/rv32i-hazard3-env" ], "git": {