Skip to content

Commit 7c5ea5b

Browse files
authored
Merge pull request #512 from avinxshKD/fix/setup-autodetect-506
feat(cli): add concore setup autodetect command
2 parents c3fea64 + 336f4f8 commit 7c5ea5b

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

concore_cli/cli.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .commands.inspect import inspect_workflow
1212
from .commands.watch import watch_study
1313
from .commands.doctor import doctor_check
14+
from .commands.setup import setup_concore
1415
from . import __version__
1516

1617
console = Console()
@@ -151,5 +152,19 @@ def doctor():
151152
sys.exit(1)
152153

153154

155+
@cli.command()
156+
@click.option("--dry-run", is_flag=True, help="Preview detected config without writing")
157+
@click.option("--force", is_flag=True, help="Overwrite existing config files")
158+
def setup(dry_run, force):
159+
"""Auto-detect tools and write concore config files"""
160+
try:
161+
ok = setup_concore(console, dry_run=dry_run, force=force)
162+
if not ok:
163+
sys.exit(1)
164+
except Exception as e:
165+
console.print(f"[red]Error:[/red] {str(e)}")
166+
sys.exit(1)
167+
168+
154169
if __name__ == "__main__":
155170
cli()

concore_cli/commands/setup.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from pathlib import Path
2+
3+
from .doctor import (
4+
TOOL_DEFINITIONS,
5+
_detect_tool,
6+
_get_platform_key,
7+
_resolve_concore_path,
8+
)
9+
10+
11+
def _pick_config_key(config_keys, plat_key):
12+
if plat_key == "windows":
13+
for key in config_keys:
14+
if key.endswith("WIN"):
15+
return key
16+
else:
17+
for key in config_keys:
18+
if key.endswith("EXE"):
19+
return key
20+
return config_keys[0] if config_keys else None
21+
22+
23+
def _detect_tool_overrides(plat_key):
24+
found = []
25+
for tool_def in TOOL_DEFINITIONS.values():
26+
config_keys = tool_def.get("config_keys", [])
27+
if not config_keys:
28+
continue
29+
candidates = tool_def["names"].get(plat_key, [])
30+
path, _ = _detect_tool(candidates)
31+
if not path:
32+
continue
33+
config_key = _pick_config_key(config_keys, plat_key)
34+
if config_key:
35+
found.append((config_key, path))
36+
return found
37+
38+
39+
def _write_text(path, content, dry_run, force, console):
40+
if path.exists() and not force:
41+
console.print(
42+
f"[yellow]![/yellow] Skipping {path.name} (already exists; use --force)"
43+
)
44+
return True
45+
if dry_run:
46+
preview = content if content else "<empty file>"
47+
console.print(f"[dim]-[/dim] Would write {path.name}:\n{preview}")
48+
return True
49+
path.write_text(content)
50+
console.print(f"[green]+[/green] Wrote {path.name}")
51+
return True
52+
53+
54+
def setup_concore(console, dry_run=False, force=False):
55+
plat_key = _get_platform_key()
56+
concore_path = _resolve_concore_path()
57+
58+
console.print(f"[cyan]CONCOREPATH:[/cyan] {concore_path}")
59+
60+
tool_overrides = _detect_tool_overrides(plat_key)
61+
docker_candidates = TOOL_DEFINITIONS["Docker"]["names"].get(plat_key, [])
62+
_, docker_command = _detect_tool(docker_candidates)
63+
octave_candidates = TOOL_DEFINITIONS["Octave"]["names"].get(plat_key, [])
64+
octave_path, _ = _detect_tool(octave_candidates)
65+
octave_found = bool(octave_path)
66+
67+
wrote_any = False
68+
69+
tools_file = Path(concore_path) / "concore.tools"
70+
if tool_overrides:
71+
tools_content = "\n".join(f"{k}={v}" for k, v in tool_overrides) + "\n"
72+
wrote_any = (
73+
_write_text(tools_file, tools_content, dry_run, force, console) or wrote_any
74+
)
75+
else:
76+
console.print("[yellow]![/yellow] No tool paths detected for concore.tools")
77+
78+
sudo_file = Path(concore_path) / "concore.sudo"
79+
if docker_command:
80+
sudo_content = f"{docker_command}\n"
81+
wrote_any = (
82+
_write_text(sudo_file, sudo_content, dry_run, force, console) or wrote_any
83+
)
84+
else:
85+
console.print(
86+
"[yellow]![/yellow] Docker/Podman not detected; not writing concore.sudo"
87+
)
88+
89+
octave_file = Path(concore_path) / "concore.octave"
90+
if octave_found:
91+
wrote_any = _write_text(octave_file, "", dry_run, force, console) or wrote_any
92+
else:
93+
console.print("[dim]-[/dim] Octave not detected; not writing concore.octave")
94+
95+
if not wrote_any:
96+
console.print("[yellow]No files written.[/yellow]")
97+
return False
98+
99+
if dry_run:
100+
console.print("[green]Dry run complete.[/green]")
101+
else:
102+
console.print("[green]Setup complete.[/green]")
103+
return True

tests/test_setup.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import tempfile
2+
import shutil
3+
import unittest
4+
from pathlib import Path
5+
from unittest.mock import patch
6+
7+
from click.testing import CliRunner
8+
from concore_cli.cli import cli
9+
10+
11+
class TestSetupCommand(unittest.TestCase):
12+
def setUp(self):
13+
self.runner = CliRunner()
14+
self.temp_dir = tempfile.mkdtemp()
15+
16+
def tearDown(self):
17+
shutil.rmtree(self.temp_dir)
18+
19+
@patch("concore_cli.commands.setup._resolve_concore_path")
20+
@patch("concore_cli.commands.setup._detect_tool")
21+
@patch("concore_cli.commands.setup._get_platform_key")
22+
def test_setup_dry_run_does_not_write(self, mock_plat, mock_detect, mock_path):
23+
mock_plat.return_value = "posix"
24+
mock_path.return_value = Path(self.temp_dir)
25+
26+
def detect_side_effect(names):
27+
if "g++" in names:
28+
return "/usr/bin/g++", "g++"
29+
if "python3" in names:
30+
return "/usr/bin/python3", "python3"
31+
if "iverilog" in names:
32+
return "/usr/bin/iverilog", "iverilog"
33+
if "octave" in names:
34+
return "/usr/bin/octave", "octave"
35+
if "docker" in names:
36+
return "/usr/bin/docker", "docker"
37+
return None, None
38+
39+
mock_detect.side_effect = detect_side_effect
40+
41+
result = self.runner.invoke(cli, ["setup", "--dry-run"])
42+
self.assertEqual(result.exit_code, 0)
43+
44+
self.assertFalse((Path(self.temp_dir) / "concore.tools").exists())
45+
self.assertFalse((Path(self.temp_dir) / "concore.sudo").exists())
46+
self.assertFalse((Path(self.temp_dir) / "concore.octave").exists())
47+
48+
@patch("concore_cli.commands.setup._resolve_concore_path")
49+
@patch("concore_cli.commands.setup._detect_tool")
50+
@patch("concore_cli.commands.setup._get_platform_key")
51+
def test_setup_writes_files(self, mock_plat, mock_detect, mock_path):
52+
mock_plat.return_value = "posix"
53+
mock_path.return_value = Path(self.temp_dir)
54+
55+
def detect_side_effect(names):
56+
if "g++" in names:
57+
return "/usr/bin/g++", "g++"
58+
if "python3" in names:
59+
return "/usr/bin/python3", "python3"
60+
if "iverilog" in names:
61+
return "/usr/bin/iverilog", "iverilog"
62+
if "octave" in names:
63+
return "/usr/bin/octave", "octave"
64+
if "docker" in names:
65+
return "/usr/bin/docker", "docker"
66+
return None, None
67+
68+
mock_detect.side_effect = detect_side_effect
69+
70+
result = self.runner.invoke(cli, ["setup"])
71+
self.assertEqual(result.exit_code, 0)
72+
73+
tools_file = Path(self.temp_dir) / "concore.tools"
74+
sudo_file = Path(self.temp_dir) / "concore.sudo"
75+
octave_file = Path(self.temp_dir) / "concore.octave"
76+
77+
self.assertTrue(tools_file.exists())
78+
self.assertTrue(sudo_file.exists())
79+
self.assertTrue(octave_file.exists())
80+
81+
tools_content = tools_file.read_text()
82+
self.assertIn("CPPEXE=/usr/bin/g++", tools_content)
83+
self.assertIn("PYTHONEXE=/usr/bin/python3", tools_content)
84+
self.assertIn("VEXE=/usr/bin/iverilog", tools_content)
85+
self.assertIn("OCTAVEEXE=/usr/bin/octave", tools_content)
86+
self.assertEqual(sudo_file.read_text().strip(), "docker")
87+
88+
@patch("concore_cli.commands.setup._resolve_concore_path")
89+
@patch("concore_cli.commands.setup._detect_tool")
90+
@patch("concore_cli.commands.setup._get_platform_key")
91+
def test_setup_no_force_keeps_existing(self, mock_plat, mock_detect, mock_path):
92+
mock_plat.return_value = "posix"
93+
mock_path.return_value = Path(self.temp_dir)
94+
95+
tools_file = Path(self.temp_dir) / "concore.tools"
96+
tools_file.write_text("CPPEXE=/old/path\n")
97+
98+
def detect_side_effect(names):
99+
if "g++" in names:
100+
return "/usr/bin/g++", "g++"
101+
if "python3" in names:
102+
return "/usr/bin/python3", "python3"
103+
return None, None
104+
105+
mock_detect.side_effect = detect_side_effect
106+
107+
result = self.runner.invoke(cli, ["setup"])
108+
self.assertEqual(result.exit_code, 0)
109+
self.assertEqual(tools_file.read_text(), "CPPEXE=/old/path\n")
110+
111+
@patch("concore_cli.commands.setup._resolve_concore_path")
112+
@patch("concore_cli.commands.setup._detect_tool")
113+
@patch("concore_cli.commands.setup._get_platform_key")
114+
def test_setup_force_overwrites_existing(self, mock_plat, mock_detect, mock_path):
115+
mock_plat.return_value = "posix"
116+
mock_path.return_value = Path(self.temp_dir)
117+
118+
tools_file = Path(self.temp_dir) / "concore.tools"
119+
tools_file.write_text("CPPEXE=/old/path\n")
120+
121+
def detect_side_effect(names):
122+
if "g++" in names:
123+
return "/usr/bin/g++", "g++"
124+
if "python3" in names:
125+
return "/usr/bin/python3", "python3"
126+
return None, None
127+
128+
mock_detect.side_effect = detect_side_effect
129+
130+
result = self.runner.invoke(cli, ["setup", "--force"])
131+
self.assertEqual(result.exit_code, 0)
132+
133+
content = tools_file.read_text()
134+
self.assertIn("CPPEXE=/usr/bin/g++", content)
135+
self.assertIn("PYTHONEXE=/usr/bin/python3", content)
136+
137+
138+
if __name__ == "__main__":
139+
unittest.main()

0 commit comments

Comments
 (0)