From d86408e3b5d996e8e067e3f04d465c9a889a9607 Mon Sep 17 00:00:00 2001 From: Avinash Kumar Deepak Date: Thu, 26 Mar 2026 12:20:13 +0530 Subject: [PATCH 1/2] feat(cli): add concore setup autodetect command --- concore_cli/cli.py | 17 +++++ concore_cli/commands/setup.py | 107 ++++++++++++++++++++++++++ tests/test_setup.py | 139 ++++++++++++++++++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 concore_cli/commands/setup.py create mode 100644 tests/test_setup.py diff --git a/concore_cli/cli.py b/concore_cli/cli.py index b5336aa..67d945f 100644 --- a/concore_cli/cli.py +++ b/concore_cli/cli.py @@ -11,6 +11,7 @@ from .commands.inspect import inspect_workflow from .commands.watch import watch_study from .commands.doctor import doctor_check +from .commands.setup import setup_concore from . import __version__ console = Console() @@ -151,5 +152,21 @@ def doctor(): sys.exit(1) +@cli.command() +@click.option( + "--dry-run", is_flag=True, help="Preview detected config without writing" +) +@click.option("--force", is_flag=True, help="Overwrite existing config files") +def setup(dry_run, force): + """Auto-detect tools and write concore config files""" + try: + ok = setup_concore(console, dry_run=dry_run, force=force) + if not ok: + sys.exit(1) + except Exception as e: + console.print(f"[red]Error:[/red] {str(e)}") + sys.exit(1) + + if __name__ == "__main__": cli() diff --git a/concore_cli/commands/setup.py b/concore_cli/commands/setup.py new file mode 100644 index 0000000..6088c48 --- /dev/null +++ b/concore_cli/commands/setup.py @@ -0,0 +1,107 @@ +from pathlib import Path + +from .doctor import ( + TOOL_DEFINITIONS, + _detect_tool, + _get_platform_key, + _resolve_concore_path, +) + + +def _pick_config_key(config_keys, plat_key): + if plat_key == "windows": + for key in config_keys: + if key.endswith("WIN"): + return key + else: + for key in config_keys: + if key.endswith("EXE"): + return key + return config_keys[0] if config_keys else None + + +def _detect_tool_overrides(plat_key): + found = [] + for tool_def in TOOL_DEFINITIONS.values(): + config_keys = tool_def.get("config_keys", []) + if not config_keys: + continue + candidates = tool_def["names"].get(plat_key, []) + path, _ = _detect_tool(candidates) + if not path: + continue + config_key = _pick_config_key(config_keys, plat_key) + if config_key: + found.append((config_key, path)) + return found + + +def _write_text(path, content, dry_run, force, console): + if path.exists() and not force: + console.print( + f"[yellow]![/yellow] Skipping {path.name} " + "(already exists; use --force)" + ) + return True + if dry_run: + preview = content if content else "" + console.print(f"[dim]-[/dim] Would write {path.name}:\n{preview}") + return True + path.write_text(content) + console.print(f"[green]+[/green] Wrote {path.name}") + return True + + +def setup_concore(console, dry_run=False, force=False): + plat_key = _get_platform_key() + concore_path = _resolve_concore_path() + + console.print(f"[cyan]CONCOREPATH:[/cyan] {concore_path}") + + tool_overrides = _detect_tool_overrides(plat_key) + docker_candidates = TOOL_DEFINITIONS["Docker"]["names"].get(plat_key, []) + _, docker_command = _detect_tool(docker_candidates) + octave_candidates = TOOL_DEFINITIONS["Octave"]["names"].get(plat_key, []) + octave_path, _ = _detect_tool(octave_candidates) + octave_found = bool(octave_path) + + wrote_any = False + + tools_file = Path(concore_path) / "concore.tools" + if tool_overrides: + tools_content = "\n".join(f"{k}={v}" for k, v in tool_overrides) + "\n" + wrote_any = ( + _write_text(tools_file, tools_content, dry_run, force, console) + or wrote_any + ) + else: + console.print("[yellow]![/yellow] No tool paths detected for concore.tools") + + sudo_file = Path(concore_path) / "concore.sudo" + if docker_command: + sudo_content = f"{docker_command}\n" + wrote_any = ( + _write_text(sudo_file, sudo_content, dry_run, force, console) + or wrote_any + ) + else: + console.print("[yellow]![/yellow] Docker/Podman not detected; not writing concore.sudo") + + octave_file = Path(concore_path) / "concore.octave" + if octave_found: + wrote_any = ( + _write_text(octave_file, "", dry_run, force, console) + or wrote_any + ) + else: + console.print("[dim]-[/dim] Octave not detected; not writing concore.octave") + + if not wrote_any: + console.print("[yellow]No files written.[/yellow]") + return False + + if dry_run: + console.print("[green]Dry run complete.[/green]") + else: + console.print("[green]Setup complete.[/green]") + return True \ No newline at end of file diff --git a/tests/test_setup.py b/tests/test_setup.py new file mode 100644 index 0000000..42a05fe --- /dev/null +++ b/tests/test_setup.py @@ -0,0 +1,139 @@ +import tempfile +import shutil +import unittest +from pathlib import Path +from unittest.mock import patch + +from click.testing import CliRunner +from concore_cli.cli import cli + + +class TestSetupCommand(unittest.TestCase): + def setUp(self): + self.runner = CliRunner() + self.temp_dir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.temp_dir) + + @patch("concore_cli.commands.setup._resolve_concore_path") + @patch("concore_cli.commands.setup._detect_tool") + @patch("concore_cli.commands.setup._get_platform_key") + def test_setup_dry_run_does_not_write(self, mock_plat, mock_detect, mock_path): + mock_plat.return_value = "posix" + mock_path.return_value = Path(self.temp_dir) + + def detect_side_effect(names): + if "g++" in names: + return "/usr/bin/g++", "g++" + if "python3" in names: + return "/usr/bin/python3", "python3" + if "iverilog" in names: + return "/usr/bin/iverilog", "iverilog" + if "octave" in names: + return "/usr/bin/octave", "octave" + if "docker" in names: + return "/usr/bin/docker", "docker" + return None, None + + mock_detect.side_effect = detect_side_effect + + result = self.runner.invoke(cli, ["setup", "--dry-run"]) + self.assertEqual(result.exit_code, 0) + + self.assertFalse((Path(self.temp_dir) / "concore.tools").exists()) + self.assertFalse((Path(self.temp_dir) / "concore.sudo").exists()) + self.assertFalse((Path(self.temp_dir) / "concore.octave").exists()) + + @patch("concore_cli.commands.setup._resolve_concore_path") + @patch("concore_cli.commands.setup._detect_tool") + @patch("concore_cli.commands.setup._get_platform_key") + def test_setup_writes_files(self, mock_plat, mock_detect, mock_path): + mock_plat.return_value = "posix" + mock_path.return_value = Path(self.temp_dir) + + def detect_side_effect(names): + if "g++" in names: + return "/usr/bin/g++", "g++" + if "python3" in names: + return "/usr/bin/python3", "python3" + if "iverilog" in names: + return "/usr/bin/iverilog", "iverilog" + if "octave" in names: + return "/usr/bin/octave", "octave" + if "docker" in names: + return "/usr/bin/docker", "docker" + return None, None + + mock_detect.side_effect = detect_side_effect + + result = self.runner.invoke(cli, ["setup"]) + self.assertEqual(result.exit_code, 0) + + tools_file = Path(self.temp_dir) / "concore.tools" + sudo_file = Path(self.temp_dir) / "concore.sudo" + octave_file = Path(self.temp_dir) / "concore.octave" + + self.assertTrue(tools_file.exists()) + self.assertTrue(sudo_file.exists()) + self.assertTrue(octave_file.exists()) + + tools_content = tools_file.read_text() + self.assertIn("CPPEXE=/usr/bin/g++", tools_content) + self.assertIn("PYTHONEXE=/usr/bin/python3", tools_content) + self.assertIn("VEXE=/usr/bin/iverilog", tools_content) + self.assertIn("OCTAVEEXE=/usr/bin/octave", tools_content) + self.assertEqual(sudo_file.read_text().strip(), "docker") + + @patch("concore_cli.commands.setup._resolve_concore_path") + @patch("concore_cli.commands.setup._detect_tool") + @patch("concore_cli.commands.setup._get_platform_key") + def test_setup_no_force_keeps_existing(self, mock_plat, mock_detect, mock_path): + mock_plat.return_value = "posix" + mock_path.return_value = Path(self.temp_dir) + + tools_file = Path(self.temp_dir) / "concore.tools" + tools_file.write_text("CPPEXE=/old/path\n") + + def detect_side_effect(names): + if "g++" in names: + return "/usr/bin/g++", "g++" + if "python3" in names: + return "/usr/bin/python3", "python3" + return None, None + + mock_detect.side_effect = detect_side_effect + + result = self.runner.invoke(cli, ["setup"]) + self.assertEqual(result.exit_code, 0) + self.assertEqual(tools_file.read_text(), "CPPEXE=/old/path\n") + + @patch("concore_cli.commands.setup._resolve_concore_path") + @patch("concore_cli.commands.setup._detect_tool") + @patch("concore_cli.commands.setup._get_platform_key") + def test_setup_force_overwrites_existing(self, mock_plat, mock_detect, mock_path): + mock_plat.return_value = "posix" + mock_path.return_value = Path(self.temp_dir) + + tools_file = Path(self.temp_dir) / "concore.tools" + tools_file.write_text("CPPEXE=/old/path\n") + + def detect_side_effect(names): + if "g++" in names: + return "/usr/bin/g++", "g++" + if "python3" in names: + return "/usr/bin/python3", "python3" + return None, None + + mock_detect.side_effect = detect_side_effect + + result = self.runner.invoke(cli, ["setup", "--force"]) + self.assertEqual(result.exit_code, 0) + + content = tools_file.read_text() + self.assertIn("CPPEXE=/usr/bin/g++", content) + self.assertIn("PYTHONEXE=/usr/bin/python3", content) + + +if __name__ == "__main__": + unittest.main() From 336f4f8cef7a950d13431af42b89b378b0cfeade Mon Sep 17 00:00:00 2001 From: Avinash Kumar Deepak Date: Thu, 26 Mar 2026 12:30:45 +0530 Subject: [PATCH 2/2] style: apply ruff formatting --- concore_cli/cli.py | 4 +--- concore_cli/commands/setup.py | 20 ++++++++------------ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/concore_cli/cli.py b/concore_cli/cli.py index 67d945f..90cf112 100644 --- a/concore_cli/cli.py +++ b/concore_cli/cli.py @@ -153,9 +153,7 @@ def doctor(): @cli.command() -@click.option( - "--dry-run", is_flag=True, help="Preview detected config without writing" -) +@click.option("--dry-run", is_flag=True, help="Preview detected config without writing") @click.option("--force", is_flag=True, help="Overwrite existing config files") def setup(dry_run, force): """Auto-detect tools and write concore config files""" diff --git a/concore_cli/commands/setup.py b/concore_cli/commands/setup.py index 6088c48..829f24d 100644 --- a/concore_cli/commands/setup.py +++ b/concore_cli/commands/setup.py @@ -39,8 +39,7 @@ def _detect_tool_overrides(plat_key): def _write_text(path, content, dry_run, force, console): if path.exists() and not force: console.print( - f"[yellow]![/yellow] Skipping {path.name} " - "(already exists; use --force)" + f"[yellow]![/yellow] Skipping {path.name} (already exists; use --force)" ) return True if dry_run: @@ -71,8 +70,7 @@ def setup_concore(console, dry_run=False, force=False): if tool_overrides: tools_content = "\n".join(f"{k}={v}" for k, v in tool_overrides) + "\n" wrote_any = ( - _write_text(tools_file, tools_content, dry_run, force, console) - or wrote_any + _write_text(tools_file, tools_content, dry_run, force, console) or wrote_any ) else: console.print("[yellow]![/yellow] No tool paths detected for concore.tools") @@ -81,18 +79,16 @@ def setup_concore(console, dry_run=False, force=False): if docker_command: sudo_content = f"{docker_command}\n" wrote_any = ( - _write_text(sudo_file, sudo_content, dry_run, force, console) - or wrote_any + _write_text(sudo_file, sudo_content, dry_run, force, console) or wrote_any ) else: - console.print("[yellow]![/yellow] Docker/Podman not detected; not writing concore.sudo") + console.print( + "[yellow]![/yellow] Docker/Podman not detected; not writing concore.sudo" + ) octave_file = Path(concore_path) / "concore.octave" if octave_found: - wrote_any = ( - _write_text(octave_file, "", dry_run, force, console) - or wrote_any - ) + wrote_any = _write_text(octave_file, "", dry_run, force, console) or wrote_any else: console.print("[dim]-[/dim] Octave not detected; not writing concore.octave") @@ -104,4 +100,4 @@ def setup_concore(console, dry_run=False, force=False): console.print("[green]Dry run complete.[/green]") else: console.print("[green]Setup complete.[/green]") - return True \ No newline at end of file + return True