diff --git a/rvctl.py b/rvctl.py index d2dd114..99c1451 100755 --- a/rvctl.py +++ b/rvctl.py @@ -3771,6 +3771,26 @@ def run_env(config: WorkspaceConfig, args: argparse.Namespace) -> None: if args.env_command == "sync": print_key_values(sync_env_tool(config, dry_run=args.dry_run)) return + if args.env_command == "sources": + series_name, card_name = resolve_selector(config, args.series, args.card) + card_path = resolve_workspace_card_path(config, series_name, card_name) + env_script = ensure_env_tool(config) + preparer = env_script.parent / "scripts" / "prepare-workspace-sources.py" + if not preparer.is_file(): + raise SystemExit( + f"Environment tool does not provide pinned workspace sources: {preparer}. " + "Run: ./stemctl env sync" + ) + mode = "check" if args.check else "prepare" + command = [str(preparer), mode, str(card_path)] + if args.dry_run: + print(f"series\t{series_name}") + print(f"card\t{card_name}") + print(f"workspace\t{card_path}") + print(f"command\t{shlex.join(command)}") + return + subprocess.run(command, check=True) + return if args.env_command == "build": profile = normalize_env_profile(args.profile) env_script = ensure_env_tool(config) @@ -4453,6 +4473,13 @@ def build_parser() -> argparse.ArgumentParser: 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_sources_parser = env_subparsers.add_parser( + "sources", help="Prepare or verify pinned Hazard3 sources in one host card workspace." + ) + env_sources_parser.add_argument("series", nargs="?", help="Series id; defaults to workspace.json.") + env_sources_parser.add_argument("card", nargs="?", help="Card id; defaults to workspace.json.") + env_sources_parser.add_argument("--check", action="store_true", help="Verify without changing the workspace.") + env_sources_parser.add_argument("--dry-run", action="store_true", help="Print the preparation command only.") env_build_parser = env_subparsers.add_parser("build", help="Build one container 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.") diff --git a/tests/test_stemctl.py b/tests/test_stemctl.py index c807ced..9f9f1fa 100644 --- a/tests/test_stemctl.py +++ b/tests/test_stemctl.py @@ -298,6 +298,14 @@ class StemctlContractTests(unittest.TestCase): self.assertEqual(by_instance.selector, "rp2350-pointers-final") self.assertEqual(by_container.selector, "3aca2c1c4c7a") + def test_env_sources_selects_a_card_and_supports_read_only_check(self) -> None: + parser = rvctl.build_parser() + args = parser.parse_args(["env", "sources", "inf", "pointers", "--check"]) + self.assertEqual(args.env_command, "sources") + self.assertEqual(args.series, "inf") + self.assertEqual(args.card, "pointers") + self.assertTrue(args.check) + def test_mcp_select_switches_both_sockets_with_one_current_symlink(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary)