Skip to content

Commit 7e81c0e

Browse files
Emin017claude
andcommitted
feat(chipcompiler): add manifest-based tool resolution for plugin manager
Adds _resolve_from_manifest() to read ~/.ecos/tools/manifest.json and resolve plugin-managed tool binaries, inserting it as the first check in _resolve_yosys_command() before CHIPCOMPILER_OSS_CAD_DIR and system PATH. Also adds pytest norecursedirs config and conftest to skip inaccessible bazel symlinks during test collection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 62577e9 commit 7e81c0e

4 files changed

Lines changed: 170 additions & 2 deletions

File tree

chipcompiler/tools/yosys/utility.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env python
2+
import json
23
import os
34
import shutil
45
import subprocess
@@ -12,6 +13,27 @@ def _sanitize_loader_env(env: dict[str, str]) -> dict[str, str]:
1213
return env
1314

1415

16+
_MANIFEST_PATH = Path.home() / ".ecos" / "tools" / "manifest.json"
17+
18+
19+
def _resolve_from_manifest(tool_name: str) -> tuple[list[str], Path | None]:
20+
"""Check ~/.ecos/tools/manifest.json for a plugin-managed tool."""
21+
if not _MANIFEST_PATH.exists():
22+
return [], None
23+
try:
24+
manifest = json.loads(_MANIFEST_PATH.read_text(encoding="utf-8"))
25+
except (json.JSONDecodeError, OSError):
26+
return [], None
27+
entry = manifest.get("installed", {}).get(tool_name)
28+
if not entry:
29+
return [], None
30+
tool_dir = Path(entry["path"])
31+
binary = tool_dir / "bin" / ("yosys.exe" if os.name == "nt" else tool_name)
32+
if binary.exists():
33+
return [str(binary)], tool_dir
34+
return [], None
35+
36+
1537
def _build_oss_cad_env(oss_path: Path, base_env: dict[str, str] | None = None) -> dict[str, str]:
1638
"""Build subprocess environment variables for OSS CAD Suite."""
1739
# TODO: Useless in nix build, consider remove this?
@@ -46,17 +68,24 @@ def _resolve_oss_yosys_paths() -> tuple[str, Path | None, Path | None]:
4668

4769
def _resolve_yosys_command() -> tuple[list[str], Path | None]:
4870
"""
49-
Resolve yosys executable from bundled runtime first, then system PATH.
71+
Resolve yosys executable: manifest first, then bundled runtime, then system PATH.
5072
5173
Returns:
5274
(command, oss_path):
5375
- command: list containing executable command or empty list if unavailable
54-
- oss_path: OSS CAD root path if bundled yosys is selected, else None
76+
- oss_path: tool root path if resolved, else None
5577
"""
78+
# 1. Check manifest (plugin-managed tools)
79+
cmd, path = _resolve_from_manifest("yosys")
80+
if cmd:
81+
return cmd, path
82+
83+
# 2. Check CHIPCOMPILER_OSS_CAD_DIR (bundled/Nix path)
5684
_, oss_path, yosys_bin = _resolve_oss_yosys_paths()
5785
if oss_path is not None and yosys_bin is not None and yosys_bin.exists():
5886
return [str(yosys_bin)], oss_path
5987

88+
# 3. System PATH
6089
if shutil.which("yosys"):
6190
return ["yosys"], None
6291

conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
collect_ignore_glob = ["bazel-*"]

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ lint.select = [
9393
]
9494
extend-include = ["*.spec"]
9595

96+
[tool.pytest.ini_options]
97+
testpaths = ["test"]
98+
norecursedirs = ["bazel-bin", "bazel-out", "bazel-ecc", "bazel-testlogs", ".venv", "dist"]
99+
96100
[tool.ty]
97101
environment.python-version = "3.11"
98102
src.include = [ "chipcompiler/**/*.py", "tests/**/*.py" ]

test/test_manifest_resolution.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import json
2+
import os
3+
from pathlib import Path
4+
from unittest.mock import patch
5+
6+
import pytest
7+
8+
9+
def test_resolve_from_manifest_found(tmp_path: Path) -> None:
10+
"""When manifest has yosys entry and binary exists, resolve it."""
11+
from chipcompiler.tools.yosys.utility import _resolve_from_manifest
12+
13+
# Set up fake tool installation
14+
tool_dir = tmp_path / "yosys" / "0.61"
15+
bin_dir = tool_dir / "bin"
16+
bin_dir.mkdir(parents=True)
17+
yosys_bin = bin_dir / "yosys"
18+
yosys_bin.write_text("#!/bin/sh\necho yosys")
19+
yosys_bin.chmod(0o755)
20+
21+
manifest = {
22+
"schema_version": 1,
23+
"installed": {
24+
"yosys": {
25+
"version": "0.61",
26+
"path": str(tool_dir),
27+
"sha256": "abc123",
28+
}
29+
},
30+
}
31+
manifest_path = tmp_path / "manifest.json"
32+
manifest_path.write_text(json.dumps(manifest))
33+
34+
with patch(
35+
"chipcompiler.tools.yosys.utility._MANIFEST_PATH",
36+
manifest_path,
37+
):
38+
cmd, tool_path = _resolve_from_manifest("yosys")
39+
assert cmd == [str(yosys_bin)]
40+
assert tool_path == tool_dir
41+
42+
43+
def test_resolve_from_manifest_no_file(tmp_path: Path) -> None:
44+
"""When manifest doesn't exist, return empty."""
45+
from chipcompiler.tools.yosys.utility import _resolve_from_manifest
46+
47+
with patch(
48+
"chipcompiler.tools.yosys.utility._MANIFEST_PATH",
49+
tmp_path / "nonexistent.json",
50+
):
51+
cmd, tool_path = _resolve_from_manifest("yosys")
52+
assert cmd == []
53+
assert tool_path is None
54+
55+
56+
def test_resolve_from_manifest_tool_not_installed(tmp_path: Path) -> None:
57+
"""When manifest exists but tool not in it, return empty."""
58+
from chipcompiler.tools.yosys.utility import _resolve_from_manifest
59+
60+
manifest = {"schema_version": 1, "installed": {}}
61+
manifest_path = tmp_path / "manifest.json"
62+
manifest_path.write_text(json.dumps(manifest))
63+
64+
with patch(
65+
"chipcompiler.tools.yosys.utility._MANIFEST_PATH",
66+
manifest_path,
67+
):
68+
cmd, tool_path = _resolve_from_manifest("yosys")
69+
assert cmd == []
70+
assert tool_path is None
71+
72+
73+
def test_resolve_from_manifest_binary_missing(tmp_path: Path) -> None:
74+
"""When manifest has entry but binary doesn't exist, return empty."""
75+
from chipcompiler.tools.yosys.utility import _resolve_from_manifest
76+
77+
tool_dir = tmp_path / "yosys" / "0.61"
78+
tool_dir.mkdir(parents=True)
79+
# Don't create the binary
80+
81+
manifest = {
82+
"schema_version": 1,
83+
"installed": {
84+
"yosys": {
85+
"version": "0.61",
86+
"path": str(tool_dir),
87+
"sha256": "abc123",
88+
}
89+
},
90+
}
91+
manifest_path = tmp_path / "manifest.json"
92+
manifest_path.write_text(json.dumps(manifest))
93+
94+
with patch(
95+
"chipcompiler.tools.yosys.utility._MANIFEST_PATH",
96+
manifest_path,
97+
):
98+
cmd, tool_path = _resolve_from_manifest("yosys")
99+
assert cmd == []
100+
assert tool_path is None
101+
102+
103+
def test_resolve_yosys_command_checks_manifest_first(tmp_path: Path) -> None:
104+
"""_resolve_yosys_command should check manifest before env var and PATH."""
105+
from chipcompiler.tools.yosys.utility import _resolve_yosys_command
106+
107+
# Set up fake manifest tool
108+
tool_dir = tmp_path / "yosys" / "0.61"
109+
bin_dir = tool_dir / "bin"
110+
bin_dir.mkdir(parents=True)
111+
yosys_bin = bin_dir / "yosys"
112+
yosys_bin.write_text("#!/bin/sh\necho yosys")
113+
yosys_bin.chmod(0o755)
114+
115+
manifest = {
116+
"schema_version": 1,
117+
"installed": {
118+
"yosys": {
119+
"version": "0.61",
120+
"path": str(tool_dir),
121+
"sha256": "abc123",
122+
}
123+
},
124+
}
125+
manifest_path = tmp_path / "manifest.json"
126+
manifest_path.write_text(json.dumps(manifest))
127+
128+
with patch(
129+
"chipcompiler.tools.yosys.utility._MANIFEST_PATH",
130+
manifest_path,
131+
):
132+
cmd, oss_path = _resolve_yosys_command()
133+
assert cmd == [str(yosys_bin)]
134+
assert oss_path == tool_dir

0 commit comments

Comments
 (0)