Integrate rvctl with rv32i and host debug profiles
This commit is contained in:
@@ -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>', '<card> [task]' or '<series> <card> [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
|
||||
|
||||
Reference in New Issue
Block a user